Motivations

Tinh chỉnh các tham số để tối ưu hóa một tiêu chuẩn truyền thống (ROC-AUC, Recall), lựa chọn các mô hình thì nên dựa vào các tiêu chuẩn này hay chúng chỉ đóng vai trò tham khảo mà thôi? Thử nghiệm dưới đây sẽ làm sáng tỏ một phần nhằm tìm kiếm câu trả lời thích hợp cho những câu hỏi trên.

Findings

Thử nghiệm với bộ số liệu GermanCredit.csv về cấp tín dụng của một ngân hàng đại Đức (dữ liệu có thể download tại đây) thì GaussianNB, Random Forest và CatBoostClassifier có Recall trung bình (n_splits = 4, n_repeats = 3) lần lượt là 0.567044, 0.347706, và 0.413778. Còn AUC trung bình của ba Classifiers lần lượt là 0.739503, 0.785501 và 0.792102. Python codes cho các kết quả này (ngoài 3 mô hình nêu tên ở trên còn khảo sát đồng thời 9 mô hình Machine Learning khác):

# Load data and conduct data pre-processing:
import pandas as pd

df_bank = pd.read_csv("C:/Users/ADMIN/Desktop/DataMining/dmba/GermanCredit.csv")
df_bank["RESPONSE"] = df_bank["RESPONSE"].map({1: 0, 0: 1})

# Drop OBS# feature:
my_df_binary = df_bank.drop(["OBS#"], axis=1)

# Define input features and target output:
Y = my_df_binary["RESPONSE"]
X = my_df_binary.drop("RESPONSE", axis=1)

# Prepare data:
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=29)

# 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.gaussian_process import GaussianProcessClassifier
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

# Initative estimators:
ran = RandomForestClassifier(random_state=9)
gbm = LGBMClassifier()
log = LogisticRegression()
gbc = GradientBoostingClassifier()
xgb = XGBClassifier()
ext = ExtraTreesClassifier()
ada = AdaBoostClassifier()
gnb = GaussianNB()
gpc = GaussianProcessClassifier()
bag = BaggingClassifier()
nnn = MLPClassifier()
cat = CatBoostClassifier()

# List of classifiers:
models = [ran, gbm, log, gbc, xgb, ext, ada, gnb, gpc, bag, nnn, cat]

# Train all classifiers:
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import cross_val_score

cv = RepeatedStratifiedKFold(n_splits=4, n_repeats=10, random_state=29)

# Cross-validation results:

import numpy as np

recall_mean = []
recall_sd = []
auc_mean = []
auc_sd = []

for mod in models:
    acc = cross_val_score(mod, X_train, y_train, scoring="recall", cv=cv, verbose=False, n_jobs=-1)
    auc = cross_val_score(mod, X_train, y_train, scoring="roc_auc", cv=cv, verbose=False, n_jobs=-1)
    # 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))

# Convert results in form of pandas frame:
df_results = pd.DataFrame({"Model": [j.__class__.__name__ for j in models],
                           "Recall_mean": recall_mean,
                           "Recall_sd": recall_sd,
                           "AUC_mean": auc_mean,
                           "AUC_sd": auc_sd})

df_results = df_results.sort_values(by="Recall_mean", ascending=False).reset_index(drop=True)

# Show results:
print(df_results)

Huấn luyện lại ba mô hình này trên train data và tính AUC trên test data thì kết quả lần lượt là 0.8085, 0.8150 và 0.8202:

Profit nếu sử dụng ba mô hình này khi ngưỡng cho phân loại thay đổi nếu lãi suất là 10% (Figure 1):

Conclusion

