{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ae7ea32e",
   "metadata": {},
   "source": [
    "# Clustering by Fund Family — Final Notebook\n",
    "\n",
    "Ce notebook construit des **familles de fonds**, crée des **features comportementales au niveau client × famille**, réalise un **clustering séparé pour chacune des top 15 familles**, puis ajoute des **analyses métier** :\n",
    "- taille économique des clusters,\n",
    "- churn par cluster,\n",
    "- comportement vs performance,\n",
    "- visualisation PCA."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "fb3f5ba7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import re\n",
    "import s3fs\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "from sklearn.preprocessing import RobustScaler\n",
    "from sklearn.cluster import KMeans\n",
    "from sklearn.metrics import silhouette_score, davies_bouldin_score\n",
    "from sklearn.decomposition import PCA\n",
    "from sklearn.impute import SimpleImputer\n",
    "\n",
    "sns.set_style(\"whitegrid\")\n",
    "pd.set_option(\"display.max_columns\", 200)\n",
    "pd.set_option(\"display.max_rows\", 200)\n",
    "\n",
    "EPS = 1e-9\n",
    "RANDOM_STATE = 42"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "945ca540",
   "metadata": {},
   "source": [
    "## 1. Accès aux données\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2d063d7d",
   "metadata": {},
   "outputs": [],
   "source": [
    "ID_COL        = \"Registrar Account - ID\"\n",
    "ISIN_COL      = \"Product - Isin\"\n",
    "FUND_COL      = \"Product - Fund\"\n",
    "ASSET_COL     = \"Product - Asset Type\"\n",
    "\n",
    "FLOW_DATE_COL = \"Centralisation Date\"\n",
    "AUM_DATE_COL  = \"Centralisation Date\"\n",
    "\n",
    "FLOW_QTY_COL  = \"Quantity - NetFlows\"\n",
    "FLOW_SUB_COL  = \"Quantity - Subscription\"\n",
    "FLOW_RED_COL  = \"Quantity - Redemption\"\n",
    "\n",
    "AUM_QTY_COL   = \"Quantity - AUM\"\n",
    "AUM_VAL_COL   = \"Value - AUM €\"\n",
    "\n",
    "REGION_COL    = \"Registrar Account - Region\"\n",
    "COUNTRY_COL   = \"RegistrarAccount - Country\"\n",
    "\n",
    "NAV_DATE_COL  = \"Dat\"\n",
    "NAV_ISIN_COL  = \"Isin\"\n",
    "NAV_PRICE_COL = \"Price (TF PartPrice)\"\n",
    "\n",
    "RATE_DATE_COL = \"Date\"\n",
    "RATE_VAL_COL  = \"Yld to Maturity\"\n",
    "\n",
    "PATH_FLOWS = \"s3://projet-bdc-data/carmignac/Flows ENSAE V2 -20251105.csv\"\n",
    "PATH_NAV   = \"s3://projet-bdc-data/carmignac/Data Modélisation/Nav/NAV_Bench_data.csv\"\n",
    "PATH_RATES = \"s3://projet-bdc-data/carmignac/Data Modélisation/market data/esterRates.csv\"\n",
    "PATH_AUM   = \"s3://projet-bdc-carmignac-g3/paco/AUM_repaired.csv\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "aeddcb9b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def robust_zscore(s):\n",
    "    med = np.nanmedian(s)\n",
    "    mad = np.nanmedian(np.abs(s - med))\n",
    "    if mad == 0 or np.isnan(mad):\n",
    "        return np.zeros(len(s))\n",
    "    return (s - med) / (1.4826 * mad)\n",
    "\n",
    "def plot_heatmap(dfc, profile_vars, cluster_col, title, figsize=(16, 5)):\n",
    "    dfc_viz = dfc[profile_vars + [cluster_col]].copy()\n",
    "    for col in profile_vars:\n",
    "        vals = pd.to_numeric(dfc_viz[col], errors=\"coerce\").to_numpy(dtype=float)\n",
    "        lo = np.nanpercentile(vals, 2)\n",
    "        hi = np.nanpercentile(vals, 98)\n",
    "        dfc_viz[col] = np.clip(vals, lo, hi)\n",
    "    prof = dfc_viz.groupby(cluster_col)[profile_vars].median()\n",
    "    prof_z = prof.apply(lambda col: robust_zscore(col.values), axis=0)\n",
    "    prof_z = prof_z.clip(-3, 3)\n",
    "    plt.figure(figsize=figsize)\n",
    "    sns.heatmap(\n",
    "        prof_z, cmap=\"RdBu_r\", center=0, annot=True, fmt=\".2f\",\n",
    "        xticklabels=profile_vars,\n",
    "        yticklabels=[f\"Cluster {i}\" for i in prof.index]\n",
    "    )\n",
    "    plt.title(title)\n",
    "    plt.xticks(rotation=45, ha=\"right\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    return prof\n",
    "\n",
    "def winsorize_mad(series, n_sigma=3):\n",
    "    vals = pd.to_numeric(series, errors=\"coerce\").to_numpy(dtype=float)\n",
    "    med = np.nanmedian(vals)\n",
    "    mad = np.nanmedian(np.abs(vals - med)) * 1.4826\n",
    "    if mad > 0:\n",
    "        return np.clip(vals, med - n_sigma * mad, med + n_sigma * mad)\n",
    "    return np.clip(vals, 0, np.nanpercentile(vals, 95))\n",
    "\n",
    "def add_months_since_last_tx_by_group(df, id_cols, active_col=\"active_month\", month_col=\"month\", suffix=\"\"):\n",
    "    col_name = f\"months_since_last_tx{suffix}\"\n",
    "    reference_date = df[month_col].max()\n",
    "    last_active = (\n",
    "        df[df[active_col] == 1]\n",
    "        .groupby(id_cols)[month_col]\n",
    "        .max()\n",
    "        .reset_index(name=\"last_active_month\")\n",
    "    )\n",
    "    last_active[col_name] = (\n",
    "        (reference_date.to_period(\"M\") - last_active[\"last_active_month\"].dt.to_period(\"M\"))\n",
    "        .apply(lambda x: x.n)\n",
    "    )\n",
    "    return last_active[[*id_cols, col_name]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "041c588b",
   "metadata": {},
   "source": [
    "## 2. Chargement et nettoyage"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "7a2e4bb5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "flows: (2514419, 25)\n",
      "aum:   (4824814, 19)\n",
      "nav:   (623914, 6)\n",
      "rates: (2826, 3)\n"
     ]
    }
   ],
   "source": [
    "df_flows = pd.read_csv(PATH_FLOWS, sep=\";\")\n",
    "df_nav   = pd.read_csv(PATH_NAV, sep=\";\")\n",
    "df_rates = pd.read_csv(PATH_RATES, sep=\";\")\n",
    "df_aum   = pd.read_csv(PATH_AUM, low_memory=False)\n",
    "\n",
    "for df, col in [\n",
    "    (df_flows, FLOW_DATE_COL),\n",
    "    (df_aum,   AUM_DATE_COL),\n",
    "    (df_nav,   NAV_DATE_COL),\n",
    "    (df_rates, RATE_DATE_COL)\n",
    "]:\n",
    "    df[col] = pd.to_datetime(df[col], errors=\"coerce\")\n",
    "    df[\"month\"] = df[col].dt.to_period(\"M\").dt.to_timestamp()\n",
    "\n",
    "for col in [FLOW_QTY_COL, FLOW_SUB_COL, FLOW_RED_COL]:\n",
    "    df_flows[col] = pd.to_numeric(df_flows[col], errors=\"coerce\")\n",
    "\n",
    "for col in [AUM_QTY_COL, AUM_VAL_COL]:\n",
    "    df_aum[col] = pd.to_numeric(df_aum[col], errors=\"coerce\")\n",
    "\n",
    "df_nav[NAV_PRICE_COL] = pd.to_numeric(df_nav[NAV_PRICE_COL], errors=\"coerce\")\n",
    "df_rates[RATE_VAL_COL] = pd.to_numeric(df_rates[RATE_VAL_COL], errors=\"coerce\")\n",
    "\n",
    "for df in [df_flows, df_aum]:\n",
    "    df[ISIN_COL] = df[ISIN_COL].astype(str).str.strip()\n",
    "    df[FUND_COL] = df[FUND_COL].astype(str).str.strip()\n",
    "\n",
    "df_nav[NAV_ISIN_COL] = df_nav[NAV_ISIN_COL].astype(str).str.strip()\n",
    "\n",
    "bad_accounts = [\"Off Distribution\", \"Private Clients\", \"Private Client\"]\n",
    "df_flows = df_flows[~df_flows[ID_COL].isin(bad_accounts)]\n",
    "df_aum   = df_aum[~df_aum[ID_COL].isin(bad_accounts)]\n",
    "\n",
    "print(\"flows:\", df_flows.shape)\n",
    "print(\"aum:  \", df_aum.shape)\n",
    "print(\"nav:  \", df_nav.shape)\n",
    "print(\"rates:\", df_rates.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66fbc3b9",
   "metadata": {},
   "source": [
    "## 3. Construction conservative des fund families"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "9aab6824",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Nombre de couples ISIN/Fund : 387\n",
      "Nombre de funds uniques     : 69\n",
      "Nombre de families uniques  : 65\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Product - Isin</th>\n",
       "      <th>Product - Fund</th>\n",
       "      <th>fund_family</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>FR0000999999</td>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>FR0010135103</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>FR0010147603</td>\n",
       "      <td>Carmignac Investissement Latitude</td>\n",
       "      <td>Carmignac Investissement Latitude</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>FR0010148981</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>FR0010148999</td>\n",
       "      <td>Carmignac Profil Réactif 75</td>\n",
       "      <td>Carmignac Profil Réactif &lt;NUM&gt;</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>FR0010149096</td>\n",
       "      <td>Carmignac Innovation</td>\n",
       "      <td>Carmignac Innovation</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>FR0010149112</td>\n",
       "      <td>Carmignac Euro-Entrepreneurs</td>\n",
       "      <td>Carmignac Euro-Entrepreneurs</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>FR0010149120</td>\n",
       "      <td>Carmignac Sécurité</td>\n",
       "      <td>Carmignac Sécurité</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>FR0010149161</td>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>FR0010149179</td>\n",
       "      <td>Carmignac Absolute Return Europe</td>\n",
       "      <td>Carmignac Absolute Return Europe</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>FR0010149203</td>\n",
       "      <td>Carmignac Multi Expertise</td>\n",
       "      <td>Carmignac Multi Expertise</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>FR0010149211</td>\n",
       "      <td>Carmignac Profil Réactif 100</td>\n",
       "      <td>Carmignac Profil Réactif &lt;NUM&gt;</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>FR0010149278</td>\n",
       "      <td>Carmignac Euro-Investissement</td>\n",
       "      <td>Carmignac Euro-Investissement</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>FR0010149302</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>FR0010306142</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>FR0010312660</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>FR0010956607</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>FR0010956615</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "      <td>Carmignac Investissement</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>FR0010956649</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>FR0011147446</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   Product - Isin                     Product - Fund  \\\n",
       "0    FR0000999999              Carmignac Court Terme   \n",
       "1    FR0010135103               Carmignac Patrimoine   \n",
       "2    FR0010147603  Carmignac Investissement Latitude   \n",
       "3    FR0010148981           Carmignac Investissement   \n",
       "4    FR0010148999        Carmignac Profil Réactif 75   \n",
       "5    FR0010149096               Carmignac Innovation   \n",
       "6    FR0010149112       Carmignac Euro-Entrepreneurs   \n",
       "7    FR0010149120                 Carmignac Sécurité   \n",
       "8    FR0010149161              Carmignac Court Terme   \n",
       "9    FR0010149179   Carmignac Absolute Return Europe   \n",
       "10   FR0010149203          Carmignac Multi Expertise   \n",
       "11   FR0010149211       Carmignac Profil Réactif 100   \n",
       "12   FR0010149278      Carmignac Euro-Investissement   \n",
       "13   FR0010149302                Carmignac Emergents   \n",
       "14   FR0010306142               Carmignac Patrimoine   \n",
       "15   FR0010312660           Carmignac Investissement   \n",
       "16   FR0010956607                Carmignac Emergents   \n",
       "17   FR0010956615           Carmignac Investissement   \n",
       "18   FR0010956649               Carmignac Patrimoine   \n",
       "19   FR0011147446                Carmignac Emergents   \n",
       "\n",
       "                          fund_family  \n",
       "0               Carmignac Court Terme  \n",
       "1                Carmignac Patrimoine  \n",
       "2   Carmignac Investissement Latitude  \n",
       "3            Carmignac Investissement  \n",
       "4      Carmignac Profil Réactif <NUM>  \n",
       "5                Carmignac Innovation  \n",
       "6        Carmignac Euro-Entrepreneurs  \n",
       "7                  Carmignac Sécurité  \n",
       "8               Carmignac Court Terme  \n",
       "9    Carmignac Absolute Return Europe  \n",
       "10          Carmignac Multi Expertise  \n",
       "11     Carmignac Profil Réactif <NUM>  \n",
       "12      Carmignac Euro-Investissement  \n",
       "13                Carmignac Emergents  \n",
       "14               Carmignac Patrimoine  \n",
       "15           Carmignac Investissement  \n",
       "16                Carmignac Emergents  \n",
       "17           Carmignac Investissement  \n",
       "18               Carmignac Patrimoine  \n",
       "19                Carmignac Emergents  "
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def build_fund_family_numeric_only(fund_name):\n",
    "    if pd.isna(fund_name):\n",
    "        return np.nan\n",
    "    s = str(fund_name).strip()\n",
    "    family = re.sub(r\"\\d+\", \"<NUM>\", s)\n",
    "    family = re.sub(r\"\\s+\", \" \", family).strip()\n",
    "    return family\n",
    "\n",
    "fund_family_table = (\n",
    "    df_aum[[ISIN_COL, FUND_COL]]\n",
    "    .dropna(subset=[ISIN_COL, FUND_COL])\n",
    "    .drop_duplicates()\n",
    "    .sort_values([ISIN_COL, FUND_COL])\n",
    "    .reset_index(drop=True)\n",
    ")\n",
    "\n",
    "fund_family_table[\"fund_family\"] = fund_family_table[FUND_COL].apply(build_fund_family_numeric_only)\n",
    "\n",
    "print(\"Nombre de couples ISIN/Fund :\", fund_family_table.shape[0])\n",
    "print(\"Nombre de funds uniques     :\", fund_family_table[FUND_COL].nunique())\n",
    "print(\"Nombre de families uniques  :\", fund_family_table[\"fund_family\"].nunique())\n",
    "\n",
    "fund_family_table.head(20)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51efc12a",
   "metadata": {},
   "source": [
    "## 4. Panel relationnel mensuel client × ISIN × mois"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0a45bb6d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "df_rel_m shape: (5184382, 20)\n"
     ]
    }
   ],
   "source": [
    "df_flows_m = (\n",
    "    df_flows\n",
    "    .dropna(subset=[ID_COL, ISIN_COL, \"month\"])\n",
    "    .assign(\n",
    "        gross_flow_qty=lambda x: x[FLOW_QTY_COL].abs(),\n",
    "        sub_qty=lambda x: x[FLOW_SUB_COL].fillna(0),\n",
    "        red_qty=lambda x: x[FLOW_RED_COL].fillna(0)\n",
    "    )\n",
    "    .groupby([ID_COL, ISIN_COL, \"month\"], as_index=False)\n",
    "    .agg(\n",
    "        net_flow_qty=(FLOW_QTY_COL, \"sum\"),\n",
    "        gross_flow_qty=(\"gross_flow_qty\", \"sum\"),\n",
    "        sub_qty=(\"sub_qty\", \"sum\"),\n",
    "        red_qty=(\"red_qty\", \"sum\"),\n",
    "        n_tx=(FLOW_QTY_COL, \"size\"),\n",
    "        region=(REGION_COL, \"last\"),\n",
    "        country=(COUNTRY_COL, \"last\")\n",
    "    )\n",
    ")\n",
    "\n",
    "df_aum_m = (\n",
    "    df_aum\n",
    "    .dropna(subset=[ID_COL, ISIN_COL, FUND_COL, \"month\"])\n",
    "    .groupby([ID_COL, ISIN_COL, FUND_COL, \"month\"], as_index=False)\n",
    "    .agg(\n",
    "        aum_qty=(AUM_QTY_COL, \"sum\"),\n",
    "        aum_val=(AUM_VAL_COL, \"sum\"),\n",
    "        asset_type=(ASSET_COL, \"last\"),\n",
    "        region=(REGION_COL, \"last\"),\n",
    "        country=(COUNTRY_COL, \"last\")\n",
    "    )\n",
    ")\n",
    "\n",
    "keys = pd.concat([\n",
    "    df_flows_m[[ID_COL, ISIN_COL, \"month\"]],\n",
    "    df_aum_m[[ID_COL, ISIN_COL, \"month\"]]\n",
    "]).drop_duplicates()\n",
    "\n",
    "df_rel_m = (\n",
    "    keys\n",
    "    .merge(df_aum_m, on=[ID_COL, ISIN_COL, \"month\"], how=\"left\")\n",
    "    .merge(df_flows_m, on=[ID_COL, ISIN_COL, \"month\"], how=\"left\", suffixes=(\"\", \"_flow\"))\n",
    ")\n",
    "\n",
    "for c in [\"aum_qty\", \"aum_val\", \"net_flow_qty\", \"gross_flow_qty\", \"sub_qty\", \"red_qty\", \"n_tx\"]:\n",
    "    df_rel_m[c] = df_rel_m[c].fillna(0)\n",
    "\n",
    "df_rel_m[\"region\"]  = df_rel_m[\"region\"].fillna(df_rel_m.get(\"region_flow\"))\n",
    "df_rel_m[\"country\"] = df_rel_m[\"country\"].fillna(df_rel_m.get(\"country_flow\"))\n",
    "\n",
    "df_rel_m[\"active_rel_month\"]  = (df_rel_m[\"gross_flow_qty\"] > 0).astype(int)\n",
    "df_rel_m[\"holding_rel_month\"] = (df_rel_m[\"aum_qty\"] > 0).astype(int)\n",
    "\n",
    "df_rel_m[\"flow_to_aum_rel\"] = np.where(\n",
    "    df_rel_m[\"aum_qty\"].abs() > 0,\n",
    "    df_rel_m[\"net_flow_qty\"] / df_rel_m[\"aum_qty\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "df_rel_m[\"turnover_rel\"] = np.where(\n",
    "    df_rel_m[\"aum_qty\"].abs() > 0,\n",
    "    df_rel_m[\"gross_flow_qty\"] / df_rel_m[\"aum_qty\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "print(\"df_rel_m shape:\", df_rel_m.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f01d71f0",
   "metadata": {},
   "source": [
    "## 5. NAV mensuel et taux"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "7f9c6e2a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Panel with NAV/rates: (5184382, 22)\n"
     ]
    }
   ],
   "source": [
    "df_nav_m = (\n",
    "    df_nav\n",
    "    .dropna(subset=[NAV_ISIN_COL, \"month\", NAV_PRICE_COL])\n",
    "    .sort_values([NAV_ISIN_COL, \"month\"])\n",
    "    .groupby([NAV_ISIN_COL, \"month\"], as_index=False)\n",
    "    .tail(1)\n",
    ")\n",
    "\n",
    "df_nav_m[\"ret_fund_m\"] = df_nav_m.groupby(NAV_ISIN_COL)[NAV_PRICE_COL].pct_change()\n",
    "\n",
    "df_nav_m = df_nav_m.rename(columns={NAV_ISIN_COL: ISIN_COL})[\n",
    "    [ISIN_COL, \"month\", \"ret_fund_m\"]\n",
    "]\n",
    "\n",
    "df_rates_m = (\n",
    "    df_rates\n",
    "    .dropna(subset=[\"month\", RATE_VAL_COL])\n",
    "    .sort_values(RATE_DATE_COL)\n",
    "    .groupby(\"month\", as_index=False)\n",
    "    .tail(1)\n",
    ")\n",
    "\n",
    "df_rates_m[\"delta_rate_m\"] = df_rates_m[RATE_VAL_COL].diff()\n",
    "df_rates_m = df_rates_m[[\"month\", \"delta_rate_m\"]]\n",
    "\n",
    "df_rel_m = df_rel_m.merge(df_nav_m, on=[ISIN_COL, \"month\"], how=\"left\")\n",
    "df_rel_m = df_rel_m.merge(df_rates_m, on=\"month\", how=\"left\")\n",
    "\n",
    "for c in [\"ret_fund_m\", \"delta_rate_m\"]:\n",
    "    df_rel_m[c] = df_rel_m[c].fillna(0)\n",
    "\n",
    "print(\"Panel with NAV/rates:\", df_rel_m.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bfa5ec6",
   "metadata": {},
   "source": [
    "## 6. Merge des familles"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "70386604",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Missing fund_family: 527529\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Product - Isin</th>\n",
       "      <th>Product - Fund</th>\n",
       "      <th>fund_family</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>LU0099161993</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>LU0164455502</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>LU0336083497</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>LU0336083810</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>LU0553415323</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>LU0592698954</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>29</th>\n",
       "      <td>LU0592699093</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>30</th>\n",
       "      <td>LU0592699259</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>37</th>\n",
       "      <td>LU0705572823</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>40</th>\n",
       "      <td>LU0807689582</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>41</th>\n",
       "      <td>LU0807690838</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>42</th>\n",
       "      <td>LU0992624949</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>53</th>\n",
       "      <td>LU0992625086</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>57</th>\n",
       "      <td>LU0992626993</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>60</th>\n",
       "      <td>LU0992627611</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>67</th>\n",
       "      <td>LU0992629153</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>69</th>\n",
       "      <td>LU0992629401</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>71</th>\n",
       "      <td>LU0992631563</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>73</th>\n",
       "      <td>LU1163533422</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>79</th>\n",
       "      <td>LU1299306321</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   Product - Isin Product - Fund fund_family\n",
       "0    LU0099161993            NaN         NaN\n",
       "3    LU0164455502            NaN         NaN\n",
       "12   LU0336083497            NaN         NaN\n",
       "15   LU0336083810            NaN         NaN\n",
       "19   LU0553415323            NaN         NaN\n",
       "21   LU0592698954            NaN         NaN\n",
       "29   LU0592699093            NaN         NaN\n",
       "30   LU0592699259            NaN         NaN\n",
       "37   LU0705572823            NaN         NaN\n",
       "40   LU0807689582            NaN         NaN\n",
       "41   LU0807690838            NaN         NaN\n",
       "42   LU0992624949            NaN         NaN\n",
       "53   LU0992625086            NaN         NaN\n",
       "57   LU0992626993            NaN         NaN\n",
       "60   LU0992627611            NaN         NaN\n",
       "67   LU0992629153            NaN         NaN\n",
       "69   LU0992629401            NaN         NaN\n",
       "71   LU0992631563            NaN         NaN\n",
       "73   LU1163533422            NaN         NaN\n",
       "79   LU1299306321            NaN         NaN"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df_rel_m = df_rel_m.merge(\n",
    "    fund_family_table[[ISIN_COL, FUND_COL, \"fund_family\"]],\n",
    "    on=[ISIN_COL, FUND_COL],\n",
    "    how=\"left\"\n",
    ")\n",
    "\n",
    "print(\"Missing fund_family:\", df_rel_m[\"fund_family\"].isna().sum())\n",
    "df_rel_m[[ISIN_COL, FUND_COL, \"fund_family\"]].drop_duplicates().head(20)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67f27ac7",
   "metadata": {},
   "source": [
    "## 7. Panel mensuel client × family × mois"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "6e7b1b9e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "df_family_month shape: (3805010, 23)\n"
     ]
    }
   ],
   "source": [
    "tmp = df_rel_m.copy()\n",
    "\n",
    "tmp[\"isin_held_flag\"]   = (tmp[\"aum_qty\"] > 0).astype(int)\n",
    "tmp[\"isin_active_flag\"] = (tmp[\"gross_flow_qty\"] > 0).astype(int)\n",
    "\n",
    "df_family_month = (\n",
    "    tmp\n",
    "    .dropna(subset=[ID_COL, \"fund_family\", \"month\"])\n",
    "    .groupby([ID_COL, \"fund_family\", \"month\"], as_index=False)\n",
    "    .agg(\n",
    "        aum_qty=(\"aum_qty\", \"sum\"),\n",
    "        aum_val=(\"aum_val\", \"sum\"),\n",
    "        net_flow_qty=(\"net_flow_qty\", \"sum\"),\n",
    "        gross_flow_qty=(\"gross_flow_qty\", \"sum\"),\n",
    "        sub_qty=(\"sub_qty\", \"sum\"),\n",
    "        red_qty=(\"red_qty\", \"sum\"),\n",
    "        n_tx=(\"n_tx\", \"sum\"),\n",
    "        n_isin_held=(\"isin_held_flag\", \"sum\"),\n",
    "        n_isin_active=(\"isin_active_flag\", \"sum\"),\n",
    "        ret_fund_m=(\"ret_fund_m\", \"mean\"),\n",
    "        delta_rate_m=(\"delta_rate_m\", \"first\"),\n",
    "        region=(\"region\", \"first\"),\n",
    "        country=(\"country\", \"first\")\n",
    "    )\n",
    "    .sort_values([ID_COL, \"fund_family\", \"month\"])\n",
    "    .reset_index(drop=True)\n",
    ")\n",
    "\n",
    "df_family_month[\"active_month\"] = (df_family_month[\"gross_flow_qty\"] > 0).astype(int)\n",
    "\n",
    "df_family_month[\"flow_to_aum_m\"] = np.where(\n",
    "    df_family_month[\"aum_qty\"].abs() > 0,\n",
    "    df_family_month[\"net_flow_qty\"] / df_family_month[\"aum_qty\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "df_family_month[\"turnover_m\"] = np.where(\n",
    "    df_family_month[\"aum_qty\"].abs() > 0,\n",
    "    df_family_month[\"gross_flow_qty\"] / df_family_month[\"aum_qty\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "df_family_month[\"sub_share_m\"] = np.where(\n",
    "    df_family_month[\"gross_flow_qty\"] > 0,\n",
    "    df_family_month[\"sub_qty\"] / df_family_month[\"gross_flow_qty\"],\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "df_family_month[\"red_share_m\"] = np.where(\n",
    "    df_family_month[\"gross_flow_qty\"] > 0,\n",
    "    df_family_month[\"red_qty\"] / df_family_month[\"gross_flow_qty\"],\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "df_family_month[\"aum_peak_to_date\"] = df_family_month.groupby([ID_COL, \"fund_family\"])[\"aum_qty\"].cummax()\n",
    "\n",
    "df_family_month[\"aum_drawdown\"] = np.where(\n",
    "    df_family_month[\"aum_peak_to_date\"] > 0,\n",
    "    1 - df_family_month[\"aum_qty\"] / df_family_month[\"aum_peak_to_date\"],\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "print(\"df_family_month shape:\", df_family_month.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e25471b",
   "metadata": {},
   "source": [
    "## 8. Part de la famille dans le portefeuille client"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "e8a57c76",
   "metadata": {},
   "outputs": [],
   "source": [
    "df_client_total_month = (\n",
    "    df_rel_m\n",
    "    .groupby([ID_COL, \"month\"], as_index=False)\n",
    "    .agg(\n",
    "        total_client_aum_qty=(\"aum_qty\", \"sum\"),\n",
    "        total_client_aum_val=(\"aum_val\", \"sum\")\n",
    "    )\n",
    ")\n",
    "\n",
    "df_family_month = df_family_month.merge(\n",
    "    df_client_total_month,\n",
    "    on=[ID_COL, \"month\"],\n",
    "    how=\"left\"\n",
    ")\n",
    "\n",
    "df_family_month[\"family_share_of_client_aum\"] = np.where(\n",
    "    df_family_month[\"total_client_aum_val\"].abs() > 0,\n",
    "    df_family_month[\"aum_val\"] / df_family_month[\"total_client_aum_val\"].abs(),\n",
    "    np.nan\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8dfff3a",
   "metadata": {},
   "source": [
    "## 9. Features temporelles au niveau client × family × mois"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "734e6d8a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(3805010, 49)\n"
     ]
    }
   ],
   "source": [
    "tmp = df_family_month.sort_values([ID_COL, \"fund_family\", \"month\"]).reset_index(drop=True)\n",
    "\n",
    "tmp[\"prev_aum\"] = tmp.groupby([ID_COL, \"fund_family\"])[\"aum_qty\"].shift(1)\n",
    "tmp[\"entry_event\"] = ((tmp[\"prev_aum\"].fillna(0) <= 0) & (tmp[\"aum_qty\"] > 0)).astype(int)\n",
    "tmp[\"full_exit_event\"] = ((tmp[\"prev_aum\"] > 0) & (tmp[\"aum_qty\"] <= 0)).astype(int)\n",
    "\n",
    "tmp[\"ret_fund_m_lag1\"] = tmp.groupby([ID_COL, \"fund_family\"])[\"ret_fund_m\"].shift(1)\n",
    "\n",
    "tmp[\"buy_on_perf\"] = ((tmp[\"net_flow_qty\"] > 0) & (tmp[\"ret_fund_m_lag1\"] > 0)).astype(int)\n",
    "tmp[\"sell_on_perf\"] = ((tmp[\"net_flow_qty\"] < 0) & (tmp[\"ret_fund_m_lag1\"] < 0)).astype(int)\n",
    "\n",
    "tmp[\"ret_fund_mean3_lag1\"] = (\n",
    "    tmp.groupby([ID_COL, \"fund_family\"])[\"ret_fund_m\"]\n",
    "       .transform(lambda s: s.shift(1).rolling(3, min_periods=1).mean())\n",
    ")\n",
    "tmp[\"ret_fund_mean6_lag1\"] = (\n",
    "    tmp.groupby([ID_COL, \"fund_family\"])[\"ret_fund_m\"]\n",
    "       .transform(lambda s: s.shift(1).rolling(6, min_periods=1).mean())\n",
    ")\n",
    "\n",
    "tmp[\"buy_on_perf_mean3\"] = ((tmp[\"net_flow_qty\"] > 0) & (tmp[\"ret_fund_mean3_lag1\"] > 0)).astype(int)\n",
    "tmp[\"sell_on_perf_mean3\"] = ((tmp[\"net_flow_qty\"] < 0) & (tmp[\"ret_fund_mean3_lag1\"] < 0)).astype(int)\n",
    "tmp[\"buy_on_perf_mean6\"] = ((tmp[\"net_flow_qty\"] > 0) & (tmp[\"ret_fund_mean6_lag1\"] > 0)).astype(int)\n",
    "tmp[\"sell_on_perf_mean6\"] = ((tmp[\"net_flow_qty\"] < 0) & (tmp[\"ret_fund_mean6_lag1\"] < 0)).astype(int)\n",
    "\n",
    "# Rolling means\n",
    "for w in [3, 6, 12]:\n",
    "    tmp[f\"turnover_mean{w}_avg\"] = (\n",
    "        tmp.groupby([ID_COL, \"fund_family\"])[\"turnover_m\"]\n",
    "           .transform(lambda s: s.rolling(w, min_periods=1).mean())\n",
    "    )\n",
    "    tmp[f\"flow_to_aum_mean{w}_avg\"] = (\n",
    "        tmp.groupby([ID_COL, \"fund_family\"])[\"flow_to_aum_m\"]\n",
    "           .transform(lambda s: s.rolling(w, min_periods=1).mean())\n",
    "    )\n",
    "\n",
    "# Performance behavior features demandées\n",
    "# Corrélation entre flow_to_aum_m et moyenne glissante passée des rendements\n",
    "def safe_corr(x, y):\n",
    "    mask = (~pd.isna(x)) & (~pd.isna(y))\n",
    "    if mask.sum() < 3:\n",
    "        return np.nan\n",
    "    if np.nanstd(x[mask]) == 0 or np.nanstd(y[mask]) == 0:\n",
    "        return np.nan\n",
    "    return np.corrcoef(x[mask], y[mask])[0, 1]\n",
    "\n",
    "# Signal buy_after_good_perf à 3m / 6m\n",
    "tmp[\"good_perf_3m\"] = (tmp[\"ret_fund_mean3_lag1\"] > 0).astype(int)\n",
    "tmp[\"good_perf_6m\"] = (tmp[\"ret_fund_mean6_lag1\"] > 0).astype(int)\n",
    "tmp[\"buy_flag\"] = (tmp[\"net_flow_qty\"] > 0).astype(int)\n",
    "tmp[\"buy_after_good_perf_3m\"] = ((tmp[\"buy_flag\"] == 1) & (tmp[\"good_perf_3m\"] == 1)).astype(int)\n",
    "tmp[\"buy_after_good_perf_6m\"] = ((tmp[\"buy_flag\"] == 1) & (tmp[\"good_perf_6m\"] == 1)).astype(int)\n",
    "\n",
    "print(tmp.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1458a477",
   "metadata": {},
   "source": [
    "## 10. Table finale client × family"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "33ce904c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "df_family_client_base shape: (53891, 58)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Registrar Account - ID</th>\n",
       "      <th>fund_family</th>\n",
       "      <th>n_months</th>\n",
       "      <th>n_active_months</th>\n",
       "      <th>flow_freq</th>\n",
       "      <th>family_aum_qty_mean</th>\n",
       "      <th>family_aum_qty_median</th>\n",
       "      <th>family_aum_qty_max</th>\n",
       "      <th>family_aum_qty_last</th>\n",
       "      <th>family_aum_val_mean</th>\n",
       "      <th>family_aum_val_last</th>\n",
       "      <th>net_flow_qty_sum</th>\n",
       "      <th>gross_flow_qty_sum</th>\n",
       "      <th>gross_flow_qty_mean</th>\n",
       "      <th>sub_qty_sum</th>\n",
       "      <th>red_qty_sum</th>\n",
       "      <th>n_tx_total</th>\n",
       "      <th>avg_n_isin_held</th>\n",
       "      <th>max_n_isin_held</th>\n",
       "      <th>net_flow_qty_vol</th>\n",
       "      <th>flow_to_aum_mean</th>\n",
       "      <th>flow_to_aum_vol</th>\n",
       "      <th>turnover_mean</th>\n",
       "      <th>turnover_vol</th>\n",
       "      <th>sub_share_mean</th>\n",
       "      <th>red_share_mean</th>\n",
       "      <th>aum_drawdown_last</th>\n",
       "      <th>aum_drawdown_max</th>\n",
       "      <th>entry_count</th>\n",
       "      <th>full_exit_count</th>\n",
       "      <th>buy_on_perf_rate</th>\n",
       "      <th>sell_on_perf_rate</th>\n",
       "      <th>buy_on_perf_mean3_rate</th>\n",
       "      <th>sell_on_perf_mean3_rate</th>\n",
       "      <th>buy_on_perf_mean6_rate</th>\n",
       "      <th>sell_on_perf_mean6_rate</th>\n",
       "      <th>turnover_mean3_avg</th>\n",
       "      <th>turnover_mean6_avg</th>\n",
       "      <th>turnover_mean12_avg</th>\n",
       "      <th>flow_to_aum_mean3_avg</th>\n",
       "      <th>flow_to_aum_mean6_avg</th>\n",
       "      <th>flow_to_aum_mean12_avg</th>\n",
       "      <th>family_share_of_client_aum_mean</th>\n",
       "      <th>family_share_of_client_aum_max</th>\n",
       "      <th>ret_fund_m_mean</th>\n",
       "      <th>delta_rate_m_mean</th>\n",
       "      <th>region</th>\n",
       "      <th>country</th>\n",
       "      <th>months_since_last_tx_family</th>\n",
       "      <th>corr_flow_ret_3m</th>\n",
       "      <th>corr_flow_ret_6m</th>\n",
       "      <th>buy_after_good_perf_share_3m</th>\n",
       "      <th>buy_after_good_perf_share_6m</th>\n",
       "      <th>log_family_aum_val_mean</th>\n",
       "      <th>gross_flow_to_aum_family</th>\n",
       "      <th>flow_direction_balance</th>\n",
       "      <th>exit_rate_per_isin_family</th>\n",
       "      <th>family_aum_final_to_peak</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>100000014</td>\n",
       "      <td>Carmignac Portfolio Climate Transition</td>\n",
       "      <td>80</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.001500</td>\n",
       "      <td>-0.008925</td>\n",
       "      <td>Switzerland</td>\n",
       "      <td>Switzerland</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>100000016</td>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>80</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000520</td>\n",
       "      <td>-0.008925</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>100000016</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>80</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.002345</td>\n",
       "      <td>-0.008925</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>100000028</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>80</td>\n",
       "      <td>2</td>\n",
       "      <td>0.025</td>\n",
       "      <td>9.529013</td>\n",
       "      <td>0.0</td>\n",
       "      <td>109.235</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1577.95273</td>\n",
       "      <td>0.0</td>\n",
       "      <td>-109.238</td>\n",
       "      <td>109.238</td>\n",
       "      <td>1.365475</td>\n",
       "      <td>0.0</td>\n",
       "      <td>-109.238</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.1</td>\n",
       "      <td>1</td>\n",
       "      <td>10.559919</td>\n",
       "      <td>-0.021356</td>\n",
       "      <td>0.060405</td>\n",
       "      <td>0.021356</td>\n",
       "      <td>0.060405</td>\n",
       "      <td>0.0</td>\n",
       "      <td>-1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.019933</td>\n",
       "      <td>0.021247</td>\n",
       "      <td>0.021228</td>\n",
       "      <td>-0.019933</td>\n",
       "      <td>-0.021247</td>\n",
       "      <td>-0.021228</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.002345</td>\n",
       "      <td>-0.008925</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>United Kingdom</td>\n",
       "      <td>121.0</td>\n",
       "      <td>0.258199</td>\n",
       "      <td>0.258199</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>7.364517</td>\n",
       "      <td>11.463727</td>\n",
       "      <td>-1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>100000038</td>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>80</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.003393</td>\n",
       "      <td>-0.008925</td>\n",
       "      <td>Switzerland</td>\n",
       "      <td>Switzerland</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  Registrar Account - ID                             fund_family  n_months  \\\n",
       "0              100000014  Carmignac Portfolio Climate Transition        80   \n",
       "1              100000016                     Carmignac Emergents        80   \n",
       "2              100000016                    Carmignac Patrimoine        80   \n",
       "3              100000028                    Carmignac Patrimoine        80   \n",
       "4              100000038                    Carmignac Patrimoine        80   \n",
       "\n",
       "   n_active_months  flow_freq  family_aum_qty_mean  family_aum_qty_median  \\\n",
       "0                0      0.000             0.000000                    0.0   \n",
       "1                0      0.000             0.000000                    0.0   \n",
       "2                0      0.000             0.000000                    0.0   \n",
       "3                2      0.025             9.529013                    0.0   \n",
       "4                0      0.000             0.000000                    0.0   \n",
       "\n",
       "   family_aum_qty_max  family_aum_qty_last  family_aum_val_mean  \\\n",
       "0               0.000                  0.0              0.00000   \n",
       "1               0.000                  0.0              0.00000   \n",
       "2               0.000                  0.0              0.00000   \n",
       "3             109.235                  0.0           1577.95273   \n",
       "4               0.000                  0.0              0.00000   \n",
       "\n",
       "   family_aum_val_last  net_flow_qty_sum  gross_flow_qty_sum  \\\n",
       "0                  0.0             0.000               0.000   \n",
       "1                  0.0             0.000               0.000   \n",
       "2                  0.0             0.000               0.000   \n",
       "3                  0.0          -109.238             109.238   \n",
       "4                  0.0             0.000               0.000   \n",
       "\n",
       "   gross_flow_qty_mean  sub_qty_sum  red_qty_sum  n_tx_total  avg_n_isin_held  \\\n",
       "0             0.000000          0.0        0.000         0.0              0.0   \n",
       "1             0.000000          0.0        0.000         0.0              0.0   \n",
       "2             0.000000          0.0        0.000         0.0              0.0   \n",
       "3             1.365475          0.0     -109.238         3.0              0.1   \n",
       "4             0.000000          0.0        0.000         0.0              0.0   \n",
       "\n",
       "   max_n_isin_held  net_flow_qty_vol  flow_to_aum_mean  flow_to_aum_vol  \\\n",
       "0                0          0.000000               NaN         0.000000   \n",
       "1                0          0.000000               NaN         0.000000   \n",
       "2                0          0.000000               NaN         0.000000   \n",
       "3                1         10.559919         -0.021356         0.060405   \n",
       "4                0          0.000000               NaN         0.000000   \n",
       "\n",
       "   turnover_mean  turnover_vol  sub_share_mean  red_share_mean  \\\n",
       "0            NaN      0.000000             NaN             NaN   \n",
       "1            NaN      0.000000             NaN             NaN   \n",
       "2            NaN      0.000000             NaN             NaN   \n",
       "3       0.021356      0.060405             0.0            -1.0   \n",
       "4            NaN      0.000000             NaN             NaN   \n",
       "\n",
       "   aum_drawdown_last  aum_drawdown_max  entry_count  full_exit_count  \\\n",
       "0                NaN               NaN            0                0   \n",
       "1                NaN               NaN            0                0   \n",
       "2                NaN               NaN            0                0   \n",
       "3                1.0               1.0            1                1   \n",
       "4                NaN               NaN            0                0   \n",
       "\n",
       "   buy_on_perf_rate  sell_on_perf_rate  buy_on_perf_mean3_rate  \\\n",
       "0               0.0                0.0                     0.0   \n",
       "1               0.0                0.0                     0.0   \n",
       "2               0.0                0.0                     0.0   \n",
       "3               0.0                0.0                     0.0   \n",
       "4               0.0                0.0                     0.0   \n",
       "\n",
       "   sell_on_perf_mean3_rate  buy_on_perf_mean6_rate  sell_on_perf_mean6_rate  \\\n",
       "0                      0.0                     0.0                      0.0   \n",
       "1                      0.0                     0.0                      0.0   \n",
       "2                      0.0                     0.0                      0.0   \n",
       "3                      0.0                     0.0                      0.0   \n",
       "4                      0.0                     0.0                      0.0   \n",
       "\n",
       "   turnover_mean3_avg  turnover_mean6_avg  turnover_mean12_avg  \\\n",
       "0                 NaN                 NaN                  NaN   \n",
       "1                 NaN                 NaN                  NaN   \n",
       "2                 NaN                 NaN                  NaN   \n",
       "3            0.019933            0.021247             0.021228   \n",
       "4                 NaN                 NaN                  NaN   \n",
       "\n",
       "   flow_to_aum_mean3_avg  flow_to_aum_mean6_avg  flow_to_aum_mean12_avg  \\\n",
       "0                    NaN                    NaN                     NaN   \n",
       "1                    NaN                    NaN                     NaN   \n",
       "2                    NaN                    NaN                     NaN   \n",
       "3              -0.019933              -0.021247               -0.021228   \n",
       "4                    NaN                    NaN                     NaN   \n",
       "\n",
       "   family_share_of_client_aum_mean  family_share_of_client_aum_max  \\\n",
       "0                              NaN                             NaN   \n",
       "1                              NaN                             NaN   \n",
       "2                              NaN                             NaN   \n",
       "3                              1.0                             1.0   \n",
       "4                              NaN                             NaN   \n",
       "\n",
       "   ret_fund_m_mean  delta_rate_m_mean          region         country  \\\n",
       "0         0.001500          -0.008925     Switzerland     Switzerland   \n",
       "1         0.000520          -0.008925  United Kingdom  United Kingdom   \n",
       "2         0.002345          -0.008925  United Kingdom  United Kingdom   \n",
       "3         0.002345          -0.008925  United Kingdom  United Kingdom   \n",
       "4         0.003393          -0.008925     Switzerland     Switzerland   \n",
       "\n",
       "   months_since_last_tx_family  corr_flow_ret_3m  corr_flow_ret_6m  \\\n",
       "0                          0.0          0.000000          0.000000   \n",
       "1                          0.0          0.000000          0.000000   \n",
       "2                          0.0          0.000000          0.000000   \n",
       "3                        121.0          0.258199          0.258199   \n",
       "4                          0.0          0.000000          0.000000   \n",
       "\n",
       "   buy_after_good_perf_share_3m  buy_after_good_perf_share_6m  \\\n",
       "0                           0.0                           0.0   \n",
       "1                           0.0                           0.0   \n",
       "2                           0.0                           0.0   \n",
       "3                           0.0                           0.0   \n",
       "4                           0.0                           0.0   \n",
       "\n",
       "   log_family_aum_val_mean  gross_flow_to_aum_family  flow_direction_balance  \\\n",
       "0                 0.000000                       NaN                     NaN   \n",
       "1                 0.000000                       NaN                     NaN   \n",
       "2                 0.000000                       NaN                     NaN   \n",
       "3                 7.364517                 11.463727                    -1.0   \n",
       "4                 0.000000                       NaN                     NaN   \n",
       "\n",
       "   exit_rate_per_isin_family  family_aum_final_to_peak  \n",
       "0                        NaN                       NaN  \n",
       "1                        NaN                       NaN  \n",
       "2                        NaN                       NaN  \n",
       "3                        1.0                       0.0  \n",
       "4                        NaN                       NaN  "
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "months_since_last_tx = add_months_since_last_tx_by_group(\n",
    "    tmp,\n",
    "    id_cols=[ID_COL, \"fund_family\"],\n",
    "    active_col=\"active_month\",\n",
    "    month_col=\"month\",\n",
    "    suffix=\"_family\"\n",
    ")\n",
    "\n",
    "# Corrélations client x family\n",
    "corr_table = (\n",
    "    tmp.groupby([ID_COL, \"fund_family\"])\n",
    "    .apply(lambda g: pd.Series({\n",
    "        \"corr_flow_ret_3m\": safe_corr(g[\"flow_to_aum_m\"].to_numpy(dtype=float), g[\"ret_fund_mean3_lag1\"].to_numpy(dtype=float)),\n",
    "        \"corr_flow_ret_6m\": safe_corr(g[\"flow_to_aum_m\"].to_numpy(dtype=float), g[\"ret_fund_mean6_lag1\"].to_numpy(dtype=float)),\n",
    "        \"buy_after_good_perf_share_3m\":\n",
    "            np.nan if g[\"buy_flag\"].sum() == 0 else g[\"buy_after_good_perf_3m\"].sum() / g[\"buy_flag\"].sum(),\n",
    "        \"buy_after_good_perf_share_6m\":\n",
    "            np.nan if g[\"buy_flag\"].sum() == 0 else g[\"buy_after_good_perf_6m\"].sum() / g[\"buy_flag\"].sum(),\n",
    "    }))\n",
    "    .reset_index()\n",
    ")\n",
    "\n",
    "df_family_client_base = (\n",
    "    tmp\n",
    "    .groupby([ID_COL, \"fund_family\"], as_index=False)\n",
    "    .agg(\n",
    "        n_months=(\"month\", \"nunique\"),\n",
    "        n_active_months=(\"active_month\", \"sum\"),\n",
    "        flow_freq=(\"active_month\", \"mean\"),\n",
    "\n",
    "        family_aum_qty_mean=(\"aum_qty\", \"mean\"),\n",
    "        family_aum_qty_median=(\"aum_qty\", \"median\"),\n",
    "        family_aum_qty_max=(\"aum_qty\", \"max\"),\n",
    "        family_aum_qty_last=(\"aum_qty\", \"last\"),\n",
    "\n",
    "        family_aum_val_mean=(\"aum_val\", \"mean\"),\n",
    "        family_aum_val_last=(\"aum_val\", \"last\"),\n",
    "\n",
    "        net_flow_qty_sum=(\"net_flow_qty\", \"sum\"),\n",
    "        gross_flow_qty_sum=(\"gross_flow_qty\", \"sum\"),\n",
    "        gross_flow_qty_mean=(\"gross_flow_qty\", \"mean\"),\n",
    "\n",
    "        sub_qty_sum=(\"sub_qty\", \"sum\"),\n",
    "        red_qty_sum=(\"red_qty\", \"sum\"),\n",
    "        n_tx_total=(\"n_tx\", \"sum\"),\n",
    "\n",
    "        avg_n_isin_held=(\"n_isin_held\", \"mean\"),\n",
    "        max_n_isin_held=(\"n_isin_held\", \"max\"),\n",
    "\n",
    "        net_flow_qty_vol=(\"net_flow_qty\", \"std\"),\n",
    "        flow_to_aum_mean=(\"flow_to_aum_m\", \"mean\"),\n",
    "        flow_to_aum_vol=(\"flow_to_aum_m\", \"std\"),\n",
    "        turnover_mean=(\"turnover_m\", \"mean\"),\n",
    "        turnover_vol=(\"turnover_m\", \"std\"),\n",
    "\n",
    "        sub_share_mean=(\"sub_share_m\", \"mean\"),\n",
    "        red_share_mean=(\"red_share_m\", \"mean\"),\n",
    "\n",
    "        aum_drawdown_last=(\"aum_drawdown\", \"last\"),\n",
    "        aum_drawdown_max=(\"aum_drawdown\", \"max\"),\n",
    "\n",
    "        entry_count=(\"entry_event\", \"sum\"),\n",
    "        full_exit_count=(\"full_exit_event\", \"sum\"),\n",
    "\n",
    "        buy_on_perf_rate=(\"buy_on_perf\", \"mean\"),\n",
    "        sell_on_perf_rate=(\"sell_on_perf\", \"mean\"),\n",
    "        buy_on_perf_mean3_rate=(\"buy_on_perf_mean3\", \"mean\"),\n",
    "        sell_on_perf_mean3_rate=(\"sell_on_perf_mean3\", \"mean\"),\n",
    "        buy_on_perf_mean6_rate=(\"buy_on_perf_mean6\", \"mean\"),\n",
    "        sell_on_perf_mean6_rate=(\"sell_on_perf_mean6\", \"mean\"),\n",
    "\n",
    "        turnover_mean3_avg=(\"turnover_mean3_avg\", \"mean\"),\n",
    "        turnover_mean6_avg=(\"turnover_mean6_avg\", \"mean\"),\n",
    "        turnover_mean12_avg=(\"turnover_mean12_avg\", \"mean\"),\n",
    "\n",
    "        flow_to_aum_mean3_avg=(\"flow_to_aum_mean3_avg\", \"mean\"),\n",
    "        flow_to_aum_mean6_avg=(\"flow_to_aum_mean6_avg\", \"mean\"),\n",
    "        flow_to_aum_mean12_avg=(\"flow_to_aum_mean12_avg\", \"mean\"),\n",
    "\n",
    "        family_share_of_client_aum_mean=(\"family_share_of_client_aum\", \"mean\"),\n",
    "        family_share_of_client_aum_max=(\"family_share_of_client_aum\", \"max\"),\n",
    "\n",
    "        ret_fund_m_mean=(\"ret_fund_m\", \"mean\"),\n",
    "        delta_rate_m_mean=(\"delta_rate_m\", \"mean\"),\n",
    "\n",
    "        region=(\"region\", \"last\"),\n",
    "        country=(\"country\", \"last\"),\n",
    "    )\n",
    "    .merge(months_since_last_tx, on=[ID_COL, \"fund_family\"], how=\"left\")\n",
    "    .merge(corr_table, on=[ID_COL, \"fund_family\"], how=\"left\")\n",
    ")\n",
    "\n",
    "# Features additionnelles \n",
    "df_family_client_base[\"log_family_aum_val_mean\"] = np.log1p(\n",
    "    df_family_client_base[\"family_aum_val_mean\"].clip(lower=0)\n",
    ")\n",
    "df_family_client_base[\"gross_flow_to_aum_family\"] = np.where(\n",
    "    df_family_client_base[\"family_aum_qty_mean\"].abs() > 0,\n",
    "    df_family_client_base[\"gross_flow_qty_sum\"] / df_family_client_base[\"family_aum_qty_mean\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "df_family_client_base[\"flow_direction_balance\"] = np.where(\n",
    "    df_family_client_base[\"gross_flow_qty_sum\"].abs() > 0,\n",
    "    df_family_client_base[\"net_flow_qty_sum\"] / df_family_client_base[\"gross_flow_qty_sum\"].abs(),\n",
    "    np.nan\n",
    ")\n",
    "df_family_client_base[\"exit_rate_per_isin_family\"] = np.where(\n",
    "    df_family_client_base[\"avg_n_isin_held\"].abs() > 0,\n",
    "    df_family_client_base[\"full_exit_count\"] / df_family_client_base[\"avg_n_isin_held\"].clip(lower=1),\n",
    "    np.nan\n",
    ")\n",
    "df_family_client_base[\"family_aum_final_to_peak\"] = np.where(\n",
    "    df_family_client_base[\"aum_drawdown_last\"].notna(),\n",
    "    1 - df_family_client_base[\"aum_drawdown_last\"],\n",
    "    np.nan\n",
    ")\n",
    "\n",
    "for col in [\n",
    "    \"net_flow_qty_vol\", \"flow_to_aum_vol\", \"turnover_vol\",\n",
    "    \"months_since_last_tx_family\", \"corr_flow_ret_3m\", \"corr_flow_ret_6m\",\n",
    "    \"buy_after_good_perf_share_3m\", \"buy_after_good_perf_share_6m\"\n",
    "]:\n",
    "    if col in df_family_client_base.columns:\n",
    "        df_family_client_base[col] = df_family_client_base[col].fillna(0)\n",
    "\n",
    "print(\"df_family_client_base shape:\", df_family_client_base.shape)\n",
    "df_family_client_base.head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8625505",
   "metadata": {},
   "source": [
    "## 11. Top 15 families"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "3c7f596f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Top 15 families:\n",
      "01 - Carmignac Patrimoine\n",
      "02 - Carmignac Sécurité\n",
      "03 - Carmignac Investissement\n",
      "04 - Carmignac Credit <NUM>\n",
      "05 - Carmignac Portfolio Sécurité\n",
      "06 - Carmignac Portfolio Flexible Bond\n",
      "07 - Carmignac Portfolio Credit\n",
      "08 - Carmignac Emergents\n",
      "09 - Carmignac Court Terme\n",
      "10 - Carmignac Portfolio Long-Short European Equities\n",
      "11 - Carmignac Portfolio Grande Europe\n",
      "12 - Carmignac Portfolio Emergents\n",
      "13 - Carmignac Portfolio Global Bond\n",
      "14 - Carmignac Portfolio Patrimoine\n",
      "15 - Carmignac Portfolio Emerging Patrimoine\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>fund_family</th>\n",
       "      <th>n_clients</th>\n",
       "      <th>total_family_aum</th>\n",
       "      <th>avg_family_aum</th>\n",
       "      <th>total_gross_flow</th>\n",
       "      <th>total_tx</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>6183</td>\n",
       "      <td>6.668324e+09</td>\n",
       "      <td>2.730239e+06</td>\n",
       "      <td>1.199596e+07</td>\n",
       "      <td>49953.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Carmignac Sécurité</td>\n",
       "      <td>2825</td>\n",
       "      <td>5.359539e+09</td>\n",
       "      <td>3.274706e+06</td>\n",
       "      <td>5.239893e+06</td>\n",
       "      <td>36860.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Carmignac Investissement</td>\n",
       "      <td>4982</td>\n",
       "      <td>4.111151e+09</td>\n",
       "      <td>9.593080e+05</td>\n",
       "      <td>1.304787e+06</td>\n",
       "      <td>20269.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Carmignac Credit &lt;NUM&gt;</td>\n",
       "      <td>436</td>\n",
       "      <td>4.082249e+09</td>\n",
       "      <td>4.098273e+06</td>\n",
       "      <td>5.065680e+06</td>\n",
       "      <td>6893.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Carmignac Portfolio Sécurité</td>\n",
       "      <td>1264</td>\n",
       "      <td>2.688287e+09</td>\n",
       "      <td>2.178355e+06</td>\n",
       "      <td>1.756042e+07</td>\n",
       "      <td>13177.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Carmignac Portfolio Flexible Bond</td>\n",
       "      <td>1431</td>\n",
       "      <td>2.483972e+09</td>\n",
       "      <td>1.498464e+06</td>\n",
       "      <td>1.029621e+06</td>\n",
       "      <td>12046.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Carmignac Portfolio Credit</td>\n",
       "      <td>1135</td>\n",
       "      <td>2.356507e+09</td>\n",
       "      <td>1.132726e+06</td>\n",
       "      <td>4.493724e+06</td>\n",
       "      <td>8018.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>4231</td>\n",
       "      <td>1.095901e+09</td>\n",
       "      <td>3.089076e+05</td>\n",
       "      <td>2.353374e+06</td>\n",
       "      <td>12011.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "      <td>1331</td>\n",
       "      <td>9.948195e+08</td>\n",
       "      <td>3.681887e+05</td>\n",
       "      <td>2.222615e+05</td>\n",
       "      <td>5745.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Carmignac Portfolio Long-Short European Equities</td>\n",
       "      <td>689</td>\n",
       "      <td>7.283139e+08</td>\n",
       "      <td>1.129733e+06</td>\n",
       "      <td>3.032355e+06</td>\n",
       "      <td>4884.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Carmignac Portfolio Grande Europe</td>\n",
       "      <td>2950</td>\n",
       "      <td>5.882891e+08</td>\n",
       "      <td>2.852247e+05</td>\n",
       "      <td>2.061285e+06</td>\n",
       "      <td>9341.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Carmignac Portfolio Emergents</td>\n",
       "      <td>650</td>\n",
       "      <td>5.466360e+08</td>\n",
       "      <td>6.443657e+05</td>\n",
       "      <td>1.722755e+06</td>\n",
       "      <td>9211.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Carmignac Portfolio Global Bond</td>\n",
       "      <td>2114</td>\n",
       "      <td>5.445575e+08</td>\n",
       "      <td>4.059678e+05</td>\n",
       "      <td>3.476952e+06</td>\n",
       "      <td>18356.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Carmignac Portfolio Patrimoine</td>\n",
       "      <td>1234</td>\n",
       "      <td>5.127245e+08</td>\n",
       "      <td>7.907827e+05</td>\n",
       "      <td>5.253474e+06</td>\n",
       "      <td>9133.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Carmignac Portfolio Emerging Patrimoine</td>\n",
       "      <td>1673</td>\n",
       "      <td>3.315211e+08</td>\n",
       "      <td>4.169396e+05</td>\n",
       "      <td>3.368469e+06</td>\n",
       "      <td>13732.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                         fund_family  n_clients  \\\n",
       "0                               Carmignac Patrimoine       6183   \n",
       "1                                 Carmignac Sécurité       2825   \n",
       "2                           Carmignac Investissement       4982   \n",
       "3                             Carmignac Credit <NUM>        436   \n",
       "4                       Carmignac Portfolio Sécurité       1264   \n",
       "5                  Carmignac Portfolio Flexible Bond       1431   \n",
       "6                         Carmignac Portfolio Credit       1135   \n",
       "7                                Carmignac Emergents       4231   \n",
       "8                              Carmignac Court Terme       1331   \n",
       "9   Carmignac Portfolio Long-Short European Equities        689   \n",
       "10                 Carmignac Portfolio Grande Europe       2950   \n",
       "11                     Carmignac Portfolio Emergents        650   \n",
       "12                   Carmignac Portfolio Global Bond       2114   \n",
       "13                    Carmignac Portfolio Patrimoine       1234   \n",
       "14           Carmignac Portfolio Emerging Patrimoine       1673   \n",
       "\n",
       "    total_family_aum  avg_family_aum  total_gross_flow  total_tx  \n",
       "0       6.668324e+09    2.730239e+06      1.199596e+07   49953.0  \n",
       "1       5.359539e+09    3.274706e+06      5.239893e+06   36860.0  \n",
       "2       4.111151e+09    9.593080e+05      1.304787e+06   20269.0  \n",
       "3       4.082249e+09    4.098273e+06      5.065680e+06    6893.0  \n",
       "4       2.688287e+09    2.178355e+06      1.756042e+07   13177.0  \n",
       "5       2.483972e+09    1.498464e+06      1.029621e+06   12046.0  \n",
       "6       2.356507e+09    1.132726e+06      4.493724e+06    8018.0  \n",
       "7       1.095901e+09    3.089076e+05      2.353374e+06   12011.0  \n",
       "8       9.948195e+08    3.681887e+05      2.222615e+05    5745.0  \n",
       "9       7.283139e+08    1.129733e+06      3.032355e+06    4884.0  \n",
       "10      5.882891e+08    2.852247e+05      2.061285e+06    9341.0  \n",
       "11      5.466360e+08    6.443657e+05      1.722755e+06    9211.0  \n",
       "12      5.445575e+08    4.059678e+05      3.476952e+06   18356.0  \n",
       "13      5.127245e+08    7.907827e+05      5.253474e+06    9133.0  \n",
       "14      3.315211e+08    4.169396e+05      3.368469e+06   13732.0  "
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "top_family_table = (\n",
    "    df_family_client_base\n",
    "    .groupby(\"fund_family\", as_index=False)\n",
    "    .agg(\n",
    "        n_clients=(ID_COL, \"nunique\"),\n",
    "        total_family_aum=(\"family_aum_val_last\", \"sum\"),\n",
    "        avg_family_aum=(\"family_aum_val_mean\", \"mean\"),\n",
    "        total_gross_flow=(\"gross_flow_qty_sum\", \"sum\"),\n",
    "        total_tx=(\"n_tx_total\", \"sum\")\n",
    "    )\n",
    "    .sort_values([\"total_family_aum\", \"n_clients\"], ascending=[False, False])\n",
    "    .reset_index(drop=True)\n",
    ")\n",
    "\n",
    "top_15_families = top_family_table.head(15)[\"fund_family\"].tolist()\n",
    "\n",
    "print(\"Top 15 families:\")\n",
    "for i, fam in enumerate(top_15_families, 1):\n",
    "    print(f\"{i:02d} - {fam}\")\n",
    "\n",
    "top_family_table.head(15)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "e517fa73",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 91072 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 91668 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Visualisation rapide des top 15 families\n",
    "top15_plot = top_family_table.head(15).copy()\n",
    "\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.barplot(data=top15_plot, y=\"fund_family\", x=\"total_family_aum\")\n",
    "plt.title(\"Top 15 fund families by total AUM\")\n",
    "plt.xlabel(\"Total family AUM\")\n",
    "plt.ylabel(\"Fund family\")\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.barplot(data=top15_plot, y=\"fund_family\", x=\"n_clients\")\n",
    "plt.title(\"Top 15 fund families by number of clients\")\n",
    "plt.xlabel(\"Number of clients\")\n",
    "plt.ylabel(\"Fund family\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb693b49",
   "metadata": {},
   "source": [
    "## 12. Features de clustering"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "0ea62167",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Nombre de features de clustering : 15\n",
      "['flow_freq', 'log_family_aum_val_mean', 'gross_flow_to_aum_family', 'avg_n_isin_held', 'exit_rate_per_isin_family', 'flow_direction_balance', 'months_since_last_tx_family', 'family_share_of_client_aum_mean', 'turnover_mean3_avg', 'turnover_mean6_avg', 'turnover_mean12_avg', 'corr_flow_ret_3m', 'corr_flow_ret_6m', 'buy_after_good_perf_share_3m', 'buy_after_good_perf_share_6m']\n"
     ]
    }
   ],
   "source": [
    "family_cluster_features = [\n",
    "    \"flow_freq\",\n",
    "    \"log_family_aum_val_mean\",\n",
    "    \"gross_flow_to_aum_family\",\n",
    "    \"avg_n_isin_held\",\n",
    "    \"exit_rate_per_isin_family\",\n",
    "    \"flow_direction_balance\",\n",
    "    \"months_since_last_tx_family\",\n",
    "    \"family_share_of_client_aum_mean\",\n",
    "    \"turnover_mean3_avg\",\n",
    "    \"turnover_mean6_avg\",\n",
    "    \"turnover_mean12_avg\",\n",
    "    \"corr_flow_ret_3m\",\n",
    "    \"corr_flow_ret_6m\",\n",
    "    \"buy_after_good_perf_share_3m\",\n",
    "    \"buy_after_good_perf_share_6m\",\n",
    "]\n",
    "\n",
    "print(\"Nombre de features de clustering :\", len(family_cluster_features))\n",
    "print(family_cluster_features)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e351d17",
   "metadata": {},
   "source": [
    "## 13. Fonctions de clustering robustes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "b7ba2680",
   "metadata": {},
   "outputs": [],
   "source": [
    "def prepare_family_matrix(df_family, features, winsorize_cols=None):\n",
    "    X = df_family[features].copy()\n",
    "\n",
    "    imputer = SimpleImputer(strategy=\"median\")\n",
    "    X_imp = pd.DataFrame(imputer.fit_transform(X), columns=features, index=X.index)\n",
    "\n",
    "    if winsorize_cols is None:\n",
    "        winsorize_cols = [\n",
    "            \"gross_flow_to_aum_family\",\n",
    "            \"turnover_mean3_avg\",\n",
    "            \"turnover_mean6_avg\",\n",
    "            \"turnover_mean12_avg\",\n",
    "            \"months_since_last_tx_family\",\n",
    "            \"family_share_of_client_aum_mean\",\n",
    "            \"exit_rate_per_isin_family\",\n",
    "            \"corr_flow_ret_3m\",\n",
    "            \"corr_flow_ret_6m\",\n",
    "            \"buy_after_good_perf_share_3m\",\n",
    "            \"buy_after_good_perf_share_6m\",\n",
    "        ]\n",
    "\n",
    "    for col in winsorize_cols:\n",
    "        if col in X_imp.columns:\n",
    "            X_imp[col] = winsorize_mad(X_imp[col], n_sigma=3)\n",
    "\n",
    "    scaler = RobustScaler()\n",
    "    X_scaled = scaler.fit_transform(X_imp)\n",
    "\n",
    "    return X_imp, X_scaled, imputer, scaler\n",
    "\n",
    "\n",
    "def evaluate_k_range(X_scaled, k_min=3, k_max=10, min_cluster_size=10, random_state=42):\n",
    "    n = X_scaled.shape[0]\n",
    "    if n <= k_min:\n",
    "        return pd.DataFrame()\n",
    "\n",
    "    k_max = min(k_max, n - 1)\n",
    "    rows = []\n",
    "\n",
    "    for k in range(k_min, k_max + 1):\n",
    "        km = KMeans(n_clusters=k, random_state=random_state, n_init=20)\n",
    "        labels = km.fit_predict(X_scaled)\n",
    "        cluster_sizes = pd.Series(labels).value_counts().sort_index()\n",
    "        min_size = int(cluster_sizes.min())\n",
    "\n",
    "        if min_size < min_cluster_size:\n",
    "            sil = np.nan\n",
    "            dbi = np.nan\n",
    "        else:\n",
    "            sil = silhouette_score(X_scaled, labels)\n",
    "            dbi = davies_bouldin_score(X_scaled, labels)\n",
    "\n",
    "        rows.append({\n",
    "            \"k\": k,\n",
    "            \"silhouette\": sil,\n",
    "            \"davies_bouldin\": dbi,\n",
    "            \"min_cluster_size\": min_size,\n",
    "            \"inertia\": km.inertia_\n",
    "        })\n",
    "\n",
    "    return pd.DataFrame(rows)\n",
    "\n",
    "\n",
    "def choose_best_k(metrics_df):\n",
    "    valid = metrics_df.dropna(subset=[\"silhouette\"]).copy()\n",
    "    if len(valid) == 0:\n",
    "        return None\n",
    "    return int(valid.sort_values([\"silhouette\", \"davies_bouldin\"], ascending=[False, True]).iloc[0][\"k\"])\n",
    "\n",
    "\n",
    "def run_family_clustering(\n",
    "    df_base,\n",
    "    family_name,\n",
    "    features,\n",
    "    k_min=3,\n",
    "    k_max=10,\n",
    "    min_clients=50,\n",
    "    min_cluster_size=10,\n",
    "    random_state=42,\n",
    "    make_plots=True\n",
    "):\n",
    "    df_family = df_base[df_base[\"fund_family\"] == family_name].copy()\n",
    "\n",
    "    # filtre qualité\n",
    "    df_family = df_family[\n",
    "        (df_family[\"n_months\"] >= 6) &\n",
    "        (df_family[\"family_aum_val_mean\"] > 0)\n",
    "    ].copy()\n",
    "\n",
    "    # filtre anti-outliers proche du notebook initial\n",
    "    for col in [\"family_aum_val_mean\", \"gross_flow_qty_sum\", \"n_tx_total\"]:\n",
    "        if col in df_family.columns and df_family[col].notna().sum() > 0:\n",
    "            cap = df_family[col].quantile(0.99)\n",
    "            df_family = df_family[df_family[col] <= cap].copy()\n",
    "\n",
    "    n_clients = df_family[ID_COL].nunique()\n",
    "    if n_clients < min_clients:\n",
    "        print(f\"[SKIP] {family_name} : seulement {n_clients} clients exploitables.\")\n",
    "        return None\n",
    "\n",
    "    X_imp, X_scaled, imputer, scaler = prepare_family_matrix(df_family, features)\n",
    "\n",
    "    metrics_df = evaluate_k_range(\n",
    "        X_scaled,\n",
    "        k_min=k_min,\n",
    "        k_max=k_max,\n",
    "        min_cluster_size=min_cluster_size,\n",
    "        random_state=random_state\n",
    "    )\n",
    "\n",
    "    if len(metrics_df) == 0:\n",
    "        print(f\"[SKIP] {family_name} : impossible d'évaluer K.\")\n",
    "        return None\n",
    "\n",
    "    best_k = choose_best_k(metrics_df)\n",
    "    if best_k is None:\n",
    "        print(f\"[SKIP] {family_name} : aucun K valide avec cluster minimal >= {min_cluster_size}.\")\n",
    "        return None\n",
    "\n",
    "    km = KMeans(n_clusters=best_k, random_state=random_state, n_init=20)\n",
    "    labels = km.fit_predict(X_scaled)\n",
    "\n",
    "    df_family[\"cluster\"] = labels\n",
    "    cluster_profile = df_family.groupby(\"cluster\")[features].median().sort_index()\n",
    "    cluster_sizes = (\n",
    "        df_family[\"cluster\"].value_counts().sort_index()\n",
    "        .rename_axis(\"cluster\").reset_index(name=\"n_clients\")\n",
    "    )\n",
    "\n",
    "    pca = PCA(n_components=2, random_state=random_state)\n",
    "    coords = pca.fit_transform(X_scaled)\n",
    "    df_family[\"pca1\"] = coords[:, 0]\n",
    "    df_family[\"pca2\"] = coords[:, 1]\n",
    "\n",
    "    # churn analysis\n",
    "    df_family[\"churn_hard\"] = (df_family[\"family_aum_final_to_peak\"] < 0.10).astype(int)\n",
    "    df_family[\"churn_soft\"] = (\n",
    "        (df_family[\"family_aum_final_to_peak\"] < 0.40) &\n",
    "        (df_family[\"aum_drawdown_last\"] > 0.40)\n",
    "    ).astype(int)\n",
    "    df_family[\"churn_warning\"] = (\n",
    "        (df_family[\"flow_direction_balance\"] < 0) &\n",
    "        (df_family[\"aum_drawdown_last\"] > 0.20)\n",
    "    ).astype(int)\n",
    "\n",
    "    result = {\n",
    "        \"family\": family_name,\n",
    "        \"n_clients\": n_clients,\n",
    "        \"best_k\": best_k,\n",
    "        \"metrics_df\": metrics_df,\n",
    "        \"df_family\": df_family,\n",
    "        \"cluster_profile\": cluster_profile,\n",
    "        \"cluster_sizes\": cluster_sizes,\n",
    "        \"features\": features,\n",
    "        \"pca_explained_var\": pca.explained_variance_ratio_,\n",
    "        \"X_scaled\": X_scaled,\n",
    "        \"labels\": labels,\n",
    "    }\n",
    "\n",
    "    if make_plots:\n",
    "        fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n",
    "        axes[0].plot(metrics_df[\"k\"], metrics_df[\"silhouette\"], marker=\"o\")\n",
    "        axes[0].set_title(f\"{family_name} - Silhouette\")\n",
    "        axes[1].plot(metrics_df[\"k\"], metrics_df[\"davies_bouldin\"], marker=\"o\")\n",
    "        axes[1].set_title(f\"{family_name} - Davies-Bouldin\")\n",
    "        axes[2].plot(metrics_df[\"k\"], metrics_df[\"inertia\"], marker=\"o\")\n",
    "        axes[2].set_title(f\"{family_name} - Inertia\")\n",
    "        for ax in axes:\n",
    "            ax.set_xlabel(\"k\")\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "        plot_heatmap(\n",
    "            df_family,\n",
    "            profile_vars=features,\n",
    "            cluster_col=\"cluster\",\n",
    "            title=f\"{family_name} - Cluster signatures\",\n",
    "            figsize=(18, 5)\n",
    "        )\n",
    "\n",
    "        plt.figure(figsize=(7, 5))\n",
    "        sns.scatterplot(\n",
    "            data=df_family, x=\"pca1\", y=\"pca2\",\n",
    "            hue=\"cluster\", palette=\"tab10\", alpha=0.8\n",
    "        )\n",
    "        plt.title(\n",
    "            f\"{family_name} - PCA projection \"\n",
    "            f\"(var explained: {result['pca_explained_var'][0]:.2f}, {result['pca_explained_var'][1]:.2f})\"\n",
    "        )\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "        plt.figure(figsize=(6, 4))\n",
    "        sns.barplot(data=cluster_sizes, x=\"cluster\", y=\"n_clients\")\n",
    "        plt.title(f\"{family_name} - Cluster sizes\")\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "        churn_summary = (\n",
    "            df_family.groupby(\"cluster\")[[\"churn_hard\", \"churn_soft\", \"churn_warning\"]]\n",
    "            .mean()\n",
    "            .reset_index()\n",
    "        )\n",
    "        churn_summary.set_index(\"cluster\")[[\"churn_hard\", \"churn_soft\", \"churn_warning\"]].plot(\n",
    "            kind=\"bar\", figsize=(10, 5)\n",
    "        )\n",
    "        plt.title(f\"{family_name} - Churn analysis by cluster\")\n",
    "        plt.ylabel(\"Rate\")\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "        print(f\"\\n=== {family_name} ===\")\n",
    "        print(f\"Clients exploitables : {n_clients}\")\n",
    "        print(f\"K retenu            : {best_k}\")\n",
    "        print(cluster_sizes)\n",
    "\n",
    "    return result"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "157de135",
   "metadata": {},
   "source": [
    "## 14. Test sur une famille"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "b5e52bda",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 70248 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 134280 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 86284 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20068 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 29180 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Patrimoine ===\n",
      "Clients exploitables : 3058\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0       2714\n",
      "1        1        188\n",
      "2        2        156\n"
     ]
    }
   ],
   "source": [
    "test_family = top_15_families[0]\n",
    "\n",
    "result_test = run_family_clustering(\n",
    "    df_base=df_family_client_base,\n",
    "    family_name=test_family,\n",
    "    features=family_cluster_features,\n",
    "    k_min=3,\n",
    "    k_max=10,\n",
    "    min_clients=50,\n",
    "    min_cluster_size=10,\n",
    "    random_state=RANDOM_STATE,\n",
    "    make_plots=True\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "976e01e0",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true
   },
   "source": [
    "## 15. Boucle sur les top 15 families"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "d4eb3668",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Patrimoine\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 70248 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 134280 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 86284 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20068 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 29180 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Patrimoine ===\n",
      "Clients exploitables : 3058\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0       2714\n",
      "1        1        188\n",
      "2        2        156\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Sécurité\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 74124 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 156188 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 115496 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 21252 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 29000 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Sécurité ===\n",
      "Clients exploitables : 1573\n",
      "K retenu            : 4\n",
      "   cluster  n_clients\n",
      "0        0       1245\n",
      "1        1        127\n",
      "2        2        116\n",
      "3        3         85\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Investissement\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 77780 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 151464 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 94960 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 22876 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 30008 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Investissement ===\n",
      "Clients exploitables : 2126\n",
      "K retenu            : 4\n",
      "   cluster  n_clients\n",
      "0        0       1835\n",
      "1        1        135\n",
      "2        2        120\n",
      "3        3         36\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Credit <NUM>\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 87960 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 131516 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 62684 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 19736 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 32744 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Credit <NUM> ===\n",
      "Clients exploitables : 370\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0         63\n",
      "1        1         51\n",
      "2        2        256\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Sécurité\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 78092 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 130720 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 103740 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20020 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 30932 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Sécurité ===\n",
      "Clients exploitables : 1125\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        894\n",
      "1        1         56\n",
      "2        2        175\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Flexible Bond\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 82608 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 159576 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 105556 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 24796 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 31332 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Flexible Bond ===\n",
      "Clients exploitables : 1053\n",
      "K retenu            : 4\n",
      "   cluster  n_clients\n",
      "0        0        777\n",
      "1        1         87\n",
      "2        2        149\n",
      "3        3         40\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Credit\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 80340 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 131876 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 94676 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 23460 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 30136 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Credit ===\n",
      "Clients exploitables : 984\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        794\n",
      "1        1        143\n",
      "2        2         47\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Emergents\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 74620 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 130416 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 84724 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 21600 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 28020 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Emergents ===\n",
      "Clients exploitables : 1724\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0       1513\n",
      "1        1        117\n",
      "2        2         94\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Court Terme\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 93252 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 132392 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 68656 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 21904 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 28952 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Court Terme ===\n",
      "Clients exploitables : 507\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        400\n",
      "1        1         44\n",
      "2        2         63\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Long-Short European Equities\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 87908 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 152340 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 75316 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 23008 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 33780 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Long-Short European Equities ===\n",
      "Clients exploitables : 586\n",
      "K retenu            : 4\n",
      "   cluster  n_clients\n",
      "0        0         48\n",
      "1        1         28\n",
      "2        2         39\n",
      "3        3        471\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Grande Europe\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 73532 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 133616 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 86988 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 23004 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 34572 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Grande Europe ===\n",
      "Clients exploitables : 1344\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0       1163\n",
      "1        1        100\n",
      "2        2         81\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Emergents\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 64060 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 131428 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 66536 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20256 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 30264 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Emergents ===\n",
      "Clients exploitables : 530\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        434\n",
      "1        1         70\n",
      "2        2         26\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Global Bond\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 81900 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 152872 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 120316 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 23328 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 31992 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Global Bond ===\n",
      "Clients exploitables : 1664\n",
      "K retenu            : 4\n",
      "   cluster  n_clients\n",
      "0        0       1213\n",
      "1        1         66\n",
      "2        2        142\n",
      "3        3        243\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Patrimoine\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 81248 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 131236 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 83592 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 24016 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 31512 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Patrimoine ===\n",
      "Clients exploitables : 1107\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        801\n",
      "1        1        271\n",
      "2        2         35\n",
      "\n",
      "================================================================================\n",
      "Running clustering for family: Carmignac Portfolio Emerging Patrimoine\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 97540 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 131468 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 88168 characters]",
      "text/plain": [
       "<Figure size 700x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20760 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 31984 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Carmignac Portfolio Emerging Patrimoine ===\n",
      "Clients exploitables : 1099\n",
      "K retenu            : 3\n",
      "   cluster  n_clients\n",
      "0        0        916\n",
      "1        1         72\n",
      "2        2        111\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>fund_family</th>\n",
       "      <th>n_clients</th>\n",
       "      <th>best_k</th>\n",
       "      <th>silhouette</th>\n",
       "      <th>davies_bouldin</th>\n",
       "      <th>min_cluster_size</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Carmignac Patrimoine</td>\n",
       "      <td>3058</td>\n",
       "      <td>3</td>\n",
       "      <td>0.895515</td>\n",
       "      <td>0.370484</td>\n",
       "      <td>156.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Carmignac Emergents</td>\n",
       "      <td>1724</td>\n",
       "      <td>3</td>\n",
       "      <td>0.894999</td>\n",
       "      <td>0.364722</td>\n",
       "      <td>94.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Carmignac Investissement</td>\n",
       "      <td>2126</td>\n",
       "      <td>4</td>\n",
       "      <td>0.880809</td>\n",
       "      <td>0.470763</td>\n",
       "      <td>36.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Carmignac Portfolio Grande Europe</td>\n",
       "      <td>1344</td>\n",
       "      <td>3</td>\n",
       "      <td>0.872593</td>\n",
       "      <td>0.411056</td>\n",
       "      <td>81.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Carmignac Portfolio Emerging Patrimoine</td>\n",
       "      <td>1099</td>\n",
       "      <td>3</td>\n",
       "      <td>0.758284</td>\n",
       "      <td>0.679727</td>\n",
       "      <td>72.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Carmignac Portfolio Emergents</td>\n",
       "      <td>530</td>\n",
       "      <td>3</td>\n",
       "      <td>0.727076</td>\n",
       "      <td>0.672260</td>\n",
       "      <td>26.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Carmignac Sécurité</td>\n",
       "      <td>1573</td>\n",
       "      <td>4</td>\n",
       "      <td>0.712026</td>\n",
       "      <td>0.883459</td>\n",
       "      <td>85.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Carmignac Portfolio Long-Short European Equities</td>\n",
       "      <td>586</td>\n",
       "      <td>4</td>\n",
       "      <td>0.706218</td>\n",
       "      <td>0.660681</td>\n",
       "      <td>28.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Carmignac Portfolio Flexible Bond</td>\n",
       "      <td>1053</td>\n",
       "      <td>4</td>\n",
       "      <td>0.675677</td>\n",
       "      <td>0.837662</td>\n",
       "      <td>40.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Carmignac Court Terme</td>\n",
       "      <td>507</td>\n",
       "      <td>3</td>\n",
       "      <td>0.674908</td>\n",
       "      <td>0.851615</td>\n",
       "      <td>44.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Carmignac Portfolio Sécurité</td>\n",
       "      <td>1125</td>\n",
       "      <td>3</td>\n",
       "      <td>0.606284</td>\n",
       "      <td>0.651454</td>\n",
       "      <td>56.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Carmignac Credit &lt;NUM&gt;</td>\n",
       "      <td>370</td>\n",
       "      <td>3</td>\n",
       "      <td>0.564300</td>\n",
       "      <td>0.910368</td>\n",
       "      <td>51.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Carmignac Portfolio Credit</td>\n",
       "      <td>984</td>\n",
       "      <td>3</td>\n",
       "      <td>0.543647</td>\n",
       "      <td>0.743567</td>\n",
       "      <td>47.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Carmignac Portfolio Global Bond</td>\n",
       "      <td>1664</td>\n",
       "      <td>4</td>\n",
       "      <td>0.539995</td>\n",
       "      <td>0.675356</td>\n",
       "      <td>66.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Carmignac Portfolio Patrimoine</td>\n",
       "      <td>1107</td>\n",
       "      <td>3</td>\n",
       "      <td>0.453988</td>\n",
       "      <td>0.843918</td>\n",
       "      <td>35.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                         fund_family  n_clients  best_k  \\\n",
       "0                               Carmignac Patrimoine       3058       3   \n",
       "1                                Carmignac Emergents       1724       3   \n",
       "2                           Carmignac Investissement       2126       4   \n",
       "3                  Carmignac Portfolio Grande Europe       1344       3   \n",
       "4            Carmignac Portfolio Emerging Patrimoine       1099       3   \n",
       "5                      Carmignac Portfolio Emergents        530       3   \n",
       "6                                 Carmignac Sécurité       1573       4   \n",
       "7   Carmignac Portfolio Long-Short European Equities        586       4   \n",
       "8                  Carmignac Portfolio Flexible Bond       1053       4   \n",
       "9                              Carmignac Court Terme        507       3   \n",
       "10                      Carmignac Portfolio Sécurité       1125       3   \n",
       "11                            Carmignac Credit <NUM>        370       3   \n",
       "12                        Carmignac Portfolio Credit        984       3   \n",
       "13                   Carmignac Portfolio Global Bond       1664       4   \n",
       "14                    Carmignac Portfolio Patrimoine       1107       3   \n",
       "\n",
       "    silhouette  davies_bouldin  min_cluster_size  \n",
       "0     0.895515        0.370484             156.0  \n",
       "1     0.894999        0.364722              94.0  \n",
       "2     0.880809        0.470763              36.0  \n",
       "3     0.872593        0.411056              81.0  \n",
       "4     0.758284        0.679727              72.0  \n",
       "5     0.727076        0.672260              26.0  \n",
       "6     0.712026        0.883459              85.0  \n",
       "7     0.706218        0.660681              28.0  \n",
       "8     0.675677        0.837662              40.0  \n",
       "9     0.674908        0.851615              44.0  \n",
       "10    0.606284        0.651454              56.0  \n",
       "11    0.564300        0.910368              51.0  \n",
       "12    0.543647        0.743567              47.0  \n",
       "13    0.539995        0.675356              66.0  \n",
       "14    0.453988        0.843918              35.0  "
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "family_results = {}\n",
    "family_summary_rows = []\n",
    "\n",
    "for fam in top_15_families:\n",
    "    print(\"\\n\" + \"=\" * 80)\n",
    "    print(f\"Running clustering for family: {fam}\")\n",
    "    print(\"=\" * 80)\n",
    "\n",
    "    res = run_family_clustering(\n",
    "        df_base=df_family_client_base,\n",
    "        family_name=fam,\n",
    "        features=family_cluster_features,\n",
    "        k_min=3,\n",
    "        k_max=10,\n",
    "        min_clients=50,\n",
    "        min_cluster_size=10,\n",
    "        random_state=RANDOM_STATE,\n",
    "        make_plots=True\n",
    "    )\n",
    "\n",
    "    if res is not None:\n",
    "        family_results[fam] = res\n",
    "        best_row = res[\"metrics_df\"].loc[res[\"metrics_df\"][\"k\"] == res[\"best_k\"]].iloc[0]\n",
    "        family_summary_rows.append({\n",
    "            \"fund_family\": fam,\n",
    "            \"n_clients\": res[\"n_clients\"],\n",
    "            \"best_k\": res[\"best_k\"],\n",
    "            \"silhouette\": best_row[\"silhouette\"],\n",
    "            \"davies_bouldin\": best_row[\"davies_bouldin\"],\n",
    "            \"min_cluster_size\": best_row[\"min_cluster_size\"]\n",
    "        })\n",
    "\n",
    "family_summary = pd.DataFrame(family_summary_rows).sort_values(\n",
    "    [\"silhouette\", \"n_clients\"],\n",
    "    ascending=[False, False]\n",
    ").reset_index(drop=True)\n",
    "\n",
    "family_summary"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c89a451",
   "metadata": {},
   "source": [
    "## 16. Analyses complémentaires"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "bb1aadbc",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_cluster_aum(df_family, result):\n",
    "    df = df_family.copy()\n",
    "    summary = df.groupby(\"cluster\").agg(\n",
    "        n_clients=(ID_COL, \"nunique\"),\n",
    "        total_aum=(\"family_aum_val_mean\", \"sum\")\n",
    "    ).reset_index()\n",
    "    summary[\"aum_share\"] = summary[\"total_aum\"] / summary[\"total_aum\"].sum()\n",
    "    print(summary)\n",
    "    plt.figure(figsize=(8,4))\n",
    "    plt.bar(summary[\"cluster\"], summary[\"aum_share\"])\n",
    "    plt.title(\"AUM Share by Cluster\")\n",
    "    plt.ylabel(\"Share of AUM\")\n",
    "    plt.xlabel(\"Cluster\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "def plot_churn_summary(df_family, result):\n",
    "    churn_summary = df_family.groupby(\"cluster\").agg(\n",
    "        churn_hard=(\"churn_hard\", \"mean\"),\n",
    "        churn_soft=(\"churn_soft\", \"mean\"),\n",
    "        churn_warning=(\"churn_warning\", \"mean\"),\n",
    "        size=(ID_COL, \"nunique\")\n",
    "    ).reset_index()\n",
    "    print(churn_summary)\n",
    "    churn_summary.set_index(\"cluster\")[[\"churn_hard\", \"churn_soft\", \"churn_warning\"]].plot(\n",
    "        kind=\"bar\", figsize=(10,5)\n",
    "    )\n",
    "    plt.title(\"Churn profile by cluster\")\n",
    "    plt.ylabel(\"Rate\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "def plot_behavior_vs_perf(df_family, result):\n",
    "    summary = df_family.groupby(\"cluster\").agg(\n",
    "        corr_3m=(\"corr_flow_ret_3m\", \"mean\"),\n",
    "        corr_6m=(\"corr_flow_ret_6m\", \"mean\"),\n",
    "        buy_after_perf_3m=(\"buy_after_good_perf_share_3m\", \"mean\"),\n",
    "        buy_after_perf_6m=(\"buy_after_good_perf_share_6m\", \"mean\")\n",
    "    ).reset_index()\n",
    "    print(summary)\n",
    "    summary.set_index(\"cluster\").plot(kind=\"bar\", figsize=(10,5))\n",
    "    plt.title(\"Behavior vs Performance by Cluster\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "def plot_pca(result):\n",
    "    X = result[\"X_scaled\"]\n",
    "    labels = result[\"labels\"]\n",
    "    pca = PCA(n_components=2, random_state=RANDOM_STATE)\n",
    "    X_pca = pca.fit_transform(X)\n",
    "    plt.figure(figsize=(6,5))\n",
    "    plt.scatter(X_pca[:,0], X_pca[:,1], c=labels, alpha=0.5)\n",
    "    plt.title(\"PCA projection of clusters\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "bf50ec0c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   cluster  n_clients     total_aum  aum_share\n",
      "0        0       2714  6.096907e+09   0.932291\n",
      "1        1        188  2.021671e+08   0.030914\n",
      "2        2        156  2.406294e+08   0.036795\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 19620 characters]",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   cluster  churn_hard  churn_soft  churn_warning  size\n",
      "0        0    0.689020    0.768976       0.154016  2714\n",
      "1        1    0.909574    0.957447       0.739362   188\n",
      "2        2    0.807692    0.897436       0.724359   156\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 24700 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   cluster   corr_3m   corr_6m  buy_after_perf_3m  buy_after_perf_6m\n",
      "0        0  0.003160  0.005071           0.034501           0.049428\n",
      "1        1  0.020468  0.035094           0.175504           0.228813\n",
      "2        2  0.033462  0.033925           0.288201           0.359633\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 33748 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 68436 characters]",
      "text/plain": [
       "<Figure size 600x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Exemple d'analyses complémentaires sur une famille\n",
    "example_family = top_15_families[0]\n",
    "if example_family in family_results:\n",
    "    res = family_results[example_family]\n",
    "    df_family = res[\"df_family\"].copy()\n",
    "\n",
    "    plot_cluster_aum(df_family, res)\n",
    "    plot_churn_summary(df_family, res)\n",
    "    plot_behavior_vs_perf(df_family, res)\n",
    "    plot_pca(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e28d72a",
   "metadata": {},
   "source": [
    "## 17. Lecture métier rapide d'une famille"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "843b8118",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "### Family: Carmignac Patrimoine\n",
      "K retenu : 3\n",
      "\n",
      "Tailles de clusters :\n",
      "cluster\n",
      "0    2714\n",
      "1     188\n",
      "2     156\n",
      "Name: count, dtype: int64\n",
      "\n",
      "Profils médians :\n",
      "         flow_freq  family_aum_val_mean  gross_flow_to_aum_family  \\\n",
      "cluster                                                             \n",
      "0           0.0000           52115.3372                    0.0000   \n",
      "1           0.0345           72721.5583                    2.5976   \n",
      "2           0.0615          511997.3731                    0.8907   \n",
      "\n",
      "         turnover_mean3_avg  turnover_mean6_avg  turnover_mean12_avg  \\\n",
      "cluster                                                                \n",
      "0                    0.0000              0.0000               0.0000   \n",
      "1                    0.0892              0.0914               0.0898   \n",
      "2                    0.0102              0.0104               0.0105   \n",
      "\n",
      "         exit_rate_per_isin_family  months_since_last_tx_family  \\\n",
      "cluster                                                           \n",
      "0                           1.0000                          0.0   \n",
      "1                           1.0000                         80.5   \n",
      "2                           0.8693                         71.0   \n",
      "\n",
      "         family_share_of_client_aum_mean  corr_flow_ret_3m  corr_flow_ret_6m  \\\n",
      "cluster                                                                        \n",
      "0                                 0.5087            0.0000            0.0000   \n",
      "1                                 0.4170            0.0111            0.0178   \n",
      "2                                 0.3587            0.0184            0.0338   \n",
      "\n",
      "         buy_after_good_perf_share_3m  buy_after_good_perf_share_6m  \n",
      "cluster                                                              \n",
      "0                                 0.0                         0.000  \n",
      "1                                 0.0                         0.000  \n",
      "2                                 0.0                         0.225  \n"
     ]
    }
   ],
   "source": [
    "def describe_family_clusters(family_name, features_for_profile=None):\n",
    "    if family_name not in family_results:\n",
    "        print(f\"Aucun résultat stocké pour {family_name}\")\n",
    "        return None\n",
    "\n",
    "    res = family_results[family_name]\n",
    "    df_family = res[\"df_family\"]\n",
    "\n",
    "    if features_for_profile is None:\n",
    "        features_for_profile = [\n",
    "            \"flow_freq\",\n",
    "            \"family_aum_val_mean\",\n",
    "            \"gross_flow_to_aum_family\",\n",
    "            \"turnover_mean3_avg\",\n",
    "            \"turnover_mean6_avg\",\n",
    "            \"turnover_mean12_avg\",\n",
    "            \"exit_rate_per_isin_family\",\n",
    "            \"months_since_last_tx_family\",\n",
    "            \"family_share_of_client_aum_mean\",\n",
    "            \"corr_flow_ret_3m\",\n",
    "            \"corr_flow_ret_6m\",\n",
    "            \"buy_after_good_perf_share_3m\",\n",
    "            \"buy_after_good_perf_share_6m\",\n",
    "        ]\n",
    "\n",
    "    summary = (\n",
    "        df_family.groupby(\"cluster\")[features_for_profile]\n",
    "        .median()\n",
    "        .round(4)\n",
    "    )\n",
    "\n",
    "    sizes = df_family[\"cluster\"].value_counts().sort_index()\n",
    "\n",
    "    print(f\"\\n### Family: {family_name}\")\n",
    "    print(f\"K retenu : {res['best_k']}\")\n",
    "    print(\"\\nTailles de clusters :\")\n",
    "    print(sizes)\n",
    "    print(\"\\nProfils médians :\")\n",
    "    print(summary)\n",
    "    return summary\n",
    "\n",
    "# Exemple\n",
    "if top_15_families[0] in family_results:\n",
    "    describe_family_clusters(top_15_families[0])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
