---
title: 'Measurement Scales'
source: 'https://academia.sh/en/courses/introduction-to-data-analytics/measurement-scales'
course: 'Introduction to Data Analytics'
language: en
updated: '2026-10-07T04:31:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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) and `reading_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

```js
// 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.

```js
// 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_m3` field 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.
