表达式和上下文

Polars 开发了自己的领域特定语言(DSL)来转换数据. 该语言非常易于使用, 并允许进行复杂的查询, 同时保持人类可读性. 表达式和上下文(将在此处介绍)对于实现这种可读性非常重要, 同时还允许Polars查询引擎优化您的查询以使其尽可能快地运行。

表达式

在Polars中,表达式是数据转换的惰性表示形式. 表达式是模块化且灵活的, 这意味着可以将它们用作构建块来构建更复杂的表达式。 以下是 Polars 表达式的基本示例:

Python
1import polars as pl
2
3pl.col("weight") / (pl.col("height") ** 2)

此表达式采用名为weight的列,并将其值除以height列中值的平方,从而计算出一个人的 BMI。 上面的代码表达了一种抽象的计算,我们可以将其保存在变量中进一步操作,或者只是打印:

Python
1bmi_expr = pl.col("weight") / (pl.col("height") ** 2)
2print(bmi_expr)
1[(col("weight")) / (col("height").pow([dyn int: 2]))]

因为表达式是惰性的,所以还没有发生任何计算。这就是我们需要上下文的原因。

上下文

Polars 表达式需要一个上下文来执行并产生结果。根据所使用的上下文,相同的 Polars 表达式可能会产生不同的结果。 常见的上下文有:

  1. select
  2. with_columns
  3. filter
  4. group_by

准备数据

Python
1from datetime import date
2
3df = pl.DataFrame(
4    {
5        "name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
6        "birthdate": [
7            date(1997, 1, 10),
8            date(1985, 2, 15),
9            date(1983, 3, 22),
10            date(1981, 4, 30),
11        ],
12        "weight": [57.9, 72.5, 53.6, 83.1],  # (kg)
13        "height": [1.56, 1.77, 1.65, 1.75],  # (m)
14    }
15)
16
17print(df)
1shape: (4, 4)
2┌────────────────┬────────────┬────────┬────────┐
3│ name           ┆ birthdate  ┆ weight ┆ height │
4│ ---            ┆ ---        ┆ ---    ┆ ---    │
5│ str            ┆ date       ┆ f64    ┆ f64    │
6╞════════════════╪════════════╪════════╪════════╡
7│ Alice Archer   ┆ 1997-01-10 ┆ 57.91.568│ Ben Brown      ┆ 1985-02-15 ┆ 72.51.779│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.61.6510│ Daniel Donovan ┆ 1981-04-30 ┆ 83.11.7511└────────────────┴────────────┴────────┴────────┘

select

select上下文将表达式应用于列,可能会生成新的列,这些列可以是聚合后的结果、其他列组合的结果或常量字符串:

Python
1bmi_expr = pl.col("weight") / (pl.col("height") ** 2)
2
3result = df.select(
4    bmi=bmi_expr,
5    avg_bmi=bmi_expr.mean(),
6    ideal_max_bmi=25,
7)
8print(result)
1shape: (4, 3)
2┌───────────┬───────────┬───────────────┐
3│ bmi       ┆ avg_bmi   ┆ ideal_max_bmi │
4│ ---       ┆ ---       ┆ ---           │
5│ f64       ┆ f64       ┆ i32           │
6╞═══════════╪═══════════╪═══════════════╡
723.79191323.43897325823.14149823.43897325919.68778723.438973251027.13469423.4389732511└───────────┴───────────┴───────────────┘

select上下文中的表达式必须生成长度相同的序列,或者必须生成一个标量。标量将被广播以匹配剩余序列的长度。文字(例如上面使用的数字)也会被广播,广播也可以在表达式中发生。(所谓广播,简单理解就是会和该列的每一个值都进行操作)

Python
1result = df.select(deviation=(bmi_expr - bmi_expr.mean()) / bmi_expr.std())
2print(result)
1shape: (4, 1)
2┌───────────┐
3│ deviation │
4│ ---       │
5│ f64       │
6╞═══════════╡
7│ 0.115645  │
8│ -0.097471 │
9│ -1.22912  │
10│ 1.210946  │
11└───────────┘

