The Number That Moves After Every Ball
You are watching an IPL chase on your phone. Mumbai need 84 runs off 36 balls, 6 wickets in hand. Under the scorecard sits a thin coloured bar: MI 63% — CSK 37%. A batter hits a boundary, and before the next ball is even bowled, the bar shifts: MI 71% — CSK 29%. Nobody typed that number in. No commentator voted on it. A trained model recalculated it the instant the match state changed, and an app pulled that number and painted it on your screen.
This chapter builds that exact system, stage by stage, with real arithmetic at every step — not a metaphor for one, an actual working model you can trace by hand. We will collect a small dataset, clean it, engineer the two numbers that matter most in a cricket chase, split it honestly, train a decision tree by computing real impurity scores, evaluate it with a real confusion matrix, watch it overfit on purpose so you can see what that failure actually looks like, and then discuss what changes when this stops being a notebook exercise and starts running live during a match. This full sequence — problem, data, cleaning, features, split, training, evaluation, deployment — is called the machine learning pipeline, and it is the same eight-stage structure behind almost every ML system you will ever build, not just cricket ones.
Stage 0: Naming the Problem Precisely
Before any data or code, you must state exactly what you are predicting. "Predict who wins" is too vague to build. Here is the precise version: given the state of a match at some point during the second innings (runs needed, balls remaining, wickets in hand, and so on), predict whether the chasing team will end up winning.
Notice the output has exactly two possible values: the chasing team wins, or it does not. This makes it a binary classification problem — you are sorting situations into one of two labelled bins (Win = 1, Loss = 0), not predicting a number like "the final score will be 187." Predicting a number is called regression; predicting a category is classification. Keeping this distinction sharp matters because it decides which tools apply — the decision tree we build in this chapter works for both, but the way you measure success is different for each, and mixing them up is a common early mistake.
Each row of data we will use is called an instance (one match situation). The columns we measure about it are features (required run rate, wickets left). The known correct answer for a completed match is the label. A model is a function that has learned, from many labelled instances, to guess the label of a new, unlabelled instance from its features alone.
The Eight Stages, at a Glance
Every ML system, from a spam filter to this cricket predictor, walks through the same sequence. Skipping a stage, or doing one carelessly, is where almost every broken model traces back to.
Stage 1: Data Collection
For every historical run-chase, you would record, ball by ball: the target score, the score and overs completed so far, wickets fallen, and — crucially — the final result once the match ended (this is the label; you only know it after the fact, from completed matches). Real IPL-scale datasets used for this kind of modelling contain ball-by-ball records for hundreds of matches. To learn the mechanics without drowning in rows, we will use a small, hand-built illustrative dataset of 16 run-chase situations — invented for this lesson, not real match records — with three raw-ish columns and one label:
| # | Wickets left | Required run rate (RRR) | Toss: chose to bowl first? | Result |
|---|---|---|---|---|
| 1 | 8 | 6.0 | Yes | Won |
| 2 | 3 | 7.0 | No | Won |
| 3 | 6 | 8.0 | Yes | Won |
| 4 | 9 | 5.0 | Yes | Won |
| 5 | 5 | 9.0 | No | Won |
| 6 | 4 | 9.0 | Yes | Lost |
| 7 | 7 | 10.0 | No | Lost |
| 8 | 3 | 11.0 | Yes | Lost |
| 9 | 2 | 12.0 | No | Lost |
| 10 | 6 | 8.5 | Yes | Lost |
| 11 | 9 | 4.0 | Yes | Won |
| 12 | 4 | 10.5 | No | Lost |
| 13 | 7 | 7.5 | Yes | Won |
| 14 | 2 | 13.0 | No | Lost |
| 15 | 5 | 9.5 | Yes | Lost |
| 16 | 8 | 6.5 | No | Won |
Stage 2: Data Cleaning
Real cricket datasets are messier than the table above. Matches abandoned by rain have no "result" label — they must be dropped, not guessed at, because forcing a fake label teaches the model something false. Team names get spelled inconsistently across seasons ("RCB" vs "Royal Challengers Bangalore" vs "Royal Challengers Bengaluru" after a 2023 rebrand) and must be standardised to one code per team, or the model will treat the same franchise as several different, unrelated teams. Text categories like "chose to bowl first: Yes/No" cannot be fed into most algorithms directly — they must be encoded as numbers, here 1 for Yes and 0 for No. None of this is glamorous, but a model trained on unencoded or inconsistently labelled data will fail in ways that look like a modelling problem but are actually a data problem — this is the single most common cause of a "broken" ML model in practice.
Stage 3: Feature Engineering — the Real Skill
Raw scorecard numbers (current score, overs bowled, target) are not directly useful to a model — you must compute the numbers that actually carry cricketing meaning. This is feature engineering, and it is usually the single biggest driver of how good a model turns out to be, more than the choice of algorithm.
Take a concrete situation: a team is chasing 180 in a 20-over match, and after 14 overs they have scored 96 for the loss of 4 wickets. From this you compute two engineered features:
- Wickets left = 10 − wickets fallen = 10 − 4 = 6.
- Required run rate (RRR) = (target − current score) ÷ overs remaining = (180 − 96) ÷ (20 − 14) = 84 ÷ 6 = 14.0 runs per over.
Compare that RRR of 14.0 to the team's current run rate so far: 96 runs ÷ 14 overs ≈ 6.9 runs per over. The team now needs to score at roughly double its own established pace — that gap, not either raw number alone, is what actually signals pressure, and it is exactly the kind of feature a raw "runs scored" column can never express by itself. This is why feature engineering matters: a target score of 180 tells you almost nothing on its own, but the derived required run rate — recomputed after every single ball — is one of the two strongest predictors of who wins a T20 chase.
Stage 4: Splitting Into Train and Test Sets
Here is a trap that is easy to fall into: if you train a model and then check how well it does on the very same data it trained on, you learn almost nothing about whether it can handle a new match it has never seen. A model can always find a way to fit data it has already memorised — that tells you about its memory, not its judgement. So before training, you set aside a portion of the data the model is never allowed to see during training, called the test set, and train only on the rest, the training set.
Using our 16 matches with a 75/25 split gives 12 training matches and 4 test matches:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, confusion_matrix
data = {
"wickets_left": [8,3,6,9,5,4,7,3,2,6,9,4,7,2,5,8],
"rrr": [6,7,8,5,9,9,10,11,12,8.5,4,10.5,7.5,13,9.5,6.5],
"toss_bowl_first": [1,0,1,1,0,1,0,1,0,1,1,0,1,0,1,0],
"result": [1,1,1,1,1,0,0,0,0,0,1,0,1,0,0,1],
}
df = pd.DataFrame(data)
X = df[["wickets_left", "rrr", "toss_bowl_first"]]
y = df["result"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=0
)
print(len(X_train), len(X_test))
# Output: 12 4
The function shuffles the rows first, so the split is not simply "the first 12 and the last 4." With random_state=0 fixed, the four matches that land in the test set are situations #2, #7, #9 and #10 from our table — the model below will never see these four during training, and we will judge it only on how well it handles them afterward.
Stage 5: Training a Decision Tree — By Hand First
A decision tree learns by playing a version of 20 Questions against the training data: at each step it asks one yes/no question about one feature ("Is the required run rate ≤ some value?"), splits the matches into two groups based on the answer, and keeps asking new questions inside each group until each group is (as close as possible to) all-one-label. The genuinely interesting part — and the part most explanations skip — is how the tree decides which question to ask first. It does not guess. It measures.
The measurement is called Gini impurity. For a group of matches where a fraction p won and the rest lost, impurity is:
Gini = 1 - (p_win^2 + p_loss^2)
A group that is all-one-label (all wins or all losses) has Gini = 0 — perfectly pure, no confusion. A group split exactly 50/50 has the worst possible Gini = 0.5 — a coin flip. Let's compute it for real, using the first 10 matches of our table (5 wins, 5 losses):
Root node, before any question is asked: 5 wins, 5 losses out of 10
p_win = 0.5, p_loss = 0.5
Gini = 1 - (0.5^2 + 0.5^2) = 1 - (0.25 + 0.25) = 0.50
A real tree tries every possible threshold of every feature and keeps whichever single question causes the biggest drop in weighted Gini. Testing every gap between RRR values, the best cut turns out to be "Is RRR ≤ 8.25?" (the midpoint between 8.0, a Win, and the next value up, 8.5, a Loss):
RRR <= 8.25 group: matches with RRR 6.0, 7.0, 8.0, 5.0
-> 4 matches: 4 wins, 0 losses
Gini = 1 - (1.0^2 + 0.0^2) = 0.00 (perfectly pure!)
RRR > 8.25 group: matches with RRR 9.0, 9.0, 10.0, 11.0, 12.0, 8.5
-> 6 matches: 1 win, 5 losses
Gini = 1 - ((1/6)^2 + (5/6)^2) = 1 - (0.028 + 0.694) = 0.278
Weighted Gini after this split = (4/10)(0.00) + (6/10)(0.278) = 0.167
Impurity reduced: 0.50 -> 0.167 (a drop of 0.333)
Now test the best possible cut on the rival feature, "Is wickets left ≤ 7.5?":
Wickets <= 7.5 group -> 8 matches: 3 wins, 5 losses
Gini = 1 - ((3/8)^2 + (5/8)^2) = 1 - (0.141 + 0.391) = 0.469
Wickets > 7.5 group -> 2 matches: 2 wins, 0 losses
Gini = 1 - (1.0^2 + 0.0^2) = 0.00
Weighted Gini after this split = (8/10)(0.469) + (2/10)(0.00) = 0.375
Impurity reduced: 0.50 -> 0.375 (a drop of only 0.125)
Even at wickets-left's own best possible cutoff, the required-run-rate question still wins: it drops impurity by 0.333 against wickets-left's best of 0.125 — over two-and-a-half times more effective, and it produces a leaf (all 4 low-RRR matches) that is completely pure, with zero confusion. A decision tree algorithm searches through exactly this kind of comparison — every feature, every candidate threshold, weighted Gini computed for each — and greedily keeps whichever single question causes the largest drop. Here that is unambiguously "RRR ≤ 8.25?" — not because a human decided run rate "feels" more important, but because the arithmetic says so.
Now let scikit-learn build and use this exact tree on the actual 12-match training set from Stage 4 (a slightly different 12 rows than the 10 used above, so its chosen threshold shifts a little — this is normal; trees are sensitive to exactly which rows they see):
tree = DecisionTreeClassifier(max_depth=1, criterion="gini", random_state=0)
tree.fit(X_train, y_train)
predictions = tree.predict(X_test)
print(predictions.tolist())
# Output: [1, 0, 0, 1]
print(y_test.tolist())
# Output: [1, 0, 0, 0]
Trace it: the tree learned one rule from training data — "if RRR ≤ 8.5, predict Win, else predict Loss" — and applied it to the four held-out matches. Three of the four predictions match the real outcome. The fourth (situation #10: 6 wickets left, RRR exactly 8.5, chose to bowl at the toss) was actually a loss, but the tree predicted a win, because 8.5 falls right on its learned boundary. This single wrong prediction is not a bug — it is the honest cost of collapsing a messy sport into two numbers, and it is exactly what Stage 6 is for measuring properly.
Stage 6: Evaluation — What "75% Accurate" Actually Means
print(accuracy_score(y_test, predictions))
# Output: 0.75
print(confusion_matrix(y_test, predictions, labels=[1, 0]))
# Output:
# [[1 0]
# [1 2]]
Accuracy is simply correct predictions ÷ total predictions = 3/4 = 0.75, or 75%. But accuracy alone hides what kind of mistakes were made, which is why the confusion matrix matters — it breaks every prediction into four buckets, comparing the real label against the predicted one:
| Predicted: Win | Predicted: Loss | |
|---|---|---|
| Actually Won | 1 (True Positive) | 0 (False Negative) |
| Actually Lost | 1 (False Positive) | 2 (True Negative) |
The single wrong prediction — situation #10, where the tree said "Win" but the team actually lost — is a False Positive: the model raised a false alarm of victory. Naming the error type matters in practice: a cricket app showing an inflated win probability is a minor embarrassment, but the same False Positive/False Negative distinction in, say, a medical test decides whether "predicted healthy but actually sick" (dangerous) is treated the same as "predicted sick but actually healthy" (merely inconvenient) — and it should not be. Always ask not just "how accurate?" but "accurate at avoiding which kind of mistake?"
Common misconception, corrected: "A higher accuracy number always means a better model." This is false, and the trap is called the majority-class baseline. Suppose, hypothetically, that in some historical sample, the team that won the toss and chose to bat first went on to win 70% of matches. A "model" that does no learning at all and simply always predicts "toss-winner wins" would score 70% accuracy — a number that sounds respectable — while having learned precisely nothing about the match in front of it. Before trusting any accuracy figure, always ask: what would a model that ignores the data entirely and just guesses the more common label score? Your real model must clearly beat that baseline, not just look impressive in isolation.
Stage 7: Overfitting — When the Tree Learns Too Much
Our stump asked exactly one question. What if we let the tree keep asking questions until every training match is classified perfectly?
for depth, label in [(1, "one question"), (2, "two levels"), (None, "unlimited")]:
t = DecisionTreeClassifier(max_depth=depth, random_state=0)
t.fit(X_train, y_train)
train_acc = accuracy_score(y_train, t.predict(X_train))
test_acc = accuracy_score(y_test, t.predict(X_test))
print(label, "train:", round(train_acc, 3), "test:", round(test_acc, 3))
# Output:
# one question train: 0.917 test: 0.75
# two levels train: 0.917 test: 0.75
# unlimited train: 1.0 test: 0.75
Letting the tree grow without limit pushed training accuracy from 91.7% all the way up to a perfect 100% — it found extra questions that let it correctly memorise the one training match its simpler self had gotten wrong. But look at the test column: it did not move at all. All that extra machinery bought zero real improvement on matches the tree hadn't already seen; it just carved increasingly specific rules to fit quirks of these 12 particular training rows. This is overfitting: a model whose training-set score keeps climbing while its held-out performance stalls or — on larger, noisier real datasets — actively gets worse, because the extra splits are chasing coincidences in the training sample rather than genuine cricketing patterns. The fix used across nearly all tree-based models is to deliberately limit growth (a maximum depth, or a minimum number of matches required before a further split is allowed), trading a little training-set perfection for a model that generalises better.
Second misconception, corrected: "The model understands cricket." It does not, and this is worth being precise about. The tree above has no concept of momentum, no notion that a set batter is more dangerous than a new one, no awareness that rain reduced the target under DLS rules, and no idea a strike bowler pulled a hamstring mid-over. It knows exactly three numbers per row: wickets left, required run rate, and a toss flag — and (as you can verify) it never even used that toss flag, because splitting on it never reduced Gini impurity enough to be worth asking:
full_tree = DecisionTreeClassifier(random_state=0).fit(X_train, y_train)
importances = {c: round(float(v), 3) for c, v in zip(X.columns, full_tree.feature_importances_)}
print(importances)
# Output:
# {'wickets_left': 0.114, 'rrr': 0.886, 'toss_bowl_first': 0.0}
The toss feature earned an importance of exactly 0.0 — the tree collected it, considered it, and discarded it as useless for this data, without anyone telling it to. A model only ever reasons about the columns you hand it, weighted by how much they actually separated wins from losses in training. If a genuinely important real-world factor (an injury, a pitch report, a rain interruption) is missing from your feature table, the model cannot "sense" it is missing — it will confidently produce a probability using only what it has, and that confidence is not the same thing as being right.
Stage 8: Deployment — From Notebook to Live Broadcast
A model sitting in a script on your laptop has predicted nothing for anyone. Turning it into the live bar under a broadcast involves a few concrete engineering steps beyond the modelling you have just done:
- Serialise the trained model — save the fitted tree object to a file (commonly with Python's
pickleorjoblib) so it does not need retraining every time it is used. - Wrap it behind an interface — typically a small API: after every ball, the app computes the current wickets-left and RRR from the live scorecard, sends those two numbers to the saved model, and receives back a predicted class or a win probability.
- Serve it at speed — the whole load-predict-respond cycle has to complete inside a fraction of a second, every single ball, for the full duration of the match.
- Monitor it in production — accuracy measured once on a 2026 test set does not stay valid forever. Rules change (a new impact-substitute rule, a different powerplay length), squads are auctioned and reshuffled every season, and pitches at a newly added venue behave differently from anything in the training data. When a model's real-world accuracy quietly drops because the world it is predicting has shifted since training, that is called concept drift, and it is the reason production models are periodically retrained on freshly collected matches rather than trained once and left alone.
Production win-probability systems used by broadcasters and cricket apps are built on this identical eight-stage skeleton, though they typically use richer models — logistic regression or gradient-boosted trees rather than one shallow tree — and dozens of engineered features (venue-specific par scores, head-to-head history, current batter's strike rate) instead of our three. The extra sophistication changes the accuracy of the number on your screen. It does not change the pipeline that produced it.
Check Your Understanding
- A run-chase situation has target = 165, current score = 110 after 15 overs of a 20-over match. Compute wickets left (if 6 have fallen) and the required run rate.
- A tree node contains 8 matches: 6 wins, 2 losses. Compute its Gini impurity.
- A historical sample has 950 matches where the team batting first won, and 50 where the team batting second won. A "model" always predicts "team batting first wins." What accuracy does it score, and why is that number misleading on its own?
- Why must the test set be data the model never saw during training? What specifically goes wrong if you evaluate a model only on its own training data?
- A deeper tree reaches 100% training accuracy but the same test accuracy as a shallow one. Name the phenomenon, and explain in one sentence why more training-set perfection did not help.
- Give one real-world factor that could affect an IPL chase's outcome but that our three-feature model has no way of knowing about — and explain what the model does when that factor is present but unmeasured.
Answers: (1) Wickets left = 10 − 6 = 4; RRR = (165 − 110) ÷ (20 − 15) = 55 ÷ 5 = 11.0 runs/over. (2) p_win = 6/8 = 0.75, p_loss = 0.25; Gini = 1 − (0.75² + 0.25²) = 1 − (0.5625 + 0.0625) = 0.375. (3) 95% accuracy; misleading because it is achievable with zero cricketing insight, purely by exploiting the class imbalance — any real model must be compared against this majority-class baseline, not judged in isolation. (4) Because a model can always fit data it has already memorised; testing on training data measures memory, not the ability to generalise to new, unseen matches. (5) Overfitting; the extra splits fit noise and coincidences specific to the training rows rather than patterns that hold on new data, so they do not transfer. (6) Any unmeasured factor works — e.g. a key batter's injury, a rain-revised DLS target, pitch deterioration under lights; the model simply proceeds using only its known features and produces a confident-looking probability that is blind to the missing factor.
Summary
- Predicting a chase's winner is binary classification: sorting match situations into Win/Loss using labelled historical instances.
- The pipeline has eight stages: problem definition, data collection, cleaning, feature engineering, train/test split, model training, evaluation, and deployment with monitoring — skipping or rushing any one of them is the usual root cause of a "broken" model.
- Feature engineering — turning raw numbers into meaningful ones like required run rate — typically matters more to a model's quality than which algorithm you pick.
- A decision tree chooses each question by testing candidate splits and picking the one that most reduces Gini impurity = 1 − (p_win² + p_loss²); it does this automatically and can end up ignoring features (like our toss flag) that never earn their keep.
- Never trust a single accuracy number without comparing it to the majority-class baseline and inspecting the confusion matrix for which type of error (false positive vs false negative) is being made.
- Overfitting shows up as training accuracy climbing while test accuracy stalls or drops — a sign the model is memorising training quirks instead of learning transferable patterns.
- A model only ever reasons about the features you give it; it has no awareness of anything you didn't measure, and its real-world accuracy can silently decay over time (concept drift), which is why deployed models are periodically retrained.