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 하는 것이 더 빠르다.
- 가장 중요한 것은 단순 반복문 형태의 순회는 최대한 지양하는 것이 성능 측면에서 도움이 된다.
'Python' 카테고리의 다른 글
Pypi 사용법 & 명령어 모음 & 폐쇄망 사용법 (1) | 2023.08.16 |
---|---|
JAX: Just Another XLA 설명 (2) | 2023.08.07 |
Transformer Pytorch 구현 (11) | 2023.07.15 |
Pytorch Profiler Tensorboard로 시각화 (1) | 2023.07.10 |
Pytorch Resource & 모델 구조 Profiler 도구 (torch profiler) (1) | 2023.07.09 |