Train → predict → evaluate
Every real ML workflow is three phases on a loop:
- Train — learn parameters from *labelled* data.
- Predict — apply what you learned to *new* data.
- Evaluate — measure how well it did, on data it never trained on.
The golden rule: split your data first, then only ever train on the training half and score on the held-out test half. Evaluating on data you trained on flatters the model — it's an open-book exam. Use a fixed random seed so the split is reproducible.
The learner here is the simplest possible: a threshold. Training tries every unique value as a cut-point, labels records 'pos' if value >= threshold else 'neg', and keeps the threshold that scores best on the training set. predict is then one comparison; evaluate runs predict over the test set and returns accuracy.
A real twist worth seeing: on a tiny test set the learned threshold may score *worse* than a hand-picked one — small samples are noisy. That's not a bug, it's why ML uses big datasets and cross-validation.
*In production: sklearn.model_selection.train_test_split plus a real estimator. The structure is identical — only the learner gets fancier.*
Syntax
import random
def train_test_split(records, test_fraction, seed):
random.seed(seed) # reproducible shuffle
shuffled = records[:]
random.shuffle(shuffled)
n_test = round(len(shuffled) * test_fraction)
return shuffled[:-n_test], shuffled[-n_test:] # (train, test)
def train_threshold(train):
best_t, best_acc = None, -1.0
for t in sorted({r['spend'] for r in train}):
acc = sum((r['spend'] >= t) == (r['label'] == 'pos') for r in train) / len(train)
if acc > best_acc:
best_acc, best_t = acc, t
return best_t
def predict(record, threshold):
return 'pos' if record['spend'] >= threshold else 'neg'Worked examples
The whole loop, end to end
import random
DATASET = [
{"spend": 450, "label": "pos"}, {"spend": 80, "label": "neg"},
{"spend": 320, "label": "pos"}, {"spend": 50, "label": "neg"},
{"spend": 210, "label": "pos"}, {"spend": 30, "label": "neg"},
{"spend": 500, "label": "pos"}, {"spend": 120, "label": "neg"},
{"spend": 280, "label": "pos"}, {"spend": 90, "label": "neg"},
]
def train_test_split(records, test_fraction, seed):
random.seed(seed)
shuffled = records[:]
random.shuffle(shuffled)
n_test = round(len(shuffled) * test_fraction)
return shuffled[:-n_test], shuffled[-n_test:]
def train_threshold(train):
best_t, best_acc = sorted({r["spend"] for r in train})[0], 0.0
for t in sorted({r["spend"] for r in train}):
correct = sum(1 for r in train if (r["spend"] >= t) == (r["label"] == "pos"))
acc = correct / len(train)
if acc > best_acc:
best_acc, best_t = acc, t
return best_t
def predict(record, threshold):
return "pos" if record["spend"] >= threshold else "neg"
def evaluate(test, threshold):
correct = sum(1 for r in test if predict(r, threshold) == r["label"])
return round(correct / len(test), 4)
train, test = train_test_split(DATASET, 0.3, 42)
print(len(train), len(test))
t = train_threshold(train)
print("threshold:", t)
print("test accuracy:", evaluate(test, t))
# Output:
# 7 3
# threshold: 280
# test accuracy: 0.6667A seeded split is reproducible
import random
def train_test_split(records, test_fraction, seed):
random.seed(seed)
shuffled = records[:]
random.shuffle(shuffled)
n_test = round(len(shuffled) * test_fraction)
return shuffled[:-n_test], shuffled[-n_test:]
data = [{"spend": s} for s in range(10)]
a, _ = train_test_split(data, 0.3, 42)
b, _ = train_test_split(data, 0.3, 42) # same seed
c, _ = train_test_split(data, 0.3, 99) # different seed
print(a == b) # True — same seed, same split
print(a == c) # False — different seed
# Output:
# True
# FalseA perfect threshold scores 1.0 on a clean dataset
def predict(record, threshold):
return "pos" if record["spend"] >= threshold else "neg"
def evaluate(test, threshold):
correct = sum(1 for r in test if predict(r, threshold) == r["label"])
return round(correct / len(test), 4)
clean = [
{"spend": 450, "label": "pos"}, {"spend": 210, "label": "pos"},
{"spend": 120, "label": "neg"}, {"spend": 30, "label": "neg"},
]
print(evaluate(clean, 200)) # threshold 200 splits it perfectly
print(predict({"spend": 300}, 200), predict({"spend": 100}, 200))
# Output:
# 1.0
# pos negCommon mistakes & gotchas
Never evaluate on training data
Score the model on the data it learned from and you measure memorisation, not skill — accuracy looks great and means nothing. Split first, train on one half, evaluate on the other. The held-out test set is the only honest scoreboard.
No seed = a different split every run
random.shuffle without random.seed(...) reorders differently each run, so your train/test split — and every metric downstream — changes between runs. Set a fixed seed for reproducible experiments; you can't compare two models if the data underneath them keeps moving.
Best-on-train isn't best-on-test
The threshold that maxes training accuracy can score worse on the test set — that's overfitting in miniature, and on a tiny test set it's almost guaranteed (here the learned 280 scores 0.6667 on 3 test rows). Real evaluation needs enough test data, or cross-validation, to be trustworthy.
Why it matters
Train → predict → evaluate is the heartbeat of every ML system, from a two-line threshold to a billion-parameter model. The discipline of holding out test data and measuring honestly is what stops you from shipping a model that aced its own homework and fails in the wild — and it's exactly the loop the boss-build pipeline wires together end to end.