# 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)