[python]Dataframe对象的多层索引(MultiIndex)

203 阅读8分钟

pandas中的多层索引(MultiIndex)允许在多个层次上组织数据,使得我们可以处理更高维度的数据,同时保持二维DataFrame的形式。多层索引可以用于行索引(index)和列索引(columns)。

这里我们主要介绍多层索引在行索引上的操作,列索引的多层索引操作类似。

  1. 创建具有多层索引的DataFrame
  2. 索引和切片
  3. 重塑数据:stack和unstack
  4. 聚合操作

下面我们通过例子来详细说明。

pandas DataFrame的多层索引(MultiIndex)操作详解

多层索引(MultiIndex)允许在多个维度上组织数据,是处理高维数据的强大工具。以下通过多个示例详细说明其操作:

1. 创建多层索引DataFrame

(1) 从元组列表创建

import pandas as pd
import numpy as np

# 创建多层索引的行
index_tuples = [
    ('北京', '2023', '第一季度'),
    ('北京', '2023', '第二季度'),
    ('北京', '2024', '第一季度'),
    ('上海', '2023', '第一季度'),
    ('上海', '2023', '第二季度'),
    ('上海', '2024', '第一季度')
]

# 创建多层索引
multi_index = pd.MultiIndex.from_tuples(
    index_tuples,
    names=['城市', '年份', '季度']
)

# 创建DataFrame
data = {
    '销售额': [1000, 1200, 1100, 800, 900, 850],
    '利润': [200, 250, 220, 150, 180, 160],
    '成本': [800, 950, 880, 650, 720, 690]
}

df = pd.DataFrame(data, index=multi_index)
print("原始DataFrame:")
print(df)
print("\n索引信息:")
print(df.index)

(2) 使用product创建多层索引

# 创建多层索引的另一种方式
cities = ['北京', '上海']
years = [2023, 2024]
quarters = ['Q1', 'Q2', 'Q3', 'Q4']

# 使用笛卡尔积创建所有组合
multi_index = pd.MultiIndex.from_product(
    [cities, years, quarters],
    names=['城市', '年份', '季度']
)

# 创建随机数据
np.random.seed(42)
data = {
    '销售额': np.random.randint(500, 2000, len(multi_index)),
    '利润': np.random.randint(50, 500, len(multi_index))
}

df_product = pd.DataFrame(data, index=multi_index)
print("使用product创建的DataFrame:")
print(df_product.head(8))

(3) 从普通DataFrame创建多层索引

# 创建普通DataFrame
df_flat = pd.DataFrame({
    '城市': ['北京', '北京', '上海', '上海'] * 2,
    '年份': [2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024],
    '季度': ['Q1', 'Q2'] * 4,
    '销售额': [1000, 1200, 800, 900, 1100, 1300, 850, 950],
    '利润': [200, 250, 150, 180, 220, 270, 160, 190]
})

# 设置多层索引
df_multi = df_flat.set_index(['城市', '年份', '季度'])
print("设置多层索引后的DataFrame:")
print(df_multi)

2. 基本索引操作

(1) 选择特定层级的数据

# 选择北京的所有数据
print("北京的所有数据:")
print(df.xs('北京'))  # xs方法用于选择特定索引值

# 选择2023年的所有数据(需要指定层级)
print("\n2023年的所有数据:")
print(df.xs('2023', level='年份'))

# 选择第一季度的所有数据
print("\n第一季度的所有数据:")
print(df.xs('第一季度', level='季度'))

(2) 切片操作

# 使用loc进行切片
print("北京2023年的数据:")
print(df.loc['北京', '2023'])

print("\n所有城市2023年第一季度的数据:")
print(df.loc[(slice(None), '2023', '第一季度'), :])

# 更简洁的切片方式(推荐)
print("\n使用IndexSlice进行切片:")
idx = pd.IndexSlice
print(df.loc[idx['北京', :, '第一季度'], :])
print("\n北京和上海2023年Q1和Q2的数据:")
print(df.loc[idx[['北京', '上海'], '2023', ['第一季度', '第二季度']], :])

