Laboratorio 7: Aprendizaje Supervisado 🔮

MDS7202: Laboratorio de Programación Científica para Ciencia de Datos

Cuerpo Docente:¶

  • Profesor: Matías Rojas y Mauricio Araneda
  • Auxiliar: Ignacio Meza D.
  • Ayudante: Rodrigo Guerra

Equipo: SUPER IMPORTANTE - notebooks sin nombre no serán revisados¶

  • Nombre de alumno 1: Johnny Godoy

Link de repositorio de GitHub: https://github.com/johnny-godoy/laboratorios-mds/blob/main/lab%207/laboratorio_7.ipynb¶

Temas a tratar¶

  • Aprendizaje Supervisado
  • Flujos de datos a través de Pipelines.

Reglas¶

  • Fecha de entrega: TBD
  • Grupos de 2 personas
  • Ausentes deberán realizar la actividad solos.
  • Cualquier duda fuera del horario de clases al foro. Mensajes al equipo docente serán respondidos por este medio.
  • Prohibidas las copias.
  • Pueden usar cualquer matrial del curso que estimen conveniente.

Objetivos principales del laboratorio¶

  • Comprender el funcionamiento de clasificadores/regresores.
  • Generar múltiples modelos predictivos.
  • Comprender las ventajas de crear modelos en pipeline vs hacer las operaciones a mano.

El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega pandas, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre DataFrames.

Importamos librerias utiles 😸¶

In [1]:
from __future__ import annotations

# Libreria Core del lab.
import numpy as np
import pandas as pd
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split

# Pre-procesamiento
from sklearn.dummy import DummyClassifier, DummyRegressor
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import FunctionTransformer
from sklearn.preprocessing import PowerTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import OrdinalEncoder

# Metricas de evaluación
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import cohen_kappa_score

# Clasificadores
from sklearn.svm import LinearSVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier

# Regresores
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR

#Libreria para plotear
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go

pd.options.plotting.backend = "plotly"

https://www.kaggle.com/antoinekrajnc/soccer-players-statistics

1. Predicciones Futboleras¶

Tras el trágico despido de la mítica mascota de Maipú, Renacín decide adentrarse como consultor en el mercado futbolero, el cuál (para variar...) está cargado en especulaciones.

Como su principal tarea será asesorar a los directivos de los clubes sobre cuál jugador comprar y cuál no, Renacín desea generar modelos predictivos que evaluén distintas características de los jugadores; todo con el fin de tomar decisiones concretas basadas en los datos.

Sin embargo, su condición de corporeo le impidió tomar la versión anterior de MDS7202, por lo que este motivo Renacín contrata a su equipo para lograr su objetivo final. Dado que aún tiene fuertes vínculos con la dirección de deportes de la municipalidad, el corporeo le entrega base de datos con las estadísticas de cada jugador para que su equipo empieze a trabajar ya con un dataset listo para ser usado.

Los Datos

Para este laboratorio deberán trabajar con los csv statsplayers.csv y salarios.pickle, donde deberán aplicar algoritmos de de aprendizaje supervisado (clasificación y regresión) en base a características que describen de jugadores de futbol.

Para comenzar cargue el dataset señalado y a continuación vea el reporte Player_Stats_Report.html (adjunto en la carpeta del enunciado) que describe las características principales del DataFrame.

In [2]:
df_players = pd.read_csv('data/stats_players.csv')
df_players
Out[2]:
Name Nationality National_Position Club_Position Height Weight Preffered_Foot Age Work_Rate Weak_foot ... Agility Jumping Heading Shot_Power Finishing Long_Shots Curve Freekick_Accuracy Penalties Volleys
0 Cristiano Ronaldo Portugal LS LW 185 80 Right 32 High / Low 4 ... 90 95 85 92 93 90 81 76 85 88
1 Lionel Messi Argentina RW RW 170 72 Left 29 Medium / Medium 4 ... 90 68 71 85 95 88 89 90 74 85
2 Neymar Brazil LW LW 174 68 Right 25 High / Medium 5 ... 96 61 62 78 89 77 79 84 81 83
3 Luis Suárez Uruguay LS ST 182 85 Right 30 High / Medium 4 ... 86 69 77 87 94 86 86 84 85 88
4 Manuel Neuer Germany GK GK 193 92 Right 31 Medium / Medium 4 ... 52 78 25 25 13 16 14 11 47 11
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
17583 Adam Dunbar Republic of Ireland NaN Sub 183 82 Right 19 Medium / Medium 1 ... 27 56 14 16 13 13 11 13 15 12
17584 Dylan McGoey Republic of Ireland NaN Sub 185 80 Right 19 Medium / Medium 2 ... 28 53 12 17 12 11 12 13 16 12
17585 Tommy Ouldridge England NaN Res 173 61 Right 18 High / Medium 2 ... 54 61 41 44 28 42 35 36 42 37
17586 Mark Foden Scotland NaN Sub 180 80 Right 21 Medium / Medium 3 ... 34 48 15 23 14 12 13 12 24 12
17587 Barry Richardson England NaN Sub 185 77 Right 47 Medium / Medium 2 ... 38 51 12 13 11 16 12 11 22 12

