Python: Analyser des données avec Pandas et Matplotlib

Pandas et Matplotlib sont deux modules Python qui permettent d'analyser des données et de les représenter sous forme de graphiques.

Ce sont deux modules très complets et par conséquent très complexes.

Je vais présenter ici une analyse complète regroupant différentes fonctions utiles permettant de retourner le résultat souhaité.

Pour l'exemple, j'ai décidé d'analyser la consommation de carburant de ma Golf VII Hybride.

Mon analyse va porter sur les 16 dernières fois où je me suis rendu à la station service.

Voici les données brutes que je vais utiliser.
A l'origine mes données sont stockées dans un fichier XML.

<ope date="737309" amount="-29.449999999999999" category="185" wording="v=20.04 d=36773"/>

Ci-dessous une représentation sous forme de tableau.
Pour chaque date (ordinal), il y a le montant et une colonne contenant le volume d'essence acheté ainsi que le kilométrage du compteur au moment du plein.

 

  date amount wording
0 737309 -29.449999999999999 v=20.04 d=36773
1 737317 -43.170000000000002 v=30.02 d=37269
2 737333 -43.100000000000001 v=30.12 d=38061
3 737348 -44.240000000000002 v=31.31 d=38850
4 737358 -50.710000000000001 v=36.48 d=39632
5 737374 -50.149999999999999 v=34.16 d=40408
6 737387 -56.600000000000001 v=37.86 d=41032
7 737398 -37.369999999999997 v=25.49 d=41548
8 737403 -32.289999999999999 v=21.54 d=41880
9 737414 -49.340000000000003 v=32.57 d=42469
10 737427 -50.920000000000002 v=33.52 d=43104
11 737441 -53.57 v=34.90 d=43815
12 737455 -30.199999999999999 v=20.70 d=44353
13 737469 -39.240000000000002 v=27.08 d=44737
14 737484 -49.450000000000003 v=33.30 d=45403
15 737500 -29.850000000000001 v=22.98 d=45940

Voici donc mon script Python, complet, entièrement détaillé à l'aide des commentaires et le résultat final à la fin de cet article.

Ce script a été développé avec Python 3.7.5 et peut être exécuté avec JupyterLab

Je commence par importer les différents modules dont j'ai besoin.
Pour l'installation des modules nécessaires, tout est expliqué ici.

from pathlib import Path
import pandas as pd
from matplotlib import pyplot as plt
from collections import defaultdict
from datetime import datetime as dt
from bs4 import BeautifulSoup as bs
import numpy as np
from IPython.core.display import HTML
from scipy import stats
# La ligne ci-dessous permet d'afficher les graphiques dans jupyterlab
%matplotlib inline

Différentes fonctions qui vont m'être utiles pour transformer les données

def toDICT(KEYS, OPES, DICT):
    for OPE in OPES:
        for KEY in KEYS:
            DICT[KEY].append(OPE.get(KEY))
    return DICT

def toDF(DICT):
    # Je créé le dataframe Pandas à partir des données du dictionnaire DICT
    DF = pd.DataFrame(data=DICT)
    # Je converti la date (ordinal) au format datetime à l'aide de la fonction "convDATE"
    DF['date'] = DF['date'].apply(convDATE)
    # Je converti le montant en "float" et je récupère la valeur absolue
    DF['amount'] = DF['amount'].apply(float).apply(abs)
    # A l'aide de la fonction "getVolDist" je récupère le volume d'essence dans le texte de la colonne "wording"
    # Pour rappel, le texte contenu dans le champ "wording": v=25.49 d=41548
    # Le volume d'essence est converti en "float" à l'aide du module numpy
    DF['vol'] = DF['wording'].apply(getVolDist, args=['v']).apply(np.float32)
    # Le volume d'essence est arrondi à deux chiffres après la virgule
    DF['vol'] = DF['vol'].apply(lambda x: round(x, 2))
    # A l'aide de la fonction "getVolDist" je récupère le kilométrage dans le texte de la colonne "wording"
    # Le kilométrage est converti en "int" à l'aide du module numpy
    DF['dist'] = DF['wording'].apply(getVolDist, args=['d']).apply(np.int32)
    # Je supprime la colonne "wording"
    DF.drop(columns=['wording'], inplace=True)
    # Je trie mon dataframe par date croissante
    DF.sort_values('date', inplace=True)
    # A l'aide de la fonction "diff", appliquée sur la colonne "dist", je calcul la différence de kilométrage entre date et date+1
    DF['kms'] = DF['dist'].diff(periods=-1)
    # Je conserve la valeur absolue pour la colonne "kms"
    DF['kms'] = DF['kms'].apply(abs)
    # Je calcul le prix au litre arrondi à trois chiffres après la virgule
    DF['px/l'] = round(DF['amount'] / DF['vol'], 3)
    # Je calcul le cumul des kilomètres à l'aide la fonction "cumsum" appliquée sur la colonne "kms"
    DF['cum kms'] = DF['kms'].cumsum()
    # Je calcul le cumul du volume à l'aide la fonction "cumsum" appliquée sur la colonne "vol"
    DF['cum vol'] = DF['vol'].cumsum()
    # Je calcul la consommation arrondi à deux chiffres après la virgule
    DF['conso'] = round(DF['cum vol'] * 100 / DF['cum kms'], 2)
    # Je remplace toutes les valeurs nulles par le chiffre 0
    DF.fillna(value=0, inplace=True)
    return DF

