機械学習では大きなファイルをちょっと切り出す、みたいな事が結構必要になるので、意外と変な知識を要求される。 その分ちょっとした事は諦めてPython使えば良いのでそういうのはやらない。

grep入門

grepの入門は以下を読む。

正規表現はこのページで共通して使うのでちょっと大変でも頑張って読もう。

sed入門

sedは正規表現にマッチした部分を置換するコマンドです。

sedは非常に高機能ですが、機械学習の実務ではセットアップのシェルスクリプトなどでsコマンドを使う程度なので、そこだけ覚えればいいでしょう。 使い方は以下です。

sed 's/正規表現/置き換える文字/'

置き換える対象は標準入力からとります。普通はパイプで使います。

一行読んでは正規表現にマッチするかテストし、マッチしたらそこを置き換えます。マッチしなかったらそのままその行を出力します。

sは置換をする、というコマンドでs以外にも様々な物がありますが、機械学習では使いません。 少し例を見てみましょう。

例.

$ echo "/some/data/0/0001.jpg" | sed 's/\.[a-z]*/.dat/'
/some/data/0/0001.dat

なお、sedの/の部分は、別の文字に変える事も出来ます。URLやパスなどでスラッシュにマッチさせたい時に便利です。 例えば sed 's|正規表現|置き換える文字|' でも良いです(私はこの縦棒を良く使います)。

例.

$ echo "/some/data/0/0001.jpg" | sed 's|/[^/]*$|/catalog.json|'
/some/data/0/catalog.json

アドレス指定

デフォルトでは全ての行に対して一行一行処理していきますが、処理する範囲を指定する事も出来ます。処理する範囲の指定をアドレス指定といいます。

プログラマの実務では正規表現の範囲指定を良く使いますが、機械学習では行単位の判定と行数の指定を使う事がたまにある、程度でしょう。行数の指定は凄く限定された使い方なのであとで別にとりあげる事にして、ここでは行判定の記法だけ解説します。

行単位の判定は、sの前に正規表現で指定します。sed /判定の為の正規表現/s/通常の置換の正規表現/置換対象文字列/という形になります。

例をあげましょう。以下のようなテキストファイルがあったとします。

$ cat test.txt
hoge 123
123
hoge ika 123 fuga

これに対してアドレス指定の無いこれまでの置換では、以下のようになります。

$ cat test.txt | sed 's/123/999/'
hoge 999
999
hoge ika 999 fuga

一方、hogeのある行だけこの置換を行う、という風に書くと以下のようになります。

$ cat test.txt | sed '/hoge/s/123/999/'
hoge 999
123
hoge ika 999 fuga

二行目が999じゃなくて123のままな事に着目してください。sed '/hoge/s/123/999/'は、「正規表現hogeに一致する行に対してだけs以下を実行する」という意味になります。 sedは各行を読んでまず/hoge/によるマッチングを試し、マッチしていなければ何もいじらずそのままの行を出力します。 マッチしていたらs以下を実行します。

マッチした所だけ出力する(nとp)

アドレス指定と組み合わせて良く使われる機能として、nオプションとpコマンドがあります。 nオプションは基本的には出力をしない、という動作を行うオプションです。 これだけでは何も表示されなくなって意味が無いので、処理した所だけ表示する、というpというコマンドと組わせます。

構文としては以下のようになります。sed -n 's/正規表現/置換文字列/p' このようにsedのコマンドラインオプションに-nを指定し、 置換の書式の最後にpを追加します。

先ほどのtest.txtの、今度はhogeをZZZに置き換えてみましょう。

$ cat test.txt | sed -n 's/hoge/ZZZ/p'
ZZZ 123
ZZZ ika 123 fuga

二行目の123が表示されなくなった事が分かると思います。

行数によるアドレス指定

アドレス指定には、行数を指定する事が出来ます。これは「X行からY行まで抜き出す」という用途でしか使われないので、 具体例を見れば十分でしょう。

10行目から15行目まで表示したいなら、sed -n '10,15p'とします。 headとtailをあわせたような振る舞いですね。

$ env | sed -n '10,15p'
GOPATH=/home/karino2/go/1.13.1
PWD=/home/karino2/Documents/linux_intro_ml
HOME=/home/karino2
GOROOT=/home/karino2/.goenv/versions/1.13.1
NAME=LETS-NOTE-RZ4
XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop

これは大規模なテキストデータの一部を抜き出していろいろ調査する時に、アドホックに作業する時に良く使います。

なお、headとtailを組み合わせたenv | head -n 15 | tail -n 6でも同じ結果が得られます。

機械学習で使った例、Jupyterのtokenだけを出力する

自分が機械学習関連で使ったシェルスクリプトには以下のような物がありました。

#!/bin/sh

