Files changed (1) hide show
  1. app.py +45 -83
app.py CHANGED
@@ -1,21 +1,6 @@
1
  # app.py
2
- # Mutual Fund Churn Explorer - Gradio app (full, fixed version)
3
- # - Option B style: infer AMC->AMC transfers when one sells and another buys the same security
4
- # - Interactive: node/company color, shape, edge color/thickness
5
- # - Select company -> shows buyers/sellers; select AMC -> shows inferred transfers
6
- # - Supports optional CSV upload to replace built-in sample dataset
7
- #
8
- # Usage:
9
- # pip install -r requirements.txt
10
- # python app.py
11
- #
12
- # requirements.txt (example)
13
- # gradio>=3.0
14
- # networkx>=2.6
15
- # plotly>=5.0
16
- # numpy
17
- # pandas
18
- # kaleido # optional if you want to export static images
19
 
20
  import gradio as gr
21
  import pandas as pd
@@ -26,7 +11,7 @@ from collections import defaultdict
26
  import io
27
 
28
  # ---------------------------
29
- # Sample dataset (editable)
30
  # ---------------------------
31
  DEFAULT_AMCS = [
32
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
@@ -39,7 +24,6 @@ DEFAULT_COMPANIES = [
39
  "Pearl Global", "Hindalco", "Tata Elxsi", "Cummins India", "Vedanta"
40
  ]
41
 
42
- # Best-effort sample mappings (you can replace by uploading CSV)
43
  SAMPLE_BUY = {
44
  "SBI MF": ["Bajaj Finance", "AU Small Finance Bank"],
45
  "ICICI Pru MF": ["HDFC Bank"],
@@ -77,13 +61,9 @@ SAMPLE_FRESH_BUY = {
77
  }
78
 
79
  # ---------------------------
80
- # Utilities: build maps from CSV or defaults
81
  # ---------------------------
82
  def maps_from_dataframe(df, amc_col="AMC", company_col="Company", action_col="Action"):
83
- """
84
- Expected actions (case-insensitive): buy, sell, complete_exit, fresh_buy
85
- Returns: (amcs, companies, buy_map, sell_map, complete_exit_map, fresh_buy_map)
86
- """
87
  amcs = sorted(df[amc_col].dropna().unique().tolist())
88
  companies = sorted(df[company_col].dropna().unique().tolist())
89
 
@@ -105,18 +85,12 @@ def maps_from_dataframe(df, amc_col="AMC", company_col="Company", action_col="Ac
105
  elif act in ("fresh_buy", "fresh", "new"):
106
  fresh_buy[a].append(c)
107
  else:
108
- # try to infer from words
109
  if "sell" in act:
110
  sell_map[a].append(c)
111
- elif "buy" in act:
112
- buy_map[a].append(c)
113
  elif "exit" in act:
114
  complete_exit[a].append(c)
115
  else:
116
- # default to buy if unclear
117
  buy_map[a].append(c)
118
-
119
- # ensure dict -> normal dict
120
  return amcs, companies, dict(buy_map), dict(sell_map), dict(complete_exit), dict(fresh_buy)
121
 
122
  def sanitize_map(m, companies_list):
@@ -125,7 +99,6 @@ def sanitize_map(m, companies_list):
125
  out[k] = [v for v in vals if v in companies_list]
126
  return out
127
 
128
- # default dataset packaging function
129
  def load_default_dataset():
130
  AMCS = DEFAULT_AMCS.copy()
131
  COMPANIES = DEFAULT_COMPANIES.copy()
@@ -136,7 +109,7 @@ def load_default_dataset():
136
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
137
 
138
  # ---------------------------
139
- # Inference: AMC->AMC transfers
140
  # ---------------------------
141
  def infer_amc_transfers(buy_map, sell_map):
142
  transfers = defaultdict(int)
@@ -153,7 +126,6 @@ def infer_amc_transfers(buy_map, sell_map):
153
  buyers = company_to_buyers.get(c, [])
154
  for s in sellers:
155
  for b in buyers:
156
- # infer s -> b transfer for this company
157
  transfers[(s,b)] += 1
158
  edge_list = []
159
  for (s,b), w in transfers.items():
@@ -165,33 +137,33 @@ def infer_amc_transfers(buy_map, sell_map):
165
  # ---------------------------
166
  def build_graph(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, include_transfers=True):
167
  G = nx.DiGraph()
168
- # add nodes
169
  for a in AMCS:
170
  G.add_node(a, type="amc", label=a)
171
  for c in COMPANIES:
172
  G.add_node(c, type="company", label=c)
173
- # add AMC->company edges
174
- def add_edge(a, c, action, weight=1):
175
  if not G.has_node(a) or not G.has_node(c):
176
  return
177
  if G.has_edge(a,c):
178
  G[a][c]["weight"] += weight
179
  G[a][c]["actions"].append(action)
180
  else:
181
- G.add_edge(a, c, weight=weight, actions=[action])
 
182
  for a, comps in BUY_MAP.items():
183
  for c in comps:
184
- add_edge(a, c, "buy", weight=1)
185
  for a, comps in SELL_MAP.items():
186
  for c in comps:
187
- add_edge(a, c, "sell", weight=1)
188
  for a, comps in COMPLETE_EXIT.items():
189
  for c in comps:
190
- add_edge(a, c, "complete_exit", weight=3)
191
  for a, comps in FRESH_BUY.items():
192
  for c in comps:
193
- add_edge(a, c, "fresh_buy", weight=3)
194
- # inferred transfers (AMC->AMC)
195
  if include_transfers:
196
  transfers = infer_amc_transfers(BUY_MAP, SELL_MAP)
197
  for s,b,attrs in transfers:
@@ -201,11 +173,11 @@ def build_graph(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, in
201
  G[s][b]["weight"] += attrs.get("weight",1)
202
  G[s][b]["actions"].append("transfer")
203
  else:
204
- G.add_edge(s, b, weight=attrs.get("weight",1), actions=["transfer"])
205
  return G
206
 
207
  # ---------------------------
208
- # Plotly visualization
209
  # ---------------------------
210
  def graph_to_plotly(G,
211
  node_color_amc="#9EC5FF",
@@ -219,9 +191,22 @@ def graph_to_plotly(G,
219
  show_labels=True,
220
  width=1400,
221
  height=900):
222
- # position: spring layout with fixed seed for reproducibility
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  pos = nx.spring_layout(G, seed=42, k=1.4)
224
- # nodes
225
  node_x = []
226
  node_y = []
227
  node_text = []
@@ -248,41 +233,39 @@ def graph_to_plotly(G,
248
  hoverinfo='text'
249
  )
250
 
251
- # edges - draw each edge as a separate trace for styling
252
  edge_traces = []
253
  for u, v, attrs in G.edges(data=True):
254
  x0, y0 = pos[u]
255
  x1, y1 = pos[v]
256
  actions = attrs.get("actions", [])
257
- weight = attrs.get("weight", 1)
258
- # priority styling
259
  if "complete_exit" in actions:
260
  color = edge_color_sell
261
  dash = "solid"
262
- width = max(edge_thickness_base * 3.5, 3)
263
  elif "fresh_buy" in actions:
264
  color = edge_color_buy
265
  dash = "solid"
266
- width = max(edge_thickness_base * 3.5, 3)
267
  elif "transfer" in actions:
268
  color = edge_color_transfer
269
  dash = "dash"
270
- width = max(edge_thickness_base * (1 + np.log1p(weight)), 1.5)
271
  elif "sell" in actions:
272
  color = edge_color_sell
273
  dash = "dot"
274
- width = max(edge_thickness_base * (1 + np.log1p(weight)), 1)
275
- else: # buy or default
276
  color = edge_color_buy
277
  dash = "solid"
278
- width = max(edge_thickness_base * (1 + np.log1p(weight)), 1)
279
 
280
  edge_traces.append(
281
  go.Scatter(
282
  x=[x0, x1, None],
283
  y=[y0, y1, None],
284
  mode='lines',
285
- line=dict(width=width, color=color, dash=dash),
286
  hoverinfo='text',
287
  text=", ".join(actions)
288
  )
@@ -346,29 +329,21 @@ def amc_transfer_summary(amc_name, BUY_MAP, SELL_MAP):
346
  fig.update_layout(title_text=f"Inferred transfers from {amc_name}", height=360, width=700)
347
  return fig, df
348
 
349
- # loop detection in inferred AMC->AMC graph (simple cycles up to length n)
350
  def detect_loops(G, max_length=6):
351
- # extract only nodes that are AMCs
352
  amc_nodes = [n for n,d in G.nodes(data=True) if d['type']=='amc']
353
- loops = []
354
- # Work on a directed graph of only amc nodes with transfer edges
355
  H = nx.DiGraph()
356
  for u,v,d in G.edges(data=True):
357
  if u in amc_nodes and v in amc_nodes and "transfer" in d.get("actions",[]):
358
  H.add_edge(u,v, weight=d.get("weight",1))
359
- # use simple cycle detection (may find many cycles)
360
  try:
361
  cycles = list(nx.simple_cycles(H))
362
  except Exception:
363
  cycles = []
364
- # filter by max_length
365
- for c in cycles:
366
- if 2 <= len(c) <= max_length:
367
- loops.append(c)
368
  return loops
369
 
370
  # ---------------------------
371
- # Gradio interface
372
  # ---------------------------
373
  def build_initial_graph_and_data():
374
  AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY = load_default_dataset()
@@ -376,14 +351,15 @@ def build_initial_graph_and_data():
376
  fig = graph_to_plotly(G)
377
  return (AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G, fig)
378
 
379
- # Prepare initial data
380
  (AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G_initial, initial_fig) = build_initial_graph_and_data()
381
 
 
 
 
382
  with gr.Blocks() as demo:
383
- gr.Markdown("# Mutual Fund Churn Explorer (inferred AMC→AMC transfers)")
384
  with gr.Row():
385
  with gr.Column(scale=3):
386
- gr.Markdown("## Controls")
387
  csv_uploader = gr.File(label="Upload CSV (optional). Columns: AMC,Company,Action", file_types=['.csv'])
388
  node_color_company = gr.ColorPicker(value="#FFCF9E", label="Company node color")
389
  node_color_amc = gr.ColorPicker(value="#9EC5FF", label="AMC node color")
@@ -398,18 +374,15 @@ with gr.Blocks() as demo:
398
  gr.Markdown("## Inspect")
399
  company_selector = gr.Dropdown(choices=COMPANIES, label="Select Company (show buyers/sellers)")
400
  amc_selector = gr.Dropdown(choices=AMCS, label="Select AMC (inferred transfers)")
401
-
402
  with gr.Column(scale=7):
403
  network_plot = gr.Plot(value=initial_fig, label="Network graph (drag to zoom)")
404
 
405
- # outputs
406
  company_plot = gr.Plot(label="Company trade summary")
407
  company_table = gr.Dataframe(headers=["Role","AMC"], interactive=False, label="Trades (company)")
408
  amc_plot = gr.Plot(label="AMC inferred transfers")
409
  amc_table = gr.Dataframe(headers=["security","buyer_amc"], interactive=False, label="Inferred transfers (AMC)")
410
  loops_text = gr.Markdown()
411
 
412
- # function to load CSV if provided and build maps
413
  def load_dataset_from_csv(file_obj):
414
  if file_obj is None:
415
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
@@ -418,8 +391,6 @@ with gr.Blocks() as demo:
418
  if isinstance(raw, bytes):
419
  raw = raw.decode('utf-8', errors='ignore')
420
  df = pd.read_csv(io.StringIO(raw))
421
- # expect columns: AMC, Company, Action
422
- # normalize column names
423
  cols = [c.strip().lower() for c in df.columns]
424
  col_map = {}
425
  for c in df.columns:
@@ -432,21 +403,16 @@ with gr.Blocks() as demo:
432
  df = df.rename(columns=col_map)
433
  required = {"AMC","Company","Action"}
434
  if not required.issubset(set(df.columns)):
435
- # can't parse - fallback to default
436
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
437
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = maps_from_dataframe(df, "AMC", "Company", "Action")
438
- # sanitize - ensure company nodes exist
439
  return amcs, companies, buy_map, sell_map, complete_exit, fresh_buy
440
  except Exception as e:
441
  print("CSV load error:", e)
442
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
443
 
444
- # callback to rebuild network
445
  def on_update(csv_file, node_color_company_val, node_color_amc_val, node_shape_company_val, node_shape_amc_val,
446
  edge_color_buy_val, edge_color_sell_val, edge_color_transfer_val, edge_thickness_val, include_transfers_val):
447
- # load dataset (possibly replaced by CSV)
448
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
449
- # ensure inputs for dropdowns updated - but here we just create fig
450
  G = build_graph(amcs, companies, buy_map, sell_map, complete_exit, fresh_buy, include_transfers=include_transfers_val)
451
  fig = graph_to_plotly(G,
452
  node_color_amc=node_color_amc_val,
@@ -458,7 +424,6 @@ with gr.Blocks() as demo:
458
  edge_color_transfer=edge_color_transfer_val,
459
  edge_thickness_base=edge_thickness_val,
460
  show_labels=True)
461
- # detect loops and prepare a small markdown summary
462
  loops = detect_loops(G, max_length=6)
463
  if loops:
464
  loops_md = "### Detected AMC transfer loops (inferred):\n"
@@ -466,7 +431,6 @@ with gr.Blocks() as demo:
466
  loops_md += f"- Loop {i}: " + " → ".join(loop) + "\n"
467
  else:
468
  loops_md = "No small transfer loops detected (based on current inferred transfer edges)."
469
- # return fig and loops text plus update choices for dropdowns (we will update lists client-side)
470
  return fig, loops_md, companies, amcs
471
 
472
  update_btn.click(on_update,
@@ -474,7 +438,6 @@ with gr.Blocks() as demo:
474
  edge_color_buy, edge_color_sell, edge_color_transfer, edge_thickness, include_transfers_chk],
475
  outputs=[network_plot, loops_text, company_selector, amc_selector])
476
 
477
- # company select callback
478
  def on_company_sel(company_name, csv_file):
479
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
480
  fig, df = company_trade_summary(company_name, buy_map, sell_map, fresh_buy, complete_exit)
@@ -484,7 +447,6 @@ with gr.Blocks() as demo:
484
 
485
  company_selector.change(on_company_sel, inputs=[company_selector, csv_uploader], outputs=[company_plot, company_table])
486
 
487
- # amc select callback
488
  def on_amc_sel(amc_name, csv_file):
489
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
490
  fig, df = amc_transfer_summary(amc_name, buy_map, sell_map)
 
1
  # app.py
2
+ # Fixed Mutual Fund Churn Explorer - Gradio app
3
+ # Fixes ValueError: numpy.float64 passed to layout.width by coercing to int and enforcing minimums.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  import gradio as gr
6
  import pandas as pd
 
11
  import io
12
 
13
  # ---------------------------
14
+ # Default sample data
15
  # ---------------------------
16
  DEFAULT_AMCS = [
17
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
 
24
  "Pearl Global", "Hindalco", "Tata Elxsi", "Cummins India", "Vedanta"
25
  ]
26
 
 
27
  SAMPLE_BUY = {
28
  "SBI MF": ["Bajaj Finance", "AU Small Finance Bank"],
29
  "ICICI Pru MF": ["HDFC Bank"],
 
61
  }
62
 
63
  # ---------------------------
64
+ # CSV -> maps utility
65
  # ---------------------------
66
  def maps_from_dataframe(df, amc_col="AMC", company_col="Company", action_col="Action"):
 
 
 
 
67
  amcs = sorted(df[amc_col].dropna().unique().tolist())
68
  companies = sorted(df[company_col].dropna().unique().tolist())
69
 
 
85
  elif act in ("fresh_buy", "fresh", "new"):
86
  fresh_buy[a].append(c)
87
  else:
 
88
  if "sell" in act:
89
  sell_map[a].append(c)
 
 
90
  elif "exit" in act:
91
  complete_exit[a].append(c)
92
  else:
 
93
  buy_map[a].append(c)
 
 
94
  return amcs, companies, dict(buy_map), dict(sell_map), dict(complete_exit), dict(fresh_buy)
95
 
96
  def sanitize_map(m, companies_list):
 
99
  out[k] = [v for v in vals if v in companies_list]
100
  return out
101
 
 
102
  def load_default_dataset():
103
  AMCS = DEFAULT_AMCS.copy()
104
  COMPANIES = DEFAULT_COMPANIES.copy()
 
109
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
110
 
111
  # ---------------------------
112
+ # Infer transfers AMC->AMC
113
  # ---------------------------
114
  def infer_amc_transfers(buy_map, sell_map):
115
  transfers = defaultdict(int)
 
126
  buyers = company_to_buyers.get(c, [])
127
  for s in sellers:
128
  for b in buyers:
 
129
  transfers[(s,b)] += 1
130
  edge_list = []
131
  for (s,b), w in transfers.items():
 
137
  # ---------------------------
138
  def build_graph(AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, include_transfers=True):
139
  G = nx.DiGraph()
 
140
  for a in AMCS:
141
  G.add_node(a, type="amc", label=a)
142
  for c in COMPANIES:
143
  G.add_node(c, type="company", label=c)
144
+
145
+ def add_edge(a,c,action,weight=1):
146
  if not G.has_node(a) or not G.has_node(c):
147
  return
148
  if G.has_edge(a,c):
149
  G[a][c]["weight"] += weight
150
  G[a][c]["actions"].append(action)
151
  else:
152
+ G.add_edge(a,c, weight=weight, actions=[action])
153
+
154
  for a, comps in BUY_MAP.items():
155
  for c in comps:
156
+ add_edge(a,c,"buy",1)
157
  for a, comps in SELL_MAP.items():
158
  for c in comps:
159
+ add_edge(a,c,"sell",1)
160
  for a, comps in COMPLETE_EXIT.items():
161
  for c in comps:
162
+ add_edge(a,c,"complete_exit",3)
163
  for a, comps in FRESH_BUY.items():
164
  for c in comps:
165
+ add_edge(a,c,"fresh_buy",3)
166
+
167
  if include_transfers:
168
  transfers = infer_amc_transfers(BUY_MAP, SELL_MAP)
169
  for s,b,attrs in transfers:
 
173
  G[s][b]["weight"] += attrs.get("weight",1)
174
  G[s][b]["actions"].append("transfer")
175
  else:
176
+ G.add_edge(s,b, weight=attrs.get("weight",1), actions=["transfer"])
177
  return G
178
 
179
  # ---------------------------
180
+ # Plotly visualizer (coerce width/height -> int with minimums)
181
  # ---------------------------
182
  def graph_to_plotly(G,
183
  node_color_amc="#9EC5FF",
 
191
  show_labels=True,
192
  width=1400,
193
  height=900):
194
+ # ensure width/height are native ints and sensible
195
+ try:
196
+ width = int(float(width))
197
+ except Exception:
198
+ width = 1400
199
+ try:
200
+ height = int(float(height))
201
+ except Exception:
202
+ height = 900
203
+ if width < 600:
204
+ width = 600
205
+ if height < 360:
206
+ height = 360
207
+
208
  pos = nx.spring_layout(G, seed=42, k=1.4)
209
+
210
  node_x = []
211
  node_y = []
212
  node_text = []
 
233
  hoverinfo='text'
234
  )
235
 
 
236
  edge_traces = []
237
  for u, v, attrs in G.edges(data=True):
238
  x0, y0 = pos[u]
239
  x1, y1 = pos[v]
240
  actions = attrs.get("actions", [])
241
+ weight = float(attrs.get("weight", 1.0))
 
242
  if "complete_exit" in actions:
243
  color = edge_color_sell
244
  dash = "solid"
245
+ width_px = max(float(edge_thickness_base) * 3.5, 3.0)
246
  elif "fresh_buy" in actions:
247
  color = edge_color_buy
248
  dash = "solid"
249
+ width_px = max(float(edge_thickness_base) * 3.5, 3.0)
250
  elif "transfer" in actions:
251
  color = edge_color_transfer
252
  dash = "dash"
253
+ width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.5)
254
  elif "sell" in actions:
255
  color = edge_color_sell
256
  dash = "dot"
257
+ width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.0)
258
+ else:
259
  color = edge_color_buy
260
  dash = "solid"
261
+ width_px = max(float(edge_thickness_base) * (1.0 + np.log1p(weight)), 1.0)
262
 
263
  edge_traces.append(
264
  go.Scatter(
265
  x=[x0, x1, None],
266
  y=[y0, y1, None],
267
  mode='lines',
268
+ line=dict(width=float(width_px), color=color, dash=dash),
269
  hoverinfo='text',
270
  text=", ".join(actions)
271
  )
 
329
  fig.update_layout(title_text=f"Inferred transfers from {amc_name}", height=360, width=700)
330
  return fig, df
331
 
 
332
  def detect_loops(G, max_length=6):
 
333
  amc_nodes = [n for n,d in G.nodes(data=True) if d['type']=='amc']
 
 
334
  H = nx.DiGraph()
335
  for u,v,d in G.edges(data=True):
336
  if u in amc_nodes and v in amc_nodes and "transfer" in d.get("actions",[]):
337
  H.add_edge(u,v, weight=d.get("weight",1))
 
338
  try:
339
  cycles = list(nx.simple_cycles(H))
340
  except Exception:
341
  cycles = []
342
+ loops = [c for c in cycles if 2 <= len(c) <= max_length]
 
 
 
343
  return loops
344
 
345
  # ---------------------------
346
+ # Build initial dataset + graph
347
  # ---------------------------
348
  def build_initial_graph_and_data():
349
  AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY = load_default_dataset()
 
351
  fig = graph_to_plotly(G)
352
  return (AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G, fig)
353
 
 
354
  (AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY, G_initial, initial_fig) = build_initial_graph_and_data()
355
 
356
+ # ---------------------------
357
+ # Gradio UI
358
+ # ---------------------------
359
  with gr.Blocks() as demo:
360
+ gr.Markdown("# Mutual Fund Churn Explorer (fixed layout issue)")
361
  with gr.Row():
362
  with gr.Column(scale=3):
 
363
  csv_uploader = gr.File(label="Upload CSV (optional). Columns: AMC,Company,Action", file_types=['.csv'])
364
  node_color_company = gr.ColorPicker(value="#FFCF9E", label="Company node color")
365
  node_color_amc = gr.ColorPicker(value="#9EC5FF", label="AMC node color")
 
374
  gr.Markdown("## Inspect")
375
  company_selector = gr.Dropdown(choices=COMPANIES, label="Select Company (show buyers/sellers)")
376
  amc_selector = gr.Dropdown(choices=AMCS, label="Select AMC (inferred transfers)")
 
377
  with gr.Column(scale=7):
378
  network_plot = gr.Plot(value=initial_fig, label="Network graph (drag to zoom)")
379
 
 
380
  company_plot = gr.Plot(label="Company trade summary")
381
  company_table = gr.Dataframe(headers=["Role","AMC"], interactive=False, label="Trades (company)")
382
  amc_plot = gr.Plot(label="AMC inferred transfers")
383
  amc_table = gr.Dataframe(headers=["security","buyer_amc"], interactive=False, label="Inferred transfers (AMC)")
384
  loops_text = gr.Markdown()
385
 
 
386
  def load_dataset_from_csv(file_obj):
387
  if file_obj is None:
388
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
 
391
  if isinstance(raw, bytes):
392
  raw = raw.decode('utf-8', errors='ignore')
393
  df = pd.read_csv(io.StringIO(raw))
 
 
394
  cols = [c.strip().lower() for c in df.columns]
395
  col_map = {}
396
  for c in df.columns:
 
403
  df = df.rename(columns=col_map)
404
  required = {"AMC","Company","Action"}
405
  if not required.issubset(set(df.columns)):
 
406
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
407
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = maps_from_dataframe(df, "AMC", "Company", "Action")
 
408
  return amcs, companies, buy_map, sell_map, complete_exit, fresh_buy
409
  except Exception as e:
410
  print("CSV load error:", e)
411
  return AMCS, COMPANIES, BUY_MAP, SELL_MAP, COMPLETE_EXIT, FRESH_BUY
412
 
 
413
  def on_update(csv_file, node_color_company_val, node_color_amc_val, node_shape_company_val, node_shape_amc_val,
414
  edge_color_buy_val, edge_color_sell_val, edge_color_transfer_val, edge_thickness_val, include_transfers_val):
 
415
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
 
416
  G = build_graph(amcs, companies, buy_map, sell_map, complete_exit, fresh_buy, include_transfers=include_transfers_val)
417
  fig = graph_to_plotly(G,
418
  node_color_amc=node_color_amc_val,
 
424
  edge_color_transfer=edge_color_transfer_val,
425
  edge_thickness_base=edge_thickness_val,
426
  show_labels=True)
 
427
  loops = detect_loops(G, max_length=6)
428
  if loops:
429
  loops_md = "### Detected AMC transfer loops (inferred):\n"
 
431
  loops_md += f"- Loop {i}: " + " → ".join(loop) + "\n"
432
  else:
433
  loops_md = "No small transfer loops detected (based on current inferred transfer edges)."
 
434
  return fig, loops_md, companies, amcs
435
 
436
  update_btn.click(on_update,
 
438
  edge_color_buy, edge_color_sell, edge_color_transfer, edge_thickness, include_transfers_chk],
439
  outputs=[network_plot, loops_text, company_selector, amc_selector])
440
 
 
441
  def on_company_sel(company_name, csv_file):
442
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
443
  fig, df = company_trade_summary(company_name, buy_map, sell_map, fresh_buy, complete_exit)
 
447
 
448
  company_selector.change(on_company_sel, inputs=[company_selector, csv_uploader], outputs=[company_plot, company_table])
449
 
 
450
  def on_amc_sel(amc_name, csv_file):
451
  amcs, companies, buy_map, sell_map, complete_exit, fresh_buy = load_dataset_from_csv(csv_file)
452
  fig, df = amc_transfer_summary(amc_name, buy_map, sell_map)