跳到内容

缺失数据

本用户指南章节介绍如何在 Polars 中处理缺失数据。

nullNaN

在 Polars 中,缺失数据用值 null 表示。此缺失值 null 用于所有数据类型,包括数值类型。

Polars 也支持浮点数列中的值 NaN(“非数值”)。值 NaN 被认为是有效的浮点值,这与缺失数据不同。我们将在下面单独讨论值 NaN

创建 Series 或 DataFrame 时,可以使用相应语言的构造将值设置为 null

DataFrame

import polars as pl

df = pl.DataFrame(
    {
        "value": [1, None],
    },
)
print(df)

DataFrame

use polars::prelude::*;
let df = df! (
    "value" => &[Some(1), None],
)?;

println!("{df}");

shape: (2, 1)
┌───────┐
│ value │
│ ---   │
│ i64   │
╞═══════╡
│ 1     │
│ null  │
└───────┘

与 pandas 的区别

在 pandas 中,用于表示缺失数据的值取决于列的数据类型。在 Polars 中,缺失数据始终用值 null 表示。

缺失数据元数据

Polars 会跟踪每个 Series 缺失数据的一些元数据。这些元数据允许 Polars 以非常高效的方式回答关于缺失值的一些基本查询,即有多少值缺失以及哪些值缺失。

要确定列中缺失了多少值,可以使用函数 null_count

null_count

null_count_df = df.null_count()
print(null_count_df)

null_count

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,哪些不是

is_null

is_null_series = df.select(
    pl.col("value").is_null(),
)
print(is_null_series)

is_null

let is_null_series = df
    .clone()
    .lazy()
    .select([col("value").is_null()])
    .collect()?;
println!("{is_null_series}");

shape: (2, 1)
┌───────┐
│ value │
│ ---   │
│ bool  │
╞═══════╡
│ false │
│ true  │
└───────┘

函数 is_null 可以在 DataFrame 的列或直接在 Series 上使用。同样,这是一个开销很小的操作,因为 Polars 已经知道结果。

为什么 Polars 要浪费内存用于有效位图?

这归结为一种权衡。通过为每列多使用一点内存,Polars 在对列执行大多数操作时可以更高效。如果不知道有效位图,每次你想计算某些东西时,都必须检查 Series 的每个位置以查看是否存在合法值。有了有效位图,Polars 会自动知道可以应用操作的位置。

填充缺失数据

Series 中的缺失数据可以通过函数 fill_null 填充。你可以通过几种不同的方式指定如何有效填充缺失数据

  • 正确数据类型的字面值;
  • Polars 表达式,例如用从另一列计算的值替换;
  • 基于相邻值的策略,例如向前或向后填充;以及
  • 插值。

为了说明每种方法的工作原理,我们首先定义一个简单的 DataFrame,其中第二列有两个缺失值

DataFrame

df = pl.DataFrame(
    {
        "col1": [0.5, 1, 1.5, 2, 2.5],
        "col2": [1, None, 3, None, 5],
    },
)
print(df)

DataFrame

let df = df! (
    "col1" => [0.5, 1.0, 1.5, 2.0, 2.5],
    "col2" => [Some(1), None, Some(3), None, Some(5)],
)?;

println!("{df}");

shape: (5, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ f64  ┆ i64  │
╞══════╪══════╡
│ 0.5  ┆ 1    │
│ 1.0  ┆ null │
│ 1.5  ┆ 3    │
│ 2.0  ┆ null │
│ 2.5  ┆ 5    │
└──────┴──────┘

用指定的字面值填充

你可以用指定的字面值填充缺失数据。这个字面值将替换所有 null 值的出现。

fill_null

fill_literal_df = df.with_columns(
    pl.col("col2").fill_null(3),
)
print(fill_literal_df)

fill_null

let fill_literal_df = df
    .clone()
    .lazy()
    .with_column(col("col2").fill_null(3))
    .collect()?;

println!("{fill_literal_df}");

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_null

fill_expression_df = df.with_columns(
    pl.col("col2").fill_null((2 * pl.col("col1")).cast(pl.Int64)),
)
print(fill_expression_df)

fill_null

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_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)

fill_null

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 来通过插值填充中间缺失数据

interpolate

fill_interpolation_df = df.with_columns(
    pl.col("col2").interpolate(),
)
print(fill_interpolation_df)

interpolate

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 可以直接创建

DataFrame

import numpy as np

nan_df = pl.DataFrame(
    {
        "value": [1.0, np.nan, float("nan"), 3.0],
    },
)
print(nan_df)

DataFrame

let nan_df = df!(
    "value" => [1.0, f64::NAN, f64::NAN, 3.0],
)?;
println!("{nan_df}");

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_nanfill_nan,它们与函数 is_nullfill_null 的工作方式相似。与缺失数据不同,Polars 不保留任何关于 NaN 值的元数据,因此函数 is_nan 需要实际计算。

nullNaN 之间的另一个区别是,数值聚合函数(如 meansum)在计算结果时会跳过缺失值,而值 NaN 会被考虑进行计算,并且通常会传播到结果中。如果需要,可以通过将 NaN 值的出现替换为 null 值来避免这种行为

fill_nan

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)

fill_nan

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 值的信息。