폴드

폴드 (Folds)

Polars는 sum_horizontal, mean_horizontal, min_horizontal처럼 열들에 걸쳐 계산을 수행하는 많은 표현식을 제공해요. 하지만 이들은 모두 폴드(fold)라고 불리는 일반 알고리즘의 특수한 경우일 뿐이고, Polars는 특화 버전으로 부족할 때 사용자가 직접 폴드를 계산할 수 있는 일반 메커니즘을 제공합니다.

출처: 공식문서

함수 fold로 계산한 폴드는 최대 속도를 위해 전체 열에 대해 연산해요. 데이터 레이아웃을 매우 효율적으로 활용하고 종종 벡터화된 실행을 합니다.

기본 예시 (Basic example)

첫 예시로 함수 foldsum_horizontal을 다시 구현해 볼게요.

import operator
import polars as pl

df = pl.DataFrame(
    {
        "label": ["foo", "bar", "spam"],
        "a": [1, 2, 3],
        "b": [10, 20, 30],
    }
)

result = df.select(
    pl.fold(
        acc=pl.lit(0),
        function=operator.add,
        exprs=pl.col("a", "b"),
    ).alias("sum_fold"),
    pl.sum_horizontal(pl.col("a", "b")).alias("sum_horz"),
)

print(result)
shape: (3, 2)
┌──────────┬──────────┐
│ sum_fold ┆ sum_horz │
│ ---      ┆ ---      │
│ i32      ┆ i64      │
╞══════════╪══════════╡
│ 11       ┆ 11       │
│ 22       ┆ 22       │
│ 33       ┆ 33       │
└──────────┴──────────┘

함수 foldfunction 파라미터로 함수 f를 기대하고, f는 두 인자를 받아야 해요. 첫 번째 인자는 누적 결과(accumulator)로 우리가 0으로 초기화했고, 두 번째 인자는 exprs 파라미터에 나열된 표현식들의 연속적인 값들을 받습니다. 우리 경우에는 두 열 "a"와 "b"이지요.

아래 코드에는 함수 fold가 위에서 하는 일을 나타내는, 명시적인 세 번째 표현식이 포함되어 있어요.

acc = pl.lit(0)
f = operator.add

result = df.select(
    f(f(acc, pl.col("a")), pl.col("b")),
    pl.fold(acc=acc, function=f, exprs=pl.col("a", "b")).alias("sum_fold"),
)

print(result)
shape: (3, 2)
┌─────────┬──────────┐
│ literal ┆ sum_fold │
│ ---     ┆ ---      │
│ i64     ┆ i32      │
╞═════════╪══════════╡
│ 11      ┆ 11       │
│ 22      ┆ 22       │
│ 33      ┆ 33       │
└─────────┴──────────┘

Python에서의 fold: 대부분의 프로그래밍 언어에는 Polars의 함수 fold가 구현하는 알고리즘을 구현한 고차 함수가 있어요. Polars fold는 Python의 functools.reduce와 매우 비슷합니다. functools.reduce의 강력함에 대해 이 글에서 더 배울 수 있어요.

초기값 acc (The initial value acc)

누적기 acc에 고르는 초기값은, 항상 그런 건 아니지만 보통 적용하려는 연산의 항등원(identity element)이에요. 예를 들어 열들에 걸쳐 곱셈을 하고 싶다면 누적기를 0으로 설정하면 올바른 결과를 얻지 못할 거예요.

result = df.select(
    pl.fold(
        acc=pl.lit(0),
        function=operator.mul,
        exprs=pl.col("a", "b"),
    ).alias("prod"),
)

print(result)
shape: (3, 1)
┌──────┐
│ prod │
│ ---  │
│ i32  │
╞══════╡
│ 0    │
│ 0    │
│ 0    │
└──────┘

이를 고치려면 누적기 acc1로 설정해야 해요.

result = df.select(
    pl.fold(
        acc=pl.lit(1),
        function=operator.mul,
        exprs=pl.col("a", "b"),
    ).alias("prod"),
)

print(result)
shape: (3, 1)
┌──────┐
│ prod │
│ ---  │
│ i32  │
╞══════╡
│ 10   │
│ 40   │
│ 90   │
└──────┘

조건부 (Conditional)

데이터프레임의 모든 열에 조건/조건자(predicate)를 적용하고 싶은 경우, 폴드는 이걸 아주 간결하게 표현하는 방법이에요.

df = pl.DataFrame(
    {
        "a": [1, 2, 3],
        "b": [0, 1, 2],
    }
)

result = df.filter(
    pl.fold(
        acc=pl.lit(True),
        function=lambda acc, x: acc & x,
        exprs=pl.all() > 1,
    )
)
print(result)
shape: (1, 2)
┌─────┬─────┐
│ a   ┆ b   │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 3   ┆ 2   │
└─────┴─────┘

위 코드는 모든 열이 1보다 큰 행들을 필터링합니다.

폴드와 문자열 데이터 (Folds and string data)

폴드는 문자열 데이터를 연결하는 데 쓸 수 있어요. 하지만 중간 열의 구체화(materialization) 때문에 이 연산은 제곱 복잡도를 갖습니다. 그래서 이 용도에는 함수 concat_str를 쓰는 것을 권장해요.

df = pl.DataFrame(
    {
        "a": ["a", "b", "c"],
        "b": [1, 2, 3],
    }
)

result = df.select(pl.concat_str(["a", "b"]))
print(result)
shape: (3, 1)
┌─────┐
│ a   │
│ --- │
│ str │
╞═════╡
│ a1  │
│ b2  │
│ c3  │
└─────┘

더 알아보기 (Learn more)