기본 연산

기본 연산 (Basic operations)

데이터프레임의 열에 대고 가장 흔히 하는 일은 역시 계산이에요. 이번 섹션에서는 산술·비교 같은 기본 연산과, 중복값 세기, 조건 분기까지 Polars에서 쓰는 기본기를 차례로 살펴볼게요. 앞으로 나올 예시들은 모두 아래 데이터프레임을 기준으로 하니, 열 이름과 값(특히 nrsnull)을 먼저 눈에 익혀 두면 좋아요.

출처: 공식문서

기본 산술 연산 (Basic arithmetic)

Polars는 길이가 같은 시리즈끼리, 또는 시리즈와 리터럴(literal) 사이의 기본 산술 연산을 지원해요. 리터럴이 시리즈와 섞일 때는 리터럴이 시리즈 길이에 맞춰 브로드캐스트(broadcast)된다는 점도 기억해 두세요.

result = df.select(
    (pl.col("nrs") + 5).alias("nrs + 5"),
    (pl.col("nrs") - 5).alias("nrs - 5"),
    (pl.col("nrs") * pl.col("random")).alias("nrs * random"),
    (pl.col("nrs") / pl.col("random")).alias("nrs / random"),
    (pl.col("nrs") ** 2).alias("nrs ** 2"),
    (pl.col("nrs") % 3).alias("nrs % 3"),
)

print(result)
shape: (5, 6)
┌─────────┬─────────┬──────────────┬──────────────┬──────────┬─────────┐
│ nrs + 5 ┆ nrs - 5 ┆ nrs * random ┆ nrs / random ┆ nrs ** 2 ┆ nrs % 3 │
│ ---     ┆ ---     ┆ ---          ┆ ---          ┆ ---      ┆ ---     │
│ i64     ┆ i64     ┆ f64          ┆ f64          ┆ i64      ┆ i64     │
╞═════════╪═════════╪══════════════╪══════════════╪══════════╪═════════╡
│ 6       ┆ -4      ┆ 0.37454      ┆ 2.669941     ┆ 1        ┆ 1       │
│ 7       ┆ -3      ┆ 1.901429     ┆ 2.103681     ┆ 4        ┆ 2       │
│ 8       ┆ -2      ┆ 2.195982     ┆ 4.098395     ┆ 9        ┆ 0       │
│ null    ┆ null    ┆ null         ┆ null         ┆ null     ┆ null    │
│ 10      ┆ 0       ┆ 0.780093     ┆ 32.047453    ┆ 25       ┆ 2       │
└─────────┴─────────┴──────────────┴──────────────┴──────────┴─────────┘

위 예시에서 눈여겨볼 점은, 산술 연산의 피연산자 중 하나가 null이면 결과도 null이 된다는 거예요. nrsnull인 네 번째 행이 전부 null로 나오는 이유죠.

Polars는 연산자 오버로딩을 지원해서 표현식 안에서 언어 고유의 산술 연산자를 그대로 쓸 수 있어요. 취향에 따라 Python에서는 아래처럼 같은 동작을 하는 이름 있는 함수를 쓸 수도 있습니다.

# Python only:
result_named_operators = df.select(
    (pl.col("nrs").add(5)).alias("nrs + 5"),
    (pl.col("nrs").sub(5)).alias("nrs - 5"),
    (pl.col("nrs").mul(pl.col("random"))).alias("nrs * random"),
    (pl.col("nrs").truediv(pl.col("random"))).alias("nrs / random"),
    (pl.col("nrs").pow(2)).alias("nrs ** 2"),
    (pl.col("nrs").mod(3)).alias("nrs % 3"),
)

print(result.equals(result_named_operators))

비교 연산 (Comparisons)

산술 연산과 마찬가지로, Polars의 비교 연산도 오버로딩된 연산자 또는 같은 이름의 함수로 쓸 수 있어요.

result = df.select(
    (pl.col("nrs") > 1).alias("nrs > 1"),  # .gt
    (pl.col("nrs") >= 3).alias("nrs >= 3"),  # ge
    (pl.col("random") < 0.2).alias("random < .2"),  # .lt
    (pl.col("random") <= 0.5).alias("random <= .5"),  # .le
    (pl.col("nrs") != 1).alias("nrs != 1"),  # .ne
    (pl.col("nrs") == 1).alias("nrs == 1"),  # .eq
)
print(result)
shape: (5, 6)
┌─────────┬──────────┬─────────────┬──────────────┬──────────┬──────────┐
│ nrs > 1 ┆ nrs >= 3 ┆ random < .2 ┆ random <= .5 ┆ nrs != 1 ┆ nrs == 1 │
│ ---     ┆ ---      ┆ ---         ┆ ---          ┆ ---      ┆ ---      │
│ bool    ┆ bool     ┆ bool        ┆ bool         ┆ bool     ┆ bool     │
╞═════════╪══════════╪═════════════╪══════════════╪══════════╪══════════╡
│ false   ┆ false    ┆ false       ┆ true         ┆ false    ┆ true     │
│ true    ┆ false    ┆ false       ┆ false        ┆ true     ┆ false    │
│ true    ┆ true     ┆ false       ┆ false        ┆ true     ┆ false    │
│ null    ┆ null     ┆ false       ┆ false        ┆ null     ┆ null     │
│ true    ┆ true     ┆ true        ┆ true         ┆ true     ┆ false    │
└─────────┴──────────┴─────────────┴──────────────┴──────────┴──────────┘