jupyter-notebook --ip=0.0.0.0 2>&1 | tee notebook_output.log | sed -n "/^ *http.*8888/s/ *http.*token=//p"

teeは標準入力を、引数のファイルと標準出力の両方に出力する、というコマンドです。 ローマ字の「T」の形から来た名前で、パイプを横にそのまま出力を流しつつ下にも落とす、みたいなイメージでしょうか。 何かトラブルが出た時にnotebook_output.logを見たり別のターミナルなどからtail -f notebook_output.logで張り付いて長いコマンドが実行されるのをまったりするのに使いますが、今回のsedの理解としては取り払っても同じです。

sedに集中する為に、同じ処理になる以下のような例を見てみましょう。

$ cat notebook_output.log | sed -n "/^ *http.*8888/s/ *http.*token=//p"

notebook_output.logは以下のような内容とします(試したければ自分でエディタでnotebook_output.logというファイルを作ってコピペしてください)。

$ cat notebook_output.log
[I 00:57:58.873 NotebookApp] Serving notebooks from local directory: /work
[I 00:57:58.874 NotebookApp] The Jupyter Notebook is running at:
[I 00:57:58.875 NotebookApp] http://(88a123b456d or 127.0.0.1):8888/?token=8b27014067bac97736498c7f627635db70142448e37b851a
[I 00:57:58.875 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 00:57:58.876 NotebookApp] No web browser found: could not locate runnable browser.
[C 00:57:58.876 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://(88a123b456d or 127.0.0.1):8888/?token=8b27014067bac97736498c7f627635db70142448e37b851a

やりたかったのは、この最後の行のtokenより下だけを出力したかった。jupyterを使う時にこれをコピペしなきゃいけないのは機械学習をやっている人なら知っていると思います。

ただ3行目にもhttpを含む行があるので、単純に置換をするだけだとやや面倒なので、アドレス指定をしてこの最後の行だけ出力させたい、と思った。 その為の正規表現が/^ *http.*8888/です。

行の先頭から空白が並んで、そのあとhttpがある。念のため8888も入っている事も足してあります。これで3行目はマッチせずに最後の行だけがマッチします。

そして置換としては、s/ *http.*token=//pとしてます。行頭からtoken=までを全部空文字列に置き換えて、結果としてそこから後ろだけを出力しています。あとで述べる後方参照を使っても良いのですが、不要だったのでこのように書いています。

以上をまとめると、以下のようになります。

$  cat notebook_output.log  | sed -n "/^ *http.*8888/s/ *http.*token=//p"
8b27014067bac97736498c7f627635db70142448e37b851a

トークンだけ出す方がコピペがちょっとやりやすかったという事ですね。これはスマホのsshアプリからちょっとした動作確認をしたい時に、タッチだけでちょうどコピペする範囲だけを選択するのが辛くて作りました。

TODO: 基本コマンドの方にteeの解説を移す

後方参照

機械学習ではあまり使いませんが普通のプログラミング業務では良く使う物に後方参照があります。

置換の時に正規表現の側を\(\)囲っておくと、そこにマッチした文字を置換する文字列の方で\1\2として参照出来ます。 1や2は何番目の括弧にマッチしたかを表します。 例を挙げましょう。

$  echo "/some/data/0/0001.jpg" | sed 's|\([0-9]\+\)/\([0-9]\+\)\.jpg|\1_\2.dat|'
/some/data/0_0001.dat

なかなか厳しい正規表現ですね。正規表現は書くのは簡単ですが他人のを読む時はちょっと憂鬱になります。

これは画像分類でMobileNetなどでfeature extractionしたあとに、そのファイルをカテゴリ名とファイル名でまとめて処理したくなった時にやった処理を改変したものです。

もともと「カテゴリid/ファイルid.jpg」という風になっているものを「カテゴリid_ファイルid.dat」に変えたかった訳ですね。

最初の括弧でカテゴリ名に、次の括弧でファイルidにマッチさせています。また、sedではプラスの前にバックスラッシュ(環境によっては円記号)が必要です。 これはawkやPythonとは違う所なので注意しましょう。

もう一つ、先ほどのjupyterの例を改変したものを見てみましょう。

$ echo "http://1234:8888/?token=8b27014067bac97736498c7f627635db70142448e37b851a" | sed 's/.*token=\(.*\)/\1/'
8b27014067bac97736498c7f627635db70142448e37b851a

こっちの方が先ほどの正規表現よりも筋は良いかもしれませんね。

さらに勉強したい人は

sedは高機能なので、ちゃんと勉強したい場合はもっとシステマティックに学ぶのが良いと思います。 以下のgnuの公式ドキュメントは十分に良く書けていると思います(少し詳細すぎるとは思いますが)。

sed, a stream editor

読む時には最初のコマンドラインオプションの山は飛ばして、3.1のsed script overviewあたりから読むと良いでしょう。

sedはかなりいろいろな事が出来るのですが、頑張りすぎると暗号みたいな物になってしまうので、やりすぎないのが大切です。 特に機械学習の場合、チームのメンバのsed力はあまり高くないはずなので、 普通のクラウドプログラマほど同僚の読解力には期待出来ないでしょう。

コマンドラインでのちょっとした処理くらいに留めておくのが正しい機械学習の分析屋だと思います。

awk入門

awkはsedを拡張してプログラム言語にしたようなコマンドです。 プログラム言語としてはPythonに似ています。

プログラム言語としてちゃんと学ぶとそれなりに機能が多いawkですが、機械学習の分析においては本格的なプログラムはPythonで行う為、awkの出番は対話的にアドホックに大きなテキストを調べたり、ちょっとした環境設定の奥の手として使う程度です。

ここでは機械学習の分析で使う事が多い機能に絞って解説します。

簡単な例

解説の前に、幾つか簡単な例を見てみます。実際に動かしてみてください。

まずはls -lの結果をいろいろ見てみます。

$ ls -l
total 88
-rwxrwxrwx 1 karino2 karino2  1847 Jan 26 21:55 _config.yml
-rwxrwxrwx 1 karino2 karino2  7987 Jan 26 21:54 index.md
-rwxrwxrwx 1 karino2 karino2 26755 Jan 28 17:29 linux_cmd.md
-rwxrwxrwx 1 karino2 karino2  1092 Jan 27 19:15 machine_admin.md
-rwxrwxrwx 1 karino2 karino2 24968 Feb  5 22:21 shell_intro.md
-rwxrwxrwx 1 karino2 karino2   192 Jan 28 17:15 sync.sh
-rwxrwxrwx 1 karino2 karino2 12611 Feb  6 19:25 text_op.md

まずはサイズだけ表示してみましょう。

$ ls -l | awk '{print $5}'

1847
7987
26755
1092
24968
192
12709

次にこれを足してみます。

$ ls -l | awk '{sum += $5} END{print sum}'
76300

サイズが5000以上の行だけ表示してみます。

$ ls -l | awk '$5>5000'
-rwxrwxrwx 1 karino2 karino2  7987 Jan 26 21:54 index.md
-rwxrwxrwx 1 karino2 karino2 26755 Jan 28 17:29 linux_cmd.md
-rwxrwxrwx 1 karino2 karino2 24968 Feb  5 22:21 shell_intro.md
-rwxrwxrwx 1 karino2 karino2 13489 Feb  6 19:29 text_op.md

ファイル名が.mdの物だけのサイズの和を求めてみます。

$ ls -l | awk '$9 ~ /.md/{sum += $5} END{print sum}'
74651

なお、以下のようにしても同じです。

$ ls -l *.md | awk '{sum += $5} END{print sum}'
74651

このようにスペースやタブで区切られたテーブルのようなデータをフィルタ的に処理するのがawkの基本となります。

パターンとアクション

awkのスクリプトはパターンとアクションがワンセットになっていて、それが並んだ物、という構造をしています。

パターン { アクション } というのが基本的な構造です。

awkは標準入力から一行ずつ読み込みパターンと一致したらアクションを行う、という事を繰り返す物です。

例えば以下のコマンドの場合、

$ ls -l | awk '/.md/{ print $9}'
index.md
linux_cmd.md
machine_admin.md
shell_intro.md
text_op.md

パターンは/.md/です。今読み込んでいる行と正規表現.mdがマッチすればアクションが実行されます。 厳密な事を言えばこの.は全ての一文字にマッチしてしまうので、amdでもbmdでもマッチしてしまいますが、エスケープするのがかったるいのでこの文書では.mdで通します。

アクションはこの場合print $9です。これは9番目のカラムをprintする、という事ですが、詳細はあとで解説します。

パターンとアクションは複数あっても良く、各行に対してそれぞれのパターンが試され、マッチした物はアクションが実行されます。

少し人工的な例ですが、以下をみて下さい。

$ ls -l | awk '/.md/{ print $9} /_/{print $9}'
_config.yml
index.md
linux_cmd.md
linux_cmd.md
machine_admin.md
machine_admin.md
shell_intro.md
shell_intro.md
text_op.md
text_op.md

各行を読み込み、.mdにマッチすればファイル名を表示し、_にマッチすればファイル名を表示します。 どちらも実行されるので、両方にマッチする場合は二回出力されている事が分かるでしょう。

また、パターンが無い場合は全ての行にマッチします。 ls -l | awk '{ print $9}'は全ての行にマッチして、各行の$9をprintします。

ドル変数とFS

awkは、読み込んだ行をフィールドセパレータで区切って$1, $2, $3...と順番にドルで始まる変数に入れます。$0には区切られていない行全体が入ります。

フィールドセパレータのデフォルトは1回以上連続する空白かタブです。

デフォルト以外のフィールドセパレータが使いたい場合は-Fオプションで指定出来ます。 正規表現を指定しますが、ほとんどはカンマを指定するくらいだと思います。

例えばenvのようにイコールで区切られるものの場合、以下のようにイコールをフィールドセパレータに指定出来ます。

$ env | awk -F '=' '/PATH/{print $1}'
GOPATH
PATH

なお、空白かタブの連続、というのはtsvで作業する時に空のカラムが入っているとくっついたり、カラムの中に空白が入っていると間違ってカラムに分割されたりとハマりがちなので注意です。 tsvファイルを扱う時は-F '\t'などと指定すると良いでしょう。

変数とNRとNF

特別な変数以外の変数は、初めて使われる時には0の初期値を持ちます。

だから以下のスクリプトは

$ ls -l *.md | awk '{sum += $5} END{print sum}'
74651

最初にアクションが実行される時はsumが0で、そこにサイズが足されていきます。

なお、ENDは全ての行を読み込み終わって処理が終わったあとの最後に実行される特別なパターンです。

特別な意味を持つ変数として、NRとNFがあります。 Number of RecordsとNumber of Fieldsの略で、NRが現在の行数、NFが現在の行のカラム数になります。

$ ls -l | awk '{print NR, NF}'
1 2
2 9
3 9
4 9
5 9
6 9
7 9
8 9

パターンいろいろ

パターンには、

  1. 何も無し(デフォルト)
  2. BEGINとかEND
  3. 正規表現
  4. 範囲指定(/reg_beg/,/reg_end/のような形)
  5. 条件式 ($5 > 5000とか)

くらいのパターンがあります。 1はまぁいいと思うので2以降を以下で簡単に解説しておきましょう。

2. BEGINとEND

BEGINは一行目を読む前に実行される特別なパターンで、変数の初期化などを行います。 ENDは最後の行の処理が終わったあとに実行される特別なパターンで、集計した結果を表示したりするのに使います。

$ ls -l *.md | awk '{sum += $5} END{print sum}'
74651

で出てきていますね。最後のENDがそれです。

3. 正規表現

正規表現はスラッシュで囲んで記述します。 以下の例では.mdが正規表現で、各行にこの正規表現がマッチしたらアクションが実行されます。

$ ls -l | awk '/.md/{ print $9}'

4. 範囲指定

ありがちな例ではXMLとかで特定の子要素を取り出す、みたいなのです。 例えばこんなのです。

$ cat test.html
<html>
        <body>
                Hello
        </body>
</html>

こういうファイルがあったとして、body下だけを取りたい場合は以下。

$ cat test.html | awk '/<body>/,/<\/body>/{print $0}'
        <body>
                Hello
        </body>

こんな感じで、/開始の正規表現/,/終了の正規表現/と書くと、 開始の正規表現がマッチした行からアクションが実行されはじめ、 終了の正規表現がマッチする行までアクションが実行され続けます。 終了の正規表現よりあとは何も実行されません。

上記の例だと、/<body>/,/<\/body>/の部分ですね。 なおスラッシュが正規表現の区切りに使われるので、</body>のスラッシュはエスケープしないといけません。めんどくさいですね。

なお、print $0の場合は$0は省略して良くて、printとだけ書く事も出来ます。 awkのこの手の省略系はたくさんあってキリが無いので、まず冗長でも基本的な書き方を覚えて、良く使うのだけおいおい省略系も覚えていく、くらいで十分でしょう。

余談ですがこの範囲指定はsedでも同じような記法で指定が出来ます。 機械学習ではうまくこのパターンでsedで扱えるほど綺麗なデータじゃない事が多いのでsedではあまり出番がありませんが。

5. 条件式

ドル変数とかNRとかを使って、==とか!=とかが書けます。 また、正規表現のマッチは~で書けて、!~でnot matchが書けます。&&とか||も使えます。

例を見ましょう。

$ ls -l | awk '$9 ~ /.md/{sum += $5} END{print sum}'
74651

このうち、$9 ~ /.md/が条件式の例です。$9.mdという正規表現とマッチしたら、 このパターンが成立したとみなされあとに続くアクションが実行されます。

ではさらに、ファイル名の拡張子が.mdで、ファイルサイズが5000以下のファイルのサイズを足してみましょう。

$ ls -l | awk '$9 ~ /.md/ && $5 < 5000{sum += $5} END{print sum}'
1092

この場合は$9 ~ /.md/ && $5 < 5000の部分がパターンですね。

他にも三行目から五行目まで表示してみましょう。

$ ls -l | awk 'NR >=3 && NR <= 5 {print}'
-rwxrwxrwx 1 karino2 karino2  7987 Jan 26 21:54 index.md
-rwxrwxrwx 1 karino2 karino2 26755 Jan 28 17:29 linux_cmd.md
-rwxrwxrwx 1 karino2 karino2  1092 Jan 27 19:15 machine_admin.md

これはls -l | sed -n '3,5p'と同じですね。

sedで出来る事はだいたいawkで出来ます。 sedの方がだいたい短くなりますが、awkだけ覚えておけばとりあえず生きてはいけます。 我らはPythonで勝負するので、この辺は最適なツールを全部理解するよりは省エネでawkだけ知っておけばいいでしょう。

実例

自分の手元に以下のようなシェルスクリプトがありました。

#!/bin/sh

jupyter-notebook --ip=`ip route | awk 'NR==2 {print $9}'`

これはdockerのコンテナの中で、ホスト側からのアクセスを許す為に--ipオプションにホストのipアドレスを入れる、という事をやっているようです。

ipというコマンドの二行目がホストのipアドレスのようですね。(適当にググってやっつけで書いたスクリプトなので良く覚えてない)。

今ip routeを実行したら以下のようになりました。

$ ip route
default via 10.138.0.1 dev eth0 
10.138.0.0/21 dev em1  proto kernel  scope link  src 10.138.0.76
...以下略...

この二行目の最後のフィールドがホストのipアドレスなんでしょう。

$ ip route | awk 'NR==2{print $9}'
10.138.0.76

元のスクリプトではこれをバッククオートで指定している訳ですね。

jupyter-notebook --ip=`ip route | awk 'NR==2 {print $9}'`

このバッククオートは中を実行して結果で置き換えるので、このコマンドはつまり、

jupyter-notebook --ip=10.138.0.76

と同じとなります。

このようにスクリプトの中からバッククォートを見つけてそれを実行してみたりして、「ようするに何が実行されるのか」を探っていくのは、トラブルが起きた時の重要なスキルとなります。

このようにちょっとしたスクリプトなどでもsedやawkは良く使いますが、その真価を発揮するのはアドホックに大きなテキストを調べる場合です。

以下ではそのようなケーススタディとして、USの特許データを少し調べていましょう。

大きなテキストを扱ってみよう

以前勉強会で、USの特許データの分析を行った時の作業を一部書いてみます。 USの特許データ自体になじみが無いとそれぞれのカラムの数字の意味などは分からないと思いますが、 テキスト処理としては簡単な事の組み合わせなので理解は出来るでしょうし、 皆もそれぞれの分野で似たような事をやった経験はあるはずです。

なお、新しいコマンドとかは出てこないつもりなので、意味が分からなければ飛ばして次に行っても構いません。

今回は、以下のcitations.csvとかを実際に触ってみます。

https://bulkdata.uspto.gov/data/patent/office/actions/bigdata/2017/

450MBくらい。一応以下のコマンドで落とせるはずだけれど、

wget https://bulkdata.uspto.gov/data/patent/office/actions/bigdata/2017/citations.csv.zip

WSLではめっちゃ遅いので、別途ブラウザで落とした。

unzipしておく。

$ sudo apt install unzip
$ unzip citations.csv.zip

実際に機械学習で扱うデータとしてはこの位は大きいという程では無くてこの位ならでかいメモリに載せてしまう事も出来るけれど、 ここではもっと大きなデータも扱えるような方法を見てみる。

大きなファイルの最初の一歩

まずはファイルのサイズを見てみる。

$ ls -l citations.csv
-rwxrwxrwx 1 karino2 karino2 2460472008 Nov 16  2017 citations.csv

2.4Gbくらい。まぁまぁでかい。(ls -hlでは2.3Gと表示される)

大きなファイルをいきなりエディタで開くと大変な事になるので、しばらくはシェル上でいろいろなコマンドを使って中を見ていく。

lsの次はheadしてみるのが普通。

$ head citations.csv
app_id,citation_pat_pgpub_id,parsed,ifw_number,action_type,action_subtype,form892,form1449,citation_in_oa
...略...
12000001,20040088473,20040088473,G92WF69VPPOPPY5,103,a,1,0,1
12000001,20030189860,20030189860,G92WF69VPPOPPY5,103,a,1,0,1

カンマ区切りでデータが入っている。 先頭だけ傾向が違う事があるので真ん中あたりもしりたい。

正確に行数を知るならwcだけど、全部読むのは時間がかかる。 とりあえず1万行目から10行くらい表示してみる。

$ cat citations.csv | sed -n '10000,10005p'

う、表示したあと帰ってこない。 これは1万5行目よりあともずーっと読み込み続けるからだ。 Ctrl-Cで中断してもいいのだけど、1万5行目で終わるようにheadを挟むのが良さそう。

$ cat citations.csv | head -n 10005 | sed -n '10000,10005p'
12000863,6883523,6883523,,,,0,1,0
12000863,20040159327,20040159327,,,,0,1,0
12000863,5709227,5709227,,,,0,1,0
12000863,20080216848,20080216848,,,,0,1,0
12000863,3395713,3395713,,,,0,1,0
12000863,5074319,5074319,,,,0,1,0

前の方では2カラム目や3カラム目は日付っぽいのが多かったが、そうとも限らないらしい。 そして1万行くらいなら一瞬でかえってくる事も分かる。

少し小さめな実験用サブセットを作る

でかいファイルはちょっと何かする時にも時間がかかったりしがちなので、 いろいろ実験するには一部を抜き出した小さいファイルを作るのが良い。 そうすればエディタでも開けるし。

先頭の行は各カラムのタイトルっぽいので、入れないという判断もあるが、 最終的には全データにしたいスクリプトのテストには入っている方が都合が良いので入れておこう。

1万行で一瞬だったので、10万行くらい抜き出してみる。

$ head -n 100000 citations.csv > small_citations.csv
$ wc small_citations.csv
 100000  109955 4179322 small_citations.csv
$ ls -hl small_citations.csv
-rwxrwxrwx 1 karino2 karino2 4.0M Feb  8 07:52 small_citations.csv

10万行で4Mb。全部で2.3Gなので1/600くらいのサイズか。行数はトータルでは6000万行くらいっぽい。

action_typeを眺めてみる

action_typeというのが入っていたり空だったりする。 いくつくらい入っているのか知りたいのでsmallの方にaction_typeの空じゃない項目がどの位あるか見てみよう。

awkとしてはカンマ区切りなのでFオプションでカンマを指定する。

まずはaction_typeがドル変数の何番目かを探してみよう。目視で数えてもいいが、適当にプリントしていく。 とりあえずあてずっぽうで$7くらいを表示してみよう。

$ head -n 1 small_citations.csv
app_id,citation_pat_pgpub_id,parsed,ifw_number,action_type,action_subtype,form892,form1449,citation_in_oa
$ head -n 1 small_citations.csv  | awk -F ',' '{print $7}'
form892

はずれ。二つ左隣かな。

$ head -n 1 small_citations.csv  | awk -F ',' '{print $5}'
action_type

あたりだ。次はaction_typeが空じゃない、をprintしてみよう。 とりあえず1000行くらいに対してやってみる。

$ head -n 1000 small_citations.csv  | awk -F ',' '$5 != "" {print $5}'
action_type
103
103
...略...

だいたいは数字っぽいが、たまにG8Y8B1YJPPOPPY5とか意味のありそうなIDが入っている。 空じゃない物はどのくらいの比率で入っているのだろう?

$ cat small_citations.csv  | awk -F ',' '$5 != "" {print $5}' | wc
  24917   25062  104052

2.5万行だからざっくり全体の2.5/10=1/4くらいか。

ざっくりとデータの特徴を調べてみる

とりあえず5万行目くらいを眺めてみる。

$ sed -n '50000,50005p' small_citations.csv
12002781,20110217697,20110217697,,,,0,1,0
12002781,6936451,6936451,,,,0,1,0
12002781,20060115857,20060115857,,,,0,1,0
12002781,20090143244,20090143244,,,,0,1,0
12002781,20020172963,20020172963,,,,0,1,0
12002781,7932034,7932034,,,,0,1,0

先頭のカラムが同じ物がしばらく並ぶ、という構成がみてとれる。

ここからは以前勉強会で聞いた事だが、これは出願するidのパテント一つにつき、 複数のパテントが引用されているという事が書かれているらしい。 出願するパテントのidはapp_id、引用されるパテントのidはparsedというカラムに入るらしい。

headを眺めると、app_idは$1でparsedは$3か。

ではapp_idが12002781な行のparsedを全部出力してみよう。

$ cat small_citations.csv | awk -F ',' '$1 == "12002781"{print $3}'
20070059741
20030138809
6255678
...

実際に試すと結構たくさん出力されて動揺する。あってるかどうか$0をprintしたりして納得したら、行数を数えてみよう。

$ cat small_citations.csv | awk -F ',' '$1 == "12002781"{print $3}' | wc
    227     227    2387

200個も引用されるのか?どういう風に参照されているかはaction_typeに入ると言っていた気がするので、app_id, parsed, action_typeを並べてみよう。

$ cat small_citations.csv | awk -F ',' '$1 == "12002781"{print $1, $3, $5}'
12002781 20070059741
12002781 20030138809
12002781 6255678
...
12002781  Designs  2 nd Edition
12002781 20060199193 103
12002781 20060141474 103
...

だいたい$5は空だが、たまに入っているのもある。$5が空じゃない、も条件に足してみよう。

$ cat small_citations.csv | awk -F ',' '$1 == "12002781" && $5 != ""{print $1, $3,
 $5}'
