Analyses en composantes principales et une de ses alternatives, le tSNE

en langage python

L'essentiel de cette page

L'analyse en composantes principales (ACP ou PCA en anglais) permet de réduire le nombre de dimensions d'un jeu de données décrit par un grand nombre de variables. Cela permet une visualisation simplifiée et une accélération des calculs.

Le module scikit-learn permet de réaliser une telle réduction mais présente certaines faiblesses (écart-type utilisé non-approprié)

0- Dans cette page, nous partirons pour l'exemple du jeu de données Iris

import seaborn as sns # nécessite le module seaborn

import matplotlib.pyplot as plt

iris = sns.load_dataset('iris') # Charger le jeu de données iris

# Les 4 colonnes de mesures qui décrivent différents iris

X = iris.iloc[:,0:4]

colors = iris.species.astype('category')

# Les couleurs déduites de chaque espèce d'iris

y = colors.cat.codes

print(X) ; print(y)

from pandas.plotting import scatter_matrix

scatter_matrix(X,c=y) ; plt.show()

1- Réaliser une ACP avec scikit-learn (sklearn)

1.1- Centrer-réduire les valeurs (optionnel)

  • 1) Méthode de base sklearn

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

scaler.fit(X)

X=scaler.transform(X)

Cette méthode prête à confusion : il semblerait que l'écart-type utilisé est celui d'une population et non d'un échantillon.

  • 2) Autre méthode sklearn

from sklearn.preprocessing import scale

X = scale(X)

  • 3) Autre méthode, pour les dataframe (pandas) - on obtient ainsi un X au format dataframe.

temp = X.sub(X.mean()) # Soustraire la moyenne de chaque colonne à chaque valeur : centrer les valeurs

X= temp.div(temp.std()) # Divisé les valeurs de la dataframe par l'écart-type de chaque colonne

1.2- Réaliser l'ACP

from sklearn.decomposition import PCA

mypca = PCA(n_components=3) # On paramètre ici pour ne garder que 3 composantes

# Modèle d'ACP

mypca.fit(X)

# Pourcentage de la variance expliquée par chacune des composantes sélectionnées.

print(mypca.singular_values_) # Valeurs de variance

print(mypca.explained_variance_ratio_) # Pourcentages

# Axes principaux dans l'espace des caractéristiques, représentant les directions de la variance maximale dans les données. Les composantes sont triées par variance expliquée.

print(mypca.components_) #

# Résultats de l'ACP

data_sortie= mypca.fit_transform(X)

# Bruit estimé lié à la covariance

print(mypca.noise_variance_)

1.3- Contrôler l'ACP par un diagramme de pareto

Un diagramme de pareto permet de s'assurer que les composantes retenues retiendront bien la majorité de la variabilité.

Un code rustique qui marche :

import numpy as np

y = list(mypca.explained_variance_ratio_)

x = range(len(y))

ycum = np.cumsum(y)

plt.bar(x,y)

plt.plot(x,ycum,"-r")

plt.show()

Remarque importante : pour qu'un pareto soit pertinent, il ne faut pas fixer le nombre de composante retenues sinon on ignore volontairement une partie de la variance perdu. Ici, si on avait eu plus de 4 variables, le choix de 3 composantes nous aurait conduit à négliger la variance des composantes au-delà de 3.


Un code plus élaboré et plus simple à utiliser :

pareto(mypca.explained_variance_ratio_)

Pour cela : copier-coller le code de la fonction ci-dessous

