Skip to content

表达式扩展

正如你在 关于表达式和上下文的章节中所见,表达式 扩展是一个功能,它使你能够编写一个可以扩展为多个 不同表达式的单一表达式,可能取决于使用该表达式的上下文的模式。

这个功能不仅仅是装饰性的或语法糖。它允许在代码中非常强大地应用 DRY原则:一个 指定多个列的单一表达式会扩展成一个表达式列表,这意味着你可以 编写一个单一的表达式并重用它所代表的计算。

在本节中,我们将展示几种表达式扩展的形式,并且我们将使用您可以在下面看到的数据框来实现这一效果:

import polars as pl

df = pl.DataFrame(
    {  # As of 14th October 2024, ~3pm UTC
        "ticker": ["AAPL", "NVDA", "MSFT", "GOOG", "AMZN"],
        "company_name": ["Apple", "NVIDIA", "Microsoft", "Alphabet (Google)", "Amazon"],
        "price": [229.9, 138.93, 420.56, 166.41, 188.4],
        "day_high": [231.31, 139.6, 424.04, 167.62, 189.83],
        "day_low": [228.6, 136.3, 417.52, 164.78, 188.44],
        "year_high": [237.23, 140.76, 468.35, 193.31, 201.2],
        "year_low": [164.08, 39.23, 324.39, 121.46, 118.35],
    }
)

print(df)
use polars::prelude::*;

// Data as of 14th October 2024, ~3pm UTC
let df = df!(
    "ticker" => ["AAPL", "NVDA", "MSFT", "GOOG", "AMZN"],
    "company_name" => ["Apple", "NVIDIA", "Microsoft", "Alphabet (Google)", "Amazon"],
    "price" => [229.9, 138.93, 420.56, 166.41, 188.4],
    "day_high" => [231.31, 139.6, 424.04, 167.62, 189.83],
    "day_low" => [228.6, 136.3, 417.52, 164.78, 188.44],
    "year_high" => [237.23, 140.76, 468.35, 193.31, 201.2],
    "year_low" => [164.08, 39.23, 324.39, 121.46, 118.35],
)?;

println!("{}", df);
shape: (5, 7)
┌────────┬───────────────────┬────────┬──────────┬─────────┬───────────┬──────────┐
│ ticker ┆ company_name      ┆ price  ┆ day_high ┆ day_low ┆ year_high ┆ year_low │
│ ---    ┆ ---               ┆ ---    ┆ ---      ┆ ---     ┆ ---       ┆ ---      │
│ str    ┆ str               ┆ f64    ┆ f64      ┆ f64     ┆ f64       ┆ f64      │
╞════════╪═══════════════════╪════════╪══════════╪═════════╪═══════════╪══════════╡
│ AAPL   ┆ Apple             ┆ 229.9  ┆ 231.31   ┆ 228.6   ┆ 237.23    ┆ 164.08   │
│ NVDA   ┆ NVIDIA            ┆ 138.93 ┆ 139.6    ┆ 136.3   ┆ 140.76    ┆ 39.23    │
│ MSFT   ┆ Microsoft         ┆ 420.56 ┆ 424.04   ┆ 417.52  ┆ 468.35    ┆ 324.39   │
│ GOOG   ┆ Alphabet (Google) ┆ 166.41 ┆ 167.62   ┆ 164.78  ┆ 193.31    ┆ 121.46   │
│ AMZN   ┆ Amazon            ┆ 188.4  ┆ 189.83   ┆ 188.44  ┆ 201.2     ┆ 118.35   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

函数 col

函数col是Polars中最常用的利用表达式扩展功能的方式。 通常用于引用数据框的一列,在本节中我们将探讨其他使用col(或其变体,在Rust中)的方式。

按列名显式展开

表达式扩展的最简单形式发生在您向函数col提供多个列名时。

下面的示例使用了一个函数 col 和多个列名,将美元值转换为欧元:

col

eur_usd_rate = 1.09  # 截至2024年10月14日

result = df.with_columns(
    (
        pl.col(
            "price",
            "day_high",
            "day_low",
            "year_high",
            "year_low",
        )
        / eur_usd_rate
    ).round(2)
)
print(result)

col

let eur_usd_rate = 1.09; // 截至2024年10月14日

let result = df
    .clone()
    .lazy()
    .with_column(
        (cols(["price", "day_high", "day_low", "year_high", "year_low"]) / lit(eur_usd_rate))
            .round(2),
    )
    .collect()?;
