AMCAnalysis / app.py
singhn9's picture
Update app.py
61b8533 verified
raw
history blame
12.6 kB
# app.py
# MBB-style chord diagram (mixed node order) for Mutual Fund churn
# Uses D3 chord layout in browser, static layout (no physics). Mobile-friendly.
import gradio as gr
import pandas as pd
import networkx as nx
import numpy as np
import json
from collections import defaultdict
# -------------------------
# DATA (same as before)
# -------------------------
AMCS = [
"SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
"UTI MF", "Axis MF", "Aditya Birla SL MF", "Mirae MF", "DSP MF"
]
COMPANIES = [
"HDFC Bank", "ICICI Bank", "Bajaj Finance", "Bajaj Finserv", "Adani Ports",
"Tata Motors", "Shriram Finance", "HAL", "TCS", "AU Small Finance Bank",
"Pearl Global", "Hindalco", "Tata Elxsi", "Cummins India", "Vedanta"
]
BUY_MAP = {
"SBI MF": ["Bajaj Finance", "AU Small Finance Bank"],
"ICICI Pru MF": ["HDFC Bank"],
"HDFC MF": ["Tata Elxsi", "TCS"],
"Nippon India MF": ["Hindalco"],
"Kotak MF": ["Bajaj Finance"],
"UTI MF": ["Adani Ports", "Shriram Finance"],
"Axis MF": ["Tata Motors", "Shriram Finance"],
"Aditya Birla SL MF": ["AU Small Finance Bank"],
"Mirae MF": ["Bajaj Finance", "HAL"],
"DSP MF": ["Tata Motors", "Bajaj Finserv"]
}
SELL_MAP = {
"SBI MF": ["Tata Motors"],
"ICICI Pru MF": ["Bajaj Finance", "Adani Ports"],
"HDFC MF": ["HDFC Bank"],
"Nippon India MF": ["Hindalco"],
"Kotak MF": ["AU Small Finance Bank"],
"UTI MF": ["Hindalco", "TCS"],
"Axis MF": ["TCS"],
"Aditya Birla SL MF": ["Adani Ports"],
"Mirae MF": ["TCS"],
"DSP MF": ["HAL", "Shriram Finance"]
}
COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]}
FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]}
def sanitize_map(m):
out = {}
for k, vals in m.items():
out[k] = [v for v in vals if v in COMPANIES]
return out
BUY_MAP = sanitize_map(BUY_MAP)
SELL_MAP = sanitize_map(SELL_MAP)
COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT)
FRESH_BUY = sanitize_map(FRESH_BUY)
# -------------------------
# Inferred AMC->AMC transfers (same heuristic)
# -------------------------
def infer_amc_transfers(buy_map, sell_map):
transfers = defaultdict(int)
c2s = defaultdict(list)
c2b = defaultdict(list)
for amc, comps in sell_map.items():
for c in comps:
c2s[c].append(amc)
for amc, comps in buy_map.items():
for c in comps:
c2b[c].append(amc)
for c in set(c2s.keys()) | set(c2b.keys()):
for s in c2s[c]:
for b in c2b[c]:
transfers[(s,b)] += 1
return transfers
transfer_counts = infer_amc_transfers(BUY_MAP, SELL_MAP)
# -------------------------
# Build mixed ordering (AMC, company, AMC, company...)
# -------------------------
def build_mixed_ordering(amcs, companies):
mixed = []
n = max(len(amcs), len(companies))
for i in range(n):
if i < len(amcs):
mixed.append(amcs[i])
if i < len(companies):
mixed.append(companies[i])
return mixed
NODES = build_mixed_ordering(AMCS, COMPANIES)
# Node types map for styling
NODE_TYPE = {n: ("amc" if n in AMCS else "company") for n in NODES}
# -------------------------
# Build flow matrix: nodes x nodes
# Matrix interpretation:
# - AMC -> Company for BUY
# - Company -> AMC for SELL
# - AMC -> AMC for inferred TRANSFER
# Fresh buy and complete exit use higher weight
# -------------------------
def build_flow_matrix(nodes):
idx = {n:i for i,n in enumerate(nodes)}
n = len(nodes)
M = [[0]*n for _ in range(n)]
# buys: AMC -> Company
for amc, comps in BUY_MAP.items():
for c in comps:
if amc in idx and c in idx:
w = 1
if amc in FRESH_BUY and c in FRESH_BUY.get(amc, []):
w = 3
M[idx[amc]][idx[c]] += w
# sells: Company -> AMC
for amc, comps in SELL_MAP.items():
for c in comps:
if amc in idx and c in idx:
w = 1
if amc in COMPLETE_EXIT and c in COMPLETE_EXIT.get(amc, []):
w = 3
# represent sell as company -> amc
M[idx[c]][idx[amc]] += w
# inferred transfers: AMC -> AMC
for (s,b), w in transfer_counts.items():
if s in idx and b in idx:
M[idx[s]][idx[b]] += w
return M
MATRIX = build_flow_matrix(NODES)
# -------------------------
# Helper summaries (unchanged)
# -------------------------
def company_trade_summary(company):
buyers = [a for a, cs in BUY_MAP.items() if company in cs]
sellers = [a for a, cs in SELL_MAP.items() if company in cs]
fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
exits = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
df = pd.DataFrame({
"Role": (["Buyer"] * len(buyers)) + (["Seller"] * len(sellers)) + (["Fresh buy"] * len(fresh)) + (["Complete exit"] * len(exits)),
"AMC": buyers + sellers + fresh + exits
})
if df.empty:
return None, pd.DataFrame([], columns=["Role","AMC"])
counts = df.groupby("Role").size().reset_index(name="Count")
fig = {
"data": [{"type":"bar","x": counts["Role"].tolist(), "y": counts["Count"].tolist()}],
"layout":{"title":f"Trades for {company}"}
}
return fig, df
def amc_transfer_summary(amc):
sold = SELL_MAP.get(amc, [])
transfers = []
for s in sold:
buyers = [a for a, cs in BUY_MAP.items() if s in cs]
for b in buyers:
transfers.append({"security": s, "buyer_amc": b})
df = pd.DataFrame(transfers)
if df.empty:
return None, pd.DataFrame([], columns=["security","buyer_amc"])
counts = df["buyer_amc"].value_counts().reset_index()
counts.columns = ["Buyer AMC","Count"]
fig = {
"data": [{"type":"bar","x": counts["Buyer AMC"].tolist(), "y": counts["Count"].tolist()}],
"layout":{"title":f"Inferred transfers from {amc}"}
}
return fig, df
# -------------------------
# Build HTML with D3 chord
# -------------------------
def make_chord_html(nodes, matrix, node_type):
nodes_json = json.dumps(nodes)
mat_json = json.dumps(matrix)
types_json = json.dumps(node_type)
# D3 chord diagram: mixed nodes around circle, modern palette
html = f"""
<div id="chord-container" style="width:100%; height:640px;"></div>
<div style="margin-top:8px;">
<button id="chord-reset" style="padding:8px 12px; border-radius:6px;">Reset</button>
</div>
<div style="margin-top:10px; font-family:sans-serif; font-size:13px;">
<b>Legend</b><br/>
<span style="display:inline-block;width:12px;height:12px;background:#2b6fa6;margin-right:6px;border-radius:2px;"></span> AMC nodes<br/>
<span style="display:inline-block;width:12px;height:12px;background:#f2c88d;margin-right:6px;border-radius:2px;"></span> Company nodes<br/>
<em style="color:#666;">Note: TRANSFER connections are inferred from simultaneous buys/sells, not explicitly reported.</em>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const NODE_NAMES = {nodes_json};
const MATRIX = {mat_json};
const NODE_TYPE = {types_json};
// Dimensions responsive
const container = document.getElementById("chord-container");
function draw() {{
container.innerHTML = ""; // clear
const width = Math.min(900, container.clientWidth || 900);
const height = Math.max(420, Math.min(700, Math.floor(width * 0.75)));
const outerRadius = Math.min(width, height) * 0.45;
const innerRadius = outerRadius * 0.86;
const svg = d3.select(container)
.append("svg")
.attr("width", "100%")
.attr("height", height)
.attr("viewBox", [-width/2, -height/2, width, height].join(" "));
// color scheme
const colorNode = d => (NODE_TYPE[d] === "amc") ? "#2b6fa6" : "#f2c88d"; // muted blue and amber
const chord = d3.chord()
.padAngle(0.02)
.sortSubgroups(d3.descending)
(MATRIX);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius + 6);
const ribbon = d3.ribbon()
.radius(innerRadius)
.padAngle(0.01);
// groups (outer arcs)
const group = svg.append("g")
.selectAll("g")
.data(chord.groups)
.enter().append("g")
.attr("class","group");
group.append("path")
.style("fill", d => colorNode(NODE_NAMES[d.index]))
.style("stroke", d => d3.color(colorNode(NODE_NAMES[d.index])).darker(0.6))
.attr("d", arc)
.attr("cursor","pointer")
.on("click", (e,d) => focusNode(d.index));
// labels
group.append("text")
.each(function(d) {{
const name = NODE_NAMES[d.index];
d.angle = (d.startAngle + d.endAngle) / 2;
this._currentAngle = d.angle;
}})
.attr("dy", ".35em")
.attr("transform", function(d) {{
const angle = (d.startAngle + d.endAngle) / 2;
const deg = angle * 180 / Math.PI - 90;
const rotate = deg;
const translate = outerRadius + 18;
return "rotate(" + rotate + ") translate(" + translate + ")" + ( (deg > 90) ? " rotate(180)" : "" );
}})
.style("font-family", "sans-serif")
.style("font-size", Math.max(10, Math.min(14, outerRadius*0.04)))
.style("text-anchor", function(d) {{
const angle = (d.startAngle + d.endAngle) / 2;
const deg = angle * 180 / Math.PI - 90;
return (deg > 90) ? "end" : "start";
}})
.text(d => NODE_NAMES[d.index]);
// ribbons (flows)
const ribbons = svg.append("g")
.attr("class","ribbons")
.selectAll("path")
.data(chord)
.enter().append("path")
.attr("d", ribbon)
.style("fill", d => colorNode(NODE_NAMES[d.source.index]) )
.style("stroke", d => d3.color(colorNode(NODE_NAMES[d.source.index])).darker(0.6) )
.style("opacity", 0.85)
.on("mouseover", function(e, d) {{
d3.select(this).transition().style("opacity", 1.0).style("filter","brightness(1.05)");
}})
.on("mouseout", function(e, d) {{
d3.select(this).transition().style("opacity", 0.85).style("filter",null);
}});
// interactivity: focus/hide
function focusNode(index) {{
// highlight groups and ribbons connected to index
ribbons.transition().style("opacity", r => (r.source.index === index || r.target.index === index) ? 1.0 : 0.08);
group.selectAll("path").transition().style("opacity", (g) => (g.index === index ? 1.0 : 0.4));
group.selectAll("text").transition().style("opacity", (g) => (g.index === index ? 1.0 : 0.45));
}}
// reset function
function resetView() {{
ribbons.transition().style("opacity", 0.85);
group.selectAll("path").transition().style("opacity", 1.0);
group.selectAll("text").transition().style("opacity", 1.0);
}}
// click outside to reset
svg.on("click", (event) => {{
const target = event.target;
if (target.tagName === "svg" || target.tagName === "g") {{
resetView();
}}
}});
// expose reset button
document.getElementById("chord-reset").onclick = resetView;
// responsive text sizing: done via font-size above
}}
// initial draw and redraw on resize
draw();
window.addEventListener("resize", () => {{
draw();
}});
</script>
"""
return html
# -------------------------
# Build Gradio app
# -------------------------
initial_html = make_chord_html(NODES, MATRIX, NODE_TYPE)
with gr.Blocks(title="MBB-style chord diagram — Mutual Fund churn") as demo:
gr.Markdown("## Mutual Fund Churn — Chord Diagram (consulting-grade)")
gr.HTML(initial_html)
gr.Markdown("### Inspect Company / AMC (unchanged)")
select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
company_plot = gr.Plot()
company_table = gr.DataFrame()
select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
amc_plot = gr.Plot()
amc_table = gr.DataFrame()
def on_company(c):
fig, df = company_trade_summary(c)
return fig, df
def on_amc(a):
fig, df = amc_transfer_summary(a)
return fig, df
select_company.change(on_company, inputs=[select_company], outputs=[company_plot, company_table])
select_amc.change(on_amc, inputs=[select_amc], outputs=[amc_plot, amc_table])
if __name__ == "__main__":
demo.launch()