Skip to content

BUG: MultiIndex.factorize dropped level dtypes and names - #68036

Open
HuzaifaChaudary wants to merge 2 commits into
pandas-dev:mainfrom
HuzaifaChaudary:fix/multiindex-factorize-dtypes
Open

BUG: MultiIndex.factorize dropped level dtypes and names#68036
HuzaifaChaudary wants to merge 2 commits into
pandas-dev:mainfrom
HuzaifaChaudary:fix/multiindex-factorize-dtypes

Conversation

@HuzaifaChaudary

@HuzaifaChaudary HuzaifaChaudary commented Sep 4, 2026

Copy link
Copy Markdown

closes #62337

MultiIndex.factorize built the uniques with self._constructor(uniques) , which goes back
through the tuples and works every level out again . so an Int32 level came back as
float64 , and the names were dropped on the way .

the codes are already right though , only the boxing was wrong . so this takes the rows back
out of the index instead , which leaves the levels alone :

first = np.unique(codes, return_index=True)[1]
uniques = self.take(first)

the uniques are now the same thing MultiIndex.unique gives you , same levels , same names ,
same dtypes . before this the two disagreed with each other .

@mroeschke on #62964 you said the real problem is algorithms.factorize being called on
self._values . that is true for the work it does , it still builds tuples . i checked
whether it is also true for the answer and it is not . 400 random multi indexes with nan ,
none and nat in them never gave back a -1 , and 4400 factorize calls comparing this against
the old path came out with the same codes and the same values every time . so the dtype loss
was only in the boxing .

a version built on self.codes would be quicker because it would skip the tuples . happy to
do that one instead if you would rather have it , it is a bigger change and i did not want to
guess at it .

one thing worth a look . test_factorize built its expected value with
MultiIndex.from_tuples , which is the same lossy path , so it was checking the old
behaviour against itself . it compares against unique now . four cases were red on that and
are green .

134504 tests pass across indexes , algos , base , groupby , reshape , frame , series ,
extension , dtypes , apply and indexing .


disclosure , as the automated contributions policy asks for . tool is claude code , model
claude opus 5 . i used it to work through the cause , to write the patch and the tests , and
to help word this description . i went over the result myself and every number quoted above
comes from a run on my own machine . sorry it was not in here when i opened it , i had not
read the policy at that point .

the uniques were rebuilt from the tuples with self._constructor(uniques), which
re-infers every level, so an Int32 level came back float64 and the names were gone.

the codes coming out of factorize are already right, only the boxing was wrong, so
the rows are taken back out of the index instead. the result now matches
MultiIndex.unique, which already kept both.

test_factorize built its expected value with MultiIndex.from_tuples, which is the
same lossy path, so it was asserting the old behaviour and is updated to compare
against unique.

Copilot AI 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.

🟢 Approval recommended

The change is narrowly scoped, matches documented intent (GH#62337), and is covered by updated and newly added regression tests plus a whatsnew entry.

Pull request overview

Fixes a MultiIndex.factorize bug where the returned uniques were reconstructed from tuple values, causing level extension dtypes (e.g. Int32) to be re-inferred as NumPy dtypes (e.g. float64) and dropping level names. The new approach derives uniques by selecting representative rows from the original MultiIndex, aligning factorize output with MultiIndex.unique.

Changes:

  • Build MultiIndex.factorize uniques via self.take(...) instead of rebuilding from tuples to preserve level dtypes and names.
  • Update factorize tests to avoid constructing expected MultiIndex uniques via the lossy tuple path, and add a focused regression test for GH#62337.
  • Document the fix in the v3.1.0 whatsnew notes.
File summaries
File Description
pandas/core/base.py Preserves MultiIndex level dtypes/names in factorize by taking rows from the original index rather than reconstructing from tuples.
pandas/tests/test_algos.py Aligns expected MultiIndex uniques with unique() and adds a regression test verifying dtype/name preservation.
doc/source/whatsnew/v3.1.0.rst Adds a release note describing the MultiIndex.factorize uniques dtype/name preservation fix.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions github-actions Bot added the Awaiting Review PR in the maintainers' court; exempt from staleness label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Awaiting Review PR in the maintainers' court; exempt from staleness

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: factorize does not preserve extension dtypes

2 participants