If any issues, questions or suggestions feel free to reach me out via e-mail or Linkedin. You can also visit my Github.

This is R replication of the code and exercises from the Udemy course “Credit Risk Modeling in Python 2022”.

if(!require('pacman')) install.packages('pacman')
pacman::p_load(dplyr, tidyr, ggplot2, pROC)
options(scipen = 20)

We load data which was prepared in that blog post. Our dataset containt 43236 rows and 47 attributes. We will fit logistic regression model to decide whether recovery rate is greater than 0 and if so, then we will fit linear regression model.

data <- read.csv('ead_lgd_preprocessed_data.csv')
data %>% dim()
[1] 43236    47

We split our data into training set and test set in proportions 80:20.

set.seed(2137)
n_obs <- nrow(data)
train_index <- sample(1:n_obs, 0.8 * n_obs)

For logistic regression model we create dummy variable whether recovery rate is 0 or greater than 0.

data <- data %>%
  mutate(recovery_rate_0_1 = if_else(recovery_rate > 0, 1, 0) %>% as.factor())

Fit logistic regression model.

log_model_1 <- glm(
  family = binomial(link = 'logit')
  ,recovery_rate_0_1 ~.
  ,data = data %>% select(-good_bad, -recovery_rate, -CCF)
  ,subset = train_index
)

Calculate adjusted multivariate p-values using p.adjust() function. Based on these we exclude from the logistic regression model the following variables:
- categorial: home_onwership, purpose, term
- continuous: emp_length, open_acc, pub_rec, acc_now_delinq, total_rev_hi_lim, mths_since_last_delinq.

log_p_values_multi <- log_model_1 %>%
  summary() %>%
  coef() %>%
  .[,4] %>% 
  p.adjust()

log_p_values_multi %>% round(5) %>% as.data.frame()

The final logistic regression model for estimating wether recovery rate is 0 or is higher than 0 is the following.

log_model_2 <- glm(
  family = binomial(link = 'logit')
  ,recovery_rate_0_1 ~.
  ,data = data %>% select(
    -good_bad, -recovery_rate, -CCF
    ,-starts_with('home_ownership'), -starts_with('purpose'), -starts_with('term')
    ,-emp_length, -open_acc, -pub_rec, -acc_now_delinq, -total_rev_hi_lim, -mths_since_last_delinq
  )
  ,subset = train_index
)

log_model_2 %>% summary()

Call:
glm(formula = recovery_rate_0_1 ~ ., family = binomial(link = "logit"), 
    data = data %>% select(-good_bad, -recovery_rate, -CCF, -starts_with("home_ownership"), 
        -starts_with("purpose"), -starts_with("term"), -emp_length, 
        -open_acc, -pub_rec, -acc_now_delinq, -total_rev_hi_lim, 
        -mths_since_last_delinq), subset = train_index)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-2.6941  -1.1045   0.7117   0.9707   2.1048  

Coefficients:
                                        Estimate   Std. Error z value             Pr(>|z|)    
