Skip to content
academia.sh

Lesson 03 / 12

Data and Model Versioning

Measuring how well two different contents given the same name are told apart: when four deliveries come under the same `measurement-2026` name with 2200, 2180, 2160, and again 2200 rows, in an unversioned setup a single name leaves all four runs ambiguous and three different test numbers, 0.8067, 0.8064, and 0.8078, stand behind the same name, while naming by content signature produces three names and brings ambiguous runs to zero; on the model side, because the weights of the four runs are exactly the same, a model identity that looks only at the weights collects the four runs into a single version and three versions separate only when the data signature is added; as the cost, the date-named scheme stores 8740 rows while the content-addressed scheme stores 6540 rows and 8740 rows are read for the signature at every delivery; the output signature does not see a change the pipeline swallowed, and when sixty records arrive twice the source signature changes while the output signature and the accuracy stay exactly the same.

Published Updated

In this lesson

  • Separates the name of a data delivery with a signature produced from its content.
  • Compares the number of runs left ambiguous by unversioned, date-named, and content-addressed schemes.
  • Shows that a model's identity is not only its weights and computes the cost of versioning in stored rows.
Contents

The previous lesson’s ledger wrote down how many rows each run read. All eighteen runs read their data from the same place, and the name of that place never changed. When the ledger says “2200 rows”, we assumed that two runs read the same 2200 rows, but that assumption was never tested anywhere.

This lesson tests that assumption. Data lineage, that is, the record of the link from a serving value back to the source, was measured in the Introduction to Data Engineering course and is not repeated here. The question here is narrower: when the content under a name changes and the name does not, how many runs become indistinguishable from one another.

  • RP19. The fictional producer brings the data in four deliveries. The deliveries are 2200, 2180, 2160, and again 2200 rows; all four are written under the same measurement-2026 name. The fourth delivery is exactly identical in content to the first.
  • RP20. A data version is the pairing of a name with a content. Three schemes are compared: unversioned (a single name, overwritten), date-named (one name per delivery), and content-addressed (the name carries the signature produced from the content).
  • RP21. A signature is a short digest produced from the content and is written within the lesson; no external library is called. A signature is not a security tool but an identity tool.
  • RP22. The baseline is the unversioned setup: a single name, overwritten content, no retroactive reproduction.
  • RP23. The units measured are the number of names, ambiguous runs, and stored rows. An ambiguous run is a run that carries the same name yet gives a different result under that name.
  • RP24. The feature code is fixed in this lesson; all four runs see the full history and round to four digits. The only thing that changes is the content of the data.
  • RP25. The label distribution is {0: 1752, 1: 448}, the base class ratio is 0.7964, and the accuracy numbers in this lesson carry no performance claim. The resolution is 0.0017.

The Content Under the Same Name

The first block rebuilds, verbatim, the fictional source, pipeline and model of the previous two lessons.

# FICTIONAL. Meter readings, the pipeline and the model are defined within the lesson; no real
# learning library or model registry product is called.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

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


REGION = ["east", "west", "north", "south", "central"]
TYPE = ["residential", "commercial"]
PERIODS = [f"2026-{a:02d}" for a in range(1, 13)]
SUBSCRIBERS = {}
for i in range(200):
    r = rng(SEED + 37 * i)
    SUBSCRIBERS[f"A{i + 1:03d}"] = {"region": REGION[int(r() * 5)], "type": TYPE[int(r() * 2)],
                                    "start": 10000 + int(r() * 60000)}


def generate_source():
    records = []
    for i, (sub, a) in enumerate(SUBSCRIBERS.items()):
        e = a["start"]
        for p in PERIODS:
            r = rng(SEED + 101 * i + 7 * PERIODS.index(p))
            e += 4 + int(r() * 46)
            records.append({"subscriber": sub, "period": p, "region": a["region"], "type": a["type"],
                            "index": e, "unit": "m3"})
    return records


def s1_screen(records):
    REQUIRED = ("subscriber", "period", "region", "index", "unit")
    keep = [k for k in records if all(a in k for a in REQUIRED)]
    return keep, len(records) - len(keep)


def s2_dedupe(records):
    seen, keep = set(), []
    for k in records:
        key = (k["subscriber"], k["period"])
        if key in seen:
            continue
        seen.add(key)
        keep.append(k)
    return keep, len(records) - len(keep)


