Data used and results
Trước hết train và đánh giá một loạt Machine Learning Classifiers. Kết quả cho thấy XGBoost có trung bình Recall (n_splits=3, n_repeats=4) là 0.7176:
# Hide warnings from Python:
import warnings
warnings.simplefilter(action="ignore", category=FutureWarning)
# Load data:
import pandas as pd
df = pd.read_csv("http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv")
# Convert categories to dummies:
df = pd.get_dummies(df)
# Impute missing data (https://academic.oup.com/bioinformatics/article/28/1/112/219101,
# https://academic.oup.com/aje/article/179/6/764/107562,
# https://github.com/epsilon-machine/missingpy):
from missingpy import MissForest
imputer = MissForest()
df_imputed = imputer.fit_transform(df)
# Convert to data frame:
df_imputed = pd.DataFrame(df_imputed)
# Rename for columns:
df_imputed.columns = df.columns
# Prepare data:
X = df_imputed.drop(labels=["BAD"], axis=1)
Y = df_imputed["BAD"]
# Standardize 0-1 for features:
from sklearn.preprocessing import MinMaxScaler
scaler_01 = MinMaxScaler()
scaler_01.fit(X)
X_scaler = scaler_01.transform(X)
# Some classifiers from Scikit-learn:
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import BaggingClassifier
from sklearn.neural_network import MLPClassifier
# LightGBM, Catboost and XGBoost:
from lightgbm import LGBMClassifier
from xgboost import XGBClassifier
from catboost import CatBoostClassifier
# Initiative estimators:
ran = RandomForestClassifier()
gbm = LGBMClassifier()
log = LogisticRegression()
gbc = GradientBoostingClassifier()
xgb = XGBClassifier()
ext = ExtraTreesClassifier()
ada = AdaBoostClassifier()
gnb = GaussianNB()
bag = BaggingClassifier()
nnn = MLPClassifier()
cat = CatBoostClassifier()
# List of classifiers:
models = [ran, gbm, log, gbc, xgb, ext, ada, gnb, bag, nnn, cat]
# Train all classifiers by using for loop:
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import cross_val_score
cv = RepeatedStratifiedKFold(n_splits=3, n_repeats=4, random_state=29)
recall_mean = []
recall_sd = []
auc_mean = []
auc_sd = []
import numpy as np
for mod in models:
acc = cross_val_score(mod, X_scaler, Y, scoring="recall", cv=cv, n_jobs=-1, verbose=False)
auc = cross_val_score(mod, X_scaler, Y, scoring="roc_auc", cv=cv, n_jobs=-1, verbose=False)
# Recall metric:
recall_mean.append(acc.mean())
recall_sd.append(np.std(acc))
# AUC metric:
auc_mean.append(auc.mean())
auc_sd.append(np.std(auc))
df_results = pd.DataFrame({"Classifier": [j.__class__.__name__ for j in models],
"Recall_mean": recall_mean,
"Recall_sd": recall_sd,
"AUC_mean": auc_mean,
"AUC_sd": auc_sd})
# Report results:
print(df_results)
Chúng ta có thể tinh chỉnh tham số theo Bayesian Optimization để cho Recall tăng lên 0.7400 (mức tăng 3% - khá là khiêm tốn) như sau:
# Define the space of parameters to search:
from skopt.space import Integer
from skopt.space import Real
from skopt.space import Categorical
from skopt.utils import use_named_args
from skopt import gp_minimize
# Space of parameters:
search_space = list()
search_space.append(Categorical(["binary:logistic"], name="objective"))
search_space.append(Integer(2, 20, name="max_depth"))
search_space.append(Integer(2, 20, name="min_child_weight"))
search_space.append(Integer(10, 1000, name="n_estimators"))
search_space.append(Real(1e-3, 100, "log-uniform", name="learning_rate"))
search_space.append(Real(1e-2, 100, "log-uniform", name="eta"))
search_space.append(Real(0.05, 0.8, name="gamma"))
search_space.append(Real(0.1, 0.9, name="subsample"))
search_space.append(Real(0.5, 1, name="colsample_bytree"))
# Define the function used to evaluate a given configuration:
@use_named_args(search_space)
def evaluate_model(**params):
model = XGBClassifier()
model.set_params(**params)
result = cross_val_score(model, X_scaler, Y, cv=cv, n_jobs=-1, scoring="recall")
estimate = result.mean()
return -1 * estimate
# Perform optimization:
result_opt = gp_minimize(func=evaluate_model, dimensions=search_space, random_state=29, verbose=True)
# Report findings:
print("Best Recall: %.3f" % (-1 * result_opt.fun))
print("Best Parameters: %s" % result_opt.x)
Huấn luyện lại XGBoost với tham số tối ưu tìm được. Recall trên test data là 0.7580:
# Best XGB:
best_param = result_opt.x
best_xgb = XGBClassifier(objective=str(best_param[0]),
max_depth=int(best_param[1]),
min_child_weight=int(best_param[2]),
n_estimators=int(best_param[3]),
learning_rate=float(best_param[4]),
eta=float(best_param[5]),
gamma=float(best_param[6]),
subsample=float(best_param[7]),
colsample_bytree=float(best_param[8]),
random_state=29,
n_jobs=-1)
# Split data:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_scaler, Y, test_size=0.3, random_state=1)
# Train best XGBClassifier:
best_xgb.fit(X_train, y_train)
pred = best_xgb.predict(X_test)
# Recall metrics:
from sklearn.metrics import recall_score
recall_score(y_test, pred)
# CM metrics:
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, pred)
print(cm)
LS0tDQp0aXRsZTogJ0JheWVzaWFuIE9wdGltaXphdGlvbiBmb3Igc2VhcmNoaW5nIG9wdGltYWwgUmVjYWxsIGZvciBYR0Jvb3N0IENsYXNzaWZpZXIgKFB5dGhvbiknDQphdXRob3I6ICdBdXRob3I6IE5ndXllbiBDaGkgRHVuZycNCnN1YnRpdGxlOiAiUHl0aG9uIE1hY2hpbmUgTGVhcm5pbmcgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGNhY2hlID0gVFJVRSwgZXZhbCA9IEZBTFNFKQ0KDQpgYGANCg0KDQojIE1vdGl2YXRpb25zDQoNClRpbmggY2jhu4luaCB0aGFtIHPhu5EgxJHhu4MgdMOsbSB0aGFtIHPhu5EgdOG7kWkgxrB1IGNobyBtw7QgaMOsbmggbMOgIGPDtG5nIHZp4buHYyB04buRbiB0aOG7nWkgZ2lhbiB2w6AgbuG6t25nIG5o4buNYy4gQmF5ZXNpYW4gT3B0aW1pemF0aW9uIGzDoCBt4buZdCBjw6FjaCB0aeG6v3AgY+G6rW4gaGnhu4d1IHF14bqjIMSR4buDIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgY2hvIGPDoWMgbcO0IGjDrG5oIE1hY2hpbmUgTGVhcm5pbmcuIA0KDQoNCiMgRGF0YSB1c2VkIGFuZCByZXN1bHRzDQoNClRyxrDhu5tjIGjhur90IHRyYWluIHbDoCDEkcOhbmggZ2nDoSBt4buZdCBsb+G6oXQgTWFjaGluZSBMZWFybmluZyBDbGFzc2lmaWVycy4gS+G6v3QgcXXhuqMgY2hvIHRo4bqleSBYR0Jvb3N0IGPDsyB0cnVuZyBiw6xuaCBSZWNhbGwgKG5fc3BsaXRzPTMsIG5fcmVwZWF0cz00KSBsw6AgMC43MTc2OiANCg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCiMgSGlkZSB3YXJuaW5ncyBmcm9tIFB5dGhvbjogDQoNCmltcG9ydCB3YXJuaW5ncw0Kd2FybmluZ3Muc2ltcGxlZmlsdGVyKGFjdGlvbj0iaWdub3JlIiwgY2F0ZWdvcnk9RnV0dXJlV2FybmluZykNCg0KIyBMb2FkIGRhdGE6DQppbXBvcnQgcGFuZGFzIGFzIHBkDQpkZiA9IHBkLnJlYWRfY3N2KCJodHRwOi8vd3d3LmNyZWRpdHJpc2thbmFseXRpY3MubmV0L3VwbG9hZHMvMS85LzUvMS8xOTUxMTYwMS9obWVxLmNzdiIpDQoNCiMgQ29udmVydCBjYXRlZ29yaWVzIHRvIGR1bW1pZXM6DQpkZiA9IHBkLmdldF9kdW1taWVzKGRmKQ0KDQojIEltcHV0ZSBtaXNzaW5nIGRhdGEgKGh0dHBzOi8vYWNhZGVtaWMub3VwLmNvbS9iaW9pbmZvcm1hdGljcy9hcnRpY2xlLzI4LzEvMTEyLzIxOTEwMSwNCiMgICAgICAgICAgICAgICAgICAgICAgaHR0cHM6Ly9hY2FkZW1pYy5vdXAuY29tL2FqZS9hcnRpY2xlLzE3OS82Lzc2NC8xMDc1NjIsDQojICAgICAgICAgICAgICAgICAgICAgIGh0dHBzOi8vZ2l0aHViLmNvbS9lcHNpbG9uLW1hY2hpbmUvbWlzc2luZ3B5KToNCg0KZnJvbSBtaXNzaW5ncHkgaW1wb3J0IE1pc3NGb3Jlc3QNCmltcHV0ZXIgPSBNaXNzRm9yZXN0KCkNCmRmX2ltcHV0ZWQgPSBpbXB1dGVyLmZpdF90cmFuc2Zvcm0oZGYpDQoNCiMgQ29udmVydCB0byBkYXRhIGZyYW1lOg0KZGZfaW1wdXRlZCA9IHBkLkRhdGFGcmFtZShkZl9pbXB1dGVkKQ0KDQojIFJlbmFtZSBmb3IgY29sdW1uczoNCmRmX2ltcHV0ZWQuY29sdW1ucyA9IGRmLmNvbHVtbnMNCg0KIyBQcmVwYXJlIGRhdGE6DQpYID0gZGZfaW1wdXRlZC5kcm9wKGxhYmVscz1bIkJBRCJdLCBheGlzPTEpDQpZID0gZGZfaW1wdXRlZFsiQkFEIl0NCg0KIyBTdGFuZGFyZGl6ZSAwLTEgZm9yIGZlYXR1cmVzOg0KZnJvbSBza2xlYXJuLnByZXByb2Nlc3NpbmcgaW1wb3J0IE1pbk1heFNjYWxlcg0Kc2NhbGVyXzAxID0gTWluTWF4U2NhbGVyKCkNCnNjYWxlcl8wMS5maXQoWCkNClhfc2NhbGVyID0gc2NhbGVyXzAxLnRyYW5zZm9ybShYKQ0KDQojIFNvbWUgY2xhc3NpZmllcnMgZnJvbSBTY2lraXQtbGVhcm46DQpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IFJhbmRvbUZvcmVzdENsYXNzaWZpZXINCmZyb20gc2tsZWFybi5saW5lYXJfbW9kZWwgaW1wb3J0IExvZ2lzdGljUmVncmVzc2lvbg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBHcmFkaWVudEJvb3N0aW5nQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBFeHRyYVRyZWVzQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBBZGFCb29zdENsYXNzaWZpZXINCmZyb20gc2tsZWFybi5uYWl2ZV9iYXllcyBpbXBvcnQgR2F1c3NpYW5OQg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBCYWdnaW5nQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLm5ldXJhbF9uZXR3b3JrIGltcG9ydCBNTFBDbGFzc2lmaWVyDQoNCiMgTGlnaHRHQk0sIENhdGJvb3N0IGFuZCBYR0Jvb3N0Og0KZnJvbSBsaWdodGdibSBpbXBvcnQgTEdCTUNsYXNzaWZpZXINCmZyb20geGdib29zdCBpbXBvcnQgWEdCQ2xhc3NpZmllcg0KZnJvbSBjYXRib29zdCBpbXBvcnQgQ2F0Qm9vc3RDbGFzc2lmaWVyDQoNCiMgSW5pdGlhdGl2ZSBlc3RpbWF0b3JzOg0KcmFuID0gUmFuZG9tRm9yZXN0Q2xhc3NpZmllcigpDQpnYm0gPSBMR0JNQ2xhc3NpZmllcigpDQpsb2cgPSBMb2dpc3RpY1JlZ3Jlc3Npb24oKQ0KZ2JjID0gR3JhZGllbnRCb29zdGluZ0NsYXNzaWZpZXIoKQ0KeGdiID0gWEdCQ2xhc3NpZmllcigpDQpleHQgPSBFeHRyYVRyZWVzQ2xhc3NpZmllcigpDQphZGEgPSBBZGFCb29zdENsYXNzaWZpZXIoKQ0KZ25iID0gR2F1c3NpYW5OQigpDQpiYWcgPSBCYWdnaW5nQ2xhc3NpZmllcigpDQpubm4gPSBNTFBDbGFzc2lmaWVyKCkNCmNhdCA9IENhdEJvb3N0Q2xhc3NpZmllcigpDQoNCiMgTGlzdCBvZiBjbGFzc2lmaWVyczoNCm1vZGVscyA9IFtyYW4sIGdibSwgbG9nLCBnYmMsIHhnYiwgZXh0LCBhZGEsIGduYiwgYmFnLCBubm4sIGNhdF0NCg0KIyBUcmFpbiBhbGwgY2xhc3NpZmllcnMgYnkgdXNpbmcgZm9yIGxvb3A6DQpmcm9tIHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uIGltcG9ydCBSZXBlYXRlZFN0cmF0aWZpZWRLRm9sZA0KZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgY3Jvc3NfdmFsX3Njb3JlDQpjdiA9IFJlcGVhdGVkU3RyYXRpZmllZEtGb2xkKG5fc3BsaXRzPTMsIG5fcmVwZWF0cz00LCByYW5kb21fc3RhdGU9MjkpDQoNCnJlY2FsbF9tZWFuID0gW10NCnJlY2FsbF9zZCA9IFtdDQphdWNfbWVhbiA9IFtdDQphdWNfc2QgPSBbXQ0KDQppbXBvcnQgbnVtcHkgYXMgbnANCg0KZm9yIG1vZCBpbiBtb2RlbHM6DQogICAgYWNjID0gY3Jvc3NfdmFsX3Njb3JlKG1vZCwgWF9zY2FsZXIsIFksIHNjb3Jpbmc9InJlY2FsbCIsIGN2PWN2LCBuX2pvYnM9LTEsIHZlcmJvc2U9RmFsc2UpDQogICAgYXVjID0gY3Jvc3NfdmFsX3Njb3JlKG1vZCwgWF9zY2FsZXIsIFksIHNjb3Jpbmc9InJvY19hdWMiLCBjdj1jdiwgbl9qb2JzPS0xLCB2ZXJib3NlPUZhbHNlKQ0KICAgICMgUmVjYWxsIG1ldHJpYzoNCiAgICByZWNhbGxfbWVhbi5hcHBlbmQoYWNjLm1lYW4oKSkNCiAgICByZWNhbGxfc2QuYXBwZW5kKG5wLnN0ZChhY2MpKQ0KICAgICMgQVVDIG1ldHJpYzoNCiAgICBhdWNfbWVhbi5hcHBlbmQoYXVjLm1lYW4oKSkNCiAgICBhdWNfc2QuYXBwZW5kKG5wLnN0ZChhdWMpKQ0KDQpkZl9yZXN1bHRzID0gcGQuRGF0YUZyYW1lKHsiQ2xhc3NpZmllciI6IFtqLl9fY2xhc3NfXy5fX25hbWVfXyBmb3IgaiBpbiBtb2RlbHNdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIlJlY2FsbF9tZWFuIjogcmVjYWxsX21lYW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVjYWxsX3NkIjogcmVjYWxsX3NkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFVQ19tZWFuIjogYXVjX21lYW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiQVVDX3NkIjogYXVjX3NkfSkNCg0KIyBSZXBvcnQgcmVzdWx0czoNCnByaW50KGRmX3Jlc3VsdHMpDQpgYGANCg0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdGhlbyBCYXllc2lhbiBPcHRpbWl6YXRpb24gxJHhu4MgY2hvIFJlY2FsbCB0xINuZyBsw6puIDAuNzQwMCAobeG7qWMgdMSDbmcgMyUgLSBraMOhIGzDoCBraGnDqm0gdOG7kW4pIG5oxrAgc2F1OiANCg0KYGBge3J9DQoNCiMgRGVmaW5lIHRoZSBzcGFjZSBvZiBwYXJhbWV0ZXJzIHRvIHNlYXJjaDoNCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IEludGVnZXINCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IFJlYWwNCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IENhdGVnb3JpY2FsDQpmcm9tIHNrb3B0LnV0aWxzIGltcG9ydCB1c2VfbmFtZWRfYXJncw0KZnJvbSBza29wdCBpbXBvcnQgZ3BfbWluaW1pemUNCg0KIyBTcGFjZSBvZiBwYXJhbWV0ZXJzOg0Kc2VhcmNoX3NwYWNlID0gbGlzdCgpDQpzZWFyY2hfc3BhY2UuYXBwZW5kKENhdGVnb3JpY2FsKFsiYmluYXJ5OmxvZ2lzdGljIl0sIG5hbWU9Im9iamVjdGl2ZSIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChJbnRlZ2VyKDIsIDIwLCBuYW1lPSJtYXhfZGVwdGgiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoSW50ZWdlcigyLCAyMCwgbmFtZT0ibWluX2NoaWxkX3dlaWdodCIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChJbnRlZ2VyKDEwLCAxMDAwLCBuYW1lPSJuX2VzdGltYXRvcnMiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgxZS0zLCAxMDAsICJsb2ctdW5pZm9ybSIsIG5hbWU9ImxlYXJuaW5nX3JhdGUiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgxZS0yLCAxMDAsICJsb2ctdW5pZm9ybSIsIG5hbWU9ImV0YSIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChSZWFsKDAuMDUsIDAuOCwgbmFtZT0iZ2FtbWEiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgwLjEsIDAuOSwgbmFtZT0ic3Vic2FtcGxlIikpDQpzZWFyY2hfc3BhY2UuYXBwZW5kKFJlYWwoMC41LCAxLCBuYW1lPSJjb2xzYW1wbGVfYnl0cmVlIikpDQoNCiMgRGVmaW5lIHRoZSBmdW5jdGlvbiB1c2VkIHRvIGV2YWx1YXRlIGEgZ2l2ZW4gY29uZmlndXJhdGlvbjoNCkB1c2VfbmFtZWRfYXJncyhzZWFyY2hfc3BhY2UpDQpkZWYgZXZhbHVhdGVfbW9kZWwoKipwYXJhbXMpOg0KICAgIG1vZGVsID0gWEdCQ2xhc3NpZmllcigpDQogICAgbW9kZWwuc2V0X3BhcmFtcygqKnBhcmFtcykNCiAgICByZXN1bHQgPSBjcm9zc192YWxfc2NvcmUobW9kZWwsIFhfc2NhbGVyLCBZLCBjdj1jdiwgbl9qb2JzPS0xLCBzY29yaW5nPSJyZWNhbGwiKQ0KICAgIGVzdGltYXRlID0gcmVzdWx0Lm1lYW4oKQ0KICAgIHJldHVybiAtMSAqIGVzdGltYXRlDQoNCiMgUGVyZm9ybSBvcHRpbWl6YXRpb246DQpyZXN1bHRfb3B0ID0gZ3BfbWluaW1pemUoZnVuYz1ldmFsdWF0ZV9tb2RlbCwgZGltZW5zaW9ucz1zZWFyY2hfc3BhY2UsIHJhbmRvbV9zdGF0ZT0yOSwgdmVyYm9zZT1UcnVlKQ0KDQojIFJlcG9ydCBmaW5kaW5nczoNCnByaW50KCJCZXN0IFJlY2FsbDogJS4zZiIgJSAoLTEgKiByZXN1bHRfb3B0LmZ1bikpDQpwcmludCgiQmVzdCBQYXJhbWV0ZXJzOiAlcyIgJSByZXN1bHRfb3B0LngpDQpgYGANCg0KSHXhuqVuIGx1eeG7h24gbOG6oWkgWEdCb29zdCB24bubaSB0aGFtIHPhu5EgdOG7kWkgxrB1IHTDrG0gxJHGsOG7o2MuIFJlY2FsbCB0csOqbiB0ZXN0IGRhdGEgbMOgIDAuNzU4MDogDQoNCg0KYGBge3J9DQojIEJlc3QgWEdCOg0KYmVzdF9wYXJhbSA9IHJlc3VsdF9vcHQueA0KYmVzdF94Z2IgPSBYR0JDbGFzc2lmaWVyKG9iamVjdGl2ZT1zdHIoYmVzdF9wYXJhbVswXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgbWF4X2RlcHRoPWludChiZXN0X3BhcmFtWzFdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0PWludChiZXN0X3BhcmFtWzJdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBuX2VzdGltYXRvcnM9aW50KGJlc3RfcGFyYW1bM10pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGxlYXJuaW5nX3JhdGU9ZmxvYXQoYmVzdF9wYXJhbVs0XSksDQogICAgICAgICAgICAgICAgICAgICAgICAgZXRhPWZsb2F0KGJlc3RfcGFyYW1bNV0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGdhbW1hPWZsb2F0KGJlc3RfcGFyYW1bNl0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnNhbXBsZT1mbG9hdChiZXN0X3BhcmFtWzddKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzYW1wbGVfYnl0cmVlPWZsb2F0KGJlc3RfcGFyYW1bOF0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHJhbmRvbV9zdGF0ZT0yOSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBuX2pvYnM9LTEpDQoNCiMgU3BsaXQgZGF0YToNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IHRyYWluX3Rlc3Rfc3BsaXQNClhfdHJhaW4sIFhfdGVzdCwgeV90cmFpbiwgeV90ZXN0ID0gdHJhaW5fdGVzdF9zcGxpdChYX3NjYWxlciwgWSwgdGVzdF9zaXplPTAuMywgcmFuZG9tX3N0YXRlPTEpDQoNCiMgVHJhaW4gYmVzdCBYR0JDbGFzc2lmaWVyOg0KYmVzdF94Z2IuZml0KFhfdHJhaW4sIHlfdHJhaW4pDQpwcmVkID0gYmVzdF94Z2IucHJlZGljdChYX3Rlc3QpDQoNCiMgUmVjYWxsIG1ldHJpY3M6DQpmcm9tIHNrbGVhcm4ubWV0cmljcyBpbXBvcnQgcmVjYWxsX3Njb3JlDQpyZWNhbGxfc2NvcmUoeV90ZXN0LCBwcmVkKQ0KDQojIENNIG1ldHJpY3M6DQpmcm9tIHNrbGVhcm4ubWV0cmljcyBpbXBvcnQgY29uZnVzaW9uX21hdHJpeA0KY20gPSBjb25mdXNpb25fbWF0cml4KHlfdGVzdCwgcHJlZCkNCnByaW50KGNtKQ0KYGBgDQoNCg0KDQo=