Duration is explicitly forbidden
The feature becomes available only after contact. A test fails if it reaches the training matrix, keeping offline quality honest.
Public data · Independently developed · Reproducible
A production-shaped machine-learning pipeline that answers a practical marketing question: under a finite contact budget, who should the team prioritize—and how much confidence should it place in that ranking?
01 / Problem
The public UCI Bank Marketing dataset records direct-marketing calls and whether each customer subscribed to a term deposit. A conventional tutorial would optimize accuracy and stop. This project treats the model as one component in a contact policy: rank candidates, select a budget, expose the value assumptions, and show the trade-off between reach and precision.
The primary user is an analytics or campaign team choosing the next audience tranche. The output is a scored and queryable decision table—not a screenshot of a notebook.
Class imbalance makes accuracy uninformative. A default 0.5 threshold ignores capacity and economics. Most importantly, call duration is only known after the call and would leak future information into a pre-contact targeting model.
02 / System
The Python package keeps ingestion, feature policy, modeling, evaluation, segmentation, and SQL export separate. That separation makes each layer testable and lets a future batch deployment replace local storage without changing the analytical contract.
03 / Design
The feature becomes available only after contact. A test fails if it reaches the training matrix, keeping offline quality honest.
The source data is ordered chronologically. Earlier observations train the models; later observations validate and test them.
Scores remain separate from contact capacity and value assumptions. Teams can inspect policy outcomes at multiple budget levels.
PCA and K-Means create diagnostic audience profiles. Unsupervised labels never override predicted response or human policy.
Key metrics include sampling uncertainty so a small apparent gain is not presented as certainty.
Scored rows are written to SQLite and ranked with window functions, demonstrating a handoff beyond Python.
04 / Results
The default 70/15/15 split selected logistic regression on 6,178 validation rows, then measured it on 6,179 later rows. These values load from a reduced pipeline artifact rather than hand-entered claims. Because the split was refined during sensitivity analysis, the final window is useful engineering evidence—not a pristine, prospectively locked confirmatory estimate.
Under an earlier 60/20/20 cutoff, logistic regression reached 0.0972 validation PR-AUC versus 0.1107 for the constant-prior baseline, so the correct decision was to reject both trained challengers. The regime sensitivity is a monitoring risk, not a result to hide.
This is propensity modeling, not causal uplift. The system identifies customers whose observed patterns resemble prior responders; it does not claim the call itself caused conversion. A randomized-treatment dataset is the correct next step for incremental-effect estimation.
05 / Reproduce
src/, typed boundaries, deterministic seeds, and a CLI.The natural AWS path is scheduled batch scoring: source objects in S3, a containerized processing job, versioned artifacts, a data-quality gate, and CloudWatch alarms for schema and score-distribution drift. This repository does not claim that deployment until an AWS account produces verifiable run evidence.
Next case study