フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

ISOに従っていないデータをpandas.to_datetime()すると500倍以上遅くなる可能性がある話とその対策

TL;DR

  • pandasの to_datetime メソッドはとても便利で、かなり乱暴にデータを突っ込んでもParseしてくれます
  • でもデータによってはparseに通常の30倍以上時間がかかる可能性があるので注意しましょう
  • ISO_8601の規格に従っていない場合はとりあえず format オプションをつけておくのが得策です。
  • コードはすべてGithubにあがってます

github.com

検証するデータ

こちらのKaggleのデータを使いたいと思います。なぜこのデータかというと実際に痛い目にあったからです。笑

Final project: predict future sales | Kaggle

df = pd.read_csv(Path.home()/'.kaggle/competitions/competitive-data-science-final-project/sales_train.csv.gz', compression='gzip')
print(df.shape) => (2935849, 6)

2935849件のデータがあります。

datetimeのparse

date カラムには下のような形式で日付のデータが入っています。

df['date'].head()
0    02.01.2013
1    03.01.2013
2    05.01.2013
3    06.01.2013
4    15.01.2013

このデータをdatetime型にparseしたい時には to_datetime を使います。

print(df['date'].dtype) # => object
print(pd.to_datetime(df['date']).dtype) # => datetime64[ns]

本当に何も考えなくても簡単にParseできましたね。

to_datetime はISO_8601に従っていない場合に急激に遅くなるケースがある

しかし、この便利な to_datetime には落とし穴があります。データがISO_8601に従っていないとき急激に速度が遅くなることです。ISO_8601とは日付と時刻を表す標準規格です。

参照: ISO 8601 - Wikipedia

今回使ったデータはISO_8601に従っていません。ちょっと時間を計測してみましょう。計測には、timeit decoraterを少しいじったものを使っています。

@timeit(repeat=3, number=10)
def convert_to_datetime(s):
    return pd.to_datetime()

ちょっと怖いので10000件だけで計測してみましょう。

df10000 = df.head(10000)
best_time, parsed_datetime = convert_to_datetime(df10000['date'])
# Best of 3 trials with 10 function calls per trial:
# Function `convert_to_datetime` ran in average of 0.883 seconds.

0.9秒弱かかっています。データは293万件以上あるので(もしO(n)だと仮定しても)Parseだけで263秒もかかってしまうことになります。最初これで全然処理が終わらなく「おかしいな…」と首を傾げていました。笑

解決策: format オプション

それではどうすればよいのかというと format オプションを使います。

@timeit(repeat=3, number=10)
def convert_to_datetime_with_format(s, format_='%d.%m.%Y'):
    return pd.to_datetime(s, format=format_)

format付きバージョンで時間を計測してみましょう。

best_time, parsed_datetime = convert_to_datetime_with_format(df10000['date'])
# Best of 3 trials with 10 function calls per trial:
# Function `convert_to_datetime_with_format` ran in average of 0.024 seconds.

0.02秒!圧倒的ではないか我軍は…!もしO(n)だと仮定すれば、7秒程度でParseが終わります。 このようにformatオプションをつけるだけでParse速度が変わることがわかりました。

念の為、計算量の増加具合(オーダー)も調べてみる

先程から「O(n)だと仮定すれば」と書いてきましたが、本当にそうなのか、調べてみましょう。

n_targets = [100, 1000, 10000, 100000, 1000000]
normal_best_times = []
format_best_times = []

for n in n_targets:
    target_df = df.head(n)

    best_time, parsed_datetime = convert_to_datetime_with_format(target_df['date'])
    format_best_times.append(best_time)

    best_time, parsed_datetime = convert_to_datetime(target_df['date'])
    normal_best_times.append(best_time)

plt.figure(figsize=(16, 8))
ax = plt.subplot()
ax.plot(n_targets, normal_best_times, color='C0', linewidth=5, label='normal')
ax.plot(n_targets, format_best_times, color='C1', linewidth=5, label='with_format')
plt.legend(fontsize=20)

f:id:mergyi:20181003030313p:plain きれいに線形(O(n))であることがわかりました。 それにしてもすごい差ですね。

実際に平均で何倍なのか見てみましょう。

ratio = (np.array(normal_best_times) / np.array(format_best_times)).mean()
print(f'with_format parse take only {ratio:.2f} times faster than normal parse time')
# with_format parse take only 34.00 times faster than normal parse time

ということで34倍早くなることがわかりました。

ちなみに

じゃあISO_8601に近い形式の場合はどのくらい計算時間が変わるのだろう?と疑問に思ったので調べてみました。 2018-10-03 という ISOに則った形式と 02.10.2018 というISOに則っていない形式でformatあり/なしでそれぞれ比べてみます。

target_s1 = pd.Series(['2018-10-03', '2018-10-02', '2018-10-01'] * 10000)
target_s2 = pd.Series(['03.10.2018', '', '01.10.2018'] * 10000)

best_time, parsed_datetime = convert_to_datetime(target_s1)
# Function `convert_to_datetime` ran in average of 0.005 seconds.

best_time, parsed_datetime = convert_to_datetime_with_format(target_s1, format_='%Y-%m-%d')
#  Function `convert_to_datetime_with_format` ran in average of 0.005 seconds.

best_time, parsed_datetime = convert_to_datetime(target_s2)
# Function `convert_to_datetime` ran in average of 2.884 seconds.

best_time, parsed_datetime = convert_to_datetime_with_format(target_s2, format_='%d.%m.%Y')
# Function `convert_to_datetime_with_format` ran in average of 0.072 seconds.

順番でいうと、 ISO = ISO with format > not ISO with format >> (越えられない壁) >> not ISO でした。ISOに従っていない形式だと ISOに従った形式の 576.8倍遅かった のでちょっと衝撃でした。formatオプションを使えば14.4倍にまで緩和できますので必ずformatオプションは使いましょう。

f:id:mergyi:20181003032859p:plain

まとめ

  • to_datetime メソッドはParseする日付データの形式によって34倍も遅くなることがわかりました。
  • それは format オプションを正しく指定することで高速化できます。
  • 遅いParseも早いParseも計算量はO(n)でした
  • 迷ったら to_datetime メソッドにはformat オプションをつけるようにしましょう。