Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

データ集計コマンドを極めてシステム処理と業務速度を爆速化するお話

Index

  1. データ集計コマンド
  2. 爆速で検索したいぜ!
    1. lookを使う
    2. LC_ALL=Cを設定する
  3. データのランダムサンプリングがしたいぜ!
    1. sedを使う
    2. awkを使う
    3. sortの--random-sortを使う
    4. Script言語を使う
    5. shufを使う
    6. ランダムサンプリング速度比較
  4. 合計と平均値を集計したいぜ!
    1. 列データ取得
    2. 重複行のカウント
    3. 合計値出力
    4. 平均値出力
  5. 複数ファイルのデータ結合がしたいぜ!
    1. 共通項目での結合
    2. 同じ行数での結合
  6. まとめ

データ集計コマンド

joinコマンドが便利過ぎて生きるのが辛い - Yuta.Kikuchiの日記 はてなブックマーク - joinコマンドが便利過ぎて生きるのが辛い - Yuta.Kikuchiの日記
lookコマンドによる二分探索が速すぎて見えない - Yuta.Kikuchiの日記 はてなブックマーク - lookコマンドによる二分探索が速すぎて見えない - Yuta.Kikuchiの日記

今日はデータ集計を行う上で絶対に覚えておいた方が良いコマンドと知識を紹介したいと思います。これを身につければシステム処理と業務効率化に大きく繋がると思います。この記事で紹介するコマンドはlook、sort、cut、uniq、shuf、awksed、join、pasteで、設定知識としてLC_ALL=Cについても軽く触れたいと思います。その他perlpythonの一行野郎についても少し書きます。下で紹介する例はとにかく一行野郎に拘って書いていますので、見づらい事は予めご了承ください。

爆速で検索したいぜ!

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 はてなブックマーク - 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

この結果をみるとgrepLC_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というコマンドを利用することです。sedawk、sortを使うのは注意が必要です。ランダムサンプリングの方法ですが2種類やり方があります。

サンプリング方法 特徴 コマンド
乱数行取得 乱数生成は処理が速い。
乱数行取得は処理が重い。
乱数の重複制御を入れる必要がある。
sedawk
全体をシャッフルする データ全体を操作するので処理が重くなる。
それでもsedawkよりは速い。
重複の制御が必要無い。
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 total

awkを使って乱数の重複を除外したのが以下の例になります。

$ 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 total

awkの問題としてランダムサンプリングのロジックが複雑になってしまうので自分で最初から書くのが大変です。また処理も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言語を使う

PerlPythonの例を書いておきます。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 100
shufを使う

shufが一番簡単かつ高速です。よって皆さん、ランダム行出力を行いたい場合はshufを使うようにしましょう!

$ shuf -n 100 data.txt
0.25s user 1.02s system 25% cpu 4.999 total
ランダムサンプリング速度比較

shufの圧勝となりました。※N/Aは処理が重たすぎて結果が取得できず。

サンプリング手法 処理速度
sedで乱数生成。重複あり 9:35.45
awkで乱数生成。重複あり 29:16.09
awkで乱数生成。重複無し 1:12:44.20 
sortでシャッフル。重複無し 2:10.29
perlでシャッフル。重複無し 53.833
pythonでシャッフル。重複無し N/A
shufでシャッフル。重複無し 4.999

合計と平均値を集計したいぜ!

列データ取得

数値の集計にはawkを利用するのがいいです。上と使用するデータを変更して以前お遊びで抽出した以下のデータを利用します。Data/ero_prediction.tsv at master · yutakikuchi/Data はてなブックマーク - 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

まとめ

  • 大量データに対して検索を行う場合はgrepではなく、lookコマンドを利用しましょう!
  • grep、sortを使う時は環境による依存を無くすだけでなく処理速度を上げるためにLC_ALL=Cを設定しましょう!
  • 大量データからランダムサンプリングする時はshufコマンドを利用しましょう!
  • 合計、平均値を求める場合はawkを利用しましょう!
  • 複数ファイルのデータ結合をしたい場合は、joinとpasteを利用しましょう!