Before You Sum Histogram Heights, Check the Bin Widths
A histogram can pass every obvious count check and still be unsafe to publish. In the synthetic fixture for this article, the bin counts are exactly [2, 2, 3], the independent membership oracle agrees with NumPy, and the density integrates to one. Yet the three density heights add to about 0.5142857143, not one. That is not a numerical defect. It is the expected consequence of area normalization across unequal bin widths, and it becomes an operational defect only when a downstream table labels those heights as “probability,” “share,” or “percent.” The NumPy 2.3 histogram contract states that density=True normalizes the integral, not the unweighted sum of the returned heights; values outside the histogram range are ignored, and the last bin includes its right edge.
This playbook treats that distinction as a release gate. The controlled scope is a finite, one-dimensional measurement array converted to float64, explicit unequal-width finite edges, and no weights. We will reconcile membership, counts, density, conditional per-bin mass, full-input mass, range exclusions, and unit conversion. The executable fixture uses deterministic synthetic values. Its observed results support the example under the recorded runtime; the local admission policy and proposed production schema define additional application requirements. Weighted histograms and broader production input conditions require separate qualification.
Write the meaning of one exported histogram row
Before approving a histogram table, define the row contract. Let an ordered edge array be e = [e0, e1, ..., ek]. Bin j spans from ej to e(j+1); for NumPy, every bin except the last is left-closed and right-open, while the final bin includes its right edge. The bin width is wj = e(j+1) - ej. With no weights, the count cj is the number of retained measurements assigned to that bin. NumPy permits an explicit edge sequence, including non-uniform widths, so width is part of the numerical meaning rather than display decoration.
The next quantity is density height. In this playbook it means the value returned by np.histogram(..., density=True). If R measurements are retained inside the declared edge range, then for this unweighted one-dimensional case the corresponding bin density is
dj = cj / (R * wj).
For measurements with physical units, density has the inverse of the measurement unit. If the measurements are seconds, density is “per second”; if the same quantities are represented in milliseconds, the numerical density must change by the reciprocal scale factor. A density height is not itself a bin share and should not receive a percent sign. That distinction also applies when the measured coordinate is dimensionless. Under NumPy’s density definition, unit-width bins make heights numerically equal to conditional masses. Other widths provide no such general guarantee.
Two dimensionless masses are needed. Conditional bin mass is cj / R: the empirical fraction among measurements retained inside the declared histogram range. Full-input bin mass is cj / N, where N is the number of measurements admitted to the histogram procedure before range exclusion. These answer different questions. A release-safe row therefore needs enough context to tell a consumer which denominator is intended. That emphasis on explicit numerical meaning complements broader Python scientific-computing foundations, but the gate here is narrower: we are validating the semantics of one exported distribution artifact, not surveying libraries.
For every published row, declare the meaning of its edges, width, count, density, and mass fields. At artifact level, preserve the measurement unit, normalization, counts, and declared range. The release question is what each field means, in what unit, and relative to which population.
Separate bin membership from vertical-axis semantics
Histogram review has two logically distinct layers. Membership semantics decide which measurements belong in which bins. Vertical-axis semantics decide what number to attach to each already-defined bin: count, relative frequency, density, retained-sample mass, or full-input mass. Mixing these layers is how a correct count can become a misleading percentage.
The distinction matters because a membership test can pass while a reporting contract fails. For the fixture used here, the explicit edges are [0, 1, 3, 8]. The three widths are therefore [1, 2, 5]. Once membership is fixed, the same counts [2, 2, 3] can be represented as counts, as fractions of seven retained observations, as fractions of nine admitted observations, or as density heights. All four are numerically related, but they are not interchangeable.
This differs from time-window membership validation, where changing an interval closure convention can move boundary events between windows while preserving a grand total. Here the numeric bins are fixed. We verify membership once, then hold it fixed while testing whether the exported heights are labeled with the right normalization, width, units, and denominator. The adjacent workflow is useful as a boundary: both insist on an independent membership contract, but this article focuses on density-versus-mass semantics.
Unequal widths are deliberate. A unit-width fixture is a weak negative control because dj = cj/R whenever every wj = 1. In that special case the density heights numerically equal conditional masses, so an implementation can confuse two different quantities and still appear correct. Unequal widths force the dimensions into view: multiplying each density by its own bin width is necessary to obtain mass.
A correct count can still be labeled as the wrong quantity
Do not diagnose density=True as “counting incorrectly” when counts match the independent oracle. NumPy documents two different output semantics: density=False returns sample counts, while density=True returns values normalized so the histogram integral over its range is one. The operational failure sits downstream when a schema, dashboard, CSV column name, API contract, or reviewer calls those density heights probability without the required multiplication by width.
That distinction changes remediation. A membership error may require recomputation from source measurements. A labeling error may be repairable if full-precision density values, exact edges, unit metadata, and denominators have been preserved. The release gate should therefore keep “membership correct?” and “published quantity correct?” as separate assertions. Passing the first never waives the second.
Admit finite measurements and explicit edges
The local gate deliberately imposes a stricter admission policy than NumPy’s full set of legal inputs. Measurements are converted to float64 and must form a nonempty, finite, one-dimensional array. Edges are also converted to float64, must be one-dimensional, and must contain at least two finite values. Widths are formed with np.diff; every width must be finite and strictly positive. The gate also rejects the case in which no measurement is retained inside the declared range.
The finiteness test uses numpy.isfinite, which returns false for NaN and positive or negative infinity. The decision to reject such input instead of filtering it is an application-policy choice, not a claim that NumPy universally requires every caller to do the same. The point is provenance: silent cleaning changes N, changes possible denominators, and can make a normalized table appear complete when source rows have actually been removed.
Likewise, this article does not turn histogram review into a general cleaning pipeline. A broader workflow for declaring data-cleaning policies can cover parsing, missingness, schema repair, duplicates, and joins. Here the release gate is intentionally narrower: finite measurement admission is binary, and rejected inputs retain a reason.
The edge policy is also explicit. NumPy accepts a sequence of monotonically increasing edges and supports non-uniform widths. This gate goes further by requiring strictly positive finite widths after conversion to float64. A repeated edge therefore fails locally before normalization. The fixture wraps np.diff in np.errstate(over="raise"), then confirms that the resulting widths are finite. These checks cover the selected invalid cases in the fixture; they are not an exhaustive qualification of every Python object, dtype coercion, overflow scenario, or adversarial input that NumPy could encounter.
Treat admission outcomes operationally. A nonfinite value, empty input, repeated edge, or zero-retained set is REJECT at the gate: do not emit an apparently approved density table. If policy owners later decide to transform such data, that transformation is a new upstream step with its own ownership and evidence, followed by a RERUN of the histogram gate. Never relabel silent filtering as though the original input had passed.
Run the complete unequal-width fixture
The fixture uses nine synthetic measurements and four declared edges:
x = [-1, 0, 0.5, 1, 2, 3, 7, 8, 9]
edges = [0, 1, 3, 8]
The complete fixture below was executed on 20 October 2026 using CPython 3.12.14 and NumPy 2.3.5. It returned PASS_13_CASES with process exit status 0 and no standard-error output. The case table summarizes those observed synthetic results. They apply to this fixture and environment; a different runtime or implementation needs its own run.
To reproduce the example, use an environment with the recorded Python and NumPy versions, save the complete code below as histogram_density.py, and run:
python histogram_density.py > histogram-result.json
"""Unequal-width histogram semantics, using fixed synthetic measurements."""
import json
import platform
import numpy as np
def require(ok, message):
if not ok:
raise RuntimeError(message)
def qualify(values, edges):
x = np.asarray(values, dtype=np.float64)
e = np.asarray(edges, dtype=np.float64)
if x.ndim != 1 or x.size == 0 or not np.isfinite(x).all():
raise ValueError(
"nonempty finite one-dimensional measurements required"
)
if e.ndim != 1 or e.size < 2 or not np.isfinite(e).all():
raise ValueError("finite one-dimensional edges required")
with np.errstate(over="raise"):
widths = np.diff(e)
if not np.isfinite(widths).all() or not (widths > 0).all():
raise ValueError("strictly increasing finite-width bins required")
# Independent direct comparisons, not a second histogram
# implementation call.
membership = [
[i for i, value in enumerate(x)
if lo <= value and
(value < hi or (j == len(widths)-1 and value == hi))]
for j, (lo, hi) in enumerate(zip(e[:-1], e[1:]))
]
oracle = np.array([len(ids) for ids in membership])
retained = int(oracle.sum())
if retained == 0:
raise ValueError("no observations in the declared range")
counts, returned_edges = np.histogram(x, bins=e, density=False)
density, = np.histogram(x, bins=e, density=True)
require(
np.arrayequal(counts, oracle),
"counts disagree with independent membership"
)
require(np.array_equal(returned_edges, e), "edges changed")
conditional_mass = counts / retained
full_mass = counts / x.size
require(np.allclose(density widths, conditional_mass, rtol=0, atol=1e-12),
"density integral mismatch")
return {"counts": counts.tolist(), "widths": widths.tolist(),
"density": density.tolist(),
"conditional_mass": conditional_mass.tolist(),
"full_mass": full_mass.tolist(),
"membership_row_indices": membership,
"n_input": int(x.size), "n_retained": retained,
"n_outside": int(x.size)-retained,
"outside_mass": (int(x.size)-retained)/int(x.size)}
def main():
x = [-1, 0, 0.5, 1, 2, 3, 7, 8, 9]
edges = [0, 1, 3, 8]
r = qualify(x, edges)
rows = []
def record(case_id, ok, evidence):
require(bool(ok), case_id)
rows.append({"id": case_id, "evidence": evidence})
d, w = np.array(r["density"]), np.array(r["widths"])
record("H01_counts_and_membership", r["counts"] == [2, 2, 3], r)
record("H02_area_not_height_sum",
np.isclose(np.dot(d, w), 1) and not np.isclose(d.sum(), 1),
{"height_sum": float(d.sum()), "area": float(np.dot(d, w))})
record("H03_conditional_bin_probability", np.allclose(dw, [2/7, 2/7, 3/7]),
{"conditional_mass": r["conditional_mass"]})
record("H04_full_population_denominator",
r["n_input"] == 9 and r["n_outside"] == 2
and np.isclose(sum(r["full_mass"])+r["outside_mass"], 1),
{"full_mass": r["full_mass"], "outside_mass": r["outside_mass"]})
converted = qualify(np.array(x)*1000, np.array(edges)*1000)
record("H05_unit_conversion", np.allclose(converted["density"], d/1000)
and np.allclose(converted["conditional_mass"],
r["conditional_mass"]),
converted)
narrow = qualify([0.1, 0.2], [0, 0.25, 0.5])
record("H06_density_can_exceed_one", narrow["density"] == [4.0, 0.0]
and narrow["conditional_mass"] == [1.0, 0.0], narrow)
record("H07_last_edge_included",
r["membership_row_indices"] == [[1, 2], [3, 4], [5, 6, 7]],
{"row_7_value": x[7],
"last_bin_row_indices": r["membership_row_indices"][-1]})
shared = np.histogram_bin_edges(x, bins=edges)
other = qualify([0.2, 1.5, 3.5], shared)
record("H08_reuse_declared_edges",
np.array_equal(shared, edges) and other["counts"] == [1, 1, 1],
other)
for case_id, values, bins in [
("H09_reject_nan", [0.5, np.nan], edges),
("H10_reject_infinity", [0.5, np.inf], edges),
("H11_reject_repeated_edge", x, [0, 1, 1, 8]),
("H12_reject_no_retained_values", [-2, 9], edges),
("H13_reject_empty_input", [], edges),
]:
try:
qualify(values, bins)
except ValueError as error:
record(case_id, True, {"rejected": True, "reason": str(error)})
else:
raise RuntimeError(case_id + " incorrectly admitted")
print(json.dumps({"status": "PASS_13_CASES",
"python": platform.python_version(),
"numpy": np.__version__, "cases": rows},
indent=2, allow_nan=False))
if name == "__main__":
main()Do not use a second histogram call as an independent oracle
The membership oracle is intentionally specified with direct comparisons over row indices. For each bin, it enumerates a source index when lo <= value < hi, except that the last bin also accepts value == hi. That rule mirrors the documented NumPy boundary convention but is implemented independently of np.histogram. NumPy documents the same half-open bins with an inclusive final bin.
The two NumPy calls are not independent oracles. density=False and density=True share the same histogram implementation family; one gives counts and the other normalized heights. Treating their mutual agreement as independent evidence would overstate the test. The direct-comparison membership list provides the orthogonal check, while exact edge equality confirms that the returned edge array did not change.
The tolerances are scoped to individual checks. Membership and counts use exact integer and equality checks. Inside qualify(), the relation between density * widths and conditional_mass uses numpy.allclose with rtol=0 and atol=1e-12. The H02–H05 diagnostic assertions also use numpy.isclose or allclose with their documented defaults, rtol=1e-5 and atol=1e-8. These approximate comparisons acknowledge floating-point normalization without weakening integer membership. NumPy warns that a default absolute tolerance may be unsuitable for small magnitudes. A production measurement scale may therefore require different tolerances, reviewed and tested separately from this nine-value fixture.
Reconcile counts before interpreting normalization
Start with row identity, not density. The declared bins are [0,1), [1,3), and [3,8]. Applying the independent oracle to the nine source values yields membership row indices [[1,2], [3,4], [5,6,7]]. Therefore the counts are [2,2,3], the widths are [1,2,5], and seven measurements are retained. Two admitted finite measurements lie outside the declared range: -1 below the first edge and 9 above the final edge.
The last-edge check is small but useful. Source row index 7 has value 8, and NumPy’s documented convention includes the rightmost edge in the final bin. Thus the last bin contains values 3, 7, and 8; its count is three. We do not turn that into a full boundary-analysis tutorial. Its purpose is to establish that the oracle and library agree on the exact declared fixture.
A release table at this stage can already expose most of the semantics:
Bin | Width | Count | Density | Conditional mass (/7) | Full-input mass (/9) |
[0,1) | 1 | 2 | 2/7 ≈ 0.285714 | 2/7 ≈ 0.285714 | 2/9 ≈ 0.222222 |
[1,3) | 2 | 2 | 1/7 ≈ 0.142857 | 2/7 ≈ 0.285714 | 2/9 ≈ 0.222222 |
[3,8] | 5 | 3 | 3/35 ≈ 0.085714 | 3/7 ≈ 0.428571 | 3/9 ≈ 0.333333 |
Count reconciliation does not authorize summing density heights as probabilities, claiming coverage of all nine inputs, or ignoring measurement units. Each passing test approves only the property it actually tested.
The executed synthetic fixture produced thirteen case results:
Case | Observed fixture evidence | Operational meaning |
H01 | Counts [2,2,3]; 7 retained of 9 | Membership/count baseline passes |
H02 | Height sum ≈0.5142857143; area 1 | Negative control against summing heights |
H03 | Masses [2/7,2/7,3/7] | Density × width matches retained-sample mass |
H04 | Bin masses /9; outside mass 2/9 | Full-input denominator preserved |
H05 | 1000× unit conversion | Density rescales; masses do not |
H06 | Density [4,0], mass [1,0] | Height may exceed one |
H07 | Value 8 retained in last bin | Final-edge boundary check |
H08 | Declared edges reused | Shared edge contract passes |
H09–H13 | NaN, infinity, repeated edge, zero retained, empty input rejected | Local admission policy fails closed |
That ledger is synthetic diagnostic evidence, not a claim about a production population. Its job is to make quantity semantics reviewable under known conditions.
Show why these density heights do not sum to one
With seven retained observations, the three density heights are determined by count divided by retained count and bin width:
• First bin: (2/7) / 1 = 2/7.
• Second bin: (2/7) / 2 = 1/7.
• Third bin: (3/7) / 5 = 3/35.
Adding those heights produces 2/7 + 1/7 + 3/35 = 18/35 ≈ 0.5142857143. Nothing in the documented density contract requires that sum to equal one. NumPy instead specifies unit-area normalization, and its own example demonstrates that raw density values can sum to something other than one while the width-weighted sum equals one.
The correct check is the discrete integral over the piecewise-constant histogram:
Σ(dj wj) = (2/7)1 + (1/7)*2 + (3/35)*5 = 2/7 + 2/7 + 3/7 = 1.
NIST distinguishes the same two ideas. One normalization divides a class count by the number of observations, making bar height a relative frequency that sums to one. Another divides by both observation count and class width, making area sum to one; under that density-style normalization, heights greater than one are permissible. NIST’s histogram guidance therefore supports the same semantic separation without depending on NumPy.
Dimensions provide another independent check. A count records observations, while count / retained_count is dimensionless. Dividing again by a width measured in, say, seconds produces 1/second. Summing 1/second values across bins without multiplying by their widths does not produce a dimensionless probability. The units themselves expose the interpretation error.
Use area, not a percentage symbol attached to height
For this fixture, the retained-sample mass of each bin is recovered by density * its_own_width. The width is not optional metadata. Applying one common multiplier would be wrong because the widths are 1, 2, and 5.
That is why a publication schema should not rename a density field to probability_pct or format it as % merely because the density integrates to one. A percentage must be dimensionless and tied to a denominator. The valid retained-sample percentages here would be about 28.57%, 28.57%, and 42.86%, obtained from [2/7,2/7,3/7]. The density heights are approximately 0.285714, 0.142857, and 0.085714 per measurement unit. The first numbers happen to match in the width-one bin; that coincidence does not generalize.
A diagnostic that merely asserts np.isclose(np.sum(density * widths), 1) is necessary for this density contract, but not sufficient for release. It says the normalized histogram area is coherent. It does not identify the denominator, prove full range coverage, preserve units, or verify labels. A passing integral test can coexist with an unsafe export.
Recover the probability mass for each retained bin
Once counts, widths, and retained count are reconciled, conditional mass follows by two equivalent routes:
conditional_mass = counts / retained_count
and
conditional_mass = density * widths
For this fixture, both routes correspond to [2/7, 2/7, 3/7]. H03 checks those expected fractions, while qualify() separately checks the density-width identity with its explicit tolerance. This is the correct empirical probability statement conditional on retention in the declared range. In plain language: among the seven admitted finite measurements that fall between 0 and 8 under the specified boundary convention, two are in the first bin, two in the second, and three in the third.
The word “conditional” is operationally important. These fractions do not describe all nine admitted measurements, because two were excluded by range. Nor do they establish a confidence interval, population parameter, or fitted probability model. The fixture contains nine deterministic synthetic values supplied for a numerical release test. Turning 2/7 into an inferential statement about an unknown real-world population would exceed the evidence.
For a distribution table intended to carry retained-sample bin probability, publish the mass directly rather than forcing every consumer to reconstruct it. A robust row might contain both density and conditional_mass, because each answers a different downstream need. Density supports comparisons that preserve area under unequal widths; mass supports statements such as “what fraction of retained observations is in this interval?” Keeping both avoids forcing a semantic conversion inside a dashboard or spreadsheet.
The reconciliation identity also provides a review invariant. For every bin j, verify
count[j] == retained_count density[j] width[j]
within a justified numerical tolerance, while exact integer membership remains the source of truth for counts. Then verify sum(conditional_mass) == 1 within tolerance. These are local arithmetic checks, not substitutes for provenance. If the edge array, measurement unit, or retained denominator is missing, a table can satisfy a numerical identity and still be unusable because its meaning cannot be reconstructed safely.
This is where the gate separates a recoverable transformation defect from an evidence defect. Wrong column labeling with intact full-precision fields may be REPAIR. Missing denominator or unit metadata may force HOLD until provenance is recovered. Missing source measurements plus rounded heights can make trustworthy reconstruction impossible.
Expose the denominator hidden by range exclusion
NumPy’s histogram documentation states that values outside the selected range are ignored. With explicit edges, the effective histogram range is determined by those edges; the fixture therefore retains seven values in [0,8] under the documented final-edge convention and leaves two outside. Density normalization then integrates to one over the retained histogram, even though the histogram does not represent every admitted input row.
That creates a common reporting trap. A reviewer sees area one and assumes “100% of the data are represented.” In the fixture, that statement is false under the full-input denominator. The bin masses relative to all nine admitted measurements are [2/9, 2/9, 3/9]. Their sum is 7/9. The separate outside mass is 2/9. Only when the bins and outside category are combined does full-input mass sum to one.
The two denominator views should be explicit:
Quantity | Formula | Fixture total | Meaning |
Conditional bin mass | count / 7 | 1 across bins | Distribution within declared range |
Full-input bin mass | count / 9 | 7/9 across bins | Share of all admitted finite measurements inside each bin |
Outside mass | outside_count / 9 | 2/9 | Share admitted but outside declared edges |
Density area | Σ density * width | 1 | Area-normalized retained histogram |
All nine values in this fixture are finite. A run containing nonfinite measurements is rejected before histogram construction, so the gate does not produce a smaller, silently filtered denominator. That policy must be stated. A different application could choose a separate upstream cleaning rule, but then its admitted-input count would need to be defined anew and the histogram gate rerun.
Publish outside counts rather than claiming complete coverage from area one
A release artifact should publish at least input_count=9, retained_count=7, outside_count=2, edge_min=0, and edge_max=8 for this fixture. These five fields make the difference between normalized area and complete source coverage explicit.
This is also why a normalized table should not be reverse-engineered into a full-input distribution when the outside count is absent. Density normalization intentionally conditions on retained observations. From a density vector and edges alone, you can recover masses that sum to one within the represented range, but you cannot infer how many admissible source measurements fell outside that range. Many different source arrays can yield the same normalized in-range histogram with different outside counts.
The operational rule is simple: coverage is a first-class field. If downstream consumers need a statement about all admitted measurements, publish the outside category or its count/mass alongside the bins. If they need only a conditional distribution inside a declared range, label it that way. Do not make readers infer denominator semantics from the fact that an integral equals one.
Change measurement units without changing bin probabilities
H05 is an additional unit-conversion test. For these fixture values, multiplying every measurement and every edge by 1000 preserves membership. The transformation is applied consistently to both values and edges. Counts remain [2,2,3], retained and outside counts remain 7 and 2, and both conditional and full-input masses are unchanged.
Widths, however, become [1000,2000,5000]. To preserve density * width = mass, the density heights must divide by 1000. The observed output confirms that relation within the H05 floating-point tolerance: converted density agrees with the original density divided by 1000, while conditional mass is preserved. The relationship follows directly from the units of a one-dimensional density.
This test catches a category of production defect that count-only validation misses. Imagine one pipeline exports measurements in meters and another in millimeters but both label the density column simply probability. If their underlying bin masses are identical, raw density heights differ by a factor of one thousand. A reviewer comparing those heights might report a dramatic distribution shift where none exists. Once units and widths are restored, the masses agree.
The required contract is therefore two-part: publish the measurement unit attached to edges, and identify density as inverse-unit. A unit conversion that changes edges and density consistently can be ACCEPT after revalidation. A conversion that changes edge values but leaves density heights untouched is REPAIR if full-precision source values and transformation logic are available; otherwise REBUILD from source measurements. Any unexplained unit mismatch is HOLD, because arithmetic can be internally consistent under the wrong unit label.
Do not compare density heights across artifacts with incompatible measurement units until they have been converted to a common unit or transformed into unitless bin masses over matching bins. The same caution applies to area: area remains one under coherent unit conversion, so an integral check alone cannot reveal a missing unit label.
Allow a valid density height greater than one
The narrow-bin control uses measurements [0.1, 0.2] with edges [0, 0.25, 0.5]. Both observations fall into the first bin. The widths are [0.25,0.25], conditional masses are [1,0], and density heights are [4,0]. Multiplying height four by width 0.25 gives area one for the first bin. That is valid density normalization.
NIST explicitly notes that values above one are permissible under area normalization, because the normalized height is count divided by observation count and class width rather than a direct probability. NumPy makes the corresponding distinction by stating that a density histogram is not a probability mass function and that raw values need not sum to one. A height of four is therefore not “400% probability.” It is four per measurement unit across a quarter-unit interval.
This point must remain separate from probability-calibrator release validation. A classifier probability is a different object with different release semantics; this fixture fits no model, calibrates no score, computes no reliability diagram, and approves no model-calibrator pair. The only probability-like quantities here are empirical bin masses obtained from counts and denominators.
The narrow-bin case is a strong negative control because it breaks the intuitive but false rule “a probability-like histogram value must be between zero and one.” A reviewer who rejects height four merely because it exceeds one is applying a mass constraint to a density. Conversely, the fact that a density may exceed one does not authorize arbitrary values: its bin area still has to reconcile to the corresponding mass, and total area must satisfy the declared normalization.
Operationally, column naming does most of the prevention. Prefer density_per_<unit> or a schema-level unit annotation rather than probability. If consumers require percentages, export conditional_mass or full_input_mass as separate fields and convert those dimensionless quantities to percentages. Never solve the interpretation problem by clipping density to [0,1]; that would corrupt the normalized area and destroy the numerical contract.
Reuse edges and retain failed admission controls
H08 tests a reusable edge contract. The fixture calls numpy.histogram_bin_edges with the already-declared sequence [0,1,3,8]. NumPy documents that a supplied edge array is passed through for use as histogram edges and shows that shared bins can be reused across multiple histograms. The fixture then applies those edges to [0.2,1.5,3.5] and obtains one count in each bin.
That case does not claim the edges are statistically optimal. NumPy’s same documentation describes several automatic estimators as practical methods for selecting bin widths, but this scoped example intentionally avoids an “optimal bins” survey. The application contract says the edges are declared and reusable. Review whether they are correct for the intended product elsewhere; here we test that the published summary preserves the declared edge set and its semantics.
The negative controls are equally important:
Case | Trigger | Local policy outcome |
H09 | Measurement array contains NaN | REJECT |
H10 | Measurement array contains infinity | REJECT |
H11 | Edge sequence repeats an edge | REJECT |
H12 | No measurements fall inside declared range | REJECT |
H13 | Empty measurement array | REJECT |
For NaN and infinity, np.isfinite provides the primitive test; the decision to reject rather than filter is local policy. For repeated edges, the gate rejects non-positive widths after np.diff. For zero-retained and empty input, it refuses to produce a density table that could look normalized despite carrying no meaningful represented observations.
Do not let nonfinite or empty inputs produce an apparently approved distribution
A production gate should preserve the reason for refusal. That matters because “no artifact” and “an all-zero or NaN-filled artifact” have different downstream failure modes. A rejected production run should emit structured failure metadata to the orchestration layer, not a distribution table that a dashboard can ingest as valid. The displayed script records expected rejection cases; a production failure-reporting interface remains a separate integration task.
The five invalid-input cases are selected diagnostic controls, not exhaustive fuzz testing. They do not establish behavior for every malformed nested object, every dtype conversion, extreme floating-point magnitude, memory condition, or future library version. Treat that boundary honestly. When the runtime, NumPy version, edge-generation logic, admission policy, or schema changes, schedule a RERUN of the qualifying fixture and preserve the actual resulting output. A prior PASS_13_CASES is evidence about the recorded environment, not an evergreen certificate.
Repair an exported table without inventing lost evidence
Repair begins by asking what evidence survived. Suppose an exported table contains full-precision density values and exact bin edges, and provenance establishes the measurement unit and that the density was normalized over retained observations. Then conditional masses can be recovered as density * width. If counts are also present, revalidate the identity against counts / retained_count. This is a bounded REPAIR because the missing or mislabeled field can be reconstructed from preserved, semantically sufficient evidence.
Do not overstate what density and edges alone can recover. They cannot reveal the original retained count, because scaling every bin count and total retained count by the same factor leaves density unchanged. They also cannot reveal how many admitted observations fell outside the edge range. A normalized table is many-to-one with respect to the original source population.
Rounded density creates a second limitation. If a CSV retained only, say, two decimal places of each height, multiplying by widths produces approximate masses, not authoritative original counts. You may be able to assess whether the table is broadly plausible, but you should not fabricate integer counts or a precise denominator from rounded values. When exact source measurements remain available, the safer path is REBUILD: recompute the histogram under the declared contract, reconcile it against the independent membership oracle, and replace the lossy table with a fully specified artifact.
Use HOLD when required provenance is absent and source reconstruction is not currently possible. Examples include unknown measurement units, missing edges, ambiguous normalization, missing denominator definition, or a table whose range coverage cannot be established. HOLD means “not publishable yet,” not “probably wrong.” It preserves uncertainty instead of converting missing evidence into a guess.
Use RERUN when the implementation and contract are believed correct but the evidence belongs to a different execution context, for example, after a NumPy upgrade, code change, environment rebuild, or material schema modification. A rerun must preserve the new environment identifiers and actual output. If it disagrees with the recorded fixture result, retain the discrepancy and investigate rather than editing expected values to make the test pass.
Finally, reserve REJECT for input that violates admission policy before a valid artifact exists. Reserve REPAIR for a bounded correction that does not require inventing missing evidence. Reserve REBUILD for regenerating the artifact from authoritative measurements when the old table has lost necessary meaning. These words should be part of the operational runbook, not improvised during an incident.
Approve the histogram contract, not just its integral
Release approval belongs to the complete artifact contract. The transformation owner is accountable for source admission, edge application, membership reconciliation, normalization, and emitted metadata. The report or data-product owner is accountable for field names, displayed units, denominator wording, range coverage, and consumer-facing interpretation. Under this proposed release policy, both roles, or their formally delegated equivalents, approve the properties they own.
A practical decision matrix is:
Decision | Minimum condition | Required next action |
ACCEPT | Counts match independent membership; widths and units are explicit; density × width reconciles to conditional mass; denominator and coverage counts are present; labels match semantics | Publish artifact and evidence bundle |
REPAIR | Meaning is recoverable from intact full-precision evidence; defect is bounded to labeling, unit conversion metadata, or derivable fields | Correct, revalidate, obtain owner approval |
RERUN | Contract is unchanged but execution evidence is stale after code/runtime/environment change | Execute full gate in identified environment and preserve output |
REBUILD | Table lost required meaning but authoritative source measurements and declared contract remain available | Recompute from source, then rerun all release checks |
HOLD | Unit, denominator, edges, normalization, or provenance cannot yet be established | Stop publication until evidence is recovered |
REJECT | Input fails the declared admission policy | Return failure reason; no histogram artifact is approved |
A diagnostic pass is necessary evidence, not publication authority. PASS_13_CASES shows that this synthetic fixture satisfied its assertions in the recorded CPython 3.12.14 / NumPy 2.3.5 environment. It does not establish that an unrelated production table used the same edges, source population, units, code revision, or denominator. It also does not validate every malformed input. Acceptance attaches to a specific artifact and its provenance.
Keep units and denominator in the published schema
A local release schema can make the intended quantity hard to misread. This is a proposed artifact design, not a built-in NumPy schema:
histogram_id
left_edge
right_edge
bin_width
measurement_unit
count
density
conditional_mass
full_input_mass
normalization = "area_over_retained_range"
input_count
retained_count
outside_count
edge_min
edge_max
source_revision
runtime_python
runtime_numpy
validation_status
validation_timestamp
For a density column, attach a unit such as 1/ms, 1/s, or 1/m through a schema registry or machine-readable metadata mechanism. For mass columns, specify the denominator in both name and metadata. conditional_mass means count divided by retained count; full_input_mass means count divided by admitted input count. The presence of both eliminates a common ambiguity at reporting boundaries.
This is part of communicating data clearly, but the acceptance criterion is numerical rather than aesthetic: a consumer should not need a chart title or tribal knowledge to discover what a field means.
Revalidation triggers should be named in the contract: change to edge values or generation logic, measurement-unit conversion, source-admission policy, histogram code, NumPy runtime, denominator definition, rounding policy, or published schema. The owner who makes such a change initiates RERUN or REBUILD as appropriate; the report owner confirms that labels and descriptions still match the regenerated values.
Develop analytics habits that keep quantities interpretable
Review distributions as data products, not pictures. Declare interval and denominator semantics, verify membership independently, reconcile counts, convert density to mass with each bin’s own width, publish outside-range counts, and preserve units. Unit conversion must change density heights consistently while leaving masses invariant.
For handoff, package the histogram table, machine-readable contract, and validation evidence together. Record the source revision, runtime, edge array, admission results, reconciliations, negative controls, and owner decision. ACCEPT only complete meaning; REPAIR only what evidence supports; RERUN after execution-context changes; REBUILD from authoritative measurements when semantic information was lost; HOLD missing provenance; REJECT inadmissible input.
Practitioners strengthening these habits can also review Refonte Learning’s Data Analytics Program. The program page lists a three-month format at 12–14 hours per week, with Python for business analytics, exploratory analysis and visualization, SQL, predictive modeling, and industry-oriented projects. It also states an admissions prerequisite of working toward a bachelor’s or higher-level degree and offers a Training Certificate and a Certificate of Internship on successful completion. These are Refonte Learning’s published program descriptions. The page does not identify this particular NumPy histogram exercise as a taught module.
The release principle remains narrower and more important than any tool choice: height, area, and denominator must each have one explicit meaning. When those meanings are preserved together with units and coverage, unequal-width histograms are straightforward to audit. When they are not, correct counts can still become incorrect claims.
