---
title: 'Analyst Roles'
source: 'https://academia.sh/en/courses/introduction-to-data-analytics/analyst-roles'
course: 'Introduction to Data Analytics'
language: en
updated: '2026-10-07T04:31:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates three job descriptions by their acceptance criteria and writes when each one is done'
    - 'Prints side by side, from a single table, how the same question turns into different outputs in three roles'
    - 'Shows the error that appears when the output of one role is read as the output of another'
---

# Analyst Roles

A one-sentence question is given to three job descriptions, and the same table produces three different outputs: the measuring role reports -228 subscribers in 90 days (-5.14%), the decision-binding role gets -3.34% with the fee criterion and, since this falls short of the -5% threshold, takes no action, and the generalizing role gives two forecasts for one year ahead, between 2,191 and 3,286. Fee per subscriber has risen 1.89% from the same rows; of the three correct numbers, two show a drop and one shows a rise. Behind a one-sentence question stand 10 written decisions.

The previous lesson assumed that a single person ran the workflow. In reality the steps of the
workflow are often divided among people who have different jobs, and where the division falls
changes what the result will be. The same question turns into a number in one pair of hands, into a
threshold in another, and into a prediction interval in a third.

This lesson separates three job descriptions and gives all three the same question: **is the
subscriber count falling across the network, and what should be done.** What is measured is how
different the three outputs from the same table are. Role names are treated here as **job
descriptions**, not as titles: what defines a role is not its name but the acceptance criterion that
says when its work is done.

- **AC17.** The records are the records of the first lesson: the same seed, the same 4,800 rows, the
  same numbers. The dataset is **fiction**.
- **AC18.** Role names are job descriptions. The same name may describe a different job somewhere
  else; what is measured is not the name but the **acceptance criterion**: when the output is such
  and such, the job counts as done.
- **AC19.** The question is given to all three in a single sentence, and all three read the same
  table. Nobody is given additional data.
- **AC20.** The fee field is a **fee unit**; the scale is the scale of the model, not any
  currency.
- **AC21.** The comparison period is 90 days and the action threshold is -5%. Both are decisions,
  and both were written **before** any number was calculated.

## Three Job Descriptions

The **measuring job description** (data analyst) is done when it delivers the number that was asked
for, together with its definition and decisions. It does not change the question. Whether the thing
asked is the right question is outside this description; the product of this role is a number and
how that number was produced.

The **decision-binding job description** (business analyst) is done when the number is tied to an
action. For that, the number must have a threshold and something to be done when the threshold is
crossed. This description includes the **authority to change the criterion of the question**: if the
quantity asked about cannot be tied to an action, it is replaced with a quantity that can.

The **generalizing job description** (data scientist) is done when the finding has been carried from
the observed period to a period not yet seen, or to rows not yet seen. Carrying requires an
assumption, and the assumption is delivered as part of the output; a generalization without an
assumption is unfinished work in this description.

The three descriptions do not do the same job, and none of them is a more advanced version of
another. The difference between them is not a difference of skill but of **where the work ends**.

## The Single Table the Three Read

