Hvordan bruges maskinlæring til at forudsige gentagelser på hospitalet? (Del 1 | Del 2)

Hvad der er i denne artikel: Dette indlæg beskriver, hvordan du kan anvende enkle maskinlæringsteknikker til at analysere sundhedsdata på interessante og meningsfulde måder. Vi bruger som eksempel en forudsigelse af tilbagetagelse af hospitaler hos diabetikere og forklarer, hvordan vi opnåede 94% nøjagtighed. Dette indlæg er resultatet af et projekt udført af et team på fire (Usman Raza, Harman Shah Singh, Ching-Yi Lin og Rohan Kar), og yderligere omformet til Deres læseglæde af Harman og mig selv. Vi tager dig gennem processen, fremhæver rationaler for vores valg og kommer lidt ind på Python-koden på bestemte punkter. Bemærk: Selvom dette menes at være tilgængeligt for de fleste, antages en vis fortrolighed med python og pandaer.

Bemærk: Fuld kode og datafiler er tilgængelige på vores Git-hub-repo her.

Hvorfor gentagelser på hospitalet betyder noget? (til sundhedspersonale)

En tilbagetagelse på hospitalet er, når en patient, der udskrives fra hospitalet, genindlægges igen inden for en bestemt periode. Hospitalets tilbagetagelsesgrader for visse betingelser betragtes nu som en indikator for hospitalets kvalitet og påvirker også omkostningerne til pleje negativt. Af denne grund oprettede Centers for Medicare & Medicaid Services hospitalets tilbagetagelsesreduktionsprogram, som har til formål at forbedre kvaliteten af ​​plejen for patienter og reducere udgifterne til sundhedsydelser ved at anvende betalingsbøder til hospitaler, der har mere end forventet tilbagetagelsesrater under visse betingelser. Selvom diabetes endnu ikke er inkluderet i sanktionerne, tilføjer programmet regelmæssigt nye sygdomsforhold til listen, nu i alt 6 for FY2018. I 2011 brugte amerikanske hospitaler over 41 milliarder dollars på diabetespatienter, der blev tilbagetaget inden for 30 dage efter udskrivning. At være i stand til at bestemme faktorer, der fører til højere tilbagetagelse hos sådanne patienter, og i overensstemmelse hermed være i stand til at forudsige, hvilke patienter der vil blive tilbagetaget, kan hjælpe hospitaler med at spare millioner af dollars, mens de forbedrer plejekvaliteten. Så med den baggrund i tankerne brugte vi et datasæt med medicinske krav (beskrivelse nedenfor) til at besvare disse spørgsmål:

  • Hvilke faktorer er de stærkeste prediktorer for tilbagetagelse af hospitaler hos diabetespatienter?
  • Hvor godt kan vi forudsige tilbagetagelse af hospitaler i dette datasæt med begrænsede funktioner?

Valg af datasæt

At finde et godt datasæt er en af ​​de første udfordringer (udover at definere et meningsfuldt spørgsmål), når man prøver metodelæringsmetoder. Den aktuelle tilstand i sundhedsverdenen er sådan, at vi let kan finde datasæt, der er rige (fulde af nyttige oplysninger), men snavset (ustruktureret indhold eller rodede skemaer) eller datasæt, der er meget rene, men ellers sterile med hensyn til de indeholdte oplysninger (se figuren nedenfor ).

Det er svært at finde gode datasæt!

Med denne begrænsning valgte vi et offentligt tilgængeligt datasæt fra UCI-arkivet (link) indeholdende de-identificerede diabetespatientmødedata for 130 amerikanske hospitaler (1999–2008) indeholdende 101.766 observationer over 10 år. Datasættet har over 50 funktioner, herunder patientkarakteristika, tilstande, test og 23 medicin. Kun diabetiske møder er inkluderet (dvs. mindst en af ​​tre primære diagnoser var diabetes). Dette datasæt er blevet brugt af Strack et al. i 2014 for en interessant analyse om det samme emne (offentliggjort her). Så vi begynder med at indlæse datasættet (csv-fil downloadet fra linket ovenfor) som en panda-dataframe:

# nødvendige biblioteker
importer IPython
importer pandaer som pd
import numpy som np
fra statistikimporttilstand
importer matplotlib.pyplot som plt
fra sklearn.preprocessing import MinMaxScaler
# indlæse originale data i dataframe og kontroller form
df_ori = pd.read_csv ("diabetic_data.csv")
print (df_ori.shape)
# undersøge datatyperne og beskrivende statistikker
print (df_ori.info ())
print (df_ori.describe ())
# lav en kopi af datarammen til forarbejdning
df = df_ori.copy (dyb = sandt)

Den første ting, man skal gøre, når man vælger et datasæt, er at gøre noget dataprofilering og se på nøglefunktioner, som vi har gjort i nedenstående tabel:

Overordnet proces

Fra dette tidspunkt fulgte vi stort set processen som vist i figuren herunder. Det er dog vigtigt at bemærke, at der ikke meget er en ren adskillelse mellem trin, og at der er en masse frem og tilbage iterationer, når man prøver forskellige tilgange til funktionsteknik og modellering. Bemærk også, at der er en hel del overlapning mellem udtryk som funktionsoprettelse, funktionsudvikling og forbehandling, afhængigt af hvem du snakker med.

Hvilke forbehandlings- og funktionstekniske teknikker skal anvendes?

Inden vi kan nå frem til faktisk modellering, er der næsten altid behov for en vis krangel med dataene. Vi anvendte tre typer metoder her:

  1. Rengøringsopgaver såsom at tabe dårlige data, håndtere manglende værdier.
  2. Ændring af eksisterende funktioner f.eks. standardisering, logtransformationer osv.
  3. Oprettelse eller afledning af nye funktioner, normalt fra eksisterende.

De individuelle trin er beskrevet detaljeret nedenfor. Bemærk dog, at det, der ligner en dejlig række af trin nu, var et resultat af mange forsøg og fejlforsøg på at se, hvad der fungerer godt til at få vores data i den bedste form.

Håndtering af manglende værdier

Først skal vi se, hvor mange manglende værdier er (som blev kodet som "?" For de fleste variabler i dataene):

for kol i df.kolonner:
    hvis df [col] .dtype == objekt:
         print (col, df [col] [df [col] == '?']. antal ())
# køn blev kodet forskelligt, så vi bruger et tilpasset antal til denne
print ('køn', df ['køn'] [df ['køn'] == 'Ukendt / ugyldig']. antal ())

Dette giver os en lang liste, men de følgende variabler havde manglende værdier:

løb 2273
vægt 98569
betaler_kode 40256
medicinsk_specialitet 49949
diag_1 21
diag_2 358
diag_3 1423
køn 3

Nu er den vigtige del - at beslutte, hvad man skal gøre:

  • Der mangler vægt i over 98% poster. På grund af den dårlige tolkbarhed af manglende værdier og lidt forudsigelig generaliserbarhed for andre patienter, er det bedste at bare droppe det.
  • Betalerkode og medicinsk specialitet i behandlende læge har også 40–50% manglende værdier. Vi besluttede at droppe disse, men der er også andre måder at håndtere sådanne manglende værdier på.
df = df.drop (['vægt', 'betaler_kode', 'medicinsk_specialitet'], akse = 1)
  • Primære (diag_1), sekundære (diag_2) og yderligere (diag_3) diagnoser var meget få manglende værdier. Teknisk set, hvis alle tre mangler, er det dårlige data. Så vi slipper kun de poster, hvor alle tre diagnoser mangler.
  • Køn har kun 3 manglende eller ugyldige værdier, så vi besluttede at slippe disse poster.
  • Endnu et rengøringstrin, der afhænger af at forstå dataene og en vis sund fornuft: Da vi forsøger at forudsige gentagelser, har de patienter, der døde under indlæggelsen på hospitalet, nul sandsynlighed for tilbagetagelse. Så vi bør fjerne disse poster (decharge_disposition = 11).
drop_Idx = sæt (df [(df ['diag_1'] == '?') & (df ['diag_2'] == '?') & (df ['diag_3'] == '?')]. indeks )
drop_Idx = drop_Idx.union (sæt (df ['køn'] [df ['køn'] == 'Ukendt / ugyldig']. indeks))
drop_Idx = drop_Idx.union (sæt (df [df ['loss_disposition_id'] == 11] .index))
new_Idx = liste (sæt (df.index) - sæt (drop_Idx))
df = df.iloc [new_Idx]
  • Vi bemærkede også, at for to variabler (lægemidler, der hedder citoglipton og undersøges), har alle poster den samme værdi. Så i det væsentlige kan disse ikke give nogen fortolkende eller diskriminerende information til forudsigelse af tilbagetagelse, og vi har også droppet disse kolonner. Teknisk set er dette ikke et manglende værdiproblem, men snarere et manglende informationsproblem.