17588 rows × 39 columns

1.1 Predicción de Seleccionados Nacionales¶

Como primera tarea, Renacín, intrigado por la posibilidad de saber qué tan reconocido es un jugador, le consulta a su equipo si es posible predecir si un jugador será o no seleccionado nacional a partir de sus estadísticas en el juego.

1.1.1 Generación de Labels para la Clasificación [Sin Puntaje]¶

Primero comience generando las labels para la clasificación. Para esto, trabaje sobre el atributo National_Position suponiendo que los valores nulos son jugadores no seleccionados para representar a su país.

Hecho esto, ¿Cuantos ejemplos por cada clase se tienen? Comente lo que observa.

Respuesta:

Valores nulos

In [3]:
print(f"Cantidad de valores nulos: {df_players.National_Position.isna().sum()}")
Cantidad de valores nulos: 16513
In [4]:
print("Valores por cada clase")
counts = df_players.National_Position.value_counts()[::-1]
fig = px.bar(x=counts.values, y=counts.index)
fig.update_layout(title="Cantidad de valores por cada clase",
                  xaxis_title="Cantidad de valores",
                  yaxis_title="Clase",
                 )
fig.show()
Valores por cada clase

Creando la etiqueta

In [5]:
y = ~df_players.National_Position.isna()
y.hist()

Donde observamos un fuerte desbalance de clases (1k vs 16k)

Igualmente, aprovechamos de crear la entrada.

In [6]:
X = df_players.drop("National_Position", axis=1)

1.1.2 Camino a la clasificación [1 punto]¶

Para preprocesar el dataset, genere un ColumnTransformer en donde especifique las transformaciones que hay que realizar para cada columna (por ejemplo StandarScaler, MinMaxScaler, OneHotEncoder, etc...) para que puedan ser utilizadas correctamente por el modelo predictivo y guardelo en algúna variable.

Luego, comente y justifique las transformaciones elegidas sobre cada una de las variables (para esto utilice el material Player_Stats_Report.html que viene en el zip del lab), al igual que las transformaciones aplicadas.

Hecho lo anterior, defina al menos 3 pipelines para la clasificación, en donde utilice el mismo ColumnTransformer definido anteriormente, pero que varie entre cada pipeline los clasificadores.

Para seleccionar los clasificadores más adecuados, utilice la siguiente guía:


Con ella, comente y justifique cada una de las decisiones tomadas al momento de desarrollar su pipeline.

Nota: Si tiene problemas al utilizar OneHotEncoder puede utilizar el parámetro handle_unknown='ignore'. Esto hace que en la codificación se omitan las categorias que no aparecen en el entrenamiento. Pregunta dudosa (no tiene puntaje), ¿esto tiene sentido a nivel de modelos?.

To-Do:

  • [x] Genere un ColumnTransformer enfocado en preprocesar los datos.
  • [x] Indicar y Justificar que preprocesamiento utiliza sobre cada columna.
  • [x] Crear 3 pipelines con diferentes clasificadores.
  • [x] Para seleccionar los clasificadores base sus decisiones en la siguiente guía
  • [x] No entrenar los pipelines aún.

Nota: No es necesario entrenar los clasificadores aún.

Respuesta:

In [7]:
exclude = {"Name", "Nationality", "Club Position",  # Alta cardinalidad
           "National_Position",  # Variable objetivo
          }
