feat: add 22 08 2025
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 303 KiB |
@@ -0,0 +1,225 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# CONFIG
|
||||||
|
# ==========================
|
||||||
|
CHEM_PATH = "data_valider_Pierre_toutes_dates_hydro.xlsx"
|
||||||
|
CHEM_SHEET = "tri_points"
|
||||||
|
|
||||||
|
TYPE_COL = "type" # 'rivière' / 'source'
|
||||||
|
HYDRO_COL = "hydrodynamique" # 'hautes-eaux' / 'moyennes-eaux' / 'étiage'
|
||||||
|
KM_COL = "km"
|
||||||
|
CAMP_COL = " nb campagne" # (il y a un espace initial dans le fichier)
|
||||||
|
|
||||||
|
# Dossier d’export
|
||||||
|
EXPORT_DIR = Path("out")
|
||||||
|
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Limites de l’axe X (km) pour tous les subplots
|
||||||
|
KM_XLIM = (0, 50) # mets à None pour auto; sinon tuple (min,max)
|
||||||
|
|
||||||
|
# === COULEURS PAR ÉTAT HYDRODYNAMIQUE ===
|
||||||
|
hautes_eaux_colors = ["#EA66FF", "#E01EFF", "#C942FF", "#B300F9", "#8000B2", "#591D71", "#2C0E38"]
|
||||||
|
moyennes_eaux_colors = ["#D8E6F2", "#B1CEE6", "#8BB5D9", "#649DCD", "#3E85C0", "#316A9A", "#254F73", "#5671F7", "#0000FF", "#18354D"]
|
||||||
|
etiage_colors = ["#FFE4B5", "#FFA500", "#FF0000", "#8B0000"]
|
||||||
|
|
||||||
|
# Valeurs EXACTES présentes dans le fichier
|
||||||
|
HYDRO_STATES = [
|
||||||
|
("hautes-eaux", hautes_eaux_colors),
|
||||||
|
("moyennes-eaux", moyennes_eaux_colors),
|
||||||
|
("étiage", etiage_colors)
|
||||||
|
]
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# PARAMÈTRES À TRACER
|
||||||
|
# (y_riv / y_src facultatifs — auto si omis)
|
||||||
|
# ==========================
|
||||||
|
parametres = [
|
||||||
|
{"variable": "Fe2+", "ylabel": "Fe2+ (mg/L)"},
|
||||||
|
{"variable": "Alcalinité", "ylabel": "HCO3- (mg/L)"},
|
||||||
|
{"variable": "Na+", "ylabel": "Na+ (mg/L)"},
|
||||||
|
{"variable": "Mg2+", "ylabel": "Mg2+ (mg/L)"},
|
||||||
|
{"variable": "T°C", "ylabel": "Température (°C)"},
|
||||||
|
{"variable": "pH", "ylabel": "pH"},
|
||||||
|
{"variable": "Cl-", "ylabel": "Cl- (mg/L)"},
|
||||||
|
{"variable": "c25°C ", "ylabel": "Conductivité (μS/cm)"},
|
||||||
|
{"variable": "DOC", "ylabel": "DOC (mg/L)"},
|
||||||
|
{"variable": "SO42-", "ylabel": "SO42- (mg/L)"},
|
||||||
|
{"variable": "SiO2", "ylabel": "SiO2 (mg/L)"},
|
||||||
|
{"variable": "PO43-", "ylabel": "PO43- (mg/L)"},
|
||||||
|
{"variable": "NO3-", "ylabel": "NO3- (mg/L)"},
|
||||||
|
{"variable": "Ca recalculé","ylabel": "Ca2+ (mg/L)"},
|
||||||
|
{"variable": "O2 mg/L", "ylabel": "O2 (mg/L)"},
|
||||||
|
{"variable": "Fluorure", "ylabel": "Fl- (mg/L)"},
|
||||||
|
{"variable": "NO2-", "ylabel": "NO2- (mg/L)"},
|
||||||
|
{"variable": "NH4+", "ylabel": "NH4+ (mg/L)"},
|
||||||
|
{"variable": "δ13C VPDB", "ylabel": "δ13C"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# UTILITAIRES
|
||||||
|
# ==========================
|
||||||
|
def sanitize_filename(s: str) -> str:
|
||||||
|
s = str(s).strip().replace("\n", " ")
|
||||||
|
for b in ['<','>',':','"','/','\\','|','?','*']:
|
||||||
|
s = s.replace(b, '_')
|
||||||
|
s = '_'.join(s.split()) # compresser espaces
|
||||||
|
return s
|
||||||
|
|
||||||
|
def nice_camp_label(c):
|
||||||
|
try:
|
||||||
|
cf = float(c)
|
||||||
|
if cf.is_integer():
|
||||||
|
return f"camp. {int(cf)}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return f"camp. {c}"
|
||||||
|
|
||||||
|
def get_ylim_for(param, df_sources, df_riviere, var, qlo=0.05, qhi=0.95, pad=0.05):
|
||||||
|
"""
|
||||||
|
Calcule (y_src, y_riv).
|
||||||
|
- Si 'y_src'/'y_riv' sont fournis dans param -> on les respecte.
|
||||||
|
- Sinon: auto depuis les données (quantiles qlo–qhi) + marge 'pad'.
|
||||||
|
y_riv est calculé sur TOUTES les données 'rivière' (tous états) pour
|
||||||
|
que les 3 subplots partagent la même échelle.
|
||||||
|
"""
|
||||||
|
# --- rivière
|
||||||
|
if "y_riv" in param:
|
||||||
|
y_riv = tuple(param["y_riv"])
|
||||||
|
else:
|
||||||
|
dr = df_riviere[[var]].dropna()
|
||||||
|
if dr.empty:
|
||||||
|
y_riv = (0, 1)
|
||||||
|
else:
|
||||||
|
lo, hi = dr[var].quantile([qlo, qhi]).values
|
||||||
|
if not np.isfinite(lo) or not np.isfinite(hi) or lo == hi:
|
||||||
|
lo, hi = float(dr[var].min()), float(dr[var].max())
|
||||||
|
span = max(hi - lo, 1e-12)
|
||||||
|
y_riv = (lo - pad*span, hi + pad*span)
|
||||||
|
|
||||||
|
# --- sources
|
||||||
|
if "y_src" in param:
|
||||||
|
y_src = tuple(param["y_src"])
|
||||||
|
else:
|
||||||
|
ds = df_sources[[var]].dropna()
|
||||||
|
if ds.empty:
|
||||||
|
y_src = y_riv
|
||||||
|
else:
|
||||||
|
lo, hi = ds[var].quantile([qlo, qhi]).values
|
||||||
|
if not np.isfinite(lo) or not np.isfinite(hi) or lo == hi:
|
||||||
|
lo, hi = float(ds[var].min()), float(ds[var].max())
|
||||||
|
span = max(hi - lo, 1e-12)
|
||||||
|
y_src = (lo - pad*span, hi + pad*span)
|
||||||
|
|
||||||
|
return y_src, y_riv
|
||||||
|
|
||||||
|
def build_campaign_color_map(df_riv, hydro_col, camp_col):
|
||||||
|
"""Map {state: {camp_id: color}} avec palettes par état (boucle si nécessaire)."""
|
||||||
|
mapping = {}
|
||||||
|
for state, palette in HYDRO_STATES:
|
||||||
|
sub = df_riv.loc[df_riv[hydro_col] == state]
|
||||||
|
camps = pd.unique(sub[camp_col].dropna())
|
||||||
|
camps_sorted = np.sort(camps)
|
||||||
|
if len(camps_sorted) == 0:
|
||||||
|
mapping[state] = {}
|
||||||
|
continue
|
||||||
|
colors = (palette * ((len(camps_sorted) // len(palette)) + 1))[:len(camps_sorted)]
|
||||||
|
mapping[state] = {c: col for c, col in zip(camps_sorted, colors)}
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# LECTURE & PRÉPA
|
||||||
|
# ==========================
|
||||||
|
chem = pd.read_excel(CHEM_PATH, sheet_name=CHEM_SHEET)
|
||||||
|
|
||||||
|
# Harmoniser 'type'
|
||||||
|
chem[TYPE_COL] = chem[TYPE_COL].astype(str).str.strip().str.lower()
|
||||||
|
|
||||||
|
# Numériques utiles
|
||||||
|
chem[KM_COL] = pd.to_numeric(chem[KM_COL], errors="coerce")
|
||||||
|
chem[CAMP_COL] = pd.to_numeric(chem[CAMP_COL], errors="coerce")
|
||||||
|
|
||||||
|
# Tri logique (km puis campagne)
|
||||||
|
chem = chem.sort_values([KM_COL, CAMP_COL])
|
||||||
|
|
||||||
|
# Sous-ensembles
|
||||||
|
df_sources = chem.loc[chem[TYPE_COL] == "source"].copy()
|
||||||
|
df_riviere = chem.loc[chem[TYPE_COL] == "rivière"].copy()
|
||||||
|
|
||||||
|
# Colonnes indispensables côté rivière
|
||||||
|
for c in [HYDRO_COL, KM_COL, CAMP_COL]:
|
||||||
|
if c not in df_riviere.columns:
|
||||||
|
raise KeyError(f"Colonne manquante pour 'rivière': {c}")
|
||||||
|
|
||||||
|
# Couleurs par campagne×état
|
||||||
|
camp_color_map = build_campaign_color_map(df_riviere, HYDRO_COL, CAMP_COL)
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# TRAÇAGE
|
||||||
|
# ==========================
|
||||||
|
for p in parametres:
|
||||||
|
var, ylabel = p["variable"], p["ylabel"]
|
||||||
|
if var not in chem.columns:
|
||||||
|
print(f"[AVERTISSEMENT] '{var}' absent → figure ignorée.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
y_src_limits, y_riv_limits = get_ylim_for(p, df_sources, df_riviere, var, qlo=0, qhi=1)
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(nrows=4, ncols=1, sharex=False, figsize=(12, 9))
|
||||||
|
# Réserve de l'espace à droite pour la légende
|
||||||
|
fig.subplots_adjust(hspace=0.18, right=0.80)
|
||||||
|
fig.suptitle(f"{var} — X = km (campagnes en courbes pour la rivière)", fontsize=12, x=0.01, ha="left")
|
||||||
|
|
||||||
|
# 1) SOURCES — X = km (nuage gris)
|
||||||
|
ax_src = axes[0]
|
||||||
|
if not df_sources.empty:
|
||||||
|
ds = df_sources[[KM_COL, var]].dropna()
|
||||||
|
ax_src.scatter(ds[KM_COL], ds[var], s=24, alpha=0.7,
|
||||||
|
edgecolors="white", linewidths=0.4, color="#666666")
|
||||||
|
ax_src.set_ylabel(ylabel + "\n(sources)")
|
||||||
|
ax_src.set_xlabel("km")
|
||||||
|
if KM_XLIM: ax_src.set_xlim(*KM_XLIM)
|
||||||
|
ax_src.set_ylim(*y_src_limits)
|
||||||
|
ax_src.grid(True, linestyle=":", alpha=0.35)
|
||||||
|
|
||||||
|
# 2–4) RIVIÈRE par ÉTAT — X = km ; une courbe par 'nb campagne'
|
||||||
|
legend_handles, legend_labels = [], []
|
||||||
|
for i, (state, palette) in enumerate(HYDRO_STATES, start=1):
|
||||||
|
ax = axes[i]
|
||||||
|
sub = df_riviere.loc[df_riviere[HYDRO_COL] == state, [KM_COL, CAMP_COL, var]]
|
||||||
|
sub = sub.dropna(subset=[KM_COL, var])
|
||||||
|
|
||||||
|
if not sub.empty:
|
||||||
|
for camp, grp in sub.groupby(CAMP_COL):
|
||||||
|
grp = grp.sort_values(KM_COL)
|
||||||
|
color = camp_color_map.get(state, {}).get(camp, (palette[0] if palette else "C0"))
|
||||||
|
h = ax.plot(grp[KM_COL], grp[var], marker="o", ms=4, lw=1.6, color=color)[0]
|
||||||
|
lab = nice_camp_label(camp)
|
||||||
|
if lab not in legend_labels:
|
||||||
|
legend_handles.append(h)
|
||||||
|
legend_labels.append(lab)
|
||||||
|
|
||||||
|
ax.set_ylabel(f"{ylabel}\n({state})")
|
||||||
|
ax.set_xlabel("km")
|
||||||
|
if KM_XLIM: ax.set_xlim(*KM_XLIM)
|
||||||
|
ax.set_ylim(*y_riv_limits)
|
||||||
|
ax.grid(True, linestyle=":", alpha=0.35)
|
||||||
|
|
||||||
|
# Légende globale (campagnes) à droite, hors du tracé
|
||||||
|
if legend_handles:
|
||||||
|
axes[1].legend(
|
||||||
|
legend_handles, legend_labels,
|
||||||
|
loc="center left", bbox_to_anchor=(1.02, 0.5),
|
||||||
|
frameon=False, ncol=1, title="Campagnes", borderaxespad=0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export
|
||||||
|
fname = sanitize_filename(f"{var}_rivieres_sources.png")
|
||||||
|
outpath = EXPORT_DIR / fname
|
||||||
|
fig.savefig(outpath, dpi=200, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
print(f"[OK] Export: {outpath.resolve()}")
|
||||||
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 281 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
pandas==2.3.2
|
||||||
|
matplotlib==3.10.5
|
||||||
|
openpyxl==3.1.5
|
||||||