File size: 10,466 Bytes
398ed20
 
 
 
cc9c8da
398ed20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc9c8da
398ed20
 
 
 
 
 
 
cc9c8da
398ed20
 
 
cc9c8da
398ed20
 
cc9c8da
 
 
 
398ed20
 
 
cc9c8da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398ed20
 
 
 
 
 
 
 
cc9c8da
 
 
398ed20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc9c8da
 
 
 
 
 
 
 
398ed20
cc9c8da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398ed20
 
 
cc9c8da
 
 
398ed20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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']
            }
        }