12002781  Designs  2 nd Edition
12002781 20060199193 102
12002781  Designs  2 nd Edition
12002781 20060199193 103
12002781 20060141474 103
12002781  Designs  2 nd Edition
12002781 20060205061 103
12002781 20060199193 103

action_typeが何なのか、とかは特許データ詳しい人に聞くしか無いが、こんな感じで中身にあたりをつけていく。

parsedは数字が7桁の物と日付っぽいものとそれ以外、となっている。 parsedが数字7桁の物の比率を数えてみよう。

まずはheadでちゃんと取れてるか確認。

$ cat small_citations.csv | awk -F ',' '$3 ~ /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/{print $3}' | head
2343564
6622200
6182004
...

次にwcする。

$ cat small_citations.csv | awk -F ',' '$3 ~ /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/{print $3}' | wc
  64811   64811  518488

6.5万行くらい。半分くらいか?意外と多いね。 ちょっと信じがたいので、3万行あたりから10個くらい表示してみよう。

$ cat small_citations.csv | awk -F ',' '$3 ~ /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/{print $3}' | sed -n '30000,30010p'
5737613
5109494
4631532
5471189
6191969
5649222
...

あってそう。

また、いろいろな事情から、「parsedが6054015でaction_typeが103の行を見たい」となったとする。 そういう場合は以下のような感じになる。

