Same job, opposite direction

Traditional programming takes rules and data and produces answers. You write the logic; the computer applies it to inputs.

Machine learning runs the arrows the other way. It takes data and answers and produces the rules. You supply examples of the right output; the algorithm works out the logic that would generate them, as you saw when fitting a delivery-time formula from eight past deliveries.

This is not a small refactor. It changes what you write, what you test, what breaks, and what you do when it breaks. The clearest way to see it is to write the same feature twice.

The rule set that never stops growing

Here is a spam filter for email subject lines, written the ordinary way.

def is_spam(subject):
    s = subject.lower()
    if "free" in s and "!" in s:
        return True
    if "winner" in s:
        return True
    if "claim your prize" in s:
        return True
    return False

for t in ["FREE gift inside!", "Free lunch tomorrow!",
          "F R E E money !!!", "Winner of the design award"]:
    print(repr(t), is_spam(t))

# 'FREE gift inside!'          True
# 'Free lunch tomorrow!'       True   <- a colleague's genuine email, blocked
# 'F R E E money !!!'          False  <- spam, delivered
# 'Winner of the design award' True   <- congratulations, blocked

Three of four cases are wrong in a way that matters. Every fix creates the next problem. Add a spaced-letters check and you break legitimate subjects that use spacing for emphasis. Exempt “winner of” and spammers write “winner in”. This is the characteristic failure of rule-based systems in messy domains: the rule count grows without bound, each rule interacts with the others, and after six months nobody can safely delete any of them.

The problem is not that the developer was careless. It is that the underlying pattern — what makes a subject line feel like spam — genuinely is a weighted combination of hundreds of weak signals. A human cannot write that down, which is precisely the second condition for reaching for machine learning.

Handing the job to the data

Now the same feature, learned. You supply labelled examples instead of logic.

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline

subjects = [
    "FREE gift inside claim now", "Winner winner claim your prize",
    "Free money click here fast", "You are a winner claim free cash",
    "Urgent claim your free prize now", "Congratulations winner free gift",
    "Are you free on Friday", "Lunch tomorrow at one",
    "Notes from the standup", "Can you review the draft",
    "Invoice for last month", "Reminder about the dentist",
]
labels = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]   # 1 = spam

clf = make_pipeline(CountVectorizer(), MultinomialNB())
clf.fit(subjects, labels)

print(clf.predict(["F R E E money",
                   "Free coffee at the standup",
                   "claim your prize now"]))
# [1 0 1]

Note what is absent. There is no if. There is no list of banned words. The CountVectorizer turns each subject into word counts and MultinomialNB learns how much each word shifts the odds towards spam. “Free coffee at the standup” comes through clean because standup and coffee pull hard the other way — a piece of nuance the rule version could not express at any length.

Twelve examples is a toy. But the shape scales: at fifty thousand labelled subjects the model keeps improving, while the rule file would have collapsed under its own weight long before.

What you trade away

The learned version is not strictly better. You have swapped one set of engineering problems for a different set, and the second set is less familiar.

Traditional code Learned model
You author The logic The training data and the loss
Behaviour is fixed by What you wrote What the data contained
Same input, same output? Always Usually, but retraining can change it
To fix a wrong case Find the branch, edit it Add examples, retrain, hope
A test failing tells you Exactly which line is wrong That accuracy dropped somewhere
Correctness target Passes every test Good enough on average
Hidden dependency Other functions Every column of the input data
Reviewable in a pull request Yes Weights, no; data and code, yes

The row that surprises people most is the fourth. In ordinary code, a bug report maps to a location. In a model there is no location. The behaviour is spread across every weight, and the lever you have is indirect: change the training data, retrain, and check whether the case is now handled without breaking six others. Sometimes it is not, and no amount of staring at the model will tell you why.

The row that costs most in production is the second-to-last. Traditional code depends on other code, which is version-controlled and greps cleanly. A model depends on the statistical shape of its input data, which nothing tracks. If another team quietly changes how a field is populated, no build fails and no test errors — accuracy simply drifts down over weeks. Silent degradation is the signature failure of machine learning in production, and it does not exist in a rule-based system.

There is also a change in what “done” means. Traditional code that fails one in twenty times is broken. A model that is right nineteen times in twenty may be excellent. You have to decide, before you start, what error rate is acceptable and what it costs.

The training data becomes source code

There is a practice consequence that catches most teams late. In the rule-based filter, everything that determines behaviour lives in a file under version control. You can read the diff, blame a line, and check out last month’s version to reproduce a bug.

In the learned filter, behaviour is determined by subjects and labels. Those are the source. If someone adds four hundred rows next Tuesday and retrains, the filter changes, and nothing in your repository records what changed or why. Six weeks later, when the model starts blocking newsletters, the question “what did we train this on?” may have no answer.

Treat the training set with the seriousness you give code. Version it, record which snapshot produced which model, and keep a held-out set of cases the model must get right so that a regression announces itself. The fact that the rule file disappeared does not mean the maintenance burden did — it moved into a place your existing tooling does not watch.

Where rules still win

Reach for ordinary code when any of these apply.

  • The rule is written down somewhere. Tax thresholds, refund windows, eligibility criteria set by a regulator. These are not patterns; they are instructions, and learning them approximately is a defect.
  • You cannot afford to be wrong. Payment authorisation logic, permission checks, anything where one mistake is a real incident.
  • The rules genuinely are few and stable. Three conditions that have not changed in five years do not need a model.
  • You have no labelled examples. Without answers to learn from, there is nothing to fit, and manufacturing labels is often the largest cost in the project.

Most production systems are hybrids, and this is the arrangement to aim for. A model scores the ambiguity; rules handle the parts that are certain and enforce the limits. A fraud system might have a model producing a risk score and hard rules that block certain countries outright and never auto-decline above a fixed amount without review. The rules provide guarantees. The model provides judgement in the space where guarantees are impossible. The mechanics of how that judgement gets fitted are covered in how machines learn from data.

Key Takeaways

  • Ask whether the rule can be written down before you model anything, because a documented rule implemented as code is faster, exact, and auditable.
  • Expect a rule-based system in a messy domain to grow until nobody can safely delete a line, and treat that growth curve as the signal to switch approach.
  • Budget for a different debugging loop, since a wrong prediction has no line number and your only lever is changing the training data and refitting.
  • Monitor your input data in production, not just your error rate, because upstream changes degrade a model silently while every test continues to pass.
  • Design hybrids by default: rules for the guarantees you must keep, a model for the judgement calls no rule can express.