def s3_consumption(records):
    table = {(k["subscriber"], k["period"]): k for k in records}
    out, dropped = [], 0
    for k in records:
        i = PERIODS.index(k["period"])
        if i == 0:
            continue
        prev = table.get((k["subscriber"], PERIODS[i - 1]))
        if prev is None:
            dropped += 1
            continue
        try:
            f = int(k["index"]) - int(prev["index"])
        except (TypeError, ValueError):
            dropped += 1
            continue
        out.append(dict(k, m3=f))
    return out, dropped


TRAIN = PERIODS[1:9]
TEST = PERIODS[9:]
THRESHOLD = 40


def rows():
    v, _ = s1_screen(generate_source())
    v, _ = s2_dedupe(v)
    v, _ = s3_consumption(v)
    return v


def label(k):
    return 1 if k["m3"] > THRESHOLD else 0


def features(records, window=None, digits=4):
    """Independent of the weights , computed once."""
    ordered = sorted(records, key=lambda k: PERIODS.index(k["period"]))
    sub, reg = {}, {}
    for k in ordered:
        sub.setdefault(k["subscriber"], []).append((PERIODS.index(k["period"]), k["m3"]))
        reg.setdefault(k["region"], []).append((PERIODS.index(k["period"]), k["m3"]))

    def mean(series, i):
        chosen = [v for d, v in series if (d < i and (window is None or d >= i - window))]
        return round(sum(chosen) / len(chosen), digits) if chosen else 0.0

    feats = {}
    for k in records:
        i = PERIODS.index(k["period"])
        feats[(k["subscriber"], k["period"])] = {
            "subscriber_mean": mean(sub[k["subscriber"]], i),
            "region_mean": mean(reg[k["region"]], i),
            "commercial": 1 if k["type"] == "commercial" else 0}
    return feats


GRID = [(a, b, c) for a in (0.6, 0.8, 1.0) for b in (0.0, 0.2, 0.4) for c in (0, 4, 8)]


def score(o, w):
    return w[0] * o["subscriber_mean"] + w[1] * o["region_mean"] + w[2] * o["commercial"]


def train(records, feats, grid=None, threshold=THRESHOLD, periods=None):
    grid = GRID if grid is None else grid
    periods = TRAIN if periods is None else periods
    subset = [k for k in records if k["period"] in periods]
    best, best_w = -1.0, None
    for w in grid:
        d = sum(1 for k in subset
                if (1 if score(feats[(k["subscriber"], k["period"])], w) > threshold else 0) == label(k))
        o = d / len(subset)
        if o > best:
            best, best_w = o, w
    return {"weights": best_w, "train_accuracy": round(best, 4),
            "candidates": len(grid), "train_rows": len(subset)}


def evaluate(records, model, feats, threshold=THRESHOLD, periods=None):
    periods = TEST if periods is None else periods
    subset = [k for k in records if k["period"] in periods]
    d = sum(1 for k in subset
            if (1 if score(feats[(k["subscriber"], k["period"])], model["weights"]) > threshold else 0)
            == label(k))
    return round(d / len(subset), 4)

The four deliveries run in the block below. In each delivery the same code reads the data under the same name, trains the model, and writes the result under the name consumption-threshold. signature produces a short digest from the content and is only reported for now; it enters no decision.

S = rows()


def signature(text):
    """A short digest produced from the content. Not security , an identity tool."""
    h = 2166136261
    for c in text:
        h = ((h ^ ord(c)) * 16777619) & 0xFFFFFFFF
    return f"{h:08x}"


def data_signature(records):
    return signature("|".join(f"{k['subscriber']}:{k['period']}:{k['m3']}" for k in records))


DELIVERIES = [2200, 2180, 2160, 2200]
RUNS = []
for no, n in enumerate(DELIVERIES, start=1):
    rec = S[:n]
    feats = features(rec)
    m = train(rec, feats)
    RUNS.append({"run": no, "data_name": "measurement-2026", "rows": n,
                 "data_signature": data_signature(rec), "model_name": "consumption-threshold",
                 "weights": m["weights"], "test": evaluate(rec, m, feats)})
print(f"{'delivery':<10}{'data name':<18}{'rows':>7}{'data signature':>16}"
      f"{'model name':>24}{'weights':>17}{'test':>9}")
for k in RUNS:
    print(f"{k['run']:<10}{k['data_name']:<18}{k['rows']:>7}{k['data_signature']:>16}"
          f"{k['model_name']:>24}{str(k['weights']):>17}{k['test']:>9.4f}")
print()
print("different test numbers:", len(set(k["test"] for k in RUNS)),
      " different weights:", len(set(k["weights"] for k in RUNS)),
      " different data contents:", len(set(k["data_signature"] for k in RUNS)))
