Skip to content

fix: repo maintenance update n40nk0 - #38

Merged
piotrlaczkowski merged 5 commits into
mainfrom
claude/repo-maintenance-update-n40nk0
Sep 3, 2026
Merged

piotrlaczkowski merged 5 commits into
mainfrom
claude/repo-maintenance-update-n40nk0

Conversation

@piotrlaczkowski

Copy link
Copy Markdown
Collaborator

No description provided.

…ecision

Three defects in the statistics pass, all silent, found by measuring what the
transforms produce against what they promise.

`path_data` pointing at a file discarded the filename and read every *.csv in
that directory. `path_data="data/train.csv"` -- the form every example in the
documentation uses, the README included -- therefore computed its statistics
over `test.csv` too. A project laid out that way had its test set folded into
the training statistics without a word. A file now means that file; a
directory still reads every CSV in it, a glob is passed through, and a path
that is none of those raises instead of reaching tf.data, which answers a
missing file with an empty dataset and statistics computed from nothing.

WelfordAccumulator accumulated in float32 and subtracted the running mean from
each raw value, so the deviations were computed at the magnitude of the values
themselves. A column around 1e8 with a spread of 1e3 came back with a variance
of MINUS 1.6e6 against a true 1.0e6; a spread of 1e-4 was wrong by four orders
of magnitude. Normalization divides by the root of that number, so the column
arrived at the model as a constant -- every ID, epoch timestamp, amount in
cents or offset sensor reading silently flattened. It now accumulates in
float64 and pools each batch from its own mean, which keeps the subtraction at
the scale of the spread: relative error near 1e-13 on the cases that broke,
and identical answers whatever the batch size.

The grouped time series statistics replaced each group with count copies of
its own mean before combining, which keeps only the variance between the group
means. Two groups of spread 25 whose means were close reported 0.73 against a
true 643. They are now pooled whole through a `merge` on the accumulator.
These numbers are written to features_stats.json, which people read; the model
never consumed them, so no output changes from this one.

813 passed, 10 skipped, 2 xfailed. New tests hold the accumulator to numpy
across five distributions including 1e8 and 1e-6, require the variance never
to be negative, pin batch-size independence, and check that naming one CSV
beside another reads only the one named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
min_value and max_value were read as the range the data was assumed to be
*in*, not the range to scale it *onto*: the transform computed
(x - min_value) / (max_value - min_value). With their defaults of 0 and 1 that
is the identity, so min-max scaling did nothing at all, and no setting could
target a range other than [0, 1] -- min_value=-1, max_value=1 gave x/2 + 0.5.

clip_values, documented as "whether to clip values to the specified range",
was the switch between those configured numbers and the data's own min and
max. It clipped nothing, and the output ran far outside the range it names.

Each feature's own range is now scaled onto [min_value, max_value], per
feature like robust-scale and quantile beside it, and clip_values clips the
result into that range. A constant column still comes back finite.

The existing test covered only clip_values=False -- the one branch that
half-worked -- and checked the range across the whole tensor, so pooling every
column into one scaling would have passed it too. It now covers the defaults,
three target ranges, per-feature independence and order preservation.

min-max is also one of the candidates transform_type="auto" chooses from, so a
column left on auto may pick it and change.

Found by comparing every named transform against its textbook definition
written out in numpy. The other nine match: log, sqrt, box-cox at four
lambdas, yeo-johnson at three, arcsinh, cube-root, logit, robust-scale, and
quantile, whose documented mapping of ranks onto [-1, 1] my first reference
had wrong, not the code. That comparison is now a test, with a check that a
transform added later cannot skip it.

830 passed, 10 skipped, 2 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Measured every feature option and inference path against what its name and its
documentation promise, using an independent reference -- numpy, pandas, scipy,
or a Keras layer adapted on the same data -- rather than against the shapes the
existing tests check. Fourteen defects, all silent: the models built, ran, and
returned numbers of the right shape.

feature_crosses did nothing at all in the default output mode. Every cross was
built and then dropped, because `_group_features_by_type` looks each processed
feature up in `features_specs` and a cross is configured with `feature_crosses`
rather than by declaring a column. Two configurations that used to fail later
now fail where they are written: crossing a float column, which built a model
that raised on its first batch, and naming a feature that is not declared,
which surfaced as a bare KeyError.