In [8]:
def get_col_index(columns: list[str], frame = X) -> list[int]:
    """Retorna los índices de las columnas dadas."""
    return list(np.where(frame.columns.isin(columns))[0])
In [9]:
int_index = get_col_index(X.select_dtypes("int").columns)
float_index = get_col_index(X.select_dtypes("float").columns)
object_index = get_col_index(set(X.select_dtypes("object").columns) - exclude)
club_position_index =  get_col_index(["Club_Position"])


preprocessor = ColumnTransformer([("int_processor", make_pipeline(SimpleImputer(strategy="median"), MinMaxScaler()), int_index),
                                  ("float_processor", make_pipeline(StandardScaler(), SimpleImputer(strategy="mean")), float_index),
                                  ("small_cat_processor", OneHotEncoder(handle_unknown='ignore', drop='first', sparse=False), object_index),
                                  ("large_cat_processor", make_pipeline(SimpleImputer(strategy="most_frequent"), OrdinalEncoder(), MinMaxScaler()), club_position_index),
                                 ])
In [10]:
linear_svc = make_pipeline(preprocessor,
                           LinearSVC())
knn = make_pipeline(preprocessor,
                    KNeighborsClassifier())
random_forest = make_pipeline(preprocessor,
                              RandomForestClassifier())
pipelines = (linear_svc, knn, random_forest)
  • Se excluye Name y Nationality por su altísima cardinalidad
  • Se excluye la variable objetivo
  • Los clasificadores elegidos fueron según el diagrama: Cómo hay menos de 100k datos, se prueba primero LinearSVC, y como no tenemos datos de texto, se usa KNN después. Siguiendo a esto, se utiliza RandomForest como método de ensamblado, aunque se pudo haber utilizado SVC.

El procesamiento se separa como tal:

  • floats: Se imputan según la media (para mantener tendencia central), y se estandariza.
  • ints: Se imputa según la mediana (así el resultado se interpreta como un int), y se normaliza a [0, 1] (pues acá puede tener menos sentido querer que sea estandarizado)
  • objetos distintos de Club_Position: Se imputa según el valor más frecuente (las estrategias numéricas no tienen sentido) y se realiza OneHotEncoding
  • Club_Position: Se imputa como antes, su cardinalidad es muy alta para OneHotEncoding, así que se usa OrdinalEncoding. Después de esto se obtienen ints, así que se normaliza.

1.1.3 Entrenemos los pipelines [1 punto]¶

Ahora, entrene los pipeline generados en los pasos anteriores. Para esto, primero separe los datos de entrenamiento en un conjunto de entrenamiento y de prueba (la proporción queda a su juicio).

En este paso, seleccione los ejemplos de forma aleatoria e intente mantener la distribución original de labels de cada clase en los conjuntos de prueba/entrenamiento. (vea la documentación de train_test_split).

Luego, entrene los pipelines

Una vez entrenado su modelo, evalue su rendimiento a través de diferentes métricas, comentando que significa cada uno de los valores obtenidos. Puede usar la función classification_report para corroborar sus resultados.

  • ¿Qué implican los valores de accuracy, precisión y recall de la clase positiva (la que indica que un jugador es seleccionado nacional)?
  • ¿Podrían mejorarse los resultados?, ¿Cómo?
  • ¿Influye la cantidad de ejemplos por clase?

To-Do:

  • [x] Separar el conjunto de datos en entrenamiento y de prueba.
  • [x] Entrenar los pipelines.
  • [x] Utilizar las métricas para evaluar los modelos generados.

Respuesta:

In [11]:
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0,
                                                    shuffle=True, stratify=y)

Agregamos la predicción Dummy para tener un entendimiento base

In [12]:
dummy_pred = DummyClassifier().fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, dummy_pred))
              precision    recall  f1-score   support

       False       0.94      1.00      0.97      4128
        True       0.00      0.00      0.00       269

    accuracy                           0.94      4397
   macro avg       0.47      0.50      0.48      4397
weighted avg       0.88      0.94      0.91      4397

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

Vemos que por el alto desbalance, este clasificador dice siempre Falso y obtiene 94% de precisión en esta clase. Nos interesará más que vea correctamente quienes sí fueron seleccionados.

