#!/usr/bin/env python # coding: utf-8 # # Wine analysis # # Importación de librerías # In[1]: import matplotlib.pyplot as plt import seaborn as sns import numpy as np import pandas as pd # sns.color_palette("husl", 9) # # Adquisición de datos # Para la realización de este trabajo, vamos a utilizar el dataset *wine* de sklearn. # In[2]: from sklearn.datasets import load_wine wines = load_wine(as_frame=True) X = wines["data"] y = wines["target"] columnas = list(X.columns) descripcion = wines["DESCR"] # ### Características del dataset # In[3]: print(descripcion[19:547]) # In[4]: X.head() # In[5]: X.info() # In[6]: X.describe().map(lambda x: round(x,2)) # In[7]: y.info() # In[8]: y.value_counts().sort_index() # Observamos que el dataset es un conjunto de 178 registros formado por 13 variables feature y una variable de salida, la cual divide al dataset en las clases "0", "1" y "2". Se comprobó también que ningún registro tiene un valor nulo. # # Normalización de datos # # Para el mismo, utilizaremos la función StandardScaler de Sklearn # In[9]: from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_scaled = pd.DataFrame(X_scaled, columns=columnas) X_scaled.head() # In[10]: X_scaled.describe().map(lambda x: round(x,2)) # # Análisis exploratorio de datos # Para esta sección, vamos a visualizar las distribuciones y gráficos para el dataset normalizado y el no normalizado. # ## I. Distribución de datos según clase # In[11]: def graficar_boxplots(X, y, titulo): fig, ax = plt.subplots(3,5,figsize=(15,10)) columnas = X.columns ax[0,2].set_title(titulo, color='green') for i, ax in enumerate(fig.axes): try: sns.boxplot(data=X,x=columnas[i],hue=y,ax=ax) except: ax.remove() # In[12]: graficar_boxplots(X, y, "Dataset sin normalizar") graficar_boxplots(X_scaled, y, "Dataset normalizado") # Se observa que la distribución de los datasets no se ve afectado por la normalización de los datos, solamente se cambian los valores de cada registro. # ## II. Dispersión de datos según el nivel de alcohol # In[13]: def graficar_dispersion(X, y, titulo): fig, ax = plt.subplots(4,3,figsize=(14,16),sharex=True) columnas = X.columns ax[0,1].set_title(titulo, color='green') for i, ax in enumerate(fig.axes): try: sns.scatterplot(data=X,x='alcohol',y=columnas[i+1],hue=y,ax=ax) except: ax.remove() # In[14]: graficar_dispersion(X, y, "Dataset sin normalizar") graficar_dispersion(X_scaled, y, "Dataset normalizado") # Nuevamente, se observa que la dispersión de los datos no se ve afectado por la normalización de los datasets, solamente se cambian los valores de cada registro. # # Se observa también que los datos se encuentran fuertemente agrupados entre otros datos de la misma clase. # ## III. Coeficientes de correlación # In[15]: def graficar_correlacion(X, y, titulo): fig, ax = plt.subplots(1, 1, figsize=(10, 8)) # Generamos la matriz de correlaciones de Pearson data = X.copy() data["target"] = y correlaciones = data.corr(method="pearson", numeric_only=False) correlaciones_redondeado = correlaciones.round(2) # Generamos el mapa de correlaciones sns.heatmap(correlaciones_redondeado, annot=True, ax=ax) # Ocultamos la grilla y seteamos el título ax.grid(False) ax.set_title(titulo, pad=20, color='green'); graficar_correlacion(X, y, "Coeficientes de correlación de Pearson de dataset sin normalizar") graficar_correlacion(X_scaled, y, "Coeficientes de correlación de Pearson de dataset normalizado") # Si comparamos los dos gráficos, vemos que la normalización de los datos no afecta los coeficientes de correlación. # # Por otro lado, observamos que las variables más correlacionadas con el target son: # * alcalinity_of_ash # * total_phenols # * flavanoids # * hue # * od280/od315_of_diluted_wines # * proline # # Mientras que las variables menos correlacionadas son: # * ash # * magnesium # * color_intensity # # Preparación de datos # Para poder calcular la precisión de los algoritmos que vamos a implementar, primero separaremos los datasets en una parte dedicada al entrenamiento de los modelos, y otra parte dedicada a los testeos de los mismos. # # Utilizaremos el mismo random_state en ambos datasets para igualar los registros de entrenamiento y de test. Incorporamos el parámetro stratify para que la proporción de clases de la variable target sea igual en el set de train y en el set de test. # In[16]: from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) X_scaled_train, X_scaled_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42, stratify=y) # # Implementación de K-nearest neighbors # Al trabajar con el algoritmo KNN, debemos tener en cuenta que se trata de un algoritmo perezoso, es decir, el algoritmo no aprende explícitamente un modelo cuando recibe los datos de entrenamiento, sino que la carga comptuacional más pesada se realiza en el momento de la predicción. Por lo tanto, se espera que la implementación sea rápido el algoritmo, pero a la hora de la predicción sea mucho más lento que otros algoritmos. # En primera instancia, vamos a crear un clasificador para cada dataset. # In[17]: from sklearn import neighbors def entrenar_knn(X_train, y_train, n_neighbors): # Creamos un clasificador, y le ajustamos los datos provenientes del entrenamiento. clf = neighbors.KNeighborsClassifier(n_neighbors, weights='uniform') clf.fit(X_train, y_train) return clf n_neighbors = 5 clf = entrenar_knn(X_train, y_train, n_neighbors) clf_scaled = entrenar_knn(X_scaled_train, y_train, n_neighbors) # Generados los clasificadores, vamos a generar el mapa de confusión y el cálculo de las precisiones de cada modelo. Para esto generaremos una función, que luego será usada en los siguientes algoritmos que se implementen. # In[18]: from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, accuracy_score from sklearn.model_selection import cross_validate, StratifiedKFold # Función que devuelve una figura con la predicción en base a un algoritmo y un conjunto de datos def obtener_precision(nombre_modelo, modelo, X_train, X_test, y_train, y_test): # Predicción predicciones = modelo.predict(X_test) # Gráfico de matriz de confusión fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,5)) fig.suptitle(nombre_modelo, fontweight="bold") ConfusionMatrixDisplay(confusion_matrix(y_test, predicciones)).plot(ax=ax[0]) # Medida de precisión kfold = StratifiedKFold(n_splits=5) cross_v = cross_validate(estimator=modelo, X=X_train, y=y_train, scoring="accuracy", cv=kfold, return_train_score=True) medidas = {"Test score": cross_v["test_score"].mean(), "Train score": cross_v["train_score"].mean()} # Impresión de medidas ax[1].bar(medidas.keys(), medidas.values(), color=["lightgreen", "pink"]) ax[1].set(ylim=[0, 1.1]) for i, medida in enumerate(medidas): ax[1].text(i, medidas[medida], round(medidas[medida], 5), ha="center") return fig obtener_precision("KNN con datos sin normalizar", clf, X_train, X_test, y_train, y_test) obtener_precision("KNN con datos normalizados", clf_scaled, X_scaled_train, X_scaled_test, y_train, y_test); # Como se puede observar, la precisión sobre el set de test es del 71% para el algoritmo con datos no normalizados, con respecto a un 95% para el algoritmo con datos normalizados. Es decir, el algoritmo genera mejores predicciones cuando los datos se encuentran normalizados. # # La normalización es sumamente importante para el algoritmo de KNN, ya que ayuda a traer a todas las variables a una escala similar, previniendo que algunas variables tengan mayor predominancia sobre otras en el cálculo de las distancias, simplemente porque tienen mayores magnitudes. # # Implementación de Árboles de decisión # De forma similar al algoritmo de KNN, crearemos una función que se encargue de instanciar el algoritmo y entrenarlo para los dos datasets. # In[19]: from sklearn.tree import DecisionTreeClassifier, plot_tree def entrenar_arbol(X_train, y_train, max_depth): arbol = DecisionTreeClassifier(max_depth=max_depth, random_state=42) arbol.fit(X_train, y_train) return arbol max_depth = 3 arbol = entrenar_arbol(X_train, y_train, max_depth) arbol_scaled = entrenar_arbol(X_scaled_train, y_train, max_depth) # Procedemos a graficar el árbol de decisión para cada dataset. # In[20]: plot_tree(arbol); # In[21]: plot_tree(arbol_scaled); # Por último, calculamos la matriz de confusión y la precisión de ambos algoritmos, reutilizando la función anteriormente desarrollada en el algoritmo de KNN. # In[22]: obtener_precision("Árbol de decisión con datos sin normalizar", arbol, X_train, X_test, y_train, y_test) obtener_precision("Árbol de decisión con datos normalizados", arbol_scaled, X_scaled_train, X_scaled_test, y_train, y_test); # Para el caso del algoritmo de árbol de decisión, observamos que normalizar los datos no supone un incremento en la precisión de las predicciones. Esto se debe a que este tipo de algoritmo no toma decisiones en base a distancias entre puntos (como KNN), por lo que diferencias entre escalas muy grandes no supone un riesgo en la integridad del modelo. # # Análisis de componentes principales (PCA) # # Nuestro objetivo será reducir el dataset normalizado a dos dimensiones, para luego aplicar algoritmos de clustering sobre los datos. # In[23]: from sklearn.decomposition import PCA pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) sns.scatterplot(x=X_pca[:,0], y=X_pca[:,1]) plt.title("Dataset reducido a dos dimensiones") plt.xlabel("Componente principal 1") plt.ylabel("Componente principal 2"); # Procedemos a calcular la varianza retenida: # In[24]: variance_ratio = np.cumsum(pca.explained_variance_ratio_) print(f'Varianza retenida con 1 componente: {round(variance_ratio[0], 2)}') print(f'Varianza retenida con 2 componentes: {round(variance_ratio[1], 2)}') # Podemos observar, por lo tanto, que los 2 componentes pueden explicar hasta el 55% de la varianza con respecto al dataset original. # # Implementación de K-means # Antes de implementar el algoritmo, vamos a dividir el dataset en un set de entrenamiento y otro de testeo, para analizar en el caso de que haya overfitting o underfitting. Utilizamos tambien la variable target para comparar la predicción del modelo una vez entrenado. # In[25]: X_pca_train, X_pca_test, y_train, y_test = train_test_split(X_pca, y, test_size=0.2, random_state=42, stratify=y) # Procedemos a entrenar el modelo # In[26]: from sklearn.cluster import KMeans def entrenar_kmeans(X_train, n_clusters, max_iter): k_means = KMeans(n_clusters=n_clusters, max_iter=max_iter, n_init=10) k_means.fit(X_train) return k_means n_clusters = 3 max_iter = 100 k_means = entrenar_kmeans(X_pca_train, n_clusters, max_iter) # Comparamos ahora la dispersión de los datos con las etiquetas originales, con respectos a las etiquetas que nos brinda la predicción # In[27]: # Obtenemos los centroides centroides = k_means.cluster_centers_ fig, ax = plt.subplots(1,2,figsize=(10,4), constrained_layout=True) # Graficamos la predicción sns.scatterplot(x=X_pca_test[:,0], y=X_pca_test[:,1], hue=k_means.predict(X_pca_test), ax=ax[0], palette=['#3498db', '#2ecc71', '#e67e22']) ax[0].set_title("Predicción con centroides") ax[0].set_xlabel("Componente principal 1") ax[0].set_ylabel("Componente principal 2") # Graficamos los centroides ax[0].scatter(x=centroides[:,0], y=centroides[:,1], c='red', marker='*', s=100) # Graficamos las etiquetas originales sns.scatterplot(x=X_pca_test[:,0], y=X_pca_test[:,1], hue=y_test, ax=ax[1], palette=['#3498db', '#2ecc71', '#e67e22']) ax[1].set_title("Etiquetas originales") ax[1].set_xlabel("Componente principal 1") ax[1].set_ylabel("Componente principal 2"); # A simple vista, vemos que la predicción generaliza de forma correcta el dataset, y se mantiene relativamente fiel con respecto a las etiquetas originales. # # Por último, calculamos la precisión. # In[28]: from sklearn import metrics print(f'Precisión con el set de entrenamiento: {round(metrics.adjusted_rand_score(y_test, k_means.predict(X_pca_test)), 5)}') print(f'Precisión con el set de prueba: {round(metrics.adjusted_rand_score(y_train, k_means.predict(X_pca_train)), 5)}') # # Implementación de Mini-Batch K-means # El algoritmo de Mini-Batch K-Means es una variante al K-means que aborda este problema al actualizar los centroides utilizando solo un subconjunto (minibatch) aleatorio de los datos en cada iteración en lugar de utilizar todo el conjunto de datos. Esto hace que el algoritmo sea más eficiente en términos de tiempo de ejecución y sea especialmente útil cuando se trabaja con grandes conjuntos de datos. # In[29]: from sklearn.cluster import MiniBatchKMeans def entrenar_mbkmeans(X_train, n_clusters, max_iter): mb_kmeans = MiniBatchKMeans(n_clusters=n_clusters, max_iter=max_iter, n_init=10, random_state=42) mb_kmeans.fit(X_train) return mb_kmeans n_clusters = 3 max_iter = 100 mb_kmeans = entrenar_mbkmeans(X_pca_train, n_clusters, max_iter) # Realizamos el mismo procedimiento de graficar la dispersión que en el algoritmo de K-means # In[30]: # Obtenemos los centroides centroides = mb_kmeans.cluster_centers_ fig, ax = plt.subplots(1,2,figsize=(10,4), constrained_layout=True) # Graficamos la predicción sns.scatterplot(x=X_pca_test[:,0], y=X_pca_test[:,1], hue=mb_kmeans.predict(X_pca_test), ax=ax[0], palette=['#3498db', '#2ecc71', '#e67e22']) ax[0].set_title("Predicción con centroides") ax[0].set_xlabel("Componente principal 1") ax[0].set_ylabel("Componente principal 2") # Graficamos los centroides ax[0].scatter(x=centroides[:,0], y=centroides[:,1], c='red', marker='*', s=100) # Graficamos las etiquetas originales sns.scatterplot(x=X_pca_test[:,0], y=X_pca_test[:,1], hue=y_test, ax=ax[1], palette=['#3498db', '#2ecc71', '#e67e22']) ax[1].set_title("Etiquetas originales") ax[1].set_xlabel("Componente principal 1") ax[1].set_ylabel("Componente principal 2"); # Por último, calculamos la precisión # In[31]: print(f'Precisión con el set de entrenamiento: {round(metrics.adjusted_rand_score(y_test, mb_kmeans.predict(X_pca_test)), 5)}') print(f'Precisión con el set de prueba: {round(metrics.adjusted_rand_score(y_train, mb_kmeans.predict(X_pca_train)), 5)}') # Como se puede observar, la precisión entre los dos algoritmos es bastante similar. Debemos tener en cuenta que el dataset contiene muy pocos datos, por lo que no se saca provecho de este algoritmo como se esperaría. # # Conclusión # **I.** Podemos decir que normalizar los datos tendrá en la mayoría de los casos un efecto positivo a la hora de entrenar los algoritmos, como se pudo observar en el modelo de KNN, mientras que en otros modelos (como el Árbol de Decisión) la precisión no se verá afectada. Sin embargo, es recomendable normalizar los datos ya que también permite que el entrenamiento de los datos se realice en menor cantidad de tiempo. # # **II.** Al disponer de las etiquetas de los registros desde el propio dataset inicial, es entendible que los algoritmos supervisados tengan mayor precisión con respecto a los no supervisados. Sin embargo, para este caso, pudimos observas que los algoritmos de clustering lograron un buen trabajo a la hora de agrupar los datos, y obtuvieron resultados similares con respecto a las etiquetas originales.