Skip to content

Count distinct values for all columns of a numeric array at once in modality detection - #1255

Open
Innixma wants to merge 4 commits into
mainfrom
modality-detection-vectorized
Open

Count distinct values for all columns of a numeric array at once in modality detection#1255
Innixma wants to merge 4 commits into
mainfrom
modality-detection-vectorized

Conversation

@Innixma

@Innixma Innixma commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

detect_feature_modalities built a pd.Series and called nunique for every column. When the array is numeric or bool, which it is whenever the input has no object columns, only the distinct-value count decides a column's modality, so it is now counted for all columns at once: sort each column, count adjacent unequal pairs, count NaN once. -0.0 equals 0.0 and inf equals inf, as under nunique(dropna=False).

The per-column early exit is kept: the first 1024 rows are counted for every column, columns that already clear every threshold keep that count (it lands in the same bucket as the full count), and only the remaining columns are counted in full, so tall data does not pay for a full sort of high-cardinality columns. Object arrays go through the per-column path as before, and the numeric decision (constant / categorical / numerical) moved into one shared helper used by both paths.

On a 22k-column float frame with a few hundred rows this takes modality detection from about 0.4 s to 0.02 s and roughly halves fit. A randomized test compares the resulting schema with the per-column decisions on float, float32, int and bool arrays with NaN, signed zeros, infinities, all-missing and constant columns, row counts on both sides of the prefix, random thresholds and random declared-categorical indices; a second test pins the count against nunique(dropna=False).

Two smaller items in the same spirit:

  • _is_numeric_pandas_series walked every value of an object column in Python (or coerced the whole column) to decide whether it is numeric; a frame with a single non-numeric column arrives as one object array, so every numeric column paid for that. pd.api.types.infer_dtype now settles the common case in C: a kind whose non-missing values are all numbers (integer, floating, mixed-integer-float, boolean, decimal, empty) makes the column numeric; string and mixed kinds still take the existing path, since a spelled-out number counts. A parametrized test checks the shortcut against the value walk on object columns of floats, ints, bools, decimals, bytes, spelled numbers, words, mixed values, complex numbers and timestamps.
  • coerce_nullable_dtypes_to_numpy evaluated two pandas dtype predicates per column; they now run once per distinct dtype and the columns are selected by membership. Same columns, same order, with a test over eleven dtypes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ELdutHiUqkvynzEP7EnPsi

Innixma and others added 3 commits September 9, 2026 20:45
…odality detection

A numeric array needs no per-column parsing: only the distinct-value count
decides, so it is counted block-wise (sort, then adjacent unequal pairs, NaN
once) instead of building a Series and calling nunique per column. The
per-column early exit is mirrored: columns whose first 1024 rows already
clear every threshold keep that count. Object arrays are unchanged. A
randomized test checks the decisions against the per-column path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELdutHiUqkvynzEP7EnPsi
`_is_numeric_pandas_series` walked every value of an object column in Python
(or coerced the whole column) to decide whether it is numeric. A frame with a
single non-numeric column arrives as one object array, so every numeric column
paid for that. `pd.api.types.infer_dtype` answers the common case in C: a kind
whose non-missing values are all numbers settles the column as numeric; string
and mixed kinds still take the existing path, since a spelled-out number counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELdutHiUqkvynzEP7EnPsi
`coerce_nullable_dtypes_to_numpy` evaluated two pandas dtype predicates per
column; a wide frame has thousands of columns and a handful of dtypes, so the
predicates now run once per distinct dtype and the columns are selected by
membership. Same columns, same order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELdutHiUqkvynzEP7EnPsi
@CLAassistant

CLAassistant commented Sep 9, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…-column test

The two existing numeric checks disagree on complex values (`float()` rejects
them, `pd.to_numeric` accepts them), so the case's answer depends on the pandas
version; the shortcut never settles it, so it is not part of what the test checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELdutHiUqkvynzEP7EnPsi
@jmkuebler

Copy link
Copy Markdown
Contributor

@arthur-priorlabs since you worked quite a bit on preprocessing?

@arthur-priorlabs arthur-priorlabs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Comment on lines +250 to +261
def _numeric_n_unique_per_column(
X: np.ndarray, *, decided_at: int
) -> np.ndarray | None:
"""Distinct values per column of a numeric or bool array, NaN counted as a value.

`None` for anything else (an object array is parsed column by column). Mirrors
the per-column early exit: a column whose first `_EARLY_EXIT_PREFIX_ROWS` rows
already hold `decided_at` distinct values keeps that prefix count, which lands
in the same bucket as the full count; only the other columns are counted in
full.
"""
if not isinstance(X, np.ndarray) or X.ndim != 2 or X.dtype.kind not in "biuf":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we bothering to check the number of distinct values in a bool array?

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.

4 participants