println!("{}", result);

shape: (5, 7)
┌────────┬───────────────────┬────────┬──────────┬─────────┬───────────┬──────────┐
│ ticker ┆ company_name      ┆ price  ┆ day_high ┆ day_low ┆ year_high ┆ year_low │
│ ---    ┆ ---               ┆ ---    ┆ ---      ┆ ---     ┆ ---       ┆ ---      │
│ str    ┆ str               ┆ f64    ┆ f64      ┆ f64     ┆ f64       ┆ f64      │
╞════════╪═══════════════════╪════════╪══════════╪═════════╪═══════════╪══════════╡
│ AAPL   ┆ Apple             ┆ 210.92 ┆ 212.21   ┆ 209.72  ┆ 217.64    ┆ 150.53   │
│ NVDA   ┆ NVIDIA            ┆ 127.46 ┆ 128.07   ┆ 125.05  ┆ 129.14    ┆ 35.99    │
│ MSFT   ┆ Microsoft         ┆ 385.83 ┆ 389.03   ┆ 383.05  ┆ 429.68    ┆ 297.61   │
│ GOOG   ┆ Alphabet (Google) ┆ 152.67 ┆ 153.78   ┆ 151.17  ┆ 177.35    ┆ 111.43   │
│ AMZN   ┆ Amazon            ┆ 172.84 ┆ 174.16   ┆ 172.88  ┆ 184.59    ┆ 108.58   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

当你列出你希望表达式扩展到的列名时,你可以预测表达式将扩展到什么。在这种情况下,进行货币转换的表达式被扩展为五个表达式的列表:

col

exprs = [
    (pl.col("price") / eur_usd_rate).round(2),
    (pl.col("day_high") / eur_usd_rate).round(2),
    (pl.col("day_low") / eur_usd_rate).round(2),
    (pl.col("year_high") / eur_usd_rate).round(2),
    (pl.col("year_low") / eur_usd_rate).round(2),
]

result2 = df.with_columns(exprs)
print(result.equals(result2))

col

let exprs = [
    (col("price") / lit(eur_usd_rate)).round(2),
    (col("day_high") / lit(eur_usd_rate)).round(2),
    (col("day_low") / lit(eur_usd_rate)).round(2),
    (col("year_high") / lit(eur_usd_rate)).round(2),
    (col("year_low") / lit(eur_usd_rate)).round(2),
];

let result2 = df.clone().lazy().with_columns(exprs).collect()?;
println!("{}", result.equals(&result2));

True

按数据类型扩展

在前面的例子中,我们必须输入五个列名,但函数col也可以方便地接受一个或多个数据类型。如果你提供数据类型而不是列名,表达式将扩展到所有匹配提供的数据类型的列。

下面的示例执行与之前完全相同的计算:

col

result = df.with_columns((pl.col(pl.Float64) / eur_usd_rate).round(2))
print(result)

dtype_col

let result = df
    .clone()
    .lazy()
    .with_column((dtype_col(&DataType::Float64) / lit(eur_usd_rate)).round(2))
    .collect()?;
println!("{}", result);

shape: (5, 7)
┌────────┬───────────────────┬────────┬──────────┬─────────┬───────────┬──────────┐
│ ticker ┆ company_name      ┆ price  ┆ day_high ┆ day_low ┆ year_high ┆ year_low │
│ ---    ┆ ---               ┆ ---    ┆ ---      ┆ ---     ┆ ---       ┆ ---      │
│ str    ┆ str               ┆ f64    ┆ f64      ┆ f64     ┆ f64       ┆ f64      │
╞════════╪═══════════════════╪════════╪══════════╪═════════╪═══════════╪══════════╡
│ AAPL   ┆ Apple             ┆ 210.92 ┆ 212.21   ┆ 209.72  ┆ 217.64    ┆ 150.53   │
│ NVDA   ┆ NVIDIA            ┆ 127.46 ┆ 128.07   ┆ 125.05  ┆ 129.14    ┆ 35.99    │
│ MSFT   ┆ Microsoft         ┆ 385.83 ┆ 389.03   ┆ 383.05  ┆ 429.68    ┆ 297.61   │
│ GOOG   ┆ Alphabet (Google) ┆ 152.67 ┆ 153.78   ┆ 151.17  ┆ 177.35    ┆ 111.43   │
│ AMZN   ┆ Amazon            ┆ 172.84 ┆ 174.16   ┆ 172.88  ┆ 184.59    ┆ 108.58   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

