缺失数据
本用户指南章节介绍如何在 Polars 中处理缺失数据。
null
和 NaN
值
在 Polars 中,缺失数据用值 null
表示。此缺失值 null
用于所有数据类型,包括数值类型。
Polars 也支持浮点数列中的值 NaN
(“非数值”)。值 NaN
被认为是有效的浮点值,这与缺失数据不同。我们将在下面单独讨论值 NaN
。
创建 Series 或 DataFrame 时,可以使用相应语言的构造将值设置为 null
shape: (2, 1)
┌───────┐
│ value │
│ --- │
│ i64 │
╞═══════╡
│ 1 │
│ null │
└───────┘
与 pandas 的区别
在 pandas 中,用于表示缺失数据的值取决于列的数据类型。在 Polars 中,缺失数据始终用值 null
表示。
缺失数据元数据
Polars 会跟踪每个 Series 缺失数据的一些元数据。这些元数据允许 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
可以在 DataFrame、DataFrame 中的列或直接在 Series 上调用。函数 null_count
是一种开销很小的操作,因为结果已经已知。
Polars 使用一种称为“有效位图”(validity bitmap)的东西来知道 Series 中哪些值缺失。有效位图是内存高效的,因为它采用位编码。如果 Series 的长度为 \(n\),则其有效位图将占用 \(n / 8\) 字节。函数 is_null
使用有效位图来高效地报告哪些值是 null
,哪些不是
shape: (2, 1)
┌───────┐
│ value │
│ --- │
│ bool │
╞═══════╡
│ false │
│ true │
└───────┘
函数 is_null
可以在 DataFrame 的列或直接在 Series 上使用。同样,这是一个开销很小的操作,因为 Polars 已经知道结果。
为什么 Polars 要浪费内存用于有效位图?
这归结为一种权衡。通过为每列多使用一点内存,Polars 在对列执行大多数操作时可以更高效。如果不知道有效位图,每次你想计算某些东西时,都必须检查 Series 的每个位置以查看是否存在合法值。有了有效位图,Polars 会自动知道可以应用操作的位置。
填充缺失数据
Series 中的缺失数据可以通过函数 fill_null
填充。你可以通过几种不同的方式指定如何有效填充缺失数据
- 正确数据类型的字面值;
- Polars 表达式,例如用从另一列计算的值替换;
- 基于相邻值的策略,例如向前或向后填充;以及
- 插值。
为了说明每种方法的工作原理,我们首先定义一个简单的 DataFrame,其中第二列有两个缺失值
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 │
└──────┴──────┘
注意:使用插值时,Series 开头和结尾的 null 值保持不变。
非数值(NaN
)值
Series 中的缺失数据始终由值 null
表示,无论 Series 的数据类型如何。浮点数据类型的列有时可能包含值 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
值的信息。