Ejemplo de uso de DBSCAN en Python para eliminación de outliers

Dentro de los algoritmos de clustering de aprendizaje no supervisado, uno de los más interesantes -y quizás no tan conocido- es DBSCAN, un algoritmo de agrupamiento basado en la densidad, que modela los clústers como cúmulos de alta densidad de puntos. Por lo cual, si un punto pertenece o no a un clúster, debe estar cerca de un montón de otros puntos de dicho clúster de datos.

Imagen vía Wikipedia.

Es un algoritmo muy útil para la detección de outliers (valores atípicos), ya que considerará como “agrupados” todos aquellos puntos de la zonas más densas (normalmente puntos válidos) y considerará como anormales aquellos puntos alejados y en zonas poco densas (normalmente valores atípicos).

¿Cómo funciona?

Básicamente tiene dos parámetros, un número épsilon (eps) y un número minPoints (min_samples), y se elige de inicio un punto arbitrario en el conjunto de datos. Si hay una cantidad de puntos mayor o igual a minPoints a una distancia épsilon del punto arbitrario, a partir de ese momento se consideran todos los puntos como parte de un clúster. A continuación, se expande ese grupo mediante la comprobación de todos los nuevos puntos y ver si ellos también tienen más puntos minPoints a una distancia épsilon, creciendo el clúster de forma recursiva en caso afirmativo.

Si el punto arbitrario escogido tiene menos de minPoints puntos en su círculo de radio épsilon, y tampoco es parte de cualquier otra agrupación, entonces, se considera un “punto de ruido” que no pertenecen a ningún grupo.

Ejemplo de uso de DBSCAN

Usaremos un algoritmo DBSCAN en Python para “limpiar” una curva de Irradiancia-Potencia de una placa fotovoltaica. Además, compararemos dicha curva con la recta ideal de potencia propuesta por el fabricante.

Partimos de un mujeres solteras en ibague tolima con los datos ya limpios y preparados con 2300 registros y dos variables:

Los primeros 15 registros del dataframe.

1- Carga de librerías

Cargamos las librerías que vamos a usar en el código.

# Carga de librerías.
import pandas as pd
from sklearn.decomposition import PCA
import sklearn.neighbors
from sklearn.neighbors import kneighbors_graph
from sklearn import preprocessing
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN

2- Visualizamos gráficamente los datos.

Con objeto de tener una visión antes de aplicar el algoritmo, graficamos los datos representando en el eje X la irradiancia y en el eje Y la potencia eléctrica. Representamos, además, la recta ideal según el fabricante para visualizar la diferencia con respecto al resultado teórico:

# Representación gráfica de las señales.
f1 = df['Irradiancia'].values
f2 = df['Potencia DC'].values
# Calculamos la recta de potencia del fabricante.
rend = 0.145
area = 1.652 * 11 * 22 * 0.001
f3 = df['Irradiancia'].values * rend * area
# Representamos gráficamente.
plt.scatter(f1, f2, s=70)
plt.scatter(f1,f3)
plt.xlabel("Irradiancia")
plt.ylabel("Potencia DC")
plt.title("Curva Irradiancia-Potencia fotovoltaica")
plt.show()
Curva Irradiancia-Potencia en bruto.

Como se puede observar, la nube de puntos sigue de forma más o menos fiel la recta de potencia del fabricante cuando los valores de irradiancia son bajos, y tiende a separase cuando la irradiancia es alta. Esto es un comportamiento normal ya que a mayor temperatura de los paneles solares menor es el rendimiento.

Además, la nube de puntos muestra muchos valores atípicos que vamos a intentar limpiar con el algoritmo DBSCAN.

3- Normalización de datos.

Como ocurre con cualquier algoritmo de http://exponentis.es/baile-griego-hombre-solo que use medidas de distancia, los datos deben ser normalizados antes de aplicar en ellos cualquier algoritmo. En este caso, usaremos un escalado MinMax() de la librería sklearn, que normalizará todos nuestros datos entre 0 y 1:

# Se normalizan los datos con MinMax()
min_max_scaler = preprocessing.MinMaxScaler() 
df_escalado = min_max_scaler.fit_transform(df)
df_escalado = pd.DataFrame(df_escalado) 
df_escalado = df_escalado.rename(columns = {0: 'Potencia DC', 1: 'Irradiancia'})

El escalado transforma el dataframe df a un dataframe escalado llamado df_escalado. Hay que convertir este nuevo dataframe a Pandas para poder seguir trabajando con este formato.

Echamos un vistazo a nuestro nuevo dataframe df_escalado:

Primeros 15 registros del dataframe normalizado.

4- Primera aplicación de DBSCAN

Aplicamos ahora el algoritmo DBSCAN sobre el dataset normalizado probando con unos parámetros “a ojo”. Elegimos, por ejemplo, el radio eps = 0,08 y el min_samples = 5.

# Ejecutamos DBSCAN
dbscan = DBSCAN(eps=0.08, min_samples = 5, metric = "euclidean").fit(df_escalado)
clusters = dbscan.fit_predict(df_escalado)
df_values = df.values
# Graficación de los clústers.
plt.scatter(df_values[:, 1], df_values[:, 0], c=clusters, cmap="plasma")
plt.xlabel("Irradiancia")
plt.ylabel("Potencia DC")

Y obtenemos la siguiente clusterización:

Clusterización de la curva Irradiancia-Potencia.

