Analyse de données sous Python - correction

In [1]:
import numpy
import pandas
import matplotlib.pyplot as plt

# permet d'afficher les graphiques dans un notebook
%matplotlib inline

Températures mondiales (anomalies)

Nous allons travailler ici sur les données de température mondiale HadCRUT4, fournies par Climate Research Unit. Vous trouverez plus d’informations sur ces données sur ce lien. Nous avons ici plus exactement l'historique des anomalies moyennes mensuelles et annuelles depuis 1850, au niveau mondial, par rapport à la période de référence 1961-1990.

Le code ci-dessous télécharge directement les dernières données disponibles et les met dans un DataFrame dont vous avez un aperçu en dessous.

In [2]:
had = pandas.read_csv("https://crudata.uea.ac.uk/cru/data/temperature/HadCRUT4-gl.dat", header=None)
donnees = pandas.DataFrame(
    [list(map(lambda v: float(v), filter(lambda v: v!= "", h.split(" ")))) for h in had[0][::2]],
    columns = ["Year", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Annual"]
)
donnees.tail()
Out[2]:
Year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Annual
165 2015.0 0.705 0.689 0.708 0.673 0.711 0.743 0.697 0.740 0.796 0.845 0.840 1.024 0.763
166 2016.0 0.934 1.111 1.106 0.937 0.707 0.744 0.744 0.790 0.729 0.598 0.553 0.620 0.797
167 2017.0 0.739 0.845 0.873 0.737 0.659 0.641 0.651 0.714 0.557 0.571 0.554 0.600 0.677
168 2018.0 0.554 0.528 0.615 0.627 0.587 0.573 0.594 0.586 0.598 0.678 0.590 0.638 0.597
169 2019.0 0.738 0.662 0.874 0.780 0.610 0.708 0.706 0.719 0.713 0.752 0.693 0.879 0.736

Vous devez donc réaliser les étapes suivantes, au propre dans un notebook :

  • Décrire rapidement les données
    • Calculer les statistiques de base sur chaque mois et sur l'année
    • Réprésenter l'évolution des anomalies annuelles sur un graphique
  • Réaliser une ACP sur les données mensuelles
  • Produire les graphiques nécessaires à l’interprétation
  • Identifier des années particulières
  • Rechercher une partition intéressante des années
  • Représenter cette partition sur le résultat de l'ACP
  • Décrire les classes ainsi obtenues

Que peut-on dire globalement ?

Description des données

Statistique de base sur chaque mois, et sur l'année

  • Aucune donnée manquante
  • Anomalies en moyenne inférieur à 0
    • plus faible en Mars, Novembre et Décembre
    • moins faible en Août
  • Des gros écarts en positif sur Février, Mars et Décembre (maximum supérieur à 1)
In [3]:
donnees.drop(columns = ("Year")).describe().round(2)
Out[3]:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Annual
count 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00 170.00
mean -0.09 -0.09 -0.13 -0.09 -0.10 -0.07 -0.05 -0.04 -0.07 -0.08 -0.11 -0.12 -0.09
std 0.36 0.37 0.37 0.34 0.31 0.30 0.29 0.30 0.30 0.32 0.34 0.35 0.31
min -0.97 -0.78 -0.83 -0.72 -0.77 -0.58 -0.57 -0.70 -0.56 -0.71 -0.77 -0.89 -0.54
25% -0.32 -0.35 -0.40 -0.34 -0.32 -0.28 -0.25 -0.25 -0.28 -0.32 -0.37 -0.36 -0.30
50% -0.11 -0.15 -0.21 -0.17 -0.17 -0.14 -0.10 -0.09 -0.11 -0.11 -0.13 -0.17 -0.16
75% 0.12 0.14 0.07 0.09 0.07 0.07 0.05 0.08 0.07 0.07 0.04 0.13 0.05
max 0.93 1.11 1.11 0.94 0.71 0.74 0.74 0.79 0.80 0.84 0.84 1.02 0.80
In [4]:
donnees.drop(columns = ("Year")).boxplot()
Out[4]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fe46c715b38>

Evolution des anomalies

On peut aussi réprésenter l'évolution des anomalies au cours du temps, pour l'ensemble des mois. Tous semblent avoir le même comportement.

In [5]:
donnees.plot(x = "Year", figsize = (12,4))
plt.show()

En se concentrant sur la moyenne annuelle des anomalies par rapport à la période de référence (1961-1990 - représentée ci-dessous), on remarque une augmentation des anomalies à partir des années 80.

In [6]:
fig, ax = plt.subplots()
donnees.plot(x = "Year", y = "Annual", figsize = (12,4), ax = ax)
rect = plt.Rectangle((1961, -1), 30, 2,
                     facecolor = "black", alpha = 0.1)
ax.add_patch(rect)
ax.axhline(y = 0, linewidth = .5, color = "dimgray", linestyle = "--")
ax.text(1962, -0.55, "Période de référence")
plt.xlabel("")
plt.show()

ACP sur les données mensuelles

On peut appliquer l'ACP sur les données standardisées (avec la fonction scale()).

In [7]:
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale

donnees_mens = donnees.drop(columns = ["Year", "Annual"])
pca = PCA()
pca.fit(scale(donnees_mens))
Out[7]:
PCA(copy=True, iterated_power='auto', n_components=None, random_state=None,
    svd_solver='auto', tol=0.0, whiten=False)

Le premier axe explique à lui seul 88 % de l'information.

In [8]:
eig = pandas.DataFrame(
    {
        "Dimension" : ["Dim" + str(x + 1) for x in range(12)], 
        "Variance expliquée" : pca.explained_variance_,
        "% variance expliquée" : numpy.round(pca.explained_variance_ratio_ * 100),
        "% cum. var. expliquée" : numpy.round(numpy.cumsum(pca.explained_variance_ratio_) * 100)
    }
)
eig
Out[8]:
Dimension Variance expliquée % variance expliquée % cum. var. expliquée
0 Dim1 10.679421 88.0 88.0
1 Dim2 0.481996 4.0 92.0
2 Dim3 0.208017 2.0 94.0
3 Dim4 0.154925 1.0 95.0
4 Dim5 0.123418 1.0 96.0
5 Dim6 0.115370 1.0 97.0
6 Dim7 0.076620 1.0 98.0
7 Dim8 0.072008 1.0 99.0
8 Dim9 0.044102 0.0 99.0
9 Dim10 0.041565 0.0 99.0
10 Dim11 0.037675 0.0 100.0
11 Dim12 0.035891 0.0 100.0
In [9]:
eig.plot.bar(x = "Dimension", y = "% variance expliquée") # permet un diagramme en barres
plt.show()
In [10]:
donnees_pca = pca.transform(donnees_mens)
donnees_pca_df = pandas.DataFrame({
    "Dim1" : donnees_pca[:,0], 
    "Dim2" : donnees_pca[:,1],
    "Year" : donnees["Year"]
})
donnees_pca_df.head()
Out[10]:
Dim1 Dim2 Year
0 -1.297973 -0.410539 1850.0
1 -0.767006 -0.432866 1851.0
2 -0.800260 -0.501338 1852.0
3 -0.944255 0.036396 1853.0
4 -0.873591 -0.134763 1854.0

En représentant les années sur le plan factoriel, et en faisant apparaître les points extrêmes, on remarque que les dernières années (2015, 2016, 2017 et 2019) se détache des autres. Ce sont les 4 années les plus chaudes depuis 1850.

In [11]:
# utilisation de subplots nécessaire car annotation du graphique
fig, ax = plt.subplots(figsize = (16,4))
donnees_pca_df.plot.scatter("Dim1", "Dim2", ax = ax) # l'option ax permet de placer les points et le texte sur le même graphique

# boucle sur chaque pays
for k in donnees_pca_df.iterrows():
    # annotation uniquement si valeur absolue sur une de 2 dimensions importantes (valeurs choisies empiriquement)
    if (abs(k[1]['Dim1']) > 8) | (abs(k[1]['Dim2']) > 1.5):
        ax.annotate(int(k[1]["Year"]), (k[1]['Dim1'], k[1]['Dim2']), fontsize = 9)
plt.xlabel("Dimension 1 (88%)") 
plt.ylabel("Dimension 2 ( 4%)")
plt.suptitle("Premier plan factoriel (92%)")
plt.show()

En représentant les mois, on remarque qu'ils sont tous situés à droite du plan factoriel. Cela confirme bien que les dernières années sont celles avec des valeurs les plus élevées.

In [12]:
n = donnees_mens.shape[0] # nb individus
p = donnees_mens.shape[1] # nb variables
eigval = (n-1) / n * pca.explained_variance_ # valeurs propres
sqrt_eigval = numpy.sqrt(eigval) # racine carrée des valeurs propres
corvar = numpy.zeros((p,p)) # matrice vide pour avoir les coordonnées
for k in range(p):
    corvar[:,k] = pca.components_[k,:] * sqrt_eigval[k]
# on modifie pour avoir un dataframe
coordvar = pandas.DataFrame({'id': donnees_mens.columns, 'COR_1': corvar[:,0], 'COR_2': corvar[:,1]})


fig, axes = plt.subplots(figsize = (4,4))
fig.suptitle("Cercle des corrélations")
axes.set_xlim(-1, 1)
axes.set_ylim(-1, 1)
# Ajout des axes
axes.axvline(x = 0, color = 'lightgray', linestyle = '--', linewidth = 1)
axes.axhline(y = 0, color = 'lightgray', linestyle = '--', linewidth = 1)
# Ajout des noms des variables
for j in range(p):
    axes.text(coordvar["COR_1"][j],coordvar["COR_2"][j], coordvar["id"][j])
# Ajout du cercle
plt.gca().add_artist(plt.Circle((0,0),1,color='blue',fill=False))

plt.show()

Classification des années

On peut chercher maintenant une partition des années sur la base de ces anomalies.

In [13]:
from sklearn.cluster import AgglomerativeClustering

hac = AgglomerativeClustering(distance_threshold=0, n_clusters=None)
hac.fit(scale(donnees_mens))
Out[13]:
AgglomerativeClustering(affinity='euclidean', compute_full_tree='auto',
                        connectivity=None, distance_threshold=0, linkage='ward',
                        memory=None, n_clusters=None)
In [14]:
from scipy.cluster.hierarchy import dendrogram

def plot_dendrogram(model, **kwargs):
    # Create linkage matrix and then plot the dendrogram

    # create the counts of samples under each node
    counts = numpy.zeros(model.children_.shape[0])
    n_samples = len(model.labels_)
    for i, merge in enumerate(model.children_):
        current_count = 0
        for child_idx in merge:
            if child_idx < n_samples:
                current_count += 1  # leaf node
            else:
                current_count += counts[child_idx - n_samples]
        counts[i] = current_count

    linkage_matrix = numpy.column_stack([model.children_, model.distances_,
                                      counts]).astype(float)

    # Plot the corresponding dendrogram
    dendrogram(linkage_matrix, **kwargs)
plt.title("CAH (Ward) sur les pays")
# plot the top three levels of the dendrogram
plot_dendrogram(hac)
plt.axhline(y = 37, linewidth = .5, color = "dimgray", linestyle = "--")
plt.axhline(y = 21, linewidth = .5, color = "dimgray", linestyle = "--")
plt.show()

Sur le graphique ci-dessous, on remarque qu'on peut choisir soit 2 classes, soit 3 classes. Ici, nous allons choisir 3 classes.

In [15]:
from sklearn.cluster import KMeans

kmeans3 = KMeans(n_clusters = 3)
kmeans3.fit(scale(donnees_mens))
Out[15]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
       n_clusters=3, n_init=10, n_jobs=None, precompute_distances='auto',
       random_state=None, tol=0.0001, verbose=0)

