4388 lines
223 KiB
Plaintext
4388 lines
223 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 23,
|
||
"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\n",
|
||
"import matplotlib.pyplot as plt"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "cfd11919-0941-400e-a516-72871881f733",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/tmp/ipykernel_1311/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_1311/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": 3,
|
||
"id": "b99e3402-fe26-4f4e-8c1c-5f07847bce94",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/tmp/ipykernel_1311/3613746644.py:1: DtypeWarning: Columns (1) have mixed types. Specify dtype option on import or set low_memory=False.\n",
|
||
" merged = pd.read_csv('merged.csv')\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"merged = pd.read_csv('merged.csv')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "34e5a815-7269-4312-bfe6-e2cd12595e57",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# 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": 5,
|
||
"id": "78c3db70-e0b6-4de2-92ca-e29cf5bf6bd1",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ============================================================\n",
|
||
"# AUM–FLOW 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": 6,
|
||
"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": 7,
|
||
"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 (1–10%)",
|
||
"High (10–30%)",
|
||
"Severe (>30%)"
|
||
],
|
||
"textinfo": "percent",
|
||
"type": "pie",
|
||
"values": {
|
||
"bdata": "AAAAAACASEAAAAAAAIBBQAAAAAAAAChAZmZmZmZmEEA=",
|
||
"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": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAFoCAYAAAB0XzViAAAQAElEQVR4AezdB5wcdf3/8c+Wy6WTTggtARJ670QhoFQTqqEjKCCISFVKUIyKoXdEEFD8I/ATREFKaAIWeu+B0CEhCQTSLneX2/Lf94Q5Jpu9u929LVNePPhmdme+853v9/md3Z3PfGfm4ln+QwABBBBAAAEEEEAAAQRCJBA3/kMAgQICzEIAAQQQQAABBBAIqgBBTlB7jnojgAAC9RBgmwgggAACCARAgCAnAJ1EFRFAAAEEEEDA3wLUDgEE/CVAkOOv/qA2CCCAAAIIIIAAAgiERaBu7SDIqRs9G0YAAQQQQAABBBBAAIFqCBDkVEOVMisnQEkIIIAAAggggAACCJQoQJBTIhjZEUAAAT8IUAcEEEAAAQQQ6FiAIKdjG5YggAACCCCAQLAEqC0CCCDgCBDkOAz8gwACCCCAAAIIIIBAWAWi1y6CnOj1OS1GAAEEEEAAAQQQQCDUAgQ5oe7eyjWOkhBAAAEEEEAAAQQQCIoAQU5Qeop6IoCAHwWoEwIIIIAAAgj4UIAgx4edQpUQQAABBBAItgC1RwABBOorQJBTX3+2jgACCCCAAAIIIBAVAdpZMwGCnJpRsyEEEEAAAQQQQAABBBCohQBBTi2UK7cNSkIAAQQQQAABBBBAAIEuBAhyugBiMQIIBEGAOiKAAAIIIIAAAl8LEOR8bcErBBBAAAEEwiVAaxBAAIGIChDkRLTjaTYCCCCAAAIIIBBVAdodfgGCnPD3MS1EAAEEEEAAAQQQQCBSAgQ5ZXU3KyGAAAIIIIAAAggggIBfBQhy/Noz1AuBIApQZwQQQAABBBBAwAcCBDk+6ASqgAACCCAQbgFahwACCCBQWwGCnNp6szUEEEAAAQQQQACBpQL8i0DVBAhyqkZLwQgggAACCCCAAAIIIFAPgWAHOfUQY5sIIIAAAggggAACCCDgawGCHF93D5VDoDwB1kIAAQQQQAABBKIsQJAT5d6n7QgggEC0BGgtAggggEBEBAhyItLRNBMBBBBAAAEEECgswFwEwidAkBO+PqVFCCCAAAIIIIAAAghEWqAiQU6kBWk8AggggAACCCCAAAII+EqAIMdX3UFlQiZAcxBAAAEEEEAAAQTqIECQUwd0NokAAghEW4DWI4AAAgggUF0Bgpzq+lI6AggggAACCCBQnAC5EECgYgIEORWjpCAEEEAAAQQQQAABBBCotEA55RHklKPGOggggAACCCCAAAIIIOBbAYIc33YNFaucACUhgAACCCCAAAIIREmAICdKvU1bEUAAAa8ArxFAAAEEEAipAEFOSDuWZiGAAAIIIIBAeQKshQACwRcgyAl+H9ICBBBAAAEEEEAAAQSqLRCo8glyAtVdVBYBBBBAAAEEEEAAAQS6EiDI6UqI5ZUToCQEEEAAAQQQQAABBGogQJBTA2Q2gQACCHQmwDIEEEAAAQQQqKwAQU5lPSkNAQQQQAABBCojQCkIIIBA2QIEOWXTsSICCCCAAAIIIIAAArUWYHvFCBDkFKNEHgQQQAABBBBAAAEEEAiMAEFOYLqqchWlJAQQQAABBBBAAAEEwixAkBPm3qVtCCBQigB5EUAAAQQQQCAkAgQ5IelImoEAAggggEB1BCgVAQQQCJ4AQU7w+owaI4AAAggggAACCNRbgO37WoAgx9fdQ+UQQAABBBBAAAEEEECgVAGCnFLFKpefkhBAAAEEEEAAAQQQQKAKAgQ5VUClSAQQ6I4A6yKAAAIIIIAAAt0TIMjpnh9rI4AAAgggUBsBtoIAAgggULQAQU7RVGREAAEEEEAAAQQQ8JsA9UGgkABBTiEV5iGAAAIIIIAAAggggEBgBQhyLLB9R8URQAABBBBAAAEEEECggABBTgEUZiGAgJmBgAACCCCAAAIIBFSAICegHUe1EUAAAQTqI8BWEUAAAQT8L0CQ4/8+ooYIIIAAAggggIDfBagfAr4SIMjxVXdQGQQQQAABBBBAAAEEEOiugH+CnO62hPURQAABBBBAAAEEEEAAgZwAQU4Ogf8R8LMAdUMAAQQQQAABBBAoTYAgpzQvciOAAAII+EOAWiCAAAIIINChAEFOhzQsQAABBBBAAAEEgiZAfRFAQAIEOVIgIYAAAggggAACCCCAQGgElgtyQtMyGoIAAggggAACCCCAAAKRFCDIiWS30+gyBFgFAQQQQAABBBBAICACBDkB6SiqiQACCPhTgFohgAACCCDgPwGCHP/1CTVCAAEEEEAAgaALUH8EEKirAEFOXfnZOAIIIIAAAggggAAC0RGoVUsJcmolzXYQQAABBBBAAAEEEECgJgIEOTVhZiOVE6AkBBBAAAEEEEAAAQQ6FyDI6dyHpQgggEAwBKglAggggAACCLQLEOS0U/ACAQQQQAABBMImQHsQQCCaAgQ50ex3Wo0AAggggAACCCAQXYHQt5wgJ/RdTAMRQAABBBBAAAEEEIiWAEFOtPq7cq2lJAQQQAABBBBAAAEEfCpAkOPTjqFaCCAQTAFqjQACCCCAAAL1FyDIqX8fUAMEEEAAAQTCLkD7EEAAgZoKEOTUlJuNIYAAAggggAACCCDgCjCtlgBBTrVkKRcBBBBAAAEEEEAAAQTqIkCQUxf2ym2UkhBAAAEEEEAAAQQQQGBZAYKcZT14hwAC4RCgFQgggAACCCAQYQGCnAh3Pk1HAAEEEIiaAO1FAAEEoiFAkBONfqaVCCCAAAIIIIAAAh0JMD90AgQ5oetSGoQAAggggAACCCCAQLQFCHIq0/+UggACCCCAAAIIIIAAAj4RIMjxSUdQDQTCKUCrEEAAAQQQQACB2gsQ5NTenC0igAACCERdgPYjgAACCFRVgCCnqrwUjgACCCCAAAIIIFCsAPkQqJQAQU6lJCkHAQQQQAABBBBAAAEEfCEQsiDHF6ZUAgEEEEAAAQQQQAABBOooQJBTR3w2jUDNBNgQAggggAACCCAQIQGCnAh1Nk1FAAEEEFhWgHcIIIAAAuEUIMgJZ7/SKgQQQAABBBBAoFwB1kMg8AIEOYHvQhqAAAIIIIAAAggggAACXoHqBDneLfAaAQQQQAABBBBAAAEEEKihAEFODbHZFAIIIIAAAggggAACCFRfgCCn+sZsAQEEEAilQPqtV63t2f/aksfus9Z7b7OW226w5hsvt8W/+601XXCGLZr8E1v4syNswXH72bwjdrWXzv2jHX9am/3sl232y/NSdt7lKbv82pRd9//SdvPtabvzvrQ98p+Mvfxa1j6ZmbXWJaFko1EIIIAAAjUQiNdgG2wCAQQQQCDIAtmsZT5535Y8eq8tvu4iW3ja923egTvYwl/8yJouPNMWXz3Fmv98hbX87U/Wet/ttuTfU63tuf9Z6o0XLf3hO5b5fLbZ4iaLZTPW0mr25TyzGZ9m7Z33svbqG1l7+vmMPfq/jN3zQMZuuSNtV16Xssnnp+zHP2uzn5ze5ry+8g8pu+VvaXvs8Yx9misuyJzUHYHiBciJAALlCsTLXZH1EEAAAQTCKZBdMM8ZoWm59Vpb9OsTnVGYBaccZot/f64teehOS38w3SyTrknjm1vMGdV5+fWsPfLfjP3ltrT9YkqbnXxWm13zp5Qzb2YuYKpJZdgIAggggIA/BIqoBUFOEUhkQQABBMIukHrtBVt8zXk2/0f72vyjxjsjNC3/uMlSrz1v1rzYd81fuMjsuZeyzujO2eel7MQz2+x3N6ScEaEm/1XXd35UCAEEEAi7AEFO2HuY9hUSYB4CCOQEdClZ81+utvnH7pMbsTnBljxyj2XnzsktCd7/CmxefCXr3NtzSm6UR/f6PPVcxlpbg9cWaowAAggg0H0BgpzuG1ICAgggEBiB7OezreXOm2zBqYc5DwVo/ectlv3is6/qH45JOmPOvT7X35S2k3IBzzU3pk0BUKo2V9iFA5FWIIAAAgEXIMgJeAdSfQQQQKArgeyihc69NIvOPs7mH7eftdxyrWU+fr+r1UKxvK3N7LkXM86lbCed2WY3/y1tc78IRdNoRC0F2BYCCAROgCAncF1GhRFAAIHiBLLzvzQ90nn+MXs5T0VLTXuluBVDmktPdnv0vxk749dtdu2f084DDULaVJqFAAII1ETAzxshyPFz71A3BBBAoAwBPR2t+abf2fwff9d5pLO1LSmjlPCuks2aPftCxnk09SVXp+yNt3IzwttcWoYAAghEUoAgJ5Ld7pdGUw8EEKikQLZpoXMp2vzjJ1rr3beaLeGu+658FeAo0Pn1BSkn8FEA1NU6LEcAAQQQ8L8AQY7/+4gaIoBA1ARKbe/iJmu57Xpz7re58yazluZSS4h8/o9mZJ1L2M4+t83eficTeQ8AEEAAgaALEOQEvQepPwIIRFegZbG13HGj6bK0lr/d6Mu/ZxO0zvl0ttkFV6btuv+XtvkLglb78NeXFiKAAALFChDkFCtFPgQQQMBHAnr08/zjvmstf73edJmaj6oWiqo8/XzGJp3TZg8+mjE9kjoUjaIRCCAQVgHaVUCAIKcACrMQQAABvwqkP/3YFpz6PdMf8cwuYqihmv2kPyR6251p4xK2aipTNgIIIFAdAYKc6rgGq1RqiwACgRBovf8OW/izwy3z8XuBqG9YKjl7ztJL2G66LW36uzthaRftQAABBMIsQJAT5t6lbQgg0C0Bv6ycWTjfFv32VGv+46VmS3gcdL365d+PZ2zyBW0241MeOV2vPmC7CCCAQLECBDnFSpEPAQQQqINA20tP2cKTD7HUy0/XYetsMl9Aozq/uTBlD/870k9gy2fhPQIIIOA7AYIc33UJFUIAAQRyArkRm+brL7amKT81/XHP3Bz+94lAKm32f39Pm/6+zsJFjOr4pFuoBgI+EKAKfhIgyPFTb1AXBBBAICeQ/uhdW/DT71nrg//IveN/vwroD4n+8ryUvT6NUR2/9hH1QgCB6AoQ5Pio76kKAghEXCCbNT0aeuEZR1pm1icRxwhG8xcsNLv092l75D+54Z1gVJlaIoAAApEQIMiJRDfTSAQCLRCJymczaWu6fLLzaGhLpSLR5jA18pY7Mnb7XWnLxalhahZtQQABBAIrQJAT2K6j4gggEBaBbDpliy/+hbU98S/jv+AKPPBIxv7w55Sl07W6Tye4VtQcAQQQqLYAQU61hSkfAQQQ6ERAAU7TBWda27P/6SQXi4Ii8OyLWbv82rS1thLoBKXPqGcIBWgSAjkBgpwcAv8jgAAC9RDIplKmACf14pP12DzbrJKAHkhw3uUpW9REoFMlYopFAAEEuhQgyFmeiDkIIIBA1QWybUus6dyfGgFO1anrsoGPZ5hNuTRl8+bXZfNsFAEEEIi8AEFO5HcBABAoVoB8lRLILmm1pt+eaqlXn6tUkZTjQ4E5n5ldeGUbIzo+7BuqhAAC4RcgyAl/H9NCBBDwkUC2ZbEt+vWJlnrjRR/Viqp0S6CTlWfnAp2LrkpZSwuXrnXCxCIEEECg4gIEORUnpUAEEECgIqMg7AAAEABJREFUsEB2cZMt+tUJln77tcIZmBtKgU9mLv1bOm1tBDqh7GAa1aEACxCopwBBTj312TYCCERGIJvJWNNFkyz97rTItJmGfi3w7gdZu/K6tKVSBDpfq/AKAQQQqJ6Aj4Oc6jWakhFAAIFaC7T85XeWeu35Wm+W7flIQE9du/qPactkCHR81C1UBQEEQipAkBPSjqVZIRagaYETWPLEv6z1nr8Grt5UuPICr7yetetvSls2S6BTeV1KRAABBL4WIMj52oJXCCCAQMUFUu+/bYuvOqfi5VLg8gJBmfPMC1mb+jBBTlD6i3oigEAwBQhygtlv1BoBBAIgkFnwpTWd+zOzVFsAaksVaynwj3vT9s57mVpukm1FV4CWIxBJAYKcSHY7jUYAgWoLZFOpXIBzmmXnza32pig/gAK6Wk335yxqYkQngN1HlRFAIAACXQc5AWgEVUQAAQT8JtD8p8ss/e6bfqsW9fGRwIKFZr//I/fn+KhLqAoCCIRIgCAnRJ1JU2orwNYQ6EhgyWNTbclDd3a0mPkItAu89U7W7nmA0Zx2EF4ggAACFRIgyKkQJMUggAACEki984YtvvY8vYxqot0lCvzz/rRNm879OSWykR0BBBDoVIAgp1MeFiKAAALFC2SXtFrTxT83S6eLX4mckRfQ/Tl/+HPaFjdHniLkADQPAQRqKUCQU0tttoUAAqEWaL3jRsvOnRPqNtK46gjo/pzb7iQ4ro4upSKAgK8FqlQ5gpwqwVIsAghESyA9e4a1/POWaDWa1lZU4H9PZezd97lsraKoFIYAApEVIMiJbNeHpuE0BAFfCDRfcx6XqfmiJ4JdiT/ekra2FA8iCHYvUnsEEPCDAEGOH3qBOiCAQKAF2h5/2FKvv+izNlCdIArMnmN230OM5gSx76gzAgj4S4Agx1/9QW0QQCBgAtnmxbb4xisCVmuq62eBex/M2KzZjOZUrY8oGAEEIiFAkBOJbqaRCCBQLYGWv15v2flfVKt4yo2gQCY3kKPL1iLYdJqMAAJ1FAjbpglywtajtAcBBGomkP7oXWud+reabY8NRUfgvQ+y9tyLuWgnOk2mpQgggEBFBQhyKsoZ5cJoOwLRE1h89RSzLAei0ev52rT4rqlpy2S4bK022mwFAQTCJkCQE7YepT0IIFATgSWP3mvp997qelvkQKBMgU9nmz37IkFOmXyshgACERcgyIn4DkDzEUCgDIElrdZ88+/LWDFiq/TuY7FBQyw+YjVLrLG2JYYOt2FDzXr0iJhDN5p79/3h/QOh3WBhVQQQQKBLAYKcLonIgAACCCwrsOTfUy27YN6yMyP+Lr7iCOsxbg/r/aMzrf8V/2cDbvufDbjxAVvhmjut/2W3WL/zbrANjh5vU37eYFdf2GBXnt9g55zVYD89PmlHHZawiXslbPONY9ZIALTMnjRrjtmTz3JJ5DIovEEg3AK0rkIC8QqVQzEIIIBAZARaeNiA09fJDTa33seeYStcd7f1v/I2633cJOux43csPnwVZ3ln//TqaTZ8mNk6o2O2zRZx23WnuP3oB0m7/LwGO/GYpG2/Xdz69+ushOgs+2duNCed5rK16PQ4LUUAgUoIEORUQtFPZVAXBBCoqkDq1ecs88kHVd2GnwtPjFnfen3/JCew6Xv25dZjp/EWW2FgxaqcTJhtuF7MvndAwi45p8HOOCnpBEADVqjYJgJX0Gefmz31HEFO4DqOCiOAQF0FCHLqys/GEUCgVgKV2k5UHxmd3HQb63fRn63fOdda4+7frWhg01nfrDUq5lzKdtGvG+zg/RLWr29nucO77J9TuTcnvL1LyxBAoBoCBDnVUKVMBBAIpUBm7hxre/7xULato0Yl1t7Q+p57vfU98yJLrLZmR9lqMn+n7eN2/i8bbN/xCevdqyab9M1G5n5p9sIrVRnN8U0bqQgCCCBQSQGCnEpqUhYCCIRaoPW+282y0TjQjA9byfpOusj6/eb3llxzHd/0q57MtsfOuWBncoON3zVujY2+qVrVK/LfJxnNqToyG0CgXYAXQRcgyAl6D1J/BBCoiUC2tcVaH7qz6G1NT8Xs8Hk9beKXvZz0i4WNttjzkKw7mpPOfHe5ppcvanDKn5vL96N5jc5yTfXeWZD7R+u5+XJvq/J/j2/vaf0vvsmSm2xTlfIrUageXLD3Hgk7Z1LSRq4Wq0SRvi/jtTezNm9+NIJs33cGFUQAAd8LEORUqYsoFgEEwiXQ9u/7zVqai27UK20JO653m90+sNlJQ2IZu27x0iDGLWSdZMb+vMLS5cp3Yt82Z9FjrUn7dmPaWU9TvdcCBTsvpRJ2aK5cva90ivXtb31yoze9f3iaWWPPShdflfIGDojZmScnbedx4f850yDi/54myKnKjkShCCAQOoHw/yqErstoEAKBFghs5Vum3l5S3ffrlbKte3x9edEqiax9no0vM5rTUYGfpGOm/Fquqd7rtYKdTZJpG1yFb+7kxltbv0tvtgYfj97IoFBK5DwO2Cdhxx+VsLBfvvY/Llkz/kMAAQSKEcj9NBSTjTwIIIBAdAWcx0bP+LBsAF2mphEYjeb09nzrTkvF7fD5Sy9n816C5g1sFODovTuKM64xVXY9Olqxx7cmWN+zLrZ4BR8F3dG2qjl/kw3jNvn0pK26cjW3Uq2yiyv38y/Mpk1nNKc4LXIhgECUBTw/t1FmoO0IIIBAxwJL/vtgxwu7WKLgRYGMsh3tucxMIz26RE3pmhWaTQGP7rdRPgUyD7cmnHtyNNV7dxTni0zM3Ht98u/z0bqlph677mu9jzm91NV8m3/o4Jj98rQG54+J+raS3azY/57KdLMEVkcgQAJUFYEyBQhyyoRjNQQQiI5A2/NPlN1Y3WejQEaXmZ3b1FjwcjVdfqZ7bzRqow3p/e8HtDr35GiqeRoJUrBzX0uy/V4fjQxNbU1qcVmpccKB1vvIU8pa1+8r6Y+JbrVZOB9I8NxLGVu8mNEcv++D1A8BBOorEPYgp766bB0BBAIvkPnoPcsunNftdmzUkDYdlxb/6IKvN+mO4uhPw+i+HneJLmNzX5c6bfjmLtbrsONLXS1Q+X9waMLWGBm+QCeVMnv5dYKcQO2MVBYBBGouQJBTc3I2iIAfBKhDsQJtLz1VbNZl8v2hKWl6jLQ7UyMwvXPH2wpUdI/OTYuTpqmW634bXZa2TY/lL0PSMncUR/fzaPRG6yi5Iz96XUpKrLmO9f7RpFJWCWTeZCJmJx6TsGFDAln9Tiv9+jSCnE6BWIgAApEXIMiJ/C4AAAIIdCbQ9vIznS3ucNnGDVmbtLCnc1+N/gaORmDO7NNqClSUnHtrvnrowLG5qS5X8z6NzS34L4sbbHxjqv2Janp89I25AMktc/fcMjdvMdPYgMHW58wLLZYs/zK3Yrbjlzx9cpHlKcclrbeiy2IqFZA8r09bPiAOSNWpJgIIIFATAYKcmjCzEQQQCKJAtm2Jpd58uayqK2DRvThu+k2/pQGOW5h7r467XA8icJd5p8qnstx53vt18st083Q4TTZY30kXWbz/wA6zhHHBkMFLR3T0qOmwtG/hIrOPZzCaE5b+DEI7qCMCQRMgyAlaj1FfBBComUD61efNUkv/QGfNNlrFDfU68GhLjBxdxS34t+g1R8Vt953D9ZP3Opes+XeHo2YIIFB3gRp949e9nVQAAQQQKFmg7eWnS17HryvEh42wHnvs79fq1aReu38rbv361mRTNdnI61yyVhNnNoIAAsEUIMgJZr9R67AI0A5fC5R7P44fG9Xr2DMicx9OR/6NjTHbd3yio8WBmz/93ay1pbhkLXAdR4URQKAmAgQ5NWFmIwggEDSBzNw5lpn5UdCqXbC+DVvvYA0bbFZwmV9nVqte39w2biNWilWr+JqWm0qbvTW9pptkYwgggEBgBAhyAtNVVBQBBGopkHqh/D8AWst6drmthh7W6/ATuswWpQwH7xcPTXM//JiRnNB0ZnENIRcCCBQpEJ5v+iIbTDYEEECgGIGwXKrW+J39LT5kxWKaHJk864yO24brhWM055OZBDmR2XFpKAIIdCKw/CKCnOVNmIMAAghY+v23g68QT1hjxB82YB38d8A+CYuFIM75ZCZ/L6eDLmY2AghEXIAgJ+I7AM1fKsC/CHgFspm0ZT6b5Z0VyNcN2+xg8QGDAln3ald6+LCYbbVZ8H8CP51dbSnKRwABBIIpEPxv+GC6U2sEEPCxQPazcBw59tzjgO4qh3r9rTYPx0/gR59wyVqod1QahwACZQmE4xu+rKazEgIIIFBYIDN7RuEFAZqbWH0tS4xZP0A1rn1V11/brLGx9tut9BY/4b6cSpMWUR5ZEEDA7wIEOX7vIeqHAAI1F8jM+bTm26z0Bnvs+J1KFxm68pLJmG22UfB/Bj8myAndvkmDEAisgI8qHvxvdx9hUhUEEAiHQHrOzMA3pGGTrQPfhlo0YItNg/8zOIMgpxa7CttAAIGACQT/2z1g4FS3UwEWIuALgUzAg5zYoKEWH7GaLyz9XokwXLL2+VzuyfH7fkb9EECg9gIEObU3Z4sIIOBzgcxsv43klAbWsPnY0laIcO4wXLK2qCnCHUjTEUAAgQ4ECHI6gGE2AghEVyAT8HtyGrhUraSdd/Qawf6DOYubzTKZiI7mlNTTZEYAgSgJEOREqbdpKwIIdCmQbVti2YXzu8zn5wzJNdf1c/V8V7dhQ4Md5Ai0aXHw26B2kBBAoDIClGJGkMNegAACCHgEMjM/8rwL4Mtkg8UGDQlgxetX5WFDYvXbeIW2zCVrFYKkGAQQCI0AQU5ourKSDaEsBKIrEPRL1eLDV45u55XZ8oEDshYv89dwrz0Sds0lDXbqj5PtW992y7hddUGDXX/50uRdpky/PjPZvuzwAxOa5SSVddmUBtP6zowS/lnUxOVqJXCRFQEEIiBQ5td6BGRoIgIIRFIg27So43YHYEliRYKcUrspFovZikNLXctMQcnu345b8us4xdYZHbN9JyTs/Q+zdtSJbXb3AxkbvWbM3GBG6zQ0xOyiq1LOsnXGxJ11LPff5hvH7MVXMvbks5ncu9L+ZySnNC9yI4BA+AUIcsLfx7QQAQRKESj3lH4p26hi3jhBTlm6Q0u8ZE2jLTt+I26P/S9jX3pu4dp687j16mn2xDNLA5W77kvbnM+ytuaopZfEDehv1taWtWnTs878ZG4AaOCAmBMwKfh5+vml65XaiEWL6juSU2p9yY8AAghUW4Agp9rClI8AAoESiAU9yOFytbL2t1Luy1GAc8jEhDPq8tKrXQcl8xeY9eoVc0Zs5uVeK5jRiI8eeJBKmX05L2saxZn2dsYJfsppQOuSctZiHQQQqLIAxddRgCCnjvhsGgEEfCgQ8CAn1ruPD1H9XyUFHMXUUsGJLkd79oWM/fn/0sut8va7S0dUxqy5dOQmP4NGdjSS89PjkzZh17gpsFl7dNwU+GjdC3+99LqmP2MAABAASURBVD4e3eejS9usyP9ihTdX5NpkQwABBMInQJDj5z6lbgggUHuBgAc5pqGB2qsFf4tLY5Mu26FLy3Q52je3jTsPD1CwMnAFs3XHxJyHD+h+GgVA7nI9fEDLmpuXXqKmDZx9bsq5X0f37ChQckdxFBgpn+Y/+UzGvrHN1/fraL3OUpwgpzMeliGAQAQFCHIi2Ok0GYGgC1S1/kEPctKpqvKEtfDFzcW1TEHM8ae1tQcpeoCA7sl58+2sXfy7pfYKXBSouGnmrKy9+37hKEqjNRrF0b04QwZ/HanosrZS4tW45+EHxbWEXAgggEC4BQhywt2/tA4BBEoViAX7azGbXv4SqlIJoph/cW6kpRrt1uOjdT+OgphC5bujOHoQwedzvw6E9IACPZSg0DqF5iWW7raFFjEPAQQQiKQAX4uR7HYajQACHQoEfSSnlNP/HSJEb0GxIznFyCiw0WVqSiv0N/vZ2W2mICZ/Xfex0hr50TJ3qvW23Spu/3uq+AcRBH23VftJCFRPgJKjKECQE8Vep80IINChQCzg1/1k5s3tsG0s6Fig3JEcBS8KYtxL1bQFvXYvVdP9N5pXKCmoyV+u91r32FPaTA8pKLReoXkEOYVUmIcAAlEWIMgpovfJggACERII+NFiZtaMCHVW5ZraXOQ9OZXbYmVL4nK1ynpSGgIIBF+AICf4fUgLEKiXQDi3G/Bn8aZnfRzOfqlyqyp5uVqVq1qw+Hji64cWFMzATAQQQCBiAgQ5EetwmosAAl0IJBu6yODvxZlPP/F3BX1Yu2w2a5/O/vqm/+5XsfYl9O5V+22yRQQQQMDPAgQ5fu4d6oYAAjUXiA8aWvNtVnSDbUss+8VnFS0y7IV98JHZkiXBbuXgQYzkBLsHI1J7molADQUIcmqIzaYQQMD/ArFhw80C/hjp1DtvGP8VL6C/cVN8bn/mHDww2CNR/lSlVgggEGSBIAU5QXam7gggEBABPV0tvtIqAalt4Wq2vfR04QXMLSgwbXqm4PygzNRjqhPckxOU7qKeCCBQIwGCnBpBsxkEqidAyZUWSAwPeJDz4lOVJglteel01t5+J9ijIIMGcqlaaHdQGoYAAmULEOSUTceKCCAQVoGgj+Rk586xzMyPwto9xberiJzvfWiWSheR0cdZuB/Hx51D1RBAoG4CBDl1o2fDCCDgV4HESqv5tWpF16vtJUZzisGa9nawR3HUxsED9S8JgeIFyIlAFAQIcqLQy7QRAQRKEogH/J4cNXbJY1M1IXUh8Nq0YN+Po+YxkiMFEgIIILCsQBlBzrIF8A4BBBAIm0B8pVUD36T0B9Mt/fZrgW9HNRvw4cdZe/f94I/krDKCe3KquZ9QNgIIBFOAICeY/Uat/ShAnUIjEB+yolmyIfDtabnvtsC3oZoNuPO+gN+Mk8OJ5eKbkcGPyXMt4X8EEECgsgLxyhZHaQgggEA4BOIhuGSt7clHLTPvi7p3iB8rMOPTrL36RvBHcVZeKWY9euQiHT8iUycEEECgjgLxOm6bTSOAAAK+FUiE4JI1y2atldGcgvvY3fcHfxRHDRu1OgGOHAKaqDYCCFRRgCCnirgUjQACwRWIB/xv5bjyrfffYZkvP3ffMs0JzP4sa8+9FPxRnFxTbA2CHDGQEEAgVAKVaQxBTmUcKQUBBEImkByzQTha1NJszf/vqnC0pUKtuPfB4D9RzaVgJMeVYIoAAggsK0CQs6wH70IgQBMQqIRAcqMtLQwPH5BF2+MPW2raK3oZ+fTOexl78tlwBDkNDWYjhke+SwFAAAEECgrEC85lJgIIIBBxgVjPXpZcf9PQKCy+5jzLplKhaU85DVnUlLWr/5jWrUrlrO67dUatFrN4nHtyjP8QQACBAgIEOQVQmIUAAghIoGHzsZqEImVmfmRLIv4Qgt/nApwFC0PRnU4j1h3DT7gD0e1/KAABBMIowDdkGHuVNiGAQEUEGrb8ZkXK8UMhH48aaTuuZvZk0yyL4n/3/ytjb70TjocNuP23yYaM4rgWTBFAoAoCAS+SICfgHUj1EUCgegLxwcMsvsqo6m2gRiXfPWF32/bAHezJ9HzbY/q9Nr1lXo227I/NvP9hxv5+TzgeGe2KrtDfbNWVCXJcD6YIIIBAvgBBTr4I7yslQDkIhEKgYfPtAtuORf362fGnHGPfW2+QNWWW3o8zL73Edp5+j32Zag1su0qpeEtL1n53Q9oy4XjWQHvTt9iEn+92DF4ggAACBQT4liyAwiwEEEDAFWio+H05bsnVnb6y2ab2jZ8caDc3LFpuQx8uWWh7vHOvtWbCNbqR39C2NnMCnHnz85cE//2mG/HzHfxepAUIIFBNAb4lq6lL2QggEHiBxNobmvXqE5h2ZGMxu/IHh9i3d9nAPkwv7rDeTzXNtoPff9iy2XDdp+I2uKkpa+ddnrI33w5f+3o2mo1Z021piKY0BQEEEKigAEFOBTEpCgEEwicQi8WsR0BGc2aPGGH7nH6MnT3UrC3b9fVZf5/3nk2a+XToOu2LeVk75+KUffhx+AIcddbGG8R5dLQgSAhERIBmlicQL2811kIAAQSiI9AQgPtyHt55J9vm8F3t39nSnpF83qwX7biP/mNLigiKgtDjs2bnApyLUvbZ3CDUtrw6bspT1cqDYy0EEIiUAEFO6LubBiKAQHcFklttb7E+/bpbTFXWb+3Z08444SibuNlKNi+zpKxt/P6z123rN++wj5aUFiCVtbEqrvTBRxn77SUpC9Pfwsnnamw025ggJ5+F9wgggMByAgQ5y5EwAwEEIiFQQiNjDT2scdd9S1ijNlnfWn9d2+mUw+3aXs3d3uBLzZ/bhq/fZlPnf9TtsmpdQCptdtfUtJ13WdqaW2q99dpub9zYuDUkeXS08R8CCCDQhQBBThdALEYAAQQk0GO3/cziCb30RfrzwRNt3IQt7Y308k9PK7eCC3IjQd955147a8bTlrFg3M/y9rsZO3tKm919f8YU7JTb9iCsF4uZfWuH6v9sB8GCOiKAAAJdCfBt2ZUQyxFAAIGcQHzAIOux/S65V/X9f/6gQXbw6cfaSav2sJZsbgijwtVRaDNl1gu20et/tb/Pe8+3oU7TYrMbb0nbBVekbc7nFUbwaXEbrR+zQQNykY5P60e1EAi5AM0LmABBTsA6jOoigED9BBr3PKR+G89t+Yntx9o2x+5jU63698683vKl7ffuA7b5G7fbvfM/zG3dP/8/+WzGzjqnzf73dMY/lapBTb61vX9GEmvQXDaBAAIIdEuAIKdbfCWsTFYEEAi8QGKVkZbcaMuatyOVSNpvf3SETdh2pM1K1/amkxebP7fx79xn20y7wx5e8EnN2+5ucEmbOUHNuZel7Ia/pG1Rk7skGtPhw8zWW5tRnGj0Nq1EAIFKCBDkVEKRMhBAoGyBoK3Yc/wBNa3yx6NG2q6nH2kX9W+zeo5bPN00x3aefreNee0WO+2TJ+3JpllVv5QtmzWbNj1rf7w5bSdPanMuT3v3/dzMmvaAPza2E6M4/ugIaoEAAoERIMgJTFdRUQQQ8INAcpNtLL7y6jWpyh37TrCxB46zFyr4cIHuVnx663y7cPZLtt20f9hKL99oP/zwMbtv/ocV/Ts7n881u/PetJ3xqza76KqUPfFMxlqXdLfmgVu/vcJ6bPTYrRnFaQfhBQIIIFCEAEFOEUhkQQABBLwCPccf6H1b8deL+va1H/3sR3bU6P62MNNW8fIrVeDsVLNd9/mb9p137rNBL91gE997wKY/lLGZL2VtwYyspTsJTFpazT74KGvPvJCxex7ImC5BO/eylJ18Vpud8es2u+fBjM39slI1DXY5u4yLW2OPWLAbQe0RqJgABSFQnABBTnFO5EIAAQTaBRq239Wq9cdBX9lsU/vGCQfZ/8UXtG8vCC+aMin7eGarvfzXtD2RG3158Jcp+8dxbU6692dt9tDklD16Xsrun9Rmd/64zX6TC2TOuThlf/hz2u68L216mIAuRVtYuSdiB4Gtyzr27WO227f5qe4SigwIIIBAnkDkvjnz2s9bBBBAoGSBmP446B4TS16vsxUy8ZhdeeSh9q2d17cP04s7y+rbZd+Yt9pyddNoTnNuRGb+J1mb+07WFs0xS7Wa9emxXFZmFBDY+zsJRnGM/xBAAIHSBQhySjdjDQTCKECbShRonHCgxVYYVOJahbPPHjHCJpx+jJ09JGupqt/OX7gOlZi79txhRRfTt4HLr7rCGjrEbPttcerKieUIIIBAIQGCnEIqzEMAAQS6EIj17G29v39SF7m6XvzwzjvZNofvak9kFnad2ec5hsxeoega9kwWnbXOGeu3+f33Tlo8N8JXvxqwZQQQQCC4AgQ5we07ao4AAnUWaNhuJ0tuvFVZtWjt1dNOP/Fom7jZSjYv08kd+mWVXp+VYh83FL3hXomis0Yy4xqrx2zTDRnFiWTnB6XR1BMBnwsQ5Pi8g6geAgj4W6D3sWeYNZR2g8lb669rO518uP2hZzDvvSnUIxu2DLNUc/EH5aWJFdpiuOcduj9RYLh7mNYhgEC1BeLV3kAH5TMbAQQQCIVAfPAw63XAUUW35cZD9rdxE7a0N3z0t2+KrnwnGbdfsHonS5dfRJCzvIk75xvbxG21VYoPGN31mCKAAAIIfC1AkPO1Ba8Q8IEAVQiiQI/xB1h81VGdVn3+oEF28OnH2smrNFhLNt1p3iAuXP+LFUuqdjJTUvbIZF6hv9lB+/LTHJkOp6EIIFA1Ab5Jq0ZLwQggEBWBWDxhvX98VofNfWL7sbbNsfvYVAv+wwU6auSKc4p/6IDKiJca5GilCKQffi9hjY2M4hj/IYAAAt0UIMjpJiCrI4AAAhJIrrGONe62n162p1Qyab897vs2YduRNivd0j4/jC8SM3qW1Kx4qqTskcg8bmzc1h7Nz3IkOruCjaQoBBAoLMC3aWEX5iKAAAIlC/Q8+Bhz/3bOx6NG2q6nHWkX9Vti1Ri06P/RF7bNZQ9b4/zm9nomWtpsi2v+bd8+4+/tadhrM9qXK+/Y86Y6yzTVe3fhqEem2Qa3PuO+LWk6PNXH2uaVOPrQVtImQp950ACzA/bhJzn0HU0DEUCgVgLGN2rNqNkQAgiEXcD52zlHnmx/23dPG3vgOHuhCg8XUGCiAGWrqx8zBTVe02RrylpW6GWPTp5gD5+3r71y6Na23t9eMAVEyjfi+Q9t5lajnGWa6r3mq8zBb8+26XtsqLclp+/MG1PyOpnWbMnrhHmFHx6RsAb+QGqYu5i2IYBAjQUIcmoMzuYCIkA1EShToGGbHe2ZbbeyhZnqDFW05oKYx8/Y3Z45bpyley77d2m07LWDtmqfP3/VQdbWM2k9Fywd7ekze4E1Detn+k9TvddrBTtzx6xoWl/vS00bfDm81FUs01ryKqFd4ds7xG2tUfwch7aDaRgCCNRFgG/VurCzUQQQCLPA71b7pm3aa0jdm6jooUz1AAAQAElEQVQRmpjFrKV/L6cuTSv2tz5zlj78QFO9Vx6N4szcvLhHQDsF5f2zypwBeXO6ftsWnj8R1HVjO8mx6spm++3JT3EnRCxCAAEEyhLgm7UsNlZCAAEEOhZoiCXsnrX2sKHJpcFFxzmrt0SXso2571WbsdVIW7DaIGdDCmRGPPO+c0+Ops775z80jeIo2Bk3+W5nme7r0frOSkX802Nm6e3MpMwa4kUUHuIsffuYnXhM0hqSsRC3MjJNo6EIIOAzgYj/xPisN6gOAgiERmBEjz72zzV3t3huJKXWjVKAsumNT5juz3l/p3XaN6/L0XSpm+7X0VQL3FGc1R5/x9747mbO/Tpab7Un3tXiLtMKqUZb8nm8y3yFMvQv7YFshYoI7LxEjuzEYxI2YAUCnMB2IhVHAIEiBOqXJfc1W7+Ns2UEEEAgzALb9F3RLll1u5o20Rvg6P6czjbu3ouTakxaT89T2nQZmxX53x4LRheZc/ls/RuXnxeVOYcflLBRq/MTHJX+pp0IIFB7Ab5ha2/OFksQICsCQRc4cdhGdtDAtWrSDDfA0eVnXQU4ujzNHcXRAww0euNW0n0ggfu+s+lm80Z0trjTZb0j+jSxHcbGbbut4p3asBABBBBAoHsCfMt2z4+1EUAAgS4F/jRyJ9ug59L7YrrM3EUGBSdjz5tqeoR031kL7JvnTm3/+zZ6mIDmrfngG869Ne7fyyn0929G3/eqffSNtdqfqKbHR4++5xXTOhrV+Wi7NbuoydLFq3xWfrt6JZeWEaV/R64Ws0O+y09vlPqctiKAQH0E+KatjztbRQCBCAk0xhP24JjxNrLH0sc3d6fp3vtqdG+NkjtqowcMPPbV38jRfDe5y73b1bw5G6zcPstb7nPH7tD+GOr2DB286PNp7w6WdD27d6LrPGHKMWTw0gcNxOPch1O9fqVkBBBAYKkAQc5SB/5FAAEEqiqwUkMf+8/ae9vwZPlBQVUrWEbhjemEtc4q/2ekR4SO9QfnBrxOOyFp/fqWAc0qCCCAQHcFIrh+PIJtpskIIIBAXQRW7dHX/rP2XjYkGY7Hiu2+aC2zbPmUPcpfNVBrDs4FOKefmLRBAyIU1QWqh6gsAgiEUYAgJ4y9Wvk2USICCFRIYHTPAfafMXvbwETwHy22+ZdfX+5WDk+yGwFSOdurxzqDB5kR4NRDnm0igEDUBQhyor4H0H4EEOiGQHmrrttroD02Zi/rHw/2WMaozweXB/DVWon0Vy9COiHACWnH0iwEEAiEAEFOILqJSiKAQNgENuo92B4Zs6f1igX3EWP9ZnXvBpN4iIOcyAc4YfvA0h4EEAicAEFO4LqMCiOAQFgENu8z1P679t42LNkrcE2KZ83aZnbz8WipwDW7qAqvurLZWadwD05RWGRCIGICNLd2AgQ5tbNmSwgggMByApvnAp0X1v2urdtz4HLL/Dzj24tGWbabIzGZVj+3sLy6bbJBzCadnLT+/XjIQHmCrIUAAghURoAgpzKONSqFzSCAQBgFVu7R155ZZz8b13dEYJq39bzVul3XTEtuOKjbpfingPG7xu34o5PW0ECA459eoSYIIBBVAYKcqPY87UYgTAIhaEvfRIM9PGaCHTVk3UC0Zs25Q7pdz1RLt4vwRQGJhNmxRyRs7z1yL3xRIyqBAAIIIECQwz6AAAII+EQgEYvbdauPs4tX2c78/uU8YFa/bquldblawAdz9Mc9zzgxYVts6s8e63YnUQACCCAQUAG+lQPacVQbAQTCK3DKihvbo2P2shWT/n0gQXpGZZ4KN6B3cPtxlRExO/tnDTZqdX5Kg9uL1DyiAjQ7AgJ8M0egk2kiAggET2D7fiNs2voH2fgVVvdd5cc2rWqZJZWpVv8A/k3UeO6Xc4+d4/aLnyZt4IDKOFAKAggggEBlBXJf1ZUtMBKl0UgEEECgBgIDko1291p72J9G7mh945UZOalEtcfOX7USxThl9AnYTforDjMnuNl3fMJ0L47TCP5BAAEEEPCdQNx3NaJCCCAQWAEqXh2BIwavY6+td6Bt0XtodTZQYqmj5+aO9Etcp6Psvf0Tu3VURWd+LGa2605x+9UZDbbqyrk3zlz+QQABBBDwqwBBjl97hnohgAACHoHVG/vZU+vuZ79caQtLWH0PsofM6e+pWfde9gxAkDN0iNlZpyZt4l4JSybKai8rIYAAAgjUWIAgp8bgbA4BBBAoVyCRC24mj9jSXl3vANu+70rlFtP99T5u6H4ZX5XQ86upHyd9+5gdsE/CfjOpwUauGvNjFakTAgEXoPoIVE+AIKd6tpSMAAIIVEVg3V4D7d9r720PjB5vG/YaVJVtdFToJi0rWqq5cgf8PSpXVEdVLnl+Yw+zCbvG7bxfNtjO4+KM3hj/IYAAAsETCHSQEzxuaowAAghUTmCX/qvaS+vtb9evPs6G1+hx09+cP7JyDciVVLkxoVxh3fxfDxLYafulwc1eeySsZ2M3C2R1BBBAAIG6CRDk1I2eDSNQNQEKjpBA3GJ25JB17Z0ND7WzV9rCelf5KWzrfjG0orrxdEWLK6swPVRgmy3i9tufN9jB+yWsX9+yimElBBBAAAEfCcR9VBeqggACCCBQpkCfXHDzqxFb2scbfc/OGbGVrdzQp8ySOl9txdmV/cMwiZoGOcu37Zvbxu1XpyftqMMSNqS2V/4tXxnmIIAAAghUTIAgp2KUFIQAAgjUX2BQotHOWmlz+yA3snPrqJ1t2z4rVrRSyZkVvoarraLVK6qwwQPN9puQsMvPbbDDD0zYiJV8eGNQUS0hEwIVEqAYBEIoQJATwk6lSQgggEAyFrcDB61lT6yzr7243kQ7dNAY65Gb1x2ZNZcMtCXzKxsQZGsY5Kw7JmY/PjLpPFBg92/HrU/v7miwLgIIIICAnwUqEeT4uX3UDQEEEIi8wCa9hthNo75lszY+wnlIwa79V7WklR6s7DR/VMUtMy3ZipfpLbCxh9mO34jbOWc12Kk/TtqmG8VM9+B48/AaAQQQQCB8AgQ54etTWuQbASqCgL8EBiYanYcU3D96vM3Z5Pt2w+o72m65gKehyBGeDb4cXvEGpVoqXqQNHGC2w9i4/eTopF06pcEOmZiw4cMqvx1KRAABBBDwrwBBjn/7hpohgAACVRNQwPODIevY1FzA88UmP7C719rDThq2kfN3d2IdbHXEnFz00MGykmZ7Mqeac2+6OZijkZm11ojZvuMTNvn0pF34qwY7bP+EbbxBzHo05MrnfwQQQACByAnEI9diGowAAgggsIxA33iDjV9hdbt01bH2ynoH2OyNj7D/W2NnO3rIurZGj/7teXt82qv9dSVfrFBisSvkqrTphkuDGl2CdtUFDXbGiUnbY+e4rTKioxCtkjWmLASqI0CpCCBQOQGCnMpZUhICCCAQCoGhyV52wMC17A+rj7N3NzzE9KQ2Xdq2ygYJG7pOzHpWeECnX2PHbHoS2jqjY7brTnE77sikXTC5wS7+TYP9+KilQY0eJqD7bjougSUIIIAAAgEXKKv6BDllsbESAgggEB2B1Xv0M13attmhCdvhp0kbf1GD7XN1g33r50nb+uiErTchbqtuGbchY2I2cPWY9c+NpvQdZtZroFmPvmbJnoWtGnMjMiusErO114rb9tvFncvN9PdqNCpzfi6Yuf7yBtP0p8cnbeJeCdtso5gNypVZuDTmIoAAAggg8LVA/OuXvEIgpAI0CwEEKi6Q6GE2cGTMVt06buvlApCtj0nYuNOS9q1fJG2XXydttykN9p0LG2zPyxps76sa7LvXL58mXNJgO09O2kHfS9j3Dkg4l5tts0Xc1lojZhrBqXilKRABBBBAIDICBDmR6WoaigACCCwrwDsEEEAAAQTCKkCQE9aepV0IIIAAAgggUI4A6yCAQAgECHJC0Ik0AQEEEPAKLG5usR+cfL6T9Nq7rNDrSedeZzfcet8yiz6bO892O/g0W3/cEc5U790Myqt13PdMEUAAAQSiIBCsNhLkBKu/qC0CCCDQqYCCmuMnXW5Pv/hmp/m0UMGKgpi7Hnhcb5dJ/3zwCZs4YZy9/tiNzlTvlUHBzuPPvGon/3Ci3pIQQAABBBDwpQBBji+7JZyVolUIIFB9gXMuu8nGbrWhnXLM/l1u7MiD9nCCmL12Hbtc3nc/mGFrrL6SM19TvdcbBTsqf+jgAXpLQgABBBBAwJcCBDm+7BYqhQACERKoWFPdS8gUvHS30DVHrmzvffipU4ymeu+O4uy5y3bOfP5BAAEEEEDArwIEOX7tGeqFAAIIlCCgS8+UfcqZR2vS7aRA5va7HzNdzqap3rujOHM+n2fbjj/OWaZ7f3SJXLc3SAEILCfADAQQQKB8AYKc8u1YEwEEEPCNgC4n0701CkqULrn2Nue+HN2fU04QosvR7r/lAudyNk3VUN2Lo2Dn5r8/ZFMmHe0sGz5skN165yNaTEIAAQQQqIUA2yhKgCCnKCYyIYAAAv4W0AiOHhLgJt2Ts/Wm69pVU0603r16drvy7ihOn949bdacL9rL02Vs7W94gQACCCCAgE8ECHJ80hE1rAabQgCBCArofho9Etq9rK0UAq3rjuIoYNLojbu+RpDc10wRQAABBBDwiwBBjl96gnoggECdBaK3eQU8urRNl7np8jbdZ/P6Wx8sB3HpH263wybuYrqETQv1+Ojzr7rVuSdHozoH7b2TZpMQQAABBBDwjQBBjm+6googgAAClRPQE9b+eOnp7ZeqKUDRvTWa725Fr19/7Ebn3hpNn7znalt/7ZHu4vapLoXbcbtN29+7ZWkd7zbaM/AiXAK0BgEEEAigAEFOADuNKiOAAAIIIIAAAgjUV4Ct+1uAIMff/UPtEEAAAQQQQAABBBBAoEQBgpwSwSqXnZIQQAABBBBAAAEEEECgGgIEOdVQpUwEEChfgDURQAABBBBAAIFuChDkdBOQ1RFAAAEEEKiFANtAAAEEEChegCCneCtyIoAAAggggAACCPhLgNogUFCAIKcgCzMRQAABBBBAAAEEEEAgqAIEOUHtOeqNAAIIIIAAAggggAACBQUIcgqyMBMBBBBAAAEEEEAAAQSCKkCQE9Seo94IIIAAAvUQYJsIIIAAAgEQIMgJQCdRRQQQQAABBBBAwN8C1A4BfwkQ5PirP6gNAggggAACCCCAAAIIdFPAN0FON9vB6ggggAACCCCAAAIIIICAI0CQ4zDwDwK+FaBiCCCAAAIIIIAAAiUKEOSUCEZ2BBBAAAE/CFAHBBBAAAEEOhYgyOnYhiUIIIAAAggggECwBKgtAgg4AgQ5DgP/IIAAAggggAACCCCAQFgE8oOcsLSLdiCAAAIIIIAAAggggEBEBQhyItrxNLtUAfIjgAACCCCAAAIIBEWAICcoPUU9EUAAAT8KUCcEEEAAAQR8KECQ48NOoUoIIIAAAgggEGwBao8AAvUVIMiprz9br7DA6299YNuOatlA2wAAEABJREFUP87WH3fEMunRJ16s8JbMPps7z3Y7+DSrRtkVr2yBAlVv1V/tcBcvbm6xH5x8vk069zp3Vl2mTz2XsbumpotOsz+rSzXZKAK+E2j5x03WctsNRSffNYAKIYBA2AVq1j6CnJpRs6FaClw15UR7/bEbnaTXx0+6PLDBSC3devfqaX+89HSbcubRtdzsctt6+vmM3X1/8WnOZ9nlyqjUjELBYKXKLqYcBZ4n/OIKUwBfTP5q5lFdFATfcOt9JW+mO+uWvLECK6gfdfJD0wKLazZLdjoRU63+dIKcv/3JWopM1ry4Zm0vZkPqn/yTL8WsV6s87n6sz4Fe12q7ldqOTmD52bdS7aQcBCRAkCMFUnAEyqjp1puua0rvffips7Z+mPQDpYMNZ8ZX/+jLX0lvvXk0TwdHSnrtLj/9nGvt45lzTAGUlrk/HCpX5asM5VXy/nBrvpYrn8rzrqu8mq95btK6mt9RUn6Vd8/DT7aPXmkdzXfL0NR7YKUDrElTrnPqP26/k5z1VBdv3dztufNUhpLbTnd5GKb5bVQ75eGHtr3/0SynGqNWG+5M+ad0AY1WXn3jXXbbtZNtx+02Lb2A3Br6PBXaJzRP+4uSPne5rM7/+oxN/OFkZ8TXmfHVP0cetIcddch4u/nvD301J5gT9zOT/30ga81T0ms/tU59pdSdOt165yM2fNgg52SQTgp5y1LZ2k+884p9rfW0fn5+7VPat5TyTbWP6Xtdy/QboD5x11dZKtN97051AmuzDUfbPx98wp3FFIHQChDkhLZraZgr0LS4xWbOnuu+LWl6ybW32c47bOGMCD12x2X2wqvTTT8c+nE7/+fH2KojhplGijRqdP8tF9jQwQOKLt9btruuyn78mVft2anXONvUQZmCEf3QdVbw0y++aU88+5qzjuqy41cHcm7dNE8HVqf+6mrnoGv9tUfalElHO/VXu7RcP37529CPpoI4/agrj9LECePssJ9MccrJzx/E9zoQ2/fIs50DF7XPTX379PLF6MlTL7xhG6+3lmmf68y3FstUB4306UC9Ftur1DZ0QDd61Mqm/b6zMvX50wGjDhDdfPrsaZ4+r+48d6qDzEWLm53Pqz6r/5j6X9NnRssVxBx3xF4FvxMO2nsn03paX3mDnPQ5ka/bBr3WPPd9mKb6rrj/0WfskH13XqZZ7n5z1wOPLzO/mDdd7V/nXXmLE5zre0nfvTq55t3H9D2uZfqO1u+Atqn9SvuX9jO9z08n/3Ci6XdG7clfxnsEwiRAkBOm3qQtBQX0o6sFe+6ynSYlpVOO2b/9zK8CGP3I6MfB/ZEpqbC8zN6ytUg/OPoBPfXYA9oPaHVQtuPYTe2hfz+nLB0mjVT9/KTDllmuA1E32NECtb9f39425/N5eltU0ijCjFmfL/OjrnK08mtvva9J4JP2jxErDrZ8v0knHFrwoFh9r7OmOvBV0gGOi6DXmucmHcC4y3TgrMvO3HV1BlYHI+7yQlNt6+U33rFtNluv0GJnnspQWe42NXW3q20qORlz/2i+tq9yc2+de6+UX0llqCzNVyrUFq2n9bVMeTpKbj6Vq+Stg3cd1UfLlfLPUmsdzVfy1k2fE+W9/Po72u+/U520TW/Z7mvN12dWJyvceflTd1uarwNGb8Cvz5Dm6fOq5d40Z+6X1rd3L+fzOmzIAFu0qNl0UkWOOsjU59Kb332tYFGBqwJYd15QpxPH79B+wKy+kbXm5bfHNVZ/6rV3ufYnzVfSSRXvMvWf+lfLlJRXy7Ut737g7iNarnxuevSJF5XduVxZQYiSlqlMla2k15qnpPWdFQr8o++8frmTH/mjqvqu1T6y165jC6zV+azO9i/tH5tvNKb9e0jfA/o+1vey6q19bNjggc4G1hy5srlXKyjA3mf3bzr7pbMw7x/9likoUnvyFvEWgVAJEOSEqjtpjCugH0r9YCm9+8EMc0dK3OXlTtdYfSVnVEgHMuWW0dF6Cj4+mTnH9j9msqnebtKPckfrdDVfBxNuObos7Y23PzAdmHW1nrtceVcePsS8P+r6gdTlDu4Pqps3iFMdKOigbOxWG3Z4QOBtl/Jr39IBgg5qNAqmwFQHtVq2qGnpWX0t00Hx+VfdusyI17MvTjMFsVqukbWLr/lr+5l/73bc1zqY0Wuvv967SQd6Gp1TWSpT9dHooru8s6nWdduhdRVMu/VRe9QuladlGqWY89mXnRXXvkwOXiOtv/F6ay7XTgU4GqVU2cqjEwiX/uF2p5zO6uZkyP3z8uvv2L9uv8QZRcm9NV1GpGl+cj+rG6w9aplF2oYOkvX5UACkOuhgdZlMXbzRAaYONNVmfX779u1lfXr3dC5F6+wgU8Xqu0T7ntbV+6CmYUMHmj4/Olmgg2a91jxvexQ4zJrzhdNXGqXWa303KY/2g+tvvqd9tEKjz5qvJBvvvqT9Uful9k8tV3L3gyfvudr5nuroM6hgQkGIkvpaI5Jav6vylcdN+s5T+xSkuvOqOdVvl7d8BdKxWMz5DlcdFGDrO1p5lFf7lGy0T3YUYCuvkoKirk6eKR8pzALhbxtBTvj7OJIt1A+lfsh0oKkgQT+kQYDo369P+4+96u8m75nlYtrhHsDpYEIHFSpHBwjFHgAXs40w5dHBQTHtUdChM6nu5SoK+HQZlM646qDjxKP2aw+WdNY1m82aDn7dshVIaHRO74vZps7I6oy/ytY6+UkHlZrnjq7pdbFJdT/hyP3as+tAX5d1KijQgZMCbrfuqvMBe+3UnrezF/lGyqt189ugAyyvh7ymvz/DCQo7q5vKUzps4i6OtcpVsKZ5hZLa4AYfhZb3z41uKlgptKyreXLRgeaWux/rnJxQYKP26yBTQZU7QuCOMnjL0zZVL++8oL7W/qfg46bbHzS99rZDgYqCOTc4UH+p73Tpr76n8vcD77qy7Ojz5uZTWSpT7zXt6jOofG4qpnw3r6ZuIKHXtUoKRjralr6HdKJAgbq+6xXY6DtD+6EuXdN8JTeg9JZTzPePNz+vEQiiAEFOEHvNB3UOShV0ZlZn7vRDoDNc3a23zuTp0iadrVXS6+6W6a7vPUvnzit3qgO7frmDN903pB/+QuXoIEt5Ci1z5ymPDjJ0MODO04GJDlDC9COpfnXb19V0wcIm54BWBw9KCqLddbSP6YBW8zUip7zuslKnctZBvw7+O1tX+6D2xc7ydLRMwb/qqqQz2m4+nfXW6JDaoGUa8VB93OXuVAdPWq7kzaNgXfuzm6+jqey0rpK2tXDR10/66qhuHZVV6nwFUhrh1WiQRrBUB404lFqOTkDoJIKS3NyDTAWgCr40X/dNaBs64C+1/CDkl6WCfQUyel2ozt7vC32veL97OjuQ12dI+4b6R0n7TKHy3XmlfgZLLd/dTjFT9bcb6KrubtJ3hOpZTBkKrDrKpyBbI1jaxzQype9pN8DW/WEaJdVJLgVA+jx1VA7zEQirAEFOWHuWdrUL6F6LdUevbrqsRwdqOujXwYfOLupHSBn1A1DMj6cuq/CeOdS6OhOpqZv0Y/7m9A9NPziap23qsiW97izp4ECXgeUHZKpbOQdfOhOvAy13m7oUSE+Dc99rmp9H87xJl0npcjUduLnzdVmKXutMtaZBTtoXdGDm3Re6as8qI4aZRsV0YOEmBdM6aDnq1AucBzpovg4wdLDfVXkdLVffFbr+Pz+/O/qSP7+r99qvtK+pnqqvRj+96zhtemzpY9i1X2r/8S7Xa+8BvgIG7cOarwNHBdp63VnSSKu27Sa3jK7q1lmZ+csUbLn3yuQv03vtAzpAVB30XgeiCt70utSkfUAHmTqj7g2cdVDft2+vUosLVH7tC9pnOqq010Mjhd6AtrMD+Y4+b4W2I/+jTr2gpM9gKeUrGPO2o1AdvPO8+5b2LzcpMFGA4s1b6LW2552vz5RGh7U/eee7r/U9rVEcvf909heaOKOd+r1z3vAPAhETIMiJWIdHsbn6odEBnM5473n4JOeJWQp8ZKHLTHRQo0BFIz6a5016opKWK+lsos7I6myt8qhc3V/x6OMvOvfQ7Hbwac6lNlruPQuuJ5F9/4DdtEqXSQcK3nW1XR2IdnU2P79g/YCqrjo7rzKUdF+E93I15dG23DyFDuzURtnpTKDKULr97sfspisnFXxqVH49gvBel9fMnD3XzrnspmWqO+WKvzj7inemgj4FHt4Dfh1Y/fWuR5xsCmrcAxAdyOlg31lQxj/aJxWAqQ86Wt0NNBUQKY+m3kBWB0nqOwXzSrqcSPnctEL/vqYgQO+9B28KMpQ0v9QkI29grO3qIQGaesvS5XE6aSA/d/4VN9zhfIb0vqO6aVkpyR3lkk1X6+kgXQei+hx2lbfQcvcgU32mkx1unjlzv3QeSuC+11SXOHZ2KaLyhCHJQvvx7Xc/5vSt9gPthwqcFRRrP9DIsE4Gqb3a7zVV0r7U2edNefJTZ59B7+dB65Vavvq0lBMi2kZ3kr73n3/l7fbvIe0z+myp3vnl6nPkBtja51dacZCTRd76DnDeeP6Rs+w9s3iJQOgECHJC16XRbpAO3HWWTIGGV0I/tDpbq2XK477XAY2SDmrc5F0v/0xzfrkqS2WqDPcstNZ3D5bc+bonwV3ublt5lDc/ab7Wc5PK13by87nvlV9tU7nuPE1VV7cMTd06aL6WK2ldLVNS+1WGytJ8LVdy5ymPktsOLQtD0oHW32/4telAQEGcm3QDc767LPKDPp053mCdNZwnIOlJSAqGVYYe/aoDrnKMdMCn+igA62x91f304w9q/1tNOnj0BrLuI2QVzH9r4im28fprtRen0QYF/nogheo79ZGn25cpUFNwrflKqot7YqA9UwcvZKTLJHXgqnW17dmffeGcUfauov1QgbjrpbzajtrUWd28ZRTzWvXRQbYO6rz5dVCoy4a03fzkDfgV7Gm5TnhotFevNc9bll6789QuvXenyi9LnRBRXbRMB55dPTVP+cKS9H2ioEb7mvYHjSzo+0btk5O7TFaa5yZ5dfR5c/N4p/q8dvYZ1OdJJzRUB11GpnVLKV8nFRY2NbeP0mt9JY20q+7aP7SfaL/S/qVlXSXtN1pX62l9vdY8raf2nPGTg9svj9UJJn225KLlbtL+dO1f/mnHHLqn8znTcr3Wd5PaKm85u/nd7xe1x53HtI4CbLpqAgQ5VaOlYAQQCIqADgoU3CmIc5P3IMwb2OXn9QahWsdd/4FbLzQlHajIQcuU9FpJBx3apsrTe2/SqIMOTHTA751f6LXKcbepAyBvHpWtbWi56qmbsvVe85X0WsuU/nbdr9qfQqg6K7/mKymf8ivptQ5avdvJf616y0zrKqndhdb11t3Np7LcvJqn5K2bW7bWVV4lld9ZnXRwq/ubvAee+W3Udtyk8lSukrbjznenmqdl3qR53vW0TO+1jiy1Pc1T0pPg9MAC7zzND1Jy+0jtzq+35qn/1VfuMnApTvcAAA4XSURBVNdCHnrtztdU7zVfSa+967rb0TIl11JlK5+2pTLcpPWVT0mfPyXX2V1Hy7Qfq2wlvdY8Jbd8tzzvVOvvtuNWztPzvPO172ldN3VWhnc9vVb93fXcqeZpmZJeu/PVXtVB871JbbjiNyc4J1rc+Wqz6qF1ZeLO11Qj0Qr8C5Wl5SQEwiJAkBPsnqT2CIRSYOvN4zZht+LTsKGxUDlo1IFLSSrXpTqY0x/m1KiRe5a8cqWXVpLO+usyPT0Zq7Q1i8vdc5/DrOd3v190sl69iyuYXI6ARkc14viDk89f7rHoTgaf/6NRSo2yKvD3eVWpHgLdFiDI6TYhBYRRQGfGdHZPZ+jC2D6/t2mbLeK21+6JotOKQ/NbFOz3OvOqM7iltkIH8zrbW866pW4raPllorPamtaz7vpO0Rl2nWmvRj16KsjZ/0jrWWSqRh3CXKb726DfB70OWlv13aLvCH1XBK3u1BeBUgUIckoVIz8CCCCAAAJBFaDeCCCAQEQECHIi0tE0EwEEEEAAAQQQQKCwAHPDJ0CQE74+pUUIIIAAAggggAACCERagCCnIt1PIQgggAACCCCAAAIIIOAXAYIcv/QE9UAgjAK0CQEEEEAAAQQQqIMAQU4d0NkkAgh0LnDzF2/b5JnPFp2mt87vvECWIuAzgWpVZ9p9GXvjrnTRqVr1oFwEEECg3gIEOfXuAbaPAALLCdzyxXT71afPFZ2mt8xbroxKzVjc3GI/OPl809836arMz+bOs90OPs1K/Vss+iOV244/rqhtdFUHvy2Xm9qmNvqtbmGsz7R7cwHO3blAp8iUagmjQtdt0v6o/VL7Z9e565dD9VM9Vd/61aKmW2ZjCFRMgCCnYpQUhAACQRTQQYSCGAUzbv11QLHrQT8zTd151Zpquxdf81ebMulo099Q8W5H25/4w8mm4Mk7X+uozuuPO8KU1Abv8s5eq0wdNGk9JZWj8tx1tC0Falqm5A3YvMuUR+/d9VQH/aFB9707VZuOOmS83fz3h9xZTEMioP7XfqD9xE35+5Mfm6r9vaPPXFf17erzo7Jl4Hroc+GW6fWSm967y5SPz4+rwRSBygiEK8ipjAmlIIAAAu0C+oN/+sN/Olhvn1nBF0+/+KZT2tabrutM9Y8OfnQQtP8xk23hosWatUw657KbbPiwQaY/bvnYHZfZ7Xc/VvTo0VMvvOEEVFpXSeWoPG1AB2inn3OtTZwwzin7tmsn23lX3tIe7P3zwSfalymP3ms91ffxZ161k384UW+XSwftvZMtWtzcXs5yGZgROIH8fUX7ktJao1a29z+a5ev2FPrMFVvhzj4/KkOfJX2mZJH/2dTnRZ8bLdNU77UOnx8pkBCovABBTuVNKREB3wlQofIFdDCnM7M60+qWotfumVpNFZDoQMVd/sob75k7WqJ1VYa7LH/60L+fs7FbbWgKptxlQwcPsPtvucAUZPTr29ud7Uy1nenvz7BD9t3Zea+8m2042lSOM6OLfxSs7bjdpu251hy5ss2a84Wpjjo4XdjUbHvusp2zfNRqw23l4UNMB3aa8e4HM2yN1VfSS2eq93qjgzW1QXXR+/yktm283lrt5eQv533wBLSvzJj1uW2z2XrLVH7SCYfa+muPbJ/n/ay4nwXtw/rMeEcJNULiHT3VMn22lJRX66hQladyTvjFFaZl7uiHliuf5ilpfeUvlPRZ0f6q/dJd7q6vsvVZcOfnTzv7/KiMzj6b+rzw+ckX5T0C1RMgyKmeLSUjgEAIBXTwdP3N9zgBiM7IXjXlxOVa+fLr79i/br/Enp16jbPs1jsfcab5/+hgSiMc+QeK+fm87+d8Ps8WLGzyzjJvoLLMgi7eaPsagdGZZx3wzZn75TIjR5qnZTo4U1HaznsffqqXpqne68BOZbiBkbOwwD86uFM+bbPA4nrNYrtlCgwbMsBisZjpsq+O+lQBiUYZNaKhz4r2JX0WFAxrJEPBhrt5BdKbbzTGCZD0GZs05br2z5jyXvqH292sppGYfXb/pjPaOOXMo53LOQ/7yZT2UUadHLj6xruc+e0rffVCdS30mVOddGLh1GMPsG9NPMW5D095v1qt4ETLtU+rXfqsdPXZ1OdFnxsVpqne8/mRBgmB6ggQ5FTHlVIRQCBAAjpo2nL3Y50zwzoLrMvE8gMJtzk6MNtx7KbOwZg7L3962MRdnJEZHfjoACh/ufu+aXGLLVrUbDpgdOcVM+3fr0/J61jefzoDrjZr9s9POkwTJ41YcbD16d3TeZ3/jwIZHbTKSFO9d0dxdIDX2ejVsMEDrW/fXvlF8j6gAgoKbrpyks2cPde0H2mfUFJgoya5AYACFOXVvJ132MIUFGiZAnuNeuggX+81X8uVL/8z5s2r5bq0U0mvlV57631NlhmB7Nenl7nznYVf/dPVZ06jUE/ec7XpM6x26XPy1arLTDRfyzXT+/np7LOpz4s+N3LSVO/9+/lRy0gIBFuAICfY/UftEUCgAgI6YNKoi842K+lMsA5WOipaZ2A7WlaL+QrAFFQU2pYOvnQQlZ90dtybX2fA1VZdtnP8pMudy9W0XAetOhDU6/ykg1Wd7dZ6mmq5Dk51sKYHC+jhCVqmwE5n7LWcFF4B7/6gfteo5iXX3rbM/WF67+6L2s9cDV0K6QYiuvRNl0lusPYod7Hd9cDjy5x0KHRvWnvm3IuPZ86xcfud5Kyj4EMnLnKzy/5fl3SqTQq8Cl3C1tHnp7PPpteLz0/ZXcOKCBQtUJUgp+itkxEBBBAIoIB7+VZ3q64RE41udBSwFCpfoz75AZjqo8BCI0fuwZcO0LxJB22FytNZch1gKrDRaEs/zz1AOsOu+3U6Curcs9Bqh/K55XeU313ONJwCOlmgpEux3BYq8PHuh3qIh/ZTJQXYGrXRpWq77biVKQhw1zvlmP2dy9HcdRUUeJe7+dyptus9UaH1Cu3z2lf75kYUu/rM6aSAgjPVz62zuy3vdJnPz5AB1tln07ueXvP5kQIJgeoJEORUz5aSEcgX4H0IBHRm94VXp7df76+DoHKbpQO9vr17lXRDvg70Ro9auf2RzLrcR/VRvYqpx5Qr/rLMU840AqMz6jr4c8+u6+BLZekMe6Gby7VM23VHcdQOBVmar6SgS1Nv0oGsHj6gvN75vA6mgB4UoJv/FQi7LfDuL+pnBTHnX3Vr+2dFeS+//o72UUONAOqStamPPL3MAwy0L+u+N23DLfuKG+5oL8ed5041AqQRSO/ooYIUJTePO1W9OvvMaZu67PKm2x907qnTSQN3XU07+/yU8tnk8yNNEgLVFSDIqa4vpSOAQMgEdHZYTzNzL41R8zq7j0XLO0s6oFOwoANAN58OgPSkKN0b9MbbHziX4egyNHe57gHQyInONKseuu9B9XKXdzbddov1TeVqXSWVo7PtOvhTOv/nx5juF9Ay5TvjJwcXvP9IN4LrvgUd2FnuPz0+Wge0Wk9lHrT3Trm5S/9X215+451lDmSXLnH/ZRo0AQXEup9Ml4apz5WOOvUCu2Tyj9v3Fz2JTPum9lEtV96+fXo596upvdp3FLD3z40eqjzNU9K+rEsftf9pPSXtU8qv5flJ83V/kLvfKr/2RQU/+Xn1vrPPnB6koIeGdDR609nnR2UX+9nk8yMtEgLVFSDIqa4vpSOAgM8FdCCWf0Cjm48fuPVC52BNB/5arnxuU3R2V5fDKOnSLI1iKJ8OtnRZjQ7SvHm967rz3akus9Fr7z0Ebjkq303apvIpaVuqk7uss/KV35tUN3c9TVWOynPz5G9b+d1l3qnq413mXS+/TJ1h19lzuXrL4HVwBbTPqJ+1D7lJN+zn97H2TXe5pnrvbbX2I5Wj8rzztW8pv5uUT8u1fqH83v1P6+hzqHlaJz919pkrVLZ3/fx65edXOzRPdVBSfb3ru6/VHpXlvlddVWeto/VVjrMs9w+fnxwC/yNQhgBBThlorIIAAtUVOHjQaPvlSlsUnUb3HFDdCnlKd0dZdLZYSaMwOnvryVLSSx3M6LG1emSu+2SqkgrweWa1SZceHfLV3/XxeXUDX711vpOw9SbEi07JnoFvcskNCNJnjs9Pyd3LChERKKaZBDnFKJEHAQRqKnDIoDE2ecSWRafRjSvUrH7eM66FzrqWUxGd/dZZ8I7O+pZTpl/WUZvUNrXRL3UKcz3W2SMX4OyVC3SKTGG26Kxt2h+1X2r/7CxfvZepfqqn6lvvurB9BIImQJATtB6jvhUQoAgEEEAAAQQQQACBMAsQ5IS5d2kbAgggUIoAeRFAAAEEEAiJAEFOSDqSZiCAAAIIIIBAdQQoFQEEgidAkBO8PqPGCCCAAAIIIIAAAgjUW8DX2yfI8XX3UDkEEEAAAQQQQAABBBAoVYAgp1Qx8ldOgJIQQAABBBBAAAEEEKiCAEFOFVApEgEEEOiOAOsigAACCCCAQPcECHK658faCCCAAAIIIFAbAbaCAAIIFC1AkFM0FRkRQAABBBBAAAEEEPCbAPUpJECQU0iFeQgggAACCCCAAAIIIBBYAYKcwHZd5SpOSQgggAACCCCAAAIIhEmAICdMvUlbEECgkgKUhQACCCCAAAIBFSDICWjHUW0EEEAAAQTqI8BWEUAAAf8LEOT4v4+oIQIIIIAAAggggIDfBaifrwQIcnzVHVQGAQQQQAABBBBAAAEEuitAkNNdwcqtT0kIIIAAAggggAACCCBQAQGCnAogUgQCCFRTgLIRQAABBBBAAIHSBAhySvMiNwIIIIAAAv4QoBYIIIAAAh0KEOR0SMMCBBBAAAEEEEAAgaAJUF8EJPD/AQAA//+tpOrrAAAABklEQVQDAGIsz1Q2frUOAAAAAElFTkSuQmCC"
|
||
},
|
||
"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 (1–10%)\",\n",
|
||
" \"High (10–30%)\",\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": [
|
||
"## AUM–FLOW CONSISTENCY & DISCONTINUITY DETECTION"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"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": 10,
|
||
"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_abs</th>\n",
|
||
" <th>max_gap_rel</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>59545</th>\n",
|
||
" <td>200127410</td>\n",
|
||
" <td>FR0010135103</td>\n",
|
||
" <td>384</td>\n",
|
||
" <td>436</td>\n",
|
||
" <td>0.880734</td>\n",
|
||
" <td>295985.42</td>\n",
|
||
" <td>3371.158214</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Registrar Account - ID Product - Isin n_ruptures total_obs \\\n",
|
||
"59545 200127410 FR0010135103 384 436 \n",
|
||
"\n",
|
||
" rupture_ratio max_gap_abs max_gap_rel \n",
|
||
"59545 0.880734 295985.42 3371.158214 "
|
||
]
|
||
},
|
||
"execution_count": 10,
|
||
"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)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 25,
|
||
"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": 27,
|
||
"id": "e5d7a5ab-40bd-452d-a6ae-d56e220c592f",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"ename": "NameError",
|
||
"evalue": "name 'plot_isin_dynamics' 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[27]\u001b[39m\u001b[32m, line 63\u001b[39m\n\u001b[32m 58\u001b[39m plt.show()\n\u001b[32m 62\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m _, row \u001b[38;5;129;01min\u001b[39;00m sample_isin.iterrows():\n\u001b[32m---> \u001b[39m\u001b[32m63\u001b[39m \u001b[43mplot_isin_dynamics\u001b[49m(\n\u001b[32m 64\u001b[39m df,\n\u001b[32m 65\u001b[39m row[\u001b[33m\"\u001b[39m\u001b[33mRegistrar Account - ID\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 66\u001b[39m row[\u001b[33m\"\u001b[39m\u001b[33mProduct - Isin\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 67\u001b[39m )\n",
|
||
"\u001b[31mNameError\u001b[39m: name 'plot_isin_dynamics' is not defined"
|
||
]
|
||
}
|
||
],
|
||
"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",
|
||
"sample_isin = rupture_isin_summary.sort_values(\n",
|
||
" \"rupture_ratio\",\n",
|
||
" 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",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 28,
|
||
"id": "aef8ceb9-28a6-4908-ae24-a88d85b64309",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"ename": "KeyError",
|
||
"evalue": "\"Column(s) ['rupture_flag'] do not exist\"",
|
||
"output_type": "error",
|
||
"traceback": [
|
||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||
"\u001b[31mKeyError\u001b[39m Traceback (most recent call last)",
|
||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[28]\u001b[39m\u001b[32m, line 6\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# 1. Aggregate rupture rate over time\u001b[39;00m\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 4\u001b[39m time_stats = (\n\u001b[32m 5\u001b[39m \u001b[43mdf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mCentralisation Date\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m \u001b[43m \u001b[49m\u001b[43m.\u001b[49m\u001b[43magg\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 7\u001b[39m \u001b[43m \u001b[49m\u001b[43mtotal_obs\u001b[49m\u001b[43m=\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrupture_flag\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcount\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[43m \u001b[49m\u001b[43mn_ruptures\u001b[49m\u001b[43m=\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrupture_flag\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43msum\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 9\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 10\u001b[39m .reset_index()\n\u001b[32m 11\u001b[39m )\n\u001b[32m 13\u001b[39m time_stats[\u001b[33m\"\u001b[39m\u001b[33mrupture_rate\u001b[39m\u001b[33m\"\u001b[39m] = (\n\u001b[32m 14\u001b[39m time_stats[\u001b[33m\"\u001b[39m\u001b[33mn_ruptures\u001b[39m\u001b[33m\"\u001b[39m] / time_stats[\u001b[33m\"\u001b[39m\u001b[33mtotal_obs\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 15\u001b[39m )\n\u001b[32m 17\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 18\u001b[39m \u001b[38;5;66;03m# 2. Smooth (optional but recommended for readability)\u001b[39;00m\n\u001b[32m 19\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/groupby/generic.py:1432\u001b[39m, in \u001b[36mDataFrameGroupBy.aggregate\u001b[39m\u001b[34m(self, func, engine, engine_kwargs, *args, **kwargs)\u001b[39m\n\u001b[32m 1429\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine_kwargs\u001b[39m\u001b[33m\"\u001b[39m] = engine_kwargs\n\u001b[32m 1431\u001b[39m op = GroupByApply(\u001b[38;5;28mself\u001b[39m, func, args=args, kwargs=kwargs)\n\u001b[32m-> \u001b[39m\u001b[32m1432\u001b[39m result = \u001b[43mop\u001b[49m\u001b[43m.\u001b[49m\u001b[43magg\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1433\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_dict_like(func) \u001b[38;5;129;01mand\u001b[39;00m result \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 1434\u001b[39m \u001b[38;5;66;03m# GH #52849\u001b[39;00m\n\u001b[32m 1435\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m.as_index \u001b[38;5;129;01mand\u001b[39;00m is_list_like(func):\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/apply.py:190\u001b[39m, in \u001b[36mApply.agg\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 187\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.apply_str()\n\u001b[32m 189\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_dict_like(func):\n\u001b[32m--> \u001b[39m\u001b[32m190\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43magg_dict_like\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 191\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m is_list_like(func):\n\u001b[32m 192\u001b[39m \u001b[38;5;66;03m# we require a list, but not a 'str'\u001b[39;00m\n\u001b[32m 193\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.agg_list_like()\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/apply.py:423\u001b[39m, in \u001b[36mApply.agg_dict_like\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 415\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34magg_dict_like\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> DataFrame | Series:\n\u001b[32m 416\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 417\u001b[39m \u001b[33;03m Compute aggregation in the case of a dict-like argument.\u001b[39;00m\n\u001b[32m 418\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 421\u001b[39m \u001b[33;03m Result of aggregation.\u001b[39;00m\n\u001b[32m 422\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m423\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43magg_or_apply_dict_like\u001b[49m\u001b[43m(\u001b[49m\u001b[43mop_name\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43magg\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/apply.py:1603\u001b[39m, in \u001b[36mGroupByApply.agg_or_apply_dict_like\u001b[39m\u001b[34m(self, op_name)\u001b[39m\n\u001b[32m 1598\u001b[39m kwargs.update({\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m: engine, \u001b[33m\"\u001b[39m\u001b[33mengine_kwargs\u001b[39m\u001b[33m\"\u001b[39m: engine_kwargs})\n\u001b[32m 1600\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m com.temp_setattr(\n\u001b[32m 1601\u001b[39m obj, \u001b[33m\"\u001b[39m\u001b[33mas_index\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mTrue\u001b[39;00m, condition=\u001b[38;5;28mhasattr\u001b[39m(obj, \u001b[33m\"\u001b[39m\u001b[33mas_index\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 1602\u001b[39m ):\n\u001b[32m-> \u001b[39m\u001b[32m1603\u001b[39m result_index, result_data = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcompute_dict_like\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1604\u001b[39m \u001b[43m \u001b[49m\u001b[43mop_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mselected_obj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mselection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkwargs\u001b[49m\n\u001b[32m 1605\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1606\u001b[39m result = \u001b[38;5;28mself\u001b[39m.wrap_results_dict_like(selected_obj, result_index, result_data)\n\u001b[32m 1607\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/apply.py:462\u001b[39m, in \u001b[36mApply.compute_dict_like\u001b[39m\u001b[34m(self, op_name, selected_obj, selection, kwargs)\u001b[39m\n\u001b[32m 460\u001b[39m is_groupby = \u001b[38;5;28misinstance\u001b[39m(obj, (DataFrameGroupBy, SeriesGroupBy))\n\u001b[32m 461\u001b[39m func = cast(AggFuncTypeDict, \u001b[38;5;28mself\u001b[39m.func)\n\u001b[32m--> \u001b[39m\u001b[32m462\u001b[39m func = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mnormalize_dictlike_arg\u001b[49m\u001b[43m(\u001b[49m\u001b[43mop_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mselected_obj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 464\u001b[39m is_non_unique_col = (\n\u001b[32m 465\u001b[39m selected_obj.ndim == \u001b[32m2\u001b[39m\n\u001b[32m 466\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m selected_obj.columns.nunique() < \u001b[38;5;28mlen\u001b[39m(selected_obj.columns)\n\u001b[32m 467\u001b[39m )\n\u001b[32m 469\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m selected_obj.ndim == \u001b[32m1\u001b[39m:\n\u001b[32m 470\u001b[39m \u001b[38;5;66;03m# key only used for output\u001b[39;00m\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/apply.py:663\u001b[39m, in \u001b[36mApply.normalize_dictlike_arg\u001b[39m\u001b[34m(self, how, obj, func)\u001b[39m\n\u001b[32m 661\u001b[39m cols = Index(\u001b[38;5;28mlist\u001b[39m(func.keys())).difference(obj.columns, sort=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m 662\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(cols) > \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m663\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mColumn(s) \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlist\u001b[39m(cols)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m do not exist\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 665\u001b[39m aggregator_types = (\u001b[38;5;28mlist\u001b[39m, \u001b[38;5;28mtuple\u001b[39m, \u001b[38;5;28mdict\u001b[39m)\n\u001b[32m 667\u001b[39m \u001b[38;5;66;03m# if we have a dict of any non-scalars\u001b[39;00m\n\u001b[32m 668\u001b[39m \u001b[38;5;66;03m# eg. {'A' : ['mean']}, normalize all to\u001b[39;00m\n\u001b[32m 669\u001b[39m \u001b[38;5;66;03m# be list-likes\u001b[39;00m\n\u001b[32m 670\u001b[39m \u001b[38;5;66;03m# Cannot use func.values() because arg may be a Series\u001b[39;00m\n",
|
||
"\u001b[31mKeyError\u001b[39m: \"Column(s) ['rupture_flag'] do not exist\""
|
||
]
|
||
}
|
||
],
|
||
"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": 14,
|
||
"id": "d6ee0c24-e14e-4c40-97d4-49879229790c",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/tmp/ipykernel_1311/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": 14,
|
||
"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": 47,
|
||
"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_1311/3061846510.py:66: 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",
|
||
"\n",
|
||
" # Initial expected path\n",
|
||
" expected = np.empty_like(obs)\n",
|
||
" expected[0] = np.nan\n",
|
||
"\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",
|
||
"\n",
|
||
" for i in range(1, len(obs) - MIN_PERSISTENCE):\n",
|
||
" if (\n",
|
||
" rel_gap[i] > REL_GAP_THR\n",
|
||
" and np.all(np.abs(gap[i:i+MIN_PERSISTENCE] - gap[i]) < GAP_TOL)\n",
|
||
" and 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",
|
||
"\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",
|
||
"\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",
|
||
"\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)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 30,
|
||
"id": "62583cfe-a6e7-4931-a63e-4273dca97ff7",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"ename": "NameError",
|
||
"evalue": "name 'df_final' 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[30]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 1\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;01mgraph_objects\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;01mgo\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;01mpandas\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;01mpd\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m df_final = \u001b[43mdf_final\u001b[49m.rename(columns={\n\u001b[32m 5\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mQuantity - AUM\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33maum_raw\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 6\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcorrected_aum\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33maum_repaired\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 7\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mQuantity - NetFlows\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mflows\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 8\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexpected_stock\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mexpected_aum_raw\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 9\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexpected_stock_corr\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mexpected_aum_repaired\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 10\u001b[39m })\n\u001b[32m 12\u001b[39m df[\u001b[33m\"\u001b[39m\u001b[33mgap_before\u001b[39m\u001b[33m\"\u001b[39m] = df[\u001b[33m\"\u001b[39m\u001b[33mQuantity - AUM\u001b[39m\u001b[33m\"\u001b[39m] - df[\u001b[33m\"\u001b[39m\u001b[33mexpected_stock\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 13\u001b[39m df[\u001b[33m\"\u001b[39m\u001b[33mgap_after\u001b[39m\u001b[33m\"\u001b[39m] = df[\u001b[33m\"\u001b[39m\u001b[33mcorrected_aum\u001b[39m\u001b[33m\"\u001b[39m] - df[\u001b[33m\"\u001b[39m\u001b[33mexpected_stock_corr\u001b[39m\u001b[33m\"\u001b[39m]\n",
|
||
"\u001b[31mNameError\u001b[39m: name 'df_final' is not defined"
|
||
]
|
||
}
|
||
],
|
||
"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 (1–10%)\",\n",
|
||
" \"High (10–30%)\",\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 AUM–flow 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": 31,
|
||
"id": "70cf0a99-bd19-41a9-9574-88647fde09ca",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"ename": "KeyError",
|
||
"evalue": "\"['Quantity - AUM', 'corrected_aum', 'Quantity - NetFlows', 'expected_stock', 'expected_stock_corr', 'gap_before', 'gap_after'] not in index\"",
|
||
"output_type": "error",
|
||
"traceback": [
|
||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||
"\u001b[31mKeyError\u001b[39m Traceback (most recent call last)",
|
||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[31]\u001b[39m\u001b[32m, line 10\u001b[39m\n\u001b[32m 5\u001b[39m df_final = df.copy()\n\u001b[32m 7\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 8\u001b[39m \u001b[38;5;66;03m# Core variables (before / after)\u001b[39;00m\n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m10\u001b[39m df_final = \u001b[43mdf_final\u001b[49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 11\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mRegistrar Account - ID\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 12\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mProduct - Isin\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 13\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mCentralisation Date\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 14\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mQuantity - AUM\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 15\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcorrected_aum\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 16\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mQuantity - NetFlows\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 17\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mexpected_stock\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 18\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mexpected_stock_corr\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mgap_before\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 20\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mgap_after\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 21\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrepair_flag\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 22\u001b[39m \u001b[43m]\u001b[49m\u001b[43m]\u001b[49m.rename(columns={\n\u001b[32m 23\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mQuantity - AUM\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33maum_raw\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 24\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcorrected_aum\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33maum_repaired\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 25\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mQuantity - NetFlows\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mflows\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 26\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexpected_stock\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mexpected_aum_raw\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 27\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexpected_stock_corr\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mexpected_aum_repaired\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 28\u001b[39m })\n\u001b[32m 30\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 31\u001b[39m \u001b[38;5;66;03m# Relative gaps\u001b[39;00m\n\u001b[32m 32\u001b[39m \u001b[38;5;66;03m# ------------------------------------------------------------\u001b[39;00m\n\u001b[32m 33\u001b[39m df_final[\u001b[33m\"\u001b[39m\u001b[33mgap_rel_before\u001b[39m\u001b[33m\"\u001b[39m] = (\n\u001b[32m 34\u001b[39m df_final[\u001b[33m\"\u001b[39m\u001b[33mgap_before\u001b[39m\u001b[33m\"\u001b[39m].abs() /\n\u001b[32m 35\u001b[39m df_final[\u001b[33m\"\u001b[39m\u001b[33mexpected_aum_raw\u001b[39m\u001b[33m\"\u001b[39m].abs().clip(lower=\u001b[32m1\u001b[39m)\n\u001b[32m 36\u001b[39m )\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/frame.py:4119\u001b[39m, in \u001b[36mDataFrame.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 4117\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_iterator(key):\n\u001b[32m 4118\u001b[39m key = \u001b[38;5;28mlist\u001b[39m(key)\n\u001b[32m-> \u001b[39m\u001b[32m4119\u001b[39m indexer = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcolumns\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_get_indexer_strict\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcolumns\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m[\u001b[32m1\u001b[39m]\n\u001b[32m 4121\u001b[39m \u001b[38;5;66;03m# take() does not accept boolean indexers\u001b[39;00m\n\u001b[32m 4122\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(indexer, \u001b[33m\"\u001b[39m\u001b[33mdtype\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) == \u001b[38;5;28mbool\u001b[39m:\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/indexes/base.py:6212\u001b[39m, in \u001b[36mIndex._get_indexer_strict\u001b[39m\u001b[34m(self, key, axis_name)\u001b[39m\n\u001b[32m 6209\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 6210\u001b[39m keyarr, indexer, new_indexer = \u001b[38;5;28mself\u001b[39m._reindex_non_unique(keyarr)\n\u001b[32m-> \u001b[39m\u001b[32m6212\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_raise_if_missing\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkeyarr\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mindexer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis_name\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 6214\u001b[39m keyarr = \u001b[38;5;28mself\u001b[39m.take(indexer)\n\u001b[32m 6215\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, Index):\n\u001b[32m 6216\u001b[39m \u001b[38;5;66;03m# GH 42790 - Preserve name from an Index\u001b[39;00m\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/indexes/base.py:6264\u001b[39m, in \u001b[36mIndex._raise_if_missing\u001b[39m\u001b[34m(self, key, indexer, axis_name)\u001b[39m\n\u001b[32m 6261\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNone of [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mkey\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m] are in the [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00maxis_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m]\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 6263\u001b[39m not_found = \u001b[38;5;28mlist\u001b[39m(ensure_index(key)[missing_mask.nonzero()[\u001b[32m0\u001b[39m]].unique())\n\u001b[32m-> \u001b[39m\u001b[32m6264\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnot_found\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m not in index\u001b[39m\u001b[33m\"\u001b[39m)\n",
|
||
"\u001b[31mKeyError\u001b[39m: \"['Quantity - AUM', 'corrected_aum', 'Quantity - NetFlows', 'expected_stock', 'expected_stock_corr', 'gap_before', 'gap_after'] not in index\""
|
||
]
|
||
}
|
||
],
|
||
"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": null,
|
||
"id": "befb2962-73fb-4cb8-b86e-3218ec103204",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"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": null,
|
||
"id": "1fc44ed4-829f-4a8a-985a-31350bdbdf6d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"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": 50,
|
||
"id": "d85728ca-55ba-4266-b881-23536eee4ba3",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"ename": "KeyError",
|
||
"evalue": "\"['corrected_aum'] not in index\"",
|
||
"output_type": "error",
|
||
"traceback": [
|
||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||
"\u001b[31mKeyError\u001b[39m Traceback (most recent call last)",
|
||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[50]\u001b[39m\u001b[32m, line 16\u001b[39m\n\u001b[32m 10\u001b[39m stocks_repaired[\u001b[33m\"\u001b[39m\u001b[33mCentralisation Date\u001b[39m\u001b[33m\"\u001b[39m] = pd.to_datetime(\n\u001b[32m 11\u001b[39m stocks_repaired[\u001b[33m\"\u001b[39m\u001b[33mCentralisation Date\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 12\u001b[39m )\n\u001b[32m 14\u001b[39m \u001b[38;5;66;03m# 2. Build repair map\u001b[39;00m\n\u001b[32m 15\u001b[39m repair_map = (\n\u001b[32m---> \u001b[39m\u001b[32m16\u001b[39m \u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 17\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mRegistrar Account - ID\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 18\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mProduct - Isin\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mCentralisation Date\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 20\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcorrected_aum\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 21\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrepair_flag\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 22\u001b[39m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 23\u001b[39m .rename(columns={\u001b[33m\"\u001b[39m\u001b[33mcorrected_aum\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mQuantity - AUM repaired\u001b[39m\u001b[33m\"\u001b[39m})\n\u001b[32m 24\u001b[39m )\n\u001b[32m 26\u001b[39m \u001b[38;5;66;03m# 3. Merge repaired quantities\u001b[39;00m\n\u001b[32m 27\u001b[39m stocks_repaired = stocks_repaired.merge(\n\u001b[32m 28\u001b[39m repair_map,\n\u001b[32m 29\u001b[39m on=[\u001b[33m\"\u001b[39m\u001b[33mRegistrar Account - ID\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mProduct - Isin\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mCentralisation Date\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 30\u001b[39m how=\u001b[33m\"\u001b[39m\u001b[33mleft\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 31\u001b[39m )\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/frame.py:4119\u001b[39m, in \u001b[36mDataFrame.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 4117\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_iterator(key):\n\u001b[32m 4118\u001b[39m key = \u001b[38;5;28mlist\u001b[39m(key)\n\u001b[32m-> \u001b[39m\u001b[32m4119\u001b[39m indexer = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcolumns\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_get_indexer_strict\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcolumns\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m[\u001b[32m1\u001b[39m]\n\u001b[32m 4121\u001b[39m \u001b[38;5;66;03m# take() does not accept boolean indexers\u001b[39;00m\n\u001b[32m 4122\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(indexer, \u001b[33m\"\u001b[39m\u001b[33mdtype\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) == \u001b[38;5;28mbool\u001b[39m:\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/indexes/base.py:6212\u001b[39m, in \u001b[36mIndex._get_indexer_strict\u001b[39m\u001b[34m(self, key, axis_name)\u001b[39m\n\u001b[32m 6209\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 6210\u001b[39m keyarr, indexer, new_indexer = \u001b[38;5;28mself\u001b[39m._reindex_non_unique(keyarr)\n\u001b[32m-> \u001b[39m\u001b[32m6212\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_raise_if_missing\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkeyarr\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mindexer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis_name\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 6214\u001b[39m keyarr = \u001b[38;5;28mself\u001b[39m.take(indexer)\n\u001b[32m 6215\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, Index):\n\u001b[32m 6216\u001b[39m \u001b[38;5;66;03m# GH 42790 - Preserve name from an Index\u001b[39;00m\n",
|
||
"\u001b[36mFile \u001b[39m\u001b[32m/opt/python/lib/python3.13/site-packages/pandas/core/indexes/base.py:6264\u001b[39m, in \u001b[36mIndex._raise_if_missing\u001b[39m\u001b[34m(self, key, indexer, axis_name)\u001b[39m\n\u001b[32m 6261\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNone of [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mkey\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m] are in the [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00maxis_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m]\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 6263\u001b[39m not_found = \u001b[38;5;28mlist\u001b[39m(ensure_index(key)[missing_mask.nonzero()[\u001b[32m0\u001b[39m]].unique())\n\u001b[32m-> \u001b[39m\u001b[32m6264\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnot_found\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m not in index\u001b[39m\u001b[33m\"\u001b[39m)\n",
|
||
"\u001b[31mKeyError\u001b[39m: \"['corrected_aum'] not in index\""
|
||
]
|
||
}
|
||
],
|
||
"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": null,
|
||
"id": "5f262605-49e8-4304-b11e-38c8bcfc6e3f",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(stocks[\"Registrar Account - ID\"].nunique())\n",
|
||
"print(df[\"Registrar Account - ID\"].nunique())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "37e9b599-aa51-4e03-b23c-2dd24e77fe38",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import pandas as pd\n",
|
||
"\n",
|
||
"df = pd.read_csv(\"AUM_repaired.csv\")\n",
|
||
"\n",
|
||
"print(df.columns)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 20,
|
||
"id": "5cfb4526-7435-4e4a-ae48-0a8d40e39d81",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/tmp/ipykernel_1311/55327206.py:8: DtypeWarning:\n",
|
||
"\n",
|
||
"Columns (1,2,3,4) have mixed types. Specify dtype option on import or set low_memory=False.\n",
|
||
"\n",
|
||
"/tmp/ipykernel_1311/55327206.py:9: DtypeWarning:\n",
|
||
"\n",
|
||
"Columns (2,3,4,5) have mixed types. Specify dtype option on import or set low_memory=False.\n",
|
||
"\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Merged dataset size: (9033269, 6)\n",
|
||
"\n",
|
||
"NUMBER OF MODIFIED OBSERVATIONS: 2263602\n",
|
||
"Share modified: 25.06 %\n",
|
||
"\n",
|
||
"NEGATIVE AUM\n",
|
||
"Before repair: 34374\n",
|
||
"After repair : 36320\n",
|
||
"\n",
|
||
"RAW AUM DISTRIBUTION\n",
|
||
"count 9.033269e+06\n",
|
||
"mean 9.106935e+03\n",
|
||
"std 1.915018e+05\n",
|
||
"min -9.918641e+06\n",
|
||
"25% 0.000000e+00\n",
|
||
"50% 0.000000e+00\n",
|
||
"75% 3.091340e+02\n",
|
||
"max 4.256300e+07\n",
|
||
"Name: Quantity - AUM_raw, dtype: float64\n",
|
||
"\n",
|
||
"REPAIRED AUM DISTRIBUTION\n",
|
||
"count 9.033269e+06\n",
|
||
"mean 9.104329e+03\n",
|
||
"std 1.914988e+05\n",
|
||
"min -9.918641e+06\n",
|
||
"25% 0.000000e+00\n",
|
||
"50% 0.000000e+00\n",
|
||
"75% 3.088430e+02\n",
|
||
"max 4.256300e+07\n",
|
||
"Name: Quantity - AUM_repaired, dtype: float64\n",
|
||
"\n",
|
||
"TOTAL AUM\n",
|
||
"Raw total : 82265397351.45718\n",
|
||
"Repaired total : 82241848877.5126\n",
|
||
"\n",
|
||
"TOP 20 AUM CHANGES\n",
|
||
" Registrar Account - ID Product - Isin Centralisation Date \\\n",
|
||
"8532368 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8532369 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8532370 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477988 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477987 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477986 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477989 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8532371 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477994 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477996 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477997 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8928641 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8928642 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8928643 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8928644 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8477995 OFF DISTRIBUTION LU0992627611 2021-12-31 \n",
|
||
"8532359 OFF DISTRIBUTION LU0992627611 2021-11-30 \n",
|
||
"8713983 OFF DISTRIBUTION LU0992627611 2021-11-30 \n",
|
||
"8713984 OFF DISTRIBUTION LU0992627611 2021-11-30 \n",
|
||
"8532357 OFF DISTRIBUTION LU0992627611 2021-11-30 \n",
|
||
"\n",
|
||
" Quantity - AUM_raw Quantity - AUM_repaired aum_diff \n",
|
||
"8532368 41251.971 5298781.613 5257529.642 \n",
|
||
"8532369 41251.971 5298781.613 5257529.642 \n",
|
||
"8532370 41251.971 5298781.613 5257529.642 \n",
|
||
"8477988 5298781.613 41251.971 -5257529.642 \n",
|
||
"8477987 5298781.613 41251.971 -5257529.642 \n",
|
||
"8477986 5298781.613 41251.971 -5257529.642 \n",
|
||
"8477989 5298781.613 41251.971 -5257529.642 \n",
|
||
"8532371 41251.971 5298781.613 5257529.642 \n",
|
||
"8477994 5298781.613 128141.894 -5170639.719 \n",
|
||
"8477996 5298781.613 128141.894 -5170639.719 \n",
|
||
"8477997 5298781.613 128141.894 -5170639.719 \n",
|
||
"8928641 128141.894 5298781.613 5170639.719 \n",
|
||
"8928642 128141.894 5298781.613 5170639.719 \n",
|
||
"8928643 128141.894 5298781.613 5170639.719 \n",
|
||
"8928644 128141.894 5298781.613 5170639.719 \n",
|
||
"8477995 5298781.613 128141.894 -5170639.719 \n",
|
||
"8532359 41251.971 5059704.980 5018453.009 \n",
|
||
"8713983 5059704.980 41251.971 -5018453.009 \n",
|
||
"8713984 5059704.980 41251.971 -5018453.009 \n",
|
||
"8532357 41251.971 5059704.980 5018453.009 \n",
|
||
"\n",
|
||
"ISIN WITH MOST MODIFICATIONS\n",
|
||
"Product - Isin\n",
|
||
"LU1623762769 0.535539\n",
|
||
"LU2490324410 0.525588\n",
|
||
"FR0013516044 0.524862\n",
|
||
"LU2931971050 0.500000\n",
|
||
"LU2931971217 0.500000\n",
|
||
"FR001400TVB3 0.500000\n",
|
||
"FR001400TU23 0.500000\n",
|
||
"FR00140139F6 0.500000\n",
|
||
"FR001400TVD9 0.500000\n",
|
||
"LU2931971134 0.500000\n",
|
||
"Name: aum_diff, dtype: float64\n",
|
||
"\n",
|
||
"REPAIR FLAG ERRORS: 2260454\n",
|
||
"\n",
|
||
"==============================\n",
|
||
"COMPARISON COMPLETED\n",
|
||
"==============================\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import pandas as pd\n",
|
||
"import numpy as np\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# LOAD DATA\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"aum_raw = pd.read_csv(\"stocks.csv\") # fichier original\n",
|
||
"aum_rep = pd.read_csv(\"AUM_repaired.csv\") # fichier réparé\n",
|
||
"\n",
|
||
"aum_raw[\"Centralisation Date\"] = pd.to_datetime(aum_raw[\"Centralisation Date\"])\n",
|
||
"aum_rep[\"Centralisation Date\"] = pd.to_datetime(aum_rep[\"Centralisation Date\"])\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# KEEP SAME KEYS\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"keys = [\n",
|
||
" \"Registrar Account - ID\",\n",
|
||
" \"Product - Isin\",\n",
|
||
" \"Centralisation Date\"\n",
|
||
"]\n",
|
||
"\n",
|
||
"aum_raw = aum_raw[keys + [\"Quantity - AUM\"]]\n",
|
||
"aum_rep = aum_rep[keys + [\"Quantity - AUM\", \"repair_flag\"]]\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# MERGE DATASETS\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df = aum_raw.merge(\n",
|
||
" aum_rep,\n",
|
||
" on=keys,\n",
|
||
" how=\"inner\",\n",
|
||
" suffixes=(\"_raw\", \"_repaired\")\n",
|
||
")\n",
|
||
"\n",
|
||
"print(\"Merged dataset size:\", df.shape)\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 1. HOW MANY VALUES CHANGED\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df[\"aum_diff\"] = df[\"Quantity - AUM_repaired\"] - df[\"Quantity - AUM_raw\"]\n",
|
||
"\n",
|
||
"n_changed = (df[\"aum_diff\"] != 0).sum()\n",
|
||
"\n",
|
||
"print(\"\\nNUMBER OF MODIFIED OBSERVATIONS:\", n_changed)\n",
|
||
"print(\"Share modified:\", round(n_changed / len(df) * 100, 2), \"%\")\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 2. NEGATIVE AUM BEFORE / AFTER\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"neg_before = (df[\"Quantity - AUM_raw\"] < 0).sum()\n",
|
||
"neg_after = (df[\"Quantity - AUM_repaired\"] < 0).sum()\n",
|
||
"\n",
|
||
"print(\"\\nNEGATIVE AUM\")\n",
|
||
"print(\"Before repair:\", neg_before)\n",
|
||
"print(\"After repair :\", neg_after)\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 3. DISTRIBUTION COMPARISON\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"print(\"\\nRAW AUM DISTRIBUTION\")\n",
|
||
"print(df[\"Quantity - AUM_raw\"].describe())\n",
|
||
"\n",
|
||
"print(\"\\nREPAIRED AUM DISTRIBUTION\")\n",
|
||
"print(df[\"Quantity - AUM_repaired\"].describe())\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 4. TOTAL AUM COMPARISON\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"print(\"\\nTOTAL AUM\")\n",
|
||
"\n",
|
||
"print(\"Raw total :\", df[\"Quantity - AUM_raw\"].sum())\n",
|
||
"print(\"Repaired total :\", df[\"Quantity - AUM_repaired\"].sum())\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 5. LARGEST MODIFICATIONS\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"largest_changes = df.sort_values(\n",
|
||
" \"aum_diff\",\n",
|
||
" key=lambda x: x.abs(),\n",
|
||
" ascending=False\n",
|
||
").head(20)\n",
|
||
"\n",
|
||
"print(\"\\nTOP 20 AUM CHANGES\")\n",
|
||
"\n",
|
||
"print(\n",
|
||
" largest_changes[\n",
|
||
" [\n",
|
||
" \"Registrar Account - ID\",\n",
|
||
" \"Product - Isin\",\n",
|
||
" \"Centralisation Date\",\n",
|
||
" \"Quantity - AUM_raw\",\n",
|
||
" \"Quantity - AUM_repaired\",\n",
|
||
" \"aum_diff\"\n",
|
||
" ]\n",
|
||
" ]\n",
|
||
")\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 6. WHICH ISIN WERE MOST MODIFIED\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"isin_changes = (\n",
|
||
" df.groupby(\"Product - Isin\")[\"aum_diff\"]\n",
|
||
" .apply(lambda x: (x != 0).mean())\n",
|
||
" .sort_values(ascending=False)\n",
|
||
" .head(10)\n",
|
||
")\n",
|
||
"\n",
|
||
"print(\"\\nISIN WITH MOST MODIFICATIONS\")\n",
|
||
"print(isin_changes)\n",
|
||
"\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 7. CHECK REPAIR FLAG CONSISTENCY\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"if \"repair_flag\" in df.columns:\n",
|
||
"\n",
|
||
" repair_flag_errors = (\n",
|
||
" (df[\"repair_flag\"] == False) &\n",
|
||
" (df[\"Quantity - AUM_raw\"] != df[\"Quantity - AUM_repaired\"])\n",
|
||
" ).sum()\n",
|
||
"\n",
|
||
" print(\"\\nREPAIR FLAG ERRORS:\", repair_flag_errors)\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"\\n==============================\")\n",
|
||
"print(\"COMPARISON COMPLETED\")\n",
|
||
"print(\"==============================\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 48,
|
||
"id": "976dd82c-5c16-44e6-aa5d-65d085714b25",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/tmp/ipykernel_1311/1498669893.py:8: DtypeWarning:\n",
|
||
"\n",
|
||
"Columns (2,3,4,5) have mixed types. Specify dtype option on import or set low_memory=False.\n",
|
||
"\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import pandas as pd\n",
|
||
"import numpy as np\n",
|
||
"import plotly.graph_objects as go\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 1. LOAD DATA\n",
|
||
"# ============================================================\n",
|
||
"aum = pd.read_csv(\"AUM_repaired.csv\")\n",
|
||
"\n",
|
||
"flows[\"Centralisation Date\"] = pd.to_datetime(flows[\"Centralisation Date\"])\n",
|
||
"aum[\"Centralisation Date\"] = pd.to_datetime(aum[\"Centralisation Date\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 49,
|
||
"id": "66c011b5-aed1-428e-bd18-44d8d814c283",
|
||
"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 (1–10%)",
|
||
"High (10–30%)",
|
||
"Severe (>30%)"
|
||
],
|
||
"textinfo": "percent",
|
||
"type": "pie",
|
||
"values": {
|
||
"bdata": "mpmZmZlZR0BmZmZmZmY7QM3MzMzMzCpAAAAAAAAAKUA=",
|
||
"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
|
||
}
|
||
}
|
||
},
|
||
"title": {
|
||
"text": "Rupture intensity distribution (AUM repaired)"
|
||
}
|
||
}
|
||
},
|
||
"image/png": "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"
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# ============================================================\n",
|
||
"# 2. PREPARE FLOWS\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"flows_clean = (\n",
|
||
" flows\n",
|
||
" .groupby(\n",
|
||
" [\"Registrar Account - ID\",\"Product - Isin\",\"Centralisation Date\"],\n",
|
||
" as_index=False\n",
|
||
" )[\"Quantity - NetFlows\"]\n",
|
||
" .sum()\n",
|
||
")\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 3. MERGE\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df = aum.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",
|
||
"# 4. SORT\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df = df.sort_values(\n",
|
||
" [\"Registrar Account - ID\",\"Product - Isin\",\"Centralisation Date\"]\n",
|
||
")\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# REBUILD ACCOUNTING IDENTITY WITH REPAIRED AUM\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df[\"prev_aum\"] = df.groupby(\n",
|
||
" [\"Registrar Account - ID\",\"Product - Isin\"]\n",
|
||
")[\"Quantity - AUM\"].shift(1)\n",
|
||
"\n",
|
||
"df[\"prev_flow\"] = df.groupby(\n",
|
||
" [\"Registrar Account - ID\",\"Product - Isin\"]\n",
|
||
")[\"Quantity - NetFlows\"].shift(1).fillna(0)\n",
|
||
"\n",
|
||
"df[\"expected_aum\"] = df[\"prev_aum\"] + df[\"prev_flow\"]\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# COMPUTE GAP\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df[\"gap\"] = df[\"Quantity - AUM\"] - df[\"expected_aum\"]\n",
|
||
"df[\"gap_abs\"] = df[\"gap\"].abs()\n",
|
||
"\n",
|
||
"EPS = 10\n",
|
||
"\n",
|
||
"df[\"rupture_flag\"] = (\n",
|
||
" df[\"prev_aum\"].notna()\n",
|
||
" & (df[\"gap_abs\"] > EPS)\n",
|
||
")\n",
|
||
"# ============================================================\n",
|
||
"# 6. COMPUTE GAP\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"df[\"gap\"] = df[\"Quantity - AUM\"] - df[\"expected_aum\"]\n",
|
||
"df[\"gap_abs\"] = df[\"gap\"].abs()\n",
|
||
"\n",
|
||
"EPS = 10\n",
|
||
"\n",
|
||
"df[\"rupture_flag\"] = (\n",
|
||
" df[\"prev_aum\"].notna()\n",
|
||
" & (df[\"gap_abs\"] > EPS)\n",
|
||
")\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 7. BUILD RUPTURE SUMMARY\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"rupture_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",
|
||
" )\n",
|
||
" .reset_index()\n",
|
||
")\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 8. SAME CLASSIFICATION AS YOUR CODE\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"rs = rupture_summary.copy()\n",
|
||
"\n",
|
||
"bins = [0, 0.01, 0.10, 0.30, 1.01]\n",
|
||
"\n",
|
||
"labels = [\n",
|
||
" \"Clean / quasi-clean (≤1%)\",\n",
|
||
" \"Moderate (1–10%)\",\n",
|
||
" \"High (10–30%)\",\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",
|
||
"# ============================================================\n",
|
||
"# 9. DISTRIBUTION\n",
|
||
"# ============================================================\n",
|
||
"\n",
|
||
"dist = (\n",
|
||
" rs[\"rupture_class\"]\n",
|
||
" .value_counts(normalize=True)\n",
|
||
" .sort_index()\n",
|
||
" * 100\n",
|
||
").round(1)\n",
|
||
"\n",
|
||
"# ============================================================\n",
|
||
"# 10. DONUT CHART\n",
|
||
"# ============================================================\n",
|
||
"\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",
|
||
" title=\"Rupture intensity distribution (AUM repaired)\",\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()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "990898ea-ceca-46bb-bfb3-c87bf289d272",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"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": null,
|
||
"id": "4d335589-c519-458d-857d-a051813b950b",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"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": null,
|
||
"id": "8a45a111-25da-4f5c-9723-c3efd25c906d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# On ajoute une colonne en % pour l’affichage\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": "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",
|
||
" \"0–0.1%\",\n",
|
||
" \"0.1–1%\",\n",
|
||
" \"1–5%\",\n",
|
||
" \"5–10%\",\n",
|
||
" \"10–25%\",\n",
|
||
" \"25–50%\",\n",
|
||
" \"50–100%\"\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 AUM–Flow 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
|
||
}
|