跳到内容

版本 0.20

重大变更

更改关于空值的默认 `join` 行为

以前,连接键中的空值被视为与其他任何值一样的值。这意味着左侧帧中的空值会与右侧帧中的空值进行连接。这既昂贵又不符合 SQL 中的默认行为。

默认行为现在已更改为忽略连接键中的空值。可以通过设置 `join_nulls=True` 来保留之前的行为。

示例

之前

>>> df1 = pl.DataFrame({"a": [1, 2, None], "b": [4, 4, 4]})
>>> df2 = pl.DataFrame({"a": [None, 2, 3], "c": [5, 5, 5]})
>>> df1.join(df2, on="a", how="inner")
shape: (2, 3)
┌──────┬─────┬─────┐
│ a    ┆ b   ┆ c   │
│ ---  ┆ --- ┆ --- │
│ i64  ┆ i64 ┆ i64 │
╞══════╪═════╪═════╡
│ null ┆ 4   ┆ 5   │
│ 2    ┆ 4   ┆ 5   │
└──────┴─────┴─────┘

之后

>>> df1.join(df2, on="a", how="inner")
shape: (1, 3)
┌─────┬─────┬─────┐
│ a   ┆ b   ┆ c   │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╡
│ 2   ┆ 4   ┆ 5   │
└─────┴─────┴─────┘
>>> df1.join(df2, on="a", how="inner", nulls_equal=True)  # Keeps previous behavior
shape: (2, 3)
┌──────┬─────┬─────┐
│ a    ┆ b   ┆ c   │
│ ---  ┆ --- ┆ --- │
│ i64  ┆ i64 ┆ i64 │
╞══════╪═════╪═════╡
│ null ┆ 4   ┆ 5   │
│ 2    ┆ 4   ┆ 5   │
└──────┴─────┴─────┘

在外连接中保留左侧和右侧的连接键

以前,外连接的结果不包含左侧和右侧帧的连接键。相反,它包含左键和右键的合并版本。这会丢失信息,并且不符合 SQL 的默认行为。

行为已更改为包含原始连接键。名称冲突通过在右连接键名称后附加后缀(默认为 `_right`)来解决。可以通过设置 `how="outer_coalesce"` 来保留之前的行为。

示例

之前

>>> df1 = pl.DataFrame({"L1": ["a", "b", "c"], "L2": [1, 2, 3]})
>>> df2 = pl.DataFrame({"L1": ["a", "c", "d"], "R2": [7, 8, 9]})
>>> df1.join(df2, on="L1", how="outer")
shape: (4, 3)
┌─────┬──────┬──────┐
│ L1  ┆ L2   ┆ R2   │
│ --- ┆ ---  ┆ ---  │
│ str ┆ i64  ┆ i64  │
╞═════╪══════╪══════╡
│ a   ┆ 1    ┆ 7    │
│ c   ┆ 3    ┆ 8    │
│ d   ┆ null ┆ 9    │
│ b   ┆ 2    ┆ null │
└─────┴──────┴──────┘

之后

>>> df1.join(df2, on="L1", how="outer")
shape: (4, 4)
┌──────┬──────┬──────────┬──────┐
│ L1   ┆ L2   ┆ L1_right ┆ R2   │
│ ---  ┆ ---  ┆ ---      ┆ ---  │
│ str  ┆ i64  ┆ str      ┆ i64  │
╞══════╪══════╪══════════╪══════╡
│ a    ┆ 1    ┆ a        ┆ 7    │
│ b    ┆ 2    ┆ null     ┆ null │
│ c    ┆ 3    ┆ c        ┆ 8    │
│ null ┆ null ┆ d        ┆ 9    │
└──────┴──────┴──────────┴──────┘
>>> df1.join(df2, on="a", how="outer_coalesce")  # Keeps previous behavior
shape: (4, 3)
┌─────┬──────┬──────┐
│ L1  ┆ L2   ┆ R2   │
│ --- ┆ ---  ┆ ---  │
│ str ┆ i64  ┆ i64  │
╞═════╪══════╪══════╡
│ a   ┆ 1    ┆ 7    │
│ c   ┆ 3    ┆ 8    │
│ d   ┆ null ┆ 9    │
│ b   ┆ 2    ┆ null │
└─────┴──────┴──────┘

`count` 现在忽略空值

Expr 和 Series 的 `count` 方法现在忽略空值。使用 `len` 来获取包含空值的计数。

请注意,`pl.count()` 和 `group_by(...).count()` 未更改。它们计算上下文中的行数,因此空值不以同样的方式适用。

这使得行为更符合 SQL 标准,其中 `COUNT(col)` 忽略空值,但 `COUNT(*)` 无论空值如何都计算行数。

示例

之前

