7  Regresja logistyczna

Problem do rozwiązania: modelowanie występowania zjawisk ekstremalnych np. przekroczeń dobowych norm wartości stężenia PM10.

Słowa kluczowe: regresja logistyczna, confusion matrix, ROC, AUC, metryki jakości modeli

7.1 Dane

Baza danych dotyczy stężeń PM10 na stacji przy ulicy Porębskiego w Gdyni. Opisano je tutaj: Section 6.1.

Dane można pobrać tutaj.

7.2 Regresja logistyczna

Załadujmy biblioteki

# Załaduj biblioteki
library(mlbench)
library(caret)
library(pROC)
library(rms)
library(ggplot2)
library(dplyr)

i dane

load("data/final_daily.rda")
summary(final_daily)
      DATE                 YEAR          MONTH             DAY       
 Min.   :2013-01-01   Min.   :2013   Min.   : 1.000   Min.   : 1.00  
 1st Qu.:2016-01-01   1st Qu.:2016   1st Qu.: 4.000   1st Qu.: 8.00  
 Median :2019-01-01   Median :2019   Median : 7.000   Median :16.00  
 Mean   :2019-01-01   Mean   :2019   Mean   : 6.523   Mean   :15.73  
 3rd Qu.:2021-12-31   3rd Qu.:2022   3rd Qu.:10.000   3rd Qu.:23.00  
 Max.   :2024-12-31   Max.   :2024   Max.   :12.000   Max.   :31.00  
                                                                     
      TMIN              TMAX              T2M               PM10        
 Min.   :-14.263   Min.   :-11.070   Min.   :-12.484   Min.   :  3.227  
 1st Qu.:  1.392   1st Qu.:  5.213   1st Qu.:  3.477   1st Qu.:  9.729  
 Median :  5.972   Median : 11.372   Median :  8.741   Median : 14.260  
 Mean   :  6.365   Mean   : 11.573   Mean   :  9.087   Mean   : 17.624  
 3rd Qu.: 12.017   3rd Qu.: 18.169   3rd Qu.: 15.385   3rd Qu.: 22.095  
 Max.   : 21.476   Max.   : 30.427   Max.   : 25.586   Max.   :112.271  
                                                       NA's   :1162     
       TP                MSL              D2M                U10        
 Min.   : 0.00000   Min.   : 972.2   Min.   :-14.4959   Min.   :-9.929  
 1st Qu.: 0.06437   1st Qu.:1009.3   1st Qu.:  0.9262   1st Qu.:-1.351  
 Median : 0.61083   Median :1015.1   Median :  5.5154   Median : 1.440  
 Mean   : 2.12145   Mean   :1014.8   Mean   :  5.8156   Mean   : 1.393  
 3rd Qu.: 2.70629   3rd Qu.:1020.8   3rd Qu.: 11.3634   3rd Qu.: 4.012  
 Max.   :54.22163   Max.   :1046.4   Max.   : 21.1175   Max.   :12.875  
                                                                        
      V10               VEL10       
 Min.   :-11.4394   Min.   : 1.023  
 1st Qu.: -1.3818   1st Qu.: 3.527  
 Median :  0.6145   Median : 4.721  
 Mean   :  0.6121   Mean   : 4.956  
 3rd Qu.:  2.7275   3rd Qu.: 6.122  
 Max.   : 10.0851   Max.   :13.967  
                                    

7.3 Przygotowanie danych wejściowych i podział na ciąg uczący i testujący

Zapiszmy otwartą przed chwilą ramkę jako dane usuwając przy okazji przypadki z brakującymi danymi PM10 (jedyna zmienna gdzie występują, dlatego zastosujemy proste filtrowanie a nie funkcja complete_cases).

dane <- final_daily %>% 
  filter(!is.na(PM10))

rm(final_daily)

W badaniach (Czernecki, Marosz, and Jędruszkiewicz 2021) wskazują na istnienie autokorelacji dobowych wartości PM10, dlatego dodamy do naszych danych przesunięte o 1 i 2 dni ciągi PM10, które pozwolą modelowi na “pozyskanie” informacji odnośnie średniodobowych stężeń PM10 w dwóch dniach poprzedzających krok dla którego wykonujemy modelowanie. Nie jest to standardowa praktyka. Za każdym razem powinna być poprzedzona analizą autokorelacji.

dane <- dane %>% 
  mutate(PM10_lag1 = lag(PM10, n = 1),
         PM10_lag2 = lag(PM10, n = 2))