def convDATE(i):
    return dt.fromordinal(int(i))

def getVolDist(s, k):
    s = s.split(' ')
    d = {x.split('=')[0]: x.split('=')[1] for x in s}
    return d.get(k)

Je récupère les donées au format XML et j'y applique les différentes transformation dont j'ai besoin.

# A l'aide du module Path, j'initialise mon fichier XML
FILE = Path('operations.xml')
# A l'aide du module BeautifulSoup, je parse mon fichier XML
XML = bs(FILE.read_text(), features='xml')
# Je recherche ensuite toutes les balises "ope" ayant un attribut "category" égal à 185
# Ceci est un exemple de mon fichier XML
# <ope date="737309" amount="-29.449999999999999" category="185" wording="v=20.04 d=36773"/>
# OPES est une liste qui contiendra toutes les balises recherchées
OPES = XML.findAll(name='ope', attrs={'category': '185'})
# J'initialise une liste avec le nom des colonnes que je souhaite avoir
KEYS = ['date','amount','vol','dist','wording']
# J'initialise un dictionnaire Python dont les valeurs seront par défaut des listes
DICT = defaultdict(list)
# J'exécute la fonction "toDict" qui va ajouter à mon dictionnaire "DICT" toutes les valeurs extraites dans le XML
DICT = toDICT(KEYS, OPES, DICT)
# J'exécute la fonction "toDF" pour créer un dataframe Pandas
DF = toDF(DICT)
# J'affiche le dataframe
HTML(DF.to_html())
  date amount vol dist kms px/l cum kms cum vol conso
0 2019-09-07 29.45 20.04 36773 496.0 1.470 496.0 20.04 4.04
1 2019-09-15 43.17 30.02 37269 792.0 1.438 1288.0 50.06 3.89
2 2019-10-01 43.10 30.12 38061 789.0 1.431 2077.0 80.18 3.86
3 2019-10-16 44.24 31.31 38850 782.0 1.413 2859.0 111.49 3.90
4 2019-10-26 50.71 36.48 39632 776.0 1.390 3635.0 147.97 4.07
5 2019-11-11 50.15 34.16 40408 624.0 1.468 4259.0 182.13 4.28
6 2019-11-24 56.60 37.86 41032 516.0 1.495 4775.0 219.99 4.61
7 2019-12-05 37.37 25.49 41548 332.0 1.466 5107.0 245.48 4.81
8 2019-12-10 32.29 21.54 41880 589.0 1.499 5696.0 267.02 4.69
9 2019-12-21 49.34 32.57 42469 635.0 1.515 6331.0 299.59 4.73
10 2020-01-03 50.92 33.52 43104 711.0 1.519 7042.0 333.11 4.73
11 2020-01-17 53.57 34.90 43815 538.0 1.535 7580.0 368.01 4.86
12 2020-01-31 30.20 20.70 44353 384.0 1.459 7964.0 388.71 4.88
13 2020-02-14 39.24 27.08 44737 666.0 1.449 8630.0 415.79 4.82
14 2020-02-29 49.45 33.30 45403 537.0 1.485 9167.0 449.09 4.90
15 2020-03-16 29.85 22.98 45940 0.0 1.299 0.0 472.07 0.00

Mes données sont enfin bien formatées.

Passons ensuite au graphique...