Figure 1 chỉ ra rằng với một tổ chức hướng đến mục tiêu tối đa hóa lợi nhuận thì:

  1. Tiêu chuẩn AUC quan trọng hơn Recall và do vậy AUC nên được coi là điều kiện cần đầu tiên khi lựa chọn mô hình nhằm mục đích tối đa hóa lợi nhuận. Thực vậy, Recall trung bình của GaussianNB cao hơn của Random Forest tới 63% nhưng nếu sử dụng GaussianNB thì mô hình này tạo la lợi nhuận tệ nhất tại phần lớn ngưỡng được chọn.

  2. Mặc dù lợi nhuận cực đại nếu sử dụng CatBoost cao hơn lợi nhuận cực đại nếu sử dụng Random Forest nhưng rõ ràng là biến động về lợi nhuận cũng cao hơn (đồng nghĩa với bất ổn/rủi ro cao hơn). Do vậy dù Random Forest không phải là mô hình tạo ra lợi nhuận cực đại nhưng mô hình này nên được lựa chọn để làm cơ sở xét cho Credit Scoring.

LS0tDQp0aXRsZTogJ1Byb2ZpdCBDcml0ZXJpb24gZm9yIHNlbGVjdGluZyBNYWNoaW5lIExlYXJuaW5nIENsYXNzaWZpZXIgKFB5dGhvbiknDQphdXRob3I6ICdBdXRob3I6IE5ndXllbiBDaGkgRHVuZycNCnN1YnRpdGxlOiAiUHl0aG9uIE1hY2hpbmUgTGVhcm5pbmcgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGNhY2hlID0gVFJVRSwgZXZhbCA9IEZBTFNFKQ0KDQpgYGANCg0KDQoNCiMgTW90aXZhdGlvbnMNCg0KVGluaCBjaOG7iW5oIGPDoWMgdGhhbSBz4buRIMSR4buDIHThu5FpIMawdSBow7NhIG3hu5l0IHRpw6p1IGNodeG6qW4gdHJ1eeG7gW4gdGjhu5FuZyAoUk9DLUFVQywgUmVjYWxsKSwgbOG7sWEgY2jhu41uIGPDoWMgbcO0IGjDrG5oIHRow6wgbsOqbiBk4buxYSB2w6BvIGPDoWMgdGnDqnUgY2h14bqpbiBuw6B5IGhheSBjaMO6bmcgY2jhu4kgxJHDs25nIHZhaSB0csOyIHRoYW0ga2jhuqNvIG3DoCB0aMO0aT8gVGjhu60gbmdoaeG7h20gZMaw4bubaSDEkcOieSBz4bq9IGzDoG0gc8OhbmcgdOG7jyBt4buZdCBwaOG6p24gbmjhurFtIHTDrG0ga2nhur9tIGPDonUgdHLhuqMgbOG7nWkgdGjDrWNoIGjhu6NwIGNobyAgbmjhu69uZyBjw6J1IGjhu49pIHRyw6puLiANCg0KIyBGaW5kaW5ncw0KDQpUaOG7rSBuZ2hp4buHbSB24bubaSBi4buZIHPhu5EgbGnhu4d1ICoqR2VybWFuQ3JlZGl0LmNzdioqIHbhu4EgY+G6pXAgdMOtbiBk4bulbmcgY+G7p2EgbeG7mXQgbmfDom4gaMOgbmcgxJHhuqFpIMSQ4bupYyAoZOG7ryBsaeG7h3UgY8OzIHRo4buDIGRvd25sb2FkIFt04bqhaSDEkcOieV0oaHR0cHM6Ly93d3cuZGF0YW1pbmluZ2Jvb2suY29tL2Jvb2svci1lZGl0aW9uKSkgdGjDrCBHYXVzc2lhbk5CLCBSYW5kb20gRm9yZXN0IHbDoCBDYXRCb29zdENsYXNzaWZpZXIgY8OzIFJlY2FsbCB0cnVuZyBiw6xuaCAobl9zcGxpdHMgPSA0LCBuX3JlcGVhdHMgPSAzKSBs4bqnbiBsxrDhu6N0IGzDoCAwLjU2NzA0NCwgMC4zNDc3MDYsIHbDoCAwLjQxMzc3OC4gQ8OybiBBVUMgdHJ1bmcgYsOsbmggY+G7p2EgYmEgQ2xhc3NpZmllcnMgbOG6p24gbMaw4bujdCBsw6AgMC43Mzk1MDMsIDAuNzg1NTAxIHbDoCAwLjc5MjEwMi4gUHl0aG9uIGNvZGVzIGNobyBjw6FjIGvhur90IHF14bqjIG7DoHkgKG5nb8OgaSAzIG3DtCBow6xuaCBuw6p1IHTDqm4g4bufIHRyw6puIGPDsm4ga2jhuqNvIHPDoXQgxJHhu5NuZyB0aOG7nWkgOSBtw7QgaMOsbmggTWFjaGluZSBMZWFybmluZyBraMOhYyk6IA0KDQpgYGB7cn0NCiMgTG9hZCBkYXRhIGFuZCBjb25kdWN0IGRhdGEgcHJlLXByb2Nlc3Npbmc6DQppbXBvcnQgcGFuZGFzIGFzIHBkDQoNCmRmX2JhbmsgPSBwZC5yZWFkX2NzdigiQzovVXNlcnMvQURNSU4vRGVza3RvcC9EYXRhTWluaW5nL2RtYmEvR2VybWFuQ3JlZGl0LmNzdiIpDQpkZl9iYW5rWyJSRVNQT05TRSJdID0gZGZfYmFua1siUkVTUE9OU0UiXS5tYXAoezE6IDAsIDA6IDF9KQ0KDQojIERyb3AgT0JTIyBmZWF0dXJlOg0KbXlfZGZfYmluYXJ5ID0gZGZfYmFuay5kcm9wKFsiT0JTIyJdLCBheGlzPTEpDQoNCiMgRGVmaW5lIGlucHV0IGZlYXR1cmVzIGFuZCB0YXJnZXQgb3V0cHV0Og0KWSA9IG15X2RmX2JpbmFyeVsiUkVTUE9OU0UiXQ0KWCA9IG15X2RmX2JpbmFyeS5kcm9wKCJSRVNQT05TRSIsIGF4aXM9MSkNCg0KIyBQcmVwYXJlIGRhdGE6DQpmcm9tIHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uIGltcG9ydCB0cmFpbl90ZXN0X3NwbGl0DQoNClhfdHJhaW4sIFhfdGVzdCwgeV90cmFpbiwgeV90ZXN0ID0gdHJhaW5fdGVzdF9zcGxpdChYLCBZLCB0ZXN0X3NpemU9MC4yLCByYW5kb21fc3RhdGU9MjkpDQoNCiMgU29tZSBjbGFzc2lmaWVycyBmcm9tIFNjaWtpdC1sZWFybjoNCmZyb20gc2tsZWFybi5lbnNlbWJsZSBpbXBvcnQgUmFuZG9tRm9yZXN0Q2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmxpbmVhcl9tb2RlbCBpbXBvcnQgTG9naXN0aWNSZWdyZXNzaW9uDQpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IEdyYWRpZW50Qm9vc3RpbmdDbGFzc2lmaWVyDQpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IEV4dHJhVHJlZXNDbGFzc2lmaWVyDQpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IEFkYUJvb3N0Q2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmdhdXNzaWFuX3Byb2Nlc3MgaW1wb3J0IEdhdXNzaWFuUHJvY2Vzc0NsYXNzaWZpZXINCmZyb20gc2tsZWFybi5uYWl2ZV9iYXllcyBpbXBvcnQgR2F1c3NpYW5OQg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBCYWdnaW5nQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLm5ldXJhbF9uZXR3b3JrIGltcG9ydCBNTFBDbGFzc2lmaWVyDQoNCiMgTGlnaHRHQk0sIENhdGJvb3N0IGFuZCBYR0Jvb3N0Og0KZnJvbSBsaWdodGdibSBpbXBvcnQgTEdCTUNsYXNzaWZpZXINCmZyb20geGdib29zdCBpbXBvcnQgWEdCQ2xhc3NpZmllcg0KZnJvbSBjYXRib29zdCBpbXBvcnQgQ2F0Qm9vc3RDbGFzc2lmaWVyDQoNCiMgSW5pdGF0aXZlIGVzdGltYXRvcnM6DQpyYW4gPSBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyKHJhbmRvbV9zdGF0ZT05KQ0KZ2JtID0gTEdCTUNsYXNzaWZpZXIoKQ0KbG9nID0gTG9naXN0aWNSZWdyZXNzaW9uKCkNCmdiYyA9IEdyYWRpZW50Qm9vc3RpbmdDbGFzc2lmaWVyKCkNCnhnYiA9IFhHQkNsYXNzaWZpZXIoKQ0KZXh0ID0gRXh0cmFUcmVlc0NsYXNzaWZpZXIoKQ0KYWRhID0gQWRhQm9vc3RDbGFzc2lmaWVyKCkNCmduYiA9IEdhdXNzaWFuTkIoKQ0KZ3BjID0gR2F1c3NpYW5Qcm9jZXNzQ2xhc3NpZmllcigpDQpiYWcgPSBCYWdnaW5nQ2xhc3NpZmllcigpDQpubm4gPSBNTFBDbGFzc2lmaWVyKCkNCmNhdCA9IENhdEJvb3N0Q2xhc3NpZmllcigpDQoNCiMgTGlzdCBvZiBjbGFzc2lmaWVyczoNCm1vZGVscyA9IFtyYW4sIGdibSwgbG9nLCBnYmMsIHhnYiwgZXh0LCBhZGEsIGduYiwgZ3BjLCBiYWcsIG5ubiwgY2F0XQ0KDQojIFRyYWluIGFsbCBjbGFzc2lmaWVyczoNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IFJlcGVhdGVkU3RyYXRpZmllZEtGb2xkDQpmcm9tIHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uIGltcG9ydCBjcm9zc192YWxfc2NvcmUNCg0KY3YgPSBSZXBlYXRlZFN0cmF0aWZpZWRLRm9sZChuX3NwbGl0cz00LCBuX3JlcGVhdHM9MTAsIHJhbmRvbV9zdGF0ZT0yOSkNCg0KIyBDcm9zcy12YWxpZGF0aW9uIHJlc3VsdHM6DQoNCmltcG9ydCBudW1weSBhcyBucA0KDQpyZWNhbGxfbWVhbiA9IFtdDQpyZWNhbGxfc2QgPSBbXQ0KYXVjX21lYW4gPSBbXQ0KYXVjX3NkID0gW10NCg0KZm9yIG1vZCBpbiBtb2RlbHM6DQogICAgYWNjID0gY3Jvc3NfdmFsX3Njb3JlKG1vZCwgWF90cmFpbiwgeV90cmFpbiwgc2NvcmluZz0icmVjYWxsIiwgY3Y9Y3YsIHZlcmJvc2U9RmFsc2UsIG5fam9icz0tMSkNCiAgICBhdWMgPSBjcm9zc192YWxfc2NvcmUobW9kLCBYX3RyYWluLCB5X3RyYWluLCBzY29yaW5nPSJyb2NfYXVjIiwgY3Y9Y3YsIHZlcmJvc2U9RmFsc2UsIG5fam9icz0tMSkNCiAgICAjIFJlY2FsbCBtZXRyaWM6DQogICAgcmVjYWxsX21lYW4uYXBwZW5kKGFjYy5tZWFuKCkpDQogICAgcmVjYWxsX3NkLmFwcGVuZChucC5zdGQoYWNjKSkNCiAgICAjIEFVQyBtZXRyaWM6DQogICAgYXVjX21lYW4uYXBwZW5kKGF1Yy5tZWFuKCkpDQogICAgYXVjX3NkLmFwcGVuZChucC5zdGQoYXVjKSkNCg0KIyBDb252ZXJ0IHJlc3VsdHMgaW4gZm9ybSBvZiBwYW5kYXMgZnJhbWU6DQpkZl9yZXN1bHRzID0gcGQuRGF0YUZyYW1lKHsiTW9kZWwiOiBbai5fX2NsYXNzX18uX19uYW1lX18gZm9yIGogaW4gbW9kZWxzXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJSZWNhbGxfbWVhbiI6IHJlY2FsbF9tZWFuLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIlJlY2FsbF9zZCI6IHJlY2FsbF9zZCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJBVUNfbWVhbiI6IGF1Y19tZWFuLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFVQ19zZCI6IGF1Y19zZH0pDQoNCmRmX3Jlc3VsdHMgPSBkZl9yZXN1bHRzLnNvcnRfdmFsdWVzKGJ5PSJSZWNhbGxfbWVhbiIsIGFzY2VuZGluZz1GYWxzZSkucmVzZXRfaW5kZXgoZHJvcD1UcnVlKQ0KDQojIFNob3cgcmVzdWx0czoNCnByaW50KGRmX3Jlc3VsdHMpDQpgYGANCg0KDQpIdeG6pW4gbHV54buHbiBs4bqhaSBiYSBtw7QgaMOsbmggbsOgeSB0csOqbiB0cmFpbiBkYXRhIHbDoCB0w61uaCBBVUMgdHLDqm4gdGVzdCBkYXRhIHRow6wga+G6v3QgcXXhuqMgbOG6p24gbMaw4bujdCBsw6AgMC44MDg1LCAwLjgxNTAgdsOgIDAuODIwMjogDQoNCg0KYGBge3J9DQojIFRyYWluIFJhbmRvbSBGb3Jlc3QsIEdhdXNzaWFuTkIgYW5kIENhdEJvb3N0Q2xhc3NpZmllcjoNCnJhbi5maXQoWF90cmFpbiwgeV90cmFpbikNCmduYi5maXQoWF90cmFpbiwgeV90cmFpbikNCmNhdC5maXQoWF90cmFpbiwgeV90cmFpbikNCg0KIyBQcm9iYWJpbGl0eToNCnBkX3JhbiA9IHJhbi5wcmVkaWN0X3Byb2JhKFhfdGVzdClbOiwgMV0NCnBkX2duYiA9IGduYi5wcmVkaWN0X3Byb2JhKFhfdGVzdClbOiwgMV0NCnBkX2NhdCA9IGNhdC5wcmVkaWN0X3Byb2JhKFhfdGVzdClbOiwgMV0NCg0KIyBBVUMgYnkgUmFuZG9tRm9yZXN0IGFuZCBHYXVzc2lhbk5COg0KZnJvbSBza2xlYXJuLm1ldHJpY3MgaW1wb3J0IHJvY19hdWNfc2NvcmUNCg0KcHJpbnQocm9jX2F1Y19zY29yZSh5X3Rlc3QsIHBkX3JhbikpDQpwcmludChyb2NfYXVjX3Njb3JlKHlfdGVzdCwgcGRfZ25iKSkNCnByaW50KHJvY19hdWNfc2NvcmUoeV90ZXN0LCBwZF9jYXQpKQ0KYGBgDQoNClByb2ZpdCBu4bq/dSBz4butIGThu6VuZyBiYSBtw7QgaMOsbmggbsOgeSBraGkgbmfGsOG7oW5nIGNobyBwaMOibiBsb+G6oWkgdGhheSDEkeG7lWkgbuG6v3UgbMOjaSBzdeG6pXQgbMOgIDEwJSAoRmlndXJlIDEpOiANCg0KIVtdKEM6L1VzZXJzL0FETUlOL0RvY3VtZW50cy9wcm9maXQuanBnKQ0KDQpgYGB7cn0NCg0KIyBGdW5jdGlvbiBjYWxjdWxhdGVzIHByb2ZpdCB3aXRoIGdpdmVuIGN1dG9mZiB3aGVuIGludGVyZXN0IHJhdGUgb2YgMTAlOg0KDQpkZWYgcHJvZml0X2J5X2N1dG9mZihjdXRvZmYsIHByZWRfcHJvYik6DQogICAgcmF0ZSA9IDAuMQ0KICAgIHByZWRfYmcgPSAocHJlZF9wcm9iID49IGN1dG9mZikuYXN0eXBlKGludCkNCiAgICBnZyA9IFhfdGVzdFsoeV90ZXN0ID09IDApICYgKHByZWRfYmcgPT0gMCldDQogICAgYmcgPSBYX3Rlc3RbKHlfdGVzdCA9PSAxKSAmIChwcmVkX2JnID09IDApXQ0KICAgIHByb2ZpdCA9IG5wLnN1bShyYXRlICogZ2dbIkFNT1VOVCJdKSAtIG5wLnN1bShiZ1siQU1PVU5UIl0pDQogICAgcmV0dXJuIHByb2ZpdA0KDQoNCmRlZiBwcm9maXQoY3V0b2ZmKToNCiAgICBwcm9fcmFuID0gcHJvZml0X2J5X2N1dG9mZihjdXRvZmY9Y3V0b2ZmLCBwcmVkX3Byb2I9cGRfcmFuKQ0KICAgIHByb19nbmIgPSBwcm9maXRfYnlfY3V0b2ZmKGN1dG9mZj1jdXRvZmYsIHByZWRfcHJvYj1wZF9nbmIpDQogICAgcHJvX2NhdCA9IHByb2ZpdF9ieV9jdXRvZmYoY3V0b2ZmPWN1dG9mZiwgcHJlZF9wcm9iPXBkX2NhdCkNCiAgICBkZl9wcm8gPSBwZC5EYXRhRnJhbWUoeyJQcm9maXRfUkFOIjogW3Byb19yYW5dLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIlByb2ZpdF9HTkIiOiBbcHJvX2duYl0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiUHJvZml0X0NBVCI6IFtwcm9fY2F0XSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJDdXRvZmYiOiBbY3V0b2ZmXX0pDQoNCiAgICByZXR1cm4gZGZfcHJvDQoNCg0KIyBJZiBjdXRvZmYgPSAwLjAyOg0KcHJvZml0XzAwMiA9IHByb2ZpdChjdXRvZmY9MC4wMikNCnByaW50KHByb2ZpdF8wMDIpDQoNCiMgUHJvZml0IGZvciB0aGUgdHdvIG1vZGVscyBieSBhIHJhbmdlIG9mIGN1dG9mZjoNCmRmX3Byb2ZpdCA9IHBkLkRhdGFGcmFtZSgpDQoNCmZvciBqIGluIG5wLmFyYW5nZSgwLjAxLCAwLjMsIDAuMDA1KToNCiAgICBkZl9qID0gcHJvZml0KGopDQogICAgZGZfcHJvZml0ID0gZGZfcHJvZml0LmFwcGVuZChkZl9qKQ0KDQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQoNCnBsdC5zdHlsZS51c2UoJ2ZpdmV0aGlydHllaWdodCcpDQoNCnBsdC5wbG90KCJDdXRvZmYiLCAiUHJvZml0X1JBTiIsIGRhdGE9ZGZfcHJvZml0LCBsYWJlbD0iUmFuZG9tRm9yZXN0IiwgbHc9MikNCnBsdC5wbG90KCJDdXRvZmYiLCAiUHJvZml0X0dOQiIsIGRhdGE9ZGZfcHJvZml0LCBsYWJlbD0iR2F1c3NpYW5OQiIsIGx3PTIpDQpwbHQucGxvdCgiQ3V0b2ZmIiwgIlByb2ZpdF9DQVQiLCBkYXRhPWRmX3Byb2ZpdCwgbGFiZWw9IkNhdEJvb3N0IiwgbHc9MikNCnBsdC50aXRsZSgiUHJvZml0IGJ5IEN1dG9mZiBhbmQgY2xhc3NpZmllciIpDQpwbHQueGxhYmVsKCJDdXRvZmYiKQ0KcGx0LnlsYWJlbCgiUHJvZml0IikNCnBsdC5sZWdlbmQoKQ0KcGx0LnNob3coKQ0KDQpgYGANCg0KDQojIENvbmNsdXNpb24NCg0KRmlndXJlIDEgY2jhu4kgcmEgcuG6sW5nIHbhu5tpIG3hu5l0IHThu5UgY2jhu6ljIGjGsOG7m25nIMSR4bq/biBt4bulYyB0acOqdSB04buRaSDEkWEgaMOzYSBs4bujaSBuaHXhuq1uIHRow6w6IA0KDQoxLiBUacOqdSBjaHXhuqluIEFVQyBxdWFuIHRy4buNbmcgaMahbiBSZWNhbGwgdsOgIGRvIHbhuq15IEFVQyBuw6puIMSRxrDhu6NjIGNvaSBsw6AgxJFp4buBdSBraeG7h24gY+G6p24gxJHhuqd1IHRpw6puIGtoaSBs4buxYSBjaOG7jW4gbcO0IGjDrG5oIG5o4bqxbSBt4bulYyDEkcOtY2ggdOG7kWkgxJFhIGjDs2EgbOG7o2kgbmh14bqtbi4gVGjhu7FjIHbhuq15LCBSZWNhbGwgdHJ1bmcgYsOsbmggY+G7p2EgR2F1c3NpYW5OQiBjYW8gaMahbiBj4bunYSBSYW5kb20gRm9yZXN0IHThu5tpIDYzJSBuaMawbmcgbuG6v3Ugc+G7rSBk4bulbmcgR2F1c3NpYW5OQiB0aMOsIG3DtCBow6xuaCBuw6B5IHThuqFvIGxhIGzhu6NpIG5odeG6rW4gdOG7hyBuaOG6pXQgdOG6oWkgcGjhuqduIGzhu5tuIG5nxrDhu6FuZyDEkcaw4bujYyBjaOG7jW4uIA0KDQoyLiBN4bq3YyBkw7kgbOG7o2kgbmh14bqtbiBj4buxYyDEkeG6oWkgbuG6v3Ugc+G7rSBk4bulbmcgQ2F0Qm9vc3QgY2FvIGjGoW4gbOG7o2kgbmh14bqtbiBj4buxYyDEkeG6oWkgbuG6v3Ugc+G7rSBk4bulbmcgUmFuZG9tIEZvcmVzdCBuaMawbmcgcsO1IHLDoG5nIGzDoCBiaeG6v24gxJHhu5luZyB24buBIGzhu6NpIG5odeG6rW4gY8WpbmcgY2FvIGjGoW4gKMSR4buTbmcgbmdoxKlhIHbhu5tpIGLhuqV0IOG7lW4vcuG7p2kgcm8gY2FvIGjGoW4pLiBEbyB24bqteSBkw7kgUmFuZG9tIEZvcmVzdCBraMO0bmcgcGjhuqNpIGzDoCBtw7QgaMOsbmggdOG6oW8gcmEgbOG7o2kgbmh14bqtbiBj4buxYyDEkeG6oWkgbmjGsG5nIG3DtCBow6xuaCBuw6B5IG7Dqm4gxJHGsOG7o2MgbOG7sWEgY2jhu41uIMSR4buDIGzDoG0gY8ahIHPhu58geMOpdCBjaG8gQ3JlZGl0IFNjb3JpbmcuIA0KDQoNCg0KDQo=