컨텍스트 (Contexts)
컨텍스트 (Contexts)
Polars는 데이터를 변형하기 위한 자체 DSL(Domain Specific Language, 도메인 특화 언어)을 갖추고 있어요. 이 언어는 사용하기 매우 쉬우면서도, 사람이 읽을 수 있는 형태로 복잡한 쿼리를 표현할 수 있게 해 줍니다. 이 언어의 두 핵심 구성 요소는 **컨텍스트(Context)**와 **익스프레션(Expression)**인데요, 익스프레션은 다음 섹션에서 다루고 여기서는 컨텍스트를 살펴볼게요.
컨텍스트는 이름에서도 짐작되듯이, 익스프레션이 평가되는 맥락을 가리킵니다. 컨텍스트는 크게 세 가지가 있어요:
- 셀렉션 (Selection):
df.select(...),df.with_columns(...) - 필터링 (Filtering):
df.filter() - 그룹화 / 집계 (Group by / Aggregation):
df.group_by(...).agg(...)
아래 예시들은 다음 DataFrame을 대상으로 진행합니다.
df = pl.DataFrame({
"nrs": [1, 2, 3, None, 5],
"names": ["foo", "ham", "spam", "egg", None],
"random": np.random.rand(5),
"groups": ["A", "A", "B", "C", "B"],
})
print(df)
use rand::{thread_rng, Rng};
let mut arr = [0f64; 5];
thread_rng().fill(&mut arr);
let df = df!(
"nrs" => &[Some(1), Some(2), Some(3), None, Some(5)],
"names" => &[Some("foo"), Some("ham"), Some("spam"), Some("eggs"), None],
"random" => &arr,
"groups" => &["A", "A", "B", "C", "B"],
)?;
println!("{}", &df);
shape: (5, 4)
┌──────┬───────┬──────────┬────────┐
│ nrs ┆ names ┆ random ┆ groups │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ str │
╞══════╪═══════╪══════════╪════════╡
│ 1 ┆ foo ┆ 0.154163 ┆ A │
│ 2 ┆ ham ┆ 0.74005 ┆ A │
│ 3 ┆ spam ┆ 0.263315 ┆ B │
│ null ┆ egg ┆ 0.533739 ┆ C │
│ 5 ┆ null ┆ 0.014575 ┆ B │
└──────┴───────┴──────────┴────────┘
셀렉션 (Selection)
셀렉션 컨텍스트는 컬럼에 대해 익스프레션을 적용합니다. select는 집계의 결과, 익스프레션의 조합, 또는 리터럴 같은 새로운 컬럼을 만들어 낼 수 있어요.
셀렉션 컨텍스트 안의 익스프레션은 모두 같은 길이의 Series를 만들어야 하거나, 길이가 1인 Series를 만들어야 합니다. 리터럴은 길이 1의 Series로 취급됩니다. 어떤 익스프레션은 길이 1의 Series를 만들고 다른 익스프레션은 더 긴 Series를 만들 때, 길이 1의 Series는 나머지 Series의 길이에 맞춰 **브로드캐스트(broadcast)**됩니다.
브로드캐스팅은 익스프레션 내부에서도 일어날 수 있다는 점도 참고하세요. 예를 들어 pl.col("value") / pl.col("value").sum() 에서는 value 컬럼의 각 원소가 그 컬럼 전체의 합으로 나뉩니다.
out = df.select(
pl.sum("nrs"),
pl.col("names").sort(),
pl.col("names").first().alias("first name"),
(pl.mean("nrs") * 10).alias("10xnrs"),
)
print(out)
let out = df.clone().lazy().select([
sum("nrs"),
col("names").sort(Default::default()),
col("names").first().alias("first name"),
(mean("nrs") * lit(10)).alias("10xnrs"),
]).collect()?;
println!("{}", out);
shape: (5, 4)
┌─────┬───────┬────────────┬────────┐
│ nrs ┆ names ┆ first name ┆ 10xnrs │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str ┆ f64 │
╞═════╪═══════╪════════════╪════════╡
│ 11 ┆ null ┆ foo ┆ 27.5 │
│ 11 ┆ egg ┆ foo ┆ 27.5 │
│ 11 ┆ foo ┆ foo ┆ 27.5 │
│ 11 ┆ ham ┆ foo ┆ 27.5 │
│ 11 ┆ spam ┆ foo ┆ 27.5 │
└─────┴───────┴────────────┴────────┘
쿼리에서 볼 수 있듯이, 셀렉션 컨텍스트는 매우 강력해서 서로 독립적(그리고 병렬적으로)으로 임의의 익스프레션을 평가할 수 있게 해 줍니다.
select 문과 비슷하게, with_columns 문도 셀렉션 컨텍스트로 들어갑니다. with_columns와 select의 가장 큰 차이는, with_columns는 기존 컬럼을 유지한 채 새로운 컬럼을 추가하는 반면 select는 기존 컬럼을 버린다는 점입니다.
df = df.with_columns(
pl.sum("nrs").alias("nrs_sum"),
pl.col("random").count().alias("count"),
)
print(df)
let out = df.clone().lazy().with_columns([
sum("nrs").alias("nrs_sum"),
col("random").count().alias("count"),
]).collect()?;
println!("{}", out);
shape: (5, 6)
┌──────┬───────┬──────────┬────────┬─────────┬───────┐
│ nrs ┆ names ┆ random ┆ groups ┆ nrs_sum ┆ count │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ str ┆ i64 ┆ u32 │
╞══════╪═══════╪══════════╪════════╪═════════╪═══════╡
│ 1 ┆ foo ┆ 0.154163 ┆ A ┆ 11 ┆ 5 │
│ 2 ┆ ham ┆ 0.74005 ┆ A ┆ 11 ┆ 5 │
│ 3 ┆ spam ┆ 0.263315 ┆ B ┆ 11 ┆ 5 │
│ null ┆ egg ┆ 0.533739 ┆ C ┆ 11 ┆ 5 │
│ 5 ┆ null ┆ 0.014575 ┆ B ┆ 11 ┆ 5 │
└──────┴───────┴──────────┴────────┴─────────┴───────┘
필터링 (Filtering)
필터링 컨텍스트는 Boolean 데이터 타입으로 평가되는 하나 이상의 익스프레션을 기준으로 DataFrame을 필터링합니다.
shape: (2, 6)
┌─────┬───────┬──────────┬────────┬─────────┬───────┐
│ nrs ┆ names ┆ random ┆ groups ┆ nrs_sum ┆ count │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ str ┆ i64 ┆ u32 │
╞═════╪═══════╪══════════╪════════╪═════════╪═══════╡
│ 3 ┆ spam ┆ 0.263315 ┆ B ┆ 11 ┆ 5 │
│ 5 ┆ null ┆ 0.014575 ┆ B ┆ 11 ┆ 5 │
└─────┴───────┴──────────┴────────┴─────────┴───────┘
그룹화 / 집계 (Group by / aggregation)
group_by 컨텍스트에서는 익스프레션이 그룹을 대상으로 동작하므로, 결과의 길이가 달라질 수 있습니다. (한 그룹은 여러 멤버를 가질 수 있으니까요.)
out = df.group_by("groups").agg(
pl.sum("nrs"), # sum nrs by groups
pl.col("random").count().alias("count"), # count group members
# sum random where name != null
pl.col("random").filter(pl.col("names").is_not_null()).sum().name.suffix("_sum"),
pl.col("names").reverse().alias("reversed names"),
)
print(out)
let out = df.lazy().group_by([col("groups")]).agg([
sum("nrs"), // sum nrs by groups
col("random").count().alias("count"), // count group members
// sum random where name != null
col("random").filter(col("names").is_not_null()).sum().name().suffix("_sum"),
col("names").reverse().alias("reversed names"),
]).collect()?;
println!("{}", out);
shape: (3, 5)
┌────────┬─────┬───────┬────────────┬────────────────┐
│ groups ┆ nrs ┆ count ┆ random_sum ┆ reversed names │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ u32 ┆ f64 ┆ list[str] │
╞════════╪═════╪═══════╪════════════╪════════════════╡
│ A ┆ 3 ┆ 2 ┆ 0.894213 ┆ ["ham", "foo"] │
│ B ┆ 8 ┆ 2 ┆ 0.263315 ┆ [null, "spam"] │
│ C ┆ 0 ┆ 1 ┆ 0.533739 ┆ ["egg"] │
└────────┴─────┴───────┴────────────┴────────────────┘
결과에서 볼 수 있듯이, 모든 익스프레션은 group_by 컨텍스트가 정의한 그룹에 대해 적용됩니다.
표준 group_by 외에도 group_by_dynamic, group_by_rolling 역시 그룹화 컨텍스트로 들어가는 진입점입니다. 추가로 List와 SQL 컨텍스트가 있는데, 이들은 이 가이드의 뒷부분에서 다룹니다. 지금 단계에서는 이해를 돕기 위해 범위에서 제외할게요.