Polars

손호준·2023년 11월 14일

Polars

https://www.pola.rs/

Polars는 속도와 메모리 효율성을 위해 구축된 강력한 rust기반의 데이터 조작 라이브러리다. Pandas와 유사한 API를 제공하지만 성능이 더 좋고, 대규모 데이터 세트를 처리하는 데 적합하다. -> 파이썬용, 러스트용 두가지 제공

Polars는 지연 평가 (lazy evaluation)와 병렬 작업 실행 기능 덕분에 메모리에 맞지 않는 대규모 데이터 세트를 처리하도록 설계되었다.
지연 평가 및 병렬 처리는 메모리 사용량을 최소화함으로써 대규모 데이터 세트를 효율적으로 처리할 수 있다. 또한 분산 컴퓨팅을 지원하며 Apache Arrow와 원활하게 통합될 수 있다.

Polars는 매우 빠른 dataframe 라이브러리를 제공하는 것을 목표로한다.

Polars 특징

  • 사용 가능한 모든 코어 활용
  • 불필요한 작업/메모리 할당을 줄이기 위해 쿼리 최적화
  • 사용 가능한 RAM보다 훨씬 큰 데이터 셋까지 처리 가능
  • 일관되고 예측 가능한 API
  • 엄격한 스키마(쿼리 실행 전, 데이터 유형을 알아야함)

Polars 에서 사용 가능한 주요 features

# Cargo.toml
[dependencies]
polars = { version = "0.26.1", features = ["lazy", "temporal", "describe", "json", "parquet", "dtype-datetime"] }

그 밖의 features 들은 공식 문서 참고(https://pola-rs.github.io/polars/user-guide/installation/#rust)

Data types

Polars는 완전히 Arrow의 데이터 타입과 메모리 구조를 따른다. 따라서 데이터 전처리를 캐시 효율적이게 하고, 프로세스 간 통신이 잘 지원된다. Utf-8(=LargeUtf8), Categorical, Object 을 제외한 대부분의 데이터 타입들은 Arrow의 구현을 따른다. 눈여겨 볼건 Nested 타입들.

그룹타입세부사항
NestedStructStruct 배열은 Vec 로 표시되며, 단일 열에 multiple/heterogenous한 값들을 묶는데 유용함
ListList배열은 리스트 값과 오프셋 배열을 갖는 child 배열을 포함(실제로 Arrow의 LargeList도 내부적으로 이렇게 되어있음)

Data structures

pandas와 마찬가지로 시리즈, 데이터 프레임이 있다. 데이터프레임에 사용하는 함수(head,tail,sample,describe)

Expressions

Polars는 다음 두가지 특성으로 핵심적인 데이터 변환(eg.group_by로 그룹 크기 계산..)을 매우 빠르게 수행할 수 있다.

  • 각 표현식에 대한 자동 쿼리 최적화
  • 여러 열에 대한 표현식의 자동 병렬화 -> 모든 표현식은 병렬로 실행됨. 표현식 내에서는 더 많은 병렬화가 진행될 수 있다.

Lazy / eager API

Polars는 두 가지 모드를 지원하는데: lazy 와 eager 이다. eager API에서 쿼리는 즉각적으로 실행되는 반면, lazy API에서 쿼리는 필요할 때만 evaluated된다. 실행을 마지막 순간까지 연기하면 상당한 성능 이점을 얻을 수 있으므로 대부분의 경우 Lazy API가 선호된다.

//eager API 예시
let df = CsvReader::from_path("docs/data/iris.csv")
    .unwrap()
    .finish()
    .unwrap();
let mask = df.column("sepal_length")?.f64()?.gt(5.0);
let df_small = df.filter(&mask)?;
let df_agg = df_small
    .group_by(["species"])?
    .select(["sepal_width"])
    .mean()?;
println!("{}", df_agg);


// lazy API 예시
let q = LazyCsvReader::new("docs/data/iris.csv")
    .has_header(true)
    .finish()?
    .filter(col("sepal_length").gt(lit(5)))
    .group_by(vec![col("species")])
    .agg([col("sepal_width").mean()]);
let df = q.collect()?;
println!("{}", df);

지연API를 사용할 경우, 모든 단계가 정의될 때까지 실행을 기다려서 쿼리 플래너가 다음과 같은 다양한 최적화를 수행할 수 있다.

  • Predicate pushdown: eg. 데이터셋을 읽는 동안 가능한한 빨리 필터를 적용한다.
  • Projection pushdown: eg. : 데이터 세트를 읽는 동안 필요한 열만 선택하므로 추가 열을 로드할 필요가 없다.

이를 통해 메모리 및 CPU의 부하가 크게 줄어들어 더 큰 데이터 세트를 메모리에 맞추고 더 빠르게 처리할 수 있다. 쿼리가 정의되면 collect를 호출하여 Polars에게 실행하겠다고 알린다.
일반적으로 중간 결과에 관심이 있거나 탐색 작업을 수행 중이고 쿼리가 어떻게 나타날지 아직 모르는 경우가 아니면 lazy API를 쓴다.

lazy API의 또 다른 이점 중 하나는 쿼리를 스트리밍 방식으로 실행할 수 있다는 점인데, 데이터를 한꺼번에 처리하는 대신 쿼리를 batches로 실행하여 메모리보다 큰 데이터 세트를 처리할 수 있다.

Polars에게 스트리밍 모드에서 쿼리를 실행하고 싶다고 알리기 위해 streaming=True인수를 collect()에 전달한다.

//Streaming 예시
let q = LazyCsvReader::new("docs/data/iris.csv")
    .has_header(true)
    .finish()?
    .filter(col("sepal_length").gt(lit(5)))
    .group_by(vec![col("species")])
    .agg([col("sepal_width").mean()]);

let df = q.with_streaming(true).collect()?;
println!("{}", df);

모든 lazy 작업이 스트리밍을 지원하는 건 아니고, 스트리밍이 지원되지 않는 작업이 있는 경우 Polars는 비스트림 모드에서 쿼리를 실행한다.

스트리밍이 지원되는 작업 list

  • filter, slice, head,tail
  • with_columns,select
  • group_by
  • join
  • sort
  • explode,melt
  • scan_csv, scan_parquet,scan_ipc
profile
Rustacean🦀

0개의 댓글