【Awkでデータ解析のすゝめ】awkのみで2つのファイルを効率的に結合させる方法


2021/02/10
2021/05/23
蛸壺の技術ブログ|awkのみで2つのファイルを効率的に結合させる方法

Awkを使ったデータサイエンス向けに不定期で紹介しているワンポイント講座シリーズの二回目です。

時系列解析するデータによってはキーになる値は共通しているのに、ラベルがバラバラのファイルとして四散して存在している場合があります。

今回はこのような作業をAwkのみを使って効率的に結合させるためのテクニックを考察してみます。


Awkでのファイルの結合

論より証拠ということで、早速具体的なAwkスクリプトを作成しながら、Awkによるパワフルかつ柔軟なファイルの結合をやってみます。

2つのファイル間でのデータ結合

まずはテスト用に以下のような2つのデータファイルを作成します。

            
            $ cat << EOF > open.csv
2020-05-14,1596
2020-05-15,1529
2020-05-18,1565
2020-05-19,1575
2020-05-20,1570
2020-05-21,1599
2020-05-22,1551
2020-05-25,1553
2020-05-26,1583
2020-05-27,1569
2020-05-28,1537
2020-05-29,1571
2020-06-01,1558
2020-06-02,1564
2020-06-03,1599
2020-06-04,1590
2020-06-05,1571
EOF

$ cat << EOF > volume.csv
2020-05-19,9500
2020-05-20,9600
2020-05-21,8300
2020-05-22,3100
2020-05-25,3800
2020-05-26,9700
2020-05-27,14300
2020-05-28,18400
2020-05-29,12200
2020-06-01,4600
2020-06-02,11200
2020-06-03,12300
2020-06-04,8000
2020-06-05,8300
2020-06-08,15200
2020-06-09,19900
2020-06-10,8100
2020-06-11,16600
EOF
        
例としてはなんでもよかったのですが、今回は、とある株式銘柄の日足データのうち、期間の始値と出来高を記録したデータを1つのファイルに結合することにしましょう。

データの見てのとおりで、キーにあたる日付の列がファイルごとにバラバラですので、単純なAwkの操作で行数同士の要素を繋ぎ合わせるだけでは正しい結合が出来ません。

キー値(ここでは1列目の日付)で共通項目を括りだしながら、ファイルを結合していくためには以下のようにAwkを使ってあげると上手くいきます。

            
            $ awk -F "," '
BEGIN {
    OFS=","
}
F == 0 {
    open_arr[$1] = $2;
    next;
}
{
    if($1 in open_arr) {
        print $1, open_arr[$1], $2;
    }
}
' F=0 open.csv F=1 volume.csv
#👇実行結果
2020-05-19,1575,9500
2020-05-20,1570,9600
2020-05-21,1599,8300
2020-05-22,1551,3100
2020-05-25,1553,3800
2020-05-26,1583,9700
2020-05-27,1569,14300
2020-05-28,1537,18400
2020-05-29,1571,12200
2020-06-01,1558,4600
2020-06-02,1564,11200
2020-06-03,1599,12300
2020-06-04,1590,8000
2020-06-05,1571,8300
        
このスクリプトが行っているポイントは、まずコマンドのファイルを読み込んでいる引数の箇所... F=0 open.csv F=1 volume.csvで、ファイル毎の識別番号をFによって指定しています。(実際にはFでなくても、FILEとかFLとかシステムで使う予約オプションに被らない変数名であればどんな変数名を使ってもOKです。)

これによってAwk内のスクリプトブロックの内部で、
F == 0 {...}というブロックがありますが、この箇所ではF=0と指定したopen.csvしか処理しなくなっているわけです。

また
F == 0 {...}内では、open_arrとして日付をキー、始値をラベルとした連想配列を作成しています。なお、nextはforループなどでいうcontinueにあたる予約子です。

open.csvから連想配列を作成後に、このnextがない場合、F == 0 {...}の次のブロック処理で止まらずに、更に後段のブロックでもopen.csvが走ってしまうことになり、余計な結果が出力されてしまします。

