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
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
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:
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
SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!