BookmarkSubscribeRSS Feed
yabwon
Onyx | Level 15

Cześć PolSUGowcy,

 

Dziś w biurze miała miejsce bardzo fajna dyskusja, której owocem jest bardzo interesujący (moim zdaniem) kod, którym chciałbym się z Wami podzielić.

 

Zadanie do wykonania było takie: mamy zbiór, który zawiera pewne dane/zmienne, jedna ze zmiennych tekstowych zawiera zapisany warunek logiczny opierający się o pozostałe dane, jeśli warunek jest spełniony obserwacja ma zostać wypisana.

 

Jak powszechnie wiadomo taka praktyka (trzymania danych i logiki przetwarzania danych w jednym miejscu) nie jest "dobrą praktyką", ale nie o tym dyskutujemy. Zadanie jakie jest każdy widzi, teraz trzeba je wykonać.

 

Ze zbioru wejściowy:

 

data testc;
length x y 8 c $ 10 code $ 50;
code = '(x > 1) and c=''A'''; x= 3; y=11; c="A"; output;
code = '(x < 1 and (9 < y < 13))'; x=-3; y=17; c="B"; output;
code = '(x < 1) or C in ("C","D","E")'; x= 3; y=11; c="C"; output;
code = '(y < 1)'; x=-3; y=11; c="D"; output;
run;

 

chcemy dostać obserwacje 1 i 3 bo wartości zmiennych "x", "y" i "c" spełniają warunek ze zmiennej "code".

 

Pierwsze podejście było takie:

1) wybierz unikalne wartości zmiennej "code"

2) użyj "filename t temp;" i "%include t;"

 

 

proc sql;
  create table code_b as 
  select distinct QUOTE(strip(code)) as q 
  from testc; 
quit;

filename testb2 TEMP lrecl=2000;

data _null_;
  file testb2;
  set code_b end=EOF;
  length _X_ $ 2000;

  if _N_ = 1 then
  do;
   put "data testb2;";
   put "set testc;";
   put "select;";
  end;

  _X_ = "when (code = " 
      || strip(q) 
      || ") do; if (" 
      || dequote(q) 
      || ") then output; end;"; 
  put _X_;

  if EOF = 1 then
  do;
   put "otherwise;";
   put "end;";
   put "run;";
  end;
run;

%include testb2 / SOURCE2;
filename testb2;

 

Wadą tego podejścia jest to, że przechodzimy przez dane dwa razy w 3 oddzielnych krokach. Odpowiedzią zaproponowany kod było: "A nie da się tego zrobić w jednym datastepie, tak żeby to się obliczało "w locie"? Pewnie SAS tego nie potrafi..." Ci z Was, którzy mnie znają wiedzą, że dla mnie takie pojęcie: "w SASie się tego nie da" nie istnieje 😉 Podrapałem się trochę w głowę i kolejna propozycja była taka:

0) użyj funkcji dosubl()

1) użyj nazwy zbioru, który akurat czytasz i numeru zmiennej w której jesteś
2) na tej podstawie zbuduj zmienną tekstową, która będzie zawierać treść datastepu który na tej jednej konkretniej obserwacji sprawdza warunek z "code"
3) zwróć wynik w makrozmiennej

 

data testc2;
  set 
    testc 
  curobs=curobs indsname=indsname
  ;
  length strX $ 1024; 
  strX=cat(
   ' options nonotes nosource; data _null_; '
  ,' set ', strip(indsname), ' (obs = ', curobs, ' firstobs = ', curobs , ');'
  ,' if (', strip(code), ') then call symputX("_TEST_",1,"G");'
  ,' else call symputX("_TEST_",0,"G");'
  ,' stop; run;'
  );
  _rc_ = dosubl(strX);
  if symget("_TEST_") = "1" then output;

  drop strX _rc_;
run;

