Lesson 06 / 09
Measurement Scales
Ten fields are placed on four scales and all 40 cells formed by multiplying them with four summaries are computed: 28 turn out to be legitimate, and 12 are numbers that were computed but that the scale does not support. Two defensible codings given to the ordinal satisfaction field — 1, 2, 3, 4, 5 and 0, 4, 6, 7, 8 — reverse the ranking of the regions: the first region falls to fifth without a single grade changing. On an interval scale the ratio of the same two readings falls from 4.67 to 1.08 when the zero point changes, while their difference stays fixed at 22.0 units.
Published Updated
In this lesson
- Places fields on four measurement scales: nominal, ordinal, interval, and ratio
- Counts by running it which summary is legitimate on each scale
- Shows that the average of an ordinal field reverses the ranking depending on the coding decision
- Calculates on two scales that a ratio statement on an interval scale depends on the zero point
Contents
The previous lesson divided the fields into quantitative and qualitative, and got stuck in two
places. The values of the satisfaction field are ordered, but nobody has measured that the
distance between 4 and 3 equals the distance between 2 and 1. The temperature_c field is
quantitative, but whether one reading can be counted as twice another was left uncertain. Both
point to the same place: the distinction between quantitative and qualitative is too coarse.
This lesson places the fields on four measurement scales. A scale says which operation is meaningful on a field’s values; and the set of meaningful operations determines which summary is legitimate. The finding of the lesson is this: that a calculation can be done does not mean it is legitimate.
- SD6. The set is the 1,400-row subscriber record from the previous lesson; two derived
fields are added:
tariff_tier(the tier into which consumption falls according to the 10, 25, and 40 m³ thresholds) andreading_day(the number of the day in the observation window on which the reading was taken). The set is fictional. - SD7. The nominal scale permits only equality. The ordinal scale adds ordering but does not report the distance between two neighboring steps.
- SD8. The interval scale makes differences meaningful; its zero point is chosen by convention and does not report an absence. The ratio scale rests on a true zero, so ratio statements are meaningful too.
- SD9. Four summaries demand four separate operations: the mode demands equality, the median ordering, the mean equal intervals, and a ratio statement a true zero. If the operation it demands is not defined on the scale, a summary is computable but not legitimate.
How Many of the Forty Cells Are Legitimate
// scales.mjs — the same set, this time every field is placed on a scale and four // summaries are computed for every field. A MODEL, the dataset is fictional. export const SEED = 20260218, RECORD_COUNT = 1400, TARIFF = [10, 25, 40]; const mix = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507); x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; }; const rng = (t) => { let s = mix(t); return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; }; const pick = (u, w) => { let t = 0; for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; }; export const REGION = [ { name: "north", code: 4, share: 0.28, base: 21, sat: [0.05, 0.10, 0.25, 0.40, 0.20] }, { name: "south", code: 3, share: 0.22, base: 17, sat: [0.02, 0.08, 0.30, 0.45, 0.15] }, { name: "east", code: 2, share: 0.18, base: 26, sat: [0.20, 0.08, 0.05, 0.12, 0.55] }, { name: "west", code: 1, share: 0.14, base: 14, sat: [0.10, 0.15, 0.25, 0.30, 0.20] }, { name: "central", code: 5, share: 0.18, base: 23, sat: [0.06, 0.12, 0.28, 0.34, 0.20] }, ]; export const REGION_CODE = Object.fromEntries(REGION.map((b) => [b.name, b.code])); export const RECORDS = Array.from({ length: RECORD_COUNT }, (_, i) => { const r = rng(SEED + i); const b = REGION[pick(r(), REGION.map((x) => x.share))]; const t = Math.max(1.2, b.base * Math.exp((r() + r() + r() - 1.5) * 0.62)); const consumption = Math.round(t * 100) / 100; const meters = pick(r(), [0.62, 0.24, 0.10, 0.04]) + 1; const visits = pick(r(), [0.38, 0.32, 0.16, 0.08, 0.04, 0.02]); const satisfaction = pick(r(), b.sat) + 1; const temperature = Math.round((6 + r() * 22) * 10) / 10; const elapsed = Math.floor(Math.pow(r(), 1.9) * 210); return { subscriber_id: 10001 + i, region: b.name, consumption_m3: consumption, meter_count: meters, visit_count: visits, satisfaction, temperature_c: temperature, days_elapsed: elapsed, reading_day: 300 - elapsed, tariff_tier: consumption < TARIFF[0] ? 1 : consumption < TARIFF[1] ? 2 : consumption < TARIFF[2] ? 3 : 4 }; }); // The scale says which operation is meaningful on the field's values. export const SCALES = { nominal: { equal: 1, order: 0, diff: 0, ratio: 0 }, ordinal: { equal: 1, order: 1, diff: 0, ratio: 0 }, interval: { equal: 1, order: 1, diff: 1, ratio: 0 }, ratio: { equal: 1, order: 1, diff: 1, ratio: 1 }, }; export const FIELDS = [ { name: "subscriber_id", scale: "nominal", f: (k) => k.subscriber_id }, { name: "region (code)", scale: "nominal", f: (k) => REGION_CODE[k.region] }, { name: "satisfaction", scale: "ordinal", f: (k) => k.satisfaction }, { name: "tariff_tier", scale: "ordinal", f: (k) => k.tariff_tier }, { name: "temperature_c", scale: "interval", f: (k) => k.temperature_c }, { name: "reading_day", scale: "interval", f: (k) => k.reading_day }, { name: "consumption_m3", scale: "ratio", f: (k) => k.consumption_m3 }, { name: "meter_count", scale: "ratio", f: (k) => k.meter_count }, { name: "visit_count", scale: "ratio", f: (k) => k.visit_count }, { name: "days_elapsed", scale: "ratio", f: (k) => k.days_elapsed }, ]; export const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length; const mode = (a) => { const s = new Map(); for (const v of a) s.set(v, (s.get(v) ?? 0) + 1); return [...s].sort((x, y) => y[1] - x[1] || x[0] - y[0])[0][0]; }; const median = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)]; const regionRatio = (f) => { const o = REGION.map((b) => mean(RECORDS.filter((k) => k.region === b.name).map(f))); return Math.max(...o) / Math.min(...o); }; // Each summary demands one operation: the mode demands equality, the median ordering, // the arithmetic mean equal intervals, a ratio statement a true zero point. export const SUMMARIES = [ { name: "mode", demands: "equal", h: (f) => mode(RECORDS.map(f)).toFixed(1) }, { name: "median", demands: "order", h: (f) => median(RECORDS.map(f)).toFixed(1) }, { name: "mean", demands: "diff", h: (f) => mean(RECORDS.map(f)).toFixed(2) }, { name: "region ratio", demands: "ratio", h: (f) => regionRatio(f).toFixed(2) }, ]; const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join(""); if (import.meta.url === `file://${process.argv[1]}`) { console.log(`model (seed ${SEED}): ${RECORD_COUNT} records, ${FIELDS.length} fields, ` + `${SUMMARIES.length} summaries — all ${FIELDS.length * SUMMARIES.length} cells can be computed`); const y = gen([17, 14, 12, 11, 11, 14, 12]); console.log("\n" + y(["field", "scale", ...SUMMARIES.map((o) => o.name), "legitimate"])); let legit = 0; for (const a of FIELDS) { const allowed = SCALES[a.scale]; const cells = SUMMARIES.map((o) => o.h(a.f) + (allowed[o.demands] ? " " : "*")); const m = SUMMARIES.filter((o) => allowed[o.demands]).length; legit += m; console.log(y([a.name, a.scale, ...cells, `${m}/${SUMMARIES.length}`])); } console.log(`\n* marked cell was computed but the scale does not support that operation`); console.log(`${legit} of ${FIELDS.length * SUMMARIES.length} cells are legitimate, ` + `${FIELDS.length * SUMMARIES.length - legit} are not`); }
model (seed 20260218): 1400 records, 10 fields, 4 summaries — all 40 cells can be computed field scale mode median mean region ratio legitimate subscriber_id nominal 10001.0 10701.0* 10700.50* 1.00* 1/4 region (code) nominal 4.0 3.0* 3.19* 5.00* 1/4 satisfaction ordinal 4.0 4.0 3.56* 1.08* 2/4 tariff_tier ordinal 2.0 2.0 2.26* 1.38* 2/4 temperature_c interval 7.7 16.8 16.98 1.03* 3/4 reading_day interval 300.0 249.0 230.61 1.03* 3/4 consumption_m3 ratio 14.5 20.0 21.31 1.86 4/4 meter_count ratio 1.0 1.0 1.55 1.09 4/4 visit_count ratio 0.0 1.0 1.20 1.11 4/4 days_elapsed ratio 0.0 51.0 69.39 1.10 4/4 * marked cell was computed but the scale does not support that operation 28 of 40 cells are legitimate, 12 are not
All forty of the forty cells gave a number and none produced an error. The legitimate ones are
28; 12 cells used an operation the scale does not support. These 12 numbers were not computed
wrongly — the region ratio of the region (code) field really is 5.00. That ratio has no
counterpart: once region codes are ordered, the fifth region does not have something “five
times” more than the first. As the scale rises the number of legitimate summaries grows, from 1
on the nominal scale to 4 on the ratio scale; the scales contain one another. This is why
placing a field on a higher scale than it has is a silent error: the calculation is done, a
number comes out, no warning arrives.
Read in the opposite direction, a second warning emerges. Being legitimate is not being useful.
The mode of the consumption_m3 field is 14.5 and is legitimate on the ratio scale; but because
the field takes 1,099 distinct values, 14.5 is a reading that appears only five times, and there
is a second value that ties with it at the same five. The mode does not describe the set; the
scale sets a lower bound, not an upper bound.
Whom the Average of an Ordinal Field Belongs To
The most dangerous of the starred cells is the 3.56 average of the satisfaction field, because
it does not look wrong. The block below measures what that number depends on.
// coding.mjs — the average of an ordinal field under two defensible codings side by side; // also the dependence of a ratio statement on the zero point on an interval scale. import { RECORDS, REGION, mean } from "./scales.mjs"; // A: the steps are equally spaced. B: the distance at the ends is larger — // "very bad" is farther from "bad" than "good" is from "very good". const CODE_A = [1, 2, 3, 4, 5], CODE_B = [0, 4, 6, 7, 8]; const GRADE = ["very bad", "bad", "fair", "good", "very good"]; const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j]) : String(v).padEnd(B[0]))).join(""); const median = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)]; console.log("satisfaction steps and the two codings"); const y0 = gen([12, 11, 8, 8]); console.log(y0(["step", "grade", "code A", "code B"])); GRADE.forEach((n, j) => console.log(y0([j + 1, n, CODE_A[j], CODE_B[j]]))); const rows = REGION.map((b) => { const l = RECORDS.filter((k) => k.region === b.name).map((k) => k.satisfaction); return { name: b.name, n: l.length, distribution: [1, 2, 3, 4, 5].map((v) => l.filter((x) => x === v).length), a: mean(l.map((v) => CODE_A[v - 1])), b: mean(l.map((v) => CODE_B[v - 1])), med: median(l) }; }); const rank = (key) => { const s = [...rows].sort((x, y) => y[key] - x[key]); return Object.fromEntries(s.map((x, j) => [x.name, j + 1])); }; const rA = rank("a"), rB = rank("b"); console.log("\nsatisfaction per region: the distribution is the same, the two codings give two rankings"); const y1 = gen([9, 9, 22, 9, 8, 9, 8, 9]); console.log(y1(["region", "records", "distribution 1..5", "code A", "rank A", "code B", "rank B", "median"])); for (const s of rows) { console.log(y1([s.name, s.n, s.distribution.join(" "), s.a.toFixed(2), rA[s.name], s.b.toFixed(2), rB[s.name], s.med])); } const moved = rows.filter((s) => rA[s.name] !== rB[s.name]).length; console.log(`${moved} regions changed rank; largest shift ` + `${rows.map((s) => Math.abs(rA[s.name] - rB[s.name])).sort((x, y) => y - x)[0]} places`); const topA = rows.find((s) => rA[s.name] === 1), topB = rows.find((s) => rB[s.name] === 1); console.log(`code A ranks ${topA.name} first, code B ranks ${topB.name} first; ` + `${topA.name} is at rank ${rB[topA.name]} under code B`); console.log(`both calculations are correct, both codings are defensible, not a single grade changed`); // Interval scale: the zero point is set by convention, the ratio depends on it. const warm = RECORDS.map((k) => k.temperature_c); const SHIFT = [0, -273.15, 32]; // the value of the same measurement at three different zero points const LABEL = ["local scale", "absolute-zero scale", "another convention"]; const a = Math.max(...warm), b = Math.min(...warm); console.log(`\nhottest reading ${a.toFixed(1)}, coldest reading ${b.toFixed(1)} (local scale)`); const y2 = gen([24, 11, 11, 9, 9]); console.log(y2(["scale", "hottest", "coldest", "diff", "ratio"])); SHIFT.forEach((d, j) => { const x = j === 2 ? a * 1.8 + 32 : a - d, y = j === 2 ? b * 1.8 + 32 : b - d; console.log(y2([LABEL[j], x.toFixed(1), y.toFixed(1), (x - y).toFixed(1), (x / y).toFixed(2)])); }); console.log(`the first two scales share a unit but have different zero points: the difference ` + `stays fixed at ${(a - b).toFixed(1)} units, the ratio falls from 4.67 to 1.08`);
satisfaction steps and the two codings step grade code A code B 1 very bad 1 0 2 bad 2 4 3 fair 3 6 4 good 4 7 5 very good 5 8 satisfaction per region: the distribution is the same, the two codings give two rankings region records distribution 1..5 code A rank A code B rank B median north 408 17 37 107 170 77 3.62 2 6.36 2 4 south 282 8 23 89 123 39 3.57 3 6.38 1 4 east 240 44 25 17 24 130 3.71 1 5.88 5 5 west 209 16 34 47 67 45 3.44 4 5.97 4 4 central 261 18 45 55 94 49 3.43 5 5.98 3 4 3 regions changed rank; largest shift 4 places code A ranks east first, code B ranks south first; east is at rank 5 under code B both calculations are correct, both codings are defensible, not a single grade changed hottest reading 28.0, coldest reading 6.0 (local scale) scale hottest coldest diff ratio local scale 28.0 6.0 22.0 4.67 absolute-zero scale 301.1 279.1 22.0 1.08 another convention 82.4 42.8 39.6 1.93 the first two scales share a unit but have different zero points: the difference stays fixed at 22.0 units, the ratio falls from 4.67 to 1.08
Both codings are defensible. The first counts the steps as equally spaced. The second takes the gap between “very bad” and “bad” as farther than the other neighboring steps: the lowest step reports a complaint, and its difference from the one above is larger than the difference between “good” and “very good”. Which one is correct the data does not say, because nothing in the data measures the distance between steps.
The result is that two different rankings come out of the same 1,400 grades. Under code A the
first region is east, under code B it is fifth; the rank of three regions changed, and the
largest shift is four places. The cause is written in the distribution column: of the 240
records of the east region, 130 are on the top step and 44 on the bottom. This polarized
distribution rises under the coding that counts the steps as equal, and sinks to the bottom
under the one that penalizes the lower end heavily. The 408 records of the north region are
gathered in the middle, and its rank does not change under either coding.
Two correct numbers contradicting each other came out of the same data. The sentences “the
region with the highest satisfaction is east” and “the region with the lowest satisfaction is
east” were both produced from the same grades, without any arithmetic error. The only
difference between them is a coding decision, and that decision appears in no report.
The median column points to a way out, but not a complete solution. The median gives the same step under any increasing coding, because it relies on nothing but ordering. Its price is resolution: four of the five regions have a median of 4. The decision rule follows from this: if a ranking is needed, the distribution itself is reported; if an average is to be given, how it was produced, which coding, is written next to it.
The last table gives the counterpart on an interval scale. The ratio of the hottest and the
coldest reading is 4.67 on the local scale; on a scale whose zero point is 273.15 units lower,
the ratio of the same two readings falls to 1.08, while their difference is 22.0 units on both
scales. For a ratio to belong to the data, zero has to report an absence. In the
consumption_m3 field zero consumption really is an absence of consumption; the statement that
one region consumed 1.86 times as much as another therefore comes from the data, not from the
scale.
Summary
- The four scales contain one another: nominal equality, ordinal ordering, interval difference, ratio division.
- All forty of the forty cells were computed and 28 turned out legitimate; 12 cells are numbers resting on an operation the scale does not support, and they produce no error.
- Being legitimate is not being useful: the mode of the
consumption_m3field is legitimate on the ratio scale but is a reading that appears five times among 1,099 distinct values and ties with another value. - The average of an ordinal field belongs to the coding: two defensible codings reverse the ranking of the regions, and the first region falls to fifth.
- On an interval scale the difference belongs to the data and the ratio to the zero point: the ratio of the same two readings is 4.67, and when zero is shifted it becomes 1.08, while their difference stays at 22.0 units.
Next Step
These two lessons asked about the type and scale of the fields, but both took the set as given. What was not asked is where the rows came from. The average consumption comes out one way in records compiled from field visits, another in a summary derived from billing records, another in readings the meters send on their own. All three were collected correctly, because each source carries a decision about who gets into the record. The next lesson separates these three sources and counts how much the same quantity shifts from source to source.
To keep your progress and take notes, Log in
My notes
Log in to take notes.