$ cat small_citations.csv | awk -F ',' '$3 == 6054015 && $5 == 103 {print}'
12000670,6054015,6054015,GJOU4LERPPOPPY5,103,a,1,0,1
12000670,6054015,6054015,GQVD1HQ1PPOPPY5,103,a,1,0,1
12000670,6054015,6054015,H0TO8TNGPXXIFW4,103,a,1,0,1
12000670,6054015,6054015,H5XUH9LBPXXIFW4,103,a,1,0,1
12000670,6054015,6054015,HBGM8M1DPXXIFW4,103,a,1,0,1

行数を足したいなら、先頭にNRを足したりも出来る。

$ cat small_citations.csv | awk -F ',' '$3 == 6054015 && $5 == 103 {print NR "," $
0}'
7399,12000670,6054015,6054015,GJOU4LERPPOPPY5,103,a,1,0,1
7402,12000670,6054015,6054015,GQVD1HQ1PPOPPY5,103,a,1,0,1
7422,12000670,6054015,6054015,H0TO8TNGPXXIFW4,103,a,1,0,1
7427,12000670,6054015,6054015,H5XUH9LBPXXIFW4,103,a,1,0,1
7439,12000670,6054015,6054015,HBGM8M1DPXXIFW4,103,a,1,0,1

app_idが14か15から始まる行だけを抜き出す

