Clustering de series temporales con PythonEnrique Blanco 13 enero, 2021 La proliferación y la ubicuidad de los datos con dependencia temporal en un amplio abanico de disciplinas genera un interés sustancial en el análisis y la extracción de series temporales. La agrupación o clustering es uno de los métodos de extracción de datos más populares, no sólo por su poder de exploración, sino también como paso previo al procesamiento de otras técnicas. tslearn es una librería de Machine Learning de Python para series temporales que ofrece herramientas para el preprocesamiento y la extracción de características, así como modelos dedicados para clustering, clasificación y regresión. En esta rápida prueba de concepto, vamos a realizar una tarea de clustering de un dataset de una sola variable bien conocido. Importando librerías Para empezar, importamos las librerías que vamos a usar: import tslearn import numpy as np import time import matplotlib.pyplot as plt Con TimeSeriesScalerMeanVariance procesaremos las time series para que tengan una media nula y una desviación típica igual a 1. from tslearn.preprocessing import TimeSeriesScalerMeanVariance Obteniendo un dataset Afortunadamente, tslearn nos ofrece un gran número de series temporales con las que podemos empezar a trastear. Podemos acceder a los UCR/UEA time series datasets de la siguiente manera. from tslearn.datasets import UCR_UEA_datasets En el siguiente enlace se puede consultar la extensa lista de datasets de series temporales a la que podemos acceder. Como se puede ver, todos estos datasets están convenientemente etiquetados, por lo que podremos comprobar la bondad de nuestro modelos una vez que hayamos terminado de clusterizar las secuencias. En este artículo vamos a trabajar con el dataset ECG5000. Este dataset contiene 20 horas de registros de electrocardiogramas (ECGs). En total se cuentan con 5,000 pares de ECGs y etiquetas. Fue publicado en «Goldberger AL, Amaral LAN, Glass L, Hausdorff JM, Ivanov PCh, Mark RG, Mietus JE, Moody GB, Peng C-K, Stanley HE. PhysioBank, PhysioToolkit, and PhysioNet: Components of a New Research Resource for Complex Physiologic Signals. Circulation 101(23)«. Figura 1. Muestra de electrocardiogramas de ECG5000. Fuente. Es muy simple acceder a las particiones de entrenamiento y testeo del dataset que nos interesa. X_test, y_test, X_train, y_train = UCR_UEA_datasets().load_dataset("ECG5000") Las etiquetas del dataset tienen el siguiente significado: 1 = N – Normal2 = SVEB – Supraventricular ectopic beats3 = VEB – Ventricular ectopic beats4 = Fusion beat5 = Unknown beat Entendiendo el dataset Lo primero que hay que comprobar es la distribución de etiquetas en el dataset. labels, counts = np.unique(y_train, return_counts=True) _labels = ['Normal', 'SVEB', 'VEB', 'Fusion', 'Unknown'] plt.figure(figsize=(8, 6)) plt.bar(_labels, counts, align='center') plt.gca().set_xticks(_labels) plt.grid() plt.ylabel('No. samples') plt.xlabel('Beat type') plt.show() Figura 2. Histograma de clases en ECG5000. Como se puede observar, el dataset está muy desbalanceado. Hay muy pocas muestras de las clases y_train > 2 en comparación con los latidos normales o SVEB. Por ello, para simplificar el análisis, vamos a partir el dataset en latidos normales (y_train | y_test = 0) y latidos anómalos (y_train | y_test = 1) y_test[y_test == 1] = 0 y_test[y_test > 1] = 1 y_train[y_train == 1] = 0 y_train[y_train > 1] = 1 El número de muestras pertenecientes a cada clase sigue estando desbalanceado, aunque no de manera tan extrema como antes. Esto nos permitirá clusterizar en dos grupos con mejores resultados. labels, counts = np.unique(y_train, return_counts=True) _labels = ['Normal', 'Anomalous'] plt.figure(figsize=(8, 6)) plt.bar(_labels, counts, align='center') plt.gca().set_xticks(_labels) plt.grid() plt.ylabel('No. samples') plt.xlabel('Beat type') plt.show() Figura 3. Histograma de clases adaptadas para ECG5000. Preprocesado del dataset y visualización Debemos escalar nuestras series temporales para poder obtener buenos resultados de clustering. X_train = TimeSeriesScalerMeanVariance().fit_transform(X_train) X_test = TimeSeriesScalerMeanVariance().fit_transform(X_test) Como vemos, contamos con 4500 secuencias de 140 elementos de una sola variable para entrenar y 500 secuencias de igual longitud para testear. print(X_train.shape) print(X_test.shape) (4500, 140, 1)(500, 140, 1) size = X_train.shape[1] n_classes = len(set(y_train)) plt.figure(figsize=(10, 10)) for i, cl in enumerate(set(y_train)): plt.subplot(n_classes, 1, i + 1) for ts in X_train[y_train == cl]: if cl == 0: plt.plot(ts.ravel(), color="green" , alpha=.15) else: plt.plot(ts.ravel(), color="red" , alpha=.15) plt.xlim(0, size - 1) plt.grid() plt.suptitle("Training time series") plt.show() Figura 4. ECG5000 Training dataset (normal – green ; anomalous – red) plt.figure(figsize=(10, 10)) for i, cl in enumerate(set(y_train)): plt.subplot(n_classes, 1, i + 1) for ts in X_test[y_test == cl]: if cl == 0: plt.plot(ts.ravel(), color="green" , alpha=.15) else: plt.plot(ts.ravel(), color="red" , alpha=.15) plt.xlim(0, size - 1) plt.grid() plt.suptitle("Test dataset time series") plt.show() Figura 5. ECG5000 Test dataset (normal – green ; anomalous – red) A simple vista, únicamente en base a la forma, es sencillo distinguir entre los latidos de un individuo sano y los de un paciente con una patología (sin especificar). Clustering de secuencias Vamos a probar dos algoritmos de clustering: K-Means y K-Shape. Para el caso de K-Means, vamos a centrarnos únicamente en la métrica «euclidean». Hay otras dos alternativas para este hiperparámetro: «dtw» y «softdtw». Se deja como ejercicio al lector, si éste lo desea. Aviso: los tiempos de ejecución en estos dos casos son ostensiblemente más largos. from tslearn.clustering import TimeSeriesKMeans, KShape, silhouette_score Time Series K-Means para dos clusters start_time = time.time() km_euc = TimeSeriesKMeans(n_clusters=2, verbose=2, n_init=10, metric="euclidean").fit(X_train) labels_euc = km_euc.labels_ print("--- %s seconds ---" % (time.time() - start_time)) ... --- 9.683111190795898 seconds --- Time Series K-Shape para dos clusters start_time = time.time() ks = KShape(n_clusters=2, verbose=True, n_init=10).fit(X_train) labels_kshape = ks.labels_ print("--- %s seconds ---" % (time.time() - start_time)) ... --- 91.33036160469055 seconds --- Las silhouette scores de ambas soluciones son muy similares: silhouette_score(X_train, labels_euc, metric="euclidean") 0.45911902366303975 silhouette_score(X_train, labels_kshape, metric="euclidean") 0.4550914094152607 Resultados de los agrupamientos Dado que contamos con las etiquetas del dataset, podemos comprobar cómo de bien hemos agrupado los electrocardiogramas entre pacientes sanos o con patologías. from sklearn.metrics import accuracy_score print('Training dataset accuracy for TimeSeriesKMeans clustering with euclidean metric: ', accuracy_score(y_train, labels_euc)) print('Training dataset accuracy for KShape clustering: ', accuracy_score(y_train, labels_kshape)) Training dataset accuracy for TimeSeriesKMeans clustering with euclidean metric: 0.9411 Training dataset accuracy for KShape clustering: 0.9391 print('Test dataset accuracy for TimeSeriesKMeans clustering with euclidean metric: ', accuracy_score(y_test, km_euc.predict(X_test))) print('Test dataset accuracy for KShape clustering: ', accuracy_score(y_test, ks.predict(X_test))) Test dataset accuracy for TimeSeriesKMeans clustering with euclidean metric: 0.958 Test dataset accuracy for KShape clustering: 0.954 La clasificación parece satisfactoria. Como vemos, tenemos una precisión cercana al 96% en el test dataset. Vamos a analizar, con matrices de confusión, los errores de Tipo I y Tipo II en los que nuestros modelos de clustering está incurriendo. Estos conceptos ya se trataron en los siguientes posts: Video Post #11: Tipos de error en Machine Learning y Tipos de error en Machine Learning, ¿los conoces?. from sklearn.metrics import confusion_matrix confusion_matrix(y_train, labels_euc) NormalAnomalousNormal256364Anomalous2011672Tabla 1. Training dataset TimeSeriesKMeans confusion matrix. confusion_matrix(y_train, labels_kshape) NormalAnomalousNormal254582Anomalous1921681Tabla 2. Training dataset KShape confusion matrix. confusion_matrix(y_test, km_euc.predict(X_test)) NormalAnomalousNormal2839Anomalous12196Tabla 3. Test dataset TimeSeriesKMeans confusion matrix. confusion_matrix(y_test, ks.predict(X_test)) NormalAnomalousNormal28311Anomalous12196Tabla 4. Test dataset KShape confusion matrix. El desempeño de ambos algoritmos es muy parecido. Dado que estamos trabajando con datos clínicos de pacientes, lo que deseamos es tener el menor error de tipo I posible. Es decir, debemos minimizar la posibilidad de que a una persona con una patología cardiaca no se le diagnostique correctamente, considerándolo una persona sana. Para ambas aproximaciones, esto sólo ocurre en 12 muestras en el test dataset. Representando los clusters y sus centroides Con las siguientes líneas de código podremos representar los distintos grupos de series temporales junto con los centroides que los definen. Estos centroides se consiguen con model.cluster_centers_. def plot_groups(model): if model == 'kmeans-euclidean': model = km_euc elif model == 'kshape': model = ks else: sys.exit('Please, provide a valid model string.') labels = model.labels_ plt.figure(figsize=(10, 10)) for yi in range(2): plt.subplot(2, 1, 1 + yi) for xx in X_train[labels == yi]: plt.plot(xx.ravel(), "k-", alpha=.15) plt.plot(model.cluster_centers_[yi].ravel(), "r-") plt.xlim(0, size-1) if yi == 0: plt.title("Normal") else: plt.title("Anomalous") plt.grid() plt.tight_layout() plt.show() plot_groups(model='kmeans-euclidean') Figura 6. Secuencias del training dataset para latidos normales (arriba) y anómalos (abajo) para TimeSeriesKMeans. En rojo, se observan los centroides de los dos clusters. def compare_cluster_centers(): plt.figure(figsize=(10, 10)) for yi in range(2): plt.subplot(2, 1, 1 + yi) plt.plot(km_euc.cluster_centers_[yi].ravel(), "-", alpha=1., label='K-Means euclidean') #plt.plot(km_softdtw.cluster_centers_[yi].ravel(), "-", alpha=0.5, label='K-Means softdtw') plt.plot(ks.cluster_centers_[yi].ravel(), "-", alpha=.5, label='K-Shape') if yi == 0: plt.title("Normal") else: plt.title("Anomalous") plt.xlim(0, size-1) plt.grid() plt.legend() plt.tight_layout() plt.show() Figura 7. Secuencias del training dataset para latidos normales (arriba) y anómalos (abajo) para KShape. En rojo, se observan los centroides de los dos clusters. Conclusiones Los centroides de los clusters son bastante similares comparando entre los dos algoritmos usados, tal y como se preveía dadas las tasas de acierto y error mostradas por las matrices de confusión. def compare_cluster_centers(): plt.figure(figsize=(10, 10)) for yi in range(2): plt.subplot(2, 1, 1 + yi) plt.plot(km_euc.cluster_centers_[yi].ravel(), "-", alpha=1., label='K-Means euclidean') plt.plot(ks.cluster_centers_[yi].ravel(), "-", alpha=.5, label='K-Shape') if yi == 0: plt.title("Normal") else: plt.title("Anomalous") plt.xlim(0, size-1) plt.grid() plt.legend() plt.tight_layout() plt.show() compare_cluster_centers_classes() Figura 8. Perfiles de los centroides para latidos normales (arriba) y anómalos (abajo) con TimeSeriesKMeans (azul) y KShape (amarillo). def compare_cluster_centers_classes(): plt.figure(figsize=(10, 6)) plt.plot(km_euc.cluster_centers_[0].ravel(), "-", c='green', alpha=.75, label='Normal') plt.plot(km_euc.cluster_centers_[1].ravel(), "-", c='red', alpha=.75, label='Anomalous') plt.xlim(0, size-1) plt.title("Cluster Centers comparison per classes") plt.tight_layout() plt.legend() plt.grid() plt.show() compare_cluster_centers_classes() Figura 9. Diferencias en el perfil de los centroides para TimeSeriesKMeans. Resulta muy interesante ver cómo con unas pocas líneas de código en Python, se puede obtener un perfil promedio de latido que nos permite diferenciar entre un ritmo cardíaco normal y un individuo con una patología. Otra opción alternativa a la clusterización de series temporales de este tipo podría abordarse a través de una reducción de dimensionalidad con PCA. Por ejemplo, se podría convertir el training dataset en un pandas dataframe de 4500 filas y 140 columnas y, sobre ese array, realizar un Principal Component Analysis. Con 2 componentes mantendríamos un 65% de varianza y con 3 componentes subiríamos hasta el 75%. Con esta aproximación y también de manera muy rápida, podríamos encontrar patrones comunes entre secuencias muy bien agrupados. Figura 10. Scatterplot de las primeras 2 componentes tras realizar PCA en el training dataset de ECG5000. Figura 11. Scatterplot 3D de las primeras 3 componentes tras realizar PCA en el training dataset de ECG5000. Os invitamos a probar a usar tslearn en problemas de clasificación, clustering y regresión de series temporales con éste y otros de los muchos datasets disponibles. Para mantenerte al día con LUCA visita nuestra página web, suscríbete a LUCA Data Speaks o síguenos en Twitter, LinkedIn y YouTube. Cómo transformar una compañía(XII): la transformación del talento internoDrones que usan IA para salvar vidas
Roberto García Esteban ChatGPT y Cloud Computing: un matrimonio bien avenido ChatGPT (quizá no sepas que son las siglas de Chat Generative Pre-Trained Transformer) está en boca de todos por su impresionante habilidad para generar textos que parecen escritos por...
Olivia Brookhouse ¿Puede la Inteligencia Artificial entender las emociones? Cuando John McCarthy y Marvin Minsky iniciaron la Inteligencia Artificial en 1956, se sorprendieron de cómo una máquina podía resolver rompecabezas increíblemente difíciles en menos tiempo que los humanos. Sin...
Javier Martínez Borreguero Automatización, Conectividad e Inteligencia Aumentada al servicio de una reindustrialización competitiva, disruptiva y sostenible Por segundo año consecutivo vuelvo a participar en el Advanced Factories (AF 2023), la mayor exposición y congreso profesional dedicado a la Industria 4.0 del sur de Europa. Un...
Nacho Palou Medidas para reducir la brecha digital de género sin esperar 32 años El informe Sociedad Digital en España 2023, de Fundación Telefónica, dedica un apartado específico para analizar la brecha de género en el ámbito del talento digital. Destaca que, si bien...
Nacho Palou Raspberry Pi para Edge AI: Inteligencia Artificial en el borde para todos Raspberry Pi es un popular ordenador muy utilizado entre desarrolladores, estudiantes y aficionados a la informática, a la robótica y a ‘cacharrear’. Entre sus virtudes están su bajo coste...
Carlos Lorenzo Ya no eres solo una empresa de productos o servicios, eres una empresa de datos Todas las empresas que operan en la actualidad son en realidad empresas de datos. Y lo son porque día a día almacenan y utilizan una gran cantidad de información:...