<jupyter_start><jupyter_text>Opis problema Pred nama je skup podataka o pacijentima koji su podvrgnuti **elektrokonverziji** (električnoj kardioverziji). Kardioverzija je procedura koja se sprovodi s ciljem uspostavljanja **sinusnog srčanog ritma** kod pacijenata sa upornom tahikardijom – povećanom srčanom frekvencijom (brojem otkucaja srca). To je čest simptom kod **pretkomorske (atrijalne) fibrilacije** – nepravilnog rada (grčenja i opuštanja) srčanih pretkomora – ali i mnogih drugih bolesti srca, kao što su akutna srčana slabost (insuficijencija) ili problemi sa krvnim pritiskom. Sama kardioverzija, kao uspostavljanje sinusnog ritma, može se postići **lekovima**. Međutim, vremenom je razvijena tehnika električne kardioverzije (elektrokonverzije), koja je danas uobičajena u kardiologiji i o kojoj je ovde reč. Ovaj pristup može podstaći stvaranje krvnih ugrušaka (trombova), tako da zahteva **antikoagulantnu profilaksu** – uzimanje lekova protiv zgrušavanja pred proceduru.Opis terapije je sledeći ([Wiki](https://sr.wikipedia.org/sr/Kardioverzija)): 'Kardioverzija električnim šokom zasniva se na upotrebi kratkotrajnog električnog šoka preko elektroda postavljenih na grudnom košu bolesnika. Jedna elektroda postavlja se na predeo vrha srca, a druga desno, na gornji deo grudne kosti (stenuma), da bi se primenom električnog šoka jednosmeme struje (DC – *direct current*) izvršila depolarizacija srčanog mišića (miokarda) i prekinula tahikardija, odnosno tahiaritmija. (...) Podrazumeva oslobađanje električnog šoka, koji je vremenski usklađen (sinhronizovan) sa električnom aktivnošću srca, prema R-zupcima u elektrokardiogramu (EKG). Ova tehnika osigurava da do električne stimulacije ne dođe tokom vulnerabilnog perioda srčanog ciklusa, koji obuhvata period od 60 do 80 msec pre, do 20 do 30 msec posle vrha T-talasa. (...) Električni šok treba da je pravilno sinhronizovan sa QRS kompleksom, što zahteva monitoring R-zupca u odgovarajućem, odabranom odvodu. Osim amplitude R-zupca, važno je da se u datom odvodu dobro vidi P-talas, što olakšava procenu ishoda procedure.'Naš zadatak je da na osnovu dobijenih podataka o pacijentima napravimo **klasifikacioni model** koji tačno predviđa **da li će procedura biti uspešna**.  Opis podataka Kada su u pitanju podaci, distribuirana Excel datoteka sadrži **202 reda** i **102 kolone**. Redovi predstavljaju pacijente i poslednja četvrtina je prazna, te korisnih podataka o pacijentima zapravo ima 147. Ovaj broj će i nadalje ostati nepromenjen, tj. nećemo izbacivati nijedan neprazan red iz skupa. Prva karakteristika je indeksna i predstavlja broj pacijenta u bazi, pa se zapravo može reći da imamo 101 atribut. Slede neke nesporne osobine, sa jasnim vrednostima (4):1. **redni broj bolesnika u godini**,2. **godine starosti pacijenta**,3. **stariji od 65 godina** – interval zatvoren sleva, uključuje i 65-godišnjake,4. **pol** – muški ili ženski.Nakon ovoga sledi grupa atributa koji predstavljaju polaznu tačku za razmatranje pacijenta, njegova klinička slika i druge indikacije (*baseline*, 28):1. **broj istorije** – broj kartona, niska oblika broj/godina,2. **datum elektrokonverzije** – pun datum procedure,3. **indikacija** – atrijalna fibrilacija (AF), treperenje (*flutter*) ili tahikardija,4. **ukupno trajanje indikacije u mesecima**,5. **trajanje tekuće epizode bolesti u mesecima**,6. **frekvenca komora na prijemnom EKG-u** – razvrstane (*binned*) vrednosti,7. **blok(ada) grane** – ima li problema sa sprovodnim sistemom srca i sa koje strane – LBBB, RBBB ili PM,8. **dimenzija LPK** – dimenzija leve pretkomore srca u milimetrima,9. **EDD LK** – end-dijastolna dimenzija leve komore u milimetrima, na kraju punjenja,10. **ESD LK** – end-sistolna dimenzija leve komore u milimetrima, na kraju pražnjenja,11. **EF LK** – ejekciona frakcija leve komore u procentima, udeo izbačene krvi pri kontrakciji,12. **MR** – mitralna regurgitacija, vraćanje krvi iz leve komore u pretkomoru kroz oslabljeni zalistak13. **TR** – trikuspidalna regurgitacija, isto kao prethodno, samo sa desne strane srca,14. **HTA** – hipertenzija, povišen krvni pritisak,15. **DM** – dijabetes (*diabetes mellitus*), šećerna bolest,16. **HLP** – hiperlipoproteinemija, povišena masnoća u krvi,17. **HOBP** – hronična opstruktivna bolest pluća, smanjen protok vazduha kroz disajne puteve,18. **pušenje** – da li je pacijent bivši ili sadašnji pušač ili pak nepušač,19. **hereditet za KVB** – nasledni faktor za kardiovaskularne bolesti,20. **ishemijska bolest srca** – smanjen protok krvi srčane arterije,21. **HF** – zatajenje srca (*heart failure*) sa očuvanom ejekcionom frakcijom ili bez nje – HFpEF ili HFrEF,22. **NYHA klasa** – mera ozbiljnosti zatajenja srca prema Njujorškom udruženju za srce,23. **prethodni TIA/CVI** – moždani udar (*cerebrovaskularni insult*) ili mini-udar (*tranzitorni ishemijski atak*),24. **HBI** – hronična bubrežna slabost (insuficijencija),25. **CHADS-Vasc** – klinički skor kojim se procenjuje rizik od moždanog udara,26. **poremećaj funkcije štitaste žlezde** – hipertireoza ili hipotireoza,27. **dosadašnje elektrokonverzije** – da li ih je bilo i koliko tačno (razvrstano),28. **SCD** – strukturna bolest srca – ishemijska, dilatativna, hipertrofična, valvularna, urođena, kombinovana.Naredni skup atributa odnosi se na terapiju pre elektrokonverzije, koja uključuje terapiju za srce i protiv zgrušavanja krvi (npr. indikatori uzimanja, 19):1. **beta blokatori** – umanju dejstvo hormona stresa i štite srce od nepravilnog rada,2. **ACE inhibitori** – smanjuju krvni pritisak i ublažavaju zatajenje srca,3. **ARB** – isto kao ACE inhibitori, samo za pacijente koji ne reaguju na prve,4. **furosemid** – diuretik, za izbacivanje suvišne tečnosti koja se gomila zbog zatajenja srca,5. **drugi diuretik** – upotreba diuretika koji nije furosemid, takođe za smanjenje pritiska,6. **spironolakton** – sličnog efekta kao prethodna dva, sa kojima se često i kombinuje,7. **Ca antagonisti** – blokatori kalcijumovog kanala, smanjuju krvni pritisak i ublažavaju bolove,8. **amiodaron** – višestruko deluje s ciljem ublažavanja aritmije, fibrilacije, tahikardije,9. **propafen** – još jedan poznati antiaritmik, usporava prebrze otkucaje srca,10. **flekainid** – takođe sprečava aritmiju, jak lek sa jakim neželjenim dejstvima,11. **sotalol** – isto kao prethodni, isto samo za najteže slučajeve zbog neželjenih dejstava,12. **digoksin** – takođe usporava rad srca i povećava snagu srčanih kontrakcija,13. **ASA** – aspirin, acetilsalicitna kiselina, jedan od najpoznatijih antitrombotika,14. **OAKT** – oralna antikoagulantna terapija – direktna, varfarin, bez nje ili bez nje nakon TEE,15. **vrednost INR** – međunarodni normalizovani odnos, brzina koagulacije – upada li u opseg \[2, 3\],16. **Plavix ili Brilique** – antitrombotici, deluju na krvne pločice i sprečavaju stvaranje ugrušaka,17. **statin** – najpoznatiji lek za smanjenje nivoa holesterola u krvi,18. **implantiran CIED** – da li je ugrađen neki uređaj u srce i koji – bradyPM, ICD, CRT-P, CRT-D,19. **vreme u mesecima od ugradnje aparata do elektrokonverzije**.Naposletku dolazimo do podataka o samoj elektrokonverziji (2):1. **sinhroni, bifazni DC šok** – primenjen unutrašnji, spoljašnji ili oba tipa elektrošoka,2. **elektrokonverzija** – delimičan ili potpun uspeh ili neki od dva tipa neuspeha.U nastavku su dodatne karakteristike o stanju pacijenta neposredno posle procedure, zatim prazne kolone rezervisane za opis stanja aparata pre i posle intervencije, podaci o terapiji posle elektrokonverzije, kao i o tome da li se nakon mesec i godinu dana održao dobar sinusni ritam. Ove informacije, međutim, vremenski slede nakon same procedure, pa ih ne uzimamo (niti bi imalo smisla uzeti ih) u razmatranje po pitanju problema predviđanja uspešnosti terapije. Zadržavamo se, dakle, na prethodno opisana 53 atributa, od kojih je poslednji ciljni, što sveukupno znači da polazimo sa tabelom dimenzija **147 × 53**.  Učitavanje podataka Za rad sa skupom podataka koristimo module *numpy* i *pandas*, a u nastavku koristimo i druge prikladne biblioteke.<jupyter_code>import numpy as np
import pandas as pd<jupyter_output><empty_output><jupyter_text>Učitavamo podatke iz distribuirane *Excel* datoteke. Uzimamo samo **neprazne kolone** (prazne su F, AI, BC, BD, kojima je zadatak da grupišu polja koje slede), i to one iz prvog dela, koje se odnose na uspešnost elektrokonverzije (do BF). Pritom prvu karakteristiku, koja predstavlja broj pacijenta u bazi, uzimamo za **indeksnu**. Odbacujemo **poslednjih 55 redova**, jer su suštinski prazni – sadrže samo broj u bazi (indeks), dok su preostale vrednosti nedostajuće. Zanemarujemo upozorenje biblioteke, koje je posledica unutrašnjeg formata ulazne datoteke i ne utiče na dalju obradu, te nas ni ne zanima.<jupyter_code>df = pd.read_excel(
    'elektrokonverzija.xlsx',
    index_col=0,
    usecols='A:E,G:AH,AJ:BB,BE:BF',
    skipfooter=55
)

df<jupyter_output>C:\Users\ACER\AppData\Local\Programs\Python\Python37\lib\site-packages\openpyxl\worksheet\_reader.py:312: UserWarning: Unknown extension is not supported and will be removed
  warn(msg)<jupyter_text>Kao **nerelevantne osobine** lako odbacujemo indeks, broj pacijenta u godini, kao i broj istorije, što potvrđujemo relativno **niskom korelacijom** sa ciljnim atributom. Iz nje je jasno da su u pitanju međusobno slične karakteristike (vrednost bliska jedinici), ali da nisu u vezi sa ciljnom (vrednost bliska nuli). Na sličan način rezonujemo da ni datum elektrokonverzije ne igra bitnu ulogu u predviđanju uspešnosti procedure, ali njega zasad ostavljamo i koristimo u nastavku. I bez upotrebe koeficijenta korelacije kao statističkog oruđa, bilo bi nam logično da broj kartona ili datum procedure ne bi trebalo da utiču na uspešnost.<jupyter_code>indeks = df.index.to_series()

brug = df.iloc[:, 0]

brist = df.iloc[:, 4]
def brojf(br):
    brs = br.split('/')[0]
    return int(brs) if brs else 207
brist = brist.map(brojf)

datum = df.iloc[:, 5]
god = datum.map(lambda x: x.year)
mes = datum.map(lambda x: x.month)
ned = datum.map(lambda x: x.week)
dan = datum.map(lambda x: x.day)

uspeh = df.iloc[:, -1]

def corr(sx, sy, x, y):
    print(f'Korelacija {sx} i {sy}: {x.corr(y):.2f}')

corr('indeksa', 'uspeha terapije', indeks, uspeh)
corr('broja u godini', 'broja istorije', brug, brist)
corr('broja u godini', 'uspeha terapije', brug, uspeh)
corr('broja istorije', 'uspeha terapije', brist, uspeh)
corr('meseca', 'nedelje', mes, ned)
corr('godine', 'uspeha terapije', god, uspeh)
corr('meseca', 'uspeha terapije', mes, uspeh)
corr('nedelje', 'uspeha terapije', ned, uspeh)
corr('dana', 'uspeha terapije', dan, uspeh)<jupyter_output>Korelacija indeksa i uspeha terapije: 0.07
Korelacija broja u godini i broja istorije: 0.94
Korelacija broja u godini i uspeha terapije: -0.11
Korelacija broja istorije i uspeha terapije: -0.10
Korelacija meseca i nedelje: 1.00
Korelacija godine i uspeha terapije: 0.01
Korelacija meseca i uspeha terapije: -0.10
Korelacija nedelje i uspeha terapije: -0.11
Korelacija dana i uspeha terapije: -0.09<jupyter_text>Istovremeno **skraćujemo imena** svih kolona na prvu reč, kako bismo u nastavku imali pregledniji prikaz tabele, ali čuvamo i originalna.<jupyter_code>df.drop(columns=df.columns[[0, 4]], inplace=True)

skrati = lambda x: x.split()[0]
df.index.rename(skrati(df.index.name), inplace=True)

kolone = df.columns
df.columns = kolone.map(skrati)

df<jupyter_output><empty_output><jupyter_text>Popunjavanje kolona Ispisujemo podatke kako bismo stekli dodatnu predstavu o atributima. Suštinski gledano, svaka izabrana kolona je **celobrojnog tipa**. Ipak, zbog sadržaja datoteke, situacija je nešto drugačija u učitanom skupu. Većina stvarno jeste celobrojna (*int64*) i zadovoljni smo njima, ali postoje dve grupe specifičnih:* **oni koji sadrže nedostajuće vrednosti** – oni su *float64*, kako bi pokrili specijalnu vrednost NaN (*not a number*, nije broj),* **oni koji sadrže mešane tipove podataka**, čak sa različitim značenjem u zavisnosti od tipa i formata – oni su *object*, tj. *O*.Izdvaja se i jedna karakteristika koja predstavlja datum elektrokonverzije (*M8\[ns\]*), i nju čuvamo za kasnije, iako je nebitna i biće izbačena na kraju.<jupyter_code>df.dtypes.to_numpy()<jupyter_output><empty_output><jupyter_text>Prvo rešavamo problem kolona sa **mešanim podacima**. Primećujemo da je po pravilu u pitanju vreme u mesecima. Iako je to u suštini jedan ceo broj, u ovim trima kolonama javljaju se i dodatne tekstualne vrednosti poput: 24god, jul.11, 20dana, 3mes, od2016... Treći atribut ima i veliki broj nedostajućih vrednosti, koje odgovaraju slučaju kada nema ugrađenog aparata, kao i specijalne vrednosti 'nije prva ECV'. Od svega ovog želimo da napravimo **cele brojeve**.<jupyter_code>mesano = np.where(df.dtypes == 'O')[0]

print('\n'.join(kolone[mesano]))<jupyter_output>ukupno trajanje AF (kada je prvi put dijagnostikovana) u mesecima
trajanje ove epizode AF u mesecima
Vreme u mesecima od ugradnje aparata do elektrokonverzije<jupyter_text>Obradom svake kolone, menjamo je red po red. Pritom se vodimo sledećim skupom **pravila transformacija**:1. Ukoliko je vrednost celobrojna, očekivano, ne menja se.2. Ukoliko je vrednost datumskog tipa, menja se razlikom u mesecima do datuma elektrokonverzije.3. Ukoliko je vrednost niska, obrađuje se na adekvatan način – umnogome zavisi od formata.4. U suprotnom je nedostajuća vrednost koja se eliminiše (popunjava) imputacijom. Na kraju procesa izbacujemo datum konverzije, koji nam je bio potreban samo za računanje razlike.<jupyter_code>from datetime import datetime

def razlika(dat1, dat2):
    raz = dat2 - dat1
    dan = raz.days
    return dan // 30

def niska(ind, val):
    if val.startswith('n'):
        return 0
    elif 'g' in val:
        ind = val.index('g')
        br = float(val[:ind])
        return int(12 * br)
    elif 'm' in val:
        ind = val.index('m')
        return int(val[:ind])
    elif 'da' in val:
        ind = val.index('d')
        br = int(val[:ind])
        return br // 30
    else:
        god = int(val[-4:])
        val = datetime(god, 1, 1)
        dat = df.loc[ind][3]
        return razlika(val, dat)

def popuni(col):
    for ind in col.index:
        val = col[ind]
        
        if isinstance(val, int):
            continue
        elif isinstance(val, datetime):
            dat = df.loc[ind][3]
            col[ind] = razlika(val, dat)
        elif isinstance(val, str):
            col[ind] = niska(ind, val)
        else: col[ind] = -1
    
    return col.astype(np.int64)

df.iloc[:, mesano] = df.iloc[:, mesano].apply(popuni)
df.drop(columns=df.columns[[3]], inplace=True)

df<jupyter_output><empty_output><jupyter_text>Sada rešavamo problem kolona sa **nedostajućim podacima**. I njih želimo da **popunimo celim brojevima**. Ovde je, međutim, situacija drugačija, pošto su nedostajuće vrednosti različite prirode. Primera radi, prva i četvrta kolona sa nedostajućim vrednostima su gusti, pa im fali samo jedna, odnosno dve vrednosti. Druga ima vrednost samo ukoliko je broj u koloni koja joj prethodni nenula. Treća je retka, pa joj svaka sedma vrednost nedostaje.<jupyter_code>nan = np.where(df.dtypes == 'float64')
print('\n'.join(df.columns[nan]))<jupyter_output>ESD
NYHA
vrednost
Implantiran<jupyter_text>Odabiramo sledeću strategiju popunjavanja prve kolone. Kako je karakteristika ESD LK viskokorelisana sa atributom EDD LK, što je i očekivano, moguće je vrednost ESD popuniti **prosekom susedstva** u EDD. Konkretno, pronalazimo vrednost kolone EDD kod pacijenta kojem ESD nedostaje. Zatim pronalazimo sve pacijente koji imaju tu istu vrednost EDD. Na kraju, računamo prosek njihove vrednosti ESD i dodeljujemo ga susednom pacijentu kome je nedostajala.<jupyter_code>esd = df.loc[:, 'ESD']
edd = df.loc[:, 'EDD']
corr('ESD LK', 'EDD LK', esd, edd)

esdi = esd.index[esd.isna()]
eddv = edd.loc[esdi].values[0]
eddi = edd.index[edd == eddv]
esdv = esd.loc[eddi]

df.loc[esdi, 'ESD'] = np.round(np.mean(esdv))
df.loc[:, 'ESD'] = df.loc[:, 'ESD'].astype(np.int64)<jupyter_output>Korelacija ESD LK i EDD LK: 0.89<jupyter_text>Drugu i četvrtu kolonu **popunjavamo nulama**. Dakle, ako neko nema zatajenje srca (*heart failure*, HF), računamo kao da je ozbiljnost stanja nulta, iako to nije validna NYHA (*New York Heart Association*) klasa. Takođe, podrazumevamo da pacijent za koga se ne zna ima li ugrađen srčani implatabilni elektronski uređaj (*cardiac implantable electronic device*, CIED) zapravo ni nema ništa ugrađeno, jer bi taj podatak najverovatnije vrlo teško mogao da promakne.<jupyter_code>df.loc[:, 'NYHA'].fillna(0, inplace=True)
df.loc[:, 'NYHA'] = df.loc[:, 'NYHA'].astype(np.int64)

df.loc[:, 'Implantiran'].fillna(0, inplace=True)
df.loc[:, 'Implantiran'] = df.loc[:, 'Implantiran'].astype(np.int64)<jupyter_output><empty_output><jupyter_text>Treću kolonu, punu nedostajućih vrednosti, a koja predstavlja indikator da li vrednost međunarodnog normalizovanog odnosa (*international normalized ratio*, INR) upada u opseg \[2, 3\], odlučujemo da **izbacimo**. U opštem slučaju, ovaj parametar, koji predstavlja brzinu zgrušavanja krvi, vrlo je značajan u terapiji atrijalne fibrilacije (AF). Međutim, on je usko povezan, te visokokorelisan, sa podatkom da li pacijent uzima antikoagulacioni lek varfarin. Tako samo dva pacijenta na varfarinu imaju vrednost INR koja ne upada u taj za njih optimalni uspeh, i to takva dva pacijenta kod kojih je elektrokonverzija bila uspešna, tako da neupadanje u opseg nije problematično. Slično važi i u obrnutom smeru, što sveukupno opravdava odluku o izbacivanju umesto popunjavanju.<jupyter_code>inr = df.loc[:, 'vrednost']
varfarin = df.loc[:, 'OAKT'] == 2
corr('INR', 'varfarina', inr, varfarin)
df.drop(columns=['vrednost'], inplace=True)

df<jupyter_output><empty_output><jupyter_text>Ciljna karakteristika Na kraju, regulišemo još i pitanje **ciljnog atributa**. To je poslednja kolona, koja govori o uspešnosti elektrokonverzije. U ulaznoj datoteci, postoje čak četiri mogućnosti za ovu kolonu – potpuni uspeh (1), delimični uspeh (2), klasični neuspeh (3), dupli neuspeh (4). Za potrebe klasifikacije, svodimo ovo na samo dve **istinitosne vrednosti** (*bool*). Dva su razloga za to. Prvi potiče od činjenice da nam nije bitno da li je uspeh delimičan ili potpun, jer je to svakako uspeh. Isto važi za neuspeh. Drugi je vezan za količinu podataka, pošto skup sadrži svega dva pacijenta sa delimičnim uspehom, a tek jednog sa duplim neuspehom.<jupyter_code>uspeh = uspeh.map(lambda x: x <= 2)

df.iloc[:, -1] = uspeh

df<jupyter_output><empty_output><jupyter_text>Primećujemo da je elektrokonverzija uglavnom **uspešna** – tek kod svakog devetog pacijenta ne uspe. To posebno otežava problem klasifikacije. Naime, u pitanju je **nebalansiran skup podataka**, pa bismo olako svim pacijentima mogli prognozirati uspešnu terapiju. Tim postupkom bismo dobili vrlo jednostavan klasifikator, kao i naizgled dobre vrednosti osnovnih mera uspešnosti klasifikacije, ali ćemo u nastavku izneti loše strane takvog lenjog pristupa.<jupyter_code>from matplotlib import pyplot as plt
plt.rcParams['figure.dpi'] = 85

vrednosti, broj = np.unique(uspeh, return_counts=True)

suma = np.sum(broj)
def oznaci(proc):
    br = int(round(proc/100*suma))
    return f'{proc:.1f}%\n({br})'

kriske, tekst, autotekst = plt.pie(broj,
                                   autopct=oznaci,
                                   textprops=dict(color='w'))

plt.legend(kriske, ['Neuspešna', 'Uspešna'],
           title='Elektrokonverzija',
           loc='center left',
           bbox_to_anchor=(1, 0, 0.5, 1),
           fontsize=12,
           title_fontsize=12)

plt.setp(autotekst, size=10, weight='bold')

plt.show()<jupyter_output><empty_output><jupyter_text>Kako vidimo, ovakav klasifikator ima visoke vrednosti dobrih osobina:* **preciznosti** (*precision*) / **pozitivne prediktivne vrednosti** (*positive predictive value*) – $PPV = \frac{TP}{TP + FP} \approx 88 \%$,* **odziva** (*recall*) / **osetljivosti** (*sensitivity*) / **stope pogođenih** (*hit rate*) / **stope stvarno pozitivnih** (*true positive rate*) – $TPR = \frac{TP}{TP + FN} = 100 \%$,* **$F_1$ skora** (harmonijska sredina prethodna dva) – $F_1 = \frac{2TP}{2TP + FP + FN} \approx 94 \%$,* **tačnosti** (*accuracy*) – $ACC = \frac{TP + TN}{TP + TN + FP + FN} \approx 88 \%$,* pojedinih **balansiranih mera kvaliteta** – pogledati izveštaj o klasifikaciji.Ovakav klasifikator ima i niske vrednosti sledećih loših osobina:* **stope promašaja** (*miss rate*) / **stope lažno negativnih** (*false negative rate*) – $FNR = \frac{FN}{FN + TP} = 0 \%$,* **stope lažnih propusta** (*false omission rate*) – $FOR = \frac{FN}{FN + TN} = 0 \%$,* **stope lažnih otkrića** (*false positive rate*) – $FPR = \frac{FP}{FP + TP} \approx 12 \%$.Ipak, ovakav klafisikator ima maksimalnu vrednost važne loše osobine:* **stope lažnih alarma** (*fall-out*, *false alarm rate*) / **stope lažno pozitivnih** (*false positive rate*) – $FPR = \frac{FP}{FP + TN} = 100 \%$.Takođe, ovakav klasifikator ima i niske vrednosti važnih dobrih osobina:* **negativne prediktivne vrednosti** (*negative predictive value*) – $NPV = \frac{TN}{TN + FN} = 0 \%$,* **specifičnosti** (*specificity*) / **selektivnosti** (*selectivity*) / **stope stvarno negativnih** (*true negative rate*) – $TNR = \frac{TN}{TN + FP} = 0 \%$,* **balansirane tačnosti** (*balanced accuracy*) – $BA = \frac{TPR + TNR}{2} = 50 \%$.Kada se sve uzme u obzir, a pogotovu balansirana tačnost, shvatamo da ovo **nije dobar pristup**.<jupyter_code>from sklearn.metrics import classification_report

uvek_uspeh = np.full_like(uspeh, True)

izvestaj = classification_report(uspeh, uvek_uspeh, zero_division=True)

print(izvestaj)<jupyter_output>precision    recall  f1-score   support

       False       1.00      0.00      0.00        17
        True       0.88      1.00      0.94       130

    accuracy                           0.88       147
   macro avg       0.94      0.50      0.47       147
weighted avg       0.90      0.88      0.83       147<jupyter_text>Dodatno, kako je pred nama problem **binarne klasifikacije** (dve klase), razmatramo još jednu korisnu meru kvaliteta. U pitanju je **površina ispod krive** (*area under curve*, AUC), i to poznate **ROC (*receiver operating characteristic*) krive**. Ovom krivom grafički je predstavljen kompromis između senzitivnosti i specifičnosti. Kako smo ranije primetili, lenji klasifikator je vrlo osetljiv, a nimalo specifičan, pa je kao takav izrazito loš, sa malom površinom ispod krive.<jupyter_code>from sklearn.metrics import roc_curve, auc

fpr, tpr, _ = roc_curve(uspeh, uvek_uspeh)
pov = auc(fpr, tpr)

plt.plot(fpr, tpr, color='darkorange',
         lw=2, label=f'ROC kriva (površina = {pov})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')

plt.xlabel('Stopa lažno pozitivnih (FPR)')
plt.ylabel('Stopa stvarno pozitivnih (TPR)')

plt.title('ROC kriva lenjog klasifikatora')
plt.legend(loc="lower right")

plt.show()<jupyter_output><empty_output><jupyter_text>Značajnost atributa Podsećanja radi, **klasifikacija** se može definisati kao problem **određivanja klase (kategorije) objekta na osnovu skupa karakteristika** koji ga opisuju. Sa te tačke gledišta, nakon prethodnog popunjavanja kolona, svaka ulazna karakteristika razmatranog skupa podataka je celobrojnog tipa (*int64*), dok je izlazna klasa istinitosnog tipa (*bool*). Ovakva postavka tipova, pri kojoj su svi suštinski numerički, daje nam širok dijapazon raspoloživih metoda klasifikacije.<jupyter_code>df.dtypes.to_numpy()<jupyter_output><empty_output><jupyter_text>Ipak, iako smo lepo skockali sve atribute, postavlja se pitanje da li je baš svaki **značajan prediktor** (*predictor importance*). Čest je slučaj u mašinskom učenju da instance sadrže određeni broj nerelevantnih karakteristika za određivanje vrednosti ciljne kolone. Još na početku smo iskoristili nisku korelaciju sa ciljnim atributom kao statističko oruđe da dodatno opravdamo izbacivanje određenih kolona. Pokušaćemo to da uradimo još jednom, sa preostalim atributima.<jupyter_code>korelacija = df.corr()
korelacija<jupyter_output><empty_output><jupyter_text>Izvlačenjem i sortiranjem svih korelacija po apsolutnoj vrednosti, možemo primetiti da postoji nekoliko atributa sa vrlo slabom vezom. Konkreno, najslabija je veza sa indikatorom uzimanja antiaritmika amiodarona. Zaista, i u grupi pacijenata koji uzimaju taj lek, i u drugoj grupi, udeo uspešnih elektrokonverzija je $\approx 88,4 \%$, kako je i sveukupno, a važi i obrnuti smer. Stoga je sasvim opravdano zaključiti da su ove dve karakteristike potpuno **nezavisne** jedna od druge. Ipak, zasad nećemo dirati nijedan atribut, ali ostavljamo ovo razmatranje kao uvod u proveru statističke nezavisnosti i uopšteno **probabilističke modele**.<jupyter_code>koruspeh = korelacija.iloc[:, -1]
koruspeh.sort_values(ascending=False, key=abs)<jupyter_output><empty_output><jupyter_text>Dosad smo razmatrali sledeće indikatore niske značajnosti prediktora:* **poznavanje domena** – jasno nam je bilo da npr. datum nije važan,* **niska korelacija sa ciljnim atributom** – uvodna i poslednja diskusija,* **veliki udeo nedostajućih vrednosti** – npr. kod vrednosti INR.Poznati su još neki načini za otkrivanje niske značajnosti prediktora:* **niska disperzija** – slabo pokrivanje raznovrsnosti u podacima,* **visoka korelacija sa nekim drugim atributom** – duplirana informacija,* **upotreba modela** – ovo očekujemo da će nam kasnije pomoći.Razmatranjem disperzija atributa, nakon normalizacije, ne primećujemo da je neka izrazito mala.<jupyter_code>from sklearn.preprocessing import normalize

norm = normalize(df, axis=0)
norm = pd.DataFrame(norm, df.index, df.columns)

var = np.var(norm, axis=0)
var.sort_values(ascending=False)<jupyter_output><empty_output><jupyter_text>Razdvojivost klasa Dosad smo se bavili karakteristikama koje opisuju pacijenta uopšteno, kao i time u kakvoj su one vezi sa uspešnosti elektrokonverzije, kao ciljnim atributom. Međutim, upitno je koliko su instance uopšte i **razdvojive** prema klasi u vektorskom prostoru osobina. Pitamo se postoji li nada da kroz prostor povučemo hiperravan ili složeniju strukturu (mada opet dovoljno prostu, kako nam model ne bi bio **preprilagođen**) koja bi adekvatno razdvojila uspešne procedure od neuspešnih. Za ovo razmatranje koristimo grafički prikaz podataka. Kako nije moguće nacrtati tačke u prostoru velike dimenzije, posežemo za **dimenzionom redukcijom**. U najpoznatije tehnike iz ovog domena svakako spada **analiza glavnih komponenti** (PCA), zasnovana na statistici i numeričkoj matematici.<jupyter_code>from sklearn.decomposition import PCA<jupyter_output><empty_output><jupyter_text>PCA je postupak kojim se višedimenzioni skup podataka razlaže na niz **ortogonalnih komponenti** koje redom pokrivaju **maksimalnu količinu varijanse**. Prvi atribut transformisanog skupa, dakle, objašnjava najveći udeo raznolikosti unutar skupa, a svaka sledeća komponenta zatim pokriva najveću preostalu moguću varijansu, pod ograničenjem da je ortogonalna na prethodne komponente. Rezultujuće karakteristike, od kojih je svaka linearna kombinacija originalnih atributa, predstavljaju nekorelisani ortogonalni bazni skup. U pitanju su realne vrednosti koje same po sebi nemaju očigledan značaj.<jupyter_code>podaci = df.iloc[:, :-1]

pca = PCA()

df_pca = pd.DataFrame(pca.fit_transform(podaci),
                      index = df.index,
                      columns = [f'pca{i}' for i in range(1, df.shape[1])])

df_pca<jupyter_output><empty_output><jupyter_text>Međutim, za svaki novi atribut poznato je koliki tačno udeo raznolikosti pokriva. Ideja dimenzione redukcije pomoću PCA jeste da se uzme što manji broj **glavnih komponenti** koje pokrivaju što veću varijansu. Kada je u pitanju prikaz koji bi nam kazao nešto o razdvojivosti skupa, najviše možemo uzeti prve tri komponente, jer smo ograničeni na 2D ili 3D sliku. Primećujemo da one pokrivaju odličnih $94,7\%$ raznolikosti, odnosno isto dobrih $89,5\%$ za prve dve.<jupyter_code>obj = pd.Series(pca.explained_variance_ratio_,
                index = df_pca.columns)
obj<jupyter_output><empty_output><jupyter_text>Izdvajamo prve tri komponente i dodatno ilustrujemo udeo objašnjene varijanse. Stubići predstavljaju varijansu koju objašnjava svaka od izvučenih glavnih komponenti, dok linija sumira pojedinačne delove, dajući ukupnu objašnjenu raznolikost, za koju smo malopre utvrdili da je zadovoljavajuća.<jupyter_code>df_pca = df_pca.iloc[:, :3]
obj = obj[:3]

def objasni(df, obj):
    plt.ylim(top=1.1)

    plt.bar(df_pca.columns, obj,
            label = 'Procenat varijanse')
    
    plt.plot(df_pca.columns, np.cumsum(obj),
             color = 'tab:orange',
             label = 'Kumulativna varijansa', 
             marker = 'x')

    plt.xlabel('Glavne komponente')
    plt.ylabel('Udeo objašnjene varijanse')
    
    plt.legend()

    plt.show()

objasni(df_pca, obj)<jupyter_output><empty_output><jupyter_text>Poslednji korak je iscrtavanje odgovarajućih slika. Na 2D grafiku vidimo veliko preklapanje klasa, što na prvi pogled ne zvuči obećavajuće. Ipak, primećuje se da postoji deo prostora po prvoj dimenziji na kom je elektrokonverzija sigurno uspešna, što bi moglo biti od koristi pri daljim razmatranjima.<jupyter_code>_, axs = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(11, 2))

for i in range(3):
    ax = axs[i]
    
    if i != 0:
        ax.scatter(df_pca[~uspeh].iloc[:, 0],
                   df_pca[~uspeh].iloc[:, 1],
                   color='tab:blue', alpha=.8,
                   label='Neuspešna')
    
    if i != 1:
        ax.scatter(df_pca[uspeh].iloc[:, 0],
                   df_pca[uspeh].iloc[:, 1],
                   color='tab:orange', alpha=.8,
                   label='Uspešna')

plt.legend()
plt.tight_layout()

plt.show()<jupyter_output><empty_output><jupyter_text>Ni 3D prikaz, iako objašnjava $5,2\%$ više raznolisti, nije ništa bolji po pitanju razdvojenosti pacijenata po uspehu procedure.<jupyter_code>from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(12, 3))