Zadziałało, cały proces odbył się w jednym(?) datastepie. Niestety, ponieważ użyliśmy funkcji dosubl(), to nasze rozwiązanie się nie skaluje... wcale. Dla zbioru zrodmuchanego do 1000 obserwacji proces trwał 3 minuty(!) Diabelnie wolno...

 

Zacząłem drapac sie w głowę mocniej i wtedy mnie "oświeciło":
1) hash tablice mają taką własność, że argumenty ich metod (np. .add() albo .defineKey()) nie muszą być statycznym tekstem, mogą być zmiennymi zawierającymi tekst, a co za tym idzie ten tekst może być dynamicznie zmieniany w fazie wykonania kodu,
2) polecenie declare hash H(dataset:"zbior"); akceptuje warunek "where="
3) prze to, że hash jest zwierzęciem "czasu wykonania" można go tworzyć i usuwać wielokrotnie w czasie wykonania datastepu(!)

 

options nonotes;
data testc3;
  set 
    testc 
  curobs=curobs indsname=indsname end=end
  ;

  if missing(code) then 
    do;
      output;
    end;
  else
    do;
      length strX $ 1024;
      strX = cat(
        strip(indsname)
        , '(obs = ', curobs
        , ' firstobs = ', curobs
        , ' where = (', strip(code), '))' 
        );
      declare hash H(dataset:strX);
      rc = H.defineKey("code");
      rc = H.defineDone();
      if H.num_items > 0 then output;
      rc = H.delete();
    end;

  if end then put _N_=;
run;
options notes;

 

Tutaj czas wykonania był znacznie lepszy 3.41 sekundy dla 1000 obserwacji i 38.84 sekundy dla 10000 obserwacji.

 

Co to oznacza? Hash tablica daje nam możliwość obliczania warunków logicznych osadzonych w danych w fazie wykonania kodu! Fajne, nie? 😉

 

All the best
Bart

 

 

_______________
Polish SAS Users Group: www.polsug.com and communities.sas.com/polsug

"SAS Packages: the way to share" at SGF2020 Proceedings (the latest version), GitHub Repository, and YouTube Video.
Hands-on-Workshop: "Share your code with SAS Packages"
"My First SAS Package: A How-To" at SGF2021 Proceedings

SAS Ballot Ideas: one: SPF in SAS, two, and three
SAS Documentation



2 REPLIES 2
yabwon
Onyx | Level 15

Cześć PolSUGowcy,

 

Pan Niels Bohr powiedział kiedyś, że: "Ekspert to taki człowiek, który popełnił wszystkie możliwe błędy w bardzo wąskiej dziedzinie.", a moja koleżanka Renata, że "Nie myli się tylko ten, który nic nie robi." 😉

 

Piszę, żeby przeprosić i wyjaśnić, że w ostatnim kodzie z poprzedniego posta (tym z hashtablicą) zrobiłem elementarny błąd, który dyskwalifikuje pomysł na używanie hashtablic w dynamicznym wykonywaniu kodu osadzonego w danych. W swoim roztargnieniu pomyliłem "kolejność wykonywania działań" operatora "WHERE=" i opcji "FIRSTOBS=" i "OBS=". 

 

Kod nie jest więc niestety rozwiązaniem ogólnym, ale w szczególnym przypadku gdy istnieje w zbiorze zmienna (np. ID) jednoznacznie identyfikująca obserwację można go jeszcze "odratować", oto przykład:

 

data test;
  test = '(x>3 or y>3) and (x*y=20)';

  x =  1; y =  2; id=1; output;
  x = 10; y =  2; id=2; output;
  x =  1; y = 20; id=3; output;
  x = 10; y = 20; id=4; output;
run;

data test4;
  set test end=EOF curobs=CUROBS;

  declare hash H(dataset:cat('test( where = ((', test, ') and ID=', CUROBS, '))'),hashexp:2);  
  H.defineKey("test",'ID');
  H.defineDone();

  if H.NUM_items > 0 then output;
  H.delete();
run;

 

All the best

Bart

