<jupyter_start><jupyter_text>Aplicação de *Clustering* e *Decision Tree*Esse notebook consiste na aplicação de técnicas de *Machine Learning* em dados de jogadores do  **[Campeonato Brasileiro](https://pt.wikipedia.org/wiki/Campeonato_Brasileiro_de_Futebol)**. As técnicas utilizadas foram **Clustering** e **Decision Tree**.  *Clustering*As técnicas de clusterização são muito importantes para termos *insights* sobre os dados disponíveis. Mesmo sem uma classificação exata dos dados, é possível que possamos tirar conclusões interessantes e reveladoras. Por exemplo, abaixo temos uma imagem que ilustra vários tipos de dados que podem ser agrupados por diferentes técnicas de clusterização.Assim, iremos utilizar os dados do [**Cartola FC**](https://globoesporte.globo.com/cartola-fc/) disponíveis [aqui](https://github.com/henriquepgomide/caRtola) para tentarmos agrupar os jogadores. Os dados são os valores do **Cartola FC** durate os anos de 2014 até 2017.**Iremos utilizar a versão dos dados disponíveis nesse repositório que já estão [limpos e prontos para análise](https://github.com/henriquepgomide/caRtola/blob/master/data/README.mddados_agregados_limposcsv).**  Descrição dos DadosDe acordo com o [fonte dos dados](https://github.com/henriquepgomide/caRtola/blob/master/data/README.md), no total são **17567** instancias e **77** características. Assim, cada linha da base de dados corresponde a uma rodada de uma jogador e as colunas as suas caracteríticas nessa rodada. Para uma visão completa de todas as 77 característica dê uma olhada na fonte, mas abaixo estão alguns dos atributos.| Características | Descrição          ||-----------------|--------------------|| Pontos          | pontuação do jogador                                      || PontosMedia     | média da pontuação do jogador                             || Preco           | preço do jogador                                          || ...             | ...                                                       || FS              | faltas sofridas                                           || PE              | passes errados                                            || A               | assistências                                              || FT              | finalizações na trave                                     || FD              | finalizações defendidas                                   || FF              | finalizações para fora                                    || G               | gols                                                      || ano             | ano dos dados                                             ||...              |...                                                        |  Visualização dos DadosInicialmente, vamos importar os dados e visualizá-los.<jupyter_code># imports
import pandas as pd 
data = pd.read_csv('dados_agregados_limpos.csv')
# brief look at the data
print(data.shape)
data.head(10)<jupyter_output>(17567, 77)<jupyter_text>De fato, podemos notar que temos **77 atributos** e **17567 instancias**.  Agrupar Jogadores - Posições IdealConsidere que um técnico está em dúvida sobre qual posição ele deveria escalar um dado jogador. Assim, queremos saber **"Qual a posição ideal de uma jogador segundo suas estastísticas?"**. Para isso, iremos **agrupar** os jogadores de acordo com as suas **métricas** e verificar quais as posições estão mais associadas a esses conjuntos.Antes, vamos modificar os dados iniciais de forma que cada linha represente o desempenho de um jogador na temporada (no caso o ano de 2017) e consideraremos a **média** de cada jogador nesse intervalo. Além disso, alguns dados **não estastísticos** dos jogodores serão desconsiderados. Ainda será removida a posição de técnico.<jupyter_code># Filter 2017 season and Remove 'tec' from columns 'Posicao'
data_2017 = data[(data['ano'] == 2017) & (data['Posicao'] != 'tec')]

# Categorical Features
cat_col = ['Status', 'variable']
dummies_df = pd.get_dummies(data_2017[cat_col])
data_2017 = pd.concat([data_2017, dummies_df], axis=1)
data_2017 = data_2017.drop(cat_col, axis=1)

# Numeric columns
columns = ['AtletaID','FS', 'PE', 'A', 'FT', 'FD', 'FF', 'G', 'I', 'PP', 'RB', 'FC', 'GC', 'CA', 'CV', 'SG', 'DD', 'DP', 'GS', 'Preco']
columns.extend(dummies_df.columns.values)

# Actual Position
data_positions = data_2017.groupby(by=['AtletaID']).max()['Posicao']

# Filters numeric values in the season 
data_2017_players = data_2017[columns].groupby(by=['AtletaID']).mean()

print(data_2017_players.shape)
print(len(data_positions))
data_2017_players.head()<jupyter_output>(696, 26)
696<jupyter_text>Assim, ficamos com um total de **696 atletas** e o total de **26 estatísticas** ao longo da temporada. Redimensionar os AtributosAntes de aplicarmos os algoritmos de clusterização, devemos reduzir a escala dos atributos para que cada um tenha uma influência uniforme. Para isso, importamos e aplicamos a classes `StandardScaler` do `scikit-learn`.<jupyter_code># Imports
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# Scaler
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data_2017_players)<jupyter_output><empty_output><jupyter_text>KMeansAgora podemos aplicar o algoritmo *KMeans* para um total esperado de 5 grupos que representam as 5 posições dos jogadores.- Goleiro- Zagueiro- Lateral- Meia- Atacante<jupyter_code># Imports
from sklearn.cluster import KMeans