```js
// series.mjs — the subscriber records of the first lesson, same seed and same numbers; a MODEL.
// The reference date is day 0; fields ending in "Day" are the number of days counted back from that date.
export const SEED = 20260601, SUBSCRIBERS = 4800, REGION = ["north", "south", "east", "west", "central"];
const rng = (t) => { let s = t >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const r = rng(SEED);
const pick = (u, d) => { let t = 0; for (const [a, p] of d) { t += p; if (u < t) return a; }
  return d.at(-1)[0]; };

export const RECORDS = Array.from({ length: SUBSCRIBERS }, (_, i) => {
  const region = REGION[Math.floor(r() * 5)];
  const tier = pick(r(), [[1, 0.46], [2, 0.34], [3, 0.20]]);
  const connectionDay = 30 + Math.floor(r() * 2900);
  // The closure probability is high in the lowest tariff tier and closures pile up in recent months.
  const closureP = [0.16, 0.09, 0.05][tier - 1];
  const closureDay = r() < closureP
    ? Math.min(connectionDay - 1, Math.floor(r() * r() * 420)) : null;
  const suspended = closureDay === null && r() < 0.085;
  const contract = closureDay !== null ? "closed" : suspended ? "suspended" : "active";
  // Days since the meter's last reading. Some closed and suspended subscribers still
  // send readings; some active subscribers (faulty meter, empty home) do not.
  const g = contract === "active" ? (r() < 0.13 ? 31 + Math.floor(r() * 300) : Math.floor(r() * 29))
    : contract === "suspended" ? (r() < 0.34 ? Math.floor(r() * 29) : 31 + Math.floor(r() * 250))
    : (r() < 0.11 ? Math.floor(r() * 29) : 31 + Math.floor(r() * 500));
  const consumption = Math.round((2 + tier * 4 + r() * 9) * 10) / 10;
  const fee = Math.round((6 + tier * tier * 7) * (0.85 + r() * 0.3));
  return { i, region, tier, connectionDay, closureDay, contract, lastReadingDay: g,
    consumption, fee, support: r() < 0.22 ? 1 + Math.floor(r() * 3) : 0 };
});

// A subscriber whose contract was open d days ago: connected by that date and not yet closed.
export const openAt = (d) => RECORDS.filter((k) =>
  k.connectionDay >= d && (k.closureDay === null || k.closureDay < d));
export const point = (d) => {
  const l = openAt(d), u = l.reduce((s, k) => s + k.fee, 0);
  return { d, n: l.length, u, per: u / l.length };
};

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(`seed ${SEED}: ${SUBSCRIBERS} subscriber records; the series below is read from a single table\n`);
  const y = gen([11, 13, 15, 20]);
  console.log(y(["days ago", "subscribers", "total fee", "fee per subscriber"]));
  for (const d of [90, 60, 30, 0]) {
    const p = point(d);
    console.log(y([d, p.n, p.u, p.per.toFixed(2)]));
  }
}
```

```
seed 20260601: 4800 subscriber records; the series below is read from a single table

days ago     subscribers      total fee  fee per subscriber
90                  4439         139508               31.43
60                  4426         139657               31.55
30                  4377         138739               31.70
0                   4211         134843               32.02
```

Four rows, three columns. The number of subscribers with an open contract goes from 4,439 to 4,211
in 90 days, and the drop is accelerating: 13 in the first 30 days, 166 in the last 30. Total fee
falls in the same direction between the ends but more slowly, and in the first 30 days it rises on
its own. The third column goes the opposite way: fee per subscriber rises at every step.

All three columns came from the same rows, and all three are correct. Which of them is the answer to
"is the subscriber count falling" is not written in the table.

## Three Outputs from the Same Question

```js
// roles.mjs — the same question, the same table, three job descriptions: three separate outputs side by side.
// Question: "is the subscriber count falling across the network, and what should be done."
import { point } from "./series.mjs";

const P0 = point(0), P30 = point(30), P90 = point(90);
const pct = (next, prev) => ((100 * (next - prev)) / prev).toFixed(2);
const THRESHOLD = -5.0, HORIZON = 365;

console.log("role 1 measuring — does not change the question; delivers the number and its decisions");
console.log(`  subscribers over 90 days ${P90.n} -> ${P0.n}: ${P0.n - P90.n} subscribers, ${pct(P0.n, P90.n)}%`);
console.log("  decisions: open contract definition, comparison period 90 days, no breakdown (3)");

console.log("\nrole 2 decision-binding — turns the criterion into fee and ties it to a threshold");
console.log(`  fee over 90 days ${P90.u} -> ${P0.u}: ${P0.u - P90.u}, ${pct(P0.u, P90.u)}%`);
const change = Number(pct(P0.u, P90.u));
console.log(`  threshold ${THRESHOLD.toFixed(2)}%: ${change <= THRESHOLD ? "exceeded, action needed" : "not exceeded, no action"}`);
console.log(`  fee per subscriber ${P90.per.toFixed(2)} -> ${P0.per.toFixed(2)}: ` +
  `${pct(P0.per, P90.per)}%`);
console.log("  decisions: criterion fee, threshold -5%, period 90 days, no breakdown (4)");

console.log("\nrole 3 generalizing — carries the question into the future; does not deliver a single number");
const extend = (p) => Math.round(P0.n + ((P0.n - p.n) / p.d) * HORIZON);
const a = extend(P30), b = extend(P90);
console.log(`  at the pace of the last 30 days, after ${HORIZON} days: ${a} subscribers`);
console.log(`  at the pace of the last 90 days, after ${HORIZON} days: ${b} subscribers`);
console.log(`  difference between two defensible windows: ${Math.abs(a - b)} subscribers`);
console.log("  decisions: forecast window, linear drift assumption, no seasonal data (3)");

console.log(`\nthree correct numbers from the same table: subscribers ${pct(P0.n, P90.n)}%, ` +
  `fee ${pct(P0.u, P90.u)}%, fee per subscriber ${pct(P0.per, P90.per)}%`);
console.log("all three come from the same rows; two show a drop, one shows a rise");
```