In [13]:
for model in pipelines:
    y_pred = model.fit(X_train, y_train).predict(X_test)
    clf = [val for key, val in model.named_steps.items() if val != "pipeline"][-1]
    print(f"Resultados para {clf.__class__.__name__}")
    print(classification_report(y_test, y_pred))
Resultados para LinearSVC
              precision    recall  f1-score   support

       False       0.94      1.00      0.97      4128
        True       0.00      0.00      0.00       269

    accuracy                           0.94      4397
   macro avg       0.47      0.50      0.48      4397
weighted avg       0.88      0.94      0.91      4397

Resultados para KNeighborsClassifier
              precision    recall  f1-score   support

       False       0.94      1.00      0.97      4128
        True       0.46      0.06      0.11       269

    accuracy                           0.94      4397
   macro avg       0.70      0.53      0.54      4397
weighted avg       0.91      0.94      0.92      4397

Resultados para RandomForestClassifier
              precision    recall  f1-score   support

       False       0.94      1.00      0.97      4128
        True       0.57      0.09      0.15       269

    accuracy                           0.94      4397
   macro avg       0.76      0.54      0.56      4397
weighted avg       0.92      0.94      0.92      4397

Respuesta:

LinearSVC tiene el peor desempeño, pues es un modelo simple que no capturó nada mejor que evaluar a todo punto como negativo.

KNN logró un mejor desempeño en clasificar efectivamente a los seleccionados sin perder desempeó para los no seleccionados. Cabe destacar que este método puede sufrir por la maldición de la dimensionalidad, por lo cual no se esperaría que fuera el mejor en esta situación.

RandomForest alivia este problema, ligeramente mejorando los resultados de KNN.

1.2 Predicción de posiciones de jugadores [2 puntos]¶

En una nueva jornada de desmesuradas transacciones deportivas, Renacín escuchó a sus colegas discutir acerca de que el precio de cada jugador depende en gran medida de la posición en la cancha en la que juega. Y además, que hay bastantes jugadores nuevos que no tienen muy claro en que posición verdaderamente brillarían, por lo que actualmente puede que actualmente estén jugando en posiciones sub-optimas.

Viendo que los resultados del primer análisis no son tan esperanzadores, el corporeo los comanda a cambiar su tarea: ahora, les solicita que construyan un clasificador enfocado en predecir la mejor posición de los jugadores en la cancha según sus características.

Para lograr esto, primero, les pide que etiqueten de la siguiente manera los valores que aparecen en el atributo Club_Position, pidiendo que agrupen los valores en los siguientes grupos:

Nota: Renacín les recalca que no deben utilizar los valores Sub y Res de esta columna.

ataque = ['ST', 'CF'] 
central_ataque = ['RW', 'CAM', 'LW'] 
central = ['RM', 'CM', 'LM'] 
central_defensa = ['RWB', 'CDM', 'LWB']
defensa = ['RB', 'CB', 'LB']
arquero = ['GK']

Cabe señalar que al igual como lo realizado con la clasificación binaria, deberá justificar en base a la guía la elección del clasificador y se deben comentar los resultados obtenidos en la clasificación.

Nota: Clasifique solamente con las clases señaladas, si observa mas clases eliminelas de la clasficación.

To-Do:

  • [x] Aplique las etiquetas descritas anteriormente en cada uno de los valores señalados en esta sección.
  • [x] Cuente cuantos por clase quedan.
  • [x] Entrene el nuevo pipeline y ejecute una evaluación de este.
  • [x] Comente los resultados obtenidos.

Respuesta:

Aplicando etiquetas

In [14]:
replace = {"ST": "ataque",
           "CF": "ataque",
           "RW": "central_ataque",
           "CAM": "central_ataque",
           "LW": "central_ataque",
           "RM": "central",
           "CM": "central",
           "LM": "central",
           "RWB": "central_defensa",
           "CDM": "central_defensa",
           "LWB": "central_defensa",
           "RB": "defensa",
           "CB": "defensa",
           "LB": "defensa",
           "GK": "arquero"
          }
posicion = df_players.Club_Position.replace(replace)
posicion = posicion[posicion.isin(set(replace.values()))]
X_new = X.drop("Club_Position", axis=1).iloc[posicion.index]

Contando clases

In [15]:
y_plot = posicion.value_counts()[::-1]
fig = px.histogram(x=y_plot.values, y=y_plot.index)
fig.update_layout(title="Cantidad de valores por cada clase",
                  xaxis_title="Cantidad de valores",
                  yaxis_title="Clase",
                 )