选择单列

  • 直接使用字符串: df.select("name")
  • 使用列表: df.select(["name"])
  • 使用元组: df.select(("name",))
  • 使用pl.col(): df.select(pl.col("name"))

它们实现的效果一样,如下所示:

1shape: (4, 1)
2┌────────────────┐
3│ name           │
4│ ---            │
5│ str            │
6╞════════════════╡
7│ Alice Archer   │
8│ Ben Brown      │
9│ Chloe Cooper   │
10│ Daniel Donovan │
11└────────────────┘

选择多列

  • 使用列表: df.select(["name","weight"])
  • 使用元组: df.select(("name","weight"))
  • 使用pl.col(str): df.select([pl.col("name"),pl.col("weight")])
  • 使用pl.col(str): df.select(pl.col("name"),pl.col("weight"))
  • 使用pl.col(tuple): df.select(pl.col("name","weight"))
  • 使用pl.col(list): df.select(pl.col(["name","weight"]))
1shape: (4, 2)
2┌────────────────┬────────┐
3│ name           ┆ weight │
4│ ---            ┆ ---    │
5│ str            ┆ f64    │
6╞════════════════╪════════╡
7│ Alice Archer   ┆ 57.9   │
8│ Ben Brown      ┆ 72.5   │
9│ Chloe Cooper   ┆ 53.6   │
10│ Daniel Donovan ┆ 83.1   │
11└────────────────┴────────┘

重命名

WARNING

下面示例一、二中新列的名称new_name不能添加双引号进行包裹, 这是语法要求。

  • df.select(new_name=pl.col("name"))
  • df.select(new_name="name")
  • df.select(pl.col("name").alias("new_name"))
1shape: (4, 1)
2┌────────────────┐
3│ new_name       │
4│ ---            │
5│ str            │
6╞════════════════╡
7│ Alice Archer   │
8│ Ben Brown      │
9│ Chloe Cooper   │
10│ Daniel Donovan │
11└────────────────┘

with_columns

用来新增一或多列。

TIP
  • with_columns与select非常相似, 主要区别在于: with_columns用来产生列;select用来选择列。
  • with_columns产生的新DataFrame包含原列以及新增列;select产生的新DataFrame只会包含选择的列。
  • 如果新增的列和原来的列重名, 则会覆盖原来的列。

新增列

Python
1result = df.with_columns(
2    bmi=bmi_expr,
3    avg_bmi=bmi_expr.mean(),
4    ideal_max_bmi=25,
5)
6print(result)
1shape: (4, 7)
2┌────────────────┬────────────┬────────┬────────┬───────────┬───────────┬───────────────┐
3│ name           ┆ birthdate  ┆ weight ┆ height ┆ bmi       ┆ avg_bmi   ┆ ideal_max_bmi │
4│ ---            ┆ ---        ┆ ---    ┆ ---    ┆ ---       ┆ ---       ┆ ---           │
5│ str            ┆ date       ┆ f64    ┆ f64    ┆ f64       ┆ f64       ┆ i32           │
6╞════════════════╪════════════╪════════╪════════╪═══════════╪═══════════╪═══════════════╡
7│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   ┆ 23.791913 ┆ 23.438973 ┆ 25            │
8│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   ┆ 23.141498 ┆ 23.438973 ┆ 25            │
9│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   ┆ 19.687787 ┆ 23.438973 ┆ 25            │
10│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   ┆ 27.134694 ┆ 23.438973 ┆ 25            │
11└────────────────┴────────────┴────────┴────────┴───────────┴───────────┴───────────────┘

列名定义-alias

