AMCAnalysis / app.py
singhn9's picture
Update app.py
5138606 verified
raw
history blame
19.5 kB
# app.py
# Fixed Mutual Fund Churn Explorer - Gradio app
# Fixes ValueError: numpy.float64 passed to layout.width by coercing to int and enforcing minimums.
import gradio as gr
import pandas as pd
import networkx as nx
import plotly.graph_objects as go
import numpy as np
from collections import defaultdict
import io
# ---------------------------
# Default sample data
# ---------------------------
DEFAULT_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"
]
DEFAULT_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"
]
SAMPLE_BUY = {
"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"]
}
SAMPLE_SELL = {
"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"]
}
SAMPLE_COMPLETE_EXIT = {
"DSP MF": ["Shriram Finance"]
}
SAMPLE_FRESH_BUY = {
"HDFC MF": ["Tata Elxsi"],
"UTI MF": ["Adani Ports"],
"Mirae MF": ["HAL"]
}
# ---------------------------
# CSV -> maps utility
# ---------------------------
def maps_from_dataframe(df, amc_col="AMC", company_col="Company", action_col="Action"):
amcs = sorted(df[amc_col].dropna().unique().tolist())
companies = sorted(df[company_col].dropna().unique().tolist())
buy_map = defaultdict(list)
sell_map = defaultdict(list)
complete_exit = defaultdict(list)
fresh_buy = defaultdict(list)
for _, row in df.iterrows():
a = str(row[amc_col]).strip()
c = str(row[company_col]).strip()
act = str(row[action_col]).strip().lower()
if act in ("buy", "b"):
buy_map[a].append(c)
elif act in ("sell", "s"):
sell_map[a].append(c)
elif act in ("complete_exit", "exit", "complete"):
complete_exit[a].append(c)
elif act in ("fresh_buy", "fresh", "new"):
fresh_buy[a].append(c)
else:
if "sell" in act:
sell_map[a].append(c)
elif "exit" in act:
complete_exit[a].append(c)
else:
buy_map[a].append(c)
return amcs, companies, dict(buy_map), dict(sell_map), dict(complete_exit), dict(fresh_buy)
def sanitize_map(m, companies_list):
out = {}
for k, vals in m.items():
out[k] = [v for v in vals if v in companies_list]
return out
def load_default_dataset():
AMCS = DEFAULT_AMCS.copy()
COMPANIES = DEFAULT_COMPANIES.copy()
BUY_MAP = sanitize_map(SAMPLE_BUY, COMPANIES)
SELL_MAP = sanitize_map(SAMPLE_SELL, COMPANIES)
COMPLETE_EXIT = sanitize_map(SAMPLE_COMPLETE_EXIT, COMPANIES)
FRESH_BUY = sanitize_map(SAMPLE_FRESH_BUY, COMPANIES)
return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
# ---------------------------
# Infer transfers AMC->AMC
# ---------------------------
def infer_amc_transfers(buy_map, sell_map):
transfers = defaultdict(int)
company_to_sellers = defaultdict(list)
company_to_buyers = defaultdict(list)
for amc, comps in sell_map.items():
for c in comps:
company_to_sellers[c].append(amc)
for amc, comps in buy_map.items():
for c in comps:
company_to_buyers[c].append(amc)
for c in set(list(company_to_sellers.keys()) + list(company_to_buyers.keys())):
sellers = company_to_sellers.get(c, [])
buyers = company_to_buyers.get(c, [])
for s in sellers:
for b in buyers:
transfers[(s,b)] += 1
edge_list = []
for (s,b), w in transfers.items():
edge_list.append((s,b, {"action": "transfer", "weight": w, "company_count": w}))
return edge_list
# ---------------------------
# Graph builder
# ---------------------------
def build_graph(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, include_transfers=True):
G = nx.DiGraph()
for a in AMCS:
G.add_node(a, type="amc", label=a)
for c in COMPANIES:
G.add_node(c, type="company", label=c)
def add_edge(a,c,action,weight=1):
if not G.has_node(a) or not G.has_node(c):
return
if G.has_edge(a,c):
G[a][c]["weight"] += weight
G[a][c]["actions"].append(action)
else:
G.add_edge(a,c, weight=weight, actions=[action])
for a, comps in BUY_MAP.items():
for c in comps:
add_edge(a,c,"buy",1)
for a, comps in SELL_MAP.items():
for c in comps:
add_edge(a,c,"sell",1)
for a, comps in COMPLETE_EXIT.items():
for c in comps:
add_edge(a,c,"complete_exit",3)
for a, comps in FRESH_BUY.items():
for c in comps:
add_edge(a,c,"fresh_buy",3)
if include_transfers:
transfers = infer_amc_transfers(BUY_MAP, SELL_MAP)
for s,b,attrs in transfers:
if not G.has_node(s) or not G.has_node(b):
continue
if G.has_edge(s,b):
G[s][b]["weight"] += attrs.get("weight",1)
G[s][b]["actions"].append("transfer")
else:
G.add_edge(s,b, weight=attrs.get("weight",1), actions=["transfer"])
return G
# ---------------------------
# Plotly visualizer (coerce width/height -> int with minimums)
# ---------------------------
def graph_to_plotly(G,
node_color_amc="#9EC5FF",
node_color_company="#FFCF9E",
node_shape_amc="circle",
node_shape_company="circle",
edge_color_buy="#2ca02c",
edge_color_sell="#d62728",
edge_color_transfer="#888888",
edge_thickness_base=1.2,
show_labels=True,
width=1400,
height=900):
# ensure width/height are native ints and sensible
try:
width = int(float(width))
except Exception:
width = 1400
try:
height = int(float(height))
except Exception:
height = 900
if width < 600:
width = 600
if height < 360:
height = 360
pos = nx.spring_layout(G, seed=42, k=1.4)
node_x = []
node_y = []
node_text = []
node_color = []
node_size = []
for n, d in G.nodes(data=True):
x, y = pos[n]
node_x.append(x)
node_y.append(y)
node_text.append(n)
if d["type"] == "amc":
node_color.append(node_color_amc)
node_size.append(44)
else:
node_color.append(node_color_company)
node_size.append(64)
node_trace = go.Scatter(
x=node_x, y=node_y,
mode='markers+text' if show_labels else 'markers',
marker=dict(color=node_color, size=node_size, line=dict(width=2, color="#222")),
text=node_text if show_labels else None,
textposition="top center",
hoverinfo='text'
)
edge_traces = []
for u, v, attrs in G.edges(data=True):
x0, y0 = pos[u]
x1, y1 = pos[v]
actions = attrs.get("actions", [])
weight = float(attrs.get("weight", 1.0))
if "complete_exit" in actions:
color = edge_color_sell
dash = "solid"
width_px = max(float(edge_thickness_base) * 3.5, 3.0)
elif "fresh_buy" in actions:
color = edge_color_buy
dash = "solid"
width_px = max(float(edge_thickness_base) * 3.5, 3.0)
elif "transfer" in actions:
color = edge_color_transfer
dash = "dash"
width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.5)
elif "sell" in actions:
color = edge_color_sell
dash = "dot"
width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.0)
else:
color = edge_color_buy
dash = "solid"
width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.0)
edge_traces.append(
go.Scatter(
x=[x0, x1, None],
y=[y0, y1, None],
mode='lines',
line=dict(width=float(width_px), color=color, dash=dash),
hoverinfo='text',
text=", ".join(actions)
)
)
fig = go.Figure(data=edge_traces + [node_trace],
layout=go.Layout(
title_text="Mutual Fund Churn Network (AMCs: blue, Companies: orange)",
title_x=0.5,
showlegend=False,
margin=dict(b=20,l=5,r=5,t=40),
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
height=height,
width=width
))
return fig
# ---------------------------
# Analysis helpers
# ---------------------------
def company_trade_summary(company_name, BUY_MAP, SELL_MAP, FRESH_BUY, COMPLETE_EXIT):
buyers = [amc for amc, comps in BUY_MAP.items() if company_name in comps]
sellers = [amc for amc, comps in SELL_MAP.items() if company_name in comps]
fresh = [amc for amc, comps in FRESH_BUY.items() if company_name in comps]
exits = [amc for amc, comps in COMPLETE_EXIT.items() if company_name in comps]
rows = []
for b in buyers:
rows.append({"Role": "Buyer", "AMC": b})
for s in sellers:
rows.append({"Role": "Seller", "AMC": s})
for f in fresh:
rows.append({"Role": "Fresh Buy", "AMC": f})
for e in exits:
rows.append({"Role": "Complete Exit", "AMC": e})
df = pd.DataFrame(rows)
if df.empty:
return None, pd.DataFrame([], columns=["Role","AMC"])
counts = df['Role'].value_counts().reindex(["Buyer","Seller","Fresh Buy","Complete Exit"]).fillna(0)
colors = {"Buyer":"green","Seller":"red","Fresh Buy":"orange","Complete Exit":"black"}
bar = go.Figure()
bar.add_trace(go.Bar(x=counts.index, y=counts.values, marker_color=[colors.get(i,"grey") for i in counts.index]))
bar.update_layout(title=f"Trade Summary for {company_name}", height=360, width=700)
return bar, df
def amc_transfer_summary(amc_name, BUY_MAP, SELL_MAP):
sold = SELL_MAP.get(amc_name, [])
transfers = []
for s in sold:
buyers = [amc for amc, comps in BUY_MAP.items() if s in comps]
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 = go.Figure(go.Bar(x=counts['Buyer AMC'], y=counts['Count'], marker_color='lightslategray'))
fig.update_layout(title_text=f"Inferred transfers from {amc_name}", height=360, width=700)
return fig, df
def detect_loops(G, max_length=6):
amc_nodes = [n for n,d in G.nodes(data=True) if d['type']=='amc']
H = nx.DiGraph()
for u,v,d in G.edges(data=True):
if u in amc_nodes and v in amc_nodes and "transfer" in d.get("actions",[]):
H.add_edge(u,v, weight=d.get("weight",1))
try:
cycles = list(nx.simple_cycles(H))
except Exception:
cycles = []
loops = [c for c in cycles if 2 <= len(c) <= max_length]
return loops
# ---------------------------
# Build initial dataset + graph
# ---------------------------
def build_initial_graph_and_data():
AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY = load_default_dataset()
G = build_graph(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, include_transfers=True)
fig = graph_to_plotly(G)
return (AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G, fig)
(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G_initial, initial_fig) = build_initial_graph_and_data()
# ---------------------------
# Gradio UI
# ---------------------------
with gr.Blocks() as demo:
gr.Markdown("# Mutual Fund Churn Explorer (fixed layout issue)")
with gr.Row():
with gr.Column(scale=3):
csv_uploader = gr.File(label="Upload CSV (optional). Columns: AMC,Company,Action", file_types=['.csv'])
node_color_company = gr.ColorPicker(value="#FFCF9E", label="Company node color")
node_color_amc = gr.ColorPicker(value="#9EC5FF", label="AMC node color")
node_shape_company = gr.Dropdown(choices=["circle","square","diamond"], value="circle", label="Company node shape")
node_shape_amc = gr.Dropdown(choices=["circle","square","diamond"], value="circle", label="AMC node shape")
edge_color_buy = gr.ColorPicker(value="#2ca02c", label="BUY edge color")
edge_color_sell = gr.ColorPicker(value="#d62728", label="SELL edge color")
edge_color_transfer = gr.ColorPicker(value="#888888", label="Transfer edge color")
edge_thickness = gr.Slider(minimum=0.5, maximum=8.0, value=1.4, step=0.1, label="Edge thickness base")
include_transfers_chk = gr.Checkbox(value=True, label="Infer AMC→AMC transfers (show loops)")
update_btn = gr.Button("Update network")
gr.Markdown("## Inspect")
company_selector = gr.Dropdown(choices=COMPANIES, label="Select Company (show buyers/sellers)")
amc_selector = gr.Dropdown(choices=AMCS, label="Select AMC (inferred transfers)")
with gr.Column(scale=7):
network_plot = gr.Plot(value=initial_fig, label="Network graph (drag to zoom)")
company_plot = gr.Plot(label="Company trade summary")
company_table = gr.Dataframe(headers=["Role","AMC"], interactive=False, label="Trades (company)")
amc_plot = gr.Plot(label="AMC inferred transfers")
amc_table = gr.Dataframe(headers=["security","buyer_amc"], interactive=False, label="Inferred transfers (AMC)")
loops_text = gr.Markdown()
def load_dataset_from_csv(file_obj):
if file_obj is None:
return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
try:
raw = file_obj.read()
if isinstance(raw, bytes):
raw = raw.decode('utf-8', errors='ignore')
df = pd.read_csv(io.StringIO(raw))
cols = [c.strip().lower() for c in df.columns]
col_map = {}
for c in df.columns:
if c.strip().lower() in ("amc","fund","manager"):
col_map[c] = "AMC"
elif c.strip().lower() in ("company","security","stock"):
col_map[c] = "Company"
elif c.strip().lower() in ("action","trade","type"):
col_map[c] = "Action"
df = df.rename(columns=col_map)
required = {"AMC","Company","Action"}
if not required.issubset(set(df.columns)):
return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = maps_from_dataframe(df, "AMC", "Company", "Action")
return amcs, companies, buy_map, sell_map, complete_exit, fresh_buy
except Exception as e:
print("CSV load error:", e)
return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
def on_update(csv_file, node_color_company_val, node_color_amc_val, node_shape_company_val, node_shape_amc_val,
edge_color_buy_val, edge_color_sell_val, edge_color_transfer_val, edge_thickness_val, include_transfers_val):
amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
G = build_graph(amcs, companies, buy_map, sell_map, complete_exit, fresh_buy, include_transfers=include_transfers_val)
fig = graph_to_plotly(G,
node_color_amc=node_color_amc_val,
node_color_company=node_color_company_val,
node_shape_amc=node_shape_amc_val,
node_shape_company=node_shape_company_val,
edge_color_buy=edge_color_buy_val,
edge_color_sell=edge_color_sell_val,
edge_color_transfer=edge_color_transfer_val,
edge_thickness_base=edge_thickness_val,
show_labels=True)
loops = detect_loops(G, max_length=6)
if loops:
loops_md = "### Detected AMC transfer loops (inferred):\n"
for i, loop in enumerate(loops, 1):
loops_md += f"- Loop {i}: " + " → ".join(loop) + "\n"
else:
loops_md = "No small transfer loops detected (based on current inferred transfer edges)."
return fig, loops_md, companies, amcs
update_btn.click(on_update,
inputs=[csv_uploader, node_color_company, node_color_amc, node_shape_company, node_shape_amc,
edge_color_buy, edge_color_sell, edge_color_transfer, edge_thickness, include_transfers_chk],
outputs=[network_plot, loops_text, company_selector, amc_selector])
def on_company_sel(company_name, csv_file):
amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
fig, df = company_trade_summary(company_name, buy_map, sell_map, fresh_buy, complete_exit)
if fig is None:
return None, pd.DataFrame([], columns=["Role","AMC"])
return fig, df
company_selector.change(on_company_sel, inputs=[company_selector, csv_uploader], outputs=[company_plot, company_table])
def on_amc_sel(amc_name, csv_file):
amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
fig, df = amc_transfer_summary(amc_name, buy_map, sell_map)
if fig is None:
return None, pd.DataFrame([], columns=["security","buyer_amc"])
return fig, df
amc_selector.change(on_amc_sel, inputs=[amc_selector, csv_uploader], outputs=[amc_plot, amc_table])
gr.Markdown("---")
gr.Markdown("**Notes:** This app *infers* direct AMC→AMC transfers when one fund sells a security and another buys the same security in the dataset. That inference is not proof of a direct bilateral trade, but it describes likely liquidity flows used to exit or absorb positions.")
if __name__ == "__main__":
demo.queue().launch(share=False)