---
title: 'What Is Machine Learning'
source: 'https://academia.sh/en/courses/introduction-to-machine-learning/what-is-machine-learning'
course: 'Introduction to Machine Learning'
language: en
updated: '2026-10-07T04:31:20+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads the number a model produces by comparing it with a non-learning baseline and justifies the choice of baseline'
    - 'Measures the difference between a hand-written rule set and a learner that derives its rules from data on the same problem'
    - 'Shows by counting separately whether the difference comes from the thresholds or from the way the conditions are combined'
---

# What Is Machine Learning

The same illicit-use suspicion problem is solved with three procedures and three numbers are printed side by side: the non-learning baseline scores 0.7533 on the held-out set, the hand-written rule set written by the field expert scores 0.7507, and the three-question decision tree that derives its rules from data scores 0.8594. The hand-written rule falls 0.0027 below the baseline, the tree rises 0.1061 above it. The difference does not come from the thresholds: when the same thresholds are moved into the hand-written rule, the number drops to 0.7480. Of the 377 subscribers in the held-out set, 93 are suspicious; the hand-written rule flags 151 subscribers and 76 turn out empty, the tree flags 66 subscribers and 13 turn out empty. The dataset is reduced from a readings table of 3,199 rows to a subscriber table of 1,260 rows, seed 20260218.

Over ninety lessons, the Data Analytics curriculum measured every step of the path from data to
decision and showed at each step that the decision moves the number. One question stayed open at
the close: every one of those decisions was made by a **human**. Which rows to count, which
threshold to choose, which channel to use. Which number a procedure that **learns its decision
from data** moves, and where that procedure's own decisions stop, was never asked.

This lesson asks that question and establishes the unit of measurement for the curriculum. From
here on, no number a model produces is written alone; a **baseline** stands beside it — the number,
on the same measure, of a procedure that tries to solve the same problem without learning. The
curriculum's rule follows from this: **a model is as good as the margin by which it beats a procedure
that is dumber than itself. A performance with no baseline written next to it counts as unmeasured.**

- **FC1.** The dataset is **fictional** and carried over from the Data Analytics curriculum: a
  readings table in long format with 3,199 rows, 1,329 subscribers, three periods, five regions,
  seed 20260218. **The label is added in this curriculum and it is fictional too**; the rule that
  produces the label is written openly in the code and is never given to any model.
- **FC2. Machine learning** is deriving the rule from examples instead of writing it by hand. The
  input is not a rule but a set of examples made of input–output pairs; the output is a decision,
  and the structure that makes that decision is called a **model**.
- **FC3. A feature** is a column given to the model, and **a label** is the column the model is asked
  to predict. Which column is a feature and which is the label in the same table comes not from the
  data but from the question being asked.
- **FC4. A baseline** is a non-learning procedure and it is **chosen explicitly in every lesson**.
  In this lesson it is predicting the most frequent class; because the classes are imbalanced, this
  procedure produces a high accuracy even without learning, and it honestly raises the bar the model
  has to clear.
- **FC5.** Every number is measured on two sets: the **training set** on which the procedure is
  built and the **held-out set** that the procedure has never seen. Why the split is made this way
  is not the subject of this topic; the Generalization topic covers it. Here the split is applied as
  the course's rule.
- **FC6.** The measure itself is a decision. Accuracy counts a visit made for nothing and a missed
  illicit case with the same weight; that weight is a choice and it is written down where it is made.
- **FC7.** In this curriculum the models are written **with the standard library and at small
  scale**. The aim is not to imitate a library but to make the behavior of the procedure measurable
  line by line.

## The Problem and the Fictional Dataset

The question is this: from meter readings, to tell apart the subscriber carrying **illicit-use or
fault suspicion** before the field team goes to the door. What exists is the readings table of the
measurement network; what is wanted is a decision for each subscriber. The table is first
regenerated with the same seed.