Text vocabularies were spelled differently from the way the layer looks them
up. `TextVectorization` lowercases and strips punctuation before splitting; the
statistics lowercased and split but kept the punctuation, so the vocabulary held
"product," and "it!" while the layer looked up "product" and "it". On ordinary
prose roughly half of every sentence fell into the out-of-vocabulary slot, with
the right output width and correct-looking counts. `max_tokens` below the
vocabulary the data holds could not build at all, and `ngrams` asked for pairs
the statistics never recorded, so every n-gram hit that same slot.

Three distribution transforms left their own domain. `log` returned NaN for any
negative value while every neighbour clamps; `logit` clipped into
[epsilon, 1 - epsilon] with an epsilon far below the float32 gap at 1.0, so the
upper bound rounded back to 1.0 and every value at or above 1 came out +inf; and
`arcsinh`, spelled out as log(x + sqrt(x*x + 1)), lost its whole negative tail
to cancellation -- -inf at -1e6.

predict() refused two of the three containers its docstring offers. A DataFrame
went straight to Keras and raised on the first categorical column; a dict of
flat lists, which is what InferenceFormatter itself produces, was missing the
column axis. A frame with a gap in it raised on the object dtype pandas gives a
mixed column, so the two documented ways in disagreed about missing values.

Dates could not read a timestamp, which is what a great many date columns hold,
and never read the format string they were given -- so a day-first format was
accepted, ignored, and met again at the first batch as an assertion buried in a
graph error. Formats are judged where they are written now, a time after the
date is dropped rather than rejected, and the assertion names the reason and
prints the value that failed.

transfo_placement="all_features" was the one configuration whose model came out
rank 3: a transformer block adds a sequence axis and that branch never removed
it, while the categorical placement one method above did.

The model advisor read skewness, kurtosis, min and max off statistics that
carried none of them, so it saw the neutral defaults and called every column
normal whatever its shape, and the rescaling factor it derived was always 1.
The accumulator now pools the third and fourth moments and the extremes
alongside the mean and variance, in float64, matching scipy to 1e-14 across
batch sizes and merges. Its generated snippet also carries the `path_data` line
without which pasting the advice raised.

A column float32 cannot hold -- Unix seconds are 128 apart near 1.6e9, so a
column spread over half a minute arrives as two or three distinct numbers --
now says so. The loss happens in the reader, before any statistic, so this is a
warning naming the column rather than a change in behaviour.

Three tests held a `batch_predict` generator and asserted it was not None, so
the model never ran in them; one of those also checked `add_season` with
`getattr` where it lives in kwargs, and another asserted a shape against a
generator object. They run the model now. A test of mine seeded from `hash()`,
which is salted per process, and compared a sample mean to a tolerance below
its own sampling error; it failed about one xdist run in three.

889 tests pass, all 77 runnable documentation blocks execute, and the six
`examples/` scripts run clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Three more defects, found by round-tripping every configuration through disk
and by feeding each time series transform a reference series.

A `tf_idf` text model saved and could not be loaded. Keras writes a `tf_idf`
vectorizer's IDF weights as layer variables and, on load, assigns them to a
layer that has no vocabulary yet and therefore no such variable, which surfaces
as "object of type 'bool' has no len()". This happens with `TextVectorization`
alone, with no KDP in the picture. Passing the vocabulary and the weights to
the constructor puts both in the config, where loading reads them; the layer
computes the same numbers either way, and it now matches a Keras layer adapted
on the same data both before and after a round trip.

A calendar time series feature could not be built at all. Its column holds
dates and everything it produces is derived from that string by
`CalendarFeatureLayer`, but the statistics pass fed the column to the numeric
accumulator and died inside a `tf.function` with "Cast string to double is not
supported". The layer had tests; the path from `PreprocessingModel` to it did
not. Calendar columns are skipped in the numeric statistics now, and with
`normalize=False` every component matches what pandas reads off the same
column.

