Repository navigation
fix(dtypes): a complex value in an object column no longer crashes cleaning - #507
Merged
Merged
Conversation
…eaning
fd.clean(pd.DataFrame({"v": [complex(1, 2), "abc", "3"]})) raised TypeError
from the numeric finalizer's integrality check (nonnull % 1 == 0), and so did
a complex value among numeric strings or Python numbers (complex,
numpy.complex128, numpy.complex64).
Cause: once one cell is complex, pd.to_numeric(errors="coerce") returns a
complex buffer and never writes the text, bytes or bool cells into it -- they
come back as uninitialised memory, whatever the last freed buffer of that
size held. Reproduced on pandas 2.3.3 and 1.5.3: after freeing a buffer of
7+7j, to_numeric([1+2j, "abc", "not a number"]) returns
[(1+2j), (7+7j), (7+7j)]. So the column passed or failed numeric_threshold
depending on what had run earlier in the process, and when it passed the
finalizer raised. This was the unexplained order dependence behind a flaky
characterization test.
Every numeric target in steps/dtypes.py is a real dtype, so a complex value
is now an unparseable straggler like any other: when pandas returns a complex
result, complex cells are masked *before* re-parsing (the garbage cannot be
filtered out afterwards), and the existing threshold, coerced_cells and
type-contamination paths handle it. Columns without a complex value never
take the new path.
tests/test_exotic_values_in_numeric_columns.py: 22 tests, two of which seed
the freed buffer so the garbage is deterministic. With the converted
characterization tests, 17 fail without this change on both py3.12/pandas
2.3.3 and py3.9/pandas 1.5.3.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
FreshData benchmark report —
|
| fixture | n_rows | n_cols | p50 s | p95 s | peak MB | repair % | false-repair % | preserve % | trust | monotonic | export % |
|---|
Authored-code reduction (Metric 6)
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.
Bug
A single complex number in an object column crashes
fd.cleanon default settings:The same happens for a complex value among numeric strings, or among Python numbers in an object column (Python
complex,numpy.complex128,numpy.complex64). It reproduces on py3.12 / pandas 2.3.3 and py3.9 / pandas 1.5.3.Cause — pandas returns uninitialised memory
Once one cell is complex,
pd.to_numeric(s, errors="coerce")returns a complex buffer and never writes the text, bytes or bool cells into it. Those cells come back as whatever the last freed buffer of that size held:This reproduces on both pandas 2.3.3 and 1.5.3. So
"abc"could read as0j,7+7jor NaN, and the column passed or failednumeric_thresholddepending on what had run earlier in the process. When it passed, the finalizer's integrality check (nonnull % 1 == 0) raised. This is also the mechanism behind a characterization test that was previously order-dependent for reasons that couldn't be identified.Fix
Every numeric target in
steps/dtypes.pyis a real dtype, so a complex value is an unparseable straggler, like a word or aFractionin the same position. In_to_numeric_or_none, when pandas returns a complex result, complex cells are masked before re-parsing (the garbage cannot be filtered out afterwards). The existing paths then handle them: the threshold count, quarantine intocoerced_cells, and_warn_type_contamination. Columns without a complex value never take the new path.Tests
tests/test_exotic_values_in_numeric_columns.py, 22 tests. They assert what happens to the complex value (preserved, recorded incoerced_cells/coerced_rows, the warning text, parity withFraction), not just "no exception". Two of them seed the freed buffer so the garbage is deterministic.tests/test_dtypes_boolean_mutants.py: two characterization tests that pinned the crash are converted into tests that it no longer crashes.src/change, on both interpreters. 34 pass with it. Also 285 passed under each of 5pytest-randomlyseeds alongside the neighbouring dtypes and numeric tests.Surveyed, not changed here
complex128column of 4+ rows crashes in the outlier step (steps/outliers.py,s.quantile). That's a different mechanism, which I'll follow up on separately.safe_to_numeric's other callers still receive pandas' garbage for complex input. No wrong public-API result has been demonstrated yet.Compatibility impact
Only columns holding a complex value change, and each such column previously raised (or, depending on process state, came back unchanged). Columns without a complex value parse exactly as before.