Skip to content

Commit 3dc7ab6

Browse files
christopherzimmermanproost
authored andcommitted
ENH: MultiIndex.from_product infers names from inputs if not explicitly provided (pandas-dev#28417)
* Updated MultiIndex.from_product to infer names * Respect None names in from_arrays
1 parent bd4df41 commit 3dc7ab6

File tree

3 files changed

+65
-4
lines changed

3 files changed

+65
-4
lines changed

doc/source/whatsnew/v1.0.0.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Enhancements
2929
Other enhancements
3030
^^^^^^^^^^^^^^^^^^
3131

32+
- :meth:`MultiIndex.from_product` infers level names from inputs if not explicitly provided (:issue:`27292`)
3233
- :meth:`DataFrame.to_latex` now accepts ``caption`` and ``label`` arguments (:issue:`25436`)
3334
- The :ref:`integer dtype <integer_na>` with support for missing values can now be converted to
3435
``pyarrow`` (>= 0.15.0), which means that it is supported in writing to the Parquet file format
@@ -78,7 +79,7 @@ Other API changes
7879
^^^^^^^^^^^^^^^^^
7980

8081
- :meth:`pandas.api.types.infer_dtype` will now return "integer-na" for integer and ``np.nan`` mix (:issue:`27283`)
81-
-
82+
- :meth:`MultiIndex.from_arrays` will no longer infer names from arrays if ``names=None`` is explicitly provided (:issue:`27292`)
8283
-
8384

8485
.. _whatsnew_1000.deprecations:

pandas/core/indexes/multi.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
dict(klass="MultiIndex", target_klass="MultiIndex or list of tuples")
6161
)
6262

63+
_no_default_names = object()
64+
6365

6466
class MultiIndexUIntEngine(libindex.BaseMultiIndexCodesEngine, libindex.UInt64Engine):
6567
"""
@@ -371,7 +373,7 @@ def _verify_integrity(self, codes=None, levels=None):
371373
return new_codes
372374

373375
@classmethod
374-
def from_arrays(cls, arrays, sortorder=None, names=None):
376+
def from_arrays(cls, arrays, sortorder=None, names=_no_default_names):
375377
"""
376378
Convert arrays to MultiIndex.
377379
@@ -425,7 +427,7 @@ def from_arrays(cls, arrays, sortorder=None, names=None):
425427
raise ValueError("all arrays must be same length")
426428

427429
codes, levels = _factorize_from_iterables(arrays)
428-
if names is None:
430+
if names is _no_default_names:
429431
names = [getattr(arr, "name", None) for arr in arrays]
430432

431433
return MultiIndex(
@@ -496,7 +498,7 @@ def from_tuples(cls, tuples, sortorder=None, names=None):
496498
return MultiIndex.from_arrays(arrays, sortorder=sortorder, names=names)
497499

498500
@classmethod
499-
def from_product(cls, iterables, sortorder=None, names=None):
501+
def from_product(cls, iterables, sortorder=None, names=_no_default_names):
500502
"""
501503
Make a MultiIndex from the cartesian product of multiple iterables.
502504
@@ -510,6 +512,11 @@ def from_product(cls, iterables, sortorder=None, names=None):
510512
names : list / sequence of str, optional
511513
Names for the levels in the index.
512514
515+
.. versionchanged:: 1.0.0
516+
517+
If not explicitly provided, names will be inferred from the
518+
elements of iterables if an element has a name attribute
519+
513520
Returns
514521
-------
515522
index : MultiIndex
@@ -542,6 +549,9 @@ def from_product(cls, iterables, sortorder=None, names=None):
542549
iterables = list(iterables)
543550

544551
codes, levels = _factorize_from_iterables(iterables)
552+
if names is _no_default_names:
553+
names = [getattr(it, "name", None) for it in iterables]
554+
545555
codes = cartesian_product(codes)
546556
return MultiIndex(levels, codes, sortorder=sortorder, names=names)
547557

pandas/tests/indexes/multi/test_constructor.py

+50
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,19 @@ def test_from_arrays_different_lengths(idx1, idx2):
348348
MultiIndex.from_arrays([idx1, idx2])
349349

350350

351+
def test_from_arrays_respects_none_names():
352+
# GH27292
353+
a = pd.Series([1, 2, 3], name="foo")
354+
b = pd.Series(["a", "b", "c"], name="bar")
355+
356+
result = MultiIndex.from_arrays([a, b], names=None)
357+
expected = MultiIndex(
358+
levels=[[1, 2, 3], ["a", "b", "c"]], codes=[[0, 1, 2], [0, 1, 2]], names=None
359+
)
360+
361+
tm.assert_index_equal(result, expected)
362+
363+
351364
# ----------------------------------------------------------------------------
352365
# from_tuples
353366
# ----------------------------------------------------------------------------
@@ -539,6 +552,43 @@ def test_from_product_iterator():
539552
MultiIndex.from_product(0)
540553

541554

555+
@pytest.mark.parametrize(
556+
"a, b, expected_names",
557+
[
558+
(
559+
pd.Series([1, 2, 3], name="foo"),
560+
pd.Series(["a", "b"], name="bar"),
561+
["foo", "bar"],
562+
),
563+
(pd.Series([1, 2, 3], name="foo"), ["a", "b"], ["foo", None]),
564+
([1, 2, 3], ["a", "b"], None),
565+
],
566+
)
567+
def test_from_product_infer_names(a, b, expected_names):
568+
# GH27292
569+
result = MultiIndex.from_product([a, b])
570+
expected = MultiIndex(
571+
levels=[[1, 2, 3], ["a", "b"]],
572+
codes=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]],
573+
names=expected_names,
574+
)
575+
tm.assert_index_equal(result, expected)
576+
577+
578+
def test_from_product_respects_none_names():
579+
# GH27292
580+
a = pd.Series([1, 2, 3], name="foo")
581+
b = pd.Series(["a", "b"], name="bar")
582+
583+
result = MultiIndex.from_product([a, b], names=None)
584+
expected = MultiIndex(
585+
levels=[[1, 2, 3], ["a", "b"]],
586+
codes=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]],
587+
names=None,
588+
)
589+
tm.assert_index_equal(result, expected)
590+
591+
542592
def test_create_index_existing_name(idx):
543593

544594
# GH11193, when an existing index is passed, and a new name is not

0 commit comments

Comments
 (0)