<jupyter_start><jupyter_text>**Clasificacion y metricas de evaluacion**<jupyter_code>## IMPORTAR LIBRERIAS DE TRABAJO

# mate
import numpy as np

# modelos
import sklearn

# trabajo con tablas
import pandas as pd

# data externa
from sklearn.datasets import fetch_openml

# interfaz con sistema operativo
import os

# graficos
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
# semilla para que los resultados sean reproducibles
np.random.seed(42)<jupyter_output><empty_output><jupyter_text>**Cargar la data MNIST**Para mas contexto sobre la data ir al siguiente [link](https://conx.readthedocs.io/en/latest/MNIST.html).<jupyter_code>mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()
# revisar data de entrenamiento
X = mnist["data"]
print(X.shape)
print(type(X))
# convirtamos X a dataframe para inspeccionar mejor
X_df = pd.DataFrame(X)
X_df.head()<jupyter_output><empty_output><jupyter_text>Inspeccionemos que valores toman los pixeles del primer digito (la primera fila):<jupyter_code>X_df.iloc[0,:].hist(bins=10)
# revisar el target
y = mnist["target"]
print(y.shape)
print(type(y))<jupyter_output>(70000,)
<class 'numpy.ndarray'><jupyter_text>Como ejemplo, reconstruyamos graficamente el primer digito (primera fila) a partir de los valores de cada una de sus columnas.<jupyter_code># plot de uno de los digitos
primer_digito = X[0]
primer_digito_imagen = primer_digito.reshape(28, 28)
plt.imshow(primer_digito_imagen, cmap=mpl.cm.binary)
plt.axis("off")
plt.show()<jupyter_output><empty_output><jupyter_text>Confirmemos lo que dice la variable `y` para este primer digito:<jupyter_code>primer_label = y[0]
print(primer_label)<jupyter_output>5<jupyter_text>Todo OK!<jupyter_code># convertir y a un formato mas eficiente
y = y.astype(np.uint8)<jupyter_output><empty_output><jupyter_text>**Entrenar modelos que encuentren los 5**- Este es un problema de clasificacion. - Es un caso binario porque el target es categorico y solo toma dos valores --cada observacion es un cinco, o no lo es--. Empezamos por definir las muestras de training y testing.<jupyter_code># Definir data de training y testing
X_train = X[:60000]
y_train = y[:60000]
X_test = X[60000:]
y_test = y[60000:]<jupyter_output><empty_output><jupyter_text>- Los valores de `y` van del 1 al 9. Pero solo nos interesa saber si el digito es un 5 o no.- Lo siguiente sera generar el vector con el ***target*** (La variable a predecir). Debera tener valor `True` siempre que `y` sea igual a 5 y `False` cuando no.- Las variables que toman valor `True` cuando se cumple cierta condicion se conocen como *flag* o *dummy*.- **Nota:** (Para Python `True`=1 y `False`=0)<jupyter_code>y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)
print(y_train_5)<jupyter_output>[ True False False ...  True False False]<jupyter_text>**Probar un modelo SGD**<jupyter_code># importar el algoritmo
from sklearn.linear_model import SGDClassifier

# Definir sus hiperparametros
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)

# entrenar el modelo
sgd_clf.fit(X_train, y_train_5)<jupyter_output><empty_output><jupyter_text>Veamos que predice el modelo para el primer digito (usamos el metodo `predict`):<jupyter_code>primer_digito = X[0]
sgd_clf.predict([primer_digito])<jupyter_output><empty_output><jupyter_text>`True`, el modelo predijo que es un 5! ⭐️ Veamos el performance de la metrica `accuracy` (el numero de aciertos totales) con validacion cruzada.<jupyter_code>from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")<jupyter_output><empty_output><jupyter_text>Todo parece estar bien ... verdad?  **Probar un modelo Naive**Crearemos manualmente un modelo trivial que siempre predecira (ingenuamente) que un digito no es 5.<jupyter_code>from sklearn.base import BaseEstimator
class ClasificadorNunca5(BaseEstimator):
    def fit(self, X, y=None):
        pass
    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

