BUG: MultiIndex.factorize dropped level dtypes and names - #68036
Open
HuzaifaChaudary wants to merge 2 commits into
Open
BUG: MultiIndex.factorize dropped level dtypes and names#68036HuzaifaChaudary wants to merge 2 commits into
HuzaifaChaudary wants to merge 2 commits into
Conversation
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.
There was a problem hiding this comment.
🟢 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.factorizeuniques viaself.take(...)instead of rebuilding from tuples to preserve level dtypes and names. - Update factorize tests to avoid constructing expected
MultiIndexuniques 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.
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.
closes #62337
MultiIndex.factorizebuilt the uniques withself._constructor(uniques), which goes backthrough the tuples and works every level out again . so an
Int32level came back asfloat64, 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 :
the uniques are now the same thing
MultiIndex.uniquegives 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.factorizebeing called onself._values. that is true for the work it does , it still builds tuples . i checkedwhether 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 againstthe 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.codeswould be quicker because it would skip the tuples . happy todo 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_factorizebuilt its expected value withMultiIndex.from_tuples, which is the same lossy path , so it was checking the oldbehaviour against itself . it compares against
uniquenow . four cases were red on that andare 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 .