Spaces:
Running
Running
Commit ·
0ab0788
0
Parent(s):
Déploiement Docker depuis workflow (structure corrigée)
Browse files- .gitignore +26 -0
- .huggingface.yaml +1 -0
- .streamlit/config.toml +15 -0
- Dockerfile +25 -0
- README.md +1 -0
- app/__init__.py +0 -0
- app/config/config.yaml +12 -0
- app/pipelines/import_config.py +94 -0
- app/pipelines/import_data.py +265 -0
- app/pipelines/import_map.py +119 -0
- app/pipelines/import_scatter.py +36 -0
- app/utils/__init__.py +0 -0
- app/utils/config_utils.py +165 -0
- app/utils/data_utils.py +223 -0
- app/utils/gev_utils.py +171 -0
- app/utils/hist_utils.py +127 -0
- app/utils/legends_utils.py +221 -0
- app/utils/map_utils.py +223 -0
- app/utils/menus_utils.py +224 -0
- app/utils/scatter_plot_utils.py +432 -0
- app/utils/show_info.py +19 -0
- app/utils/stats_utils.py +147 -0
- download_data.py +52 -0
- main.py +372 -0
- requirements.txt +21 -0
.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignorer les fichiers de cache Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.parquet
|
| 5 |
+
*.zarr
|
| 6 |
+
*.nc
|
| 7 |
+
*.csv
|
| 8 |
+
|
| 9 |
+
# On garde les .keep
|
| 10 |
+
!data/**/.keep
|
| 11 |
+
# Ajouter les config streamlit au git
|
| 12 |
+
!/.streamlit/*
|
| 13 |
+
|
| 14 |
+
# Ignore aussi d'éventuels fichiers temporaires et artefacts
|
| 15 |
+
*.tmp
|
| 16 |
+
*.bak
|
| 17 |
+
*.DS_Store
|
| 18 |
+
__pycache__/
|
| 19 |
+
*.pyc
|
| 20 |
+
*.cache
|
| 21 |
+
*.log
|
| 22 |
+
|
| 23 |
+
# Ignorer les caches Jupyter
|
| 24 |
+
.jupyter_cache/
|
| 25 |
+
presentation_files/
|
| 26 |
+
assets/
|
.huggingface.yaml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
sdk: docker
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .streamlit/config.toml
|
| 2 |
+
[theme]
|
| 3 |
+
base = "light"
|
| 4 |
+
primaryColor = "#5A7BFF"
|
| 5 |
+
backgroundColor = "#EEF2FF"
|
| 6 |
+
secondaryBackgroundColor = "#FFFFFF"
|
| 7 |
+
textColor = "#1F2D3D"
|
| 8 |
+
font = "sans serif" # "serif" | "sans serif" | "monospace"
|
| 9 |
+
|
| 10 |
+
[server]
|
| 11 |
+
headless = true
|
| 12 |
+
runOnSave = true
|
| 13 |
+
|
| 14 |
+
[browser]
|
| 15 |
+
gatherUsageStats = false
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y \
|
| 4 |
+
build-essential \
|
| 5 |
+
git \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
COPY download_data.py .
|
| 12 |
+
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
RUN pip install huggingface_hub
|
| 15 |
+
|
| 16 |
+
RUN rm -rf ~/.cache/huggingface/hub && \
|
| 17 |
+
python download_data.py
|
| 18 |
+
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
ENV MPLCONFIGDIR=/tmp/matplotlib
|
| 23 |
+
ENV PYTHONPATH=/app
|
| 24 |
+
|
| 25 |
+
CMD ["streamlit", "run", "main.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Déploiement avec snapshot_download depuis le dataset Hugging Face
|
app/__init__.py
ADDED
|
File without changes
|
app/config/config.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
years:
|
| 2 |
+
min: 1959
|
| 3 |
+
max: 2022 # choix dans le menu
|
| 4 |
+
rupture: 1985
|
| 5 |
+
|
| 6 |
+
statisticals:
|
| 7 |
+
modelised: data/statisticals/modelised
|
| 8 |
+
observed: data/statisticals/observed
|
| 9 |
+
|
| 10 |
+
gev:
|
| 11 |
+
modelised: data/gev/modelised
|
| 12 |
+
observed: data/gev/observed
|
app/pipelines/import_config.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
from app.utils.config_utils import load_config, menu_config_statisticals, menu_config_gev
|
| 4 |
+
from app.utils.menus_utils import menu_statisticals, menu_gev
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
import streamlit as st
|
| 10 |
+
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from functools import reduce
|
| 13 |
+
|
| 14 |
+
from app.utils.config_utils import *
|
| 15 |
+
from app.utils.menus_utils import *
|
| 16 |
+
from app.utils.data_utils import *
|
| 17 |
+
from app.utils.stats_utils import *
|
| 18 |
+
from app.utils.map_utils import *
|
| 19 |
+
from app.utils.legends_utils import *
|
| 20 |
+
from app.utils.hist_utils import *
|
| 21 |
+
from app.utils.scatter_plot_utils import *
|
| 22 |
+
from app.utils.show_info import show_info_metric
|
| 23 |
+
from app.utils.gev_utils import compute_return_levels_ns
|
| 24 |
+
|
| 25 |
+
import pydeck as pdk
|
| 26 |
+
import polars as pl
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
from app.pipelines.import_data import pipeline_data
|
| 30 |
+
from app.pipelines.import_map import pipeline_map
|
| 31 |
+
|
| 32 |
+
from app.utils.data_utils import standardize_year, filter_nan
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def pipeline_config(config_path: str, type: str, show_param: bool=False):
|
| 37 |
+
# Chargement de la configuration
|
| 38 |
+
config = load_config(config_path)
|
| 39 |
+
|
| 40 |
+
min_years = config["years"]["min"]
|
| 41 |
+
max_years = config["years"]["max"]
|
| 42 |
+
|
| 43 |
+
if type == "stat":
|
| 44 |
+
STATS, SEASON, SCALE = menu_config_statisticals()
|
| 45 |
+
|
| 46 |
+
params = menu_statisticals(
|
| 47 |
+
min_years,
|
| 48 |
+
max_years,
|
| 49 |
+
STATS,
|
| 50 |
+
SEASON
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
if params is None:
|
| 54 |
+
st.info("Les paramètres d’analyse ne sont pas encore définis. Merci de les configurer pour lancer l’analyse.")
|
| 55 |
+
st.stop()
|
| 56 |
+
|
| 57 |
+
stat_choice, quantile_choice, min_year_choice, max_year_choice, season_choice, scale_choice, missing_rate, show_relief, show_stations = params
|
| 58 |
+
|
| 59 |
+
return {
|
| 60 |
+
"config": config,
|
| 61 |
+
"stat_choice": stat_choice,
|
| 62 |
+
"season_choice": season_choice,
|
| 63 |
+
"stat_choice_key": STATS[stat_choice],
|
| 64 |
+
"scale_choice_key": SCALE[scale_choice],
|
| 65 |
+
"min_year_choice": min_year_choice,
|
| 66 |
+
"max_year_choice": max_year_choice,
|
| 67 |
+
"season_choice_key": SEASON[season_choice],
|
| 68 |
+
"missing_rate": missing_rate,
|
| 69 |
+
"quantile_choice": quantile_choice,
|
| 70 |
+
"scale_choice": scale_choice,
|
| 71 |
+
"show_relief": show_relief,
|
| 72 |
+
"show_stations": show_stations
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
elif type == "gev":
|
| 76 |
+
MODEL_PARAM, MODEL_NAME = menu_config_gev()
|
| 77 |
+
_, SEASON, _ = menu_config_statisticals()
|
| 78 |
+
|
| 79 |
+
params = menu_gev(
|
| 80 |
+
config,
|
| 81 |
+
MODEL_NAME,
|
| 82 |
+
MODEL_PARAM,
|
| 83 |
+
SEASON,
|
| 84 |
+
show_param=show_param
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
if params is None:
|
| 88 |
+
st.info("Les paramètres d’analyse ne sont pas encore définis. Merci de les configurer pour lancer l’analyse.")
|
| 89 |
+
st.stop()
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"config": config,
|
| 93 |
+
**params
|
| 94 |
+
}
|
app/pipelines/import_data.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
from app.utils.data_utils import (
|
| 4 |
+
load_data,
|
| 5 |
+
cleaning_data_observed,
|
| 6 |
+
dont_show_extreme,
|
| 7 |
+
add_metadata,
|
| 8 |
+
get_column_load,
|
| 9 |
+
filter_nan
|
| 10 |
+
)
|
| 11 |
+
from app.utils.stats_utils import compute_statistic_per_point
|
| 12 |
+
from app.utils.gev_utils import safe_compute_return_df, compute_delta_qT, compute_delta_stat
|
| 13 |
+
from app.utils.legends_utils import get_stat_column_name
|
| 14 |
+
|
| 15 |
+
import polars as pl
|
| 16 |
+
|
| 17 |
+
def load_data_cached(use_cache: bool):
|
| 18 |
+
if use_cache:
|
| 19 |
+
return st.cache_data(load_data_inner) # Version cachée qui retourne un DataFrame pour la sérialisation.
|
| 20 |
+
else:
|
| 21 |
+
return load_data_inner
|
| 22 |
+
|
| 23 |
+
def load_data_inner(type_data: str, echelle: str, min_year: int, max_year: int, season_key: str, col_to_load: list, config) -> pl.DataFrame:
|
| 24 |
+
return load_data(type_data, echelle, min_year, max_year, season_key, col_to_load, config)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def pipeline_data(params, config, use_cache=False):
|
| 28 |
+
|
| 29 |
+
stat_choice_key, scale_choice_key, min_year_choice, max_year_choice, season_choice_key, missing_rate, quantile_choice, scale_choice = params
|
| 30 |
+
loader = load_data_cached(use_cache)
|
| 31 |
+
|
| 32 |
+
# Colonne de statistique nécessaire au chargement
|
| 33 |
+
col_to_load, col_important = get_column_load(stat_choice_key, scale_choice_key)
|
| 34 |
+
|
| 35 |
+
if scale_choice == "Journalière":
|
| 36 |
+
scale_choice = "quotidien"
|
| 37 |
+
elif scale_choice == "Horaire":
|
| 38 |
+
scale_choice = "horaire"
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
modelised_load = loader(
|
| 42 |
+
'modelised', scale_choice if scale_choice != "quotidien" else "horaire",
|
| 43 |
+
min_year_choice,
|
| 44 |
+
max_year_choice,
|
| 45 |
+
season_choice_key,
|
| 46 |
+
col_to_load,
|
| 47 |
+
config
|
| 48 |
+
)
|
| 49 |
+
except Exception as e:
|
| 50 |
+
raise RuntimeError(f"Erreur lors du chargement des données modélisées : {e}")
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
observed_load = loader(
|
| 54 |
+
'observed', scale_choice,
|
| 55 |
+
min_year_choice,
|
| 56 |
+
max_year_choice,
|
| 57 |
+
season_choice_key,
|
| 58 |
+
col_to_load + ["nan_ratio"],
|
| 59 |
+
config
|
| 60 |
+
)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
raise RuntimeError(f"Erreur lors du chargement des données observées : {e}")
|
| 63 |
+
|
| 64 |
+
# Selection des données observées
|
| 65 |
+
len_series = 0.75*(max_year_choice-min_year_choice+1)
|
| 66 |
+
df_observed_cleaning = cleaning_data_observed(observed_load, len_series, missing_rate)
|
| 67 |
+
|
| 68 |
+
# Calcul des statistiques
|
| 69 |
+
modelised = compute_statistic_per_point(modelised_load, stat_choice_key)
|
| 70 |
+
observed = compute_statistic_per_point(df_observed_cleaning, stat_choice_key)
|
| 71 |
+
|
| 72 |
+
# Ajout de l'altitude et des lat lon
|
| 73 |
+
modelised = add_metadata(modelised, scale_choice_key, type='modelised')
|
| 74 |
+
observed = add_metadata(observed, scale_choice_key, type='observed')
|
| 75 |
+
|
| 76 |
+
# Obtention de la colonne étudiée
|
| 77 |
+
column = get_stat_column_name(stat_choice_key, scale_choice_key)
|
| 78 |
+
|
| 79 |
+
# Retrait des extrêmes pour l'affichage uniquement
|
| 80 |
+
modelised_show, observed_show = dont_show_extreme(modelised, observed, column, quantile_choice, stat_choice_key)
|
| 81 |
+
|
| 82 |
+
return {
|
| 83 |
+
"modelised_load": modelised_load,
|
| 84 |
+
"observed_load": observed_load,
|
| 85 |
+
"observed_cleaning": df_observed_cleaning,
|
| 86 |
+
"modelised_show": modelised_show,
|
| 87 |
+
"observed_show": observed_show,
|
| 88 |
+
"modelised": modelised,
|
| 89 |
+
"observed": observed,
|
| 90 |
+
"column": column
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
def pipeline_data_gev(params):
|
| 94 |
+
|
| 95 |
+
column = params["param_choice"]
|
| 96 |
+
|
| 97 |
+
BOOTSTRAP = False
|
| 98 |
+
if "_bootstrap" in params['model_name']: # dans le cas des modèles avec bootstrap
|
| 99 |
+
BOOTSTRAP = True
|
| 100 |
+
# On repasse sur les fichiers non boostrapés
|
| 101 |
+
params['model_name'] = params['model_name'].replace('_bootstrap', '')
|
| 102 |
+
|
| 103 |
+
df_modelised_load = pl.read_parquet(params["mod_dir"] / f"gev_param_{params['model_name']}.parquet")
|
| 104 |
+
df_observed_load = pl.read_parquet(params["obs_dir"] / f"gev_param_{params['model_name']}.parquet")
|
| 105 |
+
|
| 106 |
+
df_modelised = filter_nan(df_modelised_load, "xi") # xi est toujours valable
|
| 107 |
+
df_observed = filter_nan(df_observed_load, "xi") # xi est toujours valable
|
| 108 |
+
|
| 109 |
+
df_modelised = add_metadata(df_modelised, "mm_h" if params["echelle"] == "horaire" else "mm_j", type="modelised")
|
| 110 |
+
df_observed = add_metadata(df_observed, "mm_h" if params["echelle"] == "horaire" else "mm_j", type="observed")
|
| 111 |
+
|
| 112 |
+
# Étape 1 : créer une colonne avec les paramètres nettoyés
|
| 113 |
+
df_modelised = safe_compute_return_df(df_modelised)
|
| 114 |
+
df_observed = safe_compute_return_df(df_observed)
|
| 115 |
+
|
| 116 |
+
# Étape 2 : appliquer delta_qT_decennale (avec numpy)
|
| 117 |
+
T_choice = params["T_choice"] # ou récupéré dynamiquement via Streamlit
|
| 118 |
+
|
| 119 |
+
if "_break_year" in params['model_name']: # dans le cas des modèles avec point de rupture
|
| 120 |
+
year_range = params["max_year_choice"] - params["config"]["years"]["rupture"] # Δa+ = a_max - a_rupture
|
| 121 |
+
else:
|
| 122 |
+
year_range = params["max_year_choice"] - params["min_year_choice"] # Δa = a_max - a_min
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if column == "Δqᵀ":
|
| 126 |
+
# Calcul du delta qT
|
| 127 |
+
df_modelised = df_modelised.with_columns([
|
| 128 |
+
pl.struct(["mu1", "sigma1", "xi"])
|
| 129 |
+
.map_elements(lambda row: compute_delta_qT(row, T_choice, year_range, params["par_X_annees"]), return_dtype=pl.Float64)
|
| 130 |
+
.alias("Δqᵀ")
|
| 131 |
+
])
|
| 132 |
+
|
| 133 |
+
df_observed = df_observed.with_columns([
|
| 134 |
+
pl.struct(["mu1", "sigma1", "xi"])
|
| 135 |
+
.map_elements(lambda row: compute_delta_qT(row, T_choice, year_range, params["par_X_annees"]), return_dtype=pl.Float64)
|
| 136 |
+
.alias("Δqᵀ")
|
| 137 |
+
])
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
elif column in ["ΔE", "ΔVar", "ΔCV"]:
|
| 141 |
+
t_start = params["min_year_choice"]
|
| 142 |
+
t_end = params["max_year_choice"]
|
| 143 |
+
t0 = params["config"]["years"]["rupture"]
|
| 144 |
+
|
| 145 |
+
df_modelised = df_modelised.with_columns([
|
| 146 |
+
pl.struct(["mu0", "mu1", "sigma0", "sigma1", "xi"])
|
| 147 |
+
.map_elements(lambda row: compute_delta_stat(row, column, t_start, t0 , t_end, params["par_X_annees"]), return_dtype=pl.Float64)
|
| 148 |
+
.alias(column)
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
df_observed = df_observed.with_columns([
|
| 152 |
+
pl.struct(["mu0", "mu1", "sigma0", "sigma1", "xi"])
|
| 153 |
+
.map_elements(lambda row: compute_delta_stat(row, column, t_start, t0, t_end, params["par_X_annees"]), return_dtype=pl.Float64)
|
| 154 |
+
.alias(column)
|
| 155 |
+
])
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if BOOTSTRAP:
|
| 160 |
+
df_mod_bootstrap = pl.read_parquet(params["mod_dir"] / f"gev_param_{params['model_name']}_bootstrap.parquet")
|
| 161 |
+
df_obs_bootstrap = pl.read_parquet(params["obs_dir"] / f"gev_param_{params['model_name']}_bootstrap.parquet")
|
| 162 |
+
|
| 163 |
+
# Recalcule delta_qT pour chaque bootstrap
|
| 164 |
+
df_mod_bootstrap = df_mod_bootstrap.with_columns([
|
| 165 |
+
pl.struct(["mu1", "sigma1", "xi"]).map_elements(
|
| 166 |
+
lambda row: compute_delta_qT(
|
| 167 |
+
row,
|
| 168 |
+
params["T_choice"],
|
| 169 |
+
year_range,
|
| 170 |
+
params["par_X_annees"]
|
| 171 |
+
),
|
| 172 |
+
return_dtype=pl.Float64
|
| 173 |
+
).alias("Δqᵀ")
|
| 174 |
+
])
|
| 175 |
+
|
| 176 |
+
df_obs_bootstrap = df_obs_bootstrap.with_columns([
|
| 177 |
+
pl.struct(["mu1", "sigma1", "xi"]).map_elements(
|
| 178 |
+
lambda row: compute_delta_qT(
|
| 179 |
+
row,
|
| 180 |
+
params["T_choice"],
|
| 181 |
+
year_range,
|
| 182 |
+
params["par_X_annees"]
|
| 183 |
+
),
|
| 184 |
+
return_dtype=pl.Float64
|
| 185 |
+
).alias("Δqᵀ")
|
| 186 |
+
])
|
| 187 |
+
|
| 188 |
+
# Calcule les bornes de l'intervalle de confiance
|
| 189 |
+
df_ic_mod = (
|
| 190 |
+
df_mod_bootstrap
|
| 191 |
+
.group_by("NUM_POSTE")
|
| 192 |
+
.agg([
|
| 193 |
+
pl.col("Δqᵀ").quantile(0.05, "nearest").alias("Δqᵀ_q050"),
|
| 194 |
+
pl.col("Δqᵀ").quantile(0.95, "nearest").alias("Δqᵀ_q950"),
|
| 195 |
+
])
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
df_ic_obs = (
|
| 199 |
+
df_obs_bootstrap
|
| 200 |
+
.group_by("NUM_POSTE")
|
| 201 |
+
.agg([
|
| 202 |
+
pl.col("Δqᵀ").quantile(0.05, "nearest").alias("Δqᵀ_q050"),
|
| 203 |
+
pl.col("Δqᵀ").quantile(0.95, "nearest").alias("Δqᵀ_q950"),
|
| 204 |
+
])
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Forcer NUM_POSTE à être de même type (int) dans les deux DataFrames
|
| 208 |
+
df_ic_mod = df_ic_mod.with_columns([pl.col("NUM_POSTE").cast(pl.Int64)])
|
| 209 |
+
df_ic_obs = df_ic_obs.with_columns([pl.col("NUM_POSTE").cast(pl.Int64)])
|
| 210 |
+
df_modelised = df_modelised.with_columns([pl.col("NUM_POSTE").cast(pl.Int64)])
|
| 211 |
+
df_observed = df_observed.with_columns([pl.col("NUM_POSTE").cast(pl.Int64)])
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# Join à df_observed
|
| 215 |
+
df_modelised = df_modelised.join(df_ic_mod, on="NUM_POSTE", how="left")
|
| 216 |
+
df_observed = df_observed.join(df_ic_obs, on="NUM_POSTE", how="left")
|
| 217 |
+
|
| 218 |
+
# Création d'une colonne est significatif ou non (ne recoupe pas l'intervalle)
|
| 219 |
+
df_modelised = df_modelised.with_columns([
|
| 220 |
+
(
|
| 221 |
+
~((pl.col("Δqᵀ_q050") <= 0) & (pl.col("Δqᵀ_q950") >= 0))
|
| 222 |
+
).alias("is_significant")
|
| 223 |
+
])
|
| 224 |
+
|
| 225 |
+
df_observed = df_observed.with_columns([
|
| 226 |
+
(
|
| 227 |
+
~((pl.col("Δqᵀ_q050") <= 0) & (pl.col("Δqᵀ_q950") >= 0))
|
| 228 |
+
).alias("is_significant")
|
| 229 |
+
])
|
| 230 |
+
|
| 231 |
+
# Retrait des percentiles
|
| 232 |
+
modelised_show = dont_show_extreme(df_modelised, column, params["quantile_choice"])
|
| 233 |
+
observed_show = dont_show_extreme(df_observed, column, params["quantile_choice"])
|
| 234 |
+
|
| 235 |
+
if column in ["Δqᵀ", "ΔE", "ΔVar", "ΔCV"]:
|
| 236 |
+
|
| 237 |
+
val_max = max(modelised_show[column].max(), observed_show[column].max())
|
| 238 |
+
val_min = min(modelised_show[column].min(), observed_show[column].min())
|
| 239 |
+
abs_max = max(abs(val_min), abs(val_max))
|
| 240 |
+
|
| 241 |
+
return {
|
| 242 |
+
"modelised_load": df_modelised_load,
|
| 243 |
+
"observed_load": df_observed_load,
|
| 244 |
+
"modelised": df_modelised,
|
| 245 |
+
"observed": df_observed,
|
| 246 |
+
"modelised_show": modelised_show,
|
| 247 |
+
"observed_show": observed_show,
|
| 248 |
+
"column": column,
|
| 249 |
+
"vmin": -abs_max,
|
| 250 |
+
"vmax": abs_max,
|
| 251 |
+
"echelle": "diverging_zero_white",
|
| 252 |
+
"continu": True
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
else:
|
| 256 |
+
|
| 257 |
+
return {
|
| 258 |
+
"modelised_load": df_modelised_load,
|
| 259 |
+
"observed_load": df_observed_load,
|
| 260 |
+
"modelised": df_modelised,
|
| 261 |
+
"observed": df_observed,
|
| 262 |
+
"modelised_show": modelised_show,
|
| 263 |
+
"observed_show": observed_show,
|
| 264 |
+
"column": column
|
| 265 |
+
}
|
app/pipelines/import_map.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.utils.config_utils import echelle_config
|
| 2 |
+
from app.utils.map_utils import create_layer, create_scatter_layer, create_tooltip
|
| 3 |
+
from app.utils.legends_utils import formalised_legend, display_vertical_color_legend
|
| 4 |
+
|
| 5 |
+
import pydeck as pdk
|
| 6 |
+
|
| 7 |
+
def safe_min(*args):
|
| 8 |
+
return min(x for x in args if x is not None) if any(x is not None for x in args) else None
|
| 9 |
+
|
| 10 |
+
def safe_max(*args):
|
| 11 |
+
return max(x for x in args if x is not None) if any(x is not None for x in args) else None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def pipeline_map(
|
| 15 |
+
params_load,
|
| 16 |
+
n_colors:int = 15,
|
| 17 |
+
param_view: dict = {"latitude": 46.9, "longitude": 1.7, "zoom": 5}
|
| 18 |
+
):
|
| 19 |
+
# Déballage des paramètres
|
| 20 |
+
stat_choice_key, result, unit_label, height = params_load
|
| 21 |
+
|
| 22 |
+
# Echelle continue ou discrète
|
| 23 |
+
if "continu" in result:
|
| 24 |
+
continu = result["continu"]
|
| 25 |
+
elif stat_choice_key == "month":
|
| 26 |
+
continu = False
|
| 27 |
+
else:
|
| 28 |
+
continu = True
|
| 29 |
+
|
| 30 |
+
# Nombre de couleurs
|
| 31 |
+
if "categories" in result: # Discret
|
| 32 |
+
categories = result["categories"]
|
| 33 |
+
n_colors = len(categories)
|
| 34 |
+
else:
|
| 35 |
+
categories = None
|
| 36 |
+
n_colors = n_colors
|
| 37 |
+
|
| 38 |
+
# Echelle paramétrée par l'utilisateur
|
| 39 |
+
if "echelle" not in result: # Choix d'une échelle personnalisée
|
| 40 |
+
result["echelle"] = None
|
| 41 |
+
|
| 42 |
+
# On trouve alors la représéntation de la légende
|
| 43 |
+
colormap = echelle_config(continu, echelle=result["echelle"], n_colors=n_colors)
|
| 44 |
+
|
| 45 |
+
result_df_modelised_show = result["modelised_show"]
|
| 46 |
+
result_df_observed_show = result["observed_show"]
|
| 47 |
+
|
| 48 |
+
# Normalisation des valeurs modélisées
|
| 49 |
+
result_df_modelised_show, vmin_mod, vmax_mod = formalised_legend(
|
| 50 |
+
result["modelised_show"],
|
| 51 |
+
column_to_show=result["column"],
|
| 52 |
+
colormap=colormap,
|
| 53 |
+
is_categorical=not continu,
|
| 54 |
+
categories=categories
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Normalisation des observations avec les mêmes bornes
|
| 58 |
+
result_df_observed_show, vmin_obs, vmax_obs = formalised_legend(
|
| 59 |
+
result["observed_show"],
|
| 60 |
+
column_to_show=result["column"],
|
| 61 |
+
colormap=colormap,
|
| 62 |
+
is_categorical=not continu,
|
| 63 |
+
categories=categories
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# Calcul des bornes communes
|
| 67 |
+
if "vmin" in result and "vmax" in result:
|
| 68 |
+
vmin_commun, vmax_commun = result["vmin"], result["vmax"]
|
| 69 |
+
else:
|
| 70 |
+
vmin_commun = safe_min(vmin_mod, vmin_obs)
|
| 71 |
+
vmax_commun = safe_max(vmax_mod, vmax_obs)
|
| 72 |
+
|
| 73 |
+
# Mise à jour de la normalisation pour les deux ensembles de données avec les bornes communes
|
| 74 |
+
result_df_modelised_show, _, _ = formalised_legend(
|
| 75 |
+
result["modelised_show"],
|
| 76 |
+
column_to_show=result["column"],
|
| 77 |
+
colormap=colormap,
|
| 78 |
+
vmin=vmin_commun,
|
| 79 |
+
vmax=vmax_commun,
|
| 80 |
+
is_categorical=not continu,
|
| 81 |
+
categories=categories
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
result_df_observed_show, _, _ = formalised_legend(
|
| 85 |
+
result["observed_show"],
|
| 86 |
+
column_to_show=result["column"],
|
| 87 |
+
colormap=colormap,
|
| 88 |
+
vmin=vmin_commun,
|
| 89 |
+
vmax=vmax_commun,
|
| 90 |
+
is_categorical=not continu,
|
| 91 |
+
categories=categories
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Création du layer modélisé et observé
|
| 95 |
+
layer = create_layer(result_df_modelised_show)
|
| 96 |
+
scatter_layer = create_scatter_layer(result_df_observed_show)
|
| 97 |
+
|
| 98 |
+
# Tooltip
|
| 99 |
+
tooltip = create_tooltip(unit_label)
|
| 100 |
+
|
| 101 |
+
# View par défaut
|
| 102 |
+
view_state = pdk.ViewState(
|
| 103 |
+
latitude=param_view["latitude"],
|
| 104 |
+
longitude=param_view["longitude"],
|
| 105 |
+
zoom=param_view["zoom"]
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Légende vertical
|
| 109 |
+
legend = display_vertical_color_legend(
|
| 110 |
+
height,
|
| 111 |
+
colormap,
|
| 112 |
+
vmin_commun,
|
| 113 |
+
vmax_commun,
|
| 114 |
+
n_ticks=n_colors,
|
| 115 |
+
label=unit_label,
|
| 116 |
+
model_labels=categories
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
return layer, scatter_layer, tooltip, view_state, legend
|
app/pipelines/import_scatter.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import polars as pl
|
| 3 |
+
|
| 4 |
+
from app.utils.hist_utils import plot_histogramme, plot_histogramme_comparatif
|
| 5 |
+
from app.utils.scatter_plot_utils import generate_scatter_plot_interactive
|
| 6 |
+
from app.utils.data_utils import match_and_compare
|
| 7 |
+
from app.utils.stats_utils import generate_metrics
|
| 8 |
+
|
| 9 |
+
def pipeline_scatter(params_load):
|
| 10 |
+
result, stat_choice_key, scale_choice_key, stat_choice, unit_label, height = params_load
|
| 11 |
+
|
| 12 |
+
df_modelised_load = result["modelised_load"]
|
| 13 |
+
df_observed_load = result["observed_load"]
|
| 14 |
+
n_tot_mod = df_modelised_load.select(pl.col("NUM_POSTE").n_unique()).item()
|
| 15 |
+
n_tot_obs = df_observed_load.select(pl.col("NUM_POSTE").n_unique()).item()
|
| 16 |
+
|
| 17 |
+
if stat_choice_key not in ["date", "month"]:
|
| 18 |
+
echelle = "horaire" if scale_choice_key == "mm_h" else "quotidien"
|
| 19 |
+
df_obs_vs_mod = pl.read_csv(f"data/metadonnees/obs_vs_mod/obs_vs_mod_{echelle}.csv")
|
| 20 |
+
obs_vs_mod = match_and_compare(result["observed"], result["modelised"], result["column"], df_obs_vs_mod)
|
| 21 |
+
if obs_vs_mod is not None and obs_vs_mod.height > 0:
|
| 22 |
+
fig = generate_scatter_plot_interactive(obs_vs_mod, stat_choice, unit_label, height)
|
| 23 |
+
me, mae, rmse, r2 = generate_metrics(obs_vs_mod)
|
| 24 |
+
|
| 25 |
+
return n_tot_mod, n_tot_obs, me, mae, rmse, r2, fig
|
| 26 |
+
|
| 27 |
+
else:
|
| 28 |
+
fig = plot_histogramme(result["modelised"], result["column"], stat_choice, stat_choice_key, unit_label, height)
|
| 29 |
+
|
| 30 |
+
return n_tot_mod, n_tot_obs, None, None, None, None, fig
|
| 31 |
+
|
| 32 |
+
else:
|
| 33 |
+
fig = plot_histogramme_comparatif(result["observed"], result["modelised"], result["column"], stat_choice, stat_choice_key, unit_label, height)
|
| 34 |
+
|
| 35 |
+
return n_tot_mod, n_tot_obs, None, None, None, None, fig
|
| 36 |
+
|
app/utils/__init__.py
ADDED
|
File without changes
|
app/utils/config_utils.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
from matplotlib import colors as mcolors
|
| 3 |
+
from matplotlib.colors import ListedColormap
|
| 4 |
+
|
| 5 |
+
def menu_config_statisticals():
|
| 6 |
+
STATS = {
|
| 7 |
+
"Moyenne": "mean",
|
| 8 |
+
"Maximum": "max",
|
| 9 |
+
"Moyenne des maxima": "mean-max",
|
| 10 |
+
"Mois comptabilisant le plus de maximas": "month",
|
| 11 |
+
"Jour de pluie": "numday",
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
SEASON = {
|
| 15 |
+
"Année hydrologique": "hydro",
|
| 16 |
+
"Hiver": "djf",
|
| 17 |
+
"Printemps": "mam",
|
| 18 |
+
"Été": "jja",
|
| 19 |
+
"Automne": "son",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
SCALE = {
|
| 23 |
+
"Horaire": "mm_h",
|
| 24 |
+
"Journalière": "mm_j"
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
return STATS, SEASON, SCALE
|
| 28 |
+
|
| 29 |
+
def menu_config_gev():
|
| 30 |
+
MODEL_PARAM = {
|
| 31 |
+
"s_gev": {"mu0": "μ₀", "sigma0": "σ₀", "xi": "ξ"},
|
| 32 |
+
"ns_gev_m1": {"mu0": "μ₀", "mu1": "μ₁", "sigma0": "σ₀", "xi": "ξ"},
|
| 33 |
+
"ns_gev_m2": {"mu0": "μ₀", "sigma0": "σ₀", "sigma1": "σ₁", "xi": "ξ"},
|
| 34 |
+
"ns_gev_m3": {"mu0": "μ₀", "mu1": "μ₁", "sigma0": "σ₀", "sigma1": "σ₁", "xi": "ξ"},
|
| 35 |
+
"ns_gev_m1_break_year": {"mu0": "μ₀", "mu1": "μ₁", "sigma0": "σ₀", "xi": "ξ"},
|
| 36 |
+
"ns_gev_m2_break_year": {"mu0": "μ₀", "sigma0": "σ₀", "sigma1": "σ₁", "xi": "ξ"},
|
| 37 |
+
"ns_gev_m3_break_year": {"mu0": "μ₀", "mu1": "μ₁", "sigma0": "σ₀", "sigma1": "σ₁", "xi": "ξ"},
|
| 38 |
+
"best_model": {"mu0": "μ₀", "mu1": "μ₁", "sigma0": "σ₀", "sigma1": "σ₁", "xi": "ξ"}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# Liste complète des modèles avec leurs équations explicites
|
| 42 |
+
MODEL_NAME = {
|
| 43 |
+
# Stationnaire
|
| 44 |
+
"M₀(μ₀, σ₀) : μ(t) = μ₀ ; σ(t) = σ₀ ; ξ(t) = ξ": "s_gev",
|
| 45 |
+
|
| 46 |
+
# Non stationnaires simples
|
| 47 |
+
"M₁(μ, σ₀) : μ(t) = μ₀ + μ₁·t ; σ(t) = σ₀ ; ξ(t) = ξ": "ns_gev_m1",
|
| 48 |
+
"M₂(μ₀, σ) : μ(t) = μ₀ ; σ(t) = σ₀ + σ₁·t ; ξ(t) = ξ": "ns_gev_m2",
|
| 49 |
+
"M₃(μ, σ) : μ(t) = μ₀ + μ₁·t ; σ(t) = σ₀ + σ₁·t ; ξ(t) = ξ": "ns_gev_m3",
|
| 50 |
+
|
| 51 |
+
# Non stationnaires avec rupture
|
| 52 |
+
"M₁⋆(μ, σ₀) : μ(t) = μ₀ + μ₁·t₊ ; σ(t) = σ₀ ; ξ(t) = ξ en notant t₊ = t · 𝟙_{t > t₀} avec t₀ = 1985": "ns_gev_m1_break_year",
|
| 53 |
+
"M₂⋆(μ₀, σ) : μ(t) = μ₀ ; σ(t) = σ₀ + σ₁·t₊ ; ξ(t) = ξ en notant t₊ = t · 𝟙_{t > t₀} avec t₀ = 1985": "ns_gev_m2_break_year",
|
| 54 |
+
"M₃⋆(μ, σ) : μ(t) = μ₀ + μ₁·t₊ ; σ(t) = σ₀ + σ₁·t₊ ; ξ(t) = ξ en notant t₊ = t · 𝟙_{t > t₀} avec t₀ = 1985": "ns_gev_m3_break_year",
|
| 55 |
+
|
| 56 |
+
"M₃⋆ᵇ(μ, σ) : μ(t) = μ₀ + μ₁·t₊ ; σ(t) = σ₀ + σ₁·t₊ ; ξ(t) = ξ en notant t₊ = t · 𝟙_{t > t₀} avec t₀ = 1985": "ns_gev_m3_break_year_bootstrap",
|
| 57 |
+
|
| 58 |
+
# Autres
|
| 59 |
+
"M(minimisant AIC)": "best_model",
|
| 60 |
+
"M(minimisant pval)": "best_model_lrt"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return MODEL_PARAM, MODEL_NAME
|
| 64 |
+
|
| 65 |
+
def reverse_param_label(param_label: str, model_name: str, model_param_map: dict) -> str:
|
| 66 |
+
"""
|
| 67 |
+
Convertit un label unicode (e.g. 'μ₀') en nom de paramètre interne (e.g. 'mu0'),
|
| 68 |
+
en utilisant le mapping inverse de model_param_map.
|
| 69 |
+
"""
|
| 70 |
+
if model_name not in model_param_map:
|
| 71 |
+
raise ValueError(f"Modèle {model_name} non trouvé dans le mapping.")
|
| 72 |
+
|
| 73 |
+
reverse_map = {v: k for k, v in model_param_map[model_name].items()}
|
| 74 |
+
|
| 75 |
+
if param_label not in reverse_map:
|
| 76 |
+
raise ValueError(f"Label {param_label} non trouvé pour le modèle {model_name}.")
|
| 77 |
+
|
| 78 |
+
return reverse_map[param_label]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def load_config(config_path: str) -> dict:
|
| 83 |
+
with open(config_path, "r") as f:
|
| 84 |
+
return yaml.safe_load(f)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def echelle_config(type_: bool, echelle: str = None, n_colors: int = 256):
|
| 88 |
+
if type_: # Continu
|
| 89 |
+
|
| 90 |
+
if echelle == "diverging_zero_white": # Choix personnalisé
|
| 91 |
+
# Dégradé négatif (bleu) → 0 (blanc) → positif (jaune à rouge)
|
| 92 |
+
custom_colorscale = [
|
| 93 |
+
(0.0, "#08306B"), # bleu foncé
|
| 94 |
+
(0.1, "#2171B5"),
|
| 95 |
+
(0.2, "#6BAED6"),
|
| 96 |
+
(0.3, "#C6DBEF"),
|
| 97 |
+
(0.49, "#ffffff"), # blanc à 0
|
| 98 |
+
(0.5, "#ffffff"),
|
| 99 |
+
(0.6, "#ffffb2"), # jaune clair
|
| 100 |
+
(0.7, "#fecc5c"),
|
| 101 |
+
(0.8, "#fd8d3c"),
|
| 102 |
+
(0.9, "#f03b20"),
|
| 103 |
+
(1.0, "#bd0026"), # rouge foncé
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
cmap = mcolors.LinearSegmentedColormap.from_list("diverging_zero_white", custom_colorscale)
|
| 107 |
+
|
| 108 |
+
if n_colors is not None:
|
| 109 |
+
# Retourne une version discrète avec n couleurs
|
| 110 |
+
return ListedColormap([cmap(i / (n_colors - 1)) for i in range(n_colors)])
|
| 111 |
+
else:
|
| 112 |
+
return cmap
|
| 113 |
+
|
| 114 |
+
custom_colorscale = [
|
| 115 |
+
(0.0, "#FFFFE5"), # blanc
|
| 116 |
+
(0.1, "#DDEED6"),
|
| 117 |
+
(0.2, "#BCDDC8"),
|
| 118 |
+
(0.3, "#9BCCBA"),
|
| 119 |
+
(0.4, "#7ABBAC"),
|
| 120 |
+
(0.5, "#59AA9E"),
|
| 121 |
+
(0.6, "#389990"),
|
| 122 |
+
(0.7, "#29837A"),
|
| 123 |
+
(0.8, "#1C6D63"),
|
| 124 |
+
(0.9, "#0F564B"),
|
| 125 |
+
(1.0, "#003C30"),
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
cmap = mcolors.LinearSegmentedColormap.from_list("custom", custom_colorscale)
|
| 129 |
+
|
| 130 |
+
if n_colors is not None:
|
| 131 |
+
# Retourne une version discrète avec n couleurs
|
| 132 |
+
return ListedColormap([cmap(i / (n_colors - 1)) for i in range(n_colors)])
|
| 133 |
+
else:
|
| 134 |
+
return cmap
|
| 135 |
+
|
| 136 |
+
else: # Discret
|
| 137 |
+
|
| 138 |
+
couleurs_par_mois = [
|
| 139 |
+
"#ffffff", # Janvier
|
| 140 |
+
"blue", # Février
|
| 141 |
+
"green", # Mars
|
| 142 |
+
"red", # Avril
|
| 143 |
+
"orange", # Mai
|
| 144 |
+
"#00CED1", # Juin
|
| 145 |
+
"yellow", # Juillet
|
| 146 |
+
"#f781bf", # Août
|
| 147 |
+
"purple", # Septembre
|
| 148 |
+
"#654321", # Octobre
|
| 149 |
+
"darkblue", # Novembre
|
| 150 |
+
"black", # Décembre
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
return ListedColormap(couleurs_par_mois)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def get_readable_season(season_code: str) -> str:
|
| 157 |
+
"""
|
| 158 |
+
Retourne le nom humainement lisible d'une saison à partir de son code ("hydro", "djf", etc.).
|
| 159 |
+
Résultat en minuscules.
|
| 160 |
+
"""
|
| 161 |
+
_, SEASON, _ = menu_config_statisticals()
|
| 162 |
+
reverse_season = {v: k.lower() for k, v in SEASON.items()}
|
| 163 |
+
if season_code not in reverse_season:
|
| 164 |
+
raise ValueError(f"Code saison inconnu : {season_code}")
|
| 165 |
+
return reverse_season[season_code]
|
app/utils/data_utils.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import polars as pl
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from scipy.spatial import cKDTree
|
| 5 |
+
|
| 6 |
+
from app.utils.config_utils import menu_config_statisticals
|
| 7 |
+
|
| 8 |
+
def get_column_load(stat: str, scale: str):
|
| 9 |
+
if stat == "mean":
|
| 10 |
+
col = "mean_mm_h"
|
| 11 |
+
elif stat == "max":
|
| 12 |
+
col = f"max_{scale}"
|
| 13 |
+
elif stat == "mean-max":
|
| 14 |
+
col = f"max_{scale}"
|
| 15 |
+
elif stat == "month":
|
| 16 |
+
col = f"max_date_{scale}"
|
| 17 |
+
elif stat == "numday":
|
| 18 |
+
col = "n_days_gt1mm"
|
| 19 |
+
else:
|
| 20 |
+
raise ValueError(f"Stat '{stat}' is not recognized")
|
| 21 |
+
|
| 22 |
+
return ["NUM_POSTE", col], col
|
| 23 |
+
|
| 24 |
+
def load_season(year: int, season_key: str, base_path: str, col_to_load: str) -> pl.DataFrame:
|
| 25 |
+
filename = f"{base_path}/{year:04d}/{season_key}.parquet"
|
| 26 |
+
return pl.read_parquet(filename, columns=col_to_load)
|
| 27 |
+
|
| 28 |
+
def load_data(type_data: str, echelle: str, min_year: int, max_year: int, season: str, col_to_load: str, config) -> pl.DataFrame:
|
| 29 |
+
_, SEASON, _ = menu_config_statisticals()
|
| 30 |
+
if season not in SEASON.values():
|
| 31 |
+
raise ValueError(f"Saison inconnue : {season}")
|
| 32 |
+
|
| 33 |
+
base_path = f'{config["statisticals"][type_data]}/{echelle}'
|
| 34 |
+
|
| 35 |
+
dataframes = []
|
| 36 |
+
errors = []
|
| 37 |
+
|
| 38 |
+
for year in range(min_year, max_year + 1):
|
| 39 |
+
try:
|
| 40 |
+
df = load_season(year, season, base_path, col_to_load)
|
| 41 |
+
# Conversion explicite des colonnes dates uniquement si elles existent
|
| 42 |
+
for col in ["max_date_mm_h", "max_date_mm_j"]:
|
| 43 |
+
if col in df.columns:
|
| 44 |
+
df = df.with_columns(
|
| 45 |
+
pl.col(col)
|
| 46 |
+
.cast(pl.Utf8) # s'assure qu'on peut parser avec str.strptime
|
| 47 |
+
.str.strptime(pl.Datetime, format="%Y-%m-%d", strict=False)
|
| 48 |
+
.cast(pl.Utf8) # retour sous forme de string (comme dans l'ancien code Pandas)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Ajout de la colonne year
|
| 52 |
+
df = df.with_columns(pl.lit(year).alias("year"))
|
| 53 |
+
|
| 54 |
+
dataframes.append(df)
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
errors.append(f"{year} ({season}) : {e}")
|
| 58 |
+
|
| 59 |
+
if errors:
|
| 60 |
+
for err in errors:
|
| 61 |
+
st.warning(f"Erreur : {err}")
|
| 62 |
+
|
| 63 |
+
if not dataframes:
|
| 64 |
+
raise ValueError("Aucune donnée chargée.")
|
| 65 |
+
|
| 66 |
+
return pl.concat(dataframes, how="vertical")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def cleaning_data_observed(
|
| 70 |
+
df: pl.DataFrame,
|
| 71 |
+
len_serie: float = None,
|
| 72 |
+
nan_limit: float = 0.10
|
| 73 |
+
) -> pl.DataFrame:
|
| 74 |
+
"""
|
| 75 |
+
Filtre les maxima par deux critères :
|
| 76 |
+
1) on annule les valeurs d’une année si nan_ratio > nan_limit
|
| 77 |
+
2) on ne garde que les stations ayant au moins n années valides
|
| 78 |
+
"""
|
| 79 |
+
# ——— règles dépendant de l’échelle ———
|
| 80 |
+
if len_serie is None:
|
| 81 |
+
raise ValueError('Paramètre len_serie à préciser')
|
| 82 |
+
|
| 83 |
+
# Selection des saisons avec nan_limit au maximum
|
| 84 |
+
df_filter = df.filter(pl.col("nan_ratio") <= nan_limit)
|
| 85 |
+
|
| 86 |
+
# Calcul du nombre d'années valides par station NUM_POSTE
|
| 87 |
+
station_counts = (
|
| 88 |
+
df_filter.group_by("NUM_POSTE")
|
| 89 |
+
.agg(pl.col("year").n_unique().alias("num_years"))
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Sélection des NUM_POSTE avec au moins len_serie d'années valides
|
| 93 |
+
valid_stations = station_counts.filter(pl.col("num_years") >= len_serie)
|
| 94 |
+
|
| 95 |
+
# Jointure pour ne garder que les stations valides
|
| 96 |
+
df_final = df_filter.filter(
|
| 97 |
+
pl.col("NUM_POSTE").is_in(valid_stations["NUM_POSTE"])
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return df_final
|
| 101 |
+
|
| 102 |
+
def dont_show_extreme(
|
| 103 |
+
modelised: pl.DataFrame,
|
| 104 |
+
observed: pl.DataFrame,
|
| 105 |
+
column: str,
|
| 106 |
+
quantile_choice: float,
|
| 107 |
+
stat_choice_key: str = None
|
| 108 |
+
) -> tuple[pl.DataFrame, pl.DataFrame]:
|
| 109 |
+
|
| 110 |
+
if stat_choice_key not in ("month", "date"):
|
| 111 |
+
# 1) Calcul des quantiles
|
| 112 |
+
q_mod = modelised.select(
|
| 113 |
+
pl.col(column).quantile(quantile_choice, interpolation="nearest")
|
| 114 |
+
).item()
|
| 115 |
+
|
| 116 |
+
if observed is None or observed.height == 0:
|
| 117 |
+
seuil = q_mod
|
| 118 |
+
else:
|
| 119 |
+
q_obs = observed.select(
|
| 120 |
+
pl.col(column).quantile(quantile_choice, interpolation="nearest")
|
| 121 |
+
).item()
|
| 122 |
+
seuil = max(q_mod, q_obs)
|
| 123 |
+
|
| 124 |
+
# 2) Saturation des couleurs
|
| 125 |
+
clamp_expr = (
|
| 126 |
+
pl.when(pl.col(column).abs() > seuil)
|
| 127 |
+
.then(pl.lit(seuil) * pl.col(column).sign())
|
| 128 |
+
.otherwise(pl.col(column))
|
| 129 |
+
.alias(column)
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# 3) Renvoi des tableaux
|
| 133 |
+
modelised_show = modelised.with_columns(clamp_expr)
|
| 134 |
+
observed_show = observed.with_columns(clamp_expr)
|
| 135 |
+
|
| 136 |
+
else:
|
| 137 |
+
modelised_show, observed_show = modelised, observed
|
| 138 |
+
|
| 139 |
+
return modelised_show, observed_show
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def add_metadata(df: pl.DataFrame, scale: str, type: str) -> pl.DataFrame:
|
| 143 |
+
echelle = 'horaire' if scale == 'mm_h' else 'quotidien'
|
| 144 |
+
|
| 145 |
+
# Charger les metadonnées avec Polars
|
| 146 |
+
df_meta = pl.read_csv(f"data/metadonnees/{type}/postes_{echelle}.csv")
|
| 147 |
+
# Harmoniser les types des colonnes lat/lon des deux c��tés
|
| 148 |
+
df_meta = df_meta.with_columns([
|
| 149 |
+
pl.col("NUM_POSTE").cast(pl.Int32),
|
| 150 |
+
pl.col("lat").cast(pl.Float32),
|
| 151 |
+
pl.col("lon").cast(pl.Float32),
|
| 152 |
+
pl.col("altitude").cast(pl.Int32) # altitude en entier
|
| 153 |
+
])
|
| 154 |
+
|
| 155 |
+
df = df.with_columns([ # forcer ici aussi
|
| 156 |
+
pl.col("NUM_POSTE").cast(pl.Int32)
|
| 157 |
+
])
|
| 158 |
+
|
| 159 |
+
# Join sur NUM_POSTE
|
| 160 |
+
return df.join(df_meta, on=["NUM_POSTE"], how="left")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def find_matching_point(df_model: pl.DataFrame, lat_obs: float, lon_obs: float):
|
| 164 |
+
df_model = df_model.with_columns([
|
| 165 |
+
((pl.col("lat") - lat_obs) ** 2 + (pl.col("lon") - lon_obs) ** 2).sqrt().alias("dist")
|
| 166 |
+
])
|
| 167 |
+
closest_row = df_model.filter(pl.col("dist") == pl.col("dist").min()).select(["lat", "lon"]).row(0)
|
| 168 |
+
return closest_row # (lat, lon)
|
| 169 |
+
|
| 170 |
+
def match_and_compare(
|
| 171 |
+
obs_df: pl.DataFrame,
|
| 172 |
+
mod_df: pl.DataFrame,
|
| 173 |
+
column_to_show: str,
|
| 174 |
+
obs_vs_mod: pl.DataFrame = None
|
| 175 |
+
) -> pl.DataFrame:
|
| 176 |
+
|
| 177 |
+
if obs_vs_mod is None:
|
| 178 |
+
raise ValueError("obs_vs_mod must be provided with NUM_POSTE_obs and NUM_POSTE_mod columns")
|
| 179 |
+
|
| 180 |
+
obs_vs_mod = obs_vs_mod.with_columns(
|
| 181 |
+
pl.col("NUM_POSTE_obs").cast(pl.Int32)
|
| 182 |
+
).filter(
|
| 183 |
+
pl.col("NUM_POSTE_obs").is_in(obs_df["NUM_POSTE"].cast(pl.Int32))
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Renommer temporairement pour le join
|
| 187 |
+
obs = obs_df.rename({"NUM_POSTE": "NUM_POSTE_obs"})
|
| 188 |
+
mod = mod_df.rename({"NUM_POSTE": "NUM_POSTE_mod"})
|
| 189 |
+
|
| 190 |
+
obs = obs_df.with_columns(
|
| 191 |
+
pl.col("NUM_POSTE").cast(pl.Int32)
|
| 192 |
+
).rename({"NUM_POSTE": "NUM_POSTE_obs"})
|
| 193 |
+
|
| 194 |
+
mod = mod_df.with_columns(
|
| 195 |
+
pl.col("NUM_POSTE").cast(pl.Int32)
|
| 196 |
+
).rename({"NUM_POSTE": "NUM_POSTE_mod"})
|
| 197 |
+
|
| 198 |
+
obs_vs_mod = obs_vs_mod.with_columns(
|
| 199 |
+
pl.col("NUM_POSTE_obs").cast(pl.Int32),
|
| 200 |
+
pl.col("NUM_POSTE_mod").cast(pl.Int32)
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# Ajoute les valeurs observées et simulées en fonction des correspondances
|
| 204 |
+
matched = (
|
| 205 |
+
obs_vs_mod
|
| 206 |
+
.join(obs.select(["NUM_POSTE_obs", "lat", "lon", column_to_show]), on="NUM_POSTE_obs", how="left")
|
| 207 |
+
.join(mod.select(["NUM_POSTE_mod", column_to_show]), on="NUM_POSTE_mod", how="left", suffix="_mod")
|
| 208 |
+
.rename({column_to_show: "Station", f"{column_to_show}_mod": "AROME"})
|
| 209 |
+
)
|
| 210 |
+
matched = matched.select(["NUM_POSTE_obs", "lat", "lon", "NUM_POSTE_mod", "Station", "AROME"]).drop_nulls()
|
| 211 |
+
|
| 212 |
+
return matched
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def standardize_year(year: float, min_year: int, max_year: int) -> float:
|
| 216 |
+
"""
|
| 217 |
+
Normalise une année `year` entre 0 et 1 avec une transformation min-max.
|
| 218 |
+
"""
|
| 219 |
+
return (year - min_year) / (max_year - min_year)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def filter_nan(df: pl.DataFrame, columns: list[str]):
|
| 223 |
+
return df.drop_nulls(subset=columns)
|
app/utils/gev_utils.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import polars as pl
|
| 3 |
+
from scipy.special import gamma
|
| 4 |
+
|
| 5 |
+
# --- Quantile GEV ---
|
| 6 |
+
# Soit :
|
| 7 |
+
# μ(t) = μ₀ + μ₁ × t # localisation dépendante du temps
|
| 8 |
+
# σ(t) = σ₀ + σ₁ × t # échelle dépendante du temps
|
| 9 |
+
# ξ = constante # forme
|
| 10 |
+
# T = période de retour (années)
|
| 11 |
+
# p = 1 − 1 / T # probabilité non-excédée associée
|
| 12 |
+
# Avec t : année (ou covariable normalisée dans l'intervalle [0; 1]
|
| 13 |
+
# t = (annee - min_year) / (max_year - min_year) = (annee - min_year) / delta_year
|
| 14 |
+
# Une unité de t (normalisée) = Δa années (max_year - min_year)
|
| 15 |
+
|
| 16 |
+
# En notant Δa = max_year - min_year et a = annee, on a :
|
| 17 |
+
# t = (a − aₘᵢₙ) / Δa ⇒ a = aₘᵢₙ + t ⋅ Δa
|
| 18 |
+
|
| 19 |
+
# La quantile notée qᵀ(t) (précipitation pour une période de retour T à l’année t) s’écrit :
|
| 20 |
+
# qᵀ(t) = μ(t) + [σ(t) / ξ] × [ (−log(1 − p))^(−ξ) − 1 ]
|
| 21 |
+
# qᵀ(t) = (μ₀ + μ₁ × t) + [(σ₀ + σ₁ × t) / ξ] × [ (−log(1 − (1/T)))^(−ξ) − 1 ]
|
| 22 |
+
|
| 23 |
+
# Soit : z_T = [ -log(1 - 1/T) ]^(−ξ) − 1 ← constante pour un T donné
|
| 24 |
+
# Donc : qᵀ(t) = μ₀ + μ₁·t + [(σ₀ + σ₁·t) / ξ] · z_T
|
| 25 |
+
# Ou : qᵀ(t) = μ(t) + [σ(t) / ξ] · z_T
|
| 26 |
+
|
| 27 |
+
# En dérivant qᵀ par rapport à t on a :
|
| 28 |
+
# dqᵀ/dt = μ₁ + σ₁ / ξ · z_T
|
| 29 |
+
# On rappelle : a = aₘᵢₙ + t ⋅ Δa
|
| 30 |
+
# Donc : dt/da = 1 / Δa
|
| 31 |
+
|
| 32 |
+
# Alors dqᵀ/da = dqᵀ/dt · dt/da = μ₁ + σ₁ / ξ · z_T · 1 / Δa
|
| 33 |
+
|
| 34 |
+
# LA VARIATION PAR AN de qᵀ :
|
| 35 |
+
# dqᵀ/da = 1 / Δa · (μ₁ + σ₁ / ξ · z_T)
|
| 36 |
+
# DONC PAR 10 ANS :
|
| 37 |
+
# Δqᵀ₁₀ₐₙₛ = (10 / Δa) ⋅ (μ₁ + (σ₁ / ξ) ⋅ zᵀ)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def safe_compute_return_df(df: pl.DataFrame) -> pl.DataFrame:
|
| 42 |
+
REQUIRED_GEV_COLS = ["mu0", "mu1", "sigma0", "sigma1", "xi"]
|
| 43 |
+
for col in REQUIRED_GEV_COLS:
|
| 44 |
+
if col not in df.columns:
|
| 45 |
+
df = df.with_columns(pl.lit(0.0).alias(col))
|
| 46 |
+
df = df.with_columns([
|
| 47 |
+
pl.col(col).fill_null(0.0).fill_nan(0.0) for col in REQUIRED_GEV_COLS
|
| 48 |
+
])
|
| 49 |
+
return df
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def compute_return_levels_ns(params: dict, T: np.ndarray, t_norm: float) -> np.ndarray:
|
| 53 |
+
"""
|
| 54 |
+
Calcule les niveaux de retour selon le modèle NS-GEV fourni.
|
| 55 |
+
- params : dictionnaire des paramètres GEV d'un point
|
| 56 |
+
- T : périodes de retour (en années)
|
| 57 |
+
- t_norm : covariable temporelle normalisée (ex : 0 pour année moyenne)
|
| 58 |
+
"""
|
| 59 |
+
mu = params.get("mu0", 0) + params["mu1"] * t_norm if "mu1" in params else params.get("mu0", 0) # μ(t)
|
| 60 |
+
sigma = params.get("sigma0", 0) + params["sigma1"] * t_norm if "sigma1" in params else params.get("sigma0", 0) # σ(t)
|
| 61 |
+
xi = params.get("xi", 0) # xi contant
|
| 62 |
+
|
| 63 |
+
if xi != 0:
|
| 64 |
+
qT = mu + (sigma / xi) * ((-np.log(1 - 1 / T))**(-xi) - 1)
|
| 65 |
+
else:
|
| 66 |
+
qT = mu - sigma * np.log(-np.log(1 - 1/T))
|
| 67 |
+
|
| 68 |
+
return qT
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def delta_qT_X_years(mu1, sigma1, xi, T, year_range, par_X_annees):
|
| 72 |
+
"""
|
| 73 |
+
Calcule la variation décennale du quantile de retour qᵀ(t)
|
| 74 |
+
dans un modèle GEV non stationnaire avec t ∈ [0, 1].
|
| 75 |
+
|
| 76 |
+
La variation est ramenée à l’échelle des années civiles en tenant compte de la
|
| 77 |
+
durée totale du modèle (year_range = a_max - a_min).
|
| 78 |
+
Si un point de rupture est introduit year_range = a_max - a_rupture,
|
| 79 |
+
avec une Δqᵀ = 0 avant la rupture.
|
| 80 |
+
|
| 81 |
+
Δqᵀ = (par_X_annees / year_range) × (μ₁ + (σ₁ / ξ) × z_T)
|
| 82 |
+
avec :
|
| 83 |
+
- z_T = [ -log(1 - 1/T) ]^(-ξ) - 1 si ξ ≠ 0
|
| 84 |
+
= log(-log(1 - 1/T)) si ξ = 0 (Gumbel)
|
| 85 |
+
|
| 86 |
+
par_X_annees représente 10, 20, 30 ans dans Δ_10ans qᵀ
|
| 87 |
+
"""
|
| 88 |
+
try:
|
| 89 |
+
p = 1 - 1 / T
|
| 90 |
+
if xi == 0:
|
| 91 |
+
z_T = np.log(-np.log(p))
|
| 92 |
+
delta_q = (par_X_annees / year_range) * (mu1 + sigma1 * z_T)
|
| 93 |
+
else:
|
| 94 |
+
z_T = (-np.log(p))**(-xi) - 1
|
| 95 |
+
delta_q = (par_X_annees / year_range) * (mu1 + (sigma1 / xi) * z_T)
|
| 96 |
+
return float(delta_q)
|
| 97 |
+
except Exception:
|
| 98 |
+
return np.nan
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def compute_delta_qT(row, T_choice, year_range, par_X_annees):
|
| 102 |
+
return delta_qT_X_years(
|
| 103 |
+
row["mu1"],
|
| 104 |
+
row["sigma1"],
|
| 105 |
+
row["xi"],
|
| 106 |
+
T=T_choice,
|
| 107 |
+
year_range=year_range,
|
| 108 |
+
par_X_annees=par_X_annees
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# --- Espérence, variance, CV de GEV ---
|
| 113 |
+
|
| 114 |
+
def gev_moments(mu, sigma, xi):
|
| 115 |
+
if xi >= 0.5:
|
| 116 |
+
return np.nan, np.nan, np.nan # variance indéfinie
|
| 117 |
+
try:
|
| 118 |
+
mean = mu + sigma / xi * (gamma(1 - xi) - 1)
|
| 119 |
+
var = (sigma ** 2) / (xi ** 2) * (gamma(1 - 2 * xi) - gamma(1 - xi) ** 2)
|
| 120 |
+
cv = np.sqrt(var) / mean if mean != 0 else np.nan
|
| 121 |
+
return mean, var, cv
|
| 122 |
+
except Exception:
|
| 123 |
+
return np.nan, np.nan, np.nan
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def eval_params_nsgev(mu0, mu1, sigma0, sigma1, xi, t, t0):
|
| 127 |
+
mu_t = mu0 + mu1 * (t - t0)
|
| 128 |
+
sigma_t = sigma0 + sigma1 * (t - t0)
|
| 129 |
+
return gev_moments(mu_t, sigma_t, xi)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def compute_delta_stat(row, stat: str, year_start: int, year_ref: int, year_end: int, par_X_annees: int) -> float:
|
| 133 |
+
"""
|
| 134 |
+
Calcule la variation du moment statistique GEV (moyenne, variance, CV)
|
| 135 |
+
exprimée en changement moyen par 10 ans.
|
| 136 |
+
|
| 137 |
+
Parameters:
|
| 138 |
+
- row : dictionnaire contenant les paramètres GEV
|
| 139 |
+
- stat : "ΔE", "ΔVar" ou "ΔCV"
|
| 140 |
+
- year_start, year_end : années de début et de fin
|
| 141 |
+
- year_ref : année de référence (t0)
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
- Variation du moment sélectionné rapportée à 10 ans
|
| 145 |
+
"""
|
| 146 |
+
Δa = year_end - year_start
|
| 147 |
+
if Δa == 0:
|
| 148 |
+
return np.nan # évite division par zéro
|
| 149 |
+
|
| 150 |
+
# Moments aux deux dates
|
| 151 |
+
mean_start, var_start, cv_start = eval_params_nsgev(
|
| 152 |
+
mu0=row["mu0"], mu1=row.get("mu1", 0.0),
|
| 153 |
+
sigma0=row["sigma0"], sigma1=row.get("sigma1", 0.0),
|
| 154 |
+
xi=row["xi"], t=year_start, t0=year_ref
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
mean_end, var_end, cv_end = eval_params_nsgev(
|
| 158 |
+
mu0=row["mu0"], mu1=row.get("mu1", 0.0),
|
| 159 |
+
sigma0=row["sigma0"], sigma1=row.get("sigma1", 0.0),
|
| 160 |
+
xi=row["xi"], t=year_end, t0=year_ref
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
if stat == "ΔE":
|
| 164 |
+
return (mean_end - mean_start) * par_X_annees / Δa
|
| 165 |
+
elif stat == "ΔVar":
|
| 166 |
+
return (var_end - var_start) * par_X_annees / Δa
|
| 167 |
+
elif stat == "ΔCV":
|
| 168 |
+
return (cv_end - cv_start) * par_X_annees / Δa
|
| 169 |
+
else:
|
| 170 |
+
return np.nan
|
| 171 |
+
|
app/utils/hist_utils.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import plotly.express as px
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import polars as pl
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
def plot_histogramme(df: pl.DataFrame, var, stat, stat_key, unit, height):
|
| 7 |
+
df = df.to_pandas()
|
| 8 |
+
# Définir l’ordre complet des mois
|
| 9 |
+
month_order = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
| 10 |
+
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
|
| 11 |
+
|
| 12 |
+
if stat_key == 'month':
|
| 13 |
+
# Copie de sécurité
|
| 14 |
+
df = df.copy()
|
| 15 |
+
|
| 16 |
+
# Convertir le numéro du mois (1-12) en label texte
|
| 17 |
+
df[var] = df[var].astype(int)
|
| 18 |
+
df[var] = df[var].map({i+1: month_order[i] for i in range(12)})
|
| 19 |
+
|
| 20 |
+
# Calculer la répartition (pourcentage) par mois
|
| 21 |
+
counts = df[var].value_counts() # nb de lignes par mois présent
|
| 22 |
+
counts = counts.reindex(month_order, fill_value=0) # forcer l’existence de tous les mois, avec 0 pour les absents
|
| 23 |
+
|
| 24 |
+
# Convertir en pourcentage
|
| 25 |
+
total = counts.sum()
|
| 26 |
+
freq_percent = (counts / total * 100) if total > 0 else counts
|
| 27 |
+
|
| 28 |
+
# Construire un nouveau DF pour Plotly
|
| 29 |
+
hist_df = pd.DataFrame({var: freq_percent.index, 'Pourcentage': freq_percent.values})
|
| 30 |
+
|
| 31 |
+
# Plot en barres
|
| 32 |
+
fig = px.bar(
|
| 33 |
+
hist_df,
|
| 34 |
+
x=var,
|
| 35 |
+
y='Pourcentage'
|
| 36 |
+
)
|
| 37 |
+
fig.update_layout(
|
| 38 |
+
bargap=0.1, # Espacement entre barres
|
| 39 |
+
xaxis_title="", # Pas de titre horizontal
|
| 40 |
+
yaxis_title="Pourcentage de stations",
|
| 41 |
+
height=height,
|
| 42 |
+
xaxis=dict(
|
| 43 |
+
categoryorder='array',
|
| 44 |
+
categoryarray=month_order
|
| 45 |
+
)
|
| 46 |
+
)
|
| 47 |
+
else:
|
| 48 |
+
# Cas normal : on garde px.histogram
|
| 49 |
+
fig = px.histogram(
|
| 50 |
+
df,
|
| 51 |
+
x=var,
|
| 52 |
+
nbins=50,
|
| 53 |
+
histnorm='percent'
|
| 54 |
+
)
|
| 55 |
+
fig.update_layout(
|
| 56 |
+
bargap=0.1,
|
| 57 |
+
xaxis_title=f"{stat} ({unit})" if unit else f"{stat}",
|
| 58 |
+
yaxis_title="Pourcentage de stations",
|
| 59 |
+
height=height
|
| 60 |
+
)
|
| 61 |
+
return fig
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def plot_histogramme_comparatif(df_observed: pl.DataFrame, df_modelised: pl.DataFrame, var, stat, stat_key, unit, height):
|
| 65 |
+
df_observed = df_observed.to_pandas()
|
| 66 |
+
df_modelised = df_modelised.to_pandas()
|
| 67 |
+
month_order = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
| 68 |
+
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
|
| 69 |
+
|
| 70 |
+
if stat_key == 'month':
|
| 71 |
+
def prepare_df(df, label):
|
| 72 |
+
df = df.copy()
|
| 73 |
+
df[var] = df[var].astype(int)
|
| 74 |
+
df[var] = df[var].map({i + 1: month_order[i] for i in range(12)})
|
| 75 |
+
counts = df[var].value_counts()
|
| 76 |
+
counts = counts.reindex(month_order, fill_value=0)
|
| 77 |
+
total = counts.sum()
|
| 78 |
+
freq_percent = (counts / total * 100) if total > 0 else counts
|
| 79 |
+
return pd.DataFrame({
|
| 80 |
+
var: freq_percent.index,
|
| 81 |
+
'Pourcentage': freq_percent.values,
|
| 82 |
+
'Source': label
|
| 83 |
+
})
|
| 84 |
+
|
| 85 |
+
df_obs = prepare_df(df_observed, "Observé")
|
| 86 |
+
df_mod = prepare_df(df_modelised, "Modélisé")
|
| 87 |
+
hist_df = pd.concat([df_obs, df_mod], ignore_index=True)
|
| 88 |
+
|
| 89 |
+
fig = px.bar(
|
| 90 |
+
hist_df,
|
| 91 |
+
x=var,
|
| 92 |
+
y='Pourcentage',
|
| 93 |
+
color='Source',
|
| 94 |
+
barmode='group' # Affichage côte à côte
|
| 95 |
+
)
|
| 96 |
+
fig.update_layout(
|
| 97 |
+
bargap=0.15,
|
| 98 |
+
xaxis_title="",
|
| 99 |
+
yaxis_title="Pourcentage de stations",
|
| 100 |
+
height=height,
|
| 101 |
+
xaxis=dict(
|
| 102 |
+
categoryorder='array',
|
| 103 |
+
categoryarray=month_order
|
| 104 |
+
)
|
| 105 |
+
)
|
| 106 |
+
else:
|
| 107 |
+
# Affichage standard pour les autres stats
|
| 108 |
+
df_observed['Source'] = "Observé"
|
| 109 |
+
df_modelised['Source'] = "Modélisé"
|
| 110 |
+
df_all = pd.concat([df_observed, df_modelised], ignore_index=True)
|
| 111 |
+
|
| 112 |
+
fig = px.histogram(
|
| 113 |
+
df_all,
|
| 114 |
+
x=var,
|
| 115 |
+
color='Source',
|
| 116 |
+
nbins=50,
|
| 117 |
+
histnorm='percent',
|
| 118 |
+
barmode='overlay' # ou 'group' si tu veux les voir côte à côte
|
| 119 |
+
)
|
| 120 |
+
fig.update_layout(
|
| 121 |
+
bargap=0.1,
|
| 122 |
+
xaxis_title=f"{stat} ({unit})" if unit else f"{stat}",
|
| 123 |
+
yaxis_title="Pourcentage de stations",
|
| 124 |
+
height=height
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
return fig
|
app/utils/legends_utils.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from io import BytesIO
|
| 2 |
+
import base64
|
| 3 |
+
import polars as pl
|
| 4 |
+
import numpy as np
|
| 5 |
+
import datetime as dt
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
|
| 8 |
+
def get_stat_column_name(stat_key: str, scale_key: str) -> str:
|
| 9 |
+
if stat_key == "mean":
|
| 10 |
+
return f"mean_all_{scale_key}"
|
| 11 |
+
elif stat_key == "max":
|
| 12 |
+
return f"max_all_{scale_key}"
|
| 13 |
+
elif stat_key == "mean-max":
|
| 14 |
+
return f"max_mean_{scale_key}"
|
| 15 |
+
elif stat_key == "date":
|
| 16 |
+
return "date_max_h" if scale_key == "mm_h" else "date_max_j"
|
| 17 |
+
elif stat_key == "month":
|
| 18 |
+
return "mois_pluvieux_h" if scale_key == "mm_h" else "mois_pluvieux_j"
|
| 19 |
+
elif stat_key == "numday":
|
| 20 |
+
return "jours_pluie_moyen"
|
| 21 |
+
else:
|
| 22 |
+
raise ValueError(f"Statistique inconnue : {stat_key}")
|
| 23 |
+
|
| 24 |
+
def get_stat_unit(stat_key: str, scale_key: str) -> str:
|
| 25 |
+
if stat_key in ["mean", "max", "mean-max"]:
|
| 26 |
+
return "mm/h" if scale_key == "mm_h" else "mm/j"
|
| 27 |
+
elif stat_key == "sum":
|
| 28 |
+
return "mm"
|
| 29 |
+
elif stat_key == "numday":
|
| 30 |
+
return "jours"
|
| 31 |
+
else:
|
| 32 |
+
return ""
|
| 33 |
+
|
| 34 |
+
def formalised_legend(df: pl.DataFrame, column_to_show: str, colormap, vmin=None, vmax=None, is_categorical=False, categories=None):
|
| 35 |
+
df = df.clone()
|
| 36 |
+
|
| 37 |
+
if is_categorical and categories is not None:
|
| 38 |
+
# Cas spécial catégoriel : ex : best_model
|
| 39 |
+
mapping = {cat: i for i, cat in enumerate(categories)} # s_gev=0, ns_gev_m1=1, etc.
|
| 40 |
+
|
| 41 |
+
df = df.with_columns([
|
| 42 |
+
pl.col(column_to_show).map_elements(lambda x: mapping.get(x, None), return_dtype=pl.Float64).alias("value_norm")
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
vals = df["value_norm"].to_numpy()
|
| 46 |
+
colors = (255 * np.array(colormap(vals / (len(categories) - 1)))[:, :3]).astype(np.uint8)
|
| 47 |
+
|
| 48 |
+
alpha = np.full((colors.shape[0], 1), 255, dtype=np.uint8)
|
| 49 |
+
rgba = np.hstack([colors, alpha])
|
| 50 |
+
|
| 51 |
+
df = df.with_columns([
|
| 52 |
+
pl.Series("fill_color", rgba.tolist(), dtype=pl.List(pl.UInt8)),
|
| 53 |
+
pl.col(column_to_show).alias("val_fmt"), # on garde le nom du modèle comme texte
|
| 54 |
+
pl.col("lat").round(3).cast(pl.Utf8).alias("lat_fmt"),
|
| 55 |
+
pl.col("lon").round(3).cast(pl.Utf8).alias("lon_fmt"),
|
| 56 |
+
])
|
| 57 |
+
|
| 58 |
+
return df, 0, len(categories) - 1
|
| 59 |
+
|
| 60 |
+
if column_to_show.startswith("date"):
|
| 61 |
+
# Conversion correcte en datetime (Polars)
|
| 62 |
+
df = df.with_columns(
|
| 63 |
+
pl.col(column_to_show).str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S%.6f", strict=False)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# Récupération min/max en datetime Python natif
|
| 67 |
+
min_dt = df[column_to_show].min()
|
| 68 |
+
max_dt = df[column_to_show].max()
|
| 69 |
+
|
| 70 |
+
if isinstance(min_dt, dt.date):
|
| 71 |
+
min_dt = dt.datetime.combine(min_dt, dt.time.min)
|
| 72 |
+
if isinstance(max_dt, dt.date):
|
| 73 |
+
max_dt = dt.datetime.combine(max_dt, dt.time.min)
|
| 74 |
+
|
| 75 |
+
vmin = min_dt if vmin is None else vmin
|
| 76 |
+
vmax = max_dt if vmax is None else vmax
|
| 77 |
+
|
| 78 |
+
# Gestion safe des timestamps sur Windows (pré-1970)
|
| 79 |
+
def safe_timestamp(d):
|
| 80 |
+
epoch = dt.datetime(1970, 1, 1)
|
| 81 |
+
return (d - epoch).total_seconds()
|
| 82 |
+
|
| 83 |
+
vmin_ts = safe_timestamp(vmin)
|
| 84 |
+
vmax_ts = safe_timestamp(vmax)
|
| 85 |
+
|
| 86 |
+
# Ajout de la colonne normalisée dans Polars
|
| 87 |
+
df = df.with_columns([
|
| 88 |
+
((pl.col(column_to_show).cast(pl.Datetime).dt.timestamp() - vmin_ts) / (vmax_ts - vmin_ts))
|
| 89 |
+
.clip(0.0, 1.0)
|
| 90 |
+
.alias("value_norm")
|
| 91 |
+
])
|
| 92 |
+
|
| 93 |
+
val_fmt_func = lambda x: x.strftime("%Y-%m-%d")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
elif column_to_show.startswith("mois_pluvieux"):
|
| 97 |
+
df = df.with_columns(pl.col(column_to_show).cast(pl.Int32))
|
| 98 |
+
value_norm = ((df[column_to_show] - 1) / 11).clip(0.0, 1.0)
|
| 99 |
+
df = df.with_columns(value_norm.alias("value_norm"))
|
| 100 |
+
|
| 101 |
+
mois_labels = [
|
| 102 |
+
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
| 103 |
+
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
| 104 |
+
]
|
| 105 |
+
val_fmt_func = lambda x: mois_labels[int(x) - 1] if 1 <= int(x) <= 12 else "Inconnu"
|
| 106 |
+
|
| 107 |
+
vmin, vmax = 1, 12
|
| 108 |
+
|
| 109 |
+
else: # ➔ Cas général (continu)
|
| 110 |
+
if vmax is None:
|
| 111 |
+
vmax = df[column_to_show].max()
|
| 112 |
+
if vmax is None: # Que des NaN
|
| 113 |
+
return df, None, None
|
| 114 |
+
|
| 115 |
+
if vmin is None:
|
| 116 |
+
vmin = df[column_to_show].min()
|
| 117 |
+
if vmin > 0:
|
| 118 |
+
vmin = 0
|
| 119 |
+
|
| 120 |
+
value_norm = ((df[column_to_show] - vmin) / (vmax - vmin)).clip(0.0, 1.0)
|
| 121 |
+
df = df.with_columns(value_norm.alias("value_norm"))
|
| 122 |
+
|
| 123 |
+
val_fmt_func = lambda x: f"{x:.2f}"
|
| 124 |
+
|
| 125 |
+
# Application de la colormap
|
| 126 |
+
# Étape 1 : extraire les valeurs (en NumPy)
|
| 127 |
+
vals = df["value_norm"].to_numpy()
|
| 128 |
+
|
| 129 |
+
# Étape 2 : appliquer le colormap sur tout le tableau (résultat : Nx4 array RGBA)
|
| 130 |
+
colors = (255 * np.array(colormap(vals))[:, :3]).astype(np.uint8)
|
| 131 |
+
|
| 132 |
+
# Étape 3 : ajouter l'alpha (255)
|
| 133 |
+
alpha = np.full((colors.shape[0], 1), 255, dtype=np.uint8)
|
| 134 |
+
rgba = np.hstack([colors, alpha])
|
| 135 |
+
|
| 136 |
+
# Étape 4 : réinjecter dans Polars
|
| 137 |
+
fill_color = pl.Series("fill_color", rgba.tolist(), dtype=pl.List(pl.UInt8))
|
| 138 |
+
|
| 139 |
+
df = df.with_columns([
|
| 140 |
+
pl.Series("fill_color", fill_color),
|
| 141 |
+
df[column_to_show].map_elements(val_fmt_func, return_dtype=pl.String).alias("val_fmt"), # val_fmt optimisé si float
|
| 142 |
+
pl.col("lat").round(3).cast(pl.Utf8).alias("lat_fmt"),
|
| 143 |
+
pl.col("lon").round(3).cast(pl.Utf8).alias("lon_fmt")
|
| 144 |
+
])
|
| 145 |
+
|
| 146 |
+
return df, vmin, vmax
|
| 147 |
+
|
| 148 |
+
def display_vertical_color_legend(height, colormap, vmin, vmax, n_ticks=5, label="", model_labels=None):
|
| 149 |
+
if model_labels is not None:
|
| 150 |
+
# Si une liste de labels de modèles est fournie, on fait une légende discrète
|
| 151 |
+
color_boxes = ""
|
| 152 |
+
for idx, name in enumerate(model_labels):
|
| 153 |
+
rgba = colormap(idx / (len(model_labels) - 1)) # Normalisé entre 0-1
|
| 154 |
+
rgb = [int(255 * c) for c in rgba[:3]]
|
| 155 |
+
color = f"rgb({rgb[0]}, {rgb[1]}, {rgb[2]})"
|
| 156 |
+
color_boxes += (
|
| 157 |
+
f'<div style="display: flex; align-items: center; margin-bottom: 6px;">'
|
| 158 |
+
f' <div style="width: 18px; height: 18px; background-color: {color}; margin-right: 8px; border: 1px solid #ccc;"></div>'
|
| 159 |
+
f' <div style="font-size: 12px;">{name}</div>'
|
| 160 |
+
f'</div>'
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
html_legend = (
|
| 164 |
+
f'<div style="text-align: left; font-size: 13px; margin-bottom: 4px;">{label}</div>'
|
| 165 |
+
f'<div style="display: flex; flex-direction: column;">{color_boxes}</div>'
|
| 166 |
+
)
|
| 167 |
+
return html_legend
|
| 168 |
+
|
| 169 |
+
if isinstance(vmin, int) and isinstance(vmax, int) and (1 <= vmin <= 12) and (1 <= vmax <= 12):
|
| 170 |
+
mois_labels = [
|
| 171 |
+
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
| 172 |
+
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
| 173 |
+
]
|
| 174 |
+
color_boxes = ""
|
| 175 |
+
for mois in range(vmin, vmax + 1):
|
| 176 |
+
rgba = colormap((mois - 1) / 11)
|
| 177 |
+
rgb = [int(255 * c) for c in rgba[:3]]
|
| 178 |
+
color = f"rgb({rgb[0]}, {rgb[1]}, {rgb[2]})"
|
| 179 |
+
label_mois = mois_labels[mois - 1]
|
| 180 |
+
color_boxes += (
|
| 181 |
+
f'<div style="display: flex; align-items: center; margin-bottom: 4px;">'
|
| 182 |
+
f' <div style="width: 14px; height: 14px; background-color: {color}; '
|
| 183 |
+
f'border: 1px solid #ccc; margin-right: 6px;"></div>'
|
| 184 |
+
f' <div style="font-size: 12px;">{label_mois}</div>'
|
| 185 |
+
f'</div>'
|
| 186 |
+
)
|
| 187 |
+
html_mois = (
|
| 188 |
+
f'<div style="text-align: left; font-size: 13px; margin-bottom: 4px;">{label}</div>'
|
| 189 |
+
f'<div style="display: flex; flex-direction: column;">{color_boxes}</div>'
|
| 190 |
+
)
|
| 191 |
+
return html_mois
|
| 192 |
+
|
| 193 |
+
gradient = np.linspace(1, 0, 64).reshape(64, 1)
|
| 194 |
+
fig, ax = plt.subplots(figsize=(1, 3), dpi=30)
|
| 195 |
+
ax.imshow(gradient, aspect='auto', cmap=colormap)
|
| 196 |
+
ax.axis('off')
|
| 197 |
+
|
| 198 |
+
buf = BytesIO()
|
| 199 |
+
plt.savefig(buf, format="png", bbox_inches='tight', pad_inches=0, transparent=True)
|
| 200 |
+
plt.close(fig)
|
| 201 |
+
base64_img = base64.b64encode(buf.getvalue()).decode()
|
| 202 |
+
|
| 203 |
+
if isinstance(vmin, dt.datetime) and isinstance(vmax, dt.datetime):
|
| 204 |
+
ticks_seconds = np.linspace(vmax.timestamp(), vmin.timestamp(), n_ticks)
|
| 205 |
+
ticks = [dt.datetime.fromtimestamp(t).strftime("%Y-%m-%d") for t in ticks_seconds]
|
| 206 |
+
else:
|
| 207 |
+
ticks_vals = np.linspace(vmax, vmin, n_ticks)
|
| 208 |
+
ticks = [f"{val:.2f}" for val in ticks_vals]
|
| 209 |
+
|
| 210 |
+
html_gradient = f"""
|
| 211 |
+
<div style="text-align: left; font-size: 13px;">{label}</div>
|
| 212 |
+
<div style="display: flex; flex-direction: row; align-items: center; height: {height-30}px;">
|
| 213 |
+
<img src="data:image/png;base64,{base64_img}"
|
| 214 |
+
style="height: 100%; width: 20px; border: 1px solid #ccc; border-radius: 5px;"/>
|
| 215 |
+
<div style="display: flex; flex-direction: column; justify-content: space-between;
|
| 216 |
+
margin-left: 8px; height: 100%; font-size: 12px;">
|
| 217 |
+
{''.join(f'<div>{tick}</div>' for tick in ticks)}
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
"""
|
| 221 |
+
return html_gradient
|
app/utils/map_utils.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import pydeck as pdk
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import polars as pl
|
| 5 |
+
import geopandas as gpd
|
| 6 |
+
|
| 7 |
+
def prepare_layer(df: pl.DataFrame) -> pl.DataFrame:
|
| 8 |
+
cols = ["lat", "lon", "lat_fmt", "lon_fmt", "altitude", "val_fmt", "fill_color"]
|
| 9 |
+
|
| 10 |
+
if "is_significant" in df.columns:
|
| 11 |
+
cols.append("is_significant")
|
| 12 |
+
|
| 13 |
+
return df.select(cols)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def fast_to_dicts(df: pl.DataFrame) -> list[dict]:
|
| 17 |
+
cols = df.columns
|
| 18 |
+
result = []
|
| 19 |
+
|
| 20 |
+
# Conversion explicite des colonnes en listes Python natives
|
| 21 |
+
arrays = {
|
| 22 |
+
col: (
|
| 23 |
+
df[col].to_list() # pour List ou String ou autre
|
| 24 |
+
if df[col].dtype == pl.List
|
| 25 |
+
else df[col].to_numpy().tolist()
|
| 26 |
+
)
|
| 27 |
+
for col in cols
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
n = len(df)
|
| 31 |
+
for i in range(n):
|
| 32 |
+
row = {col: arrays[col][i] for col in cols}
|
| 33 |
+
result.append(row)
|
| 34 |
+
|
| 35 |
+
return result
|
| 36 |
+
|
| 37 |
+
def create_layer(df: pl.DataFrame) -> pdk.Layer:
|
| 38 |
+
layers = []
|
| 39 |
+
|
| 40 |
+
df = prepare_layer(df)
|
| 41 |
+
|
| 42 |
+
if "is_significant" in df.columns:
|
| 43 |
+
df_sig = df.filter(pl.col("is_significant"))
|
| 44 |
+
df_non_sig = df.filter(~pl.col("is_significant"))
|
| 45 |
+
else:
|
| 46 |
+
df_sig = pl.DataFrame()
|
| 47 |
+
df_non_sig = df
|
| 48 |
+
|
| 49 |
+
# Points significatifs
|
| 50 |
+
if len(df_sig) > 0:
|
| 51 |
+
df_sig = df_sig.with_columns(pl.lit("*").alias("star_text"))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
layers.append(
|
| 55 |
+
pdk.Layer(
|
| 56 |
+
"TextLayer",
|
| 57 |
+
data=fast_to_dicts(df_sig),
|
| 58 |
+
get_position=["lon", "lat"],
|
| 59 |
+
get_text="star_text",
|
| 60 |
+
get_size=5,
|
| 61 |
+
get_color=[0, 0, 0, 255], # noir
|
| 62 |
+
get_angle=0,
|
| 63 |
+
get_text_anchor="center",
|
| 64 |
+
get_alignment_baseline="bottom",
|
| 65 |
+
pickable=False
|
| 66 |
+
)
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
layers.append(
|
| 70 |
+
pdk.Layer(
|
| 71 |
+
"GridCellLayer",
|
| 72 |
+
data=fast_to_dicts(df_sig),
|
| 73 |
+
get_position=["lon", "lat"],
|
| 74 |
+
get_fill_color="fill_color",
|
| 75 |
+
cell_size=2500,
|
| 76 |
+
elevation=0,
|
| 77 |
+
elevation_scale=0,
|
| 78 |
+
lighting=None,
|
| 79 |
+
pickable=True,
|
| 80 |
+
opacity=0.2,
|
| 81 |
+
extruded=False
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Points non significatifs
|
| 86 |
+
if len(df_non_sig) > 0:
|
| 87 |
+
layers.append(
|
| 88 |
+
pdk.Layer(
|
| 89 |
+
"GridCellLayer",
|
| 90 |
+
data=fast_to_dicts(df_non_sig),
|
| 91 |
+
get_position=["lon", "lat"],
|
| 92 |
+
get_fill_color="fill_color",
|
| 93 |
+
cell_size=2500,
|
| 94 |
+
elevation=0,
|
| 95 |
+
elevation_scale=0,
|
| 96 |
+
lighting=None,
|
| 97 |
+
pickable=True,
|
| 98 |
+
opacity=0.2,
|
| 99 |
+
extruded=False
|
| 100 |
+
)
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
return layers
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def create_scatter_layer(df: pl.DataFrame, radius=1500) -> list[pdk.Layer]:
|
| 107 |
+
layers = []
|
| 108 |
+
|
| 109 |
+
df = prepare_layer(df)
|
| 110 |
+
|
| 111 |
+
if "is_significant" in df.columns:
|
| 112 |
+
df_sig = df.filter(pl.col("is_significant"))
|
| 113 |
+
df_non_sig = df.filter(~pl.col("is_significant"))
|
| 114 |
+
else:
|
| 115 |
+
df_sig = pl.DataFrame()
|
| 116 |
+
df_non_sig = df
|
| 117 |
+
|
| 118 |
+
# Points significatifs avec IconLayer (Triangle non rempli)
|
| 119 |
+
if len(df_sig) > 0:
|
| 120 |
+
layers.append(
|
| 121 |
+
pdk.Layer(
|
| 122 |
+
"ScatterplotLayer",
|
| 123 |
+
data=fast_to_dicts(df_sig),
|
| 124 |
+
get_position=["lon", "lat"],
|
| 125 |
+
get_fill_color="fill_color",
|
| 126 |
+
get_line_color=[0, 0, 0],
|
| 127 |
+
line_width_min_pixels=0.2,
|
| 128 |
+
get_radius=radius,
|
| 129 |
+
radius_scale=3,
|
| 130 |
+
radius_min_pixels=2,
|
| 131 |
+
pickable=True,
|
| 132 |
+
stroked=False
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# Points non significatifs en ScatterplotLayer classique
|
| 138 |
+
if len(df_non_sig) > 0:
|
| 139 |
+
layers.append(
|
| 140 |
+
pdk.Layer(
|
| 141 |
+
"ScatterplotLayer",
|
| 142 |
+
data=fast_to_dicts(df_non_sig),
|
| 143 |
+
get_position=["lon", "lat"],
|
| 144 |
+
get_fill_color="fill_color",
|
| 145 |
+
get_line_color=[0, 0, 0],
|
| 146 |
+
line_width_min_pixels=0.2,
|
| 147 |
+
get_radius=radius,
|
| 148 |
+
radius_scale=1,
|
| 149 |
+
radius_min_pixels=2,
|
| 150 |
+
pickable=True,
|
| 151 |
+
stroked=False
|
| 152 |
+
)
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return layers
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def create_tooltip(label: str) -> dict:
|
| 160 |
+
return {
|
| 161 |
+
"html": f"""
|
| 162 |
+
({{lat_fmt}}, {{lon_fmt}})<br>
|
| 163 |
+
{{altitude}} m<br>
|
| 164 |
+
{{val_fmt}} {label}
|
| 165 |
+
""",
|
| 166 |
+
"style": {
|
| 167 |
+
"backgroundColor": "steelblue",
|
| 168 |
+
"color": "white"
|
| 169 |
+
},
|
| 170 |
+
"condition": "altitude !== 'undefined'"
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def relief():
|
| 175 |
+
# Lire et reprojeter le shapefile
|
| 176 |
+
gdf = gpd.read_file(Path("data/external/niveaux/selection_courbes_niveau_france.shp").resolve()).to_crs(epsg=4326)
|
| 177 |
+
|
| 178 |
+
# Extraire les chemins
|
| 179 |
+
path_data = []
|
| 180 |
+
for _, row in gdf.iterrows():
|
| 181 |
+
geom = row.geometry
|
| 182 |
+
altitude = row["coordonnees"] # ou la colonne correcte (parfois 'ALTITUDE', à adapter)
|
| 183 |
+
|
| 184 |
+
if geom.geom_type == "LineString":
|
| 185 |
+
path_data.append({"path": list(geom.coords), "altitude": altitude})
|
| 186 |
+
elif geom.geom_type == "MultiLineString":
|
| 187 |
+
for line in geom.geoms:
|
| 188 |
+
path_data.append({"path": list(line.coords), "altitude": altitude})
|
| 189 |
+
|
| 190 |
+
# Couleur fixe blanc
|
| 191 |
+
return pdk.Layer(
|
| 192 |
+
"PathLayer",
|
| 193 |
+
data=path_data,
|
| 194 |
+
get_path="path",
|
| 195 |
+
get_color="[0, 0, 0, 100]",
|
| 196 |
+
width_scale=1,
|
| 197 |
+
width_min_pixels=0.5,
|
| 198 |
+
pickable=False
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
def plot_map(layers, view_state, tooltip, activate_relief: bool=False):
|
| 202 |
+
if not isinstance(layers, list):
|
| 203 |
+
layers = [layers]
|
| 204 |
+
|
| 205 |
+
# Supprime les couches nulles/indéfinies
|
| 206 |
+
layers = [layer for layer in layers if layer is not None]
|
| 207 |
+
|
| 208 |
+
if activate_relief:
|
| 209 |
+
relief_layer = relief()
|
| 210 |
+
if relief_layer is not None:
|
| 211 |
+
layers.append(relief_layer)
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
return pdk.Deck(
|
| 215 |
+
layers=layers,
|
| 216 |
+
initial_view_state=view_state,
|
| 217 |
+
tooltip=tooltip,
|
| 218 |
+
map_style=None
|
| 219 |
+
)
|
| 220 |
+
except Exception as e:
|
| 221 |
+
st.error(f"Erreur lors de la création de la carte : {e}")
|
| 222 |
+
return None
|
| 223 |
+
|
app/utils/menus_utils.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from app.utils.config_utils import reverse_param_label
|
| 5 |
+
|
| 6 |
+
def menu_statisticals(min_years: int, max_years: int, STATS, SEASON):
|
| 7 |
+
if "selected_point" not in st.session_state:
|
| 8 |
+
st.session_state["selected_point"] = None
|
| 9 |
+
|
| 10 |
+
if "run_analysis" not in st.session_state:
|
| 11 |
+
st.session_state["run_analysis"] = False
|
| 12 |
+
|
| 13 |
+
# Crée les colonnes
|
| 14 |
+
col0, col1, col2, col3, col4, col5, col6, col7 = st.columns([0.3, 0.3, 0.2, 0.25, 0.25, 0.25, 0.2, 0.2])
|
| 15 |
+
|
| 16 |
+
with col0:
|
| 17 |
+
st.selectbox("Statistique étudiée", list(STATS.keys()), key="stat_choice")
|
| 18 |
+
|
| 19 |
+
with col1:
|
| 20 |
+
st.slider(
|
| 21 |
+
"Staturation couleurs",
|
| 22 |
+
min_value=0.950,
|
| 23 |
+
max_value=1.00,
|
| 24 |
+
value=0.995,
|
| 25 |
+
step=0.001,
|
| 26 |
+
format="%.3f",
|
| 27 |
+
key="quantile_choice"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
with col2:
|
| 31 |
+
st.selectbox("Saison", list(SEASON.keys()), key="season_choice")
|
| 32 |
+
|
| 33 |
+
with col3:
|
| 34 |
+
season = st.session_state["season_choice"]
|
| 35 |
+
if season in ["Année hydrologique", "Hiver"]:
|
| 36 |
+
st.slider(
|
| 37 |
+
"Période",
|
| 38 |
+
min_value=min_years+1,
|
| 39 |
+
max_value=max_years,
|
| 40 |
+
value=(min_years+1, max_years),
|
| 41 |
+
key="year_range"
|
| 42 |
+
)
|
| 43 |
+
else:
|
| 44 |
+
st.slider(
|
| 45 |
+
"Période",
|
| 46 |
+
min_value=min_years,
|
| 47 |
+
max_value=max_years,
|
| 48 |
+
value=(min_years, max_years),
|
| 49 |
+
key="year_range"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
with col4:
|
| 53 |
+
if st.session_state["stat_choice"] in ["Cumul", "Jour de pluie"]:
|
| 54 |
+
st.selectbox("Echelle temporelle", ["Journalière"], key="scale_choice")
|
| 55 |
+
else:
|
| 56 |
+
st.selectbox("Echelle temporelle", ["Journalière", "Horaire"], key="scale_choice")
|
| 57 |
+
|
| 58 |
+
with col5:
|
| 59 |
+
st.slider(
|
| 60 |
+
"Données manquantes",
|
| 61 |
+
min_value=0.0,
|
| 62 |
+
max_value=1.0,
|
| 63 |
+
value=0.1,
|
| 64 |
+
step=0.01,
|
| 65 |
+
key="missing_rate"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
with col6:
|
| 69 |
+
st.checkbox("Courbes de niveaux", value=False, key="show_relief") # Case à cocher
|
| 70 |
+
st.checkbox("Afficher les stations", value=False, key="show_stations") # Case à cocher
|
| 71 |
+
|
| 72 |
+
with col7:
|
| 73 |
+
if st.button("Lancer l’analyse"):
|
| 74 |
+
st.session_state["run_analysis"] = True
|
| 75 |
+
|
| 76 |
+
if st.session_state["run_analysis"]:
|
| 77 |
+
return (
|
| 78 |
+
st.session_state["stat_choice"],
|
| 79 |
+
st.session_state["quantile_choice"],
|
| 80 |
+
st.session_state["year_range"][0],
|
| 81 |
+
st.session_state["year_range"][1],
|
| 82 |
+
st.session_state["season_choice"],
|
| 83 |
+
st.session_state["scale_choice"],
|
| 84 |
+
st.session_state["missing_rate"],
|
| 85 |
+
st.session_state["show_relief"],
|
| 86 |
+
st.session_state["show_stations"]
|
| 87 |
+
)
|
| 88 |
+
else:
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def menu_gev(config: dict, model_options: dict, ns_param_map: dict, SEASON, show_param: bool):
|
| 94 |
+
if "run_analysis" not in st.session_state:
|
| 95 |
+
st.session_state["run_analysis"] = False
|
| 96 |
+
|
| 97 |
+
col0, col1, col2, col3, col4, col5, col6, col7 = st.columns([0.6, 0.9, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5])
|
| 98 |
+
|
| 99 |
+
# Échelle
|
| 100 |
+
with col0:
|
| 101 |
+
Echelle = st.selectbox("Echelle temporelle", ["Journalière", "Horaire"], key="scale_choice")
|
| 102 |
+
st.session_state["echelle"] = "quotidien" if Echelle.lower() == "journalière" else "horaire"
|
| 103 |
+
st.session_state["unit"] = "mm/j" if st.session_state["echelle"] == "quotidien" else "mm/h"
|
| 104 |
+
|
| 105 |
+
# Modèle
|
| 106 |
+
with col1:
|
| 107 |
+
selected_model = st.selectbox(
|
| 108 |
+
"Modèle GEV",
|
| 109 |
+
[None] + list(model_options.keys()),
|
| 110 |
+
format_func=lambda x: "— Choisir un modèle —" if x is None else x,
|
| 111 |
+
key="model_type"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
if st.session_state["model_type"] is not None:
|
| 115 |
+
model_name = model_options[st.session_state["model_type"]]
|
| 116 |
+
st.session_state["model_name"] = model_name
|
| 117 |
+
|
| 118 |
+
# Quantile
|
| 119 |
+
with col2:
|
| 120 |
+
st.selectbox("Choix de la saison", list(SEASON.keys()), key="season_choice")
|
| 121 |
+
|
| 122 |
+
with col3:
|
| 123 |
+
st.slider(
|
| 124 |
+
"Percentile de retrait",
|
| 125 |
+
min_value=0.950,
|
| 126 |
+
max_value=1.000,
|
| 127 |
+
value=1.000,
|
| 128 |
+
step=0.001,
|
| 129 |
+
format="%.3f",
|
| 130 |
+
key="quantile_choice"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Paramètre GEV
|
| 134 |
+
with col4:
|
| 135 |
+
if show_param:
|
| 136 |
+
param_map = ns_param_map[model_name]
|
| 137 |
+
available_params = list(param_map.values()) # labels unicode
|
| 138 |
+
selected_label = st.selectbox(
|
| 139 |
+
"Paramètre GEV à afficher",
|
| 140 |
+
available_params,
|
| 141 |
+
index=0,
|
| 142 |
+
key="gev_param_choice"
|
| 143 |
+
)
|
| 144 |
+
# Conversion propre
|
| 145 |
+
st.session_state["param_choice"] = reverse_param_label(
|
| 146 |
+
selected_label, model_name, ns_param_map
|
| 147 |
+
)
|
| 148 |
+
else:
|
| 149 |
+
# st.session_state["param_choice"] = "Δqᵀ"
|
| 150 |
+
# selected_label = "Δqᵀ"
|
| 151 |
+
selected_label = st.selectbox(
|
| 152 |
+
"Quantité à afficher",
|
| 153 |
+
["Δqᵀ", "ΔE", "ΔVar", "ΔCV"],
|
| 154 |
+
index=0,
|
| 155 |
+
key="delta_param_choice"
|
| 156 |
+
)
|
| 157 |
+
st.session_state["param_choice"] = selected_label
|
| 158 |
+
|
| 159 |
+
if selected_label in ["Δqᵀ"]:
|
| 160 |
+
with col5:
|
| 161 |
+
st.slider(
|
| 162 |
+
"Niveau de retour",
|
| 163 |
+
min_value=10,
|
| 164 |
+
max_value=100,
|
| 165 |
+
value=10,
|
| 166 |
+
step=10,
|
| 167 |
+
key="T_choice"
|
| 168 |
+
)
|
| 169 |
+
else:
|
| 170 |
+
st.session_state["T_choice"] = None
|
| 171 |
+
|
| 172 |
+
if selected_label in ["Δqᵀ", "ΔE", "ΔVar", "ΔCV"]:
|
| 173 |
+
with col6:
|
| 174 |
+
st.slider(
|
| 175 |
+
"Delta annees",
|
| 176 |
+
min_value=1,
|
| 177 |
+
max_value=60,
|
| 178 |
+
value=10,
|
| 179 |
+
step=1,
|
| 180 |
+
key="par_X_annees"
|
| 181 |
+
)
|
| 182 |
+
else:
|
| 183 |
+
st.session_state["par_X_annees"] = None
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# Bouton d’analyse
|
| 187 |
+
with col7:
|
| 188 |
+
if st.button("Lancer l’analyse"):
|
| 189 |
+
st.session_state["run_analysis"] = True
|
| 190 |
+
|
| 191 |
+
if st.session_state["run_analysis"]:
|
| 192 |
+
# Valeurs par défaut
|
| 193 |
+
stat_choice_key = "max"
|
| 194 |
+
scale_choice_key = "mm_j" if st.session_state["echelle"] == "quotidien" else "mm_h"
|
| 195 |
+
season_choice_key = SEASON[st.session_state["season_choice"]]
|
| 196 |
+
min_year_choice = config["years"]["min"] + 1 if season_choice_key in ["hydro", "djf"] else config["years"]["min"]
|
| 197 |
+
max_year_choice = config["years"]["max"]
|
| 198 |
+
missing_rate = 0.15
|
| 199 |
+
# Répertoires
|
| 200 |
+
mod_dir = Path(config["gev"]["modelised"]) / st.session_state["echelle"] / season_choice_key
|
| 201 |
+
obs_dir = Path(config["gev"]["observed"]) / st.session_state["echelle"] / season_choice_key
|
| 202 |
+
|
| 203 |
+
return {
|
| 204 |
+
"echelle": st.session_state["echelle"],
|
| 205 |
+
"unit": st.session_state["unit"],
|
| 206 |
+
"model_name": st.session_state["model_name"],
|
| 207 |
+
"model_name_pres": selected_model,
|
| 208 |
+
"param_choice": st.session_state["param_choice"],
|
| 209 |
+
"param_choice_pres": selected_label,
|
| 210 |
+
"quantile_choice": st.session_state["quantile_choice"],
|
| 211 |
+
"stat_choice_key": stat_choice_key,
|
| 212 |
+
"scale_choice_key": scale_choice_key,
|
| 213 |
+
"season_choice_key": season_choice_key,
|
| 214 |
+
"season_choice": st.session_state["season_choice"],
|
| 215 |
+
"min_year_choice": min_year_choice,
|
| 216 |
+
"max_year_choice": max_year_choice,
|
| 217 |
+
"missing_rate": missing_rate,
|
| 218 |
+
"mod_dir": mod_dir,
|
| 219 |
+
"obs_dir": obs_dir,
|
| 220 |
+
"T_choice": st.session_state["T_choice"],
|
| 221 |
+
"par_X_annees": st.session_state["par_X_annees"]
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return None
|
app/utils/scatter_plot_utils.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl
|
| 2 |
+
import plotly.express as px
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
import numpy as np
|
| 5 |
+
from scipy.stats import genextreme
|
| 6 |
+
|
| 7 |
+
def generate_scatter_plot_interactive(df: pl.DataFrame, stat_choice: str, unit_label: str, height: int,
|
| 8 |
+
x_label: str = "AROME", y_label: str = "Station"):
|
| 9 |
+
df_pd = df.select(["NUM_POSTE_obs", "NUM_POSTE_mod", "lat", "lon", x_label, y_label]).to_pandas()
|
| 10 |
+
|
| 11 |
+
fig = px.scatter(
|
| 12 |
+
df_pd,
|
| 13 |
+
x=x_label,
|
| 14 |
+
y=y_label,
|
| 15 |
+
title="",
|
| 16 |
+
opacity=0.5,
|
| 17 |
+
width=height,
|
| 18 |
+
height=height,
|
| 19 |
+
labels={
|
| 20 |
+
x_label: f"{stat_choice} du modèle AROME ({unit_label})",
|
| 21 |
+
y_label: f"{stat_choice} des stations ({unit_label})"
|
| 22 |
+
},
|
| 23 |
+
hover_data={"lat": True, "lon": True}
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
precision = ".1f" if unit_label == "mm/j" else ".2f"
|
| 27 |
+
fig.update_traces(
|
| 28 |
+
hovertemplate=
|
| 29 |
+
"Lat: %{customdata[2]:.4f}<br>Lon: %{customdata[3]:.4f}<br>"
|
| 30 |
+
f"{x_label} : %{{x:{precision}}}<br>{y_label} : %{{y:{precision}}}<extra></extra>",
|
| 31 |
+
customdata=df_pd[["NUM_POSTE_obs", "NUM_POSTE_mod", "lat", "lon"]].values
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
x_range = [df_pd[x_label].min(), df_pd[x_label].max()]
|
| 35 |
+
y_range = [df_pd[y_label].min(), df_pd[y_label].max()]
|
| 36 |
+
min_diag = min(x_range[0], y_range[0])
|
| 37 |
+
max_diag = min(x_range[1], y_range[1])
|
| 38 |
+
|
| 39 |
+
# Ajouter le trait y = x sans légende
|
| 40 |
+
fig.add_trace(
|
| 41 |
+
go.Scatter(
|
| 42 |
+
x=[min_diag, max_diag],
|
| 43 |
+
y=[min_diag, max_diag],
|
| 44 |
+
mode='lines',
|
| 45 |
+
line=dict(color='red', dash='dash'),
|
| 46 |
+
showlegend=False,
|
| 47 |
+
hoverinfo='skip'
|
| 48 |
+
)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Ajouter une annotation "y = x" en bout de ligne
|
| 52 |
+
fig.add_annotation(
|
| 53 |
+
x=max_diag,
|
| 54 |
+
y=max_diag,
|
| 55 |
+
text="y = x",
|
| 56 |
+
showarrow=False,
|
| 57 |
+
font=dict(color='red'),
|
| 58 |
+
xanchor="left",
|
| 59 |
+
yanchor="bottom"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
return fig
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def generate_return_period_plot_interactive(
|
| 68 |
+
T, y_obs, y_mod,
|
| 69 |
+
label_obs="Stations", label_mod="AROME",
|
| 70 |
+
unit: str = "mm/j", height: int = 600,
|
| 71 |
+
points_obs: dict | None = None,
|
| 72 |
+
points_mod: dict | None = None
|
| 73 |
+
):
|
| 74 |
+
fig = go.Figure()
|
| 75 |
+
|
| 76 |
+
# Courbe observations
|
| 77 |
+
fig.add_trace(go.Scatter(
|
| 78 |
+
x=T,
|
| 79 |
+
y=y_obs,
|
| 80 |
+
mode="lines",
|
| 81 |
+
name=label_obs,
|
| 82 |
+
line=dict(color="blue"),
|
| 83 |
+
hovertemplate="Période : %{x:.1f} ans<br>Précipitation : %{y:.1f} " + unit + "<extra></extra>"
|
| 84 |
+
))
|
| 85 |
+
|
| 86 |
+
# Courbe modèle
|
| 87 |
+
fig.add_trace(go.Scatter(
|
| 88 |
+
x=T,
|
| 89 |
+
y=y_mod,
|
| 90 |
+
mode="lines",
|
| 91 |
+
name=label_mod,
|
| 92 |
+
line=dict(color="orange"),
|
| 93 |
+
hovertemplate="Période : %{x:.1f} ans<br>Précipitation : %{y:.1f} " + unit + "<extra></extra>"
|
| 94 |
+
))
|
| 95 |
+
|
| 96 |
+
# Points maximas observés (facultatif)
|
| 97 |
+
if points_obs is not None:
|
| 98 |
+
fig.add_trace(go.Scatter(
|
| 99 |
+
x=points_obs["year"],
|
| 100 |
+
y=points_obs["value"],
|
| 101 |
+
mode="markers",
|
| 102 |
+
name="Maximas mesurés",
|
| 103 |
+
marker=dict(color="blue", size=4, symbol="x"),
|
| 104 |
+
hovertemplate="Période : %{x:.1f} ans<br>Max observé : %{y:.1f} " + unit + "<extra></extra>"
|
| 105 |
+
))
|
| 106 |
+
|
| 107 |
+
# Maximas annuels bruts (facultatif)
|
| 108 |
+
if points_mod is not None:
|
| 109 |
+
fig.add_trace(go.Scatter(
|
| 110 |
+
x=points_mod["year"],
|
| 111 |
+
y=points_mod["value"],
|
| 112 |
+
mode="markers",
|
| 113 |
+
name="Maximas modélisés",
|
| 114 |
+
marker=dict(color="orange", size=4, symbol="x"),
|
| 115 |
+
hovertemplate="Année : %{x:.1f}<br>Max : %{y:.1f} " + unit + "<extra></extra>"
|
| 116 |
+
))
|
| 117 |
+
|
| 118 |
+
fig.update_layout(
|
| 119 |
+
xaxis=dict(
|
| 120 |
+
title="Période de retour (ans)",
|
| 121 |
+
type="log",
|
| 122 |
+
showgrid=True,
|
| 123 |
+
minor=dict(ticklen=4, showgrid=True),
|
| 124 |
+
),
|
| 125 |
+
yaxis=dict(
|
| 126 |
+
title=f"Précipitation ({unit})",
|
| 127 |
+
showgrid=True,
|
| 128 |
+
minor=dict(ticklen=4, showgrid=True),
|
| 129 |
+
),
|
| 130 |
+
template="plotly_white",
|
| 131 |
+
height=height
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return fig
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def generate_gev_density_comparison_interactive(
|
| 138 |
+
maxima_obs: np.ndarray,
|
| 139 |
+
maxima_mod: np.ndarray,
|
| 140 |
+
params_obs: dict,
|
| 141 |
+
params_mod: dict,
|
| 142 |
+
unit: str = "mm/j",
|
| 143 |
+
height: int = 500,
|
| 144 |
+
t_norm: float = 0.0, # Covariable normalisée (ex: 0 pour année médiane)
|
| 145 |
+
):
|
| 146 |
+
"""
|
| 147 |
+
Trace deux courbes de densité GEV (observée et modélisée) superposées, sans histogramme.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
# --- Récupération des paramètres observés ---
|
| 151 |
+
mu_obs = params_obs.get("mu0", 0) + params_obs.get("mu1", 0) * t_norm
|
| 152 |
+
sigma_obs = params_obs.get("sigma0", 0) + params_obs.get("sigma1", 0) * t_norm
|
| 153 |
+
xi_obs = params_obs.get("xi", 0)
|
| 154 |
+
|
| 155 |
+
# --- Récupération des paramètres modélisés ---
|
| 156 |
+
mu_mod = params_mod.get("mu0", 0) + params_mod.get("mu1", 0) * t_norm
|
| 157 |
+
sigma_mod = params_mod.get("sigma0", 0) + params_mod.get("sigma1", 0) * t_norm
|
| 158 |
+
xi_mod = params_mod.get("xi", 0)
|
| 159 |
+
|
| 160 |
+
# --- Domaine commun pour tracer ---
|
| 161 |
+
minima = min(maxima_obs.min(), maxima_mod.min()) * 0.9
|
| 162 |
+
maxima = max(maxima_obs.max(), maxima_mod.max()) * 1.1
|
| 163 |
+
x = np.linspace(minima, maxima, 500)
|
| 164 |
+
|
| 165 |
+
# --- Densités ---
|
| 166 |
+
density_obs = genextreme.pdf(x, c=-xi_obs, loc=mu_obs, scale=sigma_obs)
|
| 167 |
+
density_mod = genextreme.pdf(x, c=-xi_mod, loc=mu_mod, scale=sigma_mod)
|
| 168 |
+
|
| 169 |
+
# --- Création figure ---
|
| 170 |
+
fig = go.Figure()
|
| 171 |
+
|
| 172 |
+
fig.add_trace(go.Scatter(
|
| 173 |
+
x=x,
|
| 174 |
+
y=density_obs,
|
| 175 |
+
mode="lines",
|
| 176 |
+
name="GEV observée",
|
| 177 |
+
line=dict(color="blue"),
|
| 178 |
+
hovertemplate="Maxima : %{x:.1f} " + unit + "<br>Densité : %{y:.3f}<extra></extra>",
|
| 179 |
+
))
|
| 180 |
+
|
| 181 |
+
fig.add_trace(go.Scatter(
|
| 182 |
+
x=x,
|
| 183 |
+
y=density_mod,
|
| 184 |
+
mode="lines",
|
| 185 |
+
name="GEV modélisée",
|
| 186 |
+
line=dict(color="orange"),
|
| 187 |
+
hovertemplate="Maxima : %{x:.1f} " + unit + "<br>Densité : %{y:.3f}<extra></extra>",
|
| 188 |
+
))
|
| 189 |
+
|
| 190 |
+
fig.update_layout(
|
| 191 |
+
title="",
|
| 192 |
+
xaxis_title=f"Maximum journalier ({unit})",
|
| 193 |
+
yaxis_title="Densité",
|
| 194 |
+
template="plotly_white",
|
| 195 |
+
height=height,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
return fig
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
import numpy as np
|
| 203 |
+
import plotly.graph_objects as go
|
| 204 |
+
from scipy.stats import genextreme
|
| 205 |
+
import matplotlib.cm as cm
|
| 206 |
+
import matplotlib.colors as mcolors
|
| 207 |
+
|
| 208 |
+
def generate_gev_density_comparison_interactive_3D(
|
| 209 |
+
maxima_obs: np.ndarray,
|
| 210 |
+
maxima_mod: np.ndarray,
|
| 211 |
+
params_obs: dict,
|
| 212 |
+
params_mod: dict,
|
| 213 |
+
unit: str = "mm/j",
|
| 214 |
+
height: int = 500,
|
| 215 |
+
min_year: int = 1960,
|
| 216 |
+
max_year: int = 2015,
|
| 217 |
+
):
|
| 218 |
+
"""
|
| 219 |
+
Trace deux ensembles de courbes de densité GEV (observée et modélisée) superposées,
|
| 220 |
+
en faisant varier la couleur de violet (min_year) à jaune (max_year).
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
+
# --- Génération des années ---
|
| 224 |
+
years = np.arange(min_year, max_year + 1)
|
| 225 |
+
|
| 226 |
+
# --- Couleurs violet -> jaune ---
|
| 227 |
+
cmap = cm.get_cmap('plasma')
|
| 228 |
+
norm = mcolors.Normalize(vmin=min_year, vmax=max_year)
|
| 229 |
+
colors = [mcolors.to_hex(cmap(norm(year))) for year in years]
|
| 230 |
+
|
| 231 |
+
# --- Domaine commun pour tracer ---
|
| 232 |
+
minima = min(maxima_obs.min(), maxima_mod.min()) * 0.9
|
| 233 |
+
maxima = max(maxima_obs.max(), maxima_mod.max()) * 1.1
|
| 234 |
+
x = np.linspace(minima, maxima, 500)
|
| 235 |
+
|
| 236 |
+
# --- Création de la figure ---
|
| 237 |
+
fig = go.Figure()
|
| 238 |
+
|
| 239 |
+
for i, year in enumerate(years):
|
| 240 |
+
t_norm = (year - (min_year + max_year) / 2) / (max_year - min_year)
|
| 241 |
+
|
| 242 |
+
# Densité observée
|
| 243 |
+
mu_obs = params_obs.get("mu0", 0) + params_obs.get("mu1", 0) * t_norm
|
| 244 |
+
sigma_obs = params_obs.get("sigma0", 0) + params_obs.get("sigma1", 0) * t_norm
|
| 245 |
+
xi_obs = params_obs.get("xi", 0)
|
| 246 |
+
|
| 247 |
+
density_obs = genextreme.pdf(x, c=-xi_obs, loc=mu_obs, scale=sigma_obs)
|
| 248 |
+
|
| 249 |
+
fig.add_trace(go.Scatter(
|
| 250 |
+
x=x,
|
| 251 |
+
y=density_obs,
|
| 252 |
+
mode="lines",
|
| 253 |
+
line=dict(color=colors[i]),
|
| 254 |
+
name=f"Obs {year}",
|
| 255 |
+
hovertemplate=f"Obs {year}<br>Maxima : %{{x:.1f}} {unit}<br>Densité : %{{y:.3f}}<extra></extra>",
|
| 256 |
+
showlegend=False,
|
| 257 |
+
))
|
| 258 |
+
|
| 259 |
+
# Densité modélisée
|
| 260 |
+
mu_mod = params_mod.get("mu0", 0) + params_mod.get("mu1", 0) * t_norm
|
| 261 |
+
sigma_mod = params_mod.get("sigma0", 0) + params_mod.get("sigma1", 0) * t_norm
|
| 262 |
+
xi_mod = params_mod.get("xi", 0)
|
| 263 |
+
|
| 264 |
+
density_mod = genextreme.pdf(x, c=-xi_mod, loc=mu_mod, scale=sigma_mod)
|
| 265 |
+
|
| 266 |
+
fig.add_trace(go.Scatter(
|
| 267 |
+
x=x,
|
| 268 |
+
y=density_mod,
|
| 269 |
+
mode="lines",
|
| 270 |
+
line=dict(color=colors[i]),
|
| 271 |
+
name=f"Mod {year}",
|
| 272 |
+
hovertemplate=f"Mod {year}<br>Maxima : %{{x:.1f}} {unit}<br>Densité : %{{y:.3f}}<extra></extra>",
|
| 273 |
+
showlegend=False,
|
| 274 |
+
))
|
| 275 |
+
|
| 276 |
+
# --- Layout final ---
|
| 277 |
+
fig.update_layout(
|
| 278 |
+
title="",
|
| 279 |
+
xaxis_title=f"Maximum journalier ({unit})",
|
| 280 |
+
yaxis_title="Densité",
|
| 281 |
+
template="plotly_white",
|
| 282 |
+
height=height,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return fig
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def generate_time_series_maxima_interactive(
|
| 291 |
+
years_obs: np.ndarray,
|
| 292 |
+
max_obs: np.ndarray,
|
| 293 |
+
years_mod: np.ndarray,
|
| 294 |
+
max_mod: np.ndarray,
|
| 295 |
+
unit: str = "mm/j",
|
| 296 |
+
height: int = 500,
|
| 297 |
+
nr_year: int = 20,
|
| 298 |
+
return_levels_obs: float | None = None,
|
| 299 |
+
return_levels_mod: float | None = None
|
| 300 |
+
):
|
| 301 |
+
fig_time_series = go.Figure()
|
| 302 |
+
|
| 303 |
+
# --- Observations (seulement en 'x' sans lignes)
|
| 304 |
+
fig_time_series.add_trace(go.Scatter(
|
| 305 |
+
x=years_obs,
|
| 306 |
+
y=max_obs,
|
| 307 |
+
mode='markers',
|
| 308 |
+
name='Maximas observés',
|
| 309 |
+
marker=dict(symbol='x', size=4, color="blue")
|
| 310 |
+
))
|
| 311 |
+
|
| 312 |
+
# --- Modèle (seulement en 'x' sans lignes)
|
| 313 |
+
fig_time_series.add_trace(go.Scatter(
|
| 314 |
+
x=years_mod,
|
| 315 |
+
y=max_mod,
|
| 316 |
+
mode='markers',
|
| 317 |
+
name='Maximas modélisés',
|
| 318 |
+
marker=dict(symbol='x', size=4, color="orange")
|
| 319 |
+
))
|
| 320 |
+
|
| 321 |
+
# --- Niveau de retour 20 ans observé
|
| 322 |
+
if return_levels_obs is not None:
|
| 323 |
+
fig_time_series.add_trace(go.Scatter(
|
| 324 |
+
x=years_obs, # ➔ Utilise toutes les années observées !
|
| 325 |
+
y=return_levels_obs,
|
| 326 |
+
mode='lines',
|
| 327 |
+
name=f'NR observé {nr_year} ans',
|
| 328 |
+
line=dict(color='blue', dash='solid')
|
| 329 |
+
))
|
| 330 |
+
|
| 331 |
+
# --- Niveau de retour 20 ans modélisé
|
| 332 |
+
if return_levels_mod is not None:
|
| 333 |
+
fig_time_series.add_trace(go.Scatter(
|
| 334 |
+
x=years_mod, # ➔ Utilise toutes les années modélisées !
|
| 335 |
+
y=return_levels_mod,
|
| 336 |
+
mode='lines',
|
| 337 |
+
name=f'NR modélisé {nr_year} ans',
|
| 338 |
+
line=dict(color='orange', dash='solid')
|
| 339 |
+
))
|
| 340 |
+
|
| 341 |
+
fig_time_series.update_layout(
|
| 342 |
+
title="",
|
| 343 |
+
xaxis_title="Année",
|
| 344 |
+
yaxis_title=f"Maxima annuel ({unit})",
|
| 345 |
+
height=height,
|
| 346 |
+
template="plotly_white"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
return fig_time_series
|
| 350 |
+
|
| 351 |
+
import numpy as np
|
| 352 |
+
import plotly.graph_objects as go
|
| 353 |
+
from scipy.stats import genextreme
|
| 354 |
+
|
| 355 |
+
def generate_loglikelihood_profile_xi(
|
| 356 |
+
maxima: np.ndarray,
|
| 357 |
+
params: dict,
|
| 358 |
+
unit: str = "mm/j",
|
| 359 |
+
xi_range: float = 3,
|
| 360 |
+
height: int = 500,
|
| 361 |
+
t_norm: float = 0.0
|
| 362 |
+
):
|
| 363 |
+
"""
|
| 364 |
+
Trace le profil de log-vraisemblance autour de ξ ajusté.
|
| 365 |
+
|
| 366 |
+
- maxima : valeurs maximales (array)
|
| 367 |
+
- params : dictionnaire des paramètres GEV
|
| 368 |
+
- unit : unité des maxima
|
| 369 |
+
- xi_range : +/- intervalle autour de ξ pour tracer
|
| 370 |
+
- height : hauteur de la figure
|
| 371 |
+
- t_norm : covariable temporelle normalisée
|
| 372 |
+
"""
|
| 373 |
+
|
| 374 |
+
# Récupération des paramètres (à t_norm donné)
|
| 375 |
+
mu = params.get("mu0", 0) + params.get("mu1", 0) * t_norm
|
| 376 |
+
sigma = params.get("sigma0", 0) + params.get("sigma1", 0) * t_norm
|
| 377 |
+
xi_fit = params.get("xi", 0)
|
| 378 |
+
|
| 379 |
+
def compute_nllh(x, mu, sigma, xi):
|
| 380 |
+
if sigma <= 0:
|
| 381 |
+
return np.inf
|
| 382 |
+
try:
|
| 383 |
+
return -np.sum(genextreme.logpdf(x, c=-xi, loc=mu, scale=sigma))
|
| 384 |
+
except Exception:
|
| 385 |
+
return np.inf
|
| 386 |
+
|
| 387 |
+
# Points autour du ξ ajusté
|
| 388 |
+
xis = np.linspace(xi_fit - xi_range, xi_fit + xi_range, 200)
|
| 389 |
+
logliks = [-compute_nllh(maxima, mu, sigma, xi) for xi in xis]
|
| 390 |
+
|
| 391 |
+
# --- Création figure Plotly ---
|
| 392 |
+
fig = go.Figure()
|
| 393 |
+
|
| 394 |
+
fig.add_trace(go.Scatter(
|
| 395 |
+
x=xis,
|
| 396 |
+
y=logliks,
|
| 397 |
+
mode="lines",
|
| 398 |
+
line=dict(color="blue"),
|
| 399 |
+
name="Log-vraisemblance",
|
| 400 |
+
hovertemplate="ξ : %{x:.3f}<br>Log-likelihood : %{y:.1f}<extra></extra>"
|
| 401 |
+
))
|
| 402 |
+
|
| 403 |
+
# Conversion en array pour traitement
|
| 404 |
+
logliks = np.array(logliks)
|
| 405 |
+
|
| 406 |
+
# Filtrage des valeurs finies
|
| 407 |
+
finite_logliks = logliks[np.isfinite(logliks)]
|
| 408 |
+
|
| 409 |
+
if finite_logliks.size > 0:
|
| 410 |
+
ymin = finite_logliks.min() - 1 # Marge sous le min réel
|
| 411 |
+
ymax = finite_logliks.max()
|
| 412 |
+
else:
|
| 413 |
+
ymin, ymax = -10, 0 # Valeurs par défaut si tout est -inf
|
| 414 |
+
|
| 415 |
+
# Ajout de la ligne verticale
|
| 416 |
+
fig.add_trace(go.Scatter(
|
| 417 |
+
x=[xi_fit, xi_fit],
|
| 418 |
+
y=[ymin, ymax],
|
| 419 |
+
mode="lines",
|
| 420 |
+
line=dict(color="red", dash="dash"),
|
| 421 |
+
name=f"ξ ajusté ({xi_fit:.3f})"
|
| 422 |
+
))
|
| 423 |
+
|
| 424 |
+
fig.update_layout(
|
| 425 |
+
title="",
|
| 426 |
+
xaxis_title="ξ",
|
| 427 |
+
yaxis_title="Log-vraisemblance",
|
| 428 |
+
template="plotly_white",
|
| 429 |
+
height=height
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
return fig
|
app/utils/show_info.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def show_info_data(col, label, n_points_valides, n_points_total):
|
| 2 |
+
both_defined = n_points_valides is not None and n_points_total is not None
|
| 3 |
+
if both_defined:
|
| 4 |
+
return col.markdown(f"""
|
| 5 |
+
**{label}**
|
| 6 |
+
{n_points_valides} / {n_points_total}
|
| 7 |
+
Tx couverture : {(n_points_valides / n_points_total * 100):.1f}%
|
| 8 |
+
""")
|
| 9 |
+
else:
|
| 10 |
+
return None
|
| 11 |
+
|
| 12 |
+
def show_info_metric(col, label, metric):
|
| 13 |
+
if metric is not None:
|
| 14 |
+
return col.markdown(f"""
|
| 15 |
+
**{label}**
|
| 16 |
+
{metric:.3f}
|
| 17 |
+
""")
|
| 18 |
+
else:
|
| 19 |
+
return None
|
app/utils/stats_utils.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import polars as pl
|
| 3 |
+
import numpy as np
|
| 4 |
+
from sklearn.metrics import mean_squared_error, mean_absolute_error
|
| 5 |
+
|
| 6 |
+
def compute_statistic_per_point(df: pl.DataFrame, stat_key: str) -> pl.DataFrame:
|
| 7 |
+
cols = df.columns
|
| 8 |
+
|
| 9 |
+
if stat_key == "mean":
|
| 10 |
+
has_h = "mean_mm_h" in cols
|
| 11 |
+
if has_h:
|
| 12 |
+
df = df.with_columns(
|
| 13 |
+
(pl.col("mean_mm_h") * 24).alias("mean_mm_j")
|
| 14 |
+
)
|
| 15 |
+
return df.group_by("NUM_POSTE").agg([
|
| 16 |
+
*( [pl.col("mean_mm_h").mean().alias("mean_all_mm_h")] if has_h else [] ),
|
| 17 |
+
*( [pl.col("mean_mm_j").mean().alias("mean_all_mm_j")] if has_h else [] ),
|
| 18 |
+
])
|
| 19 |
+
|
| 20 |
+
elif stat_key == "max":
|
| 21 |
+
return df.group_by("NUM_POSTE").agg([
|
| 22 |
+
*( [pl.col("max_mm_h").max().alias("max_all_mm_h")] if "max_mm_h" in cols else [] ),
|
| 23 |
+
*( [pl.col("max_mm_j").max().alias("max_all_mm_j")] if "max_mm_j" in cols else [] ),
|
| 24 |
+
])
|
| 25 |
+
|
| 26 |
+
elif stat_key == "mean-max":
|
| 27 |
+
return df.group_by("NUM_POSTE").agg([
|
| 28 |
+
*( [pl.col("max_mm_h").mean().alias("max_mean_mm_h")] if "max_mm_h" in cols else [] ),
|
| 29 |
+
*( [pl.col("max_mm_j").mean().alias("max_mean_mm_j")] if "max_mm_j" in cols else [] ),
|
| 30 |
+
])
|
| 31 |
+
|
| 32 |
+
elif stat_key == "date":
|
| 33 |
+
res = []
|
| 34 |
+
if "max_mm_h" in cols and "max_date_mm_h" in cols:
|
| 35 |
+
df_h = (
|
| 36 |
+
df.sort("max_mm_h", descending=True)
|
| 37 |
+
.group_by("NUM_POSTE")
|
| 38 |
+
.agg(pl.col("max_date_mm_h").first().alias("date_max_h"))
|
| 39 |
+
)
|
| 40 |
+
res.append(df_h)
|
| 41 |
+
if "max_mm_j" in cols and "max_date_mm_j" in cols:
|
| 42 |
+
df_j = (
|
| 43 |
+
df.sort("max_mm_j", descending=True)
|
| 44 |
+
.group_by("NUM_POSTE")
|
| 45 |
+
.agg(pl.col("max_date_mm_j").first().alias("date_max_j"))
|
| 46 |
+
)
|
| 47 |
+
res.append(df_j)
|
| 48 |
+
|
| 49 |
+
if not res:
|
| 50 |
+
raise ValueError("Aucune date de maximum disponible.")
|
| 51 |
+
elif len(res) == 1:
|
| 52 |
+
return res[0]
|
| 53 |
+
else:
|
| 54 |
+
return res[0].join(res[1], on="NUM_POSTE", how="outer")
|
| 55 |
+
|
| 56 |
+
elif stat_key == "month":
|
| 57 |
+
exprs = []
|
| 58 |
+
if "max_date_mm_h" in cols:
|
| 59 |
+
exprs.append(
|
| 60 |
+
pl.col("max_date_mm_h")
|
| 61 |
+
.str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S%.f", strict=False)
|
| 62 |
+
.dt.month()
|
| 63 |
+
.alias("mois_max_h")
|
| 64 |
+
)
|
| 65 |
+
if "max_date_mm_j" in cols:
|
| 66 |
+
exprs.append(
|
| 67 |
+
pl.col("max_date_mm_j")
|
| 68 |
+
.str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S%.f", strict=False)
|
| 69 |
+
.dt.month()
|
| 70 |
+
.alias("mois_max_j")
|
| 71 |
+
)
|
| 72 |
+
if not exprs:
|
| 73 |
+
raise ValueError("Aucune date de maximum pour extraire les mois.")
|
| 74 |
+
|
| 75 |
+
df = df.with_columns(exprs)
|
| 76 |
+
|
| 77 |
+
mois_h = mois_j = None
|
| 78 |
+
|
| 79 |
+
if "mois_max_h" in df.columns:
|
| 80 |
+
mois_h = (
|
| 81 |
+
df.drop_nulls("mois_max_h")
|
| 82 |
+
.group_by(["NUM_POSTE", "mois_max_h"])
|
| 83 |
+
.len()
|
| 84 |
+
.sort(["NUM_POSTE", "len"], descending=[False, True])
|
| 85 |
+
.unique(subset=["NUM_POSTE"])
|
| 86 |
+
.select(["NUM_POSTE", "mois_max_h"])
|
| 87 |
+
.rename({"mois_max_h": "mois_pluvieux_h"})
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
if "mois_max_j" in df.columns:
|
| 91 |
+
mois_j = (
|
| 92 |
+
df.drop_nulls("mois_max_j")
|
| 93 |
+
.group_by(["NUM_POSTE", "mois_max_j"])
|
| 94 |
+
.len()
|
| 95 |
+
.sort(["NUM_POSTE", "len"], descending=[False, True])
|
| 96 |
+
.unique(subset=["NUM_POSTE"])
|
| 97 |
+
.select(["NUM_POSTE", "mois_max_j"])
|
| 98 |
+
.rename({"mois_max_j": "mois_pluvieux_j"})
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
if mois_h is None and mois_j is None:
|
| 102 |
+
return pl.DataFrame(schema={"NUM_POSTE": pl.Int64, "mois_pluvieux_h": pl.Int32, "mois_pluvieux_j": pl.Int32})
|
| 103 |
+
elif mois_h is None:
|
| 104 |
+
return mois_j.with_columns([pl.lit(None, dtype=pl.Int32).alias("mois_pluvieux_h")])
|
| 105 |
+
elif mois_j is None:
|
| 106 |
+
return mois_h.with_columns([pl.lit(None, dtype=pl.Int32).alias("mois_pluvieux_j")])
|
| 107 |
+
else:
|
| 108 |
+
return mois_h.join(mois_j, on="NUM_POSTE", how="outer")
|
| 109 |
+
|
| 110 |
+
elif stat_key == "numday":
|
| 111 |
+
if "n_days_gt1mm" not in df.columns:
|
| 112 |
+
raise ValueError("Colonne `n_days_gt1mm` manquante.")
|
| 113 |
+
return (
|
| 114 |
+
df.group_by("NUM_POSTE")
|
| 115 |
+
.agg(pl.col("n_days_gt1mm").mean().alias("jours_pluie_moyen"))
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
else:
|
| 119 |
+
raise ValueError(f"Statistique inconnue : {stat_key}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def generate_metrics(df: pl.DataFrame, x_label: str = "AROME", y_label: str = "Station"):
|
| 124 |
+
x = df[x_label].to_numpy()
|
| 125 |
+
y = df[y_label].to_numpy()
|
| 126 |
+
|
| 127 |
+
if len(x) != len(y):
|
| 128 |
+
st.error("Longueur x et y différente")
|
| 129 |
+
return np.nan, np.nan, np.nan, np.nan
|
| 130 |
+
|
| 131 |
+
# Filtrage des NaNs sur les deux colonnes
|
| 132 |
+
mask = ~np.isnan(x) & ~np.isnan(y)
|
| 133 |
+
x_valid = x[mask]
|
| 134 |
+
y_valid = y[mask]
|
| 135 |
+
|
| 136 |
+
if len(x_valid) == 0:
|
| 137 |
+
st.warning("Aucune donnée valide après suppression des NaN.")
|
| 138 |
+
return np.nan, np.nan, np.nan, np.nan
|
| 139 |
+
|
| 140 |
+
rmse = np.sqrt(mean_squared_error(y_valid, x_valid))
|
| 141 |
+
mae = mean_absolute_error(y_valid, x_valid)
|
| 142 |
+
me = np.mean(x_valid - y_valid)
|
| 143 |
+
|
| 144 |
+
corr = np.corrcoef(x_valid, y_valid)[0, 1] if len(x_valid) > 1 else np.nan
|
| 145 |
+
r2_corr = corr**2 if not np.isnan(corr) else np.nan
|
| 146 |
+
|
| 147 |
+
return me, mae, rmse, r2_corr
|
download_data.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from huggingface_hub import snapshot_download
|
| 2 |
+
import os
|
| 3 |
+
import traceback
|
| 4 |
+
|
| 5 |
+
cache_path = os.path.expanduser("~/.cache/huggingface/hub")
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
print("Téléchargement des métadonnées...")
|
| 9 |
+
snapshot_download(
|
| 10 |
+
repo_id="ncsdecoopman/ExtremePrecipit",
|
| 11 |
+
repo_type="dataset",
|
| 12 |
+
revision="main",
|
| 13 |
+
local_dir="data",
|
| 14 |
+
cache_dir=cache_path,
|
| 15 |
+
allow_patterns=["metadonnees/*"]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
print("Téléchargement des reliefs...")
|
| 19 |
+
snapshot_download(
|
| 20 |
+
repo_id="ncsdecoopman/ExtremePrecipit",
|
| 21 |
+
repo_type="dataset",
|
| 22 |
+
revision="main",
|
| 23 |
+
local_dir="data",
|
| 24 |
+
cache_dir=cache_path,
|
| 25 |
+
allow_patterns=["external/*"]
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
for echelle in ["quotidien", "horaire"]:
|
| 29 |
+
print(f"Téléchargement des statistiques AROMES (mod)... - Echelle {echelle}")
|
| 30 |
+
snapshot_download(
|
| 31 |
+
repo_id="ncsdecoopman/ExtremePrecipit",
|
| 32 |
+
repo_type="dataset",
|
| 33 |
+
revision="main",
|
| 34 |
+
local_dir="data",
|
| 35 |
+
cache_dir=cache_path,
|
| 36 |
+
allow_patterns=["statisticals/modelised*"]
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
print(f"Téléchargement des statistiques STATIONS observées... - Echelle {echelle}")
|
| 40 |
+
snapshot_download(
|
| 41 |
+
repo_id="ncsdecoopman/ExtremePrecipit",
|
| 42 |
+
repo_type="dataset",
|
| 43 |
+
revision="main",
|
| 44 |
+
local_dir="data",
|
| 45 |
+
cache_dir=cache_path,
|
| 46 |
+
allow_patterns=["statisticals/observed*"]
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print("Erreur pendant le téléchargement :")
|
| 51 |
+
traceback.print_exc()
|
| 52 |
+
raise SystemExit(1)
|
main.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
from app.utils.map_utils import plot_map
|
| 4 |
+
from app.utils.legends_utils import get_stat_unit
|
| 5 |
+
|
| 6 |
+
from app.pipelines.import_data import pipeline_data
|
| 7 |
+
from app.pipelines.import_config import pipeline_config
|
| 8 |
+
from app.pipelines.import_map import pipeline_map
|
| 9 |
+
from app.pipelines.import_scatter import pipeline_scatter
|
| 10 |
+
from app.utils.show_info import show_info_data, show_info_metric
|
| 11 |
+
|
| 12 |
+
st.set_page_config(layout="wide", page_title="Analyse interactive des précipitations en France (1959–2022)", page_icon="🌧️")
|
| 13 |
+
st.markdown("""
|
| 14 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
|
| 15 |
+
""", unsafe_allow_html=True)
|
| 16 |
+
|
| 17 |
+
st.markdown("""
|
| 18 |
+
<style>
|
| 19 |
+
* {
|
| 20 |
+
font-size: 10px !important;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* Responsive layout des colonnes */
|
| 24 |
+
@media screen and (max-width: 1000px) {
|
| 25 |
+
.element-container:has(> .stColumn) {
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-wrap: wrap;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.element-container:has(> .stColumn) .stColumn {
|
| 31 |
+
width: 48% !important;
|
| 32 |
+
min-width: 48% !important;
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@media screen and (max-width: 600px) {
|
| 37 |
+
.element-container:has(> .stColumn) .stColumn {
|
| 38 |
+
width: 100% !important;
|
| 39 |
+
min-width: 100% !important;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
</style>
|
| 43 |
+
""", unsafe_allow_html=True)
|
| 44 |
+
|
| 45 |
+
css = """
|
| 46 |
+
<style>
|
| 47 |
+
/* -------------------- VARIABLES GLOBALES -------------------- */
|
| 48 |
+
:root{
|
| 49 |
+
--primary:#5A7BFF;
|
| 50 |
+
--primary-light:#8FA0FF;
|
| 51 |
+
--accent:#FF7A59;
|
| 52 |
+
--bg:rgba(245,247,250,0.65);
|
| 53 |
+
--card:rgba(255,255,255,0.35);
|
| 54 |
+
--text:#1F2D3D;
|
| 55 |
+
--text-light:#6B7C93;
|
| 56 |
+
--radius:18px;
|
| 57 |
+
--shadow:0 12px 28px rgba(0,0,0,.12);
|
| 58 |
+
--blur:18px;
|
| 59 |
+
--font:"Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* -------------------- RESET & BODY -------------------- */
|
| 63 |
+
html, body, [class*="stAppViewContainer"]{
|
| 64 |
+
font-family: var(--font) !important;
|
| 65 |
+
color: var(--text);
|
| 66 |
+
}
|
| 67 |
+
body{
|
| 68 |
+
background: linear-gradient(135deg,#EEF2FF 0%,#FDFBFF 60%,#F0F4FF 100%) fixed !important;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Conteneur principal */
|
| 72 |
+
.block-container{
|
| 73 |
+
padding-top: 2.5rem !important;
|
| 74 |
+
padding-bottom: 3rem !important;
|
| 75 |
+
max-width: 98%;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* -------------------- EN-TÊTES -------------------- */
|
| 79 |
+
h1,h2,h3,h4{
|
| 80 |
+
font-weight: 600 !important;
|
| 81 |
+
letter-spacing: -0.01em;
|
| 82 |
+
color: var(--text);
|
| 83 |
+
}
|
| 84 |
+
h1{
|
| 85 |
+
font-size: 2.1rem !important;
|
| 86 |
+
margin-bottom: 1.2rem;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* -------------------- CARTES / WIDGETS -------------------- */
|
| 90 |
+
section.main > div{
|
| 91 |
+
backdrop-filter: blur(var(--blur));
|
| 92 |
+
background: var(--card);
|
| 93 |
+
border-radius: var(--radius);
|
| 94 |
+
box-shadow: var(--shadow);
|
| 95 |
+
padding: 1.5rem 1.8rem;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* -------------------- LABELS DES WIDGETS -------------------- */
|
| 99 |
+
.css-10trblm, .stSlider label, .stSelectbox label, .stNumberInput label, .stMultiSelect label{
|
| 100 |
+
font-size: 0.88rem !important;
|
| 101 |
+
font-weight: 500 !important;
|
| 102 |
+
color: var(--text-light) !important;
|
| 103 |
+
margin-bottom: .4rem !important;
|
| 104 |
+
text-transform: uppercase;
|
| 105 |
+
letter-spacing: .04em;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* -------------------- SELECTBOX -------------------- */
|
| 109 |
+
.stSelectbox > div div[data-baseweb="select"]{
|
| 110 |
+
background: rgba(255,255,255,0.55);
|
| 111 |
+
border-radius: var(--radius) !important;
|
| 112 |
+
border: 1px solid rgba(0,0,0,.05);
|
| 113 |
+
box-shadow: inset 0 2px 4px rgba(0,0,0,.04);
|
| 114 |
+
}
|
| 115 |
+
.stSelectbox > div div[data-baseweb="select"]:hover{
|
| 116 |
+
border-color: var(--primary-light);
|
| 117 |
+
}
|
| 118 |
+
.stSelectbox svg{
|
| 119 |
+
stroke: var(--primary) !important;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* -------------------- SLIDER -------------------- */
|
| 123 |
+
[data-testid="stSlider"] > div{
|
| 124 |
+
padding-top: .6rem;
|
| 125 |
+
}
|
| 126 |
+
[data-testid="stSlider"] [data-testid="stThumbValue"]{
|
| 127 |
+
background: var(--primary);
|
| 128 |
+
color: #fff;
|
| 129 |
+
border-radius: 10px;
|
| 130 |
+
padding: 2px 8px;
|
| 131 |
+
font-size: .75rem;
|
| 132 |
+
box-shadow: var(--shadow);
|
| 133 |
+
}
|
| 134 |
+
[data-testid="stSlider"] [data-testid="stTickBar"]{
|
| 135 |
+
background: rgba(0,0,0,.08);
|
| 136 |
+
}
|
| 137 |
+
[data-testid="stSlider"] [data-testid="stTrack"]{
|
| 138 |
+
background: rgba(0,0,0,.12);
|
| 139 |
+
}
|
| 140 |
+
[data-testid="stSlider"] [data-testid="stTrack"] > div{
|
| 141 |
+
background: var(--primary);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* -------------------- INPUT NUMBER / TEXT -------------------- */
|
| 145 |
+
.stNumberInput input, .stTextInput input{
|
| 146 |
+
background: rgba(255,255,255,0.6) !important;
|
| 147 |
+
border-radius: var(--radius) !important;
|
| 148 |
+
border: 1px solid rgba(0,0,0,.05) !important;
|
| 149 |
+
box-shadow: inset 0 2px 4px rgba(0,0,0,.05) !important;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* -------------------- BOUTON -------------------- */
|
| 153 |
+
.stButton>button{
|
| 154 |
+
background: var(--primary) !important;
|
| 155 |
+
color: #fff !important;
|
| 156 |
+
border: none !important;
|
| 157 |
+
border-radius: var(--radius) !important;
|
| 158 |
+
padding: .65rem 1.4rem !important;
|
| 159 |
+
font-weight: 600 !important;
|
| 160 |
+
letter-spacing: .02em;
|
| 161 |
+
transition: all .22s ease;
|
| 162 |
+
box-shadow: 0 8px 18px rgba(90,123,255,.28);
|
| 163 |
+
}
|
| 164 |
+
.stButton>button:hover{
|
| 165 |
+
background: var(--primary-light) !important;
|
| 166 |
+
transform: translateY(-2px) !important;
|
| 167 |
+
box-shadow: 0 12px 24px rgba(90,123,255,.32);
|
| 168 |
+
}
|
| 169 |
+
.stButton>button:active{
|
| 170 |
+
transform: translateY(0) scale(.98) !important;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* Petit bouton (col6) */
|
| 174 |
+
[class*="stColumn"]:nth-child(7) .stButton>button{
|
| 175 |
+
padding: .55rem .9rem !important;
|
| 176 |
+
font-size: .85rem !important;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/* -------------------- TOOLTIPS -------------------- */
|
| 180 |
+
[data-baseweb="tooltip"]{
|
| 181 |
+
backdrop-filter: blur(12px);
|
| 182 |
+
background: rgba(0,0,0,.75);
|
| 183 |
+
color: #fff;
|
| 184 |
+
border-radius: 8px;
|
| 185 |
+
font-size: .75rem;
|
| 186 |
+
padding: .4rem .65rem;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* -------------------- SIDEBAR -------------------- */
|
| 190 |
+
.sidebar .block-container{
|
| 191 |
+
padding: 1rem 1rem 2rem 1rem !important;
|
| 192 |
+
}
|
| 193 |
+
[class*="stSidebar"]{
|
| 194 |
+
background: rgba(255,255,255,0.7) !important;
|
| 195 |
+
backdrop-filter: blur(18px);
|
| 196 |
+
box-shadow: var(--shadow);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* -------------------- CHARTS -------------------- */
|
| 200 |
+
.js-plotly-plot .plotly .main-svg{
|
| 201 |
+
border-radius: var(--radius);
|
| 202 |
+
box-shadow: var(--shadow);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* -------------------- SCROLLBAR -------------------- */
|
| 206 |
+
::-webkit-scrollbar{
|
| 207 |
+
width: 8px;
|
| 208 |
+
height: 8px;
|
| 209 |
+
}
|
| 210 |
+
::-webkit-scrollbar-thumb{
|
| 211 |
+
background: var(--primary-light);
|
| 212 |
+
border-radius: 10px;
|
| 213 |
+
}
|
| 214 |
+
::-webkit-scrollbar-track{
|
| 215 |
+
background: transparent;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/* -------------------- SLIDER -------------------- */
|
| 219 |
+
/* Cacher totalement les chiffres min/max + graduations */
|
| 220 |
+
[data-testid="stSliderTickBarMin"], [data-testid="stSliderTickBarMax"]{
|
| 221 |
+
display:none !important;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
stSliderTickBarMin
|
| 225 |
+
|
| 226 |
+
/* -------------------- MASQUER MENU & FOOTER -------------------- */
|
| 227 |
+
#MainMenu{visibility:hidden;}
|
| 228 |
+
footer{visibility:hidden;}
|
| 229 |
+
header{visibility:hidden;}
|
| 230 |
+
|
| 231 |
+
/* -------------------- GRADIENT TEXT -------------------- */
|
| 232 |
+
.gradient-premium {
|
| 233 |
+
font-size: 2.5rem !important; /* Titre XXL */
|
| 234 |
+
font-weight: 800 !important; /* Plus de présence */
|
| 235 |
+
letter-spacing: -0.025em !important; /* Ajustement espacement */
|
| 236 |
+
|
| 237 |
+
/* Dégradé en trois couleurs */
|
| 238 |
+
background: linear-gradient(
|
| 239 |
+
360deg,
|
| 240 |
+
#5A7BFF 10%,
|
| 241 |
+
#5A7BFF 100%,
|
| 242 |
+
#F0F4FF 150%
|
| 243 |
+
) !important;
|
| 244 |
+
color: transparent !important;
|
| 245 |
+
-webkit-text-fill-color: transparent !important;
|
| 246 |
+
-webkit-background-clip: text !important;
|
| 247 |
+
background-clip: text !important;
|
| 248 |
+
|
| 249 |
+
/* contour/glow léger */
|
| 250 |
+
text-shadow:
|
| 251 |
+
0 0 2px rgba(255,255,255,0.8)
|
| 252 |
+
|
| 253 |
+
display: inline-block;
|
| 254 |
+
}
|
| 255 |
+
</style>
|
| 256 |
+
"""
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
st.markdown(css, unsafe_allow_html=True)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def show(
|
| 263 |
+
config_path: dict,
|
| 264 |
+
height: int=600
|
| 265 |
+
):
|
| 266 |
+
|
| 267 |
+
# Chargement des config
|
| 268 |
+
params_config = pipeline_config(config_path, type="stat")
|
| 269 |
+
config = params_config["config"]
|
| 270 |
+
stat_choice = params_config["stat_choice"]
|
| 271 |
+
season_choice = params_config["season_choice"]
|
| 272 |
+
stat_choice_key = params_config["stat_choice_key"]
|
| 273 |
+
scale_choice_key = params_config["scale_choice_key"]
|
| 274 |
+
min_year_choice = params_config["min_year_choice"]
|
| 275 |
+
max_year_choice = params_config["max_year_choice"]
|
| 276 |
+
season_choice_key = params_config["season_choice_key"]
|
| 277 |
+
missing_rate = params_config["missing_rate"]
|
| 278 |
+
quantile_choice = params_config["quantile_choice"]
|
| 279 |
+
scale_choice = params_config["scale_choice"]
|
| 280 |
+
show_relief = params_config["show_relief"]
|
| 281 |
+
show_stations = params_config["show_stations"]
|
| 282 |
+
|
| 283 |
+
# Préparation des paramètres pour pipeline_data
|
| 284 |
+
params_load = (
|
| 285 |
+
stat_choice_key,
|
| 286 |
+
scale_choice_key,
|
| 287 |
+
min_year_choice,
|
| 288 |
+
max_year_choice,
|
| 289 |
+
season_choice_key,
|
| 290 |
+
missing_rate,
|
| 291 |
+
quantile_choice,
|
| 292 |
+
scale_choice
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# Obtention des données
|
| 296 |
+
result = pipeline_data(params_load, config, use_cache=True)
|
| 297 |
+
|
| 298 |
+
# Chargement des affichages graphiques
|
| 299 |
+
unit_label = get_stat_unit(stat_choice_key, scale_choice_key)
|
| 300 |
+
params_map = (
|
| 301 |
+
stat_choice_key,
|
| 302 |
+
result,
|
| 303 |
+
unit_label,
|
| 304 |
+
height
|
| 305 |
+
)
|
| 306 |
+
layer, scatter_layer, tooltip, view_state, html_legend = pipeline_map(params_map)
|
| 307 |
+
|
| 308 |
+
col1, col2, col3 = st.columns([1, 0.15, 1])
|
| 309 |
+
|
| 310 |
+
with col1:
|
| 311 |
+
scatter_layer = None if not show_stations else scatter_layer
|
| 312 |
+
deck = plot_map([layer, scatter_layer], view_state, tooltip, activate_relief=show_relief)
|
| 313 |
+
st.markdown(
|
| 314 |
+
f"""
|
| 315 |
+
<div style='text-align: left; margin-bottom: 10px;'>
|
| 316 |
+
<b>{stat_choice} des précipitations de {min_year_choice} à {max_year_choice} ({season_choice.lower()})</b>
|
| 317 |
+
</div>
|
| 318 |
+
""",
|
| 319 |
+
unsafe_allow_html=True
|
| 320 |
+
)
|
| 321 |
+
if deck:
|
| 322 |
+
st.pydeck_chart(deck, use_container_width=True, height=height)
|
| 323 |
+
|
| 324 |
+
with col2:
|
| 325 |
+
st.markdown(html_legend, unsafe_allow_html=True)
|
| 326 |
+
|
| 327 |
+
with col3:
|
| 328 |
+
params_scatter = (
|
| 329 |
+
result,
|
| 330 |
+
stat_choice_key,
|
| 331 |
+
scale_choice_key,
|
| 332 |
+
stat_choice,unit_label,
|
| 333 |
+
height
|
| 334 |
+
)
|
| 335 |
+
n_tot_mod, n_tot_obs, me, mae, rmse, r2, scatter = pipeline_scatter(params_scatter)
|
| 336 |
+
|
| 337 |
+
st.markdown(
|
| 338 |
+
"""
|
| 339 |
+
<div style='text-align: left; font-size: 0.8em; color: grey; margin-top: 0px;'>
|
| 340 |
+
Données CP-RCM, 2.5 km, forçage ERA5, réanalyse ECMWF
|
| 341 |
+
</div>
|
| 342 |
+
""",
|
| 343 |
+
unsafe_allow_html=True
|
| 344 |
+
)
|
| 345 |
+
st.plotly_chart(scatter, use_container_width=True)
|
| 346 |
+
|
| 347 |
+
col0bis, col1bis, col2bis, col3bis, col4bis, col5bis, col6bis = st.columns(7)
|
| 348 |
+
|
| 349 |
+
show_info_data(col0bis, "CP-AROME map", result["modelised_show"].shape[0], n_tot_mod)
|
| 350 |
+
show_info_data(col1bis, "Stations", result["observed_show"].shape[0], n_tot_obs)
|
| 351 |
+
show_info_data(col2bis, "CP-AROME plot", result["modelised"].shape[0], n_tot_mod)
|
| 352 |
+
show_info_metric(col3bis, "ME", me)
|
| 353 |
+
show_info_metric(col4bis, "MAE", mae)
|
| 354 |
+
show_info_metric(col5bis, "RMSE", rmse)
|
| 355 |
+
show_info_metric(col6bis, "r²", r2)
|
| 356 |
+
|
| 357 |
+
if __name__ == "__main__":
|
| 358 |
+
config_path = "app/config/config.yaml"
|
| 359 |
+
st.markdown("""
|
| 360 |
+
<div style="text-align: center; margin-bottom: 2rem;">
|
| 361 |
+
<h1 style="
|
| 362 |
+
font-family: var(--font);
|
| 363 |
+
margin: 0;
|
| 364 |
+
">
|
| 365 |
+
<span class="gradient-premium">
|
| 366 |
+
Analyse interactive des précipitations en France — 1959 – 2022
|
| 367 |
+
</span>
|
| 368 |
+
</h1>
|
| 369 |
+
</div>
|
| 370 |
+
""", unsafe_allow_html=True)
|
| 371 |
+
|
| 372 |
+
show(config_path)
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core UI and plotting
|
| 2 |
+
streamlit==1.43.2
|
| 3 |
+
pydeck==0.9.1
|
| 4 |
+
matplotlib==3.10.0
|
| 5 |
+
plotly==5.24.1
|
| 6 |
+
streamlit_plotly_events==0.0.6
|
| 7 |
+
streamlit_folium==0.24.0
|
| 8 |
+
geopandas
|
| 9 |
+
|
| 10 |
+
# Data manipulation
|
| 11 |
+
pandas==2.2.3
|
| 12 |
+
polars==1.26.0
|
| 13 |
+
numpy==2.2.2
|
| 14 |
+
scipy==1.15.1
|
| 15 |
+
|
| 16 |
+
# File and compression utils
|
| 17 |
+
huggingface_hub==0.29.2
|
| 18 |
+
pyyaml==6.0.2
|
| 19 |
+
|
| 20 |
+
# Calculs
|
| 21 |
+
scikit-learn==1.6.1
|