Twoja kolej!

Teraz nadszedł czas na przetestowanie tych metod (regresja grzbietowa i lasso) oraz metod oceny (zestaw walidacyjny, walidacja krzyżowa) na innych zbiorach danych. Możesz pracować z zespołem nad tą częścią laboratorium.

Możesz użyć dowolnego zbioru danych zawartego w ISLR lub wybrać jeden z pakietów danych na Kaggle/Data World itp. (zmienna zależna musi być ciągła).

Pobierz zbiór danych i spróbuj określić optymalny zestaw parametrów, które należy użyć do jego modelowania!

#install.packages('ISLR')
#install.packages('dplyr')
#install.packages('glmnet')
#install.packages('caret')
#install.packages('tidyverse')
library(ISLR)
## Warning: pakiet 'ISLR' został zbudowany w wersji R 4.3.2
library(dplyr)
## Warning: pakiet 'dplyr' został zbudowany w wersji R 4.3.2
## 
## Dołączanie pakietu: 'dplyr'
## Następujące obiekty zostały zakryte z 'package:stats':
## 
##     filter, lag
## Następujące obiekty zostały zakryte z 'package:base':
## 
##     intersect, setdiff, setequal, union
library(glmnet)
## Warning: pakiet 'glmnet' został zbudowany w wersji R 4.3.2
## Ładowanie wymaganego pakietu: Matrix
## Loaded glmnet 4.1-8
library(caret)    
## Warning: pakiet 'caret' został zbudowany w wersji R 4.3.3
## Ładowanie wymaganego pakietu: ggplot2
## Warning: pakiet 'ggplot2' został zbudowany w wersji R 4.3.3
## Ładowanie wymaganego pakietu: lattice
library(tidyverse)
## Warning: pakiet 'tidyverse' został zbudowany w wersji R 4.3.3
## Warning: pakiet 'stringr' został zbudowany w wersji R 4.3.2
## Warning: pakiet 'lubridate' został zbudowany w wersji R 4.3.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ lubridate 1.9.3     ✔ tibble    3.2.1
## ✔ purrr     1.0.2     ✔ tidyr     1.3.0
## ✔ readr     2.1.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ tidyr::expand() masks Matrix::expand()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ✖ purrr::lift()   masks caret::lift()
## ✖ tidyr::pack()   masks Matrix::pack()
## ✖ tidyr::unpack() masks Matrix::unpack()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
head(Wage)
##        year age           maritl     race       education             region
## 231655 2006  18 1. Never Married 1. White    1. < HS Grad 2. Middle Atlantic
## 86582  2004  24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
## 161300 2003  45       2. Married 1. White 3. Some College 2. Middle Atlantic
## 155159 2003  43       2. Married 3. Asian 4. College Grad 2. Middle Atlantic
## 11443  2005  50      4. Divorced 1. White      2. HS Grad 2. Middle Atlantic
## 376662 2008  54       2. Married 1. White 4. College Grad 2. Middle Atlantic
##              jobclass         health health_ins  logwage      wage
## 231655  1. Industrial      1. <=Good      2. No 4.318063  75.04315
## 86582  2. Information 2. >=Very Good      2. No 4.255273  70.47602
## 161300  1. Industrial      1. <=Good     1. Yes 4.875061 130.98218
## 155159 2. Information 2. >=Very Good     1. Yes 5.041393 154.68529
## 11443  2. Information      1. <=Good     1. Yes 4.318063  75.04315
## 376662 2. Information 2. >=Very Good     1. Yes 4.845098 127.11574
data("Wage")
Wage = na.omit(Wage)

Przygotowanie danych

Zbiór danych, który analizujemy, pochodzi z pakietu ISLR i dotyczy wynagrodzeń pracowników. Zbiór ten zawiera różne cechy demograficzne i zawodowe pracowników, które mogą wpływać na wysokość wynagrodzenia.

Zmienne w analizie

  • Zmienne zależna (y):

    • wage – Wynagrodzenie pracownika (wartość ciągła, główna zmienna, którą chcemy modelować).
  • Zmienne niezależne (X):
    Zmiennymi predykcyjnymi są:

    1. age – Wiek pracownika.

    2. education – Poziom wykształcenia (kategoria nominalna, np. 1. < HS Grad, 2. HS Grad, 3. Some College, 4. College Grad, 5. Advanced Degree).

    3. jobclass – Klasa wykonywanej pracy (kategoria binarna: Industrial lub Information).

    4. health – Samoocena stanu zdrowia pracownika (<=Good lub >=Very Good).

    5. health_ins – Informacja o posiadaniu ubezpieczenia zdrowotnego (Yes lub No).

# Zmienna zależna
y <- Wage$wage
# Zmienne niezależne
X <- model.matrix(wage ~ age + education + jobclass + health + health_ins, data = Wage)[,-1]

Podział na zbiór treningowy i testowy

set.seed(123) 
train_index <- createDataPartition(y, p = 0.8, list = FALSE)
X_train <- X[train_index, ]
X_test <- X[-train_index, ]
y_train <- y[train_index]
y_test <- y[-train_index]

Dzielimy dane na zbiór treningowy i zbiór testowy, aby móc ocenić jakość modelu na danych, których wcześniej “nie widział”. Korzystamy z funkcji createDataPartition, aby losowo wybrać 80% obserwacji jako zbiór treningowy, a pozostałe 20% jako zbiór testowy. Następnie dzielimy macierz zmiennych niezależnych (X) oraz wektor zmiennej zależnej (y) na odpowiednie zestawy: treningowe (X_train, y_train) i testowe (X_test, y_test). Dzięki temu uczymy model na danych treningowych, a jego jakość weryfikujemy na danych testowych, co pozwala na obiektywną ocenę jego skuteczności.

Regresja OLS

