Skip to content

グループ化

固定ウィンドウによるグループ化

group_by_dynamic を使って、日/月/年などのテンポラル統計を計算することができます。

年間平均の例

以下の簡単な例では、Apple の株価の年間平均終値を計算します。まずはCSVからデータを読み込みます:

upsample

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

upsample

let df = CsvReader::from_path("docs/data/apple_stock.csv")
    .unwrap()
    .with_try_parse_dates(true)
    .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 │
└────────────┴────────┘

Info

日付は昇順にソートされている必要があります - そうでない場合、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 · Available on feature 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の値は、グループの開始頻度を設定します。時間期間の値は柔軟です - 例えば、1y2yに置き換えることで、2年間隔の平均を取ることができます。また、1y1y6moに置き換えることで、18ヶ月間隔の平均を取ることができます。

periodパラメーターを使って、各グループの時間期間の長さを設定することもできます。例えば、everyパラメーターを1yに、periodパラメーターを2yに設定すると、1年間隔でそれぞれ2年間のグループが作成されます。

periodパラメーターが指定されない場合は、everyパラメーターと同じ値に設定されます。つまり、everyパラメーターが1yに設定されている場合、各グループも1yのスパンになります。

every_とperiod_が等しくなる必要がないため、非常に柔軟な方法でたくさんのグループを作成できます。グループが重複したり、境界線が空いたりする可能性があります。

いくつかのパラメーター組み合わせでのウィンドウの様子を見てみましょう。退屈な例から始めましょう。🥱

  • every: 1 day -> "1d"
  • period: 1 day -> "1d"
この操作は同じサイズの隣接するウィンドウを作成します
|--|
   |--|
      |--|
  • every: 1 day -> "1d"
  • period: 2 days -> "2d"
これらのウィンドウには 1 日の重複があります
|----|
   |----|
      |----|
  • every: 2 days -> "2d"
  • period: 1 day -> "1d"
これではウィンドウの間に隙間ができます
これらの隙間のデータポイントは、どのグループにも属しません
|--|
       |--|
              |--|

truncate

truncate パラメーターは、出力の各グループに関連付けられる datetime 値を決定する Boolean 変数です。上記の例では、最初のデータポイントが 1981 年 2 月 23 日です。truncate = True(デフォルト)の場合、年間平均の最初の年の日付は 1981 年 1 月 1 日になります。一方、truncate = False の場合、年間平均の最初の年の日付は 1981 年 2 月 23 日の最初のデータポイントの日付になります。truncateDate 列に表示される内容にのみ影響し、ウィンドウの境界には影響しません。

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 · Available on feature dynamic_group_by · Available on feature dtype-date · Available on feature range

let time = polars::time::date_range(
    "time",
    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_series(),
                ))
            },
            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",
    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 · Available on feature 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: (7, 5)
┌────────┬─────────────────────┬─────────────────────┬─────────────────────┬─────┐
│ groups ┆ _lower_boundary     ┆ _upper_boundary     ┆ time                ┆ len │
│ ---    ┆ ---                 ┆ ---                 ┆ ---                 ┆ --- │
│ str    ┆ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        ┆ u32 │
╞════════╪═════════════════════╪═════════════════════╪═════════════════════╪═════╡
│ a      ┆ 2021-12-15 23:00:00 ┆ 2021-12-16 00:00:00 ┆ 2021-12-15 23:00:00 ┆ 1   │
│ 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   │
└────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────┘