当我们使用带有表达式扩展的数据类型时,我们无法预先知道单个表达式将扩展到多少列。如果我们想要确定要应用的最终表达式列表,我们需要输入数据框的模式。

如果我们不确定价格列是Float64还是Float32类型,我们可以指定这两种数据类型:

col

result2 = df.with_columns(
    (
        pl.col(
            pl.Float32,
            pl.Float64,
        )
        / eur_usd_rate
    ).round(2)
)
print(result.equals(result2))

dtype_cols

let result2 = df
    .clone()
    .lazy()
    .with_column(
        (dtype_cols([DataType::Float32, DataType::Float64]) / lit(eur_usd_rate)).round(2),
    )
    .collect()?;
println!("{}", result.equals(&result2));

True

通过模式匹配进行扩展

你也可以使用正则表达式来指定用于匹配列名的模式。为了区分普通列名和通过模式匹配扩展的列名,正则表达式分别以^$开始和结束。这也意味着模式必须与整个列名字符串匹配。

正则表达式可以与常规列名混合使用:

col

result = df.select(pl.col("ticker", "^.*_high$", "^.*_low$"))
print(result)

col

// 注意:在 `col`/`cols` 中使用正则表达式需要启用 `regex` 功能标志。
let result = df
    .clone()
    .lazy()
    .select([cols(["ticker", "^.*_high$", "^.*_low$"])])
    .collect()?;
println!("{}", result);

shape: (5, 5)
┌────────┬──────────┬───────────┬─────────┬──────────┐
│ ticker ┆ day_high ┆ year_high ┆ day_low ┆ year_low │
│ ---    ┆ ---      ┆ ---       ┆ ---     ┆ ---      │
│ str    ┆ f64      ┆ f64       ┆ f64     ┆ f64      │
╞════════╪══════════╪═══════════╪═════════╪══════════╡
│ AAPL   ┆ 231.31   ┆ 237.23    ┆ 228.6   ┆ 164.08   │
│ NVDA   ┆ 139.6    ┆ 140.76    ┆ 136.3   ┆ 39.23    │
│ MSFT   ┆ 424.04   ┆ 468.35    ┆ 417.52  ┆ 324.39   │
│ GOOG   ┆ 167.62   ┆ 193.31    ┆ 164.78  ┆ 121.46   │
│ AMZN   ┆ 189.83   ┆ 201.2     ┆ 188.44  ┆ 118.35   │
└────────┴──────────┴───────────┴─────────┴──────────┘

参数不能是混合类型

在Python中,函数col接受任意数量的字符串(作为 列名或作为 正则表达式)或任意数量的数据类型,但你不能在同一函数调用中混合使用两者:

try:
    df.select(pl.col("ticker", pl.Float64))
except TypeError as err:
    print("TypeError:", err)
TypeError: argument 'names': 'DataTypeClass' object cannot be converted to 'PyString'

选择所有列

Polars 提供了函数 all 作为简写符号,用于引用数据框的所有列:

all

result = df.select(pl.all())
print(result.equals(df))

all

let result = df.clone().lazy().select([all()]).collect()?;
println!("{}", result.equals(&df));

True

注意

函数 allcol("*") 的语法糖,但由于参数 "*" 是一个特殊情况,并且 all 读起来更像英语,因此更推荐使用 all

排除列

Polars 还提供了一种机制来从表达式扩展中排除某些列。为此,您可以使用函数 exclude,它接受的参数类型与 col 完全相同:

exclude

result = df.select(pl.all().exclude("^day_.*$"))
print(result)

exclude

let result = df
    .clone()
    .lazy()
    .select([all().exclude(["^day_.*$"])])
    .collect()?;
println!("{}", result);

