Lesson 01 / 11
Worksheet Structure
A single amount formula is copied to 60 rows, and when the reference decision changes the total drops from 13,427.16 fee units to 3,354.98: the relative reference spreads wrongly to 59 cells, 52 of them zero and 7 a wrong number that reads like an amount. The third version, written with an embedded literal, gives the same total, but when the unit rate rises from 8.4 to 9.1 it follows in 0 cells instead of 60. Of the 8 parameters on the sheet, 6 enter no formula.
Published Updated
In this lesson
- Distinguishes cell, range, and reference types on a worksheet model
- Writes the same calculation with three reference decisions and prints the totals and the wrong-cell counts side by side
- Measures how many cells follow when one parameter changes, as whether the decision can be read back from the sheet
Contents
The Introduction to Data Analytics course closed with a question: nine lessons showed that decisions move the number, but every decision was made by hand and none of them is written down anywhere; will a person who does the same calculation a second time find the same number? The spreadsheet is the first answer to that question. The calculation no longer lives in one person’s head but in a cell, and a cell can be recalculated.
This course measures how much of the answer it holds. The measure has two parts, and in every lesson two numbers sit side by side. The first carries over from the previous course: the number and the decision that produced it. The second is this course’s own measure: decision recoverability — in how many steps the second person looking at the sheet can read that decision back, and how many decisions cannot be read back at all. This lesson sets up both on a single formula.
- BO1. The worksheet here is a model: cells are a map, formulas are a short string, ranges are a slice function. The model does not imitate any application; both the copy rule and the evaluation rule are written out explicitly below.
- BO2. The dataset is the regional measurement network of the Introduction to Data Analytics course: subscriber records, meter readings, field visits, tariff tiers. Both the network and the records are fictional; the sheet here is a field list of 60 subscriptions and is generated from its own seed.
- BO3. A cell name is a column letter plus a row number. A reference is shifted as far as it is
copied if it is relative, and does not move if it is absolute (marked with
$). In a mixed reference only the marked part stays fixed. - BO4. A blank cell is read as
0in a numeric context in this model. This is a decision and it affects the result directly; the next lesson separately measures whether a blank cell is counted. - BO5. A decision counts as readable back if the second person can find, by reading the formula text of the cell, where the decision is held and what its value is. A constant number embedded inside a formula is not readable back — the number is visible, but where it came from is not.
- BO6. The charge is a fee unit; the scale is the model’s own scale.
Sheet, Cell, Range
The block below builds the model. It has three parts: the cell map, the copy rule applied by the
shift function, and the evaluation applied by the evaluate function. Evaluation is recursive —
one formula can refer to another formula.
// sheet.mjs — spreadsheet MODEL: cells are a map, a formula is a string, a range is a slice // function. The sheet and the records are fictional; no application's behavior is imitated. export const SEED = 20260602, ROWS = 60; const rng = (t) => { let s = t >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; }; const REGION = ["north", "south", "east", "west", "central"]; export const REF = /(\$?)([A-Z])(\$?)(\d+)/g; const parse = (a) => ({ s: a.charCodeAt(a[0] === "$" ? 1 : 0) - 65, r: +a.replace(/\D/g, "") }); const cellName = (s, r) => String.fromCharCode(65 + s) + r; // Copy rule: a relative reference shifts as far as it is copied, an absolute reference ($) does not. export const shift = (f, dS, dR) => f.replace(REF, (_, a, s, b, r) => (a ? "$" + s : String.fromCharCode(s.charCodeAt(0) + dS)) + (b ? "$" + r : String(+r + dR))); const FUNCS = { sum: (d) => d.reduce((a, b) => a + b, 0) }; const RANGE = /(\$?[A-Z]\$?\d+):(\$?[A-Z]\$?\d+)/g; const expand = (a, b) => { const x = parse(a), y = parse(b), l = []; for (let s = x.s; s <= y.s; s++) for (let r = x.r; r <= y.r; r++) l.push(cellName(s, r)); return l; }; export function evaluate(H, name) { const v = H[name]; if (v === undefined) return 0; // a blank cell is read as 0 in this model if (typeof v !== "string" || v[0] !== "=") return v; const expr = v.slice(1) .replace(RANGE, (_, a, b) => "[" + expand(a, b).map((c) => evaluate(H, c)).join(",") + "]") .replace(REF, (m) => String(evaluate(H, m.replace(/\$/g, "")))); return Function(...Object.keys(FUNCS), `return (${expr});`)(...Object.values(FUNCS)); } export function buildSheet() { const r = rng(SEED), H = {}; H.A1 = "subscriber"; H.B1 = "region"; H.C1 = "consumption"; H.E1 = "amount"; for (let i = 0; i < ROWS; i++) { const s = i + 2; H["A" + s] = "AB-" + String(1041 + i * 7).padStart(4, "0"); H["B" + s] = REGION[Math.floor(r() * 5)]; H["C" + s] = Math.round((2 + r() * r() * 78) * 10) / 10; } // Parameter block: label on the left, value on the right. Tariff tiers are 10/25/40 m3. H.G1 = "parameter"; H.H1 = "value"; ["unit rate", "fixed charge", "meter service", "loss share", "tier 1", "tier 2", "tier 3", "discount"].forEach((e, j) => { H["G" + (j + 2)] = e; }); [8.4, 25, 3.5, 1.2, 10, 25, 40, 0.9].forEach((v, j) => { H["H" + (j + 2)] = v; }); return H; } if (import.meta.url === `file://${process.argv[1]}`) { const H = buildSheet(); const filled = Object.keys(H).length; console.log(`seed ${SEED}: ${ROWS} subscriber rows, ${filled} filled cells`); console.log("parameter block H2:H9 = " + expand("H2", "H9").map((c) => H[c]).join(", ") + "\n"); const g = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join(""); const y = g([8, 12, 13]); console.log(y(["cell", "subscriber", "consumption"])); for (const s of [2, 3, 4, 60, 61]) console.log(y(["C" + s, H["A" + s], H["C" + s].toFixed(1)])); console.log(`\nC2:C61 total ${evaluate({ ...H, X1: "=sum(C2:C61)" }, "X1").toFixed(1)} m3`); console.log("shift('=C2*$H$2+H3', 0, 1) -> " + shift("=C2*$H$2+H3", 0, 1)); }
seed 20260602: 60 subscriber rows, 202 filled cells
parameter block H2:H9 = 8.4, 25, 3.5, 1.2, 10, 25, 40, 0.9
cell subscriber consumption
C2 AB-1041 9.8
C3 AB-1048 71.6
C4 AB-1055 6.7
C60 AB-1447 3.0
C61 AB-1454 21.2
C2:C61 total 1419.9 m3
shift('=C2*$H$2+H3', 0, 1) -> =C3*$H$2+H4
The last line holds the whole lesson. When the same formula was copied one row down, $H$2 stayed in
place and H3 became H4. Copying is a text operation: the formula inside the cell does not stay
the same but is written out shifted. The person writing decides which reference will shift, and that
decision is made with a single mark.
The Same Calculation, Three Reference Decisions
The calculation is one: the amount is consumption times the unit rate plus the fixed charge. All three versions of it look defensible. The first refers to the parameters absolutely, the second refers to them relatively, and the third writes the numbers into the formula.
// copy.mjs — the same calculation is written with three reference decisions and copied to 60 rows. import { buildSheet, evaluate, shift, ROWS } from "./sheet.mjs"; const DECISIONS = [ { name: "A: absolute ($H$2, $H$3)", f: "=C2*$H$2+$H$3" }, { name: "B: relative (H2, H3)", f: "=C2*H2+H3" }, { name: "C: embedded literal", f: "=C2*8.4+25" }, ]; // A formula is written to E2 and copied down to E61: each copy is shifted by the row difference. const fill = (H, f) => { const K = { ...H }; for (let i = 0; i < ROWS; i++) K["E" + (i + 2)] = shift(f, 0, i); return K; }; const column = (K) => Array.from({ length: ROWS }, (_, i) => evaluate(K, "E" + (i + 2))); const g = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join(""); const H = buildSheet(); const D = DECISIONS.map((k) => column(fill(H, k.f))); const y = g([8, 13, 12, 12, 12]); console.log(y(["cell", "consumption", "A amount", "B amount", "C amount"])); for (const s of [2, 3, 5, 9, 10, 30, 61]) { const i = s - 2; console.log(y(["E" + s, H["C" + s].toFixed(1), D[0][i].toFixed(2), D[1][i].toFixed(2), D[2][i].toFixed(2)])); } const t = (d) => d.reduce((a, b) => a + b, 0); // Floating-point comparison is done with a threshold: amounts are not distinguished below a cent, // so two values closer than 0.005 fee units count as "the same". const EPSILON = 0.005; const same = (a, b) => Math.abs(a - b) < EPSILON; const z = g([26, 12, 8, 10, 8]); console.log("\n" + z(["decision", "total", "wrong", "zero", "visible"])); DECISIONS.forEach((k, a) => { const wrong = D[a].filter((v, i) => !same(v, D[0][i])).length; const zero = D[a].filter((v) => same(v, 0)).length; console.log(z([k.name, t(D[a]).toFixed(2), wrong, zero, wrong - zero])); }); console.log(`\nA single formula was written to E2 and copied to ${ROWS - 1} cells.`);
cell consumption A amount B amount C amount E2 9.8 107.32 107.32 107.32 E3 71.6 626.44 1793.50 626.44 E5 26.8 250.12 42.16 250.12 E9 5.5 71.20 4.95 71.20 E10 49.1 437.44 0.00 437.44 E30 25.4 238.36 0.00 238.36 E61 21.2 203.08 0.00 203.08 decision total wrong zero visible A: absolute ($H$2, $H$3) 13427.16 0 0 0 B: relative (H2, H3) 3354.98 59 52 7 C: embedded literal 13427.16 0 0 0 A single formula was written to E2 and copied to 59 cells.
The number to report is 13,427.16 fee units. Decision B produced 3,354.98 from the same calculation; the difference is 10,072.18, three quarters of the correct total. A single mark moved that much.
The spread columns say something more important. The formula written is one; the wrong cell is
also a single cell. But copying multiplied that cell into 59 cells, and all 59 produced a wrong
value. Of these, 52 are zero — they stand out. The remaining 7 are not zero: cell E5 reads 42.16
instead of 250.12, because the shifting reference multiplied the consumption by the loss share
rather than the unit rate, and added the boundary of the first tariff tier instead of the fixed
charge. 42.16 reads like the bill of a low-consumption subscriber and cannot be told apart from the
neighboring rows. The visibility of an error is measured not by its size but by what it
resembles.
Decision C gave the same total as A: 13,427.16. The two columns are equal cell by cell. A comparison of results cannot tell these two versions apart.
Who Follows When a Parameter Changes
What tells them apart is not the result but how the sheet reacts to change. The unit rate is raised from 8.4 to 9.1 by updating a single cell.
// decision.mjs — how many cells follow when a parameter changes, and how many decisions the sheet reads back. import { buildSheet, evaluate, shift, REF, ROWS } from "./sheet.mjs"; const FORMULA = { A: "=C2*$H$2+$H$3", B: "=C2*H2+H3", C: "=C2*8.4+25" }; const fill = (H, f) => { const K = { ...H }; for (let i = 0; i < ROWS; i++) K["E" + (i + 2)] = shift(f, 0, i); K.E62 = "=sum(E2:E61)"; return K; }; const g = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join(""); const H = buildSheet(), RAISE = { ...H, H2: 9.1 }; // unit rate 8.4 -> 9.1 const y = g([10, 12, 13, 12, 11]); console.log(y(["decision", "total", "after raise", "difference", "followers"])); for (const [name, f] of Object.entries(FORMULA)) { const K = fill(H, f), Z = fill(RAISE, f); const before = Array.from({ length: ROWS }, (_, i) => evaluate(K, "E" + (i + 2))); const after = Array.from({ length: ROWS }, (_, i) => evaluate(Z, "E" + (i + 2))); // The threshold is 0.005 fee units: a difference below it cannot be told apart by rounding. const moved = before.filter((v, i) => Math.abs(v - after[i]) >= 0.005).length; console.log(y([name, evaluate(K, "E62").toFixed(2), evaluate(Z, "E62").toFixed(2), (evaluate(Z, "E62") - evaluate(K, "E62")).toFixed(2), moved])); } // Sheet scan: references and embedded literals are extracted from every formula text. const scan = (K) => { const formulas = Object.entries(K).filter(([, v]) => typeof v === "string" && v[0] === "="); const referenced = new Set(), literal = new Map(); for (const [, v] of formulas) { for (const m of v.matchAll(REF)) referenced.add(m[0].replace(/\$/g, "")); for (const s of v.replace(REF, "").match(/\d+(\.\d+)?/g) ?? []) literal.set(s, (literal.get(s) ?? 0) + 1); } const param = Array.from({ length: 8 }, (_, j) => "H" + (j + 2)); return { formulas: formulas.length, used: param.filter((c) => referenced.has(c)), dead: param.filter((c) => !referenced.has(c)), literal }; }; const z = g([10, 10, 28, 6, 18]); console.log("\n" + z(["decision", "formulas", "used", "dead", "embedded literal"])); for (const [name, f] of Object.entries(FORMULA)) { const t = scan(fill(H, f)); console.log(z([name, t.formulas, t.used.join(","), t.dead.length, [...t.literal].map(([s, n]) => `${s}x${n}`).join(" ")])); } const t = scan(fill(H, FORMULA.A)); console.log(`\nThe parameter block holds 8 values, the formulas reference ${t.used.length} of them; ` + `${t.dead.length} (${t.dead.join(", ")}) sit on the sheet but enter no ` + `calculation.`);
decision total after raise difference followers A 13427.16 14421.09 993.93 60 B 3354.98 3361.84 6.86 1 C 13427.16 13427.16 0.00 0 decision formulas used dead embedded literal A 61 H2,H3 6 B 61 H2,H3,H4,H5,H6,H7,H8,H9 0 C 61 8 8.4x60 25x60 The parameter block holds 8 values, the formulas reference 2 of them; 6 (H4, H5, H6, H7, H8, H9) sit on the sheet but enter no calculation.
The second number of this lesson is in the last column. In decision A, changing one cell moved 60 cells; in decision C, 0 cells. The C sheet still shows 13,427.16 after the raise, and that number is now wrong — yet nothing on the sheet marks it, because nothing has changed.
The scan columns state where the difference lies. In A, two decisions can be read back from the
formula text: whoever reads $H$2 goes to cell H2, reads the G2 label next to it, and finds the
unit rate. Two steps. In C the same decision repeats as 8.4 in 60 separate cells; a number is
visible, its source is not. If the unit rate is to change, all 60 formulas have to be read one by
one, and which 8.4 is the unit rate and which is something else can only be guessed. There is no
read-back step.
The last line says a third thing. The sheet holds 8 parameters and the formulas reference only 2 of them; 6 parameters, the tariff tiers included, enter no calculation. They are the residue of decisions once made on the sheet. Whoever looks at it assumes all of them are in use. A number standing on the sheet is not a number in use.
The Two Measures of a Decision
This lesson produced two numbers, and a table cannot be delivered without reporting both. The first is the amount total: 13,427.16 fee units, and 3,354.98 when the reference decision changes. The second is recoverability: in decision A the raise reached 60 cells, in decision C 0 cells; in A the parameter decision is read back in two steps, in C it cannot be read at all.
Without the second number the first is incomplete, because the same 13,427.16 came out of two different sheets, and those two sheets are not the same sheet. The course’s rule starts here: a spreadsheet is a calculation tool, not a record-keeping tool; a decision embedded in a cell is an unwritten decision.
Summary
- A worksheet is a map of cells; a formula is a string, and when copied it is written out shifted, not as it was. A relative reference shifts, an absolute reference does not.
- When the same calculation was written with three reference decisions, the total moved between 13,427.16 and 3,354.98; the difference is three quarters of the correct total.
- A single wrong reference spread by copying to 59 cells; 52 of them give zero and stand out, 7 read like an amount. The visibility of an error is measured not by its size but by what it resembles.
- A comparison of results does not separate the two versions: the sheet written with an embedded literal gives the same total, but when the unit rate changes it follows in 0 cells instead of 60.
- The recoverability of a decision is measured in steps: two steps with an absolute reference, none with an embedded literal. Of the 8 parameters on the sheet, 6 enter no formula, and whoever looks at it cannot see that.
Next Step
This lesson counted which cells a calculation spread to, but it did not ask which cells the
calculation counted. When sum(E2:E61) is written, the range holds 60 cells; how many of them
are counted? Some of the meter readings are blank, some are zero, and some are text that looks like a
number, and the three resemble each other inside a cell. The next lesson applies three counting
functions to the same column and prints three different results; it then measures which decision
the denominator of the same column’s mean depends on.
To keep your progress and take notes, Log in
My notes
Log in to take notes.