필터 (Filter)

필터 (Filter)

데이터를 다루다 보면 "전체 데이터가 아니라, 조건에 맞는 행만 뽑아서 보고 싶다"는 일이 가장 먼저 마주치는 요구죠. Polars에서 그 일을 맡는 게 바로 filter 컨텍스트예요. 이 섹션에서는 필터가 어떻게 동작하는지, 그리고 필터 조건을 만드는 비교 연산과 불리언 연산을 함께 살펴볼게요.

필터 컨텍스트 (The filter context)

filter 컨텍스트는 불리언(Boolean) 데이터 타입으로 평가되는 하나 이상의 표현식에 맞는 행만 남깁니다. 다시 말해, 각 행마다 조건이 참(true)인지 거짓(false)인지를 판단해서, 참인 행만 골라내는 거예요.

아래는 filter를 사용하는 예시입니다. 몸무게와 키, 생년월일을 담은 데이터프레임에서, 생년월일이 특정 범위 안에 들고 키가 1.7보다 큰 행만 뽑아요.

result = df.filter(
    pl.col("birthdate").is_between(date(1982, 12, 31), date(1996, 1, 1)),
    pl.col("height") > 1.7,
)
print(result)
shape: (1, 4)
┌───────────┬────────────┬────────┬────────┐
│ name      ┆ birthdate  ┆ weight ┆ height │
│ ---       ┆ ---        ┆ ---    ┆ ---    │
│ str       ┆ date       ┆ f64    ┆ f64    │
╞═══════════╪════════════╪════════╪════════╡
│ Ben Brown ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
└───────────┴────────────┴────────┴────────┘

filter에 여러 조건을 쉼표로 나열하면 그 조건들이 모두 만족되는 행만 남아요. 각 조건은 불리언 값이 되는 표현식이어야 하고요.

비교로 조건 만들기 (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이라는 것. 비교 연산은 값 자체를 만드는 게 아니라 **불리언 마스크(boolean mask)**를 만들어 내는 데 쓰여요. 이 마스크가 곧 필터의 조건이 되는 거죠.

둘째, 비교 대상이 null이면 결과도 null이라는 것. 위 표에서 nrsnull인 네 번째 행은 비교 결과가 null로 나오는 걸 볼 수 있어요. 값이 "참도 거짓도 아니다"라는 의미지요.

조건을 묶는 불리언 연산 (Boolean operations)

필터 조건 하나만으로는 부족할 때가 많아요. "A이면서 B인 행" 또는 "A이거나 B인 행"처럼 조건을 묶어야 할 때가 있죠. 그때 쓰는 게 불리언 연산자입니다. 언어에 따라 &(and), |(or), ~(not) 연산자, 또는 같은 이름의 함수 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))
shape: (5, 2)
┌─────────────────────────────┬─────────────────────────┐
│ number not null and group A ┆ random < 0.5 or group B │
│ ---                         ┆ ---                     │
│ bool                        ┆ bool                    │
╞═════════════════════════════╪═════════════════════════╡
│ true                        ┆ true                    │
│ true                        ┆ false                   │
│ false                       ┆ true                    │
│ false                       ┆ false                   │
│ false                       ┆ true                    │
└─────────────────────────────┴─────────────────────────┘
True

첫 번째 조건을 따라가 볼게요. ~pl.col("nrs").is_null()은 "nrsnull이 아니다"라는 뜻이고, 여기에 & (pl.col("groups") == "A")로 "그리고 groupsA다"라는 조건을 묶었어요. 그 결과 열은 "숫자가 null이 아니면서 A 그룹인가"라는 진실값이 되고요.

연산자로 쓴 결과(result)와 이름 있는 함수로 쓴 결과(result2)가 result.equals(result2)에서 True로 나오는 걸 보면, 두 방식은 완전히 같은 일을 한다는 걸 알 수 있어요.

이렇게 해서 만들어진 불리언 마스크나 불리언 표현식을 filter 컨텍스트에 넘기면, 원하는 조건의 행만 남긴 데이터프레임을 얻을 수 있습니다. 비교로 조건을 만들고, &·|·~로 조건을 묶고, 그 결과를 filter에 전달하는 것 — 이것이 Polars에서 행을 골라내는 기본 흐름이에요.