GenAICoder commited on
Commit
3ae1a2b
Β·
verified Β·
1 Parent(s): b45bcd6

Create app.py

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