PyConJP2022発表資料 「Pandas卒業?大規模データを様々なパッケージで高速処理してみる」
Pandas卒業?大規模データを様々なパッケージで高速処理してみる2022/10/14
View Slide
自己紹介藤根 成暢(Shigenobu Fujine)岩手県出身みずほリサーチ&テクノロジーズ株式会社(MHRT)に所属現在、先端技術研究部にてデータ分析・クラウドなどの技術検証等を担当過去の発表(PyConJP2021) scikit-learnの新機能を紹介します
アジェンダ✅自己紹介本トークの背景と狙いパッケージ紹介検証検証環境使用するデータ検証① 基本統計量検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
本トークの背景と狙いPandasは超便利!様々なデータの入出力、主要な加工・結合・集計処理をほぼサポートだけど、大きいデータにはスケールしにくいシングルスレッド ⇒ CPUコアが何十個あっても、基本1コアしか使用しないインメモリ処理 ⇒ データ規模が大きいと、ファイル読込すら失敗並列処理・分散処理をサポートするパッケージに手を出してみるも…パーティションやワーカー等、内部アーキテクチャを理解・意識した実装が必要APIがPandasと違うため、プログラムの移植が難しい or 出来ないそれでも処理時間やMemoryErrorが改善されない 😢
本トークの背景と狙い本日話すこと以下4つのパッケージで代表的なデータ処理を実装・計測した結果APIやパフォーマンスの違い、実装時のTipsを解説公式サンプルコードだけでは見えない、現実的な課題とその打開案を紹介本日話さないこと・検証しないことGPU系パッケージ(cuDF、RAPIDSなど)Sparkの各種パラメータ説明
アジェンダ✅自己紹介✅本トークの背景と狙いパッケージ紹介検証検証環境使用するデータ検証① 基本統計量検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
パッケージ紹介(Pandas)実用的なリアルデータを分析するためのパッケージIndex / カラムは、内部ではNumpyで実装多様なフォーマットや分析要件に対応した豊富で高レベルなAPI群read_* to_*CSV XLS PARQUETHTML<>HDF5 JSON{}GBQ SQL ...CSV XLS PARQUETHTML<>HDF5 JSON{}GBQ SQL ... https://pandas.pydata.org/docs/getting_started/intro_tutorials/02_read_write.html
パッケージ紹介(Dask)タスクグラフによる並列処理 + 遅延評価による最適化Pandas / Numpy / Scikit-learnを踏襲したAPIhttps://docs.dask.org/en/stable/10-minutes-to-dask.html
パッケージ紹介(Vaex)高パフォーマンス : 行規模のデータを秒単位で処理遅延評価・仮想カラムメモリ効率化(フィルタリングやカラム選択はゼロメモリ)https://vaex.io/109
パッケージ紹介(PySpark)クラスタコンピューティング用フレームワークSpark Coreをベースに、様々な言語やタスクに対応version3.2より、PySparkにPandasのAPIが統合https://www.oreilly.com/library/view/learning-spark-2nd/9781492050032/ch01.html1 import pyspark.pandas as ps2 df = pd.DataFrame(...)
(ご参考)GitHubスター数
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介検証検証環境使用するデータ検証① 基本統計量検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
検証環境ハードウェアGoogleCloud VM(N2Dインスタンス)、8vCPU/64GBOS : Debian 10.13Python3.7.12パッケージ1 pandas==1.3.52 dask==2022.2.03 vaex==4.12.04 pyspark==3.3.056 # other7 pyarrow8 numexpr9 bokeh10 graphviz11 tqdm
使用するデータセットTLC Trip Record DataNYCが公開するタクシー履歴データ乗降車した時刻、場所、人数運賃、支払方法、チップ額、空港税など選定理由オープンデータ、データ数が多い複数のデータ型がある数値、タイムスタンプ、文字列、カテゴリ値使用するデータ範囲2011/1~2022/6のYellow Taxi Trip Records月単位(例. yellow_tripdata_2022-06.parquet)計138ファイル、13.3億レコードhttps://unsplash.com/photos/x7DHDky2Jwc
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介検証✅検証環境✅使用するデータ検証① 基本統計量検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
検証① 基本統計量の算出対象のファイルパスPandasDaskVaexPySpark1 pattern = 'data/yellow_tripdata_*.parquet'1 taxi = pd.DataFrame()2 for path in Path().glob(pattern):3 taxi = pd.concat([taxi, pd.read_parquet(path)], ignore_index=True)4 stat = taxi.describe(include='all', datetime_is_numeric=True)1 stat = (dd.read_parquet(pattern)2 .describe(include='all', datetime_is_numeric=True)3 .compute())1 stat = vaex.open(pattern).describe()1 stat = ps.read_parquet(pattern).describe()
検証① 基本統計量の算出データが大きいほど、Vaexが高速かつ省メモリ
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介検証✅検証環境✅使用するデータ✅検証① 基本統計量検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
検証② 補間、フィルター、型変換欠値・外れ値の補正、正しい値や範囲のデータを抽出、適切なデータ型に変換元データには欠値や外れ値が多数あり
検証② 補間、フィルター、型変換(Pandas)numexprパッケージ + eval で、複数カラムを並列処理して高速化1 taxi = pd.DataFrame()2 for path in Path().glob('data/yellow_tripdata_*.parquet'):3 t = pd.read_parquet(path)4 5 # 欠値と外れ値を置換6 t.passenger_count = t.passenger_count.fillna(1).replace({0: 1})7 t.RatecodeID = t.RatecodeID.fillna(1)8 ...9 10 # フィルター用の一時カラムを作成11 t.eval("""12 _duration = (tpep_dropoff_datetime - tpep_pickup_datetime).dt.seconds13 _base_fare = 2.5 + (ceil(trip_distance / 0.2) + ceil(_duration / 60)) * 0.514 _base_total = fare_amount + extra + mta_tax + tip_amount + tolls_amount + improve15 ...16 """, inplace=True)17 18 # フィルター、型変換19 t = t.query(common.queries).astype(common.new_dtypes)20 21 t.drop(columns=[c for c in t.columns if c.startswith('_')], inplace=True)22 taxi = pd.concat([taxi, t], ignore_index=True)
検証② 補間、フィルター、型変換(Dask)ファイル名に含まれている年月を、パーティション毎に個別付与するのに苦労delayedを使用して、パーティション毎にファイル名情報を追加(GH#6575)主要なAPIはPandasとほぼ同じfillna、replace、astype などは、複数カラムを一括実行する方が高速1 taxi = []2 for path in Path().glob(pattern):3 year, month = map(int, path.stem.split('_')[-1].split('-'))4 _min_pickup = datetime(year, month, 1)5 _max_pickup = _min_pickup + timedelta(days=calendar.monthrange(year, month)[1], sec6 t = delayed(pd.read_parquet)(path)7 t = delayed(pd.DataFrame.assign)(t, _min_pickup=_min_pickup, _max_pickup=_max_picku8 taxi.append(t) 9 taxi = dd.from_delayed(taxi)1 taxi = taxi.fillna(dict(2 passenger_count=1, RatecodeID=1, store_and_fwd_flag='N',3 improvement_surcharge=0, congestion_surcharge=0, airport_fee=0))4 taxi = taxi.replace(dict(passenger_count={0: 1}, payment_type={0: 1}))
検証② 補間、フィルター、型変換(Dask)いざ実行すると、MemoryError!全てのworkerが同時にファイルを読み込むため、メモリ使用量が急増デフォルトでは、worker数=CPUコア数compute(num_workers=N)でworker数を減らし、メモリ使用量を制御worker数を1/2にすると、メモリ使用量は半減するが、処理時間は微増
検証② 補間、フィルター、型変換(Vaex)PandasのAPIとかなり異なる日付・時刻計算定数でカラム作成値の置換はfunc.wherenumpy.ceil()などの外部関数を使う場合、UDFを登録する必要あり除算は / ではなく numpy.divide()1 min_pickup = np.datetime64(f'{year}-{month:02}-01T00:00:00')1 taxi['_min_pickup'] = vaex.vconstant(min_pickup, len(taxi))1 taxi.payment_type = taxi.func.where(taxi.payment_type == 0, 1, taxi.payment_type)1 @vaex.register_function()2 def calc_base_fare(trip_distance, duration):3 return 2.5 + (np.ceil(np.divide(trip_distance, 0.2)) + 4 np.ceil(np.divide(duration, 60))) * 0.5
検証② 補間、フィルター、型変換(PySpark)時刻の加減計算がエラー!全部SQLで書き直したSQL文を書いてps.sql()で実行するだけ、ファイル名追加も標準機能にあり1 now = ps.DataFrame({'now': [np.datetime64('2022-10-14T13:50')]})2 now + np.timedelta64(30, 'm') # TypeError: Addition can not be applied to datetimes.1 query = """2 SELECT3 TO_DATE(CONCAT(REGEXP_EXTRACT(INPUT_FILE_NAME(), '20[1-2][0-9]-[0-1][0-9]', 0), '-04 ...5 _file_date + (INTERVAL 1 MONTH) - (INTERVAL 1 SECOND) as _max_pickup,6 ...7 TINYINT(VendorID) as VendorID,8 ...9 FROM10 parquet.`{pattern}`11 WHERE12 (VendorID in (1, 2))13 ..."""14 taxi = ps.sql(query.format(pattern=pattern)))
検証② 補間、フィルター、型変換処理時間はVaex、メモリ使用量はPySparkが優秀
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介検証✅検証環境✅使用するデータ✅検証① 基本統計量✅検証② 補間、フィルター、型変換検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
検証③ テーブル結合5つの定義テーブルを履歴テーブルと内部結合し、各IDを文字列に変換メモリ節約のため、各テーブルのデータ型はkey/valueともCategoricalDytpe型結合イメージ
検証③ テーブル結合(Pandas)mergeよりも joinの方が高速(今回のデータでは約3倍)全てのkeyをMultiIndexにして、あとは順番に joinするだけmergeと joinで出力結果が異なる!keyにNaNが含まれている場合、joinでは一致しないが、mergeでは一致CategoricalDtype型のカテゴリ値を数値に変換(NaN ⇒ -1)してから join1 taxi = taxi.reset_index().set_index(['VendorID', 'RatecodeID', 'PULocationID', 2 'DOLocationID', 'payment_type'])1 taxi = taxi.reset_index().set_index(target_columns)23 for lookup in [common.vendors, common.ratecodes, common.pulocations,4 common.dolocations, common.payment_types]:5 right = pd.DataFrame(lookup, index=pd.Index(lookup.index.codes,6 name=lookup.index.name))7 taxi = taxi.join(right, how='inner') 8 9 taxi = taxi.reset_index(drop=True).set_index('index')
検証③ テーブル結合(Dask)joinよりもmergeの方が高速Daskのset_indexでは、内部でsort等が実行されるためオーバーヘッド大定義テーブルは単一パーティションにした方が高速1 vendors = dd.from_pandas(common.vendors, npartitions=1)2 ratecodes = dd.from_pandas(common.ratecodes, npartitions=1)3 pulocations = dd.from_pandas(common.pulocations, npartitions=1)4 dolocations = dd.from_pandas(common.dolocations, npartitions=1)5 payment_types = dd.from_pandas(common.payment_types, npartitions=1) 67 taxi = (taxi.merge(vendors, left_on='VendorID', right_index=True, how='inner')8 .merge(ratecodes, left_on='RatecodeID', right_index=True, how='inner')9 .merge(pulocations, left_on='PULocationID', right_index=True, how='inner'10 .merge(dolocations, left_on='DOLocationID', right_index=True, how='inner'11 .merge(payment_types, left_on='payment_type', right_index=True, how='inne
検証③ テーブル結合(Vaex)Vaexではカラム同士の結合のみ(だけどメソッド名は join)keyにCategoricalDtype型は使えないため、floatに変換してから join1 vendors = vaex.from_pandas(common.vendors.astype({'VendorID': float}))2 ratecodes = vaex.from_pandas(common.ratecodes.astype({'RatecodeID': float}))3 pulocations = vaex.from_pandas(common.pulocations.astype({'PULocationID': float}))4 dolocations = vaex.from_pandas(common.dolocations.astype({'DOLocationID': float}))5 payment_types = vaex.from_pandas(common.payment_types.astype({'payment_type': float})67 taxi = (taxi.join(vendors, on='VendorID', how='inner')8 .join(ratecodes, on='RatecodeID', how='inner')9 .join(pulocations, on='PULocationID', how='inner')10 .join(dolocations, on='DOLocationID', how='inner')11 .join(payment_types, on='payment_type', how='inner'))
検証③ テーブル結合(PySpark)Indexにカテゴリ型(CategoricalIndex)を使用可能カラムはカテゴリ型に未対応(今後対応予定)のため、文字列に変換1 vendors = ps.DataFrame(common.vendors).astype(str)2 ratecodes = ps.DataFrame(common.ratecodes).astype(str)3 payment_types = ps.DataFrame(common.payment_types).astype(str)4 pulocations = ps.DataFrame(common.pulocations).astype(str)5 dolocations = ps.DataFrame(common.dolocations).astype(str)6 7 taxi = (taxi.merge(vendors, left_on='VendorID', right_index=True, how='inner')8 .merge(ratecodes, left_on='RatecodeID', right_index=True, how='inner')9 .merge(pulocations, left_on='PULocationID', right_index=True, how='inner'10 .merge(dolocations, left_on='DOLocationID', right_index=True, how='inner'11 .merge(payment_types, left_on='payment_type', right_index=True, how='inne12 )
検証③ テーブル結合処理時間、メモリ使用量ともにPySparkが優秀
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介検証✅検証環境✅使用するデータ✅検証① 基本統計量✅検証② 補間、フィルター、型変換✅検証③ テーブル結合検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
検証④ リソース使用量の監視(Pandas)memory_profilerやline_profilerなどの追加パッケージが必要メモリ使用量や処理時間を行単位で追跡可能途中でGCが発生すると正確に計測できないことも…1 Line # Mem usage Increment Occurrences Line Contents2 =============================================================3 4 117.6 MiB 117.6 MiB 1 @profile4 5 def func(filename):5 6 4597.1 MiB 4479.5 MiB 1 taxi = pd.read_parquet(filename)6 7 2164.8 MiB -2432.3 MiB 1 stat = taxi.describe()7 8 8 9 2062.1 MiB -102.7 MiB 1 del taxi 9
検証④ リソース使用量の監視(Dask)ProgressBar で進捗を出力、ResourceProfiler でCPU・メモリ使用量を可視化
検証④ リソース使用量の監視(Dask)Profiler でタスクの実行順序や経過時間などを出力タスクの長時間化、メモリ使用量急増などの原因調査に有効
検証④ リソース使用量の監視(Vaex)vaex.progress.tree で進捗やCPU使用量を出力widgetrich
検証④ リソース使用量の監視(PySpark)ステージ毎にタスク進捗を出力Web UIでタスク毎の処理内容、DAG、Planなどの詳細情報にアクセス可能
アジェンダ✅自己紹介✅本トークの背景と狙い✅パッケージ紹介✅検証✅検証環境✅使用するデータ✅検証① 基本統計量✅検証② 補間、フィルター、型変換✅検証③ テーブル結合✅検証④ リソース使用量の監視それでもPandasを使いたいあなたにまとめ
それでもPandasを使いたいあなたにevalで並列化数式に近い形で記述でき、可読性の面でも恩恵ありnumexprインストールを忘れずに!Pandas統合の並列処理パッケージを活用apply/mapの並列化に有効1 df = pd.DataFrame()2 df['x'] = np.arange(1e8)34 # Before5 df['x'] += 16 df['y'] = df['x'] * 27 df['z'] = df['x'] / df['y']89 # After10 df.eval("""11 x = x + 112 y = x * 213 z = x / y14 """, inplace=True)
まとめ様々なパッケージで大規模データの効率的な処理を比較検証Pandas : 小~中規模( )のデータ件数なら、Pandasだけでも十分有用Dask : Pandasと(ほぼ)同じAPIで大規模データを処理できるのが強みVaex : APIがPandasと異なるも、処理時間を最重視したい場合には有益PySpark : Python/SQLなど複数言語を使用可能、pyspark.pandasの今後に期待データや処理内容が変われば、各パッケージの優劣も変わる可能性あり手元のデータで試してみるユースケースに応じて複数のパッケージを使い分けられる状態が理想的~107
ご清聴ありがとうございました。
(免責事項)当資料は情報提供のみを目的として作成されたものであり、商品の勧誘を目的としたものではありません。本資料は、当社が信頼できると判断した各種データに基づき作成されておりますが、その正確性、確実性を保証するものではありません。また、本資料に記載された内容は予告なしに変更されることもあります。