for i in range(131, 134):
    ax = fig.add_subplot(i, projection='3d')
    
    if i != 131:
        ax.scatter(df_pca[~uspeh].iloc[:, 0],
                   df_pca[~uspeh].iloc[:, 1],
                   df_pca[~uspeh].iloc[:, 2],
                   color='tab:blue', alpha=.5,
                   label='Neuspešna')
    
    if i != 132:
        ax.scatter(df_pca[uspeh].iloc[:, 0],
                   df_pca[uspeh].iloc[:, 1],
                   df_pca[uspeh].iloc[:, 2],
                   color='tab:orange', alpha=.5,
                   label='Uspešna')

x = ax.get_xlim3d()
y = ax.get_ylim3d()
z = ax.get_zlim3d()

for ax in fig.get_axes():
    ax.set_xlim3d(x)
    ax.set_ylim3d(y)
    ax.set_zlim3d(z)

plt.legend(loc=(1.2, 0.35))
plt.tight_layout()

plt.show()<jupyter_output><empty_output><jupyter_text>Alternativno, kako **visokovarijabilni** atributi ne bi **prekomerno** uticali na transformaciju i time je, moguće, obesmislili, pre samog PCA podatke možemo da standardizujemo. Ipak, na konkretnom problemu se ispostavlja da je pokrivenost prvih nekoliko komponenti nakon standardizacije izrazito mala, pa ih nema svrhe vizuelizovati. Ovo potencijalno znači i da su atributi u dobroj meri međusobno nezavisni, odnosno može biti da dobar deo karakteristika unosi potpuno novu informaciju, pa da je zato teško smanjiti dimenzionalnost. Pod pretpostavkom da je stvarno tako, ovo dodatno otežava i sam problem klasifikacije.<jupyter_code>from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

