Sizing and TCO

ch13 · Monte Carlo

Builds on ch12.

The question

The sizing model has produced a host count. How sure are we?

ch12 took a stated workload, multiplied along three chains, took the largest of the three answers, and produced a number. Every step was arithmetic you could check by hand. The number is correct. Whether it is right is a different question. Every input to that chain was itself uncertain, and the chain has no way to say so.

This chapter builds the machinery for asking the second question. It assumes you can read code and do arithmetic. It assumes nothing about statistics.

The material

A single number is a bet you did not know you placed

Start with the model as ch12 left it.

OutputPoint estimate90% intervalUnit
hosts the model recommends5420 to 230host
hosts in the fleet54fixedhost
five-year total cost of ownership$2,002,083$1,508,230 to $2,923,724USD
cost per million requests$1.82$0.55 to $5.98USD/megarequest
cost per stored TB per month$839.64$305.33 to $2,108USD / TB / month
capex$421,214$272,130 to $658,773USD
annual opex$316,174$227,660 to $484,257USD / year
annual energy206,269159,636 to 269,269kWh / year
utilisation at the busy hour0.6440.156 to 2.48
utilisation with one host down0.6560.159 to 2.53
working set against memory0.7460.194 to 2.67
disk fill at horizon0.6700.213 to 2.12
fraction of the fleet doing nothing useful0.3210.213 to 0.457
utilisation, counting coordination0.9480.233 to 3.81
utilisation0.6440.156 to 2.48
residence time0.03670.0128 to 0.857second
time spent queueing0.02360.0021 to 0.839second
requests in the system1,562160 to 107,336request
requests in flight, if none waited556135 to 2,147request
how much the queueing view understated it1.471.27 to 1.84
fraction of the peak already built0.3280.186 to 0.584

Source — web_service-reference · every input on a slider

The first column is what the chain produced: one value per output, from one value per input. The second column is the same model, with each input allowed to be as uncertain as the person who wrote it down is.

Look at the host count. The point estimate is a real number, correctly computed. It sits inside a range that spans an order of magnitude, nearer the low end than the middle. Nothing went wrong. The calculation had no way to mention that its inputs were guesses, so it did not mention it.

Distribution of hosts the model recommendshosts the model recommends — 100,000 samples90% interval 20 to 230 · median 68p5pointp9531993960.9% of samples run on to 1,481

Everything below is how that second column was produced.

Instead of one value, a bag of values

The idea is simple. If you do not know what the growth rate will be, do not give the model one growth rate. Give it a bag of plausible growth rates. Run the model once for every value in the bag. You get a bag of answers out, and the bag is the answer.

That is Monte Carlo. Everything else is bookkeeping: how to fill the bag, and how to read it.

Two words, and you will not need many more. The bag of plausible values for an input is its distribution. One value drawn from the bag is a sample. The bag of answers that comes out the other end is the output’s distribution. Reading one is a later section of this chapter.

Where the bag comes from: pick a percentile at random

Filling the bag takes one line of code, and the same line works for every distribution.

Every distribution can be described by a function that answers one question: what value sits at this percentile? Give it 0.5 and it hands back the middle value. Give it 0.9 and it hands back the value that nine tenths of the distribution is below. Call that function the distribution’s percentile function.

Now: pick a percentile uniformly at random, between zero and one, and ask the function what value sits there. Do it a hundred thousand times. You have sampled the distribution.

def sample(spec: dict, n: int, generator: np.random.Generator) -> np.ndarray:
    """Draw ``n`` values from a declared distribution.

    Two lines, and they are the two lines of the whole chapter: draw percentiles, look up values.
    """
    shape, parameters = one_shape(spec)
    return SHAPES[shape](generator.random(n), **parameters)

generator.random(n) draws the percentiles. The percentile function turns them into values. That is the entire sampler, and it is why adding a distribution to this book is three lines rather than a new dependency.

The technique is called inverse transform sampling. Any distribution whose percentile function you can write down, you can sample. Problem 13.1 asks you to write one.

Here is the simplest:

def uniform_ppf(u: np.ndarray, minimum: float, maximum: float) -> np.ndarray:
    """Every value between two bounds, equally likely.

    Honest when the bounds are genuinely all you know — a contract that caps a price, a retention
    window somebody will choose from a range. Dishonest as a default, because it says the bounds
    are as likely as the middle, and almost nothing real is like that.
    """
    return minimum + u * (maximum - minimum)

Percentile zero gives the minimum, percentile one gives the maximum, and everything in between is a straight line. You could have guessed that one. The next one needs some thought:

def triangular_ppf(u: np.ndarray, minimum: float, likely: float, maximum: float) -> np.ndarray:
    """An expert's guess: the least it could be, the most, and the one they would bet on.

    The shape most sizing inputs arrive in, because it is the shape of the answer to "what is it,
    roughly?". Its flaw is worth stating every time it is used: it asserts that nothing outside
    the bounds can happen, and the bounds came from somebody's memory.

    The two branches meet at the mode. Below it the area grows as the square of the distance from
    the minimum, which inverts to a square root — the whole derivation, and problem 13.1 asks for
    it again for a distribution that is not here.
    """
    if not minimum <= likely <= maximum:
        raise ValueError(
            f"triangular needs min <= likely <= max, got {minimum}, {likely}, {maximum}"
        )
    if maximum == minimum:
        return np.full_like(u, minimum)
    width = maximum - minimum
    at_mode = (likely - minimum) / width
    below = minimum + np.sqrt(u * width * (likely - minimum))
    above = maximum - np.sqrt((1.0 - u) * width * (maximum - likely))
    return np.where(u < at_mode, below, above)

Two branches, meeting at the mode. Below the mode, the area under the triangle grows as the square of the distance from the minimum, so inverting it gives a square root. That is the whole derivation. Do it on paper once. Every other distribution in this chapter is the same exercise with different algebra.

Which shape for which input

Choosing a distribution is a judgement, not a technical question. It is a claim about the world, and the first claim a reviewer should argue with.

Lognormal, for prices and growth and anything that compounds. Two properties make it the right shape for those. It cannot go negative, and neither can a price. And a product of several lognormals is another lognormal. A sizing chain is a product, so the uncertainty arriving at the end of one has roughly this shape whether or not anybody chose it.

The parameters here are two percentiles rather than the mean and standard deviation of a logarithm. Nobody has an intuition for the standard deviation of a logarithm. Everybody has one for “I would be surprised if it were under eleven or over nineteen”. That is a sentence a person can say about a price, and it is what the model file records.

def lognormal_ppf(u: np.ndarray, p10: float, p90: float) -> np.ndarray:
    """Multiplicative uncertainty: the shape of prices, growth rates and anything compounding.

    Parameterised by two percentiles rather than by the mean and standard deviation of the
    logarithm, because nobody has an intuition for the second and everybody has one for the first.
    "I would be surprised if it were under 11 or over 19" is a sentence a person can say about a
    price, and it is exactly ``p10=11, p90=19``.

    Two properties earn it its place. It cannot go negative, and neither can a price. And a
    product of several of them is another one, which is what a chain of multiplications in a
    sizing model *is* — so the uncertainty that arrives at the end of the chain has this shape
    whether or not anybody chose it.
    """
    if not 0 < p10 < p90:
        raise ValueError(f"lognormal needs 0 < p10 < p90, got p10={p10}, p90={p90}")
    # Solve for the two parameters of the underlying normal from the two stated percentiles.
    log_median = (np.log(p10) + np.log(p90)) / 2.0
    log_spread = (np.log(p90) - np.log(p10)) / (2.0 * Z90)
    return np.exp(log_median + log_spread * normal_ppf(u))

Triangular, for an expert’s guess. The least it could be, the most it could be, and the one they would bet on. Most sizing inputs arrive in this shape, because it is the shape of the answer to “what is it, roughly?”. Name its flaw every time you use it: it asserts that nothing outside the bounds can happen, and the bounds came out of somebody’s memory.

Uniform, when the bounds really are all you know. A price capped by a contract. A retention window somebody will pick from a range. Honest there, and dishonest as a default. It says the extremes are as likely as the middle, and almost nothing real is like that.

Normal, for measurement error. In this book it means one thing: the standard error beside a measured constant. A number was measured, the measurement wobbles, and it is as likely to wobble high as low. It is the wrong default for a price: a normal will happily go negative and a price will not.

