Chapter 5 · Data / ML Engineer

Rule-based classifier

Before anyone reaches for a neural network, the smart move is a rule-based classifier — a handful of explicit if/elif thresholds drawn from domain knowledge. It's fast, fully explainable, and an honest baseline: if your fancy model can't beat the rules, the fancy model isn't earning its keep.

The core shape is a predict(x) -> label function: take one record, run it down a chain of conditions, return a category. Because Python's if/elif/else stops at the first true branch, order is everything — put the strongest, most specific conditions first, or a broad rule will swallow the narrow ones.

A classifier over a whole dataset is just predict in a loop — pair each record with its predicted label. That (record, label) pairing is the same shape a trained model produces; here the "model" is your rules.

*In production this kind of logic is often a Decision Tree (sklearn.tree.DecisionTreeClassifier) learned from labelled data. A decision tree is, in the end, a learned if/elif chain — you're writing the same thing by hand.*

Syntax

def predict(record):
    spend = record['spend']
    age = record['age']
    if spend >= 400:                       # most specific / strongest first
        return 'high'
    elif spend >= 150 and age >= 30:
        return 'high'
    elif spend >= 150:
        return 'medium'
    elif spend >= 50 and age >= 25:
        return 'medium'
    else:
        return 'low'

# batch: predict in a loop → list of (record, label) pairs
[(r, predict(r)) for r in records]

Worked examples

The predict() shape — one record in, one label out

def classify_customer(record):
    spend = record["spend"]
    age = record["age"]
    if spend >= 400:
        return "high"
    elif spend >= 150 and age >= 30:
        return "high"
    elif spend >= 150:
        return "medium"
    elif spend >= 50 and age >= 25:
        return "medium"
    else:
        return "low"

print(classify_customer({"age": 25, "spend": 500.0}))  # high (spend >= 400)
print(classify_customer({"age": 45, "spend": 200.0}))  # high (spend>=150 & age>=30)
print(classify_customer({"age": 22, "spend": 180.0}))  # medium (age too low for high)
print(classify_customer({"age": 28, "spend": 75.0}))   # medium (spend>=50 & age>=25)
print(classify_customer({"age": 20, "spend": 60.0}))   # low (age < 25)
# Output:
# high
# high
# medium
# medium
# low

Classify a whole batch into (record, label) pairs

def classify_customer(record):
    if record["spend"] >= 400:
        return "high"
    elif record["spend"] >= 150:
        return "medium"
    else:
        return "low"

customers = [
    {"id": "C001", "spend": 520.0},
    {"id": "C002", "spend": 180.0},
    {"id": "C003", "spend": 40.0},
]
labelled = [(c, classify_customer(c)) for c in customers]
for record, label in labelled:
    print(record["id"], "->", label)
# Output:
# C001 -> high
# C002 -> medium
# C003 -> low

Common mistakes & gotchas

Branch order changes the answer

Because elif stops at the first match, a broad rule placed early hides narrower ones. If elif spend >= 150 came *before* elif spend >= 150 and age >= 30, the age rule would never run — everything ≥150 would already be 'medium'. Most specific conditions go first.

Every path must return a label

Drop the final else and a record that matches no branch returns None — a silent, invalid label that breaks scoring downstream. A classifier should ALWAYS produce a class; keep the catch-all else.

Rules are deterministic, not learned

A rule classifier never improves on its own — it's exactly as good as the thresholds you hand-coded. That's its strength (explainable, predictable) and its ceiling (someone has to tune it). It's a baseline, not the finish line.

Why it matters

Most production ML projects should start here. A rule baseline ships in an afternoon, is trivial to explain to a stakeholder, and tells you the bar a learned model has to clear. Teams that skip it often spend weeks training a model that, it turns out, barely beats five `if` statements.