En esta práctica vamos a usar el clásico dataset de las flores de iris (iris data set), tratando de clasificar distintas variedades de la flor de iris según la longitud y anchura de sus pétalos y sépalos. Trataremos de optimizar distintas métricas y veremos como los diferentes modelos clasifican los puntos y con cuales obtenemos una mayor precisión.
La práctica está estructurada de la siguiente manera (en el que se detalla la puntuación de cada parte).
Importante: Cada ejercicio puede suponer varios minutos de ejecución, por lo que la entrega debe hacerse en formato notebook y html, donde se vea el código y los resultados, junto con los comentarios de cada ejercicio.
# Importamos librerías
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
import colorsys
import graphviz
from pandas.plotting import scatter_matrix
from matplotlib.colors import ListedColormap
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn import model_selection
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn import datasets, neighbors, tree, svm
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn import tree
from sklearn.metrics import classification_report, accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.tree import export_graphviz
%matplotlib inline
#Importamos el dataset para iniciar el análisis
#También se podría hacer a partir de la clase datasets
#iris = datasets.load_iris()
iris = pd.read_csv("Iris.csv")
Exploraremos nuestro conjunto de datos. Para ello, realizaremos las siguientes inspecciones:
Os hemos puesto en forma de comentario los análisis que tendríais que hacer
#Visualizamos los primeros 5 datos del dataset
iris = pd.read_csv("Iris.csv")
display(iris.head())
#Eliminamos la primera columna ID
print()
print('------Eliminamos la columna ID-------------')
iris = iris.drop('Id',axis=1)
display(iris.head())
#Forma, tamaño y número de valores del dataset
print()
print('------Información del dataset------')
print(iris.info())
print("El número de líneas es: " + str(iris.shape[0]) + " y el número de columnas: "+ str(iris.shape[1]))
print("No existe ningún null")
display(iris.isnull().sum())
#Resumen estadístico
print()
print('------Descripción del dataset------')
display(iris.describe())
#Grafico Sépalo - Longitud vs Ancho
fig = iris[iris.Species == 'Iris-setosa'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='blue', label='Setosa')
iris[iris.Species == 'Iris-versicolor'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='green', label='Versicolor', ax=fig)
iris[iris.Species == 'Iris-virginica'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='red', label='Virginica', ax=fig)
fig.set_xlabel('Sépalo - Longitud')
fig.set_ylabel('Sépalo - Ancho')
fig.set_title('Sépalo - Longitud vs Ancho')
plt.show()
#Grafico Pétalo - Longitud vs Ancho
fig = iris[iris.Species == 'Iris-setosa'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='blue', label='Setosa')
iris[iris.Species == 'Iris-versicolor'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='green', label='Versicolor', ax=fig)
iris[iris.Species == 'Iris-virginica'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='red', label='Virginica', ax=fig)
fig.set_xlabel('Pétalo - Longitud')
fig.set_ylabel('Pétalo - Ancho')
fig.set_title('Pétalo Longitud vs Ancho')
plt.show()
El análisis univariado es la forma más simple de analizar datos. No trata con causas o relaciones (a diferencia de la regresión) y su propósito principal es describir y encontrar patrones en los datos.
Para ello vamos a realizar lo que se conoce como Distribution Plots (o histogramas). Los gráficos de distribución se utilizan para evaluar visualmente cómo se distribuyen los puntos de datos con respecto a su frecuencia. Por lo general, los puntos de datos se agrupan en contenedores y la altura de las barras indica el número de puntos de datos (frecuencua de aparición).
import warnings
warnings.filterwarnings('ignore')
iris_setosa=iris.loc[iris["Species"]=="Iris-setosa"]
iris_virginica=iris.loc[iris["Species"]=="Iris-virginica"]
iris_versicolor=iris.loc[iris["Species"]=="Iris-versicolor"]
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"PetalLengthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"PetalWidthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"SepalLengthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"SepalWidthCm").add_legend()
plt.show()
La única conclusión es que con PetalLengthCm podemos separa la especie iris-setosa.
Un diagrama de caja (box plot) es una forma estandarizada de mostrar la distribución de datos basada en un resumen de cinco números ("mínimo", primer cuartil (Q1), mediana, tercer cuartil (Q3) y "máximo"). Los box plots nos informan sobre valores atípicos y cuáles son sus valores. También puede decirnos si los datos son simétricos, si están agrupados y si están sesgados. Para realizarlos podemos usar la función boxplot
de seaborn
.
El Violin Plot es un método para visualizar la distribución de datos numéricos de diferentes variables. Es similar al diagrama de caja (box plot) pero con un diagrama rotado en cada lado que brinda más información sobre la estimación de densidad en el eje y. La densidad se refleja y se voltea y la forma resultante se rellena creando una imagen que se parece a un violín. La ventaja de una trama de violín es que puede mostrar matices en la distribución que no son perceptibles en una gráfica de caja. Por otro lado, el diagrama de caja muestra más claramente los valores atípicos en los datos. Los gráficos de violín suelen contener más información que los gráficos de caja aunque son menos populares.
Ahora tracemos los gráficos de violín para nuestro conjunto de datos de iris. Para ello podemos utilizar la función violinplot
de seaborn
. Para su interpretación tengamos en cuenta que el rectángulo que aparece en el violin plot equivale a la información que nos da el box plot y que el círculo blanco nos indica donde está el percentil 50.
Por último realizaremos un pequeño estudio mediante un pair-plot para visualizar posibles relaciones entre nuestras variables (por pares).
En este caso emplearemos la función pairplot
de la librería seaborn
.
import warnings
warnings.filterwarnings('ignore')
sns.boxplot(x="Species",y="PetalLengthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="PetalWidthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="SepalLengthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="SepalWidthCm",data=iris)
plt.show()
display(iris.boxplot())
sns.violinplot(x="Species",y="PetalLengthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="PetalWidthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="SepalLengthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="SepalWidthCm",data=iris)
plt.show()
sns.set_style("whitegrid")
sns.pairplot(iris,hue="Species",size=3);
plt.show()
Si nos fijamos en la gráficas de dispersión que relacionan las carácteristicas del campo sepal veremos como están distribuidos de manera casi uniforme (sobretodo los correspondientes al Iris setosa), mientras que los correspondientes a versicolor y virginica tienen cualidades algo parecidas por lo que se solapan en ocasiones.
En cambio si comparamos el pétalo es una distribución mucho más uniforme en comparación con el sépalo.
Si análizamos el diagramade violín muestra que Iris Virginica tiene un valor medio más alto en longitud de pétalo, ancho de pétalo y longitud de sépalo en comparación con Versicolor y Setosa. En otro sentido, Iris Setosa tiene el mayor valor medio de ancho de sépalo. También podemos ver una diferencia significativa entre la longitud y el ancho del sépalo de Setosa contra la longitud y el ancho de sus pétalos. Esa diferencia es menor en Versicolor y Virginica. El diagrama del violín también indica que el peso del ancho del sépalo de Virginica y el ancho del pétalo están altamente concentrados alrededor de la mediana.
Respecto al diagrama de cajas, los puntos aislados que se pueden ver son los valores atípicos en los datos. Dado que estos son muy pocos en número, no tendría ningún impacto significativo en nuestro análisis.
Antes de aplicar ningún modelo, tenemos que separar los datos entre los conjuntos de train y test. Siempre trabajaremos sobre el conjunto de train y evaluaremos los resultados en el conjunto de test.
Es importante tener en cuenta que nuestra variable target es categórica. El clasificador KNeighborsClassifier
no acepta etiquetas de tipo string
, por lo que debemos tranformar estas etiquetas a números (esto es lo que conocemos como Label encoding).
Para ello dividiremos el dataset en dos arrays: X (características) e Y (etiquetas) y aplicaremos la siguiente correspondencia:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn import preprocessing
label_encoder = preprocessing.LabelEncoder()
iris = pd.read_csv("Iris.csv")
iris = iris.drop('Id',axis=1)
print('----------Aplicación de la correspondecia ----------------------')
print(iris['Species'].unique())
iris['Species']= label_encoder.fit_transform(iris['Species'])
print(iris['Species'].unique())
#X_train, X_test, y_train, y_test = train_test_split(iris[['PetalLengthCm', 'PetalWidthCm','SepalLengthCm','SepalWidthCm']],
print('----------Sepal----------------------')
X_train, X_test, y_train, y_test = train_test_split(iris[['SepalLengthCm', 'SepalWidthCm']],
iris['Species'],
test_size=0.2,
stratify=iris['Species'])
A lo largo de los ejercicios aprenderemos a visualizar gráficamente las fronteras de decisión que nos devuelven los diferentes modelos. Para este fin usaremos la función definida a continuación (que nos servirá para trazar las respectivas fronteras de decisión a lo largo de toda la PEC), la cual sigue los siguientes pasos:
Después de este proceso, ya podemos hacer el gráfico de las fronteras de decisión y añadir los puntos reales. Así veremos las areas en las que el modelo considera que son de una clase y las que considera que son de la otra. Al poner encima los puntos reales veremos si los clasifica correctamente.
# Creamos la meshgrid con los valores mínimo y máximo de 'x' i 'y'.
# La variable X es nuestro dataframe con las variables a estudiar (las del pétalo o las del sépalo)
X=iris[['SepalLengthCm', 'SepalWidthCm']].to_numpy()
# Creamos la meshgrid con los valores mínimo y máximo de 'x' i 'y'.
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
# Definimos la función que nos graficará las fronteras de decisión
def plot_decision_boundaries(model, X, y, x_min=x_min,
x_max=x_max,
y_min=y_min,
y_max=y_max, delta: float = .02) -> None:
"""Plot data points and deicision boundaries learned by the model.
Arguments:
----------
model: scikit-learn like model
X: np.array[n_samples, n_features]
Only first 2 features will be considered because it is a 2d plot.
Feature 0 in the x axis, and feature 1 in the y axis.
y: np.array
Labels for each sample.
delta: float
Increment between consecutive points when computing the grid for plotting boundaries.
Lower value for higher resolution.
"""
xx, yy = np.meshgrid(np.arange(x_min, x_max, delta),
np.arange(y_min, y_max, delta))
#Predecimos el clasificador con los valores de la meshgrid
# En este caso model será nuestra variable que contiene el modelo a estudiar, es decir K-nn, SVM,...
# Por ejemplo para K-nn sería model = KNeighborsClassifier()
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
# Creamos mapas de colores con ListedColormap para ver como separa las clases.
# En este caso usaremos:
# Iris-setosa : darkorange
# Iris-versicolor: c
# Iris-virginica: darkblue
cmap_light = ListedColormap(['orange', 'cyan', 'cornflowerblue'])
cmap_bold = ListedColormap(['darkorange', 'c', 'darkblue'])
# Ponemos el resultado en una figura de color
Z = Z.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z, cmap= cmap_light)
# Dibujamos también los puntos de entrenamiento
plt.scatter(X[:, 0], X[:, 1], c=y, cmap= cmap_bold)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.show()
def plot_decision_boundaries_bonus(x, y, labels, model,
x_min=x_min,
x_max=x_max,
y_min=y_min,
y_max=y_max,
grid_step=0.02):
xx, yy = np.meshgrid(np.arange(x_min, x_max, grid_step),
np.arange(y_min, y_max, grid_step))
# Predecimos el classifier con los valores de la meshgrid.
Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
# Hacemos reshape para tener el formato correcto.
Z = Z.reshape(xx.shape)
# Seleccionamos una paleta de color.
arr = plt.cm.coolwarm(np.arange(plt.cm.coolwarm.N))
arr_hsv = mpl.colors.rgb_to_hsv(arr[:,0:3])
arr_hsv[:,2] = arr_hsv[:,2] * 1.5
arr_hsv[:,1] = arr_hsv[:,1] * .5
arr_hsv = np.clip(arr_hsv, 0, 1)
arr[:,0:3] = mpl.colors.hsv_to_rgb(arr_hsv)
my_cmap = ListedColormap(arr)
# Hacemos el gráfico de las fronteras de decisión.
fig, ax = plt.subplots(figsize=(7,7))
plt.pcolormesh(xx, yy, Z, cmap=my_cmap)
# Añadimos los punts.
ax.scatter(x, y, c=labels, cmap='coolwarm')
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
ax.grid(False)