[Polars 총정리 1부_기본] Polars로 데이터 분석을 더 빠르게!

NewNewDaddy·2024년 12월 24일
1

데이터 분석

목록 보기
4/6
post-thumbnail

1부 - Polars 기본 활용법

2부 - Polars 심화 활용법

🔹 0. INTRO

아래 실습에 사용한 데이터 및 코드는 저의 Github에서 확인 가능합니다.

  • Python으로 데이터 분석을 시작할 때, 많은 사람들이 가장 먼저 사용하는 라이브러리가 바로 Pandas일 겁니다. Pandas는 다양한 파일 형식(csv, json, parquet)을 데이터프레임 형태로 손쉽게 읽고 다룰 수 있으며, 방대한 메소드와 기능 덕분에 데이터를 원하는 거의 모든 형태로 처리할 수 있습니다. 데이터베이스와의 통합, SQL 활용, 시각화 등 데이터 분석에 필요한 중요한 기능들을 라이브러리 하나로 대부분 해결할 수 있는 Pandas는 데이터 분석의 기본 도구로 자리 잡았습니다. 하지만 데이터 크기가 커지고 처리 속도가 중요한 상황에서는 아래와 같은 이유들로 인해 한계점이 명확해지죠.
    1. 단일 스레드 기반 처리

    • pandas는 기본적으로 단일 스레드에서 동작하도록 설계되어 있어, 대용량 데이터를 병렬 처리하지 못해 성능이 제한됩니다.

    2. Numpy에 기반한 데이터 구조

    • pandasnumpy에 기반해 행 기반(row-oriented) 데이터 구조를 사용합니다. 이는 OLAP 작업과 같은 열 중심(column-oriented) 연산에서 효율이 떨어질 수 있습니다.

    3. Python을 통한 구현

    • pandas는 python으로 구현되어 한 번에 하나의 스레드만 실행되도록 하는 GIL(Global Interpreter Lock) 제약에 따라 성능에 제한이 발생합니다.

    4. Lazy Evaluation 미지원

    • polars, spark 등에서 지원하는 Lazy Evaluation 기능은 사용자의 명령을 최적화된 실행 계획으로 결합한 후 처리하기 때문에 불필요한 계산을 피하고 성능을 최적화하는데 중요한 역할을 합니다. 하지만 pandas는 명령 실행시 즉시 데이터를 메모리에 로드하고 처리합니다. 이는 작업이 최적화되지 않아 불필요한 계산과 메모리 낭비를 초래할 수 있습니다.
  • 이번 글에서 소개할 polarspandas의 다양한 API를 활용하면서도 속도나 효율성 등에서 훨씬 뛰어난 성능을 보여줍니다. Rust 언어로 개발되어 Lazy Evaluation, Multi Core Processing, 최적화된 메모리 사용 등의 특성들을 통해 대용량 데이터를 다루는데 있어 최적화된 도구로 주목받고 있습니다.

  • 실제로 약 800MB, 1144만 행을 가진 데이터를 읽는 속도를 비교해보았을 때, 아래와 같이 polarspandas에 비해 6배 이상 빠른 것을 확인할 수 있었습니다.

  • 이렇게 뛰어난 성능 덕분에 pandas, apache spark 등 다른 데이터 분석 툴들에 비해 github stars 수도 빠르게 늘고 있는 것을 알 수 있죠.

  • 1부에서는 polars의 설치부터 데이터 조회, 필터링, 집계, 정렬, JOIN 등 polars를 활용해 데이터 분석을 하기 위한 기본적인 내용들을 전반적으로 다루어보았습니다.

🔹 1. 설치

  • Polars 공식 문서
  • 아래 명령어를 통해 python 환경에 간단히 설치가 가능하다.

    pip install polars

import polars as pl

# 최대 100행까지 잘리지 않고 출력되도록 설정
cfg = pl.Config()
cfg.set_tbl_rows(100)

🔹 2. 데이터 읽기

▪ 2-1) 파일 데이터 읽기

df = pl.read_csv("../dataset/emp.csv")

df.head()

▪ 2-2) 스키마 확인

df.schema

---
Schema([('First Name', String),
        ('Gender', String),
        ('Start Date', String),
        ('Last Login Time', String),
        ('Salary', Int64),
        ('Bonus', Float64),
        ('Senior Management', Boolean),
        ('Team', String)])

▪ 2-3) 컬럼 확인

df.columns

---
['First Name', 'Gender', 'Start Date', 'Last Login Time', 'Salary', 'Bonus', 'Senior Management', 'Team']

▪ 2-4) 컬럼별 분석 정보 확인

df.describe()

🔹 3. 컬럼 다루기

▪ 3-1) 인덱스 컬럼 생성

