Sampling¶
truthound.profiler.sampling selects rows from a Polars LazyFrame. Use a stable
source for the count and sampling passes. Results contain a data LazyFrame,
metrics and is_sampled; collecting the result evaluates any remaining plan.
SamplingConfig¶
import polars as pl
from truthound.profiler.sampling import Sampler, SamplingConfig, SamplingMethod
frame = pl.DataFrame({"x": range(1000)}).lazy()
sampler = Sampler(SamplingConfig(
strategy=SamplingMethod.RANDOM, max_rows=100, min_sample_size=100,
seed=7, confidence_level=0.95, margin_of_error=0.05,
max_memory_mb=16, enable_caching=True,
fallback_strategy=SamplingMethod.HEAD,
))
result = sampler.sample(frame)
print(result.data.collect().height, result.metrics.sample_size)
max_rows=0 removes the row cap; the statistical sample-size calculation still
applies. min_sample_size is bounded by population and any positive row cap.
max_memory_mb=0 disables the output memory check. Positive values bound the
materialized sample's estimated bytes before Sampler.sample() returns; this is
not a native heap reservation or protection against every source allocation.
enable_caching controls the adaptive strategy's bounded decision cache, not a
data/result cache. Each sampler copies its default configuration.
SamplingConfig.for_accuracy("low" | "medium" | "high" | "maximum"),
for_speed() and for_memory(max_memory_mb=100) provide presets.
calculate_sample_size(population_size, confidence_level=0.95,
margin_of_error=0.05, min_sample_size=1) uses Cochran's formula with finite
population correction. Its assumptions do not guarantee accuracy for biased or
non-independent observations.
Strategies¶
| Method | Behavior |
|---|---|
NONE |
Explicitly returns all rows, ignoring the sampling row cap. |
HEAD |
First calculated number of rows; ordering can bias the sample. |
RANDOM |
Seeded row-index hash ordering selects exactly the calculated size. |
SYSTEMATIC |
Local-RNG starting offset and bounded interval selection. |
HASH |
Stable hash ordering of the configured column, or row index by default; equal hashes use input order. |
STRATIFIED |
Proportional per-group allocation with largest-remainder rounding, then seeded selection within each group. |
RESERVOIR |
Algorithm R over native chunks retains the selected rows plus one input chunk. |
ADAPTIVE |
Chooses a strategy from dataset-size thresholds; a positive row cap still applies to small datasets. |
Random and hash ordering may require native sorting. Stratification collects group
counts and joins quotas; tiny strata can receive zero rows when the budget cannot
represent them. Reservoir retention is O(sample size), not O(1). The framework
counts rows before selection; reservoir's selection itself makes one pass. Current
Polars uses collect_batches; older Polars without that iterator materializes the
query result before slicing, so it cannot provide the same streaming memory bound.
Temporary columns never replace user columns. Fixed seeds leave process-global
random state unchanged.
Stratification and hash columns belong to strategy objects, not SamplingConfig:
from truthound.profiler.sampling import (
SamplingStrategyRegistry, StratifiedSamplingStrategy,
)
registry = SamplingStrategyRegistry()
registry.register(StratifiedSamplingStrategy(stratify_column="category"))
sampler = Sampler(SamplingConfig(strategy=SamplingMethod.STRATIFIED, seed=7), registry)
A registry also accepts HashSamplingStrategy(hash_column="id") or trusted custom
SamplingStrategy objects. Other strategy failures invoke the configured fallback;
MemoryError, TimeoutError and PermissionError propagate without another read.
The fallback does not make external callback effects safe to replay.
SamplingMetrics¶
Metrics expose original_size, sample_size, sampling_ratio, confidence_level,
margin_of_error, strategy_used, sampling_time_ms and
memory_saved_estimate_mb. is_full_scan and reduction_factor are derived
properties. Timing for lazy strategies measures plan construction; reservoir
selection and an enabled output check also execute work. Memory savings are an
estimate, not measured process peak memory.
sample_data(lf, max_rows=100_000, strategy="adaptive") is a convenience entry.
Sampler.sample_column(lf, column, config=None) selects one column before sampling.
Approved MCP Sampling¶
A private ProfilerRuntime.samplers map binds native Sampler objects, including
selected registry/configuration. sampling_sample(sampler_id, asset_id) samples a
pinned asset once and retains the concrete snapshot, checking native row metrics
and the owner memory budget. It returns sample_id and metrics.
sample_get(sample_id, columns?, offset=0, limit=100) pages rows;
sample_profile(sample_id, profiler_id) passes that snapshot to a bound native
profiler; sample_release(sample_id) releases it. Samples are owner-local and
capped at 16; they do not survive worker restart. These operations require exact
approval and sensitive-result consent. Wire requests cannot supply custom code,
source paths or registry imports.
Next Steps¶
Parallel block execution¶
ParallelBlockSampler(ParallelSamplingConfig(...)).sample(frame, SamplingConfig(...))
collects selected rows inside owned workers before returning a retained snapshot.
Block quotas add up to the requested sample size; a short last block receives its
proportional quota. Seed 0 is deterministic, result order follows block order,
and input columns are preserved. Use a stable input throughout count and reads.
execution_mode=ExecutionMode.PROCESS uses spawned processes with a serializable
Polars plan; guard the application entry point with if __name__ == "__main__":.
THREAD and ADAPTIVE use threads. Submitted work is bounded by
max_workers * (1 + prefetch_blocks). With enable_work_stealing=True, completed
work frees capacity immediately; false waits for each submitted window. This
uses a shared executor queue, so blocks_stolen is zero rather than an estimated
steal count. Priority scheduling sorts block priority before submission; equal
priorities preserve block order. Process execution does not inherit custom owner
method overrides.
Block failures propagate without sequential replay or partial-success results.
Deadlines reject late results and drain running work; they cannot interrupt a
running Polars query. Retained sample bytes, including full scans, must fit
memory_budget.available_memory_mb. This check happens after allocation and is
not a process-heap reservation. Monitoring may request garbage collection when
RSS exceeds the budget; query sorting and process serialization can use more
memory than the retained sample. Calls on one sampler are serialized.
An approved profiler factory can place
NativeSamplingBinding(strategy, SamplingConfig(...)) from
truthound.mcp.profiler_services in ProfilerRuntime.samplers. This invokes the
selected native strategy directly without fallback. sampling_sample retains
its concrete result for sample_get, sample_profile, and sample_release;
strategy, execution mode, and paths remain factory configuration.
Enterprise strategies and sample sketches¶
BlockSamplingStrategy reuses the block executor with one worker and exposes
BlockSamplingMetrics. MultiStageSamplingStrategy performs exact ranked
selection at each reduction stage (num_stages >= 1).
ColumnAwareSamplingStrategy adjusts its requested size for column complexity
without exceeding a positive SamplingConfig.max_rows. All three materialize
the returned snapshot and check output memory and late results.
ProgressiveSamplingStrategy evaluates increasing nested samples with the same
seed. Between stages it compares each column's null fraction, numeric mean and
population standard deviation, or nonnumeric unique fraction. The maximum
abs(new-old) / max(1, abs(old)) controls early stopping at
convergence_threshold; enable_progressive=False completes every distinct
stage size. ProgressiveSamplingMetrics exposes actual stages_completed,
converged, and convergence_delta. Full scans retain ordinary SamplingMetrics.
This convergence heuristic is not a statistical accuracy guarantee.
SketchBasedSampler returns SketchSamplingResult, including populated native
cardinality_sketches[column] (HyperLogLog) and
frequency_sketches[column] (CountMinSketch). These describe the retained sample,
not the original population. Each supports its native estimate/query API.
The MCP sampling port retains these native sketches under the sample ID.
sample_sketch_query(sample_id, column, sketch_value) returns estimated distinct
and frequency counts for an explicit JSON scalar (including null), without
echoing the query value. Release removes rows and sketches together; owner
restart expires both. Admission accounts for native sketch memory estimates
in addition to retained rows. Sketch construction
also checks the elapsed deadline; the retained-row budget does not include
the Python sketch tables or temporary query allocations.