|
|
|
# "Chroniques volumineuses", c'est-à-dire très longues et/ou à pas de temps très fin
|
|
|
|
|
|
|
|
(en construction ; attention, confusion entre dixièmes de mm et centièmes, vérif en cours...)
|
|
|
|
|
|
|
|
Cette session "E" est la première de deux séances consacrées au problème des "chroniques volumineuses", c'est-à-dire très longues et/ou à pas de temps très fin : * comment interpréter des chroniques qui sont "écrasées" par le tracé : rééchantillonnage, intérêt et pièges.
|
|
|
|
La session suivante, "F", proposera des pistes pour faciliter le tracé : prétraitements avant tracé, graphe avec deux vignettes, la chronique complète et une chronique zoomable, avec mention sur le premier d'un rectangle correspondant à l'étendue du graphique zoomé.
|
|
|
|
|
|
|
|
On va travailler ici avec une longue série, avec des illustrations utilisant un fichier binaire de 10 ans de données à 5 minutes de résolution. Vous pourrez travailler avec vos propres fichiers ou le fichier de débit journalier utilisé précédemment ("B"), mais les réflexions de cette session sont d'autant plus pertinentes que le fichier est "volumineux".
|
|
|
|
|
|
|
|
Pour permettre d'utiliser n'importe quel type de fichier, on va donc distinguer la fonction de lecture, qui retournera une liste de x et une liste de y (ou bien un objet pandas), et la fonction pour le tracé qui prendra en entrée une liste de x et une liste de y (ou bien un objet pandas).
|
|
|
|
|
|
|
|
## Complément : lecture d'un fichier binaire
|
|
|
|
|
|
|
|
Il n'est pas indispensable de travailler avec un fichier binaire, mais on va commenter le code ci-dessous au moins pour information.
|
|
|
|
|
|
|
|
Pour lire un fichier binaire, il faut impérativement connaître sa structure : son *nombre de valeurs*, le *type* exact des données et éventuellement le *code* correspondant aux lacunes.
|
|
|
|
Ici, on a des valeurs qui correspondent à des cumuls de lames d'eau toutes les 5 minutes, en _dixièmes_ de mm, depuis le 1er juillet 2006 inclus.
|
|
|
|
On remarque que l'on a créé aussi une variable PDT_OBS_EN_MIN, le pas de temps d'observation en minutes. Il est recommandé de définir des variables explicites plutôt que d'écrire simplement "5" quand nécessaire pour deux raisons :
|
|
|
|
- les calculs sont plus lisibles : quand on utilise directement des chiffres, ce sont comme des chiffres tombés du ciel (on les appelle _"magic number"_): cela peut être compliqué de retrouver à quoi cela correspond, même si avec un peu d'habitude on devinera ce que signifie 365 ou 288...
|
|
|
|
- il est plus facile de modifier, pour adapter par exemple la routine à la lecture de données pluviométriques qui, elles, sont à 6 minutes
|
|
|
|
Ensuite, on crée un vecteur de dates ; un "timeindex" pandas serait plus intelligent, on pourra vérifier en séance si effectivement il occupe la même place en mémoire ou pas.
|
|
|
|
Pour la lecture, on utilise np.fromfile(_nom_du_fichier_binaire_, dtype=_type de données_, count=_nombre de données_).
|
|
|
|
Ici les valeurs sont des entiers non signés dtype=np.uint16 codant des intensités en _dixièmes_ de mm, et il y en a autant que de dates dans la liste si on a bien travaillé...
|
|
|
|
On remarque un test de type "try/except" : en cas d'erreur le code ne plante pas mais des messages d'erreur s'affichent dans le terminal.
|
|
|
|
|
|
|
|
Une fois les données dans la vecteur "numpy.array" uint16_valeurs, on va créer à partir de ces éléments un conteneur de type *numpy array*, dont les éléments sont tous de même type, ce qui permet le recours à une bibliothèque de fonctions codées en C. On va effectuer deux traitements :
|
|
|
|
- passer les valeurs en millimètres si ce ne sont pas des lacunes
|
|
|
|
- leur affecter np.nan si ce sont des lacunes.
|
|
|
|
On a écrit une boucle pour faire ce traitement de manière explicite, mais il est possible d'écrire la même chose en une ligne en utilisant numpy (dès que je le retrouve dans mes codes... ).
|
|
|
|
|
|
|
|
Une spécificité de numpy est que les valeurs nan, "not a number", sont rattachés au type *float* : un vecteur de valeurs entières comportant des "nan" devra donc être de type float...
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
# LIRE UN FICHIER BINAIRE
|
|
|
|
def lire_lame_deau_radar_5min(nom_fichier_binaire):
|
|
|
|
# données correspondant au fichier binaire (elles doivent être fournies avec le fichier)
|
|
|
|
PDT_OBS_EN_MIN = 5 # pas de temps d'observation en minutes
|
|
|
|
DIVISEUR = 10
|
|
|
|
CODE_LACUNE_BINAIRE = 65535 # => -999.99 (lacune)
|
|
|
|
nb_of_days = 3653
|
|
|
|
pas_de_temps = 288 # nb de pas de temps dans une journée
|
|
|
|
len_of_tvect = nb_of_days * pas_de_temps # 1052064
|
|
|
|
recordSize = len_of_tvect * 2 # parce que ce sont des entiers codés sur 2 bytes
|
|
|
|
|
|
|
|
# création de la liste de dates
|
|
|
|
date_premiere_valeur = datetime(2006, 7, 1, 0, 5)
|
|
|
|
liste_dates = [date_premiere_valeur + i * timedelta(minutes=PDT_OBS_EN_MIN) for i in
|
|
|
|
range(len_of_tvect)] # PDT_OBS_EN_MIN = 5 minutes
|
|
|
|
|
|
|
|
if Path.is_file(Path(nom_fichier_binaire)):
|
|
|
|
print(f"nom_chemin_binaire1 : {nom_fichier_binaire} ")
|
|
|
|
|
|
|
|
# lecture des valeurs
|
|
|
|
uint16_valeurs = None
|
|
|
|
with open(nom_fichier_binaire, 'rb') as fic_binaire:
|
|
|
|
try:
|
|
|
|
uint16_valeurs = np.fromfile(fic_binaire, dtype=np.uint16, count=int(len(liste_dates)))
|
|
|
|
statut_lecture = "OK"
|
|
|
|
# ATTENTION MAUVAISE PRATIQUE, PEP8 : jamais d'exception sans en préciser le type
|
|
|
|
except:
|
|
|
|
statut_lecture = "erreur" # on pourrait sortir de la fonction avec return None
|
|
|
|
|
|
|
|
# VERIF
|
|
|
|
print("Ensemble des valeurs lues avant traitement (lacunes, diviseur...) : ", set(uint16_valeurs))
|
|
|
|
|
|
|
|
if statut_lecture == "OK":
|
|
|
|
# Traitement des lacunes (il y a plus court comme syntaxe, avec un masque)
|
|
|
|
nb_lacunes = 0
|
|
|
|
for valeur in uint16_valeurs:
|
|
|
|
if int(valeur) == int(CODE_LACUNE_BINAIRE):
|
|
|
|
nb_lacunes += 1
|
|
|
|
print("Nombre de lacunes = ", nb_lacunes)
|
|
|
|
|
|
|
|
# on travaille avec des LAMES D'EAU en centième de mm, à passer en mm
|
|
|
|
np_valeurs = np.array(
|
|
|
|
[entier / DIVISEUR if entier != int(CODE_LACUNE_BINAIRE)
|
|
|
|
else np.nan for entier in uint16_valeurs])
|
|
|
|
|
|
|
|
# ici tracé dans la foulée de la lecture
|
|
|
|
temps = datetime.now() - debut
|
|
|
|
# tracé avec plot
|
|
|
|
plt.plot(liste_dates, np_valeurs)
|
|
|
|
plt.title("Fichier binaire "+ os.path.basename(nom_fichier_binaire) + "\n sans précaution pour les étiquettes de dates" )
|
|
|
|
|
|
|
|
plt.show()
|
|
|
|
print("temps écoulé, step:", temps)
|
|
|
|
|
|
|
|
return liste_dates, np_valeurs
|
|
|
|
```
|
|
|
|
|
|
|
|
## Tracé d'une longue chronique
|
|
|
|
Pour le tracé on peut se contenter de plt.plot(liste_dates, np_valeurs) ou utiliser step en vérifiant que les créneaux sont sur le bon l'intervalle: la première valeur doit correspondre aux cinq premières minutes du 1er juillet 2006.
|
|
|
|
Cependant, si on fait varier le niveau de zoom, on constate que la gestion des étiquettes de date est hasardeuse.
|
|
|
|
On propose donc de construire une fonction séparée (c'est toujours mieux), quitte à l'appeler depuis la fonction de lecture, et de tester des instructions très pratiques qui permettent de retrouver le rendu de pandas, et même de customiser les étiquettes (on n'ira pas jusque là) : *mdates.AutoDateLocator* et *mdates.ConciseDateFormatter*.
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
# Tracer une longue série, applicable à toutes données (lues dans Hydro2, dans un binaire ou tout autre format)
|
|
|
|
import matplotlib.dates as mdates
|
|
|
|
def tracer_proprement_une_longue_serie(dates, valeurs, info_chroniques="lames d'eau radar, mm en 5 min"):
|
|
|
|
""" démonstration de "concise formatter"
|
|
|
|
|
|
|
|
"""
|
|
|
|
locator = mdates.AutoDateLocator(minticks=3, maxticks=12)
|
|
|
|
formatter = mdates.ConciseDateFormatter(locator)
|
|
|
|
|
|
|
|
# VERIF
|
|
|
|
if len(dates) != len(valeurs):
|
|
|
|
print(f"Les 2 vecteurs n'ont pas la même longueur : {len(dates)} dates et {len(valeurs)} valeurs")
|
|
|
|
return
|
|
|
|
# ça permet de quitter la fonction ; ce n'est pas la meilleure manière de faire (plutôt un else...)
|
|
|
|
|
|
|
|
print("Nombre de points : ", len(dates))
|
|
|
|
|
|
|
|
# tracé
|
|
|
|
debut = datetime.now()
|
|
|
|
plt.ion()
|
|
|
|
fig_cf, ax_t = plt.subplots()
|
|
|
|
fig_cf.suptitle("Tracé d'une longue chronique, soin apporté aux étiquettes de dates \n" + info_chroniques)
|
|
|
|
ax_t.set_xlabel("dates, format date", fontsize=14)
|
|
|
|
ax_t.set_ylabel("info_valeurs", fontsize=14)
|
|
|
|
ax_t.xaxis.set_major_locator(locator)
|
|
|
|
ax_t.xaxis.set_major_formatter(formatter)
|
|
|
|
ax_t.step(dates, valeurs)
|
|
|
|
|
|
|
|
temps = datetime.now() - debut
|
|
|
|
print("tracer_proprement_une_longue_serie, temps écoulé, step:", temps)
|
|
|
|
fig_cf.canvas.draw()
|
|
|
|
fig_cf.savefig("test.png")
|
|
|
|
plt.ioff()
|
|
|
|
plt.show()
|
|
|
|
|
|
|
|
# on ne retourne rien
|
|
|
|
```
|
|
|
|
|
|
|
|
## Comment interpréter cette chronique ?
|
|
|
|
Est-ce que l'on se rend compte de la répartition des volumes au cours de l'année ?
|
|
|
|
|
|
|
|
(à suivre) |
|
|
|
\ No newline at end of file |