Skip to content

Python Pandas: How to Concatenate MultiIndex into Single Index in Pandas

Learn how to flatten MultiIndex levels into a single descriptive string index in Pandas using efficient methods like map() with join(), handle non-string data types, and apply custom formatting for cleaner data representation.

Hierarchical (multi-level) indices in Pandas are powerful for representing structured data, but they can complicate display, export, and downstream processing. Flattening a MultiIndex into a single descriptive string index simplifies these tasks by combining the levels into readable labels like "2023_Q1" or "Sales_Revenue". This operation is common after groupby aggregations, pivot tables, or any operation that produces a MultiIndex.

In this guide, you will learn how to flatten both row and column MultiIndexes using different techniques, handle non-string data types, and choose the right approach for your formatting needs.

Using map() with join() for Fast Flattening

The most efficient method for MultiIndexes where all levels contain strings is to use .map() with a join function:

import pandas as pd

index = pd.MultiIndex.from_tuples([
    ('2023', 'Q1'),
    ('2023', 'Q2'),
    ('2024', 'Q1')
])
df = pd.DataFrame({'Sales': [100, 150, 200]}, index=index)

print("Before:")
print(df)
print()

df.index = df.index.map('_'.join)

print("After:")
print(df)

Output:

Before:
         Sales
2023 Q1    100
     Q2    150
2024 Q1    200

After:
         Sales
2023_Q1    100
2023_Q2    150
2024_Q1    200

The '_'.join function is called on each tuple in the MultiIndex, joining the level values with an underscore. You can use any separator string.

Different Separator Options

import pandas as pd

index = pd.MultiIndex.from_tuples([('2023', 'Q1'), ('2023', 'Q2')])
df = pd.DataFrame({'Value': [100, 200]}, index=index)

print(index.map(' - '.join))   # ['2023 - Q1', '2023 - Q2']
print(index.map('/'.join))     # ['2023/Q1', '2023/Q2']
print(index.map('.'.join))     # ['2023.Q1', '2023.Q2']

Output:

Index(['2023 - Q1', '2023 - Q2'], dtype='object')
Index(['2023/Q1', '2023/Q2'], dtype='object')
Index(['2023.Q1', '2023.Q2'], dtype='object')

Handling Non-String Data Types

When MultiIndex levels contain numbers or other non-string types, the join() method will raise a TypeError. You need to convert the values to strings first:

import pandas as pd

index = pd.MultiIndex.from_tuples([
    ('Category', 101),
    ('Category', 102),
    ('Category', 103)
])
df = pd.DataFrame({'Value': [10, 20, 30]}, index=index)

try:
    df.index = df.index.map('_'.join)
except TypeError as e:
    print(f"Error: {e}")

Output:

Error: sequence item 1: expected str instance, int found

The fix is to convert each element to a string before joining:

import pandas as pd

index = pd.MultiIndex.from_tuples([
    ('Category', 101),
    ('Category', 102),
    ('Category', 103)
])
df = pd.DataFrame({'Value': [10, 20, 30]}, index=index)

df.index = df.index.map(lambda x: '_'.join(map(str, x)))

print(df)

Output:

              Value
Category_101     10
Category_102     20
Category_103     30

Custom Formatting with f-strings

For more complex formatting where you need different treatment for each level, use a lambda with an f-string:

import pandas as pd

index = pd.MultiIndex.from_tuples([
    ('Alice', 'NYC'),
    ('Bob', 'LA'),
    ('Charlie', 'Chicago')
])
df = pd.DataFrame({'Score': [85, 92, 78]}, index=index)

df.index = df.index.map(lambda x: f"{x[0]} ({x[1]})")

print(df)

Output:

                   Score
Alice (NYC)           85
Bob (LA)              92
Charlie (Chicago)     78

Flattening Column MultiIndex

The same techniques work for hierarchical column names, which commonly appear after pivot table operations or multi-level aggregations:

import pandas as pd

columns = pd.MultiIndex.from_tuples([
    ('Sales', 'Q1'),
    ('Sales', 'Q2'),
    ('Profit', 'Q1')
])
df = pd.DataFrame([[100, 150, 20], [200, 250, 40]], columns=columns)

print("Before:")
print(df)
print()

df.columns = df.columns.map('_'.join)

print("After:")
print(df)

Output:

Before:
  Sales      Profit
     Q1   Q2     Q1
0   100  150     20
1   200  250     40

After:
   Sales_Q1  Sales_Q2  Profit_Q1
0       100       150         20
1       200       250         40

Flattened column names are much easier to reference in subsequent code (df['Sales_Q1'] instead of df[('Sales', 'Q1')]) and export cleanly to CSV files.

Selecting Specific Levels with get_level_values()

When your MultiIndex has three or more levels and you only want to combine some of them, use get_level_values() to extract individual levels:

import pandas as pd

index = pd.MultiIndex.from_tuples([
    ('2023', 'Jan', 'Week1'),
    ('2023', 'Jan', 'Week2'),
    ('2023', 'Feb', 'Week1')
])
df = pd.DataFrame({'Sales': [100, 120, 150]}, index=index)

level0 = df.index.get_level_values(0)
level1 = df.index.get_level_values(1)
df.index = level0 + '_' + level1

print(df)

Output:

          Sales
2023_Jan    100
2023_Jan    120
2023_Feb    150

This gives you precise control over which levels are included in the flattened index and which are discarded.

Practical Example: Flattening After GroupBy

A common scenario where MultiIndex flattening is needed is after a groupby aggregation with multiple levels:

import pandas as pd

df = pd.DataFrame({
    'Region': ['East', 'East', 'West', 'West'],
    'Product': ['A', 'B', 'A', 'B'],
    'Sales': [100, 150, 200, 250],
    'Profit': [10, 20, 30, 40]
})

grouped = df.groupby(['Region', 'Product']).sum()
print("After groupby (MultiIndex):")
print(grouped)
print()

grouped.index = grouped.index.map(lambda x: '_'.join(x))
print("After flattening:")
print(grouped)

Output:

After groupby (MultiIndex):
                Sales  Profit
Region Product               
East   A          100      10
       B          150      20
West   A          200      30
       B          250      40

After flattening:
        Sales  Profit
East_A    100      10
East_B    150      20
West_A    200      30
West_B    250      40

Quick Reference

Method Best For Handles Mixed Types
index.map('_'.join) Simple string joins No
index.map(lambda x: '_'.join(map(str, x))) Mixed data types Yes
f-string lambda Custom formatting Yes
get_level_values() Selecting specific levels Yes
  • Use index.map('_'.join) for fast flattening when all levels are strings.
  • For mixed data types, wrap with map(str, x) to convert all values to strings first.
  • Use f-string lambdas when you need custom formatting beyond simple concatenation.
  • All of these techniques apply equally to column MultiIndexes by operating on df.columns instead of df.index.
Navigation