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>。
备注:内容仅供参考。
