, ,

Statistics for Infra Engineers, Percentiles Over Averages (Infra to Data Science Series, Part 10)

The percentiles you already trust from SLOs, made the core of statistics for infra data. Why the mean sat at the 68th percentile, why mean plus two standard deviations gave an impossible negative latency, and how a change significant at p 5.57e-05 moved the median by 0.13 ms.

Infra to Data Science Series · Part 10 of 26

A director asked me one question in a review: what is our average response time. I said 40 milliseconds and the room relaxed. That number was true and the relief was misplaced, because at that same moment roughly one request in a hundred was taking a quarter of a second and the very slowest were past two seconds. An average is the number that hides the customers who are actually angry. Statistics for an operator starts right there, with the arithmetic that tells you what a single friendly summary is quietly leaving out, and none of it needs a maths degree, only the same suspicion you already bring to a green dashboard.

Who this is for: An infrastructure engineer or SRE who reads a latency graph fluently but has never written down why the mean misleads, carrying the typed cluster frame from Part 9 and a request latency export from your monitoring. Terms on first use: a percentile is the value below which that share of data falls, so p95 is the point 95 percent of requests beat; skew is how lopsided a distribution is, zero for symmetric; a bootstrap resamples your data many times to put an interval around a statistic; effect size is how big a difference is, separate from whether it is real.
Key takeaways: On 500,000 real request latencies the mean was 39.6 ms while the median was 30.2 ms and p99 was 228.1 ms, so the average sat at about the 68th percentile, not the middle. A three sigma rule broke here: mean minus two standard deviations came out at negative 64.3 ms, an impossible latency, because skew was 11.43. Standard deviation was 51.9 ms, larger than the median, while the robust IQR was 23.4 ms and MAD 10.9 ms. A p99 from 5,000 requests read 191.5 ms but its 95 percent interval ran 152.6 to 296.3 ms. A config change that was significant at p equal to 5.57e-05 moved the median by 0.13 ms, under half a percent.

Averages hide the tail your users feel

Where the project stands: last part you reshaped the cluster frame with column operations, and you also have a latency log from the same systems. This part describes that data honestly before any model touches it, because a model trained on a lie about the data learns the lie. Start with the summary everyone reaches for, the mean, and put it next to the percentiles you already trust from your SLOs. You know p95 and p99 from latency dashboards, so this is your own vocabulary, made explicit.

# tested on Python 3.10.12, numpy 2.2.6, pandas 2.3.3, scipy 1.15.3
import numpy as np
# lat: 500,000 request latencies in milliseconds, from the monitoring export
print('mean', round(lat.mean(), 1))
for p in [50, 90, 95, 99, 99.9]:
    print('p' + str(p), round(np.percentile(lat, p), 1))   # method is linear by default
print('above mean pct', round((lat > lat.mean()).mean() * 100, 1))
mean 39.6
p50 30.2
p90 63.4
p95 80.8
p99 228.1
p99.9 737.7
above mean pct 31.6

Read those seven lines slowly, because they overturn the instinct to report an average. A mean of 39.6 ms is not the middle of anything: only 31.6 percent of requests are slower than it, which places the average near the 68th percentile, dragged up by a tail it cannot see. A median of 30.2 ms is the request a typical user waits for. p99 at 228.1 ms is 7.6 times the median, and p99.9 at 737.7 ms is the slow morning nobody forgets. If you report one number to a stakeholder, report the median and one tail percentile, never the mean alone, because the mean is the average of people who are fine and people who are furious, and it flatters both away. Tail latency is exactly the signal the AI Engineering Series watches when it argues for caching and latency engineering, where p99 not the average decides whether a call budget holds.

Latency percentile ladder, log scale500,000 requests, milliseconds, the mean is not the middle10301003001000p50 30.2mean 39.6p95 80.8p99 228p99.9 738The mean sits at about the 68th percentile, and the tail runs an order of magnitude past it.Report the median and a tail percentile, not the average.
Log scale, because the percentiles span from tens to hundreds of milliseconds. A mean marker sits barely right of the median while p99 and p99.9 stretch away, which is the whole reason an average reassures you wrongly.

Distributions before summaries

Every summary number assumes a shape, and the assumption almost every tutorial hands you is the bell curve, where mean plus or minus two standard deviations covers about 95 percent of values. Operational data rarely obeys it. Latency, request size, time between failures, queue depth, these pile up near a floor and trail off into a long right tail. Compute the three sigma bounds on the latency and watch the assumption fail out loud.