3. 数据选择和查询

(1) 多条件选择

# 选择销售额大于1000的数据
high_sales = df[df['销售额'] > 1000]
print("销售额大于1000的数据:")
print(high_sales)

# 选择特定城市和年份的数据
mask = (df.index.get_level_values('城市') == '北京') & \
       (df.index.get_level_values('年份') == '2023')
print("\n北京2023年的数据:")
print(df[mask])

(2) 使用query方法

# 重置索引以便使用query
df_reset = df.reset_index()
print("使用query查询数据:")
print(df_reset.query('城市 == "北京" and 年份 == "2023"'))

# 或者直接在多层索引上使用
print("\n查询销售额大于1000且利润大于200的数据:")
print(df[df['销售额'] > 1000].query('利润 > 200'))

4. 数据聚合和统计

(1) 按层级聚合

# 按城市聚合
city_summary = df.groupby(level='城市').agg({
    '销售额': ['sum', 'mean', 'std'],
    '利润': ['sum', 'mean']
})
print("按城市聚合:")
print(city_summary)

# 按城市和年份聚合
city_year_summary = df.groupby(level=['城市', '年份']).sum()
print("\n按城市和年份聚合:")
print(city_year_summary)

(2) 使用pivot_table

# 使用透视表
pivot_df = df_flat.pivot_table(
    values='销售额',
    index=['城市', '年份'],
    columns='季度',
    aggfunc='sum',
    fill_value=0
)
print("透视表结果:")
print(pivot_df)

5. 数据重塑:stack和unstack

# 创建示例数据
df_example = pd.DataFrame({
    'A': {'X': 1, 'Y': 3},
    'B': {'X': 2, 'Y': 4}
})

# stack:将列索引转换为行索引
stacked = df_example.stack()
print("stack操作结果:")
print(stacked)
print(f"\nstack后的索引: {stacked.index}")

# unstack:将行索引转换为列索引
unstacked = stacked.unstack()
print("\nunstack操作结果:")
print(unstacked)

# 多层索引的stack/unstack
print("\n多层索引的unstack:")
# 将季度从行索引移到列索引
df_unstacked = df.unstack('季度')
print(df_unstacked)

# 将城市从行索引移到列索引
print("\n将城市移到列索引:")
print(df.unstack('城市'))

6. 排序和重组

# 按索引排序
df_sorted = df.sort_index()
print("按索引排序:")
print(df_sorted)

# 按特定层级排序
df_sorted_city = df.sort_index(level='城市')
print("\n按城市排序:")
print(df_sorted_city)

# 按数值列排序
df_sorted_sales = df.sort_values('销售额', ascending=False)
print("\n按销售额降序排序:")
print(df_sorted_sales)

# 交换索引层级
df_swapped = df.swaplevel('城市', '季度')
print("\n交换城市和季度索引层级:")
print(df_swapped)

7. 多层索引的列操作

# 创建具有多层列索引的DataFrame
arrays = [
    ['销售额', '销售额', '利润', '利润'],
    ['实际', '目标', '实际', '目标']
]
columns = pd.MultiIndex.from_arrays(arrays, names=['指标', '类型'])

# 创建数据
data_multi_col = np.random.randint(100, 1000, (6, 4))
df_multi_col = pd.DataFrame(data_multi_col, index=multi_index[:6], columns=columns)
print("具有多层列索引的DataFrame:")
print(df_multi_col)

# 选择特定列
print("\n选择所有销售额列:")
print(df_multi_col['销售额'])

print("\n选择销售额的实际值:")
print(df_multi_col[('销售额', '实际')])

# 列索引的聚合
print("\n按指标聚合:")
print(df_multi_col.groupby(level='指标', axis=1).sum())

8. 实际应用示例:销售数据分析