(Intercept)                         -4.573617112  0.268709025 -17.021 < 0.0000000000000002 ***
grade_A                              2.262087421  0.179065760  12.633 < 0.0000000000000002 ***
grade_B                              1.700315449  0.145013479  11.725 < 0.0000000000000002 ***
grade_C                              1.295375871  0.124141299  10.435 < 0.0000000000000002 ***
grade_D                              0.988976672  0.108445843   9.120 < 0.0000000000000002 ***
grade_E                              0.618683621  0.098508593   6.281  0.00000000033747697 ***
grade_F                              0.343559263  0.097897093   3.509             0.000449 ***
addr_state_NM_VA                    -0.102256548  0.068522819  -1.492             0.135621    
addr_state_NY                       -0.517773648  0.051274204 -10.098 < 0.0000000000000002 ***
addr_state_OK_TN_MO_LA_MD_NC        -0.059891111  0.049483274  -1.210             0.226152    
addr_state_CA                       -0.128848163  0.044723972  -2.881             0.003965 ** 
addr_state_UT_KY_AZ_NJ              -0.194507422  0.052819361  -3.683             0.000231 ***
addr_state_AR_MI_PA_OH_MN           -0.166935459  0.047855319  -3.488             0.000486 ***
addr_state_RI_MA_DE_SD_IN            0.016264962  0.064147357   0.254             0.799838    
addr_state_GA_WA_OR                 -0.444341598  0.056671689  -7.841  0.00000000000000448 ***
addr_state_WI_MT                    -0.368259387  0.100786316  -3.654             0.000258 ***
addr_state_IL_CT                    -0.388081534  0.062489660  -6.210  0.00000000052872774 ***
addr_state_KS_SC_CO_VT_AK_MS        -0.119854148  0.067069066  -1.787             0.073933 .  
addr_state_TX                        0.183522562  0.057231202   3.207             0.001343 ** 
addr_state_WV_NH_WY_DC_ME_ID        -0.152398035  0.116142977  -1.312             0.189467    
verification_status_Source.Verified -0.070280631  0.028261601  -2.487             0.012890 *  
verification_status_Not.Verified    -0.126733212  0.029830975  -4.248  0.00002153253608254 ***
initial_list_status_w               -0.756697306  0.028123598 -26.906 < 0.0000000000000002 ***
months_since_issue_d                 0.032940758  0.000960904  34.281 < 0.0000000000000002 ***
int_rate                             0.164344608  0.008992668  18.275 < 0.0000000000000002 ***
months_since_earliest_cr_line       -0.000525558  0.000150598  -3.490             0.000483 ***
delinq_2yrs                          0.049035676  0.015220726   3.222             0.001275 ** 
inq_last_6mths                      -0.032303551  0.009556738  -3.380             0.000724 ***
total_acc                           -0.007326276  0.001171753  -6.252  0.00000000040417920 ***
annual_inc                           0.000002310  0.000000344   6.716  0.00000000001867798 ***
dti                                 -0.011541411  0.001650186  -6.994  0.00000000000267144 ***
mths_since_last_record              -0.003098384  0.000415384  -7.459  0.00000000000008713 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 47357  on 34587  degrees of freedom
Residual deviance: 43139  on 34556  degrees of freedom
AIC: 43203

Number of Fisher Scoring iterations: 4

In that blog post we calculated ROC and AUC manually for educational purposes. Now let’s use function roc from the library pROC. Here we take TPR as sensitivities and we calculate FPR as 1 - specificities. AUC of this model is \(69.93\%\).

log_predict_prob <- predict.glm(log_model_2
                                ,data[-train_index,]
                                ,type = 'response')

log_model_roc <- roc(response = data$recovery_rate_0_1[-train_index]
                     ,predictor = log_predict_prob)
Setting levels: control = 0, case = 1
Setting direction: controls < cases
data.frame(
  FPR = 1 - log_model_roc$specificities
  ,TPR = log_model_roc$sensitivities
) %>%
  ggplot(aes(FPR, TPR)) +
    geom_line(size = 1, col = 'blue') +
    geom_segment(aes(x = 0, xend = 1, y = 0, yend = 1), size = 1, linetype = 'dashed') +
    labs(title = paste0('Recovery Rate Logistic Regression Model\nROC of Validation Set\nAUC = '
                        ,round(log_model_roc$auc, 4))) +
    theme_bw()

Now let’s take rows with recovery_rate > 0. There are 24371 observations in the filtered dataset.

data_stage_2 <- data %>%
  filter(recovery_rate > 0)

data_stage_2 %>% dim()
[1] 24371    48

For those we fit linear regression model.

lin_model_1 <- lm(
  recovery_rate ~.
  ,data = data_stage_2 %>% select(-good_bad, -recovery_rate_0_1, -CCF)
  ,subset = train_index
)

Calculate adjusted multivariate p-values using p.adjust() function. Based on these we exclude from the linear regression model the following variables:
- categorial: home_onwership, addr_state, purpose
- continuous: emp_length, months_since_earliest_cr_line, delinq_2yrs, inq_last_6mths, pub_rec, acc_now_delinq, annual_inc, mths_since_last_delinq, dti, mths_since_last_record.

lin_p_values_multi <- lin_model_1 %>%
  summary() %>%
  coef() %>%
  .[,4] %>% 
  p.adjust()

