Imputación KNN

Author

Ing Gabriel Chávez Camargo

Published

June 11, 2024

Imputación KNN

El algoritmo de k vecinos más cercanos, también conocido como KNN o k-NN, es un clasificador de aprendizaje supervisado no paramétrico, que utiliza la proximidad para hacer clasificaciones o predicciones sobre la agrupación de un punto de datos individual.

Importamos la librerías utilizadas:

Code
import numpy  as np 
import matplotlib.pyplot as plt
import pandas as pd

Versiones utilizadas

  • Versión de NumPy: 1.26.4

  • Versión de matplotlib: 3.8.3

  • Versión de pandas: 2.2.1

  • Versión de python: 3.11.3

  • Versión de scikit-learn: 1.4.2

Instalación de liberias

pip install numpy matplotlib pandas

Generación de los Datos

  1. Generación de datos sintéticos para demostrar la imputación mediante el algoritmo de vecinos más cercanos (k-NN).
Code
antiguedad_inmueble = np.array([5, 10, 15, 20, 25,
                                10, 15, 20, 25, 30,
                                15, 20, 25, 30, 35,
                                40, 45, 50, 55, 60,
                                55, 60, 65, 70, 75])
zona_inmubele=np.array([6,6,7,7,8,
               8,9,10,10,10,
               11,11,11,11,12,
               14,16,16,16,16,
               16,17,19,19,22])

precio_inmueble=np.array([11230,9624,13798,3215,19169,
               14982,15419,17286,14232,18092,
               18318,16260,22347,16710,28949,
               27309,32779,29743,30341,34088,
               32435,33909,32263,42067,42295])

datos=np.stack((antiguedad_inmueble,zona_inmubele,precio_inmueble),axis=1)

datos

data_frame_inmueble = pd.DataFrame({"antiguedad": antiguedad_inmueble,
                                    "zona": zona_inmubele,
                                    "precio": precio_inmueble}) 

Visualizamos los valores del Data Frame

Code
   data_frame_inmueble.head(1) 
antiguedad zona precio
0 5 6 11230

Visualizamos gráficamente nuestros valores

Code
fig=plt.figure(figsize=(4,4))
ax=fig.add_subplot(1,1,1, projection="3d")
ax.scatter(
          data_frame_inmueble["antiguedad"].values,
          data_frame_inmueble["zona"].values,
          data_frame_inmueble["precio"].values,
          marker="o",c="purple",s=500,alpha=0.25)

ax.scatter(
          data_frame_inmueble["antiguedad"].values,
          data_frame_inmueble["zona"].values,
          data_frame_inmueble["precio"].values,
          marker=".",c="black",s=300)

ax.set_xlabel("Antiguedad" ,fontsize=9,color="blue")
ax.set_ylabel("Zona Inmueble",color="blue")
ax.set_zlabel("Precio ($)",color="blue")
plt.tight_layout() 
plt.show()

Escalamiento de los datos

Todas las características deben considerarse igualmente importantes antes de la ejecución del algoritmo. Este problema se puede solucionar con el escalado de características.

En este caso hay una clase sklearn dedicada para la estandarización de datos que se llama StandardScaler. Está en el módulo sklearn.preprocessing.

Importa StandardScaler de la librería:

from sklearn.preprocessing import MinMaxScaler

Code
from sklearn.preprocessing import MinMaxScaler

\[X\_{\text{escalado}} = \frac{{X - X_{\text{mín}}}}{{X_{\text{máx}} - X_{\text{mín}}}}\]

  • X_esc: El valor escalado de la característica X. Después de aplicar MinMaxScaler, los valores de la característica X estarán en un rango específico, generalmente entre 0 y 1.
  • X: El valor original de la característica que queremos escalar.
  • X_min: El valor mínimo de la característica en el conjunto de datos. Representa el valor más pequeño que puede tomar la característica.
  • X_max: El valor máximo de la característica en el conjunto de datos. Representa el valor más grande que puede tomar la característica.

Pero… ¿Nuestras variables cambian al hacer el escalamiento de los datos? Figure 1.

Code
#escalamos los datos
escalador=MinMaxScaler()
escalados=escalador.fit_transform(data_frame_inmueble.values)
escalados=pd.DataFrame(escalados,columns=data_frame_inmueble.columns)
#Visualizamos los datos
fig=plt.figure(figsize=(12,12))
ax=fig.add_subplot(1,2,1, projection="3d")
ax.scatter(
          escalados["antiguedad"].values,
          escalados["zona"].values,
          escalados["precio"].values,
          marker="o",c="purple",s=600,alpha=0.25)

ax.set_title("Datos escalados")
ax.set_xlabel("Antiguedad" ,fontsize=9,color="blue")
ax.set_ylabel("Zona Inmueble",color="blue")
ax.set_zlabel("Precio ($)",color="blue")

ax=fig.add_subplot(1,2,2, projection="3d")
ax.scatter(datos.T[0],datos.T[1],datos.T[2],
           marker="o",c="Blue",s=600,alpha=0.25)
ax.set_title("Datos Originales")
ax.set_xlabel("Antiguedad" ,fontsize=9,color="blue")
ax.set_ylabel("Zona Inmueble",color="blue")
ax.set_zlabel("Precio ($)",color="blue")
plt.show()
Figure 1: Los datos realmente no cambian la escala es de 0 y 1