Python
1df = df.with_columns(
2    pl.col("name").alias("full_name")
3)
4print(df)
1shape: (4, 5)
2┌────────────────┬────────────┬────────┬────────┬────────────────┐
3│ name           ┆ birthdate  ┆ weight ┆ height ┆ full_name      │
4│ ---            ┆ ---        ┆ ---    ┆ ---    ┆ ---            │
5│ str            ┆ date       ┆ f64    ┆ f64    ┆ str            │
6╞════════════════╪════════════╪════════╪════════╪════════════════╡
7│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   ┆ Alice Archer   │
8│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   ┆ Ben Brown      │
9│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   ┆ Chloe Cooper   │
10│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   ┆ Daniel Donovan │
11└────────────────┴────────────┴────────┴────────┴────────────────┘

使用标量

Python
1df = df.with_columns(
2    pl.lit("1").alias("type_str")
3)
4df = df.with_columns(
5    pl.lit(42).alias("type_num")
6)
7
8df = df.with_columns(
9    pl.lit("1",dtype=pl.UInt8).alias("type_str_int")
10)
11
12print(df)
1shape: (4, 7)
2┌────────────────┬────────────┬────────┬────────┬──────────┬──────────┬──────────────┐
3│ name           ┆ birthdate  ┆ weight ┆ height ┆ type_str ┆ type_num ┆ type_str_int │
4│ ---            ┆ ---        ┆ ---    ┆ ---    ┆ ---      ┆ ---      ┆ ---          │
5│ str            ┆ date       ┆ f64    ┆ f64    ┆ str      ┆ i32      ┆ u8           │
6╞════════════════╪════════════╪════════╪════════╪══════════╪══════════╪══════════════╡
7│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   ┆ 1        ┆ 42       ┆ 1            │
8│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   ┆ 1        ┆ 42       ┆ 1            │
9│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   ┆ 1        ┆ 42       ┆ 1            │
10│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   ┆ 1        ┆ 42       ┆ 1            │
11└────────────────┴────────────┴────────┴────────┴──────────┴──────────┴──────────────┘

filter

上下文filter根据一个或多个计算结果为布尔数据类型的表达式来过滤数据框的行。

Python
1result = df.filter(
2    pl.col("birthdate").is_between(date(1982, 12, 31), date(1996, 1, 1)),
3    pl.col("height") > 1.7,
4)
5print(result)
1shape: (1, 4)
2┌───────────┬────────────┬────────┬────────┐
3│ name      ┆ birthdate  ┆ weight ┆ height │
4│ ---       ┆ ---        ┆ ---    ┆ ---    │
5│ str       ┆ date       ┆ f64    ┆ f64    │
6╞═══════════╪════════════╪════════╪════════╡
7│ Ben Brown ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
8└───────────┴────────────┴────────┴────────┘
等价替换
Python
1import datetime as dt
2
3result = df.filter(
4    (pl.col("birthdate").is_between(dt.date(1982, 12, 31), dt.date(1996, 1, 1))) & (pl.col("height") > 1.7)
5)

分组(group_by)和聚合(agg)

在上下文group_by中,行根据分组表达式的唯一值进行分组。然后通过agg函数可以操作分组后的结果。

