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.