shape: (5, 5)
┌────────┬───────────────────┬────────┬───────────┬──────────┐
│ ticker ┆ company_name      ┆ price  ┆ year_high ┆ year_low │
│ ---    ┆ ---               ┆ ---    ┆ ---       ┆ ---      │
│ str    ┆ str               ┆ f64    ┆ f64       ┆ f64      │
╞════════╪═══════════════════╪════════╪═══════════╪══════════╡
│ AAPL   ┆ Apple             ┆ 229.9  ┆ 237.23    ┆ 164.08   │
│ NVDA   ┆ NVIDIA            ┆ 138.93 ┆ 140.76    ┆ 39.23    │
│ MSFT   ┆ Microsoft         ┆ 420.56 ┆ 468.35    ┆ 324.39   │
│ GOOG   ┆ Alphabet (Google) ┆ 166.41 ┆ 193.31    ┆ 121.46   │
│ AMZN   ┆ Amazon            ┆ 188.4  ┆ 201.2     ┆ 118.35   │
└────────┴───────────────────┴────────┴───────────┴──────────┘

自然地,函数 exclude 也可以在函数 col 之后使用:

exclude

result = df.select(pl.col(pl.Float64).exclude("^day_.*$"))
print(result)

exclude

let result = df
    .clone()
    .lazy()
    .select([dtype_col(&DataType::Float64).exclude(["^day_.*$"])])
    .collect()?;
println!("{}", result);

shape: (5, 3)
┌────────┬───────────┬──────────┐
│ price  ┆ year_high ┆ year_low │
│ ---    ┆ ---       ┆ ---      │
│ f64    ┆ f64       ┆ f64      │
╞════════╪═══════════╪══════════╡
│ 229.9  ┆ 237.23    ┆ 164.08   │
│ 138.93 ┆ 140.76    ┆ 39.23    │
│ 420.56 ┆ 468.35    ┆ 324.39   │
│ 166.41 ┆ 193.31    ┆ 121.46   │
│ 188.4  ┆ 201.2     ┆ 118.35   │
└────────┴───────────┴──────────┘

列重命名

默认情况下,当您对列应用表达式时,结果会保留与原始列相同的名称。

保留列名可能在语义上是错误的,在某些情况下,如果出现重复的名称,Polars 甚至可能会引发错误:

from polars.exceptions import DuplicateError

gbp_usd_rate = 1.31  # As of 14th October 2024

try:
    df.select(
        pl.col("price") / gbp_usd_rate,  # This would be named "price"...
        pl.col("price") / eur_usd_rate,  # And so would this.
    )
except DuplicateError as err:
    print("DuplicateError:", err)
let gbp_usd_rate = 1.31; // As of 14th October 2024

let result = df
    .clone()
    .lazy()
    .select([
        col("price") / lit(gbp_usd_rate),
        col("price") / lit(eur_usd_rate),
    ])
    .collect();
match result {
    Ok(df) => println!("{}", df),
    Err(e) => println!("{}", e),
};
DuplicateError: the name 'price' is duplicate

It's possible that multiple expressions are returning the same default column name. If this is the case, try renaming the columns with `.alias("new_name")` to avoid duplicate column names.

为了防止此类错误,并允许用户在适当时重命名他们的列,Polars 提供了一系列函数,让您可以更改列或一组列的名称。

使用alias重命名单个列

函数 alias 在文档中已经被广泛使用,它允许你重命名一个单独的列:

alias

result = df.select(
    (pl.col("price") / gbp_usd_rate).alias("price (GBP)"),
    (pl.col("price") / eur_usd_rate).alias("price (EUR)"),
)

alias

let _result = df
    .clone()
    .lazy()
    .select([
        (col("price") / lit(gbp_usd_rate)).alias("price (GBP)"),
        (col("price") / lit(eur_usd_rate)).alias("price (EUR)"),
    ])
    .collect()?;

前缀和后缀列名

使用表达式扩展时,不能使用函数alias,因为函数alias专门设计用于重命名单个列。

当只需要在现有名称前添加静态前缀或静态后缀时,我们可以使用命名空间name中的函数prefixsuffix

name namespace · prefix · suffix

result = df.select(
    (pl.col("^year_.*$") / eur_usd_rate).name.prefix("in_eur_"),
    (pl.col("day_high", "day_low") / gbp_usd_rate).name.suffix("_gbp"),
)
print(result)

name namespace · prefix · suffix · 在功能 lazy 上可用

let result = df
    .clone()
    .lazy()
    .select([
        (col("^year_.*$") / lit(eur_usd_rate))
            .name()
            .prefix("in_eur_"),
        (cols(["day_high", "day_low"]) / lit(gbp_usd_rate))
            .name()
            .suffix("_gbp"),
    ])
    .collect()?;
println!("{}", result);