`tsfresh_feature_config` summarized a window of one. A time series feature
hands over one column per row and `window_size` is clamped to the steps
available, so the mean, the minimum and the maximum were each the value itself
and the standard deviation was zero: four requested statistics came back as the
input column three times plus a column of zeros. The width matched what
`get_output_dim()` declared and every value was finite. It raises now, naming
the configs that widen the feature, the way the wavelet already did; paired
with any of them the statistics match `pandas.rolling` exactly.

Also measured and found correct, now covered: every one of 21 configurations
round-trips through `.keras` unchanged; statistics from one file, three files
in a directory and a glob agree to ten decimal places; batch sizes 16 and 4096
agree to 1e-14 across count, mean, variance, skewness, kurtosis, min and max; a
saved statistics file is reused untouched and `overwrite_stats=True` recomputes;
custom preprocessor pipelines apply their layers; and `auto_configure` gives
each column advice that matches its shape and generates a snippet that builds.

900 tests pass, all 77 runnable documentation blocks execute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Building the same configuration twice laid the columns of the concatenated
output out differently -- five distinct layouts in six builds of the same six
features. Features are preprocessed in parallel batches, and the concatenation
walked `processed_features`, a dict keyed in whichever order the workers
happened to finish.

Nothing raised, and each build's numbers were right for the layout it chose.
The damage lands on anyone who trains a model on one build of the preprocessor
and serves it from another: every feature is read as a different one, silently.
Reloading a saved `.keras` file was never affected -- the order is baked into
its graph -- so this only bit callers who rebuilt instead of reloading.

Columns follow the declaration order now: the features in the order
`features_specs` lists them, numeric block first, then the crosses in the order
`feature_crosses` gives them, then anything else by name.
`processed_features_dims` and `_concat_feature_names` report the same order, and
each numeric feature was measured to sit at its own index across repeated
builds.

Also measured and found correct: hashed categoricals honour `hash_bucket_size`,
respond to `salt` and repeat for the same salt, and `hash_with_embedding` gives
the requested embedding width over the same buckets; `FLOAT_DISCRETIZED` puts
each value in the bin `numpy.digitize` puts it in, one-hot; and both mixture
routings send each feature to its own slice, with the predefined assignments
placing them where the reported order says they are.

906 tests pass, all 77 runnable documentation blocks execute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
@piotrlaczkowski piotrlaczkowski changed the title Claude/repo maintenance update n40nk0 fix: repo maintenance update n40nk0 Sep 3, 2026
@piotrlaczkowski
piotrlaczkowski merged commit 5f79944 into main Sep 3, 2026
2 of 4 checks passed
github-actions Bot pushed a commit that referenced this pull request Sep 3, 2026
## 1.13.0 (2026-09-03)

