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



SAS Innovate 2025: Call for Content

Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!

Submit your idea!

Discussion stats
  • 2 replies
  • 1277 views
  • 0 likes
  • 1 in conversation