Como podemos observar un escalamiento de los datos no los modifica pero la diferencia es que a todas las características las consideramos igualmente importantes antes de la ejecución del algoritmo.

Code
# visualizacion de datos escalados
escalados.head(2)  
antiguedad zona precio
0 0.000000 0.0 0.205092
1 0.071429 0.0 0.163997

Crearemos datos faltantes a diferentes filas, en nuestro data frame escalado.

Code
faltantes=escalados.values.copy()
faltantes[[2,7,12,17,22],2]=np.nan

Visualizamos los datos con valores Nulos

Code
print(faltantes)
[[0.         0.         0.20509212]
 [0.07142857 0.         0.16399693]
 [0.14285714 0.0625            nan]
 [0.21428571 0.0625     0.        ]
 [0.28571429 0.125      0.40823951]
 [0.07142857 0.125      0.30110031]
 [0.14285714 0.1875     0.3122825 ]
 [0.21428571 0.25              nan]
 [0.28571429 0.25       0.2819089 ]
 [0.35714286 0.25       0.38068066]
 [0.14285714 0.3125     0.38646366]
 [0.21428571 0.3125     0.33380246]
 [0.28571429 0.3125            nan]
 [0.35714286 0.3125     0.3453173 ]
 [0.42857143 0.375      0.65849539]
 [0.5        0.5        0.61653019]
 [0.57142857 0.625      0.75649949]
 [0.64285714 0.625             nan]
 [0.71428571 0.625      0.69411464]
 [0.78571429 0.625      0.78999488]
 [0.71428571 0.625      0.74769703]
 [0.78571429 0.6875     0.78541453]
 [0.85714286 0.8125            nan]
 [0.92857143 0.8125     0.99416581]
 [1.         1.         1.        ]]

Imputación KNN

La clase KNNImputer del módulo sklearn.impute en scikit-learn se utiliza para imputar valores faltantes en un conjunto de datos utilizando el algoritmo de k vecinos más cercanos (KNN). Este imputador reemplaza los valores faltantes utilizando los k vecinos más cercanos en el espacio de características.

from sklearn.impute import KNNImputer

En este caso utilizaremos los dos parámetros:

“uniform”: Todos los vecinos más cercanos tienen el mismo peso al imputar el valor faltante. Esto significa que cada vecino contribuye igualmente a la imputación del valor faltante.

“distance”: Los vecinos más cercanos tienen un peso inversamente proporcional a su distancia al punto con el valor faltante. Esto significa que los vecinos más cercanos tienen más influencia en la imputación que los vecinos más lejanos.

Code
from sklearn.impute import KNNImputer

# Knn uniform

imputer=KNNImputer(n_neighbors=5, weights="uniform")
imputer_uniform=imputer.fit_transform(faltantes)
# Knn Distance
imputer=KNNImputer(n_neighbors=5, weights="distance")

imputer_distance=imputer.fit_transform(faltantes)

Visualización de los dos arrays

Code
print(np.stack((imputer_uniform,imputer_distance),axis=1)[:3])
[[[0.         0.         0.20509212]
  [0.         0.         0.20509212]]

 [[0.07142857 0.         0.16399693]
  [0.07142857 0.         0.16399693]]

 [[0.14285714 0.0625     0.23712385]
  [0.14285714 0.0625     0.20241669]]]

Como podemos observar, ya no hay valores nulos en las diferentes filas después de aplicar la imputación y se observan diferentes valores [:3]. Sin embargo, para tener una mejor comprensión visual de los valores imputados, vamos a representarlos gráficamente.

Code
fig=plt.figure(figsize=(8,8))
filtro=~np.isnan(faltantes.T[2])
ax=fig.add_subplot(1,1,1, projection="3d")
#Graficamos los valores Originales
ax.scatter(
            escalados["antiguedad"].values[filtro],
          escalados["zona"].values[filtro],
          escalados["precio"].values[filtro],
           label="Originales",
           marker="o",c="blue",s=600,alpha=0.15)

#Graficamos los valores faltantes
ax.scatter(escalados["antiguedad"].values[~filtro],
          escalados["zona"].values[~filtro],
          escalados["precio"].values[~filtro],
           label="NaN/Ausentes",
           marker="*",c="cyan",s=800,alpha=0.65)
#Graficamos los valores remplazados con uniform
ax.scatter(imputer_uniform.T[0][~filtro],
           imputer_uniform.T[1][~filtro],
           imputer_uniform.T[2][~filtro],
           label="IMPUTE UNIFORM",
           marker="D",c="deeppink",s=600)
#Graficamos los valores remplazados con distance
ax.scatter(imputer_distance.T[0][~filtro],
           imputer_distance.T[1][~filtro],
           imputer_distance.T[2][~filtro],
           label="IMPUTE distance",
           marker="s",c="darkorange",s=600)


ax.set_xlabel("Antiguedad" ,fontsize=15,color="blue")
ax.set_ylabel("Zona Inmueble",fontsize=15,color="blue")
ax.set_zlabel("Precio ($)",fontsize=15,color="blue")
ax.legend(ncol=4)


plt.show()

Al observar los resultados, podemos notar la diferencia entre los valores originales, los valores faltantes (NaN), y las aproximaciones generadas por el imputador KNN con las estrategias “Uniform” y “Distance”. Es evidente que los valores imputados por KNN tienden a estar muy cerca de nuestros valores faltantes.

Fuentes:

Scikit-Learn

Canales data Scientist:

en_coders

Codigo Maquina