Spaces:
Sleeping
Sleeping
| # 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) | |