Ze względu na fakt, że dodanie przesuniętych w czasie (o jeden i dwa dni) danych generuje NA dla pierwszych kroków czasowych nowych zmiennych, jesteśmy zmuszeni ponownie wyczyścić dane z NA. Usuńmy zatem niekompletne przypadki, żeby nie trzeba było o tym pamiętać później - niektóre funkcje stosowanych bibliotek mogą być, powiedzmy delikatnie, wrażliwe na braki danych.

dane <- dane %>% 
  filter(!is.na(PM10_lag1)) %>% 
  filter(!is.na(PM10_lag2))

Stwórzmy następnie zmienną PM10_ex, która będzie przyjmować wartości 1 kiedy średnia dobowa wartość stężenia PM10 przekroczy wartość 30\(\mu \text{g/m}^3\). Zmniejszenie ustawowego limitu (50\(\mu \text{g/m}^3\)) uzasadniono tutaj Section 6.1. Usuniemy następnie zmienną PM10 - nie będzie już potrzebna. Będziemy modelować jedynie wystąpienie przekroczenia.

dane <- dane %>% 
  mutate(PM10_ex = if_else(PM10 > 30, 1, 0)) %>% 
  dplyr::select(-PM10)

Załóżmy, że będziemy kalibrować model dla wybranych miesięcy (grudzień:marzec) i usuniemy zmienne związane z czasem: DATE, YEAR, MONTH, DAY. Podczas kalibracji i oceny modelu nie będziemy ich potrzebować.

Przy czym, jeżeli chcielibyśmy na jakimś etapie procedury badawczej analizować konkretne epizody smogowe, to wówczas nie należy ich usuwać, przy czym wyłączamy je z predyktorów modelu podczas kalibracji, zapisując odpowiednią formułę definiującą nasz model.

Wybierzemy zatem nasz zakres czasowy i usuńmy niepotrzebne zmienne.

dane <- dane %>% 
  filter(MONTH %in% c(12, 1, 2, 3)) %>% 
  dplyr::select(-DATE, -YEAR, -MONTH, -DAY)

Tworzymy ramki danych do kalibracji (train) i ewaluacji modelu (test) w proporcji 7 do 3, a następnie usuwamy ramkę dane i wektor index.

set.seed(123)
index <- createDataPartition(dane$PM10_ex, p = 0.7, list = FALSE)
train <- dane[index, ]
test <- dane[-index, ]

rm(dane, index)

7.4 Regresja logistyczna

Model regresji logistycznej tworzymy, wykorzystując funkcję glm(). Wartość argumentu family ustawiamy na binomial.

Skalibrujmy model na ciągu kalibracyjnym train.

model_train <- glm(PM10_ex ~ ., 
             data = train,
             family = "binomial")
summary(model_train)

Call:
glm(formula = PM10_ex ~ ., family = "binomial", data = train)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -18.29262   14.53677  -1.258  0.20826    
TMIN          0.25993    0.23963   1.085  0.27805    
TMAX          0.56241    0.24786   2.269  0.02327 *  
T2M          -1.09003    0.47943  -2.274  0.02299 *  
TP           -0.08462    0.08944  -0.946  0.34409    
MSL           0.01755    0.01433   1.224  0.22081    
D2M           0.19665    0.12876   1.527  0.12668    
U10          -0.19100    0.06261  -3.051  0.00228 ** 
V10           0.99493    0.13926   7.144 9.05e-13 ***
VEL10        -1.30201    0.18400  -7.076 1.48e-12 ***
PM10_lag1     0.10169    0.01465   6.940 3.91e-12 ***
PM10_lag2     0.01428    0.01170   1.221  0.22223    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 742.65  on 750  degrees of freedom
Residual deviance: 292.32  on 739  degrees of freedom
AIC: 316.32

Number of Fisher Scoring iterations: 8

Na podstawie opracowanego modelu (obiekt model_train), z wykorzystaniem funkcji predict() prognozujemy serię prawdopodobieństw wystąpienia zdarzenia PM_10 powyżej 30\(\mu \text{g/m}^3\).

Otrzymując prawdopodobieństwo (wymuszamy to wartością “response” argumentu type) jako rezultat, musimy podjąć decyzję odnośnie progu wartości, który uznamy za wystarczający aby założyć wystąpienie zdarzenia. Intuicyjnie wartość 0,5 jest bardzo pociągająca. Niekoniecznie właściwa, ale na pewno pociągająca. Od niej zaczniemy.

