Предсказание банкротства было предметом интересов уже более ста лет, и оно по-прежнему занимает высокое место среди самых горячих тем в экономике. Оно имеет большое значение для принятия экономических решений, т.к. бизнес-состояние небольшой или крупной фирмы касается местного сообщества, участников отрасли и инвесторов, а также влияет на политику и глобальную экономику.
Целью прогноза банкротства является оценка финансового состояния компании и ее будущих перспектив в контексте долговременной работы на рынке используя экспертные знания о феномене и исторические данные процветающих и безуспешных компаний. В данной работе предприятия будут описываться количественно с помощью многочисленных бизнес индекаторов.
В данной работе использовались данные о финансовом состоянии польских компаний. Процесс отбора данных состоит из выбора сектора, базы данных, периода исследования и количества финансовых показателей, которые будут проанализированы. В Польше, начиная с 2004г, максимальное количество банкротств было зафиксировано в производственном секторе (the manufacturing sector), поэтому для данного исследования этот сектор представляет наибольший интерес. Данные брались из базы данных информационной службы развивающихся рынков (EMIS). Для компаний указаны финансовые показатели за целевой год и указана метка о банкротстве в течении 5 лет. Финансовые показатели были выбраны на основе анализа популярных и эффективных показателей в смежных работах.
Все используемые в данной задаче признаки, кроме целевого, числовые.
Признаки:
Attr1 : чистая прибыль / совокупные активы
Attr2 : Все (total) обязательства / совокупные активы
Attr3 : оборотный капитал / совокупные активы
Attr4 : текущие активы / краткосрочные обязательства
Attr5 : [(денежные средства + краткосрочные ценные бумаги + дебиторская задолженность - краткосрочные обязательства) / (операционные расходы - амортизация)] * 365,
Attr6 : Нераспределенная прибыль / совокупные активы
Attr7 : прибыль до начисления процентов / совокупные активы
Attr8 : балансовая стоимость собственного капитала / Все (total) обязательства
Attr9 : продажи / совокупные активы
Attr10 : капитал / совокупные активы
Attr11 : (валовая прибыль + внереализационные статьи + финансовые расходы) / совокупные активы
Attr12 : валовая прибыль / краткосрочные обязательства
Attr13 : (валовая прибыль + амортизация) / продажи
Attr14 : (валовая прибыль + interest) / совокупные активы
Attr15 : (Все (total) обязательства * 365) / (валовая прибыль + амортизация)
Attr16 : (валовая прибыль + амортизация) / Все (total) обязательства
Attr17 : совокупные активы / Все (total) обязательства
Attr18 : валовая прибыль / совокупные активы
Attr19 : валовая прибыль / продажи
Attr20 : (инвентарь * 365) / продажи
Attr21 : продажи (n) / продажи (n-1)
Attr22 : прибыль от операционной деятельности / совокупные активы
Attr23 : чистая прибыль / продажи
Attr24 : валовая прибыль (за 3 года) / совокупные активы
Attr25 : (собственный капитал - акционерный капитал) / совокупные активы
Attr26 : (чистая прибыль + амортизация) / Все (total) обязательства
Attr27 : прибыль от операционной деятельности / финансовые расходы
Attr28 : оборотный капитал / основные средства
Attr29 : натуральный логарифм от величины общих активов
Attr30 : (общие обязательства - наличные) / продажи
Attr31 : (валовая прибыль + interest) / продажи
Attr32 : (текущие обязательства * 365) / себестоимость реализованной продукции
Attr33 : операционные расходы / краткосрочные обязательства
Attr34 : операционные расходы / общие обязательства
Attr35 : прибыль от продаж / общие активы
Attr36 : общие продажи / общие активы
Attr37 : (оборотные активы - запасы) / долгосрочные обязательства
Attr38 : постоянный капитал / общие активы
Attr39 : прибыль от продаж / продажи
Attr40 : (текущие активы - запасы - дебиторская задолженность) / краткосрочные обязательства
Attr41 : общие обязательства / ((прибыль от операционной деятельности + амортизация) * (12/365))
Attr42 : прибыль от операционной деятельности / продаж
Attr43 : оборотная дебиторская задолженность + оборот запасов в днях
Attr44 : (дебиторская задолженность * 365) / продажи
Attr45 : чистая прибыль / запасы
Attr46 : (оборотные активы - запасы) / краткосрочные обязательства
Attr47 : (запасы * 365) / стоимость проданной продукции
Attr48 : EBITDA (прибыль от операционной деятельности - амортизация) / общие активы
Attr49 : EBITDA (прибыль от операционной деятельности - амортизация) / продажи
Attr50 : текущие активы / общие обязательства
Attr51 : краткосрочные обязательства / общие активы
Attr52 : (краткосрочные обязательства * 365) / стоимость проданной продукции
Attr53 : собственный капитал / основные средства
Attr54 : постоянный капитал / основные средства
Attr55 : оборотный капитал
Attr56 : (продажи - стоимость проданной продукции) / продажи
Attr57 : (текущие активы - запасы - краткосрочные обязательства) / (выручка - валовая прибыль - амортизация)
Attr58 : общие затраты / общий объем продаж
Attr59 : долгосрочные обязательства / собственный капитал
Attr60 : продажа / инвентарь
Attr61 : продажи / дебиторская задолженность
Attr62 : (краткосрочные обязательства * 365) / продажи
Attr63 : продажи / краткосрочные обязательства
Attr64 : продажи / основные средства
** Целевая переменная ** :
Загрузим исходные данные и посмотрим на них.
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import KFold
from sklearn.metrics import roc_auc_score
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
%pylab inline
def findpass(s):
if s in '?':
return '?';
else:
return float(s);
def arfftoDataframe(name, num_features):
with open(name, 'r') as inf:
X = [];
for i in range(num_features+5):
next(inf);
for line in inf:
line = line.strip();
X.append(line.split(','))
D = pd.DataFrame()
X = np.array(X)
for i in range(num_features):
D['Attr' + str(i+1)] = np.array([findpass(i) for i in X[:,i]])
D['target'] = np.array([int(i) for i in X[:,-1]])
return D
data = arfftoDataframe('1year.arff', 64)
data.info()
Видно, что только у 16 признаков полностью заполнены все значения, найдем долю пропусков для каждого из столбцов. В этом датасете, пропуски обозначенны знаком вопроса.
d = {};
for i in data.columns:
if '?' in data[i].value_counts():
d[i] = data[i].value_counts()['?']*100/len(data)
for i in d.keys():
if d[i] > 1.0:
print(i, d[i])
Для признаков Attr21 и Attr37 слишком большое количество пропусков, из-за этого их не получится восстановить с помощью интерполяции, поэтому удалим их.
data = data.drop(['Attr21', 'Attr37'], axis = 1)
del d['Attr21']
del d['Attr37']
Далее проведем работу со строками, подсчитаем количество строк у которых в признаках больше одного неизвестного значения и оставим строки где не более 3 пропусков в строке, это эквивалентное условию о том, что о компании нам известно не менее 95% информации.
strings = {};
for i in d.keys():
for j in range(len(data[i])):
if data[i][j] in '?':
if j not in strings.keys():
strings[j] = 1
else:
strings[j] += 1;
del_s = []
for i in strings.keys():
if strings[i] > 3:
del_s.append(i)
data = data.drop(del_s, axis = 0)
Выделим в отдельный массив все оставшиеся строки с пропусками и оставшиеся элементы DataFrame приведем к формату с плавающей точкой.
for i in del_s:
del strings[i]
sk = [];
for i in strings.keys():
sk += [i]
X_strings = data.loc[np.array(sk)];
data = data.drop(strings.keys(), axis = 0)
for i in d.keys():
data[i] = data[i].astype(float)
stings_without_str = [];
for i in d.keys():
k = 0;
for j in X_strings[i]:
if j in '?':
k += 1;
if k == 0:
stings_without_str += [i];
for i in stings_without_str:
del d[i];
for i in d.keys():
d[i] = X_strings[i].value_counts()['?']*100/len(data)
Посмотрим на статистику отсуствующих признаков.
data.describe()[[i for i in d.keys()]].T
Видно, что у многих признаков std, минимальное и максимальное значения очень велики, поэтому нельзя заменить средним или медианным значением. Тогда для заполнения пропусков решим дополнительную подзадачу, построим регрессию на основе дерева. Данный выбор обуславливается тем, что дерево быстро обучить и оно склонно к переобучению, что в задаче заполнения пропусков является положительной чертой.
Построим словарь индексов с номерами строк с пропусками для каждого из столбцов и на его основе построим матрицу А, элементами которой будут количество одинаковых строк у разных столбцов. На её основе выберем порядок заполнения пропущенных столбцов и количество столбцов для обучения.
dict_of_sets = {};
for i in d.keys():
nums = []
for j in X_strings.index:
if X_strings[i][j] == '?':
nums += [j];
dict_of_sets[i] = set(nums);
A = [[len(dict_of_sets[i].intersection(dict_of_sets[j])) for j in dict_of_sets.keys()] for i in dict_of_sets.keys()]
A
dict_of_sets.keys()
from sklearn.tree import DecisionTreeRegressor
def predict_and_notconcat(clf, choose_columns, X, data, d, dict_of_sets):
for col in choose_columns:
clf.fit(data.drop(col, axis=1), data[col[0]]);
pred_data = X.drop(col, axis=1).loc[dict_of_sets[col[0]]]
for i in pred_data.columns:
pred_data[i] = pred_data[i].astype(float)
pred_data[col[0]] = clf.predict(pred_data)
for i in dict_of_sets[col[0]]:
X[col[0]][i] = pred_data[col[0]][i]
del d[col[0]]
return X
clf = DecisionTreeRegressor(random_state = 0, min_samples_leaf = 2)
for col in ['Attr11', 'Attr46', 'Attr61']:
clf.fit(data.drop([col], axis=1), data[col]);
pred_data = X_strings.drop([col], axis=1).loc[dict_of_sets[col]]
X_strings = X_strings.drop(dict_of_sets[col], axis = 0)
for i in pred_data.columns:
pred_data[i] = pred_data[i].astype(float)
pred_data[col] = clf.predict(pred_data)
data = pd.concat([data, pred_data])
del d[col]
X_strings = predict_and_notconcat(clf, [['Attr5', 'Attr32']], X_strings, data, d, dict_of_sets)
X_strings = predict_and_notconcat(clf, [['Attr47', 'Attr32', 'Attr52'], ['Attr52', 'Attr32', 'Attr47']], X_strings, data, d, dict_of_sets)
for col in ['Attr32']:
clf.fit(data.drop([col], axis=1), data[col]);
pred_data = X_strings.drop([col], axis=1).loc[dict_of_sets[col]]
X_strings = X_strings.drop(dict_of_sets[col], axis = 0)
for i in pred_data.columns:
pred_data[i] = pred_data[i].astype(float)
pred_data[col] = clf.predict(pred_data)
data = pd.concat([data, pred_data])
del d[col]
X_strings = predict_and_notconcat(clf, [['Attr41', 'Attr27', 'Attr24', 'Attr45', 'Attr60']], X_strings, data, d, dict_of_sets)
X_strings = predict_and_notconcat(clf, [['Attr24', 'Attr27', 'Attr45', 'Attr60']], X_strings, data, d, dict_of_sets)
X_strings = predict_and_notconcat(clf, [['Attr45', 'Attr27', 'Attr60'], ['Attr60', 'Attr45', 'Attr27']], X_strings, data, d, dict_of_sets)
for col in ['Attr27']:
clf.fit(data.drop([col], axis=1), data[col]);
pred_data = X_strings.drop([col], axis=1).loc[dict_of_sets[col]]
X_strings = X_strings.drop(dict_of_sets[col], axis = 0)
for i in pred_data.columns:
pred_data[i] = pred_data[i].astype(float)
pred_data[col] = clf.predict(pred_data)
data = pd.concat([data, pred_data])
del d[col]
for i in X_strings.columns:
X_strings[i] = X_strings[i].astype(float)
data = pd.concat([data, X_strings])
Рассмотренные далее классификаторы возвращают вероятность принадлежности к целевому классу, в зависимости от смысла бизнес задачи (инвестиционная деятельность, хеджирование, государственная поддержка) пороги будут выбираться согласно её логики. Не зная какой-то конкретный порог заранее, следует оценивать семейство алгоритмов, параметром которого является порог. Для этого подойдет интегральная метрика roc_auc, которая и будет использоваться в дальнейшем.
В процессе исследования у нас возникнет вопрос о сравнении качества моделей и доказательства целесообразности проведенного анализа. Для решения первой задачи мы построим алгоритм случайного леса на исходных сырых данных. Выбор данного алгоритма обуславливается тем, что он наименее прихотлив к предобработке данных. Для решении второй задачи мы создадим отложенную выборку и результат полученного алгоритма сопоставим с baseline.
Поместим в отложенную выборку 10% необработанных исходных данных, так что бы пропорции классов сохранялись.
data['target'].hist(orientation='horizontal', figsize=(10,5))
print('Распределение классов:')
100*data['target'].value_counts() / data.shape[0]
Округлим до целых, получим, что 96% данных принадлежит к нулевому классу и 4% к целевому. В отложенной выборке будет 695 элементов, 28 из которых будут принадлежать к целевому классу, а остальные к нулевому.
np.random.seed(0)
A = np.hstack((random.choice(data[data['target'] == 0].index, 667, replace = False),
random.choice(data[data['target'] == 1].index, 28, replace = False)))
Deferred_selection = data.loc[A]
data.drop(A, axis = 0, inplace = True)
Из гистограммы в прошлом пункте видно, что классы сильно не сбалансированны, поэтому здесь и далее в алгоритмах будет использоваться параметр class_weight = 'balanced'. Найдем, используя кросс-валидацию, лучшие параметры алгоритма случайного леса на исходных сырых данных.
forrest_param = {'n_estimators' : range(50, 120, 10), 'max_depth' : range(1,5), 'min_samples_leaf': range(1,10)}
clf = RandomForestClassifier(n_estimators = 50, max_depth = 10, min_samples_leaf = 5, class_weight = 'balanced', random_state = 0)
tree_grid = GridSearchCV(clf, forrest_param, cv=30, n_jobs=-1, scoring = 'roc_auc')
tree_grid.fit(data.drop(['target'], axis=1), data['target'])
print(tree_grid.best_params_)
print(tree_grid.best_score_)
Построим прогноз для отложенной выборки и получим baseline.
clf = RandomForestClassifier(n_estimators = 80, max_depth = 4, min_samples_leaf = 6, class_weight = 'balanced', random_state = 0)
clf.fit(data.drop(['target'], axis=1), data['target'])
pred = clf.predict_proba(Deferred_selection.drop(['target'], axis=1))
roc_auc_score(Deferred_selection['target'], pred[:,1])
Построим матрицу ковариации для анализа линейных зависимостей между признаками.
sns.set(font_scale=2.5)
plt.figure(figsize=(60,30))
corr_matrix=data.corr()
sns.heatmap(corr_matrix,annot=True,fmt = ".2f",cbar = True,cmap='PuOr',annot_kws={"size":17})
Видно, что довольно много линейно зависимых признаков. Попробуем сжать признаковое пространство, оставив лишь несколько независимых. Т.к. признаки имеют разный масштаб, перед их трансформацией отмасштабируем их, это никак не повлияет на матрицу корреляции. Не забудем и про отложенную выборку.
for i in data.drop(['target'], axis = 1).columns:
scaler = StandardScaler()
scaler.fit(data[i])
data[i] = scaler.transform(data[i])
Deferred_selection[i] = scaler.transform(Deferred_selection[i]);
С помощью алгоритма PCA выясним какое количество новых компонент будут давать 90% дисперсии.
pca = PCA()
pca.fit(data.drop(['target'], axis=1))
X_reduced = pca.fit_transform(data.drop(['target'], axis=1))
plt.figure(figsize=(10,7))
plt.plot(np.cumsum(pca.explained_variance_ratio_), color='k', lw=2)
plt.xlabel('Number of components')
plt.ylabel('Total explained variance')
plt.xlim(1, 62)
plt.yticks(np.arange(0, 1.1, 0.1))
plt.axvline(67, c='b')
plt.axhline(0.9, c='r')
plt.show();
sum = 0.0;
k = 1;
for i in pca.explained_variance_ratio_:
sum += i;
if sum < 0.9:
k += 1;
print(k)
Вычисления показывают, что 16 признаков достаточно для хорошего приближения исходных признаков.
pca = PCA(16)
X_reduced = pca.fit_transform(data.drop(['target'], axis=1))
Deferred_selection_reduced = pca.transform(Deferred_selection.drop(['target'], axis=1))
del_columns = data.drop(['target'], axis=1).columns
for i in del_columns:
del data[i]
del Deferred_selection[i]
for i in range(16):
data['Attr'+str(i+1)] = X_reduced[:,i]
Deferred_selection['Attr'+str(i+1)] = Deferred_selection_reduced[:,i]
Построим и посмотрим на распределения признаков.
for i in data.drop(['target'], axis = 1).columns:
fig, ax = plt.subplots(ncols=3, sharey=True, figsize=(17,4))
sns.distplot(data[data['target'] == 0][i], ax=ax[0])
sns.distplot(data[data['target'] == 1][i], ax=ax[0], color="orange")
sns.distplot(data[data['target'] == 0][i], ax=ax[1])
sns.distplot(data[data['target'] == 1][i], ax=ax[2], color="orange")
Видно, что все признаки распределены на достаточно большом отрезке, причем большая часть элементов сосредоточена в центре распределения, а на концах лишь носители нулевого класса. Поэтому предположим, что экстремальные значения признаков характерны только для нулевого класса. На основе этого добавим простой классификатор, если точка не лежит внутри некоторой области в признаковом пространстве, то мы автоматически классифицируем её нулевым признаком. Данный подход опасен тем, что, зная лишь часть данных, можно переобучиться на них и построить слишком жесткие рамки, постараемся этого избежать
Для аппроксимации границы области воспользуемся классифицирующем деревом. Далее анализ опущен ввиду своей громоздкости, но его этапы были следующими:
Построение границ для каждого из признаков;
Соотнесение границы с нашими данными.
В процессе второго этапа, почти во всех случаях граница была увеличена не менее чем в 1.5 раза, для снижения вероятности переобучения
Скорректированные отрезки для признаков имеют следующий вид:
Attr1 -> [-5.37, 3.3]
Attr2 -> [-19.85, 41.63]
Attr3 -> [-16.36, 54.2]
Attr4 -> [-6, 44.01]
Attr5 -> [-7.56, 4.44]
Attr6 -> [-3.09, 0.135]
Attr7 -> [-3.15, 3.15]
Attr8 -> [-11.895, 8]
Attr9 -> [-10, 2]
Attr10 -> [-5, 5]
Attr11 -> [-5, 5]
Attr12 -> [-5, 5]
Attr13 -> [-5, 5]
Attr14 -> [-5, 5]
Attr15 -> [-2.09, 1.19]
Attr16 -> [-5, 8]
Уберем из рассмотрения все примеры, где хотя бы один из признаков лежит в экстремальных значениях.
Barrier = {'1' : [-5.37, 3.3], '2' : [-19.85, 41.63], '3' : [-16.36, 54.2],
'4' : [-6, 44.01], '5' : [-7.56, 4.44], '6' : [-3.09, 0.135],
'7' : [-3.15, 3.15], '8' : [-11.895, 8], '9' : [-10, 2],
'10' : [-5, 5], '11' : [-5, 5], '12' : [-5, 5],
'13' : [-5, 5], '14' : [-5, 5], '15' : [-2.09, 1.19],
'16' : [-5, 8]}
def Check_Barrier(b, string):
for i in b.keys():
if string[int(i)] < b[i][0] or string[int(i)] > b[i][1]:
return False
return True
X_new = [];
XX = np.array(data)
for i in XX:
if Check_Barrier(Barrier, i):
X_new += [i]
X_new = np.array(X_new)
help_d = {'target': X_new[:,0]}
df = pd.DataFrame(data=help_d)
for i in range(1, 17):
df['Attr'+str(i)] = np.array(X_new[:,i])
Проведем эту процедуру и для отложенной выборки, разделив её на 2 части.
XX = np.array(Deferred_selection)
X_new = [];
X_old = [];
for i in XX:
if Check_Barrier(Barrier, i):
X_new += [i];
else:
X_old += [i];
X_new = np.array(X_new)
X_old = np.array(X_old)
help_d = {'target': X_new[:,0]}
DSinB = pd.DataFrame(data=help_d)
help_d = {'target': X_old[:,0]}
DSnotinB = pd.DataFrame(data=help_d)
for i in range(1, 17):
DSinB['Attr'+str(i)] = np.array(X_new[:,i])
DSnotinB['Attr'+str(i)] = np.array(X_old[:,i])
Отмасштабируем полученные признаки, отрисуем их и проверим их линейную независимость.
for i in df.drop(['target'], axis = 1).columns:
scaler = StandardScaler()
scaler.fit(df[i])
df[i] = scaler.transform(df[i])
DSinB[i] = scaler.transform(DSinB[i])
for i in df.drop(['target'], axis = 1).columns:
fig, ax = plt.subplots(ncols=3, sharey=True, figsize=(17,4))
sns.distplot(df[df['target'] == 0][i], ax=ax[0])
sns.distplot(df[df['target'] == 1][i], ax=ax[0], color="orange")
sns.distplot(df[df['target'] == 0][i], ax=ax[1])
sns.distplot(df[df['target'] == 1][i], ax=ax[2], color="orange")
sns.set(font_scale=2.5)
plt.figure(figsize=(60,30))
corr_matrix=df.corr()
sns.heatmap(corr_matrix,annot=True,fmt = ".2f",cbar = True,cmap='PuOr',annot_kws={"size":17})
К сожалению у нас снова появились скоррелированные признаки, но масштаб бедствий не так серьезен, как раньше.
Перед тем, как перейти к созданию пол. признаков определимся с основными моделями задачи. Будем использовать логистическую регрессию и случайный лес. Найдем для каждого класса лучшие параметры модели.
RANDOM_STATE = 0
forrest_param = {'n_estimators' : range(50, 160, 10), 'max_depth' : range(1,5), 'min_samples_leaf': range(1,10)}
clf = RandomForestClassifier(random_state = RANDOM_STATE, n_estimators = 50, max_depth = 10, min_samples_leaf = 5)
tree_grid = GridSearchCV(clf, forrest_param, cv=30, n_jobs=-1, scoring = 'roc_auc')
tree_grid.fit(df.drop(['target'], axis=1), df['target'])
print(tree_grid.best_params_)
print(tree_grid.best_score_)
log_param = {'penalty' : ['l1', 'l2'], 'C' : np.linspace(0.0001, 1.0, 1000)}
clf_log = LogisticRegression(random_state = RANDOM_STATE, class_weight = 'balanced');
log_grid = GridSearchCV(clf_log, log_param, cv=30, n_jobs=-1, scoring = 'roc_auc')
log_grid.fit(df.drop(['target'], axis=1), df['target'])
print(log_grid.best_params_)
print(log_grid.best_score_)
Обратим внимание на то, что логистическая регрессия показала лучший результат на кросс-валидации, получим результат предсказания моделей для отложенной выборки. Будем строить результат для её урезанной части, т.к. для остальной части результат известен и он завысит качество модели.
clf = RandomForestClassifier(n_estimators = 50, max_depth = 2, min_samples_leaf = 7, class_weight = 'balanced', random_state = 0)
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
print('random forrest roc_auc score = ', roc_auc_score(DSinB['target'], pred[:,1]))
LogisticRegression(C = 0.039135135135135134, penalty = 'l2', random_state = 0, class_weight = 'balanced');
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
print('Logist regression roc_auc score = ', roc_auc_score(DSinB['target'], pred[:,1]))
Построим для каждой из модели пол. признаки. Строить будем следующим образом:
найдем все сочетания произведения признаков;
Каждое произведение добавим к признакам и найдем на сколько повышается качество модели на кросс-валидации;
Отранжируем список и возьмем произведение с максимальным результатом;
Повторять этот алгоритм будем пока произведения признаков будут улучшать качество модели.
Построим биномильные признаки для логистической регрессии.
def Cross_val_score_in_this(clf, X, y, kf):
res = []
for train_index, test_index in kf.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
clf.fit(X_train, y_train);
y_pred = clf.predict_proba(X_test);
res += [roc_auc_score(y_test, y_pred[:,1])];
res = np.array(res);
return res.mean();
def definite_new_columns(clf, df, min_value, cc):
Columns = [i for i in df.drop('target', axis = 1).columns];
Bin_columns = {};
for i in df.drop('target', axis = 1).columns:
Bin_columns[i] = [j for j in Columns];
del Columns[0];
max_value = 0;
max_name = 0;
i_max = 0;
j_max = 0;
for i in Bin_columns.keys():
for j in Bin_columns[i]:
NC = np.array(df[i]);
NC_1 = np.array(df[j]);
df['New_col'] = np.array(df[i])*np.array(df[j])
X_train = np.array(df.drop(['target'], axis=1))
Y_train = np.array(df['target'])
p = Cross_val_score_in_this(clf, X_train, Y_train, kf) - min_value;
if p > 0.0 and p > max_value and i+' x '+j not in cc:
max_value = p;
max_name = i+' x '+j;
i_max = i;
j_max = j;
del df['New_col']
return [i_max, j_max, max_name, max_value]
RANDOM_STATE = 0
stop_columns = []
kf = KFold(n_splits = 21, shuffle = True, random_state = 0)
clf = LogisticRegression(random_state = RANDOM_STATE, class_weight = 'balanced', C = 0.039135135135135134, penalty = 'l2')
X_train = np.array(df.drop(['target'], axis=1))
Y_train = np.array(df['target'])
nc = Cross_val_score_in_this(clf, X_train, Y_train, kf)
while 1 > 0:
r = definite_new_columns(clf, df, nc, stop_columns);
if r[0] == 0:
break;
stop_columns += [r[2]]
if r[3] > 0:
df[r[2]] = np.array(df[r[0]])*np.array(df[r[1]])
stop_columns = [i for i in df.columns]
X_train = np.array(df.drop(['target'], axis=1))
Y_train = np.array(df['target'])
nc = Cross_val_score_in_this(clf, X_train, Y_train, kf)
print(str(r[2])+' '+str(nc))
print('finish')
Качество модели удалось немного повысить, теперь вернемся к исходным признакам и построим биномильные признаки для случайного леса.
vanila = ['target', 'Attr1', 'Attr2', 'Attr3', 'Attr4', 'Attr5', 'Attr6', 'Attr7',
'Attr8', 'Attr9', 'Attr10', 'Attr11', 'Attr12', 'Attr13', 'Attr14',
'Attr15', 'Attr16']
for i in df.columns:
if i not in vanila:
del df[i]
RANDOM_STATE = 0
stop_columns = []
kf = KFold(n_splits = 21, shuffle = True, random_state = 0)
clf = RandomForestClassifier(random_state = RANDOM_STATE, n_estimators = 50, max_depth = 2, min_samples_leaf = 7)
X_train = np.array(df.drop(['target'], axis=1))
Y_train = np.array(df['target'])
nc = Cross_val_score_in_this(clf, X_train, Y_train, kf)
while 1 > 0:
r = definite_new_columns(clf, df, nc, stop_columns);
if r[0] == 0:
break;
stop_columns += [r[2]]
if r[3] > 0:
df[r[2]] = np.array(df[r[0]])*np.array(df[r[1]])
stop_columns = [i for i in df.columns]
X_train = np.array(df.drop(['target'], axis=1))
Y_train = np.array(df['target'])
nc = Cross_val_score_in_this(clf, X_train, Y_train, kf)
print(str(r[2])+' '+str(nc))
print('finish')
Качество модели повысилось незначительно, это можно объяснить самой структурой модели. Случайный лес и так может строить нелинейные поверхности разделения, поэтому добавления нелинейных признаков для него "погоды не сделают".
Посмотрим насколько повысился результат на отложенной выборке.
df['Attr8 x Attr13'] = df['Attr8']*df['Attr13']
df['Attr11 x Attr15'] = df['Attr11']*df['Attr15']
df['Attr10 x Attr10'] = df['Attr10']*df['Attr10']
df['Attr11 x Attr15 x Attr11 x Attr15'] = df['Attr11 x Attr15'] * df['Attr11 x Attr15']
df['Attr9 x Attr16'] = df['Attr10']*df['Attr10']
df['Attr2 x Attr11 x Attr15 x Attr11 x Attr15'] = df['Attr2']*df['Attr11 x Attr15 x Attr11 x Attr15']
df['Attr10 x Attr13'] = df['Attr10']*df['Attr13']
df['Attr7 x Attr10 x Attr13'] = df['Attr7']*df['Attr10 x Attr13']
df['Attr10 x Attr14'] = df['Attr10']*df['Attr14']
df['Attr6 x Attr7'] = df['Attr6']*df['Attr7']
df['Attr14 x Attr11 x Attr15'] = df['Attr14']*df['Attr11 x Attr15']
DSinB['Attr8 x Attr13'] = DSinB['Attr8']*DSinB['Attr13']
DSinB['Attr11 x Attr15'] = DSinB['Attr11']*DSinB['Attr15']
DSinB['Attr10 x Attr10'] = DSinB['Attr10']*DSinB['Attr10']
DSinB['Attr11 x Attr15 x Attr11 x Attr15'] = DSinB['Attr11 x Attr15'] * DSinB['Attr11 x Attr15']
DSinB['Attr9 x Attr16'] = DSinB['Attr10']*DSinB['Attr10']
DSinB['Attr2 x Attr11 x Attr15 x Attr11 x Attr15'] = DSinB['Attr2']*DSinB['Attr11 x Attr15 x Attr11 x Attr15']
DSinB['Attr10 x Attr13'] = DSinB['Attr10']*DSinB['Attr13']
DSinB['Attr7 x Attr10 x Attr13'] = DSinB['Attr7']*DSinB['Attr10 x Attr13']
DSinB['Attr10 x Attr14'] = DSinB['Attr10']*DSinB['Attr14']
DSinB['Attr6 x Attr7'] = DSinB['Attr6']*DSinB['Attr7']
DSinB['Attr14 x Attr11 x Attr15'] = DSinB['Attr14']*DSinB['Attr11 x Attr15']
LogisticRegression(C = 0.039135135135135134, penalty = 'l2', random_state = 0, class_weight = 'balanced');
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
print('Logist regression roc_auc score = ', roc_auc_score(DSinB['target'], pred[:,1]))
vanila = ['target', 'Attr1', 'Attr2', 'Attr3', 'Attr4', 'Attr5', 'Attr6', 'Attr7',
'Attr8', 'Attr9', 'Attr10', 'Attr11', 'Attr12', 'Attr13', 'Attr14',
'Attr15', 'Attr16']
for i in df.columns:
if i not in vanila:
del df[i]
del DSinB[i]
df['Attr1 x Attr1'] = df['Attr1']*df['Attr1']
df['Attr13 x Attr14'] = df['Attr13']*df['Attr14']
DSinB['Attr1 x Attr1'] = DSinB['Attr1']*DSinB['Attr1']
DSinB['Attr13 x Attr14'] = DSinB['Attr13']*DSinB['Attr14']
clf = RandomForestClassifier(n_estimators = 50, max_depth = 2, min_samples_leaf = 7, class_weight = 'balanced', random_state = 0)
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
print('random forrest roc_auc score = ', roc_auc_score(DSinB['target'], pred[:,1]))
Результат на отложенной выборки упал, следовательно новые признаки "подстроены" под обучающие данные. Для леса мы просто не будем их использовать, для логистической регрессии попробуем удалить лишние признаки для повышения качества.
vanila = ['target', 'Attr1', 'Attr2', 'Attr3', 'Attr4', 'Attr5', 'Attr6', 'Attr7',
'Attr8', 'Attr9', 'Attr10', 'Attr11', 'Attr12', 'Attr13', 'Attr14',
'Attr15', 'Attr16']
for i in df.columns:
if i not in vanila:
del df[i]
for i in DSinB.columns:
if i not in vanila:
del DSinB[i]
df['Attr8 x Attr13'] = df['Attr8']*df['Attr13']
df['Attr11 x Attr15'] = df['Attr11']*df['Attr15']
df['Attr10 x Attr10'] = df['Attr10']*df['Attr10']
df['Attr11 x Attr15 x Attr11 x Attr15'] = df['Attr11 x Attr15'] * df['Attr11 x Attr15']
df['Attr9 x Attr16'] = df['Attr10']*df['Attr10']
df['Attr2 x Attr11 x Attr15 x Attr11 x Attr15'] = df['Attr2']*df['Attr11 x Attr15 x Attr11 x Attr15']
df['Attr10 x Attr13'] = df['Attr10']*df['Attr13']
df['Attr7 x Attr10 x Attr13'] = df['Attr7']*df['Attr10 x Attr13']
df['Attr10 x Attr14'] = df['Attr10']*df['Attr14']
df['Attr6 x Attr7'] = df['Attr6']*df['Attr7']
df['Attr14 x Attr11 x Attr15'] = df['Attr14']*df['Attr11 x Attr15']
DSinB['Attr8 x Attr13'] = DSinB['Attr8']*DSinB['Attr13']
DSinB['Attr11 x Attr15'] = DSinB['Attr11']*DSinB['Attr15']
DSinB['Attr10 x Attr10'] = DSinB['Attr10']*DSinB['Attr10']
DSinB['Attr11 x Attr15 x Attr11 x Attr15'] = DSinB['Attr11 x Attr15'] * DSinB['Attr11 x Attr15']
DSinB['Attr9 x Attr16'] = DSinB['Attr10']*DSinB['Attr10']
DSinB['Attr2 x Attr11 x Attr15 x Attr11 x Attr15'] = DSinB['Attr2']*DSinB['Attr11 x Attr15 x Attr11 x Attr15']
DSinB['Attr10 x Attr13'] = DSinB['Attr10']*DSinB['Attr13']
DSinB['Attr7 x Attr10 x Attr13'] = DSinB['Attr7']*DSinB['Attr10 x Attr13']
DSinB['Attr10 x Attr14'] = DSinB['Attr10']*DSinB['Attr14']
DSinB['Attr6 x Attr7'] = DSinB['Attr6']*DSinB['Attr7']
DSinB['Attr14 x Attr11 x Attr15'] = DSinB['Attr14']*DSinB['Attr11 x Attr15']
while 1 > 0:
poly_columns = []
for i in df.columns:
if i not in vanila:
poly_columns += [i]
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
nc = roc_auc_score(DSinB['target'], pred[:,1])
ncmax = 0;
imax = ''
for i in poly_columns:
clf.fit(df.drop(['target', i], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target', i], axis=1))
nc1 = roc_auc_score(DSinB['target'], pred[:,1])
if nc1 > ncmax:
ncmax = nc1;
imax = i;
if ncmax > nc:
print(imax, ncmax)
del df[imax]
del DSinB[imax]
else:
break
Удалив часть ненужных признаков, мы повысили исходный результат.
Добавим к последнему полученному результату строки отделенные построенной нами поверхностью и получим финальный результат.
clf.fit(df.drop(['target'], axis=1), df['target'])
pred = clf.predict_proba(DSinB.drop(['target'], axis=1))
true_result = [i for i in DSinB['target']]
for i in range(len(DSnotinB)):
true_result += [1];
true_pred += [1]
true_result = np.array(true_result)
true_pred = np.array(true_pred)
nc = roc_auc_score(true_result, true_pred)
print(nc)
Результат сильно превосходит baseline, следовательно анализ можно считать успешным. Укажем недостатки проведенного нами анализа:
Данные на последнем этапе еще можно дальше преобразовывать для получения все более точных прогнозов.
Способ, которым мы обрезали данные, не совсем корректен. Он сильно зависит от самих данных и в реальных бизнес задачах для грамотной расстановки границ нужно привлекать эксперта в данной области.