PAYATHON 2026

Pandas DataFrame 如何按类别 pivot,并让各列从首行排列?

数据小陈

可以用 groupby('catYM').cumcount() 为每个类别生成从 0 开始的组内序号,再用它作为 pivot 的行索引。这样每个类别都从第 0 行开始排列,结果的行数等于记录最多的类别的长度。

示例与处理代码

import pandas as pd

 df = pd.DataFrame({
    'catYM': [202601, 202601, 202602, 202603, 202603, 202501, 202501],
    'prod': [20, 26, 34, 45, 18, 19, 20]
})

创建 DataFrame 时,df 前不需要缩进。完整处理代码如下:

import pandas as pd

df = pd.DataFrame({
    'catYM': [202601, 202601, 202602, 202603, 202603, 202501, 202501],
    'prod': [20, 26, 34, 45, 18, 19, 20]
})

dfpivot = (
    df.assign(_row=df.groupby('catYM').cumcount())
      .pivot(index='_row', columns='catYM', values='prod')
      .rename_axis(index=None)
)

print(dfpivot)

结果为:

catYM  202501  202601  202602  202603
0        19.0    20.0    34.0    45.0
1        20.0    26.0     NaN    18.0

为什么需要组内序号

原来的代码没有指定 index:

df.pivot(columns='catYM', values='prod')

此时,pivot 使用原 DataFrame 的索引作为行索引。例如,类别 202501 的两个值位于原索引 5 和 6,转换后仍然放在第 5、6 行,其他位置就会出现 NaN。

cumcount() 按记录在每个组内的出现顺序编号:

    catYM  prod  _row
0  202601    20     0
1  202601    26     1
2  202602    34     0
3  202603    45     0
4  202603    18     1
5  202501    19     0
6  202501    20     1

使用 _row 作为新行索引后,每个类别的第一条记录落在第 0 行,第二条落在第 1 行。类别 202602 只有一条记录,所以第 1 行保留 NaN。

.rename_axis(index=None) 清除辅助索引名称 _row,使显示格式与预期一致。它不会改变数据,也不会删除列轴名称 catYM。

为什么 reset_index() 不能单独解决

reset_index(drop=True) 只会重新生成整个 DataFrame 的行索引,不会让每一列分别向上移动。

示例中的原索引已经是连续的 0 到 6,因此无论在原始 pivot 前还是后调用 reset_index(drop=True),都无法得到目标布局。要改变记录转换时使用的行坐标,需要用组内序号。

顺序和缺失值的处理

上述写法保留每个类别内部的原始记录顺序。如果需要按其他字段排序,应先对原 DataFrame 排序,再生成组内序号。

生成序号时,每条记录都会占据一个位置。若原始 prod 中已经存在 NaN,它也会保留对应位置,不会被自动删除。

示例中的整数在转换后显示为浮点数,因为结果包含 NaN。如果希望使用支持缺失值的整数类型,可以转换为 Pandas 的 Int64:

dfpivot = dfpivot.astype('Int64')

这种转换适用于 prod 的有效值均可表示为整数的情况;转换后缺失值显示为 <NA>。

备注:内容仅供参考。