Repository navigation
Single-apply feature kernels, numpy-exact parity and tsfresh fuzzing - #17
Merged
Merged
Conversation
Criterion benches stocks_minimal / stocks_efficient time
lazy_feature_df(...).collect() on test_data/all_stocks_5yr.csv (619k
rows, 505 groups, 5 value columns), loaded outside the timed loop.
python_benchmarks/stocks_bench.py runs the same extraction end to end
for hyperfine, from in-memory data or from scan_csv.
Baseline on an M1 Max:
criterion stocks_minimal 16.5 ms, stocks_efficient 267 ms
hyperfine minimal memory 681 ms, scan 186 ms
efficient memory 953 ms, scan 501 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
extract_features passed the LazyFrame to Rust, which pyo3-polars serialises together with any in-memory data in its plan (52 MB and ~0.5 s for the 619k-row stocks data). The non-streaming path now collects only the needed columns in Python (free for in-memory data; projection pushdown still applies to scan_* sources) and hands Rust a DataFrame, which crosses via the Arrow C interface without copying. streaming=True keeps the LazyFrame path. Adds the internal ExtractionSettings._input_columns and extract_features_df; the public API is unchanged. A new test asserts both paths give identical frames for every tier, with and without dynamic group-by. hyperfine, stocks data, M1 Max: minimal in-memory 681 -> 209 ms scan 210 -> 203 ms (A/B, noise) efficient in-memory 953 -> 514 ms scan 502 -> 480 ms (A/B, noise) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six counting-based reoccurrence features each hashed every value of every group separately. They are now one expression: the group is sorted once, runs of equal values are counted, and every enabled feature is computed from those runs (each keeping its own NaN rule). The individual public functions remain and delegate to it. The six columns now appear together, so the output column order changes (names and values do not); noted in the new CHANGELOG.md. Verified by comparing every output value by name for 3 configs x 3 tiers. criterion A/B, stocks data, M1 Max: stocks_efficient 264 -> 198 ms (-25%); stocks_minimal unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
number_peaks copied and rotated the whole series twice for every shift 1..n and every configured n (about 138 copies per group with the default n = 1, 3, 5, 10, 50). It now computes each position's support (how many neighbours it beats on both sides, capped at the largest n, stopping at the first neighbour that is not smaller) in one pass and counts peaks for all n from it. The public number_peaks delegates to the new number_peaks_multi; output columns, order and values are unchanged. Adds a randomised comparison against tsfresh (200 series with ties, NaN and ±inf, n = 1, 3, 5, 10, 50), skipped when tsfresh is not installed. criterion A/B, stocks data, M1 Max: stocks_efficient 202 -> 178 ms (-16%); stocks_minimal unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aggregate_on_chunks copied every chunk (chunk.to_owned()) before aggregating it, and skip_nan_sample_var collected each chunk's non-NaN values into a new array just to call var(). Chunks are now passed as views, and the variance runs Welford's algorithm with exactly the operations of ndarray's var over the filtered iterator, so results stay bit-identical (verified by exact output snapshots for 3 configs x 3 tiers). The median aggregator still collects one Vec per chunk to sort. criterion A/B, stocks data, M1 Max: stocks_efficient 169 -> 144 ms (-14%); stocks_minimal unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the merge commit of PR #16; no content changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add f64_values, which hands an extractor the group's values as one contiguous Float64 chunk; ArrayView1 over it replaces the per-group to_ndarray copy (and the 2-D to 1-D conversion in symmetry_looking). Output is byte-identical on the snapshot configs; the speed difference is within criterion noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndarray Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rray Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use f64_values and an ArrayView1 over the group's values instead of the per-group to_ndarray copy, as in statistics. sample_entropy does not drop nulls, so nulls are filled with NaN as to_ndarray did (the result stays NaN), and the two unwraps on the conversion are gone. Output is byte-identical on the snapshot configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benches/tools holds the scripts used for the performance work: an output snapshot for bit-identical checks, and hot-spot and before/after summaries of samply profiles (macOS and Linux idle/copy/alloc frame names). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux, perf samples only running threads, so idle time shows up as rayon spinning (crossbeam epoch/deque) rather than sleeping; count it as idle. glibc has no symbols, so its allocator internals appear as fun_<addr> under malloc/free; classify a sample by its innermost named frame. Lower the documented sampling rate to 500 Hz, which samply keeps up with on an 80-thread machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
About 10,000 stations with about 10,000 values each, in random order: long, interleaved series, where per-value cost and gathering each group dominate (the stocks bench has short series that are already sorted by key). The data is generated by python_benchmarks/1brc and not in git, so the bench skips itself when test_data/measurements.txt is missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The single-apply rework changes column order, so add a compare mode that matches columns by name and reports every value that is not bit-identical, and snapshot a dynamic (time-window) group-by too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every apply makes Polars collect each group's values into a list; on unsorted data (1BRC) that was half of extraction time. Add kernels: a feature is a Kernel computed from a per-group GroupContext, which shares mean, variance, min and max (computed once, with the same code as before). One apply per value column runs every kernel and returns the group's features as a list, which lazy_feature_df unpacks into named columns after the group-by. Move the statistics features to kernels (median, quantile and length stay Polars expressions for now). Column order changes: kernel features now come after the remaining expressions. Values are bit-identical on the snapshot configs; verified on the server (fmt, clippy, cargo test, rustdoc, pytest 354 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lazy list.get per feature made every call plan and optimise a second query, about 4 ms of the 24 ms of stocks Minimal. Split the packed lists directly from their offsets and flat values into named columns. stocks Minimal 24.5 -> 21.7 ms, Efficient 88.8 -> 84.5 ms (server). Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
count_above/below_mean, count_above/below, longest_strike_above/below_mean, number_crossing_m, range_count and number_peaks become kernels; features with parameters compute all parameter values in one kernel. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
first/last_location_of_maximum/minimum and index_mass_quantile become kernels. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mean_absolute_change, absolute_sum_of_changes, cid_ce and mean_change become kernels; the consecutive differences are computed once per group in the context. cid_ce keeps its column names (normalize_t/_f). Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
has_duplicate_max/min become kernels, and ReoccurrenceFeatures is itself the kernel for the six counting-based features (one sort per group, as before); each feature's documentation moves to its field. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
linear_trend and agg_linear_trend become kernels; all agg_linear_trend configurations share one kernel. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The context now records whether the group had nulls: sample_entropy treats nulls as NaN, which always gives NaN, so it returns NaN for such groups exactly as before. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
autocorrelation, c3 and time_reversal_asymmetry_statistic become kernels. c3 and time_reversal_asymmetry_statistic index the lagged values directly instead of copying and rotating the series twice per lag, multiplying in the same order (bit-identical); roll is no longer needed. Snapshot identical by name; verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l groups Median and quantile become kernels sharing one sorted copy per group, using Polars' linear-interpolation formula (linear_quantile). Two behaviour changes, each with a regression test: - A group without non-null values gave null for median and quantile but NaN for every other feature; now NaN, as tsfresh (which sees NaN). - For groups with nulls, Polars counted the nulls into the quantile's interpolation index, losing the last bits; the result now equals np.quantile of the non-null values exactly. Every other value is bit-identical on the snapshot configs. With this, every feature except length runs in the single kernel apply per value column; the unused NaN struct helpers are removed. Verified on the server (fmt, clippy, cargo test, rustdoc, pytest 356 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full sort per group made stocks Minimal (where the median is the only order statistic) 26.4 ms; select_nth, as Polars' quantile_slice, brings it to 22.7 ms. Same neighbours, so bit-identical (snapshot identical to the previous commit). Quantiles keep the shared sorted copy. Also document the new column order on aggregators. Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New column order, the median/quantile changes for all-null groups and groups with nulls, and the measured speed-ups (and the stocks Minimal slowdown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ndarray_stats checks every comparison for NaN through partial_cmp and an error path, about 9 ms of CPU each on stocks Minimal. One pass for both with exactly its semantics (NaN if any value is NaN, first value wins on ties), also for absolute_maximum without its temporary array. Snapshot identical to the previous commit; verified on the server. Wall time of stocks Minimal is unchanged (23.5 ms, within noise): the arithmetic is not what limits it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The variance used ndarray's Welford algorithm and the mean its unrolled sum; both differed from tsfresh's np.mean / np.var in the last bits (variance exact in 118 of 408 test series, mean in 266). Add numpy_sum, numpy 2.x's pairwise summation, and compute the mean and the two-pass population variance with it: 408 of 408 exact. Every feature built on the context's mean or variance follows. This also fixes threshold features when a value sits exactly on the threshold: ratio_beyond_r_sigma of [1, 3, 3, -2, 7, 7, 0, 1] with r=1.5 gave 0.375 and is now 0, as tsfresh. As a side effect the variance no longer goes through software FMA (mul_add without the fma target feature). Regression tests compare mean/variance/std with numpy bit for bit (3 of 5 failed before) and add the ratio_beyond_r_sigma case (0.375 before). Verified on the server (fmt, clippy, cargo test, rustdoc, pytest 362). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Differential fuzzing with tsfresh as the specification: Hypothesis generates series (ties, NaN, ±inf, signed zeros, long series for numpy's pairwise summation) and each feature of the parity config is compared with tsfresh's calculator, called as tsfresh's extract_features calls it (numpy array, or pandas Series where the calculator asks for one). numpy-based features must match bit for bit; pandas/scipy/BLAS-based families use a 1e-9 tolerance; documented deviations and tsfx-only columns are excluded. Opt-in: marker fuzz, excluded by default (addopts), run with pytest -m fuzz; Hypothesis profiles default (200 examples per feature) and ci (10). hypothesis joins the dev dependencies. The first run finds 11 features that differ; they are fixed in the following commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer. tsfresh takes the np.mean of the last n values of np.sort(np.abs(x)) and returns NaN unless len(x) > n: - tsfx returned a value for exactly n values ([0.0] with n=1 gave 0.0); - tsfx dropped NaN (it sorted with OrderedFloat), while np.sort puts NaN last, so it is the largest absolute value and the result is NaN. Sort ascending with NaN last and take numpy's mean of the last n values. Regression tests: three generated tsfresh vectors (all failed before), and test_mean_n_absolute_max now expects NaN for 7 values with n=7, as tsfresh. ordered-float is no longer needed. Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer: sum_values, absolute_sum_of_changes, mean_absolute_change, root_mean_square, autocorrelation, c3 and time_reversal_asymmetry_statistic summed straight through (ndarray sum, mean or dot), while tsfresh's np.sum / np.mean sum pairwise, so results differed in the last bits. Use numpy_sum with exactly tsfresh's terms. The context no longer needs the shared differences. Regression tests: the fuzzer's shrunk counterexamples with tsfresh's values, compared exactly (all 7 failed before). Snapshot: only last-bit changes (at most 3e-15 relative). Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer: the median and quantile used Polars' interpolation formula, while tsfresh uses np.median (the np.mean of the middle values) and np.quantile (numpy's _lerp, b - (b - a) * (1 - t) for t >= 0.5), so results differed in the last bits. numpy_median (still a selection, O(n)) and numpy_quantile copy numpy exactly, including that an infinite neighbour at an exact index gives NaN in np.quantile. Regression tests: the fuzzer's shrunk counterexamples and the infinity case with tsfresh's values, compared exactly (3 failed before). Snapshot: last-bit changes only. Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates) tsfresh's extract_features passes agg_linear_trend a numpy array, so the chunk aggregates are numpy's: var with ddof=0, and a NaN makes mean, min, max and var NaN. tsfx followed tsfresh's unit tests instead, which pass a pandas Series (var with ddof=1, NaN skipped), so tsfresh users got different values. Found by the tsfresh fuzzer. Aggregate chunks with numpy semantics (numpy_mean, population_var, extremes); the median aggregator, a tsfx addition, follows np.median. A group with chunk_size or fewer values gives NaN, as tsfresh. The pandas helpers (skip_nan_*, aggregate_on_chunks) are gone. Regression tests: the agg_linear_trend parity vectors regenerated with a numpy array, as extract_features calls it (23 cases failed before). Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer: quantile([inf], 0.1) gave inf, tsfresh NaN. At or beyond the last index np.quantile uses the last value for both neighbours but computes the weight from the replacement index -1, so it evaluates b - (b - a) * (1 - t) with t = h + 1: the last value when finite, NaN when infinite. numpy_quantile now does the same instead of returning the last value directly. Regression tests: [inf] at q=0.1 and [1, inf] at q=1.0 (both failed before), and a finite case. Verified on the server; the full fuzz run then passes quantile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer: agg_linear_trend's var slope for [0, 0, 1, 0, 0, 24468, 0] was -0.1111111044883728, tsfresh -0.1111111119389534 (7e-9 relative). The one-pass sum(i * y) - n * mean_i * mean_y cancels when the values span many orders of magnitude; linregress centres the data first. calculate_sequential_ols now uses centred sums with numpy summation (used by linear_trend and agg_linear_trend). Regression test: that case against tsfresh with a 1e-9 relative tolerance (failed before). Snapshot: changes up to 5e-13 relative. Verified on the server; the full fuzz run (200 examples per feature) now passes all 49 features. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the normal tests, install the pinned tsfresh/numpy/pandas/scipy versions the bit-exact parity is checked against and run the fuzz tests with the Hypothesis ci profile (10 examples per feature, about 30 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ip ci] The parity tests copy input/expected pairs from tsfresh's test suite; MIT asks for the copyright and permission notice with copies of substantial portions. Add the full licence text next to the test file and point to it from its header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the tsfresh fuzzer (in a later random run): sum_of_reoccurring_values of [0, 1, 2, 3, 131072, 131072, 393216, 393216, 1.5, 3.0127e-06, 3.0127e-06] was 524288.0000030126, tsfresh 524288.0000030127. tsfresh takes np.sum(counts * unique) over every distinct value (sorted, 0 for values that occur once), pairwise; tsfx summed only the recurring values, one after another. sum_of_reoccurring_data_points has the same structure. Both now build the same terms in np.unique's order and use numpy_sum; the documented deviation (a single NaN or ±inf is left out) stays. Regression tests: that series for both features against tsfresh, exact (both failed before). Snapshot unchanged; full fuzz and three extra seeds for both features pass. Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HYPOTHESIS_PROFILE=thorough runs 5000 examples per feature, for hunting rare cases in a few features (pytest -m fuzz -k <feature>). Used to check index_mass_quantile and symmetry_looking, whose comparisons could flip on a last-bit summation difference: no counterexample in 5000 examples each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by a targeted search with series that hit the thresholds exactly in real arithmetic (a block followed by permuted copies): the comparison then depends on the last bit. - index_mass_quantile divided the cumulative sum by ndarray's sum of |x|; tsfresh divides by np.sum (pairwise). 7538 of 20000 such series differed, e.g. [0.7, 1.7, 0.57, 0.4, 0.57, -1.7, -0.7, 0.4] at q=0.5 gave 0.5, tsfresh 0.625. The total now uses numpy_sum. - symmetry_looking compared |mean - median| from its own mean and median; tsfresh uses np.mean and np.median. 39 of 200000 differed. It now uses the context's numpy mean and numpy_median. After the fix: 0 mismatches in 200000 series each. The old median_mut and the ndarray-stats, noisy_float and num dependencies are no longer needed. Regression tests: the shortest counterexample of each, against tsfresh (both failed before). Snapshot unchanged; full fuzz passes. Verified on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quarter of the generated series are now a block of decimals followed by sign-flipped permutations of it, so cumulative shares of |x| and the ratios of mean, median and range hit thresholds exactly in real arithmetic, and only floating-point rounding decides comparisons such as index_mass_quantile's and symmetry_looking's. The random series almost never did, which is why the fuzzer missed the bugs fixed in the previous commit. Full run passes all 49 features. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes extraction substantially faster and brings every feature to bit-for-bit parity with tsfresh's
extract_features, checked by a new differential fuzzer that runs in CI.lengthis now computed in a singleapplyper value column (aKernelper feature over a shared per-group context), instead of oneapplyper feature (~60 for Efficient). Earlier commits remove the plan serialisation for in-memory data and several per-feature copies.lengthfirst, then all features of each value column in turn, in tier order. Column names are unchanged. If you select columns by position (e.g.df.to_numpy()into a trained model), select by name or retrain.agg_linear_trendfollows tsfresh'sextract_features, which passes the calculator a numpy array. Thevaraggregator is now the population variance (ddof = 0, wasddof = 1), and a NaN in a chunk makes the result NaN instead of being skipped. tsfx had followed tsfresh's unit tests, which pass a pandas Series. Themedianaggregator (a tsfx addition) followsnp.median.mean_n_absolute_maxis NaN unless there are more thannvalues, and NaN if the series contains NaN (as tsfresh).median/quantileof a group without non-null values is NaN (was null), like every other feature.mean,variance,standard_deviationand everything built on them,sum_values,absolute_sum_of_changes,mean_absolute_change,root_mean_square,autocorrelation,c3,time_reversal_asymmetry_statistic,index_mass_quantile,symmetry_looking,sum_of_reoccurring_values,sum_of_reoccurring_data_points,median,quantile,linear_trend,agg_linear_trend. Threshold features can change where a value sits exactly on the threshold:ratio_beyond_r_sigma([1, 3, 3, -2, 7, 7, 0, 1], r=1.5)is now 0, as tsfresh (was 0.375).Every change has a regression test that failed before the fix. See
CHANGELOG.md.Performance
Criterion, 2× Xeon Gold 6230 (80 threads), against the state after the in-place-read refactor (baseline
views), measured at the final commite6b5c72:The parity fixes are speed-neutral (within noise of the measurement before them). The stocks Minimal slowdown is accepted. Polars' lazy group-by alone costs ~12.6 ms there (computing the groups ~4.4 ms), and on short, already-sorted groups merging
applys saves nothing to offset the per-applyoverhead.Earlier commits on this branch, measured on an M1 Max (hyperfine, Python end to end): in-memory Minimal 681 → 183 ms and Efficient 953 → 323 ms, by handing in-memory data to Rust without serialising the query plan; plus faster reoccurrence,
number_peaksandagg_linear_trend.Testing
pytest: 384 passed.python_tests/test_fuzz_tsfresh.py, differential fuzzing of every feature against tsfresh with Hypothesis. Series include ones built to hit thresholds exactly (a block followed by permuted copies), where only the last bit decides a comparison. numpy-based features must match bit for bit; pandas/scipy/BLAS-based families use a 1e-9 tolerance; documented deviations are excluded. Opt-in (pytest -m fuzz, 200 examples per feature;HYPOTHESIS_PROFILE=thoroughruns 5000 for targeted hunts); CI runs a 10-example pass with pinned tsfresh 0.21.2 / numpy 2.4.4 / pandas 3.0.2 / scipy 1.17.1 (49 passed).cargo test,cargo clippy --all-targets -- -D warnings, rustdoc with-D warnings,cargo fmt --check.Also in this PR
benches/: a stocks criterion bench, a 1BRC bench on the first 100M rows (skips itself without the data), andbenches/tools/(output snapshot and comparison, samply hot-spot and before/after summaries).hypothesisjoins the dev dependencies;ordered-float,ndarray-stats,noisy_floatandnumare no longer used.python_tests/TSFRESH_LICENSE: tsfresh's MIT licence for the test vectors copied from its test suite.No version bump in this PR (still 0.3.3); the behaviour changes warrant a minor bump at release.
🤖 Generated with Claude Code