Skip to content

结构体

数据类型 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 表达式总是在单个系列上操作并返回另一个系列。 Struct 是一种数据类型,它允许我们将多个列作为表达式的输入,或从表达式中输出多个列。 因此,当我们使用 value_counts 时,可以使用数据类型 Struct 来指定每个值及其计数。

从字典推断数据类型 Struct

在构建系列或数据框时,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

// 不认为我们可以在rust中以相同的方式实现,但这种方式有效
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,  # 显示带有`null`值的最终结构。
)
print(null_rating_series)

Series

// 通过提交PR来贡献Python示例的Rust翻译。

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

提取Struct的单个值

假设我们需要从上面创建的系列中的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

// 通过提交PR来贡献Python示例的Rust翻译。

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

// 通过提交PR来贡献Python示例的Rust翻译。

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 上可用

// 通过提交 PR 来贡献 Python 示例的 Rust 翻译。

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())
    // 错误:如果你尝试这样做,.is_duplicated() 不可用
    // 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. 说某件事无法完成是一个相当大胆的声明。如果你证明我们错了,请告诉我们!