* chore: save last release version for recovery [skip ci] ([302132d](302132d))
* fix: repo maintenance update n40nk0 (#38) ([5f79944](5f79944)), closes [#38](#38)
* fix(KDP): build the transformer block, and make the documented API real ([3193f3b](3193f3b))
* fix(KDP): compute the statistics from the right rows, at the right precision ([a76c574](a76c574))
* fix(KDP): deliver the embedding and Feature-MoE options that were discarded ([97cc77e](97cc77e))
* fix(KDP): dict-mode Feature MoE survives a save/load round trip ([ea95742](ea95742))
* fix(KDP): end generated API docs with one newline, not two ([7e7aa19](7e7aa19))
* fix(KDP): fill gaps at the ends of a series and name the inert options ([e22b654](e22b654))
* fix(KDP): honour preferred_distribution however it is spelled ([e4a5b7e](e4a5b7e))
* fix(KDP): let MissingValueHandlerLayer see NaN, and showcase the direct-use layers ([64d2abe](64d2abe))
* fix(KDP): make auto_configure work without features_specs, as documented ([30f86d1](30f86d1)), closes [hi#cardinality](https://github.com/hi/issues/cardinality)
* fix(KDP): make AutoLagSelectionLayer work with its default settings ([7c37096](7c37096))
* fix(KDP): make Feature MoE routing independent of the batch ([0f3ca9f](0f3ca9f))
* fix(KDP): make feature_moe_sparsity real and report expert assignments ([a84b82f](a84b82f))
* fix(KDP): make min-max scale onto the range it is given ([883ded3](883ded3))
* fix(KDP): make text output modes reachable and correct the date/text docs ([d9917f2](d9917f2))
* fix(KDP): make the documented examples run, and fix what running them exposed ([32f0e30](32f0e30))
* fix(KDP): make the saved model, the calendar feature and tsfresh work ([ac6bd85](ac6bd85))
* fix(KDP): one DistributionType, and stop publishing docs for deleted classes ([14e9bd5](14e9bd5))
* fix(KDP): pad the features add_feature_moe_to_model stacks ([67c84c3](67c84c3))
* fix(KDP): put the output columns in the order the features were declared ([7cb956f](7cb956f))
* fix(KDP): read the season from the month, not the cosine of the year ([d532c2c](d532c2c))
* fix(KDP): reject option values that silently disabled the feature they name ([820be2c](820be2c))
* fix(KDP): repair four feature-level options that could not work ([4f3e4ea](4f3e4ea))
* fix(KDP): repair generate_multi_step_forecast and cover the advisor branches ([3d29c43](3d29c43))
* fix(KDP): repair options that were accepted and never did what they said ([11085c1](11085c1))
* fix(KDP): repair silent data defects in seasons, time series options and serialization (#37) ([3e8616a](3e8616a)), closes [#37](#37)
* fix(KDP): repair three time series options and the widths they report ([1fb8a30](1fb8a30))
* fix(KDP): repair week_of_year and the clock components of CalendarFeatureLayer ([3554af0](3554af0))
* fix(KDP): report an empty categorical vocabulary and read every lag channel ([83eb49a](83eb49a))
* fix(KDP): route real features through the mixture, not arbitrary slices ([faf2c15](faf2c15))
* fix(KDP): stop predefined MoE routing silently erasing unassigned features ([73a9646](73a9646))
* fix(KDP): stop tests leaking a stats file into the working directory ([2e10d69](2e10d69))
* fix(KDP): stop TextPreprocessingLayer writing a config it cannot read back ([d702b02](d702b02))
* fix(KDP): stop the generated snippet writing options the feature ignores ([f82d96a](f82d96a))
* fix(KDP): stop two layer tests racing over one file name in the working directory ([585e366](585e366))
* test(KDP): cover value fidelity and serialization, and pin a MoE dict-mode defect ([20a557e](20a557e))
* test(KDP): hold layer options to their own evidence in the dead-option guard ([f381a6f](f381a6f))
* test(KDP): make the two assertion-free tests actually assert ([baf89af](baf89af))
* test(KDP): probe the MoE reachability check with several values, not one ([972b859](972b859))
* test(KDP): split the time series tests that mixed calendar with numeric configs ([e75a84e](e75a84e))
* docs(KDP): correct the numeric and passthrough pages, and run the examples in CI ([2fa3f84](2fa3f84))
* docs(KDP): publish the API reference and guard the docs against the code ([ec335a3](ec335a3))
* docs(KDP): publish the API reference for the mixture of experts and the advisor ([60a8997](60a8997))
* docs(KDP): rewrite the categorical and distribution-aware pages against the code ([99c0a9f](99c0a9f))
* docs(KDP): say what the time series section covers ([c2cf4cc](c2cf4cc))
* docs(KDP): state the output width tabular attention produces ([9ecc4c7](9ecc4c7))
* docs(KDP): stop four examples asking one column to be a date and a number ([02d1a09](02d1a09))
* docs(KDP): unpack what load_model returns, and guard the pattern ([0f1cb93](0f1cb93))
* feat(KDP): feature importances rank features, and doc examples get called ([a935051](a935051))
* style(KDP): format the new PerFeatureDense tests ([b4602e7](b4602e7))
* maint(KDP): keep the example scripts out of the repository root ([cb57b07](cb57b07))
* refactor(KDP): remove unreachable code and cover the auto transform ([2a38f25](2a38f25))
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://UnicoLab.github.io/keras-data-processor/pr-preview/pr-38/

Built to branch gh-pages at 2026-09-03 14:06 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants