스키마
스키마
Polars DataFrame나 LazyFrame의 스키마는 컬럼 이름과 그 데이터 타입을 정의하는 역할을 해요. 스키마는 DataFrame 또는 LazyFrame에 .collect_schema 메서드를 호출해 확인할 수 있습니다.
출처: 공식문서
import polars as pl
lf = pl.LazyFrame({"foo": ["a", "b", "c"], "bar": [0, 1, 2]})
print(lf.collect_schema())
Schema({'foo': String, 'bar': Int64})
이 스키마는 lazy API에서 아주 중요한 역할을 해요.
lazy API에서의 타입 검사
lazy API의 장점 중 하나는 데이터가 처리되기 전에 Polars가 스키마를 검사한다는 점이에요. 이 검사는 lazy 쿼리를 실행할 때 일어납니다.
다음처럼 문자열 컬럼 foo에 .round 표현식을 호출하는 간단한 예를 볼게요.
lf = pl.LazyFrame({"foo": ["a", "b", "c"]}).with_columns(pl.col("foo").round(2))
.round 표현식은 숫자 데이터 타입을 가진 컬럼에서만 유효해요. 문자열 컬럼에 .round를 호출하면 collect로 쿼리를 평가할 때 InvalidOperationError가 발생합니다. 이 스키마 검사는 collect를 호출할 때, 즉 데이터가 처리되기 전에 이루어집니다.
try:
print(lf.collect())
except Exception as e:
print(f"{type(e).__name__}: {e}")
InvalidOperationError: rounding ('half_to_even') can only be used on numeric types This error occurred in the following expression: col("foo").round()
이 쿼리를 eager 모드로 실행했다면, 앞 단계의 데이터 처리가 모두 끝난 뒤에야 오류를 발견할 수 있었을 거예요. 반면 lazy 쿼리를 실행하면 Polars는 파이프라인에서 실제로 시간이 걸리는 데이터 처리 단계가 시작되기 전에 잠재적인 InvalidOperationError를 체크해 줍니다.
lazy API는 스키마를 알고 있어야 한다
lazy API에서 Polars 쿼리 옵티마이저는 쿼리 플랜의 모든 단계에서 스키마를 추론할 수 있어야 해요. 즉 스키마를 미리 알 수 없는 연산은 lazy API에서 쓸 수 없다는 뜻입니다.
스키마를 미리 알 수 없는 연산의 대표적인 예가 .pivot 연산이에요. .pivot에서 새 컬럼 이름은 데이터 중 한 컬럼의 값에서 나옵니다. 이런 컬럼 이름은 미리 알 수 없기 때문에 .pivot은 lazy API에서 사용할 수 없습니다.
lazy API에 없는 연산 다루기
파이프라인에 lazy API에서 제공되지 않는 연산이 있다면, 보통 이런 순서로 처리하는 게 좋습니다.
- 파이프라인을 그 지점까지 lazy 모드로 실행한다.
.collect로 실행해DataFrame으로 구체화(materialize)한다.DataFrame에서 lazy가 아닌 연산을 수행한다..lazy로 다시LazyFrame으로 바꿔 lazy 모드를 이어간다.
이런 lazy가 아닌 연산을 다루는 예를 하나 보여드릴게요. 여기서는:
- 간단한
DataFrame을 만들고, .lazy로LazyFrame으로 바꾸고,.with_columns로 변환을 수행하고,- pivot 전에
.collect로 쿼리를 실행해DataFrame을 얻고, DataFrame에서.pivot을 수행하고,- 다시 lazy 모드로 바꾸고,
.filter를 수행하고,- 마지막에
.collect로 쿼리를 실행해DataFrame을 얻습니다.
lazy_eager_query = (
pl.LazyFrame(
{
"id": ["a", "b", "c"],
"month": ["jan", "feb", "mar"],
"values": [0, 1, 2],
}
)
.with_columns((2 * pl.col("values")).alias("double_values"))
.collect()
.pivot(index="id", on="month", values="double_values", aggregate_function="first")
.lazy()
.filter(pl.col("mar").is_null())
.collect()
)
print(lazy_eager_query)
shape: (2, 4)
┌─────┬──────┬──────┬──────┐
│ id ┆ jan ┆ feb ┆ mar │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ i64 │
╞═════╪══════╪══════╪══════╡
│ a ┆ 0 ┆ null ┆ null │
│ b ┆ null ┆ 2 ┆ null │
└─────┴──────┴──────┴──────┘