df.with_row_index()

▪ 3-2) 특정 컬럼의 데이터 선택

  • 아래와 같이 두 가지 방법으로 하나의 컬럼 데이터만 추출해낼 수 있다.
# 1
df['Gender']

# 2
df.get_column('Gender')

▪ 3-3) 여러 컬럼 선택

# 1
df[['Gender', 'Salary']]

# 2
df.select('Gender', 'Salary')

▪ 3-4) 새로운 컬럼 생성

  • Apache Spark와 비슷하게 with_columns 메소드를 활용해 새로운 컬럼 생성이 가능하며, 다양한 방법으로 표현이 가능하다.
  • alias를 활용하거나, key-value 형태로 작성함으로써 새로운 컬럼을 생성할 수 있다.
# Bonus에 대한 금액 계산 컬럼(surplus) 추가
# 1
df.with_columns(
    (df["Salary"]*df['Bonus']/100).round(2).alias('surplus')
    )

# 2
df.with_columns(
    (pl.col("Salary")*pl.col('Bonus')/100).round(2).alias('surplus')
    )

# 3
df.with_columns(
    surplus = (pl.col("Salary")*pl.col('Bonus')/100).round(2)
    )

▪ 3-5) 컬럼 이름 변경

# 변경할 이름에 대한 dict 정보 생성
rename_dict = {
        'Bonus' : 'bonus',
        'Gender' : 'gender'
        }

df.rename(rename_dict).head()

▪ 3-6) 컬럼 삭제

# 단일 컬럼 삭제
df.drop('Bonus')

# 복수 컬럼 삭제
df.drop('Bonus', 'Salary')
df.drop(['Bonus', 'Salary'])

# 특정 타입의 컬럼 삭제
import polars.selectors as cs

df.drop(cs.string()) # string 타입 컬럼 drop
df.drop(cs.integer()) # INT 타입 컬럼 drop

🔹 4. Null 값 다루기

▪ 4-1) 컬럼별 null값 확인

# 각 컬럼에 대한 null값 count를 출력한다.
df.null_count()

▪ 4-2) null 데이터 채우기

df.fill_null(0) # null값을 0으로 채우기

df.fill_null(strategy='forward') # 'forward', 'backward', 'min', 'max', 'mean', 'zero', 'one'

▪ 4-3) null 데이터 없애기

# 전체 컬럼 대상
df.drop_nulls()

# 특정 컬럼 대상
df.drop_nulls(subset='Gender')

# 특정 타입 대상
import polars.selectors as cs

df.drop_nulls(subset=cs.string())

▪ 4-4) 기타 (drop_in_place)

## drop 대상 컬럼 저장 + drop 내용 바로 반영 (python의 pop과 비슷)

pop_df = df.drop_in_place('Gender') # 'Gender' 컬럼 데이터 저장

df # 'Gender' 컬럼이 drop된 df 저장

🔹 5. 데이터 타입 다루기

▪ 5-1) 데이터 타입 변경

# INT -> STRING 변경
df = df.with_columns(
    Salary = pl.col("Salary").cast(pl.String)
    )
    
# STRING -> INT 변경
df = df.with_columns(
    Salary = pl.col("Salary").cast(pl.Int32)
    )

▪ 5-2) 날짜 관련 타입

# Start Date를 datetime 형식으로 변환
df = df.with_columns(
    pl.col("Start Date").str.strptime(pl.Date, format="%m/%d/%Y").alias("Start Date")
    )

# Last Login Time을 시간 형식으로 변환
df = df.with_columns(
    pl.col("Last Login Time").str.strptime(pl.Time, format="%I:%M %p").alias("Last Login Time")
    )

▪ 5-3) 날짜 정보 추출

df.with_columns(
    year = pl.col("Start Date").dt.year(),
    month = pl.col("Start Date").dt.month(),
    day = pl.col("Start Date").dt.day()
    ).head()

# 이 외에도 weekday, week, epoch, hour, minute, second, month_start, month_end 등 시간 관련 다양한 메소드가 있다.

🔹 6. 데이터 필터링

▪ 6-1) 단일 필터

df.filter(pl.col('Gender').is_null())

df.filter(pl.col('Gender') == 'Female')

df.filter(pl.col('Bonus') >= 10)

df.filter(pl.col('Bonus').is_between(8, 12))

df.filter(pl.col('Team').is_in(['Finance', 'Sales']))

▪ 6-2) 다중 필터

and_condition = (pl.col('Gender') == 'Female') & (pl.col('Bonus') >= 10)

or_condition = (pl.col('Gender') == 'Female') | (pl.col('Bonus') >= 10)

df.filter(and_condition)

🔹 7. ORDER BY

