Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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()
|