W kolejnym kroku dopasowaliśmy model regresji liniowej OLS (ang. Ordinary Least Squares) na zbiorze treningowym. Celem było oszacowanie wpływu zmiennych niezależnych na zmienną zależną oraz ocena jakości predykcji modelu na zbiorze testowym. Nie ma uniwersalnej reguły, która pozwoliłaby nam stwierdzić na tym etapie który z modeli sprawdzi się lepiej ridge lub lasso. Spodziewamy się natomiast, że obie metody regularizacji poradzą sobie lepiej z ewentualną współliniowością i lepiej uogólnią wyniki w porównaniu z klasycznym modelem OLS. W praktyce:

# Dopasowanie modelu OLS
ols_model <- lm(wage ~ age + education + jobclass + health + health_ins, data = Wage[train_index, ])
summary(ols_model)
## 
## Call:
## lm(formula = wage ~ age + education + jobclass + health + health_ins, 
##     data = Wage[train_index, ])
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -99.490 -19.656  -3.572  13.878 214.065 
## 
## Coefficients:
##                              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)                  67.19440    3.91866  17.147  < 2e-16 ***
## age                           0.48904    0.06278   7.789 9.95e-15 ***
## education2. HS Grad           7.94836    2.67757   2.968  0.00302 ** 
## education3. Some College     17.53744    2.83965   6.176 7.71e-10 ***
## education4. College Grad     31.22250    2.86542  10.896  < 2e-16 ***
## education5. Advanced Degree  54.09232    3.14541  17.197  < 2e-16 ***
## jobclass2. Information        4.14379    1.49159   2.778  0.00551 ** 
## health2. >=Very Good          8.75607    1.59218   5.499 4.21e-08 ***
## health_ins2. No             -18.39689    1.58634 -11.597  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 34.51 on 2393 degrees of freedom
## Multiple R-squared:  0.3105, Adjusted R-squared:  0.3082 
## F-statistic: 134.7 on 8 and 2393 DF,  p-value: < 2.2e-16

Wszystkie uwzględnione zmienne okazały się statystycznie istotne (p < 0,05), co oznacza, że wiek, poziom wykształcenia, rodzaj wykonywanej pracy, stan zdrowia oraz posiadanie ubezpieczenia zdrowotnego mają wpływ na wysokość płacy. Szczególnie duże znaczenie ma wykształcenie — Advanced Degree wiąże się z aż o około 54 USD/godz. wyższymi zarobkami w porównaniu do osób z wykształceniem niższym niż szkoła średnia. Równie istotny, choć negatywny wpływ, ma brak ubezpieczenia zdrowotnego, który obniża płacę o około 18 USD/godz. Z kolei każdy rok więcej w metryce odpowiada wzrostowi wynagrodzenia o ok. 0,49 USD/godz., a lepszy stan zdrowia (>= Very Good) przekłada się na dodatkowe +8,76 USD/godz. Pracownicy sektora „Information” zarabiają też średnio o 4,14 USD/godz. więcej niż osoby zatrudnione w „Industrial”.

Wskaźnik R2≈0,31 oznacza, że model wyjaśnia ok. 31% zmienności płacy, co w badaniach społeczno-ekonomicznych można uznać za umiarkowane dopasowanie (biorąc pod uwagę, że często istnieje wiele pominiętych czynników wpływających na zarobki). MSE = 1288,62 pokazuje skalę przeciętnego kwadratowego błędu predykcji, a Residual Standard Error = 34,51 wskazuje, o ile średnio pomyłka w prognozie odbiega od obserwowanej wartości wynagrodzenia. Choć model nie tłumaczy całej złożoności czynników kształtujących płace, jasno wskazuje, że wyższe wykształcenie, posiadanie ubezpieczenia zdrowotnego, lepszy stan zdrowia, praca w sektorze informacyjnym i wiek to kluczowe determinanty wyższych zarobków.

Regresja Ridge

# Przygotowanie danych dla glmnet
X_train_matrix <- as.matrix(X_train)
X_test_matrix <- as.matrix(X_test)

# Dopasowanie modelu Ridge (α = 0)
ridge_model <- cv.glmnet(X_train_matrix, y_train, alpha = 0)
plot(ridge_model)

ridge_lambda <- ridge_model$lambda.min
cat("Optimal Ridge Lambda:", ridge_lambda, "\n")
## Optimal Ridge Lambda: 1.580485
# Predykcje i ocena
ridge_pred <- predict(ridge_model, s = ridge_lambda, newx = X_test_matrix)
ridge_mse <- mean((y_test - ridge_pred)^2)
ridge_r2 <- 1 - (sum((y_test - ridge_pred)^2) / sum((y_test - mean(y_test))^2))

cat("Ridge MSE:", ridge_mse, "\nRidge R^2:", ridge_r2, "\n")
## Ridge MSE: 1295.187 
## Ridge R^2: 0.2880225

