GenAICoder commited on
Commit
e03bba4
·
verified ·
1 Parent(s): 0f25d3c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -0
app.py CHANGED
@@ -382,6 +382,127 @@ def generate_portfolio_summary():
382
  except Exception as e:
383
  return f"Error generating summary: {str(e)}"
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  # ---------------------------------------------------
386
  # DYNAMIC DROPDOWNS
387
  # ---------------------------------------------------
@@ -820,4 +941,40 @@ with gr.Blocks() as app:
820
  outputs=summary_table
821
  )
822
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  app.launch()
 
382
  except Exception as e:
383
  return f"Error generating summary: {str(e)}"
384
 
385
+
386
+ # ---------------------------------------------------
387
+ # PORTFOLIO OVERVIEW (Calendar Snapshot)
388
+ # ---------------------------------------------------
389
+
390
+
391
+ def _detect_date_column(df):
392
+ candidates = [
393
+ "observation_date",
394
+ "observation_month",
395
+ "obs_date",
396
+ "date",
397
+ "calendar_month",
398
+ "month",
399
+ "report_date"
400
+ ]
401
+ for c in candidates:
402
+ if c in df.columns:
403
+ return c
404
+ return None
405
+
406
+
407
+ def get_calendar_months():
408
+ date_col = _detect_date_column(master_df)
409
+ if date_col is None:
410
+ return []
411
+ ser = pd.to_datetime(master_df[date_col], errors="coerce")
412
+ months = ser.dt.to_period("M").astype(str).dropna().unique().tolist()
413
+ months.sort()
414
+ return months
415
+
416
+
417
+ def _filter_master_by_month(as_of_month):
418
+ # as_of_month expected like "YYYY-MM"
419
+ date_col = _detect_date_column(master_df)
420
+ if date_col is None or not as_of_month:
421
+ return master_df.copy()
422
+ ser = pd.to_datetime(master_df[date_col], errors="coerce").dt.to_period("M").astype(str)
423
+ return master_df[ser == as_of_month].copy()
424
+
425
+
426
+ def generate_portfolio_overview(as_of_month, segment):
427
+ """
428
+ Returns a small DataFrame with key portfolio snapshot metrics for the selected calendar month and segment.
429
+ Metrics: Total Accounts, Open Accounts (balance>0), Bad Accounts (dpd>=30), Overall NCL Rate (dollar %), Average FICO.
430
+ """
431
+ df = _filter_master_by_month(as_of_month)
432
+
433
+ # If segment filtering required in future, `segment` param can be used to filter further
434
+
435
+ total_accounts = df["account_id"].nunique() if "account_id" in df.columns else 0
436
+
437
+ open_accounts = (
438
+ df.loc[df["balance"] > 0, "account_id"].nunique()
439
+ if "balance" in df.columns
440
+ else total_accounts
441
+ )
442
+
443
+ bad_accounts = (
444
+ df.loc[df["dpd"].fillna(0) >= 30, "account_id"].nunique()
445
+ if "dpd" in df.columns
446
+ else 0
447
+ )
448
+
449
+ # overall NCL rate (dollar-based) fallback logic
450
+ ncl_cols = [c for c in df.columns if "ncl" in c.lower()]
451
+ overall_ncl_rate = None
452
+ if len(ncl_cols) > 0 and "balance" in df.columns:
453
+ ncl_sum = df[ncl_cols[0]].sum(skipna=True)
454
+ bal_sum = df["balance"].sum(skipna=True)
455
+ overall_ncl_rate = (ncl_sum / bal_sum * 100) if bal_sum > 0 else None
456
+ else:
457
+ # fallback: use bad balance / total balance as proxy
458
+ if "balance" in df.columns and "dpd" in df.columns:
459
+ bad_bal = df.loc[df["dpd"].fillna(0) >= 30, "balance"].sum()
460
+ bal_sum = df["balance"].sum()
461
+ overall_ncl_rate = (bad_bal / bal_sum * 100) if bal_sum > 0 else None
462
+
463
+ if overall_ncl_rate is None:
464
+ overall_ncl_rate = float("nan")
465
+ else:
466
+ overall_ncl_rate = round(overall_ncl_rate, 2)
467
+
468
+ # average fico
469
+ avg_fico = None
470
+ if "fico_score" in df.columns:
471
+ avg_fico = round(df["fico_score"].dropna().mean(), 1)
472
+ elif "fico_band" in df.columns:
473
+ # attempt to map band midpoints like "600-650" -> 625
474
+ def band_mid(b):
475
+ try:
476
+ parts = b.split("-")
477
+ return (int(parts[0]) + int(parts[1])) / 2
478
+ except Exception:
479
+ return None
480
+ mid_vals = df["fico_band"].dropna().apply(band_mid).dropna()
481
+ avg_fico = round(mid_vals.mean(), 1) if not mid_vals.empty else float("nan")
482
+ else:
483
+ avg_fico = float("nan")
484
+
485
+ overview = pd.DataFrame({
486
+ "Metric": [
487
+ "As Of Month",
488
+ "Total Accounts",
489
+ "Open Accounts",
490
+ "Bad Accounts (dpd>=30)",
491
+ "Overall NCL Rate (%)",
492
+ "Average FICO"
493
+ ],
494
+ "Value": [
495
+ as_of_month if as_of_month else "All",
496
+ int(total_accounts),
497
+ int(open_accounts),
498
+ int(bad_accounts),
499
+ overall_ncl_rate,
500
+ avg_fico
501
+ ]
502
+ })
503
+
504
+ return overview
505
+
506
  # ---------------------------------------------------
507
  # DYNAMIC DROPDOWNS
508
  # ---------------------------------------------------
 
941
  outputs=summary_table
942
  )
943
 
944
+ # =================================================
945
+ # TAB 4: PORTFOLIO OVERVIEW (Calendar Snapshot)
946
+ # =================================================
947
+
948
+ with gr.TabItem("📅 Portfolio Overview"):
949
+
950
+ gr.Markdown("## Portfolio Snapshot by Calendar Month")
951
+
952
+ with gr.Row():
953
+ calendar_month_dropdown = gr.Dropdown(
954
+ choices=get_calendar_months(),
955
+ value=(get_calendar_months()[-1] if len(get_calendar_months()) > 0 else None),
956
+ label="Calendar Month (YYYY-MM)"
957
+ )
958
+
959
+ overview_segment_dropdown = gr.Dropdown(
960
+ choices=[
961
+ "fico_band",
962
+ "sourcing_channel",
963
+ "city_tier",
964
+ "occupation_type"
965
+ ],
966
+ value="fico_band",
967
+ label="Segment (for drill)"
968
+ )
969
+
970
+ gen_overview_btn = gr.Button("Generate Snapshot", variant="primary")
971
+
972
+ overview_table = gr.Dataframe(label="Portfolio Overview")
973
+
974
+ gen_overview_btn.click(
975
+ fn=generate_portfolio_overview,
976
+ inputs=[calendar_month_dropdown, overview_segment_dropdown],
977
+ outputs=overview_table
978
+ )
979
+
980
  app.launch()