GenAICoder commited on
Commit
885bddc
Β·
verified Β·
1 Parent(s): 5920b4c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1104 -0
app.py ADDED
@@ -0,0 +1,1104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 [], ""
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({"role": "user", "content": question})
405
+ history.append({"role": "assistant", "content": answer})
406
+ return history, history, ""
407
+
408
+
409
+ # ---------------------------------------------------
410
+ # PORTFOLIO OVERVIEW (Calendar Snapshot)
411
+ # ---------------------------------------------------
412
+
413
+
414
+ def _detect_date_column(df):
415
+ candidates = [
416
+ "reporting_month",
417
+ "observation_date",
418
+ "observation_month",
419
+ "obs_date",
420
+ "date",
421
+ "calendar_month",
422
+ "month",
423
+ "report_date"
424
+ ]
425
+ for c in candidates:
426
+ if c in df.columns:
427
+ return c
428
+ return None
429
+
430
+
431
+ def get_calendar_months():
432
+ date_col = _detect_date_column(master_df)
433
+ if date_col is None:
434
+ return []
435
+ ser = pd.to_datetime(master_df[date_col], errors="coerce")
436
+ months = ser.dt.to_period("M").astype(str).dropna().unique().tolist()
437
+ months.sort()
438
+ return months
439
+
440
+
441
+ def _filter_master_by_month(as_of_month):
442
+ # as_of_month expected like "YYYY-MM"
443
+ date_col = _detect_date_column(master_df)
444
+ if date_col is None or not as_of_month:
445
+ return master_df.copy()
446
+ ser = pd.to_datetime(master_df[date_col], errors="coerce").dt.to_period("M").astype(str)
447
+ return master_df[ser == as_of_month].copy()
448
+
449
+
450
+ def generate_portfolio_overview(as_of_month, segment):
451
+ """
452
+ Returns a small DataFrame with key portfolio snapshot metrics for the selected calendar month and segment.
453
+ Metrics: Total Accounts, Open Accounts (balance>0), Bad Accounts (dpd>=30), Overall NCL Rate (dollar %), Average FICO.
454
+ """
455
+ df = _filter_master_by_month(as_of_month)
456
+
457
+ # If a segmentation column is provided, return per-segment breakdown
458
+ valid_segments = [
459
+ "fico_band",
460
+ "sourcing_channel",
461
+ "city_tier",
462
+ "occupation_type"
463
+ ]
464
+
465
+ if segment in valid_segments and segment in df.columns:
466
+ grp = segment
467
+
468
+ total_accounts = df.groupby(grp)["account_id"].nunique()
469
+
470
+ if "balance" in df.columns:
471
+ open_accounts = (
472
+ df.loc[df["balance"] > 0].groupby(grp)["account_id"].nunique()
473
+ )
474
+ total_balance = df.groupby(grp)["balance"].sum()
475
+ else:
476
+ open_accounts = pd.Series(0, index=total_accounts.index)
477
+ total_balance = pd.Series(0, index=total_accounts.index)
478
+
479
+ if "dpd" in df.columns:
480
+ bad_accounts = (
481
+ df.loc[df["dpd"].fillna(0) >= 30].groupby(grp)["account_id"].nunique()
482
+ )
483
+ bad_balance = (
484
+ df.assign(_bad_balance=df["balance"].where(df["dpd"].fillna(0) >= 30, 0))
485
+ .groupby(grp)["_bad_balance"].sum()
486
+ ) if "balance" in df.columns else pd.Series(0, index=total_accounts.index)
487
+ else:
488
+ bad_accounts = pd.Series(0, index=total_accounts.index)
489
+ bad_balance = pd.Series(0, index=total_accounts.index)
490
+
491
+ # NCL if present
492
+ ncl_cols = [c for c in df.columns if "ncl" in c.lower()]
493
+ if len(ncl_cols) > 0:
494
+ total_ncl = df.groupby(grp)[ncl_cols[0]].sum()
495
+ ncl_rate = (total_ncl / total_balance * 100).round(2).fillna(0)
496
+ else:
497
+ ncl_rate = (bad_balance / total_balance * 100).round(2).fillna(0)
498
+
499
+ # average fico per group if available
500
+ if "fico_score" in df.columns:
501
+ avg_fico = df.groupby(grp)["fico_score"].mean().round(1)
502
+ else:
503
+ avg_fico = pd.Series(float("nan"), index=total_accounts.index)
504
+
505
+ result = pd.DataFrame({
506
+ "Segment": total_accounts.index,
507
+ "Total_Accounts": total_accounts.values,
508
+ "Open_Accounts": open_accounts.reindex(total_accounts.index).fillna(0).astype(int).values,
509
+ "Bad_Accounts": bad_accounts.reindex(total_accounts.index).fillna(0).astype(int).values,
510
+ "Total_Balance": total_balance.reindex(total_accounts.index).fillna(0).values,
511
+ "NCL_Rate_pct": ncl_rate.reindex(total_accounts.index).fillna(0).values,
512
+ "Avg_FICO": avg_fico.reindex(total_accounts.index).fillna(float("nan")).values
513
+ })
514
+
515
+ # Sort by NCL rate descending
516
+ result = result.sort_values("NCL_Rate_pct", ascending=False).reset_index(drop=True)
517
+
518
+ return result
519
+
520
+ # Default: single-line overview
521
+ total_accounts = df["account_id"].nunique() if "account_id" in df.columns else 0
522
+
523
+ open_accounts = (
524
+ df.loc[df["balance"] > 0, "account_id"].nunique()
525
+ if "balance" in df.columns
526
+ else total_accounts
527
+ )
528
+
529
+ bad_accounts = (
530
+ df.loc[df["dpd"].fillna(0) >= 30, "account_id"].nunique()
531
+ if "dpd" in df.columns
532
+ else 0
533
+ )
534
+
535
+ # overall NCL rate (dollar-based) fallback logic
536
+ ncl_cols = [c for c in df.columns if "ncl" in c.lower()]
537
+ overall_ncl_rate = None
538
+ if len(ncl_cols) > 0 and "balance" in df.columns:
539
+ ncl_sum = df[ncl_cols[0]].sum(skipna=True)
540
+ bal_sum = df["balance"].sum(skipna=True)
541
+ overall_ncl_rate = (ncl_sum / bal_sum * 100) if bal_sum > 0 else None
542
+ else:
543
+ # fallback: use bad balance / total balance as proxy
544
+ if "balance" in df.columns and "dpd" in df.columns:
545
+ bad_bal = df.loc[df["dpd"].fillna(0) >= 30, "balance"].sum()
546
+ bal_sum = df["balance"].sum()
547
+ overall_ncl_rate = (bad_bal / bal_sum * 100) if bal_sum > 0 else None
548
+
549
+ if overall_ncl_rate is None:
550
+ overall_ncl_rate = float("nan")
551
+ else:
552
+ overall_ncl_rate = round(overall_ncl_rate, 2)
553
+
554
+ # average fico
555
+ avg_fico = None
556
+ if "fico_score" in df.columns:
557
+ avg_fico = round(df["fico_score"].dropna().mean(), 1)
558
+ elif "fico_band" in df.columns:
559
+ def band_mid(b):
560
+ try:
561
+ parts = b.split("-")
562
+ return (int(parts[0]) + int(parts[1])) / 2
563
+ except Exception:
564
+ return None
565
+ mid_vals = df["fico_band"].dropna().apply(band_mid).dropna()
566
+ avg_fico = round(mid_vals.mean(), 1) if not mid_vals.empty else float("nan")
567
+ else:
568
+ avg_fico = float("nan")
569
+
570
+ overview = pd.DataFrame({
571
+ "Metric": [
572
+ "As Of Month",
573
+ "Total Accounts",
574
+ "Open Accounts",
575
+ "Bad Accounts (dpd>=30)",
576
+ "Overall NCL Rate (%)",
577
+ "Average FICO"
578
+ ],
579
+ "Value": [
580
+ as_of_month if as_of_month else "All",
581
+ int(total_accounts),
582
+ int(open_accounts),
583
+ int(bad_accounts),
584
+ overall_ncl_rate,
585
+ avg_fico
586
+ ]
587
+ })
588
+
589
+ return overview
590
+
591
+ # ---------------------------------------------------
592
+ # DYNAMIC DROPDOWNS
593
+ # ---------------------------------------------------
594
+
595
+ def update_analysis_dropdown(
596
+ dataset
597
+ ):
598
+
599
+ # -----------------------------------------
600
+ # ACQUISITION
601
+ # -----------------------------------------
602
+
603
+ if dataset == "Acquisition":
604
+
605
+ return gr.update(
606
+ choices=[
607
+ "Portfolio Mix",
608
+ "Credit Line Concentration"
609
+ ],
610
+ value="Portfolio Mix"
611
+ )
612
+
613
+ # -----------------------------------------
614
+ # PERFORMANCE
615
+ # -----------------------------------------
616
+
617
+ elif dataset == "Performance":
618
+
619
+ return gr.update(
620
+ choices=[
621
+ "30+@3",
622
+ "30+@6",
623
+ "60+@6",
624
+ "Yr1 NCL"
625
+ ],
626
+ value="30+@6"
627
+ )
628
+
629
+
630
+ def update_category_dropdown(
631
+ dataset
632
+ ):
633
+
634
+ # -----------------------------------------
635
+ # ACQUISITION
636
+ # -----------------------------------------
637
+
638
+ if dataset == "Acquisition":
639
+
640
+ return gr.update(
641
+ choices=[
642
+ "fico_band",
643
+ "sourcing_channel",
644
+ "city_tier",
645
+ "occupation_type"
646
+ ],
647
+ value="fico_band"
648
+ )
649
+
650
+ # -----------------------------------------
651
+ # PERFORMANCE
652
+ # -----------------------------------------
653
+
654
+ elif dataset == "Performance":
655
+
656
+ return gr.update(
657
+ choices=[
658
+ "Overall",
659
+ "Channel",
660
+ "FICO",
661
+ "City Tier",
662
+ "Occupation"
663
+ ],
664
+ value="Overall"
665
+ )
666
+
667
+ # ---------------------------------------------------
668
+ # MASTER ROUTER
669
+ # ---------------------------------------------------
670
+
671
+ def run_analysis(
672
+ dataset,
673
+ analysis,
674
+ category
675
+ ):
676
+
677
+ # -----------------------------------------
678
+ # ACQUISITION
679
+ # -----------------------------------------
680
+
681
+ if dataset == "Acquisition":
682
+
683
+ return run_acquisition_analysis(
684
+ analysis_type=analysis,
685
+ category=category
686
+ )
687
+
688
+ # -----------------------------------------
689
+ # PERFORMANCE
690
+ # -----------------------------------------
691
+
692
+ elif dataset == "Performance":
693
+
694
+ return run_performance_analysis(
695
+ metric_name=analysis,
696
+ view_level=category
697
+ )
698
+
699
+ else:
700
+
701
+ return pd.DataFrame()
702
+
703
+ # ---------------------------------------------------
704
+ # GRADIO UI
705
+ # ---------------------------------------------------
706
+
707
+ with gr.Blocks() as app:
708
+
709
+ gr.Markdown(
710
+ "# Risk Analytics Manager Agent - Phase 2"
711
+ )
712
+
713
+ with gr.Tabs():
714
+
715
+ # =================================================
716
+ # TAB 1: BASIC ANALYSIS (Phase 1)
717
+ # =================================================
718
+
719
+ with gr.TabItem("πŸ“Š Basic Analysis"):
720
+
721
+ gr.Markdown(
722
+ "## Phase 1: Acquisition & Performance Analysis"
723
+ )
724
+
725
+ with gr.Row():
726
+
727
+ dataset_dropdown = gr.Dropdown(
728
+ choices=[
729
+ "Acquisition",
730
+ "Performance"
731
+ ],
732
+ value="Acquisition",
733
+ label="Dataset"
734
+ )
735
+
736
+ analysis_dropdown = gr.Dropdown(
737
+ choices=[
738
+ "Portfolio Mix",
739
+ "Credit Line Concentration"
740
+ ],
741
+ value="Portfolio Mix",
742
+ label="Analysis"
743
+ )
744
+
745
+ category_dropdown = gr.Dropdown(
746
+ choices=[
747
+ "fico_band",
748
+ "sourcing_channel",
749
+ "city_tier",
750
+ "occupation_type"
751
+ ],
752
+ value="fico_band",
753
+ label="Category / View"
754
+ )
755
+
756
+ # -----------------------------------------
757
+ # DYNAMIC DROPDOWNS
758
+ # -----------------------------------------
759
+
760
+ dataset_dropdown.change(
761
+ fn=update_analysis_dropdown,
762
+ inputs=dataset_dropdown,
763
+ outputs=analysis_dropdown
764
+ )
765
+
766
+ dataset_dropdown.change(
767
+ fn=update_category_dropdown,
768
+ inputs=dataset_dropdown,
769
+ outputs=category_dropdown
770
+ )
771
+
772
+ # -----------------------------------------
773
+ # RUN BUTTON
774
+ # -----------------------------------------
775
+
776
+ run_button = gr.Button(
777
+ "Run Analysis",
778
+ variant="primary"
779
+ )
780
+
781
+ output_table = gr.Dataframe()
782
+
783
+ run_button.click(
784
+ fn=run_analysis,
785
+ inputs=[
786
+ dataset_dropdown,
787
+ analysis_dropdown,
788
+ category_dropdown
789
+ ],
790
+ outputs=output_table
791
+ )
792
+
793
+ # =================================================
794
+ # TAB 2: VINTAGE CURVES (Phase 2)
795
+ # =================================================
796
+
797
+ with gr.TabItem("πŸ“ˆ Vintage Curves"):
798
+
799
+ gr.Markdown(
800
+ "## Phase 2: Vintage Delinquency Curves Analysis"
801
+ )
802
+
803
+ with gr.Row():
804
+
805
+ metric_dropdown = gr.Dropdown(
806
+ choices=[
807
+ "30+@3",
808
+ "30+@6",
809
+ "60+@6",
810
+ "Yr1 NCL"
811
+ ],
812
+ value="30+@6",
813
+ label="Delinquency Metric"
814
+ )
815
+
816
+ vintage_chart_type = gr.Radio(
817
+ choices=["Single Metric", "All Metrics Comparison"],
818
+ value="Single Metric",
819
+ label="Chart Type"
820
+ )
821
+
822
+ def update_vintage_view(metric, chart_type):
823
+ if chart_type == "Single Metric":
824
+ return generate_vintage_curve_single(metric)
825
+ else:
826
+ return generate_vintage_curves_comparison()
827
+
828
+ vintage_chart = gr.Plot(
829
+ label="Vintage Curve"
830
+ )
831
+
832
+ gen_vintage_btn = gr.Button(
833
+ "Generate Vintage Curve",
834
+ variant="primary"
835
+ )
836
+
837
+ gen_vintage_btn.click(
838
+ fn=update_vintage_view,
839
+ inputs=[metric_dropdown, vintage_chart_type],
840
+ outputs=vintage_chart
841
+ )
842
+
843
+ gr.Markdown(
844
+ "### Segmented Vintage Curves"
845
+ )
846
+
847
+ with gr.Row():
848
+
849
+ segment_metric = gr.Dropdown(
850
+ choices=[
851
+ "30+@3",
852
+ "30+@6",
853
+ "60+@6",
854
+ "Yr1 NCL"
855
+ ],
856
+ value="30+@6",
857
+ label="Metric"
858
+ )
859
+
860
+ segment_category = gr.Dropdown(
861
+ choices=[
862
+ "fico_band",
863
+ "sourcing_channel",
864
+ "city_tier",
865
+ "occupation_type"
866
+ ],
867
+ value="fico_band",
868
+ label="Category"
869
+ )
870
+
871
+ segmented_chart = gr.Plot(
872
+ label="Segmented Vintage Curve"
873
+ )
874
+
875
+ gen_segment_btn = gr.Button(
876
+ "Generate Segmented Curve",
877
+ variant="primary"
878
+ )
879
+
880
+ gen_segment_btn.click(
881
+ fn=generate_segmented_vintage_curve,
882
+ inputs=[segment_metric, segment_category],
883
+ outputs=segmented_chart
884
+ )
885
+
886
+ # =================================================
887
+ # TAB 3: SEGMENT RANKING (Phase 2)
888
+ # =================================================
889
+
890
+ with gr.TabItem("⚠️ Segment Ranking"):
891
+
892
+ gr.Markdown(
893
+ "## Phase 2: High-Risk Segment Analysis"
894
+ )
895
+
896
+ # --------- HEATMAP SECTION ---------
897
+
898
+ gr.Markdown(
899
+ "### πŸ”₯ Overall Risk Heatmap"
900
+ )
901
+
902
+ gr.Markdown(
903
+ "Risk scores across all delinquency metrics and segments"
904
+ )
905
+
906
+ heatmap_chart = gr.Plot(
907
+ label="Risk Heatmap"
908
+ )
909
+
910
+ gen_heatmap_btn = gr.Button(
911
+ "Generate Risk Heatmap",
912
+ variant="primary"
913
+ )
914
+
915
+ gen_heatmap_btn.click(
916
+ fn=generate_segment_risk_heatmap_chart,
917
+ outputs=heatmap_chart
918
+ )
919
+
920
+ gr.Markdown(
921
+ "---"
922
+ )
923
+
924
+ # --------- HIGH-RISK RANKING SECTION ---------
925
+
926
+ gr.Markdown(
927
+ "### πŸ“Š High-Risk Segments Ranking"
928
+ )
929
+
930
+ with gr.Row():
931
+
932
+ ranking_metric = gr.Dropdown(
933
+ choices=[
934
+ "30+@3",
935
+ "30+@6",
936
+ "60+@6",
937
+ "Yr1 NCL"
938
+ ],
939
+ value="30+@6",
940
+ label="Metric"
941
+ )
942
+
943
+ ranking_category = gr.Dropdown(
944
+ choices=[
945
+ "fico_band",
946
+ "sourcing_channel",
947
+ "city_tier",
948
+ "occupation_type"
949
+ ],
950
+ value="fico_band",
951
+ label="Category"
952
+ )
953
+
954
+ ranking_chart = gr.Plot(
955
+ label="High-Risk Segments"
956
+ )
957
+
958
+ gen_ranking_btn = gr.Button(
959
+ "Generate Risk Ranking",
960
+ variant="primary"
961
+ )
962
+
963
+ gen_ranking_btn.click(
964
+ fn=generate_high_risk_segments_ranking,
965
+ inputs=[ranking_metric, ranking_category],
966
+ outputs=ranking_chart
967
+ )
968
+
969
+ gr.Markdown(
970
+ "---"
971
+ )
972
+
973
+ # --------- MULTI-CATEGORY COMPARISON ---------
974
+
975
+ gr.Markdown(
976
+ "### πŸ”€ Cross-Category Risk Comparison"
977
+ )
978
+
979
+ comparison_metric = gr.Dropdown(
980
+ choices=[
981
+ "30+@3",
982
+ "30+@6",
983
+ "60+@6",
984
+ "Yr1 NCL"
985
+ ],
986
+ value="30+@6",
987
+ label="Metric"
988
+ )
989
+
990
+ comparison_chart = gr.Plot(
991
+ label="Multi-Category Comparison"
992
+ )
993
+
994
+ gen_comparison_btn = gr.Button(
995
+ "Generate Comparison",
996
+ variant="primary"
997
+ )
998
+
999
+ gen_comparison_btn.click(
1000
+ fn=generate_multi_category_comparison,
1001
+ inputs=comparison_metric,
1002
+ outputs=comparison_chart
1003
+ )
1004
+
1005
+ gr.Markdown(
1006
+ "---"
1007
+ )
1008
+
1009
+ # --------- PORTFOLIO SUMMARY ---------
1010
+
1011
+ gr.Markdown(
1012
+ "### πŸ“‹ Portfolio Risk Summary"
1013
+ )
1014
+
1015
+ summary_table = gr.Dataframe(
1016
+ label="Risk Summary"
1017
+ )
1018
+
1019
+ gen_summary_btn = gr.Button(
1020
+ "Generate Summary",
1021
+ variant="primary"
1022
+ )
1023
+
1024
+ gen_summary_btn.click(
1025
+ fn=generate_portfolio_summary,
1026
+ outputs=summary_table
1027
+ )
1028
+
1029
+ # =================================================
1030
+ # TAB 4: PORTFOLIO OVERVIEW (Calendar Snapshot)
1031
+ # =================================================
1032
+
1033
+ with gr.TabItem("πŸ“… Portfolio Overview"):
1034
+
1035
+ gr.Markdown("## Portfolio Snapshot by Calendar Month")
1036
+
1037
+ with gr.Row():
1038
+ calendar_month_dropdown = gr.Dropdown(
1039
+ choices=get_calendar_months(),
1040
+ value=(get_calendar_months()[-1] if len(get_calendar_months()) > 0 else None),
1041
+ label="Calendar Month (YYYY-MM)"
1042
+ )
1043
+
1044
+ overview_segment_dropdown = gr.Dropdown(
1045
+ choices=[
1046
+ "fico_band",
1047
+ "sourcing_channel",
1048
+ "city_tier",
1049
+ "occupation_type"
1050
+ ],
1051
+ value="fico_band",
1052
+ label="Segment (for drill)"
1053
+ )
1054
+
1055
+ gen_overview_btn = gr.Button("Generate Snapshot", variant="primary")
1056
+
1057
+ overview_table = gr.Dataframe(label="Portfolio Overview")
1058
+
1059
+ gen_overview_btn.click(
1060
+ fn=generate_portfolio_overview,
1061
+ inputs=[calendar_month_dropdown, overview_segment_dropdown],
1062
+ outputs=overview_table
1063
+ )
1064
+
1065
+ # =================================================
1066
+ # TAB 5: CHAT WITH YOUR DATA
1067
+ # =================================================
1068
+
1069
+ with gr.TabItem("πŸ’¬ Chat with Your Data"):
1070
+
1071
+ gr.Markdown(
1072
+ "## Ask questions in plain language and get risk manager insights"
1073
+ )
1074
+
1075
+ with gr.Row():
1076
+ ai_month_dropdown = gr.Dropdown(
1077
+ choices=["All"] + get_calendar_months(),
1078
+ value="All" if len(get_calendar_months()) > 0 else None,
1079
+ label="As Of Month (YYYY-MM or All)"
1080
+ )
1081
+
1082
+ ai_segment_dropdown = gr.Dropdown(
1083
+ choices=["", "fico_band", "sourcing_channel", "city_tier", "occupation_type"],
1084
+ value="",
1085
+ label="Segment Context"
1086
+ )
1087
+
1088
+ ai_question = gr.Textbox(
1089
+ lines=3,
1090
+ placeholder="Ask about NCL, vintage performance, high-risk segments, or portfolio trends..."
1091
+ )
1092
+
1093
+ ai_chatbot = gr.Chatbot()
1094
+ ai_history = gr.State([])
1095
+
1096
+ ai_ask_btn = gr.Button("Ask AI", variant="primary")
1097
+
1098
+ ai_ask_btn.click(
1099
+ fn=ask_ai_question,
1100
+ inputs=[ai_question, ai_month_dropdown, ai_segment_dropdown, ai_history],
1101
+ outputs=[ai_chatbot, ai_history, ai_question]
1102
+ )
1103
+
1104
+ app.launch()