Z kolei, przechodząc do regresji grzbietowej (ridge), zastosowano walidację krzyżową w celu ustalenia optymalnej wartości parametru regularyzacji λ. Z przedstawionego wykresu widać, jak średni błąd kwadratowy (MSE) zmienia się w zależności od log⁡(λ): w okolicach log⁡(λ)≈0.46 (czyli λ≈1,58bserwujemy jego minimum. Ostatecznie, przy tym „najlepszym” λ Ridge MSE wynosi 1295,19, a R^2 to 0,288.

W porównaniu z modelem OLS, w którym MSE było na poziomie ok. 1288,62, a R2≈0,292R^2 , różnice są niewielkie. Ridge delikatnie ustępuje OLS pod względem błędu, ale w zamian zyskujemy większą stabilność i kontrolę nad współliniowością (zwłaszcza przy większej liczbie zmiennych). W tej sytuacji przewaga ridge nie jest mocno widoczna w samych miarach dopasowania, choć w praktyce może poprawiać odporność modelu na potencjalne zakłócenia czy wahania w danych.

Regresja Lasso

# Dopasowanie modelu Lasso (α = 1)
lasso_model <- cv.glmnet(X_train_matrix, y_train, alpha = 1)
plot(lasso_model)

# Optymalne lambda
lasso_lambda <- lasso_model$lambda.min
cat("Optimal Lasso Lambda:", lasso_lambda, "\n")
## Optimal Lasso Lambda: 0.02575796
# Predykcje i ocena
lasso_pred <- predict(lasso_model, s = lasso_lambda, newx = X_test_matrix)

lasso_mse <- mean((y_test - lasso_pred)^2)
lasso_r2 <- 1 - (sum((y_test - lasso_pred)^2) / sum((y_test - mean(y_test))^2))

cat("Lasso MSE:", lasso_mse, "\nLasso R^2:", lasso_r2, "\n")
## Lasso MSE: 1288.678 
## Lasso R^2: 0.2916008

Kontynuując analizę, w przypadku regresji Lasso zastosowano podobną procedurę walidacji krzyżowej, która wyłoniła optymalne λ≈0,0258 Dla tej wartości kary Lasso MSE wynosi 1288,68, a R2≈0,292. Wyniki są więc niemal identyczne jak w modelu OLS (gdzie MSE i R2R^2R2 miały odpowiednio wartości ~1288,62 i 0,2916). Lasso nie przyniosło w tym konkretnym zestawie danych istotnego zysku w predykcji, ale w przeciwieństwie do OLS potrafi zredukować znaczenie mniej istotnych zmiennych, „zerując” niektóre współczynniki. W efekcie może dawać prostszy, bardziej zinterpretowalny model kosztem zbliżonego lub lekko wyższego błędu. Jednak w naszej sytuacji różnica w stosunku do OLS jest minimalna, co pokazuje, że żaden z dodanych mechanizmów regularizacji (ridge czy lasso) nie zapewnia wyraźnej przewagi nad klasyczną regresją liniową w tym zestawie danych.

Ważność predyktorów (Lasso)

# Ważność zmiennych dla Lasso
lasso_coefficients <- coef(lasso_model, s = lasso_lambda)
lasso_coefficients_df <- data.frame(
  Variable = rownames(lasso_coefficients),
  Coefficient = as.vector(lasso_coefficients)
)
lasso_coefficients_df <- lasso_coefficients_df %>% filter(Coefficient != 0)

print(lasso_coefficients_df)
##                      Variable Coefficient
## 1                 (Intercept)  67.8501543
## 2                         age   0.4869979
## 3         education2. HS Grad   7.3559273
## 4    education3. Some College  16.9337841
## 5    education4. College Grad  30.6286741
## 6 education5. Advanced Degree  53.4917935
## 7      jobclass2. Information   4.1429969
## 8        health2. >=Very Good   8.7235583
## 9             health_ins2. No -18.4037668

W podsumowaniu porównań między OLS, ridge i lasso okazało się dość niespodziewanie, że model OLS osiągnął minimalnie lepsze wskaźniki dopasowania (np. niższe MSE) niż metody z regularizacją. Mimo to, ostatecznie zdecydowano się wybrać model Lasso. Głównym powodem jest jego potencjał do stabilnej selekcji zmiennych i ograniczania ewentualnego przeuczenia, chociaż w tym konkretnym zestawie danych żaden współczynnik nie został wyzerowany (czyli Lasso wybrało identyczny zestaw predyktorów, co OLS). Z punktu widzenia interpretacji najważniejsze wnioski pozostają spójne z wynikami modelu klasycznego: najwyższe dodatnie przełożenie na płacę zapewnia wyższe wykształcenie (szczególnie Advanced Degree), a największy negatywny wpływ ma brak ubezpieczenia zdrowotnego. Choć rezultaty były nieco zaskakujące (spodziewano się, że regularizacja poprawi wyniki względem OLS), Lasso nadal oferuje korzystne cechy (np. odporność na współliniowość i możliwość eliminacji mniej istotnych zmiennych w innych scenariuszach), dlatego ostatecznie wybrano je do dalszych analiz.

LS0tDQp0aXRsZTogJ05pZWtsYXN5Y3puZSBtZXRvZHkgc3RhdHlzdHlraScNCnN1YnRpdGxlOiAnUmVndWxhcnl6YWNqYScNCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCINCmF1dGhvcjogIkRhd2lkIE5hdXMsIFdlcm9uaWthIE5pZHpnb3Jza2EiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIHRoZW1lOiBjZXJ1bGVhbg0KICAgIGhpZ2hsaWdodDogdGV4dG1hdGUNCiAgICBmb250c2l6ZTogMTBwdA0KICAgIHRvYzogeWVzDQogICAgY29kZV9kb3dubG9hZDogeWVzDQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiBubw0KICAgIGRmX3ByaW50OiBkZWZhdWx0DQogICAgdG9jX2RlcHRoOiA1DQplZGl0b3Jfb3B0aW9uczogDQogIG1hcmtkb3duOiANCiAgICB3cmFwOiA3Mg0KLS0tDQoNCiMgVHdvamEga29sZWohDQoNClRlcmF6IG5hZHN6ZWTFgiBjemFzIG5hIHByemV0ZXN0b3dhbmllIHR5Y2ggbWV0b2QgKHJlZ3Jlc2phIGdyemJpZXRvd2EgaQ0KbGFzc28pIG9yYXogbWV0b2Qgb2NlbnkgKHplc3RhdyB3YWxpZGFjeWpueSwgd2FsaWRhY2phIGtyennFvG93YSkgbmENCmlubnljaCB6YmlvcmFjaCBkYW55Y2guIE1vxbxlc3ogcHJhY293YcSHIHogemVzcG/FgmVtIG5hZCB0xIUgY3rEmcWbY2nEhQ0KbGFib3JhdG9yaXVtLg0KDQpNb8W8ZXN6IHXFvHnEhyBkb3dvbG5lZ28gemJpb3J1IGRhbnljaCB6YXdhcnRlZ28gdyAqKklTTFIqKiBsdWIgd3licmHEhw0KamVkZW4geiBwYWtpZXTDs3cgZGFueWNoIG5hIEthZ2dsZS9EYXRhIFdvcmxkIGl0cC4gKHptaWVubmEgemFsZcW8bmEgbXVzaQ0KYnnEhyBjacSFZ8WCYSkuDQoNClBvYmllcnogemJpw7NyIGRhbnljaCBpIHNwcsOzYnVqIG9rcmXFm2xpxIcgb3B0eW1hbG55IHplc3RhdyBwYXJhbWV0csOzdywNCmt0w7NyZSBuYWxlxbx5IHXFvHnEhyBkbyBqZWdvIG1vZGVsb3dhbmlhIQ0KDQpgYGB7cn0NCiNpbnN0YWxsLnBhY2thZ2VzKCdJU0xSJykNCiNpbnN0YWxsLnBhY2thZ2VzKCdkcGx5cicpDQojaW5zdGFsbC5wYWNrYWdlcygnZ2xtbmV0JykNCiNpbnN0YWxsLnBhY2thZ2VzKCdjYXJldCcpDQojaW5zdGFsbC5wYWNrYWdlcygndGlkeXZlcnNlJykNCmxpYnJhcnkoSVNMUikNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KGdsbW5ldCkNCmxpYnJhcnkoY2FyZXQpICAgIA0KbGlicmFyeSh0aWR5dmVyc2UpDQoNCmhlYWQoV2FnZSkNCg0KZGF0YSgiV2FnZSIpDQpXYWdlID0gbmEub21pdChXYWdlKQ0KYGBgDQoNCiMjICoqUHJ6eWdvdG93YW5pZSBkYW55Y2gqKg0KDQpaYmnDs3IgZGFueWNoLCBrdMOzcnkgYW5hbGl6dWplbXksIHBvY2hvZHppIHogcGFraWV0dSAqKklTTFIqKiBpIGRvdHljenkNCioqd3luYWdyb2R6ZcWEIHByYWNvd25pa8OzdyoqLiBaYmnDs3IgdGVuIHphd2llcmEgcsOzxbxuZSBjZWNoeSBkZW1vZ3JhZmljem5lDQppIHphd29kb3dlIHByYWNvd25pa8Ozdywga3TDs3JlIG1vZ8SFIHdwxYJ5d2HEhyBuYSB3eXNva2/Fm8SHIHd5bmFncm9kemVuaWEuDQoNCiMjIyAqKlptaWVubmUgdyBhbmFsaXppZSoqDQoNCi0gICAqKlptaWVubmUgemFsZcW8bmEqKiAoYHlgKToNCg0KICAgIC0gICBgd2FnZWAg4oCTIFd5bmFncm9kemVuaWUgcHJhY293bmlrYSAod2FydG/Fm8SHIGNpxIVnxYJhLCBnxYLDs3duYQ0KICAgICAgICB6bWllbm5hLCBrdMOzcsSFIGNoY2VteSBtb2RlbG93YcSHKS4NCg0KLSAgICoqWm1pZW5uZSBuaWV6YWxlxbxuZSoqIChgWGApOlwNCiAgICBabWllbm55bWkgcHJlZHlrY3lqbnltaSBzxIU6DQoNCiAgICAxLiAgKipgYWdlYCoqIOKAkyBXaWVrIHByYWNvd25pa2EuDQoNCiAgICAyLiAgKipgZWR1Y2F0aW9uYCoqIOKAkyBQb3ppb20gd3lrc3p0YcWCY2VuaWEgKGthdGVnb3JpYSBub21pbmFsbmEsIG5wLg0KICAgICAgICBgMS4gPCBIUyBHcmFkYCwgYDIuIEhTIEdyYWRgLCBgMy4gU29tZSBDb2xsZWdlYCwNCiAgICAgICAgYDQuIENvbGxlZ2UgR3JhZGAsIGA1LiBBZHZhbmNlZCBEZWdyZWVgKS4NCg0KICAgIDMuICAqKmBqb2JjbGFzc2AqKiDigJMgS2xhc2Egd3lrb255d2FuZWogcHJhY3kgKGthdGVnb3JpYSBiaW5hcm5hOg0KICAgICAgICBgSW5kdXN0cmlhbGAgbHViIGBJbmZvcm1hdGlvbmApLg0KDQogICAgNC4gICoqYGhlYWx0aGAqKiDigJMgU2Ftb29jZW5hIHN0YW51IHpkcm93aWEgcHJhY293bmlrYSAoYDw9R29vZGAgbHViDQogICAgICAgIGA+PVZlcnkgR29vZGApLg0KDQogICAgNS4gICoqYGhlYWx0aF9pbnNgKiog4oCTIEluZm9ybWFjamEgbyBwb3NpYWRhbml1IHViZXpwaWVjemVuaWENCiAgICAgICAgemRyb3dvdG5lZ28gKGBZZXNgIGx1YiBgTm9gKS4NCg0KYGBge3J9DQojIFptaWVubmEgemFsZcW8bmENCnkgPC0gV2FnZSR3YWdlDQojIFptaWVubmUgbmllemFsZcW8bmUNClggPC0gbW9kZWwubWF0cml4KHdhZ2UgfiBhZ2UgKyBlZHVjYXRpb24gKyBqb2JjbGFzcyArIGhlYWx0aCArIGhlYWx0aF9pbnMsIGRhdGEgPSBXYWdlKVssLTFdDQoNCmBgYA0KDQojIyAqKlBvZHppYcWCIG5hIHpiacOzciB0cmVuaW5nb3d5IGkgdGVzdG93eSoqDQoNCmBgYHtyfQ0KDQpzZXQuc2VlZCgxMjMpIA0KdHJhaW5faW5kZXggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5LCBwID0gMC44LCBsaXN0ID0gRkFMU0UpDQpYX3RyYWluIDwtIFhbdHJhaW5faW5kZXgsIF0NClhfdGVzdCA8LSBYWy10cmFpbl9pbmRleCwgXQ0KeV90cmFpbiA8LSB5W3RyYWluX2luZGV4XQ0KeV90ZXN0IDwtIHlbLXRyYWluX2luZGV4XQ0KDQpgYGANCg0KRHppZWxpbXkgZGFuZSBuYSB6YmnDs3IgdHJlbmluZ293eSBpIHpiacOzciB0ZXN0b3d5LCBhYnkgbcOzYyBvY2VuacSHIGpha2/Fm8SHDQptb2RlbHUgbmEgZGFueWNoLCBrdMOzcnljaCB3Y3plxZtuaWVqICJuaWUgd2lkemlhxYIiLiBLb3J6eXN0YW15IHogZnVua2NqaQ0KYGNyZWF0ZURhdGFQYXJ0aXRpb25gLCBhYnkgbG9zb3dvIHd5YnJhxIcgODAlIG9ic2Vyd2FjamkgamFrbyB6YmnDs3INCnRyZW5pbmdvd3ksIGEgcG96b3N0YcWCZSAyMCUgamFrbyB6YmnDs3IgdGVzdG93eS4gTmFzdMSZcG5pZSBkemllbGlteQ0KbWFjaWVyeiB6bWllbm55Y2ggbmllemFsZcW8bnljaCAoYFhgKSBvcmF6IHdla3RvciB6bWllbm5laiB6YWxlxbxuZWogKGB5YCkNCm5hIG9kcG93aWVkbmllIHplc3Rhd3k6IHRyZW5pbmdvd2UgKGBYX3RyYWluYCwgYHlfdHJhaW5gKSBpIHRlc3Rvd2UNCihgWF90ZXN0YCwgYHlfdGVzdGApLiBEemnEmWtpIHRlbXUgdWN6eW15IG1vZGVsIG5hIGRhbnljaCB0cmVuaW5nb3d5Y2gsIGENCmplZ28gamFrb8WbxIcgd2VyeWZpa3VqZW15IG5hIGRhbnljaCB0ZXN0b3d5Y2gsIGNvIHBvendhbGEgbmEgb2JpZWt0eXduxIUNCm9jZW7EmSBqZWdvIHNrdXRlY3pub8WbY2kuDQoNCiMjICoqUmVncmVzamEgT0xTKioNCg0KVyBrb2xlam55bSBrcm9rdSAqKmRvcGFzb3dhbGnFm215IG1vZGVsIHJlZ3Jlc2ppIGxpbmlvd2VqIE9MUyoqIChhbmcuDQoqT3JkaW5hcnkgTGVhc3QgU3F1YXJlcyopIG5hIHpiaW9yemUgdHJlbmluZ293eW0uIENlbGVtIGJ5xYJvIG9zemFjb3dhbmllDQp3cMWCeXd1IHptaWVubnljaCBuaWV6YWxlxbxueWNoIG5hIHptaWVubsSFIHphbGXFvG7EhSBvcmF6IG9jZW5hIGpha2/Fm2NpDQpwcmVkeWtjamkgbW9kZWx1IG5hIHpiaW9yemUgdGVzdG93eW0uIE5pZSBtYSB1bml3ZXJzYWxuZWogcmVndcWCeSwga3TDs3JhDQpwb3p3b2xpxYJhYnkgbmFtIHN0d2llcmR6acSHIG5hIHR5bSBldGFwaWUga3TDs3J5IHogbW9kZWxpIHNwcmF3ZHppIHNpxJkNCmxlcGllaiByaWRnZSBsdWIgbGFzc28uIFNwb2R6aWV3YW15IHNpxJkgbmF0b21pYXN0LCDFvGUgb2JpZSBtZXRvZHkNCnJlZ3VsYXJpemFjamkgcG9yYWR6xIUgc29iaWUgbGVwaWVqIHogZXdlbnR1YWxuxIUgd3Nww7PFgmxpbmlvd2/Fm2NpxIUgaQ0KbGVwaWVqIHVvZ8OzbG5pxIUgd3luaWtpIHcgcG9yw7N3bmFuaXUgeiBrbGFzeWN6bnltIG1vZGVsZW0gT0xTLiBXDQpwcmFrdHljZToNCg0KYGBge3J9DQoNCg0KIyBEb3Bhc293YW5pZSBtb2RlbHUgT0xTDQpvbHNfbW9kZWwgPC0gbG0od2FnZSB+IGFnZSArIGVkdWNhdGlvbiArIGpvYmNsYXNzICsgaGVhbHRoICsgaGVhbHRoX2lucywgZGF0YSA9IFdhZ2VbdHJhaW5faW5kZXgsIF0pDQpzdW1tYXJ5KG9sc19tb2RlbCkNCg0KYGBgDQoNCldzenlzdGtpZSB1d3pnbMSZZG5pb25lIHptaWVubmUgb2themHFgnkgc2nEmSBzdGF0eXN0eWN6bmllIGlzdG90bmUgKHAgXDwNCjAsMDUpLCBjbyBvem5hY3phLCDFvGUgd2llaywgcG96aW9tIHd5a3N6dGHFgmNlbmlhLCByb2R6YWogd3lrb255d2FuZWoNCnByYWN5LCBzdGFuIHpkcm93aWEgb3JheiBwb3NpYWRhbmllIHViZXpwaWVjemVuaWEgemRyb3dvdG5lZ28gbWFqxIUgd3DFgnl3DQpuYSB3eXNva2/Fm8SHIHDFgmFjeS4gU3pjemVnw7NsbmllIGR1xbxlIHpuYWN6ZW5pZSBtYSB3eWtzenRhxYJjZW5pZSDigJQNCioqQWR2YW5jZWQgRGVncmVlKiogd2nEhcW8ZSBzacSZIHogYcW8IG8gb2tvxYJvIDU0IFVTRC9nb2R6LiB3ecW8c3p5bWkNCnphcm9ia2FtaSB3IHBvcsOzd25hbml1IGRvIG9zw7NiIHogd3lrc3p0YcWCY2VuaWVtIG5pxbxzenltIG5pxbwgc3prb8WCYQ0KxZtyZWRuaWEuIFLDs3duaWUgaXN0b3RueSwgY2hvxIcgbmVnYXR5d255IHdwxYJ5dywgbWEgKipicmFrIHViZXpwaWVjemVuaWENCnpkcm93b3RuZWdvKiosIGt0w7NyeSBvYm5pxbxhIHDFgmFjxJkgbyBva2/Fgm8gMTggVVNEL2dvZHouIFoga29sZWkga2HFvGR5IHJvaw0Kd2nEmWNlaiB3IG1ldHJ5Y2Ugb2Rwb3dpYWRhIHd6cm9zdG93aSB3eW5hZ3JvZHplbmlhIG8gb2suIDAsNDkgVVNEL2dvZHouLA0KYSBsZXBzenkgc3RhbiB6ZHJvd2lhIChcPj0gVmVyeSBHb29kKSBwcnpla8WCYWRhIHNpxJkgbmEgZG9kYXRrb3dlICs4LDc2DQpVU0QvZ29kei4gUHJhY293bmljeSBzZWt0b3JhIOKAnkluZm9ybWF0aW9u4oCdIHphcmFiaWFqxIUgdGXFvCDFm3JlZG5pbyBvIDQsMTQNClVTRC9nb2R6LiB3acSZY2VqIG5pxbwgb3NvYnkgemF0cnVkbmlvbmUgdyDigJ5JbmR1c3RyaWFs4oCdLg0KDQpXc2thxbpuaWsgUjLiiYgwLDMxIG96bmFjemEsIMW8ZSBtb2RlbCB3eWphxZtuaWEgb2suIDMxJSB6bWllbm5vxZtjaSBwxYJhY3ksIGNvDQp3IGJhZGFuaWFjaCBzcG/FgmVjem5vLWVrb25vbWljem55Y2ggbW/FvG5hIHV6bmHEhyB6YSB1bWlhcmtvd2FuZQ0KZG9wYXNvd2FuaWUgKGJpb3LEhWMgcG9kIHV3YWfEmSwgxbxlIGN6xJlzdG8gaXN0bmllamUgd2llbGUgcG9taW5pxJl0eWNoDQpjenlubmlrw7N3IHdwxYJ5d2FqxIVjeWNoIG5hIHphcm9ia2kpLiAqKk1TRSA9IDEyODgsNjIqKiBwb2thenVqZSBza2FsxJkNCnByemVjacSZdG5lZ28ga3dhZHJhdG93ZWdvIGLFgsSZZHUgcHJlZHlrY2ppLCBhICoqUmVzaWR1YWwgU3RhbmRhcmQgRXJyb3IgPQ0KMzQsNTEqKiB3c2thenVqZSwgbyBpbGUgxZtyZWRuaW8gcG9tecWCa2EgdyBwcm9nbm96aWUgb2RiaWVnYSBvZA0Kb2JzZXJ3b3dhbmVqIHdhcnRvxZtjaSB3eW5hZ3JvZHplbmlhLiBDaG/EhyBtb2RlbCBuaWUgdMWCdW1hY3p5IGNhxYJlag0KesWCb8W8b25vxZtjaSBjenlubmlrw7N3IGtzenRhxYJ0dWrEhWN5Y2ggcMWCYWNlLCBqYXNubyB3c2thenVqZSwgxbxlIHd5xbxzemUNCnd5a3N6dGHFgmNlbmllLCBwb3NpYWRhbmllIHViZXpwaWVjemVuaWEgemRyb3dvdG5lZ28sIGxlcHN6eSBzdGFuDQp6ZHJvd2lhLCBwcmFjYSB3IHNla3RvcnplIGluZm9ybWFjeWpueW0gaSB3aWVrIHRvIGtsdWN6b3dlIGRldGVybWluYW50eQ0Kd3nFvHN6eWNoIHphcm9ia8Ozdy4NCg0KIyMgUmVncmVzamEgUmlkZ2UNCg0KYGBge3J9DQoNCiMgUHJ6eWdvdG93YW5pZSBkYW55Y2ggZGxhIGdsbW5ldA0KWF90cmFpbl9tYXRyaXggPC0gYXMubWF0cml4KFhfdHJhaW4pDQpYX3Rlc3RfbWF0cml4IDwtIGFzLm1hdHJpeChYX3Rlc3QpDQoNCiMgRG9wYXNvd2FuaWUgbW9kZWx1IFJpZGdlICjOsSA9IDApDQpyaWRnZV9tb2RlbCA8LSBjdi5nbG1uZXQoWF90cmFpbl9tYXRyaXgsIHlfdHJhaW4sIGFscGhhID0gMCkNCnBsb3QocmlkZ2VfbW9kZWwpDQpyaWRnZV9sYW1iZGEgPC0gcmlkZ2VfbW9kZWwkbGFtYmRhLm1pbg0KY2F0KCJPcHRpbWFsIFJpZGdlIExhbWJkYToiLCByaWRnZV9sYW1iZGEsICJcbiIpDQoNCiMgUHJlZHlrY2plIGkgb2NlbmENCnJpZGdlX3ByZWQgPC0gcHJlZGljdChyaWRnZV9tb2RlbCwgcyA9IHJpZGdlX2xhbWJkYSwgbmV3eCA9IFhfdGVzdF9tYXRyaXgpDQpyaWRnZV9tc2UgPC0gbWVhbigoeV90ZXN0IC0gcmlkZ2VfcHJlZCleMikNCnJpZGdlX3IyIDwtIDEgLSAoc3VtKCh5X3Rlc3QgLSByaWRnZV9wcmVkKV4yKSAvIHN1bSgoeV90ZXN0IC0gbWVhbih5X3Rlc3QpKV4yKSkNCg0KY2F0KCJSaWRnZSBNU0U6IiwgcmlkZ2VfbXNlLCAiXG5SaWRnZSBSXjI6IiwgcmlkZ2VfcjIsICJcbiIpDQpgYGANCg0KWiBrb2xlaSwgcHJ6ZWNob2R6xIVjIGRvICoqcmVncmVzamkgZ3J6YmlldG93ZWogKHJpZGdlKSoqLCB6YXN0b3Nvd2Fubw0Kd2FsaWRhY2rEmSBrcnp5xbxvd8SFIHcgY2VsdSB1c3RhbGVuaWEgb3B0eW1hbG5laiB3YXJ0b8WbY2kgcGFyYW1ldHJ1DQpyZWd1bGFyeXphY2ppIM67LiBaIHByemVkc3Rhd2lvbmVnbyB3eWtyZXN1IHdpZGHEhywgamFrIMWbcmVkbmkgYsWCxIVkDQprd2FkcmF0b3d5IChNU0UpIHptaWVuaWEgc2nEmSB3IHphbGXFvG5vxZtjaSBvZCBsb2figaEozrspOiB3IG9rb2xpY2FjaA0KbG9n4oGhKM67KeKJiDAuNDYgKGN6eWxpIM674omIMSw1OGJzZXJ3dWplbXkgamVnbyBtaW5pbXVtLiBPc3RhdGVjem5pZSwgcHJ6eSB0eW0NCuKAnm5hamxlcHN6eW3igJ0gzrsgKipSaWRnZSBNU0UqKiB3eW5vc2kgKioxMjk1LDE5KiosIGEgKipSXF4yKiogdG8NCioqMCwyODgqKi4NCg0KVyBwb3LDs3duYW5pdSB6IG1vZGVsZW0gT0xTLCB3IGt0w7NyeW0gTVNFIGJ5xYJvIG5hIHBvemlvbWllIG9rLiAxMjg4LDYyLCBhDQpSMuKJiDAsMjkyUl4yICwgKipyw7PFvG5pY2Ugc8SFIG5pZXdpZWxraWUqKi4gUmlkZ2UgZGVsaWthdG5pZSB1c3TEmXB1amUgT0xTDQpwb2Qgd3pnbMSZZGVtIGLFgsSZZHUsIGFsZSB3IHphbWlhbiB6eXNrdWplbXkgKip3acSZa3N6xIUgc3RhYmlsbm/Fm8SHIGkNCmtvbnRyb2zEmSBuYWQgd3Nww7PFgmxpbmlvd2/Fm2NpxIUqKiAoenfFgmFzemN6YSBwcnp5IHdpxJlrc3plaiBsaWN6YmllDQp6bWllbm55Y2gpLiBXIHRlaiBzeXR1YWNqaSBwcnpld2FnYSByaWRnZSBuaWUgamVzdCBtb2NubyB3aWRvY3puYSB3DQpzYW15Y2ggbWlhcmFjaCBkb3Bhc293YW5pYSwgY2hvxIcgdyBwcmFrdHljZSBtb8W8ZSBwb3ByYXdpYcSHICoqb2Rwb3Jub8WbxIcNCm1vZGVsdSoqIG5hIHBvdGVuY2phbG5lIHpha8WCw7NjZW5pYSBjenkgd2FoYW5pYSB3IGRhbnljaC4NCg0KIyMgUmVncmVzamEgTGFzc28NCg0KYGBge3J9DQoNCiMgRG9wYXNvd2FuaWUgbW9kZWx1IExhc3NvICjOsSA9IDEpDQpsYXNzb19tb2RlbCA8LSBjdi5nbG1uZXQoWF90cmFpbl9tYXRyaXgsIHlfdHJhaW4sIGFscGhhID0gMSkNCnBsb3QobGFzc29fbW9kZWwpDQoNCiMgT3B0eW1hbG5lIGxhbWJkYQ0KbGFzc29fbGFtYmRhIDwtIGxhc3NvX21vZGVsJGxhbWJkYS5taW4NCmNhdCgiT3B0aW1hbCBMYXNzbyBMYW1iZGE6IiwgbGFzc29fbGFtYmRhLCAiXG4iKQ0KDQojIFByZWR5a2NqZSBpIG9jZW5hDQpsYXNzb19wcmVkIDwtIHByZWRpY3QobGFzc29fbW9kZWwsIHMgPSBsYXNzb19sYW1iZGEsIG5ld3ggPSBYX3Rlc3RfbWF0cml4KQ0KDQpsYXNzb19tc2UgPC0gbWVhbigoeV90ZXN0IC0gbGFzc29fcHJlZCleMikNCmxhc3NvX3IyIDwtIDEgLSAoc3VtKCh5X3Rlc3QgLSBsYXNzb19wcmVkKV4yKSAvIHN1bSgoeV90ZXN0IC0gbWVhbih5X3Rlc3QpKV4yKSkNCg0KY2F0KCJMYXNzbyBNU0U6IiwgbGFzc29fbXNlLCAiXG5MYXNzbyBSXjI6IiwgbGFzc29fcjIsICJcbiIpDQpgYGANCg0KS29udHludXVqxIVjIGFuYWxpesSZLCB3IHByenlwYWRrdSAqKnJlZ3Jlc2ppIExhc3NvKiogemFzdG9zb3dhbm8gcG9kb2JuxIUNCnByb2NlZHVyxJkgd2FsaWRhY2ppIGtyennFvG93ZWosIGt0w7NyYSB3ecWCb25pxYJhIG9wdHltYWxuZSDOu+KJiDAsMDI1OCBEbGEgdGVqDQp3YXJ0b8WbY2kga2FyeSAqKkxhc3NvIE1TRSoqIHd5bm9zaSAqKjEyODgsNjgqKiwgYSBSMuKJiDAsMjkyLiBXeW5pa2kgc8SFDQp3acSZYyBuaWVtYWwgaWRlbnR5Y3puZSBqYWsgdyBtb2RlbHUgT0xTIChnZHppZSBNU0UgaSBSMlJeMlIyIG1pYcWCeQ0Kb2Rwb3dpZWRuaW8gd2FydG/Fm2NpIFx+MTI4OCw2MiBpIDAsMjkxNikuIExhc3NvIG5pZSBwcnp5bmlvc8WCbyB3IHR5bQ0Ka29ua3JldG55bSB6ZXN0YXdpZSBkYW55Y2ggaXN0b3RuZWdvIHp5c2t1IHcgcHJlZHlrY2ppLCBhbGUgdw0KcHJ6ZWNpd2llxYRzdHdpZSBkbyBPTFMgcG90cmFmaSB6cmVkdWtvd2HEhyB6bmFjemVuaWUgbW5pZWogaXN0b3RueWNoDQp6bWllbm55Y2gsIOKAnnplcnVqxIVj4oCdIG5pZWt0w7NyZSB3c3DDs8WCY3p5bm5pa2kuIFcgZWZla2NpZSBtb8W8ZSBkYXdhxIcNCnByb3N0c3p5LCBiYXJkemllaiB6aW50ZXJwcmV0b3dhbG55IG1vZGVsIGtvc3p0ZW0gemJsacW8b25lZ28gbHViIGxla2tvDQp3ecW8c3plZ28gYsWCxJlkdS4gSmVkbmFrIHcgbmFzemVqIHN5dHVhY2ppIHLDs8W8bmljYSB3IHN0b3N1bmt1IGRvIE9MUyBqZXN0DQptaW5pbWFsbmEsIGNvIHBva2F6dWplLCDFvGUgxbxhZGVuIHogZG9kYW55Y2ggbWVjaGFuaXptw7N3IHJlZ3VsYXJpemFjamkNCihyaWRnZSBjenkgbGFzc28pIG5pZSB6YXBld25pYSB3eXJhxbpuZWogcHJ6ZXdhZ2kgbmFkIGtsYXN5Y3puxIUgcmVncmVzasSFDQpsaW5pb3fEhSB3IHR5bSB6ZXN0YXdpZSBkYW55Y2guDQoNCiMjIyBXYcW8bm/Fm8SHIHByZWR5a3RvcsOzdyAoTGFzc28pDQoNCmBgYHtyfQ0KDQoNCiMgV2HFvG5vxZvEhyB6bWllbm55Y2ggZGxhIExhc3NvDQpsYXNzb19jb2VmZmljaWVudHMgPC0gY29lZihsYXNzb19tb2RlbCwgcyA9IGxhc3NvX2xhbWJkYSkNCmxhc3NvX2NvZWZmaWNpZW50c19kZiA8LSBkYXRhLmZyYW1lKA0KICBWYXJpYWJsZSA9IHJvd25hbWVzKGxhc3NvX2NvZWZmaWNpZW50cyksDQogIENvZWZmaWNpZW50ID0gYXMudmVjdG9yKGxhc3NvX2NvZWZmaWNpZW50cykNCikNCmxhc3NvX2NvZWZmaWNpZW50c19kZiA8LSBsYXNzb19jb2VmZmljaWVudHNfZGYgJT4lIGZpbHRlcihDb2VmZmljaWVudCAhPSAwKQ0KDQpwcmludChsYXNzb19jb2VmZmljaWVudHNfZGYpDQpgYGANCg0KVyBwb2RzdW1vd2FuaXUgcG9yw7N3bmHFhCBtacSZZHp5IE9MUywgcmlkZ2UgaSBsYXNzbyBva2F6YcWCbyBzacSZIGRvxZvEhw0Kbmllc3BvZHppZXdhbmllLCDFvGUgKiptb2RlbCBPTFMqKiBvc2nEhWduxIXFgiBtaW5pbWFsbmllIGxlcHN6ZSB3c2thxbpuaWtpDQpkb3Bhc293YW5pYSAobnAuIG5pxbxzemUgTVNFKSBuacW8IG1ldG9keSB6IHJlZ3VsYXJpemFjasSFLiBNaW1vIHRvLA0Kb3N0YXRlY3puaWUgemRlY3lkb3dhbm8gc2nEmSAqKnd5YnJhxIcgbW9kZWwgTGFzc28qKi4gR8WCw7N3bnltIHBvd29kZW0gamVzdA0KamVnbyBwb3RlbmNqYcWCIGRvIHN0YWJpbG5laiBzZWxla2NqaSB6bWllbm55Y2ggaSBvZ3JhbmljemFuaWENCmV3ZW50dWFsbmVnbyBwcnpldWN6ZW5pYSwgY2hvY2lhxbwgdyB0eW0ga29ua3JldG55bSB6ZXN0YXdpZSBkYW55Y2ggxbxhZGVuDQp3c3DDs8WCY3p5bm5payBuaWUgem9zdGHFgiB3eXplcm93YW55IChjenlsaSBMYXNzbyB3eWJyYcWCbyBpZGVudHljem55DQp6ZXN0YXcgcHJlZHlrdG9yw7N3LCBjbyBPTFMpLiBaIHB1bmt0dSB3aWR6ZW5pYSBpbnRlcnByZXRhY2ppDQpuYWp3YcW8bmllanN6ZSB3bmlvc2tpIHBvem9zdGFqxIUgc3DDs2puZSB6IHd5bmlrYW1pIG1vZGVsdSBrbGFzeWN6bmVnbzoNCm5hand5xbxzemUgZG9kYXRuaWUgcHJ6ZcWCb8W8ZW5pZSBuYSBwxYJhY8SZIHphcGV3bmlhIHd5xbxzemUgd3lrc3p0YcWCY2VuaWUNCihzemN6ZWfDs2xuaWUgQWR2YW5jZWQgRGVncmVlKSwgYSBuYWp3acSZa3N6eSBuZWdhdHl3bnkgd3DFgnl3IG1hIGJyYWsNCnViZXpwaWVjemVuaWEgemRyb3dvdG5lZ28uIENob8SHIHJlenVsdGF0eSBiecWCeSBuaWVjbyB6YXNrYWt1asSFY2UNCihzcG9kemlld2FubyBzacSZLCDFvGUgcmVndWxhcml6YWNqYSBwb3ByYXdpIHd5bmlraSB3emdsxJlkZW0gT0xTKSwNCioqTGFzc28qKiBuYWRhbCBvZmVydWplIGtvcnp5c3RuZSBjZWNoeSAobnAuIG9kcG9ybm/Fm8SHIG5hIHdzcMOzxYJsaW5pb3dvxZvEhw0KaSBtb8W8bGl3b8WbxIcgZWxpbWluYWNqaSBtbmllaiBpc3RvdG55Y2ggem1pZW5ueWNoIHcgaW5ueWNoDQpzY2VuYXJpdXN6YWNoKSwgZGxhdGVnbyBvc3RhdGVjem5pZSB3eWJyYW5vIGplIGRvIGRhbHN6eWNoIGFuYWxpei4NCg==