Choosing badly is not a rounding error. It is a claim about what can happen, made in a model file that will outlive the meeting it came from. So the toolkit refuses an input that is sampled without naming its shape and saying why. Every input in this book records who claimed it, on what basis, and which of the four shapes it is:

InputProvenanceSource
annual growth factorassumptionch04 — lognormal because growth compounds and cannot be negative. The p10/p90 say: surprised below 12% a year, surprised above 60%. One factor for requests and for records, because users drive both.
cache marginassumptionch08 — how much of the fleet’s memory is kept free of the working set, for its own daily swing and for the process heaps. Declared once and used by the chain and by the ceiling
contentionassumptionch07 — fitted from two measurements, where two exist. Triangular because a fit gives a central value and a range rather than a shape, and its maximum is the claim to distrust: a serial fraction can always be worse than the one you measured
cores per hostvendor claimspec sheet: physical cores. A hyperthread is not a core, and a sheet that counts threads doubles this number without doubling the work a host does (ch07)
crosstalkassumptionch07 — fitted, and the harder of the two to fit. Lognormal because it spans an order of magnitude and cannot be negative, and because a shape should not assert a hard upper bound on a quantity this weakly fitted
disk marginassumptionch11 — room to re-replicate a dead host’s records onto the survivors, plus what a filesystem needs to keep allocating well. Declared once and used by the chain and by the ceiling
disk per hostvendor claimspec sheet: one local drive. Decimal TB, not TiB — appendix D, and it is a 10% difference
electricity priceassumptionall-in delivered rate including transmission. Lognormal: it cannot go negative and its history is multiplicative
fully loaded salaryassumptionsalary, employer costs, tooling and overhead. Lognormal because pay is right-skewed and cannot go negative; the p90 is a senior engineer in an expensive city
horizonassumptionthe refresh cycle this fleet is bought against
host powervendor claimtypical draw under load, per host as configured. Triangular, and one of the few inputs here whose bounds are physical rather than editorial: a host cannot draw less than it idles at, or more than its supply will give it
host pricevendor claimchassis, CPU, memory, drives and boot media, as configured. Lognormal like any price, and wide because a host is a configuration rather than a commodity
hosts in the fleetassumptionthe sizing decision, taken the way it is usually taken: hosts_recommended evaluated at every input’s point estimate. Change this number and watch the ceilings move — that is the exercise of ch12
share of records touched in a busy hourassumptionch08 — the working set as a share of everything held. Triangular; nobody measures this and everybody has an opinion, and the maximum is a service whose users all look at the same week’s data
hours per yearfactby definition, 365.25 x 24. The quarter-day is worth about a fifth of a per cent over five years — less than this model’s other errors, and free to get right
index overheadassumptionindexes, the write-ahead log and journals as a multiplier on stored bytes. Triangular, and the bounds are for a service with a few indexes per table — a search-heavy one is off the top of this range
licence per corevendor claimthe platform software’s per-core licence, as quoted. Lognormal: a price, and a wide one, because it is the line item most often negotiated. It is what makes the core count a cost as well as a capacity (ch17)
licence per hostvendor claimthe price list: per core, with no per-host charge. A quote that licenses per host instead puts its figure here and zero in licence_per_core, and the two totals are then compared like for like
one-off cost of moving to this designassumptionnothing to migrate: the platform the plan already runs on, on the hosts already quoted. A challenger’s scenario overrides this with the team’s own estimate of the move, marked as what it is
network price per hostassumptionswitch ports, optics and cabling, amortised per host. Triangular rather than lognormal, although it is a price: it is a bill of materials divided by a host count somebody chose, so the bounds are the plausible designs rather than a market
one corefactdefinition
one hostfactdefinition
one requestfactdefinition
one yearfactdefinition
os reserveassumptionthe share of memory the kernel, the agents and the page cache floor keep before the service sees any. Triangular: a floor, a usual figure, and a host with too many agents on it
peak request rate, day oneassumptionch04 — the busy hour, not the daily mean. Triangular because this is an engineer’s min/likely/max and pretending to more shape than that would be invention.
peak-to-mean ratioassumptionch04 — the busy hour against the daily mean. Triangular, and it is a property of your traffic that belongs to the estate target: nothing here can measure it
PUEassumptionch16 — facility overhead. A multiplier on IT load, and the single number a colocation contract is most likely to disagree with you about. Triangular: the minimum is a good building, the maximum is a poor one, and below one is impossible
queueing marginassumptionch06 — how far under the knee the fleet is sized to run at the busy hour. Declared once, here, and used by the sizing chain and by the ceiling that checks it, so the two cannot drift apart
ram per hostvendor claimspec sheet: the modules fitted. The sheet says 64 GB and means GiB — appendix D — and the operating system will report less than either, which is os_reserve’s job
replication factorassumptionthree copies of every record, so that a host can die and take its disks with it. A different durability scheme substitutes its own factor here and the rest of the model is unchanged, which is the point of it being a node
seconds per yearfactby definition, 365.25 x 86,400
CPU time per requestassumptionheld as an assumption because no reference machine is declared in rig/machine.yml; make measure-rig on a declared machine replaces this node with a measured one. Triangular because it has not been measured: once it is, the shape becomes a normal around the measurement, which is a change of claim and not only of numbers (ch13)
engineers, full-time equivalentassumptionengineers this fleet occupies, full-time equivalent. Triangular, and the shape cannot express what actually happens: people are not divisible, so the real distribution is lumpy in the way ch08 calls a regime change
records held, day oneassumptionstated workload (ch02) — what the service holds today: its database and the objects users have uploaded, before replication, indexes or compression
support ratevendor claimannual support as a fraction of capital cost. Triangular because it is negotiated inside a band the market sets rather than drawn from one: the spread is what different buyers get, not what varies from year to year
utilisation the model will admit toassumptionwhere this model stops being about queues (ch06)
37 inputs6 fact, 8 vendor claim, 23 assumption

Source — web_service-reference · every input on a slider

The tally at the bottom is the honest summary of any model. This many of the numbers are traceable. This many are somebody’s sales material. This many were decided in a room.

Running the bag through the model

Nothing changes.

The model is a graph of quantities, each computed from the ones before it. Evaluating it at a point walks the graph in order, doing arithmetic on numbers. Sampling it walks the same graph in the same order, doing the same arithmetic on arrays. a * b means the same thing whether a and b are two numbers or two hundred thousand. The evaluator in this book has one expression walker, and hands it a different set of functions for each pass.

So uncertainty propagates for free. An input with a distribution becomes an array. Everything downstream of it becomes an array. Everything else stays a single number and broadcasts.

A web service and its data, on a fleet of Linux hosts — dependency graph, showing only what feeds five-year total cost of ownershipinputderivedmeasuredceilingyou decidethroughput the fleet canactually reachannual energyannual energy costannual growth factorannual licencesannual opexannual staff costannual supportmean request rate over thehorizonaverage storedcores busy at the busyhourlimit on working setagainst memorycache margincapexrequests in the systemcontentionlimit on utilisation,counting coordinationcores in the fleetcores per hostcost per million requestscost per stored TB permonthcrosstalklimit on disk fill athorizondisk margindisk per hosteffective utilisationelectricity pricefacility powerlimit on utilisation withone host downmemory the service canuse, whole fleetfully loaded salaryfraction of the peakalready builthorizonhorizon periodshost capexhost counthost powerhost pricehosts in the fleethosts left when one dieshosts for memoryhosts for requestshosts for storagehosts the model recommendsshare of records touchedin a busy hourhours per yearrequests in flight, ifnone waitedindex overheadinstalled disklicence per corelicence per hostlifecycle opexthroughput if scaling werefreemean request rate athorizonone-off cost of moving tothis designnetwork capexnetwork price per hostone coreone hostone requestone yearhow much the queueing viewunderstated itos reservewhere adding hosts stopshelpingpeak request rate athorizonpeak request rate, day onepeak-to-mean ratioPUElimit on utilisation atthe busy hourqueueing marginmemory the service canuse, per hostram per hostraw disk needed at horizonraw bytes per stored byterecord compression ratioreplication factorrequests over horizonresidence timescaling efficiencylimit on fraction of thefleet doing nothing usefulseconds per yearCPU time per requestservice timethroughput of one hostaloneengineers, full-timeequivalentrecords held at horizonrecords held, day onesupport ratefive-year total cost ofownershiputilisationutilisation with one hostdownutilisation the model willadmit toutilisation, countingcoordinationtime spent queueingworking set at horizon