shape: (5, 4)
┌──────────────────┬─────────────────┬──────────────┬─────────────┐
│ in_eur_year_high ┆ in_eur_year_low ┆ day_high_gbp ┆ day_low_gbp │
│ ---              ┆ ---             ┆ ---          ┆ ---         │
│ f64              ┆ f64             ┆ f64          ┆ f64         │
╞══════════════════╪═════════════════╪══════════════╪═════════════╡
│ 217.642202       ┆ 150.53211       ┆ 176.572519   ┆ 174.503817  │
│ 129.137615       ┆ 35.990826       ┆ 106.564885   ┆ 104.045802  │
│ 429.678899       ┆ 297.605505      ┆ 323.694656   ┆ 318.717557  │
│ 177.348624       ┆ 111.431193      ┆ 127.954198   ┆ 125.78626   │
│ 184.587156       ┆ 108.577982      ┆ 144.908397   ┆ 143.847328  │
└──────────────────┴─────────────────┴──────────────┴─────────────┘

动态名称替换

如果静态前缀/后缀不够,命名空间 name 还提供了函数 map,该函数接受一个可调用对象,该对象接受旧列名并生成新列名:

name namespace · map

# 还有 `.name.to_uppercase`,所以这个 `.map` 的使用是多余的。
result = df.select(pl.all().name.map(str.upper))
print(result)

name namespace · map · 在功能 lazy 上可用

// 还有 `name().to_uppercase()`,所以这个 `map` 的使用是多余的。
let result = df
    .clone()
    .lazy()
    .select([all()
        .name()
        .map(|name| Ok(PlSmallStr::from_string(name.to_ascii_uppercase())))])
    .collect()?;
println!("{}", result);

shape: (5, 7)
┌────────┬───────────────────┬────────┬──────────┬─────────┬───────────┬──────────┐
│ TICKER ┆ COMPANY_NAME      ┆ PRICE  ┆ DAY_HIGH ┆ DAY_LOW ┆ YEAR_HIGH ┆ YEAR_LOW │
│ ---    ┆ ---               ┆ ---    ┆ ---      ┆ ---     ┆ ---       ┆ ---      │
│ str    ┆ str               ┆ f64    ┆ f64      ┆ f64     ┆ f64       ┆ f64      │
╞════════╪═══════════════════╪════════╪══════════╪═════════╪═══════════╪══════════╡
│ AAPL   ┆ Apple             ┆ 229.9  ┆ 231.31   ┆ 228.6   ┆ 237.23    ┆ 164.08   │
│ NVDA   ┆ NVIDIA            ┆ 138.93 ┆ 139.6    ┆ 136.3   ┆ 140.76    ┆ 39.23    │
│ MSFT   ┆ Microsoft         ┆ 420.56 ┆ 424.04   ┆ 417.52  ┆ 468.35    ┆ 324.39   │
│ GOOG   ┆ Alphabet (Google) ┆ 166.41 ┆ 167.62   ┆ 164.78  ┆ 193.31    ┆ 121.46   │
│ AMZN   ┆ Amazon            ┆ 188.4  ┆ 189.83   ┆ 188.44  ┆ 201.2     ┆ 118.35   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

请参阅API参考以获取命名空间name的完整内容。

以编程方式生成表达式

表达式扩展是一个非常实用的功能,但它并不能解决所有问题。例如,如果我们想要计算数据框中股票价格的日振幅和年振幅,表达式扩展就无法帮助我们。

首先,你可能会考虑使用一个for循环:

result = df
for tp in ["day", "year"]:
    result = result.with_columns(
        (pl.col(f"{tp}_high") - pl.col(f"{tp}_low")).alias(f"{tp}_amplitude")
    )
