GenAICoder commited on
Commit
78e07b3
Β·
verified Β·
1 Parent(s): 8b52145

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1179 -0
app.py ADDED
@@ -0,0 +1,1179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ import pandas as pd
4
+ import gradio as gr
5
+
6
+ # ---------------------------------------------------
7
+ # HELPERS
8
+ # ---------------------------------------------------
9
+
10
+ from helper.vintage_helpers import (
11
+ create_booking_vintage
12
+ )
13
+
14
+ from helper.data_merger import (
15
+ merge_acq_perf
16
+ )
17
+
18
+ # ---------------------------------------------------
19
+ # METRICS
20
+ # ---------------------------------------------------
21
+
22
+ from metrics.mix_metrics import (
23
+ calculate_vintage_mix,
24
+ calculate_limit_mix
25
+ )
26
+
27
+ # ---------------------------------------------------
28
+ # ANALYTICS
29
+ # ---------------------------------------------------
30
+
31
+ from analytics.performance_analysis import (
32
+ generate_metric_view
33
+ )
34
+
35
+ from analytics.ai_assistant import (
36
+ generate_ai_answer
37
+ )
38
+
39
+ from analytics.deep_dive_agentic import (
40
+ run_deep_dive_analysis
41
+ )
42
+
43
+ # ---------------------------------------------------
44
+ # VISUALIZATIONS - VINTAGE CURVES
45
+ # ---------------------------------------------------
46
+
47
+ from visualizations.vintage_curves import (
48
+ generate_delinquency_metric_chart,
49
+ generate_multi_metric_comparison,
50
+ generate_segment_delinquency_curve
51
+ )
52
+
53
+ # ---------------------------------------------------
54
+ # VISUALIZATIONS - SEGMENT RANKING
55
+ # ---------------------------------------------------
56
+
57
+ from visualizations.segment_ranking import (
58
+ generate_segment_risk_heatmap,
59
+ generate_segment_risk_ranking,
60
+ generate_multi_category_risk_comparison,
61
+ calculate_portfolio_risk_summary
62
+ )
63
+
64
+ # ---------------------------------------------------
65
+ # LOAD DATA
66
+ # ---------------------------------------------------
67
+
68
+ acq = pd.read_csv(
69
+ "data/acquisition.csv"
70
+ )
71
+
72
+ perf = pd.read_csv(
73
+ "data/performance.csv"
74
+ )
75
+
76
+ # ---------------------------------------------------
77
+ # CREATE BOOKING VINTAGE
78
+ # ---------------------------------------------------
79
+
80
+ acq = create_booking_vintage(
81
+ acq,
82
+ booking_date_col="booking_date"
83
+ )
84
+
85
+ # ---------------------------------------------------
86
+ # CREATE MASTER PERFORMANCE DATASET
87
+ # ---------------------------------------------------
88
+
89
+ master_df = merge_acq_perf(
90
+ acq_df=acq,
91
+ perf_df=perf
92
+ )
93
+
94
+ # ---------------------------------------------------
95
+ # ACQUISITION ANALYSIS
96
+ # ---------------------------------------------------
97
+
98
+ def run_acquisition_analysis(
99
+ analysis_type,
100
+ category
101
+ ):
102
+
103
+ # -----------------------------------------
104
+ # PORTFOLIO MIX
105
+ # -----------------------------------------
106
+
107
+ if analysis_type == "Portfolio Mix":
108
+
109
+ result = (
110
+ acq.groupby(
111
+ ["booking_vintage", category]
112
+ )
113
+ .agg(
114
+ count=("account_id", "nunique"),
115
+ balance=("credit_limit", "sum")
116
+ )
117
+ .reset_index()
118
+ )
119
+
120
+ vintage_total = (
121
+ result.groupby("booking_vintage")["count"]
122
+ .transform("sum")
123
+ )
124
+
125
+ result["rate"] = (
126
+ result["count"] / vintage_total
127
+ ) * 100
128
+
129
+ result["rate"] = (
130
+ result["rate"]
131
+ .round(2)
132
+ )
133
+
134
+ # -----------------------------------------
135
+ # CREDIT LINE CONCENTRATION
136
+ # -----------------------------------------
137
+
138
+ elif analysis_type == "Credit Line Concentration":
139
+
140
+ result = (
141
+ acq.groupby(
142
+ ["booking_vintage", category]
143
+ )
144
+ .agg(
145
+ count=("account_id", "nunique"),
146
+ balance=("credit_limit", "sum")
147
+ )
148
+ .reset_index()
149
+ )
150
+
151
+ vintage_total = (
152
+ result.groupby("booking_vintage")["balance"]
153
+ .transform("sum")
154
+ )
155
+
156
+ result["rate"] = (
157
+ result["balance"] / vintage_total
158
+ ) * 100
159
+
160
+ result["rate"] = (
161
+ result["rate"]
162
+ .round(2)
163
+ )
164
+
165
+ else:
166
+
167
+ return pd.DataFrame()
168
+
169
+ # -----------------------------------------
170
+ # STANDARDIZED OUTPUT
171
+ # -----------------------------------------
172
+
173
+ result = result.rename(
174
+ columns={
175
+ "booking_vintage": "Vintage",
176
+ category: "Category",
177
+ "count": "Count",
178
+ "balance": "Balance",
179
+ "rate": "Rate"
180
+ }
181
+ )
182
+
183
+ return result[
184
+ [
185
+ "Vintage",
186
+ "Category",
187
+ "Count",
188
+ "Balance",
189
+ "Rate"
190
+ ]
191
+ ]
192
+
193
+ # ---------------------------------------------------
194
+ # PERFORMANCE ANALYSIS
195
+ # ---------------------------------------------------
196
+
197
+ def run_performance_analysis(
198
+ metric_name,
199
+ view_level
200
+ ):
201
+
202
+ # -----------------------------------------
203
+ # VIEW MAPPING
204
+ # -----------------------------------------
205
+
206
+ view_mapping = {
207
+
208
+ "Overall": None,
209
+
210
+ "Channel":
211
+ "sourcing_channel",
212
+
213
+ "FICO":
214
+ "fico_band",
215
+
216
+ "City Tier":
217
+ "city_tier",
218
+
219
+ "Occupation":
220
+ "occupation_type"
221
+ }
222
+
223
+ group_col = view_mapping[
224
+ view_level
225
+ ]
226
+
227
+ # -----------------------------------------
228
+ # CALL ANALYTICS ENGINE
229
+ # -----------------------------------------
230
+
231
+ result = generate_metric_view(
232
+ df=master_df,
233
+ metric_name=metric_name,
234
+ group_col=group_col
235
+ )
236
+
237
+ # -----------------------------------------
238
+ # STANDARDIZE OUTPUT
239
+ # -----------------------------------------
240
+
241
+ if group_col is not None:
242
+
243
+ result = result.rename(
244
+ columns={
245
+ group_col: "Category"
246
+ }
247
+ )
248
+
249
+ else:
250
+
251
+ result["Category"] = "Overall"
252
+
253
+ # -----------------------------------------
254
+ # IDENTIFY RATE COLUMN
255
+ # -----------------------------------------
256
+
257
+ rate_col = [
258
+ col for col in result.columns
259
+ if "rate" in col.lower()
260
+ ][0]
261
+
262
+ # -----------------------------------------
263
+ # OUTPUT FORMAT
264
+ # -----------------------------------------
265
+
266
+ final_result = pd.DataFrame()
267
+
268
+ final_result["Vintage"] = (
269
+ result["booking_vintage"]
270
+ )
271
+
272
+ final_result["Category"] = (
273
+ result["Category"]
274
+ )
275
+
276
+ final_result["Count"] = (
277
+ result["total_accounts"]
278
+ )
279
+
280
+ final_result["Balance"] = (
281
+ result["total_balance"]
282
+ )
283
+
284
+ final_result["Rate"] = (
285
+ result[rate_col]
286
+ .round(2)
287
+ )
288
+
289
+ return final_result
290
+
291
+ # ---------------------------------------------------
292
+ # VINTAGE CURVES ANALYSIS
293
+ # ---------------------------------------------------
294
+
295
+ def generate_vintage_curve_single(
296
+ metric_name
297
+ ):
298
+ """Generate single vintage curve for a metric."""
299
+ try:
300
+ fig = generate_delinquency_metric_chart(
301
+ df=master_df,
302
+ metric_name=metric_name,
303
+ chart_type="line"
304
+ )
305
+ return fig
306
+ except Exception as e:
307
+ return f"Error generating vintage curve: {str(e)}"
308
+
309
+
310
+ def generate_vintage_curves_comparison():
311
+ """Generate comparison of all vintage curves."""
312
+ try:
313
+ fig = generate_multi_metric_comparison(
314
+ df=master_df,
315
+ metrics=["30+@3", "30+@6", "60+@6", "Yr1 NCL"]
316
+ )
317
+ return fig
318
+ except Exception as e:
319
+ return f"Error generating comparison: {str(e)}"
320
+
321
+
322
+ def generate_segmented_vintage_curve(
323
+ metric_name,
324
+ category
325
+ ):
326
+ """Generate vintage curve segmented by category."""
327
+ try:
328
+ fig = generate_segment_delinquency_curve(
329
+ df=master_df,
330
+ metric_name=metric_name,
331
+ category=category
332
+ )
333
+ return fig
334
+ except Exception as e:
335
+ return f"Error generating segmented curve: {str(e)}"
336
+
337
+ # ---------------------------------------------------
338
+ # SEGMENT RANKING ANALYSIS
339
+ # ---------------------------------------------------
340
+
341
+ def generate_segment_risk_heatmap_chart():
342
+ """Generate risk heatmap across all segments and metrics."""
343
+ try:
344
+ fig = generate_segment_risk_heatmap(
345
+ df=master_df
346
+ )
347
+ return fig
348
+ except Exception as e:
349
+ return f"Error generating heatmap: {str(e)}"
350
+
351
+
352
+ def generate_high_risk_segments_ranking(
353
+ metric_name,
354
+ category
355
+ ):
356
+ """Generate ranking of high-risk segments."""
357
+ try:
358
+ fig = generate_segment_risk_ranking(
359
+ df=master_df,
360
+ metric_name=metric_name,
361
+ category=category,
362
+ top_n=10
363
+ )
364
+ return fig
365
+ except Exception as e:
366
+ return f"Error generating ranking: {str(e)}"
367
+
368
+
369
+ def generate_multi_category_comparison(
370
+ metric_name
371
+ ):
372
+ """Generate risk comparison across all categories."""
373
+ try:
374
+ fig = generate_multi_category_risk_comparison(
375
+ df=master_df,
376
+ metric_name=metric_name
377
+ )
378
+ return fig
379
+ except Exception as e:
380
+ return f"Error generating comparison: {str(e)}"
381
+
382
+
383
+ def generate_portfolio_summary():
384
+ """Generate portfolio risk summary."""
385
+ try:
386
+ summary_df = calculate_portfolio_risk_summary(
387
+ df=master_df
388
+ )
389
+ return summary_df
390
+ except Exception as e:
391
+ return f"Error generating summary: {str(e)}"
392
+
393
+
394
+ def ask_ai_question(question, as_of_month, segment, history):
395
+ if not question or str(question).strip() == "":
396
+ return history or [], history or [], ""
397
+ try:
398
+ answer = generate_ai_answer(
399
+ question=question,
400
+ df=master_df,
401
+ as_of_month=as_of_month,
402
+ segment=segment if segment != "" else None
403
+ )
404
+ except Exception as exc:
405
+ answer = f"AI error: {str(exc)}"
406
+
407
+ history = history or []
408
+ history.append({"role": "user", "content": question})
409
+ history.append({"role": "assistant", "content": answer})
410
+ return history, history, ""
411
+
412
+
413
+ def run_deep_dive_on_question(question):
414
+ if not question or str(question).strip() == "":
415
+ return "Please enter a question."
416
+ try:
417
+ result = run_deep_dive_analysis(question, acq, perf, master_df)
418
+
419
+ # Check if analysis was successful
420
+ if not result["success"]:
421
+ return f"**Error in Deep Dive Analysis:**\n{result.get('error', 'Unknown error')}"
422
+
423
+ output = f"**Question:** {result['question']}\n\n"
424
+
425
+ # Display each requirement and its result
426
+ if result["requirements"]:
427
+ output += "## Analysis Requirements\n\n"
428
+ for i, req in enumerate(result["requirements"], 1):
429
+ output += f"**Requirement {i}:** {req.get('title', 'Untitled')}\n"
430
+ output += f"- {req.get('description', 'No description')}\n\n"
431
+ if "code" in req:
432
+ output += f"Generated Code:\n```python\n{req['code']}\n```\n\n"
433
+
434
+ # Display execution results
435
+ if result["all_results"]:
436
+ output += "## Execution Results\n\n"
437
+ for i, res in enumerate(result["all_results"], 1):
438
+ output += f"**Requirement {i} Results:**\n"
439
+ if res["success"]:
440
+ output += f"βœ“ Executed successfully\n"
441
+ output += f"```\n{res['result']}\n```\n\n"
442
+ else:
443
+ output += f"βœ— Execution failed: {res['error']}\n\n"
444
+
445
+ # Display synthesis & insights
446
+ output += "## Analyst Interpretation\n\n"
447
+ output += f"{result['interpretation']}"
448
+
449
+ return output
450
+ except Exception as exc:
451
+ return f"Deep dive analysis error: {str(exc)}"
452
+
453
+
454
+ # ---------------------------------------------------
455
+ # PORTFOLIO OVERVIEW (Calendar Snapshot)
456
+ # ---------------------------------------------------
457
+
458
+
459
+ def _detect_date_column(df):
460
+ candidates = [
461
+ "reporting_month",
462
+ "observation_date",
463
+ "observation_month",
464
+ "obs_date",
465
+ "date",
466
+ "calendar_month",
467
+ "month",
468
+ "report_date"
469
+ ]
470
+ for c in candidates:
471
+ if c in df.columns:
472
+ return c
473
+ return None
474
+
475
+
476
+ def get_calendar_months():
477
+ date_col = _detect_date_column(master_df)
478
+ if date_col is None:
479
+ return []
480
+ ser = pd.to_datetime(master_df[date_col], errors="coerce")
481
+ months = ser.dt.to_period("M").astype(str).dropna().unique().tolist()
482
+ months.sort()
483
+ return months
484
+
485
+
486
+ def _filter_master_by_month(as_of_month):
487
+ # as_of_month expected like "YYYY-MM"
488
+ date_col = _detect_date_column(master_df)
489
+ if date_col is None or not as_of_month:
490
+ return master_df.copy()
491
+ ser = pd.to_datetime(master_df[date_col], errors="coerce").dt.to_period("M").astype(str)
492
+ return master_df[ser == as_of_month].copy()
493
+
494
+
495
+ def generate_portfolio_overview(as_of_month, segment):
496
+ """
497
+ Returns a small DataFrame with key portfolio snapshot metrics for the selected calendar month and segment.
498
+ Metrics: Total Accounts, Open Accounts (balance>0), Bad Accounts (dpd>=30), Overall NCL Rate (dollar %), Average FICO.
499
+ """
500
+ df = _filter_master_by_month(as_of_month)
501
+
502
+ # If a segmentation column is provided, return per-segment breakdown
503
+ valid_segments = [
504
+ "fico_band",
505
+ "sourcing_channel",
506
+ "city_tier",
507
+ "occupation_type"
508
+ ]
509
+
510
+ if segment in valid_segments and segment in df.columns:
511
+ grp = segment
512
+
513
+ total_accounts = df.groupby(grp)["account_id"].nunique()
514
+
515
+ if "balance" in df.columns:
516
+ open_accounts = (
517
+ df.loc[df["balance"] > 0].groupby(grp)["account_id"].nunique()
518
+ )
519
+ total_balance = df.groupby(grp)["balance"].sum()
520
+ else:
521
+ open_accounts = pd.Series(0, index=total_accounts.index)
522
+ total_balance = pd.Series(0, index=total_accounts.index)
523
+
524
+ if "dpd" in df.columns:
525
+ bad_accounts = (
526
+ df.loc[df["dpd"].fillna(0) >= 30].groupby(grp)["account_id"].nunique()
527
+ )
528
+ bad_balance = (
529
+ df.assign(_bad_balance=df["balance"].where(df["dpd"].fillna(0) >= 30, 0))
530
+ .groupby(grp)["_bad_balance"].sum()
531
+ ) if "balance" in df.columns else pd.Series(0, index=total_accounts.index)
532
+ else:
533
+ bad_accounts = pd.Series(0, index=total_accounts.index)
534
+ bad_balance = pd.Series(0, index=total_accounts.index)
535
+
536
+ # NCL if present
537
+ ncl_cols = [c for c in df.columns if "ncl" in c.lower()]
538
+ if len(ncl_cols) > 0:
539
+ total_ncl = df.groupby(grp)[ncl_cols[0]].sum()
540
+ ncl_rate = (total_ncl / total_balance * 100).round(2).fillna(0)
541
+ else:
542
+ ncl_rate = (bad_balance / total_balance * 100).round(2).fillna(0)
543
+
544
+ # average fico per group if available
545
+ if "fico_score" in df.columns:
546
+ avg_fico = df.groupby(grp)["fico_score"].mean().round(1)
547
+ else:
548
+ avg_fico = pd.Series(float("nan"), index=total_accounts.index)
549
+
550
+ result = pd.DataFrame({
551
+ "Segment": total_accounts.index,
552
+ "Total_Accounts": total_accounts.values,
553
+ "Open_Accounts": open_accounts.reindex(total_accounts.index).fillna(0).astype(int).values,
554
+ "Bad_Accounts": bad_accounts.reindex(total_accounts.index).fillna(0).astype(int).values,
555
+ "Total_Balance": total_balance.reindex(total_accounts.index).fillna(0).values,
556
+ "NCL_Rate_pct": ncl_rate.reindex(total_accounts.index).fillna(0).values,
557
+ "Avg_FICO": avg_fico.reindex(total_accounts.index).fillna(float("nan")).values
558
+ })
559
+
560
+ # Sort by NCL rate descending
561
+ result = result.sort_values("NCL_Rate_pct", ascending=False).reset_index(drop=True)
562
+
563
+ return result
564
+
565
+ # Default: single-line overview
566
+ total_accounts = df["account_id"].nunique() if "account_id" in df.columns else 0
567
+
568
+ open_accounts = (
569
+ df.loc[df["balance"] > 0, "account_id"].nunique()
570
+ if "balance" in df.columns
571
+ else total_accounts
572
+ )
573
+
574
+ bad_accounts = (
575
+ df.loc[df["dpd"].fillna(0) >= 30, "account_id"].nunique()
576
+ if "dpd" in df.columns
577
+ else 0
578
+ )
579
+
580
+ # overall NCL rate (dollar-based) fallback logic
581
+ ncl_cols = [c for c in df.columns if "ncl" in c.lower()]
582
+ overall_ncl_rate = None
583
+ if len(ncl_cols) > 0 and "balance" in df.columns:
584
+ ncl_sum = df[ncl_cols[0]].sum(skipna=True)
585
+ bal_sum = df["balance"].sum(skipna=True)
586
+ overall_ncl_rate = (ncl_sum / bal_sum * 100) if bal_sum > 0 else None
587
+ else:
588
+ # fallback: use bad balance / total balance as proxy
589
+ if "balance" in df.columns and "dpd" in df.columns:
590
+ bad_bal = df.loc[df["dpd"].fillna(0) >= 30, "balance"].sum()
591
+ bal_sum = df["balance"].sum()
592
+ overall_ncl_rate = (bad_bal / bal_sum * 100) if bal_sum > 0 else None
593
+
594
+ if overall_ncl_rate is None:
595
+ overall_ncl_rate = float("nan")
596
+ else:
597
+ overall_ncl_rate = round(overall_ncl_rate, 2)
598
+
599
+ # average fico
600
+ avg_fico = None
601
+ if "fico_score" in df.columns:
602
+ avg_fico = round(df["fico_score"].dropna().mean(), 1)
603
+ elif "fico_band" in df.columns:
604
+ def band_mid(b):
605
+ try:
606
+ parts = b.split("-")
607
+ return (int(parts[0]) + int(parts[1])) / 2
608
+ except Exception:
609
+ return None
610
+ mid_vals = df["fico_band"].dropna().apply(band_mid).dropna()
611
+ avg_fico = round(mid_vals.mean(), 1) if not mid_vals.empty else float("nan")
612
+ else:
613
+ avg_fico = float("nan")
614
+
615
+ overview = pd.DataFrame({
616
+ "Metric": [
617
+ "As Of Month",
618
+ "Total Accounts",
619
+ "Open Accounts",
620
+ "Bad Accounts (dpd>=30)",
621
+ "Overall NCL Rate (%)",
622
+ "Average FICO"
623
+ ],
624
+ "Value": [
625
+ as_of_month if as_of_month else "All",
626
+ int(total_accounts),
627
+ int(open_accounts),
628
+ int(bad_accounts),
629
+ overall_ncl_rate,
630
+ avg_fico
631
+ ]
632
+ })
633
+
634
+ return overview
635
+
636
+ # ---------------------------------------------------
637
+ # DYNAMIC DROPDOWNS
638
+ # ---------------------------------------------------
639
+
640
+ def update_analysis_dropdown(
641
+ dataset
642
+ ):
643
+
644
+ # -----------------------------------------
645
+ # ACQUISITION
646
+ # -----------------------------------------
647
+
648
+ if dataset == "Acquisition":
649
+
650
+ return gr.update(
651
+ choices=[
652
+ "Portfolio Mix",
653
+ "Credit Line Concentration"
654
+ ],
655
+ value="Portfolio Mix"
656
+ )
657
+
658
+ # -----------------------------------------
659
+ # PERFORMANCE
660
+ # -----------------------------------------
661
+
662
+ elif dataset == "Performance":
663
+
664
+ return gr.update(
665
+ choices=[
666
+ "30+@3",
667
+ "30+@6",
668
+ "60+@6",
669
+ "Yr1 NCL"
670
+ ],
671
+ value="30+@6"
672
+ )
673
+
674
+
675
+ def update_category_dropdown(
676
+ dataset
677
+ ):
678
+
679
+ # -----------------------------------------
680
+ # ACQUISITION
681
+ # -----------------------------------------
682
+
683
+ if dataset == "Acquisition":
684
+
685
+ return gr.update(
686
+ choices=[
687
+ "fico_band",
688
+ "sourcing_channel",
689
+ "city_tier",
690
+ "occupation_type"
691
+ ],
692
+ value="fico_band"
693
+ )
694
+
695
+ # -----------------------------------------
696
+ # PERFORMANCE
697
+ # -----------------------------------------
698
+
699
+ elif dataset == "Performance":
700
+
701
+ return gr.update(
702
+ choices=[
703
+ "Overall",
704
+ "Channel",
705
+ "FICO",
706
+ "City Tier",
707
+ "Occupation"
708
+ ],
709
+ value="Overall"
710
+ )
711
+
712
+ # ---------------------------------------------------
713
+ # MASTER ROUTER
714
+ # ---------------------------------------------------
715
+
716
+ def run_analysis(
717
+ dataset,
718
+ analysis,
719
+ category
720
+ ):
721
+
722
+ # -----------------------------------------
723
+ # ACQUISITION
724
+ # -----------------------------------------
725
+
726
+ if dataset == "Acquisition":
727
+
728
+ return run_acquisition_analysis(
729
+ analysis_type=analysis,
730
+ category=category
731
+ )
732
+
733
+ # -----------------------------------------
734
+ # PERFORMANCE
735
+ # -----------------------------------------
736
+
737
+ elif dataset == "Performance":
738
+
739
+ return run_performance_analysis(
740
+ metric_name=analysis,
741
+ view_level=category
742
+ )
743
+
744
+ else:
745
+
746
+ return pd.DataFrame()
747
+
748
+ # ---------------------------------------------------
749
+ # GRADIO UI
750
+ # ---------------------------------------------------
751
+
752
+ with gr.Blocks() as app:
753
+
754
+ gr.Markdown(
755
+ "# Risk Analytics Manager Agent - Phase 2"
756
+ )
757
+
758
+ with gr.Tabs():
759
+
760
+ # =================================================
761
+ # TAB 1: BASIC ANALYSIS (Phase 1)
762
+ # =================================================
763
+
764
+ with gr.TabItem("πŸ“Š Basic Analysis"):
765
+
766
+ gr.Markdown(
767
+ "## Phase 1: Acquisition & Performance Analysis"
768
+ )
769
+
770
+ with gr.Row():
771
+
772
+ dataset_dropdown = gr.Dropdown(
773
+ choices=[
774
+ "Acquisition",
775
+ "Performance"
776
+ ],
777
+ value="Acquisition",
778
+ label="Dataset"
779
+ )
780
+
781
+ analysis_dropdown = gr.Dropdown(
782
+ choices=[
783
+ "Portfolio Mix",
784
+ "Credit Line Concentration"
785
+ ],
786
+ value="Portfolio Mix",
787
+ label="Analysis"
788
+ )
789
+
790
+ category_dropdown = gr.Dropdown(
791
+ choices=[
792
+ "fico_band",
793
+ "sourcing_channel",
794
+ "city_tier",
795
+ "occupation_type"
796
+ ],
797
+ value="fico_band",
798
+ label="Category / View"
799
+ )
800
+
801
+ # -----------------------------------------
802
+ # DYNAMIC DROPDOWNS
803
+ # -----------------------------------------
804
+
805
+ dataset_dropdown.change(
806
+ fn=update_analysis_dropdown,
807
+ inputs=dataset_dropdown,
808
+ outputs=analysis_dropdown
809
+ )
810
+
811
+ dataset_dropdown.change(
812
+ fn=update_category_dropdown,
813
+ inputs=dataset_dropdown,
814
+ outputs=category_dropdown
815
+ )
816
+
817
+ # -----------------------------------------
818
+ # RUN BUTTON
819
+ # -----------------------------------------
820
+
821
+ run_button = gr.Button(
822
+ "Run Analysis",
823
+ variant="primary"
824
+ )
825
+
826
+ output_table = gr.Dataframe()
827
+
828
+ run_button.click(
829
+ fn=run_analysis,
830
+ inputs=[
831
+ dataset_dropdown,
832
+ analysis_dropdown,
833
+ category_dropdown
834
+ ],
835
+ outputs=output_table
836
+ )
837
+
838
+ # =================================================
839
+ # TAB 2: VINTAGE CURVES (Phase 2)
840
+ # =================================================
841
+
842
+ with gr.TabItem("πŸ“ˆ Vintage Curves"):
843
+
844
+ gr.Markdown(
845
+ "## Phase 2: Vintage Delinquency Curves Analysis"
846
+ )
847
+
848
+ with gr.Row():
849
+
850
+ metric_dropdown = gr.Dropdown(
851
+ choices=[
852
+ "30+@3",
853
+ "30+@6",
854
+ "60+@6",
855
+ "Yr1 NCL"
856
+ ],
857
+ value="30+@6",
858
+ label="Delinquency Metric"
859
+ )
860
+
861
+ vintage_chart_type = gr.Radio(
862
+ choices=["Single Metric", "All Metrics Comparison"],
863
+ value="Single Metric",
864
+ label="Chart Type"
865
+ )
866
+
867
+ def update_vintage_view(metric, chart_type):
868
+ if chart_type == "Single Metric":
869
+ return generate_vintage_curve_single(metric)
870
+ else:
871
+ return generate_vintage_curves_comparison()
872
+
873
+ vintage_chart = gr.Plot(
874
+ label="Vintage Curve"
875
+ )
876
+
877
+ gen_vintage_btn = gr.Button(
878
+ "Generate Vintage Curve",
879
+ variant="primary"
880
+ )
881
+
882
+ gen_vintage_btn.click(
883
+ fn=update_vintage_view,
884
+ inputs=[metric_dropdown, vintage_chart_type],
885
+ outputs=vintage_chart
886
+ )
887
+
888
+ gr.Markdown(
889
+ "### Segmented Vintage Curves"
890
+ )
891
+
892
+ with gr.Row():
893
+
894
+ segment_metric = gr.Dropdown(
895
+ choices=[
896
+ "30+@3",
897
+ "30+@6",
898
+ "60+@6",
899
+ "Yr1 NCL"
900
+ ],
901
+ value="30+@6",
902
+ label="Metric"
903
+ )
904
+
905
+ segment_category = gr.Dropdown(
906
+ choices=[
907
+ "fico_band",
908
+ "sourcing_channel",
909
+ "city_tier",
910
+ "occupation_type"
911
+ ],
912
+ value="fico_band",
913
+ label="Category"
914
+ )
915
+
916
+ segmented_chart = gr.Plot(
917
+ label="Segmented Vintage Curve"
918
+ )
919
+
920
+ gen_segment_btn = gr.Button(
921
+ "Generate Segmented Curve",
922
+ variant="primary"
923
+ )
924
+
925
+ gen_segment_btn.click(
926
+ fn=generate_segmented_vintage_curve,
927
+ inputs=[segment_metric, segment_category],
928
+ outputs=segmented_chart
929
+ )
930
+
931
+ # =================================================
932
+ # TAB 3: SEGMENT RANKING (Phase 2)
933
+ # =================================================
934
+
935
+ with gr.TabItem("⚠️ Segment Ranking"):
936
+
937
+ gr.Markdown(
938
+ "## Phase 2: High-Risk Segment Analysis"
939
+ )
940
+
941
+ # --------- HEATMAP SECTION ---------
942
+
943
+ gr.Markdown(
944
+ "### πŸ”₯ Overall Risk Heatmap"
945
+ )
946
+
947
+ gr.Markdown(
948
+ "Risk scores across all delinquency metrics and segments"
949
+ )
950
+
951
+ heatmap_chart = gr.Plot(
952
+ label="Risk Heatmap"
953
+ )
954
+
955
+ gen_heatmap_btn = gr.Button(
956
+ "Generate Risk Heatmap",
957
+ variant="primary"
958
+ )
959
+
960
+ gen_heatmap_btn.click(
961
+ fn=generate_segment_risk_heatmap_chart,
962
+ outputs=heatmap_chart
963
+ )
964
+
965
+ gr.Markdown(
966
+ "---"
967
+ )
968
+
969
+ # --------- HIGH-RISK RANKING SECTION ---------
970
+
971
+ gr.Markdown(
972
+ "### πŸ“Š High-Risk Segments Ranking"
973
+ )
974
+
975
+ with gr.Row():
976
+
977
+ ranking_metric = gr.Dropdown(
978
+ choices=[
979
+ "30+@3",
980
+ "30+@6",
981
+ "60+@6",
982
+ "Yr1 NCL"
983
+ ],
984
+ value="30+@6",
985
+ label="Metric"
986
+ )
987
+
988
+ ranking_category = gr.Dropdown(
989
+ choices=[
990
+ "fico_band",
991
+ "sourcing_channel",
992
+ "city_tier",
993
+ "occupation_type"
994
+ ],
995
+ value="fico_band",
996
+ label="Category"
997
+ )
998
+
999
+ ranking_chart = gr.Plot(
1000
+ label="High-Risk Segments"
1001
+ )
1002
+
1003
+ gen_ranking_btn = gr.Button(
1004
+ "Generate Risk Ranking",
1005
+ variant="primary"
1006
+ )
1007
+
1008
+ gen_ranking_btn.click(
1009
+ fn=generate_high_risk_segments_ranking,
1010
+ inputs=[ranking_metric, ranking_category],
1011
+ outputs=ranking_chart
1012
+ )
1013
+
1014
+ gr.Markdown(
1015
+ "---"
1016
+ )
1017
+
1018
+ # --------- MULTI-CATEGORY COMPARISON ---------
1019
+
1020
+ gr.Markdown(
1021
+ "### πŸ”€ Cross-Category Risk Comparison"
1022
+ )
1023
+
1024
+ comparison_metric = gr.Dropdown(
1025
+ choices=[
1026
+ "30+@3",
1027
+ "30+@6",
1028
+ "60+@6",
1029
+ "Yr1 NCL"
1030
+ ],
1031
+ value="30+@6",
1032
+ label="Metric"
1033
+ )
1034
+
1035
+ comparison_chart = gr.Plot(
1036
+ label="Multi-Category Comparison"
1037
+ )
1038
+
1039
+ gen_comparison_btn = gr.Button(
1040
+ "Generate Comparison",
1041
+ variant="primary"
1042
+ )
1043
+
1044
+ gen_comparison_btn.click(
1045
+ fn=generate_multi_category_comparison,
1046
+ inputs=comparison_metric,
1047
+ outputs=comparison_chart
1048
+ )
1049
+
1050
+ gr.Markdown(
1051
+ "---"
1052
+ )
1053
+
1054
+ # --------- PORTFOLIO SUMMARY ---------
1055
+
1056
+ gr.Markdown(
1057
+ "### πŸ“‹ Portfolio Risk Summary"
1058
+ )
1059
+
1060
+ summary_table = gr.Dataframe(
1061
+ label="Risk Summary"
1062
+ )
1063
+
1064
+ gen_summary_btn = gr.Button(
1065
+ "Generate Summary",
1066
+ variant="primary"
1067
+ )
1068
+
1069
+ gen_summary_btn.click(
1070
+ fn=generate_portfolio_summary,
1071
+ outputs=summary_table
1072
+ )
1073
+
1074
+ # =================================================
1075
+ # TAB 4: PORTFOLIO OVERVIEW (Calendar Snapshot)
1076
+ # =================================================
1077
+
1078
+ with gr.TabItem("πŸ“… Portfolio Overview"):
1079
+
1080
+ gr.Markdown("## Portfolio Snapshot by Calendar Month")
1081
+
1082
+ with gr.Row():
1083
+ calendar_month_dropdown = gr.Dropdown(
1084
+ choices=get_calendar_months(),
1085
+ value=(get_calendar_months()[-1] if len(get_calendar_months()) > 0 else None),
1086
+ label="Calendar Month (YYYY-MM)"
1087
+ )
1088
+
1089
+ overview_segment_dropdown = gr.Dropdown(
1090
+ choices=[
1091
+ "fico_band",
1092
+ "sourcing_channel",
1093
+ "city_tier",
1094
+ "occupation_type"
1095
+ ],
1096
+ value="fico_band",
1097
+ label="Segment (for drill)"
1098
+ )
1099
+
1100
+ gen_overview_btn = gr.Button("Generate Snapshot", variant="primary")
1101
+
1102
+ overview_table = gr.Dataframe(label="Portfolio Overview")
1103
+
1104
+ gen_overview_btn.click(
1105
+ fn=generate_portfolio_overview,
1106
+ inputs=[calendar_month_dropdown, overview_segment_dropdown],
1107
+ outputs=overview_table
1108
+ )
1109
+
1110
+ # =================================================
1111
+ # TAB 5: DEEP DIVE AGENTIC ANALYSIS
1112
+ # =================================================
1113
+
1114
+ with gr.TabItem("πŸ”¬ Deep Dive Analysis"):
1115
+
1116
+ gr.Markdown(
1117
+ "## Agentic Code Generation & Execution\n\n"
1118
+ "Ask a complex question and the AI will:\n"
1119
+ "1. Generate pandas code to analyze the data\n"
1120
+ "2. Execute the code safely\n"
1121
+ "3. Interpret the results with risk management insights"
1122
+ )
1123
+
1124
+ deep_dive_question = gr.Textbox(
1125
+ lines=4,
1126
+ placeholder="E.g., 'Compare 30+ delinquency trends across fico bands and identify worst performing vintages.'",
1127
+ label="Analysis Question"
1128
+ )
1129
+
1130
+ deep_dive_run_btn = gr.Button("Run Deep Dive Analysis", variant="primary")
1131
+
1132
+ deep_dive_output = gr.Markdown(label="Analysis Output")
1133
+
1134
+ deep_dive_run_btn.click(
1135
+ fn=run_deep_dive_on_question,
1136
+ inputs=deep_dive_question,
1137
+ outputs=deep_dive_output
1138
+ )
1139
+
1140
+ # =================================================
1141
+ # TAB 6: CHAT WITH YOUR DATA
1142
+ # =================================================
1143
+
1144
+ with gr.TabItem("πŸ’¬ Chat with Your Data"):
1145
+
1146
+ gr.Markdown(
1147
+ "## Ask questions in plain language and get risk manager insights"
1148
+ )
1149
+
1150
+ with gr.Row():
1151
+ ai_month_dropdown = gr.Dropdown(
1152
+ choices=["All"] + get_calendar_months(),
1153
+ value="All" if len(get_calendar_months()) > 0 else None,
1154
+ label="As Of Month (YYYY-MM or All)"
1155
+ )
1156
+
1157
+ ai_segment_dropdown = gr.Dropdown(
1158
+ choices=["", "fico_band", "sourcing_channel", "city_tier", "occupation_type"],
1159
+ value="",
1160
+ label="Segment Context"
1161
+ )
1162
+
1163
+ ai_question = gr.Textbox(
1164
+ lines=3,
1165
+ placeholder="Ask about NCL, vintage performance, high-risk segments, or portfolio trends..."
1166
+ )
1167
+
1168
+ ai_chatbot = gr.Chatbot()
1169
+ ai_history = gr.State([])
1170
+
1171
+ ai_ask_btn = gr.Button("Ask AI", variant="primary")
1172
+
1173
+ ai_ask_btn.click(
1174
+ fn=ask_ai_question,
1175
+ inputs=[ai_question, ai_month_dropdown, ai_segment_dropdown, ai_history],
1176
+ outputs=[ai_chatbot, ai_history, ai_question]
1177
+ )
1178
+
1179
+ app.launch()