delivery  data name            rows  data signature              model name          weights     test
1         measurement-2026     2200        bd88a253   consumption-threshold    (0.6, 0.4, 0)   0.8067
2         measurement-2026     2180        f008c71c   consumption-threshold    (0.6, 0.4, 0)   0.8064
3         measurement-2026     2160        cebfcc57   consumption-threshold    (0.6, 0.4, 0)   0.8078
4         measurement-2026     2200        bd88a253   consumption-threshold    (0.6, 0.4, 0)   0.8067

different test numbers: 3  different weights: 1  different data contents: 3

In the table, two columns never change: the data name is measurement-2026 in all four rows, and the model name is consumption-threshold in all four rows. One more column never changes: the weights are (0.6, 0.4, 0) in all four runs, that is, the model file that is produced is exactly the same. Two columns change: the content signature takes three different values, and the test accuracy gives three different numbers.

How Many Contents a Name Has

What is to be counted is now clear. If a naming scheme leaves runs that carry the same name yet give different results, those runs are ambiguous: which content they came from cannot be read from the name.

def measure(identity):
    """How many names there are , how many runs stay ambiguous , how many rows are stored."""
    group = {}
    for k in RUNS:
        group.setdefault(identity(k), []).append(k)
    ambiguous = sum(len(v) for v in group.values() if len(set(x["test"] for x in v)) > 1)
    return len(group), ambiguous, sum(v[-1]["rows"] for v in group.values())


print(f"{'scheme':<18}{'data names':>12}{'ambiguous runs':>16}{'stored rows':>14}")
for name, ident in (("unversioned", lambda k: k["data_name"]),
                    ("date named", lambda k: f"{k['data_name']}@t{k['run']}"),
                    ("content addressed", lambda k: f"{k['data_name']}@{k['data_signature']}")):
    a, b, s = measure(ident)
    print(f"{name:<18}{a:>12}{b:>16}{s:>14}")
print()
print(f"{'model identity':<26}{'versions':>10}{'ambiguous runs':>16}")
for name, ident in (("weights only", lambda k: signature(str(k["weights"]))),
                    ("data signature + weights", lambda k: signature(f"{k['data_signature']}|{k['weights']}"))):
    a, b, _ = measure(ident)
    print(f"{name:<26}{a:>10}{b:>16}")
print()
print("rows read for the signature:", sum(DELIVERIES))
scheme              data names  ambiguous runs   stored rows
unversioned                  1               4          2200
date named                   4               0          8740
content addressed            3               0          6540

model identity              versions  ambiguous runs
weights only                       1               4
data signature + weights           3               0

rows read for the signature: 8740

In the unversioned setup there is a single name, and all four runs are ambiguous. This row is the data-side form of the course’s rule: when the number 0.8078 goes into a report, the content that produced it no longer exists, because the fourth delivery overwrote the third. The stored rows column confirms it: the unversioned scheme takes up the least room, 2200 rows, and in return loses the whole history.

The date-named scheme brings the ambiguity to zero, but it has a flaw: it produces four names although there are three contents. The first and the fourth deliveries are exactly the same content, and since the date scheme cannot see this, it stores the same 2200 rows twice. The content-addressed scheme covers the same ambiguity with three names and 6540 rows; the fourth delivery takes up no new room, because its signature is already in the ledger.

What the Signature Signs

data_signature signs the pipeline’s output, that is, the triple of subscriber, period, and computed consumption. This is a choice and it has a cost: a producer-side change that the pipeline swallows never shows up in the output signature. The block below measures this. Suppose the producer sends sixty records twice in July; the source has changed, but the deduplication stage drops the repeated rows.

def source_signature(records):
    return signature("|".join(f"{k['subscriber']}:{k['period']}:{k['index']}" for k in records))


def feed_pipeline(source):
    v, _ = s1_screen(source)
    v, _ = s2_dedupe(v)
    v, _ = s3_consumption(v)
    return v


raw = generate_source()
# The producer sent 60 records twice: the source changed , the pipeline swallows it.
duplicated = raw + [dict(k) for k in raw if k["subscriber"] <= "A060" and k["period"] == "2026-07"]
print(f"{'delivery':<12}{'source rows':>13}{'source signature':>18}"
      f"{'output rows':>13}{'output signature':>18}{'test':>9}")
for name, source in (("intact", raw), ("duplicated", duplicated)):
    c = feed_pipeline(source)
    feats2 = features(c)
    m2 = train(c, feats2)
    print(f"{name:<12}{len(source):>13}{source_signature(source):>18}{len(c):>13}"
          f"{data_signature(c):>18}{evaluate(c, m2, feats2):>9.4f}")
