跳到内容

列表和数组

Polars 对两种同构容器数据类型有第一优先级的支持:ListArray。Polars 支持对这两种数据类型进行许多操作,并且它们的 API 有重叠,因此本用户指南的目标是阐明何时选择一种数据类型而不是另一种。

列表 vs 数组

List 数据类型

List 数据类型适用于值是长度可变的同构一维容器的列。

下面的 DataFrame 包含三个 List 数据类型的列的示例

List

from datetime import datetime
import polars as pl

df = pl.DataFrame(
    {
        "names": [
            ["Anne", "Averill", "Adams"],
            ["Brandon", "Brooke", "Borden", "Branson"],
            ["Camila", "Campbell"],
            ["Dennis", "Doyle"],
        ],
        "children_ages": [
            [5, 7],
            [],
            [],
            [8, 11, 18],
        ],
        "medical_appointments": [
            [],
            [],
            [],
            [datetime(2022, 5, 22, 16, 30)],
        ],
    }
)

print(df)

List

// Contribute the Rust translation of the Python example by opening a PR.

shape: (4, 3)
┌─────────────────────────────────┬───────────────┬───────────────────────┐
│ names                           ┆ children_ages ┆ medical_appointments  │
│ ---                             ┆ ---           ┆ ---                   │
│ list[str]                       ┆ list[i64]     ┆ list[datetime[μs]]    │
╞═════════════════════════════════╪═══════════════╪═══════════════════════╡
│ ["Anne", "Averill", "Adams"]    ┆ [5, 7]        ┆ []                    │
│ ["Brandon", "Brooke", … "Brans… ┆ []            ┆ []                    │
│ ["Camila", "Campbell"]          ┆ []            ┆ []                    │
│ ["Dennis", "Doyle"]             ┆ [8, 11, 18]   ┆ [2022-05-22 16:30:00] │
└─────────────────────────────────┴───────────────┴───────────────────────┘

请注意,Polars 的 List 数据类型不同于 Python 的 list 类型,后者中的元素可以是任何类型。如果您想在列中存储真正的 Python 列表,可以使用 Object 数据类型,但您的列将不具备我们将要讨论的列表操作功能。

Array 数据类型

Array 数据类型适用于值是具有已知固定形状的任意维度的同构容器的列。

下面的 DataFrame 包含两个 Array 数据类型的列的示例。

Array

df = pl.DataFrame(
    {
        "bit_flags": [
            [True, True, True, True, False],
            [False, True, True, True, True],
        ],
        "tic_tac_toe": [
            [
                [" ", "x", "o"],
                [" ", "x", " "],
                ["o", "x", " "],
            ],
            [
                ["o", "x", "x"],
                [" ", "o", "x"],
                [" ", " ", "o"],
            ],
        ],
    },
    schema={
        "bit_flags": pl.Array(pl.Boolean, 5),
        "tic_tac_toe": pl.Array(pl.String, (3, 3)),
    },
)

print(df)

Array · 可用在 dtype-array 特性标志下

// Contribute the Rust translation of the Python example by opening a PR.

shape: (2, 2)
┌───────────────────────┬─────────────────────────────────┐
│ bit_flags             ┆ tic_tac_toe                     │
│ ---                   ┆ ---                             │
│ array[bool, 5]        ┆ array[str, (3, 3)]              │
╞═══════════════════════╪═════════════════════════════════╡
│ [true, true, … false] ┆ [[" ", "x", "o"], [" ", "x", "… │
│ [false, true, … true] ┆ [["o", "x", "x"], [" ", "o", "… │
└───────────────────────┴─────────────────────────────────┘

上面的示例展示了如何指定“bit_flags”和“tic_tac_toe”列的数据类型为 Array,并使用其中包含的元素数据类型和每个数组的形状进行参数化。

通常,Polars 出于性能原因不会推断列的数据类型为 Array,而是默认为 List 数据类型的相应变体。在 Python 中,此规则的一个例外是当您提供 NumPy 数组来构建列时。在这种情况下,Polars 从 NumPy 获得了所有子数组都具有相同形状的保证,因此一个 \(n + 1\) 维数组将生成一个 \(n\) 维数组的列

Array

import numpy as np

array = np.arange(0, 120).reshape((5, 2, 3, 4))  # 4D array

print(pl.Series(array).dtype)  # Column with the 3D subarrays

Array · 可用在 dtype-array 特性标志下

// Contribute the Rust translation of the Python example by opening a PR.

Array(Int64, shape=(2, 3, 4))

何时使用哪种

简而言之,优先使用 Array 数据类型而不是 List,因为它更节省内存且性能更高。如果不能使用 Array,则使用 List

  • 当列中的值没有固定形状时;或者
  • 当您需要仅在列表 API 中可用的函数时。

使用列表

list 命名空间

Polars 提供了许多用于处理 List 数据类型值的功能,这些功能都分组在 list 命名空间中。我们现在将稍微探索一下这个命名空间。

以前是 arr,现在是 list

在 Polars 的早期版本中,列表操作的命名空间曾是 arr。现在 arrArray 数据类型的命名空间。如果您在 StackOverflow 或其他来源上找到对 arr 命名空间的引用,请注意这些来源*可能*已过时。

下面定义的 weather DataFrame 包含来自某个区域不同气象站的数据。当气象站无法获取结果时,会记录错误代码而不是当时的实际温度。

weather = pl.DataFrame(
    {
        "station": [f"Station {idx}" for idx in range(1, 6)],
        "temperatures": [
            "20 5 5 E1 7 13 19 9 6 20",
            "18 8 16 11 23 E2 8 E2 E2 E2 90 70 40",
            "19 24 E9 16 6 12 10 22",
            "E2 E0 15 7 8 10 E1 24 17 13 6",
            "14 8 E0 16 22 24 E1",
        ],
    }
)

print(weather)
// Contribute the Rust translation of the Python example by opening a PR.
shape: (5, 2)
┌───────────┬─────────────────────────────────┐
│ station   ┆ temperatures                    │
│ ---       ┆ ---                             │
│ str       ┆ str                             │
╞═══════════╪═════════════════════════════════╡
│ Station 1 ┆ 20 5 5 E1 7 13 19 9 6 20        │
│ Station 2 ┆ 18 8 16 11 23 E2 8 E2 E2 E2 90… │
│ Station 3 ┆ 19 24 E9 16 6 12 10 22          │
│ Station 4 ┆ E2 E0 15 7 8 10 E1 24 17 13 6   │
│ Station 5 ┆ 14 8 E0 16 22 24 E1             │
└───────────┴─────────────────────────────────┘

程序化创建列表

鉴于前面定义的 weather DataFrame,我们很可能需要对每个站点捕获的温度进行一些分析。为了实现这一点,我们首先需要能够获取单个温度测量值。我们可以使用 str 命名空间来完成此操作

str.split

weather = weather.with_columns(
    pl.col("temperatures").str.split(" "),
)
print(weather)

str.split

// Contribute the Rust translation of the Python example by opening a PR.

shape: (5, 2)
┌───────────┬──────────────────────┐
│ station   ┆ temperatures         │
│ ---       ┆ ---                  │
│ str       ┆ list[str]            │
╞═══════════╪══════════════════════╡
│ Station 1 ┆ ["20", "5", … "20"]  │
│ Station 2 ┆ ["18", "8", … "40"]  │
│ Station 3 ┆ ["19", "24", … "22"] │
│ Station 4 ┆ ["E2", "E0", … "6"]  │
│ Station 5 ┆ ["14", "8", … "E1"]  │
└───────────┴──────────────────────┘

一个自然的后续操作是展开温度列表,以便每个测量值都在其自己的行中

explode

result = weather.explode("temperatures")
print(result)

explode

// Contribute the Rust translation of the Python example by opening a PR.

shape: (49, 2)
┌───────────┬──────────────┐
│ station   ┆ temperatures │
│ ---       ┆ ---          │
│ str       ┆ str          │
╞═══════════╪══════════════╡
│ Station 1 ┆ 20           │
│ Station 1 ┆ 5            │
│ Station 1 ┆ 5            │
│ Station 1 ┆ E1           │
│ Station 1 ┆ 7            │
│ …         ┆ …            │
│ Station 5 ┆ E0           │
│ Station 5 ┆ 16           │
│ Station 5 ┆ 22           │
│ Station 5 ┆ 24           │
│ Station 5 ┆ E1           │
└───────────┴──────────────┘

然而,在 Polars 中,我们通常不需要这样做就可以对列表元素进行操作。

对列表进行操作

Polars 提供了几种对 List 数据类型列的标准操作。与您可以使用字符串执行的操作类似,列表可以通过函数 headtailslice 进行切片

list namespace

result = weather.with_columns(
    pl.col("temperatures").list.head(3).alias("head"),
    pl.col("temperatures").list.tail(3).alias("tail"),
    pl.col("temperatures").list.slice(-3, 2).alias("two_next_to_last"),
)
print(result)

list namespace

// Contribute the Rust translation of the Python example by opening a PR.

shape: (5, 5)
┌───────────┬──────────────────────┬────────────────────┬────────────────────┬──────────────────┐
│ station   ┆ temperatures         ┆ head               ┆ tail               ┆ two_next_to_last │
│ ---       ┆ ---                  ┆ ---                ┆ ---                ┆ ---              │
│ str       ┆ list[str]            ┆ list[str]          ┆ list[str]          ┆ list[str]        │
╞═══════════╪══════════════════════╪════════════════════╪════════════════════╪══════════════════╡
│ Station 1 ┆ ["20", "5", … "20"]  ┆ ["20", "5", "5"]   ┆ ["9", "6", "20"]   ┆ ["9", "6"]       │
│ Station 2 ┆ ["18", "8", … "40"]  ┆ ["18", "8", "16"]  ┆ ["90", "70", "40"] ┆ ["90", "70"]     │
│ Station 3 ┆ ["19", "24", … "22"] ┆ ["19", "24", "E9"] ┆ ["12", "10", "22"] ┆ ["12", "10"]     │
│ Station 4 ┆ ["E2", "E0", … "6"]  ┆ ["E2", "E0", "15"] ┆ ["17", "13", "6"]  ┆ ["17", "13"]     │
│ Station 5 ┆ ["14", "8", … "E1"]  ┆ ["14", "8", "E0"]  ┆ ["22", "24", "E1"] ┆ ["22", "24"]     │
└───────────┴──────────────────────┴────────────────────┴────────────────────┴──────────────────┘

列表中元素的逐个计算

如果我们需要识别给出最多错误的气象站,我们需要

  1. 尝试将测量值转换为数字;
  2. 按行计算列表中非数值(即 null 值)的数量;并且
  3. 将此输出列重命名为“errors”,以便我们轻松识别这些站点。

要执行这些步骤,我们需要对列表值中的每个测量值执行一次类型转换操作。函数 eval 用作对列表元素执行操作的入口点。在其中,您可以使用上下文 element 来单独引用列表的每个元素,然后您可以在该元素上使用任何 Polars 表达式

element

result = weather.with_columns(
    pl.col("temperatures")
    .list.eval(pl.element().cast(pl.Int64, strict=False).is_null())
    .list.sum()
    .alias("errors"),
)
print(result)

element

// Contribute the Rust translation of the Python example by opening a PR.

shape: (5, 3)
┌───────────┬──────────────────────┬────────┐
│ station   ┆ temperatures         ┆ errors │
│ ---       ┆ ---                  ┆ ---    │
│ str       ┆ list[str]            ┆ u32    │
╞═══════════╪══════════════════════╪════════╡
│ Station 1 ┆ ["20", "5", … "20"]  ┆ 1      │
│ Station 2 ┆ ["18", "8", … "40"]  ┆ 4      │
│ Station 3 ┆ ["19", "24", … "22"] ┆ 1      │
│ Station 4 ┆ ["E2", "E0", … "6"]  ┆ 3      │
│ Station 5 ┆ ["14", "8", … "E1"]  ┆ 2      │
└───────────┴──────────────────────┴────────┘

另一种替代方法是使用正则表达式检查测量值是否以字母开头

element

result2 = weather.with_columns(
    pl.col("temperatures")
    .list.eval(pl.element().str.contains("(?i)[a-z]"))
    .list.sum()
    .alias("errors"),
)
print(result.equals(result2))

element

// Contribute the Rust translation of the Python example by opening a PR.

True

如果您不熟悉 str 命名空间或正则表达式中的 (?i) 符号,现在是了解如何在 Polars 中使用字符串和正则表达式的好时机。

逐行计算

函数 eval 让我们能够访问列表元素,pl.element 指的是每个单独的元素,但我们也可以使用 pl.all() 来指代列表中的所有元素。

为了实际演示这一点,我们首先创建一个包含更多天气数据的新 DataFrame

weather_by_day = pl.DataFrame(
    {
        "station": [f"Station {idx}" for idx in range(1, 11)],
        "day_1": [17, 11, 8, 22, 9, 21, 20, 8, 8, 17],
        "day_2": [15, 11, 10, 8, 7, 14, 18, 21, 15, 13],
        "day_3": [16, 15, 24, 24, 8, 23, 19, 23, 16, 10],
    }
)
print(weather_by_day)
// Contribute the Rust translation of the Python example by opening a PR.
shape: (10, 4)
┌────────────┬───────┬───────┬───────┐
│ station    ┆ day_1 ┆ day_2 ┆ day_3 │
│ ---        ┆ ---   ┆ ---   ┆ ---   │
│ str        ┆ i64   ┆ i64   ┆ i64   │
╞════════════╪═══════╪═══════╪═══════╡
│ Station 1  ┆ 17    ┆ 15    ┆ 16    │
│ Station 2  ┆ 11    ┆ 11    ┆ 15    │
│ Station 3  ┆ 8     ┆ 10    ┆ 24    │
│ Station 4  ┆ 22    ┆ 8     ┆ 24    │
│ Station 5  ┆ 9     ┆ 7     ┆ 8     │
│ Station 6  ┆ 21    ┆ 14    ┆ 23    │
│ Station 7  ┆ 20    ┆ 18    ┆ 19    │
│ Station 8  ┆ 8     ┆ 21    ┆ 23    │
│ Station 9  ┆ 8     ┆ 15    ┆ 16    │
│ Station 10 ┆ 17    ┆ 13    ┆ 10    │
└────────────┴───────┴───────┴───────┘

现在,我们将计算各站点每日温度的百分比排名。Polars 不直接提供此功能,但由于表达式非常灵活,我们可以创建自己的最高温度百分比排名表达式。我们来试一下

element · rank

rank_pct = (pl.element().rank(descending=True) / pl.element().count()).round(2)

result = weather_by_day.with_columns(
    # create the list of homogeneous data
    pl.concat_list(pl.all().exclude("station")).alias("all_temps")
).select(
    # select all columns except the intermediate list
    pl.all().exclude("all_temps"),
    # compute the rank by calling `list.eval`
    pl.col("all_temps").list.eval(rank_pct, parallel=True).alias("temps_rank"),
)

print(result)

element · rank

// Contribute the Rust translation of the Python example by opening a PR.

shape: (10, 5)
┌────────────┬───────┬───────┬───────┬────────────────────┐
│ station    ┆ day_1 ┆ day_2 ┆ day_3 ┆ temps_rank         │
│ ---        ┆ ---   ┆ ---   ┆ ---   ┆ ---                │
│ str        ┆ i64   ┆ i64   ┆ i64   ┆ list[f64]          │
╞════════════╪═══════╪═══════╪═══════╪════════════════════╡
│ Station 1  ┆ 17    ┆ 15    ┆ 16    ┆ [0.33, 1.0, 0.67]  │
│ Station 2  ┆ 11    ┆ 11    ┆ 15    ┆ [0.83, 0.83, 0.33] │
│ Station 3  ┆ 8     ┆ 10    ┆ 24    ┆ [1.0, 0.67, 0.33]  │
│ Station 4  ┆ 22    ┆ 8     ┆ 24    ┆ [0.67, 1.0, 0.33]  │
│ Station 5  ┆ 9     ┆ 7     ┆ 8     ┆ [0.33, 1.0, 0.67]  │
│ Station 6  ┆ 21    ┆ 14    ┆ 23    ┆ [0.67, 1.0, 0.33]  │
│ Station 7  ┆ 20    ┆ 18    ┆ 19    ┆ [0.33, 1.0, 0.67]  │
│ Station 8  ┆ 8     ┆ 21    ┆ 23    ┆ [1.0, 0.67, 0.33]  │
│ Station 9  ┆ 8     ┆ 15    ┆ 16    ┆ [1.0, 0.67, 0.33]  │
│ Station 10 ┆ 17    ┆ 13    ┆ 10    ┆ [0.33, 0.67, 1.0]  │
└────────────┴───────┴───────┴───────┴────────────────────┘

使用数组

创建数组列

正如我们上面所见,Polars 通常不会自动推断 Array 数据类型。您必须在创建 Series/DataFrame 时指定 Array 数据类型,或者显式转换列,除非您从 NumPy 数组创建列。

arr 命名空间

Array 数据类型是最近才引入的,其提供的功能仍处于起步阶段。即便如此,arr 命名空间仍聚合了您可用于处理数组的几个函数。

以前是 arr,现在是 list

在 Polars 的早期版本中,列表操作的命名空间曾是 arr。现在 arrArray 数据类型的命名空间。如果您在 StackOverflow 或其他来源上找到对 arr 命名空间的引用,请注意这些来源*可能*已过时。

API 文档应该能让您很好地了解 arr 命名空间中的函数,我们在此介绍几个

arr namespace

df = pl.DataFrame(
    {
        "first_last": [
            ["Anne", "Adams"],
            ["Brandon", "Branson"],
            ["Camila", "Campbell"],
            ["Dennis", "Doyle"],
        ],
        "fav_numbers": [
            [42, 0, 1],
            [2, 3, 5],
            [13, 21, 34],
            [73, 3, 7],
        ],
    },
    schema={
        "first_last": pl.Array(pl.String, 2),
        "fav_numbers": pl.Array(pl.Int32, 3),
    },
)

result = df.select(
    pl.col("first_last").arr.join(" ").alias("name"),
    pl.col("fav_numbers").arr.sort(),
    pl.col("fav_numbers").arr.max().alias("largest_fav"),
    pl.col("fav_numbers").arr.sum().alias("summed"),
    pl.col("fav_numbers").arr.contains(3).alias("likes_3"),
)
print(result)

`arr namespace` · 可用在 dtype-array 特性标志下

// Contribute the Rust translation of the Python example by opening a PR.

shape: (4, 5)
┌─────────────────┬───────────────┬─────────────┬────────┬─────────┐
│ name            ┆ fav_numbers   ┆ largest_fav ┆ summed ┆ likes_3 │
│ ---             ┆ ---           ┆ ---         ┆ ---    ┆ ---     │
│ str             ┆ array[i32, 3] ┆ i32         ┆ i32    ┆ bool    │
╞═════════════════╪═══════════════╪═════════════╪════════╪═════════╡
│ Anne Adams      ┆ [0, 1, 42]    ┆ 42          ┆ 43     ┆ false   │
│ Brandon Branson ┆ [2, 3, 5]     ┆ 5           ┆ 10     ┆ true    │
│ Camila Campbell ┆ [13, 21, 34]  ┆ 34          ┆ 68     ┆ false   │
│ Dennis Doyle    ┆ [3, 7, 73]    ┆ 73          ┆ 83     ┆ true    │
└─────────────────┴───────────────┴─────────────┴────────┴─────────┘