from scipy import stats
mu, sd = lat.mean(), lat.std()
print('mean', round(mu, 1), 'std', round(sd, 1))
print('mean+2std', round(mu + 2*sd, 1))
print('mean-2std', round(mu - 2*sd, 1))         # the tell
print('flagged by mean+2std pct', round((lat > mu + 2*sd).mean() * 100, 2))
print('skew', round(stats.skew(lat), 2))         # 0 would be symmetric
mean 39.6 std 51.9
mean+2std 143.4
mean-2std -64.3
flagged by mean+2std pct 1.52
skew 11.43

There is the failure, printed in one line: mean minus two standard deviations is negative 64.3 ms. No request finishes in negative time, so a rule that produces an impossible lower bound is telling you the shape is wrong, not that some latencies are below zero. Skew of 11.43, where a bell curve is 0, confirms a hard right lean. An upper bound of 143.4 ms looks plausible until you notice it flags only 1.52 percent of requests as outliers, sitting awkwardly between p95 at 80.8 ms and p99 at 228.1 ms, describing neither. That lesson contradicts the first thing most stats courses teach: do not summarise operational data with mean and standard deviation until you have looked at its shape, because the moment the data is skewed those two numbers describe a distribution you do not have. Plot a histogram, or at least read the percentiles, first. Distribution mechanics get built properly in the Data Science Series part on probability and distributions, which this series assumes rather than repeats.

One production gotcha worth carrying from the start: you cannot average percentiles. If three shards each report a p99 of 200 ms, the fleet p99 is not 200 ms, and averaging the three is meaningless, because a percentile is a position in a sorted list, not a quantity you add. To combine tail latency across shards you keep a histogram or a sketch such as t-digest per shard and merge those, then read the percentile off the merged shape. I have watched a dashboard proudly average per node p99 values and report a fleet number that was wrong by a factor that shifted with traffic, the kind of quiet error that survives for months because it always looks reasonable.

flowchart LR
  A[Raw metric column] --> B[Check shape, histogram or percentiles]
  B --> C[Center, median not mean]
  C --> D[Spread, IQR or MAD]
  D --> E[Interval, bootstrap the tail]
  E --> F[Honest summary for the model]
Describe an operational column in one pass, shape first, then a center, a robust spread and an interval, before any of it feeds a model. Each step replaces a default that lies on skewed data.

Spread that survives a long tail

If the standard deviation is misleading on skewed data, you still need a way to say how spread out the bulk of requests is. Two robust measures do the job without letting a handful of two second requests dominate. An interquartile range, IQR, is the width of the middle half, p75 minus p25. A median absolute deviation, MAD, is the median of how far each point sits from the median. Both ignore the extremes by construction, which is the point.

from scipy import stats
q1, q3 = np.percentile(lat, [25, 75])
print('std   ', round(lat.std(), 1))
print('IQR   ', round(q3 - q1, 1))
print('MAD   ', round(stats.median_abs_deviation(lat), 1))
print('median', round(np.median(lat), 1))
std    51.9
IQR    23.4
MAD    10.9
median 30.2

Look at the gap. Standard deviation is 51.9 ms, larger than the median itself, because it squares distances and the tail carries the squares. IQR at 23.4 ms and MAD at 10.9 ms describe where the bulk of requests actually live, tight around 30 ms. When you build an anomaly rule later in this series, this is the difference between a threshold that fires on ordinary traffic and one that fires on genuine outliers. A common robust flag is a value more than three MADs from the median, which here is about 30.2 plus three times 10.9, near 63 ms, a threshold that tracks the real bulk instead of being inflated by the very outliers you want to catch.

Verdict: For skewed operational data reach for the median with IQR or MAD, and treat the mean with standard deviation as a report you only trust once a histogram shows a rough bell. My pick for a default outlier flag is three MADs from the median, which held steady here while mean plus two standard deviations produced an impossible negative bound. What to avoid is the reflex of mean plus or minus two standard deviations on latency, request size or inter arrival time, the three places infra data is most skewed and the rule fails hardest.

Confidence in a single number

You quote a p99 in a review and it lands as a fact. It is really an estimate from a sample, and estimates wobble. A bootstrap makes the wobble visible: resample the data with replacement thousands of times, recompute the statistic each time, and read off the spread. scipy has this built in, and it has one sharp edge that catches everyone on the first try, so here is the wrong call and its real error before the fix.

from scipy import stats
sample = np.random.default_rng(7).choice(lat, 5000, replace=False)
ci = stats.bootstrap(sample, lambda x, axis=-1: np.percentile(x, 99, axis=axis))
ValueError: each sample in `data` must contain two or more observations along `axis`.

bootstrap expects data as a sequence of samples, not a single array, so it read each latency value as its own one element sample and refused. A fix is one character, wrap the array in a tuple, then ask for the interval.

ci = stats.bootstrap((sample,), lambda x, axis=-1: np.percentile(x, 99, axis=axis),
                     n_resamples=2000, confidence_level=0.95, random_state=7)
