Repository navigation
Conversation
The imputers' missing_values=np.nan (and any other non-finite float in fitted
state) was written as a bare NaN token, so every default spec file was rejected
by strict JSON parsers outside Python (JavaScript, Go, Rust, JSON schema
validators). The encoder passed floats and float arrays through unchanged, and
the spec was dumped with the default allow_nan=True.
Non-finite floats, as scalars, as float-array elements and as float elements
of object arrays (e.g. a NaN category of an un-imputed one-hot encoder), are
now encoded as a tagged value such as {"__float__": "nan"} and decoded back,
and specs are dumped with allow_nan=False, both by to_spec(path) and for
fingerprint_. Specs written by earlier versions, with bare NaN tokens, still
load and reproduce the same output. fingerprint_ changes for every fitted
preprocessor whose state holds a non-finite float (every one with an imputer).
The to_spec / from_spec docstrings, the serialize module docstring and the
reproducibility guide describe the strict format.
Fixes #80
This branch has not been deployed
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.
Fixes #80.
Every spec that
to_spec(path)wrote for an imputingPreprocessor, which is the default, contained bareNaNtokens for the imputers'missing_values=np.nan. RFC 8259 does not allow these, so strict JSON parsers (JavaScript'sJSON.parse, Go, Rust, Jackson with its default settings) rejected the file. Only Python's own reader accepted it.Root cause
_encodepassed Python and numpy floats, float arrays and object-array elements through unchanged, including NaN and ±inf.to_spec(path)andfingerprint_then dumped them with the defaultallow_nan=True, which writesNaN,Infinityand-Infinitytokens.Fix
_encode_float(pretab/compose/serialize.py) tags a non-finite float as{"__float__": "nan"}(or"inf","-inf")._encodeapplies it to Python floats andnp.floatingscalars, and, through the new_map_elementshelper, to the elements of a float array that holds a non-finite value._json_safe, which builds the informationalparamsblock, tags floats as well.main, object arrays are written with a rawtolist(), and a NaN category of an un-imputed one-hot encoder sits in such an array._encode_object_elementtags the non-finite float elements of object arrays and leaves every other element asmainwrites it._decodeturns the tag back into a float, and_decode_ndarrayrestores tagged elements of float and object arrays (_decode_float_element).Preprocessor.to_spec(path)andPreprocessor.fingerprint_dump withallow_nan=False.Behaviour changes
to_spec()dicts contain{"__float__": "nan"}(or"inf","-inf") where they hadNaN,Infinityor-Infinity, including in theparamsblock. The files parse with any RFC 8259 parser.fingerprint_changes for every fitted preprocessor whose state holds a non-finite float, which is every preprocessor with an imputer. Preprocessors without imputers keep their fingerprint.NaNtokens, and untagged floats decode as before. Checked with specs written bymainfor four configurations (default, one-hot with a NaN category,add_missing_indicator=Truewithbspline,ple; 5 to 9NaNtokens each): on this branch each loads, reproducestransformbyte for byte and re-saves as strict JSON. Its fingerprint is the new one, equal to that of the same fit made on this branch.Documentation
docs/core_concepts/reproducibility.md("Portable serialization"): the spec is strict JSON, non-finite floats are stored as a tagged value, and specs written by PreTab 1.0.0 with bareNaNtokens still load.pretab/compose/serialize.pymodule docstring: strict JSON and the tag.Preprocessor.to_specdocstring,pathparameter: written as strict UTF-8 JSON, with the tag format.Preprocessor.from_specdocstring,sourceparameter: specs written by PreTab 1.0.0 still load.Tests
test_default_spec_file_is_strict_json: a defaultPreprocessorfitted on data with missing values writes a file withoutNaN/Infinitythat parses with a strict parser and reloads with identical output and fingerprint. Fails onmain, passes here.test_nan_category_in_an_object_array_is_strict_json: one-hot encoding without categorical imputation keeps NaN in the object arraycategories_; the spec is strict JSON, contains the tag, and reloads with identical output and fingerprint. Fails onmain, passes here.test_non_finite_floats_round_trip: the array[[1, nan], [inf, -inf]]and the scalarsnanand-infsurvive a strict-JSON round trip, with the array's dtype. Fails onmain, passes here.test_specs_with_bare_nan_tokens_still_load: a spec whose tags are rewritten to bareNaN, as 1.0.0 wrote them, loads and reproduces the output. Guard test: passes on both by design.Reproduction
Output of the script from #80 on this branch:
The script from the issue, plus a count of the tagged values and the
node -e 'JSON.parse(...)'check that the issue describes.Checks
just test: 1442 passed, 59 skipped, 7 xfailed; coverage 93.69% (gate 90%).scripts/quickstart.pypasses.just docs: strict build succeeds.just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).Merge notes
Base:
main. The element-wise encoding of object arrays and the_spec_jsonhelper come with the PR for to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66 (fix(serialization): encode object arrays and builtin dtypes in specs #132), which is not onmain. Without them,allow_nan=Falsewould maketo_specraise for a NaN category in an object array. This branch therefore adds_encode_object_elementand_decode_float_elementand passesallow_nan=Falsedirectly to the two dumps.The PR for to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66 (fix(serialization): encode object arrays and builtin dtypes in specs #132) changes the same code. Both add the same
_map_elementshelper, which git merges once, and they conflict in the ndarray branch of_encode, in_decode_ndarray, into_specandfingerprint_inpretab/preprocessor.py, and at the end oftests/integration/test_serialization.py, where both append tests. Resolution:_encode: keep to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66's object branch,data = _map_elements(data, obj.ndim, _encode)(_encodethen tags non-finite floats itself), followed by this PR'selif obj.dtype.kind == "f" and not np.isfinite(obj).all(): data = _map_elements(data, obj.ndim, _encode_float)._decode_ndarray: keep to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66's element-by-element object branch (its_decodeknows the__float__tag), then decode float arrays withdata = _map_elements(data, len(payload["shape"]), _decode)whendtype.kind == "f"._encode_object_elementand_decode_float_element.pretab/preprocessor.py: keep to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66'sto_specandfingerprint_and passallow_nan=Falsein its_spec_json(json.dumps(spec, allow_nan=False, **kwargs)) instead of this PR's two direct dumps.Resolved this way, the full suite passes (1449 passed, 59 skipped, 7 xfailed), and the scripts from to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66 and Default to_spec() files contain bare NaN tokens and are not valid JSON for strict parsers #80 both show the fixed behaviour.
The PR for Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 (fix(transformers): record and check DataFrame feature names #135) changes
_representation_summaryand the..exceptionsimport inpretab/compose/serialize.py. Git merges it cleanly with this branch.This change was previously part of #86, which is closed in favour of one pull request per issue.