```
role 1 measuring — does not change the question; delivers the number and its decisions
  subscribers over 90 days 4439 -> 4211: -228 subscribers, -5.14%
  decisions: open contract definition, comparison period 90 days, no breakdown (3)

role 2 decision-binding — turns the criterion into fee and ties it to a threshold
  fee over 90 days 139508 -> 134843: -4665, -3.34%
  threshold -5.00%: not exceeded, no action
  fee per subscriber 31.43 -> 32.02: 1.89%
  decisions: criterion fee, threshold -5%, period 90 days, no breakdown (4)

role 3 generalizing — carries the question into the future; does not deliver a single number
  at the pace of the last 30 days, after 365 days: 2191 subscribers
  at the pace of the last 90 days, after 365 days: 3286 subscribers
  difference between two defensible windows: 1095 subscribers
  decisions: forecast window, linear drift assumption, no seasonal data (3)

three correct numbers from the same table: subscribers -5.14%, fee -3.34%, fee per subscriber 1.89%
all three come from the same rows; two show a drop, one shows a rise
```

The first role answers the question as it stands: in 90 days, 228 subscribers were lost, 5.14%.
Three decisions come with it. There is no judgment inside this output: it does not say whether 5.14%
is bad, because saying so requires a written threshold, and the threshold is outside this job
description.

The second role changes the criterion and writes down its reason: a subscriber count cannot be tied
to an action, because what the lost subscribers carried is not visible in the count. With the fee
criterion the drop is 3.34%, which falls short of the -5% threshold; the decision is "no action." The
third line writes the same movement with the opposite sign: fee per subscriber has **risen** 1.89%.
Why three numbers from the same rows go in different directions will be printed in the next
section.

There is a danger here, and it should be named. Changing the criterion and landing below the
threshold differs from disliking a number and looking for another one in only one respect: **if the
threshold was written before the number, it is a decision; if it was written after, it is a
justification.** AC21 wrote the threshold before the calculation; that is why the output of the
second role is defensible. If the same threshold had been pulled to -3% after the result was seen,
the calculation would stay the same and the output would stop being a decision.

The third role does not deliver a single number, and this is not a shortcoming but a requirement of
the job description. Two defensible forecasts come out of the same series: 2,191 subscribers at the
pace of the last 30 days, 3,286 at the pace of the last 90. The difference is 1,095 subscribers, a
quarter of today's count. To choose between the two one would need to know whether the drop is
seasonal or ongoing, and there is no history at hand to separate them. The depth requirement of the
second lesson comes back here as a question: four periods of history do not show an annual wave.
The real output of the third role is therefore not a number but a **data request**.

## The Breakdown None of the Three Wrote

The decision lists of the three roles share a line: **no breakdown.** All three worked on the network
total, because the acceptance criterion of each was satisfied by the total. Yet why the three numbers
diverge from one another is written in a single breakdown.

```js
// breakdown.mjs — the place where all three roles wrote "no breakdown": how the loss spreads across tariff tiers.
import { openAt } from "./series.mjs";

const T0 = openAt(0), T90 = openAt(90);
const count = (l, b) => l.filter((k) => k.tier === b).length;
const feeSum = (l, b) => l.filter((k) => k.tier === b).reduce((s, k) => s + k.fee, 0);
const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0]))).join("");

const y = gen([10, 10, 10, 9, 12, 12, 10]);
console.log(y(["tier", "90 days", "today", "change", "fee 90", "fee today", "fee chg."]));
for (const b of [1, 2, 3]) {
  console.log(y([b, count(T90, b), count(T0, b), count(T0, b) - count(T90, b),
    feeSum(T90, b), feeSum(T0, b), feeSum(T0, b) - feeSum(T90, b)]));
}
const net = T0.length - T90.length;
const t1 = count(T0, 1) - count(T90, 1);
console.log(`\nof the ${net} subscription change, ${t1} is in tier 1: ` +
  `${((100 * t1) / net).toFixed(1)}%`);
console.log(`tier 1 carries ${(feeSum(T90, 1) / count(T90, 1)).toFixed(2)} fee per subscriber, ` +
  `tier 3 ${(feeSum(T90, 3) / count(T90, 3)).toFixed(2)}`);
```