print()
print(f"{'delivery':<10}{'rows':>7}{'test rows':>11}{'resolution':>12}")
for n in DELIVERIES[:3]:
    test_rows = [k for k in S[:n] if k["period"] in TEST]
    print(f"{n:<10}{n:>7}{len(test_rows):>11}{1 / len(test_rows):>12.4f}")
delivery      source rows  source signature  output rows  output signature     test
intact               2400          d501f04f         2200          bd88a253   0.8067
duplicated           2460          93576832         2200          bd88a253   0.8067

delivery     rows  test rows  resolution
2200         2200        600      0.0017
2180         2180        594      0.0017
2160         2160        588      0.0017

The source signature changed, the output signature stayed exactly the same, and so did the accuracy. The version records of the two deliveries point to the same rows, although the content that came from the producer is different. This is the correct behavior of the pipeline, and it is a limit of versioning: the output signature does not see a change the pipeline swallowed. In the opposite direction it is also lacking; a scheme that signs the source flags a version difference here although the data going to the model has not changed at all. The two signatures answer two separate questions, and a data version stays incomplete unless it is written which one is kept.

The lower table closes another detail. As the delivery gets smaller, the test set gets smaller too: 600, 594, and 588 rows. Across the three deliveries the resolution stays at 0.0017, but the numbers being compared were not measured on the same set. For this reason the difference between 0.8067 and 0.8064 cannot be read as a performance difference.

A Model’s Identity Is Not Only Its Weights

The lower table carries the lesson’s quietest finding. The model file is the same in all four runs, so a scheme that names the model by its own content collects the four runs into a single version and leaves all four ambiguous. This is the mirror image of the error on the data side: there a name concealed three contents, here a content conceals three numbers.

The four model files look interchangeable, because they carry the same weights. They are not interchangeable: each was measured on a different data set, and the numbers they report are 0.8067, 0.8064, and 0.8078. The differences of 0.0014 and 0.0011 in between are below the resolution of 0.0017, and the numbers were moreover not measured on the same set; the reported number is not enough to tell these four files apart. When the data signature is added to the weights, the identity splits into three versions and ambiguous runs fall to 0. A model version is not the signature of the model file but the joint signature of the model file and the data version that produced it.

The Cost of Versioning and What It Does Not Say

The cost has three items. The first is stored rows: the content-addressed scheme holds 6540 rows, 2.97 times the unversioned scheme. The second is rows read: since the signature is computed from the content, all the rows are read once more at every delivery, 8740 rows over four deliveries. The third is the added step: the write operation now has to compute a signature and append a ledger row.

What is gained in return is narrow and exact: versioning says which content produced which number. What it does not say is which content is correct. Which of the three deliveries came in incomplete, which was whole, and whether the number 0.8078 can be trusted are things none of the three signatures tells; what tells them is the checks measured in the Introduction to Data Engineering course. Versioning is not a quality tool but an identity tool, and its contribution is measured only by the number of ambiguous runs.

Summary

  • Four deliveries brought 2200, 2180, 2160, and again 2200 rows under the same measurement-2026 name; the name never changed, the content received three different signatures, and the test accuracy came out 0.8067, 0.8064, and 0.8078.
  • In the unversioned scheme there is a single name and all four runs are ambiguous; in the content-addressed scheme there are three names and ambiguous runs are 0.
  • The date-named scheme closes the ambiguity in the same way but produces four names and stores 8740 rows; the content-addressed scheme does the same job with three names and 6540 rows, that is, 2.97 times the unversioned scheme, because it sees that the fourth delivery is the same as the first.
  • The output signature does not see a change the pipeline swallowed: when sixty records arrive twice, the source signature changes and the output signature and the accuracy stay exactly the same. A data version stays incomplete unless it is written which signature is kept.
  • The model file is exactly the same in all four runs. A model identity that looks only at the weights collects the four runs into a single version; when the data signature is added, three versions separate and ambiguous runs become 0.
  • Versioning says which content produced which number, but it does not say which is correct; that question belongs to the checks.

Next Step

In three lessons the run itself was recorded and versioned: which items it was run with was written down, which content it read was tied to a signature, and the three contents behind the same name were told apart. All of this work was done on the training side. It was never asked whether the feature that goes into the model could be computed by one piece of code during training and by another piece of code when called in production. The next lesson puts those two paths side by side and measures a single question: when the weights are exactly the same, why does the accuracy drop.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close