TL;DR
- pandasの
to_datetime
メソッドはとても便利で、かなり乱暴にデータを突っ込んでもParseしてくれます - でもデータによってはparseに通常の30倍以上時間がかかる可能性があるので注意しましょう
- ISO_8601の規格に従っていない場合はとりあえず
format
オプションをつけておくのが得策です。 - コードはすべてGithubにあがってます
検証するデータ
こちらの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に従っていません。ちょっと時間を計測してみましょう。計測には、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)
きれいに線形(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オプションは使いましょう。
まとめ
to_datetime
メソッドはParseする日付データの形式によって34倍も遅くなることがわかりました。- それは format オプションを正しく指定することで高速化できます。
- 遅いParseも早いParseも計算量はO(n)でした
- 迷ったら
to_datetime
メソッドにはformat オプションをつけるようにしましょう。