Skip to content

Use native factorization for object string categories - #476

Open
harsh21234i wants to merge 2 commits into
reflex-dev:mainfrom
harsh21234i:fix/native-object-factorizer-471
Open

Use native factorization for object string categories#476
harsh21234i wants to merge 2 commits into
reflex-dev:mainfrom
harsh21234i:fix/native-object-factorizer-471

Conversation

@harsh21234i

@harsh21234i harsh21234i commented Aug 7, 2026

Copy link
Copy Markdown

Summary

Fixes #471

  • Add a safe native factorization path for homogeneous object-backed
    string categories.

  • Preserve existing fallback behavior for mixed objects, null-like values,
    bytes, and custom string semantics.

  • Keep the existing low-cardinality probe as the fast-path gate.

  • Preserve category ordering, codes, and count semantics.

  • Add regression coverage proving only unique labels cross the Python
    label path.

Testing

  • tests/test_custom_ramps_and_palette.py: 90 passed
  • tests/test_components.py: 103 passed
  • Ruff check and format passed
  • git diff --check passed

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved categorical factorization for arrays containing only Python strings.
    • Ensured unique labels and factorization results match fixed-width string behavior.
    • Preserved distinct categories containing NUL characters.
    • Retained safe fallback handling for mixed values, missing values, custom string-like values, and unsuitable near-unique arrays.
  • Tests

    • Added coverage for string-only, mixed-value, non-string, NUL-containing, and near-unique category factorization scenarios.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fbf42e34-e36f-42b7-8923-a9072d77400d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6d73f and f1c9c6a.

📒 Files selected for processing (2)
  • python/xy/channels.py
  • tests/test_custom_ramps_and_palette.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/xy/channels.py

📝 Walkthrough

Walkthrough

Object arrays containing only exact Python strings are sampled and normalized to fixed-width Unicode before categorical factorization. The native factorizer uses the selected array. Mixed, missing, NUL-containing, and near-unique values retain the fallback path.

Changes

Categorical factorization

Layer / File(s) Summary
Native object-string factorization
python/xy/channels.py, tests/test_custom_ramps_and_palette.py
Bounded sampling and dtype-aware thresholds select eligible object-string arrays for native factorization. Validation excludes unsupported values and NUL-containing strings. Tests verify category order, codes, counts, label-call limits, parity, sampled rejection, and fallback behavior.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: alek99

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling native factorization for object string categories.
Linked Issues check ✅ Passed The changes implement the native object-string path, preserve fallback semantics, avoid label materialization, and add targeted regression coverage for issue #471.
Out of Scope Changes check ✅ Passed The code and test changes directly support native object-string factorization and the linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_custom_ramps_and_palette.py (1)

416-422: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert exact fallback labels and codes.

The current assertions only verify structural properties. len(categories) == len(set(categories)) checks a property already enforced by the implementation, and len(codes) == len(values) does not verify code assignments.

Assert the expected categories and codes for each parameter. Add a str subclass or custom string-like case to verify that the exact-type guard preserves custom label semantics.

Strengthen the fallback assertions
 `@pytest.mark.parametrize`(
-    "values",
+    ("values", "expected_categories", "expected_codes"),
     [
-        np.array(["a", None, "a"], dtype=object),
-        np.array(["a", 1, "a"], dtype=object),
-        np.array([b"a", b"b", b"a"], dtype=object),
+        (
+            np.array(["a", None, "a"], dtype=object),
+            ["(missing)", "a"],
+            [1, 0, 1],
+        ),
+        (
+            np.array(["a", 1, "a"], dtype=object),
+            ["1", "a"],
+            [1, 0, 1],
+        ),
+        (
+            np.array([b"a", b"b", b"a"], dtype=object),
+            ["a", "b"],
+            [0, 1, 0],
+        ),
     ],
 )
-def test_mixed_object_categories_keep_the_fallback(values):
+def test_mixed_object_categories_keep_the_fallback(
+    values, expected_categories, expected_codes
+):
     categories, codes, counts = channels._factorize_categories(values)