```python
# data.py — MODEL: the regional measurement network is regenerated with the same seed.
# The dataset is fictional; no real subscriber, meter or region is represented.
import math

SEED, RAW, M32 = 20260218, 1400, 0xFFFFFFFF
PERIODS = ["2026-01", "2026-02", "2026-03"]
REGION = [("north", 0.28, 21), ("south", 0.22, 17), ("east", 0.18, 26),
          ("west", 0.14, 14), ("central", 0.18, 23)]


def rng(t: int):
    """Linear congruential generator: the seed is written in the code, the output is the same from run to run."""
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value() -> float:
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u: float, w: list[float]) -> int:
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


_subscribers, _dropped = [], []
for i in range(RAW):
    r = rng(SEED + i)
    b = REGION[pick(r(), [x[1] for x in REGION])]
    k = {"subscriber_id": 10001 + i, "region": b[0], "base": b[2],
         "household_size": pick(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]),
         "satisfaction": pick(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1}
    (_dropped if r() < 0.046 else _subscribers).append(k)
_valid = {a["subscriber_id"] for a in _subscribers}

READINGS = []
for k in _subscribers + _dropped:
    r = rng(SEED + 7000 + k["subscriber_id"])
    for d in range(pick(r(), [0.05, 0.12, 0.21, 0.62])):
        t = 0.0 if r() < 0.038 else math.floor(k["base"] * math.exp(
            (r() + r() + r() - 1.5) * 0.62) * (1 - d * 0.05) * 100 + 0.5) / 100
        if k["subscriber_id"] in _valid:
            READINGS.append({"subscriber_id": k["subscriber_id"], "region": k["region"],
                             "period": PERIODS[d], "consumption_m3": t})
        r()
print(f"fictional readings table (seed {SEED}): {len(READINGS)} rows, {len(_valid)} subscribers")
```

```
fictional readings table (seed 20260218): 3199 rows, 1329 subscribers
```

This table cannot be given to a model directly. The decision is per subscriber, whereas the table is
per reading, and a subscriber is represented by one, two or three rows. The shape the model will see
must be **a single row per subscriber**; each row carries nine numbers that summarize a subscriber
and one label.

```python
# summary.py — the readings table is reduced to a single row per subscriber and the label is added.
# The rule that produces the label is FICTIONAL, written openly in the code and not given to the model.
FEATURES = ["mean", "per_person", "drop", "zero_readings", "reading_count",
            "satisfaction", "household_size", "highest", "lowest"]

_groups = {}
for o in READINGS:
    _groups.setdefault(o["subscriber_id"], []).append(o)

SUBSCRIBERS = []
for k in _subscribers:
    o = _groups.get(k["subscriber_id"])
    if not o:
        continue
    t = [x["consumption_m3"] for x in o]
    mean = sum(t) / len(t)
    a = {"subscriber_id": k["subscriber_id"], "region": k["region"],
         "household_size": k["household_size"], "satisfaction": k["satisfaction"],
         "reading_count": len(t), "mean": mean, "highest": max(t),
         "lowest": min(t), "zero_readings": sum(1 for v in t if v == 0),
         "per_person": mean / max(1, k["household_size"]),
         "drop": (t[0] - t[-1]) / t[0] if len(t) > 1 and t[0] > 0 else 0.0}
    r = rng(SEED + 31000 + a["subscriber_id"])
    latent = (1.30 * a["zero_readings"] + 1.25 * (a["drop"] > 0.45)
              + 1.05 * (a["per_person"] > 12.0) + 0.65 * (a["reading_count"] < 3)
              + 0.45 * (a["satisfaction"] <= 2) - 1.55)
    a["suspicion"] = 1 if latent + (r() + r() - 1.0) * 1.1 > 0 else 0
    SUBSCRIBERS.append(a)

r = rng(SEED + 90001)
TRAIN, HELD_OUT = [], []
for a in SUBSCRIBERS:
    (TRAIN if r() < 0.70 else HELD_OUT).append(a)


def accuracy(predict, split: list[dict]) -> float:
    return sum(1 for a in split if predict(a) == a["suspicion"]) / len(split)


def positives(k: list[dict]) -> int:
    return sum(a["suspicion"] for a in k)


print(f"model table: {len(SUBSCRIBERS)} subscribers, {len(FEATURES)} features, 1 label")
print(f"label: {positives(SUBSCRIBERS)} suspicious, {len(SUBSCRIBERS) - positives(SUBSCRIBERS)} clean, "
      f"suspicious rate {100 * positives(SUBSCRIBERS) / len(SUBSCRIBERS):.1f} percent")
print(f"train {len(TRAIN)} subscribers ({positives(TRAIN)} suspicious), "
      f"held-out {len(HELD_OUT)} subscribers ({positives(HELD_OUT)} suspicious)")
```