fig.show()

Se realiza el split

In [16]:
X_train, X_test, y_train, y_test = train_test_split(X_new, posicion, random_state=0,
                                                    shuffle=True, stratify=posicion)

Tal como antes, se probarán los clasificadores según la guía dada, pero en este caso, se probarán los clasificadores uno por uno, porque si es suficiente uno de bajo calibre, no necesitamos subirlo.

Las decisiones de transformaciones serán similares a las anteriores, con las consideraciones:

  • Club_Position ya no es variable, así que recalculamos los índices
  • Cada clasificador puede tener su propio procesamiento, dependiendo de como este funcione.
In [17]:
int_index = get_col_index(X_new.select_dtypes("int").columns, X_new)
float_index = get_col_index(X_new.select_dtypes("float").columns, X_new)
object_index = get_col_index(set(X_new.select_dtypes("object").columns) - exclude)

Nuevamente, miremos el Dummy

In [18]:
dummy_pred = DummyClassifier().fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, dummy_pred))
                 precision    recall  f1-score   support

        arquero       0.00      0.00      0.00       158
         ataque       0.00      0.00      0.00       108
        central       0.00      0.00      0.00       227
 central_ataque       0.00      0.00      0.00       145
central_defensa       0.00      0.00      0.00        52
        defensa       0.30      1.00      0.46       295

       accuracy                           0.30       985
      macro avg       0.05      0.17      0.08       985
   weighted avg       0.09      0.30      0.14       985

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\metrics\_classification.py:1327: UndefinedMetricWarning:

Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.

Solamente predice la clase mayoritaria defensa, y por eso los pésimos resultados: Solamente 0.08 de f1-score como macro promedio.

Hay menos de 100k datos, así que empezamos por LinearSVC

In [19]:
svc_processor = ColumnTransformer([("int_processor", make_pipeline(SimpleImputer(strategy="median"), MinMaxScaler()), int_index),
                                   ("float_processor", make_pipeline(StandardScaler(), SimpleImputer(strategy="mean")), float_index),
                                   ("small_cat_processor", OneHotEncoder(handle_unknown='ignore', drop='first', sparse=False), object_index),
                                 ])
linear_svc = make_pipeline(svc_processor, LinearSVC())
y_pred = linear_svc.fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, y_pred))
                 precision    recall  f1-score   support

        arquero       1.00      1.00      1.00       158
         ataque       0.76      0.86      0.81       108
        central       0.61      0.66      0.63       227
 central_ataque       0.53      0.30      0.39       145
central_defensa       0.62      0.19      0.29        52
        defensa       0.80      0.98      0.88       295

       accuracy                           0.75       985
      macro avg       0.72      0.66      0.67       985
   weighted avg       0.73      0.75      0.73       985

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

Este modelo logra un desempeño perfecto para los arqueros, y bastante aceptable en general, con el resultado más débil siendo un mal recall para central_defensa que es la categoría minoritaria. Con un macro promedio de 0.67 del f1-score, uno podría quedar satisfecho con esto, pero vale la pena seguir probando otros modelos. Seguimos según la guía con KNN, que puede usar el mismo procesamiento sin problema

In [20]:
knn = make_pipeline(svc_processor, KNeighborsClassifier())
y_pred = knn.fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, y_pred))
C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

                 precision    recall  f1-score   support

        arquero       1.00      1.00      1.00       158
         ataque       0.65      0.73      0.69       108
        central       0.50      0.56      0.52       227
 central_ataque       0.41      0.24      0.30       145
central_defensa       0.13      0.04      0.06        52
        defensa       0.74      0.88      0.81       295

       accuracy                           0.67       985
      macro avg       0.57      0.57      0.56       985
   weighted avg       0.64      0.67      0.65       985

En cada clase, estos resultados son peores que el modelo lineal.

Progresamos con RandomForest, que como está basado en árboles, no requiere escalamiento (mejorando interpretabilidad si se quiere), y puede lidiar con OrdinalEncoding en vez de OneHotEncoding, lo cual en ocasiones mejora el desempeño.