Se observa que DBSCAN ha creado dos grupos: el amarillo que corresponde al núcleo denso de la nube de puntos y el azul oscuro, que coge los puntos más alejados de ésta nube central.

Parece, a priori, que hemos elegido una parametrización que deja escapar muchos posibles outliers alrededor del grupo denso central, por lo que quizás seria conveniente probar con otros valores de eps y min_samples. Una opción para evitar esta parametrización “a ojo” es elegir los parámetros en función de una curva elbow, tal como se ve en el siguiente apartado.

5. Parametrización de DBSCAN

Aunque ajustando “a ojo” los parámetros en el apartado anterior ya podríamos terminar nuestro clustering (e ignorar este apartado y pasar al siguiente), podemos afinar de forma un poco más precisa utilizando la técnica del codo también conocida como la famosa curva elbow. Esta técnica consiste en fijar un valor min_samples y, partir de ahí, graficar todas los radios eps de los puntos ordenados por distancia, de forma que, cuando los radios comienzan a aumentar de forma exponencial (el codo de la curva) significa que nos alejamos de la zona de alta densidad (valores normales) y entramos en la zona de baja densidad (valores atípicos).

Creamos la curva elbow de la siguiente forma:

## Parametrización de DBSCAN.
estimator = PCA (n_components = 2)
X_pca = estimator.fit_transform(df_escalado)
dist = sklearn.neighbors.DistanceMetric.get_metric('euclidean')
matsim = dist.pairwise(X_pca)
minPts  = 5 # Fijamos el parámetro minPts
A = kneighbors_graph(X_pca, minPts, include_self=False)
Ar = A.toarray()
seq = []
for i,s in enumerate(X_pca):
    for j in range(len(X_pca)):
        if Ar[i][j] != 0:
            seq.append(matsim[i][j])
seq.sort()
plt.plot(seq)
plt.show()

Obtenemos el siguiente resultado:

Curva elbow en DBSCAN.

De forma aproximada, podemos fijar el valor del radio epsilon entre 0,0125 y 0,025, que es donde la curva comienza a crecer de forma exponencial.

Un valor de eps = 0,0125 será mucho más restrictivo, eligiendo solo puntos de zonas muy densas, y un valor de eps = 0,025 será más “relajado”, permitiendo valores de zonas menos densas.

6. Clusterización con DBSCAN.

Después de aplicar la curva elbow, decidimos quedarnos en un punto medio y elegir un eps = 0,01875 con un min_samples = 5. Volvemos a aplicar DBSCAN con estos parámetros:

# Ejecutamos DBSCAN
dbscan = DBSCAN(eps=0.01875, min_samples = 5, metric = "euclidean").fit(df_escalado)
clusters = dbscan.fit_predict(df_escalado)
df_values = df.values
# Graficación de los clústers.
plt.scatter(df_values[:, 1], df_values[:, 0], c=clusters, cmap="plasma")
plt.xlabel("Irradiancia")
plt.ylabel("Potencia DC")
Clusterización de la curva Irradiancia-Potencia.

Se observan claramente tres clústers:

  • Clúster -1: son los puntos azul oscuros, valores poco densos que DBSCAN interpreta como outliers.
  • Clúster 0: de color rosa oscuro, son los datos más densos -y por tanto normales- de la curva.
  • Clúster 1: de color amarillo, es un pequeño grupo que no pertenece al clúster 0 pero no tienen porqué ser valores anómalos.

7. Recuento de outliers y etiquetación de clústeres.

Una vez aplicado el algoritmo de DBSCAN, hacemos recuento de valores detectados como outliers y etiquetamos los clústers en el dataset:

# Vemos cada uno de los clusters cuántos valores tiene.
copy = pd.DataFrame()
copy['Potencia DC']=df['Potencia DC'].values
copy['Irradiancia']=df['Irradiancia'].values
copy['label'] = clusters;
cantidadGrupo =  pd.DataFrame()
cantidadGrupo['cantidad']=copy.groupby('label').size()
cantidadGrupo
Datos en cada clúster.

El código anterior nos muestra que 207 registros de 2300 (9% de los datos) han sido etiquetados en el clúster -1, es decir, como outliers, por el algoritmo DBSCAN.

En el nuevo dataframe llamado copy hemos añadido una nueva columna llamada ‘label‘ que etiqueta los clúster a los que pertenecen:

La nueva columna “label” etiqueta los clústers de DBSCAN.

8. Eliminación de outliers.

Una vez con los datos etiquetados, procedemos a eliminar los datos que DBSCAN ha etiquetado como outliers y volvemos representar la gráfica sin estos puntos:

# Eliminamos los puntos marcados como outliers y representamos gráficamente.
copy = copy.drop(copy[copy['label'] == -1].index)

# Gráfica del dataframe "limpio"
f1 = copy['Irradiancia'].values
f2 = copy['Potencia DC'].values
f3 = copy['Irradiancia'].values * rend * area

plt.scatter(f1, f2, s=70)
plt.scatter(f1,f3)
plt.xlabel("Irradiancia")
plt.ylabel("Potencia DC")
plt.title("Curva Irradiancia-Potencia fotovoltaica sin oultliers")
plt.show()
Curva de irradiancia-potencia “limpiada” de outliers con DBSCAN.
Para saber más:
Clustering basado en densidad.
Ejemplo con DBSCAN.
How to Create a Unsupervised Learning Model with DBSCAN.
DBSCAN: Density-Based Clustering Essentials.

2 comentarios en “Ejemplo de uso de DBSCAN en Python para eliminación de outliers”

Deja una respuesta