Töltsük be a Moodle-n található StackOverflowHungary2020.xlsx adattáblát, ami Stack Overflow programozói közösségi oldal 2020-as felmérése a világ amatőr és profi programozóiról 60 változó szerint. A teljes adatbázis (és a korábbi+újabb évek felmérései) erről a linkről elérhető.
A mi Moodle-n található Excel fájlunkban csak a 2020-as felmérés 210 magyar kitöltőjének válaszai szerepelnek az alábbi 9 változó szerint :
Állítsuk be a Working Directory-t, majd olvassuk be az Excel fájlt egy ProgData névre hallgató data frame-be:
setwd("~/Oktatás 2021221/Öko. I/R jegyzet")
library(readxl)
ProgData <- readxl::read_excel("StackOverflowHungary2020.xlsx")
str(ProgData)
## tibble [210 x 9] (S3: tbl_df/tbl/data.frame)
## $ Age : num [1:210] 31 27 22 31 24 33 43 23 38 22 ...
## $ Age1stCode : num [1:210] 12 13 11 14 15 24 10 20 12 9 ...
## $ YearsCodePro: num [1:210] 8 6 5 3 2 1 20 0.5 15 3 ...
## $ MonthlyHuf : num [1:210] 788080 1023642 1702347 511981 337703 ...
## $ Gender : chr [1:210] "Man" "Man" "Man" "Man" ...
## $ EdLevel : chr [1:210] "Unfinished university" "Master’s degree" "Bachelor’s degree" "Master’s degree" ...
## $ Employment : chr [1:210] "Employed full-time" "Employed full-time" "Independent contractor, freelancer, or self-employed" "Employed full-time" ...
## $ JobSat : chr [1:210] "Slightly dissatisfied" "Slightly satisfied" "Very satisfied" "Slightly satisfied" ...
## $ OpSys : chr [1:210] "Windows" "Windows" "Windows" "Linux-based" ...
Láthatjuk, hogy meg is van mind a \(210\) megfigyelésünk és mind a \(9\) változónk. Azt vehetjük észre rögtön az str eredményéből, hogy az utolsó 5 oszlopban lévő változót érdemes factor adattípusúvá konvertálni, mivel nominális, azaz szöveges jelentéstartalmú változók ezek, relatíve kevés lehetséges értékkel.
Ha “egy kupacban”, azaz egymás mellett lévő oszlopokon szeretnénk ugyan azt a műveletet végrehajtani egy R data frame-ben, akkor sokat könnyít az életünkön a lapply függvény. Ez a függvény egy data frame kijelölt oszlopain hajtja végre ugyan azt a műveletet. Jelen esetben a factorrá konvertálást a ProgData data frame utolsó 5, azaz \(5-9.\) oszlopain:
ProgData[,5:9] <- lapply(ProgData[,5:9], as.factor)
str(ProgData)
## tibble [210 x 9] (S3: tbl_df/tbl/data.frame)
## $ Age : num [1:210] 31 27 22 31 24 33 43 23 38 22 ...
## $ Age1stCode : num [1:210] 12 13 11 14 15 24 10 20 12 9 ...
## $ YearsCodePro: num [1:210] 8 6 5 3 2 1 20 0.5 15 3 ...
## $ MonthlyHuf : num [1:210] 788080 1023642 1702347 511981 337703 ...
## $ Gender : Factor w/ 3 levels "Man","NoAnswer",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ EdLevel : Factor w/ 6 levels "Bachelor’s degree",..: 6 3 1 3 5 1 3 4 2 5 ...
## $ Employment : Factor w/ 3 levels "Employed full-time",..: 1 1 3 1 1 3 3 1 1 3 ...
## $ JobSat : Factor w/ 5 levels "Neither satisfied nor dissatisfied",..: 2 3 5 3 3 3 3 3 3 3 ...
## $ OpSys : Factor w/ 3 levels "Linux-based",..: 3 3 3 1 3 1 3 3 3 2 ...
Voilá, meg is vagyunk a konvertálásokkal, és nem kellett mind az 5 oszlopra végigpötyögni ugyan azt a kódot. :)
Ebből a ProgData adatbázisból nagyon sok érdekes modellt lehetne építeni. Adná magát egy sima OLS regresszió a MonthlyHuf eredményváltozóra pl. De most nem ebbe az irányba indulunk el, hanem a JobSat-ra, mint eredményváltozóra akarunk egy regressziós modellt építeni!
Tehát meg akarjuk tudni, hogy mitől függ az, hogy egy programozó mennyire elégedett a munkájával.
A problémáink ott kezdődnek, hogy mint látjuk a fenti str eredményből, a JobSat változó egy nominális, azaz szövegesen megadott változó, 5 lehetséges értékkel. A levels függvénnyel meg is nézhetjük, hogy mik ezek az értékek:
levels(ProgData$JobSat)
## [1] "Neither satisfied nor dissatisfied" "Slightly dissatisfied"
## [3] "Slightly satisfied" "Very dissatisfied"
## [5] "Very satisfied"
Szóval, az eredményváltozónk nominális, én nem csak 2 lehetséges értéke van, hanem 5. Szóval, az eddig ismert, sima bináris logisztikus regressziót elfelejthetjük. Jön helyette a multinomiális logisztikus regresszió! Éljen! :)
De mindjárt látjuk, hogy nem akkora spanyolviasz ez a multinomiális logit modell. Lássuk a matematikai modellt!
Szóval, az eredményváltozó, azaz \(Y\) nomiális mérési skálájú (szöveges adat), több mint 2 lehetséges értékkel.
Esetünkben \(Y=JobSat\), melynek értékkészlete:
Ekkor azt mondjuk, hogy \(K=5\), azaz \(K\) az \(Y\) értékkészletét jelöli.
Magyarázóváltozóknak első körben tekintsünk csak 3 mennyiségi változót:
Ekkor a magyarázóváltozók száma \(k=3\).
A jelölések bevezetése után jön a TRÜKK! Az \(Y\) eredményváltozóra vezessünk be egy dummy kódolást referencia kategóriával!
Azaz:
És minden dummy változóra építsünk külön-külön bináris logisztikus regressziót. Ezzel \(5-1=4\) db regressziónk lesz, ahol a bináris logit \(0\) szintje az eredményváltozó referencia kategóriája:
Ezeket az egyenleteket emeljük \(e\)-re (\(\exp\)), majd szorozzuk fel \(P(Y=5)\)-tel:
Egyszerűsítsük egy kicsit a fenti egyenleteket azzal, hogy amiket a regressziós egyenletekből visszakapunk, az valójában mindig az aktuális, \(i\)-edik dummy változónk logitja. Azaz: \[\exp(Logit(Y_i))=\exp(\beta_{i1} \times X_1 +\beta_{i2} \times X_2 + \beta_{i3} \times X_3 + \beta_{i0}), \forall i\]
Ezzel a 4 regressziós egyenletünk:
Tehát, 4 db regressziós egyenletünk van. A 4 db dummy-val leírható eredményváltozó kategória oddsának logaritmusát (logitját) becslik meg közvetlenül a regressziók. Ha pedig tudnánk, hogy mi a modell szerint a referencia kategóriába esés valószínűsége, azaz esetünkben \(P(Y=5)\), akkor a fenti modellegyenletekből minden egyéb \(P(Y=i)\) valószínűséget is meg tudnánk becsülni!
Gondolkozzunk akkor! Nyilván igaz a valószínűségekre, hogy: \[P(Y=1) + P(Y=2) + P(Y=3) + P(Y=4) + P(Y=5) = 1\]
Ebből pedig következik, hogy: \[P(Y=5) = 1 - P(Y=1) - P(Y=2) - P(Y=3) - P(Y=4)\]
Ezen a ponton pedig felhasználhatjuk a modellegyenletekből a \(P(Y=i)\) értékekre kapott \(P(Y=i)=P(Y=5) \times \exp(Logit(Y_i))\) összefüggéseket és ezeket behelyettesíthetjük a fenti \(P(Y=5)\)-öt megadó formulába: \[P(Y=5) = 1 - [P(Y=5) \times \exp(Logit(Y_1))] - [P(Y=5) \times \exp(Logit(Y_2))] - … - [P(Y=5) \times \exp(Logit(Y_4))]\]
Azaz, egyszerűbb alakban, \(-P(Y=5)\)-öt kiemelve: \[P(Y=5)=1-P(Y=5) \times \sum_{i=1}^4{\exp(Logit(Y_i))}\]
Ha ebből az egyenletből pedig kifejezzük \(P(Y=5)\)-öt, akkor már egy egészen kulturált formulára jutunk. \[P(Y=5)=\frac{1}{1+\sum_i{\exp(Logit(Y_i))}}\]
Ezt az összefüggést felhasználva megvan a többi valószínűség becslése is az \(Y_i, i=1,2,3,4\) dummy változókra felírt modellegyenletek \(P(Y=i)=P(Y=5) \times \exp(Logit(Y_i))\) alapján:
Végül pedig azt az értéket becsüljük a regresszióval \(Y\)-ra, aminek a legnagyobb a \(P(Y=i),i=1,2,3,4,5\) bekövetezési valószínűsége!
Ez alapján megvan az is hogyan lehet a multinomiális logit modellegyenlet együtthatóit, azaz \(\beta_{ij}\)-it megbecsülni maximum likelihood elven!
Hiszen a bináris logit modellben a \(\beta\) együtthatók változtatásával a \(-\ln(L)=-(\sum_{y=1}{ln(P(Y=1))}+\sum_{y=0}{ln(P(Y=0))})\) negatív log-likelihood értéket minimalizáltunk.
Most is ez a helyzet, csak kicsit több tagú lesz a negatív log-likelihood. Hiszen most az \(Y\) eredményváltozónak nem csak 2 db \(0\) és \(1\) kategóriája van, hanem \(5\) db kategóriát kell figyelembe venni: \[-\ln(L)=-(\sum_{y=1}{ln(P(Y=1))}+...+\sum_{y=5}{ln(P(Y=5))})\]
Tehát, a likelihood érték meghatározása egy mintaelemre most is ugyan azon az elven megy, mint a bináris logit esetében. Azt mondjuk, hogy a valóságban \(y=i\) elégedettségi szintbe tartozó programozók a modell szerint az egyenletekből kapott \(P(Y=i)\) valószínűséggel fordulnak elő a világban. Mivel most id lépésben feltesszük, hogy a 210 elemű mintánk FÜGGETLEN megfigyelésekből áll, a teljes minta bekövetkezési valószínűsége az egyéni Likelihood értékek szorzata. Azaz logaritmus szinten a teljes minta log-likelihoodja a logaritmizált egyéni likelihoodok összege!
És kész, ezzel a \(-\ln(L)\) minimalizálással meg is vannak minden egyenletben a \(\beta_{ij}\) együtthatók maximum likelihood becslései! :)
Ez azért is jó hír, mert ha a modell egészének az illeszkedése leírható egy \(-\ln(L)\)-el, mint a bináris logit modell esetében, akkor az azt jelenti, hogy a multinomiális esetben az \(IC\)-k és a pszeudo R-négyzet mutató és a \(\chi^2\)-féle illeszkedésvizsgálat ugyan úgy számíthatók, mint a bináris logit esetében!
Na, akkor ezek után csináljunk egy ilyen multinomiális logit modellt R-ben végre!
De mielőtt tényleg belevágunk be kéne állítani egy értelmes referencia kategóriát a nominális eredményvátozónknak. Ez legyen most a legalacsonyabb elégedettségi szint, a Very dissatisfied. Használhatjuk itt is a relevel függvényt, mint a nominális magyarázóváltozóknál tettük:
ProgData$JobSat <- relevel(ProgData$JobSat, ref = "Very dissatisfied")
A függvény, ami megalkotja nekünk a multinomiális logisztikus regressziót multinom névre hallgat és a nnet csomagban lakik. Szóval, telepítsük a csomagot és hivatkozzuk is be R-be:
install.packages("nnet")
library(nnet) # Szokásos módon ne törődjünk az esetleges Warningokkal! :)
## Warning: package 'nnet' was built under R version 4.1.2
A multinom függvény igazából teljesen úgy használható, mint a sima lm függvény. Mivel csak multinomiális logit modelleket csinál ez a függvény, nem kell benne szórakozni a family paraméterrel, mint a glm függvényben kellett.
Első körben magyarázóváltozónk az elégedettségi szinthez (JobSat) legyen az a 3 db, amit az elején is mondtunk: Age, YearsCodePro és MonthlyHuf.
multi_logit <- multinom(JobSat ~ Age + YearsCodePro + MonthlyHuf, data = ProgData)
## # weights: 25 (16 variable)
## initial value 337.981962
## iter 10 value 325.590915
## iter 20 value 301.285247
## final value 301.237278
## converged
summary(multi_logit)
## Call:
## multinom(formula = JobSat ~ Age + YearsCodePro + MonthlyHuf,
## data = ProgData)
##
## Coefficients:
## (Intercept) Age YearsCodePro
## Neither satisfied nor dissatisfied -0.7592164 0.056689359 -0.06215057
## Slightly dissatisfied 0.2416452 0.054695642 -0.05323145
## Slightly satisfied 1.5794347 0.003601398 0.01897727
## Very satisfied 2.3996360 -0.056026755 0.09288114
## MonthlyHuf
## Neither satisfied nor dissatisfied -1.389351e-07
## Slightly dissatisfied -7.399280e-07
## Slightly satisfied -3.679717e-07
## Very satisfied 2.350005e-10
##
## Std. Errors:
## (Intercept) Age YearsCodePro
## Neither satisfied nor dissatisfied 1.963987e-13 6.525743e-12 1.816675e-12
## Slightly dissatisfied 2.820433e-13 9.256965e-12 2.478016e-12
## Slightly satisfied 1.734406e-13 5.810523e-12 1.846768e-12
## Very satisfied 8.636479e-14 2.930356e-12 9.909756e-13
## MonthlyHuf
## Neither satisfied nor dissatisfied 2.838243e-07
## Slightly dissatisfied 2.877590e-07
## Slightly satisfied 2.426048e-07
## Very satisfied 2.276008e-07
##
## Residual Deviance: 602.4746
## AIC: 634.4746
No, hát most a summary egy jó nagy táblát dobott vissza, hiszen \(5-1=4\) db egyenletünk van. És így sok helyünk nincs, szóval a magyarázóváltozók parciális teszteléséhez, azaz a \(H_0: \beta_{ij}=0\)-hoz tartozó p-értékeket nem is adja meg a summary, csak az egyes együtthatók standard hibáit.
Ezen kívül leolvasható még az eredményből a modell \(-2\ln(L)\) értéke, mint Residual Deviance a sima bináris logithoz hasonló módon, és az ebből számított \(AIC\).
Egyelőre elemezzük, hogy mit mutatnak meg nekünk az együtthatók, azaz a \(§\beta_{ij}\) értékek! Mivel minden egyenlet egy \(Logit(Y_i)\) transzformációra lett felírva, így az értelmezéshez érdemes lesz őket \(e\)-re emelni az exp függvénnyel, mint a sima bináris esetben tettük. Először kiolvassuk az együtthatókat a coef függvénnyel, aztán jöhet az \(e\)-adik hatvány:
exp(coef(multi_logit))
## (Intercept) Age YearsCodePro
## Neither satisfied nor dissatisfied 0.468033 1.0583270 0.9397414
## Slightly dissatisfied 1.273342 1.0562191 0.9481605
## Slightly satisfied 4.852212 1.0036079 1.0191585
## Very satisfied 11.019164 0.9455138 1.0973313
## MonthlyHuf
## Neither satisfied nor dissatisfied 0.9999999
## Slightly dissatisfied 0.9999993
## Slightly satisfied 0.9999996
## Very satisfied 1.0000000
Amit így kapunk azok megintcsak az egyes \(Y_i\) kategóriák ODDS-aira vett marginális hatásai a magyarázóváltozóknak! Csak most ezekhez az oddsokhoz hozzátartozik, hogy mindig az egyenletekből kihagyott referencia kategóriához képest értendők!!
Nézzük végig mit jelentenek ez alapján az Age \(e\)-ra emelt együtthatói az egyes egyenletekben:
Összességében tehát elmondható, hogy a kor növekedésével (változatlan programozási tapasztalat és fizetés mellett), várhatóan csökken a programozók elégedettsége a munkájukkal. Mivel a “legpozitív hangulatú”, Very satisfied kategória oddsaira hat negatívan a magasabb kor, és a második legpozitívabb Slightly satisfied kategória oddsain lényegében nem változtat a referencia, Very dissatisfied kategóriáoz képest.
Ezek alapján pedig azt tudjuk elmondani a YearsCodePro változóra nézve, hogy minél több éve foglalkozik a programozó profi szinten programozással (változatlan életkor és fizetés mellett), várhatóan annál elégedettebb a munkájával. Ugyanis, a két “legpozitív hangulatú”, Very satisfied és Slightly satisfied kategória oddsaira hat pozitívan (1-nél nagyobb \(e\)-re emelt együtthatókkal) a magas profi programozási tapasztalat a Very dissatisfied kategóriához képest.
Érdekes módon a változatlan életkor és profi programozási tapasztalat mellett úgy tűnik, mintha a munkával vett elégedettségre nem hatna érdemben a fizetés, mivel minden elégedettségi kategóriára nézve az \(e\)-re emelt együtthatója közel \(1\). Azaz, a ceteris paribus magasabb fizetés várhatóan érdemben nem növeli és nem csökkenti az egyéb elégedettségi kategóriák oddsait a Very dissatisfied kategóriához képest.
Szumma szummárum, a fiatal, de még így is nagy programozási tapasztalattal bíró programozók a legelégedettebbek a munkájukkal, fizetéstől függetlenül. Ezt mondja jelenleg nekünk a multinomiális logit modell.
NODE, a magyarázóváltozók közül melyek a szignifikánsak és hogyan tudunk közöttük ez alapján egy fontossági sorrendet felállítani az elégedettségi kategóriák előrejelzését nézve?
Mivel egy magyarázóváltozónak jelenleg mind a négy egeynletben szerepelnek együtthatói, így azt a \(H_0\)-t, ami szerint az adott magyarázóváltozó nem szignifikáns, úgy lehet leafordítani a multinomiális logit modellünk nyelvére, hogy a magyarázóváltozónak mind a négy egyenletben egyszerre tekinthető az együtthatója 0-nak a mintán kívüli világban! Ezzel az alábbi lesz a konkrétan megvizsgált \(H_0\) és \(H_1\) párunk:
A fenti \(H_0\) és \(H_1\) pároshoz próbafüggvényt úgy kapunk, hogy legyártjuk azt a modellt, amiből a tesztelt változót kihagyjuk, és összenézzük a két modell \(-2\ln(L)\) értékét. A bővebb modell \(-2\ln(L)\) értékét jelöljük \(-2\ln(L_{H_1})\)-nek, míg a nullhipotézisben feltételezett szűkebb modellét \(-2\ln(L_{H_0})\)-nak. A próbafüggvény ezek után e két érték különbsége, ami igaz \(H_0\) esetén \(\chi^2_m\) eloszlású, pont mint a sima bináris logisztikus regresszió \(\chi^2\)-féle illeszkedésvizsgálata esetében:
A különbség annyi, hogy itt \(-2\ln(L_{H_0})\) nem a nullmodell, hanem az egy változóval szűkebb modell \(-2\ln(L)\) értéke. Illetve, az \(m\) szabadságfok a kihagyott \(\beta\)-k száma, azaz gyakorlatilag az egyenletek száma. Hiszen egy magyarázóváltozó minden egyenletben csak egyszer, egy \(\beta\)-val szerepel. Kivéve persze a nominális magyarázóváltozókat, hiszen ők minden egyenletben annyiszor szerepelnek, ahány lehetséges értékük van, mínusz 1. Azaz ők az \(m\) szabadságfokhoz dummy-k száma * egyenletek száma értéket adnak hozzá :)
A \(\chi^2\)-féle illeszkedésvizsgálat most bemutatott alkalmazását a multinomiális logit modellekben az lmtest csomag lrtest függvénye valósítja meg. Az lr elnevezés a likelihood-ratio rövidítésből jön, mivel a log-likelihoodok különbsége az eredeti (nem logaritmált) likelihood-ok hányadosa. :)
Az lrtest függvénybe egyszerűen csak bele kell rakni a vizsgált modell R objektumát, és a változó nevét, amit vizsgálunk. Pl. az Age változó szignifikanciája a modellünkben:
library(lmtest)
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
lrtest(multi_logit, "Age")
## # weights: 20 (12 variable)
## initial value 337.981962
## iter 10 value 326.036197
## iter 20 value 303.806372
## final value 303.806030
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ Age + YearsCodePro + MonthlyHuf
## Model 2: JobSat ~ YearsCodePro + MonthlyHuf
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 16 -301.24
## 2 12 -303.81 -4 5.1375 0.2735
A p-érték \(27.35\%\), ami elég magas, az Age változó nem szignifikánsnak vehető minden szokásos szignifikanci-szinten. Lássuk akkor a többi változót:
lrtest(multi_logit, "YearsCodePro")
## # weights: 20 (12 variable)
## initial value 337.981962
## iter 10 value 308.824226
## iter 20 value 305.462539
## final value 305.462515
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ Age + YearsCodePro + MonthlyHuf
## Model 2: JobSat ~ Age + MonthlyHuf
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 16 -301.24
## 2 12 -305.46 -4 8.4505 0.0764 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
lrtest(multi_logit, "MonthlyHuf")
## # weights: 20 (12 variable)
## initial value 337.981962
## iter 10 value 304.669638
## final value 303.238079
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ Age + YearsCodePro + MonthlyHuf
## Model 2: JobSat ~ Age + YearsCodePro
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 16 -301.24
## 2 12 -303.24 -4 4.0016 0.4058
Hm, úgy tűnik, hogy csak a YearsCodePro változó mondható szignifikánsnak, és az is csak \(\alpha=10\%\)-on. Ez szomorú, de hát van még minekünk több lehetséges magyarázóváltozónk, nézzük meg, hogy szuperál a modell, ha minden lehetséges magyarázóváltozót beleveszünk!
Mielőtt ész nélkül megcsinálnánk a minden magyarázóváltozót használó teljes modellt, nézzünk azért rá a változóink egyszerű leíró statisztikáira a summary-vel:
summary(ProgData)
## Age Age1stCode YearsCodePro MonthlyHuf
## Min. :21.00 Min. : 3.00 Min. : 0.500 Min. : 61284
## 1st Qu.:26.00 1st Qu.:10.25 1st Qu.: 3.000 1st Qu.: 511981
## Median :31.00 Median :13.00 Median : 6.000 Median : 767652
## Mean :32.17 Mean :13.92 Mean : 8.688 Mean : 888295
## 3rd Qu.:38.00 3rd Qu.:16.00 3rd Qu.:14.000 3rd Qu.:1023682
## Max. :54.00 Max. :35.00 Max. :33.000 Max. :6649792
## Gender EdLevel
## Man :200 Bachelor’s degree :71
## NoAnswer: 6 Doctoral degree :11
## Woman : 4 Master’s degree :54
## Other :14
## Secondary school :19
## Unfinished university:41
## Employment
## Employed full-time :167
## Employed part-time : 10
## Independent contractor, freelancer, or self-employed: 33
##
##
##
## JobSat OpSys
## Very dissatisfied :15 Linux-based: 58
## Neither satisfied nor dissatisfied:23 MacOS : 27
## Slightly dissatisfied :39 Windows :125
## Slightly satisfied :69
## Very satisfied :64
##
Amit itt rögtön ki tudunk szúrni az a Gender változó. Ezt nem érdemes magyarázóváltozóként alkalmazni a logit modellben, mert lényegében csak egyféle lehetséges értéke van, a férfi. A többi kategória gyakorisága nagyon ritka. Ez nem meglepő amúgy, programozókról beszélünk… :)
A többi lehetséges magyarázóváltozó első szemmelverésre rendben lévőnek tűnik, nem látszik semmi durva outlier pl. a felső kvartilisek és a maximumok viszonyából sem a numerikus változókban.
Akkor futtassuk a modellt, ami a JobSat magyarázatára minden egyéb magyarázóváltozót használ, csak a Gendert nem, majd nézzük meg szépen minden magyarázóváltozó szignifikanciáját az LR-teszttel:
multi_logit <- multinom(JobSat ~ .-Gender, data = ProgData)
## # weights: 75 (56 variable)
## initial value 337.981962
## iter 10 value 321.589734
## iter 20 value 284.983944
## iter 30 value 277.192749
## iter 40 value 276.509416
## iter 50 value 276.417469
## iter 60 value 276.358376
## iter 70 value 276.357310
## iter 70 value 276.357307
## iter 70 value 276.357307
## final value 276.357307
## converged
lrtest(multi_logit, "Age")
## # weights: 70 (52 variable)
## initial value 337.981962
## iter 10 value 322.149564
## iter 20 value 280.906386
## iter 30 value 279.003929
## iter 40 value 278.684730
## iter 50 value 278.553336
## iter 60 value 278.549269
## final value 278.549244
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age1stCode + YearsCodePro + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 52 -278.55 -4 4.3839 0.3565
lrtest(multi_logit, "Age1stCode")
## # weights: 70 (52 variable)
## initial value 337.981962
## iter 10 value 325.208447
## iter 20 value 279.944675
## iter 30 value 278.308986
## iter 40 value 277.979728
## iter 50 value 277.892274
## iter 60 value 277.873682
## final value 277.873437
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + YearsCodePro + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 52 -277.87 -4 3.0323 0.5524
lrtest(multi_logit, "YearsCodePro")
## # weights: 70 (52 variable)
## initial value 337.981962
## iter 10 value 326.104558
## iter 20 value 282.181743
## iter 30 value 280.232862
## iter 40 value 279.824473
## iter 50 value 279.734431
## iter 60 value 279.724666
## final value 279.724527
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 52 -279.73 -4 6.7344 0.1506
lrtest(multi_logit, "MonthlyHuf")
## # weights: 70 (52 variable)
## initial value 337.981962
## iter 10 value 304.164320
## iter 20 value 279.270860
## iter 30 value 278.010536
## iter 40 value 277.760426
## iter 50 value 277.687197
## iter 60 value 277.680882
## final value 277.680831
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 52 -277.68 -4 2.647 0.6185
lrtest(multi_logit, "EdLevel")
## # weights: 50 (36 variable)
## initial value 337.981962
## iter 10 value 322.020791
## iter 20 value 298.505987
## iter 30 value 290.090623
## iter 40 value 289.881605
## iter 50 value 289.850645
## final value 289.850460
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 36 -289.85 -20 26.986 0.1356
lrtest(multi_logit, "Employment")
## # weights: 65 (48 variable)
## initial value 337.981962
## iter 10 value 321.721269
## iter 20 value 289.500207
## iter 30 value 281.355972
## iter 40 value 280.726635
## iter 50 value 280.665703
## iter 60 value 280.641506
## final value 280.641226
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + EdLevel +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 48 -280.64 -8 8.5678 0.3801
lrtest(multi_logit, "OpSys")
## # weights: 65 (48 variable)
## initial value 337.981962
## iter 10 value 321.734240
## iter 20 value 285.693568
## iter 30 value 281.354767
## iter 40 value 280.980166
## iter 50 value 280.901543
## iter 60 value 280.898815
## final value 280.898803
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + EdLevel +
## Employment
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 56 -276.36
## 2 48 -280.90 -8 9.083 0.3353
Ajjaj az a helyzet, hogy egy magyarázóváltozónk sem szignifikáns! Ez azt sugallja, hogy az aktuális modellünk NEM tér el szignifikánsan a nullmodelltől.
Gyártsuk is le a nullmodellt, és győződjünk meg erről az LR-teszt azon verziójával, amit akkor kapunk, ha azt gondoljuk a \(H_0\)-ban, hogy minden \(\beta\) minden egyenletben nullának vehető a mintán kívüli világban \(\forall\beta_{ij}=0\).
Ez a folyamat úgy történik pont, mint a sima bináris logit modellnél történt:
# Nullmodell = Magyarázóváltpzók nélküli modell építése
multi_logit_null <- multinom(JobSat ~ 1, data = ProgData)
## # weights: 10 (4 variable)
## initial value 337.981962
## iter 10 value 308.954691
## iter 10 value 308.954691
## final value 308.954691
## converged
# Khi-négyzet próba a nullmodellre és az aktuális modellre
anova(multi_logit, multi_logit_null)
## Model
## 1 1
## 2 (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender + EdLevel + Employment + OpSys) - Gender
## Resid. df Resid. Dev Test Df LR stat. Pr(Chi)
## 1 836 617.9094 NA NA NA
## 2 784 552.7146 1 vs 2 52 65.19477 0.1034081
A p-érték \(10.34\%\), tehát éppenhogy, de még \(\alpha=10\%\)-on sem lehet elvetni azt a \(H_0\)-t, hogy a modellünkben minden \(\beta_{ij}=0\).
Na, ilyet aztán még nem láttunk! Most akkor mivan?
Nos a megfejtésünk az, hogy túl sok paraméterünk, azaz túl sok \(\beta\)-nk van a modellben a megfigyelések számához képest!!
Általános ajánlás, hogy egy regressziós modellben az elemszám, azaz \(n\) legyen nagyobb, mint a paraméterek számának ötszöröse: \(n > p \times 5\) vagy \(\frac{n}{p} > 5\).
Ez azért van, mert minél több \(\beta\)-ja, azaz paramétere van a regressziónknak annál jobban tud ráilleszkedni a megfigyelt adatokra.
Hiszen annál több lehetősége van a negatív log-likelihoodot, tehát \(-\ln(L)\)-t vagy az \(SSE\)-t minimalizálni. Azaz a modell túlilleszkedik.
Ez az a jelenség, ami ellen védekezünk modellszelekciónál a Wald-próbával és az \(IC\)-k használatával, mivel ezek az eszközök büntetik a feleslegesen bevont extra magyarázóváltozókat, azaz \(\beta\)-kat. De a túlillesztés egy modellben önmagában is jelentkezhet gondot, ha az elemszámhoz képest túl sok a \(\beta\).
Ez a jelenség, hogy egy modell önmagában is túlillesztett legyen az elemszámhoz képest a sima lineáris és bináris logisztikus regresszióknál nem gyakran áll fenn, mivel nagyon sok magyarázóváltozó kell hozzá. Pl. a mostani \(210\)-es elemszámunkhoz \(\frac{n}{5}=\frac{210}{5}=42\) paraméter kell, hogy a határt elérjük.
Ellenben most a multinomiális regressziónál van \(4\) egyenletünk, ami alapból minden \(\beta\)-t négyszerez, és van ugye nominális magyarázóváltozó is, ami sok-sok dummy-t jelent…Nézzük meg hány \(\beta\) is van most a modellben:
length(coef(multi_logit))
## [1] 56
Ínnye, \(56>42\)… :( Tehát a modellünkben túl sok paraméter van a megfigyelések számához képest. Ilyenkor pedig minden hipotézisvizsgálat elkezd “túl óvatos lenni”, mondván egy ennyire túlillesztett modellben semmi változó hatása nem lehet szignifikáns a mintán kívüli világan!
Hogyan tegyük helyre a kérdést? Hát csökkentsük az egyenletek számát! Ezt hogy lehet elérni? A ritka eredményváltozó kategóriák összevonásával!
Ezekben a ritka gyakoriságú kategóriákban valószínűleg egyébként is elég béna a modell magyarázóereje, szóval valószínűleg nem kár az összevonásukért. :)
Az összevonandó kategóriák kijelöléséhez lássuk először az eredményváltozónk egyes értékeinek gyakoriságait!
summary(ProgData$JobSat)
## Very dissatisfied Neither satisfied nor dissatisfied
## 15 23
## Slightly dissatisfied Slightly satisfied
## 39 69
## Very satisfied
## 64
Úgy látszik, hogy a 3 legritkább kategória tök logikusan egybevonható, mivel ezek a “nem satisfied” elégedettségi szintek.
Tegyük is meg ezt a lépést!
Először character adattípusúvá kell konvertálnunk a JobSat változót, hogy az értékkészletét meg tudjuk buherálni, majd elvégezzük a 3 kérdéses kategória átnevezését Not satisfied-á. Végül újra factor adattípusra lépünk vissza, hogy továbbra is tudjon a multinomiális modellünk mit kezdeni a változóval, és beállítjuk az új, “Not satisfied” kategóriát referenciának.
Ez a szép hosszadalmas lépéssorozat R kód szinten a következőképp néz ki:
ProgData$JobSat <- as.character(ProgData$JobSat)
ProgData$JobSat[ProgData$JobSat == "Neither satisfied nor dissatisfied"] <- "Not satisfied"
ProgData$JobSat[ProgData$JobSat == "Slightly dissatisfied"] <- "Not satisfied"
ProgData$JobSat[ProgData$JobSat == "Very dissatisfied"] <- "Not satisfied"
ProgData$JobSat <- as.factor(ProgData$JobSat)
ProgData$JobSat <- relevel(ProgData$JobSat, ref = "Not satisfied")
summary(ProgData$JobSat)
## Not satisfied Slightly satisfied Very satisfied
## 77 69 64
Szuper, a summary eredménye alapján, tök jól összevonásra került a 3 ritka kategória. Most már az eredményváltozónak csak 3, többi kevésbé hasonló gyakoriságú csoportja van, így a multinom modellnek csak \(3-1=2\) egyenlete lesz mindösszesen.
Lássuk is akkor az új modellt, és paramétereinek (azaz \(\beta\)-inak) számát!
multi_logit <- multinom(JobSat ~ .-Gender, data = ProgData)
## # weights: 45 (28 variable)
## initial value 230.708581
## iter 10 value 217.156107
## iter 20 value 208.119464
## iter 30 value 208.107978
## iter 30 value 208.107977
## iter 30 value 208.107977
## final value 208.107977
## converged
length(coef(multi_logit))
## [1] 28
Na már csak \(28\) paraméterünk van, ami kisebb, mint \(\frac{n}{5}=\frac{210}{5}=42\). Hurrá!
Akkor lásszuk van-e már szignifikáns változónk az LR-tesztek szerint:
lrtest(multi_logit, "Age")
## # weights: 42 (26 variable)
## initial value 230.708581
## iter 10 value 213.243280
## iter 20 value 210.237437
## final value 210.231796
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age1stCode + YearsCodePro + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 26 -210.23 -2 4.2476 0.1196
lrtest(multi_logit, "Age1stCode")
## # weights: 42 (26 variable)
## initial value 230.708581
## iter 10 value 212.536379
## iter 20 value 209.360065
## final value 209.353919
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + YearsCodePro + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 26 -209.35 -2 2.4919 0.2877
lrtest(multi_logit, "YearsCodePro")
## # weights: 42 (26 variable)
## initial value 230.708581
## iter 10 value 214.245861
## iter 20 value 211.517476
## final value 211.506235
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + MonthlyHuf + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 26 -211.51 -2 6.7965 0.03343 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
lrtest(multi_logit, "MonthlyHuf")
## # weights: 42 (26 variable)
## initial value 230.708581
## iter 10 value 211.419515
## iter 20 value 208.515045
## final value 208.507922
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + EdLevel + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 26 -208.51 -2 0.7999 0.6704
lrtest(multi_logit, "EdLevel")
## # weights: 30 (18 variable)
## initial value 230.708581
## iter 10 value 222.483208
## iter 20 value 216.669008
## final value 216.668572
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + Employment +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 18 -216.67 -10 17.121 0.07172 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
lrtest(multi_logit, "Employment")
## # weights: 39 (24 variable)
## initial value 230.708581
## iter 10 value 219.610428
## iter 20 value 210.379812
## final value 210.371184
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + EdLevel +
## OpSys
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 24 -210.37 -4 4.5264 0.3394
lrtest(multi_logit, "OpSys")
## # weights: 39 (24 variable)
## initial value 230.708581
## iter 10 value 218.451457
## iter 20 value 211.273584
## final value 211.271181
## converged
## Likelihood ratio test
##
## Model 1: JobSat ~ (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender +
## EdLevel + Employment + OpSys) - Gender
## Model 2: JobSat ~ Age + Age1stCode + YearsCodePro + MonthlyHuf + EdLevel +
## Employment
## #Df LogLik Df Chisq Pr(>Chisq)
## 1 28 -208.11
## 2 24 -211.27 -4 6.3264 0.1761
No, egész tűrhető a dolog! A YearsCodePro szignifikáns változó lett \(\alpha=5\%\)-on, míg az EdLevel \(\alpha=5\%\)-on. A két igazán nem szignifikáns változónk \(20\%\) feltti p-értékekkel az Age1stCode, az Employment és a MonthlyHuf.
Vegyük ki ezt a 3 elég haszontalannak tűnő változót a modellből, és nézzik meg az \(IC\)-k, és az LR-teszt szerint javult-e ezzel a modell. Most az LR-teszt \(\chi^2\)-próbájához a p-értéket az anova függvénnyel számoljuk R-ben, mivel ezzel a függvénnyel lehet úgy használni a tesztet, hogy két (akár több, mint egy változóban eltérő modellt) hasonlítunk össze. Ugye az lrtest függvényben mindig csak egy adott változó \(\beta\)-inak kinullázását vizsgáltuk.
Ne feledjük, hogy az LR-tesztben a \(H_0\) továbbra is az, hogy a szűkített modell a preferált!
multi_logit_szuk <- multinom(JobSat ~ Age + YearsCodePro + EdLevel + OpSys, data = ProgData)
## # weights: 33 (20 variable)
## initial value 230.708581
## iter 10 value 212.437840
## iter 20 value 211.910561
## final value 211.910444
## converged
# minden eszköz a szűkített modellt preferálja
AIC(multi_logit, multi_logit_szuk)
## df AIC
## multi_logit 28 472.2160
## multi_logit_szuk 20 463.8209
BIC(multi_logit, multi_logit_szuk)
## df BIC
## multi_logit 28 565.935
## multi_logit_szuk 20 530.763
anova(multi_logit, multi_logit_szuk)
## Model
## 1 Age + YearsCodePro + EdLevel + OpSys
## 2 (Age + Age1stCode + YearsCodePro + MonthlyHuf + Gender + EdLevel + Employment + OpSys) - Gender
## Resid. df Resid. Dev Test Df LR stat. Pr(Chi)
## 1 400 423.8209 NA NA NA
## 2 392 416.2160 1 vs 2 8 7.604933 0.4729804
Oké: mindkét \(IC\) a szűkített modellnél kisebb, és az LR-teszt p-értéke is jó magas, azaz minden szokásos \(\alpha\)-n elfogadhatjuk \(H_0\)-t. Szumma, szummárum, a szűkített modellünk jobban illeszkedik az adatokra!!
Lássunk hát pár együttható értelmezést a szűkített modellből, természetesen \(e\) hatványra emelve!
exp(coef(multi_logit_szuk))
## (Intercept) Age YearsCodePro EdLevelDoctoral degree
## Slightly satisfied 0.8905682 0.9835542 1.051245 0.3674308
## Very satisfied 19.6831061 0.8724643 1.184561 3.5242892
## EdLevelMaster’s degree EdLevelOther EdLevelSecondary school
## Slightly satisfied 1.228248 1.638280 2.1607138
## Very satisfied 2.012958 0.817426 0.7179254
## EdLevelUnfinished university OpSysMacOS OpSysWindows
## Slightly satisfied 3.814164 0.5008606 0.7802951
## Very satisfied 2.742998 0.4183552 0.3739768
Lássuk, a két relatíve szignifikánsnak vehető változót: a YearsCodePro-t és az EdLevelt!
No, akkor próbáljuk megtudni, hogy úgy összegészében mennyire jó előrejelzéseket készít a modellünk a programozók elégedettségére!
Ehhez kérjük le a modellünk becsléseit mind a \(210\) megfigyelésünk JobSat értékére. Mivel a modell a predikciók során csak annyit csinál, hogy mindenkire kiszámolja mindhárom kategória bekövetkezési valószínűségét, és kiválasztja a legnagyobb valószínűségű kategóriát, így az R predict függvényével simán le lehet kérni közvetlenül a becslést az eredményváltozó legvalószínűbb kategóriájára. Ezt a predict függvényben egy type="class beállítással érhetjük el.
Az első két paraméter ugyan az a függvényben, amit már megszoktunk a bináris logitnál is: a modell, amiből előrejelzéseket kérünk és a data frame, aminek a soraira kellenek az előrejelzések:
ProgData$JobSatPredicted <- predict(multi_logit_szuk, newdata = ProgData, type = "class")
str(ProgData)
## tibble [210 x 10] (S3: tbl_df/tbl/data.frame)
## $ Age : num [1:210] 31 27 22 31 24 33 43 23 38 22 ...
## $ Age1stCode : num [1:210] 12 13 11 14 15 24 10 20 12 9 ...
## $ YearsCodePro : num [1:210] 8 6 5 3 2 1 20 0.5 15 3 ...
## $ MonthlyHuf : num [1:210] 788080 1023642 1702347 511981 337703 ...
## $ Gender : Factor w/ 3 levels "Man","NoAnswer",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ EdLevel : Factor w/ 6 levels "Bachelor’s degree",..: 6 3 1 3 5 1 3 4 2 5 ...
## $ Employment : Factor w/ 3 levels "Employed full-time",..: 1 1 3 1 1 3 3 1 1 3 ...
## $ JobSat : Factor w/ 3 levels "Not satisfied",..: 1 2 3 2 2 2 2 2 2 2 ...
## $ OpSys : Factor w/ 3 levels "Linux-based",..: 3 3 3 1 3 1 3 3 3 2 ...
## $ JobSatPredicted: Factor w/ 3 levels "Not satisfied",..: 2 3 1 1 2 1 3 1 3 1 ...
nagyon jó, megvannak a JobSat becslések! Ezekből már a bináris esehet hasonló módon jöhet a klasszifikációs mátrix a table függvénnyel. Most mentsük is el ezt a klasszifikációs mátrixot egy külön R objektumba, mielőtt megnéznénk az értékeit!
klassz_matrix <- table(ProgData[,c("JobSat", "JobSatPredicted")])
klassz_matrix
## JobSatPredicted
## JobSat Not satisfied Slightly satisfied Very satisfied
## Not satisfied 46 18 13
## Slightly satisfied 29 21 19
## Very satisfied 21 11 32
Nagyon jó! Látjuk az eredményből, hogy van pl. \(46\) Not satisfied programozónk, akiket helyesen azonosít a modell, míg van \(13\) valóságban Not satisfied programozónk, akiket a modell tévesen Very satisfiednak prediktált.
A klasszifikációs mátrixból ugyan úgy számolhatunk Accuracy, Precision és Recall mutatókat, mint bináris esetben. Annyi lesz az extra, hogy Precisionból és Recallból értelemszerűen \(3\) érték lesz a \(3\) kategóriára és nem csak \(2\).
Lássuk az Accuracy-t! Itt ugye a mátrix átlójában szereplő helyes találatok számát kell összeadni, és elosztani a teljes elemszámmal, azaz a mátrix elemeinek teljes összegével. Ezt a műveletet könnyen el tudjuk végezni a klassz_matrix objektumon. Hiszen be tudjuk vetni az átlóban lakó elemek kilvasására a diag függvényt:
round((sum(diag(klassz_matrix))/sum(klassz_matrix))*100,2) # szorzunk 100-al és 2 tizedesre is kerekítünk
## [1] 47.14
Azt mondja az eredmény, hogy a megfigyelések majdnem felét, \(47.14\%\)-át helyesen sorolta be a modell. Tűrhető. “Nem jó, de nem is tragikus.”
Lássuk a Precision mutatókat! Ebben a mátrixban most az oszlopokban vannak a prediktált kategóriák. Azaz minden kategóriában a megfelelő átló értéket kell osztani az adott kategória oszlopösszegével. Tehát, pl. a “Slightly satisfied” kategóriában a Precision a \(\frac{21}{18+21+11}\) műveletet jelenti. Ezt R-ben gy tudjuk könnyen kicsikarni, hogy az átlóból kiválasztjuk a 2. elemet a számálóba, majd a nevezőben összeadjuk a 2. oszlop értékeit. A 2. oszlopát a mátrixnak pedig klassz_matrix[,2] módon lehet kiválasztani.
Alkalmazzuk a fenti elvet mindhárom eredményváltozó kategóriánkra!
# Not satisfied
round((sum(diag(klassz_matrix)[1])/sum(klassz_matrix[,1]))*100,2)
## [1] 47.92
# Slightly satisfied
round((sum(diag(klassz_matrix)[2])/sum(klassz_matrix[,2]))*100,2)
## [1] 42
# Very satisfied
round((sum(diag(klassz_matrix)[3])/sum(klassz_matrix[,3]))*100,2)
## [1] 50
Ezek szerint a legpontosabban a modellünk a Very satisfied kategóriát jelzi előre: a Very satisfied kategóriájú becslései az esetek felében, \(50\%\)-ában bejönnek!
Végül nézzük a Recallokat! Most a valós eredményváltozó kategóriák a mátrix soraiban vannak, így csak annyir kell változtatni a Precision esetében használt képleteken, hogy a helyes találatok számát az adott kategóriában (ami az átlóban van) nem az adott kategória oszlopösszegéhez, hanem sorösszegéhez kell viszonyítani a nevezőben! A klasszifikációs mátrixunk \(i\)-edik sorát pedig klassz_matrix[i,] módon lehet kiválasztani.
Alkalmazzuk a fenti elvet mindhárom eredményváltozó kategóriánkra!
# Not satisfied
round((sum(diag(klassz_matrix)[1])/sum(klassz_matrix[1,]))*100,2)
## [1] 59.74
# Slightly satisfied
round((sum(diag(klassz_matrix)[2])/sum(klassz_matrix[2,]))*100,2)
## [1] 30.43
# Very satisfied
round((sum(diag(klassz_matrix)[3])/sum(klassz_matrix[3,]))*100,2)
## [1] 50
Az eredmények alapján a modellünk igen jó a Not satisfied kategóriák felderítésében, a valóságban Not satisfied programozók majdnem \(60\%\)-át helyesen azonosította.
Mivel most nincs cut-off értékünk, hiszen egyszerűen a legnagyobb valószínűséggel bekövetkező kategóriát prediktálja a modell, így a klasszifikációs mátrixnak ez az egy verziója van multinomiális esetben. Nincs lehetőségünk most arra, hogy a Precison/Recall értékekkel játszunk a modell megváltoztatása nélkül.
A modell becslési pontosságát meg tudjuk mérni multinomiális esetben is a McFadden-féle pszeudo R-négyzet mutatóval. Hiszen láttuk a 2. fejezetben, hogy a multinomiális modell egészének az illeszkedése is leírható egy \(-\ln(L)\)-el, mint a bináris logit modell esetében. Ez nyilván igaz a nullmodellre is.
Szóvak a pszeudo \(R^2\) alábbi képlete a multinomiális esetben is alkalmazható:
A dolog szépséghibája, hogy a bináris esetben használt függvény a a pszeudo \(R^2\)-re a DescTools csomagból multinomiális esetben nem működik, így manuálisan kell kiszámítanunk az értéket.
De ezt meg is tudjuk tenni minden további nélkül, mivel a deviance függvénnyel ki tudjuk szedni a multinomiális modellekből is a \(-2\ln(L)\) értékeket. A kettes szorzó a fenti képlet szempontjából pedig nem számít, hiszen kiegyszerűsödik az osztás során.
Számoljuk is akkor ki ezt a csudiszép pszeudo \(R^2\)-et!
# -2*ln(L) a viszgálttárgymodellre
neg_lnL_targy <- deviance(multi_logit_szuk)
# nullmodell legyártása, és a -2*ln(L) értékének elmentése
multi_null <- multinom(JobSat ~ 1, data = ProgData)
## # weights: 6 (2 variable)
## initial value 230.708581
## final value 230.097698
## converged
neg_lnL_null <- deviance(multi_null)
# pszeudo R-négyzet számolása
(neg_lnL_null-neg_lnL_targy)/neg_lnL_null
## [1] 0.07904144
Az eredményünk alapján a szűkített modellünk \(7.9\%\)-kal ad jobb becsléseket a JobSat értékekre, mint a magyarázóváltozók nélküli nullmodell. Ez egy \(10\%\) alatti R-négyzet, szóval a modellünk magyarázóereje ez alapján gyengének minősíthető. De legalább több, mint a semmi. :)
Plusz, azt láttuk a klasszifikációs mtárixból, hogy pl. a valóságban Not satisfied programozók azonosításában egész jól szuperál a modell. Szóval, gyenge, de valamire mégiscsak jó. :)
Záróakkordként nézzük meg hogyan lehet a multinomiális modellben a magyarázóváltozók valószínűségekre gyakorolt marginális hatásait vizualizálni.
Mivel lássuk be, több egyenlet együtthatóiból leolvasgatni az ODDSokra gyakorolt marginális hatásokat kellően agymegsüllyesztő vállalkozás. :)
Lássunk valami emészthetőbbet!
Először nézzük meg hogy az egyes JobSat kategóriáknak milyen bekövetkezési valószínűséget ad a szűkített multinomiális modellünk. Első körben egy külön, BecsultVszek névre hallgató data frame-be rakjuk el ezeket a valószínűségeket. Előcsalni őket a multinomiális modellből a predict függvénnyel lehet type = "probs" beállítással:
BecsultVszek <- predict(multi_logit_szuk, newdata = ProgData, type = "probs")
str(BecsultVszek)
## num [1:210, 1:3] 0.222 0.362 0.404 0.368 0.418 ...
## - attr(*, "dimnames")=List of 2
## ..$ : chr [1:210] "1" "2" "3" "4" ...
## ..$ : chr [1:3] "Not satisfied" "Slightly satisfied" "Very satisfied"
Nagyon jó, egy \(3\) oszlopos data frame lett az eredmény a \(3\) eredményváltozó kategória bekövetkezési valószínűségeivel mind a \(210\) megfigyelt programozóra.
Kössük össze ezeket a valószínűségeket az eredeti ProgData data frame-el egy új, ProgDataWithProbs nevű data frame-ben régi barátunkkal, a cbind függvénnyel:
ProgDataWithProbs <- cbind(ProgData, BecsultVszek)
str(ProgDataWithProbs)
## 'data.frame': 210 obs. of 13 variables:
## $ Age : num 31 27 22 31 24 33 43 23 38 22 ...
## $ Age1stCode : num 12 13 11 14 15 24 10 20 12 9 ...
## $ YearsCodePro : num 8 6 5 3 2 1 20 0.5 15 3 ...
## $ MonthlyHuf : num 788080 1023642 1702347 511981 337703 ...
## $ Gender : Factor w/ 3 levels "Man","NoAnswer",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ EdLevel : Factor w/ 6 levels "Bachelor’s degree",..: 6 3 1 3 5 1 3 4 2 5 ...
## $ Employment : Factor w/ 3 levels "Employed full-time",..: 1 1 3 1 1 3 3 1 1 3 ...
## $ JobSat : Factor w/ 3 levels "Not satisfied",..: 1 2 3 2 2 2 2 2 2 2 ...
## $ OpSys : Factor w/ 3 levels "Linux-based",..: 3 3 3 1 3 1 3 3 3 2 ...
## $ JobSatPredicted : Factor w/ 3 levels "Not satisfied",..: 2 3 1 1 2 1 3 1 3 1 ...
## $ Not satisfied : num 0.222 0.362 0.404 0.368 0.418 ...
## $ Slightly satisfied: num 0.525 0.266 0.25 0.28 0.465 ...
## $ Very satisfied : num 0.253 0.372 0.345 0.353 0.117 ...
Oké, akkor mevannak a becsült valószínűségekkel kiegészített adatok is! Erre az egészre azért van szükség, mert a marginális hatások ábrázolásához ezt a data frame-t kicsit átdolgozzuk, és nem akarjuk, hogy véletlen az eredeti tábla elromoljon.
Első körben mondjuk két legszignifikánsabbnak vehető magyarázóváltozó valószínűségekre gyakorolt hatásait vizsgáljuk meg: a YearsCodePro és az EdLevel hatásait.
Szűkítsük le a ProgDataWithProbs tábla oszlopait egy új, temp_tabla objektumban úgy, hogy csak ezt a két változót és a három valószínűséget jelölő oszlopot tartalmazza:
temp_tabla <- ProgDataWithProbs[,c("YearsCodePro", "EdLevel",
"Not satisfied","Slightly satisfied",
"Very satisfied")]
# nézzünk bele az eredményül kapott temp_tabla első 6 sorába
head(temp_tabla)
## YearsCodePro EdLevel Not satisfied Slightly satisfied
## 1 8 Unfinished university 0.2220232 0.5249303
## 2 6 Master’s degree 0.3616731 0.2662595
## 3 5 Bachelor’s degree 0.4043842 0.2504966
## 4 3 Master’s degree 0.3677947 0.2795227
## 5 2 Secondary school 0.4175159 0.4653321
## 6 1 Bachelor’s degree 0.5555360 0.3009031
## Very satisfied
## 1 0.2530466
## 2 0.3720674
## 3 0.3451191
## 4 0.3526827
## 5 0.1171520
## 6 0.1435609
Ok, úgy néz ki jók vagyunk! Csak az a \(2+3=5\) oszlop szerepel itt, amit akartunk!
Ezen a ponton alakítsuk át a becsült valószínűségekkel kiegészített táblát úgy, hogy az egyes elégedettségi kategóriák valószínűségei ne oszlopokban, hanem külön sorokban szerepeljenek.
Tehát így minden programozónak \(3\) sora lesz a \(3\) elégedettségi kategória miatt. Így az új táblának összesen \(210 \times 3=630\) sora lesz.
A művelethez a reshape2 csomag melt függvényét vetjük be. Telepítsük is a csomagot és hivatkozzuk be a függvényeit az R-be:
install.packages("reshape2")
library(reshape2) # Szokásos módon ne törődjünk az esetleges Warningokkal! :)
## Warning: package 'reshape2' was built under R version 4.1.2
A melt függvényt magát úgy alkalmazzuk, hogy először megadjuk a data frame-t, amin dolgozunk, ez most nekünk a temp_tabla. Utána megadjuk az id.vars paraméterben, hogy mely változókat NE törje be több sorba. Ez a két vizsgált magyarázóváltozónk lesz: YearsCodePro és EdLevel. Végül pedig egy value.name paraméterben megadjuk, hogy mi legyen annak az oszlopnak a neve, amiben a \(3\) kategória valószínűségei egységesen tárolásra kerülnek. Erre logikus név most nekünk a Probability.
temp_tabla <- melt(temp_tabla, id.vars = c("YearsCodePro", "EdLevel"), value.name = "Probability")
# nézzük meg hány sora van az új temp_tabla-nak
nrow(temp_tabla)
## [1] 630
# nézzünk bele az eredményül kapott temp_tabla első 6 sorába
head(temp_tabla)
## YearsCodePro EdLevel variable Probability
## 1 8 Unfinished university Not satisfied 0.2220232
## 2 6 Master’s degree Not satisfied 0.3616731
## 3 5 Bachelor’s degree Not satisfied 0.4043842
## 4 3 Master’s degree Not satisfied 0.3677947
## 5 2 Secondary school Not satisfied 0.4175159
## 6 1 Bachelor’s degree Not satisfied 0.5555360
Oké, a várakozásainknak megfelelően megvan a \(210 \times 3=630\) soros új temp_tabla, amiben külön soron vannak a \(3\) eredményváltozó kategória multinomiális modell szerinti valószínűségei minden megfigyelésre.
Ebből a táblából pedig már simán lehet ábrázolni a 3 elégedettségi kategória valószínűségeit a YearsCodePro változó függvényében vonaldiagramon! Csak ggplot2 kell hozzá!
Az ábrán x koordináták a YearsCodePro értékek, y koordináták a valószínűségek, és külön színt kapnak az egyes eredményváltozó, azaz \(JobSat\) kategóriák, amiket a variable oszlopban tárolunk a temp_tabla data frame-ben:
library(ggplot2)
ggplot(temp_tabla, aes(x = YearsCodePro, y = Probability, colour = variable)) +
geom_line(size = 1) # vonal vastagítása a size-al
Láthatjuk, hogy kb \(15\) év munkatapasztalat után egyértelműen magasabban futnak a kék és zöld vonalak, azaz a Very satisfied és Slightly satisfied kategóriák bekövetkezései valószínűségei. Tehát, hasonló dolgot látunk valószínűségek szintjén is, amit a YearsCodePro együtthatói is elmondtak az oddsokról: a tapasztalat növekedésével nőnek a pozitív hangulatú kategóriák (Very satisfied és Slightly satisfied) oddsai a Not satisfied kategóriához képest.
Az ábrát fel is turbózhatjuk! Egy facet_grid nevű extra réteg alkalmazásával megnézetjük a YearsCodePro marginális hatásait EdLevel kategóriánként külön-külön.
A lentó kódban az EdLevel ~ . felírás technikai szükségszerűség, míg a scales="free" beállítás azt eredményezi, hogy minden EdLevel esetén külön skálázása lesz a diagram tengelyeinek.
ggplot(temp_tabla, aes(x = YearsCodePro, y = Probability, colour = variable)) + geom_line(size = 1) +
facet_grid(EdLevel ~ ., scales = "free")
Érdekes, hogy a valószínűségekre a YearsCodePro-nak alapvetően a Bachelor’s degree és Master’s degree végzettségek esetén van hatása. A többi iskolai végzettség esetén nem annyira.
Pl. a Unfinished university végzettség esetén kb. minden YearsCodePro érték mellett a két elégedett kategória (Very satisfied és Slightly satisfied) a legnagyobb valószínűségű.
Esetleg még a doktori vézettség esetén látványos, hogy a tapasztalat növekedésével csökken folyamatosan a Not satisfied kategória valószínűsége.
Félév végi ujjgyakorlatként elkészíthetjük még a fenti ábrát egy lépésben OpSys szerinti megbontásban is!
Ne feledjük, hogy ha az OpSys változót is használni akarjuk, akkor a temp_tabla data frame-t is újra kell generálni!
# a tábla szűkítést és a melt függvény alkalmazást lehet egyben is az alább látható módon
temp_tabla <- melt(ProgDataWithProbs[,c("YearsCodePro", "OpSys", "Not satisfied",
"Slightly satisfied", "Very satisfied")],
id.vars = c("YearsCodePro", "OpSys"), value.name = "Probability")
head(temp_tabla)
## YearsCodePro OpSys variable Probability
## 1 8 Windows Not satisfied 0.2220232
## 2 6 Windows Not satisfied 0.3616731
## 3 5 Windows Not satisfied 0.4043842
## 4 3 Linux-based Not satisfied 0.3677947
## 5 2 Windows Not satisfied 0.4175159
## 6 1 Linux-based Not satisfied 0.5555360
ggplot(temp_tabla, aes(x = YearsCodePro, y = Probability, colour = variable)) + geom_line(size = 1) +
facet_grid(OpSys ~ ., scales = "free")
Úgy látszik a MacOS-en dolgozók eléggé sokáig Not satisfied-ok legnagyobb valószínűséggel. De \(20\) év munkatapasztalat után már legnagyobb valószínűségel Very Satisfied-ok…a Mac-hez úgy látszik idő kell. :)
Ellenben a Linuxosok már \(10\) év munkatapasztalat után eléggé Very Satisfied-nak festenek legnagyobb valószínűséggel.