▪ 7-1) 단일 컬럼 정렬

# Bonus 컬럼을 기준으로 내림차순 정렬
df.sort(
    by='Bonus', 
    descending=True
    )

▪ 7-2) 다중 컬럼 정렬

# Team 컬럼은 오름차순, Salary 컬럼은 내림차순 정렬
df.sort(
    by=['Team', 'Salary'], 
    descending=[False, True],
    nulls_last=True
    )

🔹 8. GROUP BY

▪ 8-1) group_by

df.group_by('Gender').agg(
    gender_cnt=pl.col('Gender').count()
    )

df.group_by('Gender').agg(
    pl.count('Gender').alias('gender_cnt')
    )
    
df.group_by('Team').agg(
    cnt = pl.count('Team'),
    max_sal = pl.max('Salary'),
    mean_sal = pl.mean('Salary'),
    min_sal = pl.min('Salary'),
    mean_bonus = pl.mean('Bonus'),
    ).sort('mean_sal', descending=True)

▪ 8-2) group_by_dynamic

  • datetime 컬럼을 지정된 간격(1일, 1시간 등)으로 그룹화하여 집계할 때 사용하는 메소드.
  • group_by_dynamic 실습은 battle.csv 데이터를 사용한다.
df = pl.read_csv("../dataset/battle.csv")

"""
group_by_dynamic(
    index_column         # 시간 기반 그룹화를 위한 타임스탬프 열을 지정
    every                # 그룹화 간격(예: '1d'는 1일 단위)
    period               # 그룹화 윈도우의 기간(기본값은 every와 동일)
    offset               # 그룹화 윈도우의 시작점을 조정하는 오프셋
    include_boundaries   # 그룹 경계(윈도우 시작 및 끝)을 결과에 포함할지 여부
    closed               # 윈도우 경계가 왼쪽, 오른쪽, 또는 양쪽에서 닫혔는지 지정
    group_by             # 추가로 그룹화할 컬럼 지정
)
"""
df.group_by_dynamic(
    index_column='battle_datetime',
    every="3h"
    ).agg(
        gb = pl.col('winner_id'),
        cnt = pl.col('id').count(),
        id_max = pl.col('winner_id').max(),
        id_min = pl.col('winner_id').min(),
        ).head()

🔹 9. JOIN

  • trainer_pokemon.csv, pokemon.csv 데이터를 사용한다.

▪ 9-1) join

tp = pl.read_csv("../dataset/trainer_pokemon.csv")
pk = pl.read_csv("../dataset/pokemon.csv")

tp.join(
    other=pk,
    left_on='pokemon_id',
    right_on='id',
    how='left' # 'inner', 'left', 'right', 'full', 'semi', 'anti', 'cross'
    )
"""
inner: 두 테이블 모두에서 값이 일치하는 행을 반환
left: 왼쪽 테이블의 모든 행과 오른쪽 테이블에서 일치하는 행을 반환
right: 오른쪽 테이블의 모든 행과 왼쪽 테이블에서 일치하는 행을 반환
full: 왼쪽 또는 오른쪽 테이블 중 하나라도 일치하는 경우 모든 행을 반환
cross: 두 테이블의 행 간 데카르트 곱을 반환
semi: 오른쪽 테이블에서 일치하는 값이 있는 왼쪽 테이블의 행만 반환
anti: 오른쪽 테이블에서 일치하지 않는 값만 있는 왼쪽 테이블의 행을 반환
"""

▪ 9-2) join_asof

  • 정렬된 데이터를 기준으로 근접한 값을 기반으로 테이블을 조인하는 메소드로 시계열 데이터 처리시 유용하다.
  • 아래 예시의 경우 공통 컬럼인 time을 기준으로 가까운 값들끼리 join이 된 것을 확인할 수 있다.
df1 = pl.DataFrame({
    "time": [10, 20, 30],
    "value": ["a", "b", "c"]
})

df2 = pl.DataFrame({
    "time": [13, 23, 33],
    "value_2": ["x", "y", "z"]
})

df1.join_asof(
    df2, 
    on="time", 
    strategy='nearest' # 'backward', 'forward', 'nearest'
    ).head()
    
---
┌──────┬───────┬─────────┐
│ time┆ value ┆ value_2│
│ ---------    │
│ i64 ┆ strstr    │
╞══════╪═══════╪═════════╡
│ 10  ┆ a     ┆ x      │
│ 20  ┆ b     ┆ y      │
│ 30  ┆ c     ┆ z      │
└──────┴───────┴─────────┘

🔹 10. 참고 자료

profile
데이터 엔지니어의 작업공간 / #PYTHON #CLOUD #SPARK #AWS #GCP #NCLOUD

0개의 댓글