```
model table: 1260 subscribers, 9 features, 1 label
label: 327 suspicious, 933 clean, suspicious rate 26.0 percent
train 883 subscribers (234 suspicious), held-out 377 subscribers (93 suspicious)
```

Of the 1,329 subscribers, 1,260 entered the model table; 69 subscribers have no readings at all and
there is nothing to summarize for them. The suspicious rate is 26.0 percent. This rate directly sets
the bar the model has to clear: a procedure that says "clean" to everyone without thinking is right
for 74 percent of the subscribers.

The rule that produces the label sits in the block and is not a real illicit-use detection. Zero
readings, a sharp drop between periods, high consumption per person, a missing period and low
satisfaction are gathered into one score, noise is added on top, and if the score passes zero the
subscriber counts as suspicious. The noise is deliberate: a noiseless label turns into a puzzle that
any procedure which splits the features finely enough hits exactly, and nothing is measured.

## The Baseline and the Hand-Written Rule

Two procedures are written. The first learns nothing: it looks at the most frequent class in the
training set and says that to everyone. The second is the rule in the field expert's head — if there
is a zero reading, if consumption dropped sharply, or if consumption per person is high, the
subscriber is flagged. The thresholds come from experience.

```python
# rule.py — two procedures: the non-learning baseline and the hand-written rule set.
from collections import Counter

most_frequent = Counter(a["suspicion"] for a in TRAIN).most_common(1)[0][0]


def baseline(a: dict) -> int:
    return most_frequent


def hand_written(a: dict) -> int:
    """The rule set the field expert wrote: the thresholds come from experience."""
    if a["zero_readings"] >= 1:
        return 1
    if a["drop"] > 0.50:
        return 1
    if a["per_person"] > 15.0:
        return 1
    return 0


W = [31, 12, 12]
fmt = lambda h: str(h[0]).ljust(W[0]) + str(h[1]).rjust(W[1]) + str(h[2]).rjust(W[2])
print(fmt(["procedure", "train", "held-out"]))
for name, f in [("baseline: most frequent class", baseline), ("hand-written rule set", hand_written)]:
    print(fmt([name, f"{accuracy(f, TRAIN):.4f}", f"{accuracy(f, HELD_OUT):.4f}"]))
```

```
procedure                             train    held-out
baseline: most frequent class        0.7350      0.7533
hand-written rule set                0.7622      0.7507
```

The expert's rule beats the baseline by 0.0272 on the training set, but on the held-out set it falls
0.0027 **below** it. Three reasonable rules, three reasonable thresholds and domain knowledge were
not enough to beat a procedure that learns nothing. This result is a finding in itself: had the
baseline not been written, the 0.7507 in the table could have been reported as "the rule set
makes the right decision for three in four subscribers."

## The Learner That Derives Its Rules from Data

The third procedure takes the **form** of the rule from the human and the **content** from the data.
The form is this: pick a feature, pick a threshold, split the set in two, and repeat the same step
in each part. Which feature to pick with which threshold is found by search; the quantity searched
for is **how mixed the two parts are in terms of the label** after the split. This structure is called
a **decision tree**.

