Skip to content

缺失数据

本用户指南部分教授如何在Polars中处理缺失数据。

nullNaN

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

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

在创建系列或数据框时,您可以使用适合您语言的适当构造将值设置为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 会跟踪每个系列中缺失数据的一些元数据。这些元数据使得 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 可以在数据框、数据框中的列或直接在系列上调用。函数 null_count 是一个廉价的操作,因为结果已经已知。

Polars 使用一种称为“有效性位图”的东西来知道系列中哪些值是缺失的。有效性位图是内存高效的,因为它是位编码的。如果一个系列的长度为 \(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 可以直接用于数据框的列或系列上。同样,这是一个廉价的操作,因为结果已经由 Polars 知道了。

Why does Polars waste memory on a validity bitmap?

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

填充缺失数据

系列中的缺失数据可以使用函数fill_null来填充。您可以通过几种不同的方式指定如何有效地填充缺失数据:

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

为了说明这些方法的工作原理,我们首先定义一个简单的数据框,其中第二列有两个缺失值:

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  │
└──────┴──────┘

非数字,或NaN

在系列中缺失的数据仅由值null表示,无论系列的数据类型如何。具有浮点数据类型的列有时可能具有值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的信息在 关于浮点数数据类型的部分