以上のテクニックから2つのファイルから適切にデータを結合できるようなAwkスクリプトができました。

複数のファイルの結合を同時に行う

先程は2つのファイル間での結合操作を行いましたが、折角なのでもっと応用的なところも狙ってみます。

場合によっては複数の結合したいファイルがあるときもあります。

先程説明していた2つのファイルづつ結合させて、さらに結合後のファイルをまた別のファイルと結合....を繋いでいくとなんとか泥臭く複数ファイルの結合できるかとは思いますが、一気に複数のファイルをAwkで捌くことも可能です。

試しに3つのファイルで結合操作する例を挙げておきます。このテクニックを応用すると、ファイルが4つでも5つでも一気に結合することが可能になります。

まずは先程の利用例からもう一つ、以下のようなとある株式銘柄の日足の終値を持つ
close.csvを作成しておきます。

            
            $ cat << EOF > close.csv
2020-05-15,1565
2020-05-18,1540
2020-05-19,1567
2020-05-20,1598
2020-05-21,1551
2020-05-22,1541
2020-05-25,1578
2020-05-26,1569
2020-05-27,1569
2020-05-28,1570
2020-05-29,1558
2020-06-01,1554
2020-06-02,1588
2020-06-03,1569
2020-06-04,1565
2020-06-05,1566
2020-06-08,1563
2020-06-09,1536
2020-06-10,1549
2020-06-11,1516
2020-06-12,1482
2020-06-15,1479
2020-06-16,1539
2020-06-17,1539
2020-06-18,1550
EOF
        
以下が同時に3つのデータファイルを結合するAwkスクリプトになります。

            
            $ awk -F "," '
BEGIN {
    OFS=","
}
F == 0 {
    open_arr[$1] = $2;
    next;
}
F == 1 {
    if($1 in open_arr) {
        close_arr[$1] = $2;
    }
    next;
}
{
    if($1 in open_arr) {
        print $1, open_arr[$1], close_arr[$1], $2;
    }
}
' F=0 open.csv F=1 close.csv F=2 volume.csv
#👇実行結果
2020-05-19,1575,1567,9500
2020-05-20,1570,1598,9600
2020-05-21,1599,1551,8300
2020-05-22,1551,1541,3100
2020-05-25,1553,1578,3800
2020-05-26,1583,1569,9700
2020-05-27,1569,1569,14300
2020-05-28,1537,1570,18400
2020-05-29,1571,1558,12200
2020-06-01,1558,1554,4600
2020-06-02,1564,1588,11200
2020-06-03,1599,1569,12300
2020-06-04,1590,1565,8000
2020-06-05,1571,1566,8300
        
シンプルな実装でなかなか痒いところまで手が届くような結合になっていると思います。


おまけ〜データ解析しないならjoinコマンドの方が手っ取り早い

余談ですが単なる2つのファイルの結合だけを行う場合にはjoinコマンドが利用できるので、awkを使う必要もありません。

例えば今回の例で言うと以下にようになります。

            
            $ join -t"," -1 1 open.csv -2 1 volume.csv
2020-05-19,1575,9500
2020-05-20,1570,9600
2020-05-21,1599,8300
2020-05-22,1551,3100
2020-05-25,1553,3800
2020-05-26,1583,9700
2020-05-27,1569,14300
2020-05-28,1537,18400
2020-05-29,1571,12200
2020-06-01,1558,4600
2020-06-02,1564,11200
2020-06-03,1599,12300
2020-06-04,1590,8000
2020-06-05,1571,8300
        
この程度ならjoinでも良いのですが、データを数値的に変形したり、データが複数のファイルに複雑に広がっている場合などに、高度な解析を加える必要がある場合には、joinコマンドを使うよりAwkによる処理を検討したほうが良いでしょう。


Awkで複数の標準入力の結合