>>> df = pl.DataFrame({"a": [1, 2, None]})
>>> df.select(pl.col("a").count())
shape: (1, 1)
┌─────┐
│ a   │
│ --- │
│ u32 │
╞═════╡
│ 3   │
└─────┘

之后

>>> df.select(pl.col("a").count())
shape: (1, 1)
┌─────┐
│ a   │
│ --- │
│ u32 │
╞═════╡
│ 2   │
└─────┘
>>> df.select(pl.col("a").len())  # Mirrors previous behavior
shape: (1, 1)
┌─────┐
│ a   │
│ --- │
│ u32 │
╞═════╡
│ 3   │
└─────┘

`NaN` 值现在被视为相等

浮点 `NaN` 值在 Polars 操作中被视为不相等。这已得到纠正,以更好地符合用户预期和现有标准。

虽然这被视为一个错误修复,但将其包含在本指南中是为了引起用户注意,其工作流程可能包含 `NaN` 值。

示例

之前

>>> s = pl.Series([1.0, float("nan"), float("inf")])
>>> s == s
shape: (3,)
Series: '' [bool]
[
        true
        false
        true
]

之后

>>> s == s
shape: (3,)
Series: '' [bool]
[
        true
        true
        true
]

断言工具更新为精确检查和 `NaN` 相等性

断言工具函数 `assert_frame_equal` 和 `assert_series_equal` 会使用容差参数 `atol` 和 `rtol` 进行近似检查,除非 `check_exact` 设置为 `True`。这可能导致一些令人惊讶的行为,因为整数通常被认为是精确值。现在整数值总是进行精确检查。要进行非精确检查,请先转换为浮点数。

此外,`nans_compare_equal` 参数已移除,`NaN` 值现在总是被视为相等,这是之前的默认行为。此参数此前已被弃用,但为了方便 `NaN` 相等性的更改,已在标准弃用期结束前将其移除。

示例

之前

>>> from polars.testing import assert_frame_equal
>>> df1 = pl.DataFrame({"id": [123456]})
>>> df2 = pl.DataFrame({"id": [123457]})
>>> assert_frame_equal(df1, df2)  # Passes

之后

>>> assert_frame_equal(df1, df2)
...
AssertionError: DataFrames are different (value mismatch for column 'id')
[left]:  [123456]
[right]: [123457]

允许实例化所有 `DataType` 对象

Polars 数据类型是 `DataType` 类的子类。我们曾有一个“技巧”,它会自动将未带任何参数实例化的数据类型转换为 `class`,而不是实际实例化它们。这样做的目的是允许将数据类型指定为 `Int64` 而不是 `Int64()`,这更简洁。然而,当直接使用数据类型对象时,这导致了一些意外行为,特别是对于像 `Datetime` 这样在许多情况下*会*被实例化的数据类型存在差异。

今后,实例化一个数据类型将始终返回该类的一个实例。Polars 同时处理类和实例,因此之前的简洁语法仍然可用。返回数据类型的方法,如 `Series.dtype` 和 `DataFrame.schema`,现在总是返回已实例化的数据类型对象。

如果您之前没有使用相等运算符 (`==`),您可能需要更新一些数据类型检查,并更新一些类型提示。

示例

之前

>>> s = pl.Series([1, 2, 3], dtype=pl.Int8)
>>> s.dtype == pl.Int8
True
>>> s.dtype is pl.Int8
True
>>> isinstance(s.dtype, pl.Int8)
False

之后

>>> s.dtype == pl.Int8
True
>>> s.dtype is pl.Int8
False
>>> isinstance(s.dtype, pl.Int8)
True

更新 `Decimal` 和 `Array` 数据类型的构造函数

数据类型 `Decimal` 和 `Array` 的参数已调换。新的构造函数应该更符合用户预期。

示例

之前

>>> pl.Array(2, pl.Int16)
Array(Int16, 2)
>>> pl.Decimal(5, 10)
Decimal(precision=10, scale=5)

之后

>>> pl.Array(pl.Int16, 2)
Array(Int16, width=2)
>>> pl.Decimal(10, 5)
Decimal(precision=10, scale=5)

`DataType.is_nested` 从属性更改为类方法

这是一个小的更改,但对于正确更新非常重要。未能相应更新可能导致错误的逻辑,因为 Python 会将该*方法*评估为 `True`。例如,`if dtype.is_nested` 现在将始终评估为 `True`,无论数据类型如何,因为它返回的是方法本身,而 Python 认为方法是“真值”。

示例

之前

>>> pl.List(pl.Int8).is_nested
True

之后

>>> pl.List(pl.Int8).is_nested()
True

datetime 组件 `dt.month`、`dt.week` 使用更小的整数数据类型