df = df.drop (['citoglipton', 'undersøge'], akse = 1)

Oprettelse og / eller omkodning af nye funktioner

Dette er yderst subjektivt og afhænger delvis af en viden om sundhedsydelser og giver mening om de potentielle forhold mellem funktioner. Der er måske tusinder af måder at prøve her. Vi prøvede nogle (ingen er perfekte), og her er grunden.

Udnyttelse af tjenester: Dataene indeholder variabler for antal indlagte patienter (indlæggelser), besøg på alarmrum og ambulante besøg for en given patient i det foregående år. Dette er (rå) mål for, hvor meget hospital / klinik tjenester en person har brugt det sidste år. Vi tilføjede disse tre for at oprette en ny variabel kaldet serviceudnyttelse (se figur nedenfor). Ideen var at se, hvilken version der giver os bedre resultater. Indrømmet, at vi ikke anvendte nogen særlig vægtning på de tre ingredienser i serviceudnyttelse, men vi ville prøve noget simpelt på dette tidspunkt.

At gøre dette i python er ret ligetil:

df ['service_utilization'] = df ['number_outpatient'] +
   df ['number_emergency'] + df ['number_inpatient']

Antal medicinændringer: Datasættet indeholder 23 funktioner til 23 medikamenter (eller kombinationer), der angiver for hver af disse, uanset om der er foretaget en ændring i denne medicin eller ikke under patientens aktuelle ophold på hospitalet. Ændring af medicin for diabetikere ved indlæggelse har vist af tidligere forskning at være forbundet med lavere tilbagetagelsesrater. Vi besluttede at tælle, hvor mange ændringer der blev foretaget i alt for hver patient, og erklærede, at en ny funktion. Begrundelsen her var at både forenkle modellen og muligvis opdage et forhold til antallet af ændringer uanset hvilket lægemiddel der blev ændret. I python gøres dette ved:

nøgler = ['metformin', 'repaglinid', 'nateglinide', 'chlorpropamid', 'glimepiride', 'glipizide', 'glyburid', 'pioglitazone', 'rosiglitazon', 'acarbose', 'miglitol', 'insulin' , 'glyburid-metformin', 'tolazamid', 'metformin-pioglitazon', 'metformin-rosiglitazon', 'glimepiride-pioglitazon', 'glipizid-metformin', 'troglitazon', 'tolbutamid', 'acetohexamid']
til col in nøgler:
    colname = str (col) + 'temp'
    df [colname] = df [col] .applik (lambda x: 0 hvis (x == 'Nej' eller x == 'Steady') ellers 1)
df ['numchange'] = 0
til col in nøgler:
    colname = str (col) + 'temp'
    df ['numchange'] = df ['numchange'] + df [colname]
    del df [colname]

For at kontrollere resultatet af dette gør vi brug af metoden value_counts (), der giver en dejlig tilspidsende fordeling:

df [ 'numchange']. value_counts ()
0 72868
1 25832
2 1308
3 107
4 5
Navn: numchange, dtype: int64

Antal anvendte medicin: En anden muligvis beslægtet faktor kan være det samlede antal medicin, som patienten har brugt (hvilket kan indikere sværhedsgraden af ​​deres tilstand og / eller intensiteten af ​​plejen). Så vi oprettede en anden funktion ved at tælle de medikamenter, der blev brugt under mødet (nøglerne i koden nedenfor fortsættes ovenfra):

df ['nummed'] = 0
til col in nøgler:
    df ['nummed'] = df ['nummed'] + df [col]

Sådan kontrolleres output:

df [ 'nummed']. value_counts ()
1 46438
0 22844
2 21712
3 7738
4 1325
5 58
6 5
Navn: nummed, dtype: int64