# Predykcja prawdopodobieństwa wystąpienia zdarzenia
pred_prob_train <- predict(model_train, 
                           newdata = train, 
                           type = "response")

# ustalamy arbitralnie prawdopodobieństo na 0,5 i dokonujemy
# reklasyfikacji naszej serii pradopodobieństw na zmienna binarną
pred_class_train <- ifelse(pred_prob_train > 0.5, 1, 0)

Następnie porównujemy rzeczywiste wystąpienia zjawiska z prognozowanymi przez model - tworzymy tzw. macierz pomyłek (confusion matrix). Funkcja confusionMatrix() pochodzi z pakietu caret i jako wejście potrzebuje wektorów typu czynnik (factor), więc “w locie” dokonujemy tej konwersji.

confusionMatrix(data = as.factor(pred_class_train),
                reference = as.factor(train$PM10_ex),
                positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 581  35
         1  23 112
                                          
               Accuracy : 0.9228          
                 95% CI : (0.9013, 0.9408)
    No Information Rate : 0.8043          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.7469          
                                          
 Mcnemar's Test P-Value : 0.1486          
                                          
            Sensitivity : 0.7619          
            Specificity : 0.9619          
         Pos Pred Value : 0.8296          
         Neg Pred Value : 0.9432          
             Prevalence : 0.1957          
         Detection Rate : 0.1491          
   Detection Prevalence : 0.1798          
      Balanced Accuracy : 0.8619          
                                          
       'Positive' Class : 1               
                                          

7.4.1 Wybrane metryki jakości

Wytrenowaliśmy model i poddaliśmy ocenie jego jakość z wykorzystaniem macierzy pomyłek.

Jak widać, na domyślnym “wyjściu” z funkcji glm(), pojawia znaczna liczba metryk, które można wykorzystać w analizie jakości modelu. Poniżej omówię wybrane z nich. Ich znajomość jest niezbędna w przypadku oceny jakości modeli klasyfikacyjnych. Niestety w ich obfitym uniwersum panuje spory bałagan. Szczegółowy opis i wzory wykorzystywane w funkcji confusionMatrix() można odszukać w helpie pakietu caret.

Główne źródło zamieszczonych poniżej informacji to strona WWRP/WGNE Joint Working Group on Forecast Verification Research. Polecam uważne jej przeczytanie - zawiera mnóstwo przydatnych informacji, dotyczących ewaluacji modeli.

Zacznijmy od macierzy pomyłek inaczej zwanej confusion matix. Pozostawiam nazewnictwo angielskie, uzupełniając je o funkcjonujące nazwy alternatywne.

Struktura macierzy pomyłek.

OBSERVED NO OBSERVED YES TOTAL
FORECAST NO CORRECT NEGATIVE
(TN - true negative)
MISSES
(FN - false negative)
FORECAST NO
FORECAST YES FALSE ALARMS
(FP - false positive)
HITS
(TP - true positive)
FORECAST YES
TOTAL OBSERVED NO OBSERVED YES TOTAL

Accuracy

\[ \text{Accuracy} = \frac{HITS+\text{CORRECT NEGATIVES}}{TOTAL} \]

Odpowiada na pytanie: jaki odsetek prognoz był poprawny.

Zakres: 0 do 1,

Idealny model: 1

Opis miary: prosta, intuicyjna. Może być myląca, ponieważ jest silnie uzależniona od najczęściej występującej kategorii, a niedoszacowanie występowania zdarzeń ekstremalnych przez model może znacznie ograniczać jej stosowalność.


Bias score (frequency bias)

\[ \text{BIAS} = \frac{HITS+\text{FALSE ALARMS}}{HITS + MISSES} \]

Odpowiada na pytanie: jak prognozowana częstotliwość zdarzeń „YES” ma się do obserwowanej częstości zdarzeń „YES”?

Zakres: 0 do \(\infty\)

Wartość idealna: 1

Opis miary: mierzy stosunek częstości zdarzeń prognozowanych do częstości zdarzeń obserwowanych. Wskazuje, czy model ma tendencję do niedoszacowania (BIAS<1) lub przeszacowania (BIAS>1) prognozowanych zdarzeń. Nie mierzy stopnia zgodności prognozy z obserwacjami, mierzy jedynie częstości względne.


Probability of detection (POD) - aka H, HIT RATE, SENSITIVITY, RECAL lub TRUE POSITIVE RATE

\[ \text{POD} = \frac{HITS}{HITS + MISSES} \]

Odpowiada na pytanie: jaki odsetek obserwowanych “YES” został poprawnie prognozowany.

Zakres: 0 do 1

Wartość idealna: 1

Opis miary: wrażliwa na liczbę poprawnych prognoz “YES” (HITS - true positives), ale ignoruje fałszywe alarmy. Bardzo wrażliwa na klimatologiczną częstość występowania zdarzenia. Dobra miara w przypadku analizy rzadkich zdarzeń. Powinna być stosowana w połączeniu ze współczynnikiem fałszywych alarmów (FALSE ALARM RATIO). POD jest również składową krzywej ROC, szeroko stosowanej w prognozach probabilistycznych.


Specificity aka TRUE NEGATIVE RATE (1-False Alarm Rate)

\[ \text{Specificity} = \frac{\text{CORRECT NEGATIVE}}{\text{CORRECT NEGATIVE} + \text{FALSE ALARMS}} \]

Odpowiada na pytanie: Jaki odsetek odpowiedzi “NO” był poprawnie prognozowany.

Zakres: 0 do 1

Wartość idealna: 1


False Alarm Ratio (FAR)

\[ \text{FAR} = \frac{\text{FALSE ALARMS}}{HITS + \text{FALSE ALARMS}} \]

Odpowiada na pytanie: jaki odsetek prognozowanych “YES” w rzeczywistości nie wystąpiło (czyli były fałszywymi alarmami)

Zakres: 0 do 1.

Wartość idealna: 1

Opis miary: czuła na fałszywe alarmy, ale ignoruje chybienia. Bardzo czuła na klimatologiczną częstość występowania zdarzenia. Należy ja stosować w połączeniu z prawdopodobieństwem wykrycia (POD).


Probability of false detection aka FALSE ALARM RATE, FALSE POSITIVE RATE, czasami oznaczana również jako F

\[ \text{FALSE ALARM RATE} = \frac{\text{FALSE ALARMS}}{\text{CORRECT NEGATIVES} + \text{FALSE ALARMS}} \]

Odpowiada na pytanie: jaki odsetek obserwowanych “NO” zostało niepoprawnie prognozowanych jako “YES”.

Zakres: 0 do 1

Wartość idealna: 0

Opis miary: wrażliwa na fałszywe alarmy, ale ignoruje chybienia. Nie jest często raportowana w przypadku prognoz deterministycznych, ale stanowi ważny element ROC, szeroko stosowanej w prognozach probabilistycznych.


Success Ratio (SR)

\[ \text{SR} = \frac{\text{HITS}}{\text{HITS} + \text{FALSE ALARMS}} \]

Odpowiada na pytanie: Jaki odsetek prognoz “YES” był poprawny.

Zakres: 0 do 1

Wartość idealna: 1

Opis miary: podaje informacje o prawdopodobieństwie wystąpienia obserwowanego zdarzenia, zakładając, że zostało ono prognozowane. Jest wrażliwa na fałszywe alarmy, ale ignoruje chybienia. SR jest równy 1-FAR.


Threat score (CSI - critical success index)

\[ \text{TS} = \frac{\text{HITS}}{\text{HITS} + MISSES + \text{FALSE ALARMS}} \]

Odpowiada na pytanie: jak dobrze prognoza “YES” koresponduje do obserwowanych zdarzeń “YES”

Zakres: 0 do 1

Wartość idealna: 1

Opis miary: mierzy odsetek obserwowanych i/lub prognozowanych zdarzeń, które zostały prawidłowo przewidziane. Można to rozumieć jako dokładność, gdy true negatives zostaną usunięte z analizy. TS dotyczy tylko prognoz, “które są istotne”. Wrażliwy na trafienia, karze zarówno za chybienia(MISSES), jak i fałszywe alarmy (FALSE ALARMS). Nie rozróżnia źródła błędu prognozy. Zależy od klimatologicznej częstości zdarzeń (gorsze wyniki dla rzadszych zdarzeń), ponieważ niektóre trafienia mogą wystąpić wyłącznie w wyniku przypadku.


Heidke skill score (Cohen’s k) - HSS

\[ \text{HSS} = \frac{(HITS + \text{CORRECT NEGATIVES})-\text{EXPECTED CORRECT}_{random}}{N - \text{EXPECTED CORRECT}_{random}} \]

gdzie:

\[ \text{EXPECTED CORRECT}_{random} = \frac{1}{N}((TP + FN)(TP + FP)+(TN + FN)(TN+FP)) \]

w powyższym wzorze wyjątkowo (ze względów technicznych) posłużyłem się skrócona notacją elementów confusion matrix

Odpowiada na pytanie: Jaka jest dokładność prognozy w porównaniu do prognozy losowej

Zakres: -1 do 1, 0 oznacza brak umiejętności (skill) modelu

Wartość idealna: 1

Opis miary: Mierzy odsetek poprawnych prognoz po wyeliminowaniu prognoz, które byłyby poprawne wyłącznie dzięki przypadkowi. Jest to forma uogólnionego wyniku jakości (tzw. skill modelu), gdzie wynik w liczniku to liczba poprawnych prognoz, a punktem odniesienia w tym przypadku jest prognoza przypadkowa.


7.4.2 Krzywa ROC i AUC

Metryki omówione w poprzednim rozdziale dotyczą macierzy pomyłek, tworzonych już po zastosowaniu określonego progu odcięcia. W celu oceny modelu w pełnym zakresie progów odcięcia należy wykreślić i przeanalizować tzw. krzywą ROC (Receiver Operating Characteristic). Pozwala ona na obliczenie popularnej metryki AUC (area under curve), której wartość wskazuje na ogólną jakość modelu (im bliższa jest 1, tym model lepszy). Dodatkowo z jej wykorzystaniem możemy porównywać modele oraz zoptymalizować wybór progu odcięcia.

Jednym z najszybszych sposób wykreślenia ROC i obliczenia AUC jest wykorzystanie runkcji roc() z pakietu pROC.

roc_obj <- roc(response = train$PM10_ex,
               predictor = pred_prob_train)
Setting levels: control = 0, case = 1
Setting direction: controls < cases
plot(roc_obj, col = "blue", main = "ROC Curve")

auc(roc_obj)
Area under the curve: 0.9623

Nie wygląda ona zbyt efektownie, ale aby “na szybko” podejrzeć jej kształt, wystarczy.

Za chwilę wykreślimy ROC jeszcze na dwa sposoby.

Wróćmy do problemu doboru odpowiedniego punktu odcięcia. Prawdopodobieństwo 0,5 mimo, że podpowiadane przez intuicję, nie jest zazwyczaj optymalnym wyborem.

Wystarczy spojrzeć na poniższą ROC (tym razem z pakietu verification, pozwalającego na oznaczenie potencjalnych punktów odcięcia na wykresie).

library(verification)
roc.plot(train$PM10_ex, pred_prob_train, 
         thresholds = NULL,
         binormal = FALSE,
         legend = FALSE, 
         leg.text = NULL, 
         plot = "emp", 
         CI = F,
         n.boot = 1000, alpha = 0.05, tck = 0.01, 
         plot.thres = seq(0.1, 0.9, 0.1), 
         show.thres = TRUE, 
         main = "ROC Curve", 
         xlab = "False Alarm Rate",
         ylab = "Hit Rate",
         extra = FALSE)

Lepszą wartością progu byłaby wartość między 0,2 a 0,3, ale w jaki sposób to określić.

Tutaj z pomocą przychodzi metoda Youden’a (Ruopp et al. 2008) oparta o indeks J Youden’a (celem jest wybór takiego progu odcięcia, który najlepiej równoważy czułość (Sensitivity) i swoistość (Specificity1), czyli minimalizuje liczbę błędów klasyfikacji), lub identyfikacja punktu najbliższego górnemu lewemu rogowi wykresu, który wyznacza model idealny. Pozwala to na określenie optymalnego wyboru punktu odcięcia. W naszym przypadku posłużymy się metodą Youdena.

library(pROC)
roc_obj <- roc(train$PM10_ex, pred_prob_train)
Setting levels: control = 0, case = 1
Setting direction: controls < cases
opt_cut <- coords(roc_obj, 
                  x = "best", 
                  best.method = "youden", 
                  ret = "threshold")
opt_cut
  threshold
1 0.2955465
#opt_cut[[1]]

Teraz przygotowujemy macierz pomyłek z nowym progiem

# Zaczynamy od predykcji klas z wykorzystaniem wektora pred_prob_train
pred_class_youden <- ifelse(pred_prob_train > 
opt_cut[[1]], 1, 0)


# Macierz pomyłek
confusionMatrix(data = as.factor(pred_class_youden),
                reference = as.factor(train$PM10_ex))
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 550  15
         1  54 132
                                          
               Accuracy : 0.9081          
                 95% CI : (0.8852, 0.9278)
    No Information Rate : 0.8043          
    P-Value [Acc > NIR] : 4.188e-15       
                                          
                  Kappa : 0.7348          
                                          
 Mcnemar's Test P-Value : 4.770e-06       
                                          
            Sensitivity : 0.9106          
            Specificity : 0.8980          
         Pos Pred Value : 0.9735          
         Neg Pred Value : 0.7097          
             Prevalence : 0.8043          
         Detection Rate : 0.7324          
   Detection Prevalence : 0.7523          
      Balanced Accuracy : 0.9043          
                                          
       'Positive' Class : 0               
                                          

Jak widać nastąpił wyraźny wzrost jakości pod kątem wykrywalności. W wersji z nowym progiem aż 132 przypadki przekroczeń stężen PM_10 z łącznie 147 zostały poprawnie zaklasyfikowane (true positives). W przypadku domyślnego progu (0,5) błędów (false negatives) było ponad dwukrotnie więcej (35 przypadków). Wniosek: optymalizacja progu odcięcia działa i znacznie polepsza nasz model pod tym kątem. Niemniej jednak przed nami kolejny kluczowy element analizy - ewaluacja na danych nie biorących udziału w kalibracji, która pozwoli na obiektywną ocenę zdolności generalizacji modelu.

7.5 Ewaluacja na danych testowych

Dokonajmy ewaluacji naszego modelu na danych testowych z wyznaczonym wcześniej optymalnym punktem odcięcia.

# Predykcja prawdopodobieństw oraz klas przekroczenia PM10
# z wykorzytaniem określonego metodą Youden'a progu odcięcia. 

pred_prob_test <- predict(model_train, newdata = test, type = "response")

pred_class_test <- ifelse(pred_prob_test > opt_cut[[1]], 1, 0)
confusionMatrix(as.factor(pred_class_test),
                as.factor(test$PM10_ex))
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 234  14
         1  28  45
                                         
               Accuracy : 0.8692         
                 95% CI : (0.8273, 0.904)
    No Information Rate : 0.8162         
    P-Value [Acc > NIR] : 0.00694        
                                         
                  Kappa : 0.6006         
                                         
 Mcnemar's Test P-Value : 0.04486        
                                         
            Sensitivity : 0.8931         
            Specificity : 0.7627         
         Pos Pred Value : 0.9435         
         Neg Pred Value : 0.6164         
             Prevalence : 0.8162         
         Detection Rate : 0.7290         
   Detection Prevalence : 0.7726         
      Balanced Accuracy : 0.8279         
                                         
       'Positive' Class : 0              
                                         

A teraz porównajmy obie ROC

W tym celu wypreparujemy dane dotyczące Sensitivity oraz Specificity z obiektów stworzonych przy wykorzystaniu biblioteki pROC , tak aby wykreślić wiele krzywych ROC na jednym wykresie ggplot2.

library(pROC)
#stworzenie obiektóe ROC
roc_train <- pROC::roc(train$PM10_ex, pred_prob_train)
roc_test <- pROC::roc(test$PM10_ex, pred_prob_test)
auc(roc_train)
Area under the curve: 0.9623
auc(roc_test)
Area under the curve: 0.9215
# "wyciągamy" wartości sensitivity i specificity z obiektów
# roc_train i roc_test , żeby wykreślić je w ggplot

gg_roc_train <- data.frame(sens = roc_train$sensitivities,
                           spec = roc_train$specificities)
gg_roc_test <- data.frame(sens = roc_test$sensitivities, 
                          spec = roc_test$specificities)

ggplot(gg_roc_train, aes(x = spec, y = sens))+
         geom_path()+
  geom_path(data = gg_roc_test, color = "red")+
  scale_x_reverse()+
  labs(
    x = "Specificity (1-False Alarm Rate)",
    y = "Sensitivity"
  )+
  theme_bw()

Wygląda na to, że nasz model całkiem nieźle sprawdza się na danych testowych (czerwona krzywa ROC). Widoczny jest nieznaczny spadek jakości, no ale tego należało się spodziewać. Miara AUC jest tylko nieco niższa (0,92 względem 0,96) niż dla modelu ocenianego na podstawie danych wykorzystanych w kalibracji.

7.5.1 Nierównowaga w danych

Nasze zmienne cechują się nierównowagą, czyli znaczą różnicą między liczebnością przypadków przekroczenia progu 30\(\mu \text{g/m}^3\), a pozostałymi. Może to powodować problemy w kontekście aplikacyjności modelu, ponieważ nie będzie on miał wystarczającej bazy przypadków z przekroczeniem aby “nauczyć się” je rozpoznawać. Trzeba to zweryfikować.

Najpierw zerknijmy na proporcje.

table(train$PM10_ex)

  0   1 
604 147 
prop.table(table(train$PM10_ex))

       0        1 
0.804261 0.195739 

W ciągu kalibrującym zaledwie 19,5% danych to przypadki przekroczenia normy PM10.

Posłużymy się procedurą overscaling’u (przy czym zastosujemy metodę over), aby zrównoważyć tą proporcję

library(ROSE)
train_both <- ovun.sample(PM10_ex ~ ., 
                          data = train, 
                          method = "over", 
                          N = 1200)$data

zerknijmy teraz na proporcję

table(train_both$PM10_ex)

  0   1 
604 596 
prop.table(table(train_both$PM10_ex))

        0         1 
0.5033333 0.4966667 

I ponownie skalibrujmy model (model_train_both) ze zrównoważonymi liczebnościami. Zobaczymy czy poprawi to jego jakość podczas ewaluacji na danych testowych.

model_train_both <- glm(PM10_ex ~ ., 
             data = train_both,
             family = binomial)
summary(model_train)

Call:
glm(formula = PM10_ex ~ ., family = "binomial", data = train)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -18.29262   14.53677  -1.258  0.20826    
TMIN          0.25993    0.23963   1.085  0.27805    
TMAX          0.56241    0.24786   2.269  0.02327 *  
T2M          -1.09003    0.47943  -2.274  0.02299 *  
TP           -0.08462    0.08944  -0.946  0.34409    
MSL           0.01755    0.01433   1.224  0.22081    
D2M           0.19665    0.12876   1.527  0.12668    
U10          -0.19100    0.06261  -3.051  0.00228 ** 
V10           0.99493    0.13926   7.144 9.05e-13 ***
VEL10        -1.30201    0.18400  -7.076 1.48e-12 ***
PM10_lag1     0.10169    0.01465   6.940 3.91e-12 ***
PM10_lag2     0.01428    0.01170   1.221  0.22223    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 742.65  on 750  degrees of freedom
Residual deviance: 292.32  on 739  degrees of freedom
AIC: 316.32

Number of Fisher Scoring iterations: 8

Nie będziemy liczyć ponownie wszystkich statystyk. Zerknijmy, czy poprawiła się nasza ROC.

Musimy obliczyć nowy punkt odcięcia dla tego modelu.

library(pROC)

pred_prob_train_both <- predict(model_train_both, newdata = train, type = "response")

roc_obj_both <- roc(train$PM10_ex, pred_prob_train_both)
opt_cut_both <- coords(roc_obj_both, 
                       x = "best", 
                       best.method = "youden", 
                       ret = "threshold")
opt_cut_both
  threshold
1 0.5286766
#opt_cut_both[[1]]

Widać, że wartość ta znacznie różni się od progu przed równoważeniem danych.

Predykcja prawdopodobieństwa na ciągu testowym dla zrównoważonych danych.

# 
pred_prob_test_both <- predict(model_train_both,
                               newdata = test, 
                               type = "response")
library(pROC)

roc_train <- pROC::roc(train$PM10_ex, pred_prob_train)
roc_test <- pROC::roc(test$PM10_ex, pred_prob_test)
roc_test_both <- pROC::roc(test$PM10_ex, pred_prob_test_both)
auc(roc_train)
Area under the curve: 0.9623
auc(roc_test)
Area under the curve: 0.9215
auc(roc_test_both)
Area under the curve: 0.9213
gg_roc_train <- data.frame(sens = roc_train$sensitivities, 
                           spec = roc_train$specificities)

gg_roc_test <- data.frame(sens = roc_test$sensitivities, 
                          spec = roc_test$specificities)

gg_roc_test_both <- data.frame(sens = roc_test_both$sensitivities, 
                               spec = roc_test_both$specificities)

ggplot(gg_roc_train, aes(x = spec, y = sens))+
         geom_path()+
  geom_path(data = gg_roc_test, color = "red")+
  geom_path(data = gg_roc_test_both, color = "darkgreen")+
  labs(
    x = "Specificity (1-False Alarm Rate)",
    y = "Sensitivity"
  )+
  scale_x_reverse()+
  theme_bw()

Zielona linia to ROC po oversamplingu. W zasadzie można zaryzykować stwierdzenie, że nie zaznacza się wzrost jakości modelu względem opartego jedynie na danych oryginalnych z niezrównoważonymi liczebnościami klas PM10_ex. Niemniej jednak obliczmy confusion matrix dla tego wariantu.

# Predykcja prawdopodobieństw na podstawie modelu ze zrównoważoną
# liczbą przypadków ekstremalnych i wyznaczenie wystapienia przypadków
# ekstremalnych na podstawie optymalnego punktu oscięcia.

pred_prob_test_both <- predict(model_train_both, newdata = test, type = "response")

pred_class_test_both <- ifelse(pred_prob_test_both > opt_cut_both[[1]], 1, 0)


confusionMatrix(data = as.factor(pred_class_test_both),
                reference = as.factor(test$PM10_ex),
                positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 229  12
         1  33  47
                                         
               Accuracy : 0.8598         
                 95% CI : (0.817, 0.8959)
    No Information Rate : 0.8162         
    P-Value [Acc > NIR] : 0.023095       
                                         
                  Kappa : 0.5894         
                                         
 Mcnemar's Test P-Value : 0.002869       
                                         
            Sensitivity : 0.7966         
            Specificity : 0.8740         
         Pos Pred Value : 0.5875         
         Neg Pred Value : 0.9502         
             Prevalence : 0.1838         
         Detection Rate : 0.1464         
   Detection Prevalence : 0.2492         
      Balanced Accuracy : 0.8353         
                                         
       'Positive' Class : 1              
                                         

Wynik potwierdza ocenę wizualną ROC i nie mamy tutaj do czynienia z wyraźną poprawą jakości modelu na skutek zrównoważenia klas.

7.6 Szybkie statystyki modelu

Jeżeli zależy nam na obliczeniu tylko wybranych charakterystyk np. w celu szybkiego przygotowania zestawienia dla wielu modeli, polecam bibliotekę verification. Ma ona, co prawda, swoje lata i czasami potrafi sprawić nieco problemów (np, jeżeli pojawiają braki danych w seriach), jednak zazwyczaj sprawuje się poprawnie.

library(verification)

Obliczmy dla modelu model_train_both następujące metryki: Accuracy (PC - percent correct), CSI (Threat Score), BIAS, POD i HSS. Stworzymy następnie ramkę danych metrics z wynikami.

pc <-  verify(obs=test$PM10_ex, 
       pred = pred_class_test_both, 
       frcst.type = "binary", 
       obs.type = "binary")$PC

csi <-  verify(obs=test$PM10_ex, 
       pred = pred_class_test_both, 
       frcst.type = "binary", 
       obs.type = "binary")$TS


bias <- verify(obs=test$PM10_ex, 
       pred = pred_class_test_both, 
       frcst.type = "binary", 
       obs.type = "binary")$BIAS

pod <- verify(obs=test$PM10_ex, 
       pred = pred_class_test_both, 
       frcst.type = "binary", 
       obs.type = "binary")$POD

hss <- verify(obs=test$PM10_ex, 
       pred = pred_class_test_both, 
       frcst.type = "binary", 
       obs.type = "binary")$HSS

metrics = round(data.frame(pc = pc, csi = csi, bias = bias, pod = pod, hss = hss), digits = 2)
metrics
    pc  csi bias pod  hss
1 0.86 0.51 1.36 0.8 0.59

Jak widać zastosowanie regresji logistycznej (kalibracja modelu) nie jest trudne. Bardziej złożonym zagadnieniem jest ocena i porównanie jakości modeli. Tym bardziej, że istnieje legion metryk, które często nazywane są w różny sposób w literaturze, co dodatkowo utrudnia zorientowanie się w ich gąszczu. Dobór metryk jakości zawsze powinien być podyktowany potencjalnym zastosowaniem kalibrowanego modelu oraz charakterem zmiennej wyjaśnianej.

Po więcej usystematyzowanych informacji zapraszamy na kursy DataCraft LAB.


  1. Specificity = 1 - False Alarm Rate↩︎