-    assert len(categories) == len(set(categories))
-    assert len(codes) == len(values)
+    assert categories == expected_categories
+    np.testing.assert_array_equal(codes, expected_codes)
     assert counts is None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_custom_ramps_and_palette.py` around lines 416 - 422, Strengthen
test_mixed_object_categories_keep_the_fallback by asserting the exact expected
categories and codes instead of only their lengths and uniqueness. Include a
custom str subclass or string-like value in the parametrized inputs, and verify
that _factorize_categories preserves its custom label semantics through the
exact-type guard while counts remains None.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/xy/channels.py`:
- Around line 410-412: Refactor the logic around _normalize_object_strings and
_use_native_fixed_factorizer to probe object-string eligibility and width via a
validation-only _object_string_width helper, without allocating the full Unicode
array. Pass the resulting normalized item size into the probe so its threshold
behavior is preserved, and only perform full normalization after the native path
is selected; keep fallback factorization from retaining an unnecessary Unicode
copy.
- Around line 386-390: Update the string normalization loop around the visible
type check to return None when any string contains a NUL codepoint, allowing the
existing Python fallback to preserve distinct values such as "a" and "a\x00".
Add a regression test covering these two values and verifying they remain
separate categories with correct labels, codes, and counts.

---

Nitpick comments:
In `@tests/test_custom_ramps_and_palette.py`:
- Around line 416-422: Strengthen test_mixed_object_categories_keep_the_fallback
by asserting the exact expected categories and codes instead of only their
lengths and uniqueness. Include a custom str subclass or string-like value in
the parametrized inputs, and verify that _factorize_categories preserves its
custom label semantics through the exact-type guard while counts remains None.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2650696e-d586-43e4-8f59-3c401dda3c84

📥 Commits

Reviewing files that changed from the base of the PR and between 37c3d91 and 5c6d73f.

📒 Files selected for processing (2)
  • python/xy/channels.py
  • tests/test_custom_ramps_and_palette.py

Comment thread python/xy/channels.py
Comment thread python/xy/channels.py Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread python/xy/channels.py
Comment thread python/xy/channels.py
Comment thread python/xy/channels.py Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="python/xy/channels.py">

<violation number="1" location="python/xy/channels.py:436">
P3: The two native-factorizer gates on an object column both call `_use_native_fixed_factorizer(arr, ...)` with the same object array, so the bounded probe and its `np.unique` distinct-count are recomputed identically on the same sample for the `sample_width` check and the full `width` check. Since the full-scan width is always >= the sample width, the second gate only re-evaluates the same probe with a different `normalized_itemsize` ratio. Consider computing the probe once (e.g., sampling inside the caller, or precomputing `distinct` and the probe length once) and only re-deriving the `near_unique` ratio for each width, which avoids duplicated work on the hot categorical path this code is explicitly tuned for.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread python/xy/channels.py
arr, normalized_itemsize=4 * max(sample_width, 1)
):
width = _object_string_width(arr)
if width is not None and _use_native_fixed_factorizer(

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.

P3: The two native-factorizer gates on an object column both call _use_native_fixed_factorizer(arr, ...) with the same object array, so the bounded probe and its np.unique distinct-count are recomputed identically on the same sample for the sample_width check and the full width check. Since the full-scan width is always >= the sample width, the second gate only re-evaluates the same probe with a different normalized_itemsize ratio. Consider computing the probe once (e.g., sampling inside the caller, or precomputing distinct and the probe length once) and only re-deriving the near_unique ratio for each width, which avoids duplicated work on the hot categorical path this code is explicitly tuned for.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/xy/channels.py, line 436:

<comment>The two native-factorizer gates on an object column both call `_use_native_fixed_factorizer(arr, ...)` with the same object array, so the bounded probe and its `np.unique` distinct-count are recomputed identically on the same sample for the `sample_width` check and the full `width` check. Since the full-scan width is always >= the sample width, the second gate only re-evaluates the same probe with a different `normalized_itemsize` ratio. Consider computing the probe once (e.g., sampling inside the caller, or precomputing `distinct` and the probe length once) and only re-deriving the `near_unique` ratio for each width, which avoids duplicated work on the hot categorical path this code is explicitly tuned for.</comment>

<file context>
@@ -404,11 +418,25 @@ def _factorize_categories(
+            arr, normalized_itemsize=4 * max(sample_width, 1)
+        ):
+            width = _object_string_width(arr)
+            if width is not None and _use_native_fixed_factorizer(
+                arr, normalized_itemsize=4 * max(width, 1)
+            ):
</file context>

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.

Use the native factorizer for low-cardinality object categorical channels

1 participant