print(result)
let mut result = df.clone().lazy();
for tp in ["day", "year"] {
    let high = format!("{}_high", tp);
    let low = format!("{}_low", tp);
    let aliased = format!("{}_amplitude", tp);
    result = result.with_column((col(high) - col(low)).alias(aliased))
}
let result = result.collect()?;
println!("{}", result);
shape: (5, 9)
┌────────┬──────────────┬────────┬──────────┬───┬───────────┬──────────┬─────────────┬─────────────┐
│ ticker ┆ company_name ┆ price  ┆ day_high ┆ … ┆ year_high ┆ year_low ┆ day_amplitu ┆ year_amplit │
│ ---    ┆ ---          ┆ ---    ┆ ---      ┆   ┆ ---       ┆ ---      ┆ de          ┆ ude         │
│ str    ┆ str          ┆ f64    ┆ f64      ┆   ┆ f64       ┆ f64      ┆ ---         ┆ ---         │
│        ┆              ┆        ┆          ┆   ┆           ┆          ┆ f64         ┆ f64         │
╞════════╪══════════════╪════════╪══════════╪═══╪═══════════╪══════════╪═════════════╪═════════════╡
│ AAPL   ┆ Apple        ┆ 229.9  ┆ 231.31   ┆ … ┆ 237.23    ┆ 164.08   ┆ 2.71        ┆ 73.15       │
│ NVDA   ┆ NVIDIA       ┆ 138.93 ┆ 139.6    ┆ … ┆ 140.76    ┆ 39.23    ┆ 3.3         ┆ 101.53      │
│ MSFT   ┆ Microsoft    ┆ 420.56 ┆ 424.04   ┆ … ┆ 468.35    ┆ 324.39   ┆ 6.52        ┆ 143.96      │
│ GOOG   ┆ Alphabet     ┆ 166.41 ┆ 167.62   ┆ … ┆ 193.31    ┆ 121.46   ┆ 2.84        ┆ 71.85       │
│        ┆ (Google)     ┆        ┆          ┆   ┆           ┆          ┆             ┆             │
│ AMZN   ┆ Amazon       ┆ 188.4  ┆ 189.83   ┆ … ┆ 201.2     ┆ 118.35   ┆ 1.39        ┆ 82.85       │
└────────┴──────────────┴────────┴──────────┴───┴───────────┴──────────┴─────────────┴─────────────┘

不要这样做。相反,以编程方式生成所有你想要计算的表达式,并在上下文中只使用一次。通俗地说,你希望将for循环与上下文with_columns交换。实际上,你可以做类似以下的事情:

def amplitude_expressions(time_periods):
    for tp in time_periods:
        yield (pl.col(f"{tp}_high") - pl.col(f"{tp}_low")).alias(f"{tp}_amplitude")


result = df.with_columns(amplitude_expressions(["day", "year"]))
print(result)
let mut exprs: Vec<Expr> = vec![];
for tp in ["day", "year"] {
    let high = format!("{}_high", tp);
    let low = format!("{}_low", tp);
    let aliased = format!("{}_amplitude", tp);
    exprs.push((col(high) - col(low)).alias(aliased))
}
let result = df.clone().lazy().with_columns(exprs).collect()?;
println!("{}", result);
shape: (5, 9)
┌────────┬──────────────┬────────┬──────────┬───┬───────────┬──────────┬─────────────┬─────────────┐
│ ticker ┆ company_name ┆ price  ┆ day_high ┆ … ┆ year_high ┆ year_low ┆ day_amplitu ┆ year_amplit │
│ ---    ┆ ---          ┆ ---    ┆ ---      ┆   ┆ ---       ┆ ---      ┆ de          ┆ ude         │
│ str    ┆ str          ┆ f64    ┆ f64      ┆   ┆ f64       ┆ f64      ┆ ---         ┆ ---         │
│        ┆              ┆        ┆          ┆   ┆           ┆          ┆ f64         ┆ f64         │
╞════════╪══════════════╪════════╪══════════╪═══╪═══════════╪══════════╪═════════════╪═════════════╡
│ AAPL   ┆ Apple        ┆ 229.9  ┆ 231.31   ┆ … ┆ 237.23    ┆ 164.08   ┆ 2.71        ┆ 73.15       │
│ NVDA   ┆ NVIDIA       ┆ 138.93 ┆ 139.6    ┆ … ┆ 140.76    ┆ 39.23    ┆ 3.3         ┆ 101.53      │
│ MSFT   ┆ Microsoft    ┆ 420.56 ┆ 424.04   ┆ … ┆ 468.35    ┆ 324.39   ┆ 6.52        ┆ 143.96      │
│ GOOG   ┆ Alphabet     ┆ 166.41 ┆ 167.62   ┆ … ┆ 193.31    ┆ 121.46   ┆ 2.84        ┆ 71.85       │
│        ┆ (Google)     ┆        ┆          ┆   ┆           ┆          ┆             ┆             │
│ AMZN   ┆ Amazon       ┆ 188.4  ┆ 189.83   ┆ … ┆ 201.2     ┆ 118.35   ┆ 1.39        ┆ 82.85       │
└────────┴──────────────┴────────┴──────────┴───┴───────────┴──────────┴─────────────┴─────────────┘

