Repository navigation
fix: repo maintenance update n40nk0 - #38
Merged
Merged
Conversation
…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
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))
|
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.
No description provided.