Ici je ne peux avoir que 3 composantes (car j'ai 4 variables à la base). Mes deux premières composantes absorbent bien la majorité de la variabilité.

Code de la fonction pareto() à copier-coller (cliquer sur le titre)

def pareto(data) :

from matplotlib.ticker import PercentFormatter

import numpy as np

y = list(data)

x = range(len(data))

ycum = np.cumsum(y)/sum(y)*100

fig, ax = plt.subplots()

ax.bar(x,y,color="yellow")

ax2 = ax.twinx()

ax2.plot(x,ycum, color="C1", marker="D", ms=7)

ax2.axhline(y=80,color="r")

ax2.yaxis.set_major_formatter(PercentFormatter())

ax.tick_params(axis="y", colors="C0")

ax2.tick_params(axis="y", colors="C1")

plt.ylim(0,110)

plt.show()

2- Visualiser les résultats de l'ACP

biplot - graphique qui représente la projection des individus sur deux composantes sélectionnées et la corrélation des variables avec ces composantes.

biplot(score=data_sortie[:,0:2],

coeff=np.transpose(mypca.components_[0:2, :]),cat=y,

cmap="viridis")

plt.show()

Pour avoir accès à la fonction biplot(), copier-coller le code ci-dessous à la fin de cette partie.

On peut obtenir directement un affichage avec le bon nom des variables si X est au format pandas :


biplot(mypca,x=X,cat=y,components=[0,1])

plt.show()

components permet ici d'indiquer quelles dimensions/composantes l'on souhaite afficher...

On peut aussi paramétrer la palette de couleurs et indiquer des labels pour les variables ou les individus :

  • cmap

  • coeff_labels

  • score_labels

Lorsqu'on travaille avec jeu de données trop grandes, on peut ne tracer que les nappes convexes des catégories afin de voir si elles ne se superposent pas

Paramètre : bigdata, au-delà de 100 valeurs, on passe en nappes convexes

Remarque : ici X est au format dataframe de pandas (et non numpy.array) : il a été centré-réduit selon la méthode 3.

biplot(score=data_sortie[:,0:2],

coeff=np.transpose(mypca.components_[0:2, :]),cat=y, bigdata=100,coeff_labels = list(X.columns),density=False)

plt.show()

On voit ainsi ici que 2 catégories se superposent.

Lorsque le paramètre BigData fait basculer en un affichage d'un grand jeu de valeurs, on peut aussi demander une graphique de densité


biplot(score=data_sortie[:,0:2],cat=y, bigdata=100,density=True)

plt.show()

On voit ainsi les 3 catégories à travers les 3 nappes convexes et l'on perçoit bien où se trouvent la majorité des points s'il y en avait trop pour tous les voir.

code source pour les cartes de densité

Code de la fonction biplot() à copier-coller (cliquer sur le titre)

#######################################

# biplot

# version 12/11/2021

#######################################

import matplotlib.pyplot as plt

from scipy.spatial import ConvexHull

import numpy as np

import pandas as pd

import matplotlib as mpl

import matplotlib.cm as cm

import seaborn as sns

from sklearn.decomposition import PCA

def biplot(pca=[],x=None,y=None,components=[0,1],score=None,coeff=None,coeff_labels=None,score_labels=None,circle='T',bigdata=1000,cat=None,cmap="viridis",density=True):

if isinstance(pca,PCA)==True :

coeff = np.transpose(pca.components_[components, :])

score= pca.fit_transform(x)[:,components]

if isinstance(x,pd.DataFrame)==True :

coeff_labels = list(x.columns)

if score is not None : x = score

if x.shape[1]>1 :

xs = x[:,0]

ys = x[:,1]

else :

xs = x

ys = y

if (len(xs) != len(ys)) : print("Warning ! x et y n'ont pas la même taille !")

scalex = 1.0/(xs.max() - xs.min())

scaley = 1.0/(ys.max() - ys.min())

#x_c = xs * scalex

#y_c = ys * scaley

temp = (xs - xs.min())

x_c = temp / temp.max() * 2 - 1

temp = (ys - ys.min())

y_c = temp / temp.max() * 2 - 1

data = pd.DataFrame({"x_c":x_c,"y_c":y_c})

print("Attention : pour des facilités d'affichage, les données sont centrées-réduites")

if cat is None : cat = [0]*len(xs)

elif len(pd.Series(cat)) == 1 : cat = list(pd.Series(cat))*len(xs)

elif len(pd.Series(cat)) != len(xs) : print("Warning ! Nombre anormal de catégories !")

cat = pd.Series(cat).astype("category")

fig = plt.figure(figsize=(6,6),facecolor='w')

ax = fig.add_subplot(111)

# Affichage des points

if (len(xs) < bigdata) :

ax.scatter(x_c,y_c, c = cat.cat.codes,cmap=cmap)

if density==True : print("Warning ! Le mode density actif n'apparait que si BigData est paramétré.")

# Affichage des nappes convexes (BigData)

else :

#color

norm = mpl.colors.Normalize(vmin=0, vmax=(len(np.unique(cat.cat.codes)))) #-(len(np.unique(c)))

cmap = cmap

m = cm.ScalarMappable(norm=norm, cmap=cmap)

if density==True :

sns.set_style("white")

sns.kdeplot(x="x_c",y="y_c",data=data)

if len(np.unique(cat)) <= 1 :

sns.kdeplot(x="x_c",y="y_c",data=data, cmap="Blues", shade=True, thresh= 0)

else :

for i in np.unique(cat) :

color_temp = m.to_rgba(i)

sns.kdeplot(x="x_c",y="y_c",data=data[cat==i], color=color_temp,

shade=True, thresh=0.25, alpha=0.25)

for cat_temp in cat.cat.codes.unique() :

x_c_temp = [x_c[i] for i in range(len(x_c)) if (cat.cat.codes[i] == cat_temp)]

y_c_temp = [y_c[i] for i in range(len(y_c)) if (cat.cat.codes[i] == cat_temp)]

points = [ [ None ] * len(x_c_temp) ] * 2

points = np.array(points)

points = points.reshape(len(x_c_temp),2)

points[:,0] = x_c_temp

points[:,1] = y_c_temp

hull = ConvexHull(points)

temp = 0

for simplex in hull.simplices:

color_temp = m.to_rgba(cat_temp)

plt.plot(points[simplex, 0], points[simplex, 1],color=color_temp)#, linestyle='dashed')#linewidth=2,color=cat)

if (temp == 0) :

plt.xlim(-1,1)

plt.ylim(-1,1)

temp = temp+1

if coeff is not None :

if (circle == 'T') :

x_circle = np.linspace(-1, 1, 100)

y_circle = np.linspace(-1, 1, 100)

X, Y = np.meshgrid(x_circle,y_circle)

F = X**2 + Y**2 - 1.0

#fig, ax = plt.subplots()

plt.contour(X,Y,F,[0])

n = coeff.shape[0]

for i in range(n):

plt.arrow(0, 0, coeff[i,0], coeff[i,1],color = 'r',alpha = 0.5,

head_width=0.05, head_length=0.05)

if coeff_labels is None:

plt.text(coeff[i,0]* 1.15, coeff[i,1] * 1.15, "Var"+str(i+1), color = 'g', ha = 'center', va = 'center')

else:

plt.text(coeff[i,0]* 1.15, coeff[i,1] * 1.15, coeff_labels[i], color = 'g', ha = 'center', va = 'center')

if score_labels is not None :

for i in range(len(score_labels)) :

temp_x = xs[i] * scalex

temp_y = ys[i] * scaley

plt.text(temp_x,temp_y,list(score_labels)[i])

plt.xlim(-1.2,1.2)

plt.ylim(-1.2,1.2)

plt.xlabel("PC{}".format(1))

plt.ylabel("PC{}".format(2))

plt.grid(linestyle='--')

plt.show()



3- Une alternative l'ACP, le plongement par tSNE

Le principe du tSNE est de réduire les dimensions en considérant que tous les points sont reliés par des petits ressorts. De façon stochastique, et par itération, le tSNE va essayer de proposer un modèle de réduction des dimensions correspondant à un modèle d'énergie minimisée.

Ce site n'explique pas le fonctionnement du tSNE mais comment en faire un en python avec sci-kit learn (module sklearn).

  • 1) Mettre en place le tSNE

from sklearn.manifold import TSNE

# Instanciation

tsne = TSNE(n_components=2) # On peut choisir de réduire à 2 dimensions à plus

# Itérations

X_embedded = tsne.fit_transform(X)

# Résultat

print(X_embedded.shape)

  • 2) Visualiser les résultats

# 2 composantes

from matplotlib import pyplot as plt

plt.scatter(X_embedded[:,0],X_embedded[:,1],c=y, cmap='Set1')

plt.show()

# Si on refait le tSNE avec 3 composantes au lieu de 2 : on voit que le passage de 2 à 3 composantes change la sortie du tSNE

from matplotlib import pyplot as plt

plt.subplot(121)

plt.scatter(X_embedded[:,0],X_embedded[:,1],c=y, cmap='Set1') # c2 = f(c1)

plt.subplot(122)

plt.scatter(X_embedded[:,0],X_embedded[:,2],c=y, cmap='Set1') # c3 = f(c2)

plt.show()

On peut aussi utiliser la fonction biplot() ci-dessus (partie ACP) pour visualiser rapidement les catégories représentées par des nappes convexes lorsqu'on dispose de trop de données :

biplot(x=X_embedded[:,0],y=X_embedded[:,1],c=y,bigdata=50)

plt.show()

Plutôt que les points sont représentés ici les contours des nuages des points