In [21]:
tree_processor = ColumnTransformer([("int_processor", SimpleImputer(strategy="median", add_indicator=True), int_index),
                                   ("float_processor", SimpleImputer(strategy="mean", add_indicator=True), float_index),
                                   ("small_cat_processor", OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1,
                                                                          encoded_missing_value=-2), object_index),
                                 ])
tree = make_pipeline(tree_processor, RandomForestClassifier())
y_pred = tree.fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, y_pred))
                 precision    recall  f1-score   support

        arquero       1.00      1.00      1.00       158
         ataque       0.79      0.83      0.81       108
        central       0.59      0.71      0.64       227
 central_ataque       0.56      0.28      0.37       145
central_defensa       0.75      0.17      0.28        52
        defensa       0.80      0.97      0.88       295

       accuracy                           0.75       985
      macro avg       0.75      0.66      0.66       985
   weighted avg       0.74      0.75      0.73       985

Obtenemos resultados muy similares a LinearSVC. Si queremos mejorar los resultados, podemos seguir con la guía y utilizar:

  • SVC con distintos kernels
  • ExtraTreesClassifier, la variante de RF dada por sklearn
  • Una variante de Boosting dada por sklearn (Gradient, Histogram, Adaptative)

1.3 Predicción de Sueldos [2 puntos]¶

Queriendo ahondar aún más en el mercado del balompíe, Renacin, logra obtener (de una manera no muy formal) los sueldos de múltiples futbolistas y los guarda en el archivo sueldos.csv. Con ellos les solicita que generen un regresor que les permita predecir el sueldo de los futbolistas en base a las características de los pichichis, esto, debido a su motivación por invertir y/o realizar especulación sobre los sueldos de jugadores.

Renacin es claro señalando que deben seguir utilizando la guía y comenten cada uno de los pasos realizados, para obtener su regresión lineal. Señalándoles que no aceptara un $R^2$ inferior a 0.35 para el modelo solicitado.

Para esta parte usted tiene total libertad en la generación del regresor, la unica exigencia es que utilice un pipeline para generar la regresión y utilice la metrica $R^2$ para medir el rendimiento de esta.

To-Do:

  • [x] Explique en que consiste la métrica $R^2$
  • [x] Generar un pipeline para la regresión.
  • [x] Obtener un regresor con un $R^2$ superior a $0.35$.
  • [x] Comente sus resultados y si es posible mejorar los resultados obtenidos. ¿Se necesitarían más datos o otros tipos de características o una combinación de ambos?

Respuesta

La métrica $R^2$ de un modelo $f$ que está hecho para predecir $y$ se define como tal:

$$ 1 - \frac{\text{ECM}(f)}{\text{ECM}(\bar{y})} $$

Donde ECM es el error cuadrático medio, y $\bar{y}$ es el predictor trivial que siempre retorna la media.

En particular, modelos buenos tienen un ECM muy pequeño, así que $R^2$ es cercano a $1$ (y este es su valor máximo, dado que el ECM es no negativo). Por otro lado, si el modelo es esencialmente el trivial, este valor se parece a 0. Modelos peores que el trivial tienen $R^2$ negativo, así que esta métrica tiene una intepretación:

  • Si $R^2 < 0$, el modelo es peor que el trivial
  • Si $R^2 = 0$, el modelo es peor igual el trivial
  • Mientras más cercano sea $R^2$ a $1$, mejor es el modelo
  • Si $R^2 = 1$, el modelo es perfecto
In [22]:
X_temp = X.copy()
X_temp["Posicion"] = posicion
sueldos = pd.read_csv('data/sueldos.csv').drop("Unnamed: 0", axis=1)
df_sueldos = pd.merge(X_temp, sueldos, left_on='Name', right_on='Player')
df_sueldos
Out[22]:
Name Nationality Club_Position Height Weight Preffered_Foot Age Work_Rate Weak_foot Skill_Moves ... Shot_Power Finishing Long_Shots Curve Freekick_Accuracy Penalties Volleys Posicion Player Weekly Salary
0 Cristiano Ronaldo Portugal LW 185 80 Right 32 High / Low 4 5 ... 92 93 90 81 76 85 88 central_ataque Cristiano Ronaldo 1248536.0
1 Lionel Messi Argentina RW 170 72 Left 29 Medium / Medium 4 4 ... 85 95 88 89 90 74 85 central_ataque Lionel Messi 1538905.0
2 Neymar Brazil LW 174 68 Right 25 High / Medium 5 5 ... 78 89 77 79 84 81 83 central_ataque Neymar 797726.0
3 Luis Suárez Uruguay ST 182 85 Right 30 High / Medium 4 4 ... 87 94 86 86 84 85 88 ataque Luis Suárez 508923.0
4 Manuel Neuer Germany GK 193 92 Right 31 Medium / Medium 4 1 ... 25 13 16 14 11 47 11 arquero Manuel Neuer 326233.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1861 Phillip Menzel Germany Res 191 83 Right 18 Medium / Medium 3 1 ... 25 5 10 12 8 15 5 NaN Phillip Menzel 2034.0
1862 Manuel Akanji Switzerland Sub 187 85 Right 21 Medium / Medium 2 2 ... 38 25 18 26 27 40 31 NaN Manuel Akanji 54176.0
1863 Moritz Nicolas Germany Res 195 87 Right 19 Medium / Medium 2 1 ... 22 9 10 17 14 17 11 NaN Moritz Nicolas 2262.0
1864 Giacomo Satalino Italy Sub 188 70 Right 17 Medium / Medium 1 1 ... 19 6 5 13 11 18 9 NaN Giacomo Satalino 2827.0
1865 Nicolò Zaniolo Italy Res 185 80 Right 17 High / Low 3 2 ... 50 49 42 54 51 49 39 NaN Nicolò Zaniolo 28187.0

1866 rows × 41 columns

In [23]:
X_reg = df_sueldos.drop(["Weekly Salary", "Name", "Player", "Nationality"], axis=1)
y_reg = df_sueldos["Weekly Salary"]
X_train, X_test, y_train, y_test = train_test_split(X_reg, y_reg, random_state=0, shuffle=True)

Vamos a seguir la guía tal como antes, empezando al notar que siguen habiendo menos de 100k muestras. No priorizaremos que pocas características sean importantes, así que empezaremos con los dos recomendados: RidgeRegression y SVR lineal. Por simplicidad, utilizaremos siempre el mismo pipeline para todo, muy similar a los anteriores. Notemos que los regresores de sklearn implementan $R^2$ en su método score.

In [24]:
int_index = get_col_index(X_reg.select_dtypes("int").columns, X_reg)
float_index = get_col_index(X_reg.select_dtypes("float").columns, X_reg)
object_index = get_col_index(set(X_reg.select_dtypes("object").columns) - exclude)
reg_processor = ColumnTransformer([("int_processor", make_pipeline(SimpleImputer(strategy="median"), MinMaxScaler()), int_index),
                                   ("float_processor", make_pipeline(StandardScaler(), SimpleImputer(strategy="mean")), float_index),
                                   ("small_cat_processor", OneHotEncoder(handle_unknown='ignore', drop='first', sparse=False), object_index),
                                 ])
In [25]:
models = (Ridge(), SVR(kernel="linear"))
for model in models:
    pipeline = make_pipeline(reg_processor, model)
    score = pipeline.fit(X_train, y_train).score(X_test, y_test)
    print(f"R^2 para {model.__class__.__name__}: {score}")
R^2 para Ridge: 0.2507424762140851
C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

R^2 para SVR: -0.1115151394538163

Ninguno de estos dos cumple el requisito de $R^2$, con SVR peor que el modelo dummy, por lo cual avanzamos a Random Forest y kernel rbf.

In [26]:
models = (RandomForestRegressor(), SVR(kernel="rbf"))
for model in models:
    pipeline = make_pipeline(reg_processor, model)
    score = pipeline.fit(X_train, y_train).score(X_test, y_test)
    print(f"R^2 para {model.__class__.__name__}: {score}")
C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

R^2 para RandomForestRegressor: 0.5617814005119344
R^2 para SVR: -0.11187892479809713
C:\Users\David\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\preprocessing\_encoders.py:188: UserWarning:

Found unknown categories in columns [0] during transform. These unknown categories will be encoded as all zeros

SVR sigue con un pésimo resultado (peor que dummy), pero RF logra $R^2$ de 0.6, muy satisfactorio dado el objetivo planteado de $0.35$, por lo cual ya no se considerará necesario mejorar los modelos ni agregar datos/características.

Conclusión¶

Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.



Created in deepnote.com Created in Deepnote