03.3 Clustering. Análisis de microarrays¶
Objetivos de la práctica
Utilizar datos de microarrays para agrupar genes en función de sus patrones de expresión.
Comparar los grupos obtenidos con los diferentes tipos de cáncer anotados.
El conjunto de datos NCI60 contiene información genética de 64 líneas celulares cancerígenas. Para cada una de ellas, se ha cuantificado la expresión de 6830 genes mediante tecnología microarray. Se conoce el tipo de cáncer (histopatología) al que pertenece cada línea celular y se puede utilizar esta información para evaluar si el método de clustering (agglomerative hierarchical clustering) es capaz de agrupar correctamente las líneas empleando los niveles de expresión génica.
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd
import numpy as np
from skimage import io
from IPython import display
Carga de Datos¶
Descargar el archivo que se encuentra en Moodle con el nombre nci_data.csv y situarlo en una carpeta de nombre data en el path donde se encuentre el cuaderno Jupyter.
df = pd.read_csv('data/nci_data.csv')
df.head()
Gene | s1 | s2 | s3 | s4 | s5 | s6 | s7 | s8 | s9 | ... | s55 | s56 | s57 | s58 | s59 | s60 | s61 | s62 | s63 | s64 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | g1 | 0.300 | 0.679961 | 0.940 | 0.280 | 0.485 | 0.310 | -0.830 | -0.190 | 0.460 | ... | 0.010 | -0.620 | -0.380 | 0.04998 | 0.650 | -0.030 | -0.270 | 0.210 | -0.050 | 0.350 |
1 | g2 | 1.180 | 1.289961 | -0.040 | -0.310 | -0.465 | -0.030 | 0.000 | -0.870 | 0.000 | ... | -1.280 | -0.130 | 0.000 | -0.72002 | 0.640 | -0.480 | 0.630 | -0.620 | 0.140 | -0.270 |
2 | g3 | 0.550 | 0.169961 | -0.170 | 0.680 | 0.395 | -0.100 | 0.130 | -0.450 | 1.150 | ... | -0.770 | 0.200 | -0.060 | 0.41998 | 0.150 | 0.070 | -0.100 | -0.150 | -0.090 | 0.020 |
3 | g4 | 1.140 | 0.379961 | -0.040 | -0.810 | 0.905 | -0.460 | -1.630 | 0.080 | -1.400 | ... | 0.940 | -1.410 | 0.800 | 0.92998 | -1.970 | -0.700 | 1.100 | -1.330 | -1.260 | -1.230 |
4 | g5 | -0.265 | 0.464961 | -0.605 | 0.625 | 0.200 | -0.205 | 0.075 | 0.005 | -0.005 | ... | -0.015 | 1.585 | -0.115 | -0.09502 | -0.065 | -0.195 | 1.045 | 0.045 | 0.045 | -0.715 |
5 rows × 65 columns
Los 64 tipos de cancer se encuentra en el archivo nci_label.txt también situado en Moodle y se descarga a la misma carpeta data
df_lbl = pd.read_csv('data/nci_label.txt', header=None, names=["Cell"])
df_lbl.head()
labels_ok = df_lbl.values
np.size(labels_ok), np.unique(labels_ok)
(64,
array(['BREAST', 'CNS', 'COLON', 'K562A-repro', 'K562B-repro', 'LEUKEMIA',
'MCF7A-repro', 'MCF7D-repro', 'MELANOMA', 'NSCLC', 'OVARIAN',
'PROSTATE', 'RENAL', 'UNKNOWN'], dtype=object))
La estructura del fichero nci_data.csv es:
Una columna que sirve para enumerar las 6830 expresiones genéticas y que no se utilizará.
64 columnas con los tipos de cancer.
Por tanto, se toman las 64 últimas columnas, por lo que es necesario transponer la matriz X_trap para obtener el conjunto inicial de entrenamiento, que se denomina X_ini.
X_ini ya tiene 64 filas por cada tipo histológico de cancer con 3680 características, siendo cada uno de ellos la expresión de un gen.
X_trap = df.values[:,1:65]
X_ini = np.transpose(X_trap)
X_ini.shape
(64, 6830)
Estandarización de los datos de entrada
Para poder probar con datos estandarizados o no estandarizados en la siguiente celda tenemos las dos opciones de forma que en la matriz X con la que se va a trabajar se podrá volcar el dato estandarizado o no, según se comente o descomente la última línea de código
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X_ini)
#X = X_ini
Elección del número de clusters¶
Criterio de Inercia (suma de distancias al cuadrado a los centroides)¶
Se calcula la inercia relativa a Suma de Distancias al Cuadrado del modelo completo, que se puede calcular con 1 cluster
from sklearn.cluster import KMeans
import sklearn.metrics as metrics
km = KMeans(n_clusters=1, init='k-means++', max_iter=300, tol=1e-04, random_state=1)
km.fit(X)
tss=km.inertia_
distortions = []
for i in range(2, 20):
km = KMeans(n_clusters=i, init='k-means++', max_iter=300, tol=1e-04)
km.fit(X)
distortions.append(km.inertia_/tss)
plt.figure(figsize=(7, 4), dpi=80)
plt.plot(range(2,20), distortions, label="SSE rel. vs K")
plt.xlabel("Número de Clusters")
plt.ylabel("Inercia relativa")
plt.legend(loc='upper right')
plt.show()
Conclusión del gráfico del codo: no resulta muy concluyente por el escaso número de registros de la matriz X, por lo que se echa mano de otro criterio de validación intrínseca que es el criterio de la Silueta
Criterio de la Silueta¶
El coeficiente de silueta se calcula utilizando la distancia media dentro del grupo (a) y la distancia media del grupo más cercano (b) para cada muestra. El Coeficiente de silueta para una muestra es (b - a) / max(a, b). Para aclarar, b es la distancia entre una muestra y el grupo más cercano del que la muestra no forma parte. El coeficiente de silueta solo se define si el número de etiquetas es \(2 \ge N_{labels} \ge N_{samples} - 1\)
El mejor valor es 1 y el peor valor es -1. Los valores cercanos a 0 indican clústeres superpuestos. Los valores negativos generalmente indican que una muestra se ha asignado al conglomerado equivocado, ya que un conglomerado diferente es más similar.
from sklearn.cluster import KMeans
import sklearn.metrics as metrics
siluetas = []
for i in range(2, 20):
km = KMeans(n_clusters=i, init='k-means++', max_iter=300, tol=1e-04, random_state=1)
km.fit(X)
silueta = metrics.silhouette_score(X,km.labels_,metric="euclidean",sample_size=1000)
#print(i, km.inertia_, tss, km.inertia_/tss)
siluetas.append(silueta)
plt.figure(figsize=(7, 4), dpi=80)
plt.plot(range(2,20), siluetas, label="Silueta vs K")
plt.xlabel("Número de Clusters")
plt.ylabel("Silueta")
plt.legend(loc='upper right')
plt.show()
Conclusión del gráfico de la Silueta: Los mejores valores están primero en 8 y luego en 16 grupos.
Vamos a crear una variable con el número de grupos para poder cambiar dicho valor sin tocar el resto de código
Como se sabe que hay 14 tipos de cancer, este debería ser el valor teórico que nos debería haber salido si el clustering ajustara perfectamente la realidad
print("Total tipos de cancer", np.size(np.unique(labels_ok)))
totalClusters = 14
Total tipos de cancer 14
Cluster Jerarquico¶
Se calcula con el número de clusters indicado anteriormente
from sklearn.cluster import AgglomerativeClustering
ac = AgglomerativeClustering(n_clusters=totalClusters, affinity='euclidean', linkage='complete', compute_distances=True)
#ac = AgglomerativeClustering(n_clusters=14, affinity='euclidean', linkage='complete', distance_threshold=None)
y_ag = ac.fit_predict(X)
y_ag, ac.distances_[-1], 0.7*ac.distances_[-1], ac.n_clusters_
(array([ 2, 2, 2, 9, 1, 1, 1, 1, 0, 2, 2, 2, 2, 2, 6, 6, 6,
4, 4, 8, 6, 6, 0, 0, 0, 0, 0, 6, 0, 0, 2, 0, 0, 3,
3, 3, 3, 3, 13, 13, 12, 0, 10, 0, 10, 10, 10, 10, 5, 5, 5,
5, 0, 0, 11, 7, 7, 7, 9, 7, 7, 7, 7, 7]),
163.48974041519423,
114.44281829063596,
14)
Se echa un primer vistazo al aspecto que tiene la agrupación conseguida
etiquetas=np.unique(y_ag)
numEtiquetas = np.size(etiquetas)
maxSizeCluster=0
for et in etiquetas:
print(et, np.size(y_ag[y_ag==et]), y_ag[y_ag==et])
0 14 [0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1 4 [1 1 1 1]
2 9 [2 2 2 2 2 2 2 2 2]
3 5 [3 3 3 3 3]
4 2 [4 4]
5 4 [5 5 5 5]
6 6 [6 6 6 6 6 6]
7 8 [7 7 7 7 7 7 7 7]
8 1 [8]
9 2 [9 9]
10 5 [10 10 10 10 10]
11 1 [11]
12 1 [12]
13 2 [13 13]
Se lista con mayor detalle los grupos obtenidos y los tipos de cancer asignados a cada uno de ellos con su nombre
pd.set_option('display.width', 1000)
tiposCancerGrupo = ['' for i in range(numEtiquetas)]
for iCancer in range(np.size(y_ag)):
iCluster = y_ag[iCancer]
tiposCancerGrupo[iCluster] += str(labels_ok[iCancer]) + ";"
for i in range(np.size(tiposCancerGrupo)):
print(i, tiposCancerGrupo[i])
0 ['NSCLC'];['MELANOMA'];['PROSTATE'];['OVARIAN'];['OVARIAN'];['OVARIAN'];['OVARIAN'];['PROSTATE'];['NSCLC'];['NSCLC'];['COLON'];['COLON'];['NSCLC'];['NSCLC'];
1 ['BREAST'];['CNS'];['CNS'];['BREAST'];
2 ['CNS'];['CNS'];['CNS'];['NSCLC'];['RENAL'];['RENAL'];['RENAL'];['RENAL'];['NSCLC'];
3 ['LEUKEMIA'];['K562B-repro'];['K562A-repro'];['LEUKEMIA'];['LEUKEMIA'];
4 ['BREAST'];['NSCLC'];
5 ['MCF7A-repro'];['BREAST'];['MCF7D-repro'];['BREAST'];
6 ['RENAL'];['RENAL'];['RENAL'];['UNKNOWN'];['OVARIAN'];['OVARIAN'];
7 ['MELANOMA'];['BREAST'];['BREAST'];['MELANOMA'];['MELANOMA'];['MELANOMA'];['MELANOMA'];['MELANOMA'];
8 ['RENAL'];
9 ['RENAL'];['MELANOMA'];
10 ['COLON'];['COLON'];['COLON'];['COLON'];['COLON'];
11 ['NSCLC'];
12 ['LEUKEMIA'];
13 ['LEUKEMIA'];['LEUKEMIA'];
Función a la medida para graficar el Dendograma
Disponible en el “03.0ClusteringUtilidades.ipynb”
run 03.0_ClusteringUtilidades.ipynb
load done!
titulo="Análisis de Microarrays"
subtitulo="Tipos de Cáncer"
figSize=(15,10)
plot_dendrogram(ac, titulo, subtitulo, figSize, leaf_font_size=10)
Se valida el cluster jerárquico¶
ac = AgglomerativeClustering(n_clusters=14, affinity='euclidean', linkage='complete')
y_ag = ac.fit_predict(X)
from sklearn.metrics.cluster import rand_score
from sklearn.metrics.cluster import adjusted_rand_score
y_ok = [lbl[0] for lbl in labels_ok]
## Rand - Puntuación de similitud entre 0.0 y 1.0, (1.0 significa coincidencia perfecta)
R=rand_score(labels_true=y_ok, labels_pred=y_ag)
ARI=adjusted_rand_score(labels_true=y_ok, labels_pred=y_ag)
R, ARI, y_ok
(0.8720238095238095,
0.2645020065669464,
['CNS',
'CNS',
'CNS',
'RENAL',
'BREAST',
'CNS',
'CNS',
'BREAST',
'NSCLC',
'NSCLC',
'RENAL',
'RENAL',
'RENAL',
'RENAL',
'RENAL',
'RENAL',
'RENAL',
'BREAST',
'NSCLC',
'RENAL',
'UNKNOWN',
'OVARIAN',
'MELANOMA',
'PROSTATE',
'OVARIAN',
'OVARIAN',
'OVARIAN',
'OVARIAN',
'OVARIAN',
'PROSTATE',
'NSCLC',
'NSCLC',
'NSCLC',
'LEUKEMIA',
'K562B-repro',
'K562A-repro',
'LEUKEMIA',
'LEUKEMIA',
'LEUKEMIA',
'LEUKEMIA',
'LEUKEMIA',
'COLON',
'COLON',
'COLON',
'COLON',
'COLON',
'COLON',
'COLON',
'MCF7A-repro',
'BREAST',
'MCF7D-repro',
'BREAST',
'NSCLC',
'NSCLC',
'NSCLC',
'MELANOMA',
'BREAST',
'BREAST',
'MELANOMA',
'MELANOMA',
'MELANOMA',
'MELANOMA',
'MELANOMA',
'MELANOMA'])
Acceso a las rutinas de cálculo de la Variación de Información de Meilă
from sklearn.metrics.cluster import mutual_info_score
from sklearn.metrics.cluster import adjusted_mutual_info_score
VI = variationInformation(y_ok, y_ag)
## El VI devuelve un valor de 0 cuando las dos particiones son idénticas (es decir, coinciden perfectamente)
## El VI ajustado es un valor independiente del número de registros N
adj_VI = adjustedVariationInformation(y_ok, y_ag)
VI, adj_VI
(0.244285988549783, 0.05873836403990501)
Representación desde PCA¶
from sklearn.decomposition import PCA
pca = PCA(n_components=None)
X_pca = pca.fit_transform(X)
Varianzas=pca.explained_variance_ratio_
var_exp = pca.explained_variance_ratio_
tot = len(var_exp)
cum_var_exp = np.cumsum(var_exp)
import matplotlib.pyplot as plt
plt.bar(range(1,tot+1), var_exp, alpha=0.5, align='center',label='Varianza explicada inidividual')
plt.step(range(1,tot+1), cum_var_exp, where='mid',label='Varianza explicada acumulada')
plt.ylabel('Ratio de varianza explicada')
plt.xlabel('Indice de Característica Principal')
plt.legend(loc='best')
plt.show()
pca = PCA(n_components=3)
X_pca = pca.fit_transform(X)
lstEtiquetas = np.squeeze(labels_ok, axis=1)
etiquetas=np.unique(lstEtiquetas)
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'brown', 'navy', 'coral', 'lime', 'orange', 'violet', 'wheat']
plt.figure(figsize=(12, 6))
for l, c in zip(etiquetas, colors):
plt.scatter(X_pca[lstEtiquetas==l, 0], X_pca[lstEtiquetas==l, 1], c=c, label=l, marker='.')
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.legend(loc='upper right')
plt.show()
lstEtiquetas = np.squeeze(labels_ok, axis=1)
etiquetas=np.unique(lstEtiquetas)
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'brown', 'navy', 'coral', 'lime', 'orange', 'violet', 'wheat']
fig=plt.figure(figsize=(12, 6))
ax = fig.add_subplot(projection='3d')
for l, color in zip(etiquetas, colors):
#ax.scatter(X_pca[lstEtiquetas==l, 0], X_pca[lstEtiquetas==l, 1], X_pca[lstEtiquetas==l, 2], c=c, label=l, marker='.')
ax.scatter(X_pca[lstEtiquetas==l, 0], X_pca[lstEtiquetas==l, 1], X_pca[lstEtiquetas==l, 2], c=color, label=l, marker='.')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
ax.set_zlabel('PC3')
#plt.legend(loc='upper left')
plt.show()
Método del Codo sobre las 3 componentes principales
from sklearn.mixture import GaussianMixture
bic = []
for i in range(1, 11):
gmm = GaussianMixture(n_components=i, init_params='kmeans')
gmm.fit(X_pca)
bic.append(gmm.bic(X_pca))
plt.figure(figsize=(7, 4), dpi=80)
plt.plot(range(1,11), bic, label="BIC vs k")
plt.xlabel("Número de Clusters")
plt.ylabel("BIC")
plt.legend(loc='upper right')
plt.show()