跳到内容

结构体

Struct 数据类型是一种复合数据类型,可以在单个列中存储多个字段。

Python 类比

对于 Python 用户来说,Struct 数据类型有点像 Python 字典。更棒的是,如果您熟悉 Python 类型提示,可以将 Struct 数据类型视为 typing.TypedDict

在本用户指南页面中,我们将了解 Struct 数据类型出现的情况,理解其出现的原因,并学习如何使用 Struct 值。

让我们从一个数据框开始,该数据框记录了美国某些州几部电影的平均评分

DataFrame

import polars as pl

ratings = pl.DataFrame(
    {
        "Movie": ["Cars", "IT", "ET", "Cars", "Up", "IT", "Cars", "ET", "Up", "Cars"],
        "Theatre": ["NE", "ME", "IL", "ND", "NE", "SD", "NE", "IL", "IL", "NE"],
        "Avg_Rating": [4.5, 4.4, 4.6, 4.3, 4.8, 4.7, 4.5, 4.9, 4.7, 4.6],
        "Count": [30, 27, 26, 29, 31, 28, 28, 26, 33, 28],
    }
)
print(ratings)

DataFrame

use polars::prelude::*;
let ratings = df!(
        "Movie"=> ["Cars", "IT", "ET", "Cars", "Up", "IT", "Cars", "ET", "Up", "Cars"],
        "Theatre"=> ["NE", "ME", "IL", "ND", "NE", "SD", "NE", "IL", "IL", "NE"],
        "Avg_Rating"=> [4.5, 4.4, 4.6, 4.3, 4.8, 4.7, 4.5, 4.9, 4.7, 4.6],
        "Count"=> [30, 27, 26, 29, 31, 28, 28, 26, 33, 28],

)?;
println!("{}", &ratings);

shape: (10, 4)
┌───────┬─────────┬────────────┬───────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count │
│ ---   ┆ ---     ┆ ---        ┆ ---   │
│ str   ┆ str     ┆ f64        ┆ i64   │
╞═══════╪═════════╪════════════╪═══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    │
│ IT    ┆ ME      ┆ 4.4        ┆ 27    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    │
│ Cars  ┆ ND      ┆ 4.3        ┆ 29    │
│ Up    ┆ NE      ┆ 4.8        ┆ 31    │
│ IT    ┆ SD      ┆ 4.7        ┆ 28    │
│ Cars  ┆ NE      ┆ 4.5        ┆ 28    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    │
│ Up    ┆ IL      ┆ 4.7        ┆ 33    │
│ Cars  ┆ NE      ┆ 4.6        ┆ 28    │
└───────┴─────────┴────────────┴───────┘

遇到 Struct 数据类型

一个常见的操作会导致生成 Struct 列,那就是在探索性数据分析中常用的 value_counts 函数。检查某个州在数据中出现的次数可以这样做:

value_counts

result = ratings.select(pl.col("Theatre").value_counts(sort=True))
print(result)

value_counts · 在功能 dtype-struct 上可用

let result = ratings
    .clone()
    .lazy()
    .select([col("Theatre").value_counts(true, true, "count", false)])
    .collect()?;
println!("{result}");

shape: (5, 1)
┌───────────┐
│ Theatre   │
│ ---       │
│ struct[2] │
╞═══════════╡
│ {"NE",4}  │
│ {"IL",3}  │
│ {"ME",1}  │
│ {"ND",1}  │
│ {"SD",1}  │
└───────────┘

这是一个相当出乎意料的输出,特别是如果您来自没有这种数据类型的工具。不过,我们没有危险。要恢复到更熟悉的输出,我们所需要做的就是在 Struct 列上使用 unnest 函数。

unnest

result = ratings.select(pl.col("Theatre").value_counts(sort=True)).unnest("Theatre")
print(result)

unnest

let result = ratings
    .clone()
    .lazy()
    .select([col("Theatre").value_counts(true, true, "count", false)])
    .unnest(["Theatre"])
    .collect()?;
println!("{result}");

shape: (5, 2)
┌─────────┬───────┐
│ Theatre ┆ count │
│ ---     ┆ ---   │
│ str     ┆ u32   │
╞═════════╪═══════╡
│ NE      ┆ 4     │
│ IL      ┆ 3     │
│ ME      ┆ 1     │
│ ND      ┆ 1     │
│ SD      ┆ 1     │
└─────────┴───────┘

unnest 函数会将 Struct 的每个字段转换为单独的列。

