시간 기반 그룹핑
시간 기반 그룹핑 (Grouping)
시간 데이터를 일·월·년 단위로 묶어 통계를 내야 할 때가 많아요. Polars는 group_by_dynamic으로 고정된 시간 창(window)으로 그룹핑하고, rolling으로 데이터 값에 따라 움직이는 창으로 그룹핑하는 두 가지 방식을 제공합니다. 이 섹션에서는 두 방식과 그 파라미터를 차례로 살펴볼게요.
출처: 공식문서
고정된 창으로 그룹핑 (Grouping by fixed windows)
group_by_dynamic을 사용해 행을 일/월/년 등으로 그룹핑하면 시간 기반 통계를 계산할 수 있어요.
연평균 예시 (Annual average example)
아래 간단한 예시에서 Apple 주식 가격의 연평균 종가를 계산할게요. 먼저 CSV에서 데이터를 로드합니다:
from datetime import date, datetime
import polars as pl
df = pl.read_csv("docs/assets/data/apple_stock.csv", try_parse_dates=True)
df = df.sort("Date")
print(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칼럼의 평균값을 구한다:
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)
연평균 종가는 다음과 같습니다:
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 ┆ … ┆ 2013 │
└────────────┴───────────┴──────┘
group_by_dynamic 파라미터 (Parameters for group_by_dynamic)
동적 창(dynamic window)은 다음으로 정의됩니다:
- every: 창의 간격(interval)을 나타냄
- period: 창의 지속 시간(duration)을 나타냄
- offset: 창의 시작을 오프셋하는 데 사용
every의 값은 그룹이 얼마나 자주 시작하는지를 정해요. 시간 기간 값은 유연해서, 예를 들어:
1y를2y로 바꿔 2년 간격의 평균을 구할 수 있고1y를1y6mo로 바꿔 18개월 기간의 평균을 구할 수 있어요
또한 period 파라미터로 각 그룹의 시간 기간이 얼마나 길지 정할 수 있습니다. 예를 들어 every를 1y로, period를 2y로 설정하면, 그룹이 1년 간격으로 시작하면서 각 그룹이 2년을 아우르는 형태가 돼요.
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 파라미터는 출력에서 각 그룹에 연결되는 datetime 값이 무엇인지 결정하는 불리언 변수예요. 위 예시에서 첫 번째 데이터 포인트는 1981년 2월 23일에 있어요. truncate = True(기본값)라면 연평균의 첫 해의 날짜는 1981년 1월 1일이 됩니다. 그러나 truncate = False라면 연평균의 첫 해의 날짜는 첫 데이터 포인트의 날짜인 1981년 2월 23일이 돼요. truncate는 Date 칼럼에 표시되는 것만 영향 줄 뿐, 창 경계에는 영향을 주지 않는다는 점을 기억하세요.
group_by_dynamic에서 표현식 사용 (Using expressions in group_by_dynamic)
그룹 바이 연산에서 mean 같은 단순한 집계만 사용할 수 있는 건 아니에요. Polars에서 사용 가능한 전체 표현식 범위를 쓸 수 있습니다.
아래 코드 조각에서는 2021년의 매일 ("1d")인 date range를 만들고 이것을 DataFrame으로 바꿔요.
그런 다음 group_by_dynamic에서 매월 ("1mo") 시작하고 창 길이가 1개월인 동적 창을 만듭니다. 그 동적 창에 맞는 값들은 해당 그룹에 배정되어, 강력한 표현식 API로 집계할 수 있어요.
아래 예시에서는 group_by_dynamic으로 다음을 계산합니다:
- 월말까지 남은 일 수
- 한 달의 일 수
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)
롤링 창으로 그룹핑 (Grouping by rolling windows)
롤링 연산인 rolling은 group_by/agg 컨텍스트로 들어가는 또 하나의 입구예요. 다만 창이 every와 period 파라미터로 고정되는 group_by_dynamic과 달리, 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의 수와 같아요.
그룹 바이 연산 결합 (Combining group by operations)
롤링 및 동적 그룹 바이 연산은 일반 그룹 바이 연산과 결합할 수 있어요.
아래는 동적 그룹 바이의 예시입니다.
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)
out = df.group_by_dynamic(
"time",
every="1h",
closed="both",
group_by="groups",
include_boundaries=True,
).agg(pl.len())
print(out)
더 알아보기 (Learn more)
- 시간 기반 그룹핑을 이용한 다운샘플링은 리샘플링 (Resampling) 문서를 참고하세요.