さきほどまでは複数のファイルを指定して、データをスマートに結合・統合する例を紹介しましたが、既に変数としてメモリに取り込んであるデータをファイルとして一旦ローカルに保存して、そのファイルをもう一度読み込むのはあまり好ましいやり方ではありません。

どうにかローカルファイルとして読み込まず、複数のデータをそのまま標準入力としてAwkで捌きたいというやり方もここで考えてみましょう。

複数の標準入力をファイルの代わりに扱う

まずは理解しやすいように単純なサンプルコードで、標準入力からcatコマンドにデータの中身をリダイレクトしてみましょう。

            
            #👇データの生成
$ OPEN_DATA=$(
cat << EOF
2020-05-19,1575
2020-05-20,1570
2020-05-21,1599
2020-05-22,1551
EOF
)

#👇データを標準入力としてcatにリダイレクト
$ cat <(echo "$OPEN_DATA")
2020-05-19,1575
2020-05-20,1570
2020-05-21,1599
2020-05-22,1551
        
ここで使った標準入力のリダイレクトのテクニックは、コマンド2 <(コマンド1)という構文を利用します。細かくいうと、コマンド2と<の間には必ず空白文字を入れていなければシンタックスエラーになり、<(の間は離してもシンタックスエラーになりますので注意が必要です。

この構文はコマンド1で実行された標準出力がそのままリダイレクトされて、コマンド2の標準入力としてパイプされるので、作用としては
コマンド1 | コマンド2と同様になります。

この
コマンド2 <(コマンド1)で何が嬉しいかというと、パイプライン処理(|)と違って、入力ストリームが複数同時に利用できるようになる点です。

どういうことかというと、先程のコマンドに加えてさらに以下のコマンドも実行してみましょう。

            
            #👇別のデータを生成
$ VOL_DATA=$(
cat << EOF
2020-05-19,9500
2020-05-20,9600
2020-05-21,8300
2020-05-22,3100
EOF
)

#👇データを標準入力(マルチストリーム)としてcatにリダイレクト
$ cat <(echo "$OPEN_DATA") <(echo "$VOL_DATA")
2020-05-19,1575
2020-05-20,1570
2020-05-21,1599
2020-05-22,1551
2020-05-19,9500
2020-05-20,9600
2020-05-21,8300
2020-05-22,3100
        
これで分かるのは、<(コマンド1) <(コマンド2) <(コマンド3) ...と繰り返すことで複数の標準入力が順番に直列のデータとして与えることが出来るようになっていることが分かります。

標準入力のマルチストリームをAwkで処理する

先程の例で、複数の標準入力をまとめてリダイレクトできる方法は理解していただけたかと思いますが、これをAwkと組み合わせると面白い処理が可能です。

前の節でも説明したように、ファイルの識別変数Fを利用してファイル入力を区別させていましたが、なんと標準入力の各ストリームにも同様に識別変数を付けることが出来る(!)のです。

以下のコマンドで確認してみましょう。

            
            #👇データを標準入力(マルチストリーム)をAwkで合成
$ awk -F"," '
BEGIN {
    OFS=","
}
F == 0 {
    open_arr[$1] = $2;
    next;
}
{
    if($1 in open_arr) {
        print $1, open_arr[$1], $2;
    }
}
' F=0 <(echo "$OPEN_DATA") F=1 <(echo "$VOL_DATA")
#👇合成結果
2020-05-19,1575,9500
2020-05-20,1570,9600
2020-05-21,1599,8300
2020-05-22,1551,3100
        
ということで、期待通りに高度で効率のよいデータ行の合成が成功していることが分かります。


まとめ

今回はAwkによる複数のデータファイルを同時に捌くような効率的な処理方法の実装を説明していきました。

内容的にはデータサイエンス方面の用途を想定していますが、このテクニックを覚えたらエクセルを使う日々のデスクワークの業務にも使えますので是非ともAwk脳を鍛えて、煩わしい業務の効率化を図ってもいかがかと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。