← Math in Practice
Concept Change, quantities, and comparison

Percentages and percentage points

Describe what changed without losing the starting rate or its denominator.

After a checkout release, the dashboard says the request error rate doubled. The displayed rate moved from 0.5% to 1%. That is both a 0.5 percentage-point increase and a 100% relative increase; the first describes the absolute gap, while the second compares the gap with the starting rate.

The judgment to keep

Rebuild each rate from its event count and denominator. Then name whether you mean a percentage-point difference or a relative percent change, and check that the windows are comparable before explaining the cause.

TypeScriptGo Error rates · rollout comparisons · relative change
01 / Read the release report

“The error rate doubled” describes a change, but not the whole incident.

A team reviews two illustrative 10-minute windows around a checkout release. Before the release, 18 of 3,600 requests returned a server error. In the comparison window after release, 36 of 3,600 did. Both requests and counts below are invented for teaching; they are not production telemetry.

The counts support a higher observed error fraction in the second window. They do not yet tell us whether the release caused it. Traffic mix, dependency health, regional routing, sampling, and a change in the event definition could all affect the comparison. First reproduce the numbers and state what each percentage is a percentage of.

Case file / Checkout rollout The count of failed requests rose in the comparison window.
Before
18 failed requests ÷ 3,600 total = 0.5%.
After
36 failed requests ÷ 3,600 total = 1.0%.
Window
Two illustrative, equal-length 10-minute periods.
Question
How large is the change, and what should the team check before attributing it?
Leave with: the event being counted, each numerator and denominator, the comparison window, and the claim the numbers actually support.
02 / Rebuild the rates

A percentage is a ratio whose denominator defines the population.

A percentage expresses a fraction per hundred. For an observed request error rate, divide the number of requests that meet the stated failure condition by all requests in the chosen population, then multiply by 100:

error rate = failed requests ÷ total requests × 100

Before the release: 18 ÷ 3,600 = 0.005, then 0.005 × 100 = 0.5%. After: 36 ÷ 3,600 = 0.01, then 0.01 × 100 = 1.0%. The fraction is dimensionless: requests divided by requests. The multiplication by 100 changes how it is displayed, not the underlying fraction.

Illustrative checkout requests · matching 10-minute windows
WindowFailed requestsTotal requestsCalculationObserved rate
Before183,60018 ÷ 3,600 × 1000.5%
After363,60036 ÷ 3,600 × 1001.0%
Checkpoint: say “what divided by which whole?” before comparing two rates.
03 / Name the change

Percentage points measure the gap; relative change measures it against a baseline.

Subtract one percentage rate from another to find a percentage-point change: 1.0% − 0.5% = 0.5 percentage points. This is the absolute difference between the rates. The “percent” unit stays attached to the rates, so their difference is measured in percentage points.

For relative percent change, divide that difference by the starting value: (after − before) ÷ before × 100. Here that is (1.0% − 0.5%) ÷ 0.5% × 100 = 100%. The starting rate is the denominator. Saying “up 100%” without the baseline can sound larger or smaller than the absolute change actually is; saying “up half a point” alone may also hide that the measured rate doubled.

Same observations · two distinct comparisons
MeasureCalculationResultQuestion answered
Point change1.0% − 0.5%+0.5 percentage pointsHow many percentage points separate the rates?
Relative change(1.0% − 0.5%) ÷ 0.5% × 100+100%How large is the gap compared with the starting rate?
04 / Check the comparison

Arithmetic can be right while the comparison is still unfair.

Equal-length windows are a start, not a proof of comparability. Confirm that both rates count the same kind of request and the same failure outcome. Check route, region, client type, traffic source, and whether retries or health checks enter the denominator. A shifted mix can change the total rate even when each group’s rate stays constant.

Then inspect the timeline. Did the increase begin with the rollout? Were there changes to a dependency, traffic, or monitoring at the same time? Did counts come from sampled or delayed telemetry? Recalculate by a useful segment and compare raw counts. These checks separate “the dashboard number changed” from “this release caused more failures.”

Ask next: which raw counts, segments, event rules, and time boundaries would confirm that these two rates describe the same process?
05 / Change the counts

Try a smaller baseline and watch the relative change move.

Keep the event definition fixed: the number of requests that returned the selected server error, among all requests in each window. The calculator recomputes each rate independently. It shows the point difference and relative change separately; it does not assess whether the windows are statistically or operationally comparable.

Compare two observed error rates

Enter whole request counts for two windows using the same failure definition.

Before rate0.500%18 / 3600
After rate1.000%36 / 3600
Percentage-point change+0.500 ppafter rate − before rate
Relative percent change+100.0%(after − before) ÷ before

This is descriptive arithmetic, not a significance test or causal diagnosis. A rate from a small or changing population may be noisy.

