Intro
안녕하세요, 오늘은 python의 built-in 모듈 중 하나인 timeit
에 대해서 소개해드리겠습니다.
timeit
은 우리가 작성한 코드의 실행 시간을 측정하는데 사용됩니다. 우리가 작성하고자 하는 로직을 구현하는 방법이 여러 가지가 있을 때, 더 빠른 방법을 선택하기 위한 테스트를 진행할 때 timeit
을 사용합니다.
timeit
을 사용하는 방법은 크게 두가지가 있습니다.
timeit
모듈 import해서,timeit.timeit()
사용하기- IPython magic command
%%timeit
사용하기
저는 jupyter notebook에서 두 번째 방법을 이용해서 수행 시간을 비교하는 테스트를 해보겠습니다.
Test1: list comprehension vs. map function
python의 기본 기능인 list comprehesion과 map의 속도를 비교해보겠습니다.
%%timeit
"-".join([str(n) for n in range(100000)])
>> 21.7 ms ± 1.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
"-".join(map(str, range(100000)))
>> 17.1 ms ± 653 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
map이 list comprehension보다 빠른 것을 확인할 수 있습니다. 여기서 한가지 차이 점은, list comprehension 실험 결과는 10 loops, map 실험 결과는 100 loops를 돌았다고 나옵니다. 이는 %%timeit
magic command를 사용해서 속도를 측정할 때, 정확한 측정을 위해 해당 코드를 여러 번 실행해서 걸리는 시간을 평균냅니다. 그리고 이 숫자는 연산의 복잡도에 따라서 자동으로 정해집니다. (간단한 연산은 더 많이, 복잡한 연산은 더 조금)
위 실험의 경우 map 실험이 복잡도가 더 낮은 연산으로 구분되어서, list comprehension실험보다 많은 loop가 돈 것을 확인할 수 있습니다.
만약에 loop수를 통일하고 싶다면, -n [NUM_LOOPS]
인자를 주면 됩니다. 아래처럼요
%%timeit -n 10
"-".join(map(str, range(100000)))
>> 17.7 ms ± 1.33 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Test2: (pandas) list comprehension vs. map function
pandas DataFrame에도, 특정 columns의 값에 동일한 함수를 적용시켜주는 map함수가 존재합니다. 하지만 듣기로는 pandas의 map함수는 잘 설계되어있지 않다고 들었는데요, 실제로 그런지 확인해보겠습니다.
아래와 같이 데이터를 준비하겠습니다.
import pandas as pd
import numpy as np
data_2d = np.random.rand(100000, 5)
df = pd.DataFrame(data_2d, columns=[f'data{idx}' for idx in range(len(data_2d[1]))])
df.head()
>> data0 data1 data2 data3 data4
0 0.275805 0.693519 0.644440 0.277043 0.173847
1 0.598473 0.796305 0.106011 0.824349 0.061107
2 0.664825 0.998174 0.575312 0.225689 0.274685
3 0.577050 0.705411 0.800339 0.623300 0.254203
4 0.828888 0.291775 0.347555 0.277151 0.27
그리고 값을 소수점 2자리 까지만 고려해주는 round_value
함수를 정의해준 뒤, list comprehension과 map의 속도를 비교해보겠습니다.
def round_value(x):
return np.round(x, 2)
%%timeit
df['data0'].map(round_value)
>> 989 ms ± 17.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
[round_value(value) for value in df['data0'].values]
>> 692 ms ± 38.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
소문이 사실이었습니다. pandas의 경우, map보다 list comprehension의 속도가 훨씬 빨랐습니다!
map을 사용했을 때 코드 가독성이 더 좋긴 하지만, 속도가 큰 차이가 나니 앞으로는 list comprehension을 사용하는 것이 좋겠네요.
Test3: (pandas) loc( ) vs. at( ) vs. iloc( ) vs. iat( )
위 4가지 함수들은 모두 DataFrame상에서 특정위치의 값을 가져오거나, 바꿀 때 사용할 수 있습니다.
앞에 i
가 붙은 함수들을 index를 통해서만 접근이 가능하고, 그렇지 않는 함수들은 column 이름 등을 통해서 접근이 가능합니다.
또한 (i)at
과 (i)loc
의 차이점은, 한 번에 하나의 값에 접근할 것인지, 여러 값에 접근할 것인지에 따라 차이가 있습니다. 즉, (i)at
가 하나의 값을 찾거나 수정하는데 더 특화되어 있다고 볼 수 있습니다.
바로 속도 비교 실험을 진행하겠습니다.
# at
%%timeit
[df.at[idx, 'data1'] for idx in df.index]
>> 401 ms ± 31.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# loc
%%timeit
[df.loc[idx, 'data1'] for idx in df.index]
>> 661 ms ± 13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# iat
%%timeit
[df.iat[idx, 1] for idx in df.index]
>> 1.9 s ± 22.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# iloc
%%timeit
[df.iloc[idx, 1] for idx in df.index]
>> 2.16 s ± 16.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
실험 결과, (i)loc
보다는 (i)at
이 빠르고, i
로 시작하는 함수보다는 그렇지 않은 함수가 더 빠르단 걸 알 수 있습니다. 신기하네요, 상황에 따라서 i
가 붙은 함수를 사용할 수 있겠지만, 가능하면 하나의 값만 가져올 때는 at
, 여러 개의 값을 가져올 때는 loc
를 사용하는 것이 좋겠네요.
마무리
오늘은 python의 timeit
모듈에 대해서 알아보고, 평소에 궁금했던 것들을 테스트해 봤습니다. 글 읽어주셔서 감사합니다!
References
'[Python]' 카테고리의 다른 글
[Python] 강건한 프로그래밍을 위한 type hint (feat. typing 모듈) (1) | 2021.07.25 |
---|---|
[Python] Mixin 이란? (0) | 2021.07.10 |
[Python] Decorator(4) - dataclass, functools (0) | 2021.07.04 |
[Python] Decorator(3) - @classmethod, @staticmethod (0) | 2021.06.28 |
[Python] Decorator (2) - @property (0) | 2021.06.20 |