반응형

Intro


안녕하세요, 오늘은 python의 built-in 모듈 중 하나인 timeit에 대해서 소개해드리겠습니다.

timeit은 우리가 작성한 코드의 실행 시간을 측정하는데 사용됩니다. 우리가 작성하고자 하는 로직을 구현하는 방법이 여러 가지가 있을 때, 더 빠른 방법을 선택하기 위한 테스트를 진행할 때 timeit을 사용합니다.

timeit을 사용하는 방법은 크게 두가지가 있습니다.

  1. timeit모듈 import해서, timeit.timeit() 사용하기
  2. 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


Python3.8 timeit docs
IPython timeit magic commands

반응형

+ Recent posts