""" ============================================================================= CARMIGNAC × ENSAE — Pipeline : Performance → Flux nets ============================================================================= Pipeline complet : 1. Chargement & exploration 2. Table de correspondance shareClass → ISIN (clé de jointure) 3. Jointure AUM (stocks) × Performance (weekly_perf) 4. Feature engineering : features de performance décalées + percentile 5. Construction de la variable cible : flux nets (ΔAum proxy) 6. Modèle prédictif : Random Forest avec walk-forward validation 7. Analyse d'importance des variables (SHAP-like permutation importance) NOTE : Ce script utilise les fichiers *_head.csv pour la démonstration. Remplacer les chemins par les fichiers complets pour l'analyse finale. Dépendances : pandas, numpy, scikit-learn, matplotlib, seaborn ============================================================================= """ import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import seaborn as sns from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_absolute_error, r2_score from sklearn.inspection import permutation_importance import warnings warnings.filterwarnings('ignore') # ── Style global ────────────────────────────────────────────────────────────── plt.rcParams.update({ 'figure.facecolor': 'white', 'axes.facecolor': '#f8f9fa', 'axes.grid': True, 'grid.alpha': 0.4, 'font.family': 'DejaVu Sans', }) COLORS = ['#1f4e79', '#2e75b6', '#70ad47', '#ed7d31', '#a50026', '#ffc000'] # ============================================================================= # 1. CHARGEMENT DES DONNÉES # ============================================================================= print("=" * 60) print("1. CHARGEMENT DES DONNÉES") print("=" * 60) # ── Remplacer par les chemins vers les fichiers complets ────────────────────── PATH_STOCKS = "equity_stocks_head.csv" # → fichier AUM mensuel par compte PATH_PERF = "weekly_perf_head.csv" # → performances hebdomadaires stocks = pd.read_csv(PATH_STOCKS, index_col=0) perf = pd.read_csv(PATH_PERF, index_col=0) # Parsing des dates stocks['Centralisation Date'] = pd.to_datetime(stocks['Centralisation Date']) perf['Date'] = pd.to_datetime(perf['Date']) print(f"stocks : {stocks.shape[0]:,} lignes × {stocks.shape[1]} colonnes") print(f"perf : {perf.shape[0]:,} lignes × {perf.shape[1]} colonnes") print(f"\nstocks — plage dates : {stocks['Centralisation Date'].min().date()} → {stocks['Centralisation Date'].max().date()}") print(f"perf — plage dates : {perf['Date'].min().date()} → {perf['Date'].max().date()}") print(f"perf — périodes disponibles : {sorted(perf['perfPeriod'].unique())}") # ============================================================================= # 2. TABLE DE CORRESPONDANCE shareClass_name → ISIN # ============================================================================= # # Problème : weekly_perf n'a pas d'ISIN, stocks n'a pas le nom complet # de shareclass. La jointure se fait en deux temps : # a) Extraction d'un nom court depuis chaque source # b) Matching fuzzy sur ce nom court + Type shareclass + Devise # # En production : remplacer par la table de référence ISIN complète # fournie par Morningstar (fichier Peers.csv) qui contient Name + ISIN. # ============================================================================= print("\n" + "=" * 60) print("2. TABLE DE CORRESPONDANCE shareClass → ISIN") print("=" * 60) # ── Extraction du nom de stratégie (nom court) depuis perf ─────────────────── # Exemples : # "Carmignac Pf Asia Discovery A EUR Acc" → "Asia Discovery", type=A, ccy=EUR # "Carmignac Investissement F EUR Acc" → "Investissement", type=F, ccy=EUR def parse_shareclass_name(name): """ Extrait (strategy_name, shareclass_type, currency) depuis le nom complet. Logique : on retire le préfixe Carmignac / Carmignac Pf, puis on parse le suffixe " X YYY Acc" en fin de chaîne. """ s = name.strip() for prefix in ['Carmignac Portfolio ', 'Carmignac Pf ', 'Carmignac ']: if s.startswith(prefix): s = s[len(prefix):] break # Suffix pattern : " A EUR Acc" ou " F USD Acc" etc. import re m = re.search(r'\s+([A-Z])\s+([A-Z]{3})\s+Acc\s*$', s) if m: strategy = s[:m.start()].strip() sc_type = m.group(1) currency = m.group(2) else: strategy = s sc_type = None currency = None return strategy, sc_type, currency perf_parsed = perf['shareClass_name'].drop_duplicates().apply( lambda x: pd.Series(parse_shareclass_name(x), index=['strategy_name', 'sc_type', 'currency']) ) perf_parsed['shareClass_name'] = perf['shareClass_name'].drop_duplicates().values print("Shareclass parsées depuis perf :") print(perf_parsed.to_string(index=False)) # ── Extraction du nom court depuis stocks ───────────────────────────────────── stocks['strategy_name'] = (stocks['Product - Fund'] .str.replace('Carmignac Portfolio ', '', regex=False) .str.replace('Carmignac ', '', regex=False) .str.strip()) # ── Correspondance ISIN depuis stocks : fund + type + currency ──────────────── isin_ref = (stocks[['strategy_name', 'Product - Shareclass Type', 'Product - Shareclass Currency', 'Product - Isin']] .drop_duplicates() .rename(columns={ 'Product - Shareclass Type': 'sc_type', 'Product - Shareclass Currency': 'currency', 'Product - Isin': 'isin' })) # ── Jointure sur (strategy_name, sc_type, currency) ────────────────────────── mapping = perf_parsed.merge(isin_ref, on=['strategy_name', 'sc_type', 'currency'], how='left') print("\nTable de correspondance shareClass_name → ISIN :") print(mapping[['shareClass_name', 'strategy_name', 'sc_type', 'currency', 'isin']].to_string(index=False)) matched = mapping['isin'].notna().sum() print(f"\nMatch : {matched}/{len(mapping)} shareclass liées à un ISIN") if matched < len(mapping): unmatched = mapping[mapping['isin'].isna()]['shareClass_name'].tolist() print(f"⚠ Non matchées (à compléter manuellement ou via Peers.csv) :") for u in unmatched: print(f" - {u}") # Enrichissement de perf avec l'ISIN perf = perf.merge(mapping[['shareClass_name', 'isin', 'strategy_name']], on='shareClass_name', how='left') # ============================================================================= # 3. CONSTRUCTION DU PANEL MENSUEL # ============================================================================= # # Objectif : une ligne = (compte client, fonds, mois) # Colonnes : AUM_t, puis features de performance sur les mois précédents # # Alignement temporel : # - stocks : snapshot mensuel (fin de mois) # - perf : données hebdomadaires → on prend la valeur la plus récente # avant ou à la date de snapshot mensuel # ============================================================================= print("\n" + "=" * 60) print("3. CONSTRUCTION DU PANEL MENSUEL") print("=" * 60) # ── Pivot perf : une ligne par (isin, date_hebdo, perfPeriod) ──────────────── perf_pivot = (perf .dropna(subset=['isin']) .pivot_table(index=['isin', 'Date'], columns='perfPeriod', values=['return', 'percentile'], aggfunc='mean') ) # Aplatir les colonnes multi-index perf_pivot.columns = ['_'.join(col).strip() for col in perf_pivot.columns] perf_pivot = perf_pivot.reset_index() perf_pivot['Date'] = pd.to_datetime(perf_pivot['Date']) print(f"perf_pivot shape : {perf_pivot.shape}") print(f"Colonnes de performance : {[c for c in perf_pivot.columns if c not in ['isin','Date']]}") # ── Merge as-of : pour chaque snapshot stocks, trouver la perf hebdo ──────── # la plus récente avant ou égale à la date de snapshot stocks_sorted = stocks.sort_values('Centralisation Date') perf_sorted = perf_pivot.sort_values('Date') # Merge as-of par ISIN merged_parts = [] for isin_val in stocks_sorted['Product - Isin'].unique(): s_isin = stocks_sorted[stocks_sorted['Product - Isin'] == isin_val].copy() p_isin = perf_sorted[perf_sorted['isin'] == isin_val].copy() if p_isin.empty: merged_parts.append(s_isin) continue merged = pd.merge_asof( s_isin.sort_values('Centralisation Date'), p_isin.sort_values('Date'), left_on='Centralisation Date', right_on='Date', direction='backward', tolerance=pd.Timedelta('35 days') # max 5 semaines d'écart ) merged_parts.append(merged) panel = pd.concat(merged_parts, ignore_index=True) perf_cols = [c for c in panel.columns if c not in stocks.columns and c != 'isin' and c != 'Date'] print(f"\nPanel après merge : {panel.shape}") print(f"Colonnes de perf jointes : {perf_cols}") n_matched = panel[perf_cols[0]].notna().sum() if perf_cols else 0 print(f"Lignes avec performance jointe : {n_matched}/{len(panel)}") # ============================================================================= # 4. FEATURE ENGINEERING # ============================================================================= # # Features construites par compte × fonds × mois : # # [A] Performance absolue décalée # - perf_6Mo : rendement sur 6 mois (lag=0, observé à t) # - perf_1Yr : rendement sur 1 an # # [B] Performance relative (percentile Morningstar) # - pct_6Mo : percentile dans la catégorie sur 6 mois # - pct_1Yr : percentile dans la catégorie sur 1 an # # [C] Features client (RFM proxy depuis AUM) # - aum_t : encours à t (proxy du M de RFM) # - aum_lag1 : encours à t-1 mois # - aum_lag3 : encours à t-3 mois # - aum_growth_1m : croissance MoM de l'AUM # - aum_growth_3m : croissance sur 3 mois # # [D] Variable cible : flux_net_proxy = AUM(t+1) - AUM(t) # (approximation des flux nets en l'absence des transactions brutes) # NOTE : avec les données de flux bruts (souscriptions + rachats), # remplacer par flux_net = sum(souscriptions) - sum(rachats) # sur la période t → t+1. # # ============================================================================= print("\n" + "=" * 60) print("4. FEATURE ENGINEERING") print("=" * 60) # ── Tri du panel ────────────────────────────────────────────────────────────── panel = panel.sort_values(['Registrar Account - ID', 'Product - Isin', 'Centralisation Date']) # ── [C] Features AUM (par compte × fonds) ──────────────────────────────────── panel['aum_lag1'] = panel.groupby(['Registrar Account - ID', 'Product - Isin'])['Value - AUM €'].shift(1) panel['aum_lag3'] = panel.groupby(['Registrar Account - ID', 'Product - Isin'])['Value - AUM €'].shift(3) panel['aum_growth_1m'] = (panel['Value - AUM €'] - panel['aum_lag1']) / (panel['aum_lag1'].abs() + 1) panel['aum_growth_3m'] = (panel['Value - AUM €'] - panel['aum_lag3']) / (panel['aum_lag3'].abs() + 1) # ── [D] Variable cible : ΔAum(t → t+1) ────────────────────────────────────── panel['aum_next'] = panel.groupby(['Registrar Account - ID', 'Product - Isin'])['Value - AUM €'].shift(-1) panel['flux_net_proxy'] = panel['aum_next'] - panel['Value - AUM €'] # ── Sélection des features ──────────────────────────────────────────────────── # Colonnes de performance disponibles (dépend du contenu de perf) PERF_COLS_AVAILABLE = [c for c in perf_cols if any( tag in c for tag in ['6Mo', '1Yr', '6mo', '1yr', '6MoRet', '1YrRet'] )] PCT_COLS_AVAILABLE = [c for c in perf_cols if 'percentile' in c.lower()] # Si données head (seulement 1YrRet) → on utilise ce qui est disponible FEATURE_COLS = ( ['Value - AUM €', 'aum_lag1', 'aum_lag3', 'aum_growth_1m', 'aum_growth_3m'] + PERF_COLS_AVAILABLE + PCT_COLS_AVAILABLE ) FEATURE_COLS = [c for c in FEATURE_COLS if c in panel.columns] print(f"Features sélectionnées ({len(FEATURE_COLS)}) :") for f in FEATURE_COLS: n_valid = panel[f].notna().sum() print(f" {f:<40} → {n_valid:,} valeurs non-nulles") TARGET = 'flux_net_proxy' # ── Dataset modèle ──────────────────────────────────────────────────────────── model_data = panel.dropna(subset=FEATURE_COLS + [TARGET]).copy() print(f"\nDataset pour modélisation : {model_data.shape[0]:,} lignes") # ============================================================================= # 5. MODÈLE PRÉDICTIF — WALK-FORWARD VALIDATION # ============================================================================= # # Validation walk-forward (expanding window) : # - Évite le data leakage temporel # - À chaque fold : train = tout le passé, test = le mois suivant # - On calcule MAE, R² sur la fenêtre de test # # Modèles comparés : # 1. Baseline : moyenne mobile (benchmark naïf) # 2. Ridge Regression : modèle linéaire régularisé # 3. Random Forest : non-linéaire, robuste aux outliers # 4. Gradient Boosting : state-of-the-art sur données tabulaires # # ============================================================================= print("\n" + "=" * 60) print("5. WALK-FORWARD VALIDATION") print("=" * 60) if model_data.empty: print("⚠ Pas assez de données (fichiers head) pour la modélisation.") print(" Le pipeline est prêt — relancer avec les fichiers complets.") RUN_MODEL = False else: RUN_MODEL = True dates_sorted = sorted(model_data['Centralisation Date'].unique()) N_DATES = len(dates_sorted) MIN_TRAIN = max(2, N_DATES // 3) # au moins 1/3 des dates en train print(f"Dates disponibles : {N_DATES} | Min train : {MIN_TRAIN} snapshots") if RUN_MODEL and N_DATES > MIN_TRAIN: results = [] models = { 'Ridge': Ridge(alpha=1.0), 'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42, n_jobs=-1), 'Gradient Boost': GradientBoostingRegressor(n_estimators=100, max_depth=3, learning_rate=0.05, random_state=42), } scaler = StandardScaler() for test_idx in range(MIN_TRAIN, N_DATES): train_dates = dates_sorted[:test_idx] test_date = dates_sorted[test_idx] train = model_data[model_data['Centralisation Date'].isin(train_dates)] test = model_data[model_data['Centralisation Date'] == test_date] X_train = train[FEATURE_COLS].fillna(0) y_train = train[TARGET] X_test = test[FEATURE_COLS].fillna(0) y_test = test[TARGET] if len(X_test) == 0: continue X_train_sc = scaler.fit_transform(X_train) X_test_sc = scaler.transform(X_test) # Baseline : moyenne de l'AUM passé comme prédiction de flux baseline_pred = np.zeros(len(y_test)) baseline_mae = mean_absolute_error(y_test, baseline_pred) for model_name, model in models.items(): X_tr = X_train_sc if model_name == 'Ridge' else X_train X_te = X_test_sc if model_name == 'Ridge' else X_test model.fit(X_tr, y_train) preds = model.predict(X_te) results.append({ 'test_date': test_date, 'model': model_name, 'mae': mean_absolute_error(y_test, preds), 'r2': r2_score(y_test, preds) if len(y_test) > 1 else np.nan, 'baseline_mae': baseline_mae, 'n_test': len(y_test), }) results_df = pd.DataFrame(results) print("\nRésultats agrégés (médiane sur tous les folds) :") summary = (results_df.groupby('model') .agg(MAE_median=('mae', 'median'), R2_median=('r2', 'median'), MAE_mean=('mae', 'mean')) .round(4)) print(summary) baseline_mae_median = results_df['baseline_mae'].median() print(f"\nBaseline (zéro) MAE médiane : {baseline_mae_median:.4f}") else: if RUN_MODEL: print("⚠ Pas assez de dates distinctes pour le walk-forward.") print(" Modélisation ignorée sur données head — OK sur données complètes.") results_df = pd.DataFrame() # ============================================================================= # 6. IMPORTANCE DES VARIABLES # ============================================================================= print("\n" + "=" * 60) print("6. IMPORTANCE DES VARIABLES") print("=" * 60) if RUN_MODEL and not model_data.empty and len(model_data) > 10: X_all = model_data[FEATURE_COLS].fillna(0) y_all = model_data[TARGET] rf_final = RandomForestRegressor(n_estimators=200, max_depth=6, random_state=42, n_jobs=-1) rf_final.fit(X_all, y_all) importances = pd.Series(rf_final.feature_importances_, index=FEATURE_COLS).sort_values(ascending=False) print("Importance des features (Random Forest) :") print(importances.round(4).to_string()) # Permutation importance (plus robuste) perm = permutation_importance(rf_final, X_all, y_all, n_repeats=10, random_state=42, n_jobs=-1) perm_imp = pd.Series(perm.importances_mean, index=FEATURE_COLS).sort_values(ascending=False) print("\nPermutation importance :") print(perm_imp.round(4).to_string()) else: importances = pd.Series(dtype=float) perm_imp = pd.Series(dtype=float) print("Importance des variables : données insuffisantes (head CSV).") print("Simuler les noms de features attendues :") expected = FEATURE_COLS if FEATURE_COLS else [ 'Value - AUM €', 'aum_lag1', 'aum_lag3', 'aum_growth_1m', 'aum_growth_3m', 'return_6MoRet', 'return_1YrRet', 'percentile_6MoRet', 'percentile_1YrRet' ] print(" " + ", ".join(expected)) # ============================================================================= # 7. VISUALISATIONS # ============================================================================= print("\n" + "=" * 60) print("7. GÉNÉRATION DES VISUALISATIONS") print("=" * 60) fig = plt.figure(figsize=(18, 20)) fig.patch.set_facecolor('white') gs = gridspec.GridSpec(4, 2, figure=fig, hspace=0.45, wspace=0.35) # ── [A] Distribution des AUM par fonds ─────────────────────────────────────── ax1 = fig.add_subplot(gs[0, :]) aum_by_fund = stocks.groupby('strategy_name')['Value - AUM €'].sum().sort_values(ascending=False) bars = ax1.bar(aum_by_fund.index, aum_by_fund.values / 1e6, color=COLORS[:len(aum_by_fund)]) ax1.set_title('AUM total par fonds (données disponibles)', fontsize=13, fontweight='bold', pad=10) ax1.set_ylabel('AUM (M€)') ax1.tick_params(axis='x', rotation=20) for bar, val in zip(bars, aum_by_fund.values): ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, f'{val/1e6:.1f}M', ha='center', va='bottom', fontsize=8) # ── [B] Évolution temporelle de l'AUM ──────────────────────────────────────── ax2 = fig.add_subplot(gs[1, 0]) aum_time = stocks.groupby('Centralisation Date')['Value - AUM €'].sum() ax2.fill_between(aum_time.index, aum_time.values / 1e6, alpha=0.3, color=COLORS[0]) ax2.plot(aum_time.index, aum_time.values / 1e6, color=COLORS[0], linewidth=2) ax2.set_title('AUM agrégé — évolution temporelle', fontsize=12, fontweight='bold') ax2.set_ylabel('AUM (M€)') ax2.tick_params(axis='x', rotation=20) # ── [C] Distribution des performances ──────────────────────────────────────── ax3 = fig.add_subplot(gs[1, 1]) perf_cols_ret = [c for c in perf.columns if 'return' == c] if perf_cols_ret: for col in perf_cols_ret[:3]: ax3.hist(perf[col].dropna(), bins=30, alpha=0.6, label=col) ax3.legend() else: ax3.hist(perf['return'].dropna(), bins=30, color=COLORS[1], alpha=0.8, edgecolor='white') ax3.set_xlabel('Rendement 1 an (%)') ax3.set_title('Distribution des performances (1YrRet)', fontsize=12, fontweight='bold') ax3.set_ylabel('Fréquence') # ── [D] Scatter : performance vs percentile ────────────────────────────────── ax4 = fig.add_subplot(gs[2, 0]) if 'return' in perf.columns and 'percentile' in perf.columns: sc = ax4.scatter(perf['return'], perf['percentile'], alpha=0.5, c=COLORS[0], edgecolors='none', s=25) ax4.set_xlabel('Rendement 1 an (%)') ax4.set_ylabel('Percentile dans la catégorie') ax4.set_title('Performance vs Rang relatif (peer percentile)', fontsize=12, fontweight='bold') # Ligne de référence médiane ax4.axhline(50, color='red', linestyle='--', alpha=0.5, label='Médiane (50e pct)') ax4.legend(fontsize=9) # ── [E] Importance des variables (si disponible) ───────────────────────────── ax5 = fig.add_subplot(gs[2, 1]) if not importances.empty: colors_imp = [COLORS[2] if 'perf' in f or 'return' in f or 'percentile' in f else COLORS[0] for f in importances.index] ax5.barh(importances.index[::-1], importances.values[::-1], color=colors_imp[::-1]) ax5.set_title('Importance des features (Random Forest)', fontsize=12, fontweight='bold') ax5.set_xlabel('Importance (Gini impurity)') # Légende from matplotlib.patches import Patch legend_els = [Patch(color=COLORS[2], label='Features performance'), Patch(color=COLORS[0], label='Features AUM/comportement')] ax5.legend(handles=legend_els, fontsize=8) else: # Afficher le schéma du pipeline à la place ax5.axis('off') pipeline_text = ( "PIPELINE — FEATURES ATTENDUES\n\n" "■ AUM features (comportement):\n" " • Value - AUM € (encours actuel)\n" " • aum_lag1, aum_lag3\n" " • aum_growth_1m, aum_growth_3m\n\n" "■ Performance features (moyen terme):\n" " • return_6MoRet\n" " • return_1YrRet\n\n" "■ Relative performance (peer):\n" " • percentile_6MoRet\n" " • percentile_1YrRet\n\n" "→ Relancer avec données complètes\n" " pour obtenir les importances réelles." ) ax5.text(0.05, 0.95, pipeline_text, transform=ax5.transAxes, fontsize=9.5, verticalalignment='top', fontfamily='monospace', bbox=dict(boxstyle='round', facecolor='#eaf2fb', alpha=0.8)) ax5.set_title('Features du modèle', fontsize=12, fontweight='bold') # ── [F] Résultats walk-forward (si disponible) ─────────────────────────────── ax6 = fig.add_subplot(gs[3, :]) if not results_df.empty: for model_name, grp in results_df.groupby('model'): ax6.plot(grp['test_date'], grp['mae'], marker='o', label=model_name, linewidth=1.5) ax6.axhline(results_df['baseline_mae'].median(), color='black', linestyle='--', label='Baseline (zéro)', linewidth=1.5) ax6.set_title('Walk-Forward Validation — MAE par modèle', fontsize=12, fontweight='bold') ax6.set_ylabel('MAE (€)') ax6.legend() ax6.tick_params(axis='x', rotation=20) else: ax6.axis('off') # Schéma du walk-forward ax6.set_xlim(0, 10) ax6.set_ylim(0, 3) ax6.set_title('Walk-Forward Validation — Schéma', fontsize=12, fontweight='bold') ax6.set_facecolor('white') colors_wf = [COLORS[0], COLORS[2], COLORS[3]] for fold_i in range(5): # Fenêtre train ax6.barh(2, fold_i + 2, left=0, height=0.35, color=COLORS[0], alpha=0.3 + fold_i * 0.08) # Fenêtre test ax6.barh(2, 1, left=fold_i + 2, height=0.35, color=COLORS[3], alpha=0.8) ax6.text(3, 2.55, 'Train (expanding window)', fontsize=10, color=COLORS[0], fontweight='bold') ax6.text(5.5, 2.55, 'Test', fontsize=10, color=COLORS[3], fontweight='bold') ax6.text(0.2, 1.4, "Fold 1 : train t₁…t₂ → test t₃\n" "Fold 2 : train t₁…t₃ → test t₄\n" "Fold 3 : train t₁…t₄ → test t₅\n" " ...\n" "→ Évite tout data leakage temporel\n" "→ MAE et R² calculés sur chaque fenêtre de test", fontsize=10, fontfamily='monospace', bbox=dict(boxstyle='round', facecolor='#eaf2fb', alpha=0.8)) ax6.set_yticks([]) ax6.set_xticks([]) plt.suptitle('Carmignac × ENSAE — Pipeline : Performance → Flux nets', fontsize=15, fontweight='bold', y=1.01) output_path = '/mnt/user-data/outputs/carmignac_pipeline_viz.png' plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white') plt.close() print(f"✅ Visualisation sauvegardée : {output_path}") # ============================================================================= # 8. RÉSUMÉ & INSTRUCTIONS POUR LES DONNÉES COMPLÈTES # ============================================================================= print("\n" + "=" * 60) print("8. RÉSUMÉ & PROCHAINES ÉTAPES") print("=" * 60) print(""" PIPELINE IMPLÉMENTÉ ─────────────────── Étape 1 — Chargement • equity_stocks_head.csv : AUM mensuels par (compte, fonds, shareclass) • weekly_perf_head.csv : performances hebdomadaires par shareclass Étape 2 — Jointure (clé construite) • Parsing shareClass_name → (strategy, type, currency) • Matching vers ISIN via stocks • merge_asof temporel (tolérance ±35j) ⚠ En production : utiliser Peers.csv (Morningstar) comme table de référence ISIN complète pour éviter les non-matchés. Étape 3 — Feature Engineering • AUM features : lag 1m, lag 3m, croissance 1m, croissance 3m • Perf absolue : return_6MoRet, return_1YrRet (lags à t) • Perf relative : percentile_6MoRet, percentile_1YrRet (vs peers) • Variable cible : ΔAum(t→t+1) [proxy flux nets] ⚠ En production : remplacer ΔAum par flux_net = souscriptions - rachats Étape 4 — Modèles • Baseline : prédiction zéro • Ridge Regression (linéaire régularisée) • Random Forest (non-linéaire, robuste) • Gradient Boosting (state-of-the-art tabulaire) Étape 5 — Validation • Walk-forward expanding window (pas de data leakage) • Métriques : MAE, R² POUR LANCER SUR LES DONNÉES COMPLÈTES ────────────────────────────────────── 1. Remplacer PATH_STOCKS et PATH_PERF par les vrais fichiers 2. Ajouter le fichier Peers.csv dans la fonction parse_shareclass_name (jointure directe par ISIN si disponible dans perf complet) 3. Remplacer flux_net_proxy par les vraies transactions brutes (fichier flux quotidiens → agrégation mensuelle par compte × fonds) 4. Ajouter des features macro (€STR, indices obligataires) depuis market_data/esterRates.csv et Eur_Gov_Indices.xlsx LECTURE DES RÉSULTATS ────────────────────── La littérature (Sirri & Tufano 1998) prédit une relation CONVEXE : → Les fonds en haut de percentile (top quartile) attirent des flux disproportionnés → Les fonds en bas ne perdent pas symétriquement (« smart money ») → Tester une feature non-linéaire : percentile² ou dummy top/bottom quartile """)