BUG: read_csv dtype="category" now infers numeric and boolean categories - #64659
Conversation
d7d7f3f to
37f36ed
Compare
4cc29ce to
3c3a4ec
Compare
|
This pull request is stale because it has been open for thirty days with no activity. Please update and respond to this comment if you're still interested in working on this. |
3c3a4ec to
a27655c
Compare
cc26d98 to
7bce9e2
Compare
Previously, the c and python parsers produced string categories for numeric columns (e.g., categories=['1', '2'] with object dtype), while the pyarrow engine correctly inferred numeric types. Now all engines consistently infer proper types for categories. closes pandas-dev#56044 Co-Authored-By: Claude Opus 4.6 <[email protected]>
Use explicit dtype mapping instead of accessing .numpy_dtype on a union type that the type checkers can't narrow. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…type_backend is defined The block referenced `dtype_backend` inside `_normalize_timezone_dtypes`, which only takes `df`. Every pyarrow-backed IO path hit a NameError at runtime, breaking Type Checking, Doctests, Doc Build, and pyarrow IO tests. Moved the block into `_post_convert_dtypes` (after the astype), where `dtype_backend` is already a parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…read_csv dtype="category" - Merge categories when distinct strings convert to the same number (e.g. "1" and "1.0") instead of raising on duplicate categories - Skip numeric inference when thousands/decimal are set, since to_numeric is unaware of those options - Only downcast IntegerDtype categories that the pyarrow workaround introduced, not categories the user explicitly requested; restrict the downcast to read_csv (dtype is not None) and make it safe for duplicate column labels - Add tests for the above plus large-integer behavior - Move the whatsnew entry to the notable bug fixes section Co-Authored-By: Claude Fable 5 <[email protected]>
The c and python engines produced string categories for boolean columns under dtype="category" while the pyarrow engine inferred bool categories. All engines now infer boolean categories, mirroring each engine's non-categorical inference (numeric first, then boolean). GH#56044 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ory" - Defer category-dtype inference in low_memory mode until chunks are concatenated; per-chunk inference could produce differing category dtypes, breaking union_categoricals - Keep string categories when to_numeric turns "" into NaN (na_filter=False / keep_default_na=False) - Keep object dtype for empty inferred categories (all-NA columns) Co-Authored-By: Claude Fable 5 <[email protected]>
… docs - factor _from_converted_categories out of _from_inferred_categories so the low_memory deferred path runs to_numeric once on the unioned categories - update stale io.rst note obsoleted by GH#56044 inference - pin dtype_backend and QUOTE_NONNUMERIC category behavior in tests; scope the whatsnew claim to the default dtype_backend Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2148c92 to
6bce594
Compare
…tegory" With dtype_backend="pyarrow" the python engine's values arrive as ArrowDtype strings, whose kind is "U" rather than "O", so the guard in _maybe_convert_categories bailed and no inference was applied at all. Categories now infer to int64[pyarrow], matching a non-categorical read. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
# Conflicts: # doc/source/whatsnew/v3.1.0.rst # pandas/_libs/parsers.pyx
…tegories unsorted With the c and python engines, a CategoricalDtype specifying ordered=True but not categories silently produced an unordered result, while the pyarrow engine honored it. Separately, low_memory=True (the c engine default) left string categories in the order the file's buffer chunks happened to produce them, rather than sorted as every other path gives. Also restore the pre-GH#56044 defaults of Categorical._from_inferred_categories so callers other than the parsers are unaffected, and collapse _from_converted_categories to a single recode pass. Co-Authored-By: Claude Opus 5 <[email protected]>
…ed categories Follow-up fixes on top of the GH#56044 inference change: - with quoting=QUOTE_NONNUMERIC the deferred low-memory path now gives float64 categories like the eager path, casting before de-duplication so that values colliding at float64 precision merge into one category - an all-NA chunk no longer breaks union_categoricals when the other chunks inferred a non-object category dtype - to_numeric raises OverflowError for integers too large to represent as float64; such columns keep string categories rather than propagating it - under PANDAS_FUTURE_INFER_STRING=0 the pyarrow engine kept StringDtype categories for ordered categoricals, since CategoricalDtype.__eq__ ignores the categories' dtype when ordered and the astype was a no-op - read_fwf and read_excel coverage for the shared inference Co-Authored-By: Claude Opus 5 <[email protected]>
# Conflicts: # doc/source/whatsnew/v3.1.0.rst
…ed_categories The numeric/boolean inference now runs via _maybe_convert_categories and _from_converted_categories at the parser call sites, so the CSV-specific knobs (thousands/decimal, QUOTE_NONNUMERIC) stay in io/ and _from_inferred_categories keeps its previous signature. Also fixes signed-zero and huge-integer categories differing between low_memory=True and low_memory=False, and corrects the io.rst advice to convert thousands/decimal string categories with to_numeric, which silently read "1.000" as 1.0 rather than 1000. Co-Authored-By: Claude Opus 5 <[email protected]>
# Conflicts: # pandas/_libs/parsers.pyx
# Conflicts: # pandas/_libs/parsers.pyx
# Conflicts: # pandas/io/parsers/c_parser_wrapper.py
|
I hit the all-NA low-memory case addressed here when upgrading a legacy codebase from pandas 2.3.3 to 3.0.5 import io
import pandas as pd
N_COLS = 4097
empty_row = "," * (N_COLS - 1)
csv = "\n".join([empty_row] * 128 + [empty_row + "EQ"])
pd.read_csv(
io.StringIO(csv),
names=range(N_COLS),
dtype={N_COLS - 1: "category"},
)Results: The last comparison seems to pin the regression to the pandas-3 string inference change. Since this affects the default |
# Conflicts: # pandas/_libs/parsers.pyx
|
@mroeschke ok with calling this a bugfix? |
…gine The c tokenizer matches the default True/False spellings with strncasecmp, so a plain read infers bool for "tRuE" while dtype="category" kept string categories. Fold those spellings for the c engine only, after consulting true_values/false_values, which the tokenizer checks first. Also keep the arrow backing for inferred boolean categories, document the low_memory all-NA-chunk crash and the per-chunk inference that chunksize now does, and cover the ordered-categorical fix that reaches every reader built on an Arrow table. Co-Authored-By: Claude Opus 5 <[email protected]>
mroeschke
left a comment
There was a problem hiding this comment.
Yeah I'm OK calling this a "bug fix"
| Three further inconsistencies in the ``c`` and ``python`` engines are fixed. | ||
| ``low_memory=True`` (the default for the ``c`` engine) no longer leaves string | ||
| categories in the order the file's buffer chunks happened to produce them, and | ||
| no longer raises ``TypeError: dtype of categories must be the same`` when one | ||
| of a column's buffer chunks holds only missing values. A | ||
| :class:`CategoricalDtype` that specifies ``ordered=True`` without specifying | ||
| ``categories`` no longer discards ``ordered``: |
There was a problem hiding this comment.
These listed as bullet-points would be easier to read
| which already inferred numeric and boolean types for these columns without | ||
| ``dtype="category"``. |
There was a problem hiding this comment.
Does this specifically apply to read_excel or all 3 functions listed here
| :class:`CategoricalDtype` that specifies ``ordered=True`` without specifying | ||
| ``categories`` no longer discards ``ordered``: | ||
|
|
||
| .. ipython:: python |
There was a problem hiding this comment.
Would be good to add a "if you preferred the old behavior, do this" excerpt here too (if there is one)
Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
|
#62242 merged this morning and kinda went in the opposite direction. @jorisvandenbossche since you reviewed/merged that, do you feel strongly about this API: AFAICT that wasn't the main thrust of #62242, but I don't want to change this out from under you. |
The pyarrow engine parsed a categorical with no explicit categories as large_string, so the categories were the raw text from the file instead of the inferred values the other engines now give (GH#56044). This reverses one branch of pandas-devGH-62242, which aligned the pyarrow engine with the c and python engines as they behaved before this PR changed them. Its dtype=str and dtype=object handling, and every issue it cites, are unaffected. Co-Authored-By: Claude Opus 5 <[email protected]>
# Conflicts: # doc/source/whatsnew/v3.1.0.rst # pandas/io/parsers/c_parser_wrapper.py # pandas/tests/io/excel/test_readers.py # pandas/tests/io/parser/dtypes/test_categorical.py # pandas/tests/io/parser/test_read_fwf.py
Summary
read_csv(..., dtype="category")with thecandpythonengines produced string categories for numeric and boolean columns (e.g.categories=['1', '2']withobjectdtype), while thepyarrowengine inferred them. Now all three engines infer numeric and boolean categories, matching the types those columns get withoutdtype="category".Changes
Categorical._maybe_convert_categories/_from_converted_categories: new helpers that infer numeric and boolean categories whendtypedoes not provide them. Distinct strings that convert to the same value (e.g."1"and"1.0", or"True"and"TRUE") are merged and recoded, matching the pyarrow engine. Thecengine's tokenizer matches the defaultTrue/Falsespellings case-insensitively, so its category inference does too -- after consultingtrue_values/false_values, which the tokenizer checks first. The parsers call them;_from_inferred_categorieskeeps its previous signature.thousands/decimalare non-default, sinceto_numericis unaware of those options — such columns keep string categories rather than mis-parsing (e.g."1.000"withthousands="."becoming1.0). Underquoting=csv.QUOTE_NONNUMERICthe inference producesfloat64instead, matching the csv module's cast-every-unquoted-field-to-float semantics and the non-categorical read.PythonParser._cast_types: removed theensure_string_arrayhack that cast values back to strings "for consistency with the c-parser"._maybe_convert_string_to_object: withfuture.infer_stringdisabled, an ordered categorical column keptstrcategories where an unordered one gotobject, becauseCategoricalDtype.__eq__ignores the categories' dtype whenorderedand theastypeno-opped. This reaches every reader that builds a frame from an Arrow table (read_parquet,read_feather,read_orc, ...), not justread_csv, so it has its own whatsnew line and aread_feathertest._post_convert_dtypes: convertIntegerDtypecategories (from the BUG: read_csv loses precision whenengine='pyarrow'and dtype Int64 #56136 lossy-float64 workaround) back to numpy, but only forread_csvwith a userdtype, and never for categories the user explicitly requested._pyarrow_parse_type: aCategoricalDtypewith no explicitcategoriesis left to pyarrow's type inference instead of being parsed aslarge_string, so the pyarrow engine infers categories like the other two. See the note below.Three further inconsistencies in the
c/pythonengines are fixed along the way:CategoricalDtypespecifyingordered=Truewithoutcategoriesno longer discardsordered.low_memory=True(the c engine default) no longer leaves categories in buffer-chunk order.TypeError: dtype of categories must be the samewhen other chunks did infer categories.Notes for reviewers
dtype="category"read. Flagging since API: categories.dtype with pd.read_csv(..., dtype='category') #56044 has no recorded discussion.read_table,read_fwfandread_excel, which already inferred numeric/boolean types for these columns withoutdtype="category". Covered by tests.chunksize/iterator=True, so chunks of one column can now come back with different category dtypes; previously they were always strings and so always concat-able. The buffer chunkinglow_memory=Truedoes inside a single read adds no such divergence -- it infers once, after those chunks are combined. Noted in the whatsnew and pinned in tests.dtype="category"aslarge_stringso the categories are the raw file text, aligning pyarrow with thec/pythonbehavior as it stood at the time. This PR changes that behavior, so one branch of_pyarrow_parse_typeis reversed here and thedtype="category"clause is dropped from its whatsnew entry. Itsdtype=stranddtype=objecthandling is untouched, and all three issues it cites (index_col in read_csv and read_table ignores dtype argument #9435, BUG: pyarrow read_csv engine stripping leading zeros with dtype=str #57666, BUG:read_csv()withengine="pyarrow"converts numeric string even whendtype=stris specified #58260) are aboutdtype=str/index_col, not categoricals — its own tests for them still pass. Concretely: with the raw-text rule there is no way to ask for a categorical with inferred value type at read time, while string categories stay available viadtype="str"plus.astype("category"), or an explicitCategoricalDtype. One consequence worth calling out: zero-padded identifier columns (zip codes, account numbers) now lose their leading zeros underdtype="category", exactly as they already do without it. Noted in the whatsnew with the escape hatch.dtype_backendis only partly honored for the categories: thecengine always gives numpy-backed categories, as does thepythonengine withdtype_backend="numpy_nullable"(BUG: read_csv(dtype="category") ignores dtype_backend for the c and python engines #66382). The current behavior is pinned in tests.