245 lines
7.7 KiB
Python
245 lines
7.7 KiB
Python
"""Calculs statistiques liés aux corrélations (instantanées, décalées, glissantes)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Callable, Literal, Sequence
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
from meteo.variables import Variable
|
||
|
||
from .core import _ensure_datetime_index
|
||
|
||
__all__ = ['compute_correlation_matrix', 'compute_correlation_matrix_for_variables', 'compute_correlation_matrices_for_methods', 'compute_lagged_correlation', 'compute_rolling_correlation_series', 'compute_rolling_correlations_for_pairs', 'transform_correlation_matrix']
|
||
|
||
CorrelationMethod = Literal["pearson", "spearman", "kendall"]
|
||
CorrelationTransform = Literal["identity", "absolute", "square"]
|
||
|
||
|
||
def compute_correlation_matrix(
|
||
df: pd.DataFrame,
|
||
*,
|
||
method: CorrelationMethod = "pearson",
|
||
) -> pd.DataFrame:
|
||
"""
|
||
Calcule la matrice de corrélation entre toutes les colonnes numériques
|
||
du DataFrame.
|
||
|
||
Attention :
|
||
- La direction du vent est traitée ici comme une variable scalaire 0–360°,
|
||
ce qui n'est pas idéal pour une analyse circulaire. On affinera plus tard
|
||
si besoin (représentation en sin/cos).
|
||
"""
|
||
numeric_df = df.select_dtypes(include=["number"])
|
||
corr = numeric_df.corr(method=method)
|
||
return corr
|
||
|
||
def compute_correlation_matrix_for_variables(
|
||
df: pd.DataFrame,
|
||
variables: Sequence[Variable],
|
||
*,
|
||
method: CorrelationMethod = "pearson",
|
||
) -> pd.DataFrame:
|
||
"""
|
||
Calcule la matrice de corrélation pour un sous-ensemble de variables,
|
||
dans un ordre bien défini.
|
||
|
||
Paramètres
|
||
----------
|
||
df :
|
||
DataFrame contenant les colonnes à analyser.
|
||
variables :
|
||
Séquence de Variable décrivant les colonnes à prendre en compte.
|
||
method :
|
||
Méthode de corrélation pandas (pearson, spearman, ...).
|
||
|
||
Retour
|
||
------
|
||
DataFrame :
|
||
Matrice de corrélation, index et colonnes dans le même ordre que
|
||
`variables`, avec les colonnes pandas correspondant aux noms de colonnes
|
||
du DataFrame (ex: "temperature", "humidity", ...).
|
||
"""
|
||
columns = [v.column for v in variables]
|
||
missing = [c for c in columns if c not in df.columns]
|
||
if missing:
|
||
raise KeyError(f"Colonnes manquantes dans le DataFrame : {missing!r}")
|
||
|
||
numeric_df = df[columns].astype(float)
|
||
corr = numeric_df.corr(method=method)
|
||
|
||
# On s'assure de l'ordre
|
||
corr = corr.loc[columns, columns]
|
||
return corr
|
||
|
||
|
||
def transform_correlation_matrix(
|
||
corr: pd.DataFrame,
|
||
*,
|
||
transform: CorrelationTransform | Callable[[pd.DataFrame], pd.DataFrame] = "identity",
|
||
) -> pd.DataFrame:
|
||
"""Applique une transformation générique sur une matrice de corrélation."""
|
||
|
||
if callable(transform):
|
||
return transform(corr)
|
||
|
||
if transform == "identity":
|
||
return corr
|
||
if transform == "absolute":
|
||
return corr.abs()
|
||
if transform == "square":
|
||
return corr.pow(2)
|
||
|
||
raise ValueError(f"Transformation de corrélation inconnue : {transform!r}")
|
||
|
||
|
||
def compute_correlation_matrices_for_methods(
|
||
df: pd.DataFrame,
|
||
variables: Sequence[Variable],
|
||
*,
|
||
methods: Sequence[CorrelationMethod],
|
||
transform: CorrelationTransform | Callable[[pd.DataFrame], pd.DataFrame] = "identity",
|
||
) -> dict[str, pd.DataFrame]:
|
||
"""Calcule plusieurs matrices de corrélation en une seule passe."""
|
||
|
||
if not methods:
|
||
raise ValueError("La liste des méthodes de corrélation est vide.")
|
||
|
||
matrices: dict[str, pd.DataFrame] = {}
|
||
for method in methods:
|
||
corr = compute_correlation_matrix_for_variables(df, variables, method=method)
|
||
matrices[method] = transform_correlation_matrix(corr, transform=transform)
|
||
|
||
return matrices
|
||
|
||
def compute_lagged_correlation(
|
||
df: pd.DataFrame,
|
||
var_x: Variable,
|
||
var_y: Variable,
|
||
*,
|
||
max_lag_minutes: int = 360,
|
||
step_minutes: int = 10,
|
||
method: Literal["pearson", "spearman"] = "pearson",
|
||
) -> pd.DataFrame:
|
||
"""
|
||
Calcule la corrélation entre deux variables pour une série de décalages
|
||
temporels (lags).
|
||
|
||
Convention :
|
||
- lag > 0 : X "précède" Y de `lag` minutes.
|
||
On corrèle X(t) avec Y(t + lag).
|
||
- lag < 0 : Y "précède" X de |lag| minutes.
|
||
On corrèle X(t) avec Y(t + lag), lag étant négatif.
|
||
|
||
Implémentation :
|
||
- On utilise un DataFrame avec les deux colonnes,
|
||
puis on applique un `shift` sur Y.
|
||
"""
|
||
if var_x.column not in df.columns or var_y.column not in df.columns:
|
||
raise KeyError("Les colonnes demandées ne sont pas présentes dans le DataFrame.")
|
||
|
||
series_x = df[var_x.column]
|
||
series_y = df[var_y.column]
|
||
|
||
lags = range(-max_lag_minutes, max_lag_minutes + 1, step_minutes)
|
||
results: list[tuple[int, float]] = []
|
||
|
||
for lag in lags:
|
||
# Y décalé de -lag : pour lag positif, on corrèle X(t) à Y(t + lag)
|
||
shifted_y = series_y.shift(-lag)
|
||
pair = pd.concat([series_x, shifted_y], axis=1).dropna()
|
||
|
||
if pair.empty:
|
||
corr = np.nan
|
||
else:
|
||
corr = pair.iloc[:, 0].corr(pair.iloc[:, 1], method=method)
|
||
|
||
results.append((lag, corr))
|
||
|
||
lag_df = pd.DataFrame(results, columns=["lag_minutes", "correlation"])
|
||
lag_df = lag_df.set_index("lag_minutes")
|
||
|
||
return lag_df
|
||
|
||
def compute_rolling_correlation_series(
|
||
df: pd.DataFrame,
|
||
var_x: Variable,
|
||
var_y: Variable,
|
||
*,
|
||
window_minutes: int,
|
||
min_valid_fraction: float = 0.6,
|
||
step_minutes: int | None = None,
|
||
method: Literal["pearson", "spearman"] = "pearson",
|
||
) -> pd.Series:
|
||
"""
|
||
Calcule la corrélation glissante X/Y sur une fenêtre temporelle.
|
||
Retourne une série indexée par l'instant de fin de fenêtre.
|
||
"""
|
||
if not 0 < min_valid_fraction <= 1:
|
||
raise ValueError("min_valid_fraction doit être dans l'intervalle ]0, 1].")
|
||
|
||
for col in (var_x.column, var_y.column):
|
||
if col not in df.columns:
|
||
raise KeyError(f"Colonne absente du DataFrame : {col}")
|
||
|
||
_ensure_datetime_index(df)
|
||
pair = df[[var_x.column, var_y.column]].dropna().sort_index()
|
||
|
||
if pair.empty:
|
||
return pd.Series(dtype=float, name=f"{var_x.key}→{var_y.key}")
|
||
|
||
window = f"{window_minutes}min"
|
||
min_periods = max(1, int(window_minutes * min_valid_fraction))
|
||
if method not in {"pearson"}:
|
||
raise NotImplementedError(
|
||
"Les corrélations glissantes ne supportent actuellement que la méthode 'pearson'."
|
||
)
|
||
|
||
rolling_corr = pair[var_x.column].rolling(
|
||
window=window,
|
||
min_periods=min_periods,
|
||
).corr(pair[var_y.column])
|
||
|
||
rolling_corr = rolling_corr.dropna()
|
||
rolling_corr.name = f"{var_x.key}→{var_y.key}"
|
||
|
||
if step_minutes and step_minutes > 1:
|
||
rolling_corr = rolling_corr.resample(f"{step_minutes}min").mean().dropna()
|
||
|
||
return rolling_corr
|
||
|
||
def compute_rolling_correlations_for_pairs(
|
||
df: pd.DataFrame,
|
||
pairs: Sequence[tuple[Variable, Variable]],
|
||
*,
|
||
window_minutes: int,
|
||
min_valid_fraction: float = 0.6,
|
||
step_minutes: int | None = None,
|
||
method: Literal["pearson", "spearman"] = "pearson",
|
||
) -> pd.DataFrame:
|
||
"""
|
||
Calcule les corrélations glissantes pour plusieurs paires et aligne les
|
||
résultats dans un DataFrame (index temps, colonnes = 'x→y').
|
||
"""
|
||
series_list: list[pd.Series] = []
|
||
for var_x, var_y in pairs:
|
||
corr = compute_rolling_correlation_series(
|
||
df=df,
|
||
var_x=var_x,
|
||
var_y=var_y,
|
||
window_minutes=window_minutes,
|
||
min_valid_fraction=min_valid_fraction,
|
||
step_minutes=step_minutes,
|
||
method=method,
|
||
)
|
||
if not corr.empty:
|
||
series_list.append(corr)
|
||
|
||
if not series_list:
|
||
return pd.DataFrame()
|
||
|
||
result = pd.concat(series_list, axis=1)
|
||
result = result.sort_index()
|
||
return result
|