结构体
数据类型 Struct 是一种复合数据类型,可以在单个列中存储多个字段。
Python 类比
对于Python用户来说,数据类型Struct有点像Python的字典。更棒的是,如果你熟悉Python的类型系统,你可以将数据类型Struct视为typing.TypedDict。
在本用户指南页面中,我们将看到Struct数据类型出现的情况,我们将理解它为何会出现,并了解如何处理Struct值。
让我们从一个数据框开始,该数据框记录了美国一些州中几部电影的平均评分:
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)
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函数,通常用于探索性数据分析。检查一个州在数据中出现的次数是这样完成的:
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函数:
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:
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)
// 不认为我们可以在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值或错误:
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)
// 通过提交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来实现这一点:
result = rating_series.struct.field("Movie")
print(result)
let result = rating_series.struct_()?.field_by_name("Movie")?;
println!("{}", result);
shape: (2,)
Series: 'Movie' [str]
[
"Cars"
"Toy Story"
]
重命名Struct的单个字段
如果我们需要重命名Struct列的单个字段怎么办?我们使用函数
rename_fields:
result = rating_series.struct.rename_fields(["Film", "State", "Value"])
print(result)
// 通过提交PR来贡献Python示例的Rust翻译。
shape: (2,)
Series: 'ratings' [struct[3]]
[
{"Cars","NE",4.5}
{"Toy Story","ME",4.9}
]
为了能够实际看到字段名称被更改,我们将创建一个数据框,其中唯一的列是结果,然后我们使用函数unnest,以便每个字段成为其自己的列。列名将反映我们刚刚进行的重命名操作:
print(
result.to_frame().unnest("ratings"),
)
// 通过提交PR来贡献Python示例的Rust翻译。
shape: (2, 3)
┌───────────┬───────┬───────┐
│ Film ┆ State ┆ Value │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 │
╞═══════════╪═══════╪═══════╡
│ Cars ┆ NE ┆ 4.5 │
│ Toy Story ┆ ME ┆ 4.9 │
└───────────┴───────┴───────┘
Struct 列的实际用例
识别重复行
让我们回到ratings数据。我们想要识别在“电影”和“剧院”级别存在重复的情况。
这是数据类型 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”列。
然后我们可以这样做:
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.
现在,为了计算这些参数上的阿克曼函数的值,我们首先创建一个带有字段m和n的Struct,然后使用函数map_elements将函数ack应用于每个值:
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函数应用于您的数据。
-
说某件事无法完成是一个相当大胆的声明。如果你证明我们错了,请告诉我们!↩