lin_p_values_multi %>% round(5) %>% as.data.frame()

For those we fit linear regression model. We obtain pretty simple model, but with very poor \(R^2\).

lin_model_2 <- lm(
  recovery_rate ~.
  ,data = data %>% select(
    -good_bad, -recovery_rate_0_1, -CCF
    ,-starts_with('home_ownership'), -starts_with('purpose'), -starts_with('addr_state')
    ,-emp_length, -months_since_earliest_cr_line, -delinq_2yrs, -inq_last_6mths, -pub_rec
    ,-acc_now_delinq, -annual_inc, -mths_since_last_delinq, -dti, -mths_since_last_record
  )
  ,subset = train_index
)

lin_model_2 %>% summary()

Call:
lm(formula = recovery_rate ~ ., data = data %>% select(-good_bad, 
    -recovery_rate_0_1, -CCF, -starts_with("home_ownership"), 
    -starts_with("purpose"), -starts_with("addr_state"), -emp_length, 
    -months_since_earliest_cr_line, -delinq_2yrs, -inq_last_6mths, 
    -pub_rec, -acc_now_delinq, -annual_inc, -mths_since_last_delinq, 
    -dti, -mths_since_last_record), subset = train_index)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.38690 -0.05695 -0.02815  0.04626  0.96283 

Coefficients:
                                          Estimate     Std. Error t value             Pr(>|t|)    
(Intercept)                         -0.00572931021  0.01079144771  -0.531             0.595483    
grade_A                              0.02931672314  0.00720118758   4.071   0.0000468965584607 ***
grade_B                              0.02020574669  0.00584901888   3.455             0.000552 ***
grade_C                              0.01422911340  0.00503091505   2.828             0.004682 ** 
grade_D                              0.00979628555  0.00441305373   2.220             0.026436 *  
grade_E                              0.00618242156  0.00402198002   1.537             0.124264    
grade_F                              0.00435179162  0.00401625777   1.084             0.278575    
verification_status_Source.Verified -0.00322360218  0.00117223139  -2.750             0.005963 ** 
verification_status_Not.Verified    -0.00165376640  0.00125547956  -1.317             0.187767    
initial_list_status_w               -0.01591725813  0.00119200421 -13.353 < 0.0000000000000002 ***
term_36                             -0.00393913235  0.00119090460  -3.308             0.000942 ***
months_since_issue_d                 0.00005864143  0.00003787667   1.548             0.121578    
int_rate                             0.00370628227  0.00036459848  10.165 < 0.0000000000000002 ***
open_acc                            -0.00083015190  0.00013736378  -6.043   0.0000000015238842 ***
total_acc                            0.00019470063  0.00005766265   3.377             0.000735 ***
total_rev_hi_lim                     0.00000016961  0.00000002201   7.705   0.0000000000000134 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.08898 on 34572 degrees of freedom
Multiple R-squared:  0.02238,   Adjusted R-squared:  0.02196 
F-statistic: 52.77 on 15 and 34572 DF,  p-value: < 0.00000000000000022

As we see below residuals of the linear regression model ain’t behave well. These are not normally distributed and heteroscedastic. In another blog post we will estimate beta regression instead of two staged logistic and lienar regression.

par(mfrow = c(2,2))
lin_model_2 %>% plot(which = 1:3)
acf(lin_model_2$residuals, main = '')

Finally, in order to predict recovery rate a given loan first we have to decide whether recovery rate is 0 or above 0 using logistic regression model and if it’s above 0 we predict it with linear regression model. To do that we simply multiply predicted values from both models. Let’s take a threshold of 0.5 for predictions from logistic regression model.

lin_predict <- predict(lin_model_2
                       ,data[-train_index,])

log_predict_class <- if_else(log_predict_prob > 0.5, 0, 1)

recovery_rate_summary = data.frame(
  Actual = data$recovery_rate[-train_index]
  ,Predicted = log_predict_class * lin_predict
) %>%
  pivot_longer(cols = 1:2, names_to = 'Recovery Rate', values_to = 'Value')

ggplot(recovery_rate_summary, aes(Value, fill = `Recovery Rate`)) +
  geom_density(alpha= 0.5) +
  theme_bw() +
  labs(x = '', y ='', title = 'Densities of Actual and Predicted Recovery Rates')