```python
# tree.py — the learner that derives its rules from data: a three-question decision tree
# built by greedy splitting. No library is called; the split criterion and stopping rule are here.
DEPTH, MIN_SIZE = 3, 40


def impurity(k: list[dict]) -> float:
    """How mixed the two labels are in a set: zero if it has a single label."""
    if not k:
        return 0.0
    p = positives(k) / len(k)
    return 2 * p * (1 - p)


def best_split(k: list[dict]):
    best, choice = impurity(k) * len(k), None
    for feat in FEATURES:
        for threshold in sorted({a[feat] for a in k})[1:]:
            left = [a for a in k if a[feat] < threshold]
            right = [a for a in k if a[feat] >= threshold]
            m = impurity(left) * len(left) + impurity(right) * len(right)
            if m < best - 1e-9:  # floating-point equality is not tested, a small margin is left
                best, choice = m, (feat, threshold, left, right)
    return choice


def majority(k: list[dict]) -> int:
    return 1 if 2 * positives(k) > len(k) else 0


def fit(k: list[dict], depth: int):
    b = best_split(k) if depth and len(k) >= MIN_SIZE else None
    if b is None:
        return majority(k)
    left, right = fit(b[2], depth - 1), fit(b[3], depth - 1)
    return left if left == right else (b[0], b[1], left, right)


def apply(node, a: dict) -> int:
    while isinstance(node, tuple):
        node = node[2] if a[node[0]] < node[1] else node[3]
    return node


def show(d, indent: str = "") -> None:
    if not isinstance(d, tuple):
        print(f"{indent}-> {'suspicious' if d else 'clean'}")
        return
    print(f"{indent}{d[0]} < {d[1]:.3f}")
    show(d[2], indent + "  ")
    show(d[3], indent + "  ")


TREE = fit(TRAIN, DEPTH)
show(TREE)
print()
print(fmt(["procedure", "train", "held-out"]))
FIRST_THREE = [("baseline: most frequent class", baseline), ("hand-written rule set", hand_written),
               ("tree learned from data", lambda a: apply(TREE, a))]
for name, f in FIRST_THREE:
    print(fmt([name, f"{accuracy(f, TRAIN):.4f}", f"{accuracy(f, HELD_OUT):.4f}"]))
```

```
drop < 0.461
  per_person < 12.060
    -> clean
    reading_count < 3.000
      -> suspicious
      -> clean
  lowest < 6.000
    -> suspicious
    per_person < 13.052
      -> clean
      -> suspicious

procedure                             train    held-out
baseline: most frequent class        0.7350      0.7533
hand-written rule set                0.7622      0.7507
tree learned from data               0.8709      0.8594
```

The tree beats the baseline by 0.1061 on the held-out set. This is the number the lesson measures,
and it carries meaning not alone but inside the three-row table: on the same problem the
non-learning procedure gives 0.7533, the rule written with domain knowledge 0.7507, and the rule
derived from data 0.8594.

The thresholds the tree found are close to the thresholds of the rule that produces the label. The
label rule uses 0.45 for the drop and 12.0 for consumption per person; the tree finds 0.461 and
12.060. The tree never saw these numbers; it looked only at the features and labels of 883
subscribers.

## Where the Difference Comes From

The difference looks as if it comes from the thresholds — the expert guessed 0.50 and 15.0, the tree
found 0.461 and 12.060. This explanation can be tested: the thresholds in the expert's rule are
replaced with the tree's, and the number is measured again.

