跳到内容

分组

按固定窗口分组

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

年平均示例

在以下简单示例中,我们计算 Apple 股票价格的年平均收盘价。我们首先从 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,我们可以以非常灵活的方式创建许多分组。它们可能重叠或在它们之间留下边界。

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

  • every: 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
|----|
   |----|
      |----|
  • every: 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 中可用的全部表达式。

在下面的代码片段中,我们创建了一个 date range,包含 2021 年的每一"1d"),并将其转换为一个 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 · 在功能 dtype-date 上可用 · 在功能 range 上可用 · 在功能 dynamic_group_by 上可用

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 example has false
            .reverse()
            .head(Some(3))
            .alias("day/eom"),
        ((col("time").last() - col("time").first()).map(
            // had to use map as .duration().days() is not available
            |s| {
                Ok(Some(
                    s.duration()?
                        .into_iter()
                        .map(|d| d.map(|v| v / 1000 / 24 / 60 / 60))
                        .collect::<Int64Chunked>()
                        .into_column(),
                ))
            },
            GetOutput::from_type(DataType::Int64),
        ) + lit(1))
        .alias("days_in_month"),
    ])
    .collect()?;
println!("{}", &out);

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 不同的是,其中窗口由参数 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   │
└────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────┘