Project_Carmignac/carmignac_pipeline.py
2026-03-10 22:01:34 +00:00

630 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
=============================================================================
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
""")