Project_Carmignac/analyse_rupture.ipynb

4453 lines
964 KiB
Plaintext
Raw Normal View History

2026-02-02 00:52:00 +01:00
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "338730e2-a6de-4d4f-b438-efe3feb139ab",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import plotly.graph_objects as go"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cfd11919-0941-400e-a516-72871881f733",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_3828/1940519970.py:1: DtypeWarning: Columns (1,2,3,4) have mixed types. Specify dtype option on import or set low_memory=False.\n",
" stocks=pd.read_csv('stocks.csv')\n",
"/tmp/ipykernel_3828/1940519970.py:2: DtypeWarning: Columns (1,2,3,4) have mixed types. Specify dtype option on import or set low_memory=False.\n",
" flows = pd.read_csv('flows.csv')\n"
]
}
],
"source": [
"stocks=pd.read_csv('stocks.csv')\n",
"flows = pd.read_csv('flows.csv')"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "b99e3402-fe26-4f4e-8c1c-5f07847bce94",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_3828/3613746644.py:1: DtypeWarning:\n",
"\n",
"Columns (1) have mixed types. Specify dtype option on import or set low_memory=False.\n",
"\n"
]
}
],
"source": [
"merged = pd.read_csv('merged.csv')"
]
},
{
"cell_type": "code",
"execution_count": 71,
"id": "34e5a815-7269-4312-bfe6-e2cd12595e57",
"metadata": {},
"outputs": [
{
"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>Product - Isin</th>\n",
" <th>n_ruptures</th>\n",
" <th>obs</th>\n",
" <th>rupture_ratio</th>\n",
" <th>max_gap</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>59545</th>\n",
" <td>200127410</td>\n",
" <td>FR0010135103</td>\n",
" <td>434</td>\n",
" <td>436</td>\n",
" <td>0.995413</td>\n",
" <td>295985.420</td>\n",
" </tr>\n",
" <tr>\n",
" <th>59547</th>\n",
" <td>200127410</td>\n",
" <td>FR0010148981</td>\n",
" <td>317</td>\n",
" <td>319</td>\n",
" <td>0.993730</td>\n",
" <td>67134.706</td>\n",
" </tr>\n",
" <tr>\n",
" <th>79698</th>\n",
" <td>PRIVATE CLIENT</td>\n",
" <td>LU0992630599</td>\n",
" <td>154</td>\n",
" <td>155</td>\n",
" <td>0.993548</td>\n",
" <td>529752.634</td>\n",
" </tr>\n",
" <tr>\n",
" <th>14438</th>\n",
" <td>366441</td>\n",
" <td>FR0010148981</td>\n",
" <td>142</td>\n",
" <td>143</td>\n",
" <td>0.993007</td>\n",
" <td>86246.897</td>\n",
" </tr>\n",
" <tr>\n",
" <th>14436</th>\n",
" <td>366441</td>\n",
" <td>FR0010135103</td>\n",
" <td>142</td>\n",
" <td>143</td>\n",
" <td>0.993007</td>\n",
" <td>439262.588</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7476</th>\n",
" <td>365143</td>\n",
" <td>LU0099161993</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>10655.422</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7139</th>\n",
" <td>365096</td>\n",
" <td>FR0010148981</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>962.783</td>\n",
" </tr>\n",
" <tr>\n",
" <th>39691</th>\n",
" <td>420259</td>\n",
" <td>FR0010149120</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>50336.226</td>\n",
" </tr>\n",
" <tr>\n",
" <th>39688</th>\n",
" <td>420259</td>\n",
" <td>FR0010135103</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>17170.593</td>\n",
" </tr>\n",
" <tr>\n",
" <th>39697</th>\n",
" <td>420259</td>\n",
" <td>FR0010306142</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>16087.801</td>\n",
" </tr>\n",
" <tr>\n",
" <th>38491</th>\n",
" <td>419717</td>\n",
" <td>FR0010135103</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>1421.255</td>\n",
" </tr>\n",
" <tr>\n",
" <th>37166</th>\n",
" <td>419097</td>\n",
" <td>FR0010312660</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>1354.356</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2492</th>\n",
" <td>312933</td>\n",
" <td>FR0010312660</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>686.223</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2491</th>\n",
" <td>312933</td>\n",
" <td>FR0010306142</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>65668.045</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2490</th>\n",
" <td>312933</td>\n",
" <td>FR0010149302</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>964.557</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2487</th>\n",
" <td>312933</td>\n",
" <td>FR0010149203</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>2037.774</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2484</th>\n",
" <td>312933</td>\n",
" <td>FR0010149120</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>2156.823</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9505</th>\n",
" <td>365452</td>\n",
" <td>FR0011269182</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>153.588</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2524</th>\n",
" <td>312933</td>\n",
" <td>LU0099161993</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>1754.657</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2540</th>\n",
" <td>312933</td>\n",
" <td>LU0807690911</td>\n",
" <td>129</td>\n",
" <td>130</td>\n",
" <td>0.992308</td>\n",
" <td>3049.651</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Registrar Account - ID Product - Isin n_ruptures obs rupture_ratio \\\n",
"59545 200127410 FR0010135103 434 436 0.995413 \n",
"59547 200127410 FR0010148981 317 319 0.993730 \n",
"79698 PRIVATE CLIENT LU0992630599 154 155 0.993548 \n",
"14438 366441 FR0010148981 142 143 0.993007 \n",
"14436 366441 FR0010135103 142 143 0.993007 \n",
"7476 365143 LU0099161993 129 130 0.992308 \n",
"7139 365096 FR0010148981 129 130 0.992308 \n",
"39691 420259 FR0010149120 129 130 0.992308 \n",
"39688 420259 FR0010135103 129 130 0.992308 \n",
"39697 420259 FR0010306142 129 130 0.992308 \n",
"38491 419717 FR0010135103 129 130 0.992308 \n",
"37166 419097 FR0010312660 129 130 0.992308 \n",
"2492 312933 FR0010312660 129 130 0.992308 \n",
"2491 312933 FR0010306142 129 130 0.992308 \n",
"2490 312933 FR0010149302 129 130 0.992308 \n",
"2487 312933 FR0010149203 129 130 0.992308 \n",
"2484 312933 FR0010149120 129 130 0.992308 \n",
"9505 365452 FR0011269182 129 130 0.992308 \n",
"2524 312933 LU0099161993 129 130 0.992308 \n",
"2540 312933 LU0807690911 129 130 0.992308 \n",
"\n",
" max_gap \n",
"59545 295985.420 \n",
"59547 67134.706 \n",
"79698 529752.634 \n",
"14438 86246.897 \n",
"14436 439262.588 \n",
"7476 10655.422 \n",
"7139 962.783 \n",
"39691 50336.226 \n",
"39688 17170.593 \n",
"39697 16087.801 \n",
"38491 1421.255 \n",
"37166 1354.356 \n",
"2492 686.223 \n",
"2491 65668.045 \n",
"2490 964.557 \n",
"2487 2037.774 \n",
"2484 2156.823 \n",
"9505 153.588 \n",
"2524 1754.657 \n",
"2540 3049.651 "
]
},
"execution_count": 71,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"\n",
"# 1. Prepare stock dataset ISIN-by-ISIN\n",
"stocks_isin = stocks[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"Quantity - AUM\"\n",
"]].copy()\n",
"\n",
"stocks_isin[\"Centralisation Date\"] = pd.to_datetime(stocks_isin[\"Centralisation Date\"])\n",
"\n",
"stocks_isin = stocks_isin.sort_values(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
")\n",
"\n",
"# 2. Prepare flows dataset ISIN-by-ISIN\n",
"flows_isin = flows[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"Quantity - NetFlows\"\n",
"]].copy()\n",
"\n",
"flows_isin[\"Centralisation Date\"] = pd.to_datetime(flows_isin[\"Centralisation Date\"])\n",
"\n",
"flows_isin = (\n",
" flows_isin\n",
" .groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
" )[\"Quantity - NetFlows\"]\n",
" .sum()\n",
" .reset_index()\n",
")\n",
"\n",
"# 3. Merge stocks & flows ISIN-by-ISIN\n",
"merged_isin = stocks_isin.merge(\n",
" flows_isin,\n",
" on=[\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" how=\"left\"\n",
")\n",
"\n",
"merged_isin[\"Quantity - NetFlows\"] = merged_isin[\"Quantity - NetFlows\"].fillna(0)\n",
"\n",
"# 4. Compute expected stock per ISIN for each account\n",
"merged_isin[\"prev_stock\"] = (\n",
" merged_isin\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])[\"Quantity - AUM\"]\n",
" .shift(1)\n",
")\n",
"\n",
"merged_isin[\"prev_netflows\"] = (\n",
" merged_isin\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])[\"Quantity - NetFlows\"]\n",
" .shift(1)\n",
" .fillna(0)\n",
")\n",
"\n",
"merged_isin[\"expected_stock\"] = (\n",
" merged_isin[\"prev_stock\"] + merged_isin[\"prev_netflows\"]\n",
")\n",
"\n",
"# 5. Detect ruptures ISIN-by-ISIN (no aggregation)\n",
"TOL = 1e-6\n",
"\n",
"merged_isin[\"gap\"] = (\n",
" merged_isin[\"Quantity - AUM\"] - merged_isin[\"expected_stock\"]\n",
")\n",
"\n",
"merged_isin[\"rupture_flag\"] = (\n",
" merged_isin[\"prev_stock\"].notna()\n",
" & (merged_isin[\"gap\"].abs() > TOL)\n",
")\n",
"\n",
"# 6. Summarize ruptures per (Account, ISIN)\n",
"rupture_isin_summary = (\n",
" merged_isin\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .agg(\n",
" n_ruptures=(\"rupture_flag\", \"sum\"),\n",
" obs=(\"rupture_flag\", \"count\"),\n",
" rupture_ratio=(\"rupture_flag\", \"mean\"),\n",
" max_gap=(\"gap\", lambda x: x.abs().max())\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"# Sort by worst ISIN trajectories\n",
"rupture_isin_summary = rupture_isin_summary.sort_values(\n",
" \"rupture_ratio\",\n",
" ascending=False\n",
")"
]
},
{
"cell_type": "markdown",
"id": "16213cb2-07d8-4e82-b9bb-252554ec47b9",
"metadata": {},
"source": [
"# Détection des ruptures"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "78c3db70-e0b6-4de2-92ca-e29cf5bf6bd1",
"metadata": {},
"outputs": [],
"source": [
"# ============================================================\n",
"# AUMFLOW CONSISTENCY & RUPTURE DETECTION (FINAL VERSION)\n",
"# ============================================================\n",
"# ------------------------------------------------------------\n",
"# 1. Keep relevant columns\n",
"# ------------------------------------------------------------\n",
"stocks_clean = stocks[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"Quantity - AUM\"\n",
"]].copy()\n",
"\n",
"flows_clean = flows[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"Quantity - NetFlows\"\n",
"]].copy()\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 2. Date formatting\n",
"# ------------------------------------------------------------\n",
"stocks_clean[\"Centralisation Date\"] = pd.to_datetime(stocks_clean[\"Centralisation Date\"])\n",
"flows_clean[\"Centralisation Date\"] = pd.to_datetime(flows_clean[\"Centralisation Date\"])\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 3. Aggregate flows per day\n",
"# ------------------------------------------------------------\n",
"flows_clean = (\n",
" flows_clean\n",
" .groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" as_index=False\n",
" )[\"Quantity - NetFlows\"]\n",
" .sum()\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 4. Merge stocks and flows\n",
"# ------------------------------------------------------------\n",
"df = stocks_clean.merge(\n",
" flows_clean,\n",
" on=[\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" how=\"left\"\n",
")\n",
"\n",
"df[\"Quantity - NetFlows\"] = df[\"Quantity - NetFlows\"].fillna(0)\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 5. Sort and compute expected stock\n",
"# ------------------------------------------------------------\n",
"df = df.sort_values(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
")\n",
"\n",
"df[\"prev_stock\"] = df.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - AUM\"].shift(1)\n",
"\n",
"df[\"prev_flows\"] = df.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - NetFlows\"].shift(1).fillna(0)\n",
"\n",
"df[\"expected_stock\"] = df[\"prev_stock\"] + df[\"prev_flows\"]\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 6. Compute gaps\n",
"# ------------------------------------------------------------\n",
"df[\"gap\"] = df[\"Quantity - AUM\"] - df[\"expected_stock\"]\n",
"df[\"gap_abs\"] = df[\"gap\"].abs()\n",
"df[\"gap_rel\"] = df[\"gap_abs\"] / df[\"expected_stock\"].abs().clip(lower=1)\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 7. Detect ruptures (economic rule)\n",
"# ------------------------------------------------------------\n",
"TAU_ABS = 10.0 # minimum absolute gap (shares)\n",
"TAU_REL = 0.005 # minimum relative gap (0.5%)\n",
"\n",
"df[\"rupture_flag\"] = (\n",
" df[\"prev_stock\"].notna()\n",
" & (df[\"gap_abs\"] > TAU_ABS)\n",
" & (df[\"gap_rel\"] > TAU_REL)\n",
")\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 8. Remove end-of-sample false positives (edge effects)\n",
"# ------------------------------------------------------------\n",
"last_date = df[\"Centralisation Date\"].max()\n",
"\n",
"df[\"rupture_flag\"] = np.where(\n",
" (df[\"rupture_flag\"]) & (df[\"Centralisation Date\"] == last_date),\n",
" False,\n",
" df[\"rupture_flag\"]\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a9783dc1-e225-4142-8b6f-6f9e620b4b3d",
"metadata": {},
"outputs": [],
"source": [
"# ------------------------------------------------------------\n",
"# 9. ISIN-level summary (AFTER CLEANING)\n",
"# ------------------------------------------------------------\n",
"rupture_isin_summary = (\n",
" df.groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .agg(\n",
" n_ruptures=(\"rupture_flag\", \"sum\"),\n",
" total_obs=(\"rupture_flag\", \"count\"),\n",
" rupture_ratio=(\"rupture_flag\", \"mean\"),\n",
" max_gap=(\"gap_abs\", \"max\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 10. Account-level summary (AFTER CLEANING)\n",
"# ------------------------------------------------------------\n",
"rupture_summary = (\n",
" df.groupby(\"Registrar Account - ID\")\n",
" .agg(\n",
" n_ruptures=(\"rupture_flag\", \"sum\"),\n",
" total_obs=(\"rupture_flag\", \"count\"),\n",
" rupture_ratio=(\"rupture_flag\", \"mean\"),\n",
" max_gap=(\"gap_abs\", \"max\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"\n",
"# ------------------------------------------------------------\n",
"# 11. Outputs\n",
"# ------------------------------------------------------------\n",
"df.to_csv(\"aum_flow_gaps.csv\", index=False)\n",
"rupture_isin_summary.to_csv(\"rupture_isin_summary.csv\", index=False)\n",
"rupture_summary.to_csv(\"rupture_summary.csv\", index=False)"
]
},
{
"cell_type": "code",
"execution_count": 75,
"id": "f5b62558-c27a-4428-a193-8b97e0ce6b6a",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hole": 0.45,
"hoverinfo": "label+percent",
"labels": [
"Clean / quasi-clean (≤1%)",
"Moderate (110%)",
"High (1030%)",
"Severe (>30%)"
],
"textinfo": "percent",
"type": "pie",
"values": {
"bdata": "mpmZmZmZSEBmZmZmZqZGQM3MzMzMzBRAMzMzMzMz0z8=",
"dtype": "f8"
}
}
],
"layout": {
"legend": {
"orientation": "h",
"title": {
"text": "Rupture ratio"
},
"x": 0.5,
"xanchor": "center",
"y": -0.15,
"yanchor": "top"
},
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"sequentialminus": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
}
}
},
"image/png": "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
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Base\n",
"rs = rupture_summary.copy()\n",
"\n",
"# Classes simplifiées\n",
"bins = [0, 0.01, 0.10, 0.30, 1.01]\n",
"labels = [\n",
" \"Clean / quasi-clean (≤1%)\",\n",
" \"Moderate (110%)\",\n",
" \"High (1030%)\",\n",
" \"Severe (>30%)\"\n",
"]\n",
"\n",
"rs[\"rupture_class\"] = pd.cut(\n",
" rs[\"rupture_ratio\"],\n",
" bins=bins,\n",
" labels=labels,\n",
" include_lowest=True\n",
")\n",
"\n",
"# Distribution en %\n",
"dist = (\n",
" rs[\"rupture_class\"]\n",
" .value_counts(normalize=True)\n",
" .sort_index()\n",
" * 100\n",
").round(1)\n",
"\n",
"# Donut chart\n",
"fig = go.Figure(\n",
" data=[go.Pie(\n",
" labels=dist.index,\n",
" values=dist.values,\n",
" hole=0.45,\n",
" textinfo=\"percent\",\n",
" hoverinfo=\"label+percent\"\n",
" )]\n",
")\n",
"\n",
"fig.update_layout(\n",
" legend=dict(\n",
" orientation=\"h\", # horizontale\n",
" yanchor=\"top\",\n",
" y=-0.15, # en dessous du graphe\n",
" xanchor=\"center\",\n",
" x=0.5\n",
" ),\n",
" legend_title_text=\"Rupture ratio\"\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"id": "e52cd650-df05-490d-af59-e66c058f955d",
"metadata": {},
"source": [
"## AUMFLOW CONSISTENCY & DISCONTINUITY DETECTION"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "a7efe494-f5fa-43f8-8446-942fc2d3bd4c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Detection threshold epsilon (trimmed 99th percentile): 40.03%\n"
]
}
],
"source": [
"# ------------------------------------------------------------\n",
"# 1. Keep relevant columns\n",
"# ------------------------------------------------------------\n",
"stocks_clean = stocks[\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\", \"Quantity - AUM\"]\n",
"].copy()\n",
"\n",
"flows_clean = flows[\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\", \"Quantity - NetFlows\"]\n",
"].copy()\n",
"\n",
"# ------------------------------------------------------------\n",
"# 2. Date formatting\n",
"# ------------------------------------------------------------\n",
"stocks_clean[\"Centralisation Date\"] = pd.to_datetime(stocks_clean[\"Centralisation Date\"])\n",
"flows_clean[\"Centralisation Date\"] = pd.to_datetime(flows_clean[\"Centralisation Date\"])\n",
"\n",
"# ------------------------------------------------------------\n",
"# 3. Aggregate flows per day\n",
"# ------------------------------------------------------------\n",
"flows_clean = (\n",
" flows_clean\n",
" .groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" as_index=False\n",
" )[\"Quantity - NetFlows\"]\n",
" .sum()\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 4. Merge stocks and flows\n",
"# ------------------------------------------------------------\n",
"df = stocks_clean.merge(\n",
" flows_clean,\n",
" on=[\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" how=\"left\"\n",
")\n",
"\n",
"df[\"Quantity - NetFlows\"] = df[\"Quantity - NetFlows\"].fillna(0)\n",
"\n",
"# ------------------------------------------------------------\n",
"# 5. Sort and reconstruct expected stock\n",
"# ------------------------------------------------------------\n",
"df = df.sort_values(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
")\n",
"\n",
"df[\"prev_stock\"] = (\n",
" df.groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" [\"Quantity - AUM\"]\n",
" .shift(1)\n",
")\n",
"\n",
"df[\"prev_flows\"] = (\n",
" df.groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" [\"Quantity - NetFlows\"]\n",
" .shift(1)\n",
" .fillna(0)\n",
")\n",
"\n",
"df[\"expected_stock\"] = df[\"prev_stock\"] + df[\"prev_flows\"]\n",
"\n",
"# ------------------------------------------------------------\n",
"# 6. Compute accounting gaps\n",
"# ------------------------------------------------------------\n",
"df[\"gap\"] = df[\"Quantity - AUM\"] - df[\"expected_stock\"]\n",
"df[\"gap_abs\"] = df[\"gap\"].abs()\n",
"\n",
"# Relative gap normalised by previous stock\n",
"df[\"gap_rel\"] = (\n",
" df[\"gap_abs\"] /\n",
" df[\"prev_stock\"].abs().replace(0, np.nan)\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 7. Calibration sample (valid regime)\n",
"# ------------------------------------------------------------\n",
"valid_gaps = df.loc[\n",
" df[\"gap_rel\"].notna() & (df[\"prev_stock\"] > 0),\n",
" \"gap_rel\"\n",
"]\n",
"\n",
"# ------------------------------------------------------------\n",
"# 8. Robust, data-driven threshold (epsilon)\n",
"# ------------------------------------------------------------\n",
"# Step 1 — trim extreme breaks to avoid calibrating on resets\n",
"gap_rel_trimmed = valid_gaps[\n",
" valid_gaps <= valid_gaps.quantile(0.90)\n",
"]\n",
"\n",
"# Step 2 — define epsilon on the upper tail of the trimmed distribution\n",
"EPSILON = gap_rel_trimmed.quantile(0.99)\n",
"\n",
"# ------------------------------------------------------------\n",
"# 9. Detect discontinuities (diagnostic rule)\n",
"# ------------------------------------------------------------\n",
"df[\"rupture_flag\"] = (\n",
" df[\"prev_stock\"].notna()\n",
" & (df[\"prev_stock\"] > 0)\n",
" & (df[\"gap_rel\"] > EPSILON)\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 10. Remove end-of-sample edge effects\n",
"# ------------------------------------------------------------\n",
"last_date = df[\"Centralisation Date\"].max()\n",
"\n",
"df.loc[\n",
" (df[\"rupture_flag\"]) &\n",
" (df[\"Centralisation Date\"] == last_date),\n",
" \"rupture_flag\"\n",
"] = False\n",
"\n",
"# ------------------------------------------------------------\n",
"# 11. ISIN-level summary\n",
"# ------------------------------------------------------------\n",
"rupture_isin_summary = (\n",
" df.groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .agg(\n",
" n_ruptures=(\"rupture_flag\", \"sum\"),\n",
" total_obs=(\"rupture_flag\", \"count\"),\n",
" rupture_ratio=(\"rupture_flag\", \"mean\"),\n",
" max_gap_abs=(\"gap_abs\", \"max\"),\n",
" max_gap_rel=(\"gap_rel\", \"max\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 12. Account-level summary\n",
"# ------------------------------------------------------------\n",
"rupture_summary = (\n",
" df.groupby(\"Registrar Account - ID\")\n",
" .agg(\n",
" n_ruptures=(\"rupture_flag\", \"sum\"),\n",
" total_obs=(\"rupture_flag\", \"count\"),\n",
" rupture_ratio=(\"rupture_flag\", \"mean\"),\n",
" max_gap_abs=(\"gap_abs\", \"max\"),\n",
" max_gap_rel=(\"gap_rel\", \"max\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 13. Outputs\n",
"# ------------------------------------------------------------\n",
"df.to_csv(\"aum_flow_gaps.csv\", index=False)\n",
"rupture_isin_summary.to_csv(\"rupture_isin_summary.csv\", index=False)\n",
"rupture_summary.to_csv(\"rupture_summary.csv\", index=False)\n",
"\n",
"print(f\"Detection threshold epsilon (trimmed 99th percentile): {EPSILON:.2%}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d7454212-1493-4715-a436-c331931f92fa",
"metadata": {},
"outputs": [
{
"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>Product - Isin</th>\n",
" <th>n_ruptures</th>\n",
" <th>total_obs</th>\n",
" <th>rupture_ratio</th>\n",
" <th>max_gap</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>79298</th>\n",
" <td>OFF DISTRIBUTION</td>\n",
" <td>GB00BQXJRP97</td>\n",
" <td>1</td>\n",
" <td>21</td>\n",
" <td>0.047619</td>\n",
" <td>4.254300e+07</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Registrar Account - ID Product - Isin n_ruptures total_obs \\\n",
"79298 OFF DISTRIBUTION GB00BQXJRP97 1 21 \n",
"\n",
" rupture_ratio max_gap \n",
"79298 0.047619 4.254300e+07 "
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rupture_isin_summary.sort_values(\"rupture_ratio\").head(1)\n",
"rupture_isin_summary.sort_values(\"rupture_ratio\", ascending=False).head(1)\n",
"rupture_isin_summary.sort_values(\"max_gap\", ascending=False).head(1)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "b4040847-e0cf-4aa5-966c-d1fbf3935b7d",
"metadata": {},
"outputs": [],
"source": [
"def plot_isin_evolution(df, account_id, isin, title_suffix=\"\"):\n",
" sub = df[\n",
" (df[\"Registrar Account - ID\"] == account_id) &\n",
" (df[\"Product - Isin\"] == isin)\n",
" ].copy()\n",
"\n",
" if sub.empty:\n",
" print(\"No data for this (account, ISIN).\")\n",
" return\n",
"\n",
" plt.figure(figsize=(10,4))\n",
"\n",
" # Stock observé\n",
" plt.plot(\n",
" sub[\"Centralisation Date\"],\n",
" sub[\"Quantity - AUM\"],\n",
" label=\"Observed stock\",\n",
" linewidth=2\n",
" )\n",
"\n",
" # Stock attendu\n",
" plt.plot(\n",
" sub[\"Centralisation Date\"],\n",
" sub[\"expected_stock\"],\n",
" label=\"Expected stock\",\n",
" linestyle=\"--\"\n",
" )\n",
"\n",
" # Ruptures\n",
" rupt = sub[sub[\"rupture_flag\"]]\n",
" plt.scatter(\n",
" rupt[\"Centralisation Date\"],\n",
" rupt[\"Quantity - AUM\"],\n",
" color=\"red\",\n",
" label=\"Rupture\",\n",
" zorder=5\n",
" )\n",
"\n",
" plt.title(f\"ISIN {isin} — Account {account_id} {title_suffix}\")\n",
" plt.xlabel(\"Date\")\n",
" plt.ylabel(\"AUM (shares)\")\n",
" plt.legend()\n",
" plt.grid(True)\n",
" plt.tight_layout()\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "e5d7a5ab-40bd-452d-a6ae-d56e220c592f",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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ezf7XBfmcyWEKB7p8iSEKJTMzEw2b95M79696dOnj11rRZ7c3Fzmzp1L165dadKkCd26dePjjz8u9P6MjAxefvll2rRpQ1hYGGPGjOHIkSO2/QV1ZwEYOXIkzz33nO11XpeSjRs30qtXLyIiIpg4cSKXL18GrN1ZHnzwQQB69uxJSEgIDzzwQIHXrdVq8fDwsNvm7++Pt7c3Fy9etG3btm0bERER+Pv727b17t2bjIwM/v77bwD+/vtvMjMz6d27t11eERERbN261S6vnj17otFo7PL666+/bN1Btm7dSvXq1QkLC7Ol6d69O1qtlu3btwPg6uqKs7OzXdlDQkJs9/JmvLy8btnK8O8015e3osir8ypVquTbpyjKLY+fMGEC586dY9OmTSVetlvJzs5m7969du8XjUZDz5497d4vBSlM/Xl7e9t159qxYwcJCQmMGjXqhsecP3+emJgYuzK5urrSqVMnuzKZzWb0er3dse7u7vm6AN6qTGazGcAuL3d3dwC7Ll2Fee8V5nMlhCieivfXXwhRIW3evBmj0UifPn249957ycrK4o8//rBL88ILL/D5558zbNgwPv/8cx577DGuXr1a6P0zZsxg7dq1PPnkk3zwwQcoisJDDz1EUlJSkct74cIFPv74Y5566in++9//cvToUV5//XUAGjduzMsvvwzAhx9+yI8//sgrr7xSpLyvXr1KcHCwbVt0dLStC0qeoKAgHB0dOXfuHADnzp3DycmJGjVq2KWrU6eOLU1mZiYJCQnUqVMnX5rc3FxiYmJueD5HR0eqV69uy6sg+/fvB7Arex6TyYTBYGDNmjXs2LGD4cOH50tjNpvJzMxk27ZtrFy58qYPnxVBrVq1cHFx4b///S87d+4s8oNj7dq16d69O59//nkplfDGYmJiMJlMBb4X4uPj841jKkzdmM1msrKy2L9/P998841dmkOHDuHl5cW+ffvo3r07jRo1YtCgQezevduWJjo62laGf5fp+vfdwIED2bFjB+vXr8dgMHD48GG++eabAseO3KxMbdu2pVq1asyePZsrV65w5coV3n77bYKCgmjfvn0h7qJVYT9XQojikTEUQohCWbNmDTVr1qRJkyaA9YF0zZo19OvXD4DTp0/zyy+/8MYbbzBs2DDbcXl9z2+1/+TJk6xbt44PP/zQ9u1nmzZt6NKlC4sWLeKZZ54pUnlTU1NZunQp1apVA+DSpUu89957WCwW9Ho99erVAyA0NLTAh+ubmTVrFgEBAXb979PS0vK1ZAB4eHiQlpZmS5P37eqN0qSnpwPkS5eXd2pqqi2vgsZ+eHp62vL6N5PJxHvvvUdYWBjNmjWz23flyhXbA5pWq+WVV16hU6dO+fIIDw+3PZRPmTKlwKCjInF3d+e1117jlVde4eGHH8bR0ZEWLVowZMgQ23v3ViZOnMiwYcPYvXs3rVu3LuUSX5NX1zd6L6SlpeHi4mLbXpi66du3r+3Bf+DAgTz55JO2fYmJiRiNRl555RWeeuopqlevzldffcWkSZNYt24d/v7+tjL9+71+/XsYoFu3brz66qtMmzbN1uLQv39/u/MVpkwuLi588803trEYANWrV+eLL77I1/p2M4X9XAkhikcCCiHELRkMBv78808eeugh27Y+ffqwaNEiDAYDer2ev/76C41
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxIAAAEgCAYAAAAg6UVEAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAjA1JREFUeJzs3XdcVfX/wPHXHVzWhYsIguA2AReK4h6VWZZmOTJXy7TUlmVqWv0a2jJHpZWWo22WaaXfNFc50lxlTtziACf7Ahe4957fHzeu3AC9XOEyfD8fDx7AOZ97zvvcD+dy3/ezVIqiKAghhBBCCCFECajLOwAhhBBCCCFE5SOJhBBCCCGEEKLEJJEQQgghhBBClJgkEkIIIYQQQogSk0RCCCGEEEIIUWKSSAghhBBCCCFKTBIJIYQQQgghRIlJIiGEEEIIIYQoMUkkhBBCCCGEECUmiYQQ4qpycnJo1aoVMTExZGdnl3c41+Xs2bPMnj2b1NTUq5azWCx8+umnDBo0iNjYWDp16sSYMWNISEgo8piPPfYYLVu2pHPnzsyaNQur1VroeB988AGdO3emZcuWPP7444WOtWXLFp599lluvvlmIiMjWbJkSaFzbdy4kUGDBtGuXTuio6Pp1asXX3/9NYqiOJRLSUnh+eefp1WrVrRr147JkydjMpkcYo6MjCzya/jw4Q7H+vHHH+nduzctW7akW7duvPvuu4X+DrZv3859991H8+bNufXWW/n0008LxVSW8q9n69at9m2ZmZlMnz6dO+64g+bNm9O5c2eGDRvGunXr7GVmz55N165dHa4jMjKSW265hby8PIdzTJw4kcGDB181jgcffLDI5/TXX3+1ny9/W1RUFF26dOGZZ57hzJkzhY51rTrM9+WXX9KtWzeio6MZMmQIhw4dcti/b98+JkyYwO23305kZCTvvfdekbE7c75vvvmG4cOHExsbS2RkJKdOnbrq87Fx40YiIyOLfN5Wr15Nr169aNasGT169GDp0qUO+3Nzc5k6dSqDBg2iefPmDvWUz2g08v7779OvXz9iYmK45ZZb+L//+z9SUlIKlY2Li2PIkCFER0fTrVs3vv7666vGLoRwjra8AxBCVGwbN24kMzMTgA0bNnDXXXeVc0SuS0hI4MMPP+See+4hICCg2HImk4kFCxZw33338dRTT2Eymfjoo494+OGH+fnnn/H19QVsb3aGDx+OwWBg1qxZnD9/nrfffhuNRsOTTz5pP95HH33E559/zsSJE6lZsyYfffQRI0aMYPny5Xh4eACwefNmjh8/zs0338x3331XZFypqam0a9eOESNG4Ovry86dO3nzzTcxm8088sgj9nLPPPMMFy9e5N133yUnJ4e33noLk8nEW2+9BUCNGjUKnePy5cs8+eSTdOnSxb5tzZo1TJw4kREjRtC5c2dOnDjBzJkzMRqNTJ48GYDTp08zYsQIbrvtNsaMGcPBgweZNWsWGo2mUFLiThMmTGDv3r088cQTNGjQgAsXLrB161b+/PNPunfvftXHnjt3jhUrVtCvX78Sn7dz5848/fTTDtvq1atn/zkwMJA5c+agKAonT55k5syZPP744/z888/odDp7uWvVIcDSpUt55513eO6552jWrBmff/45jz76KL/88gvVqlUD4O+//2bPnj20bt26yDfYJTnf8uXLUavVdOjQgTVr1lz1ecjLy+Ptt9+mevXqhfbt2rWLMWPGMHjwYCZNmsSWLVt46aWX8PX15c477wRs9+DSpUtp0aIFTZs2JTExsdBxEhMT+fHHH7n//vtp1aoVly9f5oMPPmD06NF88803aDQaAJKTkxk2bBjR0dF88sknHDhwgLfeegu9Xk+fPn2ueh1CiGtQhBDiKsaMGaN069ZNufXWW5Wnn366vMO5Ltu2bVMiIiKU+Pj4q5Yzm81KWlqaw7bz588rUVFRyvLly+3bfvzxR6Vp06bK+fPn7ds+/fRTJSYmRjGZTIqiKEp2drbSsmVLZd68eQ7HatKkicOxLBaL/eeIiAjl+++/d+qann/+eaVfv37233fu3KlEREQoe/bssW/75ZdflKioKCUxMbHY43z99ddKZGSkcu7cOfu2MWPGKIMHD3YoN2vWLKVt27b2319++WWlR48eDvG/8847SmxsrJKTk+PUNVyvM2fOKBEREcqWLVsURbE9vxEREcpvv/1WqKzVarX/PGvWLKVLly723/P/PoYOHarcddddDmVfeOEFZdCgQVeN44EHHlCef/75Yvf/93yKYqubiIgIZdeuXfZtztbhbbfdprz22mv23zMzM5V27dopc+bMsW8rWC+33nqrMnPmzEJxOXu+/GM5cx999tlnysCBA4t83oYNG6Y88sgjDtuefvpp5a677nLYlv/8F/W85V9vdna2w7Z//vlHiYiIUP766y/7tg8//FBp3769kpWVZd/26quvKnfccUex8QshnCNdm4QQxcrKymLDhg3ceeed3HXXXQ6tE/ny8vKYNWsW3bp1o1mzZtx22218/PHHTu/PzMzklVdeoX379kRHR/PAAw+wf/9++/6iuq0ADB48mIkTJ9p/z+96sm7dOnr06EFMTAyPPfYYFy9eBGzdVh566CEA7rjjDiIjI3nwwQeLvG6NRoO/v7/DtpCQEKpVq8bZs2ft2/744w9iYmIICQmxb7vzzjvJzMzk77//BmyfCGdlZdk/ac0/VkxMDJs3b7ZvU6tdezkOCAjAbDbbf9+8eTPh4eFER0fbt3Xv3h2NRsOWLVuKPc7KlStp1aoVoaGh9m0WiwW9Xu9Qzs/Pz6Hr1qFDh2jfvr1D/B07diQ9PZ3du3e7dE3XK7/Oi/o0XKVSXfPxI0aM4OTJk6xfv77UY/uvyMhIAC5cuGDf5kwdnjp1ijNnzjj8Xfn4+HDzzTeX+O/K2b8ZZ/9Gk5OT+fjjjx3uz4IOHTpEx44dHbZ17NiR48ePO9xf16orHx8fvLy8HLblP5//vU9vvvlmvL297dvuvPNO4uPji+xWJoRwniQSQohibdiwgezsbO666y569uyJyWTi999/dyjz0ksvMW/ePAYMGMC8efN48sknSU5Odnr/pEmTWLVqFc899xzvv/8+KpWKhx9+mKSkpBLHe/r0aT7++GOef/553nzzTQ4cOMCUKVMAaNq0Ka+88goAH3zwAd999x2vvvpqiY6dnJxM3bp17dvi4+OpX7++Q7natWuj0+k4efIkACdPnsTT05NatWo5lGvQoIG9TElZLBaysrL4448/WL58OUOGDLlqTDqdjvDw8GLPd+HCBf766y969uzpsL1Pnz5s3bqVNWvWYDQa2bdvH1999ZVDn3eTyWTvnlXwfAAnTpxw6fquV7169fD29ubNN9/kzz//JDc3t0SPr1+/Pt27d2fevHklPreiKJjNZvuXxWK5avlz584BEB4ebt/mTB3Gx8cDtr+jglz5u3Llb+ZqZs2aRadOnWjZsmWR+8vybyY/ef3vfVrU81Qa5xPiRidjJIQQxVq5ciV16tShWbNmgO2f88qVK7n77rsBOHbsGD///DNvvPEGAwYMsD8uv2/5tfYfOXKE1atX88EHH9g/WW3fvj233norCxcuZPz48SWKNy0tjR9++IGaNWsCcP78eWbMmIHVakWv13PTTTcB0LhxY4c3Gs6YOnUqoaGhDv3r09PTC7VcAPj7+5Oenm4v4+fnd9UyJdWyZUv7m+PRo0czcOBAh5iKGv9hMBiKPd+qVatQqVT06NHDYfttt93Ga6+9xtixY+2Dj++55x6ee+45e5m6deuyb98+h8fl/56WllbyiysFfn5+vP7667z66qs88sgj6HQ6YmNj6d+/v/1v91oee+wxBgwYwPbt22nXrp3T5/7f//7H//73P/vvderUYe3atQ5lzGYziqIQHx/PzJkz6dixIy1atLDvd6YO85/b//79ufJ35crfTHEOHz7Mzz//zIoVK4otU1Z/M2azmRkzZhAdHV3o+fzvPWgwGOz7hBCuk0RCCFEko9HIpk2bePjhh+3b7rrrLhYuXIjRaESv17Njxw7UajX33ntvkce41v4
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxMAAAEgCAYAAADPKy56AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAb1pJREFUeJzt3Xd4U+XbB/BvZveEDsqmpWVDS6HsLUtA9kZARIYggkzxRYY/EUFE9haRIUuWspEhU0FERtktdENXutuM8/5RGxtaaBranrT9fq6Li+acJyf3uZM2uXOeIREEQQAREREREVE+ScUOgIiIiIiIiicWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0REREREZBIWE0QEAEhPT4efnx98fX2RmpoqdjhvJDQ0FCtWrEB8fPxr22m1Wqxfvx4DBw6Ev78/mjdvjkmTJiEsLCzXY44ePRoNGjRAixYtsHz5cuh0uhzH++6779CiRQs0aNAAH3zwQa7Hunz5Mnr16oW6deuiS5cuOHLkSI42N27cwLBhw9CwYUM0a9Ys17jatWsHHx8fg3/Dhg0zaPPy/qx/nTp1Mmh3/PhxvP3226hTpw46deqEffv2Gex/8uQJPv/8c3Tq1An169dH586dsX79emg0mtfmuKC1a9cO3377rf62IAjYs2cP3nnnHTRo0AABAQHo27cv1q1bp29z9epV+Pj44OnTp/ptPj4+qFGjBh4/fmxw/J9//hk+Pj6vPa8VK1agVatWue573f1nzpyJQYMGGWwLDAzE4MGDUa9ePbRr1w7btm3Lcb979+7h3XffRb169dC8eXN8/fXXyMjIMGgTHx+P6dOnw9/fH76+vvjoo4/w/PlzgzY7d+7Eu+++i4CAAAQEBGD06NF48OBBrudx5MgR/Wu0adOm+Oijj/T7EhMT8eGHH6J169aoW7cuWrZsiVmzZuV4vO3bt2PUqFHw9/fPkf/8xBQbG4sFCxagV69eqFWrVo4cAv89x7n9mzNnTq7nSEQFQy52AERkHs6dO4fk5GQAwNmzZ9GlSxeRIzJdWFgYVq5ciR49esDR0fGV7dLS0rBp0yb07dsXEyZMQFpaGlatWoXhw4fj4MGDsLGxAQBkZGRg1KhRcHBwwPLlyxEZGYmFCxdCJpPhww8/1B9v1apV2LJlC2bOnIly5cph1apVeP/993Ho0CEoFAoAwOPHjzFmzBh07doVM2bMwIULF/DJJ5+gbNmyaNy4sT7+UaNGoXHjxli2bBmSk5Px3XffYcyYMThw4ADk8v/+dPfu3RsDBgzQ37a1tTU4x127dhnc1mg0GDlyJFq2bKnfdu3aNUyaNAmDBg3CrFmzcPHiRcyePRs2Njbo3LkzAODSpUu4ffs2RowYgWrVquHu3btYvny5/kOsWLZu3YrFixdjzJgxaNiwIZKTk3Hz5k2cPXsWY8aMee19BUHAhg0b8NVXXxVRtIZiY2MxcuRI1KtXD+vWrcOdO3fw5ZdfwtbWFj179gQAJCQkYMSIEfDx8cHy5csRFhaGJUuWIC0tzeBD8scff4xnz55h3rx5kMvlWLp0KUaPHo19+/bpXy8bNmxA69atMXLkSEilUmzZsgVDhw7F4cOH4ebmpj/Wzp07sXDhQnzwwQeYOXMm4uLicOXKFf3+jIwM2NjYYPLkyShXrhwiIiKwcuVKjB07Fnv27IFMJgMAHDp0CFKpFE2bNsWJEydyzYExMUVFReHYsWNo0KDBK4u82rVr53itBwYGYu7cuQavdSIqBAIRkSAIkyZNEtq1aye0bdtWmDhxotjhvJErV64I3t7eQnBw8GvbaTQaQaVSGWyLjIwUatSoIRw6dEi/bf/+/ULt2rWFyMhI/bb169cLvr6+QlpamiAIgpCamio0aNBA2LBhg8GxatWqZXCsWbNmCd26dRO0Wq1+2/vvvy+MHDlSf3v79u1C7dq1hZSUlBznFBgYqN/Wtm1bYenSpXnmI7szZ84I3t7ewvXr1/XbRo4cKYwYMcKg3cSJE4UuXbrob8fGxuY41rp164S6desKGo0mXzG8iZfPuWPHjsJXX32Vo51Op9P/nNvrwdvbWxgyZIhQu3ZtITw8XL993759gre3t6BWq18Zw/Lly4WWLVvmuu91958xY4YwcOBA/e2VK1cKTZo0MXieP//8c6Fjx47622vWrBEaNWokJCUl6bdt27ZNqFmzpv71eP36dcHb21u4du2avs39+/cFb29v4ddff9Vve/k5TE5OFho3biysWbNGvy06Olpo0KCBsGfPnleef24uXrwoeHt7Cw8ePNBvy3qNv+730ZiYsv+uvJzD1/nqq68MfkeJqHCwmxMRISUlBWfPnkXnzp3RpUsXg6sUWdRqNZYvX4527dqhTp06aN++PVavXm30/uTkZMyZMwdNmjRBvXr1MHToUNy+fVu/PzQ0FD4+Prh06ZLB4w4aNAgzZ87U387qKnLq1Cl06tQJvr6+GD16tL6LxdWrV/Huu+8CADp27Jhr158sMpkM9vb2Btvc3Nzg5OSE0NBQ/bYLFy7A19fX4Nvbzp07Izk5GX/99RcA4K+//kJKSor+m/ysY/n6+uL33383OFbHjh0hlUoNjvXHH3/ou65otVooFApYWlrq29jZ2eV6Dvl15MgRlCtXDr6+vvpt9+7dQ7NmzQzaNWvWDI8fP9bnwcnJKcexfHx8kJ6ejujo6AKJzRTPnz9HmTJlcmyXSCR53vftt9+Gq6srNm/eXBih5enChQto3bo1rKys9Ns6d+6M4OBghISEAMh8burXr6+/SgYATZs2hVarxcWLF/VtrKys0LBhQ30bb29vuLi44Pz58/ptLz+H1tbWqFSpksFr/dixYwCAd955J1/nknUFMPuVg+yv8VcxJiZjjvMyQRBw7NgxtG/fHhYWFvm+PxEZj8UEEeHs2bNITU1Fly5d0LVrV6SlpeHMmTMGbWbPno0NGzagX79+2LBhAz788EPExsYavX/WrFk4evQoJk+ejGXLlkEikWD48OGIiYnJd7zPnj3D6tWr8cknn+B///sf7ty5gwULFgDI7O6Q1f3ju+++w65du/D555/n69ixsbGoXLmyfltwcDCqVq1q0K5ixYpQKpUICgoCAAQFBcHCwgIVKlQwaFetWjV9m5SUFERFRaFatWo52qjVav0HyLfeegtSqRTLli1DfHw8wsLCsGzZMvj6+sLHx8fgvjt37kTt2rXRuHFjfZeUV8nIyMDp06fRpUsXgw/baWlp+m5YWZRKJYDMsRKv8vfff8PGxgbOzs6vbFPYatSogS1btuCXX35BQkJCvu4rl8sxcuRI7N2797V5KyzBwcG5vhaA//JuzHOTnp5u0PUte7vXPX9JSUl49OgRKlWqpN/2zz//oGrVqti9ezdatGiBOnXqYOjQobh3716O++t0OqjVajx9+hRLly6Fr68vatSoYcyp5ysmU/z9998IDw9H165d3+g4RJQ3jpkgIhw5cgSVKlVCnTp1AACVK1fGkSNH0K1bNwDAo0ePcPDgQXzxxRfo16+f/n69e/c2av+DBw9w/PhxfPfdd/pv7ps0aYK2bdti8+bNmDZtWr7iValU2Lt3L8qVKwcAiIyMxDfffAOdTgdbW1t4eXkBAGrWrGlQFBhj0aJFcHd3R4cOHfTbEhISclzBAAB7e3v9B9iEhIRcrx5kb5OYmAgg51WGrGOrVCoAgLu7O7Zs2YKxY8di7dq1+nPZvHmzQRHQvn17NGjQAK6urrh37x5WrlyJBw8eYO/evbl+m3v+/HkkJSXlGA9TuXJl3Lp1y2Bb1u2smF4WHh6OrVu3YtCgQTk+7BalOXPmYPz48fjkk08gkUhQo0YNdO3aFSNGjNB/6H6dvn37YtWqVdi2bRsmTpxYBBH/J7fXjIODg34fkPncnDhxAlqtVj8W4eXnplKlSkhMTERwcDCqVKkCIPOKTWRk5Gufm+XLl0Mikeh/TwEgOjoaQUFBWLduHWbOnAk7OzusXLkSo0ePxokTJwyuosy
"text/plain": [
"<Figure size 800x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Option B (alternative) : les plus sévères\n",
"# sample_isin = problematic_isin.sort_values(\n",
"# \"rupture_ratio\", ascending=False\n",
"# ).head(10)\n",
"\n",
"def plot_isin_dynamics_clean(df, account_id, isin):\n",
" sub = df[\n",
" (df[\"Registrar Account - ID\"] == account_id) &\n",
" (df[\"Product - Isin\"] == isin)\n",
" ].sort_values(\"Centralisation Date\")\n",
"\n",
" if sub.empty:\n",
" return\n",
"\n",
" fig, ax = plt.subplots(figsize=(7.5, 3))\n",
"\n",
" # AUM observé\n",
" ax.plot(\n",
" sub[\"Centralisation Date\"],\n",
" sub[\"Quantity - AUM\"],\n",
" label=\"Observed AUM\",\n",
" linewidth=2,\n",
" color=\"black\"\n",
" )\n",
"\n",
" # AUM attendu\n",
" ax.plot(\n",
" sub[\"Centralisation Date\"],\n",
" sub[\"expected_stock\"],\n",
" label=\"Flow-implied AUM\",\n",
" linestyle=\"--\",\n",
" linewidth=2,\n",
" color=\"grey\"\n",
" )\n",
"\n",
" # Ruptures\n",
" rupt = sub[sub[\"rupture_flag\"]]\n",
" ax.scatter(\n",
" rupt[\"Centralisation Date\"],\n",
" rupt[\"Quantity - AUM\"],\n",
" color=\"red\",\n",
" s=25,\n",
" zorder=5,\n",
" label=\"Discontinuity\"\n",
" )\n",
"\n",
" ax.set_title(f\"Account {account_id} — ISIN {isin}\", fontsize=11)\n",
" ax.set_xlabel(\"\")\n",
" ax.set_ylabel(\"AUM (shares)\")\n",
" ax.legend(loc=\"best\")\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"\n",
"for _, row in sample_isin.iterrows():\n",
" plot_isin_dynamics(\n",
" df,\n",
" row[\"Registrar Account - ID\"],\n",
" row[\"Product - Isin\"]\n",
" )\n"
]
},
{
"cell_type": "code",
"execution_count": 79,
"id": "aef8ceb9-28a6-4908-ae24-a88d85b64309",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABKQAAAHoCAYAAAB3gTdgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAA/cNJREFUeJzs3Xd8G/X9P/DXnbbkbcd2dkKAJJDlEMIMhE3ZZAGlFCi0UChQSmn5QgdQCqUFfoVAW0opUPYmbMoqpUACNIEk4ARCduIML8mSrHHj94d7Z8mWZE1L53s9Hw8e2Cfp7nPSW4ru7ffn/RFUVVVBREREREREREQ0SMRiD4CIiIiIiIiIiMyFCSkiIiIiIiIiIhpUTEgREREREREREdGgYkKKiIiIiIiIiIgGFRNSREREREREREQ0qJiQIiIiIiIiIiKiQcWEFBERERERERERDSompIiIiIiIiIiIaFCZJiGlKAqCwSAURSn2UIiIiIiIiIiITM00CalQKITm5maEQqFiD4WIiIiIiIiIyNRMk5AiSoeqqvB6vVBVtdhDIRoUjHkyG8Y8mRHjnsyGMU9mZMS4Z0KKKIaqqohEIoZ6ExPlgjFPZsOYJzNi3JPZMObJjIwY90xIERERERERERHRoGJCioiIiIiIiIiIBhUTUkQxBEFAeXk5BEEo9lCIBgVjnsyGMU9mxLgns2HMkxkZMe6txR4AUSkRBAFOp7PYwyAaNIx5MhvGPJkR457MhjFPZmTEuGeFFFEMVVXR0dFhqEZwRLlgzJPZMObJjBj3ZDaMeTIjI8Y9E1JEMVRVhSRJhnoTE+WCMU9mw5gnM2Lck9kw5smMjBj3TEgREREREREREdGgYkKKiIiIiIiIiIgGFRNSRDEEQUBlZaWhViYgygVjnsyGMU9mxLgns2HMkxkZMe6ZkCKKIQgC7Ha7od7ERLlgzJPZMObJjBj3ZDaM+cxt3boVEydOxKZNm5Le55prrsFPf/rTQRwVZcKIcc+EFFEMRVHQ2toKRVGKPRSiQcGYJ7NhzJMZMe7JbIwW8+eccw4mTpyIJ554Im673+9HU1PTgImiTC1evBhnnXVW3vZXbOkk00pRvl8Ho8U9wIQUUT9GWpWAKB8Y82Q2jHkyI8Y9mY3RYn7PPffEk08+GbdtyZIlGDlyZJFGVHiRSKTYQ4gjy3JekjnFPC+jxT0TUkRERERERERFdMQRR6C1tRWff/65vu2JJ57AGWec0e++//rXvzBv3jzst99+OO6443D//ffHJVImTpyIhx9+GGeeeSaamppw8skn49NPPwUAvPjii7j33nvx2WefoampCU1NTfptALB8+XKccsopaGpqwqJFi7Bu3bqE4/3jH/+Ic845J27b7t27MWXKFDQ3Nyd8zDnnnIMbb7wRV155Jfbff3/cdNNNWLZsGSZOnAhJkvT7PffcczjssMP036+55hpcccUVuO666zBr1izMnTsX9913n377SSedBAA49dRT0dTUhF/96lcAgCOPPBJPP/103BgmTpyIDz/8EAD0Y7/yyis47rjjMH36dLS1tcHn8+HXv/41jjjiCBxwwAH4/ve/jy1btiQ8p9jxPvroozjyyCNxwAEHAAAeffRRnHTSSZg5cyYOOeQQXH311Whvbx/wdfjmm29w0UUX4eCDD8acOXNw/fXXIxgMJj2+kTEhRUREREREREOaJEmIRCKD8l9sciVdFosFCxcu1Kftffrpp/D7/Zg7d27c/VauXIkf/ehHuPDCC7Fs2TLcfvvteOCBB/CPf/wj7n5PP/00br31VnzyySc48MADcfXVVwMATjnlFFx00UWYMWMGVqxYgRUrVmDWrFn645YsWYK//e1vWLp0KRobG3H99dcnHO8ZZ5yB5cuXY+PGjfq2Z555Bvvssw8mT56c9Dyfe+45nHbaaVi2bBn+7//+L+3n56233sLUqVPx0Ucf4Y9//CPuu+8+vPjiiwCAl19+WR/7ihUrcOONN6a9XwB49dVX8cQTT2D58uWoqanBpZdeCr/fj+effx7vv/8+9t57b1x00UWIRqNJ99Ha2oo1a9bgpZde0hNedXV1WLx4MT799FM89dRT2LhxI2666SYAyV+H9vZ2nH322TjooIPwr3/9C0uWLMHGjRtx8803Z3RORsGEFFEMQRBQXV1tqEZwRLlgzJPZMObJjBj3ZDZ9Y16WZezcuRO7d+8elP927doFWZYzHveiRYvwxhtvwOfz4fHHH8fChQv7vW+feeYZHH744TjhhBNgtVoxZcoUXHjhhf36T33ve9/D2LFjYbVasWjRImzfvh2tra0DjuHSSy9FfX09HA4H5s2bh1WrViW83/Dhw3HYYYfp0wwVRcHTTz+NM888M+X+jzzySBx++OEQRREul2vA8WgmTpyIM888EzabDTNmzMDChQvx7LPPpv34VK666ipUV1fDbrdjzZo1elKrqqoKdrsdP/nJT7B169a46rVErr32Wng8Hv28jjvuOIwfPx6iKGLkyJH4wQ9+gA8++CDlPpYsWYKxY8fivPPOg91uR01NDS677DK88MILA8aUET/rrcUeAFEpEQQBoiga6k1MlAvGPJkNY55Kkc/ng8PhgMPhKMj+GfdkNn1j3mKxoKGhYdCaPYuiCIvFkvHjGhsbccABB+D+++/HO++8g5///Of9+hG1tLRgzz33jNs2ZswYtLS0xG2rr6/Xf9YSJIFAAHV1dSnH0PdxoVAIkiTBau2fOvj2t7+Nq6++GldeeSU++ugjdHV14YQTTki5/1GjRqW8Pd3HjRo1Cm+99VZW+0q1702bNkGSJBx++OH97rdjx46k+6ipqemXYPvnP/+Jv//979i0aRPC4TBUVUUwGIQsy0njY+PGjfjiiy/iqtZUVYUgCGhtbUVDQ0PSMRjxs54JKaIYiqKgra0NtbW1EEUWENLQx5gns2HMUykKBoNQVbVgCSnGPZlNophPlFApRWeddRZ+8IMf4Nhjj0V9fT22bt0ad/vw4cOxefPmuG2bN2/G8OHD0z5GvhIWhx56KMrLy/Hmm2/ilVdewamnngqn05nRsT0eDwCgu7sb5eXlAIBdu3b1e1zf52Hbtm16cibZ55rH44nrvbRz586E94t9fF1dHWw2Gz766CPYbLaU55JsH0BP8uqKK67AH/7wBxxzzDFwOBx488038aMf/UhvPJ7odRg2bBhmzpzZbwpmOoz4WW+MURIRERERDVGqqmY1vYeIhp45c+bggQcewLXXXpvw9vnz5+O9997DG2+8AVmW8eWXX+L+++9P2Pw8mWHDhqGlpQXhcDinsQqCgDPPPBP33nsv3nvvvQGn6yUybtw4eDwePPHEE1AUBc3NzXjqqaf63W/t2rV4+umnIUkSVq5ciaeffhrz5s0D0FOdJIoiNmzYEPeYKVOm4JVXXoHP54Pf78ftt98+4Hj2228/7LXXXrj++uvR1tYGAPB6vXjjjTfQ3d2d9nkFAgEoioLq6mo4HA5s3LgR9957b9x9Er0O8+bNQ3NzMx599FF0d3dDVVW0tLTkrRqs1DAhRURERERUZIM1lYiISpsgCDjooIPQ2NiY8Pbp06fjzjvvxF/+8hfsv//+uOKKK3DOOefgu9/9btrHOOGEEzB+/HjMmTMHs2bNiltlL1Pz5s3Dhg0bMGPGjH5TCdNRVlaGW2+9FU899RT2228/3H777Vi0aFG/+x199NH47LPPcOCBB+Kyyy7D9773PZx66qkAAKfTiSuvvBK/+MUvMGvWLL0R+49//GOUlZXh8MMPx7x583DMMccMOB6LxYIHHngALpcLCxcuRFNTE0499VS8+eabGVWWTZgwAVdddRV+/vOfo6mpCddccw1OOeWUuPskeh1GjBiBJ554Ah9++CGOOeYYzJo1CxdccAHWrl2b9rGNRFC1erEhLhgMorm5GZMnT4bb7S72cKhEGbHMkSgXjHkyG8Y8laLt27fDarXG9W7JJ8Y9mQ1jfvDIsoy5c+fi6quv7pdwyZdrrrkGkiThtttuK8j+hwojxr0xRkk0SERRNNQbmChXjHkyG8Y8lSJVVQtaIcW4J7N
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ------------------------------------------------------------\n",
"# 1. Aggregate rupture rate over time\n",
"# ------------------------------------------------------------\n",
"time_stats = (\n",
" df.groupby(\"Centralisation Date\")\n",
" .agg(\n",
" total_obs=(\"rupture_flag\", \"count\"),\n",
" n_ruptures=(\"rupture_flag\", \"sum\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"time_stats[\"rupture_rate\"] = (\n",
" time_stats[\"n_ruptures\"] / time_stats[\"total_obs\"]\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 2. Smooth (optional but recommended for readability)\n",
"# ------------------------------------------------------------\n",
"time_stats[\"rupture_rate_ma\"] = (\n",
" time_stats[\"rupture_rate\"]\n",
" .rolling(window=6, center=True) # 6 periods ≈ half-year\n",
" .mean()\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# 3. Professional plot\n",
"# ------------------------------------------------------------\n",
"plt.figure(figsize=(12, 5))\n",
"\n",
"plt.plot(\n",
" time_stats[\"Centralisation Date\"],\n",
" time_stats[\"rupture_rate\"] * 100,\n",
" color=\"lightgray\",\n",
" linewidth=1,\n",
" alpha=0.6,\n",
" label=\"Monthly rupture rate\"\n",
")\n",
"\n",
"plt.plot(\n",
" time_stats[\"Centralisation Date\"],\n",
" time_stats[\"rupture_rate_ma\"] * 100,\n",
" color=\"#1f77b4\",\n",
" linewidth=2.5,\n",
" label=\"6-month moving average\"\n",
")\n",
"\n",
"plt.ylabel(\"Rupture rate (%)\")\n",
"plt.xlabel(\"Date\")\n",
"\n",
"plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
"plt.legend(frameon=False)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 87,
"id": "d6ee0c24-e14e-4c40-97d4-49879229790c",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_3828/1047489516.py:6: FutureWarning:\n",
"\n",
"DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
"\n"
]
},
{
"data": {
"text/plain": [
"has_reset\n",
"True 64192\n",
"False 15545\n",
"Name: count, dtype: int64"
]
},
"execution_count": 87,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"EPS = 1e-6 # seuil numérique\n",
"\n",
"reset_candidates = (\n",
" df\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .apply(\n",
" lambda g: (\n",
" (g[\"Quantity - AUM\"].abs() < EPS) &\n",
" (g[\"expected_stock\"].abs() < EPS)\n",
" ).any()\n",
" )\n",
" .reset_index(name=\"has_reset\")\n",
")\n",
"\n",
"reset_candidates[\"has_reset\"].value_counts()\n"
]
},
{
"cell_type": "code",
"execution_count": 81,
"id": "d0e1d404-47b1-4894-8f66-621cd726c524",
"metadata": {},
"outputs": [
{
"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>Product - Isin</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>73456</th>\n",
" <td>200127803</td>\n",
" <td>LU1299303229</td>\n",
" </tr>\n",
" <tr>\n",
" <th>59874</th>\n",
" <td>200127435</td>\n",
" <td>FR001400KAV4</td>\n",
" </tr>\n",
" <tr>\n",
" <th>61773</th>\n",
" <td>200127728</td>\n",
" <td>LU0992627967</td>\n",
" </tr>\n",
" <tr>\n",
" <th>50039</th>\n",
" <td>200058108</td>\n",
" <td>LU2809794220</td>\n",
" </tr>\n",
" <tr>\n",
" <th>71797</th>\n",
" <td>200085616</td>\n",
" <td>FR0010149179</td>\n",
" </tr>\n",
" <tr>\n",
" <th>...</th>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>26363</th>\n",
" <td>411905</td>\n",
" <td>LU0099161993</td>\n",
" </tr>\n",
" <tr>\n",
" <th>79339</th>\n",
" <td>OFF DISTRIBUTION</td>\n",
" <td>LU0807690168</td>\n",
" </tr>\n",
" <tr>\n",
" <th>44191</th>\n",
" <td>200000674</td>\n",
" <td>FR0010135103</td>\n",
" </tr>\n",
" <tr>\n",
" <th>122</th>\n",
" <td>13463</td>\n",
" <td>FR0010147603</td>\n",
" </tr>\n",
" <tr>\n",
" <th>42493</th>\n",
" <td>422778</td>\n",
" <td>FR0010149179</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>792 rows × 2 columns</p>\n",
"</div>"
],
"text/plain": [
" Registrar Account - ID Product - Isin\n",
"73456 200127803 LU1299303229\n",
"59874 200127435 FR001400KAV4\n",
"61773 200127728 LU0992627967\n",
"50039 200058108 LU2809794220\n",
"71797 200085616 FR0010149179\n",
"... ... ...\n",
"26363 411905 LU0099161993\n",
"79339 OFF DISTRIBUTION LU0807690168\n",
"44191 200000674 FR0010135103\n",
"122 13463 FR0010147603\n",
"42493 422778 FR0010149179\n",
"\n",
"[792 rows x 2 columns]"
]
},
"execution_count": 81,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"single_rupture"
]
},
{
"cell_type": "code",
"execution_count": 92,
"id": "601f61b8-0115-431d-97de-6ec5a0f1d4f4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" Before repair After repair Repaired points\n",
"0 756392 22357 18440\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_3828/2440789620.py:64: FutureWarning:\n",
"\n",
"DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
"\n"
]
}
],
"source": [
"GAP_TOL = 1e-6\n",
"REL_GAP_THR = 0.05\n",
"MIN_PERSISTENCE = 3\n",
"\n",
"df = merged_isin.copy().sort_values(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
")\n",
"\n",
"df[\"corrected_aum\"] = df[\"Quantity - AUM\"]\n",
"df[\"repair_flag\"] = False\n",
"\n",
"def repair_group(g):\n",
" g = g.copy()\n",
"\n",
" obs = g[\"Quantity - AUM\"].values\n",
" flows = g[\"Quantity - NetFlows\"].values\n",
"\n",
" corrected = obs.copy()\n",
" expected = np.empty_like(obs)\n",
" expected[0] = np.nan\n",
"\n",
" # initial expected path\n",
" for t in range(1, len(obs)):\n",
" expected[t] = corrected[t-1] + flows[t-1]\n",
"\n",
" gap = obs - expected\n",
" rel_gap = np.abs(gap) / np.maximum(np.abs(expected), 1.0)\n",
"\n",
" idx = None\n",
" for i in range(1, len(obs) - MIN_PERSISTENCE):\n",
" if (\n",
" rel_gap[i] > REL_GAP_THR and\n",
" np.all(np.abs(gap[i:i+MIN_PERSISTENCE] - gap[i]) < GAP_TOL) and\n",
" np.all(np.abs(np.diff(flows[i:i+MIN_PERSISTENCE])) < GAP_TOL)\n",
" ):\n",
" idx = i\n",
" break\n",
"\n",
" if idx is None:\n",
" return g\n",
"\n",
" # apply correction\n",
" shift = gap[idx]\n",
" corrected[idx:] = obs[idx:] - shift\n",
" g.loc[g.index[idx]:, \"repair_flag\"] = True\n",
"\n",
" # rebuild expected stock AFTER correction\n",
" expected_corr = np.empty_like(obs)\n",
" expected_corr[0] = np.nan\n",
" for t in range(1, len(obs)):\n",
" expected_corr[t] = corrected[t-1] + flows[t-1]\n",
"\n",
" g[\"corrected_aum\"] = corrected\n",
" g[\"expected_stock_corr\"] = expected_corr\n",
"\n",
" return g\n",
"\n",
"df = (\n",
" df\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"], group_keys=False)\n",
" .apply(repair_group)\n",
")\n",
"\n",
"# Recompute gaps & ruptures\n",
"df[\"gap_before\"] = df[\"Quantity - AUM\"] - df[\"expected_stock\"]\n",
"df[\"gap_after\"] = df[\"corrected_aum\"] - df[\"expected_stock_corr\"]\n",
"\n",
"df[\"rupture_before\"] = df[\"gap_before\"].abs() > GAP_TOL\n",
"df[\"rupture_after\"] = df[\"gap_after\"].abs() > GAP_TOL\n",
"\n",
"summary = pd.DataFrame({\n",
" \"Before repair\": [df[\"rupture_before\"].sum()],\n",
" \"After repair\": [df[\"rupture_after\"].sum()],\n",
" \"Repaired points\": [df[\"repair_flag\"].sum()]\n",
"})\n",
"\n",
"print(summary)\n"
]
},
{
"cell_type": "code",
"execution_count": 103,
"id": "62583cfe-a6e7-4931-a63e-4273dca97ff7",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"domain": {
"x": [
0,
0.48
]
},
"hole": 0.45,
"hoverinfo": "label+percent",
"labels": [
"Clean / quasi-clean (≤1%)",
"Moderate (110%)",
"High (1030%)",
"Severe (>30%)"
],
"name": "Before repair",
"textinfo": "percent",
"type": "pie",
"values": {
"bdata": "ZmZmZmYmTUDNzMzMzEw3QM3MzMzMzCNAzczMzMzMIEA=",
"dtype": "f8"
}
},
{
"domain": {
"x": [
0.52,
1
]
},
"hole": 0.45,
"hoverinfo": "label+percent",
"labels": [
"Clean / quasi-clean (≤1%)",
"Moderate (110%)",
"High (1030%)",
"Severe (>30%)"
],
"name": "After repair",
"textinfo": "percent",
"type": "pie",
"values": {
"bdata": "mpmZmZn5WEAAAAAAAAAAAAAAAAAAAAAAmpmZmZmZuT8=",
"dtype": "f8"
}
}
],
"layout": {
"annotations": [
{
"showarrow": false,
"text": "Before repair",
"x": 0.24,
"y": 0.5
},
{
"showarrow": false,
"text": "After repair",
"x": 0.76,
"y": 0.5
}
],
"legend": {
"orientation": "h",
"title": {
"text": "Rupture ratio"
},
"x": 0.5,
"xanchor": "center",
"y": -0.15,
"yanchor": "top"
},
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
],
"sequentialminus": [
[
0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"text": "Distribution of AUMflow rupture intensity before vs after repair (fixed ε)"
}
}
},
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAFoCAYAAAB0XzViAAAQAElEQVR4AeydBZwcRdrGn5HNStwNSEIIAYIluAeXw12ODw45ONwlyKHBncPh4LA73A44NBxuySGBQAhJgLjbZmXk66eX2vROZmZnZke6e578UtvdJW+99a/q6nqrqnuCcf0TAREQAREQAREQAREQAREQAR8RCEL/REAEkhCQlwiIgAiIgAiIgAiIgFcJyMjxas1JbxEQAREoBQHlKQIiIAIiIAIeICAjxwOVJBVFQAREQAREQATcTUDaiYAIuIuAjBx31Ye0EQEREAEREAEREAEREAG/EChZOWTklAy9MhYBERABERABERABERABESgEARk5haAqmfkjIEkiIAIiIAIiIAIiIAIikCUBGTlZAlN0ERABEXADAekgAiIgAiIgAiKQmoCMnNRsFCICIiACIiACIuAtAtJWBERABGwCMnJsDPojAiIgAiIgAiIgAiIgAn4lUH7lkpFTfnWuEouACIiACIiACIiACIiArwnIyPF19eavcJIkAiIgAiIgAiIgAiIgAl4hICPHKzUlPUVABNxIQDqJgAiIgAiIgAi4kICMHBdWilQSAREQAREQAW8TkPYiIAIiUFoCMnJKy1+5i4AIiIAIiIAIiIAIlAsBlbNoBGTkFA21MhIBERABERABERABERABESgGARk5xaCcvzwkSQREQAREQAREQAREQAREoBUCMnJaAaRgERABLxCQjiIgAiIgAiIgAiKwgoCMnBUsdCYCIiACIiAC/iKg0oiACIhAmRKQkVOmFa9ii4AIiIAIiIAIiEC5ElC5/U9ARo7/61glFAEREAEREAEREAEREIGyIiAjJ6fqViIREAEREAEREAEREAEREAG3EpCR49aakV4i4EUC0lkEREAEREAEREAEXEBARo4LKkEqiIAIiIAI+JuASicCIiACIlBcAjJyistbuYmACIiACIiACIiACDQR0F8RKBgBGTkFQyvBIiACIiACIiACIiACIiACpSDgbSOnFMSUpwiIgAiIgAiIgAiIgAiIgKsJyMhxdfVIORHIjYBSiYAIiIAIiIAIiEA5E5CRU861r7KLgAiIQHkRUGlFQAREQATKhICMnDKpaBVTBERABERABERABJITkK8I+I+AjBz/1alKJAIiIAIiIAIiIAIiIAJlTSAvRk5Z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
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import plotly.graph_objects as go\n",
"import pandas as pd\n",
"\n",
"# ============================================================\n",
"# Parameters (fixed epsilon)\n",
"# ============================================================\n",
"GAP_EPS = 100 # fixed tolerance for accounting identity\n",
"\n",
"# ============================================================\n",
"# 1. Define ruptures using a FIXED epsilon\n",
"# ============================================================\n",
"df = df.copy()\n",
"\n",
"df[\"rupture_before\"] = df[\"gap_before\"].abs() > GAP_EPS\n",
"df[\"rupture_after\"] = df[\"gap_after\"].abs() > GAP_EPS\n",
"\n",
"# ============================================================\n",
"# 2. Rupture ratios BEFORE repair\n",
"# ============================================================\n",
"rupture_summary_before = (\n",
" df\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .agg(\n",
" n_obs=(\"rupture_before\", \"count\"),\n",
" n_ruptures=(\"rupture_before\", \"sum\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"rupture_summary_before[\"rupture_ratio\"] = (\n",
" rupture_summary_before[\"n_ruptures\"] /\n",
" rupture_summary_before[\"n_obs\"]\n",
")\n",
"\n",
"# ============================================================\n",
"# 3. Rupture ratios AFTER repair\n",
"# ============================================================\n",
"rupture_summary_after = (\n",
" df\n",
" .groupby([\"Registrar Account - ID\", \"Product - Isin\"])\n",
" .agg(\n",
" n_obs=(\"rupture_after\", \"count\"),\n",
" n_ruptures=(\"rupture_after\", \"sum\")\n",
" )\n",
" .reset_index()\n",
")\n",
"\n",
"rupture_summary_after[\"rupture_ratio\"] = (\n",
" rupture_summary_after[\"n_ruptures\"] /\n",
" rupture_summary_after[\"n_obs\"]\n",
")\n",
"\n",
"# ============================================================\n",
"# 4. Rupture intensity classes (fixed bins)\n",
"# ============================================================\n",
"bins = [0.0, 0.01, 0.10, 0.30, 1.0]\n",
"labels = [\n",
" \"Clean / quasi-clean (≤1%)\",\n",
" \"Moderate (110%)\",\n",
" \"High (1030%)\",\n",
" \"Severe (>30%)\"\n",
"]\n",
"\n",
"rupture_summary_before[\"rupture_class\"] = pd.cut(\n",
" rupture_summary_before[\"rupture_ratio\"],\n",
" bins=bins,\n",
" labels=labels,\n",
" include_lowest=True\n",
")\n",
"\n",
"rupture_summary_after[\"rupture_class\"] = pd.cut(\n",
" rupture_summary_after[\"rupture_ratio\"],\n",
" bins=bins,\n",
" labels=labels,\n",
" include_lowest=True\n",
")\n",
"\n",
"# ============================================================\n",
"# 5. Distribution (%)\n",
"# ============================================================\n",
"dist_before = (\n",
" rupture_summary_before[\"rupture_class\"]\n",
" .value_counts(normalize=True)\n",
" .sort_index()\n",
" * 100\n",
").round(1)\n",
"\n",
"dist_after = (\n",
" rupture_summary_after[\"rupture_class\"]\n",
" .value_counts(normalize=True)\n",
" .sort_index()\n",
" * 100\n",
").round(1)\n",
"\n",
"# ============================================================\n",
"# 6. Donut chart: BEFORE vs AFTER (fixed epsilon)\n",
"# ============================================================\n",
"fig = go.Figure()\n",
"\n",
"fig.add_trace(go.Pie(\n",
" labels=dist_before.index,\n",
" values=dist_before.values,\n",
" hole=0.45,\n",
" name=\"Before repair\",\n",
" domain=dict(x=[0.0, 0.48]),\n",
" textinfo=\"percent\",\n",
" hoverinfo=\"label+percent\"\n",
"))\n",
"\n",
"fig.add_trace(go.Pie(\n",
" labels=dist_after.index,\n",
" values=dist_after.values,\n",
" hole=0.45,\n",
" name=\"After repair\",\n",
" domain=dict(x=[0.52, 1.0]),\n",
" textinfo=\"percent\",\n",
" hoverinfo=\"label+percent\"\n",
"))\n",
"\n",
"fig.update_layout(\n",
" title=\"Distribution of AUMflow rupture intensity before vs after repair (fixed ε)\",\n",
" annotations=[\n",
" dict(text=\"Before repair\", x=0.24, y=0.5, showarrow=False),\n",
" dict(text=\"After repair\", x=0.76, y=0.5, showarrow=False),\n",
" ],\n",
" legend=dict(\n",
" orientation=\"h\",\n",
" yanchor=\"top\",\n",
" y=-0.15,\n",
" xanchor=\"center\",\n",
" x=0.5\n",
" ),\n",
" legend_title_text=\"Rupture ratio\"\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 100,
"id": "70cf0a99-bd19-41a9-9574-88647fde09ca",
"metadata": {},
"outputs": [],
"source": [
"# ============================================================\n",
"# FINAL DATASETS AFTER REPAIR\n",
"# ============================================================\n",
"\n",
"df_final = df.copy()\n",
"\n",
"# ------------------------------------------------------------\n",
"# Core variables (before / after)\n",
"# ------------------------------------------------------------\n",
"df_final = df_final[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"Quantity - AUM\",\n",
" \"corrected_aum\",\n",
" \"Quantity - NetFlows\",\n",
" \"expected_stock\",\n",
" \"expected_stock_corr\",\n",
" \"gap_before\",\n",
" \"gap_after\",\n",
" \"repair_flag\"\n",
"]].rename(columns={\n",
" \"Quantity - AUM\": \"aum_raw\",\n",
" \"corrected_aum\": \"aum_repaired\",\n",
" \"Quantity - NetFlows\": \"flows\",\n",
" \"expected_stock\": \"expected_aum_raw\",\n",
" \"expected_stock_corr\": \"expected_aum_repaired\"\n",
"})\n",
"\n",
"# ------------------------------------------------------------\n",
"# Relative gaps\n",
"# ------------------------------------------------------------\n",
"df_final[\"gap_rel_before\"] = (\n",
" df_final[\"gap_before\"].abs() /\n",
" df_final[\"expected_aum_raw\"].abs().clip(lower=1)\n",
")\n",
"\n",
"df_final[\"gap_rel_after\"] = (\n",
" df_final[\"gap_after\"].abs() /\n",
" df_final[\"expected_aum_repaired\"].abs().clip(lower=1)\n",
")\n",
"df_final.to_csv('df_repaired.csv')"
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "befb2962-73fb-4cb8-b86e-3218ec103204",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Temporary reset glitches repaired (Type 3): 8376\n"
]
}
],
"source": [
"# ============================================================\n",
"# TYPE 3 REPAIR — TEMPORARY RESET TO ZERO (ONE BLOCK)\n",
"# ============================================================\n",
"\n",
"df_type3 = df_repaired.copy()\n",
"df_type3 = df_type3.sort_values(\n",
" [\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"]\n",
")\n",
"\n",
"# Create lead/lag variables\n",
"df_type3[\"aum_prev\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - AUM\"].shift(1)\n",
"\n",
"df_type3[\"aum_next\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - AUM\"].shift(-1)\n",
"\n",
"df_type3[\"flow_prev\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - NetFlows\"].shift(1)\n",
"\n",
"df_type3[\"flow_next\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - NetFlows\"].shift(-1)\n",
"\n",
"# ------------------------------------------------------------\n",
"# Detection of temporary reset\n",
"# ------------------------------------------------------------\n",
"df_type3[\"type3_flag\"] = (\n",
" (df_type3[\"Quantity - AUM\"] == 0)\n",
" & (df_type3[\"aum_prev\"] > 0)\n",
" & (df_type3[\"aum_next\"] == df_type3[\"aum_prev\"])\n",
" & (df_type3[\"flow_prev\"].fillna(0) == 0)\n",
" & (df_type3[\"Quantity - NetFlows\"] == 0)\n",
" & (df_type3[\"flow_next\"].fillna(0) == 0)\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# Repair: smooth the glitch (replace 0 by previous stock)\n",
"# ------------------------------------------------------------\n",
"df_type3.loc[df_type3[\"type3_flag\"], \"Quantity - AUM\"] = (\n",
" df_type3.loc[df_type3[\"type3_flag\"], \"aum_prev\"]\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# Recompute temporal chain AFTER repair\n",
"# ------------------------------------------------------------\n",
"df_type3[\"prev_stock\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - AUM\"].shift(1)\n",
"\n",
"df_type3[\"prev_flows\"] = df_type3.groupby(\n",
" [\"Registrar Account - ID\", \"Product - Isin\"]\n",
")[\"Quantity - NetFlows\"].shift(1).fillna(0)\n",
"\n",
"df_type3[\"expected_stock\"] = (\n",
" df_type3[\"prev_stock\"] + df_type3[\"prev_flows\"]\n",
")\n",
"\n",
"df_type3[\"gap\"] = df_type3[\"Quantity - AUM\"] - df_type3[\"expected_stock\"]\n",
"df_type3[\"gap_abs\"] = df_type3[\"gap\"].abs()\n",
"df_type3[\"gap_rel\"] = (\n",
" df_type3[\"gap_abs\"] /\n",
" df_type3[\"expected_stock\"].abs().clip(lower=1)\n",
")\n",
"\n",
"df_type3[\"rupture_flag\"] = (\n",
" df_type3[\"prev_stock\"].notna()\n",
" & (df_type3[\"gap_abs\"] > TAU_ABS)\n",
" & (df_type3[\"gap_rel\"] > TAU_REL)\n",
")\n",
"\n",
"# ------------------------------------------------------------\n",
"# Diagnostic output\n",
"# ------------------------------------------------------------\n",
"n_type3 = df_type3[\"type3_flag\"].sum()\n",
"print(f\"Temporary reset glitches repaired (Type 3): {n_type3}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 72,
"id": "1fc44ed4-829f-4a8a-985a-31350bdbdf6d",
"metadata": {},
"outputs": [
{
"ename": "IndexError",
"evalue": "index 0 is out of bounds for axis 0 with size 0",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mIndexError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[72]\u001b[39m\u001b[32m, line 28\u001b[39m\n\u001b[32m 25\u001b[39m \u001b[38;5;66;03m# Localiser la rupture\u001b[39;00m\n\u001b[32m 26\u001b[39m rupture_idx = sub.index[sub[\u001b[33m\"\u001b[39m\u001b[33mrupture_flag\u001b[39m\u001b[33m\"\u001b[39m]]\n\u001b[32m---> \u001b[39m\u001b[32m28\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m sub.index.get_loc(\u001b[43mrupture_idx\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m) > \u001b[32m1\u001b[39m:\n\u001b[32m 29\u001b[39m \u001b[38;5;66;03m#print(sub[[\"Centralisation Date\", \"Quantity - AUM\", \"expected_stock\", \"gap\", \"rupture_flag\"]].head(100))\u001b[39;00m\n\u001b[32m 30\u001b[39m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[32m 32\u001b[39m \u001b[38;5;66;03m# Vérifier si la rupture est à la première date\u001b[39;00m\n",
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/indexes/base.py:5401\u001b[39m, in \u001b[36mIndex.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 5398\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_integer(key) \u001b[38;5;129;01mor\u001b[39;00m is_float(key):\n\u001b[32m 5399\u001b[39m \u001b[38;5;66;03m# GH#44051 exclude bool, which would return a 2d ndarray\u001b[39;00m\n\u001b[32m 5400\u001b[39m key = com.cast_scalar_indexer(key)\n\u001b[32m-> \u001b[39m\u001b[32m5401\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mgetitem\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 5403\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, \u001b[38;5;28mslice\u001b[39m):\n\u001b[32m 5404\u001b[39m \u001b[38;5;66;03m# This case is separated from the conditional above to avoid\u001b[39;00m\n\u001b[32m 5405\u001b[39m \u001b[38;5;66;03m# pessimization com.is_bool_indexer and ndim checks.\u001b[39;00m\n\u001b[32m 5406\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._getitem_slice(key)\n",
"\u001b[31mIndexError\u001b[39m: index 0 is out of bounds for axis 0 with size 0"
]
}
],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ------------------------------------------------------------\n",
"# 1. Sélection des ISIN avec exactement 1 rupture\n",
"# ------------------------------------------------------------\n",
"one_rupture_isin = rupture_isin_summary[\n",
" rupture_isin_summary[\"n_ruptures\"] == 1\n",
"][[\"Registrar Account - ID\", \"Product - Isin\"]].head(100)\n",
"\n",
"results = []\n",
"\n",
"# ------------------------------------------------------------\n",
"# 2. Boucle de correction test\n",
"# ------------------------------------------------------------\n",
"for _, row in one_rupture_isin.iterrows():\n",
" acc = row[\"Registrar Account - ID\"]\n",
" isin = row[\"Product - Isin\"]\n",
"\n",
" sub = df[\n",
" (df[\"Registrar Account - ID\"] == acc) &\n",
" (df[\"Product - Isin\"] == isin)\n",
" ].sort_values(\"Centralisation Date\").copy()\n",
"\n",
" # Localiser la rupture\n",
" rupture_idx = sub.index[sub[\"rupture_flag\"]]\n",
"\n",
" if sub.index.get_loc(rupture_idx[0]) > 1:\n",
" #print(sub[[\"Centralisation Date\", \"Quantity - AUM\", \"expected_stock\", \"gap\", \"rupture_flag\"]].head(100))\n",
" continue\n",
"\n",
" # Vérifier si la rupture est à la première date\n",
" first_idx = sub.index[0]\n",
" if rupture_idx[0] != first_idx:\n",
" continue\n",
"\n",
" # ----- Réparation : décaler expected_stock -----\n",
" sub[\"expected_stock_fixed\"] = sub[\"expected_stock\"].shift(-1)\n",
"\n",
" # Recalcul des gaps\n",
" sub[\"gap_fixed\"] = sub[\"Quantity - AUM\"] - sub[\"expected_stock_fixed\"]\n",
" sub[\"gap_abs_fixed\"] = sub[\"gap_fixed\"].abs()\n",
" sub[\"gap_rel_fixed\"] = sub[\"gap_abs_fixed\"] / sub[\"expected_stock_fixed\"].abs().clip(lower=1)\n",
"\n",
" # Recalcul rupture\n",
" sub[\"rupture_fixed\"] = (\n",
" sub[\"expected_stock_fixed\"].notna()\n",
" & (sub[\"gap_abs_fixed\"] > TAU_ABS)\n",
" & (sub[\"gap_rel_fixed\"] > TAU_REL)\n",
" )\n",
"\n",
" results.append({\n",
" \"Registrar Account - ID\": acc,\n",
" \"Product - Isin\": isin,\n",
" \"ruptures_before\": sub[\"rupture_flag\"].sum(),\n",
" \"ruptures_after\": sub[\"rupture_fixed\"].sum()\n",
" })\n",
"\n",
"# ------------------------------------------------------------\n",
"# 3. Résultats agrégés\n",
"# ------------------------------------------------------------\n",
"repair_test = pd.DataFrame(results)\n",
"\n",
"summary = repair_test.groupby(\n",
" [\"ruptures_before\", \"ruptures_after\"]\n",
").size().reset_index(name=\"count\")\n",
"\n",
"repair_test, summary\n"
]
},
{
"cell_type": "code",
"execution_count": 106,
"id": "d85728ca-55ba-4266-b881-23536eee4ba3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Share of repaired observations: 2.1230%\n",
"Number of repaired rows: 117,105\n"
]
}
],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"# ============================================================\n",
"# Rebuild STOCKS dataset using repaired AUM quantities\n",
"# ============================================================\n",
"\n",
"# 1. Copy original stocks\n",
"stocks_repaired = stocks.copy()\n",
"stocks_repaired[\"Centralisation Date\"] = pd.to_datetime(\n",
" stocks_repaired[\"Centralisation Date\"]\n",
")\n",
"\n",
"# 2. Build repair map\n",
"repair_map = (\n",
" df[[\n",
" \"Registrar Account - ID\",\n",
" \"Product - Isin\",\n",
" \"Centralisation Date\",\n",
" \"corrected_aum\",\n",
" \"repair_flag\"\n",
" ]]\n",
" .rename(columns={\"corrected_aum\": \"Quantity - AUM repaired\"})\n",
")\n",
"\n",
"# 3. Merge repaired quantities\n",
"stocks_repaired = stocks_repaired.merge(\n",
" repair_map,\n",
" on=[\"Registrar Account - ID\", \"Product - Isin\", \"Centralisation Date\"],\n",
" how=\"left\"\n",
")\n",
"\n",
"# 4. Store original quantity\n",
"stocks_repaired[\"Quantity - AUM original\"] = stocks_repaired[\"Quantity - AUM\"]\n",
"\n",
"# 5. Replace Quantity - AUM where repaired\n",
"stocks_repaired[\"Quantity - AUM\"] = np.where(\n",
" stocks_repaired[\"repair_flag\"] == True,\n",
" stocks_repaired[\"Quantity - AUM repaired\"],\n",
" stocks_repaired[\"Quantity - AUM\"]\n",
")\n",
"\n",
"# 6. Recompute monetary values (unit value unchanged)\n",
"stocks_repaired[\"nav_ccy\"] = (\n",
" stocks_repaired[\"Value - AUM CCY\"] /\n",
" stocks_repaired[\"Quantity - AUM original\"]\n",
")\n",
"\n",
"stocks_repaired[\"nav_eur\"] = (\n",
" stocks_repaired[\"Value - AUM €\"] /\n",
" stocks_repaired[\"Quantity - AUM original\"]\n",
")\n",
"\n",
"stocks_repaired[\"Value - AUM CCY\"] = (\n",
" stocks_repaired[\"Quantity - AUM\"] *\n",
" stocks_repaired[\"nav_ccy\"]\n",
")\n",
"\n",
"stocks_repaired[\"Value - AUM €\"] = (\n",
" stocks_repaired[\"Quantity - AUM\"] *\n",
" stocks_repaired[\"nav_eur\"]\n",
")\n",
"\n",
"# 7. Cleanup helper columns\n",
"stocks_repaired = stocks_repaired.drop(\n",
" columns=[\n",
" \"Quantity - AUM repaired\",\n",
" \"Quantity - AUM original\",\n",
" \"nav_ccy\",\n",
" \"nav_eur\"\n",
" ]\n",
")\n",
"\n",
"# ============================================================\n",
"# Sanity checks (CORRECT WAY)\n",
"# ============================================================\n",
"\n",
"# Share of observations repaired\n",
"repair_share = stocks_repaired[\"repair_flag\"].mean()\n",
"\n",
"# Ensure only repaired points were modified\n",
"n_modified = stocks_repaired[\"repair_flag\"].sum()\n",
"\n",
"print(f\"Share of repaired observations: {repair_share:.4%}\")\n",
"print(f\"Number of repaired rows: {n_modified:,}\")\n",
"\n",
"stocks_repaired.to_csv('AUM_repaired.csv')"
]
},
{
"cell_type": "code",
"execution_count": 107,
"id": "5f262605-49e8-4304-b11e-38c8bcfc6e3f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15532\n",
"15532\n"
]
}
],
"source": [
"print(stocks[\"Registrar Account - ID\"].nunique())\n",
"print(df[\"Registrar Account - ID\"].nunique())"
]
},
{
"cell_type": "code",
"execution_count": 73,
"id": "990898ea-ceca-46bb-bfb3-c87bf289d272",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1400x700 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"\n",
"df = merged_isin.copy()\n",
"\n",
"# Ajouter année / mois\n",
"df[\"year\"] = df[\"Centralisation Date\"].dt.year\n",
"df[\"month\"] = df[\"Centralisation Date\"].dt.month\n",
"\n",
"# 1. Nombre total de lignes par mois\n",
"total = df.groupby([\"year\", \"month\"]).size().reset_index(name=\"total_lines\")\n",
"\n",
"# 2. Nombre de ruptures par mois\n",
"ruptures = df[df[\"rupture_flag\"]].groupby([\"year\", \"month\"]).size().reset_index(name=\"n_ruptures\")\n",
"\n",
"# 3. Merge pour obtenir total + ruptures\n",
"ratio = total.merge(ruptures, on=[\"year\",\"month\"], how=\"left\")\n",
"ratio[\"n_ruptures\"] = ratio[\"n_ruptures\"].fillna(0)\n",
"\n",
"# 4. Proportion (en %)\n",
"ratio[\"rupture_ratio\"] = ratio[\"n_ruptures\"] / ratio[\"total_lines\"]\n",
"\n",
"# 5. Pivot pour heatmap\n",
"heatmap_ratio = ratio.pivot(index=\"year\", columns=\"month\", values=\"rupture_ratio\").fillna(0)\n",
"\n",
"# 6. Plot\n",
"plt.figure(figsize=(14, 7))\n",
"sns.heatmap(\n",
" heatmap_ratio, \n",
" cmap=\"Reds\",\n",
" linewidths=.3,\n",
" linecolor=\"grey\",\n",
" annot=True,\n",
" fmt=\".2%\",\n",
" cbar_kws={'label': 'Proportion de ruptures'}\n",
")\n",
"\n",
"plt.title(\"Heatmap de la proportion de ruptures (par année et mois)\", fontsize=16)\n",
"plt.xlabel(\"Mois\")\n",
"plt.ylabel(\"Année\")\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 69,
"id": "4d335589-c519-458d-857d-a051813b950b",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'merged_isin' is not defined",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[69]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m df = \u001b[43mmerged_isin\u001b[49m.copy()\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m# Ajouter year / month au cas où\u001b[39;00m\n\u001b[32m 4\u001b[39m df[\u001b[33m\"\u001b[39m\u001b[33myear\u001b[39m\u001b[33m\"\u001b[39m] = df[\u001b[33m\"\u001b[39m\u001b[33mCentralisation Date\u001b[39m\u001b[33m\"\u001b[39m].dt.year\n",
"\u001b[31mNameError\u001b[39m: name 'merged_isin' is not defined"
]
}
],
"source": [
"df = merged_isin.copy()\n",
"\n",
"# Ajouter year / month au cas où\n",
"df[\"year\"] = df[\"Centralisation Date\"].dt.year\n",
"df[\"month\"] = df[\"Centralisation Date\"].dt.month\n",
"\n",
"# Merge géographique\n",
"df = df.merge(\n",
" geo[[\"Registrar Account - ID\", \"country\"]],\n",
" on=\"Registrar Account - ID\",\n",
" how=\"left\"\n",
")\n",
"\n",
"df[\"country\"] = df[\"country\"].fillna(\"UNKNOWN\")\n",
"\n",
"# Total des lignes par pays\n",
"total_country = df.groupby(\"country\").size().reset_index(name=\"total_obs\")\n",
"\n",
"# Nombre de ruptures\n",
"rupt_country = (\n",
" df[df[\"rupture_flag\"]]\n",
" .groupby(\"country\")\n",
" .size()\n",
" .reset_index(name=\"ruptures\")\n",
")\n",
"\n",
"# Merge + ratios\n",
"country_stats = total_country.merge(rupt_country, on=\"country\", how=\"left\")\n",
"country_stats[\"ruptures\"] = country_stats[\"ruptures\"].fillna(0)\n",
"country_stats[\"rupture_ratio\"] = country_stats[\"ruptures\"] / country_stats[\"total_obs\"]\n",
"\n",
"# Tri (rupture ratio décroissant)\n",
"country_stats = country_stats.sort_values(\"rupture_ratio\", ascending=False)"
]
},
{
"cell_type": "code",
"execution_count": 70,
"id": "8a45a111-25da-4f5c-9723-c3efd25c906d",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'country_stats' is not defined",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[70]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# On ajoute une colonne en % pour laffichage\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mplotly\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexpress\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpx\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m country_stats_plot = \u001b[43mcountry_stats\u001b[49m.copy()\n\u001b[32m 5\u001b[39m country_stats_plot[\u001b[33m\"\u001b[39m\u001b[33mrupture_pct\u001b[39m\u001b[33m\"\u001b[39m] = country_stats_plot[\u001b[33m\"\u001b[39m\u001b[33mrupture_ratio\u001b[39m\u001b[33m\"\u001b[39m] * \u001b[32m100\u001b[39m\n\u001b[32m 7\u001b[39m \u001b[38;5;66;03m# Tri décroissant par proportion de ruptures\u001b[39;00m\n",
"\u001b[31mNameError\u001b[39m: name 'country_stats' is not defined"
]
}
],
"source": [
"# On ajoute une colonne en % pour laffichage\n",
"import plotly.express as px\n",
"\n",
"country_stats_plot = country_stats.copy()\n",
"country_stats_plot[\"rupture_pct\"] = country_stats_plot[\"rupture_ratio\"] * 100\n",
"\n",
"# Tri décroissant par proportion de ruptures\n",
"country_stats_plot = country_stats_plot.sort_values(\"rupture_ratio\", ascending=False)\n",
"\n",
"fig = px.bar(\n",
" country_stats_plot,\n",
" x=\"country\",\n",
" y=\"rupture_ratio\",\n",
" hover_data={\n",
" \"rupture_pct\": ':.2f',\n",
" \"ruptures\": True,\n",
" \"total_obs\": True,\n",
" \"rupture_ratio\": False, # on cache la version décimale\n",
" },\n",
" labels={\n",
" \"country\": \"Pays\",\n",
" \"rupture_ratio\": \"Proportion de ruptures\",\n",
" \"rupture_pct\": \"% de ruptures\",\n",
" \"ruptures\": \"Nb de ruptures\",\n",
" \"total_obs\": \"Nb d'observations\"\n",
" },\n",
" title=\"Proportion de ruptures par pays (avec volumes au survol)\"\n",
")\n",
"\n",
"# Format en %\n",
"fig.update_yaxes(tickformat=\".1%\")\n",
"\n",
"fig.update_layout(\n",
" xaxis_tickangle=-45,\n",
" bargap=0.2\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a0db693e-af06-463d-884d-49b88f19e83d",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "a4af9841-6cf9-4d27-8096-ac878e866bc6",
"metadata": {},
"outputs": [],
"source": [
"rs = rupture_summary.copy()\n",
"\n",
"# 1. Stats numériques classiques\n",
"print(\"\\n=== BASIC NUMERIC STATS ===\")\n",
"print(rs[\"rupture_ratio\"].describe(percentiles=[0.01, 0.05, 0.10, 0.25, 0.5, 0.75, 0.90, 0.95, 0.99]))\n",
"\n",
"\n",
"# 2. Distribution par classes (bins)\n",
"\n",
"rs[\"rupture_bucket\"] = pd.cut(\n",
" rs[\"rupture_ratio\"],\n",
" bins=[0, 0.001, 0.01, 0.05, 0.10, 0.25, 0.50, 1.01],\n",
" labels=[\n",
" \"00.1%\",\n",
" \"0.11%\",\n",
" \"15%\",\n",
" \"510%\",\n",
" \"1025%\",\n",
" \"2550%\",\n",
" \"50100%\"\n",
" ],\n",
" include_lowest=True\n",
")\n",
"\n",
"# Ajouter la catégorie \"0%\"\n",
"rs[\"rupture_bucket\"] = rs[\"rupture_bucket\"].cat.add_categories(\"0%\")\n",
"\n",
"# Remplacer les 0% exacts\n",
"rs.loc[rs[\"rupture_ratio\"] == 0, \"rupture_bucket\"] = \"0%\"\n",
"\n",
"bucket_counts = rs[\"rupture_bucket\"].value_counts().sort_index()\n",
"print(bucket_counts)\n",
"\n",
"\n",
"# 3. Pourcentages\n",
"bucket_percent = (bucket_counts / len(rs) * 100).round(2)\n",
"\n",
"print(\"\\n=== DISTRIBUTION (PERCENT) ===\")\n",
"print(bucket_percent)\n",
"\n",
"\n",
"# 4. Nombre de comptes totalement propres\n",
"no_rupture = (rs[\"n_ruptures\"] == 0).sum()\n",
"print(f\"\\nComptes avec 0 rupture = {no_rupture} ({no_rupture/len(rs)*100:.2f}%)\")\n",
"\n",
"# 5. Comptes extrêmement problématiques\n",
"severe = (rs[\"rupture_ratio\"] > 0.75).sum()\n",
"print(f\"Comptes avec rupture_ratio > 75% = {severe} ({severe/len(rs)*100:.2f}%)\")\n",
"\n",
"medium = (rs[\"rupture_ratio\"] > 0.10).sum()\n",
"print(f\"Comptes avec rupture_ratio > 10% = {medium} ({medium/len(rs)*100:.2f}%)\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f39a9a5a-5f4e-4cac-9f63-e6952582b6ff",
"metadata": {},
"outputs": [],
"source": [
"import plotly.express as px\n",
"\n",
"fig = px.histogram(\n",
" rs,\n",
" x=\"rupture_ratio\",\n",
" nbins=50,\n",
" title=\"Distribution du rupture_ratio\",\n",
" labels={\"rupture_ratio\": \"Rupture Ratio\"},\n",
")\n",
"fig.update_layout(bargap=0.05)\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "70132995-8379-44b6-8ff6-f09524c4e4d0",
"metadata": {},
"outputs": [],
"source": [
"# --- 1. Filtres de base ---\n",
"merged[\"year\"] = merged[\"Centralisation Date\"].dt.year\n",
"\n",
"# Filtrer uniquement l'année 2021\n",
"ruptures_2021 = merged[(merged[\"year\"] == 2021) & (merged[\"rupture_flag\"] == True)].copy()\n",
"\n",
"print(\"Nombre total de ruptures en 2021 :\", len(ruptures_2021))\n",
"\n",
"# --- 2. Classification du type de gap ---\n",
"ruptures_2021[\"gap_type\"] = np.where(ruptures_2021[\"gap\"] > 0, \"positive\", \"negative\")\n",
"\n",
"# --- 3. Statistiques globales ---\n",
"gap_counts = ruptures_2021[\"gap_type\"].value_counts()\n",
"gap_percent = ruptures_2021[\"gap_type\"].value_counts(normalize=True) * 100\n",
"\n",
"print(\"\\n=== RUPTURES 2021 — POSITIVES vs NEGATIVES ===\")\n",
"print(gap_counts)\n",
"print(\"\\n(%)\")\n",
"print(gap_percent.map(lambda x: f\"{x:.2f}%\"))\n",
"\n",
"# --- 4. Intensité des écarts ---\n",
"intensity_stats = ruptures_2021.groupby(\"gap_type\")[\"gap\"].describe()\n",
"print(\"\\n=== STATISTIQUES DES GAPS ===\")\n",
"print(intensity_stats)\n",
"\n",
"# --- 5. Visualisation rapide ---\n",
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"\n",
"plt.figure(figsize=(10,5))\n",
"sns.histplot(data=ruptures_2021, x=\"gap\", hue=\"gap_type\", bins=80, kde=True)\n",
"plt.xlim(-merged[\"gap\"].abs().max(), merged[\"gap\"].abs().max())\n",
"plt.title(\"Distribution des gaps de rupture en 2021\")\n",
"plt.xlabel(\"Gap (AUM_{t} Expected AUM_{t})\")\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1faf943a-4703-4b19-a867-2670ac3a5209",
"metadata": {},
"outputs": [],
"source": [
"# --- 1. ADD YEAR ---\n",
"merged[\"year\"] = merged[\"Centralisation Date\"].dt.year\n",
"\n",
"# --- 2. DEFINE PERIODS ---\n",
"conditions = [\n",
" merged[\"year\"] < 2021,\n",
" merged[\"year\"] == 2021,\n",
" merged[\"year\"] > 2021\n",
"]\n",
"\n",
"period_labels = [\"before_2021\", \"during_2021\", \"after_2021\"]\n",
"\n",
"merged[\"period\"] = np.select(\n",
" conditions,\n",
" period_labels,\n",
" default=\"unknown\"\n",
")\n",
"\n",
"# --- 3. CREATE GAP TYPE & FILTER ONLY RUPTURES ---\n",
"merged[\"gap_type\"] = np.where(\n",
" merged[\"gap\"] > 0, \"positive\",\n",
" np.where(merged[\"gap\"] < 0, \"negative\", \"zero\")\n",
")\n",
"\n",
"ruptures = merged[merged[\"rupture_flag\"] == True].copy()\n",
"\n",
"# --- 4. TOTAL OBS PER PERIOD ---\n",
"total_obs = merged.groupby(\"period\").size().rename(\"total_obs\")\n",
"\n",
"# --- 5. TOTAL RUPTURES PER PERIOD ---\n",
"rupture_counts = ruptures.groupby(\"period\").size().rename(\"rupture_count\")\n",
"\n",
"# --- 6. PROPORTION OF RUPTURES ---\n",
"rupture_ratio = (rupture_counts / total_obs).rename(\"rupture_ratio\")\n",
"\n",
"# --- 7. POSITIVE / NEGATIVE GAPS (% among ruptures) ---\n",
"gap_dist = (\n",
" ruptures.groupby([\"period\", \"gap_type\"])\n",
" .size()\n",
" .groupby(level=0)\n",
" .apply(lambda x: (x / x.sum()) * 100) # % par période\n",
")\n",
"\n",
"\n",
"# --- 8. MERGE AND DISPLAY ---\n",
"summary = pd.concat([total_obs, rupture_counts, rupture_ratio], axis=1)\n",
"summary[\"rupture_ratio\"] = (summary[\"rupture_ratio\"] * 100).round(2)\n",
"\n",
"print(\"\\n=== RUPTURE SUMMARY (in %) ===\")\n",
"print(summary)\n",
"\n",
"print(\"\\n=== GAP POSITIVE / NEGATIVE DISTRIBUTION (in %) ===\")\n",
"print(gap_dist)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5abee764-b890-4ea1-8f98-5a0ff1512611",
"metadata": {},
"outputs": [],
"source": [
"from plotly.subplots import make_subplots\n",
"import plotly.graph_objects as go\n",
"\n",
"# --- 1. DEFINE PERIODS ---\n",
"merged[\"period2\"] = np.where(\n",
" merged[\"Centralisation Date\"] < pd.Timestamp(\"2021-09-01\"),\n",
" \"Before Sep 2021\",\n",
" \"After Sep 2021\"\n",
")\n",
"\n",
"ruptures = merged[merged[\"rupture_flag\"] == True].copy()\n",
"\n",
"# --- 2. Ensure gap_type exists + no missing categories ---\n",
"ruptures[\"gap_type\"] = ruptures[\"gap_type\"].replace({\"zero\": \"positive\"}) # zero is equivalent to no-flow change\n",
"\n",
"# --- 3. Compute gap counts ---\n",
"gap_counts = (\n",
" ruptures.groupby([\"period2\", \"gap_type\"])\n",
" .size()\n",
" .unstack(fill_value=0)\n",
")\n",
"\n",
"# Ensure both columns exist\n",
"for col in [\"positive\", \"negative\"]:\n",
" if col not in gap_counts.columns:\n",
" gap_counts[col] = 0\n",
"\n",
"gap_counts = gap_counts[[\"positive\", \"negative\"]]\n",
"\n",
"# --- 4. Extract values ---\n",
"before_vals = gap_counts.loc[\"Before Sep 2021\"].values\n",
"after_vals = gap_counts.loc[\"After Sep 2021\"].values\n",
"\n",
"# --- 5. MAKE TWO DONUT CHARTS ---\n",
"fig = make_subplots(\n",
" rows=1, cols=2,\n",
" specs=[[{\"type\": \"pie\"}, {\"type\": \"pie\"}]],\n",
" subplot_titles=(\"Before Sep 2021\", \"After Sep 2021\")\n",
")\n",
"\n",
"fig.add_trace(\n",
" go.Pie(\n",
" labels=[\"Negative gaps\", \"Positive gaps\"],\n",
" values=before_vals,\n",
" marker_colors=[\"#E67E22\", \"#3498DB\"],\n",
" hole=0.45,\n",
" textinfo=\"label+percent\"\n",
" ),\n",
" row=1, col=1\n",
")\n",
"\n",
"fig.add_trace(\n",
" go.Pie(\n",
" labels=[\"Negative gaps\", \"Positive gaps\"],\n",
" values=after_vals,\n",
" marker_colors=[\"#E67E22\", \"#3498DB\"],\n",
" hole=0.45,\n",
" textinfo=\"label+percent\"\n",
" ),\n",
" row=1, col=2\n",
")\n",
"\n",
"fig.update_layout(\n",
" title=\"Nature des ruptures (positive / negative)\\nAvant vs Après Septembre 2021\",\n",
" showlegend=True\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3aa3b8a0-f499-495a-9171-2e09d0bb1e5f",
"metadata": {},
"outputs": [],
"source": [
"import plotly.graph_objects as go\n",
"\n",
"# --- 1. Compute gap counts by period ---\n",
"gap_counts = (\n",
" ruptures.groupby([\"period2\", \"gap_type\"])\n",
" .size()\n",
" .unstack(fill_value=0)\n",
")\n",
"\n",
"# Ensure both columns exist\n",
"for col in [\"positive\", \"negative\"]:\n",
" if col not in gap_counts.columns:\n",
" gap_counts[col] = 0\n",
"\n",
"gap_counts = gap_counts[[\"positive\", \"negative\"]]\n",
"\n",
"# --- 2. Extract values ---\n",
"before_vals = gap_counts.loc[\"Before Sep 2021\"].values\n",
"after_vals = gap_counts.loc[\"After Sep 2021\"].values\n",
"\n",
"# --- 3. Plot : TWO PIE CHARTS side by side ---\n",
"fig = make_subplots(\n",
" rows=1, cols=2,\n",
" specs=[[{\"type\": \"pie\"}, {\"type\": \"pie\"}]],\n",
" subplot_titles=(\"Before 2021\", \"After 2021\")\n",
")\n",
"\n",
"fig.add_trace(\n",
" go.Pie(\n",
" labels=[\"Negative gaps\", \"Positive gaps\"],\n",
" values=before_vals,\n",
" marker_colors=[\"#E67E22\", \"#3498DB\"],\n",
" hole=0.35\n",
" ),\n",
" row=1, col=1\n",
")\n",
"\n",
"fig.add_trace(\n",
" go.Pie(\n",
" labels=[\"Negative gaps\", \"Positive gaps\"],\n",
" values=after_vals,\n",
" marker_colors=[\"#E67E22\", \"#3498DB\"],\n",
" hole=0.35\n",
" ),\n",
" row=1, col=2\n",
")\n",
"\n",
"fig.update_layout(\n",
" title=\"Répartition des ruptures (positive / negative)\\nAvant vs Après 2021\"\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d4f0dc74-649d-4105-9a1a-44a18d126a3c",
"metadata": {},
"outputs": [],
"source": [
"import plotly.graph_objects as go\n",
"\n",
"# --- 1. Define periods ---\n",
"merged[\"period2\"] = np.where(\n",
" merged[\"Centralisation Date\"] < pd.Timestamp(\"2021-09-01\"),\n",
" \"Before Sep 2021\",\n",
" \"After Sep 2021\"\n",
")\n",
"\n",
"# --- 2. Keep only ruptures ---\n",
"ruptures = merged[merged[\"rupture_flag\"] == True].copy()\n",
"\n",
"# --- 3. Count ruptures per period ---\n",
"rupture_counts = ruptures[\"period2\"].value_counts().reindex(\n",
" [\"Before Sep 2021\", \"After Sep 2021\"]\n",
").fillna(0)\n",
"\n",
"# --- 4. Pie chart ---\n",
"fig = go.Figure(data=[\n",
" go.Pie(\n",
" labels=rupture_counts.index,\n",
" values=rupture_counts.values,\n",
" hole=0.45,\n",
" marker_colors=[\"#2ECC71\", \"#E74C3C\"],\n",
" textinfo=\"percent+value\",\n",
" )\n",
"])\n",
"\n",
"fig.update_layout(\n",
" title=\"Répartition des ruptures\"\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ecccd73c-00a6-4ff3-b213-e85b98ec5a55",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"# 1. Filtre sur la période post-Sept 2021\n",
"cutoff = pd.Timestamp(\"2021-09-01\")\n",
"post = merged[merged[\"Centralisation Date\"] >= cutoff].copy()\n",
"\n",
"# 2. On ne garde que les ruptures\n",
"post_rupt = post[post[\"rupture_flag\"] == True].copy()\n",
"\n",
"# 3. Gap absolu + gap relatif (% du stock)\n",
"post_rupt[\"gap_abs\"] = post_rupt[\"gap\"].abs()\n",
"post_rupt[\"gap_rel\"] = post_rupt[\"gap_abs\"] / post_rupt[\"Quantity - AUM\"].replace(0, np.nan)\n",
"\n",
"# 4. Percentiles globaux\n",
"p90 = post_rupt[\"gap_abs\"].quantile(0.90)\n",
"p95 = post_rupt[\"gap_abs\"].quantile(0.95)\n",
"p99 = post_rupt[\"gap_abs\"].quantile(0.99)\n",
"\n",
"# 5. Classification automatique\n",
"def classify_gap(gap, gap_rel, acct):\n",
" # RESET → énorme choc (technique)\n",
" if gap_abs >= p99 or gap_rel >= 0.90:\n",
" return \"reset\"\n",
"\n",
" # SPIKE → très gros gap mais isolé\n",
" if gap_abs >= p95:\n",
" return \"spike\"\n",
"\n",
" # SHIFT → décalage permanent\n",
" # Test : moyenne des gaps du compte\n",
" return None\n",
"\n",
"# Calcul du shift (décalage directionnel)\n",
"shift_info = post_rupt.groupby(\"Registrar Account - ID\")[\"gap\"].mean().rename(\"avg_gap\")\n",
"\n",
"post_rupt = post_rupt.merge(shift_info, on=\"Registrar Account - ID\", how=\"left\")\n",
"\n",
"post_rupt[\"gap_type2\"] = np.where(\n",
" post_rupt[\"gap_abs\"] >= p99, \"reset\",\n",
" np.where(post_rupt[\"gap_abs\"] >= p95, \"spike\",\n",
" np.where(post_rupt[\"avg_gap\"].abs() > post_rupt[\"gap_abs\"].median(), \"shift\", \"micro\")))\n",
" \n",
"# 6. Statistiques globales\n",
"stats = post_rupt[\"gap_type2\"].value_counts(normalize=True).round(3) * 100\n",
"print(\"\\n=== DISTRIBUTION DES TYPES DE GAPS POST-2021 ===\")\n",
"print(stats)\n",
"\n",
"# 7. Stats par client\n",
"client_stats = (\n",
" post_rupt.groupby(\"Registrar Account - ID\")[\"gap_type2\"]\n",
" .value_counts(normalize=True)\n",
" .rename(\"ratio\")\n",
" .mul(100)\n",
" .reset_index()\n",
")\n",
"\n",
"# 8. Stats par ISIN\n",
"isin_stats = (\n",
" post_rupt.groupby(\"Product - Isin\")[\"gap_type2\"]\n",
" .value_counts(normalize=True)\n",
" .rename(\"ratio\")\n",
" .mul(100)\n",
" .reset_index()\n",
")\n",
"\n",
"print(\"\\n=== TOP ISIN PAR RESET ===\")\n",
"print(isin_stats[isin_stats[\"gap_type2\"]==\"reset\"].sort_values(\"ratio\", ascending=False).head(10))\n",
"\n",
"print(\"\\n=== TOP CLIENTS PAR RESET ===\")\n",
"print(client_stats[client_stats[\"gap_type2\"]==\"reset\"].sort_values(\"ratio\", ascending=False).head(10))\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2efc5e0-bc35-4fa7-ab5d-6be616964446",
"metadata": {},
"outputs": [],
"source": [
"import plotly.graph_objects as go\n",
"\n",
"# --- Data from your output ---\n",
"labels = [\"Micro-ruptures\", \"Décalage\", \"Anomalies ponctuelles\", \"Remise à zéro\"]\n",
"values = [50.4, 44.6, 4.0, 1.0]\n",
"\n",
"# --- Pie chart ---\n",
"fig = go.Figure(\n",
" data=[go.Pie(\n",
" labels=labels,\n",
" values=values,\n",
" hole=0.35, # donut style (plus lisible)\n",
" textinfo='percent',\n",
" marker=dict(colors=[\"#3498DB\", \"#E67E22\", \"#9B59B6\", \"#E74C3C\"])\n",
" )]\n",
")\n",
"\n",
"fig.update_layout(\n",
" title=\"Typologie des ruptures depuis Septembre 2021\",\n",
" legend_title=\"Type de gap\",\n",
")\n",
"\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "744e04b6-3f34-40c9-95fe-a5605e7c7f02",
"metadata": {},
"outputs": [],
"source": [
"merged[\"gap_abs\"] = merged[\"gap\"].abs()\n",
"\n",
"merged[\"gap_rel\"] = (\n",
" merged[\"gap_abs\"] /\n",
" merged[\"Quantity - AUM\"].replace(0, np.nan)\n",
")\n",
"\n",
"merged.loc[merged[\"rupture_flag\"], \"gap_rel\"].describe(\n",
" percentiles=[0.5, 0.75, 0.9, 0.95, 0.99]\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d20625e-1045-4b7a-ab64-3381997e4131",
"metadata": {},
"outputs": [],
"source": [
"# uniquement sur les ruptures\n",
"df_r = merged[merged[\"rupture_flag\"]].copy()\n",
"\n",
"# seuils globaux (descriptifs, pas \"optimisés\")\n",
"q90 = df_r[\"gap_abs\"].quantile(0.90)\n",
"q99 = df_r[\"gap_abs\"].quantile(0.99)\n",
"\n",
"# moyenne directionnelle par compte\n",
"avg_gap_by_account = (\n",
" df_r.groupby(\"Registrar Account - ID\")[\"gap\"]\n",
" .mean()\n",
" .rename(\"avg_gap\")\n",
")\n",
"\n",
"df_r = df_r.merge(avg_gap_by_account, on=\"Registrar Account - ID\", how=\"left\")\n",
"\n",
"def classify_gap(row):\n",
" if row[\"gap_abs\"] >= q99:\n",
" return \"reset\"\n",
" if row[\"gap_abs\"] >= q90:\n",
" return \"spike\"\n",
" if abs(row[\"avg_gap\"]) > row[\"gap_abs\"]:\n",
" return \"shift\"\n",
" return \"micro\"\n",
"\n",
"df_r[\"discontinuity_type\"] = df_r.apply(classify_gap, axis=1)\n",
"df_r[\"discontinuity_type\"].value_counts(normalize=True) * 100\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "02806629-e454-4e10-82be-6e2239091088",
"metadata": {},
"outputs": [],
"source": [
"merged[\"year\"] = merged[\"Centralisation Date\"].dt.year\n",
"\n",
"yearly_stats = merged.groupby(\"year\").agg(\n",
" total_obs=(\"gap\", \"count\"),\n",
" ruptures=(\"rupture_flag\", \"sum\")\n",
").reset_index()\n",
"\n",
"yearly_stats[\"rupture_rate\"] = (\n",
" yearly_stats[\"ruptures\"] / yearly_stats[\"total_obs\"]\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2edf2c55-45e7-4aad-b4f9-5c35178abad6",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"df_r = merged[merged[\"rupture_flag\"]].copy()\n",
"\n",
"plt.figure(figsize=(12,4))\n",
"plt.hist(df_r[\"gap_abs\"], bins=100, log=True)\n",
"plt.title(\"Distribution of absolute gaps (log scale)\")\n",
"plt.xlabel(\"Absolute gap\")\n",
"plt.ylabel(\"Frequency (log)\")\n",
"plt.show()\n",
"\n",
"plt.figure(figsize=(12,4))\n",
"plt.hist(df_r[\"gap_rel\"].dropna(), bins=100, log=True)\n",
"plt.title(\"Distribution of relative gaps (|gap| / AUM)\")\n",
"plt.xlabel(\"Relative gap\")\n",
"plt.ylabel(\"Frequency (log)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "981f2ec6-574b-41ea-b4bf-45be54aeda1f",
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(10,4))\n",
"plt.plot(yearly_stats[\"year\"], yearly_stats[\"rupture_rate\"], marker=\"o\")\n",
"plt.title(\"Evolution of AUMFlow inconsistency rate over time\")\n",
"plt.xlabel(\"Year\")\n",
"plt.ylabel(\"Rupture rate\")\n",
"plt.grid(True)\n",
"plt.show()\n"
]
}
],
"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.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}