trans = Pipeline([('scaler', StandardScaler()), ('pca', PCA())])

df_pca = pd.DataFrame(trans.fit_transform(podaci),
                      index = df.index,
                      columns = [f'{i}' for i in range(1, df.shape[1])])

obj = pd.Series(trans.named_steps['pca'].explained_variance_ratio_,
                index = df_pca.columns)

plt.figure(figsize=(13.5, 3))

objasni(df_pca, obj)<jupyter_output><empty_output><jupyter_text>O razdvojivost Još nešto o razdvojivosti podataka može se saznati primenom **nelinearne dimenzione redukcije**. Prethodno razmatrana analiza glavnih komponenti spada u metode linearne dekompozicije. Redukcija se postiže odbacivanjem komponenti koje objašnjavaju mali udeo raznolikosti u podacima, što je ograničenje. S druge strane, nelinearni metodi ulazne podatke direktno mapiraju u **mnogostrukosti** (*manifold*) manjih dimenzija. U nastavku je razmotrena upotreba **ravnomerne aproksimacije i projekcije mnogostrukosti** (UMAP), koja čuva kako globalnu strukturu podataka, tako i lokalnu gustinu.<jupyter_code>import umap
import umap.plot<jupyter_output><empty_output><jupyter_text>Rezultati, međutim, nisu ništa više obećavajući nego dosad – u skoro svakom delu prostora nalaze se i uspešne i neuspešne elektrokonverzije.<jupyter_code>mapper = umap.UMAP(random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(densmap=True, random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)<jupyter_output><empty_output><jupyter_text>Izmena minimalne razdaljine donekle menja rezultujuću topologiju, ali je suština po pitanju razdvojivosti nepromenjena.<jupyter_code>mapper = umap.UMAP(min_dist=0.001, random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(min_dist=0.5, random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)<jupyter_output><empty_output><jupyter_text>Ni promenom broja suseda ne dolazi do značajnih promena, mada se možda bolje uočavaju neki 'klasteri' uspešnih terapija.<jupyter_code>mapper = umap.UMAP(n_neighbors=5, random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(n_neighbors=30, random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)<jupyter_output><empty_output><jupyter_text>Nema poboljšanja ni kada se primeni neka druga mera različitosti ili sličnosti, bilo da je u pitanju metrika ili ne.<jupyter_code>mapper = umap.UMAP(metric='correlation', random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(metric='cosine', random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(metric='chebyshev', random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)
mapper = umap.UMAP(metric='mahalanobis', random_state=0).fit(podaci)
_ = umap.plot.points(mapper, labels=uspeh, width=1200, height=500)<jupyter_output><empty_output><jupyter_text>Čuvanje podataka Gotove podatke **opisujemo**, i to ne sveukupno, već posebno po klasama, kako je i uobičajeno kod razmatranja ovakvih problema. Takođe predstavljamo i **p-vrednosti** poređenja raspodele atributa po klasama, izračunate pomoću Eps-Singltonovog ili $\chi^2$ testa, a koje kazuju koliko su one slične.<jupyter_code>from scipy.stats import chi2_contingency, epps_singleton_2samp

stats = pd.concat((df[uspeh].describe().T,
                   df[~uspeh].describe().T), axis=1)

p_val = []

for stat in stats.index:
    vred = np.unique(df[stat])
    n = len(vred)
    
    if n > 10:
        p_val.append(
            epps_singleton_2samp(
                df[uspeh][stat],
                df[~uspeh][stat]
            )[1]
        )
    else:
        vred = np.unique(df[stat])
        tab = np.empty((2, n), dtype=int)
        
        for i, t in enumerate(vred):
            tab[0][i] = len(df[uspeh & (df[stat] == t)])
            tab[1][i] = len(df[~uspeh & (df[stat] == t)])
        
        p_val.append(chi2_contingency(tab)[1])

stats['p_value'] = p_val

stats<jupyter_output><empty_output><jupyter_text>Na samom kraju pripreme, čuvamo prečišćeni skup podataka u novu datoteku. Završavamo sa tabelom dimenzija **147 × 49**.<jupyter_code>df.to_csv('elektroprecisceno.csv')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09976804550726238, 'fraction_numerical': 0.014552383056276577, 'mean_word_length': 4.817670682730924, 'pattern_counts': {'":': 0, '<': 128, '<?xml version=': 0, '>': 128, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '33', 'dir': 'jupyter-structured-clean-dedup', 'id': '299637', 'license': 'MIT', 'n_tokens_mistral': 15110, 'n_tokens_neox': 14688, 'n_words': 4049, 'path': 'sveske/elektropriprema.ipynb', 'repo_name': 'matfija/Elektrokonverzija', 'size': '36214'}