def analyze_sales_data():
    """销售数据分析示例"""
    # 创建更复杂的数据
    np.random.seed(42)
    
    # 创建索引
    cities = ['北京', '上海', '广州']
    years = [2022, 2023, 2024]
    quarters = ['Q1', 'Q2', 'Q3', 'Q4']
    products = ['产品A', '产品B', '产品C']
    
    # 创建多层索引
    index = pd.MultiIndex.from_product(
        [cities, years, quarters, products],
        names=['城市', '年份', '季度', '产品']
    )
    
    # 创建数据
    data = {
        '销售额': np.random.randint(1000, 10000, len(index)),
        '成本': np.random.randint(500, 8000, len(index)),
        '销量': np.random.randint(10, 500, len(index))
    }
    
    df_full = pd.DataFrame(data, index=index)
    df_full['利润'] = df_full['销售额'] - df_full['成本']
    df_full['利润率'] = df_full['利润'] / df_full['销售额']
    
    print("完整数据集形状:", df_full.shape)
    print("\n前10行数据:")
    print(df_full.head(10))
    
    # 1. 分析各城市年度销售额
    print("\n各城市年度销售额:")
    city_year_sales = df_full.groupby(['城市', '年份'])['销售额'].sum().unstack('年份')
    print(city_year_sales)
    
    # 2. 分析各产品季度趋势
    print("\n各产品季度销售额趋势:")
    product_quarter_sales = df_full.groupby(['产品', '季度'])['销售额'].mean().unstack('产品')
    print(product_quarter_sales)
    
    # 3. 查找利润率最高的组合
    print("\n利润率最高的前10个组合:")
    top_profit = df_full.nlargest(10, '利润率')
    print(top_profit[['销售额', '成本', '利润', '利润率']])
    
    # 4. 创建数据透视表
    print("\n数据透视表:各城市各产品的平均利润率:")
    pivot = pd.pivot_table(
        df_full,
        values='利润率',
        index=['城市', '产品'],
        columns='年份',
        aggfunc='mean'
    )
    print(pivot)
    
    return df_full

# 运行示例
df_complete = analyze_sales_data()

9. 性能优化技巧

# 1. 使用sort_index提高查询性能
df_sorted = df.sort_index()
print("排序后的索引:", df_sorted.index.is_monotonic_increasing)

# 2. 使用categorical类型节省内存
df['城市'] = df['城市'].astype('category')
df['年份'] = df['年份'].astype('category')
print("\n内存使用情况:")
print(df.memory_usage(deep=True))

# 3. 避免在多层索引上使用循环
# 不好的做法
bad_results = []
for city in df.index.get_level_values('城市').unique():
    for year in df.index.get_level_values('年份').unique():
        # 循环操作
        
# 好的做法 - 使用groupby
good_results = df.groupby(['城市', '年份']).apply(lambda x: x.mean())

10. 常见问题和解决方案

# 问题1:重置索引
print("重置索引(保持所有层级):")
print(df.reset_index())

print("\n重置索引(删除某些层级):")
print(df.reset_index(level='季度'))

# 问题2:处理缺失的组合
# 使用reindex填充缺失的组合
full_index = pd.MultiIndex.from_product(
    [['北京', '上海'], ['2023', '2024'], ['第一季度', '第二季度', '第三季度']],
    names=['城市', '年份', '季度']
)
df_reindexed = df.reindex(full_index)
print("\n重新索引后的DataFrame(有NaN):")
print(df_reindexed)

# 问题3:扁平化多层索引列
df_flat_cols = df_multi_col.copy()
df_flat_cols.columns = ['_'.join(col).strip() for col in df_flat_cols.columns.values]
print("\n扁平化列名后的DataFrame:")
print(df_flat_cols.head())

总结

多层索引的主要操作要点:

  1. 创建:使用MultiIndex.from_tuplesfrom_productset_index
  2. 选择:使用xsloc配合sliceIndexSlice
  3. 聚合:使用groupby指定level参数
  4. 重塑:使用stackunstack转换行列
  5. 排序:使用sort_index按索引层级排序
  6. 优化:保持索引有序,使用分类数据类型

多层索引虽然学习曲线较陡,但一旦掌握,可以极大地提高处理复杂结构化数据的效率。它特别适合时间序列分析、面板数据分析和多维数据报表等场景。