データ集計コマンドを極めてシステム処理と業務速度を爆速化するお話
Index
データ集計コマンド
joinコマンドが便利過ぎて生きるのが辛い - Yuta.Kikuchiの日記
lookコマンドによる二分探索が速すぎて見えない - Yuta.Kikuchiの日記今日はデータ集計を行う上で絶対に覚えておいた方が良いコマンドと知識を紹介したいと思います。これを身につければシステム処理と業務効率化に大きく繋がると思います。この記事で紹介するコマンドはlook、sort、cut、uniq、shuf、awk、sed、join、pasteで、設定知識としてLC_ALL=Cについても軽く触れたいと思います。その他perl、pythonの一行野郎についても少し書きます。下で紹介する例はとにかく一行野郎に拘って書いていますので、見づらい事は予めご了承ください。
爆速で検索したいぜ!
lookを使う
検索するコマンドで誰もがgrepを使っていると思いますがドキュメント全てを検索対象としてしまうので処理速度が遅くなります。grep以外に検索するための良いコマンドとしてlookというものがあります。lookは予め検索対象のデータをsortしておく必要がありますが、2分探索が可能なので繰り返しシステムが大量データから検索する場合に有効だと思います。以下1000万行のデータに対してgrepとlookを使った時の検索時間の比較になります。使用するデータのFormatは英数字の32Byte文字列になります。
#!/usr/bin/env perl use warnings; use strict; use String::Random; open(FH, ">>data.txt"); for(my $i=0; $i<10000000; $i++ ) { my $rand_str = String::Random->new->randregex('[A-Za-z0-9]{32}'); print FH $rand_str . "\n"; } close(FH);$ head -n 5 data.txt eEiWsUhytV79rhOl50JEM7Dm1DFvGwfB enOU4fPD74pNsnfzWKzBMpbmo3XNylry Xa1fSlpIJ5ljG3vJ4g6z6zFr5dqbQiey Yb27y5H1Uw5QbymfKyqmRejC7kcLcF6H 0KkDXpMXmNJATBbIxUArt4mVevMUSwRD $ awk "NR==5000000" data.txt oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2 $ grep "oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2" data.txt 0.05s user 1.40s system 9% cpu 14.964 total $ sort data > sort_data.txt 28.81s user 9.37s system 40% cpu 1:35.24 total $ look "oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2" sort_data.txt 0.00s user 0.00s system 41% cpu 0.012 total
検索方法 検索時間 grep 14.964 look 0.012 grepに比べてlookの方が圧倒的に速い結果になりました。皆さん、検索の高速化を行いたい場合は一度ファイルをsortした上でlookコマンドを使うようにしましょう!
LC_ALL=Cを設定する
bash - Implications of LC_ALL=C to speedup grep - Stack Overflow
LC_ALLを設定しないと環境設定毎にsortの結果に違いが出るというのは有名な話ですが、LC_ALL=Cを設定するとgrepが速くなるらしいので試してみます。LC_*はLocaleに関する環境変数です。上で実行した例に対してLC_ALL=Cを加えてみます。$ LC_ALL=C grep "oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2" data.txt 0.06s user 0.79s system 7% cpu 10.747 total $ LC_ALL=C grep "oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2" data.txt 0.06s user 0.91s system 8% cpu 11.172 total $ LC_ALL=C grep "oAQ7d7MpN7WV69U5CMeY4EeWqpJ8hXh2" data.txt 0.05s user 0.87s system 9% cpu 9.916 total
検索方法 検索時間 grep 14.964 LC_ALL=C grep 10.747 この結果をみるとgrepはLC_ALL=Cを付けた方が1.4倍処理が速くなっている事が分かりました。
同様にsortコマンドでも影響があるかどうかを確認してみます。$ LC_ALL=C sort data.txt > sort_data.txt 11.59s user 6.43s system 24% cpu 1:14.03 total $ LC_ALL=C sort data.txt > sort_data.txt 11.76s user 5.17s system 23% cpu 1:10.59 total $ LC_ALL=C sort data.txt > sort_data.txt 12.48s user 6.36s system 22% cpu 1:23.57 total
sort方法 sort時間 sort 1:35.24 LC_ALL=C sort 1:14.03 sortの場合はLC_ALL=Cを付けた方が1.28倍処理が速くなっている事が分かりました。なので皆さん、LC_ALL=Cの設定は忘れないようにしましょう!
データのランダムサンプリングがしたいぜ!
shufを使うと吉
大容量のデータを全て集計対象とするのではなく、必要なデータをランダムでサンプリングする方法を紹介します。ネット上に沢山やり方が紹介されたりしていますが、結論から言っておくと一番簡単なのはcoreutilsに含まれるshufというコマンドを利用することです。sed、awk、sortを使うのは注意が必要です。ランダムサンプリングの方法ですが2種類やり方があります。
サンプリング方法 特徴 コマンド 乱数行取得 乱数生成は処理が速い。
乱数行取得は処理が重い。
乱数の重複制御を入れる必要がある。sed、awk 全体をシャッフルする データ全体を操作するので処理が重くなる。
それでもsed、awkよりは速い。
重複の制御が必要無い。shuf、sort、script言語 以下の実験では上と同様に1000万行のデータを使用します。
sedを使う
必要な行数の乱数を発生させsedで取得します。sedは置換だけでなく行数指定で対象データを取得する事ができます。
$ LC_ALL=C; file=data.txt; lines=`wc -l < $file`; for i in {1..100}; do sed -n $((1+$RANDOM%$lines))p $file; done 63.83s user 76.11s system 24% cpu 9:35.45 total上の例には2つ問題があります。乱数の発生に$RANDOMと変数を利用していますが最大値32767までしか取得できないところに注意が必要です。man bashというコマンドを打ってみると以下の事が記載されています。$RANDOMを使用する場合はデータの大きさに合わせて$RANDOM * 1000などの処理を入れてください。
RANDOM Each time this parameter is referenced, a random integer between 0 and 32767 is generated. The sequence of random numbers may be initialized by assigning a value to RANDOM. If RANDOM is unset, it loses its special properties, even if it is subsequently reset.
2つ目の問題は同じ乱数が発生されても制御していない点です。これでは結果に同じ行数のデータが含まれる可能性があるので、厳密にデータをサンプリングしたい場合は重複制御をする必要があります。
awkを使う
awkのsrand()、rand()を使って乱数を生成する方法を紹介します。ただしこの方法も上のsedと同様で乱数の重複制御がされていないものです。
$ LC_ALL=C; num=100; file=data.txt; lines=`wc -l < $file`; list=`awk -v lines=$lines -v num=$num "BEGIN{ srand(); for(i=1;i<=num;i++) print int(rand()*lines+1) }"`; for i in `echo $list | xargs echo`; do awk "NR==$i" $file ;done 146.86s user 238.94s system 21% cpu 29:16.09 totalawkを使って乱数の重複を除外したのが以下の例になります。
$ LC_ALL=C; file=data.txt; awk 'BEGIN{ srand() } { lines[++d]=$0 } END{ while (1){ if (e==d) {break} RANDOM = int(1 + rand() * d); if ( RANDOM in lines ){ print lines[RANDOM]; delete lines[RANDOM]; ++e; } } }' $file | head -n 100 9.13s user 59.22s system 1% cpu 1:12:44.20 totalawkの問題としてランダムサンプリングのロジックが複雑になってしまうので自分で最初から書くのが大変です。また処理もsedと比較すると重たくなります。
sortの--random-sortを使う
sortのオプションである--random-sortを使用します。行のデータをハッシュ関数に掛けてハッシュ値毎にランダムにsortします。記述が簡単なので導入し易いと思います。
$ LC_ALL=C sort --random-sort data.txt | head -n 100 90.36s user 2.79s system 71% cpu 2:10.29 totalこのランダムsortも注意が必要です。1つ目は全行のハッシュ値の計算に時間がかかってしまいます。2つ目ハッシュ値によるランダムsortになるので同じハッシュ値の行はかならず連続出力されます。3つ目は実装されているsortのバージョンに寄っては--random-sortが使えない可能性があります。
Script言語を使う
PerlとPythonの例を書いておきます。Pythonの例はデータ量が多いと実行にとてつもない時間がかかってしまうので利用はお勧めしません。しかし男ならScript言語には頼らず出来ればコマンドおよびシェルで実現したいものです。
$ perl -MList::Util=shuffle -e 'print shuffle(<>)' < data.txt | head -n 100 3.78s user 7.88s system 21% cpu 53.833 total$ python -c 'import random;import sys;d=open("data.txt","r").readlines();random.shuffle(d);[sys.stdout.write(str(i)) for i in d ]' | head -n 100shufを使う
shufが一番簡単かつ高速です。よって皆さん、ランダム行出力を行いたい場合はshufを使うようにしましょう!
$ shuf -n 100 data.txt 0.25s user 1.02s system 25% cpu 4.999 total
合計と平均値を集計したいぜ!
列データ取得
数値の集計にはawkを利用するのがいいです。上と使用するデータを変更して以前お遊びで抽出した以下のデータを利用します。Data/ero_prediction.tsv at master · yutakikuchi/Data
まずは集計したいデータの列を取得したいのでcutを利用します。cutの-fオプションで列数を指定すると取得できます。
$ head -n 5 ero_prediction.tsv Name:愛内希 Bust:80 Waist:57 Hip:83 Bra:C Name:愛内萌 Bust:86 Waist:56 Hip:82 Bra:E Name:相川とも子 Bust:83 Waist:56 Hip:83 Bra:D Name:愛川ひな Bust:83 Waist:57 Hip:85 Bra:C Name:藍川めぐみ Bust:95 Waist:60 Hip:88 Bra:G $ cut -f 2 ero_prediction.tsv Bust:80 Bust:86 Bust:83重複行のカウント
次にsortとuniqを使ってデータの重複をカウントします。uniqの利用制限としてlookと同様に事前にsortする必要があるためにsortを使います。また重複のカウントはuniqの-cオプションでできます。出力された結果は1列目が重複個数、2列名がデータになります。出力結果を更に重複個数で降順にsortします。
$ cut -f 2 ero_prediction.tsv | sort | uniq -c | sort -k 1,1 -r 126 Bust:83 76 Bust:82 68 Bust:85列の順番変更
通常はデータ=>個数の順番になっているのが普通だと思うので、上の結果の列を入れ替えます。列の入れ替えはawkを利用します。またExcel等に張りつけがし易いように列間のdelimiterにtabを設定します。
$ cut -f 2 ero_prediction.tsv | sort | uniq -c | sort -k 1,1 -r | awk '{print $2 "\t" $1}' Bust:83 126 Bust:82 76 Bust:85 68合計値出力
列データの合計値を取得します。合計値の計算はawkの条件文とENDを使います。この場合BEGINは省略可能です。
$ cut -f 2 ero_prediction.tsv | sed "s/Bust://g" | awk '{total += $1} END {print total}' 51553 $ cut -f 2 ero_prediction.tsv | sed "s/Bust://g" | awk 'BEGIN{ total =0} {total += $1} END {print total}' 51553平均値出力
合計値が求められたら平均値は行数で割るだけなので上の命令文に行数で割る処理を加えてあげるだけです。awkのNRという定数に行数が記録される仕様になっています。
$ cut -f 2 ero_prediction.tsv | sed "s/Bust://g" | awk '{total += $1} END {print total/NR}' 84.5131
複数ファイルのデータ結合がしたいぜ!
共通項での結合
異なるデータを持つ複数のファイルを共通のKeyで連結するにはjoinを利用します。共通のKeyを持たない行に関しては連結の対象外になります。
$ cat data_1.txt data_2.txt A 100円 B 300円 C 200円 D 400円 E 500円 A Category1 B Category2 C Category3 D Category4 $ join data_1.txt data_2.txt A 100円 Category1 B 300円 Category2 C 200円 Category3 D 400円 Category4同じ行数での結合
同じ行数でデータを結合したい場合はpasteを利用します。joinと異なり共通のkeyが無くても同一行数だけで結合します。
$ paste data_1.txt data_2.txt A 100円 A Category1 B 300円 B Category2 C 200円 C Category3 D 400円 D Category4 E 500円