结构体
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 表达式总是对单个 Series 进行操作并返回另一个 Series。Struct
是一种数据类型,它允许我们将多列作为表达式的输入,或者从表达式中输出多列。因此,当我们使用 value_counts
时,我们可以使用 Struct
数据类型来指定每个值及其计数。
从字典推断 Struct
数据类型
在构建 Series 或 DataFrame 时,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)
// 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
值或错误。
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)
// 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
函数来实现这一点。
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)
// 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
函数,使每个字段成为自己的列。列名将反映我们刚刚执行的重命名操作。
print(
result.to_frame().unnest("ratings"),
)
// 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
数据类型大放异彩的地方
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”列。
然后我们可以这样做
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.
现在,要计算这些参数上的阿克曼函数值,我们首先创建一个包含字段 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 函数应用于数据的信息。
-
声称某事无法完成是相当大胆的说法。如果您能证明我们错了,请告诉我们! ↩