ML Foundations

Sampling N rows with replacement is not a guaranteed full pass

An epoch-sized draw can repeat some row IDs and omit others. Check visit counts when the training contract requires every row exactly once.

Evidence and testing

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

Draw count and row coverage are different contracts

For a dataset with N rows, requesting N random indices does not by itself mean that every row is visited. NumPy Generator.choice defaults to replacement, which permits selecting the same population entry multiple times. Without an explicit probability vector, it samples uniformly, but uniform sampling still permits repeats. [1]

The distinction matters when a training loop labels N draws as one epoch. That may be an intentional sampling budget, or the intended contract may be one visit to every row. Name the contract before treating repeated or missing IDs as a bug. The example uses unique integer row IDs, so repeated values unambiguously represent repeated visits.

References: [1]

Count visits rather than checking only output length

Run python examples/sampling_epoch_coverage.py. In the fixed five-row fixture, a replacement draw returns five indices but visits row one twice and never visits row zero. The output length check passes; a per-row visit-count check exposes the difference. This observed draw illustrates a possibility, not a claim that every replacement draw must contain duplicates.

The file then requests all five indices with replace=False and separately generates permutation(5). Assertions check that each result contains exactly the complete index range and that every row count is one. NumPy documents permutation of an integer as a randomized ordering of the corresponding arange. Those properties support a full-pass index schedule when every returned index is consumed. [1, 2]

Two boundary cases keep the claim precise: a four-index draw without replacement is unique but incomplete, while requesting six indices from five without replacement raises ValueError in the executed example. Uniqueness alone is not coverage.

Executed with Python 3.12.14 and NumPy 2.3.5 using seed zero and a five-row synthetic population. Exact stdout and source-byte SHA-256 are preserved in private evidence.

"""Compare a fixed replacement draw with two full-coverage sampling choices."""
import numpy as np

n = 5
replacement = np.random.default_rng(0).choice(n, size=n, replace=True)
counts = np.bincount(replacement, minlength=n)
missing = np.flatnonzero(counts == 0)
repeated = np.flatnonzero(counts > 1)
assert replacement.size == n and counts.sum() == n
assert missing.size > 0 and repeated.size > 0
assert np.unique(replacement).size < n

without = np.random.default_rng(0).choice(n, size=n, replace=False)
permutation = np.random.default_rng(0).permutation(n)
for sample in (without, permutation):
    assert sample.size == n
    assert np.array_equal(np.sort(sample), np.arange(n))
    assert np.array_equal(np.bincount(sample, minlength=n), np.ones(n, dtype=int))

# A smaller sample without replacement is unique, but still not a full pass.
partial = np.random.default_rng(0).choice(n, size=n-1, replace=False)
assert np.unique(partial).size == n-1
assert set(range(n)) - set(partial.tolist())
print('replacement draw:', replacement.tolist())
print('visits per row:', counts.tolist())
print('missing row IDs:', missing.tolist())
print('repeated row IDs:', repeated.tolist())
print('without-replacement visits:', np.bincount(without, minlength=n).tolist())
print('permutation visits:', np.bincount(permutation, minlength=n).tolist())
print('smaller unique sample covers every row:', False)
try:
    np.random.default_rng(0).choice(n, size=n+1, replace=False)
except ValueError:
    print('oversized no-replacement request rejected:', True)
else:
    raise AssertionError('expected oversized sample rejection')

Recorded output

replacement draw: [4, 3, 2, 1, 1]
visits per row: [0, 2, 1, 1, 1]
missing row IDs: [0]
repeated row IDs: [1]
without-replacement visits: [1, 1, 1, 1, 1]
permutation visits: [1, 1, 1, 1, 1]
smaller unique sample covers every row: False
oversized no-replacement request rejected: True

References: [1], [2]

Audit the indices that the consumer actually uses

When a full pass is required, retain row IDs through the loading path and verify their counts after the consumer has processed them. An index generator can produce complete coverage while later batching, filtering or early termination discards rows. The NumPy fixture checks index-generation behavior only; it does not exercise a framework loader or distributed training.

Sampling with replacement remains a valid choice when repeated observations are intentional. Keep its draw budget distinct from a claim about unique examples seen. The recorded seed and runtime make this small failure reproducible here; the measured result is coverage, not training quality or speed.

References: [1], [2]

Sources

  1. NumPy Generator.choice: replacement and sample size
  2. NumPy Generator.permutation: shuffled index ranges