A latency report may need to answer a direct operational question: what fraction of these observations finished in at most twenty milliseconds? An empirical cumulative distribution function answers that question without selecting an interpolated percentile. For a sample of n observations, its value at a threshold is the count of observations less than or equal to that threshold divided by n.

Prerequisites: Python 3.12 and a finite in-memory numeric sample. No scientific packages are needed. The demonstration values are synthetic durations in milliseconds, and every observation receives equal weight.

The inclusive direction follows the NIST definition of a cumulative distribution function. The implementation sorts once and uses Python's bisect_right to locate the position after all values equal to the threshold.

Complete executable example

Save as ecdf.py. The closure retains an immutable sorted snapshot so later changes to the caller's list do not change an already prepared report.

from bisect import bisect_right
from math import isfinite

def make_ecdf(values):
    values = tuple(values)
    if not values or any(type(x) not in (int, float) or not isfinite(x) for x in values):
        raise ValueError("nonempty finite numeric sample required")
    ordered = tuple(sorted(values))
    def fraction_at_most(threshold):
        if type(threshold) not in (int, float) or not isfinite(threshold):
            raise ValueError("finite numeric threshold required")
        return bisect_right(ordered, threshold) / len(ordered)
    return fraction_at_most

values = [10, 20, 20, 50]
f = make_ecdf(values)
assert [f(x) for x in (9, 10, 19, 20, 50, 100)] == [0, .25, .25, .75, 1, 1]
assert f(20) == sum(x <= 20 for x in values) / len(values)
values.append(0)
assert f(20) == .75  # snapshot is independent of caller mutation
for bad in ([], [float('nan')], [True]):
    try:
        make_ecdf(bad)
    except ValueError:
        pass
    else:
        raise AssertionError(bad)
try:
    f(float('inf'))
except ValueError:
    pass
else:
    raise AssertionError('infinite threshold accepted')
print(f'at most 20 ms: {f(20):.0%}')
print('ties, boundaries, snapshot, and invalid-input checks passed')

Run and interpret

python3 ecdf.py

Expected output:

at most 20 ms: 75%
ties, boundaries, snapshot, and invalid-input checks passed

Three observations are at or below twenty: ten and both occurrences of twenty. Both ties belong in the numerator. Using the insertion position before equal elements would incorrectly count only the ten, yielding twenty-five percent. The independent direct-count assertion protects against this particular boundary error.

The function returns zero below the smallest observation and one at or above the largest observation. Between observed values it remains constant. This stepwise behavior is expected; the sample does not justify inventing intermediate observations.

Edge cases and troubleshooting

An empty sample has no denominator, so construction fails. NaN would undermine ordinary ordering and is rejected. Booleans are rejected even though Python treats them as integer-like values. Infinite thresholds are also rejected by this application's finite-input policy.

The routine is intended for ordinary integer and floating-point measurements. Extremely large arbitrary-precision integers are outside its demonstrated range. Normalize measurement units before construction; combining milliseconds and seconds produces a numerically valid but meaningless report. If negative durations are invalid for your application, add that domain check before calling this generic numeric helper.

Sorting requires memory proportional to the sample size and typically O(n log n) preprocessing time. Each threshold lookup then uses a binary search. This example is not a streaming approximation and does not compute confidence intervals or weighted distributions. The reported percentage describes the supplied observations, not a guarantee about future service performance.

Continue with data-science tutorials and Python tutorials.