이전 포스트에 이어 numpy와 관련된 더욱 빠른 연산을 위해 사용할 수 있는 도구들과 이들에 대한 비교를 해보자.
numexpr
numpy는 A * B + C와 같은 연산을 하면 먼저 A * B의 결과를 임시 벡터에 저장한 다음 C를 더한다.
하지만 numexpr 모듈은 계산에 사용한 전체 벡터를 취합해서 캐시 미스와 임시 벡터 사용을 최소화하는 아주 효율적인 코드로 컴파일한다.
또한 멀티 코어를 사용하면 이를 활용할 수 있도록 최적화하고 연산을 병렬화해주는 OpenMP도 지원한다.
numexpr은 변수를 사용한 식을 단순히 문자열로 표현하기만 하면 된다.
from numexpr import evaluate
def evolve(grid, dt, next_grid, D=1):
laplacian(grid, next_grid)
evaluate("next_grid * D *dt + grid", out=next_grid)
위 코드에서는 out 인자를 사용해 numexpr이 결과를 반환하기 위해 새로운 벡터를 할당하지 않도록 했다.
Pandas
Pandas는 과학기술 파이썬 생태계에서 사실상 표준인 데이터 조작 도구로 표 형태의 데이터 처리에 사용한다.
(엑셀과 비슷하게) 균일하지 않은 데이터 유형으로 이뤄진 표(DataFrame)를 쉽게 조작하게 해주고, 시계열 연산을 강력히 지원한다.
Pandas의 내부 모델을 살펴보고 데이터를 빠르고 효율적으로 처리할 수 있는 여러가지 방법들에 대해 살펴보자.
Pandas 내부 모델
pandas는 엑셀 시트와 비슷한 2차원 자료구조를 사용한다.
DataFrame에 대한 연산은 한 열에 속한 모든 셀에 적용된다. 이런 열에 대한 연산은 종종 임시 중간 배열을 생성해서 RAM을 소모한다.
연산은 단일 스레드로 실행되며 GIL 때문에 제한될 수 있다.
하지만 파이썬이 개선되면서 병렬연산을 할 수 있게 되었다.
내부적으로 BlockManager가 같은 dtype으로 이뤄진 열을 하나로 묶어서 같은 타입으로 이뤄진 열에 대한 행 단위 연산을 더 빠르게 수행하게 해준다.
또한, pandas는 내부적으로 numpy의 dtype과 pandas의 dtype을 섞어서 사용한다.
numpy의 dtype으로는 int8, int64, float16, float64, bool등이 있고, pandas의 dtype은 categorical과 datetimetz가 있다.
또한 numpy의 dtype을 사용해서 생긴 부작용이 있다.
numpy의 int나 bool 객체는 NaN 값을 인식하지 못한다. (numpy의 float는 인식한다.)
그래서 pandas에서 numpy의 int나 bool이 들어있는 열에 NaN값이 들어가면 float타입으로 바뀐다.
int가 float로 변환되면 수의 정밀도가 줄어들고, bool이 float로 변환되면 데이터 크기가 최소 2배이상 커진다.
그래서 pandas의 확장 타입으로 Int64(대문자 'I' 사용), Int32, Int8을 사용한다. 이들은 NaN값을 bool로 사용하고, 나머지 값들은 numpy의 int형을 사용한다.
또한 boolean이라는 타입으로 NaN을 인식하는 bool형 객체를 사용한다.
사용방법에 따른 속도의 차이
사이킷런과 numpy
사이킷런에서 LinearRegression 모델을 사용하는 것과 numpy의 직접적인 선형대수구현을 비교해보면 사이킷런이 더 오래걸림을 확인할 수 있다. 그 이유는 내부동작과정은 똑같지만, 사이킷런에서는 실수를 예방하기 위해 타입이 잘못되었거나 NaN이 들어있는지 검사를 하기 때문에 훨씬 느린 것이다.
하지만 이는 개발자의 생산성을 떨어뜨리는 디버깅을 줄여주므로 검사를 활성화하는 것이 일반적이다.
pandas 사용법들
DataFrame의 행 데이터를 연산하는 방법들에 대해 알아보자.
- iloc을 통해 naive하게 접근하는 방식은 원소를 역참조한 후 값을 저장해야 하므로 비용이 많이 든다.
- 좀 더 파이써닉한 방식인 df.iterrows()를 사용하는 방식은 iloc을 사용한 방식보다 빠르다.
- 위의 두 방식은 for문을 사용하므로 중간 참조가 필요하지만 df.apply를 사용한 방식은 중간참조가 필요없어 조금 더 빠르다. (하지만 새로운 Series는 생성된다.)
- df.apply에 인자로 raw=True를 주면 내부적으로 Series가 아닌 numpy 배열에 직접 접근해 연산하므로 가장 빠르다. (이 방식은 저수준 객체를 사용하므로 Numba나 사이썬을 이용해 컴파일할 수 있다.)
마지막으로 여기에 Dask나 Swifter를 사용해 pandas 연산을 여러 행의 그룹으로 나눠서 병렬적으로 실행하는 방법을 추가하면 엄청난 속도향상을 얻을 수 있다. (10장에서 다룬다.)
어떤 결과 DataFrame를 만들때는, 리스트를 만든 후 해당 리스트를 통해 Series나 DataFrame을 구성할 것을 권장한다.
만약 concat을 통해 뒤에 항목을 붙이는 방식으로 만든다면 임시 Series를 만들어야 하기 때문이다.
또한 apply를 사용한 방식은 Dask를 통해 연산을 병렬화할 수 있다는 점과 단위 테스트를 간결하게 작성할 수 있다는 점이 이점이다.
그 외의 팁들
- 카디널리티가 작은 문자열(예를 들어 'yes'/'no'나 'A', 'B', 'C')은 astype('category')로 변환해서 사용해라.
- int64, float64를 사용중인데 더 작은 범위로 사용해도 된다면 astype()을 사용해 더 작은 타입으로 변환해라.
- 처리할 데이터를 준비하는 과정에서 큰 DataFrame을 조작하면 to_pickle()로 이를 영속화해 사용할 수 있다.
- 모든 처리 코드에 단위 테스트를 추가해라.
- pandas보다 더 빠른 도구인 modin이라는 도구가 있다.
- GPU에 초점을 맞춘 cuDF
- Vaex는 pandas와 비슷한 인터페이스를 지원하고 지연 계산을 활용해 RAM의 용량을 넘는 아주 큰 데이터셋을 처리할 수 있다.
또한 문자열을 많이 처리하는 연산에 특화됐다.
'Python' 카테고리의 다른 글
[고성능 파이썬] 7.1 C언어로 컴파일하기 (0) | 2023.02.14 |
---|---|
[고성능 파이썬] 6.1 행렬과 벡터연산 (0) | 2023.02.10 |
[고성능 파이썬] 10.3 (추가) NSQ와 GCP를 이용한 원격 클러스터링 (0) | 2023.02.08 |
[고성능 파이썬] 10.2 클러스터와 작업 큐 - NSQ와 도커 (0) | 2023.02.07 |
[고성능 파이썬] 10.1 클러스터와 작업큐 (0) | 2023.02.06 |