LS0tDQp0aXRsZTogIkNyZWRpdCBSaXNrIE1vZGVsaW5nIC0gTEdEIE1vZGVsIEVzdGltYXRpb24gd2l0aCBUd28gU3RhZ2UgUmVncmVzc2lvbiINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCklmIGFueSBpc3N1ZXMsIHF1ZXN0aW9ucyBvciBzdWdnZXN0aW9ucyBmZWVsIGZyZWUgdG8gcmVhY2ggbWUgb3V0IHZpYSBlLW1haWwgPHdpZWN6eW5za2lwYXdlbEBnbWFpbC5jb20+IG9yIFtMaW5rZWRpbl0oaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2luL3Bhd2VsLXdpZWN6eW5za2kvKS4gWW91IGNhbiBhbHNvIHZpc2l0IG15IFtHaXRodWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9wYXdlbC13aWVjenluc2tpKS4NCg0KVGhpcyBpcyBSIHJlcGxpY2F0aW9uIG9mIHRoZSBjb2RlIGFuZCBleGVyY2lzZXMgZnJvbSB0aGUgVWRlbXkgY291cnNlIFsiQ3JlZGl0IFJpc2sgTW9kZWxpbmcgaW4gUHl0aG9uIDIwMjIiXShodHRwczovL3d3dy51ZGVteS5jb20vY291cnNlL2NyZWRpdC1yaXNrLW1vZGVsaW5nLWluLXB5dGhvbi8pLg0KDQpgYGB7ciBsb2FkX2xpYnJhcmllc30NCmlmKCFyZXF1aXJlKCdwYWNtYW4nKSkgaW5zdGFsbC5wYWNrYWdlcygncGFjbWFuJykNCnBhY21hbjo6cF9sb2FkKGRwbHlyLCB0aWR5ciwgZ2dwbG90MiwgcFJPQykNCm9wdGlvbnMoc2NpcGVuID0gMjApDQpgYGANCg0KV2UgbG9hZCBkYXRhIHdoaWNoIHdhcyBwcmVwYXJlZCBpbiBbdGhhdCBibG9nIHBvc3RdKGh0dHBzOi8vcnB1YnMuY29tL3Bhd2VsLXdpZWN6eW5za2kvODk4MDA2KS4gT3VyIGRhdGFzZXQgY29udGFpbnQgNDMyMzYgcm93cyBhbmQgNDcgYXR0cmlidXRlcy4gV2Ugd2lsbCBmaXQgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbCB0byBkZWNpZGUgd2hldGhlciByZWNvdmVyeSByYXRlIGlzIGdyZWF0ZXIgdGhhbiAwIGFuZCBpZiBzbywgdGhlbiB3ZSB3aWxsIGZpdCBsaW5lYXIgcmVncmVzc2lvbiBtb2RlbC4NCmBgYHtyIGxvYWRfZGF0YX0NCmRhdGEgPC0gcmVhZC5jc3YoJ2VhZF9sZ2RfcHJlcHJvY2Vzc2VkX2RhdGEuY3N2JykNCmRhdGEgJT4lIGRpbSgpDQpgYGANCg0KV2Ugc3BsaXQgb3VyIGRhdGEgaW50byB0cmFpbmluZyBzZXQgYW5kIHRlc3Qgc2V0IGluIHByb3BvcnRpb25zIDgwOjIwLg0KYGBge3IgZGF0YV9zcGxpdH0NCnNldC5zZWVkKDIxMzcpDQpuX29icyA8LSBucm93KGRhdGEpDQp0cmFpbl9pbmRleCA8LSBzYW1wbGUoMTpuX29icywgMC44ICogbl9vYnMpDQpgYGANCg0KRm9yIGxvZ2lzdGljIHJlZ3Jlc3Npb24gbW9kZWwgd2UgY3JlYXRlIGR1bW15IHZhcmlhYmxlIHdoZXRoZXIgcmVjb3ZlcnkgcmF0ZSBpcyAwIG9yIGdyZWF0ZXIgdGhhbiAwLg0KYGBge3IgcmVjb3ZlcnlfcmF0ZV9kdW1teX0NCmRhdGEgPC0gZGF0YSAlPiUNCiAgbXV0YXRlKHJlY292ZXJ5X3JhdGVfMF8xID0gaWZfZWxzZShyZWNvdmVyeV9yYXRlID4gMCwgMSwgMCkgJT4lIGFzLmZhY3RvcigpKQ0KYGBgDQoNCkZpdCBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsLg0KYGBge3IgbG9nX21vZGVsXzF9DQpsb2dfbW9kZWxfMSA8LSBnbG0oDQogIGZhbWlseSA9IGJpbm9taWFsKGxpbmsgPSAnbG9naXQnKQ0KICAscmVjb3ZlcnlfcmF0ZV8wXzEgfi4NCiAgLGRhdGEgPSBkYXRhICU+JSBzZWxlY3QoLWdvb2RfYmFkLCAtcmVjb3ZlcnlfcmF0ZSwgLUNDRikNCiAgLHN1YnNldCA9IHRyYWluX2luZGV4DQopDQpgYGANCg0KQ2FsY3VsYXRlICoqYWRqdXN0ZWQgbXVsdGl2YXJpYXRlIHAtdmFsdWVzKiogdXNpbmcgKnAuYWRqdXN0KCkqIGZ1bmN0aW9uLiBCYXNlZCBvbiB0aGVzZSB3ZSBleGNsdWRlIGZyb20gdGhlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gbW9kZWwgdGhlIGZvbGxvd2luZyB2YXJpYWJsZXM6IFwNCiAtIGNhdGVnb3JpYWw6IGhvbWVfb253ZXJzaGlwLCBwdXJwb3NlLCB0ZXJtIFwNCiAtIGNvbnRpbnVvdXM6IGVtcF9sZW5ndGgsIG9wZW5fYWNjLCBwdWJfcmVjLCBhY2Nfbm93X2RlbGlucSwgdG90YWxfcmV2X2hpX2xpbSwgbXRoc19zaW5jZV9sYXN0X2RlbGlucS4NCmBgYHtyIGxvZ19tb2RlbF9wX2FkanVzdH0NCmxvZ19wX3ZhbHVlc19tdWx0aSA8LSBsb2dfbW9kZWxfMSAlPiUNCiAgc3VtbWFyeSgpICU+JQ0KICBjb2VmKCkgJT4lDQogIC5bLDRdICU+JSANCiAgcC5hZGp1c3QoKQ0KDQpsb2dfcF92YWx1ZXNfbXVsdGkgJT4lIHJvdW5kKDUpICU+JSBhcy5kYXRhLmZyYW1lKCkNCmBgYA0KDQpUaGUgZmluYWwgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbCBmb3IgZXN0aW1hdGluZyB3ZXRoZXIgcmVjb3ZlcnkgcmF0ZSBpcyAwIG9yIGlzIGhpZ2hlciB0aGFuIDAgaXMgdGhlIGZvbGxvd2luZy4NCmBgYHtyIGxvZ19tb2RlbF8yfQ0KbG9nX21vZGVsXzIgPC0gZ2xtKA0KICBmYW1pbHkgPSBiaW5vbWlhbChsaW5rID0gJ2xvZ2l0JykNCiAgLHJlY292ZXJ5X3JhdGVfMF8xIH4uDQogICxkYXRhID0gZGF0YSAlPiUgc2VsZWN0KA0KICAgIC1nb29kX2JhZCwgLXJlY292ZXJ5X3JhdGUsIC1DQ0YNCiAgICAsLXN0YXJ0c193aXRoKCdob21lX293bmVyc2hpcCcpLCAtc3RhcnRzX3dpdGgoJ3B1cnBvc2UnKSwgLXN0YXJ0c193aXRoKCd0ZXJtJykNCiAgICAsLWVtcF9sZW5ndGgsIC1vcGVuX2FjYywgLXB1Yl9yZWMsIC1hY2Nfbm93X2RlbGlucSwgLXRvdGFsX3Jldl9oaV9saW0sIC1tdGhzX3NpbmNlX2xhc3RfZGVsaW5xDQogICkNCiAgLHN1YnNldCA9IHRyYWluX2luZGV4DQopDQoNCmxvZ19tb2RlbF8yICU+JSBzdW1tYXJ5KCkNCmBgYA0KDQpbSW4gdGhhdCBibG9nIHBvc3RdKGh0dHBzOi8vcnB1YnMuY29tL3Bhd2VsLXdpZWN6eW5za2kvODk1MzA3KSB3ZSBjYWxjdWxhdGVkIFJPQyBhbmQgQVVDIG1hbnVhbGx5IGZvciBlZHVjYXRpb25hbCBwdXJwb3Nlcy4gTm93IGxldCdzIHVzZSBmdW5jdGlvbiAqcm9jKiBmcm9tIHRoZSBsaWJyYXJ5ICpwUk9DKi4gSGVyZSB3ZSB0YWtlIFRQUiBhcyBzZW5zaXRpdml0aWVzIGFuZCB3ZSBjYWxjdWxhdGUgRlBSIGFzIDEgLSBzcGVjaWZpY2l0aWVzLiBBVUMgb2YgdGhpcyBtb2RlbCBpcyAkNjkuOTNcJSQuDQpgYGB7ciBsb2dfcm9jfQ0KbG9nX3ByZWRpY3RfcHJvYiA8LSBwcmVkaWN0LmdsbShsb2dfbW9kZWxfMg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAsZGF0YVstdHJhaW5faW5kZXgsXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAsdHlwZSA9ICdyZXNwb25zZScpDQoNCmxvZ19tb2RlbF9yb2MgPC0gcm9jKHJlc3BvbnNlID0gZGF0YSRyZWNvdmVyeV9yYXRlXzBfMVstdHJhaW5faW5kZXhdDQogICAgICAgICAgICAgICAgICAgICAscHJlZGljdG9yID0gbG9nX3ByZWRpY3RfcHJvYikNCg0KZGF0YS5mcmFtZSgNCiAgRlBSID0gMSAtIGxvZ19tb2RlbF9yb2Mkc3BlY2lmaWNpdGllcw0KICAsVFBSID0gbG9nX21vZGVsX3JvYyRzZW5zaXRpdml0aWVzDQopICU+JQ0KICBnZ3Bsb3QoYWVzKEZQUiwgVFBSKSkgKw0KICAgIGdlb21fbGluZShzaXplID0gMSwgY29sID0gJ2JsdWUnKSArDQogICAgZ2VvbV9zZWdtZW50KGFlcyh4ID0gMCwgeGVuZCA9IDEsIHkgPSAwLCB5ZW5kID0gMSksIHNpemUgPSAxLCBsaW5ldHlwZSA9ICdkYXNoZWQnKSArDQogICAgbGFicyh0aXRsZSA9IHBhc3RlMCgnUmVjb3ZlcnkgUmF0ZSBMb2dpc3RpYyBSZWdyZXNzaW9uIE1vZGVsXG5ST0Mgb2YgVmFsaWRhdGlvbiBTZXRcbkFVQyA9ICcNCiAgICAgICAgICAgICAgICAgICAgICAgICxyb3VuZChsb2dfbW9kZWxfcm9jJGF1YywgNCkpKSArDQogICAgdGhlbWVfYncoKQ0KDQpgYGANCg0KTm93IGxldCdzIHRha2Ugcm93cyB3aXRoICpyZWNvdmVyeV9yYXRlKiA+IDAuIFRoZXJlIGFyZSAyNDM3MSBvYnNlcnZhdGlvbnMgaW4gdGhlIGZpbHRlcmVkIGRhdGFzZXQuDQpgYGB7ciByZWNvdmVyeV9zdWJzZXR9DQpkYXRhX3N0YWdlXzIgPC0gZGF0YSAlPiUNCiAgZmlsdGVyKHJlY292ZXJ5X3JhdGUgPiAwKQ0KDQpkYXRhX3N0YWdlXzIgJT4lIGRpbSgpDQpgYGANCg0KRm9yIHRob3NlIHdlIGZpdCBsaW5lYXIgcmVncmVzc2lvbiBtb2RlbC4NCmBgYHtyIGxpbl9tb2RlbF8xfQ0KbGluX21vZGVsXzEgPC0gbG0oDQogIHJlY292ZXJ5X3JhdGUgfi4NCiAgLGRhdGEgPSBkYXRhX3N0YWdlXzIgJT4lIHNlbGVjdCgtZ29vZF9iYWQsIC1yZWNvdmVyeV9yYXRlXzBfMSwgLUNDRikNCiAgLHN1YnNldCA9IHRyYWluX2luZGV4DQopDQpgYGANCg0KQ2FsY3VsYXRlICoqYWRqdXN0ZWQgbXVsdGl2YXJpYXRlIHAtdmFsdWVzKiogdXNpbmcgKnAuYWRqdXN0KCkqIGZ1bmN0aW9uLiBCYXNlZCBvbiB0aGVzZSB3ZSBleGNsdWRlIGZyb20gdGhlIGxpbmVhciByZWdyZXNzaW9uIG1vZGVsIHRoZSBmb2xsb3dpbmcgdmFyaWFibGVzOiBcDQogLSBjYXRlZ29yaWFsOiBob21lX29ud2Vyc2hpcCwgYWRkcl9zdGF0ZSwgcHVycG9zZSBcDQogLSBjb250aW51b3VzOiBlbXBfbGVuZ3RoLCBtb250aHNfc2luY2VfZWFybGllc3RfY3JfbGluZSwgZGVsaW5xXzJ5cnMsIGlucV9sYXN0XzZtdGhzLCBwdWJfcmVjLCBhY2Nfbm93X2RlbGlucSwgYW5udWFsX2luYywgbXRoc19zaW5jZV9sYXN0X2RlbGlucSwgZHRpLCBtdGhzX3NpbmNlX2xhc3RfcmVjb3JkLg0KYGBge3IgbGluX21vZGVsX3BfYWRqdXN0fQ0KbGluX3BfdmFsdWVzX211bHRpIDwtIGxpbl9tb2RlbF8xICU+JQ0KICBzdW1tYXJ5KCkgJT4lDQogIGNvZWYoKSAlPiUNCiAgLlssNF0gJT4lIA0KICBwLmFkanVzdCgpDQoNCmxpbl9wX3ZhbHVlc19tdWx0aSAlPiUgcm91bmQoNSkgJT4lIGFzLmRhdGEuZnJhbWUoKQ0KYGBgDQoNCkZvciB0aG9zZSB3ZSBmaXQgbGluZWFyIHJlZ3Jlc3Npb24gbW9kZWwuIFdlIG9idGFpbiBwcmV0dHkgc2ltcGxlIG1vZGVsLCBidXQgd2l0aCB2ZXJ5IHBvb3IgJFJeMiQuDQpgYGB7ciBsaW5fbW9kZWxfMn0NCmxpbl9tb2RlbF8yIDwtIGxtKA0KICByZWNvdmVyeV9yYXRlIH4uDQogICxkYXRhID0gZGF0YSAlPiUgc2VsZWN0KA0KICAgIC1nb29kX2JhZCwgLXJlY292ZXJ5X3JhdGVfMF8xLCAtQ0NGDQogICAgLC1zdGFydHNfd2l0aCgnaG9tZV9vd25lcnNoaXAnKSwgLXN0YXJ0c193aXRoKCdwdXJwb3NlJyksIC1zdGFydHNfd2l0aCgnYWRkcl9zdGF0ZScpDQogICAgLC1lbXBfbGVuZ3RoLCAtbW9udGhzX3NpbmNlX2VhcmxpZXN0X2NyX2xpbmUsIC1kZWxpbnFfMnlycywgLWlucV9sYXN0XzZtdGhzLCAtcHViX3JlYw0KICAgICwtYWNjX25vd19kZWxpbnEsIC1hbm51YWxfaW5jLCAtbXRoc19zaW5jZV9sYXN0X2RlbGlucSwgLWR0aSwgLW10aHNfc2luY2VfbGFzdF9yZWNvcmQNCiAgKQ0KICAsc3Vic2V0ID0gdHJhaW5faW5kZXgNCikNCg0KbGluX21vZGVsXzIgJT4lIHN1bW1hcnkoKQ0KYGBgDQoNCkFzIHdlIHNlZSBiZWxvdyByZXNpZHVhbHMgb2YgdGhlIGxpbmVhciByZWdyZXNzaW9uIG1vZGVsIGFpbid0IGJlaGF2ZSB3ZWxsLiBUaGVzZSBhcmUgbm90IG5vcm1hbGx5IGRpc3RyaWJ1dGVkIGFuZCBoZXRlcm9zY2VkYXN0aWMuIEluIGFub3RoZXIgYmxvZyBwb3N0IHdlIHdpbGwgZXN0aW1hdGUgKipiZXRhIHJlZ3Jlc3Npb24qKiBpbnN0ZWFkIG9mIHR3byBzdGFnZWQgbG9naXN0aWMgYW5kIGxpZW5hciByZWdyZXNzaW9uLg0KYGBge3IgcmVzaWR1YWxzfQ0KcGFyKG1mcm93ID0gYygyLDIpKQ0KbGluX21vZGVsXzIgJT4lIHBsb3Qod2hpY2ggPSAxOjMpDQphY2YobGluX21vZGVsXzIkcmVzaWR1YWxzLCBtYWluID0gJycpDQpgYGANCg0KRmluYWxseSwgaW4gb3JkZXIgdG8gcHJlZGljdCByZWNvdmVyeSByYXRlIGEgZ2l2ZW4gbG9hbiBmaXJzdCB3ZSBoYXZlIHRvIGRlY2lkZSB3aGV0aGVyIHJlY292ZXJ5IHJhdGUgaXMgMCBvciBhYm92ZSAwIHVzaW5nIGxvZ2lzdGljIHJlZ3Jlc3Npb24gbW9kZWwgYW5kIGlmIGl0J3MgYWJvdmUgMCB3ZSBwcmVkaWN0IGl0IHdpdGggbGluZWFyIHJlZ3Jlc3Npb24gbW9kZWwuIFRvIGRvIHRoYXQgd2Ugc2ltcGx5IG11bHRpcGx5IHByZWRpY3RlZCB2YWx1ZXMgZnJvbSBib3RoIG1vZGVscy4gTGV0J3MgdGFrZSBhIHRocmVzaG9sZCBvZiAwLjUgZm9yIHByZWRpY3Rpb25zIGZyb20gbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbC4NCmBgYHtyIGNvbWJpbmV9DQpsaW5fcHJlZGljdCA8LSBwcmVkaWN0KGxpbl9tb2RlbF8yDQogICAgICAgICAgICAgICAgICAgICAgICxkYXRhWy10cmFpbl9pbmRleCxdKQ0KDQpsb2dfcHJlZGljdF9jbGFzcyA8LSBpZl9lbHNlKGxvZ19wcmVkaWN0X3Byb2IgPiAwLjUsIDAsIDEpDQoNCnJlY292ZXJ5X3JhdGVfc3VtbWFyeSA9IGRhdGEuZnJhbWUoDQogIEFjdHVhbCA9IGRhdGEkcmVjb3ZlcnlfcmF0ZVstdHJhaW5faW5kZXhdDQogICxQcmVkaWN0ZWQgPSBsb2dfcHJlZGljdF9jbGFzcyAqIGxpbl9wcmVkaWN0DQopICU+JQ0KICBwaXZvdF9sb25nZXIoY29scyA9IDE6MiwgbmFtZXNfdG8gPSAnUmVjb3ZlcnkgUmF0ZScsIHZhbHVlc190byA9ICdWYWx1ZScpDQoNCmdncGxvdChyZWNvdmVyeV9yYXRlX3N1bW1hcnksIGFlcyhWYWx1ZSwgZmlsbCA9IGBSZWNvdmVyeSBSYXRlYCkpICsNCiAgZ2VvbV9kZW5zaXR5KGFscGhhPSAwLjUpICsNCiAgdGhlbWVfYncoKSArDQogIGxhYnMoeCA9ICcnLCB5ID0nJywgdGl0bGUgPSAnRGVuc2l0aWVzIG9mIEFjdHVhbCBhbmQgUHJlZGljdGVkIFJlY292ZXJ5IFJhdGVzJykNCmBgYA==