ここまでなんとなく理解した事などを元に詳しい人に聞いたり協議した結果、app_idが14か15から始まっている行だけを分析対象にしよう、という事になった。

という事でオリジナルの大きなcitations.csvをいじろう。

ここまでいじってみた感じとしては、app_idは上の方から順番に並んでいそう。 あまり確かな事は分からないが、とりあえず14から始まる最初の行を探してそのあとだけに切ってみれば、少なくとも目的のデータは全部入るんじゃないか?と思い、調べてみる。

まずは14で始まる行数を調べる。

$ grep -n '^14' citations.csv | head
41934284:14000001,6427585,6427585,,,,1,0,0
41934285:14000001,7744330,7744330,,,,1,0,0
41934286:14000001,6652214,6652214,,,,1,0,0
...

4193万行くらいから始まるらしい。

全部で何行だろうか?wcすると帰ってこないので行数だけ表示のオプションを--helpで調べて-lをつける。

$ wc -l citations.csv
58862279 citations.csv

さきほどおおざっぱには6000万行くらい、と予測を立てていたのだが、だいたい一致してそう。

headを見るとapp_idは12から始まっている。12、13、14、15はそれぞれどれくらいあるんだろう? とりあえず12と14と15だけ数えてみよう。

この辺は気を付けないとずっと帰ってこないスクリプトになりがちなので注意がいる。 早さではgrepが早い。こんな感じでどうだろう?

