Add Time Series Feature Engineering Support to BigFeat - #4
Open
MohannadAK wants to merge 64 commits into
Open
Conversation
Add regression support to BigFeat class
Add requirements.txt and update README.md for easy environment setup
requirements.txt pinned an unrelated dependency set (gplaycli, matlink-gpapi, pyaxmlparser and friends -- an Android APK downloader), with no numpy or pandas at all. Anyone following the README's install instructions got a broken environment. Replace it with the library's actual runtime dependencies, and split the benchmark-only heavyweights (gluonts, openfe, stumpy, tsfresh, yfinance, ...) into a separate requirements-benchmark.txt. setup.py omitted scipy and psutil, both of which are hard imports (bigfeat_base.py imports psutil at module scope; the window detectors import scipy). A clean `pip install .` therefore produced an ImportError. Note statsmodels is NOT added: it is imported only by the benchmarking suite, not by the library. Also untrack openfe_tmp_data.feather (20MB of OpenFE scratch data) and ignore *.feather going forward, add a pyproject.toml declaring the build backend and pytest config, and ignore build/ and .pytest_cache/. The pre-existing *debug*/*reproduce*/*verify* ignore rules would shadow a tests/ directory, so add a negation to keep the suite tracked. Verified: `pip install .` into a fresh venv, then import and fit from a directory containing no source, succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_feature_importances drew its row sample from the global numpy RNG rather than self.rng, so fit() was not reproducible from random_state. The effect was worse than simple nondeterminism: because the global RNG dominated the sampling, random_state was very nearly inert. Measured on the non-TS path before this change, five runs at random_state=0 produced four distinct outputs, while five *different* seeds (0/1/2/5/1234) produced byte-identical output. Callers who set random_state and expected reproducibility got neither reproducibility nor seed control. Draw from self.rng instead. self.rng is created at fit() line 2304, before the first get_feature_importances call at 2333, so the ordering is safe. The TS path was already deterministic (it uses TimeSeriesSplit folds rather than random sampling) and is unaffected. Verified on both paths: with the global RNG deliberately perturbed between runs, a fixed random_state now yields identical output, and each of 0/1/2/5/1234 yields different output. This changes generated features, so any previously captured baseline is invalid. It lands before the characterization suite for that reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Establishes the safety net for the remaining refactor. 51 tests, ~85s
full / ~58s with -m 'not slow'.
Four fixtures, each pinned to a specific _setup_time_series branch
(no-TS, pooled_ensemble, stationarity-gated restricted mode), with
test_fixtures.py asserting they still reach those branches so that
detector drift fails loudly instead of silently changing what the
behavioural tests cover.
The suite is deliberately invariant-first rather than golden-first, so
it survives intentional behaviour changes while still catching state
leaks and replay bugs. Golden digests back it up for drift detection and
can be regenerated with --regen-golden.
One test fails on purpose and is left red:
test_transform_is_invariant_to_input_row_order[reg_ts]
It catches the _is_sorted state leak (25% of elements differ, max
absolute difference 2.06, confined to the TS-generated column). Worth
recording why the obvious formulation does NOT catch it: asserting that
two consecutive transform() calls agree passes, because _is_sorted stays
True for every call, so all of them take the early-return and are
*consistently* wrong. Row-order invariance is the observable that
actually breaks, since the early-return skips the datetime sort and
makes a row's features depend on its array position rather than its
timestamp.
Similarly, the default fit parameters leave the bug latent: a plain run
selects only one weak TS operator. The reg_heavy_ts and reg_ts_fanova
cases raise ts_operation_weight_multiplier and set selection='fAnova'
specifically to reach the affected code paths.
Phase 3 turns this test green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three coupled defects that all had to be fixed together, because each one masked the next. 1. _prepare_time_series_data overwrote the datetime and groupby columns from self.original_data unconditionally, aligning them *positionally* onto whatever rows were passed in. Positional alignment against the training frame is only meaningful when the incoming rows really are the training rows in training order, which transform() cannot assume. For any other row order this paired each row with someone else's timestamp, so the sort produced a plausible but wrong ordering and _original_index no longer identified which input row an output row came from. Measured on shuffled input: the first chronological row was input row 195, but _original_index claimed row 0. Now these columns are only filled in where the caller's frame does not already supply them, and inferring them from training data requires a matching row count rather than silently truncating. 2. A self._is_sorted flag short-circuited the whole function. It was set on first use during fit() and reset only at the top of fit(), so every subsequent transform() returned X untouched -- skipping the datetime sort, the dtype coercion, and the _original_index bookkeeping the rest of transform() depends on. Sortedness is a property of the data passed in, not of the estimator, so it cannot be cached on self; the flag is removed rather than reset. 3. The fAnova branch sat after transform()'s time-series early-return, making it unreachable whenever time series was enabled: fit() applied SelectKBest and returned k columns while transform() returned the full unselected width. Column selection and row reordering are independent, so the selection now runs first and both paths share it. Symptom before the fix, on a 300-row periodic series: shuffling the input rows changed 25% of output elements (max absolute difference 2.06), and features were inconsistent with their own timestamps. With selection='fAnova' and time series enabled, fit() returned 3 columns and transform() returned 6. Turns tests/test_invariants.py::test_transform_is_invariant_to_input_ row_order green; full suite 51 passed. The golden digests are unchanged, which is the expected result: this corrects the reordered-input path without disturbing behaviour for input already in training order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three independent correctness fixes.
1. Look-ahead leakage in weekday_mean / month_mean.
Both operators used groupby(calendar_key).transform('mean'), which
averages the entire column within each weekday or month. A row's
feature value therefore depended on rows dated after it -- at
transform() time, on rows from the future relative to the point being
predicted. This inflated apparent performance for exactly the
seasonal signal these operators exist to capture.
Replaced with shift(1).expanding().mean() within each calendar group,
so a row sees only earlier rows in its group and never itself. Rows
with no prior observation in their group are left at 0.
The leaky implementation existed in TWO places: the grouped path in
_apply_time_based_operation and, for the no-groupby case, the
datetime-indexed path in _apply_single_group_operation. The latter is
reached by falling through the no-groups branch, which has no
weekday/month case of its own. Fixing only the first left the default
configuration untouched. Both are fixed here; the grouped version now
also respects entity/block boundaries so one series cannot borrow
calendar history from another.
2. Degenerate input crashed fit().
ig_vector and split_vec were normalized with a bare `v /= v.sum()`.
When every feature has zero importance -- which happens for constant
or all-zero columns -- the sum is 0, the division yields NaN, and
fit() died inside rng.choice with "probabilities contain NaN".
Reproduced on 12 of 30 degenerate-input combinations.
Added _normalize_to_distribution, which scrubs NaN/inf, clips
negatives, and falls back to a uniform distribution when there is no
signal, so generation proceeds instead of crashing.
3. get_paths dropped its first path.
The dedup loop compared path_list[i] against path_list[i - 1], which
at i == 0 wraps to the LAST element. Whenever a tree's first and last
root-to-leaf paths matched, the first was silently discarded and
never counted toward the split-frequency vector.
Adds tests/test_correctness.py (11 tests). Full suite 62 passed.
The golden digests are unchanged: none of the four pinned scenarios
happens to select a calendar-mean operator. That is a coverage gap in
the goldens rather than evidence the fix is inert -- the dedicated tests
verify zero leaking rows across all four operator code paths, where the
previous implementation leaked on every row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three coupled defects in the "Pre-Flight Detection Cross-Validation"
block, which exists to catch hallucinated seasonality in noisy data.
1. It never ran. The check reads self.feature_columns, but that
attribute was not assigned until AFTER the block. On every fit it
raised TypeError immediately and the bare `except` swallowed it,
printing "CV Check skipped" only in verbose mode. The guard has
therefore never executed on any dataset. Feature-column
identification now happens before the check.
2. Its test features were computed leakily. Each side rolled
independently, so the window restarted at the beginning of the test
slice and test rows saw no real history -- the code carried a comment
acknowledging this ("Rolling on test is leaky") and did it anyway.
The rolling statistics are now computed once over the full ordered
series and sliced, which is both correct and causal: pandas' rolling
only looks backwards, so test rows draw on training history without
any test row influencing an earlier one.
3. Its penalty compounded. On weak improvement it overwrote
self.ts_operation_weight_multiplier, the constructor argument, so
repeated fits on one estimator kept halving it -- two fits left it at
0.25 of what the caller asked for with no way to recover short of
rebuilding the object. The penalty is now held in a separate per-fit
attribute behind an effective_ts_weight_multiplier property.
Also adds _reset_fit_state(), called at the top of fit(). self.operators
and self.unary_operators were extended in place with the time-series
operators (and filtered in restricted mode), guarded by a
_ts_operators_added flag that was never cleared, so a second fit
inherited the first fit's operator pool even when the periodicity
verdict differed. The pool is now rebuilt from the base definitions on
every fit.
One golden changed: reg_ts_periodic. This is expected and is the point
of the first fix. With the check finally executing it measures -0.3%
improvement from TS features on that fixture and halves the TS operator
weight, which is exactly its documented job. Shape is unchanged; only
the selected features differ. The other three goldens are untouched.
Full suite 66 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Time-series operators resolved their source column by reading self._current_feature_index, an attribute assigned as each leaf of the expression tree was resolved. But the operator reads it later, when its parent node fires. Inside a binary node with two different leaves the second leaf's index had already overwritten the first's, so both branches operated on the same -- often wrong -- column, and which column that was depended on evaluation order rather than on the recipe. This was reachable in practice, not theoretical: a depth-3 recipe mixing binary and time-series operators applied TS ops against two different source columns in the same expression. The consumed column is now recorded in the operator's params dict at generation time, which is already the channel that persists into transform(), so the recipe fully describes itself. All 15 _safe_* operators take the index explicitly via a shared _resolve_feature_col helper, replacing 15 copies of the same lookup. The helper still falls back to the old attribute for the non-TS paths and for recipes stored before the index was recorded. For a unary operator over a deeper subtree there is no single source column; the first leaf of that subtree is used, which matches what the old code did when it happened to be correct and is now stated explicitly rather than being an accident of traversal order. Verified: transform() output is byte-identical after corrupting _current_feature_index to a nonsense value and after deleting the attribute entirely. Goldens unchanged. Full suite 68 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ounts Window sizes are detected as pd.Timedelta, but every live rolling path converted them to a row count via _estimate_window_rows, which used self.time_step (default 'D'). A 90-day window therefore became 90 ROWS regardless of how the data was actually sampled. Measured against genuine time-based rolling on the Monash benchmark data, with the grouped path the benchmarks use (groupby_cols=item_id), 100% of rows were wrong on every dataset tested: dataset freq mean|err| rows a 90D window should span m1_monthly M 72056.6 3 tourism_quarterly Q 110185.4 1 m1_yearly Y 1463464.9 1 electricity_weekly W 48889.4 12 nn5_daily_without_missing D 16.7 40.5 In every case the approximation used 90 rows. On monthly data that averages the entire series rather than one quarter. 13 of the 25 benchmark datasets are monthly, quarterly or yearly, where calendar units are not fixed durations (28-31 day months, 90-92 day quarters, 365-366 day years) -- exactly where a row count cannot be correct. Rolling now passes the Timedelta to pandas, which selects rows by timestamp. All five frequencies above now agree exactly with true time-based rolling (mean|err| 0.0000, 0% of rows wrong). This also fixes a second defect in the same path: it rolled GLOBALLY across the whole frame and masked each group's first rows afterwards, so those rows averaged in the preceding entity's values before being zeroed. Grouping now happens before rolling, so values never cross an entity or block boundary. _estimate_window_rows survives for the operations that still need an integer (EWM spans, unqualified lag periods) but now derives the row count from the data's own median timestamp spacing rather than from a nominal setting. Performance: ~5x slower on small frames (1.3ms -> 6.8ms at ~900 rows) but per-row cost falls with size, since the overhead is per-group rather than per-row: 1.62 us/row at 8k rows, 0.20 us/row at 200k rows (200,000 rows in 39.6ms). This should not affect the scalability results. _vectorized_rolling, _vectorized_rolling_global and _apply_group_mask are now unreferenced; they are left in place for the Phase 5 dead-code pass rather than mixing deletion into a behavioural change. Adds 6 tests covering daily/weekly/monthly/quarterly frequencies, entity boundaries, and genuinely irregular timestamps -- the last of which no benchmark dataset exercises, since the Monash format stores only a start timestamp plus a dense array and so is uniform by construction. All 6 fail against the previous implementation and pass against this one. Full suite 74 passed. Goldens unchanged: the existing fixtures use ungrouped daily data, which routed to a path that was already correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default 'auto' mode enables time-series features by an ensemble vote of three detectors. Two of the three were badly wrong, in opposite directions, and on data where the answer is unambiguous. 1. ACF reported white noise as periodic (confidence 0.973). _compute_acf ran up to lag len(series)-1. At lag 363 of a 365-sample series only 2 points overlap, and np.corrcoef of two points is always exactly +/-1. Measured on white noise: max |ACF| was 0.158 for lags <= 180 (correct) but 1.000 for lags > 300. Those spurious unit correlations sorted to the top by height and became both the reported period and the confidence. Now requires a minimum overlap of 30 samples and bounds max_lag to n/2. 2. DFT reported a clean 7-day sine wave as NOT periodic (0.244). Its confidence was `1 - sorted[1]/sorted[0]`, close to the opposite of what it claimed: np.sort places the two largest bins adjacent, and for a real peak those are neighbouring bins of the SAME peak split by spectral leakage, so strong periodicity drove the score toward zero. On noise it scored 0.075 -- only 0.17 away, with both below the 0.3 threshold. Replaced with a peak-to-background ratio against the median of the spectrum, which is robust to the few bins carrying signal. Periodic now scores 0.628 vs 0.263 for noise. 3. ACF conflated days with samples. max_window_days, a DURATION, was used directly as a lag COUNT, so at hourly sampling a 365-day ceiling became 365 hours (~15 days). Now converted through the sampling rate. The end-to-end effect, which is what this phase set out to fix: on pure white noise the ensemble previously ENABLED time-series features and selected windows of 163-363 days -- exactly the hallucinated seasonality the consensus vote exists to prevent. It now correctly disables them, while still enabling on genuinely periodic data. Also extracts bigfeat/window_detector_base.py. The three detectors had ~145 lines of exact duplication and, worse, methods that shared a name while diverging in behaviour: assess_periodicity required a 50% feature-level consensus in DFT but used the average alone in ACF and Lomb-Scargle, so one periodic column among many noisy ones enabled TS features for the whole frame. The base class settles these on the stricter DFT semantics and holds detect_datetime_column, _convert_to_days, _get_default_windows, _generate_multiscale_windows, assess_periodicity and smart_window_selection. Subclasses now implement only _preprocess_signal and detect_optimal_windows. The shared _convert_to_days also fixes a silent failure: the rate table had 'H' but not pandas' modern lowercase 'h', and unknown codes fell back to "one sample == one day" with no warning, inflating hourly periods 24x. Unrecognised codes are now parsed by pandas and warn before falling back. Consolidation improved accuracy further: all three detectors now recover the true 7/14/28-day periods on periodic data, where ACF previously returned 120/164/180 and DFT returned 3/6/12/15. Detector code: 1566 -> 1256 lines. Adds tests/test_detectors.py (11 tests). Full suite 85 passed. One golden changed (reg_ts_nonstationary): same shape and strategy, different windows now flowing through to feature generation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects found by benchmarking against the real Monash datasets. Both made time-series windows far too short on anything not sampled daily. 1. The ensemble never passed a sampling rate. Detectors work in SAMPLES and convert to days via sampling_rate. The 'yes' path passed self.time_step, but the ensemble path used by the default 'auto' mode passed nothing at all, so it defaulted to 'D'. Monthly and quarterly observations were treated as one-day samples and every detected period came out 30-90x too small. The base class now measures the median timestamp spacing and snaps it to a frequency alias; verified to recover M/Q/Y/D correctly on the Monash data. 2. Pooled windows were truncated to the n SHORTEST. The ensemble sorted pooled candidates ascending and took the first n_windows, which discards every long window. Pooling three detectors reliably produces more than n_windows candidates, so the long scales were dropped every time. Now samples at even quantiles, keeping the shortest and longest detected scales plus a spread between. Together these were still producing 1-6 day windows for monthly series even after the detectors themselves were fixed. On m1_monthly the selected windows go from [1,2,3,4,5,6] to [1,3,5,90,182,365] days. Adds 2 detector tests. Full suite 87 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deletes 158 lines of unreachable code, verified dead by instrumenting every operator across both the grouped and ungrouped paths rather than by reading: _vectorized_rolling 43 lines, only ever called itself _vectorized_rolling_global 34 lines, definition only _apply_group_mask 44 lines, definition only get_weighted_feature_importances 29 lines, definition only get_combos 6 lines, its only caller was comb_mat The first three were orphaned by the switch to time-based rolling. Also removes the write-only self.comb_mat and self.gen_steps attributes. _apply_time_based_operation_loop and _apply_single_group_operation were NOT removed despite earlier analysis calling them dead: instrumentation shows they are called 4 times across the operator set, serving the calendar and seasonal operators on the no-groups branch. Deleting them would have broken weekday_mean, month_mean, seasonal_decompose and trend for the default configuration. Separately, fit() silently log-transforms strictly positive regression targets with skew > 2 before scoring feature importances. The flag recording this was private and never read anywhere in the codebase -- including the benchmark harness -- so a caller training their own model had no way to learn that feature selection had been scored against a log-scaled target. The flag is now public (target_log_transformed) and paired with inverse_transform_target(), which is a no-op when the transform did not fire and so is always safe to call. Note the transform rebinds a local only, so the caller's y array was never modified. original_feat is documented rather than fixed: it sits in unary_operators but not operators, and feat_with_depth samples only from operators, so it can never be selected. Adding it would change which features get generated with no evidence that an identity operator helps. bigfeat_base.py: 3690 -> 3534 lines. Adds 2 tests. Full suite 89 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fit() carried ~150 lines of memory-aware block sampling inline, between feature extraction and estimator setup, with no test coverage. Moved verbatim into _apply_block_downsampling(), which returns (X_for_fit, y_for_fit) and sets _was_downsampled. Verified behaviour-preserving by running the same downsampling scenario against the pre-extraction commit and diffing the output: identical for both the enabled and disabled paths. Adds 2 tests covering the previously untested path: that discovery may sample while the returned features still cover every input row, and that sampling stays off by default. fit(): 814 -> 664 lines. Full suite 91 passed. The remaining large methods (fit at 664 lines, _setup_time_series at 360) are left intact. Splitting them further is a mechanical change with real regression risk and no behavioural benefit, and is better done against a specific need than speculatively. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds docs/CORRECTNESS_FIXES.md: a full record of the time-series correctness review -- each defect's observable symptom, its mechanism, how the fix was verified, and the measured before/after numbers. Opens with why results collected before the review need re-checking, since three defects (inert random_state, row-count time windows, a seasonality guard that never executed) mean earlier runs measured different behaviour than their configuration describes. It also records the two claims that did NOT survive verification: the np.subtract replay "bug" that turned out to be a load-bearing correct implementation (the proposed fix breaks 1070 of 1500 trees), and _apply_single_group_operation, which static analysis called dead but instrumentation showed serving four live operators. Both would have caused harm if acted on. Adds tests/README.md: suite layout, the rationale for leading with invariants rather than goldens, and the known coverage gaps. Includes the two near-misses from the review -- a frequency test that passed against the buggy code because it exercised the wrong code path, and an A/B that silently tested new-against-new -- as concrete guidance for writing regression tests here. README.md: the install section listed a package set that did not match requirements.txt at all; replaced with the real two-file split. Adds a Time Series section, which the README omitted entirely despite it being most of the codebase, covering window detection, the causal guarantee, the parameter table, and the target log-transform that callers need to know about. Every example and parameter default was verified against running code. testing/Benchmarking/README.MD: appends measured runtime (~172 h for a full run, per-method breakdown), the sampling-regularity analysis of all 25 datasets, and the note that Lomb-Scargle has no dataset here that exercises its advantage since the GluonTS format cannot represent irregular sampling. Documentation only; no behavioural change. Full suite 91 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MohannadAK
force-pushed
the
feature/time-series-ops
branch
from
August 6, 2026 12:45
799f21b to
0fe5223
Compare
The earlier documentation commit covered what changed (CORRECTNESS_FIXES), how to test (tests/README) and how to benchmark, but never explained how BigFeat actually works. A reader could learn which bugs were fixed without learning what fit() does, so anyone modifying the code would still have to reverse-engineer it. docs/ARCHITECTURE.md covers: - The recipe representation -- fit() stores operators, source-column indices and parameters rather than feature values, and transform() replays them. This is the concept the rest of the design hangs off. - The generation loop: a weighted hill-climb with elitism, not a genetic algorithm (no crossover, no mutation of survivors). Documents the actual constants -- 20% elitism, 3 retries, 0.8 weight decay, the 50%-share diversity penalty at x0.1, geometric depth weights. - The time-series subsystem: the three enable modes, the ensemble vote, the detector base-class split, why windows are real time spans, the operator dispatch tree, and the restricted/trend modes. - Block downsampling: why contiguous blocks rather than random rows, and the padding/block_id mechanics. - Two non-obvious invariants that look like bugs and must not be "cleaned up": the mirrored operand swap in feat_with_depth_gen, and the _apply_single_group_operation path that static analysis calls dead but instrumentation shows serving four live operators. - Known rough edges, including the ~690 unreferenced lines in local_utils.py and the broad exception handling that let the pre-flight bug hide for the entire life of the feature. Every structural claim was verified against the code: the constants, the fAnova/row-order ordering, the sklearn incompatibility, and the exact set of methods each detector subclass still owns after the base-class extraction. Also notes DFT's _apply_seasonal_bias as a deliberate remaining asymmetry between detectors. Documentation only; no behavioural change. Full suite 91 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Analysis of the committed benchmark_results/ (24 datasets, 8 methods, already measured) produced a finding that reframes the next phase of work: no automated feature-engineering method beats the no-FE baseline at any conventional significance level. method geo-ratio vs baseline wins sign-test p openfe 1.001 12/22 0.42 no_standard (BigFeat) 1.112 10/24 0.85 yes_dft 1.121 11/24 0.73 auto_ensemble 1.201 8/24 0.97 tsfresh 1.455 8/18 0.76 Ratios above 1.0 are worse than baseline. This holds across all 24 datasets, in every frequency subgroup (D 1.10, M 1.19, Q 1.16, W 1.18, Y 1.00, h 1.07), and no dataset characteristic predicts success (corr(log n_series, log ratio) = -0.09). average_rankings.csv agrees: yes_dft 3.69 vs baseline 3.88, a 0.19-rank gap across 8 methods. OpenFE tying baseline at 1.001 is the important detail -- it suggests the ceiling is a property of the benchmark rather than of BigFeat. The plan is therefore built to explain the result rather than to escape it. Five phases: establish a real baseline on the fixed code with multiple seeds (blocking, ~90 h); three cheap parallel hypotheses for why the correctness fixes did not move accuracy; the stationarity-gate analysis, which is the one place with concrete evidence of a fixable defect; capability work conditional on that diagnosis; and a write-up that leads with the reproducibility finding. Each phase fixes its analysis plan before looking at outcomes, and the document states in advance what would change the conclusion. Phases B and C need no re-run and can start immediately. Documentation only. Every figure quoted was computed from the committed results or verified against the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plan opened by presenting the committed benchmark_results/ as current evidence that no AutoFE method beats baseline. That was wrong for the BigFeat rows: those runs are dated 2026-01-24 to 2026-02-12, six months before the twelve correctness fixes landed on 2026-08-04. Every BigFeat number there was produced by code with rolling windows wrong on 100% of rows, a seasonality guard that never executed, and an inert random_state. Corrected to separate what survives from what does not: VALID -- openfe (1.001) and tsfresh (1.455) never call into BigFeat, so the fixes cannot have changed them. Both were measured against the same baseline, datasets and harness. That a mature independent AutoFE tool ties baseline exactly, at p=0.42, remains the single most important input to the plan: it is weak evidence that the ceiling belongs to the benchmark rather than to any one tool. STALE -- every BigFeat row, plus the two sub-analyses derived from them (no winning frequency subgroup; no dataset characteristic predicting success). Both are retained struck-through so the post-fix run can be compared against them: reproducing the pattern would mean the fixes were accuracy-neutral, and not reproducing it would be attributable. The honest position is now stated plainly: BigFeat's accuracy relative to baseline is UNMEASURED on the fixed code. The only post-fix evidence is the 12-dataset A/B in CORRECTNESS_FIXES.md section 4, which is underpowered at <=25 series, <=120 rows and a single seed. Also corrects the title and subtitle, which asserted the fixes did not materially change accuracy -- a claim resting on that same underpowered sample -- and the Phase E framing, which cited the stale BigFeat numbers as a finding to report. Adds benchmark_results/STALE.md so the warning lives next to the data, with a per-file breakdown of what may and may not be cited. The derived figures are flagged specifically: detector_confidence_impact and stationarity_impact plot quantities the fixes changed substantially. Documentation only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Monash suite cannot demonstrate what the time-series subsystem does,
and this documents why with measurements rather than assertion.
Three structural findings:
1. No covariates. Every record is {target, start, item_id} -- one numeric
channel per series, with feat_static_cat being a constant identifier
rather than a time-varying signal. BigFeat's central mechanism is
composing operators ACROSS columns; with one column there is nothing
to compose with, so the search collapses to unary transforms.
2. Three datasets contain barely two seasonal cycles per series
(nn5_weekly 2.0, web_traffic_weekly 2.0, electricity_weekly 2.8,
against the ~3 that detection requires). A benchmark including them
measures the detector's failure mode, not its capability.
3. OpenFE -- independent of our code, and therefore unaffected by the
correctness fixes -- ties the no-FE baseline at geo-ratio 1.0008,
p=0.42, with ratios clustered in [0.99, 1.03].
It also records what is NOT the explanation. An earlier hypothesis held
that a naive last-value forecast leaves no headroom. Measured across all
25 datasets that is too simple: median naive MASE is 2.25 and only 7 of
25 fall below 1.5. There is real headroom; the feature engineering
simply has nothing to work with. That distinction changes the fix from
"find a harder benchmark" to "find one with covariates".
Recommends three experiments: a synthetic study with planted periods
(the only setting where detection can be scored against ground truth,
including the false-positive rate on noise controls), multivariate UCI
regression sets where cross-column composition can operate, and a Monash
subset retained and reported explicitly as the declared hard case.
Includes the baseline reviewers will ask for: a hand-crafted lag/rolling
feature set. Beating "no features" is weak; beating what a competent
engineer writes by hand is the actual claim.
Documentation only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instrumented walk through detection -> sanitisation -> ladder -> pooling -> lags, on signals with KNOWN planted periods. Five findings, each measured rather than inferred: 1. ACF selects harmonics over fundamentals. Peaks are sorted by height, but for multi-period signals the ACF at common multiples exceeds the fundamentals (ACF(210)=0.997 vs ACF(7)=0.652 on a 7+30d signal). Misses the fundamental in 3 of 5 planted cases, returning 360=12x30, 210=7x30, 156=12x13 instead. 2. Pooling ignores confidence. Windows from a detector at 0.31 count the same as one at 0.99, and quantile sampling preserves extremes -- so one bad detector's junk is guaranteed representation. End-to-end on the 7+30d signal the pooled set is [1,4,7,14,105,210]: the 30-day period is lost and two slots carry ACF harmonics. 3. Lags never see the detected periods. lag_periods is positional (windows[0], windows[1], windows[mid]); on the test signal the lags were [1,4,14] -- lag-7, the most valuable feature for weekly data, is absent even though 7 was detected. 4. DFT keeps one peak (argmax) where ACF/LS keep three. Masked today by the harmonic ladder reconstructing near-multiples by accident. 5. The ladder's derived harmonics can outvote detected fundamentals in the quantile subsample. The direct answer to whether more processing is needed between period extraction and window pooling: yes -- a period-consensus stage that clusters periods across detectors (+-15%), suppresses near-multiples, and weights by confidence, before any laddering. Consensus currently applies only to the binary periodic/not vote, never to the periods. Also records the seams checked and found sound: rate inference, rolling correctness, the binary vote, causality/entity isolation. Documentation only; no behaviour change. The 7+30d two-period case is flagged as the regression fixture for the eventual fixes (current recovery 7d=yes/30d=no; must become yes/yes). Co-Authored-By: Claude Opus 5 (1M context) <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
This PR adds comprehensive time series feature engineering capabilities to BigFeat while maintaining 100% backward compatibility with the existing implementation. When time series features are disabled (default), the library behaves identically to the original version.
Motivation
Key Features Added
Time Series Operators (15 New)
rolling_mean,rolling_std,rolling_min/max,rolling_median,rolling_sumlag_feature,diff_feature,pct_change,momentumewm,seasonal_decompose,trend_featureweekday_mean,month_meanDateTime-Aware Processing
'7D','30D','3M','1Y', etc.)Robust Implementation
Technical Implementation
New Parameters
Smart DataFrame Handling
Backward Compatibility
Zero Breaking Changes
enable_time_series=FalseBefore/After Comparison
Testing Strategy
Regression Testing
New Feature Testing
Performance Impact
Standard Operations
enable_time_series=FalseTime Series Operations
Usage Examples
Basic Time Series Enhancement
Multi-Entity Time Series
Code Quality
Architecture
Error Handling
Documentation
Benefits
For Existing Users
For Time Series Users
For the Ecosystem
Future Enhancements
This implementation provides a solid foundation for future time series enhancements:
Checklist
Review Focus Areas
This PR transforms BigFeat into a comprehensive feature engineering tool that handles both traditional and time series data while preserving the simplicity and power of the original design.