HOMEPROJECTSATTRITION CONSOLE

ATTRITION CONSOLE

Employee attrition risk - ML scoring & operator console

Category
ML application - internal decision-support tool for HR (supervised binary classification wrapped in a scoring service and operator console)
Type
ML Application
Role
Solo full-stack ML developer - data cleaning and feature engineering, model selection and threshold tuning, the inference service and its train/serve-parity test suite, the API contract, and the entire frontend console
VISIT LIVE
01
OVERVIEW

Attrition Console predicts which employees are at risk of leaving and, more importantly, why, so an HR partner knows who to have a conversation with. An HR analyst either fills in one employee's details in a guided form or uploads a headcount CSV export; the service returns a calibrated leaving probability, a keep/flag decision against a cost-tuned threshold, and a per-employee breakdown of which factors raised or lowered the risk. A what-if panel lets the analyst toggle real interventions (stop overtime, promote now, raise pay 20%, improve satisfaction) and see the probability move, and a threshold-sweep chart shows how many people get flagged at each cut-off. The product is deliberately scoped as a talk-to list, not a manage-out list, and that constraint is stated in the UI and the OpenAPI description.

02
ARCHITECTURE

THREE LAYERS, ONE-WAY ARTIFACT FLOW

An offline Colab notebook does all learning and writes model.pkl, encoder.pkl, scaler.pkl, label_encoder.pkl, feature_order.json and a rich metadata.json. The FastAPI service loads that directory once at startup and never re-derives anything from request data. The Next.js console never touches the scorer directly. Artifacts only ever move notebook → backend/artifacts/, never back.

TRAIN/SERVE SKEW AS A TESTED INVARIANT

backend/app/ml/preprocessing.py is a hand-verified port of the notebook's transformation functions, and test_parity_with_notebook_pipeline asserts the service reproduces the notebook's probability to four decimal places. Skew is the failure mode that degrades predictions silently, so it gets a test rather than a comment, and three real skew bugs were found and fixed this way.

STATELESS LOAD-ONCE PREDICTOR

Artifacts load in the FastAPI lifespan handler; a failed load leaves the app up with /health reporting degraded rather than crash-looping. handle_unknown="ignore" on the encoder turns an unseen JobRole into an all-zero block instead of a 500, since availability beats purity in a scoring endpoint.

BUSINESS LOGIC IN METADATA, NOT CODE

The decision threshold (0.12) is chosen in the notebook by minimising 10·FN + 1·FP, persisted to metadata.json, and overridable per request. scikit-learn is pinned in requirements.txt to metadata["versions"]["sklearn"] because unpickling across minor versions is undefined behaviour.

TWO-PROCESS SPLIT WITH A PROXY SEAM

The browser talks only to Next.js route handlers, which call FastAPI server-side. API_BASE_URL is server-only and never NEXT_PUBLIC_*, keeping the scoring service off the public internet and giving one place for auth, rate limiting and audit logging.

03
KEY MODULES

Single-employee scoring form

One FIELD_SPECS table in lib/types.ts is the single source of truth: every field renders as number / select / scale, validates (mirroring the Pydantic schema), and shows a hint automatically. Presets seed realistic profiles.

CSV batch scoring

Upload an HR export (up to 1,000 rows) to /predict/csv; missing required columns are named back, unseen categories degrade gracefully, and the response carries per-row decisions plus a flagged count.

Probability gauge

A hand-rolled SVG radial gauge with a decision-threshold tick (Recharts has no good radial-with-marker equivalent).

Contribution breakdown

Signed bars showing which factors raised or lowered this person's risk; direction is carried in text (“↑ raises risk”), never by colour alone.

What-if scenarios

Toggle HR-actionable levers (stop overtime, promote now, +20% pay, raise satisfaction) and compare probabilities side by side. Immutable attributes are deliberately excluded from the levers.

Threshold-sweep chart

Flag count and precision/recall across every threshold, so HR can tune flag volume to staffing capacity; callers can override the threshold per request.

Model card & honesty banners

/model/info surfaces live metrics, threshold rationale, required fields and pinned library versions; an amber banner shows whenever the model was not trained on real data or a sklearn version drift is detected.

04
HIGHLIGHTS
  • Made train/serve skew a failing test instead of a code comment, a 4-decimal parity assertion between the notebook and the service, and used it to catch three separate silent-degradation bugs.
  • Pushed every business decision (threshold, cost ratio, fitted medians, valid ranges, library versions) into a persisted metadata.json so the model can be re-tuned and redeployed without a code change.
  • Tuned the decision threshold from an explicit 10·FN + 1·FP cost function rather than accepting 0.5, turning a timid 0.38-recall classifier into an 0.85-recall talk-to list, and exposed the whole threshold curve in the UI.
  • Engineered 30 raw HR columns into 85 features (log transforms, peer-relative pay gaps, tenure and promotion-stagnation ratios, composite satisfaction/burnout scores) and kept the model a regularised logistic regression so every prediction stays explainable.
  • Treated it as an HR system throughout: the model outputs who to talk to, not who to manage out, stated in both the UI and the OpenAPI description, with a fairness checklist shipped in the README as a precondition for real use.

FULL STACK

Python 3.12pandasNumPyscikit-learn 1.6imbalanced-learn (SMOTE)SHAPJupyter / ColabjoblibFastAPIPydantic v2UvicornpytestDocker / docker-composeNext.js 14React 18TypeScriptTailwind CSS v4shadcn/ui + Radix / Base UIRecharts