Spaces:
Running
Running
| 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'] | |
| } | |
| } |