缺失数据
本用户指南部分教授如何在Polars中处理缺失数据。
null 和 NaN 值
在Polars中,缺失数据由值null表示。这个缺失值null用于所有数据类型,包括数值类型。
Polars 还支持浮点数列中的值 NaN(“非数字”)。值 NaN 被视为有效的浮点数值,这与缺失数据不同。我们将在下面单独讨论值 NaN。
在创建系列或数据框时,您可以使用适合您语言的适当构造将值设置为null:
shape: (2, 1)
┌───────┐
│ value │
│ --- │
│ i64 │
╞═══════╡
│ 1 │
│ null │
└───────┘
与pandas的区别
在pandas中,用于表示缺失数据的值取决于列的数据类型。
在Polars中,缺失数据始终由值null表示。
缺失数据元数据
Polars 会跟踪每个系列中缺失数据的一些元数据。这些元数据使得 Polars 能够以非常高效的方式回答一些关于缺失值的基本查询,即有多少值缺失以及哪些值缺失。
要确定一列中有多少缺失值,你可以使用函数 null_count:
null_count_df = df.null_count()
print(null_count_df)
let null_count_df = df.null_count();
println!("{}", null_count_df);
shape: (1, 1)
┌───────┐
│ value │
│ --- │
│ u32 │
╞═══════╡
│ 1 │
└───────┘
函数 null_count 可以在数据框、数据框中的列或直接在系列上调用。函数 null_count 是一个廉价的操作,因为结果已经已知。
Polars 使用一种称为“有效性位图”的东西来知道系列中哪些值是缺失的。有效性位图是内存高效的,因为它是位编码的。如果一个系列的长度为 \(n\),那么它的有效性位图将花费 \(n / 8\) 字节。函数 is_null 使用有效性位图来高效地报告哪些值是 null,哪些不是:
shape: (2, 1)
┌───────┐
│ value │
│ --- │
│ bool │
╞═══════╡
│ false │
│ true │
└───────┘
函数 is_null 可以直接用于数据框的列或系列上。同样,这是一个廉价的操作,因为结果已经由 Polars 知道了。
Why does Polars waste memory on a validity bitmap?
这一切都归结为一种权衡。 通过每列使用更多的内存,Polars 在执行大多数列操作时可以更加高效。 如果不知道有效性位图,每次你想计算某些东西时,都必须检查系列的每个位置,看看是否存在合法值。 有了有效性位图,Polars 自动知道可以应用操作的位置。
填充缺失数据
系列中的缺失数据可以使用函数fill_null来填充。您可以通过几种不同的方式指定如何有效地填充缺失数据:
- 正确数据类型的字面量;
- 一个Polars表达式,例如用从另一列计算的值替换;
- 基于相邻值的策略,例如向前或向后填充;以及
- 插值。
为了说明这些方法的工作原理,我们首先定义一个简单的数据框,其中第二列有两个缺失值:
shape: (5, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f64 ┆ i64 │
╞══════╪══════╡
│ 0.5 ┆ 1 │
│ 1.0 ┆ null │
│ 1.5 ┆ 3 │
│ 2.0 ┆ null │
│ 2.5 ┆ 5 │
└──────┴──────┘
使用指定的字面值填充
您可以使用指定的字面值填充缺失的数据。这个字面值将替换所有出现的null值:
shape: (5, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f64 ┆ i64 │
╞══════╪══════╡
│ 0.5 ┆ 1 │
│ 1.0 ┆ 3 │
│ 1.5 ┆ 3 │
│ 2.0 ┆ 3 │
│ 2.5 ┆ 5 │
└──────┴──────┘
然而,这实际上只是函数fill_null用Polars表达式的结果中的相应值替换缺失值的一般情况的一个特例,如下所示。
使用表达式填充
在一般情况下,缺失数据可以通过从通用Polars表达式的结果中提取相应的值来填充。例如,我们可以用第一列值的两倍来填充第二列:
fill_expression_df = df.with_columns(
pl.col("col2").fill_null((2 * pl.col("col1")).cast(pl.Int64)),
)
print(fill_expression_df)
let fill_expression_df = df
.clone()
.lazy()
.with_column(col("col2").fill_null((lit(2) * col("col1")).cast(DataType::Int64)))
.collect()?;
println!("{}", fill_expression_df);
shape: (5, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f64 ┆ i64 │
╞══════╪══════╡
│ 0.5 ┆ 1 │
│ 1.0 ┆ 2 │
│ 1.5 ┆ 3 │
│ 2.0 ┆ 4 │
│ 2.5 ┆ 5 │
└──────┴──────┘
基于邻近值的策略填充
你也可以通过基于邻近值的填充策略来填补缺失数据。
两种较简单的策略是寻找紧接在被填充的null值之前或之后的第一个非null值:
fill_forward_df = df.with_columns(
pl.col("col2").fill_null(strategy="forward").alias("forward"),
pl.col("col2").fill_null(strategy="backward").alias("backward"),
)
print(fill_forward_df)
let fill_literal_df = df
.clone()
.lazy()
.with_columns([
col("col2")
.fill_null_with_strategy(FillNullStrategy::Forward(None))
.alias("forward"),
col("col2")
.fill_null_with_strategy(FillNullStrategy::Backward(None))
.alias("backward"),
])
.collect()?;
println!("{}", fill_literal_df);
shape: (5, 4)
┌──────┬──────┬─────────┬──────────┐
│ col1 ┆ col2 ┆ forward ┆ backward │
│ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ i64 ┆ i64 ┆ i64 │
╞══════╪══════╪═════════╪══════════╡
│ 0.5 ┆ 1 ┆ 1 ┆ 1 │
│ 1.0 ┆ null ┆ 1 ┆ 3 │
│ 1.5 ┆ 3 ┆ 3 ┆ 3 │
│ 2.0 ┆ null ┆ 3 ┆ 5 │
│ 2.5 ┆ 5 ┆ 5 ┆ 5 │
└──────┴──────┴─────────┴──────────┘
您可以在API文档中找到其他填充策略。
填充插值
此外,您可以使用函数interpolate而不是函数fill_null来通过插值填充缺失数据:
fill_interpolation_df = df.with_columns(
pl.col("col2").interpolate(),
)
print(fill_interpolation_df)
let fill_interpolation_df = df
.clone()
.lazy()
.with_column(col("col2").interpolate(InterpolationMethod::Linear))
.collect()?;
println!("{}", fill_interpolation_df);
shape: (5, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f64 ┆ f64 │
╞══════╪══════╡
│ 0.5 ┆ 1.0 │
│ 1.0 ┆ 2.0 │
│ 1.5 ┆ 3.0 │
│ 2.0 ┆ 4.0 │
│ 2.5 ┆ 5.0 │
└──────┴──────┘
非数字,或NaN值
在系列中缺失的数据仅由值null表示,无论系列的数据类型如何。具有浮点数据类型的列有时可能具有值NaN,这可能会与null混淆。
特殊值 NaN 可以直接创建:
shape: (4, 1)
┌───────┐
│ value │
│ --- │
│ f64 │
╞═══════╡
│ 1.0 │
│ NaN │
│ NaN │
│ 3.0 │
└───────┘
它也可能作为计算的结果出现:
df = pl.DataFrame(
{
"dividend": [1, 0, -1],
"divisor": [1, 0, -1],
}
)
result = df.select(pl.col("dividend") / pl.col("divisor"))
print(result)
let df = df!(
"dividend" => [1.0, 0.0, -1.0],
"divisor" => [1.0, 0.0, -1.0],
)?;
let result = df
.clone()
.lazy()
.select([col("dividend") / col("divisor")])
.collect()?;
println!("{}", result);
shape: (3, 1)
┌──────────┐
│ dividend │
│ --- │
│ f64 │
╞══════════╡
│ 1.0 │
│ NaN │
│ 1.0 │
└──────────┘
信息
默认情况下,在pandas中,整数列中的NaN值会导致该列被转换为浮点数据类型。
在Polars中不会发生这种情况;相反,会引发异常。
NaN 值被视为一种浮点数据类型,并且在 Polars 中不被视为缺失数据。这意味着:
NaN值在函数null_count中不被计数;并且- 当你使用专门的函数
fill_nan方法时,NaN值会被填充,但使用函数fill_null时不会填充。
Polars 具有函数 is_nan 和 fill_nan,它们的工作方式与函数 is_null 和 fill_null 类似。与缺失数据不同,Polars 不保存任何关于 NaN 值的元数据,因此函数 is_nan 需要进行实际计算。
null 和 NaN 值之间的另一个区别是,数值聚合函数(如 mean 和 sum)在计算结果时会跳过缺失值,而 NaN 值会被考虑在内,并且通常会传播到结果中。如果需要,可以通过将 NaN 值替换为 null 来避免这种行为:
mean_nan_df = nan_df.with_columns(
pl.col("value").fill_nan(None).alias("replaced"),
).select(
pl.all().mean().name.suffix("_mean"),
pl.all().sum().name.suffix("_sum"),
)
print(mean_nan_df)
let mean_nan_df = nan_df
.clone()
.lazy()
.with_column(col("value").fill_nan(Null {}.lit()).alias("replaced"))
.select([
col("*").mean().name().suffix("_mean"),
col("*").sum().name().suffix("_sum"),
])
.collect()?;
println!("{}", mean_nan_df);
shape: (1, 4)
┌────────────┬───────────────┬───────────┬──────────────┐
│ value_mean ┆ replaced_mean ┆ value_sum ┆ replaced_sum │
│ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪═══════════════╪═══════════╪══════════════╡
│ NaN ┆ 2.0 ┆ NaN ┆ 4.0 │
└────────────┴───────────────┴───────────┴──────────────┘
你可以了解更多关于值NaN的信息在
关于浮点数数据类型的部分。