$ grep '^12' citations.csv | wc -l
19182111
$ grep '^14' citations.csv | wc -l
15797910
$ grep '^15' citations.csv | wc -l
1130086

15は少ないが、おおざっぱには1700万行くらいづつあるのだろう。

14から始まる行と15から始まる行を抜き出す方法を考える。 結構なサイズのファイルなので、気を付けないとなかなか終わらない。

勉強の為に幾つか考えられる方法を書いてみよう。

なお、こうして抜き出したファイルはしばらく時間が経つとどこから来たのか分からなくなりがちなので、必ず作業をしたらそのコマンドをコピペしてどこかに残しておこう。そうしたファイルの由来に関するメモはgit内のどこかにあるのが望ましい。

案1 tailでまず該当範囲だけ抜き出す

自分が最初に思いついた方法はこれ。

14で始まる行は、41934284行目から始まる事が分かる。 全部で58862279行だったので、58862279-41934284=16927995行の末尾を取り出せば良い。

$ awk 'BEGIN{print (58862279-41934284)}'
16927995

細かい所で1引いたりが必要な気もするので少し大目に引いてheadで確認

$ tail -n 16927997 citations.csv | head
13999999,3017051,3017051,,,,0,1,0
14000001,6427585,6427585,,,,1,0,0
14000001,7744330,7744330,,,,1,0,0
...

