版本 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", join_nulls=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
用于日期时间组件的较小整数数据类型 dt.month, dt.week
大多数日期时间组件,如month和week,以前会返回UInt32类型。这已更新为最小的适当有符号整数类型。这应该会减少内存消耗。
| Method | Dtype old | Dtype new |
|---|---|---|
| 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 重新实现,行为略有不同
新的实现大部分是向后兼容的。请注意以下几点:
- 确定返回数据类型的逻辑已更改。您可能需要指定
return_dtype来覆盖推断的数据类型,或者利用新的函数 签名(单独的old和new参数)来影响返回类型。 - 之前通过使用结构体列来引用其他列作为默认值的解决方法不再有效。现在它按预期工作,不再需要解决方法。
示例
之前:
>>> 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时才会首次遇到它。这是一个相对影响较大的变化,这就是我们在这里提到它的原因。
| Old name | New name |
|---|---|
cumfold |
cum_fold |
cumreduce |
cum_reduce |
cumsum |
cum_sum |
cumprod |
cum_prod |
cummin |
cum_min |
cummax |
cum_max |
cumcount |
cum_count |