Skip to article frontmatterSkip to article content

partons d’un jeu de données réel qui décrit des grandeurs géophysiques sur une période plus de 10 ans avec une fréquence de 1h

import pandas as pd

pd.read_excel

ce qui va nous donner une occasion de travailler sur un fichier .xlsx
pour cela le point d’entrée c’est pd.read_excel, mais il faut savoir que cela demande une dépendance supplémentaire

du coup si nécessaire, faites l’installation de openpyxl comme indiqué, par exemple:

# uncomment if needed
# %pip install openpyxl

chargement

le premier chargement “naif” avec read_excel nous permet de voir que la donnée temporelle ressemble à ceci

18/08/2006 21:00

aussi on va utiliser ce format pour charger “proprement” notre dataframe; cela va avoir deux avantages

# c'est un peu long à charger
# aussi on va ranger ça dans un coin 
# et si nécessaire, on copiera ça au lieu de recharger

FORMAT = "%d/%m/%Y %H:%M"

try:
    df_original
    print("using preloaded data")
except NameError:
    df_original = pd.read_excel("data/DB_Galion.xlsx", date_format=FORMAT)
# repartir d'ici si on a besoin de repartir de 0:

df = df_original.copy()
df.head()
Loading...

pour info, les données en question signifient ceci:

colonnenom complet
NPNiveau Puits
PAPression Atmosphérique
MGMarée Océanique (Marée Géographique)
ETMarée terrestre (Earth Tide)
# quelques ordres de grandeur

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 117188 entries, 0 to 117187
Data columns (total 5 columns):
 #   Column             Non-Null Count   Dtype         
---  ------             --------------   -----         
 0   Date_Heure_locale  117188 non-null  datetime64[ns]
 1   NP                 116096 non-null  object        
 2   PA                 114921 non-null  float64       
 3   MG                 115843 non-null  float64       
 4   ET                 117188 non-null  float64       
dtypes: datetime64[ns](1), float64(3), object(1)
memory usage: 4.5+ MB

ça fait tout de même 120.000 entrées de 4 mesures chacune; ça ne va pas être possible de faire une inspection visuelle de tout ça

voyons un peu le genre de chose qu’on peut faire automatiquement pour s’assurer de la complétude / cohérence de ces données

préparation

pour commencer, la todo list est souvent celle-ci:

ici notre colonne candidat pour être l’index, c’est cette colonne temporelle, c’est vraiment naturel

unicité ?

# pour être propre, on veut s'assurer que la colonne en question
# est bien à valeurs uniques 

# normalement value_counts() ne devrait contenir que des 1
counts = df.Date_Heure_locale.value_counts()
dups = counts[counts != 1]

# en fait on a plusieurs entrées correspondant à ces moments-là:
dups
Date_Heure_locale 2012-03-25 01:00:00 2 2019-03-31 01:00:00 2 2018-03-25 01:00:00 2 2008-03-30 01:00:00 2 2013-03-31 01:00:00 2 2014-03-30 01:00:00 2 2017-03-26 01:00:00 2 2009-03-29 01:00:00 2 2007-03-25 01:00:00 2 2016-03-27 01:00:00 2 2015-03-29 01:00:00 2 2010-03-28 01:00:00 2 2011-03-27 01:00:00 2 Name: count, dtype: int64
# pour combien de dates a-t-on 2 données

len(dups)
13
# voyons un peu ces multi-entrées

# on ne peut pas encore écrire directement ceci
# car justement on n'a pas encore mis la date en index
#df.loc[dups.index]

# on passe par une df temporaire qui a le bon index
# et on regarde les 3 premiers doublons

df.set_index('Date_Heure_locale').loc[dups.index[:3]]
Loading...

mmh, c’est assez étrange, les 3 premières colonnes ont des données quasi identiques au moment du doublon, mais pas la 4ème...

compte tenu du faible nombre de lignes concernées, on va prendre une décision conservatoire, qui est de garder, pour chacun de ces 13 instants, la moyenne des deux mesures

sauf que, si on essaie de faire ça ce stade, on est confronté au fait que la colonne NP est de type object...

une colonne de type object

df.dtypes
Date_Heure_locale datetime64[ns] NP object PA float64 MG float64 ET float64 dtype: object

pour bien trouver tous les soucis avec cette colonne:

# voyons ce que donne pd.to_numeric

try:
    pd.to_numeric(df.NP)
except Exception as exc:
    print(f"OOPS {type(exc)} {exc=}")
OOPS <class 'ValueError'> exc=ValueError('Unable to parse string " " at position 1695')

ce qui nous indique qu’il y a au moins un endroit où la colonne NP contient une chaine composée d’un espace; voyons combien il y en a de ce genre

(df.NP == ' ').value_counts()
NP False 113624 True 3564 Name: count, dtype: int64

fort heureusement on peut forcer la conversion comme ceci