06 / Practice in code

Make the zero baseline and invalid denominator explicit.

The implementations return rates in percent, the difference in percentage points, and a relative change when the starting rate is nonzero. A zero baseline has no defined relative percent change: dividing by zero is not a meaningful way to summarize it. The count checks also enforce the lesson’s event model, where one request is either failed or not failed.

Compare the same calculation in TypeScript and Go.

Both implementations validate counts and keep point change separate from relative change.

TypeScriptError rate comparison · explicit denominator
rates.ts
export type RateComparison = {
	beforePercent: number;
	afterPercent: number;
	percentagePointChange: number;
	relativePercentChange: number | null;
};

export function compareRates(
	beforeEvents: number,
	beforeTotal: number,
	afterEvents: number,
	afterTotal: number
): RateComparison {
	for (const [name, value] of [
		['before events', beforeEvents],
		['before total', beforeTotal],
		['after events', afterEvents],
		['after total', afterTotal]
	] as const) {
		if (!Number.isSafeInteger(value) || value < 0) {
			throw new RangeError(`${name} must be a non-negative safe integer`);
		}
	}
	if (beforeTotal === 0 || afterTotal === 0) {
		throw new RangeError('each comparison window must contain at least one request');
	}
	if (beforeEvents > beforeTotal || afterEvents > afterTotal) {
		throw new RangeError('failed requests cannot exceed total requests');
	}

	const beforePercent = (beforeEvents / beforeTotal) * 100;
	const afterPercent = (afterEvents / afterTotal) * 100;
	const percentagePointChange = afterPercent - beforePercent;
	const relativePercentChange =
		beforePercent === 0 ? null : (percentagePointChange / beforePercent) * 100;

	return { beforePercent, afterPercent, percentagePointChange, relativePercentChange };
}

const comparison = compareRates(18, 3_600, 36, 3_600);
console.log(comparison);
GoError rate comparison · explicit denominator
rates.go
package main

import (
	"errors"
	"fmt"
)

type RateComparison struct {
	BeforePercent         float64
	AfterPercent          float64
	PercentagePointChange float64
	RelativePercentChange *float64
}

func compareRates(beforeEvents, beforeTotal, afterEvents, afterTotal int64) (RateComparison, error) {
	if beforeEvents < 0 || afterEvents < 0 || beforeTotal < 0 || afterTotal < 0 {
		return RateComparison{}, errors.New("counts must be non-negative")
	}
	if beforeTotal == 0 || afterTotal == 0 {
		return RateComparison{}, errors.New("each comparison window must contain at least one request")
	}
	if beforeEvents > beforeTotal || afterEvents > afterTotal {
		return RateComparison{}, errors.New("failed requests cannot exceed total requests")
	}

	beforePercent := float64(beforeEvents) / float64(beforeTotal) * 100
	afterPercent := float64(afterEvents) / float64(afterTotal) * 100
	percentagePointChange := afterPercent - beforePercent

	var relativePercentChange *float64
	if beforePercent > 0 {
		change := percentagePointChange / beforePercent * 100
		relativePercentChange = &change
	}

	return RateComparison{
		BeforePercent:         beforePercent,
		AfterPercent:          afterPercent,
		PercentagePointChange: percentagePointChange,
		RelativePercentChange: relativePercentChange,
	}, nil
}

func main() {
	comparison, err := compareRates(18, 3600, 36, 3600)
	if err != nil {
		panic(err)
	}
	if comparison.RelativePercentChange == nil {
		fmt.Printf("before %.2f%%, after %.2f%%, change %.2f percentage points, relative change undefined\n",
			comparison.BeforePercent, comparison.AfterPercent, comparison.PercentagePointChange)
		return
	}
	fmt.Printf("before %.2f%%, after %.2f%%, change %.2f percentage points, relative change %.1f%%\n",
		comparison.BeforePercent, comparison.AfterPercent, comparison.PercentagePointChange,
		*comparison.RelativePercentChange)
}
07 / Carry the judgment forward

A relative change needs a nonzero, useful baseline.

Suppose a metric moves from 0 failed requests out of 3,600 to 9 out of 3,600. The rates are 0% and 0.25%. The point change is well defined: +0.25 percentage points. Relative percent change is undefined because the baseline rate is zero. Do not manufacture a finite answer by silently adding an arbitrary pseudocount; if you need such a statistical model, state and justify it separately.

Now change the comparison: the earlier window contains 3,600 requests, while the later one contains 36,000. The rate still normalizes the volumes, but the larger later sample may make the estimated fraction more stable under similar conditions. It does not automatically make the populations comparable or explain what changed. Preserve both numerators and denominators.

Question to keep: What event is counted, among which requests, in which windows—and am I reporting an absolute point difference or a relative change from a meaningful baseline?