nunca_5_clf = ClasificadorNunca5()
cross_val_score(nunca_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")<jupyter_output><empty_output><jupyter_text>Un clasificador aparentemente tan malo tiene una precision tremenda. Por que?<jupyter_code># recordar cuantos digitos tenemos en la data de entrenamiento
print(len(y_train_5))
# cuantos de estos son 5
print(y_train_5.sum())<jupyter_output>5421<jupyter_text>Solo el 10% de las observaciones son 5, asi que si siempre predigo que el digito no es 5, acertare el 90% de las veces. 🙀  **Analizar mas a fondo el performance de un clasificador**<jupyter_code>from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix<jupyter_output><empty_output><jupyter_text>**Matriz de confusion** Primero veamos el ideal. Cual seria la matriz de confusion de un modelo que acierta perfectamente.<jupyter_code>y_train_perfect_predictions = y_train_5 
confusion_matrix(y_train_5, y_train_perfect_predictions)
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
confusion_matrix(y_train_5, y_train_pred)<jupyter_output><empty_output><jupyter_text>**Precision***Precision* es la proporcion de observaciones que nuestro modelo predijo correctamente como `5` del total predicciones correctas e incorrectas.<jupyter_code>from sklearn.metrics import precision_score
precision_score(y_train_5, y_train_pred)<jupyter_output><empty_output><jupyter_text>Tambien se puede calcular el precison a partir de la matriz de confusion:<jupyter_code>cm = confusion_matrix(y_train_5, y_train_pred)
print(cm)
# calcular precision
cm[1, 1] / (cm[0, 1] + cm[1, 1])<jupyter_output><empty_output><jupyter_text>**Recall***Recall* es la proporcion de observaciones que nuestro modelo predijo como `5`, sobre la cantidad real de `5`s en la data.<jupyter_code>from sklearn.metrics import recall_score
recall_score(y_train_5, y_train_pred)<jupyter_output><empty_output><jupyter_text>Tambien se puede calcular el *recall* a partir de la matriz de confusion:<jupyter_code>cm = confusion_matrix(y_train_5, y_train_pred)
print(cm)
# calcular recall
cm[1, 1] / (cm[1, 0] + cm[1, 1])<jupyter_output><empty_output><jupyter_text>**Umbrales de decision y el trade-off Precission-Recall**- La prediccion en un modelo de clasificacion consiste en asignar un score a las observaciones sobre las que se esta prediciendo (*scoring*).- Cuando la prediccion una observacion sobrepasa cierto umbral de decision (*threshold*) se asigna el label correspondiente (`True` o 1 en un problema binario).- El umbral puede ser modificado por el analista. De hecho, el umbral depende de nuestra sensibilidad frente a falsos positivos o negativos. En un modelo SGD el umbral de decision es 0. Cuando una observacion tiene un `score` mayor que 0, se considera un positivo (`True` en nuestro caso). Veamos el score del primer digito que ya habiamos guardado en la variable `primer_digito`. Para obtener el score de un m,modelo de scikitlearn, usamos el metodo `decision_function()`.<jupyter_code>y_score = sgd_clf.decision_function([primer_digito])
y_score<jupyter_output><empty_output><jupyter_text>Efectivamente el score es mayor que cero. Por eso se le asigno el label `True`. Que pasa si modificamos el *threshold*? Primero partamos del original, es decir 0.<jupyter_code>threshold = 0
primer_digito_pred = (y_score > threshold)
primer_digito_pred<jupyter_output><empty_output><jupyter_text>Ahora cambiemos el threshold a 8000, cual seria la prediccion?<jupyter_code>threshold = 8000
primer_digito_pred = (y_score > threshold)
primer_digito_pred<jupyter_output><empty_output><jupyter_text>Como saber que umbral es mejor usar? Primero calculemos el *score* de todas nuestras observaciones.<jupyter_code>y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")<jupyter_output><empty_output><jupyter_text>Ahora graficaremos el **recall** y **precision** para distintos valores del **threshold**.<jupyter_code>from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
    plt.legend(loc="center right", fontsize=16) 
    plt.xlabel("Threshold", fontsize=16)        
    plt.grid(True)                              
    plt.axis([-50000, 50000, 0, 1])             

recall_90_precision = recalls[np.argmax(precisions >= 0.90)]
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]

plt.figure(figsize=(8, 4))                                                                  
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.plot([threshold_90_precision, threshold_90_precision], [0., 0.9], "r:")                 
plt.plot([-50000, threshold_90_precision], [0.9, 0.9], "r:")                                
plt.plot([-50000, threshold_90_precision], [recall_90_precision, recall_90_precision], "r:")
plt.plot([threshold_90_precision], [0.9], "ro")                                             
plt.plot([threshold_90_precision], [recall_90_precision], "ro")                                                                        
plt.show()<jupyter_output><empty_output><jupyter_text>Otra forma de evidenciar el *trade-off* es graficando *precision* vs *recall*:<jupyter_code>(y_train_pred == (y_scores > 0)).all()

def plot_precision_vs_recall(precisions, recalls):
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0, 1, 0, 1])
    plt.grid(True)

plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.plot([recall_90_precision, recall_90_precision], [0., 0.9], "r:")
plt.plot([0.0, recall_90_precision], [0.9, 0.9], "r:")
plt.plot([recall_90_precision], [0.9], "ro")
plt.show()<jupyter_output><empty_output>