반응형

Pandas 란?

  • Pandas는 파이썬에서 데이터 처리와 분석을 위한 라이브러리로, numpy를 기반으로 개발되었다.
  • Pandas는 DataFrame과 Series라는 데이터 구조를 사용하여, 데이터를 쉽게 처리할 수 있도록 한다.
  • Pandas는 C 언어 등, low level의 언어로 개발된 numpy를 기반으로 하고, 수년간의 버전 업그레이드로 그 자체에 최적화를 해놓았기 때문에, 일반적으로 Pandas에서 제공하는 함수들을 사용하는 것이 성능 면에서 가장 좋다.
  • 하지만, 데이터 크기와 연산의 복잡성에 따라, 특정한 상황에서는 Pandas의 성능을 최적화하기 위한 방법이 필요하다.

 

Pandas의 데이터 처리 최적화 원리

  • Pandas는 기본적으로 low level 언어로 최적화가 많이 되었기 때문에, Pandas 데이터 처리를 위한 연산 과정에 Python 언어로 처리하는 과정이 생략되는 것이 좋다. 
  • Pandas는 메모리 위에서 동작한다. 이에 따라, 메모리의 가용 용량을 벗어나는 데이터 처리 및 연산은 한 번에 처리할 수 없다. 따라서, 메모리를 효율적으로 사용할 수 있도록 변경하는 것이 좋다.

 

Pandas 데이터 로드

  • 사실, Pandas를 많이 활용하는 이유 중 하나는 Pandas의 Dataframe이  SQL 테이블 형태와 거의 유사하기 때문이다.
  • Pandas에 들어갈 데이터를 Code 내부에서 주입하는 경우도 있지만, 대부분의 경우, Database나, CSV File 등에서 Import 해오는 경우가 많다.
  •  Pandas는 앞서 말한대로, 메모리에 DataFrame을 올려놓고, 연산하는 형태이기 때문에, 메모리가 연산을 효율적으로 처리할 수 있도록, 작은 단위의 필요한 데이터만 사용하는 것이 연산 측면에서 유리하다.

 

1. Query 및 파일 최적화

  • Pandas에서 SQL이나 CSV 등의 Raw 형태의 데이터를 읽고, 이를 Filtering하여Filtering 하여 사용하는 경우가 많은데, 이는 Raw 데이터 전체를 메모리 올려, 메모리 & I/O 부담을 증가시킨다. 따라서, 필요한 데이터만 미리 Filtering 하여 가져오는 것이 좋다.
  • 이렇게 Pandas에서 필요한 데이터만 가져오면, 전체 Series의 갯수(Row 수)가 줄기 때문에, Index 활용 측면에서도 유리하다.
  • 예시로, 한국어 형태소 분석을 위해 SQL 테이블에서 1주일치 데이터를 읽어서, 처리하였는데, 하루씩 읽어서 7번 처리하는 게 속도 면에서 더 효율적이었다.  
  • 마찬가지로, 필요한 칼럼만 가져오는 것이 성능 면에서 유리하다.
df = pd.read_csv('raw_data.csv', usecols=['col1', 'col2', ...])

 

2. Data Type 미리 지정

  • Pandas는 자료 구조형 선언의 제약을 받지 않는 파이썬 위에서 돌아가기 때문에, 읽어온 데이터를 통해 Data Type을 추론하는 과정이 포함된다. 
  • Data Type을 사용자가 미리 지정하면, 1) Data Type 추론 과정을 줄일 수 있고, 2) 실제 필요한 데이터 크기에 맞는 정도만 메모리를 할당하기 때문에, 성능 면에서 유리하다.
  • 다만, Data Type을 미리 지정하는 것은, 추후 연산 시에 메모리를 효율적으로 사용할 수 있다는 장점이 있지만, 읽는 속도 자체에는 큰 영향을 미치지 않는다.
dtypes = {'col1': 'int', 'col2': 'float', ...}
df = pd.read_csv('raw_data.csv', dtype=dtypes)

 

