Skip to content

分组

按固定窗口分组

我们可以使用group_by_dynamic来计算时间统计,将行按天/月/年等进行分组。

年平均示例

在以下简单示例中,我们计算了苹果股票价格的年度平均收盘价。我们首先从CSV加载数据:

upsample

df = pl.read_csv("docs/assets/data/apple_stock.csv", try_parse_dates=True)
df = df.sort("Date")
print(df)

upsample

let df = CsvReadOptions::default()
    .map_parse_options(|parse_options| parse_options.with_try_parse_dates(true))
    .try_into_reader_with_file_path(Some("docs/assets/data/apple_stock.csv".into()))
    .unwrap()
    .finish()
    .unwrap()
    .sort(
        ["Date"],
        SortMultipleOptions::default().with_maintain_order(true),
    )?;
println!("{}", &df);

shape: (100, 2)
┌────────────┬────────┐
│ Date       ┆ Close  │
│ ---        ┆ ---    │
│ date       ┆ f64    │
╞════════════╪════════╡
│ 1981-02-23 ┆ 24.62  │
│ 1981-05-06 ┆ 27.38  │
│ 1981-05-18 ┆ 28.0   │
│ 1981-09-25 ┆ 14.25  │
│ 1982-07-08 ┆ 11.0   │
│ …          ┆ …      │
│ 2012-05-16 ┆ 546.08 │
│ 2012-12-04 ┆ 575.85 │
│ 2013-07-05 ┆ 417.42 │
│ 2013-11-07 ┆ 512.49 │
│ 2014-02-25 ┆ 522.06 │
└────────────┴────────┘

信息

日期按升序排序 - 如果没有按这种方式排序,group_by_dynamic 的输出将不正确!

为了获取年度平均收盘价,我们告诉group_by_dynamic我们希望:

  • Date列以年度(1y)为基础进行分组
  • 取每年Close列的平均值:

group_by_dynamic

annual_average_df = df.group_by_dynamic("Date", every="1y").agg(pl.col("Close").mean())

df_with_year = annual_average_df.with_columns(pl.col("Date").dt.year().alias("year"))
print(df_with_year)

group_by_dynamic · 在功能 dynamic_group_by 上可用

let annual_average_df = df
    .clone()
    .lazy()
    .group_by_dynamic(
        col("Date"),
        [],
        DynamicGroupOptions {
            every: Duration::parse("1y"),
            period: Duration::parse("1y"),
            offset: Duration::parse("0"),
            ..Default::default()
        },
    )
    .agg([col("Close").mean()])
    .collect()?;

let df_with_year = annual_average_df
    .lazy()
    .with_columns([col("Date").dt().year().alias("year")])
    .collect()?;
println!("{}", &df_with_year);

年平均收盘价如下:

shape: (34, 3)
┌────────────┬───────────┬──────┐
│ Date       ┆ Close     ┆ year │
│ ---        ┆ ---       ┆ ---  │
│ date       ┆ f64       ┆ i32  │
╞════════════╪═══════════╪══════╡
│ 1981-01-01 ┆ 23.5625   ┆ 1981 │
│ 1982-01-01 ┆ 11.0      ┆ 1982 │
│ 1983-01-01 ┆ 30.543333 ┆ 1983 │
│ 1984-01-01 ┆ 27.583333 ┆ 1984 │
│ 1985-01-01 ┆ 18.166667 ┆ 1985 │
│ …          ┆ …         ┆ …    │
│ 2010-01-01 ┆ 278.265   ┆ 2010 │
│ 2011-01-01 ┆ 368.225   ┆ 2011 │
│ 2012-01-01 ┆ 560.965   ┆ 2012 │
│ 2013-01-01 ┆ 464.955   ┆ 2013 │
│ 2014-01-01 ┆ 522.06    ┆ 2014 │
└────────────┴───────────┴──────┘

group_by_dynamic的参数

动态窗口由以下定义:

  • every: 表示窗口的间隔
  • period: 表示窗口的持续时间
  • offset: 可用于偏移窗口的开始位置

every 的值设置了组开始的频率。时间段的值是灵活的 - 例如,我们可以采用:

  • 通过将1y替换为2y来计算2年间隔的平均值
  • 通过将1y替换为1y6mo来计算18个月期间的平均值

我们还可以使用period参数来设置每个组的时间段长度。例如,如果我们将every参数设置为1y,并将period参数设置为2y,那么我们将得到以一年为间隔的组,其中每个组跨越两年。

如果未指定period参数,则将其设置为等于every参数,因此如果every参数设置为1y,则每个组也跨越1y

因为every不必等于period,我们可以以非常灵活的方式创建许多组。它们可能会重叠或在它们之间留下边界。