Every node in that graph has a distribution once the model has been sampled, not just the ones at the end. The interactive version of this figure will show you any of them. That is the fastest way to find out where an interval got wide: you walk the chain until the histograms stop being narrow.

Reading the answer

Distribution of five-year total cost of ownershipfive-year total cost of ownership — 100,000 samples90% interval $1,508,230 to $2,923,724 · median $2,097,968p5pointp95$837,022$2,119,308$3,401,5950.8% of samples run on to $5,966,168

Two words, and then we are done with vocabulary.

A percentile is the value a given fraction of the bag is below. The 5th percentile is the value only one sample in twenty came in under.

An interval is the gap between two of them. This book reports the gap between the 5th and the 95th percentile, calls it the 90% interval, and deliberately does not call it a confidence interval. That phrase means something precise to a statistician and something vaguer to everybody else. What is meant here is the plain reading: the model put nine tenths of its belief in this range.

Why two percentiles rather than the smallest and the largest answer in the bag: the ends are properties of how many answers you collected, not of the model. Collect ten times as many and the largest gets larger, every time, because the unlucky combinations had more chances to turn up. The middle settles, and ch14 measures how quickly.

Note where the red line sits relative to the middle of the distribution. For a chain of multiplications with skewed inputs, the answer you get from the average inputs is not the average answer, and it is not the middle one either. There is a theorem behind that. You do not need it. You need to have seen it happen once.

How often each ceiling is breached

The ceilings are what the machinery was built for, and a spreadsheet has no equivalent of this table.

CeilingAt the planHeadroomAllowedLimitVerdictOver allowedOver limit
working set against memory0.7525%0.751.00ok49%35%
utilisation, counting coordination0.9530%0.701.00into the margin65%48%
disk fill at horizon0.6725%0.751.00ok44%29%
utilisation with one host down0.6630%0.701.00ok47%30%
utilisation at the busy hour0.6430%0.701.00ok46%30%
fraction of the fleet doing nothing useful0.3250%0.501.00ok1%0%

Source — web_service-reference · every input on a slider

At the point estimate, every ceiling but one is fine. That is not surprising. ch12 sized the fleet from the point estimates, so of course it satisfies the ceilings it was sized against. The one it was not sized against is the one that is not fine. The last two columns are the same model asked a different question: across everything this model thinks could happen, how often is this limit breached?

That is a sizing answer. Not “you need this many hosts”, but “at this many hosts, this is how often the thing you were trying to avoid happens anyway”. Somebody can take responsibility for the second. Nobody can take responsibility for the first, because it does not say anything.

Once the question is in that form, it has a price. Here is the same model with a bigger fleet bought:

CeilingAt the planHeadroomAllowedLimitVerdictOver allowedOver limit
working set against memory0.3125%0.751.00ok13%6%
utilisation, counting coordination0.6730%0.701.00ok50%34%
disk fill at horizon0.2825%0.751.00ok8%3%
utilisation with one host down0.2730%0.701.00ok12%5%
utilisation at the busy hour0.2730%0.701.00ok12%5%
fraction of the fleet doing nothing useful0.6050%0.501.00into the margin90%0%

Source — web_service-sized_for_growth · every input on a slider

What the extra capital buys is the difference between two percentages. Whether it is worth it is not a modelling question, and ch21 is about how to put it to the person whose decision it is.

The seed

Every sampled result in this book records the seed its random numbers came from, and the sample count, and a hash of the sampler’s source. Run it again with those three and you get the same numbers to the last digit.

That is not fastidiousness. An unseeded simulation is a measurement nobody can repeat, and a figure nobody can repeat is a figure nobody can check. This book refuses that everywhere else, and has no reason to start allowing it here.

