跳到内容

表达式扩展

正如您在表达式与上下文一节中看到的,表达式展开是一个特性,它使您能够编写一个可以展开为多个不同表达式的单个表达式,这可能取决于表达式所使用的上下文的模式。

此功能不仅仅是装饰或语法糖。它允许在您的代码中非常强大地应用 DRY(Don't Repeat Yourself) 原则:指定多列的单个表达式会展开为一个表达式列表,这意味着您可以编写一个单一表达式并重用其所代表的计算。

在本节中,我们将展示几种表达式展开的形式,我们将为此使用您在下面看到的数据框:

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  # As of 14th October 2024

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; // As of 14th October 2024

let result = df
    .clone()
    .lazy()
    .with_column(
        (cols(["price", "day_high", "day_low", "year_high", "year_low"]) / lit(eur_usd_rate))
            .round(2, RoundMode::default()),
    )
    .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, RoundMode::default()),
    (col("day_high") / lit(eur_usd_rate)).round(2, RoundMode::default()),
    (col("day_low") / lit(eur_usd_rate)).round(2, RoundMode::default()),
    (col("year_high") / lit(eur_usd_rate)).round(2, RoundMode::default()),
    (col("year_low") / lit(eur_usd_rate)).round(2, RoundMode::default()),
];

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, RoundMode::default()),
    )
    .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   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

当我们使用数据类型进行表达式展开时,我们无法预先知道单个表达式将展开成多少列。如果我们要确定最终要应用的表达式列表,我们需要输入数据框的模式(schema)。

如果我们不确定价格列是 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, RoundMode::default()),
    )
    .collect()?;
println!("{}", result.equals(&result2));

True

按模式匹配展开

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

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

col

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

col

// NOTE: Using regex inside `col`/`cols` requires the feature flag `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 · 可在 feature 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

# There is also `.name.to_uppercase`, so this usage of `.map` is moot.
result = df.select(pl.all().name.map(str.upper))
print(result)

name namespace · map · 可在 feature lazy 中使用

// There is also `name().to_uppercase()`, so this usage of `map` is moot.
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   │
└────────┴───────────────────┴────────┴──────────┴─────────┴───────────┴──────────┘

有关 name 命名空间的完整内容,请参阅 API 参考。

程序化生成表达式

表达式展开是一个非常有用的功能,但它并不能解决所有问题。例如,如果我们想计算数据框中股票价格的每日和年度振幅,表达式展开将无济于事。

起初,您可能会考虑使用 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!("{tp}_high");
    let low = format!("{tp}_low");
    let aliased = format!("{tp}_amplitude");
    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!("{tp}_high");
    let low = format!("{tp}_low");
    let aliased = format!("{tp}_amplitude");
    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 运算符组合多个选择器

运算符 操作
A | B 并集
A & B 交集
A - B 差集
A ^ B 对称差集
~A 补集

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

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 子模块中可用的函数进行分组。

数据类型选择器

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

选择器函数 匹配的数据类型
binary Binary (二进制)
boolean Boolean (布尔)
by_dtype 指定为参数的数据类型
categorical Categorical (范畴)
date Date (日期)
datetime Datetime,可选按时间单位/时区过滤
decimal Decimal (小数)
duration Duration,可选按时间单位过滤
float 所有浮点类型,不考虑精度
integer 所有整数类型,包括有符号和无符号,不考虑精度
numeric 所有数值类型,即整数、浮点数和 Decimal
signed_integer 所有有符号整数类型,不考虑精度
string String (字符串)
temporal 所有时间数据类型,即 DateDatetimeDuration
time Time (时间)
unsigned_integer 所有无符号整数类型,不考虑精度

列名模式选择器

根据列名模式进行匹配的选择器

选择器函数 选择的列
alpha 名称为字母的列
alphanumeric 名称为字母数字的列(字母和数字 0-9)
by_name 指定为参数的列名
contains 名称包含指定子字符串的列
digit 名称为数字的列(仅包含数字 0-9)
ends_with 名称以给定子字符串结尾的列
matches 名称匹配给定正则表达式模式的列
starts_with 名称以给定子字符串开头的列

位置选择器

根据列位置进行匹配的选择器

选择器函数 选择的列
all 所有列
by_index 指定索引处的列
first 上下文中的第一列
last 上下文中的最后一列

杂项函数

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

函数 行为
as_expr* 将选择器转换为表达式
exclude 选择除匹配给定名称、数据类型或选择器之外的所有列
expand_selector 相对于特定数据框或目标模式展开选择器以匹配列
is_selector 检查给定对象/表达式是否为选择器

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