让我们看看一些参数组合的窗口会是什么样子。让我们从无聊的开始。🥱

  • 每:1天 -> "1d"
  • period: 1 天 -> "1d"
this creates adjacent windows of the same size
|--|
   |--|
      |--|
  • every: 1天 -> "1d"
  • period: 2天 -> "2d"
these windows have an overlap of 1 day
|----|
   |----|
      |----|
  • 每:2天 -> "2d"
  • period: 1天 -> "1d"
this would leave gaps between the windows
data points that in these gaps will not be a member of any group
|--|
       |--|
              |--|

truncate

truncate 参数是一个布尔变量,用于确定输出中每个组关联的日期时间值。在上面的示例中,第一个数据点是1981年2月23日。如果 truncate = True(默认值),则年平均中的第一年的日期是1981年1月1日。然而,如果 truncate = False,则年平均中的第一年的日期是1981年2月23日的第一个数据点的日期。请注意,truncate 仅影响 Date 列中显示的内容,并不影响窗口边界。

group_by_dynamic 中使用表达式

我们不仅限于在分组操作中使用像mean这样的简单聚合——我们可以使用Polars中提供的所有表达式。

在下面的代码片段中,我们创建了一个包含2021年每一天("1d")的date range,并将其转换为一个DataFrame

然后在group_by_dynamic中,我们创建动态窗口,这些窗口每"1mo")开始一次,并且窗口长度为1个月。匹配这些动态窗口的值随后被分配到该组,并可以使用强大的表达式API进行聚合。

下面我们展示一个使用group_by_dynamic计算的示例:

  • 距离月底的天数
  • 一个月中的天数

group_by_dynamic · DataFrame.explode · date_range

df = (
    pl.date_range(
        start=date(2021, 1, 1),
        end=date(2021, 12, 31),
        interval="1d",
        eager=True,
    )
    .alias("time")
    .to_frame()
)

out = df.group_by_dynamic("time", every="1mo", period="1mo", closed="left").agg(
    pl.col("time").cum_count().reverse().head(3).alias("day/eom"),
    ((pl.col("time") - pl.col("time").first()).last().dt.total_days() + 1).alias(
        "days_in_month"
    ),
)
print(out)

group_by_dynamic · DataFrame.explode · date_range · 在功能 dynamic_group_by 上可用 · 在功能 dtype-date 上可用 · 在功能 range 上可用

let time = polars::time::date_range(
    "time".into(),
    NaiveDate::from_ymd_opt(2021, 1, 1)
        .unwrap()
        .and_hms_opt(0, 0, 0)
        .unwrap(),
    NaiveDate::from_ymd_opt(2021, 12, 31)
        .unwrap()
        .and_hms_opt(0, 0, 0)
        .unwrap(),
    Duration::parse("1d"),
    ClosedWindow::Both,
    TimeUnit::Milliseconds,
    None,
)?
.cast(&DataType::Date)?;

let df = df!(
    "time" => time,
)?;