4- Adapter l'affichage des résultats de l'ACP ou du tSNE

4.1- Afficher les labels des points dans un graphique simple (non biplot)

Pour afficher les labels des points, le plus simple est de faire une boucle avec la fonction plt.text de matplotlib.pyplot :

from matplotlib import pyplot as plt

plt.scatter(data_sortie[:,0],data_sortie[:,1],c=col, cmap='Set1')

plt.show()

Sera à remplacer par :

Remarque : cette figure ne représente pas un résultat d'ACP mais un résultat d'une méthode alternative, le tSNE.

# listes des labels des individus

labels = list(range(1,80,1))

plt.scatter(data_sortie[:,0],data_sortie[:,1],c=col, cmap='Set1')

plt.grid(color='white', linestyle='solid')

plt.title("Scatter Plot", size=20)

for i in range(len(labels)) :

plt.text(data_sortie[i,0]data_sortie[i,1],labels[i])

plt.show()

  • On peut aussi adapter le code de la façon suivante :

labels = list(range(1,80,1))

fig, ax = plt.subplots()

plt.scatter(data_sortie[:,0],data_sortie[:,1],c=col, cmap='Set1')

ax.grid(color='white', linestyle='solid')

ax.set_title("Scatter Plot (with tooltips!)", size=20)


for label, x, y in zip(labels, data_sortie[:,0], data_sortie[:,1]):

plt.annotate(

label,

xy=(x, y), xytext=(-20, 20),

textcoords='offset points', ha='right', va='bottom',

bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),

arrowprops=dict(arrowstyle = '->', connectionstyle='arc3,rad=0'))


plt.show()

Remarque :

  • Il existe une alternative où le graphique sera construit avec le module mpld3 et affiché dans l'explorateur internet : lien externe. Il suffira alors de passer la souris au-dessus de chaque point pour avoir son nom.

4.2- Afficher 3 composantes avec un graphique 3D

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()

ax = fig.gca(projection='3d')

ax.scatter(data_sortie[:,0],data_sortie[:,1],

data_sortie[:,2],

zdir='z',s=40,depthshade=True,c=y)

plt.show()

Remarque

Une fois la réduction de dimension faite, il peut être important de réaliser des classification : on peut alors faire appelle à différentes méthodes telles le k-means ou la classification hiérarchique ascendante (CAH).

  • Réaliser des réductions dimensionnelles sous R telles l'ACP, le cmdscale, la projection de Fischer...