Key takeaways

  • Give the model a bag of plausible values instead of one, and the bag of answers is the answer. Filling the bag is one line: pick a percentile at random and ask the distribution what value sits there.

  • The shape is a claim about the world, and the first thing a reviewer should argue with. Lognormal for what compounds, triangular for an expert’s guess, uniform when the bounds really are all you know, and normal for measurement error alone.

  • Sampling the model is the same walk of the same graph, on arrays instead of numbers. Every node gets a distribution, not only the outputs, so you can walk the chain to where the interval got wide.

  • Read the middle, not the ends. The smallest and largest answers are properties of how many you drew. The interval between two percentiles is a property of the model, and the answer at the average inputs is neither the average answer nor the middle one.

  • The ceilings are what the machinery is for. Not you need this many hosts, but at this many, this is how often the thing you were avoiding happens anyway, which is an answer somebody can be accountable for.

What this cannot tell you

Whether the model has the right shape. Everything above takes the structure as given and asks what the inputs are worth. If a cost line is missing, if a ceiling was never declared, or if two quantities were multiplied that should have been added, sampling will propagate the error beautifully and report a confident interval around the wrong answer. That is structural error. It is invisible to every technique in this chapter, and it is the subject of ch20 · The missing node.

Whether the shapes were chosen honestly. A triangular with generous bounds and a lognormal with tight ones will give different intervals for the same input, and nothing here can tell you which was right. The distribution is an assumption like any other, and this book makes you write it in a file with your name on it for that reason.

How the inputs were drawn together. The sampler above draws each input on its own. The intervals above were not produced that way. This model declares two pairs that move together: a host’s price and the network’s, quoted by the same supply chain; and the busy hour and what a request costs, because a busier service is a slower one per request. The evaluator applies the pairing after the draw. So every figure on this page already carries it, and this chapter has not said so until now. Drawing those pairs independently would make every interval here narrower, which is the direction that gets a plan approved. ch14 names the pairs, and measures what they were worth.

Whether a hundred thousand samples was enough. This chapter assumed it and did not establish it. ch14 has the argument, and the way to work it out for a model of your own.

How likely any of this is. The interval is a statement about the model’s declared inputs. It is not a forecast, it carries no track record, and its 95th percentile is not a promise. It is the best available summary of what you have written down. That is worth a great deal more than a single number, and a great deal less than knowledge.

Problems

Four. The first three are in tests/monte_carlo/ and are graded against definitions the tests compute for themselves. The fourth has no test and no known answer.

13.1 — Add a distribution. Implement the percentile function for a shape this book does not have: a quantity known to within a factor, whose density is proportional to one over the value. Derive it as the chapter derived the triangular’s. The test grades it against the density itself, integrated at test time, so there is nothing to look up.

tests/monte_carlo/stubs.py · log_uniform_ppfyours to edit
def log_uniform_ppf(u: np.ndarray, minimum: float, maximum: float) -> np.ndarray:
    """Problem 13.1 — the percentile function of a shape this book does not have.

    A quantity you know to within a factor and no better: as likely to sit anywhere in its range
    on a multiplicative scale, so that doubling is as plausible as halving wherever you start. Its
    density is proportional to one over the value, between ``minimum`` and ``maximum`` and zero
    outside them::

        density(x) = 1 / (x * ln(maximum / minimum))     for minimum <= x <= maximum

    That is the whole specification. It is the shape for a ballpark: how many distinct label
    values a service will turn out to carry, the size of a table nobody has counted.

    Your job is the percentile function: given percentiles in (0, 1), return the values that sit
    at them. Derive it the way the chapter derived the triangular's: the area under the density
    from the minimum up to a value, set equal to the percentile, then inverted. It comes out as
    one line.

    The test grades it against the density rather than against a formula. It integrates the
    density numerically up to each value you return and checks that the area is the percentile
    you were given. So there is nothing to look up, and a function that is monotonic and bounded
    but the wrong shape fails.
    """
    raise NotImplementedError("problem 13.1")

The same check at a desk: python3 -m pytest tests/monte_carlo/test_problem_1_ppf.py -m problem

13.2 — Sample a model by hand. The test hands you two of the web service model’s inputs, each with the distribution the model file declares for it. Sample both yourself, without the toolkit’s sampler, work the cost they feed through by hand, and reproduce the interval this book publishes for it to within sampling error. The point is to discover how small the machinery is.

