Skip to content
academia.sh

Lesson 04 / 09

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.

Published Updated

In this lesson

  • 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
Contents

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

// 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

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

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close