# J'initialise mon graphique
fig, axe = plt.subplots(figsize=(10.6,8))
# Je définis mon axe "x" avec les valeurs de la colonne "date" exceptée la dernière valeur
x = DF.iloc[:-1]['date'].dt.strftime('%Y-%m-%d')
# Je définis mon axe "y" avec les valeurs de la colonne "conso" exceptée la dernière valeur
y = DF.iloc[:-1]['conso']
# Je créé un graphique de type barres et de couleurs bleues 
axe.bar(x, y, label='Consommation', color='#1B80EA')
# J'ajoute un label à l'axe "y"
axe.set_ylabel('Consommation')
# J'ajoute un label à l'axe "x"
axe.set_xlabel('Période')
# Je définis la limite minimum de l'axe "y" en prenant en compte la valeur entière minimum de ma consommation
axe.set_ylim(int(DF.iloc[:-1]['conso'].min()))
# J'ajoute un titre au graphique en y indiquant la consommation moyenne calculée à l'aide de la fonction "mean" appliquée sur la colonne "conso" et arrondi à deux chiffres après la virgule 
axe.set_title('Golf VII: Conso moyenne: {} l/100kms'.format(round(DF.iloc[:-1].conso.mean(), 2)))
# Je formate automatiquement l'affichage des dates pour l'axe "x"
fig.autofmt_xdate()
# A partir d'ici, je créé un courbe de tendance sur la consommation
# Je définis un axe "x" temporaire contenant uniquement les valeurs de l'index. De 0 à 14
_x = DF.iloc[:-1].index
# A l'aide de la fonction "linregress" du module "scipy", je calcul la tendance de la consommation
lr = stats.linregress(_x, y)
# J'ajoute au graphique, sur les mêmes axes "x" & "y" la courbe de tendance de couleur rouge
axe.plot(x, lr.intercept + lr.slope * _x, marker='.', color='r', label='Tendance Consommation')
# Je positionne la légende dans le coin supérieur gauche
axe.legend(loc='upper left')
# Je créé un nouvel axe "y" 
ax2 = axe.twinx()
# Je définis mon nouvel axe "y" avec les valeurs de la colonne "px/l" exceptée la dernière valeur
y = DF.iloc[:-1]['px/l']
# Je crée un graphique de type ligne et de couleur or
ax2.plot(x, y, color='#C7A986', marker='o', label='px/l')
# J'ajoute un label au second axe "y"
ax2.set_ylabel('Prix au litre')
# Je positionne la légende en bas à droite
ax2.legend(loc='lower right')
# J'affiche le graphique
plt.show()

Je vais maintenant faire une synthèse de mes données.
Je souhaite avoir une représentation de ma consommation à chaque fin de mois.

# La fonction "resample" appliquée sur un dataframe permet de redéfinir les données
# Je souhaite regrouper mes données pour chaque fin de mois "M" appliqué sur la colonne "date
R = DF.resample('M',on='date')
# Je créé un nouveau dataframe à partir de ma synthèse et en agrégeant les données 
DF2 = R.agg({'amount':sum,'vol':sum,'dist':max,'kms':sum,'px/l':np.mean,'cum kms':max,'cum vol':max,'conso':np.mean})
# Je formate les différents nombres
DF2['conso'] = DF2['conso'].apply(lambda x: round(x, 2))
DF2['px/l'] = DF2['px/l'].apply(lambda x: round(x, 3))
DF2['kms'] = DF2['kms'].apply(int)
DF2['cum kms'] = DF2['cum kms'].apply(int)
# J'affiche le dataframe
HTML(DF2.to_html())
  amount vol dist kms px/l cum kms cum vol conso
date                
2019-09-30 72.62 50.06 37269 1288 1.454 1288 50.06 3.96
2019-10-31 138.05 97.91 39632 2347 1.411 3635 147.97 3.94
2019-11-30 106.75 72.02 41032 1140 1.482 4775 219.99 4.45
2019-12-31 119.00 79.60 42469 1556 1.493 6331 299.59 4.74
2020-01-31 134.69 89.12 44353 1633 1.504 7964 388.71 4.82
2020-02-29 88.69 60.38 45403 1203 1.467 9167 449.09 4.86
2020-03-31 29.85 22.98 45940 0 1.299 0 472.07 0.00

Intéressant comme synthèse.
Les dates de fin de mois ont été automatiquement calculées.

Et maintenant, le petit graphique qui va bien.

fig, axe = plt.subplots(figsize=(10.6,8))
x = DF2.iloc[:-1].index.strftime('%Y-%m-%d')
y = DF2.iloc[:-1]['conso']
axe.bar(x, y, label='conso', color='#1B80EA')
axe.set_ylabel('Consommation')
axe.set_xlabel('Période')
axe.set_ylim(int(y.min()))
_x = range(len(DF2.iloc[:-1].index))
lr = stats.linregress(_x, y)
axe.plot(x, lr.intercept + lr.slope * _x, marker='.', color='r', label='Tendance Consommation')
axe.legend(loc='upper left')
axe.set_title('Golf VII: Conso moyenne: {} l/100kms'.format(round(y.mean(), 2)))
fig.autofmt_xdate()
ax2 = axe.twinx()
y = DF2.iloc[:-1]['px/l']
ax2.plot(x, y, color='#C7A986', marker='o', label='px/l')
ax2.set_ylabel('Prix au litre')
ax2.legend(loc='lower right')
fig.savefig(FILE.parent / 'Analyse Conso Fin De Mois.png', format='png')
plt.show()

Vraiment bluffant Pandas et Matplotlib

Les dataframes Pandas offre tout un tas de possibilités qu'il est impossible de résumé en un seul article.

Les pages de documentation des projets sont très bien fournies.