print('sample p99', round(np.percentile(sample, 99), 1))
print('95 pct CI ', round(ci.confidence_interval.low, 1), 'to',
      round(ci.confidence_interval.high, 1))
sample p99 191.5
95 pct CI  152.6 to 296.3

That point estimate is 191.5 ms, and the interval runs from 152.6 to 296.3 ms, a width of nearly 144 ms. That is the honest reading: a p99 measured off 5,000 requests is not one number, it is a range, and the tail percentiles are the shakiest because so few points define them. So when a p99 jumps from 190 to 240 ms between two short windows, check whether that move even clears the interval before you page anyone, because often it does not. Report a tail percentile with its interval, or widen the window until the interval tightens.

Before and after a change, and the significance trap

You ship a config change and want to prove it helped. A textbook move is a significance test, and on operational data that test will mislead you in a specific, predictable way. Take 400,000 request latencies before a change and 400,000 after, where the after set is genuinely a hair faster, and run a Mann-Whitney U test, which compares distributions without assuming a bell curve.

from scipy import stats
print('before median', round(np.median(before), 2))
print('after median ', round(np.median(after), 2))
res = stats.mannwhitneyu(before, after, alternative='greater')
print('p-value', format(res.pvalue, '.2e'))
gain = np.median(before) - np.median(after)
print('median gain ms', round(gain, 2), 'which is', round(100*gain/np.median(before), 2), 'pct')
before median 29.95
after median  29.81
p-value 5.57e-05
median gain ms 0.13 which is 0.44 pct

A p-value of 5.57e-05 is significant by any bar you were taught, and the change it certifies is 0.13 ms, under half a percent of the median. Nothing is wrong with the test. A trap hides in what significance means: it answers whether a difference is real, not whether it is worth anything, and on 400,000 samples even a difference too small to feel clears the threshold easily, because the test grows more sensitive as the sample grows. Report the effect size first, the 0.13 ms, and let the p-value confirm it is real rather than lead the story. This is the same discipline the Data Science Series applies to model evaluation and leakage, where a number that looks decisive is worthless until you know what it is really measuring.

War story: I once set a latency alert at mean plus two standard deviations because that is the outlier rule everyone quotes, 143 ms on our numbers. It stayed silent through a slow morning where p99 sat above 380 ms for forty minutes and support took 19 tickets before we noticed, because the fat tail had already pushed the standard deviation so high that 143 ms was comfortably inside normal. We moved the alert to fire when p99 over a five minute window crossed 150 ms, and it caught the next event in about 90 seconds. That mean based threshold was not slightly off, it was structurally blind to the exact failure it was built to catch, and I had shipped it with a straight face.

Statistics checklist and project status

Where the project stands now: the latency export and the cluster frame are described honestly, with the median, tail percentiles, a robust spread and an interval around the shaky numbers, which is the footing every model from Part 13 onward stands on. Keep the table below as the reference artifact, one row per question you actually ask about operational data and the statistic that answers it without lying.

Question you are askingStatistic to useNot this
Typical experiencemedian, p50mean, the tail drags it up
Worst common casep95 or p99max, one fluke sets it
Spread of the bulkIQR or MADstandard deviation on skewed data
How sure of a numberbootstrap intervalthe point estimate alone
Did a change helpeffect size, then a testthe p-value alone
Is this point unusualthree MADs from the medianmean plus two standard deviations
MetricValue, msWhat it answers
mean39.6almost nothing useful here
p50, median30.2the request a typical user waits for
p9580.8a slow but common request
p99228.1the tail users complain about
p99.9737.7the worst one in a thousand

Describe before you model checklist

  1. Look at the shape, a histogram or the percentiles, before quoting any single summary.
  2. Lead with the median and one tail percentile, not the mean.
  3. On skewed data use IQR or MAD for spread, not standard deviation.
  4. Put a bootstrap interval around any tail percentile you plan to alert on.
  5. For a before and after, report the effect size first and let the p-value confirm it.
  6. Flag outliers by distance from the median in MADs, not by mean plus two standard deviations.

Compute five numbers on your own latency this week

Do one thing after this part. Pull a week of one real latency series you own, and compute five numbers on it: the median, p95, p99, the IQR, and a bootstrap interval around the p99. Compare the median to whatever average your dashboard reports today, and I expect the gap to surprise you the way 30.2 against 39.6 surprised the room in that review. Those five numbers are the honest description this series builds every later model on. Next part turns to probability and distributions for real systems, so you can name the shapes your data actually takes instead of assuming a bell curve that is not there.

Infra to Data Science Series · Part 10 of 26
« Previous: Part 9  |  Guide  |  Next: Part 11 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading