Пришло время для нового приключения: первый день Serie A Data Scientist только начался!

Пока на улице бушует толпа, здесь, в офисе, мы должны начать работать… ммм… давайте сделаем импорт

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn import datasets, svm
from sklearn.tree import DecisionTreeClassifier
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score

Сегодня у нас плохой день: американские владельцы только что прибыли с помпой и обстоятельствами, объявив, что они выиграют Скудетто в течение 3 лет.

Как? ==› Они понятия не имеют… но им нужно было сформировать ожидания.

После того, как шоу закончилось, они приходят к нам, своим недавно нанятым специалистам по данным…! Они приносят приданое в размере 40 миллионов долларов в портфеле, чтобы вложить их в четкие и чрезвычайно подробные идеи о профиле, который им нужен для того, чтобы сделать скачок вперед.

Американцы: ‹‹‹ Купим нападающего! ›››

DS: «…что за нападающий?»

Американцы: ‹‹‹ Купим нападающего! ›››

ДС: «Эммм… Мы поняли, но… центральный нападающий? Вингер?? Быстрый??? Мощный???? Ты хоть знаешь, что…»

Американцы: «‹‹‹Мы всё знаем!››› Всего за 71 миллион долларов нам всё рассказала группа экспертов. Нам дали мегасписок тысяч игроков, данные о стрельбе, пасах, технике, маркировке…‹‹‹Мы все знаем!›››

DS: «… список нападающих, которых мы придумываем…»

Американцы: ‹‹‹… Купим нападающий! ›››

DS: «Хорошо, понятно!!! С 71 миллионом мы знаем жизнь и мнения каждого игрока, существующего в Европе, но мы не знаем, кто из них нападающий…»

… (смущенное молчание)…

Американцы: «…Ты скажи нам…к вечеру…иначе вы все уволены, понимаете?! Да ладно, это не страшно…»

(Дыхание)

«Мы должны быть дипломатичными… — говорю я маленькому пирату, живущему во мне как проводнику, — …осторожно выбирай следующие слова, Ведат. Они могут быть последними в этой компании…

(Дыхание)

«… вы приносите короны и головы побежденных королей к ступеням моего города! Вы оскорбляете мою науку. Вы угрожаете моему народу рабством и увольнением! О, я тщательно подбирал слова, американец. Возможно, тебе следовало сделать то же самое».

Американцы: «Это богохульство! Это безумие!"

"Безумие?!…"

… мой духовный проводник делает паузу для драматического эффекта, а затем на смеси косовско-итальянско-испанского издает леденящий душу крик:

(Вдох… и возвращение в реальность.)

Американцы: «…иначе вы все уволены, понимаете?!…»

Мечты закончились, и пока Ведат кричит в моей голове, а американцы уходят довольные, мой разум начинает вспоминать набор данных, который я нашел в Интернете несколькими днями ранее, и он может нам помочь. Давайте прочитаем это прямо оттуда, так как время истекает:

link = "https://raw.githubusercontent.com/ianni-phd/Datasets/main/2022-12-05_soccer-players.csv"
df = pd.read_csv(link, sep=';', decimal=',')
df.head(11)

… данных не так много, но они полные (и намного дешевле 71 миллиона): по каждому игроку я знаю особенности и то, защитник ли он («Д»), полузащитник (« С") или... да еще и нападающий ("А")!!!

Мы могли бы проанализировать данные и разработать пороговую систему, но нет времени! Выход один:

  • [FIT] обучение модели на тех немногих данных, которые у нас есть;
  • [PREDICT] просит модель предсказать роль каждого из тысяч европейских игроков!

Это может сработать. Какие функции мы собираемся выбрать? Каждый?

"Очевидно…! Всем известно, что сочувствие — очень характерная черта забастовки…»

… подожди, Ведат! Давайте попробуем с двумя прямо сейчас! Выстрел и Отметка кажутся двумя функциями, важными для разделения ролей.

Мы можем попытаться упростить модель и уменьшить количество классов с 3 до n_classes = 2: Strickers vs Rest мира. Давайте воспользуемся методом pd.factorize(), чтобы взять категориальную переменную и преобразовать ее в столбец целых чисел (тот же метод возвращает категории в правильном порядке… полезно !):

dict_2_classes = {"D":"other", "C":"other", "A":"strickers"}
df["Classe"] = df["Classe"].apply(lambda x: dict_2_classes[x])
classes_labels = list(pd.factorize(df["Classe"])[1])
df["Classe"] = pd.factorize(df["Classe"])[0]
n_classes = len(classes_labels)

Хорошо, теперь мы можем взять пару (пара) функций, которые мы определили ранее, помня, что

  • df — это датафрейм… неудобоваримый для моделей!
  • df[pair] по-прежнему является фреймом данных… неудобоваримым для моделей!
  • df[pair].values наконец-то является np.array!!
pair = ["Shoot","Marking"]
X_2_cols = df[pair].values

print(X_2_cols.shape)
X_2_cols

И сейчас? Два признака, чтобы определить, кто нападающий, а кто нет… Упрощая, можно сказать, что:

‹если у тебя хороший бросок и ты очень плохо стреляешь… может ты нападающий!›

К тому же не может быть, чтобы игрок был очень плох и в стрельбе, и в нанесении ударов…

… ммм, может быть, мы должны забыть о ручных правилах: давайте просто покончим с этим и давайте возьмем модель за главную… наше дорогое старое-мудрое Дерево!

Мы обеспечиваем ему броски и маркировку для каждого игрока (X_2_cols), мы говорим, является ли игрок нападающим или нет (y)… и он сделает все остальное. Может быть, мы могли бы также установить какой-то параметр… нет, пожалуйста, не делайте таких рож, я знаю: «МЫ ТОРОПИМСЯ»!

# definition of the model (without defining parameters...!!!)
clf = DecisionTreeClassifier()
# training of the model
clf.fit(X_2_cols, y)

Все, что для этого потребовалось, — это подгонка, и власть в наших руках. Независимо от того, сколько игроков они нам дадут, от стрельбы и маркировки наше дерево скажет нам, является ли он нападающим или мы должны заставить его исчезнуть прямо в люке. Знаешь, наш Оракул тааак непогрешим!… не так ли?

# prediction on the training data
y_prev = clf.predict(X_2_cols)
# accuracy on the training data
accuracy = accuracy_score(y_prev, y)
accuracy
100,0%

Посмотрите, как красиво это синее 100%… позвольте мне перевести это маленькое число: наше Дерево ИДЕАЛЬНО. Конец истории. Статья закончена, Мы лучшие.

Ложь. Наше дерево непогрешимо, НО на игроках оно уже изучено! Мммм ... мы искренне верим, поэтому мы УВЕРЕНЫ, что производительность будет идеальной и для неизвестных игроков. Давай попробуем! Давайте выберем полузащитника и подождем его ответа!

# Forecast Rabiot
print("Rabiot:")
rabiot_shoot_marking = np.array((64,64)).reshape(1,2)
y_prev_rabiot = clf.predict(rabiot_shoot_marking)
print("Features: ", rabiot_shoot_marking)
print("Model prediction: ", y_prev_rabiot)

Плохие новости, центральный полузащитник — это наклейка для нашего Дерева (!!!)… ммм, пора начинать собирать вещи: завтра утром нам придется подправить свое резюме и транслировать его повсюду.

… да, но просто чтобы придать смысл концу этого дня… как, черт возьми, модель создает границу??! Я уверен, что существует метод, показывающий граничную кривую (DecisionBoundaryDisplay), и, к счастью, мы находимся в 2d.

# creating a palette of colors
import matplotlib.colors
cmap_yg_gr_lb = matplotlib.colors.ListedColormap(["yellow", "green", "lightblue"])
cmap_yg_gr_lb

DecisionBoundaryDisplay.from_estimator(
    clf,
    X_2_cols,
    cmap=cmap_yg_gr_lb, 
    response_method="predict",
    xlabel=pair[0],
    ylabel=pair[1],
)

Очевидно, непростой способ отделить синий от желтого… особенно если учесть, что большинство наклеек находится в правом нижнем углу. Но… зачем Древо придумало эти странные правила? Не слишком ли он сосредоточился на тренировочных данных?! Он что, слишком глубоко учился?!?… кто-то убрал это с дороги?!… и КТО?!?!?

DecisionBoundaryDisplay.from_estimator(
    clf,
    X_2_cols,
    cmap=cmap_yg_gr_lb, 
    response_method="predict",
)

# Plot the training points
for i, color in zip(range(n_classes), plot_colors):
    print(i)
    print(color)
    idx = np.where(y == i)
    plt.title("Accuracy on training: "+str(accuracy))
    plt.scatter(
        X_2_cols[idx, 0],
        X_2_cols[idx, 1],
        c=color,
        label=classes_labels[i],
    )
plt.legend()

ЧТО??? Мой пират!!!!!

Тем не менее, в синей области слева виновен не только Ведат: с ним Фелипе Андерсон, у которого стрелковая способность ниже, чем у его коллег…

Да, но... Фелипе нападающий?! Данные часто представляют собой упрощение реальности. Если подумать, реальных ролей на футбольных полях может быть даже больше, чем 11, и мы сгруппируем их всех в «наклейки» и «другие». Тот факт, что Фелипе Андерсон занимает промежуточную позицию между нападающим и полузащитником и классифицируется как нападающий, представляет собой физиологический «шум» в большинстве реальных проблем.

Этот «шум» и аномальные характеристики нашего Пирата заставляют модель создать синюю область слева. Тем не менее, они не единственные аномалии.

На самом деле над его коллегами стоит Мюриэль, чья способность к маркировке выше, чем у других нападающих. Потом есть Ибра, который… тот… Ибра просто делает то, что хочет, и алгоритмы ссссшшшшш..!

Понятно, что у данных есть свои особенности, и алгоритм слишком сильно на них полагается, создавая правила, которые кажутся немного иррациональными.

С одобрения Ибры мы должны попытаться помочь алгоритму, пытаясь спасти его от застревания в очень конкретных случаях. Каким-то образом мы должны ограничить его способность к обучению, чтобы он не… преувеличивал! Итак, мы накладываем

Правило А: модель может создать подгруппу (лист) только в том случае, если подгруппа содержит не менее двух игроков.

Мы можем легко сделать это с помощью параметра min_samples_leaf.

clf = DecisionTreeClassifier(min_samples_leaf=2)
clf.fit(X_2_cols, y)

Производительность модели на известных данных (обучающем наборе) не идеальна, как раньше…

y_prev = clf.predict(X_2_cols)
accuracy = accuracy_score(y_prev, y)
accuracy
95,7%

… но глядя на модель Boundary, кажется, что «дела идут лучше».

После того, как мы бросили вызов Ибре, мы начинаем чувствовать себя всемогущими: мы делаем еще один шаг к более эффективной способности к обобщению и используем еще один параметр, чтобы избежать слишком глубокого обучения модели. Итак, мы накладываем

Правило Б: модель не может иметь слишком много «уровней» по глубине.

Таким образом, мы можем использовать max_depth, чтобы ограничить «объем исследования» модели.

clf = DecisionTreeClassifier( min_samples_leaf = 2, 
                              max_depth = 4
)
clf.fit(X_2_cols, y)

Модель еще немного ухудшается на известных данных, достигая точности менее 95%:

y_prev = clf.predict(X_2_cols)
accuracy = accuracy_score(y_prev, y)
accuracy
94,2%

С другой стороны, мы с оптимизмом смотрим на следующих игроков, потому что шаблон разделения кажется гораздо более объяснимым, чем раньше:

Хорошо, мы использовали только две функции… что, если мы попробуем и все остальные!?!!

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.inspection import DecisionBoundaryDisplay
# reading the datasdet
df = pd.read_csv("https://raw.githubusercontent.com/ianni-phd/Datasets/main/2022-12-05_soccer-players.csv", sep=';', decimal=',')
# defining the classes
dict_2_classes = {"D":"other", "C":"other", "A":"strickers"}
df["Class"] = df["Class"].apply(lambda x: dict_2_classes[x])
classes_labels = list(pd.factorize(df["Class"])[1])
df["Class"] = pd.factorize(df["Class"])[0]
n_classes = len(classes_labels)
features = ["Pass","Marking","Shoot","Control"]
target = ["Class"]
X = df[features].values
y = df["Class"].values
# Parameters
plot_colors = ["ygb" if n_classes==3 else "yb"][0]
# Define the size and aspect of the final plot
fig, ax = plt.subplots(2, 3, figsize=(12, 8))
fig.tight_layout(h_pad=0.5, w_pad=0.5, pad=2.5)
# deciding the features to compare
pairs_to_focus = [["Shoot","Pass"], ["Shoot","Control"], 
                  ["Shoot","Sympathy"], ["Control","Marking"],
                  ["Shoot","Marking"], ["Marking","Pass"]]
for pairidx, pair in enumerate(pairs_to_focus):
    # We only take the two corresponding features
    X_2_cols = df[pair].values
    # Train and result on train (accuracy)
    clf = DecisionTreeClassifier(max_depth=4).fit(X_2_cols, y)
    y_prev = clf.predict(X_2_cols)
    accuracy = np.round(accuracy_score(y_prev, y),3)
    # Plot the decision boundary
    ax = plt.subplot(2, 3, pairidx + 1)
    plt.tight_layout(h_pad=1.5, w_pad=1.5, pad=2.5)
    DecisionBoundaryDisplay.from_estimator(
        clf,
        X_2_cols,
        cmap=cmap_yg_gr_lb,
        response_method="predict",
        ax=ax,
        xlabel=pair[0],
        ylabel=pair[1],
    )
    # Plot the training points
    for i, color in zip(range(n_classes), plot_colors):
        idx = np.where(y == i)
        plt.title("Accuracy: "+str(accuracy))
        plt.scatter(
            X_2_cols[idx, 0],
            X_2_cols[idx, 1],
            c=color,
            label=classes_labels[i],
            edgecolor="black",
            s=15,
        )
plt.suptitle("Decision surface of decision trees trained on pairs of features on the Players dataset")
plt.legend(loc="lower right", borderpad=0, handletextpad=0)
_ = plt.axis("tight")

Мы можем заметить, видя точность, что Стрелять и Контроль недостаточно, чтобы различить стикеры… почему? Потому что здесь много полузащитников с отличным броском и контролем! Мммм, мы кое-что упускаем... например, Pass, который идеально подходил бы для гораздо лучшего различения.

Наконец-то мы можем быть крайне удивлены тем фактом, что Сочувствие не очень помогает…! :)

Несмотря на то, что по причинам визуализации нам нужно было использовать две функции, учитывая, что 3 или более функций сделают алгоритм намного более эффективным для нашей цели классификации. Если мы собираемся сохранить нашу работу до следующего эпизода, мы посмотрим, как это сделать...!!!