分组
按固定窗口分组
我们可以使用group_by_dynamic来计算时间统计,将行按天/月/年等进行分组。
年平均示例
在以下简单示例中,我们计算了苹果股票价格的年度平均收盘价。我们首先从CSV加载数据:
df = pl.read_csv("docs/assets/data/apple_stock.csv", try_parse_dates=True)
df = df.sort("Date")
print(df)
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列的平均值:
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中,窗口由参数every和period固定。而在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。
组合分组操作
滚动和动态分组操作可以与普通的分组操作结合使用。
下面是一个带有动态分组的示例。
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)
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 │
└─────────────────────┴────────┘
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 │
└────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────┘