tests/monte_carlo/stubs.py · sample_two_inputsyours to edit
def sample_two_inputs(declared: dict[str, dict], seed: int, samples: int) -> dict[str, np.ndarray]:
    """Problem 13.2 — sample a model by hand.

    ``declared`` holds two of the web service model's inputs, ``staff_fte`` and
    ``fully_loaded_salary``, each keyed to the distribution the model declares for it, as the
    file writes it. One is a triangular::

        {"triangular": {"minimum": ..., "likely": ..., "maximum": ...}}

    and one is a lognormal::

        {"lognormal": {"p10": ..., "p90": ...}}

    The test reads both out of ``models/web_service/model.yaml`` and hands them to you, so a
    change to the model changes what you are given instead of going stale in a number typed
    here. Sample what you are handed.

    Return a dictionary with three keys. ``staff_fte`` and ``fully_loaded_salary`` each hold
    ``samples`` draws from their distribution, seeded with ``seed``; ``annual_staff_cost`` holds
    what those draws imply, worked draw by draw through the model's own formula for it, which
    you read out of the same file.

    Do it the way the chapter did: pick the percentiles uniformly at random, then ask each
    distribution what value sits at each one. Use ``numpy`` and ``sizing.normal`` if you like.
    Do **not** use ``sizing.mc`` or ``sizing.evaluate``: the point of this one is to find out how
    little machinery there is between a declared distribution and a bag of numbers, and between
    the bag and an interval the book publishes.

    The test compares your inputs' percentiles against what the declared distributions say, and
    your cost's interval against the one the book publishes for the reference scenario, each to
    within the sampling error the count allows — which means it also checks that you understood
    what "to within sampling error" has to mean here. These two inputs are declared independent
    of everything else in the model, which is what makes the interval reachable by hand; ch14 is
    about the ones that are not.
    """
    raise NotImplementedError("problem 13.2")

The same check at a desk: python3 -m pytest tests/monte_carlo/test_problem_2_by_hand.py -m problem

13.3 — Where the point estimate sits. For each output of the web service model, find the share of the sampled answers that fall below the point estimate. For a cost it is near a half. For the recommended host count it is not, and the chain says why. Name the step that moved it.

tests/monte_carlo/stubs.py · where_the_point_sitsyours to edit
def where_the_point_sits(
    samples: dict[str, np.ndarray], point: dict[str, float]
) -> dict[str, float]:
    """Problem 13.3 — where the point estimate sits among the sampled answers.

    ``samples`` holds every output's draws and ``point`` the same outputs' point estimates: the
    value the model computes from every input's middle. Return, for every output in ``point``,
    the fraction of that output's draws that fall below its point estimate.

    For a quantity that is one input passed straight through, the answer is a half. For a product
    of the model's shapes it is close to a half. For the web service's recommended host count it
    is not, by more than sampling error, and the chain says why: at least two of its steps are
    not multiplications, a maximum over three chains and a rounding up inside each, and a point
    estimate walks through them as if they were.

    Then finish the last line of this docstring with one sentence naming the step that moves the
    point furthest from the middle, and in which direction. The test grades the fractions, not
    the sentence; whoever reads your model will grade the sentence.

    What moved it:
    """
    raise NotImplementedError("problem 13.3")

The same check at a desk: python3 -m pytest tests/monte_carlo/test_problem_3_where_the_point_sits.py -m problem

13.4 — Defend a distribution. No test: the invoices are yours, and so is the shape you would defend.

Take a price you pay, find two years of invoices for it, and decide which of the four shapes in this chapter you would use and why. Then check what the last two years would have looked like under your choice. If the answer embarrasses you, that is the exercise working.

A good answer names the shape, the reason it and not another, and the two ends of the band, as a sentence somebody could disagree with. What would show it wrong is the invoices: more than a couple of the twenty-four outside the band you declared means the band was too narrow, and none anywhere near its ends means it was too wide to be a claim at all.

Where to go next

Metropolis and Ulam’s original paper [Metropolis & Ulam (1949)] is seven pages, is readable without any statistics, and is a useful corrective to the idea that this is a modern technique.

numpy.random’s documentation on generators and seeding is worth twenty minutes, particularly the part about why default_rng exists and what it replaced.

ch14 picks up the two things this chapter used without establishing: the correlations the intervals above already carry, and the sample count.