这会产生相同的最终结果,并且通过一次性指定所有表达式,我们为 Polars 提供了以下机会:

  1. 在优化查询方面做得更好;以及
  2. 并行化实际计算的执行。

更灵活的列选择

Polars 附带了一个名为 selectors 的子模块,该模块提供了许多函数,允许您为表达式扩展编写更灵活的列选择。

警告

此功能在Rust中尚不可用。请参考Polars问题 #10594

作为第一个示例,这里展示了我们如何使用函数 stringends_with,以及 selectors 中的函数所支持的集合操作,来选择所有字符串列和名称以 "_high" 结尾的列:

selectors

import polars.selectors as cs

result = df.select(cs.string() | cs.ends_with("_high"))
print(result)

// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
shape: (5, 4)
┌────────┬───────────────────┬──────────┬───────────┐
│ ticker ┆ company_name      ┆ day_high ┆ year_high │
│ ---    ┆ ---               ┆ ---      ┆ ---       │
│ str    ┆ str               ┆ f64      ┆ f64       │
╞════════╪═══════════════════╪══════════╪═══════════╡
│ AAPL   ┆ Apple             ┆ 231.31   ┆ 237.23    │
│ NVDA   ┆ NVIDIA            ┆ 139.6    ┆ 140.76    │
│ MSFT   ┆ Microsoft         ┆ 424.04   ┆ 468.35    │
│ GOOG   ┆ Alphabet (Google) ┆ 167.62   ┆ 193.31    │
│ AMZN   ┆ Amazon            ┆ 189.83   ┆ 201.2     │
└────────┴───────────────────┴──────────┴───────────┘

子模块 selectors 提供了 一些根据列的数据类型匹配的选择器, 其中最有用的函数是匹配整个类型类别的函数,例如 cs.numeric 用于所有数值数据类型,或 cs.temporal 用于所有时间数据类型。

子模块 selectors 还提供了 一些基于列名模式匹配的选择器 这使得指定您可能想要检查的常见模式更加方便,例如上面展示的 函数 cs.ends_with

结合选择器与集合操作

我们可以使用集合操作和常用的Python运算符来组合多个选择器:

Operator Operation
A | B Union
A & B Intersection
A - B Difference
A ^ B Symmetric difference
~A Complement

下一个示例匹配名称中包含下划线的所有非字符串列:

selectors

result = df.select(cs.contains("_") - cs.string())
print(result)

// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
shape: (5, 4)
┌──────────┬─────────┬───────────┬──────────┐
│ day_high ┆ day_low ┆ year_high ┆ year_low │
│ ---      ┆ ---     ┆ ---       ┆ ---      │
│ f64      ┆ f64     ┆ f64       ┆ f64      │
╞══════════╪═════════╪═══════════╪══════════╡
│ 231.31   ┆ 228.6   ┆ 237.23    ┆ 164.08   │
│ 139.6    ┆ 136.3   ┆ 140.76    ┆ 39.23    │
│ 424.04   ┆ 417.52  ┆ 468.35    ┆ 324.39   │
│ 167.62   ┆ 164.78  ┆ 193.31    ┆ 121.46   │
│ 189.83   ┆ 188.44  ┆ 201.2     ┆ 118.35   │
└──────────┴─────────┴───────────┴──────────┘

解决运算符歧义

表达式函数可以在选择器的基础上进行链式调用:

selectors

result = df.select((cs.contains("_") - cs.string()) / eur_usd_rate)
print(result)

// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
shape: (5, 4)
┌────────────┬────────────┬────────────┬────────────┐
│ day_high   ┆ day_low    ┆ year_high  ┆ year_low   │
│ ---        ┆ ---        ┆ ---        ┆ ---        │
│ f64        ┆ f64        ┆ f64        ┆ f64        │
╞════════════╪════════════╪════════════╪════════════╡
│ 212.211009 ┆ 209.724771 ┆ 217.642202 ┆ 150.53211  │
│ 128.073394 ┆ 125.045872 ┆ 129.137615 ┆ 35.990826  │
│ 389.027523 ┆ 383.045872 ┆ 429.678899 ┆ 297.605505 │
│ 153.779817 ┆ 151.174312 ┆ 177.348624 ┆ 111.431193 │
│ 174.155963 ┆ 172.880734 ┆ 184.587156 ┆ 108.577982 │
└────────────┴────────────┴────────────┴────────────┘

