Personal-Loan Portfolio Analytics with dbt + DuckDB + Streamlit
A production-style FinTech analytics platform that models 100K+ loan applications, simulates a realistic 18-month payment lifecycle, builds analytical marts with dbt, and surfaces credit-risk and unit-economics insights through an interactive 6-page Streamlit dashboard with a live approval-threshold scenario simulator.
A digital lender wants to grow approvals while controlling credit losses. Leadership needs answers to five questions:
- Which customer segments are profitable?
- Which approved customers become delinquent?
- How do approval rules affect revenue and loss?
- What is the expected loss by vintage?
- What happens to profit if approval thresholds are loosened or tightened?
| Finding | Detail |
|---|---|
| Portfolio scale | 79K funded loans, $1.1B funded principal |
| Charge-off rate | 3.1% (realistic personal-loan range: 2-7%) |
| 30+ DPD peaks | Months 3-5 on book — classic vintage hump |
| Risk-revenue tradeoff | Raising threshold from 600→740 cuts losses 78% but cuts revenue 75% |
| Highest-margin segment | Good/Very-Good credit bands via referral channel |
| CAC payback | <1 month average — driven by interest revenue front-loading |
| Layer | Tool | Why |
|---|---|---|
| Warehouse | DuckDB | File-based, zero infra, dbt-supported |
| Transformation | dbt-core + dbt-duckdb | Industry-standard FinTech tool |
| Tests / contracts | dbt schema tests + singular SQL tests | Standard data-quality layer |
| Analysis | Python (pandas, statsmodels, scikit-learn) | Forecasts, simulation, ML scoring |
| Dashboard | Streamlit + Plotly | Interactive, deployable |
| Documentation | Sphinx + Furo | Production-quality docs |
| Environment | pyenv + hatchling | Reproducible builds |
Synthetic Data Generator (Python)
↓ 6 Parquet files in data/raw/
dbt Project
↓ sources.yml → 6 staging views (with schema tests)
↓ intermediate models (loan lifetime, payment history)
↓ 5 mart tables (applications, credit_risk, vintage_curves, unit_economics, portfolio_daily)
↓ + 2 seeds (APR by risk, CAC by channel) + 2 macros + 110 data tests
DuckDB Warehouse (data/fintech.duckdb)
↓
Python Analysis (vintage curves, Holt-Winters forecast, threshold simulator, logistic-regression PD model)
↓
Streamlit Dashboard (6 pages with interactive simulator)
Prerequisites: pyenv with pyenv-virtualenv.
# 1. Create the virtual environment
pyenv install -s 3.13.3
pyenv virtualenv 3.13.3 fintech_env
pyenv local fintech_env
# 2. Install all dependencies (Python + dbt-utils)
make all-env
# 3. Run the full pipeline (generate data → dbt build → Python analysis)
make pipeline
# 4. Launch the dashboard
make appThe dashboard opens at http://localhost:8501.
Run make help to see all available targets.
| Page | What it shows |
|---|---|
| Executive Overview | Portfolio KPIs, 90-day trends, charge-off rate |
| Credit Risk | DPD distribution, segment-level risk, charge-off rates by credit band |
| Vintage Curves | Plotly heatmap (vintage × MOB) — the classic FinTech credit-risk view |
| Unit Economics | CAC, contribution margin, payback by acquisition channel |
| Scenario Simulator | Interactive slider for approval threshold → live revenue / loss / margin recompute |
| Segment Deep Dive | Multi-filter view (credit band × channel × vintage) across all KPIs |
dbt_project/
├── dbt_project.yml # project config
├── profiles.yml # DuckDB adapter
├── packages.yml # dbt-utils dependency
│
├── models/
│ ├── _sources.yml # 6 raw Parquet sources with tests
│ ├── staging/ # 6 stg_* views (clean + cast)
│ │ └── _staging.yml # 30+ schema tests
│ ├── intermediate/ # ephemeral CTEs
│ │ ├── int_loan_lifetime.sql
│ │ └── int_payment_status_history.sql
│ └── marts/ # 5 analytical mart tables
│ └── _marts.yml # mart-level contracts + tests
│
├── seeds/
│ ├── apr_by_risk_band.csv # APR by credit-score band
│ └── acquisition_cost_by_channel.csv # CAC reference
│
├── macros/
│ ├── categorize_dpd_bucket.sql # standard DPD bucketing
│ └── compute_expected_loss.sql # PD × LGD × EAD formula
│
└── tests/
├── assert_no_negative_principal.sql
└── assert_payments_match_loan_term.sql
A hiring manager opening this project sees a complete dbt project: sources, seeds, staging, intermediate, marts, schema tests, singular tests, macros, contracts, lineage — exactly what FinTech JDs (Affirm, Chime, Block, Brex) screen for.
fintech-credit-risk-analytics/
├── Makefile # pyenv-aware build targets (incl. dbt)
├── pyproject.toml # deps: dev / dbt / streamlit / docs
├── .python-version # locks pyenv virtualenv
│
├── data/
│ ├── raw/ # 6 generated Parquet files (gitignored)
│ ├── sample/ # small CSVs for inspection
│ └── fintech.duckdb # dbt build target (gitignored)
│
├── dbt_project/ # See dbt structure above
│
├── python/src/
│ ├── generate_data.py # synthetic loan-lifecycle simulator
│ ├── analysis.py # vintage curve, forecast, simulator, scoring
│ └── metrics.py # KPI helpers
│
├── streamlit_app/
│ ├── app.py # entry point
│ ├── pages/ # 6 dashboard pages
│ └── utils/ # cached DuckDB loaders
│
├── tests/ # 85 Python tests (4 files)
│
└── docs/ # Sphinx documentation
Two layers of testing:
| Layer | Count | Run via |
|---|---|---|
| dbt data tests | 110 (schema + singular) | make dbt-build |
| Python pytest | 85 (unit + integration) | make test |
make lint runs ruff over all Python code.
Full project documentation built with Sphinx:
make docs # live-reload server on port 8000
make docs-build # one-shot HTML build to docs/_build/htmldbt's own lineage docs:
make dbt-docs # serves at port 8080MIT