形状变换#
宽表和长表不是两种数据,而是同一份数据的两种组织方式。画热力图、输入机器学习模型时希望是宽表,每个类别占一列;做分组统计、画时间序列时又希望是长表,类别名单独占一列。Polars 为此准备了一组互逆的方法。
这一章依次看 pivot 与 unpivot、unstack、to_dummies:前两个在长短之间来回转换,unstack 按固定步长把值展开成多列,to_dummies 把类别展开成 0 与 1 的列。
形状变换看起来只是把值挪个位置,实际上决定了后续能做什么运算:宽表适合按列比较,长表适合分组和绘图。选错形状,往往要多写很多中间步骤。
import polars as pl
from helper.jupyter import row
透视表#
透视表把行和列重新组织,可以理解成把长表变成宽表。pivot() 走这个方向,unpivot() 把宽表退回长表。两者并不是简单的转置:转置只交换行列位置,pivot() 还要把某一列的值拆成新的列名,并对重复的格子做聚合。
也正因为有聚合这一步,pivot() 需要三个角色:哪一列的值成为新列名(on)、哪一列作为行键(index)、哪一列填充单元格(values)。同一组 index 与列名出现多个值时,必须由 aggregate_function 决定留哪一个。
pivot#
pivot() 把指定列的值变成新的列名,用另一列作行键,剩下的列作为单元格内容。下面的例子用 name 作列标签、index 作行标签、value 作单元格的值:A、B、C 三个取值变成三列,每个 index 占一行。
同一行同一列出现多个值时,用 aggregate_function 指定的方式合并,这里是 first,也就是只保留第一个。从结果可以看到行数不变,只是排成了新的形状;如果某个格子没有值,就填入 null。
df = pl.DataFrame(
dict(
index=[0, 1, 2, 0, 1, 2, 0, 1, 2, 1],
name=["A", "A", "A", "B", "B", "B", "C", "C", "C", "A"],
value=[10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
score=[1, 3, 5, 4, 3, 2, 1, 2, 3, 4],
)
)
# 用 name 做列、index 做行,把 value 摆进去(重复时取第一个)
df1 = df.pivot("name", index="index", values="value", aggregate_function="first")
row(df, df1)
shape: (10, 4)
|
shape: (3, 4)
|
aggregate_function 决定怎么合并重复的格子,常用的有三种。
sum:求和,适合同一格有多次记录、需要累加的场景。mean:求平均,适合需要削弱重复记录影响的场景。任意表达式:传
pl.element()会把匹配的值收成一个列表,原样保留。
下面把三种结果并排比较,结果列的类型也会跟着变:sum 与 mean 得到数值,pl.element() 得到列表。
# 同一组合的值分别按「求和」「平均」「列表」来合并
df2 = df.pivot("name", index="index", values="value", aggregate_function="sum")
df3 = df.pivot("name", index="index", values="value", aggregate_function="mean")
df4 = df.pivot("name", index="index", values="value", aggregate_function=pl.element())
row(df2, df3, df4)
shape: (3, 4)
|
shape: (3, 4)
|
shape: (3, 4)
|
values 传多列时,每一列都会展开,结果列名由值列名、分隔符与透视列值组合而成,其中分隔符由 separator 指定,默认是下划线。例如 value 与 score 两列、A 与 B 两个取值,会得到 value_A、value_B、score_A、score_B 四列。
列名一长就容易混淆,所以在多维透视时换一个更醒目的分隔符是常见做法。
# 同时展开 value 和 score 两列
df5 = df.pivot("name", index="index", values=["value", "score"], aggregate_function=pl.element())
df6 = df.pivot("name", index="index", values=["value", "score"], aggregate_function="max", separator=":")
row(df5, df6)
shape: (3, 7)
|
shape: (3, 7)
|
unpivot() 是 pivot() 的逆操作,把宽表退回长表:on 指定的列会被叠成一列,index 指定的列原样保留。
on:要叠起来的列。可以给列名、列名列表或选择器。要排除键列时用cs.exclude()最直接,pl.col()这样的表达式则不支持。index:原样保留的列。variable_name:存放原列名的新列名。value_name:存放值的新列名。
下面的例子把 index 以外的列都叠起来,原列名放进 name,值放进 value。
from polars import selectors as cs
# 把 index 以外的列叠成长表
df_u1 = df1.unpivot(on=cs.exclude("index"), index="index", variable_name="name", value_name="value")
row(df1, df_u1)
shape: (3, 4)
|
shape: (9, 3)
|
values 传多列得到的 df6,要还原就得把列名拆开再 pivot 回去,因为原来的信息被压进了 value:A 这样的列名里。
unpivot(on=cs.exclude('index'), index='index'):先退回长表,variable列里是value:A、score:B这样的原列名。pl.col('variable').str.split(':'):按冒号切开,得到['value', 'A']。.list.to_struct(fields=['colname', 'name']):转成两个字段的结构体。.struct.unnest():展开成colname与name两列。pivot('colname', index=['index', 'name'], values='value'):用原列名 pivot 回去,形状就还原了。
这一串步骤看起来多,但每一步只做一件事,出错时也容易定位在哪一步形状不对。
(
df6.unpivot(on=cs.exclude("index"), index="index")
.with_columns(
pl.col("variable")
.str.split(":") # value:A → ['value', 'A']
.list.to_struct(fields=["colname", "name"])
.struct.unnest() # 拆成两列
)
.pivot("colname", index=["index", "name"], values="value")
)
| index | name | value | score |
|---|---|---|---|
| i64 | str | i64 | i64 |
| 0 | "A" | 10 | 1 |
| 1 | "A" | 100 | 4 |
| 2 | "A" | 30 | 5 |
| 0 | "B" | 40 | 4 |
| 1 | "B" | 50 | 3 |
| 2 | "B" | 60 | 2 |
| 0 | "C" | 70 | 1 |
| 1 | "C" | 80 | 2 |
| 2 | "C" | 90 | 3 |
unstack#
unstack() 接近 NumPy 的 reshape:它不按值聚合,而是按固定步长把一列的值展开成多列。step 是每组的行数,how 是展开方向,'vertical' 行优先、'horizontal' 列优先,不够的位置用 fill_values 填补。
与 pivot() 的区别在于:pivot() 用列里的取值决定新列名,unstack() 只按位置切分,新列名是固定的。Polars 2.0 中这些参数全是关键字专用,因此必须写成 df.unstack(step=4, ...),传位置参数会直接报错。
# 每 4 行纵向展开一次(不够的格子用 -1 和 '?' 填)
df_vertical = df.unstack(step=4, how="vertical", columns=["index", "name"], fill_values=[-1, "?"])
# 每 3 行横向展开一次
df_horizontal = df.unstack(step=3, how="horizontal", columns="index")
row(df, df_vertical, df_horizontal)
shape: (10, 4)
|
shape: (4, 6)
|
shape: (4, 3)
|
to_dummies() 把类别列展开成 0 与 1 的哑变量,也就是 one-hot 编码:命中为 1,否则为 0。给多列就每列各自展开,列名是原列名与类别值的组合。
哑变量适合把类别交给线性模型、距离计算或只接受数值的第三方库;它会把一列拆成多列,列数等于类别数,类别很多时要留意内存。
drop_first=False:默认值,保留所有类别。drop_first=True:去掉第一个类别作为基准,做回归时用来避免多重共线性。
# 把 index 和 name 展开成哑变量
df_original = df.filter(pl.col("index") < 2).select("index", "name", "value")
df_dummies1 = df_original.to_dummies(columns=["index", "name"], drop_first=False)
df_dummies2 = df_original.to_dummies(columns=["index", "name"], drop_first=True)
row(df_original, df_dummies1, df_dummies2)
shape: (7, 3)
|
shape: (7, 6)
|
shape: (7, 4)
|