ML Foundations

Keep probability 1.0 in the final histogram bin

A searchsorted bin-ID calculation needs an explicit final-edge rule to match NumPy histogram. Validate the score range before adjusting that endpoint.

Evidence and testing

Review record: Tensor Almanac automated evidence gate (no independent human review). Example versions, recorded outputs and limitations appear below.

The final interval has a different endpoint rule

When assigning model confidence scores to bins, a probability of exactly one needs a defined destination. NumPy histogram includes the rightmost edge in its final bin. Earlier bins include their lower edge and exclude their upper edge. For edges zero, 0.5 and one, a score of 0.5 belongs in the second bin, and so does one. [1]

If a report uses histogram counts but computes per-score bin IDs separately, make both paths use the same convention. Otherwise, the aggregate count can include a score that the per-bin statistics discard or treat as out of range. The fixture below tests that agreement without calculating a calibration metric.

References: [1]

Translate insertion positions into bin IDs carefully

np.searchsorted(edges, scores, side="right") returns insertion positions after equal edges. Subtracting one gives the intended bin for internal boundaries, but at the final edge it produces the number of bins, which is not a valid zero-based bin ID. This follows from the documented right-side insertion rule and the possible return value equal to the length of the edge array. [2]

The helper first validates finite one-dimensional scores in the closed unit interval and strictly increasing finite edges starting at zero and ending at one. Only then does it cap the calculated ID at the final valid bin. Under that contract, the final endpoint is the sole case requiring the adjustment. Capping unvalidated scores would silently map values above one into a valid bin.

Run python examples/probability_histogram_edges.py. Its five-score fixture produces two observations in the first bin and three in the second. Assertions compare the complete bin IDs with hand-derived expectations, compare their counts with np.histogram, and check that the total preserves every accepted score.

Executed with Python 3.12.14 and NumPy 2.3.5 on bounded float64 fixtures. Exact stdout and source-byte SHA-256 are preserved in private evidence.

"""Match per-score bin IDs to NumPy histogram's closed final interval."""
import numpy as np


def probability_bins(scores, edges):
    x = np.asarray(scores, dtype=np.float64)
    e = np.asarray(edges, dtype=np.float64)
    if e.ndim != 1 or e.size < 2 or not np.isfinite(e).all():
        raise ValueError('expected finite one-dimensional edges')
    if e[0] != 0 or e[-1] != 1 or not (np.diff(e) > 0).all():
        raise ValueError('expected strictly increasing edges from zero to one')
    if x.ndim != 1 or not np.isfinite(x).all() or ((x < 0) | (x > 1)).any():
        raise ValueError('expected finite probabilities in the closed unit interval')
    ids = np.searchsorted(e, x, side='right') - 1
    # Input validation makes the final endpoint the only overflow-bin case.
    return np.minimum(ids, e.size - 2)


edges = np.array([0., 0.5, 1.])
scores = np.array([0., 0.25, 0.5, 0.75, 1.])
naive = np.searchsorted(edges, scores, side='right') - 1
assert naive.tolist() == [0, 0, 1, 1, 2]
ids = probability_bins(scores, edges)
counts = np.bincount(ids, minlength=len(edges) - 1)
reference, _ = np.histogram(scores, bins=edges)
assert ids.tolist() == [0, 0, 1, 1, 1]
assert counts.tolist() == [2, 3] and np.array_equal(counts, reference)
assert int(counts.sum()) == scores.size
neighbors = [np.nextafter(0.5, 0.), 0.5, np.nextafter(0.5, 1.)]
assert probability_bins(neighbors, edges).tolist() == [0, 1, 1]
assert probability_bins([], edges).size == 0
# Check a second, nonuniform edge configuration against the histogram API.
other_edges = [0., 0.1, 0.7, 1.]
other_ids = probability_bins(scores, other_edges)
assert np.array_equal(np.bincount(other_ids, minlength=3), np.histogram(scores, bins=other_edges)[0])
print('naive bin IDs:', naive.tolist())
print('histogram-compatible IDs:', ids.tolist())
print('counts:', counts.tolist())
print('boundary neighbors:', probability_bins(neighbors, edges).tolist())
print('empty scores and nonuniform edges: passed')
rejected = 0
for bad_scores, bad_edges in (([-0.1], edges), ([1.1], edges), ([np.nan], edges), (scores, [0., 0.5, 0.5, 1.]), (scores, [0., 0.5, 0.4, 1.])):
    try:
        probability_bins(bad_scores, bad_edges)
    except ValueError:
        rejected += 1
assert rejected == 5
print('invalid fixtures rejected:', rejected)

Recorded output

naive bin IDs: [0, 0, 1, 1, 2]
histogram-compatible IDs: [0, 0, 1, 1, 1]
counts: [2, 3]
boundary neighbors: [0, 1, 1]
empty scores and nonuniform edges: passed
invalid fixtures rejected: 5

References: [1], [2]

Test boundaries and invalid data explicitly

The example checks the representable values immediately below and above 0.5 as well as the boundary itself. It also checks an empty score array and compares counts for nonuniform edges. Five rejection cases cover scores below zero, above one and NaN, plus duplicated or descending edges.

This helper intentionally rejects invalid probabilities instead of omitting or clipping them. It requires edges covering the complete unit interval; it does not implement every histogram range, weights or density option. Matching these finite fixtures establishes the chosen assignment convention, not the calibration or predictive quality of a model.

References: [1], [2]

Sources

  1. NumPy histogram: interval conventions and explicit edges
  2. NumPy searchsorted: insertion positions and side