```
tier         90 days     today   change      fee 90   fee today  fee chg.
1               2058      1899     -159       26758       24706     -2052
2               1482      1420      -62       50512       48430     -2082
3                899       892       -7       62238       61707      -531

of the -228 subscription change, -159 is in tier 1: 69.7%
tier 1 carries 13.00 fee per subscriber, tier 3 69.23
```

69.7% of the loss is in the first tariff tier. A subscriber in the first tier carries 13.00 fee
units, one in the third tier 69.23, more than five times as much. These two numbers explain all three
at once: the subscriber count drops sharply because the loss is in the most crowded tier, total fee
drops more slowly because that tier carries the least fee, and fee per subscriber rises because the
rows pulling the average down are leaving.

The breakdown itself is also a decision, and breaking down by tariff tier was not the only option;
breaking down by region, by subscriber age, or by consumption level would have given a different
table. But none of the three roles wrote any breakdown at all. **Whatever an acceptance criterion
makes mandatory to deliver is produced; whatever it does not make mandatory is not produced, even
when it is needed.** The line that would have made the three correct numbers readable together was
inside the job description of none of them.

## When an Output Is Read in the Wrong Role

All three outputs are correct, but when one is read in place of another, a wrong sentence appears.

If the first role's "-228 subscribers" output is read as a decision, the reader fills in the missing
threshold from their own head, and a limit that nobody wrote ends up in the report. If the second
role's "threshold not exceeded" output is read as a measurement, the information that 228
subscribers left is lost: that sentence does not measure, it compares against a threshold. If the
third role's "3,286" is read as a measurement, the linear drift assumption begins to travel as a
fact, whereas the same series gives 2,191 with a different window.

The total count says something too. Behind a one-sentence question stand **10 decisions** written by
the three roles. None of them changes the data, and all of them change the result. If one of the
three outputs sits in a report, the job description that produced it and the decisions that were
written should sit next to it. Otherwise the rule of the course applies: a number whose decisions
are not written counts as unmeasured.

## Summary

- Three job descriptions are separated by their acceptance criteria: the measuring role delivers the
  number and its decisions, the decision-binding role delivers the threshold the number is tied to,
  and the generalizing role delivers a generalization together with its assumption.
- Three correct numbers come from the same table: subscribers -5.14%, total fee -3.34%, fee per
  subscriber 1.89%. Two show a drop and one shows a rise.
- Changing the criterion is a decision if the threshold was written before the calculation and a
  justification if it was written after; the only thing that makes the distinction is the order.
- The generalizing role's two defensible forecasts lie between 2,191 and 3,286, a gap of 1,095
  subscribers; since there is no history at hand to make the choice, the output is not a number but
  a data request.
- Why the three numbers go in different directions is written in a single breakdown: 69.7% of the
  loss is in the lowest tariff tier, and the acceptance criterion of none of the three roles makes a
  breakdown mandatory.
- Behind a one-sentence question stand 10 written decisions; an output cannot be reported without
  writing which job description it came from.

## Next Step

Throughout this topic, four lessons did the same thing: they made the question measurable, changed
the definition, and wrote down how much the number moved. The table itself was always taken as given.
All the decisions taken were on the side of the question: which column is the criterion, which
window, which threshold, which period.

In the last block of this lesson an operation was performed and never questioned: the average of the
fee field was taken and reported as "fee per subscriber." The same calculation could have been
applied to the tariff tier field, and it would have given a number, the average of the tiers. That
number can also be calculated, written down, and put in a table, and it would mean nothing. What
decides the difference between the two is not the calculation but **what kind of thing** the field
is. Contract status is a class name, tariff tier is an ordered level, fee is a measured quantity,
and subscriber ID is a label that looks like a number. The next topic builds this distinction and
shows the number that appears when the type of a field is assigned wrongly.