Kategorisering af diagnoser: Datasættet indeholdt op til tre diagnoser for en given patient (primær, sekundær og yderligere). Hver af disse havde imidlertid 700–900 unikke ICD-koder, og det er ekstremt vanskeligt at inkludere dem i modellen og fortolke meningsfuldt. Derfor kollapsede vi disse diagnosekoder i 9 sygdomskategorier på en næsten lignende måde som gjort i den originale publikation ved hjælp af dette datasæt. Disse 9 kategorier inkluderer kredsløb, åndedrætsorganer, fordøjelsesbesvær, diabetes, skade, muskuloskeletalt, genitourinary, neoplasms og andre. Selvom vi gjorde dette til primære, sekundære og yderligere diagnoser, besluttede vi til sidst kun at bruge den primære diagnose i vores model. At gøre dette i python var lidt besværligt, fordi vi godt kortlægger sygdomsreglerne til bestemte kategorienavne. Nedenstående kode skal demonstrere dette let.

# Opret en duplikat af diagnosekolonnen
df ['level1_diag1'] = df ['diag_1']
# sygdomskoder, der starter med V eller E, er i kategorien “anden”; så koder dem til 0
df.loc [df ['diag_1']. str.contains ('V'), ['level1_diag1']] = 0
df.loc [df ['diag_1']. str.contains ('E'), ['level1_diag1']] = 0
# erstatt også de ukendte værdier med -1
df ['level1_diag1'] = df ['level1_diag1']. erstatte ('?', -1)
# iterere og genkode sygdomskoder mellem visse intervaller til bestemte kategorier
for indeks, række i df.iterrows ():
    if (række ['niveau1_diag1']> = 390 og række ['niveau1_diag1'] <460) eller (np.floor (række ['niveau1_diag1']) == 785):
        df.loc [indeks, 'niveau1_diag1'] = 1
    elif (række ['niveau1_diag1']> = 460 og række ['niveau1_diag1'] <520) eller (np.floor (række ['niveau1_diag1']) == 786):
        df.loc [indeks, 'niveau1_diag1'] = 2
    elif (række ['niveau1_diag1']> = 520 og række ['niveau1_diag1'] <580) eller (np.floor (række ['niveau1_diag1']) == 787):
        df.loc [indeks, 'niveau1_diag1'] = 3
    elif (np.floor (række ['niveau1_diag1']) == 250):
        df.loc [indeks, 'niveau1_diag1'] = 4
    elif (række ['niveau1_diag1']> = 800 og række ['niveau1_diag1'] <1000):
        df.loc [indeks, 'niveau1_diag1'] = 5
    elif (række ['niveau1_diag1']> = 710 og række ['niveau1_diag1'] <740):
        df.loc [indeks, 'niveau1_diag1'] = 6
    elif (række ['niveau1_diag1']> = 580 og række ['niveau1_diag1'] <630) eller (np.floor (række ['niveau1_diag1']) == 788):
        df.loc [indeks, 'niveau1_diag1'] = 7
    elif (række ['niveau1_diag1']> = 140 og række ['niveau1_diag1'] <240):
        df.loc [indeks, 'niveau1_diag1'] = 8
    andet:
        df.loc [indeks, 'niveau1_diag1'] = 0
# konverter denne variabel til flydende type for at aktivere beregninger senere
df ['level1_diag1'] = df ['level1_diag1']. astype (float)

Nu for at kontrollere resultatet:

df [[ 'diag_1', 'level1_diag1']]. hoved (15) .T

På dette tidspunkt vil vi måske gemme det udførte arbejde langt ind i en csv-fil som sikkerhedskopi, så det senere kan indlæses fra dette punkt uden at skulle udføre alle ovenstående trin.

df.to_csv ( './ diabetes_data_preprocessed.csv')

Sammenlægning af nogle andre variabler: Ligesom diagnoser var der en hel del kategorier for optagelseskilde, optagelsestype og decharge disposition. Vi kollapsede disse variabler i færre kategorier, hvor det gav mening. F.eks. Svarer indlæggelsestyper 1, 2 og 7 til nødsituation, akut pleje og traumer, og blev derfor kombineret i en enkelt kategori, da dette alle er ikke-valgfaglige situationer. Af kortfattetheds skyld er prøvepythonkode kun angivet nedenfor for optagelsestype:

df ['admission_type_id'] = df ['admission_type_id']. erstatte (2,1)
df ['admission_type_id'] = df ['admission_type_id']. erstatte (7,1)
df ['admission_type_id'] = df ['admission_type_id']. erstatte (6,5)
df ['admission_type_id'] = df ['admission_type_id']. erstatte (8,5)