# Model with 5 clusters
kmeans_2017_5 = KMeans(n_clusters=5, random_state=42)

# Model grouping
labels_5 = kmeans_2017_5.fit_predict(scaled_data)<jupyter_output><empty_output><jupyter_text>Podemos usar a **Matriz de Confusão** para avaliarmos o resultado.<jupyter_code># Creates a Dataframe for the possible positions
pos_df_5 = pd.DataFrame({'Class':labels_5, 'Position':data_positions})

ct_5 = pd.crosstab(pos_df_5['Class'], pos_df_5['Position'])
ct_5<jupyter_output><empty_output><jupyter_text>Podemos ver que com exceção do **Goleiro**, os dados não foram suficientes para conseguirmos separar os jogadores entre suas respectivas posições. Ou ainda, que os jogadores de diferentes posições possuem métricas similares, como fica nítido nos casos  de **Atacante** e **Meia**. Talvez métodos de classificação mais genéricos pudessem ser mais eficientes.Então, considere que as posições dos jogadores sejam definidas por suas características, como descrito abaixo:- **Ataque**: Atacante- **Meio-Campo**: Meia- **Defensa**: Goleiro / Zagueiro / Lateral<jupyter_code># Relabeling function
def generic_positions(x):
    if (x == 'gol' or x == 'zag' or x == 'lat'):
        return 'Defesa'
    elif (x == 'ata'):
        return 'Ataque'
    else:
        return 'Meio-Campo'

# Currect Position
data_positions_3 = data_positions.apply(generic_positions)

# Model with 3 clusters
kmeans_2017_3 = KMeans(n_clusters=3, random_state=42)

# Model grouping
labels_3 = kmeans_2017_3.fit_predict(scaled_data)

# Creates a Dataframe for the possible positions
pos_df_3 = pd.DataFrame({'Class':labels_3, 'Position':data_positions_3})

ct_3 = pd.crosstab(pos_df_3['Class'], pos_df_3['Position'])
ct_3<jupyter_output><empty_output><jupyter_text>Podemos notar que os jogadores definitivamente defensivos podem ser definidos pela **classe 1**, enquanto que outras classes mostram jogadores com características mais híbridas entre **Defesa**, **Meio-Campo** e **Ataque**.**Assim, talvez essas classificações ajudem o treinador a colocar o jogador em uma posição mais defensiva/ofensiva.**  t-SNEApesar do problema ter 19 dimensões, podemos usar técnicas para tentarmos visualizar o resultado em gráficos bidimensionais. Umas dessas técnicas é *t-SNE* e iremos aplicá-la abaixo.<jupyter_code># Labels
labels = pos_df['position'].astype('category').cat.codes
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

# TSNE model
model = TSNE(learning_rate=100)
transformed = model.fit_transform(scaled_data)

xs = transformed[:, 0]
ys = transformed[:, 1]
plt.scatter(xs, ys, c=labels)
plt.show()<jupyter_output><empty_output><jupyter_text>Pode-se notar que os dados são bastante sobrepostos, o que corrobora com os resultados obtidos no **KMeans**.  *Decision Tree*Agora, vamos tentar criar um modelo de árvore de decisão que tente classificar a posição de um jogador de acordo com suas métricas. Resumidamente, a árvore de decisão é método usado tanto em classificação quanto em regressão que tenta criar uma sequencia de condições que melhor separe os dados.Como nas etapas anteriores já prepareamos os dados, podemos iniciar a sequencia para a criação de um modelo de *Machine Learning*. Iniciando pela divisão dos dados entre **Treinamento** e **Teste**.<jupyter_code>from sklearn.model_selection import train_test_split