3. chunk 옵션 사용

  • 위의 방법을 사용해도, 어쩔수 없이 대용량 데이터를 모두 사용해야 하는 경우가 많다.
  • 이와 같은 경우에는 DataFrame을 chunk 단위로 처리하는 것이 효율적이다.
  • chuncksize를 지정해줘서, 한 번에 읽을(메모리에 올릴) Series(row) 수를 지정할 수 있다.
  • 하지만, 전체 데이터가 같이 필요한 것들(특정 칼럼 sort 등)은 처리가 까다롭기 때문에, 다른 행들 간의 연산이 비교적 적은 경우에 활용하는 것이 좋다.
for chunk in pd.read_csv('raw_data.csv', chunksize=10000):
    processing(chunk)

 

4. Dask 사용

  • 만약, 메모리가 감당하기 어려운 정도의 어려운 정도의 데이터 양과 연산이 포함된다면, 대용량 데이터를 분산 처리하기 위해 개발된, Dask를 사용할 수 있다.
  • Dask는 Pandas와 달리, Disk에 저장된 데이터를 처리하기 때문에, 여러 머신에서 분산처리가 가능하고, 지연 연산을 사용하기 때문에, 실제 연산을 최적화하는 과정이 포함된다.(SQL의 실행 Plan을 생각하면 된다.) 따라서, 초 대용량 데이터 처리에는 Dask의 강점이 있다.
  • 하지만, 메모리가 감당 가능한 수준의 연산에서는 메모리와 디스크의 속도 차이 등 때문에, Pandas가 유리하다.
import dask.dataframe as dd
if __name__ == "__main__":
	df = dd.read_csv('raw_data.csv')

 

 

Pandas 연산 & 조회