Omkodning af nogle variabler: Det originale datasæt anvendte strengværdier til køn, race, medikamentændring og hvert af de 23 anvendte lægemidler. For bedre at passe disse variabler ind i vores model fortolker vi variablerne til numeriske binære variabler for at afspejle deres art. For eksempel kodede vi funktionen "medicinsk ændring" fra "Nej" (ingen ændring) og "Ch" (ændret) til 0 og 1. Koden gives nedenfor:

df ['Change'] = df ['Change']. erstatte ('Ch', 1)
df ['Change'] = df ['Change']. erstatte ('Nej', 0)
df ['gender'] = df ['gender']. erstatte ('Male', 1)
df ['gender'] = df ['gender']. erstatte ('Female', 0)
df ['diabetesMed'] = df ['diabetesMed']. erstatte ('Ja', 1)
df ['diabetesMed'] = df ['diabetesMed']. erstatte ('Nej', 0)
# tasterne er de samme som før
til col in nøgler:
    df [col] = df [col] .replace ('Nej', 0)
    df [col] = df [col] .replace ('Steady', 1)
    df [col] = df [col] .replace ('Op', 1)
    df [col] = df [col] .replace ('Ned', 1)

Vi reducerede også både A1C-testresultat og glukoseserumtestresultat i kategorier af normalt, unormalt og ikke testet.

df ['A1Cresult'] = df ['A1Cresult']. erstatte ('> 7', 1)
df ['A1Cresult'] = df ['A1Cresult']. erstatte ('> 8', 1)
df ['A1Cresult'] = df ['A1Cresult']. erstatte ('Norm', 0)
df ['A1Cresult'] = df ['A1Cresult']. erstatte ('None', -99)
df ['max_glu_serum'] = df ['max_glu_serum']. erstatte ('> 200', 1)
df ['max_glu_serum'] = df ['max_glu_serum']. erstatte ('> 300', 1)
df ['max_glu_serum'] = df ['max_glu_serum']. erstatte ('Norm', 0)
df ['max_glu_serum'] = df ['max_glu_serum']. erstatte ('Ingen', -99)

Omkodning af udgangsvariablen: Det resultat, vi ser på, er, om patienten bliver tilbagetaget til hospitalet inden for 30 dage eller ej. Variablen har faktisk <30,> 30 og Ingen tilbagetagelseskategorier. For at reducere vores problem til en binær klassificering kombinerede vi tilbagetagelsen efter 30 dage og ingen tilbagetagelse i en enkelt kategori:

df ['readmitted'] = df ['readmitted']. erstatte ('> 30', 0)
df ['readmitted'] = df ['readmitted']. erstatte ('<30', 1)
df ['readmitted'] = df ['readmitted']. erstatte ('NO', 0)

Håndtering af alder: Der er forskellige måder at håndtere dette på. Datasættet giver os kun alder som 10-årskategorier, så vi ved ikke den nøjagtige alder for hver patient. Den forrige undersøgelse af dette datasæt anvendte alderskategorier som nominelle variabler, men vi ønskede at være i stand til at se effekten af ​​stigende alder på tilbagetagelse, selvom på en grov måde. For at gøre det antager vi, at patientens alder i gennemsnit ligger midt i alderskategorien. For eksempel, hvis patientens alderskategori er 20-30 år, antager vi alderen = 25 år. Så vi konverterede alderskategorier til midtpunkter, hvilket resulterede i en numerisk variabel:

age_dict = {'[0-10)': 5, '[10-20)': 15, '[20-30)': 25, '[30-40)': 35, '[40-50)' : 45, '[50-60)': 55, '[60-70)': 65, '[70-80)': 75, '[80-90)': 85, '[90-100)' : 95}
df ['age'] = df.age.map (age_dict)
df ['age'] = df ['age']. astype ('int64')
print (df.age.value_counts ())
75 25564
65 22186
55 17102
85 16708
45 9626
35 3765
95 2669
25 1650
15 690
5 160
Navn: alder, dtype: int64

Sammenlægning af flere møder for samme patient

Nogle patienter i datasættet havde mere end et møde. Vi kunne ikke regne dem som uafhængige møder, fordi det skævt resultaterne over for de patienter, der havde flere møder. Således prøvede vi flere teknikker til at kollapse og konsolidere flere møder for samme patient, såsom:

  1. Overvejer mere end 2 gentagelser på tværs af flere møder som tilbagetagelse for sammenbrudt post.
  2. I betragtning af gennemsnitligt ophold på hospitalet på tværs af flere møder.
  3. I betragtning af procentdelen af ​​medicinændringerne på tværs af flere møder
  4. I betragtning af det samlede antal møder, der skal erstatte mødeets unikke ID
  5. I betragtning af kombinationen af ​​diagnoser på tværs af flere møder som en liste

