Kredi Risk Değerlendirme Birol Yüceoğlu, Migros T.A.Ş. Yeh ve Lien (2009) tarafından kredi riskini değerlendirmek amacıyla kullanılan bir veri setini kullanacağız. Veri setinde eksik değerler veri setine benim tarafından yerleştirildi. Orijinal veri setini linkten indirebilirsiniz: http://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients (http://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients) Etiketli bir veri setiyle çalışacağımız için güdümlü öğrenme tekniklerinden yararlanacağız. Neler yapacağız? Güdümlü öğrenme kullanarak ve müşterilerin 6 aylık örüntülerine bakarak kredi ödemesinde sıkıntı olacak müşterileri tahmin edeceğiz. Veri setini inceleyerek yeni öznitelikler türeteceğiz. Kullandığımız yöntemlerin başarısını inceleyeceğiz. Veri setini (train, test) okutarak başlayalım
In [1]: #Paketleri yükleyelim %matplotlib inline import pandas as pd import numpy as np import matplotlib.pyplot as plt from IPython.display import display,html #Veri setlerini okutalım ve inceleyelim df_train = pd.read_excel('credit_scoring_train.xls') df_test = pd.read_excel('credit_scoring_test.xls') print("veri çerçevesinin ilk satırları") display(df_train.head()) print("öznitelikler") for i in df_train.columns: print(i)
Veri çerçevesinin ilk satırları CUSTOMER_ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAYMENT_1 PAYMENT_2 PAYMENT_3 PAYMENT_4 0 461090387729 10000.0 Male NaN Married 43.0-1 0 0 0 1 456333788060 10000.0 Female NaN Single 24.0-1 0 0 0 2 530658128376 10000.0 Female NaN Single 21.0 0 0 0 0 3 546506894510 10000.0 Male NaN Single 35.0 3 2 0 0 4 469501329931 10000.0 Male Gradschool Single 47.0 0 0 0-1 5 rows 25 columns Öznitelikler CUSTOMER_ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAYMENT_1 PAYMENT_2 PAYMENT_3 PAYMENT_4 PAYMENT_5 PAYMENT_6 REMAINING_AMOUNT_1 REMAINING_AMOUNT_2 REMAINING_AMOUNT_3 REMAINING_AMOUNT_4 REMAINING_AMOUNT_5 REMAINING_AMOUNT_6 PAYMENT_AMOUNT_1 PAYMENT_AMOUNT_2 PAYMENT_AMOUNT_3 PAYMENT_AMOUNT_4 PAYMENT_AMOUNT_5 PAYMENT_AMOUNT_6 default
Veri Sözlüğü Veride aşağıdaki kolonlar yer almaktadır: CUSTOMER_ID: Müşteriye özel ID. LIMIT_BAL: Kredi limiti. SEX: Cinsiyet (Male, Female). EDUCATION: Eğitim (Gradschool, University, Highschool). MARRIAGE: Medeni durum (Married, Single). AGE: Yaş (year). PAYMENT_1 - PAYMENT_6: Ay bazında geçmiş ödemelerdeki gecikme (1 - Eylül 2005, 6 - Nisan 2005 olacak şekilde). REMAINING_AMOUNT_1 - REMAINING_AMOUNT_6: Kalan borç miktarı (1 - Eylül 2005, 6 - Nisan 2005 olacak şekilde). PAYMENT_AMOUNT_1 - PAYMENT_AMOUNT_6: Önceki ödemeler (1 - Eylül 2005, 6 - Nisan 2005 olacak şekilde). default: Çıktı değeri, borcun zamanında ödenip ödenmediği (0 = Zamanında, 1 = Geç) Önce sayısal kolonlara bakarak eksik değer olup olmadığına bakalım.
In [2]: print("veri çerçevesinin boyutları: " + str(np.shape(df_train))) print("öznitelik kolonlarının özeti") print(df_train['limit_bal'].describe()) print(df_train['age'].describe()) print(df_train[[i for i in df_train.columns if 'REMAINING_AMOUNT' in i]].describe()) print(df_train[[i for i in df_train.columns if 'PAYMENT' in i]].describe())
Veri çerçevesinin boyutları: (19800, 25) Öznitelik kolonlarının özeti count 19797.000000 mean 167964.220842 std 129539.225759 min 10000.000000 25% 50000.000000 50% 140000.000000 75% 240000.000000 max 1000000.000000 Name: LIMIT_BAL, dtype: float64 count 19781.000000 mean 35.540064 std 9.220722 min 21.000000 25% 28.000000 50% 34.000000 75% 41.000000 max 79.000000 Name: AGE, dtype: float64 REMAINING_AMOUNT_1 REMAINING_AMOUNT_2 REMAINING_AMOUNT_3 \ count 19800.000000 19800.000000 1.980000e+04 mean 51265.968131 49428.558232 4.719719e+04 std 73465.137845 71323.105122 6.942574e+04 min -165580.000000-33350.000000-1.572640e+05 25% 3605.750000 3066.250000 2.694750e+03 50% 22357.500000 21394.000000 2.010050e+04 75% 66868.750000 64229.500000 6.039650e+04 max 964511.000000 983931.000000 1.664089e+06 REMAINING_AMOUNT_4 REMAINING_AMOUNT_5 REMAINING_AMOUNT_6 count 19800.000000 19800.000000 19800.000000 mean 43617.471111 40696.433485 39142.899798 std 64693.959172 61500.580818 60140.545390 min -170000.000000-81334.000000-339603.000000 25% 2312.500000 1761.750000 1256.000000 50% 19147.000000 18129.500000 17177.000000 75% 55406.500000 50837.000000 49512.250000 max 891586.000000 927171.000000 961664.000000 PAYMENT_1 PAYMENT_2 PAYMENT_3 PAYMENT_4 PAYMENT_5 \ count 19800.000000 19800.000000 19800.000000 19800.000000 19800.000000 mean 0.074646-0.012374-0.034747-0.078384-0.113182 std 0.986337 1.029932 1.020871 0.982815 0.940187
min -1.000000-1.000000-1.000000-1.000000-1.000000 25% -1.000000-1.000000-1.000000-1.000000-1.000000 50% 0.000000 0.000000 0.000000 0.000000 0.000000 75% 0.000000 0.000000 0.000000 0.000000 0.000000 max 8.000000 7.000000 8.000000 7.000000 7.000000 PAYMENT_6 PAYMENT_AMOUNT_1 PAYMENT_AMOUNT_2 PAYMENT_AMOUNT_3 \ count 19800.000000 19800.000000 1.980000e+04 19800.000000 mean -0.128333 5674.351010 5.914844e+03 5300.064646 std 0.946333 16069.451498 2.249296e+04 17327.253010 min -1.000000 0.000000 0.000000e+00 0.000000 25% -1.000000 1000.000000 8.315000e+02 396.000000 50% 0.000000 2124.000000 2.018000e+03 1822.000000 75% 0.000000 5037.250000 5.000000e+03 4626.250000 max 7.000000 505000.000000 1.684259e+06 896040.000000 PAYMENT_AMOUNT_4 PAYMENT_AMOUNT_5 PAYMENT_AMOUNT_6 count 19800.000000 19800.000000 19800.000000 mean 4849.301414 4766.921313 5259.505101 std 15489.765788 14775.652597 18047.774868 min 0.000000 0.000000 0.000000 25% 285.000000 255.750000 125.000000 50% 1500.000000 1500.000000 1500.000000 75% 4052.000000 4127.500000 4045.250000 max 528897.000000 426529.000000 528666.000000 Kredi limiti ve yaş kolonlarında eksik değerler oldğunu görüyoruz. Bu gözlemleri analizden çıkarabilir ya da eksik değerleri kendimiz doldurabiliriz. Burada hangisini tercih edeceğimiz eksik gözlem sayısı ve özniteliğin önemine göre değişebilir. Yaş için bir değer kullanarak eksikleri giderelim. bunun için ortalama ya da median değeri kullanılabilir. Kredi limiti için de öneminden dolayı bu kolonu eksik olan müşterileri analizden çıkaralım Eksik değerleri doldurmak için scikit-learn'deki Imputer fonksiyonunu kullanalım.
In [3]: print("veri çerçevesinin silme işleminden önceki boyutları: " + str(np.shape(df_train))) # LIMIT_BAL değeri boş olan kolonları siliyoruz df_train = df_train[df_train['limit_bal'] >0] print("veri çerçevesinin silme işleminden sonraki boyutları: " + str(np.shape(df_train))) #AGE kolonunu median değeriyle dolduruyoruz. Missing değer ve strategy (nasıl dolduracağımız) bilgisini verel im. from sklearn.preprocessing import Imputer imp = Imputer(missing_values='NaN', strategy='median') df_train['age'] = imp.fit_transform(df_train['age'].values.reshape(-1,1)).ravel() print("eksik değerler silindikten sonra özniteliklerin özetleri") print(df_train['age'].describe()) print(df_train['limit_bal'].describe()) Veri çerçevesinin silme işleminden önceki boyutları: (19800, 25) Veri çerçevesinin silme işleminden sonraki boyutları: (19797, 25) Eksik değerler silindikten sonra özniteliklerin özetleri count 19797.000000 mean 35.539375 std 9.216861 min 21.000000 25% 28.000000 50% 34.000000 75% 41.000000 max 79.000000 Name: AGE, dtype: float64 count 19797.000000 mean 167964.220842 std 129539.225759 min 10000.000000 25% 50000.000000 50% 140000.000000 75% 240000.000000 max 1000000.000000 Name: LIMIT_BAL, dtype: float64 Şimdi de kategorik değişkenler içeren kolonları inceleyelim.
In [4]: print(df_train[['sex', 'EDUCATION', 'MARRIAGE']].describe()) SEX EDUCATION MARRIAGE count 19797 19517 19555 unique 2 3 2 top Female University Single freq 11962 9235 10570 Eğitim ve medeni durumla ilgili kolonlarda eksik değerler var. Müşterinin finansal durumuyla ilgili bilgi içerebileceği için ve eksik değerlere sahip müşteri sayısı fazla olduğu için bu kolonların değerlerinin eksik olduğunu belirtelim. Bunu yapmak için eksik değerli kolonları 'MISSING' anahtar kelimesiyle belirtelim. In [5]: #Boş olan değerleri istediğimiz değerle doldurmanın bir diğer yolu da fillna kullanmak df_train['education'].fillna('missing_edu', inplace = True) df_train['marriage'].fillna('missing_mar', inplace = True) display(df_train['education'].head()) 0 MISSING_EDU 1 MISSING_EDU 2 MISSING_EDU 3 MISSING_EDU 4 Gradschool Name: EDUCATION, dtype: object Artık her müşterinin medeni durumu Single, Married, MISSING_MAR değerlerinden birine sahip. Ancak bu kolonu hala tahmin için kullanmamaız mümkün değil. Bunu yapmak için kolonu nümerik değerlere çevirmemiz gerekiyor. Burada sıklıkla yapılabilen bir hata her bir durum için bir sayı atamaktır (0 = married, 1 = single, 2 = MISSING_MAR gibi). Bunun yerine takma değişkenler (dummy variable) kullanabiliriz. Her bir değer için ayrı bir kolon oluşturulup ikili değerlerle gözlemlerin ait olduğu değer belirtilir. bu amaçla pandas altında get_dummies fonksiyonunu kullanabiliriz.
In [6]: # EDUCATION kolonunu 4 adet dummy değişkene çeviriyoruz. df_education = df_train[ 'EDUCATION' ] df_education = pd.get_dummies(df_education) display(df_education.head()) # MARRIAGE kolonunu 3 adet dummy değişkene çeviriyoruz. df_marriage = df_train[ 'MARRIAGE' ] df_marriage = pd.get_dummies(df_marriage) display(df_marriage.head()) Gradschool Highschool MISSING_EDU University 0 0 0 1 0 1 0 0 1 0 2 0 0 1 0 3 0 0 1 0 4 1 0 0 0 MISSING_MAR Married Single 0 0 1 0 1 0 0 1 2 0 0 1 3 0 0 1 4 0 0 1 Artık yapmamız gereken bütün bu yaptıklarımı bir araya getirip train ve test veri setlerini oluşturmak. Bu amaçla ID, EDUCATION, MARRIAGE kolonlarını çıkararak yeni oluşturduğumuz veri çerçeveleriyle önceki ver çerçevesini birleştireceğiz. Aynı işlemi SEX kolonu için de yapacağız.
In [7]: #Kullanmayacağımız kolonları siliyoruz df_train.drop(['customer_id', 'EDUCATION', 'MARRIAGE'], axis=1, inplace=true) #Veri çerçevelerini birleştiriyoruz df_train = pd.concat([df_train, df_education, df_marriage], axis=1) #Kategorik değişkenleri dummye çevirmenin bir başka yolu df_train['male'] = (df_train.sex == 'Male')*1 df_train['female'] = (df_train.sex == 'Female')*1 df_train.drop('sex', axis=1, inplace=true) display(df_train.head()) LIMIT_BAL AGE PAYMENT_1 PAYMENT_2 PAYMENT_3 PAYMENT_4 PAYMENT_5 PAYMENT_6 REMAINING_AMOUNT_ 0 10000.0 43.0-1 0 0 0-1 -1 17560 1 10000.0 24.0-1 0 0 0 0-1 5742 2 10000.0 21.0 0 0 0 0 0-1 7691 3 10000.0 35.0 3 2 0 0 0-1 10281 4 10000.0 47.0 0 0 0-1 -1-1 7968 5 rows 30 columns Son olarak yeni öznitelikler türetelim. Müşterilerin kredi limitleri oldukça önemli bir bilgi gibi duruyor. Büyük olasılıkla yaşlandıkça limitiniz de artacak. 'LIMIT_PER_AGE' adlı bir kolonla limiti yaşa bölerek yeni bir değişken oluşturalım. Bu sayede aynı limite sahip iki müşteriden daha genç olanını ayırdedebileceğimiz bir değişken oluşturuyoruz. Genç ve yüksek limitli bir müşteri daha cazip bir müşteri olabilir. Aynı zamanda bankaya olan borcunuzla maddi durumunuzun da ilişkisini kurmak yararlı olabilir. Aynı borca sahip iki müşteriden limiti daha yüksek olan daha az riskli bir müşteri olabilir. REMAINING_AMOUNT_1 kolonunu LIMIT_BAL kolununa bölerek yeni bir değişken türetelim. In [8]: #Yeni değişkenler oluşturuyoruz. df_train['limit_per_age'] = df_train['limit_bal'] / df_train['age'] df_train['relative_remaining_amount'] = df_train['remaining_amount_1'] / df_train['limit_bal']
Aynı adımları test verisi için de yapalım. In [9]: df_test = pd.read_excel('credit_scoring_test.xls') df_test['age'] = imp.transform(df_test['age'].values.reshape(-1, 1)).ravel() df_test['education'].fillna('missing_edu', inplace = True) df_test['marriage'].fillna('missing_mar', inplace = True) df_education = df_test[ 'EDUCATION' ] df_education = pd.get_dummies(df_education) df_marriage = df_test[ 'MARRIAGE' ] df_marriage = pd.get_dummies(df_marriage) df_test.drop(['customer_id', 'EDUCATION', 'MARRIAGE'], axis=1, inplace=true) df_test = pd.concat([df_test, df_education, df_marriage], axis=1) df_test['limit_per_age'] = df_test['limit_bal'] / df_test['age'] df_test['relative_remaining_amount'] = df_test['remaining_amount_1'] / df_test['limit_bal'] df_test['male'] = (df_test.sex == 'Male')*1 df_test['female'] = (df_test.sex == 'Female')*1 df_test.drop('sex', axis=1, inplace=true) df_test = df_test[df_train.columns] Sınıflandırma ve Değerlendirme Bu aşamada gradient boosting yöntemini kullanacağız (sklearn.ensemble.gradientboostingclassifier). In [10]: #Hedef değişkenleri (default adlı kolon) veri çerçevesinden ayırıyoruz. y_train = df_train.default y_test = df_test.default df_train.drop('default', axis=1, inplace=true) df_test.drop('default', axis=1, inplace=true)
Şu an için modelin parametreleriyle oynamıyoruz. Şimdi modeli kuralım. In [11]: #Modeli kurup eğitiyoruz from sklearn.ensemble import GradientBoostingClassifier gbc = GradientBoostingClassifier() gbc.fit(df_train,y_train) Out[11]: GradientBoostingClassifier(criterion='friedman_mse', init=none, learning_rate=0.1, loss='deviance', max_depth=3, max_features=none, max_leaf_nodes=none, min_impurity_decrease=0.0, min_impurity_split=none, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=100, presort='auto', random_state=none, subsample=1.0, verbose=0, warm_start=false) Sonuçları değerlendirelim.
In [12]: # Modeli değerlendirelim. Bunun için doğruluk ve eğri altı alana bakalım from sklearn.metrics import accuracy_score, roc_auc_score, roc_curve, confusion_matrix print('gradient Boosting') print('accuracy') print(accuracy_score(y_test, gbc.predict(df_test))) print('auc score') print(roc_auc_score(y_test, gbc.predict_proba(df_test)[:,1])) print('confusion Matrix') df_confusion = pd.dataframe(confusion_matrix(y_test, gbc.predict(df_test))) df_confusion.columns = ['Predicted not default', 'Predicted default'] df_confusion.index = ['Actual not default', 'Actual default'] display(df_confusion) Gradient Boosting Accuracy 0.817549019608 AUC score 0.776683445458 Confusion Matrix Predicted not default Predicted default Actual not default 7488 413 Actual default 1448 851 Eğri altı alanı görselleştirelim. Kredisini ödeyemeyecek müşterilerin %80'ini tahmin etmek istersek ödeyecek müşteriler için ne kadarlık bir hata payımız oluyor?
In [13]: a_gbc, b_gbc, c_gbc = roc_curve(y_test,gbc.predict_proba(df_test)[:,1]) plt.figure(figsize=(10,10)) plt.plot(a_gbc,b_gbc, c='black', label = 'Gradient Boosting', linewidth = 3) plt.title('area Under Curve', fontsize = 16) plt.ylabel('true positive rate', fontsize = 14) plt.xlabel('1 - True negative rate', fontsize = 14) plt.legend(loc = 4) plt.show()
Eğri altı alan true positive rate (borcunu ödeyemeyeceği doğru bilinen müşteri yüzdesi) ile true negative rate (borcunu ödeyeceği doğru bilinen müşteri yüzdesi) arasındaki ilişkiyi gösterir. True positive rate 0 ise true negative rate 1 oluyor (Herkes ödeyecek şeklinde bir tahmine denk geliyor). Diğer uca bakarsak, herkesi ödeyemeyecek şeklinde tahmin ettiğimizde ödeyecek müşterilerin tamamını yanlış sınıflandırmış oluyoruz. Borcunu ödeyemeyecek müşterilerin %75'ini bilmek istersek borcunu ödeyemeyecek müşterilerde %35 civarında yanlış tahmin yapmamız gerekiyor. Fine Tuning Scikit-learndeki classifier yöntemleri için bir çok parametre seçme opsiyonumuz var. Genelde varsayılan parametreler oldukça başarılı sonuçlar verse de parametrelerle oynamak sonuçları iyileştirebilir. Biz de iterasyon sayısını (n_estimators), modeli oluşturan karar ağaçlarının derinliklerini (max_depth) ve yaprak düğümlerindeki en az gözlem sayısını (min_samples_leaf) değiştirerek başlayalım. Değerlendirme için eğri altı alan değerini kullanacağız. Parametreleri denemek için Grid Search modülünü kullanalım. Bu modülü kullanarak belirleyelim.
In [14]: from sklearn.model_selection import GridSearchCV from time import time # Grid search ile denenecek model parametreleri ve değerleri parameters = {"max_depth": [3, 10], "min_samples_leaf": [20, 100], "n_estimators": [50]} # Kullanacağımız model gbc = GradientBoostingClassifier() #Grid search modelini oluşturuyoruz grid_search = GridSearchCV(gbc, param_grid=parameters, scoring = 'roc_auc') start = time() #Modeli eğitiyoruz grid_search.fit(df_train, y_train) print("gridsearchcv took %.2f seconds for %d candidate parameter settings." % (time() - start, len(grid_search.grid_scores_))) GridSearchCV took 35.09 seconds for 4 candidate parameter settings. C:\Users\byuceoglu\AppData\Local\Continuum\anaconda3\lib\site-packages\sklearn\model_selection\_search.py:74 7: DeprecationWarning: The grid_scores_ attribute was deprecated in version 0.18 in favor of the more elabora te cv_results_ attribute. The grid_scores_ attribute will not be available from 0.20 DeprecationWarning) In [15]: print(grid_search.best_estimator_) print(roc_auc_score(y_test, grid_search.best_estimator_.predict_proba(df_test)[:,1])) GradientBoostingClassifier(criterion='friedman_mse', init=none, learning_rate=0.1, loss='deviance', max_depth=3, max_features=none, max_leaf_nodes=none, min_impurity_decrease=0.0, min_impurity_split=none, min_samples_leaf=100, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=50, presort='auto', random_state=none, subsample=1.0, verbose=0, warm_start=false) 0.778590940443
Özniteliklerin Değerlendirilmesi Gradient Boosting için kullanacağımız parametreleri belirledikten sonra modeli bu parametrelerle kurup önemli öznitelikleri belirleyelim. Bu amaçla feature_importances_ değerlerine bakacağız.
In [16]: gbc= GradientBoostingClassifier(n_estimators=50, max_depth=3, min_samples_leaf=100) gbc.fit(df_train, y_train) # Özniteliklerin önemi feature_importance = gbc.feature_importances_ df_features = pd.dataframe(feature_importance) df_features.columns = ['Importance'] df_features.index = df_train.columns # Maximum 100 olacak şekilde ağırlıklandırıyoruz feature_importance = 100.0 * (feature_importance / feature_importance.max()) sorted_idx = np.argsort(feature_importance) pos = np.arange(sorted_idx.shape[0]) +.5 plt.figure(figsize=(10,10)) plt.barh(pos, feature_importance[sorted_idx], align='center') plt.yticks(pos, df_train.columns[sorted_idx]) plt.xlabel('relative Importance') plt.title('variable Importance') plt.show()
Bizim oluşturduğumuz öznitelikler oldukça yukarılarda çıktı. Eğitim ve cinsiyetle ilgili değişkenler ise genelde alt sıralarda En önemli değişken ise 1 önceki ayın ödemesindeki gecikme.
References Yeh, I. C., & Lien, C. H. (2009). The comparisons of data mining techniques for the predictive accuracy of probability of default of credit card clients. Expert Systems with Applications, 36(2), 2473-2480.