Ces trois classes se répartissent comme suit :

  • Classe 0 : années avec des valeurs globalement proche de 0
  • Classe 1 : années chaudes (valeurs élévés)
  • Classe 2 : années froides (valeurs faibles)
In [16]:
donnees_k3 = donnees_mens.assign(classe = kmeans3.labels_)
donnees_k3.groupby("classe").mean()
Out[16]:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
classe
0 -0.325306 -0.350859 -0.399800 -0.344565 -0.331894 -0.294106 -0.254988 -0.263647 -0.289106 -0.318224 -0.362659 -0.369188
1 0.483500 0.519115 0.535577 0.525269 0.459115 0.487577 0.492269 0.509846 0.480962 0.501077 0.486500 0.460846
2 0.007814 0.005746 -0.036153 -0.000814 -0.015407 0.006085 0.008746 0.026576 0.015153 0.018068 -0.014661 -0.011966
In [17]:
col = { 0: "gray", 1: "darkred", 2: "steelblue"}
donnees_pca_k3 = donnees_pca_df.assign(classe = kmeans3.labels_)
donnees_pca_k3.plot.scatter(x = "Dim1", y = "Dim2", c = [col[k] for k in kmeans3.labels_])
plt.show()

Conclusion

Il semble ainsi clair qu'il y a un échauffement global de la planète, en particulier sur les dernières années comme on peut le voir sur le graphique ci-dessous.

In [18]:
donnees_trans = donnees.drop(columns =["Year", "Annual"]).transpose() 

fig, ax = plt.subplots(figsize = (12, 8))
im = ax.imshow(donnees_trans, cmap = "coolwarm", aspect = 4)

locs, labels = plt.xticks()
ax.set_xticklabels([int(l) for l in locs + 1850])

month = list(donnees.columns[1:13])
ax.set_yticks(numpy.arange(len(month)))
ax.set_yticklabels(month)

plt.setp(ax.get_xticklabels(), rotation = 45, ha = "right", rotation_mode = "anchor")

fig.colorbar(im, aspect = 1)

fig.tight_layout()
plt.show()