Python
1result = df.group_by(
2    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
3).agg(pl.col("name"))
4print(result)
1shape: (2, 2)
2┌────────┬─────────────────────────────────┐
3│ decade ┆ name                            │
4│ ---    ┆ ---                             │
5│ i32    ┆ list[str]                       │
6╞════════╪═════════════════════════════════╡
7│ 1990   ┆ ["Alice Archer"]                │
8│ 1980   ┆ ["Ben Brown", "Chloe Cooper", … │
9└────────┴─────────────────────────────────┘

在上面的示例中,统计了每十年出生的人名。

还可以指定任意数量的分组表达式,group_by会根据指定表达式中的不同值对行进行分组。根据出生年代和身高是否低于1.7米进行分组:

Python
1result = df.group_by(
2    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
3    (pl.col("height") < 1.7).alias("short?"),
4).agg(pl.col("name"))
5print(result)
1shape: (3, 3)
2┌────────┬────────┬─────────────────────────────────┐
3│ decade ┆ short? ┆ name                            │
4│ ---    ┆ ---    ┆ ---                             │
5│ i32    ┆ bool   ┆ list[str]                       │
6╞════════╪════════╪═════════════════════════════════╡
7│ 1990   ┆ true   ┆ ["Alice Archer"]                │
8│ 1980   ┆ false  ┆ ["Ben Brown", "Daniel Donovan"… │
9│ 1980   ┆ true   ┆ ["Chloe Cooper"]                │
10└────────┴────────┴─────────────────────────────────┘

分组聚合后生成的DataFrame,在左侧是每个分组表达式各占一列,接着是聚合表达式的结果。可以根据需要指定任意数量的聚合表达式:

Python
1result = df.group_by(
2    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
3    (pl.col("height") < 1.7).alias("short?"),
4).agg(
5    pl.len(),
6    pl.col("height").max().alias("tallest"),
7    pl.col("weight").mean().alias("avg_weight"),
8    pl.col("height").mean().alias("avg_height"),
9)
10print(result)
1shape: (3, 6)
2┌────────┬────────┬─────┬─────────┬────────────┬────────────┐
3│ decade ┆ short? ┆ len ┆ tallest ┆ avg_weight ┆ avg_height │
4│ ---    ┆ ---    ┆ --- ┆ ---     ┆ ---        ┆ ---        │
5│ i32    ┆ bool   ┆ u32 ┆ f64     ┆ f64        ┆ f64        │
6╞════════╪════════╪═════╪═════════╪════════════╪════════════╡
7│ 1980   ┆ false  ┆ 2   ┆ 1.77    ┆ 77.8       ┆ 1.76       │
8│ 1990   ┆ true   ┆ 1   ┆ 1.56    ┆ 57.9       ┆ 1.56       │
9│ 1980   ┆ true   ┆ 1   ┆ 1.65    ┆ 53.6       ┆ 1.65       │
10└────────┴────────┴─────┴─────────┴────────────┴────────────┘

表达式扩展

上面最后一个例子包含两个分组表达式和四个聚合表达式。仔细观察就会发现,最后两个聚合表达式涉及到了两个不同的列:“weight”和“height”。但它们的计算处理逻辑是相同,是否可以将其合成一个或者简写?

Polars表达式支持一项称为表达式扩展的功能,表达式扩展就像是将相同转换应用于多个列时的简写符号。正如上面示例,表达式:

Python
1[
2    pl.col("weight").mean().alias("avg_weight"),
3    pl.col("height").mean().alias("avg_height"),
4]

可以简写为:

Python
1pl.col("weight", "height").mean().name.prefix("avg_")
最后的示例可以简写为
Python
1result = df.group_by(
2    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
3    (pl.col("height") < 1.7).alias("short?"),
4).agg(
5    pl.len(),
6    pl.col("height").max().alias("tallest"),
7    pl.col("weight", "height").mean().name.prefix("avg_"),
8)
9print(result)

该表达式会展开为两个独立的表达式,Polar 可以并行执行它们。但在有些情况下,有可能无法预先知道一个表达式会展开成多少个独立的表达式,那么可以通过数据类型进行扩展,如下例子:

Python
1(pl.col(pl.Float64) * 1.1).name.suffix("*1.1")

此表达式将把所有数据类型为Float64的列乘以1.1。具体示例:

Python
1expr = (pl.col(pl.Float64) * 1.1).name.suffix("*1.1")
2result = df.select(expr)
3print(result)
1shape: (4, 2)
2┌────────────┬────────────┐
3│ weight*1.1 ┆ height*1.1 │
4│ ---        ┆ ---        │
5│ f64        ┆ f64        │
6╞════════════╪════════════╡
7│ 63.69      ┆ 1.716      │
8│ 79.75      ┆ 1.947      │
9│ 58.96      ┆ 1.815      │
10│ 91.41      ┆ 1.925      │
11└────────────┴────────────┘

如果DataFrame中不存在Float64类型的数据,那么结果将会是一个空的:

1shape: (0, 0)
2┌┐
3╞╡
4└┘