opyを使ったCSV操作のベンチマーク
Sun Jul 4 13:43:37 JST 2021 (modified: Sun Jul 4 17:40:35 JST 2021)
views: 2723, keywords:opy,シェル芸 この記事は最終更新日が4年以上前のものです。
Pythonでワンライナーを書くためのラッパーコマンドopyについて、CSVの読み書き機能のベンチマークをしてみました。1億行、4.2GBのCSVファイルを使いました。
先に結論を書いておくと、grepやawkを駆使する場合にくらべて時間はかかりますが、そこそこ妥当な速度で処理ができました。データの中に改行があるCSVデータを処理するときには有効です。
使ったマシン
Intel Core i9-10885H(物理コア8個)を搭載したThinkPad P1です。DRAMの量は64GB。
準備
ベンチマーク用のデータをhttps://file.ueda.tech/DATA_COLLECTION/TESTDATA.gzから落とします。
$ wget https://file.ueda.tech/DATA_COLLECTION/TESTDATA.gz
$ ls -l
合計 1277320
-rw-rw-r-- 1 ueda ueda 1307970445 2月 14 2016 TESTDATA.gz
$ ls -lh
合計 1.3G
-rw-rw-r-- 1 ueda ueda 1.3G 2月 14 2016 TESTDATA.gz内容を確認してから、CSV形式にします。
### zcatで内容確認 ###
$ zcat TESTDATA.gz | head -n 3
2377 高知県 -9,987,759 2001年1月5日
2910 鹿児島県 5,689,492 1992年5月6日
8458 大分県 1,099,824 2010年2月22日
### 3列目をダブルクォートで囲んで、全体をカンマ区切りに直す ###
$ zcat TESTDATA.gz | awk '{print $1","$2",\""$3"\","$4}' | head -n 3
2377,高知県,"-9,987,759",2001年1月5日
2910,鹿児島県,"5,689,492",1992年5月6日
8458,大分県,"1,099,824",2010年2月22日
### 全データを処理 ###
$ time zcat TESTDATA.gz | awk '{print $1","$2",\""$3"\","$4}' > TESTDATA.csv
real 0m37.401s
user 0m56.342s
sys 0m4.499s
### 行数の確認(1億行)###
$ wc -l TESTDATA.csv
100000000 TESTDATA.csv
### サイズは4.2GB ###
$ ls -lh TESTDATA.csv
-rw-rw-r-- 1 ueda ueda 4.2G 7月 4 13:41 TESTDATA.csvテスト1: 特定のデータだけ抽出
3列目が富山県のデータだけを抽出してみましょう。
grep、awkの場合
grepだと4秒弱で抽出できます。ただし、列の構成を無視できるのなら、これで十分です。awkの場合は-F,で2列目を指定すると、30秒ちょいかかりました。
$ time grep 富山県 TESTDATA.csv > ans
real 0m3.841s
user 0m3.181s
sys 0m0.573s
$ head -n 3 ans
7163,富山県,"1,371,974",1994年5月26日
2528,富山県,"6,407,486",1992年10月1日
1320,富山県,"5,784,634",2009年3月7日
$ time awk -F, '$2=="富山県"' TESTDATA.csv > ans
real 0m30.454s
user 0m29.714s
sys 0m0.676s
opyでCSVファイルを一度に読み込む場合
3分30秒なので、awkの7倍くらいと、なかなか検討しています。
$ time cat TESTDATA.csv | opy -t csv '[T[k] for k in T.keys() if T[k][1] == "富山県"]' > ans
real 3m27.988s
user 3m15.827s
sys 0m20.873s
$ head -n 3 ans
['7163', '富山県', '1,371,974', '1994年5月26日']
['2528', '富山県', '6,407,486', '1992年10月1日']
['1320', '富山県', '5,784,634', '2009年3月7日']
### 空白区切りに戻す場合 ###
$ time cat TESTDATA.csv | opy -t csv '[" ".join(T[k]) for k in T.keys() if T[k][1] == "富山県"]' > ans
real 3m30.300s
user 3m14.338s
sys 0m18.589s
$ head -n 3 ans
7163 富山県 1,371,974 1994年5月26日
2528 富山県 6,407,486 1992年10月1日
1320 富山県 5,784,634 2009年3月7日ただし、全部CSVをDRAMに読み込むので、めちゃくちゃメモリ使います。(この例だと46.1GBですね・・・)
$ while sleep 1 ; do top -b -n 1 -p 246057 ; done
・・・
top - 13:58:37 up 3 days, 5:15, 1 user, load average: 1.78, 1.57, 1.23
(略)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
246057 ueda 20 0 46.1g 46.1g 6180 R 100.0 73.8 3:20.37 python3
・・・opyでCSVファイルを一行ずつ読み込む場合
次のように、-cを使うと1行ずつCSVとしてレコードを読んでくれる(さらに-CでCSVで出力してくれる)ので、DRAMもほぼ使わずワンライナーも短く書けるのですが、時間がかかります。
### 100万行で17秒なので、1億行なら1700秒(28分強)。 ###
$ time cat TESTDATA.csv | head -n 1000000 | opy -cC 'F2=="富山県"' > ans
real 0m17.129s
user 0m17.126s
sys 0m0.426s
$ head -n 3 ans
"7163","富山県","1,371,974","1994年5月26日"
"2528","富山県","6,407,486","1992年10月1日"
"1320","富山県","5,784,634","2009年3月7日"テスト2: 数字の前処理+大小比較
3列目の数字からカンマを取って、絶対値が100万未満のレコードを抽出してみます。awkでも2分近くかかります。
### カンマを消す作業は、他の列のデータがクォートされていないことを利用しており、若干ズルです。 ###
$ time cat TESTDATA.csv | awk -F'"' '{gsub(/,/,"",$2);print}' | awk -F, '$3<1000000 && $3 > -1000000' > ans
real 1m51.880s
user 2m43.002s
sys 0m8.259s opyだと2倍強時間がかかりますが、データ構造をちゃんと見た上での作業時間なので、なかなか優秀です。ただし、DRAMはバカ食いしてます。
$ time cat TESTDATA.csv | opy -t csv '[T[k] for k in T.keys() if -1000000 < int(T[k][2].replace(",","")) < 1000000 ]' > ans
real 3m58.216s
user 3m41.913s
sys 0m18.827s
ueda@uedap1:~/tmp2$ head -n3 ans
['1518', '和歌山県', '-988,312', '2008年12月7日']
['3669', '島根県', '-397,852', '2006年11月3日']
['8931', '山梨県', '-583,286', '2007年6月21日']テスト3: ソート
4列目の年月日でデータをソートしてみます。sortはファイルからデータを読むとコア数だけ並列処理してくれるので、一度中間ファイルに入れてからソートします。
$ time ( cat TESTDATA.csv | awk -F, '{a=$NF;gsub(/[年月日]/," ",a);print a,$0}' > tmp ; sort -k1,3n tmp | awk '{print $NF}' > ans )
real 7m36.403s
user 20m21.787s
sys 0m22.701s
$ head -n3 ans
0000,石川県,"6,774,912",1990年1月1日
0000,兵庫県,"-6,384,895",1990年1月1日
0001,東京都,"340,556",1990年1月1日 opyでやってみましょう・・・と挑戦したんですが、「そんなでかいデータ扱えるか💢」と叱られました。
$ time cat TESTDATA.csv | opy -t csv -m datetime '{a=[ [datetime.datetime.strptime(e[3], "%Y年%m月%d日"),e] for e in T.values()]};{a.sort(key=lambda x:x[0])};[*[e[1] for e in a]]' > ans
強制終了
real 9m14.406s
user 8m49.024s
sys 0m23.651s妥協して1000万行のソートの結果を示しておきます。1千万件でコア1個だけ使って1分半なら、まあまあ妥当でしょう。
$ time cat TESTDATA.csv | head -n 10000000 | opy -t csv -m datetime '[*sorted(T.values(), key=lambda x:datetime.datetime.strptime(x[3], "%Y年%m月%d日"))]' > ans
real 1m32.006s
user 1m29.025s
sys 0m3.676s
$ head -n 3 ans
['5390', '群馬県', '7,216,266', '1990年1月1日']
['9017', '山口県', '7,573,861', '1990年1月1日']
['0894', '栃木県', '6,389,064', '1990年1月1日']とりあえず以上です。
ノート
Tweet