let out = df
    .clone()
    .lazy()
    .group_by_dynamic(
        col("time"),
        [],
        DynamicGroupOptions {
            every: Duration::parse("1mo"),
            period: Duration::parse("1mo"),
            offset: Duration::parse("0"),
            closed_window: ClosedWindow::Left,
            ..Default::default()
        },
    )
    .agg([
        col("time")
            .cum_count(true) // python 示例中有 false
            .reverse()
            .head(Some(3))
            .alias("day/eom"),
        ((col("time").last() - col("time").first()).map(
            // 必须使用 map,因为 .duration().days() 不可用
            |s| {
                Ok(Some(
                    s.duration()?
                        .into_iter()
                        .map(|d| d.map(|v| v / 1000 / 24 / 60</

shape: (12, 3)
┌────────────┬──────────────┬───────────────┐
│ time       ┆ day/eom      ┆ days_in_month │
│ ---        ┆ ---          ┆ ---           │
│ date       ┆ list[u32]    ┆ i64           │
╞════════════╪══════════════╪═══════════════╡
│ 2021-01-01 ┆ [31, 30, 29] ┆ 31            │
│ 2021-02-01 ┆ [28, 27, 26] ┆ 28            │
│ 2021-03-01 ┆ [31, 30, 29] ┆ 31            │
│ 2021-04-01 ┆ [30, 29, 28] ┆ 30            │
│ 2021-05-01 ┆ [31, 30, 29] ┆ 31            │
│ …          ┆ …            ┆ …             │
│ 2021-08-01 ┆ [31, 30, 29] ┆ 31            │
│ 2021-09-01 ┆ [30, 29, 28] ┆ 30            │
│ 2021-10-01 ┆ [31, 30, 29] ┆ 31            │
│ 2021-11-01 ┆ [30, 29, 28] ┆ 30            │
│ 2021-12-01 ┆ [31, 30, 29] ┆ 31            │
└────────────┴──────────────┴───────────────┘

按滚动窗口分组

滚动操作,rolling,是进入group_by/agg上下文的另一个入口。但与group_by_dynamic不同,在group_by_dynamic中,窗口由参数everyperiod固定。而在rolling中,窗口根本不是固定的!它们由index_column中的值决定。

所以想象一下,有一个时间列,其值为 {2021-01-06, 2021-01-10} 和一个 period="5d",这将创建以下窗口:

2021-01-01   2021-01-06
    |----------|

       2021-01-05   2021-01-10
             |----------|

因为滚动分组的窗口总是由DataFrame列中的值决定,所以组的数量总是等于原始的DataFrame

组合分组操作

滚动和动态分组操作可以与普通的分组操作结合使用。

下面是一个带有动态分组的示例。

DataFrame

df = pl.DataFrame(
    {
        "time": pl.datetime_range(
            start=datetime(2021, 12, 16),
            end=datetime(2021, 12, 16, 3),
            interval="30m",
            eager=True,
        ),
        "groups": ["a", "a", "a", "b", "b", "a", "a"],
    }
)
print(df)

DataFrame

let time = polars::time::date_range(
    "time".into(),
    NaiveDate::from_ymd_opt(2021, 12, 16)
        .unwrap()
        .and_hms_opt(0, 0, 0)
        .unwrap(),
    NaiveDate::from_ymd_opt(2021, 12, 16)
        .unwrap()
        .and_hms_opt(3, 0, 0)
        .unwrap(),
    Duration::parse("30m"),
    ClosedWindow::Both,
    TimeUnit::Milliseconds,
    None,
)?;
let df = df!(
    "time" => time,
    "groups"=> ["a", "a", "a", "b", "b", "a", "a"],
)?;
println!("{}", &df);

shape: (7, 2)
┌─────────────────────┬────────┐
│ time                ┆ groups │
│ ---                 ┆ ---    │
│ datetime[μs]        ┆ str    │
╞═════════════════════╪════════╡
│ 2021-12-16 00:00:00 ┆ a      │
│ 2021-12-16 00:30:00 ┆ a      │
│ 2021-12-16 01:00:00 ┆ a      │
│ 2021-12-16 01:30:00 ┆ b      │
│ 2021-12-16 02:00:00 ┆ b      │
│ 2021-12-16 02:30:00 ┆ a      │
│ 2021-12-16 03:00:00 ┆ a      │
└─────────────────────┴────────┘

group_by_dynamic

out = df.group_by_dynamic(
    "time",
    every="1h",
    closed="both",
    group_by="groups",
    include_boundaries=True,
).agg(pl.len())
print(out)

group_by_dynamic · 在功能 dynamic_group_by 上可用

let out = df
    .clone()
    .lazy()
    .group_by_dynamic(
        col("time"),
        [col("groups")],
        DynamicGroupOptions {
            every: Duration::parse("1h"),
            period: Duration::parse("1h"),
            offset: Duration::parse("0"),
            include_boundaries: true,
            closed_window: ClosedWindow::Both,
            ..Default::default()
        },
    )
    .agg([len()])
    .collect()?;
println!("{}", &out);

shape: (6, 5)
┌────────┬─────────────────────┬─────────────────────┬─────────────────────┬─────┐
│ groups ┆ _lower_boundary     ┆ _upper_boundary     ┆ time                ┆ len │
│ ---    ┆ ---                 ┆ ---                 ┆ ---                 ┆ --- │
│ str    ┆ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        ┆ u32 │
╞════════╪═════════════════════╪═════════════════════╪═════════════════════╪═════╡
│ a      ┆ 2021-12-16 00:00:00 ┆ 2021-12-16 01:00:00 ┆ 2021-12-16 00:00:00 ┆ 3   │
│ a      ┆ 2021-12-16 01:00:00 ┆ 2021-12-16 02:00:00 ┆ 2021-12-16 01:00:00 ┆ 1   │
│ a      ┆ 2021-12-16 02:00:00 ┆ 2021-12-16 03:00:00 ┆ 2021-12-16 02:00:00 ┆ 2   │
│ a      ┆ 2021-12-16 03:00:00 ┆ 2021-12-16 04:00:00 ┆ 2021-12-16 03:00:00 ┆ 1   │
│ b      ┆ 2021-12-16 01:00:00 ┆ 2021-12-16 02:00:00 ┆ 2021-12-16 01:00:00 ┆ 2   │
│ b      ┆ 2021-12-16 02:00:00 ┆ 2021-12-16 03:00:00 ┆ 2021-12-16 02:00:00 ┆ 1   │
└────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────┘