# Train / Test Split
X_train, X_test, y_train, y_test = train_test_split(scaled_data, labels, test_size=0.25, random_state=42)

print(X_train.shape)
print(X_test.shape)<jupyter_output>(522, 26)
(174, 26)<jupyter_text>Assim, temos um total de **522** instancias para treinamento e **174** para validação.Finalmente, podemos criar e aplicar o **Modelo**.<jupyter_code>from sklearn.tree import DecisionTreeClassifier

# Label binarizer necessary for 1 hot encode y
y_train = label_binarize(y_train, classes= [0, 1, 2, 3, 4])
y_test = label_binarize(y_test, classes= [0, 1, 2, 3, 4])

dtc = DecisionTreeClassifier(random_state=42)
dtc.fit(X_train, y_train)<jupyter_output><empty_output><jupyter_text>Agora podemos aplicar métricas ao modelo e analisar o seu desempenho. Essa métrica é a **Área sob a Curva ROC**.<jupyter_code>from sklearn.metrics import accuracy_score
from sklearn.preprocessing import label_binarize

# Predictions to train values
train_pred = dtc.predict(X_train)

# Predictions to test values
test_pred = dtc.predict(X_test)

# Errors
train_error = roc_auc_score(train_pred, y_train)
test_error = roc_auc_score(test_pred, y_test)

print("Error in train data: " + str(train_error))
print("Error in test data: " + str(test_error))
print("Max depth: " + str(dtc.tree_.max_depth))<jupyter_output>Error in train data: 1.0
Error in test data: 0.7901187381647625
Max depth: 16<jupyter_text>Nota-se que o modelo está ***Overfitting*** os dados de treinamento. Iremos tentar ajustar esse problema de Variância **reduzindo o tamanho da árvore** de 16 para no máximo 9.<jupyter_code>dtc = DecisionTreeClassifier(random_state=42, max_depth=9)
dtc.fit(X_train, y_train)

# Predictions to train values
train_pred = dtc.predict(X_train)

# Predictions to test values
test_pred = dtc.predict(X_test)

# Errors
train_error = roc_auc_score(train_pred, y_train)
test_error = roc_auc_score(test_pred, y_test)

print("Error in train data: " + str(train_error))
print("Error in test data: " + str(test_error))<jupyter_output>Error in train data: 0.9709368820794255
Error in test data: 0.8303261795823248<jupyter_text>Nota-se que a diferença de performance do modelo no treinamento e teste deminui, entranto ainda há *overfitting*. Vamos alterar outros **hiperparâmetros** do modelo em busca de resultados melhores. Outros parâmetros e seus significados estão mostrados abaixo.Parâmetro | Descrição---|---`max_depth` | Profundidade máxima da árvore`min_sample_leaf` | Número mínimo de amostra para que seja um nó de folha`min_samples_split` | Número mínimo de amostra necessparios para dividir um nó interno<jupyter_code>dtc = DecisionTreeClassifier(random_state=42, max_depth=9, min_samples_leaf=15, min_samples_split=5)
dtc.fit(X_train, y_train)

# Predictions to train values
train_pred = dtc.predict(X_train)

# Predictions to test values
test_pred = dtc.predict(X_test)

# Errors
train_error = roc_auc_score(train_pred, y_train)
test_error = roc_auc_score(test_pred, y_test)

print("Error in train data: " + str(train_error))
print("Error in test data: " + str(test_error))<jupyter_output>Error in train data: 0.8921297813693982
Error in test data: 0.8527951460149001
{'fraction_non_alphanumeric': 0.09420350641724687, 'fraction_numerical': 0.024914017280429496, 'mean_word_length': 4.1387931034482754, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 43, 'https://': 5, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 28, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '12', 'dir': 'jupyter-structured-clean-dedup', 'id': '628474', 'license': 'MIT', 'n_tokens_mistral': 4196, 'n_tokens_neox': 3944, 'n_words': 1186, 'path': 'Clustering.ipynb', 'repo_name': 'Fernandohf/caRtola', 'size': '11921'}