Upload
vodieu
View
215
Download
0
Embed Size (px)
Citation preview
Fredrik Hansson
• Konsult, SAS Institute, 2001-2007
• SAS-utvecklare, UCR, 2007-2017
Fokus på SAS/BASE och SAS/MACRO
Kvalitetsregister mäter vårdkvalitet
• Insamling av vårddata (inmatning eller integration med journalsystem)
• Analys/rapportering av insamlat data (görs till största delen i SAS)
• Vilket sjukhus ger bäst vård? Vad kan vi kopiera till andra sjukhus?
• Är något fel hos de sjukhus har sämst resultat?
• Vilka behandlingar är bäst?
• Får kvinnor och män lika bra behandling?
Fotregistret
Bra vårdkvalitet
• Ordinerar ultraljudsbehandling till patienter med diagnos ”hälsporre”.
• Ordinerar mjukgörande kräm till patienter med diagnos ”torra fötter”.
Om en vårdgivare inte uppfyller dessa krav i tillräckligt hög utsträckning behöver man göra en genomgång av vårdgivarens vårdprocess.
Kvalitetsregister bidrar till
• Att Sverige har bland de bästa överlevnadsfrekvenserna efterhjärtattack, stroke, bröst- och ändtarmscancer.
• Att Sverige är ledande inom områden som akut hjärtsjukvård, diabetesvård och höftledsoperationer.
• Utveckling av innovativa e-hälsotjänster, patient-centreradetillvägagångssätt och beslutssödsfunktionalitet.
Kundkrav
1. Patienter som har ett torrhetsindex > 40 eller har självsprickor, ska kategoriseras som patienter med torra fötter.
proc format library=format.formats;
value tf
1="Torra fötter"
2="Ej torra fötter";
quit;
data work.besok;
set base.besok;
attrib tf
length=3
format=tf.;
* Torrhetsindex över 40 eller självsprickor innebär
* att patienten lider av torra fötter ;
if torrhetsindex > 40 or Sjalvsprickor="JA" then tf = 2;
else tf = 1;
run;
Vad i hela friden är tf?
proc format library=format.formats;
value torra_fotter
1="Torra fötter"
2="Ej torra fötter";
quit;
data work.besok;
set base.besok;
attrib torra_fotter
length=3
format=torra_fotter.;
if torrhetsindex > 40 or Sjalvsprickor="JA" then torra_fotter = 2;
else torra_fotter = 1;
run;
tf -> torra_fotter
Exempel på andra dåliga variabelnamn
• temp
• x
• y
• [Programmerarens namn]
Exempel från verkligheten!
Annat viktigt att namnsätta bra
• Macroprogram och macrovariabler
• Dataset
• Format
• Libnames (endast 8 tecken)
• Antagligen mycket mer…
trettiotva_tecken_ar_mycket---------1----1----2----2----3------5----0----5----0----5----0--
proc format library=format.formats;
value torra_fotter
1="Torra fötter"
2="Ej torra fötter";
quit;
data work.besok;
set base.besok;
attrib torra_fotter
length=3
format=torra_fotter.;
if torrhetsindex > 40 or Sjalvsprickor="JA" then torra_fotter = 2;
else torra_fotter = 1;
run;
tf -> torra_fotterÄr det någon som har sett buggen?
proc format library=format.formats;
value $torra_fotter
"JA"="Torra fötter"
"NEJ"="Ej torra fötter";
quit;
data work.besok;
set base.besok;
attrib torra_fotter
length=$16
format=$torra_fotter.;
if torrhetsindex > 40 or Sjalvsprickor="JA" then torra_fotter = "NEJ";
else torra_fotter = "JA";
run;
Sifferkoder utbytta mot läsbara textkoder.
Buggen blir uppenbar.
Sifferkoder eller textkoder
• Sifferkoder kan användas för att få data presenterat i en viss ordning• Man kan lägga till en sorteringssiffra i text-koder (”01-JA”)
• Sifferkoder tar mindre utrymme på disk• Ja. Men är det verkligen så att du alltid måste vara restriktiv med utrymme?
Är det inte bara gammal vana? Kan inte läsbar kod vara mer värd än diskutrymme? Arbetstid kostar ju också. För att inte tala om buggar…
proc format library=format.format;
value $torra_fotter
"JA"="Torra fötter"
"NEJ"="Ej torra fötter";
quit;
data work.besok_torra_fotter;
set base.besok;
attrib torra_fotter
length=$16
format=$torra_fotter.;
if torrhetsindex > 40 or Sjalvsprickor="JA" then torra_fotter = "JA";
else torra_fotter = "NEJ";
run;
Buggen är fixad.
Beskrivande variabelnamn och
användning av läsbara koder minskar behovet
av kommentarer i koden…
.. och minskar behovet av extern
dokumentation.
Sammanfattning variabelnamn och textkoder
KundkravBra vårdkvalitet
• Ordinerar ultraljudsbehandling av hälsporre mer än 95% av fallen.
• Ordinerar mjukgörande kräm till torra fötter i mer än 75% av fallen.
Kvalitetsindex-variabler
1=Uppfyller kvalitetsmålet
0=Uppfyller inte kvalitetsmålet
.=Ska inte vara med i urvalet
(Vi ska senare beräkna ett medelvärde. Det berättigar användningen av 1,0 istället för Ja,Nej.)
data work.kvalitet_per_besok;
set work.besok_torra_fotter;
attrib
QI_mjukgorande_kram_ordinerad length=3
QI_ultraljudsbeh_ordinerad length=3;
if torra_fotter = "JA" then do;
if mjukgorandeKramUtskriven="JA" then QI_mjukgorande_kram_ordinerad=1;
else QI_mjukgorande_kram_ordinerad=0;
end;
if Halsporre = "JA" then do;
if ultraljudsbehandling_ordinerad="JA" then QI_ultraljudsbeh_ordinerad=1;
else QI_ultraljudsbeh_ordinerad=0;
end;
run;
Vad händer om mjukgorandeKramUtskriven=""?
Hur vet förvaltaren om utvecklaren har tänkt rätt, eller
bara har missat att det skulle kunna hända?
<KLIPP>
if torra_fotter = "JA" then do;
if mjukgorandeKramUtskriven="JA" then do;
QI_mjukgorande_kram_ordinerad=1;
end;
else if mjukgorandeKramUtskriven in("NEJ", "") then do;
QI_mjukgorande_kram_ordinerad=0;
end;
else do;
ERROR "Programmet xxxxxx.sas har tagit emot ett oväntat värde (”
mjukgorandeKramUtskriven") i kolumnen mjukgorandeKramUtskriven i tabellen
work.besok_torra_fotter";
abort abend;
end;
end;
else do;
QI_mjukgorande_kram_ordinerad=.;
end;
<KLIPP>
Nu är det tydligt vad som händer om
mjukgorandeKramUtskriven=""
Felhantering för oväntade/felaktiga
värden
Att tänka på i if-satser och select-when
• Gör det tydligt vad som ska hända för alla väntade värden
• Se till att programmet inte släpper igenom oväntade/felaktiga värden.• Det är oftast värre att ett fel passerar obemärkt, än att nattbatchen kraschar!
Kvalitetsindikatorerna är de viktigaste affärsreglerna i registret.Vi behöver verifiera att beräkningarna är korrekta!
Alternativ 1 (”Bror Duktig”)• Skapa upp ett test-in-dataset och ett test-ut-dataset.
• Beräkna manuellt för varje rad i test-in-datasetet vad kvalitets-indikatorerna ska få för värden. Spara i test-ut-datasetet.
• Kör koden mot test-indata-setet. Jämför resultatet med testutdatasetet (t.ex. med proc compare).
• Håll testdataseten uppdaterade i framtiden, så att inte tillägg, av nya variabler förstör testfallet.
Alternativ 2 (”Slarvern”)• Ta en kaffepaus• Lägg tio minuter på att testköra med produktionsdata.• Göra några stickprov för att kontrollera att det verkar stämma.
Alternativ 3• Bryt ut de viktiga affärsreglerna till funktioner som:
• gör datasteget ovan mer läsbart.• gör affärsregeln testbar separat från övrig programlogik (enhetstestning).• gör affärsregeln återanvändbar.
• Skriv testfall för funktionerna• Ta en kaffepaus
Kvalitetsindikatorerna är de viktigaste affärsreglerna i registret.Vi behöver verifiera att beräkningarna är korrekta!
proc FCMP
Proc för att skapa egna funktioner
Säljs in som ett sätt att skapa egna generella funktioner.
Det största användningsområdet är att göra egna funktioner med väldigt specifika syften. OBS! Åsikt.
Åter till programmet
• Efter att vi kompleterade if-satserna med else-if och else, blev datasteget långt, och ganska svårläst.
<KLIPP>
if torra_fotter = "JA" then do;
if mjukgorandeKramUtskriven="JA" then do;
QI_mjukgorande_kram_ordinerad=1;
end;
else if mjukgorandeKramUtskriven in("NEJ", "") then do;
QI_mjukgorande_kram_ordinerad=0;
end;
else do;
ERROR "Programmet xxxxxx.sas har tagit emot ett oväntat värde (”
mjukgorandeKramUtskriven") i kolumnen mjukgorandeKramUtskriven i tabellen
work.besok_torra_fotter";
abort abend;
end;
end;
else do;
QI_mjukgorande_kram_ordinerad=.;
end;
<KLIPP>
Och lika mycket kod till för ultraljudsbehandling av
hälsporre…
data work.kvalitet_per_besok;
set work.besok_torra_fotter;
attrib
QI_mjukgorande_kram_ordinerad length=3
QI_ultraljudsbeh_ordinerad length=3;
QI_mjukgorande_kram_ordinerad=
ratt_ordination_torra_fotter(torra_fotter, mjukgorandeKramUtskriven);
QI_ultraljudsbeh_ordinerad=
ratt_ordination_halsporre(Halsporre, ultraljudsbehandling_ordinerad);
run;
De komplicerade if-satserna har brutits ut till funktioner
Exempel på funktion i proc FCMPproc fcmp outlib=work.functions.testdata;
function ratt_ordination_torra_fotter(torra_fotter $, mjukgorandeKramUtskriven $);
if torra_fotter = "JA" then do;
if mjukgorandeKramUtskriven="JA" then do;
return(1);
end;
else if mjukgorandeKramUtskriven in("NEJ", "") then do;
return(0);
end;
else do;
put "ERROR: Funktionen ratt_ordination_torra_fotter har tagit emot ett oväntat
värde (" mjukgorandeKramUtskriven") i parametern mjukgorandeKramUtskriven";
abort;
return(.);
end;
end;
else do;
return(.);
end;
endsub;
run;
Men…
• …om man bryter ut logiken, så ser man den ju inte i programmet.• Sant. Men så fort logiken blir lite komplicerad, så gör det datasteget svårt att
överblicka. Och då är vinsten att bryta ut logiken större.
• …det där kan jag göra med ett macro.• Sant. Men det kräver mycket disciplin att skapa macron som är tydligt
avgränsade i funktionalitet. Styrkan med funktioner, är att man inte kan göra allt som man kan göra i macron.
Skapa en test-funktionfunction test_numeric_funktions(testanrop, forvantat_svar, testId $, beskrivning $) $;
if testanrop = forvantat_svar then do;
put "Testet " testid " passerade utan problem";
return("OK");
end;
else do;
put " ";
put "Testet " testid " fallerade.";
put "ERROR: Testet fallerade.";
put "Funktionen returnerade:" testanrop ". Förväntat svar var: " forvantat_svar ;
put beskrivning;
put " ";
return("FAIL");
end;
endsub;
Kör testfallen i ett data _null_-steg
data _null_;
svar=test_numeric_funktions(ratt_ordination_halsporre("JA", "JA"),
1 /* Förväntat svar */,
"TEST_0001" /* Test-id */,
"Testet kontrollerar att patienter med hälsporre, som
ordinerats ultraljudsbehandling får en etta (uppfyller
kvalitetskraven)");
svar=test_numeric_funktions(ratt_ordination_halsporre("JA", "NEJ"),
0 /* Förväntat svar */,
"TEST_0002" /* Test-id */,
"Testet kontrollerar att patienter med hälsporre, som
inte ordinerats ultraljudsbehandling får en nolla
(uppfyller ej kvalitetskraven)");
run;
Och sen en snabb titt i loggen
Testet TEST_0001 passerade utan problem
Testet TEST_0002 passerade utan problem
ERROR: Testet TEST_0003 fallerade.
Funktionen returnerade: 0 Förväntat svar var: .
Provocerar fram ett misslyckat test
NOTE: DATA statement used (Total process time):
real time 0.14 seconds
cpu time 0.06 seconds
Sammanfattning funktioner
• Gör programkod mer läsbart.
• Separerar affärsregeln från övrig programlogik. Underlättar test!
• Gör affärsregeln återanvändbar.
%printemptymsg(urval_data);
%if &nobs gt 0 %then %do;
…men hur ska man kunna veta det?
&nobs skapas som englobal macrovariabel i“printemptymessage”
Och vad gör macrotegentligen?
Namnsättning…
Det ska vara tydligt vad som returneras från ett macro
%let nobs=%printemptymsg(urval_data);
%if &nobs gt 0 %then %do;
%printemptymsg(urval_data, out_number_of_observations=nobs);
%if &nobs gt 0 %then %do;
%count_observations(urval_data,
out_number_of_observations= number_of_observations);
%if &number_of_observations gt 0 %then %do;
%let number_of_observations=%count_observations(urval_data);
%if &number_of_observations gt 0 %then %do;
Och det där med namnsättning…
%macro abort_report(abort_condition=,
Message=);
%if not (&abort_condition) %then %do;
%return;
%end;
%else %do;
%stpbegin;
proc odstext;
p "&Message";
run;
%stpend;
endsas;
%end;
%mend abort_report;
Pekpinnar om macron
• Det ska vara tydligt vad som returneras.• %let returvariabel=%mitt_macro(eventuella_inparametrar=);
• %mitt_macro(eventuella_inparametrar=, returvariabel1=, returvariabel2=);
• Packa inte in kod i macron, om det inte är nödvändigt.
• Låt ett macro ha en uppgift. Om det är svårt att namnsätta macrot, beror det antagligen på att det gör för många saker.
Hoppa över review för att slippa det jobbiga feedback-steget
• Gemensamt ansvar för att det blir ett bra program• Färre dumheter (som inte påverkar programmets funktion
men bara gör det svårläst), bollas sällan tillbaka vid vanlig review, men stoppas vid parprogrammering.
• Kunskapsutbyte integreras i det dagliga arbetet.
Programmera tillsammans istället
Har vi råd att parprogrammera?
Att ha två programmerare dubblar ju kostnaden för att utveckla en programvara!
En bugg är alltid dyrare ju senare den hittas
Bilden från http://blog.celerity.com/the-true-cost-of-a-software-bug
Siffror från NIST-studien
Men vi skriver ju inte så många buggar
Industristandard: "about 15 - 50 errors per 1000 lines of delivered code."Microsoftapplikationer: "about 10 - 20 defects per 1000 lines of code during in-house testing, and 0.5 defect per KLOC (KLOC IS CALLED AS 1000 lines of code) in released product.”
"Code Complete" by Steve McConnellhttp://amartester.blogspot.se/2007/04/bugs-per-lines-of-code.html
80% av kostnaden för en mjukvara kommer efter att man har skrivit den första gången."In the modern software environment, 80% of the cost of the software is spent after the software is written the first time..."
John Munson, professor of computer science,the University of Idahohttps://www.fastcompany.com/28121/they-write-right-stuff
En bättre produkt med lägre förvaltningskostnader
Skriv läsbar och testbar kodJobba mer/bättre
tillsammansAnvänd verktyg när det går
Nästan slut nu….
Övriga små tips
• Googla ”SAS coding conventions” (men hoppa över sådant som inte spelar roll för kodens läsbarhet)
• Använd ett versionshanteringssystem (t.ex. GIT, SVN, CVS)
Det var allt
Diskutera mer?
• Beskrivande namnsättning.
• Använd läsbara koder (ej siffror).
• Skriv kod som explicit beskriver vad som ska hända för varje väntat fall.
• Skriv kod som hanterar oväntade fall.
• Använd funktioner för att kapsla in viktig logik.
• Det ska vara tydligt vilka eventuella macrovariabler som returneras från ett macro.
• Undvik att lägga kod i ett macro så långt det är möjligt.
• Skriv macron som gör en sak. Dela annars upp i fler macron.
• Använd ett versionshanteringssystem
• Använd verktyg (DI Studio, VA, m.fl.) när det går.
• Ge parprogrammering en ordentlig chans.