비교 결과는 모두 bool로 나오고, 비교 대상이 null이면 그 결과도 null이라는 점이 포인트예요. 이 불리언 결과가 곧 필터의 재료가 되지요.

불리언·비트 연산 (Boolean and bitwise operations)

언어에 따라 불리언 연산 "and", "or", "not"을 &, |, ~ 연산자로 쓸 수도 있고, 같은 이름의 함수로 쓸 수도 있어요.

# Boolean operators & | ~
result = df.select(
    ((~pl.col("nrs").is_null()) & (pl.col("groups") == "A")).alias(
        "number not null and group A"
    ),
    ((pl.col("random") < 0.5) | (pl.col("groups") == "B")).alias(
        "random < 0.5 or group B"
    ),
)

print(result)

# Corresponding named functions `and_`, `or_`, and `not_`.
result2 = df.select(
    (pl.col("nrs").is_null().not_().and_(pl.col("groups") == "A")).alias(
        "number not null and group A"
    ),
    ((pl.col("random") < 0.5).or_(pl.col("groups") == "B")).alias(
        "random < 0.5 or group B"
    ),
)
print(result.equals(result2))

여기서 짚고 넘어갈 작은 비밀이 있는데요. Python의 함수 이름이 and_, or_, not_인 이유는 and, or, not이 Python 예약어이기 때문이에요. 또 이 키워드들을 불리언 연산자로 쓰면 Truthy·Falsy 맥락에서 피연산자를 해석해 버리기 때문에, Polars는 차선책으로 비트 연산자 &, |, ~를 불리언 연산자로 오버로딩했어요.

이 연산자들은 각각의 비트 연산으로도 쓰일 수 있고, 이때는 비트 연산자 ^ / 함수 xor도 함께 쓸 수 있습니다.

result = df.select(
    pl.col("nrs"),
    (pl.col("nrs") & 6).alias("nrs & 6"),
    (pl.col("nrs") | 6).alias("nrs | 6"),
    (~pl.col("nrs")).alias("not nrs"),
    (pl.col("nrs") ^ 6).alias("nrs ^ 6"),
)

print(result)

고유값 세기 (Counting unique values)

시리즈 안의 고유값 개수를 세는 함수는 두 가지가 있어요. n_unique는 정확한 고유값 개수를 세는 함수인데, 아주 큰 데이터셋에서는 꽤 느릴 수 있어요. 그럴 때 근삿값으로 충분하다면 approx_n_unique를 쓸 수 있는데, 이 함수는 HyperLogLog++ 알고리즘으로 결과를 추정합니다.

long_df = pl.DataFrame({"numbers": np.random.randint(0, 100_000, 100_000)})

result = long_df.select(
    pl.col("numbers").n_unique().alias("n_unique"),
    pl.col("numbers").approx_n_unique().alias("approx_n_unique"),
)

print(result)

고유값과 그 개수를 함께 알고 싶다면 Polars가 제공하는 value_counts 함수를 쓰면 돼요. 이 함수는 결과를 struct로 돌려주는데, struct는 뒤쪽 섹션에서 살펴볼 데이터 타입입니다.

result = df.select(
    pl.col("names").value_counts().alias("value_counts"),
)

print(result)

고유값만 담긴 시리즈나 고유 개수만 담긴 시리즈가 필요한 경우도 한 함수면 끝이에요.

result = df.select(
    pl.col("names").unique(maintain_order=True).alias("unique"),
    pl.col("names").unique_counts().alias("unique_counts"),
)

print(result)

unique에서 maintain_order=True를 지정하는 이유가 있어요. 그렇게 해야 결과 순서가 unique_counts 결과의 순서와 일치하거든요. 자세한 내용은 API 참조를 확인하세요.

조건 분기 (Conditionals)

Polars는 삼항 연산자와 비슷한 것을 함수 when으로 지원하는데, 이어지는 함수 then과 선택적인 함수 otherwise가 뒤따라요. when은 조건(predicate) 표현식을 받고, True로 평가되는 값은 then 안쪽 표현식 값으로, False로 평가되는 값은 otherwise 안쪽 값(또는 otherwise를 주지 않으면 null)으로 바꿉니다.

아래 예시는 "nrs" 열의 숫자에 Collatz 추측의 한 단계를 적용한 거예요. 홀수면 3배한 뒤 1을 더하고, 짝수면 2로 나누는 로직이죠.

result = df.select(
    pl.col("nrs"),
    pl.when(pl.col("nrs") % 2 == 1)  # Is the number odd?
    .then(3 * pl.col("nrs") + 1)  # If so, multiply by 3 and add 1.
    .otherwise(pl.col("nrs") // 2)  # If not, divide by 2.
    .alias("Collatz"),
)

print(result)

when-then 블록을 연달아 여러 개 이어 붙이면 Python의 elif 문처럼 임의 개수의 조건 분기를 만들 수도 있어요. 이때 Polars는 각 값에 대해, 앞선 조건들이 모두 실패했을 때에만 체인에서 더 안쪽에 있는 대체 표현식을 고려합니다.

더 알아보기 (Learn more)