在处理表格数据的时候,我们常常遇到这样的需求:某一行需要根据某个计数值被复制成多行。比如一条购买记录里商品数量是三,就希望展开为三条明细。Polars作为用Rust编写的高性能DataFrame库,提供了repeat_by和flatten两个表达式方法来优雅地解决这一类行复制问题。这两个方法组合使用,可以在不写循环的情况下完成向量化展开,既简洁又高效。

repeat_by方法的基本用法与原理
repeat_by是Polars中Expr对象的一个方法,它的作用是:对列中的每一个元素,根据给定的整数表达式重复该元素,生成一个列表(List类型)。例如,如果我们有一列数量qty,对某一列值调用repeat_by('qty'),就会把该列每个值重复qty次,放进一个列表里。这一步并不会增加行数,而是把单行内容变成嵌套结构。理解这一点非常重要,因为很多初学者会误以为repeat_by直接就完成了行复制,实际上它只是第一步的准备工作。
下面用一个简单示例说明。假设我们有一个商品表,包含商品名称和数量,希望把名称按数量复制成列表。我们先创建一个包含item和qty两列的DataFrame,然后对item列调用repeat_by方法,传入qty列作为重复次数的依据。执行后,结果中的item_repeated列是一个列表列,apple对应['apple', 'apple'],banana对应['banana', 'banana', 'banana'],cherry对应['cherry']。可以看到,repeat_by非常直观地完成了"按次数重复"的语义,而且完全在引擎内部完成,没有Python层循环。
import polars as pl
# 创建商品表
df = pl.DataFrame({
'item': ['apple', 'banana', 'cherry'],
'qty': [2, 3, 1]
})
# 对item列使用repeat_by,按qty列的值重复
res = df.with_columns(
pl.col('item').repeat_by('qty').alias('item_repeated')
)
print(res)
从输出结果可以清楚地看到,原始的行数仍然是三行,但item_repeated列中每一行的值变成了一个列表。这种嵌套结构是Polars列式存储模型的自然产物,它允许我们在不改变DataFrame行数的情况下,预先计算出每个元素需要展开后的完整内容。repeat_by方法接收的参数是一个整数表达式,可以是列名,也可以是常量或更复杂的表达式,灵活性很高。
用flatten将嵌套列表展开为多行
repeat_by生成的列表仍然停留在单列中,要想真正"复制行",还需要把列表拆开成独立的行。这时就要用到flatten方法。flatten是DataFrame或LazyFrame的方法,也可以作为表达式使用,它的功能是消除一层嵌套,将列表中的每个元素变为单独的行,同时保留其他列的对应值。简单来说,flatten就是把List类型的列"压平",让每个列表元素各占一行。
我们将上一步的结果继续处理。对item_repeated列调用flatten后,输出已经是一个普通的DataFrame,item_repeated列变成了六行,每行一个水果名称,qty列的值也跟着原本的行对齐保留。如果我们在最开始就用with_columns生成重复列表,然后对整个DataFrame调用flatten,就能一步得到展开后的明细表。实际编码中常写成链式调用,代码更加简洁。
# 接上面res的结果,将列表列展开为多行
expanded = res.select(
pl.col('item_repeated').flatten(),
pl.col('qty')
)
print(expanded)
# 也可以写成链式调用,一步到位
detail = df.with_columns(
pl.col('item').repeat_by('qty').alias('item')
).flatten()
print(detail)
这样得到的detail里,item列直接是展开后的多行字符串,原qty列完整保留。相比用df.filter加循环拼接,这种写法既简洁又高效。flatten的核心优势在于它是向量化的操作,在Rust端原生执行,不需要在Python层逐行处理。当列表长度不一致时,flatten也能正确处理,它会按照每个列表的实际长度展开,不会强制对齐到统一长度。这种特性使得repeat_by和flatten的组合非常适合处理真实世界中不规则的数据展开需求。
处理缺失值与复杂字段场景
在真实数据里,repeat_by依赖的整数表达式可能包含空值。如果qty列有null,repeat_by会报错或产生未知行为,因此要先处理缺失。通常可以用fill_null赋予默认值,比如填0表示不复制,或填1保证至少一行。这个预处理步骤非常关键,因为Polars在遇到null作为重复次数时无法确定应该生成多少个元素,直接调用会导致运行时错误或数据不一致。
下面通过一个包含缺失值的示例来演示完整的处理流程。我们创建一个item和qty列都有null的DataFrame,先用fill_null(0)处理qty列的空值,然后再调用repeat_by和flatten。这里把qty的null填为0,banana就不会产生复制行。如果原表还有其他字段,如价格、分类,它们都会随着repeat_by生成的列表在flatten时自动广播对齐,不需要我们手动做笛卡尔积或者merge。
# 创建包含缺失值的数据
df2 = pl.DataFrame({
'item': ['apple', 'banana', None],
'qty': [2, None, 3]
})
# 先处理缺失值,再执行行复制
safe = df2.with_columns(
pl.col('qty').fill_null(0).alias('qty_safe')
).with_columns(
pl.col('item').repeat_by('qty_safe').alias('item')
).flatten()
print(safe)
这是Polars列式存储模型带来的便利:所有列在同一行号上保持长度一致。当某一列被展开为多行时,其他列会自动按照对应关系进行广播,无需额外的join操作。需要注意的是,如果item本身也是null,repeat_by会生成一个包含null的列表,flatten后该null会成为一个独立的行。如果这不是期望的行为,可以在repeat_by之前先用filter过滤掉item为null的行,或者在flatten之后用drop_nulls清理。对于包含日期、结构体等复杂类型的列,repeat_by和flatten同样适用,因为它们操作的是列级别的数据结构,与元素的具体类型无关。
性能对比与适用场景分析
很多初学者会用Python的for循环遍历DataFrame,把每一行append多次,这种写法在十万级以上数据就会非常慢,而且内存占用高。repeat_by加flatten完全在Rust端向量化执行,不仅代码短,而且速度通常快几十倍。我们可以用简单的时间测试感受差异:创建一个包含十万行数据的DataFrame,其中重复次数列n的值在1到5之间循环,展开后的总行数约为三十万行。
import time
# 构造十万行测试数据
big = pl.DataFrame({
'id': pl.arange(0, 100000, dtype=pl.Int32),
'n': pl.Series([1, 2, 3, 4, 5] * 20000)
})
# 使用repeat_by加flatten向量化展开
t0 = time.time()
out = big.with_columns(
pl.col('id').repeat_by('n').alias('id')
).flatten()
print('polars repeat_by:', time.time() - t0)
print('展开后行数:', out.height)
在普通笔记本上,这段展开二十多万行的操作一般只需几十毫秒。它特别适合做数据增强、日志展开、订单明细生成、模拟用户行为等任务。需要注意的是,如果重复次数极大(如某些n为十万),展开后行数会暴涨,要提前估算内存;此时可考虑分块处理或使用LazyFrame的流式执行来控制内存峰值。另外,当重复次数列的值分布极度不均匀时,比如少数行有极大的重复次数,展开操作可能导致内存激增,建议先用describe或quantile检查数据分布,再决定是否需要分批处理。
小结与要点回顾
通过repeat_by我们先按指定字段把元素变成列表,再用flatten压平,就完成了高效的行复制。核心要点是:repeat_by接收整数表达式作为重复次数,生成List类型的嵌套列;flatten消除嵌套结构,将列表元素展开为独立行;缺失值要提前用fill_null处理,避免运行时错误;其他列在flatten时会自动广播对齐,无需手动关联。掌握这套组合,能让你在用Polars做数据变形时避开低效循环,写出更地道的向量化代码。
在实际项目中,这套方法组合的应用场景非常广泛。比如电商系统中的订单展开,一条订单记录包含多个商品及数量,需要展开为明细行进行后续分析;日志处理中,一条聚合日志可能包含多个子事件,需要展开后逐条分析;数据增强场景下,需要根据权重或次数复制样本以平衡数据分布。无论哪种场景,repeat_by和flatten都能提供一致的、高性能的解决方案。建议在使用时始终关注数据规模和内存占用,对于超大数据集优先考虑LazyFrame模式,让Polars的查询优化器自动规划执行策略,从而在保证正确性的同时最大化性能表现。