tailに渡すのは16927996で良さそう。

$ tail -n 16927996 citations.csv > target_cand.csv

あとはgrepでもawkでも使ってフィルタリングすれば良い。

案2 grepで二回フィルタリング

今回はたまたま条件が行頭なので、grep一発でフィルタリング出来る。

grepなら頑張れば全部検索出来る程度の量しか無いので、二回程度なら待ってれば良い、という話もある。 自分なら最終的にはこれを選ぶと思う。

$ grep '^14' citations.csv > citations_14.csv
$ grep '^15' citations.csv > citations_15.csv

あとはcatでつなげれば良い。 なお、こういう作業をする時はこまめにheadにつなげて確認しながら行うと良い。

さらにカラムの名前も欲しいなら、head -n 1 citations.csvもあわせてcatしてやれば良い。

今回はたかだが二回なので待てば良いが、作業内容によっては100回とか1000回程度行う場合もある。 そういう時は前述のようにtailで抜き出しておく方がずっと早くなるので、この辺はタスクに応じて適切な方法を考える。

なお、awkでもgrepでも出来る事は、だいたいgrepが一番早い。

案3 grepで一回でフィルタリング

少し考えれば、正規表現で一回で抜き出す事も出来る。

$ grep '^1[45]' citations.csv > citations_target.csv