为什么 value_counts 返回 Struct

Polars 表达式总是对单个 Series 进行操作并返回另一个 Series。Struct 是一种数据类型,它允许我们将多列作为表达式的输入,或者从表达式中输出多列。因此,当我们使用 value_counts 时,我们可以使用 Struct 数据类型来指定每个值及其计数。

从字典推断 Struct 数据类型

在构建 Series 或 DataFrame 时,Polars 会将字典转换为 Struct 数据类型。

Series

rating_series = pl.Series(
    "ratings",
    [
        {"Movie": "Cars", "Theatre": "NE", "Avg_Rating": 4.5},
        {"Movie": "Toy Story", "Theatre": "ME", "Avg_Rating": 4.9},
    ],
)
print(rating_series)

Series

// Don't think we can make it the same way in rust, but this works
let rating_series = df!(
    "Movie" => &["Cars", "Toy Story"],
    "Theatre" => &["NE", "ME"],
    "Avg_Rating" => &[4.5, 4.9],
)?
.into_struct("ratings".into())
.into_series();
println!("{}", &rating_series);

shape: (2,)
Series: 'ratings' [struct[3]]
[
    {"Cars","NE",4.5}
    {"Toy Story","ME",4.9}
]

字段的数量、名称和类型都是从第一个遇到的字典中推断出来的。随后的不一致可能导致 null 值或错误。

Series

null_rating_series = pl.Series(
    "ratings",
    [
        {"Movie": "Cars", "Theatre": "NE", "Avg_Rating": 4.5},
        {"Mov": "Toy Story", "Theatre": "ME", "Avg_Rating": 4.9},
        {"Movie": "Snow White", "Theatre": "IL", "Avg_Rating": "4.7"},
    ],
    strict=False,  # To show the final structs with `null` values.
)
print(null_rating_series)

Series

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

shape: (3,)
Series: 'ratings' [struct[4]]
[
    {"Cars","NE","4.5",null}
    {null,"ME","4.9","Toy Story"}
    {"Snow White","IL","4.7",null}
]

提取 Struct 的单个值

假设我们需要从上面创建的 Series 中的 Struct 中只获取字段 "Movie"。我们可以使用 field 函数来实现这一点。

struct.field

result = rating_series.struct.field("Movie")
print(result)

struct.field_by_name

let result = rating_series.struct_()?.field_by_name("Movie")?;
println!("{result}");

shape: (2,)
Series: 'Movie' [str]
[
    "Cars"
    "Toy Story"
]

重命名 Struct 的单个字段

如果我们需要重命名 Struct 列中的单个字段怎么办?我们使用 rename_fields 函数。

struct.rename_fields

result = rating_series.struct.rename_fields(["Film", "State", "Value"])
print(result)

struct.rename_fields

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

shape: (2,)
Series: 'ratings' [struct[3]]
[
    {"Cars","NE",4.5}
    {"Toy Story","ME",4.9}
]

为了实际看到字段名称已更改,我们将创建一个数据框,其中唯一列是结果,然后使用 unnest 函数,使每个字段成为自己的列。列名将反映我们刚刚执行的重命名操作。

struct.rename_fields

print(
    result.to_frame().unnest("ratings"),
)

struct.rename_fields

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

shape: (2, 3)
┌───────────┬───────┬───────┐
│ Film      ┆ State ┆ Value │
│ ---       ┆ ---   ┆ ---   │
│ str       ┆ str   ┆ f64   │
╞═══════════╪═══════╪═══════╡
│ Cars      ┆ NE    ┆ 4.5   │
│ Toy Story ┆ ME    ┆ 4.9   │
└───────────┴───────┴───────┘

Struct 列的实际用例

识别重复行

让我们回到 ratings 数据。我们想识别在“电影”和“影院”级别上存在重复的情况。

这就是 Struct 数据类型大放异彩的地方

is_duplicated · struct

result = ratings.filter(pl.struct("Movie", "Theatre").is_duplicated())
print(result)

is_duplicated · Struct · 在功能 dtype-struct 上可用

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

shape: (5, 4)
┌───────┬─────────┬────────────┬───────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count │
│ ---   ┆ ---     ┆ ---        ┆ ---   │
│ str   ┆ str     ┆ f64        ┆ i64   │
╞═══════╪═════════╪════════════╪═══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    │
│ Cars  ┆ NE      ┆ 4.5        ┆ 28    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    │
│ Cars  ┆ NE      ┆ 4.6        ┆ 28    │
└───────┴─────────┴────────────┴───────┘

