pandas
Reads the CSV and applies the dictionary mappings that turn text columns into numbers.
AWPF Summer Semester 2022 · Machine Learning
We trained a decision tree on IBM's HR dataset to answer one binary question from six facts about a person's career. This page lets you drive the model yourself — move the inputs and watch the tree walk its checkpoints, one split at a time, to a verdict.
01 — The goal
Predict whether a person would receive more or less than $65 per hour, based on a chosen set of career factors.
Our first idea was to predict a person's exact salary. That turned out to be a much harder problem than we could do justice to. Constraining the output to a single true/false boundary made the problem tractable — and made a decision tree the natural fit, because a tree is a chain of yes/no questions that ends in exactly that kind of answer.
The $65 threshold isn't arbitrary: it sits almost exactly at the median hourly rate in the dataset (), which splits the employees into two groups of near-equal size. That matters — a balanced target means a model can't score well just by always guessing the same answer.
02 — The data
The source is the IBM HR Analytics Employee Attrition & Performance dataset — a fictional HR dataset published by IBM and widely used for teaching. Each row is one employee, with 35 attributes covering their role, tenure, education, satisfaction and pay.
Most of the useful columns arrive as text. A decision tree needs numbers, so every non-numeric factor was mapped through a Python dictionary before training. Some columns were already scored on a scale — Education and Performance Rating run from 1 (worst) to 5 (best) as published, so they passed through untouched.
| Factor | Mapping |
|---|---|
| Business Travel | Travel_Frequently 1 · Travel_Rarely 2 · Non-Travel 3 |
| Department | Research & Development 1 · Sales 2 · Human Resources 3 |
| Majors | Life Sciences 1 · Medical 2 · Human Resources 3 · Technical Degree 4 · Marketing 5 · Other 6 |
| Gender | Male 1 · Female 2 |
| Marital Status | Married 0 · Divorced 1 · Single 2 |
| Ability to Work Over Time | No 0 · Yes 1 |
| Job role | 0–8, alphabetical from Healthcare Representative to Sales Representative |
The 1,470 rows were divided into 1,100 for training and 300 held back for testing, with 70 left unused. The tree never sees the test rows while learning, so its score on them is an honest estimate of how it would handle a stranger.
The draw is stratified: rows are sampled at random from within each salary class separately, rather than sliced off one shuffled list. That guarantees both halves carry the same balance as the source data — — so the test set can't accidentally end up easier or harder than what the tree trained on.
03 — What was used
The original work lives in two Jupyter notebooks. Everything below runs in Python; this page is the trained tree exported to JSON and re-implemented in about forty lines of JavaScript, so the prediction happens in your browser with no server involved.
Reads the CSV and applies the dictionary mappings that turn text columns into numbers.
The array maths underneath everything else — scikit-learn is built on it.
DecisionTreeClassifier for the model itself, plus the train/test split and the accuracy metrics.
Ordinary least squares, used to check which factors looked worth keeping.
Renders the fitted tree as a diagram — the wide image further down this page.
No framework, no build step. The tree ships as JSON and is walked client-side.
04 — The interactive predictor
Set the six factors below. The tree starts at its root and, at every checkpoint, compares one of your values against a threshold it learned during training — going left or right until it runs out of questions. The verdict is whatever the training rows that landed in that final leaf mostly did.
The tree predicts
—
05 — How it splits
At each node the algorithm tries every factor and every possible cut-point, and keeps the one that leaves the two resulting groups as pure as possible — meaning each side contains mostly one answer rather than a mix.
Purity is scored with the Gini index. For a group where a fraction pi of rows belong to class i:
Gini = 1 − Σ pi2
A group that is entirely one class scores 0. A 50/50 group scores 0.5 — the worst possible for two classes. A split is judged by the weighted average Gini of the two groups it creates, and the algorithm keeps splitting until it can't lower that number any further. That stopping rule is precisely why the tree grows as deep as it does.
You can see this working in the predictor above: the Gini value shown at each checkpoint generally falls as you move down the path.
Gini importance — the total impurity each factor removed across the whole tree, normalised to sum to 1.
06 — Does it actually work?
A model is only worth as much as its performance on data it has never seen. Held back from training, those 300 test rows give the verdict. This section reports what they say, including where the original 2022 write-up got it wrong.
The report's central evidence for the chosen factors was a multivariable regression
reporting R² = 0.902, read as "90% of the variation is explained." The
regression was run as sm.OLS(y, x) — with no constant term added.
Without an intercept, statsmodels reports an uncentered R², which measures variation around zero rather than around the mean. Because hourly rates are all large positive numbers ($30–$100), a model predicting roughly "$65-ish" for everyone scores near-perfectly on that measure while explaining nothing at all. Refit with an intercept, the same five factors give R² = 0.0014.
This is a genuinely easy mistake to make — statsmodels flags it only in a footnote reading "R² is computed without centering."
Each mark is one complete re-run: a fresh stratified 1,100/300 draw, a tree fitted from scratch, scored on that run's own held-out rows. The vertical rule is the always-guess baseline.
Pearson r across all 1,470 rows. The axis spans just ±0.05; a factor worth modelling would usually reach ±0.3 or beyond.
Accuracy against tree depth. The gap between the two lines is overfitting, drawn.
Rows are the truth, columns are the prediction.
And the reason is visible in the correlation chart. Every factor correlates with hourly
rate at |r| < 0.023. HourlyRate in this dataset was
generated at random by IBM — it was never linked to the other columns. There is no signal
in it to find, so no algorithm and no amount of tuning would have found one.
sm.add_constant(x), and treat any R² above 0.9 on messy human data as a red flag rather than a result.MonthlyIncome instead — it correlates strongly with job level and tenure, and is the column with real structure in it.07 — In closing
The pipeline is sound: acquire a dataset, map its categorical columns to numbers, select
factors, fit a DecisionTreeClassifier, render it, and interrogate the result.
Every one of those steps does what it should, and the predictor above is the real fitted
tree — the same splits, thresholds and leaf counts, exported node by node.
What it also demonstrates, unintentionally but usefully, is the single most important habit in applied machine learning: a model that fits is not the same as a model that works. The training accuracy near 89% was real. So was the R² of 0.902. Both were measuring the wrong thing, and only held-out data said so.
Move the sliders above and you can watch a long chain of perfectly logical checkpoints arrive at an answer worth about as much as a coin flip. That is a more useful thing to have built than a model that quietly worked.