Vortex-Flux / src /Analytics /VortexOutFlux.py
klydekushy's picture
Update src/Analytics/VortexOutFlux.py
cc9c8da verified
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error
from scipy import stats
from datetime import datetime, timedelta
class VortexOutFlux:
"""
Classe pour analyser et prédire les flux de sortie mensuels
Utilise la régression linéaire pour des prédictions continues
"""
def __init__(self, df_forecasting):
"""
Initialise l'analyseur avec les données de prévision
Args:
df_forecasting: DataFrame avec colonnes 'Date' et 'Montant_Total_Sortie'
"""
self.df = df_forecasting.copy()
self._prepare_data()
def _prepare_data(self):
"""Prépare les données pour l'analyse"""
# Conversion de la date
self.df['Date'] = pd.to_datetime(self.df['Date'], errors='coerce')
# Suppression des lignes avec dates ou montants invalides
self.df.dropna(subset=['Date', 'Montant_Total_Sortie'], inplace=True)
# Suppression des montants à 0
self.df = self.df[self.df['Montant_Total_Sortie'] != 0]
# Tri par date
self.df = self.df.sort_values('Date').reset_index(drop=True)
# Conversion en valeurs ordinales pour la régression
self.df['date_ordinal'] = self.df['Date'].apply(lambda x: x.toordinal())
def train_linear_model(self):
"""
Entraîne le modèle de régression linéaire
Returns:
dict: Modèle sklearn + métriques + intervalles de confiance
"""
if len(self.df) < 2:
return {'error': 'Données insuffisantes pour entraîner le modèle'}
X = self.df[['date_ordinal']].values
y = self.df['Montant_Total_Sortie'].values
# Modèle sklearn
model_lr = LinearRegression()
model_lr.fit(X, y)
# Prédictions sur données historiques
y_pred = model_lr.predict(X)
# Calcul des résidus
residuals = y - y_pred
# Métriques de performance
mae = mean_absolute_error(y, y_pred)
rmse = np.sqrt(mean_squared_error(y, y_pred))
# Calcul manuel du R² et de la corrélation
ss_res = np.sum(residuals**2)
ss_tot = np.sum((y - np.mean(y))**2)
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
r_coefficient = np.sqrt(r_squared) if r_squared >= 0 else 0
# Calcul de la pente (coefficient de régression)
pente = model_lr.coef_[0]
# Calcul de l'écart-type des résidus pour les intervalles de confiance
std_error = np.sqrt(np.sum(residuals**2) / (len(y) - 2)) if len(y) > 2 else np.std(residuals)
# Calcul de l'intervalle de prédiction (approximation avec ±2*std_error)
# Pour un intervalle de confiance à 95%, on utilise environ 1.96 * std_error
prediction_interval = 1.96 * std_error
return {
'model_lr': model_lr,
'mae': mae,
'rmse': rmse,
'pente': pente,
'r_squared': r_squared,
'r_coefficient': r_coefficient,
'historical_predictions': y_pred,
'std_error': std_error,
'prediction_interval': prediction_interval
}
def predict_next_months(self, n_months=3):
"""
Prédit les n prochains mois
Args:
n_months: Nombre de mois à prédire (défaut: 3)
Returns:
dict: Prédictions avec intervalles de confiance
"""
models = self.train_linear_model()
if 'error' in models:
return models
# Dernière date dans les données
last_date = self.df['Date'].max()
# Génération des dates futures
future_dates = []
for i in range(1, n_months + 1):
future_date = last_date + pd.DateOffset(months=i)
# Ajuster au premier jour du mois
future_date = future_date.replace(day=1)
future_dates.append(future_date)
future_dates = pd.to_datetime(future_dates)
future_ordinals = np.array([d.toordinal() for d in future_dates]).reshape(-1, 1)
# Prédictions
predictions = models['model_lr'].predict(future_ordinals)
# Calcul des intervalles de confiance manuellement
# Intervalle de prédiction = prédiction ± (facteur_t * erreur_standard * racine(1 + 1/n + distance²))
n = len(self.df)
X_mean = self.df['date_ordinal'].mean()
X_std = self.df['date_ordinal'].std()
lower_bounds = []
upper_bounds = []
for ordinal in future_ordinals.flatten():
# Distance standardisée de la moyenne
distance = (ordinal - X_mean) / X_std if X_std > 0 else 0
# Facteur d'ajustement pour la distance (plus on s'éloigne, plus l'intervalle s'élargit)
adjustment = np.sqrt(1 + 1/n + distance**2)
# Intervalle = prédiction ± marge d'erreur ajustée
margin = models['prediction_interval'] * adjustment
pred_idx = np.where(future_ordinals.flatten() == ordinal)[0][0]
pred_value = predictions[pred_idx]
lower_bounds.append(max(0, pred_value - margin)) # Ne peut pas être négatif
upper_bounds.append(pred_value + margin)
predictions_df = pd.DataFrame({
'Date': future_dates,
'Montant_Predit': predictions,
'Borne_Inf': lower_bounds,
'Borne_Sup': upper_bounds
})
return {
'predictions': predictions_df,
'models': models
}
def get_interpretation(self, models):
"""
Génère une interprétation intelligente des résultats
Args:
models: Dictionnaire des modèles et métriques
Returns:
dict: Interprétations textuelles
"""
pente = models['pente']
r_squared = models['r_squared']
r_coefficient = models['r_coefficient']
mae = models['mae']
rmse = models['rmse']
# Interprétation de la tendance
if pente > 0:
tendance = "CROISSANCE"
direction = "à la hausse"
impact = "augmentation"
else:
tendance = "DÉCROISSANCE"
direction = "à la baisse"
impact = "diminution"
# Pente mensuelle approximative (conversion jour ordinal -> mois)
pente_mensuelle = pente * 30.44 # Moyenne de jours par mois
# Force de la corrélation
if r_coefficient >= 0.8:
force_correlation = "très forte"
elif r_coefficient >= 0.6:
force_correlation = "forte"
elif r_coefficient >= 0.4:
force_correlation = "modérée"
else:
force_correlation = "faible"
# Qualité du modèle
if r_squared >= 0.7:
qualite = "excellent"
elif r_squared >= 0.5:
qualite = "bon"
elif r_squared >= 0.3:
qualite = "acceptable"
else:
qualite = "faible"
# Moyenne des flux
moyenne_flux = self.df['Montant_Total_Sortie'].mean()
# Pourcentage de variation mensuelle
pct_variation = (abs(pente_mensuelle) / moyenne_flux) * 100
interpretations = {
'tendance': tendance,
'direction': direction,
'impact': impact,
'pente_mensuelle': pente_mensuelle,
'force_correlation': force_correlation,
'qualite_modele': qualite,
'pct_variation': pct_variation,
'message_principal': f"Les flux de sortie montrent une tendance {direction} avec une {impact} moyenne de {abs(pente_mensuelle):,.0f} XOF par mois ({pct_variation:.1f}% du flux moyen).",
'message_fiabilite': f"Le modèle présente une qualité {qualite} avec un R² de {r_squared:.2%} et une corrélation {force_correlation} (R = {r_coefficient:.3f}).",
'message_precision': f"L'erreur moyenne de prédiction (MAE) est de {mae:,.0f} XOF, avec une erreur quadratique (RMSE) de {rmse:,.0f} XOF."
}
return interpretations
def generate_visualization_data(self, predictions_result):
"""
Prépare les données pour la visualisation
Args:
predictions_result: Résultat de predict_next_months()
Returns:
dict: Données formatées pour graphiques
"""
if 'error' in predictions_result:
return predictions_result
predictions_df = predictions_result['predictions']
models = predictions_result['models']
# Données historiques
historical_data = {
'dates': self.df['Date'].tolist(),
'values': self.df['Montant_Total_Sortie'].tolist(),
'predictions_hist': models['historical_predictions'].tolist()
}
# Données futures
future_data = {
'dates': predictions_df['Date'].tolist(),
'predictions': predictions_df['Montant_Predit'].tolist(),
'lower_bound': predictions_df['Borne_Inf'].tolist(),
'upper_bound': predictions_df['Borne_Sup'].tolist()
}
# Résidus
residuals = self.df['Montant_Total_Sortie'].values - models['historical_predictions']
residuals_data = {
'values': residuals.tolist(),
'predictions': models['historical_predictions'].tolist()
}
return {
'historical': historical_data,
'future': future_data,
'residuals': residuals_data,
'interpretations': self.get_interpretation(models),
'metrics': {
'mae': models['mae'],
'rmse': models['rmse'],
'r_squared': models['r_squared'],
'r_coefficient': models['r_coefficient'],
'pente': models['pente']
}
}