我们也可以使用 is_unique 在此级别识别唯一情况!

多列排名

假设我们知道存在重复项,我们希望选择哪个评分具有更高的优先级。我们可以说“Count”列是最重要的,如果“Count”列出现平局,则我们考虑“Avg_Rating”列。

然后我们可以这样做

is_duplicated · struct

result = ratings.with_columns(
    pl.struct("Count", "Avg_Rating")
    .rank("dense", descending=True)
    .over("Movie", "Theatre")
    .alias("Rank")
).filter(pl.struct("Movie", "Theatre").is_duplicated())

print(result)

is_duplicated · Struct · 在功能 dtype-struct 上可用

let result = ratings
    .clone()
    .lazy()
    .with_columns([as_struct(vec![col("Count"), col("Avg_Rating")])
        .rank(
            RankOptions {
                method: RankMethod::Dense,
                descending: true,
            },
            None,
        )
        .over([col("Movie"), col("Theatre")])
        .alias("Rank")])
    // .filter(as_struct(&[col("Movie"), col("Theatre")]).is_duplicated())
    // Error: .is_duplicated() not available if you try that
    // https://github.com/pola-rs/polars/issues/3803
    .filter(len().over([col("Movie"), col("Theatre")]).gt(lit(1)))
    .collect()?;
println!("{result}");

shape: (5, 5)
┌───────┬─────────┬────────────┬───────┬──────┐
│ Movie ┆ Theatre ┆ Avg_Rating ┆ Count ┆ Rank │
│ ---   ┆ ---     ┆ ---        ┆ ---   ┆ ---  │
│ str   ┆ str     ┆ f64        ┆ i64   ┆ u32  │
╞═══════╪═════════╪════════════╪═══════╪══════╡
│ Cars  ┆ NE      ┆ 4.5        ┆ 30    ┆ 1    │
│ ET    ┆ IL      ┆ 4.6        ┆ 26    ┆ 2    │
│ Cars  ┆ NE      ┆ 4.5        ┆ 28    ┆ 3    │
│ ET    ┆ IL      ┆ 4.9        ┆ 26    ┆ 1    │
│ Cars  ┆ NE      ┆ 4.6        ┆ 28    ┆ 2    │
└───────┴─────────┴────────────┴───────┴──────┘

这是一组相当复杂的需求,在 Polars 中优雅地实现了!要了解更多关于上面使用的 over 函数,请参阅用户指南中关于窗口函数的章节

在单个表达式中使用多列

如前所述,如果您需要将多列作为输入传递给表达式,Struct 数据类型也很有用。例如,假设我们想在数据框的两列上计算阿克曼函数。目前无法通过组合 Polars 表达式来计算阿克曼函数1,因此我们定义了一个自定义函数

def ack(m, n):
    if not m:
        return n + 1
    if not n:
        return ack(m - 1, 1)
    return ack(m - 1, ack(m, n - 1))
// Contribute the Rust translation of the Python example by opening a PR.

现在,要计算这些参数上的阿克曼函数值,我们首先创建一个包含字段 mnStruct,然后使用 map_elements 函数将 ack 函数应用于每个值。

map_elements

values = pl.DataFrame(
    {
        "m": [0, 0, 0, 1, 1, 1, 2],
        "n": [2, 3, 4, 1, 2, 3, 1],
    }
)
result = values.with_columns(
    pl.struct(["m", "n"])
    .map_elements(lambda s: ack(s["m"], s["n"]), return_dtype=pl.Int64)
    .alias("ack")
)

print(result)

// Contribute the Rust translation of the Python example by opening a PR.
shape: (7, 3)
┌─────┬─────┬─────┐
│ m   ┆ n   ┆ ack │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╡
│ 0   ┆ 2   ┆ 3   │
│ 0   ┆ 3   ┆ 4   │
│ 0   ┆ 4   ┆ 5   │
│ 1   ┆ 1   ┆ 3   │
│ 1   ┆ 2   ┆ 4   │
│ 1   ┆ 3   ┆ 5   │
│ 2   ┆ 1   ┆ 5   │
└─────┴─────┴─────┘

请参阅用户指南的这一部分,了解更多关于将用户定义的 Python 函数应用于数据的信息


  1. 声称某事无法完成是相当大胆的说法。如果您能证明我们错了,请告诉我们!