ただ、これが案2より早いかどうかは試してみないと分からない。 正規表現は基本的にはめちゃくちゃ早いのだけど、grepは内部で相当頑張って最適化するのでこの辺はどれがいいかは試してみないと良く分からない。

一般的な指針と注意点

grepの-nオプションで行数にあたりをつけて、head, tail, sedなどで目的のあたりを切り出す、というのは大きなデータでは良くやる手順になる。 grep, head, tailは早い。

少し大めにターゲットの付近を切り出したら、そのあとはawkやsedなどの少し遅くなりそうな処理でも良い。 遅いと言っても相対的な話なので、100万行くらいなら一瞬でしょう。

大きなXMLで目的の要素があるXMLを切り出す、みたいな時もこのパターンが多い。

xmlが連結されたばかでかいファイルがあって、その中で、<doc-number>08160929</doc-number>が入っている<xml>...</xml>を抜き出したい、というような場合、

  1. まずgrep -nで該当しそうな行を調べる
  2. 目的の行の前後1万行とか十分そうな量をhead, tail, sedなどで切り出す
  3. awk /<xml>/,/</xml>/{print}などで切り出す

という形で作業するととりあえず一つ抜き出す事は出来る。

大きなファイルを扱うのは面倒が多いが、抜き出した物を精査するのは普通に作業出来るはずなので、1や2の段階ではいつも少し不要な物も入るくらいに大きく対象を取って、3の所は細かく調べながらやるのが良い。

あまり最初から一気に複雑なスクリプトを書いて片付けようとせず、headやgrepやcatなど原始的なコマンドを組み合わせて、 途中途中で適当に中間ファイルなどに吐き出して一部は手動も混ぜながら作業する方が良い。

こうしたアドホックな作業はやってみると例外も多く完全に自動化は手間がかかる事が多いので、適度に手動で作業するのがポイント。

また、こうしたアドホックな対話的調査というのはやった内容があとに残らない為、ほかのチームメンバが後から追試したい時などに解読するのが大変になりがち。 あとでまとめて文書にして残そう、みたいに考えるのは、だいたいやらないまま放置される運命なので、 なるべく普段の作業が自然に第三者も追える形にしておく方が望ましい。

だから大きなファイルから抜き出す部分などはある程度はシェル上で作業するのも仕方ないが、ある程度対象のサイズを絞る事に成功したら、そこからはなるべくJupyter上で作業する方が良い。

シェルも最初のうちはぶわーっと出力が出てしまう事が多くてそうした物はJupyter上に残しづらいのでターミナルの方がいいが、ある程度方針が固まって来たらJupyter上の!コマンドでどうにか出来ないか検討した方がいい。