Imidlertid fandt vi f.eks. Funktionerne ”diagnose” det ikke meningsfuldt at kombinere flere kategoriske værdier i en matrix til opbygning af datamodel. Vi overvejede derefter første møde og sidste møde separat som mulige repræsentationer af flere møder. Sidste møder gav imidlertid ekstremt ubalancerede data til readmission (96/4 Readmissions vs No Readmissions), og derfor besluttede vi at bruge første møder af patienter med flere møder. Dette resulterede i, at datasættet blev reduceret til ca. 70.000 møder:

df2 = df.drop_duplicates (subset = ['patient_nbr'], keep = 'first')
df2.shape
(70442, 55)

Logtransformation

En foreløbig analyse af vores numeriske træk afslørede, at mange af disse var meget skæve og havde høj kurtose. Som reference er skævheden af ​​en normal fordeling 0, og overskydende kurtose (forskel i faktisk kurtose fra den ideelle normale fordelingsværdi på 3), som returneret af kurtosis () -funktionen for en normal fordeling er 0, hvilket ville påvirke standardiseringen. Funktioner såsom antal akutbesøg, serviceudnyttelse, antal indlæggelser på indlagte patienter og antallet af polikliniske besøg havde høj skævhed og kurtose. Således udførte vi logtransformation, hvor en skæv eller kurtose ud over grænserne for -2 ≤ skæv og kurtose ≤ 2. Da log (0) ikke er defineret, besluttede vi at bruge følgende regel:

  1. Beregn log (x) for enhver funktion x hvis procentdel af 0s i x ≤ 2%, efter at nulene er fjernet. Dette sikrede, at vi ikke fjernede poster, der har forudsigelsesevne for andre kolonner, i bulk.
  2. Beregn log1p (x) ellers (log1p (x) betyder log (x + 1), mens nulerne bevares).

For at udføre den faktiske logtransformation er python-koden temmelig ligetil. Vi opretter en ny kolonne og indstiller den til np.log eller np.log1p i den kolonne, der skal transformeres:

df [colname + "_log"] = np.log (df [colname])
df [colname + "_log1p"] = np.log1p (df [colname])

Hvor man bruger log1p i stedet for log, afhænger af, hvor små værdierne er. Vi har en separat tutorial, der kommer frem til dette, og vil omfatte kode til både at kontrollere og udføre logtransformationer automatisk. Kom snart tilbage for det.

Standardisering

Da vi havde brugt logtransformation for at sikre, at de numeriske variabler havde en gaussisk-lignende eller normal fordeling (før logtransformationen) eller blev log-transformeret for at sikre en normal fordeling, besluttede vi at standardisere vores numeriske funktioner ved hjælp af formlen:

At kode dette i python er en simpel funktion, der tager arrays og dataframes som input og returnerer deres standardiserede versioner:

def standardisere (raw_data):
    return ((raw_data - np.mean (raw_data, axis = 0)) / np.std (raw_data, axis = 0))
# numerics er en liste over alle numeriske funktioner
df2 [numerics] = standardisere (df2 [numerics])

Fjernelse af outliers

Da vi sikrede, at alle vores variabler var næsten normale efter logtransformation (hvor nødvendigt), skulle distributionerne være mere eller mindre normale. Brug af dækningsreglen til normal distribution, som kan sammenfattes med følgende diagram:

Billedkilde: http://www.muelaner.com/wp-content/uploads/2013/07/Standard_deviation_diagram.png

Noget inden for 3 SD'er på hver side af middelværdien ville omfatte 99,7% af vores data og de resterende 0,3% vi behandlede som outliers. Ved hjælp af denne logik begrænsede vi vores data til inden for 3 SD'er på hver side fra gennemsnittet for hver numeriske kolonne:

df2 = df2 [(np.abs (sp.stats.zscore (df2 [numerics])) <3) .all (axis = 1)]

Interaktionsbetingelser

