A single, governed master dataset of 48,790 U.S. adults — built once, used everywhere — feeding both a Power BI analytics layer for human exploration and a reusable AI framework that benchmarked five ML algorithms across two controlled feature-engineering experiments. The goal wasn't just to predict who earns more than $50K; it was to make the why legible to a non-technical decision-maker, and to know precisely when to stop iterating and call the framework validated.
The starting brief called for two things that are easy to build separately and dangerous to build separately: a Power BI dashboard for exploring workforce demographics, and ML models predicting income bracket. The tempting shortcut is two datasets — one imputed and encoded for modeling, one left more readable for BI. I deliberately avoided that. Two datasets drift: a fix applied to one never reaches the other, and by the second refresh cycle "the data" means two different things depending on who you ask.
Instead, everything here traces back to one governed pipeline (build_master_dataset.py)
that produces a single master table, adult_master.csv / .parquet — 48,790
rows, 26 columns. Power BI reads the human-readable income_bracket and categorical
columns directly; the ML pipeline reads the numeric income_target flag and the same
underlying predictors. One source of truth, two consumers.
Unknown and paired with a boolean *_was_missing flag, so the fact that a
value was missing stays fully auditable instead of being silently smoothed over.
The Power BI layer isn't a KPI wall — it's built around one reusable DAX pattern,
High Income Rate vs Overall, that can drop onto any breakdown (occupation, education,
region, work-hours category) and immediately show whether that segment sits above or below the
population baseline. Twenty measures in total, plus three sort-key columns so categories like
"Part-time (<20)" through "Overtime (60+)" render in logical rather than alphabetical order.
Rather than trust a single model's built-in feature importance, I computed permutation importance across all four tuned models on a held-out sample and looked for agreement. The signal was consistent: relationship status and marital status dominate, followed by capital gains, education, age, occupation, and hours worked.
Relative ranking from permutation importance averaged across an early four-model sweep during the BI phase — this is what directly informed the 9-feature, BI-guided set used in Experiment B below.
Two findings were worth calling out explicitly to a non-technical reader rather than leaving buried
in a model file. Married individuals — specifically the "Husband" and "Wife" relationship
categories — show a high-income rate near 45–47%, against roughly 1.5% for adults still
classified as someone's child, a gap that reflects age and career-stage confounding as much as
marital status itself. And fnlwgt, a U.S. Census sampling weight included in the raw
data, has effectively zero correlation with income (Pearson r = −0.006) — it was excluded from
modeling entirely rather than left in to add noise.
The BI-phase feature analysis above fed into something more deliberate than one more model run: a
reusable AI execution framework, with a single project.json as the source of truth for
preprocessing, train/test split, metrics, checkpointing, and output structure. Only the model
implementation and the feature list changed between experiments — which made it possible to ask a
real question rather than just tune harder: does the full 17-feature set actually earn its
keep, or does the 9-feature, BI-guided set (education, workclass, occupation, hours worked, capital
gain/loss, marital status, sex, race) hold its own? Five algorithms — Logistic Regression,
Random Forest, XGBoost, SVM, and CatBoost — were benchmarked under both feature sets, using the
dataset's original 32,537 / 16,253 train/test split to stay comparable with the published UCI
benchmark.
| Model (9-feature, BI-guided) | Accuracy | >50K Precision | >50K Recall | >50K F1 | ROC-AUC |
|---|---|---|---|---|---|
| Logistic Regression | 85.50% | 0.742 | 0.605 | 0.666 | 90.44% |
| Random Forest | 81.13% | 0.570 | 0.858 | 0.685 | 91.59% |
| XGBoost — champion | 87.96% | 0.790 | 0.676 | 0.729 | 92.79% |
| SVM | 86.09% | 0.778 | 0.586 | 0.669 | 89.13% |
| CatBoost | 87.85% | 0.795 | 0.664 | 0.723 | 92.64% |
Champion result, 9-feature Experiment B. The 17-feature baseline (Experiment A) reached a very similar place — XGBoost there hit 87.76% accuracy / 93.46% ROC-AUC, marginally higher on ROC-AUC alone — which is itself the interesting finding: eight fewer features, no meaningful performance given up.
On the recommendation: XGBoost was the champion on the 9-feature set — highest accuracy (87.96%) of the five, with CatBoost close behind (87.85%) and the best >50K precision of the group. Random Forest, by contrast, caught the most genuine >50K cases (85.8% recall) at the cost of more false positives — which model is "right" depends on whether a downstream decision punishes missed opportunities or wasted outreach more, a business question I flagged rather than answered for the reader. What mattered more than picking a 0.1%-margin winner was what the two experiments together showed: reducing 17 features to 9 BI-guided ones cost almost nothing, which is exactly the kind of result that makes a framework reusable rather than a one-off tune.
Project 5 was concluded because its engineering and research objectives were achieved, not because it hit a wall it couldn't explain. Two controlled feature-engineering experiments and five algorithms converged on the same answer: the champion model (XGBoost, 9 features) reached 87.96% accuracy and 92.79% ROC-AUC — matching, and in places exceeding, the roughly 87–90% accuracy / 0.90–0.93 ROC-AUC range independently reported across decades of published work on this exact UCI Adult Income benchmark, using algorithms well beyond the five evaluated here.
The remaining ceiling isn't a flaw in the dataset or a sign of biased or poor modelling — it reflects a structural property of the problem itself. Income here was collected as a binary threshold (≤$50K / >$50K) rather than as a continuous value, so a meaningful share of records sit close to that line with very similar feature profiles on both sides of it. No classifier, however sophisticated, can fully separate cases that are genuinely alike in the features available to it. A ceiling that reproduces across every published study, using every mainstream algorithm, is a property of the data — not a gap in the modelling effort.
Two directions were deliberately considered and declined as reasons to keep the project open: a fairness-calibrated deployment pass (a deployment-readiness enhancement, not a modelling fix, and out of scope for closure), and re-running against a dataset with continuous rather than binary income values (a different structural problem entirely — regression rather than classification — and therefore a new project, not more work on this one). Neither changes the conclusion: this dataset has given up what it can support, and the reusable framework built to get here is now ready to be pointed at richer data.