_______________
Polish SAS Users Group: www.polsug.com and communities.sas.com/polsug

"SAS Packages: the way to share" at SGF2020 Proceedings (the latest version), GitHub Repository, and YouTube Video.
Hands-on-Workshop: "Share your code with SAS Packages"
"My First SAS Package: A How-To" at SGF2021 Proceedings

SAS Ballot Ideas: one: SPF in SAS, two, and three
SAS Documentation



yabwon
Onyx | Level 15

Cześć PolSUGowcy,

 

już od dłuższego czasu (dokładnie od 16 lutego 2020) miałem zamiar wrócić do tego wątku.

 

Po bardzo ciekawej (choć krótkiej) dyskusji z Panem Christianem Graffeuille (@ChrisNZ) okazało się, że jeszcze nic nie jest stracone, tzn. kod do dynamicznego wykonywania kodu z użyciem hash-tablicy jest jeszcze do odratowania. Okazuje się, że zbiory w bibliotekach silnika SPDE(Scalable Performance Data Engine) maję możliwość wskazywania obserwacji (opcje zbioru startobs= i endobs=), które są wybieranie do procesowania przed użyciem klauzuli WHERE.

 

Dokumentacja do startobs= zawiera przykład potwierdzający:

https://documentation.sas.com/?docsetId=engspde&docsetTarget=n1gc7pzgxq0p7ln1othy6n8iw39o.htm&docset...

 

Oznacza to, że jeśli przeniesiemy nasz zbiór z "kodem w danych" do biblioteki SPDE jesteśmy w stanie wykonać zadanie poprawnie:

/* utwoz podfolder na SPDE w WORKu */
options dlcreatedir;
libname x "%sysfunc(pathname(work))\spde\";
libname x SPDE "%sysfunc(pathname(work))\spde\";

/* skopiuj dane do biblioteki SPDE */
data x.testc;
 length x y 8 c $ 10 code $ 50;
 code = '(x > 1) and c=''A''';           x= 3; y=11; c="A"; output;
 code = '(x < 1 and (9 < y < 13))';      x=-3; y=17; c="B"; output;
 code = '(x < 1) or C in ("C","D","E")'; x= 3; y=11; c="C"; output;
 code = '(y < 1)';                       x=-3; y=11; c="D"; output;
run;


/* options nonotes; */ /* odkomentuj zeby miec czysty log */
data testc3;
  set 
    x.testc 
  curobs=curobs indsname=indsname end=end
  ;

  if missing(code) then 
    do;
      output;
    end;
  else
    do;
      length strX $ 1024;
      strX = cat(
        strip(indsname)
        , '(startobs = ', curobs  /* <-- */
        , '   endobs = ', curobs  /* <-- */
        , '    where = (', strip(code), '))' 
        );
      declare hash H(dataset:strX);
      rc = H.defineKey("code");
      rc = H.defineDone();
      if H.num_items > 0 then output;
      rc = H.delete();
    end;

  if end then put _N_=;
run;
/*options notes;*/

Można by rzec: mit ot tym, że nie da się wykonywać dynamicznie kodu osadzonego w danych w SAS - obalony 😉

 

All the best

Bart

_______________
Polish SAS Users Group: www.polsug.com and communities.sas.com/polsug

"SAS Packages: the way to share" at SGF2020 Proceedings (the latest version), GitHub Repository, and YouTube Video.
Hands-on-Workshop: "Share your code with SAS Packages"
"My First SAS Package: A How-To" at SGF2021 Proceedings

SAS Ballot Ideas: one: SPF in SAS, two, and three
SAS Documentation



Ready to join fellow brilliant minds for the SAS Hackathon?

Build your skills. Make connections. Enjoy creative freedom. Maybe change the world. Registration is now open through August 30th. Visit the SAS Hackathon homepage.

Register today!
Discussion stats
  • 2 replies
  • 1154 views
  • 0 likes
  • 1 in conversation