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'] } }