[실험 데이터셋]

  • Pandas 연산 테스트를 위해, 예시 데이터로 Kaggle 데이터셋을 사용했다. (https://www.kaggle.com/datasets/jordankrishnayah/45m-headlines-from-2007-2022-10-largest-sites?resource=download)
  • 사용할 데이터는, 4.5M 분량의 2007년부터 2022년 주요 언론사의 기사 제목 headline 데이터이다. 데이터는 총, 4405392개 row로 구성되어 있고, [Date, Publication, Headline, URL]의 4개의 칼럼으로 구성되어 있다.

 

1. 반복문 최적화

  • Pandas 연산에서 가장 큰 성능 개선 포인트는 반복문 연산이다.
  • Pandas가 Python 언어로 동작하기 때문에, Python의 list의 개념으로 Dataframe을 다루기 때문에, 이런 문제가 많이 발생한다.
  • Pandas를 For문을 통해, 각 row에 접근하는 경우에는, 각 row마다 연산을 각각 실행한다. 이에 따라, 데이터 크기가 커질수록 연산의 Overhead는 가중화된다.
  • 가장 쉬운 방법은 Pandas의 apply를 사용하는 것이다. 
  • 또한, Numpy Vectorize를 사용하는 방법, iterrows, itertuples를 사용하는 방법들이 있는데, 일반적으로 itertuples와 numpy vectorize는 Pandas Apply보다 좋은 성능을 보인다고 알려져 있다.
  • 추가적으로 멀티스레드를 이용하여, Pandas 연산을 병렬화 하고, 효율적으로 처리하는 swifter가 있다.

 

(테스트 상황)

  • headline 데이터셋에서 URL의 "http://" or "https://" 부분을 제거하는 작업을 테스트
  • headline 데이터셋에서 Date의 연도를 제거하는 작업을 테스트

 

(1) 단순 For문

for i in range(data.shape[0]):
    data["URL"][i] = data["URL"][i].replace('http://', '').replace('https://', '')

→ 실행 시간 : 94848.09 s(1000 row 실행시간: 21.53으로 추정)

 

for i in range(data.shape[0]):
    data["Date"][i] = data["Date"][i]%10000

→ 실행 시간 : 572.70s(1000 row 실행시간: 0.13으로 추정)

 

 

(2) Pandas Apply

data["URL"] = data["URL"].apply(lambda x: x.replace('http://', '').replace('https://', ''))

→ 실행 시간 : 1.47s

data["Date"] = data["Date"].apply(lambda x: x % 10000)

→ 실행 시간 : 0.88s

 

 

(3) Numpy Vectorize

  • numpy에서 제공하는 vectorize를 통해, 연산하고자 하는 함수를 vectorize화 할 수 있다.
  • numpy를 import 하여 쉽게 사용할 수 있다.
import numpy as np
vectorize_function = np.vectorize(lambda x: x.replace('http://', '').replace('https://', ''))
data["URL"] = vectorize_function(data["URL"])

→ 실행 시간 : 59.09s

import numpy as np
vectorize_function = np.vectorize(lambda x: x % 10000)
data["Date"] = vectorize_function(data["Date"])

→ 실행 시간 : 0.46s

 

(4) Vector화 된, For문 사용

  • Pandas는 numpy로 만들어졌기 때문에, numpy의 데이터를 조회하기 위한 iterator의 순회문을 사용하면, 빠르게 데이터를 순회할 수 있다.

[iterrows]

temp_date = []
for i, row in data.iterrows():
	temp_url.append(row["URL"].replace('http://', '').replace('https://', ''))
data["URL"] = temp_url

→ 실행 시간 : 88.10s

temp_date = []
for i, row in data.iterrows():
	temp_date.append(row["Date"]%10000)
data["Date"] = temp_date

→ 실행 시간 : 86.14s

 

 

[itertuples]

temp_url = []
for row in data.itertuples(index=False):
    temp_url.append(row.URL.replace('http://', '').replace('https://', ''))
data["URL"] = temp_url

→ 실행 시간 : 3.37s

temp_date = []
for row in data.itertuples(index=False):
	temp_date.append(row.Date%10000)
data["Date"] = temp_date

→ 실행 시간 : 2.69s

 

 

(5) Swifter

  • Swifter는 pandas의 apply를 멀티스레드를 통해, 병렬화하여 빠르게 처리하도록 하는 파이썬 패키지이다.
  • pip install swifter를 통해, 설치 가능하다.
data["URL"] = data["URL"].swifter.apply(lambda x: x.replace('http://', '').replace('https://', ''))

→ 실행 시간 : 3.98s

data["Date"] = data["Date"].swifter.apply(lambda x: x % 10000)

→ 실행 시간 : 0.37s

 

[실험 결과] 

  • Python 영역의 연산을 활용하는 경우(Python 라이브러리 or String 사용 등)에는 apply나 itertuples를 사용한 순회가 가장 좋은 성능을 보인다. → 첫 번째 실험
  • Python 영역의 연산을 활용하지 않는 경우, 즉, Cython으로 변환이 가능한 연산등은 np.vectorize가 가장 좋은 성능을 보인다.
  • 데이터의 타입, 크기, 연산에 따라, 가장 적합한 연산은 다르겠지만,
    • 일반적으로 apply나 itertuples를 사용한 순회가 가장 좋다.(일반적으로 대용량에선 itertuples가 apply보다 낫다고 함.)
    • 연산이 간단한 경우(Cython으로 변환이 될만한 간단한 연산)에는 np.vectorize를 통해 최적화가 가능하다.
    • 단순 for문은 사용하지 않는 것이 좋다. 
    • Swifter는 데이터의 크기가 매우 크고, 연산이 복잡하지 않은 연산에서 효과적이다.

 

2. 특정 조건 데이터 조회

  • 특정 조건 데이터 조회는 Pandas에서 자주 사용된다. 
  • 연산에 비해, 긴 시간이 걸리지는 않지만, 데이터가 많고, 연산이 복잡해질수록 조건에 맞는 데이터를 찾는 시간이 오래 걸린다.

(테스트 상황)

  • headline 데이터셋에서 2022년부터 데이터 중, New York Times의 데이터를 찾으려고 한다.

 

(1) Boolean Type으로 indexing

  • 가장 일반적인 방법이다. 여러 개의 칼럼들의 조건의 boolean 형태로 각각 연산하여 구할 True인 값만 가져올 수 있다.
filtered_data = data[(data["Date"]>20220000) & (data["Publication"]=='New York Times')]

→ 실행 시간 :0.14s

 

 

(2) loc를 이용한 indexing

  • Boolean Type으로 indexing과 동일하다.
filtered_data = data.loc[(data["Date"]>20220000) & (data["Publication"]=='New York Times')]

→ 실행 시간 :0.14s

 

 

 

(3) query를 사용한 조회

  • Dataframe은 query를 지원한다. (하지만, like 등의 조건은 지원하지 않는다.)
  • 참조하는 칼럼이 많고, 데이터가 클수록, query를 내부적으로 최적화하는 단계가 있어 더 좋은 성능을 보인다.
filtered_data = data.query("Date >20220000 and Publication == 'New York Times'")

→ 실행 시간 :0.07s

 

 

 

(4) isin을 사용한 indexing

  • 큰 범위애서 보면, Boolean Type으로 indexing에 속하는데, 특정 문자열과 일치하는 조건을 찾을 때는, boolean type에 isin을 넣어주면 더 빨리 찾을 수 있다.
filtered_data = data[(data["Publication"].isin(['New York Times'])) & (data["Date"] > 20220000)]

→ 실행 시간 :0.05s

 

 

(5) itertuples를 사용한 순회

  • 순회를 이용한 데이터 indexing은 별로 좋은 방법은 아니다.
  • 하지만, 연산과 조회를 같이하는 경우에는 한 번의 순회에 조회 조건을 넣어, 데이터를 찾는 것도 고려해 볼 수 있다.
find_index = []
for i, row in enumerate(data.itertuples(index=False), start=0):
    if row.Date > 20220000 and row.Publication == 'New York Times':
        find_index.append(i)

filtered_data = data.iloc[find_index]

→ 실행 시간 :2.01s

 

[실험 결과] 

  • 일반적으로 사용되는 boolean을 사용하는 것이 좋다고 알려져 있지만, 참조하는 칼럼이 많고, 데이터가 많을 경우에는 query를 사용하는 것이 효과적일 수 있다.
  • Boolean Type도 단순히 조건을 넣어서 indexing 하는 것보다, isin  같은 pandas 연산자를 함께 사용해서 데이터를 찾는 것이 효율적이다.

 

3. 문자열 포함 검색

  • SQL에서는 LIKE라는 특정 문자열을 포함했는지 여부를 찾는 방법이 있지만, Pandas에서는 LIKE를 지원하지 않는다. 
  • 생각보다, 특정 문자를 포함하는지 여부를 점검하는 경우가 많은데, 이런 경우 어느 방법이 효과적일까?

 

(테스트 상황)

  • headline 데이터셋에서 URL이 https 형식을 사용하는 row만 추출하려 한다.

(1) Pandas str contains를 사용한 검색

  • Pandas에서는 문자열 존재 여부를 체크해 주는 str.contains가 존재한다. 
filtered_data = data[data["URL"].str.contains("https://")]

→ 실행 시간 :1.18s

 

 

(2) apply를 통한, indexing (in)

  • Python에서 특정 문자가 포함되었는지 여부는 in을 통해 쉽게 파악할 수 있다.
  • pandas의 apply를 통한 indexing을 이용해 쉽게 문자열을 검색할 수 있다.
filtered_data = data[data["URL"].apply(lambda x: "https://" in x)]

→ 실행 시간 : 0.62s

 

 

(3) apply를 통한, indexing (startswith)

  • in과 마찬가지지만, in은 위치를 특정해주지는 못한다. 이럴 때는 startswith나 endwith 등을 사용하여 indexing 할 수 있다.
filtered_data = data[data["URL"].apply(lambda x: x.startswith("https://"))]

→ 실행 시간 : 0.69s

 

 

(4) apply를 통한, indexing (re)

  • in과 statrswith 등으로 문자열 검색이 가능하지만, 실제 검색하고자 하는 데이터들은 특정 형태를 가지고 있는 경우가 많다. 
  • 이런 경우, 보통 for문을 통한 순회를 가장 먼저 생각하는데, 위에서 보인대로, 단순 for문은 너무 많은 시간이 걸린다.
  • 이런 경우, apply에 re를 사용하여, 정규 표현식 형태로 문자를 검색할 수 있다.
import re
filtered_data = data[data["URL"].apply(lambda x: True if re.search("https://", x) else False)]

→ 실행 시간 : 2.38s

 

 

[실험 결과] 

  • str contains보다, pandas apply를 통해, boolean 형태의 output을 내는 함수를 정의하고, 이것을 indexing 하는 것이 더 빠르다.
  • 가장 중요한 것은 단순 반복문 형태의 순회는 최대한 지양하는 것이 성능 측면에서 도움이 된다.

 

 

+ Recent posts