然而,一些操作符已经被重载,以便在Polars选择器和表达式上都能操作。 例如,选择器上的操作符~表示 集合操作“补集”,而在表达式上 表示布尔操作的否定。

当你使用一个选择器,然后想在表达式中使用作为选择器集合操作符的操作符时,你可以使用函数as_expr

下面,我们想要对列“has_partner”、“has_kids”和“has_tattoos”中的布尔值进行取反。如果我们不小心,操作符~和选择器cs.starts_with("has_")的组合实际上会选择我们不关心的列:

people = pl.DataFrame(
    {
        "name": ["Anna", "Bob"],
        "has_partner": [True, False],
        "has_kids": [False, False],
        "has_tattoos": [True, False],
        "is_alive": [True, True],
    }
)

wrong_result = people.select((~cs.starts_with("has_")).name.prefix("not_"))
print(wrong_result)
// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
shape: (2, 2)
┌──────────┬──────────────┐
│ not_name ┆ not_is_alive │
│ ---      ┆ ---          │
│ str      ┆ bool         │
╞══════════╪══════════════╡
│ Anna     ┆ true         │
│ Bob      ┆ true         │
└──────────┴──────────────┘

正确的解决方案使用 as_expr

result = people.select((~cs.starts_with("has_").as_expr()).name.prefix("not_"))
print(result)
// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
shape: (2, 3)
┌─────────────────┬──────────────┬─────────────────┐
│ not_has_partner ┆ not_has_kids ┆ not_has_tattoos │
│ ---             ┆ ---          ┆ ---             │
│ bool            ┆ bool         ┆ bool            │
╞═════════════════╪══════════════╪═════════════════╡
│ false           ┆ true         ┆ false           │
│ true            ┆ true         ┆ true            │
└─────────────────┴──────────────┴─────────────────┘

调试选择器

当你不确定手头是否有Polars选择器时,可以使用函数cs.is_selector来检查:

is_selector

print(cs.is_selector(~cs.starts_with("has_").as_expr()))

// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
False

这应该可以帮助你避免任何你认为你在使用表达式但实际上是在使用选择器的模糊情况。

另一个有用的调试工具是函数 expand_selector。给定一个目标框架或模式,你可以检查给定的选择器将扩展到哪些列:

expand_selector

print(
    cs.expand_selector(
        people,
        cs.starts_with("has_"),
    )
)

// Selectors are not available in Rust yet.
// Refer to https://github.com/pola-rs/polars/issues/10594
('has_partner', 'has_kids', 'has_tattoos')

完整参考

下表按行为类型分组了子模块selectors中可用的函数。

数据类型选择器

根据列的数据类型匹配的选择器:

Selector function Data type(s) matched
binary Binary
boolean Boolean
by_dtype Data types specified as arguments
categorical Categorical
date Date
datetime Datetime, optionally filtering by time unit/zone
decimal Decimal
duration Duration, optionally filtering by time unit
float All float types, regardless of precision
integer All integer types, signed and unsigned, regardless of precision
numeric All numeric types, namely integers, floats, and Decimal
signed_integer All signed integer types, regardless of precision
string String
temporal All temporal data types, namely Date, Datetime, and Duration
time Time
unsigned_integer All unsigned integer types, regardless of precision

列名模式选择器

基于列名模式匹配的选择器:

Selector function Columns selected
alpha Columns with alphabetical names
alphanumeric Columns with alphanumeric names (letters and the digits 0-9)
by_name Columns with the names specified as arguments
contains Columns whose names contain the substring specified
digit Columns with numeric names (only the digits 0-9)
ends_with Columns whose names end with the given substring
matches Columns whose names match the given regex pattern
starts_with Columns whose names start with the given substring

位置选择器

基于列位置匹配的选择器:

Selector function Columns selected
all All columns
by_index The columns at the specified indices
first The first column in the context
last The last column in the context

杂项函数

子模块 selectors 还提供了以下函数:

Function Behaviour
as_expr* Convert a selector to an expression
exclude Selects all columns except those matching the given names, data types, or selectors
expand_selector Expand selector to matching columns with respect to a specific frame or target schema
is_selector Check whether the given object/expression is a selector

*as_expr 不是定义在子模块 selectors 上的函数,而是定义在 selectors 上的方法。