```python
# separate.py — does the difference come from the thresholds: the expert's rule keeps the same form,
# its thresholds are replaced with the values the tree found.
def hand_written_tree_thresholds(a: dict) -> int:
    return 1 if (a["zero_readings"] >= 1 or a["drop"] > 0.461
                 or a["per_person"] > 12.060) else 0


W2 = [31, 13, 8, 14, 8]
fmt2 = lambda h: str(h[0]).ljust(W2[0]) + "".join(str(v).rjust(W2[i + 1])
                                                  for i, v in enumerate(h[1:]))
print(f"held-out set: {len(HELD_OUT)} subscribers, {positives(HELD_OUT)} suspicious")
print(fmt2(["procedure", "flagged", "correct", "empty visit", "missed"]))
PROCEDURES = FIRST_THREE + [("hand-written, tree thresholds", hand_written_tree_thresholds)]
for name, f in PROCEDURES:
    flagged = [a for a in HELD_OUT if f(a) == 1]
    d = positives(flagged)
    print(fmt2([name, len(flagged), d, len(flagged) - d, positives(HELD_OUT) - d]))
print()
for name, f in PROCEDURES:
    print(f"{name:<31}held-out accuracy {accuracy(f, HELD_OUT):.4f}")
```

```
held-out set: 377 subscribers, 93 suspicious
procedure                            flagged correct   empty visit  missed
baseline: most frequent class              0       0             0      93
hand-written rule set                    151      75            76      18
tree learned from data                    66      53            13      40
hand-written, tree thresholds            184      91            93       2

baseline: most frequent class  held-out accuracy 0.7533
hand-written rule set          held-out accuracy 0.7507
tree learned from data         held-out accuracy 0.8594
hand-written, tree thresholds  held-out accuracy 0.7480
```

The explanation collapsed. When the expert's rule is run with the tree's thresholds, the number
**dropped** from 0.7507 to 0.7480. The difference is not in the thresholds but in **the way the
conditions are combined**. The expert's rule joins three conditions with "or": every subscriber for
whom any one holds is flagged. The tree uses the same conditions nested — it looks at consumption per
person only in the branch where the drop is small, asks the number of readings as a second condition
in that branch, and in the branch where the drop is large it moves to an entirely different feature.

The count table gives the result. The expert's rule flags 151 subscribers and finds 75 of them
correctly; 76 visits go to waste. The tree flags 66 subscribers, finds 53 of them correctly, and 13
visits go to waste. The tree catches fewer illicit cases — 53 against 75 — but it produces about six
times fewer empty visits, and because the accuracy measure counts an empty visit with the same weight
as a missed illicit case, the tree wins.

This is the concrete counterpart of FC6. If the cost of an empty visit is lower than the cost of a
missed illicit case, the measure changes, and when the measure changes the ranking can change too:
the procedure in the last row catches 91 of the 93 suspicious subscribers and misses only 2, but with
93 empty visits. Which procedure is "good" is determined together with the weighting buried inside
accuracy.

## Summary

- Machine learning is deriving the rule from examples instead of writing it by hand; in this lesson
  the rule is a three-question decision tree derived from a training set of 883 subscribers.
- In this lesson the baseline is predicting the most frequent class and it scores 0.7533 on the
  held-out set; because the suspicious rate is 26.0 percent, even a non-learning procedure produces a
  number that looks high.
- The hand-written rule set scores 0.7507 on the held-out set and stays below the baseline; the tree
  learned from data scores 0.8594 and beats the baseline by 0.1061.
- The difference does not come from the thresholds: when the same thresholds are moved into the
  hand-written rule, the number drops to 0.7480. The difference comes from whether the conditions are
  nested or joined with "or".
- The accuracy measure counts an empty visit and a missed illicit case equally; while the tree catches
  53 illicit cases and produces 13 empty visits, the expert's rule catches 75 and produces 76 empty
  visits, and if the measure changes the ranking can change too.

## Next Step

This lesson used a single setup: every subscriber's label was known and the procedure saw all 883
labels. In this curriculum the label was produced for free, not in the field. In reality a label means
a field visit, and visits are countable. The next lesson builds the same problem in four separate
setups — supervised, unsupervised, semi-supervised and reinforcement — and counts how many labels each
setup asks for: how much of the difference above the baseline remains when the work is done with 60
labels instead of all 883, what an unlabeled setup can extract from the same set, and how a procedure
that learns the label by visiting spends its budget.