Variabler kan have indbyrdes afhængige effekter på tilbagetagelse, kaldet interaktioner. Vi kan identificere mulige kandidater til interaktionsbetingelser ved at se på hvad der giver teoretisk mening og ved at observere en korrelationsmatrix af forudsigelsesvariablerne for at se hvilke der synes meget korrelerede. Oprettelse af en korrelationsmatrix i python er let, men vi ønsker en ren måde at sortere korrelationsværdierne på. Da dette resulterer i en lang liste, er vi også nødt til at tilsidesætte standardvisningsstørrelsen på jupyter notebook til lister:

# øg rækkevisningsgrænsen
pd.options.display.max_rows = 400
# beregne korrelationsmatrix og gemme som absolutte værdier
c = df2.corr (). abs ()
# sæt bordet
s = c.unstack ()
# sorter værdierne i faldende rækkefølge
så = s.sort_values ​​(stigende = falsk)
# vis den mest relevante del af listen
så [38: 120]

Dette giver os nogle pænt formaterede output til gennemgang. Nedenfor viser vi kun tre for at demonstrere to typer situationer:

number_outpatient_log1p service_utilization_log1p 0.753627
nummert ændring 0,731185
diabetesMed nummeret 0,706872

På denne liste er der to slags situationer:

  1. En variabel er indeholdt i / derivat af en anden: I ovenstående eksempler er antallet af polikliniske besøg en del af serviceudnyttelsen. Vi skabte imidlertid denne funktion selv, så i dette tilfælde er vores beslutning ikke at sætte disse i samme funktionssæt (vi brugte to funktionssæt beskrevet senere). Tilsvarende er diabetesMed (enhver ordineret diabetisk medicin) indeholdt i antallet af anvendte medicin. Vi besluttede at droppe diabetesMed fra analyse i dette tilfælde, for det er vel forstået, at alle disse patienter får diabetisk medicin.
  2. Mulig faktisk ko-varians: Den anden situation er en faktisk ko-varians mellem to variabler. Dette ser ud til at være tilfældet med antallet af medicin, og om der blev foretaget en ændring eller ej, og det giver også en vis intuitiv mening. Så vi oprettede interaktionsbetingelser for sådanne tilfælde.

I python blev dette gjort ved hjælp af:

interactterms = [('num_medications', 'time_in_hospital'), ('num_medications', 'num_procedures'), ('time_in_hospital', 'num_lab_procedures'),
('num_medications', 'num_lab_procedures'), ('num_medications', 'number_diagnoses'),
('alder', 'antal_diagnoser'), ('ændring', 'num_medications'), ('antal_diagnoser', 'time_in_hospital'), ('num_medications', 'numchange')]
 
 til inter i interaktionstermer:
    name = inter [0] + '|' + inter [1]
    df [navn] = df [inter [0]] * df [inter [1]]

Data balance

Data var meget ubalanceret med hensyn til gentagelser (kun 10% poster til 30-dages tilbagetagelse), hvilket førte til høj nøjagtighed. Derudover kunne den høje nøjagtighed tilskrives ikke den almindelighed, som vores model har til forskellige patientjournaler, men til basisnøjagtigheden på 90%: at forudsige, at ingen patient ville blive tilbagetaget. Dette fremgik af den dårlige præcision og tilbagekaldelse af vores model til at forudsige gentagelser af patienter. Vi brugte syntetisk minoritet over-sampling-teknik (SMOTE) til at overprøve vores underrepræsenterede klasse af tilbagetagelser og opnå lige repræsentation af vores overrepræsenterede og underrepræsenterede klasser.

fra imblearn.over_sampling import SMOTE
fra samlinger importerer tæller
print ('Original datasætform {}'. format (Tæller (tog_output))
sm = SMOTE (random_state = 20)
train_input_new, train_output_new = sm.fit_sample (train_input, train_output)
print ('Ny datasætform {}'. format (Tæller (train_output_new)))

Mere om teknikken findes her. Nedenstående øjebliksbillede af forudsigelser fra en af ​​vores modeller før og efter datafbalancering viser effekten som markant lavere type 2-fejl:

Som vi kan se, er type-2-fejl betydeligt reduceret efterbalancering, hvilket indikerer en bedre tilbagekaldelse af vores model. Dette betyder en lavere andel af tilbagetagne personer, der er forudsagt som ikke tilbagetagne: en kritisk beregning for hospitaler og forsikringsagenturer både ud fra et økonomisk og forebyggende sundhedsmæssigt perspektiv.

Klik her for at læse del 2 af dette indlæg!