大多数 datetime 组件,例如 `month` 和 `week`,以前会返回 `UInt32` 类型。这已更新为最小的适当有符号整数类型。这应该会减少内存消耗。

方法 旧数据类型 新数据类型
year i32 i32
iso_year i32 i32
quarter u32 i8
month u32 i8
week u32 i8
day u32 i8
weekday u32 i8
ordinal_day u32 i16
hour u32 i8
minute u32 i8
second u32 i8
millisecond u32 i32*
microsecond u32 i32
nanosecond u32 i32

*严格来说,`millisecond` 可以是 `i16`。未来可能会更新。

示例

之前

>>> from datetime import date
>>> s = pl.Series([date(2023, 12, 31), date(2024, 1, 1)])
>>> s.dt.month()
shape: (2,)
Series: '' [u32]
[
        12
        1
]

之后

>>> s.dt.month()
shape: (2,)
Series: '' [u8]
[
        12
        1
]

Series 在没有数据时现在默认为 `Null` 数据类型

这取代了之前初始化为 `Float32` 类型的行为。

示例

之前

>>> pl.Series("a", [None])
shape: (1,)
Series: 'a' [f32]
[
        null
]

之后

>>> pl.Series("a", [None])
shape: (1,)
Series: 'a' [null]
[
        null
]

`replace` 重新实现,行为略有不同

新的实现大多向后兼容。请注意以下几点:

  1. 确定返回数据类型的逻辑已更改。您可能需要指定 `return_dtype` 来覆盖推断的数据类型,或者利用新的函数签名(单独的 `old` 和 `new` 参数)来影响返回类型。
  2. 以前通过使用结构列将其他列引用为默认值的变通方法不再奏效。现在它只是按预期工作,无需任何变通方法。

示例

之前

>>> df = pl.DataFrame({"a": [1, 2, 2, 3], "b": [1.5, 2.5, 5.0, 1.0]}, schema={"a": pl.Int8, "b": pl.Float64})
>>> df.select(pl.col("a").replace({2: 100}))
shape: (4, 1)
┌─────┐
│ a   │
│ --- │
│ i8  │
╞═════╡
│ 1   │
│ 100 │
│ 100 │
│ 3   │
└─────┘
>>> df.select(pl.struct("a", "b").replace({2: 100}, default=pl.col("b")))
shape: (4, 1)
┌───────┐
│ a     │
│ ---   │
│ f64   │
╞═══════╡
│ 1.5   │
│ 100.0 │
│ 100.0 │
│ 1.0   │
└───────┘

之后

>>> df.select(pl.col("a").replace({2: 100}))
shape: (4, 1)
┌─────┐
│ a   │
│ --- │
│ i64 │
╞═════╡
│ 1   │
│ 100 │
│ 100 │
│ 3   │
└─────┘
>>> df.select(pl.col("a").replace({2: 100}, default=pl.col("b")))  # No struct needed
shape: (4, 1)
┌───────┐
│ a     │
│ ---   │
│ f64   │
╞═══════╡
│ 1.5   │
│ 100.0 │
│ 100.0 │
│ 1.0   │
└───────┘

`value_counts` 结果列从 `counts` 重命名为 `count`

`value_counts` 方法的结果结构字段已从 `counts` 重命名为 `count`。

示例

之前

>>> s = pl.Series("a", ["x", "x", "y"])
>>> s.value_counts()
shape: (2, 2)
┌─────┬────────┐
│ a   ┆ counts │
│ --- ┆ ---    │
│ str ┆ u32    │
╞═════╪════════╡
│ x   ┆ 2      │
│ y   ┆ 1      │
└─────┴────────┘

之后

>>> s.value_counts()
shape: (2, 2)
┌─────┬───────┐
│ a   ┆ count │
│ --- ┆ ---   │
│ str ┆ u32   │
╞═════╪═══════╡
│ x   ┆ 2     │
│ y   ┆ 1     │
└─────┴───────┘

更新 `read_parquet` 以使用对象存储而非 fsspec

如果您之前使用 `read_parquet`,则不再需要将 `fsspec` 作为可选依赖项安装。新的对象存储实现在 `scan_parquet` 中已经在使用。在某些情况下,例如凭据的检测方式和下载的执行方式,它可能会有略微不同的行为。

结果 `DataFrame` 在不同版本之间应该是相同的。

弃用

累积函数从 `cum*` 重命名为 `cum_*`

严格来说,此弃用是在 `0.19.14` 版本中引入的,但许多用户在升级到 `0.20` 时会首次遇到它。这是一个影响相对较大的更改,因此我们在此提及。

旧名称 新名称
cumfold cum_fold
cumreduce cum_reduce
cumsum cum_sum
cumprod cum_prod
cummin cum_min
cummax cum_max
cumcount cum_count