from IPython.core.display import HTML
HTML("""
<style>
div.cell { /* Tunes the space between cells */
margin-top:1em;
margin-bottom:1em;
}
div.text_cell_render h1 { /* Main titles bigger, centered */
font-size: 2.2em;
line-height:1.4em;
text-align:center;
}
div.text_cell_render h2 { /* Parts names nearer from text */
margin-bottom: -0.4em;
}
div.text_cell_render { /* Customize text cells */
font-family: 'Times New Roman';
font-size:1.5em;
line-height:1.4em;
padding-left:3em;
padding-right:3em;
}
</style>
""")
По первым 5 минутам игры предсказать, какая из команд победит: Radiant или Dire?
https://inclass.kaggle.com/c/dota-2-win-probability-prediction
Dota 2 — многопользовательская компьютерная игра жанра MOBA. Игроки играют между собой матчи. В каждом матче участвует две команды, 5 человек в каждой. Одна команда играет за светлую сторону (The Radiant), другая — за тёмную (The Dire). Цель каждой команды — уничтожить главное здание базы противника (трон).
Всего в игре чуть более 100 различных героев (персонажей). Герои различаются между собой своими характеристиками и способностями. От комбинации выбранных героев во многом зависит успех команды.
Игроки могут получать золото и опыт за убийство чужих героев или прочих юнитов. Накопленный опыт влияет на уровень героя, который в свою очередь позволяет улучшать способности. За накопленное золото игроки покупают предметы, которые улучшают характеристики героев или дают им новые способности.
После смерти герой отправляется в "таверну" и возрождается только по прошествии некоторого времени, таким образом команда на некоторое время теряет игрока, однако игрок может досрочно выкупить героя из таверны за определенную сумму золота.
Игра заканчивается, когда одна из команд разрушет определенное число "башен" противника и уничтожает трон.
match_id
: идентификатор матча в наборе данныхstart_time
: время начала матча (unixtime)lobby_type
: тип комнаты, в которой собираются игроки (расшифровка в dictionaries/lobbies.csv
)
Наборы признаков для каждого игрока (игроки команды Radiant — префикс rN
, Dire — dN
):
rN_hero
: герой игрока (расшифровка в dictionaries/heroes.csv)rN_level
: максимальный достигнутый уровень героя (за первые 5 игровых минут)rN_xp
: максимальный полученный опытrN_gold
: достигнутая ценность герояrN_lh
: число убитых юнитовrN_kills
: число убитых игроковrN_deaths
: число смертей герояrN_items
: число купленных предметовПризнаки события "первая кровь" (first blood). Если событие "первая кровь" не успело произойти за первые 5 минут, то признаки принимают пропущенное значение
first_blood_time
: игровое время первой кровиfirst_blood_team
: команда, совершившая первую кровь (0 — Radiant, 1 — Dire)first_blood_player1
: игрок, причастный к событиюfirst_blood_player2
: второй игрок, причастный к событиюПризнаки для каждой команды (префиксы radiant_
и dire_
)
radiant_bottle_time
: время первого приобретения командой предмета "bottle"radiant_courier_time
: время приобретения предмета "courier" radiant_flying_courier_time
: время приобретения предмета "flying_courier" radiant_tpscroll_count
: число предметов "tpscroll" за первые 5 минутradiant_boots_count
: число предметов "boots"radiant_ward_observer_count
: число предметов "ward_observer"radiant_ward_sentry_count
: число предметов "ward_sentry"radiant_first_ward_time
: время установки командой первого "наблюдателя", т.е. предмета, который позволяет видеть часть игрового поляИтог матча (данные поля отсутствуют в тестовой выборке, поскольку содержат информацию, выходящую за пределы первых 5 минут матча)
duration
: длительностьradiant_win
: 1, если победила команда Radiant, 0 — иначеtower_status_radiant
tower_status_dire
barracks_status_radiant
barracks_status_dire
# магическая функция, чтобы графики рисовались в ноутбуке, а не в отдельном окне
%matplotlib inline
import matplotlib # графики
import numpy as np # библиотека для вычислений, там есть все полезные математические функции
import matplotlib.pyplot as plt # графики
import pandas as pd # утилиты для работы с данными, умеет csv, sql и так далее...
pd.set_option('display.max_columns', 500)
import seaborn as sns # красивые графики
sns.set_style("dark")
plt.rcParams['figure.figsize'] = 16, 12 # увеличиваем размер картинок
import datetime
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier, GradientBoostingClassifier, VotingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.linear_model import SGDClassifier, Lasso, RidgeClassifier, LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.base import BaseEstimator, TransformerMixin
from xgboost import XGBClassifier
from sklearn.preprocessing import PolynomialFeatures
from scipy import sparse
# df - data frame.
# Загружаем тренировочную выборку
df = pd.read_csv("data/features.csv.zip", compression='zip', index_col='match_id')
df.head()
Для начала посмотрим информацию о датафреме и базовые характеристики признаков
df.info()
df.describe()
Если внимательно посмотреть, то мы увидим что в данных есть пропуски - NaN значения.
Для начала посмотрим какие признаки имеют пропуски
nan_features = [i for i, v in df.count().iteritems() if v < df.shape[0]]
nan_features
df[nan_features].describe()
Так как у нас есть только первые 5 минут игры, то некоторые характеристки еще не были заполнены. Напрмимер, время покупки курьера или first blood.
Интересные детали в данных:
Предлагаю время заполнить max + std
Для категориальных признаков завести специальный номер
def fill_na(df, nan_features):
times = [f for f in nan_features if f.find('time') != -1]
categorical = [f for f in nan_features if f.find('time') == -1]
df[times] = df[times].fillna(301.0)
df[categorical] = df[categorical].fillna(-1)
return df
fill_na(df, nan_features)
[i for i, v in df.count().iteritems() if v < df.shape[0]]
def get_X(df):
return df.drop(['start_time', 'duration', 'radiant_win', 'tower_status_radiant', 'tower_status_dire','barracks_status_radiant', 'barracks_status_dire'], axis=1)
def get_Y(df):
return df['radiant_win']
X = get_X(df)
y = get_Y(df)
support = np.linspace(0, 1, df.shape[0])
plt.plot(support, df['first_blood_time'].sort_values())
В качестве базового предсказателя часто берут какой-то константный предсказатель, чтобы понять, что хуже него ни в коем случае быть нельзя.
В нашем случае выборка сбалансированная, поэтому если мы будем просто предсказывать что победит вторая команда, то мы получим точность в 50%.
# почти сбалансированные данные, отлично
sns.countplot(data=df, x='radiant_win')
clf = DummyClassifier(random_state=42)
scores = cross_val_score(clf, X, y, scoring='roc_auc')
print("ROC_AUC: mean={}, std={}".format(scores.mean(), 2*scores.std()))
Иногда полезно попробовать парочку моделей на чистых признаках без предподготовки. Это хорошая стартовая линия, по ней мы сможем ориентироваться, улучшаем ли мы модель или нет.
Можно поробовать:
Для оценки качества предсказания будем пользоваться кросс-валидацией. Это когда данные разбивают случайным образом на наборы, тренируются на них и предсказывают, а потом получается среднее значение качества и погрешность.
В задачах бинарной классификации можно использовать метрики accuracy, roc_auc, f1, log_loss, precision, recall.
1- хорошо, 0 - плохо $$F1 = 2 * \frac{precision * recall}{precision + recall} = \frac{2TP}{2TP + FP + FN}$$
def examine(clf, X, y, scoring='roc_auc'):
cv = KFold(n_splits=5, shuffle=True, random_state=42)
start_time = datetime.datetime.now()
scores = cross_val_score(clf, X, y, scoring=scoring, cv=cv, n_jobs=4)
end_time = datetime.datetime.now()
result = (clf.__class__.__name__, scoring, scores.mean(), scores.std()*2, end_time-start_time)
return result
def print_report(result):
print('{0}. {1} {2:.5f} +-{3:.5f}. Time to fit: {4}'.format(result[0], result[1], result[2], result[3], result[4]))
classifiers = [
DummyClassifier(random_state=42),
Lasso(random_state=42),
RidgeClassifier(random_state=42),
SGDClassifier(random_state=42),
AdaBoostClassifier(random_state=42),
GradientBoostingClassifier(n_estimators=30, random_state=42),
RandomForestClassifier(n_estimators=30, random_state=42),
]
results = [examine(clf, X, y) for clf in classifiers]
results = sorted(results, key=lambda el: el[2])
for r in results:
print_report(r)
Достаточно ли хорошее качество получилось у нас? Может быть и невозможно предсказать лучше? Но стоит учесть, что мы совсем никак не подготавливали признаки. Мы не показали что там есть какие-то закономерности. Категориальные признаки мы просто рассматривали как числа, что не совсем верно(откуда между категориями может появиться отношение порядка?) и так далее.
Поэтому далее нужно анализировать признаки. Зафиксируем какой-нибудь алгоритм классификации и будем проверять, как он реагирует на новые признаки.
К счастью, некоторые модели внутри себя содержат информацию о важности признаков для них. В первую очередь стоит посмотреть на эти данные.
def describeImportance(clf, X):
indices = np.argsort(clf.feature_importances_)[::-1]
for f in range(X.shape[1]):
print('%d. feature %d %s (%f)' % (f + 1, indices[f], X.columns[indices[f]],
clf.feature_importances_[indices[f]]))
def describeCoef(clf, X):
coefs = clf.coef_[0]
indices = np.argsort(np.abs(coefs))[::-1]
for f in range(X.shape[1]):
print('%d. feature %d %s (%f)' % (f, indices[f], X.columns[indices[f]],
coefs[indices[f]]))
clf = RidgeClassifier(normalize=True)
clf.fit(X, y)
describeCoef(clf, X)
clf = RandomForestClassifier(n_estimators=50,random_state=42, n_jobs=4)
clf.fit(X, y)
describeImportance(clf, X)
Один классификатор говорит что деньги и опыт важен и время покупки и использования каких-то предметов, а другой говорит важны убийства персонажей и их уровни. Такое разное "восприятие" из-за неподготовленности признаков. Лучше доаверять в этом плане деревьям, так как для них без разницы как представленны признаки.
Детальный анализ предметной области подсказывает нам, что должны быть важны следующие признаки:
И нам не важны характеристики каждого героя по отдельности, можно посмотреть в целом на команду. Среднее, сумма золота, предметов, опыта и так далее. То есть то, что алгоритм машинного обучения сам не сможет выявить. Это наше эмперическая креативная идея.
Обычно, алгоритмы машинного обучения работаю лучше если распределения признаков близки к нормальным. Если есть смещения то из лучше выровнять. Если есть прерывистые изменения, то их лучше сгладить. Обычно помогают логарифмы, возведения в степени.
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_first_ward_time'], ax=ax1)
sns.distplot(df['radiant_first_ward_time'], ax=ax2)
sns.distplot(df['dire_first_ward_time'] - df['radiant_first_ward_time'], ax=ax3)
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_bottle_time'], ax=ax1)
sns.distplot(df['radiant_bottle_time'], ax=ax2)
sns.distplot(df['dire_bottle_time'] - df['radiant_bottle_time'], ax=ax3)
# Весьма логично, что чем больше разница времени покупки важного артефакта, тем больше шансов победить
df['delta_bottle_time'] = df['dire_bottle_time'] - df['radiant_bottle_time']
h = df.groupby(['delta_bottle_time'])['radiant_win'].sum() / df.groupby(['delta_bottle_time'])['radiant_win'].count()
sns.jointplot(h.index, h.values, kind="reg")
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_tpscroll_count'], ax=ax1)
sns.distplot(df['radiant_tpscroll_count'], ax=ax2)
sns.distplot(df['dire_tpscroll_count'] - df['radiant_tpscroll_count'], ax=ax3)
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_courier_time'], ax=ax1)
sns.distplot(df['radiant_courier_time'], ax=ax2)
sns.distplot(df['dire_courier_time'] - df['radiant_courier_time'], ax=ax3)
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_flying_courier_time'], ax=ax1)
sns.distplot(df['radiant_flying_courier_time'], ax=ax2)
sns.distplot(df['dire_flying_courier_time'] - df['radiant_flying_courier_time'], ax=ax3)
# Очень сильный признак - наличие ботинок
df['delta_flying_courier_time'] = df['dire_flying_courier_time'] - df['radiant_flying_courier_time']
h = df.groupby(['delta_flying_courier_time'])['radiant_win'].sum() / df.groupby(['delta_flying_courier_time'])['radiant_win'].count()
sns.jointplot(h.index, h.values, kind="reg")
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_boots_count'], ax=ax1)
sns.distplot(df['radiant_boots_count'], ax=ax2)
sns.distplot(df['dire_boots_count'] - df['radiant_boots_count'], ax=ax3)
# Очень сильный признак - наличие ботинок
df['delta_boots_count'] = df['radiant_boots_count'] - df['dire_boots_count']
h = df.groupby(['delta_boots_count'])['radiant_win'].sum() / df.groupby(['delta_boots_count'])['radiant_win'].count()
sns.jointplot(h.index, h.values, kind="reg")
fig = plt.figure()
ax1 = plt.subplot2grid((3, 1), (0, 0), colspan=2)
ax2 = plt.subplot2grid((3, 1), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 1), (2, 0), colspan=2)
sns.distplot(df['dire_ward_observer_count'], ax=ax1)
sns.distplot(df['radiant_ward_observer_count'], ax=ax2)
sns.distplot(df['dire_ward_observer_count'] - df['radiant_ward_observer_count'], ax=ax3)
sns.distplot(df['first_blood_time'])
# какая команда сделал first blood тоже влияет
df.groupby(['first_blood_team'])['radiant_win'].sum() / df.groupby(['first_blood_team'])['radiant_win'].count()
"""Метод для подготовки данных.
Убирает все ненужные признаки, оставляет только полезные и проводит аггрегацию некоторых.
Заметим, что у нас теперь нет информации о героях совсем!
"""
def prepareData(X):
X_ = pd.DataFrame(index=X.index)
#X_['first_blood_team'] = X['first_blood_team']
# Для этих признаков лучше больше, поэтому разность в прямом направлении
dire_gold = X['d5_gold'] + X['d4_gold'] + X['d3_gold'] + X['d2_gold'] + X['d1_gold']
radiant_gold = X['r5_gold'] + X['r4_gold'] + X['r3_gold'] + X['r2_gold'] + X['r1_gold']
X_['gold_delta'] = radiant_gold - dire_gold # дельта золота на команду
dire_lh = X['d5_lh'] + X['d4_lh'] + X['d3_lh']+ X['d2_lh'] +X['d1_lh']
radiant_lh = X['r5_lh'] + X['r4_lh'] + X['r3_lh']+ X['r2_lh'] +X['r1_lh']
X_['lh_delta'] = radiant_lh - dire_lh # дельта числа убитых юнитов
dire_items = X['d5_items'] + X['d4_items'] + X['d3_items'] + X['d2_items'] + X['d1_items']
radiant_items = X['r5_items'] + X['r4_items'] + X['r3_items'] + X['r2_items'] + X['r1_items']
X_['items_delta'] = radiant_items - dire_items # дельта числа купленных предметов
dire_boots_count = X['dire_boots_count']
radiant_boots_count = X['radiant_boots_count']
X_['boots_count_delta'] = radiant_boots_count - dire_boots_count # дельта числа ботинок на команду
dire_xp = X['d5_xp'] + X['d4_xp'] + X['d3_xp'] + X['d2_xp'] + X['d1_xp']
radiant_xp = X['r5_xp'] + X['r4_xp'] + X['r3_xp'] + X['r2_xp'] + X['r1_xp']
X_['xp_delta'] = radiant_xp - dire_xp # дельта опыта на команду. Нам не нужны поэтому уровни, так как они зависимы друг от друга, но не совсем линейно
dire_kills = X['d5_kills'] + X['d4_kills'] + X['d3_kills'] + X['d2_kills'] + X['d1_kills']
radiant_kills = X['r5_kills'] + X['r4_kills'] + X['r3_kills'] + X['r2_kills'] + X['r1_kills']
X_['kills_delta'] = radiant_kills - dire_kills # дельта килов героев на команду
X_['tpscroll_count_delta'] = X['radiant_tpscroll_count'] - X['dire_tpscroll_count'] # дельта числа свитков телепортации
X_['ward_observer_count_delta'] = X['radiant_ward_observer_count'] - X['dire_ward_observer_count']
X_['ward_sentry_count_delta'] = X['radiant_ward_sentry_count'] - X['dire_ward_sentry_count']
# Для времени лучше меньше чем больше поэтому разность в другую сторону
X_['bottle_time_delta'] = X['dire_bottle_time'] - X['radiant_bottle_time']
X_['flying_courier_time_delta'] = X['dire_flying_courier_time'] - X['radiant_flying_courier_time']
X_['courier_time_delta'] = X['dire_courier_time'] - X['radiant_courier_time']
X_['first_ward_time_delta'] = X['dire_first_ward_time'] - X['radiant_first_ward_time']
return X_
def drawJointPlots(df):
X = prepareData(df)
y = df['radiant_win']
d = pd.concat([X, y], axis=1)
for column in X.columns:
h = d.groupby([column])['radiant_win'].sum() / d.groupby([column])['radiant_win'].count()
sns.jointplot(h.index, h.values, kind="reg")
drawJointPlots(df)