pd.to_numeric(df.NP, errors='coerce')
0 4.600 1 4.600 2 4.600 3 4.600 4 4.600 ... 117183 4.662 117184 4.661 117185 4.663 117186 4.663 117187 4.662 Name: NP, Length: 117188, dtype: float64

cette fois la conversion se fait correctement; il ne faut pas oublier par contre de bien adopter la nouvelle colonne - car avec la cellule précédente on a calculé un nouvelle colonne mais elle ne fait pas partie de la dataframe

# comme on est satisfait on remplace la colonne dans la dataframe

df['NP'] = pd.to_numeric(df.NP, errors='coerce')
# et du coup maintenant on a bien des nombres partout

df.dtypes
Date_Heure_locale datetime64[ns] NP float64 PA float64 MG float64 ET float64 dtype: object

la moyenne

on peut à présent faire la moyenne pour les doublons

df = df.groupby('Date_Heure_locale')[['NP', 'PA', 'MG', 'ET']].mean()
df.head()
Loading...
# et pour être bien sûr

# maintenant value_counts() ne doit contenir que des 1
counts = df.reset_index().Date_Heure_locale.value_counts()
dups = counts[counts != 1]

if len(dups) == 0:
    print("OK !")
OK !

c’est terminé

et on n’a même pas besoin d’adopter l’index puisque ça a déjà été fait par le groupby..

df.head(2)
Loading...

aperçu

notre objectif: passer le moins de temps possible pour voir une vague idée de ces données

# version simplissime: vraiment pas top

df.plot();
<Figure size 640x480 with 1 Axes>

la première amélioration vient au prix d’une ligne qu’on mentionne généralement au début (autour de par exemple la ligne import pandas as pd)

%matplotlib ipympl pour des graphiques interactifs

%matplotlib ipympl
# une fois qu'on a choisi ce mode on obtient des visus interactives
# on peut agrandir la figure, zoomer, se déplacer, etc...

df.plot();
Loading...

figsize

si vous voulez choisir une taille par défaut pour les figures

# il y a plein d'options pour faire ça
# j'aime bien celle-ci, mais bon...

from IPython.core.pylabtools import figsize
figsize(8, 6)
# et à partir de là...

df.plot();
Loading...
# sachant qu'on peut toujours choisir la taille pour une figure donnée

df.plot(figsize=(3, 3));
Loading...

subplots=True

comme les échelles ne sont pas forcément les mêmes, ou pour plein d’autres raisons, on peut avoir envie de voir les données indépendamment les unes des autres

df.plot(subplots=True);
Loading...

alpha=0.1

si on insiste pour voir les 4 données dans la même vue, comme on avait fait pour commencer, il y a tellement de données en X qu’on ne voit que la dernière colonne !
dans ces cas-là le canal alpha (la transparence) est notre meilleur allié

df.plot(alpha=0.1);
Loading...

les trous dans la donnée (aka nan)

à ce stade on a donc des nan - au moins dans NP, ça on en est sûr, et dans les autres colonnes peut-être aussi

# dans une colonne en particulier
df.NP.isna().sum()
np.int64(4656)
# si on veut une information plus globale, par exemple
df.isna().sum(axis=0)
NP 4656 PA 2267 MG 1345 ET 0 dtype: int64

comment les calculer ?

ce qu’on aimerait savoir maintenant, c’est est-ce que les ‘trous’ sont plutôt éparpillés ou plutôt groupés; et pour ça on va

si on avait une série parfaitement pleine, on n’obtiendrait que des valeurs de “1h” dans ce résultat
et donc en éliminant de cela les valeurs “1h”, on obtient la liste des trous dans la donnée (c’est-à-dire quand y a-t-il eu un trou, et combien de temps a-t-il duré)

# par exemple avec la colonne NP qui a les plus beaux trous
NP = df.NP.copy()

# on isole les lignes qui ont une valeur
NP_defined = NP[NP.notna()]

# ce qui nous intéresse ce sont les timestamps
NP_times = NP_defined.index

# combien de mesures en tout, combien de mesures pertinentes
NP.shape, NP_times.shape
((117175,), (112519,))
# la grosse astuce consiste à mettre les timestamps comme valeurs
# et pour ça on utilise la fonction pd.to_series 
# souvenez-vous que NP_times est un objet Index

NP_times_series = NP_times.to_series()
NP_times_series
Date_Heure_locale 2006-08-18 00:00:00 2006-08-18 00:00:00 2006-08-18 01:00:00 2006-08-18 01:00:00 2006-08-18 02:00:00 2006-08-18 02:00:00 2006-08-18 03:00:00 2006-08-18 03:00:00 2006-08-18 04:00:00 2006-08-18 04:00:00 ... 2019-12-30 15:00:00 2019-12-30 15:00:00 2019-12-30 16:00:00 2019-12-30 16:00:00 2019-12-30 17:00:00 2019-12-30 17:00:00 2019-12-30 18:00:00 2019-12-30 18:00:00 2019-12-30 19:00:00 2019-12-30 19:00:00 Name: Date_Heure_locale, Length: 112519, dtype: datetime64[ns]

on décale

# et du coup maintenant on peut décaler (avec shift()) les valeurs de 1 cran

NP_times_series.shift()
Date_Heure_locale 2006-08-18 00:00:00 NaT 2006-08-18 01:00:00 2006-08-18 00:00:00 2006-08-18 02:00:00 2006-08-18 01:00:00 2006-08-18 03:00:00 2006-08-18 02:00:00 2006-08-18 04:00:00 2006-08-18 03:00:00 ... 2019-12-30 15:00:00 2019-12-30 14:00:00 2019-12-30 16:00:00 2019-12-30 15:00:00 2019-12-30 17:00:00 2019-12-30 16:00:00 2019-12-30 18:00:00 2019-12-30 17:00:00 2019-12-30 19:00:00 2019-12-30 18:00:00 Name: Date_Heure_locale, Length: 112519, dtype: datetime64[ns]
# et surtout faire la différence entre les deux, qui normalement doit donner 1h

delta = NP_times_series - NP_times_series.shift()
delta.head(3)
Date_Heure_locale 2006-08-18 00:00:00 NaT 2006-08-18 01:00:00 0 days 01:00:00 2006-08-18 02:00:00 0 days 01:00:00 Name: Date_Heure_locale, dtype: timedelta64[ns]
# il faut enlever la première ligne, évidemment
# car le shift y a mis NaT

delta = delta.iloc[1:]

digression: comment fabriquer un timedelta

# c'est assez simple, on fait par exemple

pd.to_timedelta("01:00:00")
Timedelta('0 days 01:00:00')
# ou encore, c'est assez flexible:

pd.to_timedelta(1, 'h')
Timedelta('0 days 01:00:00')

reprenons

nous en sommes donc au stade où on veut enlever de delta les valeurs correspondant à 1h

holes = delta[delta != pd.to_timedelta(1, 'h')]
print(f"nous avons trouvé {len(holes)} trous dans la colonne NP")
holes
nous avons trouvé 76 trous dans la colonne NP
Date_Heure_locale 2006-11-06 08:00:00 9 days 18:00:00 2006-11-24 12:00:00 0 days 07:00:00 2007-03-25 03:00:00 0 days 02:00:00 2007-04-13 01:00:00 10 days 17:00:00 2007-06-25 01:00:00 27 days 17:00:00 ... 2019-08-30 14:00:00 0 days 02:00:00 2019-09-07 03:00:00 1 days 08:00:00 2019-09-17 02:00:00 2 days 00:00:00 2019-09-24 10:00:00 2 days 01:00:00 2019-11-07 00:00:00 8 days 01:00:00 Name: Date_Heure_locale, Length: 76, dtype: timedelta64[ns]

on les affiche

# si on veut les montrer
# c'est mieux de transformer en heures (sinon on a des nano-secondes apparemment)
# en plus comme on n'a pas une dataframe, avec le driver %matlotlib ipympl
# il faut créer une nouvelle figure

import matplotlib.pyplot as plt

plt.figure()
(holes / pd.to_timedelta(1, 'h')).plot(style=['r.']);
Loading...

question

utilisez le zoom pour trouver à quelle époque a eu lieu la coupure la plus longue dans l’acquisition de NP

# on peut le retrouver comme ceci

when = holes.idxmax()
how_long = holes.loc[when]

print(f"the longest outage was {how_long} long and occurred on {when}")
the longest outage was 79 days 15:00:00 long and occurred on 2012-06-26 14:00:00

un peu de lissage

on regarde cette fois la courbe ‘PA’; utilisez le zoom pour regarder environ un mois

plt.figure()
PA = df.PA
PA.plot(alpha=0.4);
Loading...

on voit que la fluctuation en tendance est obscurcie par des variations de plus haute fréquence; on va essayer de lisser ce signal pour ne conserver que la tendance de fond

normalement si vous zoomez encore davantage, vous devriez percevoir que la haute fréquence est de l’ordre de la demie journée

rolling

on va calculer le rolling de ce signal avec deux périodes (12h et 24h), et afficher tout cela

# rolling windowed: on essaie avec ces deux durées
# souvenez-vous que l'on a une mesure par heure

smooth24 = PA.rolling(24, center=True).mean()
smooth12 = PA.rolling(12, center=True).mean()

plt.figure()
PA.plot(alpha=0.2)

smooth12.plot(style=['g'], alpha=0.5);
smooth24.plot(style=['r'], alpha=0.5);
Loading...

à nouveau, je vous invite à zoomer dans le graphique pour juger de la pertinence de chacune de ces deux approximations

laquelle garderiez-vous ?