---
title: 'Building Narrative with Data'
source: 'https://academia.sh/en/courses/visualization-reporting-and-analysis-at-scale/building-narrative-with-data'
course: 'Visualization, Reporting and Analysis at Scale'
language: en
updated: '2026-10-07T04:31:14+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes the narrative that runs from finding to recommendation as a five-step chain of premises and separates the premises each step carries'
    - 'Counts the inference the reader is forced to make when a step is removed and measures how many of these inferences fall in the wrong direction'
    - 'Settles at which step of the narrative the margin of variation must stand by comparing three placements'
---

# Building Narrative with Data

When one step is removed from the five-step chain that runs from finding to recommendation, the premises the reader is forced to complete on their own are counted: removing the finding leaves 4 inferences open, removing the comparison 3, removing the candidate cause 2, removing the uncertainty 3, removing the recommendation 3. Of the eleven wrong defaults the chain carries, nine strengthen the recommendation and two weaken it — the gaps are not filled at random. The place of the margin of variation of the Statistical Analysis and Exploratory Data Analysis course is settled: if the margin is never written, 4 inferences and 3 wrong defaults remain; if it is written only at the end in a limitations paragraph, 1 inference and 1 wrong default remain; when it is written next to the finding and in the uncertainty step, both go to zero.

The previous three lessons fixed the number: the layout was split to support the decision, the
definition of the metric was made ungameable, and the data cut-off and the refresh lag were written
next to the number. What the reader holds is still a number. For a number to turn into a decision it
has to sit in a chain, and if the links of the chain are missing, the reader completes them on their
own.

The measure of this lesson is **the gap that a skipped step leaves**: the number of inferences the
reader is forced to make when a link is removed. A second number is written next to it: how many of
the defaults of these inferences are wrong with respect to the dataset, and how many of those wrong
defaults **strengthen** the recommendation.

- **RP19.** The dataset is inherited from the Data Collection and Preparation course and is **fictional**: a reading table of 3,199 rows,
  1,329 subscribers, three periods, five regions. The seed is written into the code.
- **RP20.** The finding comes from the Statistical Analysis and Exploratory Data Analysis course: the east mean is 24.674 m³ (557 readings), the north 20.210
  m³ (921 readings), the gap 4.463 m³. **The test computation is not repeated here**; the numbers
  are read from there.
- **RP21.** Both numbers of the margin are read from the Statistical Analysis and Exploratory Data Analysis course: when the region labels were reshuffled
  2,000 times, the observed gap was not exceeded in any run; the spread of the four defensible
  decisions on mass, sample, zero readings and the number of periods is 0.206 m³.
- **RP22.** The narrative chain is **fictional**: five steps, sixteen premises, fifteen of them
  needed to defend the recommendation.
- **RP23.** When a step is removed, the reader is assumed to complete the premises of that step
  on their own. The default of each premise and the direction of the default are written in the
  lesson.
- **RP24.** The recommendation is also **fictional**: a review of the tariff tier boundary in the
  east region.

## The Chain

```python
# narrative.py — MODEL: the fictional dataset from K03 is generated with the same seed; on top
# of it a five-step narrative chain that runs from finding to recommendation is built.
import math

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


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


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


SUBSCRIBERS, DISCARDED = [], []
for i in range(RAW):
    r = rng(SEED + i)
    b = REGION[pick(r(), [x[1] for x in REGION])]
    pick(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05])
    pick(r(), [0.08, 0.14, 0.27, 0.34, 0.17])
    (DISCARDED if r() < 0.046 else SUBSCRIBERS).append(
        {"subscriber_id": 10001 + i, "region": b[0], "base": b[2]})
VALID = {k["subscriber_id"] for k in SUBSCRIBERS}
READINGS = {}
for k in SUBSCRIBERS + DISCARDED:
    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.setdefault(k["subscriber_id"], {})[d + 1] = t
        r()

MEANS = {}
for b, _, _ in REGION:
    v = [t for k in SUBSCRIBERS if k["region"] == b
         for t in READINGS.get(k["subscriber_id"], {}).values()]
    MEANS[b] = (sum(v) / len(v), len(v))
GAP = MEANS["east"][0] - MEANS["north"][0]
WIDEST = max(MEANS.values())[0] - min(MEANS.values())[0]
ORDERED = sorted(MEANS, key=lambda b: -MEANS[b][0])

# Narrative chain: each step carries a set of premises. Premise fields: code, step,
# content, the default the reader falls to if the step is missing, whether the default is wrong
# for the dataset, whether it is needed to defend the recommendation, whether the default
# strengthens the recommendation (+1/-1).
PREMISES = [
    ("O1", "finding", f"east mean {MEANS['east'][0]:.3f} m3",
     "number unknown", False, True, 0),
    ("O2", "finding", f"north mean {MEANS['north'][0]:.3f} m3",
     "number unknown", False, True, 0),
    ("O3", "finding", f"numbers rest on {MEANS['east'][1]} and {MEANS['north'][1]} readings",
     "observation count is large", True, True, 1),
    ("O4", "finding", "the number in the finding was written with its margin",
     "number is exact", True, True, 1),
    ("O5", "comparison", f"gap {GAP:.3f} m3", "gap unknown", False, True, 0),
    ("O6", "comparison", f"gap is {GAP / MEANS['north'][0] * 100:.1f} percent of north",
     "gap is small", True, True, -1),
    ("O7", "comparison",
     f"widest gap of the five regions {WIDEST:.3f} m3 ({ORDERED[0]}-{ORDERED[-1]})",
     "gap is the only important gap", True, True, 1),
    ("O8", "candidate cause", "candidate: east base consumption is higher",
     "no cause exists", False, True, 0),
    ("O9", "candidate cause", "rival candidate: reading coverage gap",
     "a single cause exists", True, True, 1),
    ("O10", "candidate cause", "rival candidate: tariff tier distribution",
     "a single cause exists", True, False, 1),
    ("O11", "uncertainty", "K05: not exceeded in 2,000 reshuffles", "margin not measured", False, True, 0),
    ("O12", "uncertainty", "K05: spread of four decisions 0.206 m3", "margin is zero", True, True, 1),
    ("O13", "uncertainty", "what the margin does not cover was named",
     "margin covers everything", True, True, 1),
    ("O14", "recommendation", "recommended action was written", "action is the reader's choice", True, True, -1),
    ("O15", "recommendation", "reversibility of the action was written",
     "action is reversible", True, True, 1),
    ("O16", "recommendation", "which metric the action will be measured by", "no measurement will be done", True, True, 1),
]
STEPS = ["finding", "comparison", "candidate cause", "uncertainty", "recommendation"]
print(f"fictional dataset (seed {SEED}): {len(SUBSCRIBERS)} subscribers, "
      f"{sum(len(v) for v in READINGS.values())} readings")
print(f"finding: east {MEANS['east'][0]:.3f} m3 ({MEANS['east'][1]} readings), "
      f"north {MEANS['north'][0]:.3f} m3 ({MEANS['north'][1]} readings), gap {GAP:.3f} m3")
print(f"chain: {len(STEPS)} steps, {len(PREMISES)} premises, "
      f"{sum(1 for o in PREMISES if o[5])} of them needed to defend the recommendation")
for a in STEPS:
    print(f"  {a:<15} " + ", ".join(o[0] for o in PREMISES if o[1] == a))
```

```
fictional dataset (seed 20260218): 1329 subscribers, 3199 readings
finding: east 24.674 m3 (557 readings), north 20.210 m3 (921 readings), gap 4.463 m3
chain: 5 steps, 16 premises, 15 of them needed to defend the recommendation
  finding         O1, O2, O3, O4
  comparison      O5, O6, O7
  candidate cause O8, O9, O10
  uncertainty     O11, O12, O13
  recommendation  O14, O15, O16
```

Here the narrative is not a style but a **set of premises**. If the recommendation is to be
defensible, the premises standing behind it must have been given to the reader; every
premise that is not given is produced by the reader. What the model does is count this
production.

The name of the third step is also deliberately not "cause" but **candidate cause**. The report
writes that the base consumption of the east region is higher as a candidate, puts the two rival
candidates beside it, and does not decide among them; there is no mechanism in the dataset that
would make this decision. Writing the candidates is not an evasion; it is the only way to keep the
reader from falling onto a single candidate.

## The Skipped Step

```python

# One step is removed from the chain and the premises the reader is forced to complete on
# their own are counted as needed. Wrong default: a completion that is not true for the dataset.
def inference_gap(removed=None, dropped=frozenset()):
    missing = [o for o in PREMISES if o[5] and (o[1] == removed or o[0] in dropped)]
    wrong = [o for o in missing if o[4]]
    return missing, wrong, sum(1 for o in wrong if o[6] > 0)


print(f"{'removed step':<22}{'reader inference':>20}{'wrong default':>19}"
      f"{'strengthening':>21}")
for a in [None] + STEPS:
    e, y, g = inference_gap(a)
    print(f"{a or 'none (full chain)':<22}{len(e):>20}{len(y):>19}{g:>21}")
print(f"\nall wrong defaults in the whole chain and their directions")
for o in PREMISES:
    if o[4]:
        print(f"  {o[0]:<4} {o[1]:<16} the reader falls to this default: {o[3]:<30}"
              f"{'strengthens the recommendation' if o[6] > 0 else 'weakens the recommendation'}")
every_wrong = [o for o in PREMISES if o[4]]
print(f"of {len(every_wrong)} wrong defaults in total, {sum(1 for o in every_wrong if o[6] > 0)} "
      f"strengthen the recommendation, {sum(1 for o in every_wrong if o[6] < 0)} weaken it")
```

```
removed step              reader inference      wrong default        strengthening
none (full chain)                        0                  0                    0
finding                                  4                  2                    2
comparison                               3                  2                    1
candidate cause                          2                  1                    1
uncertainty                              3                  2                    2
recommendation                           3                  3                    2

all wrong defaults in the whole chain and their directions
  O3   finding          the reader falls to this default: observation count is large    strengthens the recommendation
  O4   finding          the reader falls to this default: number is exact               strengthens the recommendation
  O6   comparison       the reader falls to this default: gap is small                  weakens the recommendation
  O7   comparison       the reader falls to this default: gap is the only important gap strengthens the recommendation
  O9   candidate cause  the reader falls to this default: a single cause exists         strengthens the recommendation
  O10  candidate cause  the reader falls to this default: a single cause exists         strengthens the recommendation
  O12  uncertainty      the reader falls to this default: margin is zero                strengthens the recommendation
  O13  uncertainty      the reader falls to this default: margin covers everything      strengthens the recommendation
  O14  recommendation   the reader falls to this default: action is the reader's choice weakens the recommendation
  O15  recommendation   the reader falls to this default: action is reversible          strengthens the recommendation
  O16  recommendation   the reader falls to this default: no measurement will be done   strengthens the recommendation
of 11 wrong defaults in total, 9 strengthen the recommendation, 2 weaken it
```

The first row of the table confirms the definition of the chain: in the full chain no inference is
left to the reader, because all fifteen of the fifteen premises needed to defend the
recommendation have been written. This row is not a measure of success but the measuring
instrument's own zero point; the remaining five rows are read against it.

None of the five steps is free. When the finding is removed, four inferences are left to the reader;
when the uncertainty or the recommendation is removed, three each; when the comparison is removed,
three; when the candidate cause is removed, two. The numbers are close to each other and, read
alone, do not say which step is more expensive to skip.

The second and the third columns make the distinction. Of the eleven wrong defaults, nine
**strengthen** the recommendation and only two weaken it. The gaps are not filled at random: when
the reader cannot see a margin next to a number, the reader counts the margin as zero; when the
reader cannot see a rival candidate cause, the reader assumes there is a single cause; when the
number of observations is not written, the reader accepts that the number comes from a large
dataset. All three make the recommendation look sturdier than it is.

The result is a methodological rule. Skipping a narrative step makes the recommendation **easier to
defend**, and this ease is independent of the author's intent. The two weakening defaults do not
break this rule; on the contrary, they explain it: when the reader does not see the size of the gap,
the reader counts it as small, and when the recommended action is not written, the reader chooses the
action. These two are not skipped, because they do not serve the author; the steps that are skipped
are the steps that serve once they are skipped.

## The Place of the Margin

At this point the place of the margin of variation of the Statistical Analysis and Exploratory Data Analysis course in the chain can be settled. Three placements
are tried.

```python

# The place of the margin in the narrative is tried with three placements. The margin was
# measured in K05 and is not recomputed here; what is asked is at which step of the chain it stands.
PLACEMENT = [("margin not written at all", {"O4", "O11", "O12", "O13"}),
             ("margin only at the end, in a limitations paragraph", {"O4"}),
             ("margin next to the finding and in the uncertainty step", set())]
print(f"{'placement of the margin':<58}{'inference':>9}{'wrong':>8}{'strengthening':>15}")
for name, dropped in PLACEMENT:
    e, y, g = inference_gap(dropped=dropped)
    print(f"{name:<58}{len(e):>9}{len(y):>8}{g:>15}")

print("\n--- chain of the third placement ---------------------------------")
for a in STEPS:
    print(f"  [{a}]")
    for o in PREMISES:
        if o[1] == a:
            print(f"     {o[0]:<4} {o[2]}{'' if o[5] else '   (not needed)'}")
```

```
placement of the margin                                   inference   wrong  strengthening
margin not written at all                                         4       3              3
margin only at the end, in a limitations paragraph                1       1              1
margin next to the finding and in the uncertainty step            0       0              0

--- chain of the third placement ---------------------------------
  [finding]
     O1   east mean 24.674 m3
     O2   north mean 20.210 m3
     O3   numbers rest on 557 and 921 readings
     O4   the number in the finding was written with its margin
  [comparison]
     O5   gap 4.463 m3
     O6   gap is 22.1 percent of north
     O7   widest gap of the five regions 10.896 m3 (east-west)
  [candidate cause]
     O8   candidate: east base consumption is higher
     O9   rival candidate: reading coverage gap
     O10  rival candidate: tariff tier distribution   (not needed)
  [uncertainty]
     O11  K05: not exceeded in 2,000 reshuffles
     O12  K05: spread of four decisions 0.206 m3
     O13  what the margin does not cover was named
  [recommendation]
     O14  recommended action was written
     O15  reversibility of the action was written
     O16  which metric the action will be measured by
```

The second placement is the common one, and it leaves one inference open. The margin is written, but
it is written at the end: while reading the finding the reader saw no interval next to the number and
had already formed the "the number is exact" default. The limitations paragraph that comes at the end
does not take this default back; it only clears the author's record. The margin has not one place but
**two places**, and the first is next to the number in the finding.

The second is the uncertainty step, and there the margin says two different things. The gap that was
not exceeded in the 2,000 reshuffles of the Statistical Analysis and Exploratory Data Analysis course closes a single question: would this gap come out if the
region labels were distributed at random. The 0.206 m³ spread of the four defensible decisions is an
entirely different uncertainty, and the reshuffling margin does not measure it at all. This is why a
third premise stands in the chain: **what the margin does not cover is named.** When it is not
named, the default the reader falls to is "the margin covers everything", and this default too is
among those that strengthen the recommendation.

The last three premises of the chain tie the recommendation to an action: the action is written,
whether it is reversible is written, the metric by which it will be measured is written. The last
one ties the previous two lessons to this point. If the metric is not written, the recommendation is
never tested; if the metric is written, its definition has to be tight, otherwise the number that
evaluates the recommendation can be moved without moving what it measures.

## Summary

- The narrative is not a style but a five-step chain of premises: finding, comparison, candidate
  cause, uncertainty, recommendation. The chain carries sixteen premises and fifteen of them are
  needed to defend the recommendation.
- When a step is removed, the number of inferences left to the reader is between 2 and 4; this number
  alone does not say which step is more expensive.
- Of the chain's eleven wrong defaults, nine strengthen the recommendation and two weaken it;
  skipping a step makes the recommendation easier to defend, and this ease is independent of the
  author's intent.
- The margin of variation has not one place but two: next to the number in the finding and in the
  uncertainty step. A separate limitations paragraph at the end leaves 1 inference open, and that
  inference is the "the number is exact" default.
- The uncertainty step also names what the margin does not cover: the 2,000-reshuffle margin of the Statistical Analysis and Exploratory Data Analysis course closes
  only the question of random distribution of the labels, and does not measure the 0.206 m³ spread of
  the four decisions.
- The third step writes not the cause but the candidate causes; the last three premises of the
  chain tie the recommendation to an action, to the reversibility of the action and to the metric
  that will test it.

## Next Step

The report now stands on a chain: the numbers are in a layout that can be read back, the definitions
of the metrics are tight, the cut-off of the source is written, the links of the narrative are
complete. All of this concerns producing a single report **once**. Whether the same report can be
produced again three months later, by another person, so that it gives the same numbers, was never
asked. The next lesson counts this: how many items have to be fixed to produce the same report a
second time, how much the number moves when one is missing, and how many items the report adds on top
of the items counted by the Data Collection and Preparation course and the Data Analysis with Python course.
