630 lines
28 KiB
Python
630 lines
28 KiB
Python
"""
|
||
=============================================================================
|
||
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
|
||
""")
|