見出し画像

bashでデータ処理(ワンライナーの小技を使った初期分析のすすめ)

みなさんこんにちは。くにです。
データを分析するときには、pythonやRのデータフレームに取り込んで処理する人が多いと思います。一方、小さなデータであれば、bashなどのLinuxの標準シェルでできることも結構あります。

Jupyter Notebookが登場してからデータ分析の作業が随分と楽になりました。その一方で、シェルの操作をあまり経験しないままデータの前処理や分析実務を行う人も増えてきました。

そこで、この記事では、サンプルデータを使いながらbashによる簡単なデータ処理をご紹介します。1行で処理をするためのコマンド、いわゆるワンライナーの小技です。基本的なコマンドばかりですが、覚えると何かと便利です。例えば、コンソールで生データを確認したいときや、Linuxのログ情報を見たいときなど。

bashでのデータ処理例をご紹介するために後半は多少トリッキーなワンライナーを掲載しています。実務的にはあるところからはpythonやRで処理するのが楽だと思いますので、その辺は割り引いてご覧くださいませ。

本文に入る前に、この記事で取り上げたデータ処理用のコマンドをまとめておきます。


ファイルを見る:cat, head, tail, shuf
行を数える:wc
テキストを置換する:sed
重複行を数える:sort | uniq
列を切り出す:cut, awk
条件に合う行を抽出する:grep
簡単な集計をする:awk


サンプルデータと動作環境

サンプルデータ

Suzuki Lab.さまが公開している「Twitter日本語評判分析データセット」を利用させていただきました。携帯電話などの話題を含むツイートデータで、534,962件ほどあります。残念ながら本文は含まれていませんが、ポジネガのラベルがついています。
こうしたラベル付きのデータは大変貴重ですね。今回の記事でも楽しく触ることができました。Suzuki Lab.のみなさまには御礼申し上げます。

動作環境

手持ちのノートPCを使いました。Windows 10の上にWSL2+DockerでLinux環境を構築しました。以下のDockerファイルを使用しています。(Ubuntu 20.4)
https://hub.docker.com/r/jupyter/datascience-notebook/

まずデータを見てみる

データが手元にやってきたらまず見てみる、というのが基本的な戦略になります。中身が分からないものを分析することはできません。

解凍 (bzip2)
先ほどご紹介したTwitter日本語評判分析データセットのページから、ファイルtweets_open.csv.bz2をダウンロードしました。bz2で圧縮されたcsvファイルですので、まずは解凍します。これで、tweets_open.csvを触れるようになりました。

$ bzip2 -d tweets_open.csv.bz2

中身をチェック (head)
早速中身をみていきます。ファイルを見るにはcatコマンドを使うとよいのですが、今回はデータ件数がそこそこありそうですので、catだと流れてしまいますね。そこで、先頭から複数行だけ見ることができるheadコマンドを使ってみます。10行ほどみることができました。桁がそろった綺麗なcsvファイルのよですね。ヘッダーがついてないことも分かりました。

$ head tweets_open.csv
10025,10000,522407718091366400,0,0,1,1,0
10026,10000,522407768003592192,0,0,1,0,0
10027,10000,522408018642628609,0,0,1,1,0
10028,10000,522408394871672832,0,0,0,1,0
10029,10000,522408454778929153,0,0,0,1,0
10030,10000,522408539814260736,0,1,0,0,0
10031,10000,522409005574942720,0,1,0,0,0
10032,10000,522409063154339840,0,0,0,1,0
10033,10000,522409073057091584,0,0,0,0,1
10034,10000,522409669369671680,0,0,0,1,0

データ件数を確認 (wc)
外観をチェックすためにwcコマンドを使ってデータ件数を確認します。'-l'オプションを付けるとファイルの行数を出力できます。記事に記載しているとおり534962件と出力されました。

$ wc -l tweets_open.csv
534962 tweets_open.csv

カンマの数を確認してみる1 (sed)
csvデータはカンマ(,)が重要になります。一見すると良さそうなデータでしたが、カンマの数はそろっているでしょうか。そこで、sed コマンドでカンマ(,)以外の文字を削除して眺めてみます。sedコマンドは正規表現を使って文字列の加工ができるので大変便利です。ここでは、一行ごとにカンマ(,)以外の文字を消して出力してみました。全部をみると流れてしまうので、とりあえずheadコマンドで取り出した先頭10行に適用してみました。
基本的にコマンドの出力結果は標準出力として表示されますが、パイプ'|'を使うことで次のコマンドへ渡すことができます。これを使うと余分な中間ファイルを使うことなく、処理をつなげていくことができます。

$ head tweets_open.csv | sed -e "s/[^,]//g" 
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,
,,,,,,,

カンマの数を確認してみる2 (sort, uniq)
sedコマンドでカンマだけ取り出せることが分かったのですが、すべての行でカンマの数がそろっているか確認したいですね。50万行以上あるので目視で確認するのは大変です。そこで、先ほどsedコマンドで取り出したカンマ行のバリエーションを探ってみましょう。もし、すべての行でカンマの数がそろっていたらバリエーションは一つしかないはずです。
これを確かめるためにsortとuniqコマンドを使います。これらコマンドを組み合わせて使うことで、重複行を削除することができます。
結果、カンマが7つ並んだものだけ残りましたので、どの行もカンマの数が揃っていることになりますね。この方法は簡便なものですので、ダブルクォーテーション(")でエスケープ処理がされているデータには利用できません。ご注意ください。

$ cat tweets_open.csv | sed -e "s/[^,]//g" | sort | uniq
,,,,,,,

ファイルの末端を確認する (tail)
念のためデータの最後を確認してみます。tailコマンドを使うとファイルの最後から任意の行を取り出しして表示することができます。表示をしてみると最後の行が改行されていないことがわかりました。それに、最終列のデータも欠損しているようですね。少々わかりにくいですが、最終行の末尾の$はプロンプトです。

$ tail tweets_open.csv
2723936,10021,703558361531305984,0,0,0,1,0
2723937,10021,703557929929015297,0,0,0,1,1
2723938,10021,703557862958637056,0,0,0,1,0
2723939,10021,703557811662262272,0,0,0,1,0
2723940,10021,703557607751954432,0,0,0,1,0
2723941,10021,703557489241903104,0,0,0,1,0
2723942,10021,703557257800253440,0,0,0,1,0
2723943,10021,703557125193101317,0,0,0,1,0
2723944,10021,703557108210372608,0,0,0,0,1
2723945,10021,703556628990177280,0,0,0,1,$

試しに、cutコマンドで1列目だけ取り出して件数を数えてみました。cutコマンドは'-d'オプションで区切り文字をカンマ(,)に指定し、'-f'オプションで列番号を指定することができます。そして、その出力結果をパイプ'|'を使ってwcコマンドにわたすと件数を数えることができます。
数えてみると534963件となりました。最終行の欠損を補間すればもう1件データがあることになりますね。先ほどファイル全体にwcコマンドを実行したときには534962件と出力されていましたが、wcコマンドでは改行コードの数を数えていることがわかりますね。

$ cat tweets_open.csv | cut -d',' -f1 | wc -l
534963

初期分析してみる1(ジャンル)

この先はpandasやRのデータフレームで見るのが早いのかもしれませんが、せっかくなのでbashで分析してみます。まずジャンルに着目してみました。

ジャンル毎のデータ件数を数えてみる (cut, sort, uniq)
今回のデータには2列目に「ジャンル」というカテゴリ情報が付与されていました。データ提供元のWebページによれば、ジャンルは9種類とのことでした。

10000: エクスペリア,Xperia
10001: アクオス,Aquos
10002: アクオス,Aquos
10020: ココロボ(シャープが開発した自動掃除機)
10021: iPhone
10022: パナソニック,Panasonic
10024: コンビニにあるコピー機
10025: ルンバ,Rommba
10026: シャープ
引用元: http://www.db.info.gifu-u.ac.jp/sentiment_analysis/



まず、ジャンルが本当に9種類なのか、これ以外にも何か紛れていないのか見てみましょう。
cutコマンドで第2列目を取り出します。そして、その出力結果に対して、先ほど使ったsort, uniqコマンドを使うことで、ジャンルの種類を確かめることができます。さらに、uniqコマンドで'-c'オプションを使うと、ジャンル毎の件数をみることができます。この出力結果から、件数が結構違うことがわかりました。
'-c'オプション付きでsortやuniqコマンドを実行するとやや時間がかかることもあります。そのような時は 'LC_ALL=C' を指定すると処理が速くなります。これはバイナリで並べ替えてくれるからなのですが、並び順がバイナリ順になるので注意が必要です。

$ cat tweets_open.csv | cut -d',' -f2 | sort | uniq -c
 81021 10000
 17492 10001
 71058 10002
 11664 10020
 79733 10021
 68609 10022
 68428 10024
 68468 10025
 68490 10026

ジャンル毎のデータ件数の割合を数えてみる (awk)
上に示した方法でジャンルの件数がわかりましたが、偏りがありそうなので割合が気になりますね。そこで、awkコマンドを使ってワンライナーで割合を出してみましょう。awkはデータ処理用のスクリプト言語ですが、ワンライナーでもよく使われます。
まず、cutコマンドのように列を抽出することからやってみます。'-F'オプションで入力データの区切り文字を指定し、引数に処理内容を記載してきます。'{'と'}'で囲まれた処理がデータ行ごとに処理される内容です。2列目($2)の情報を行ごとにprint文で出力すればcutと同じ働きになります。

$ head tweets_open.csv | awk -F, '{print $2}'
10000
10000
10000
10000
10000
10000
10000
10000
10000
10000

次に、先ほどのジャンル毎のカウント処理をawkコマンドで再現してみます。コード例では第2列目($2)を連想配列'a'のキーに指定しつつ、カウントアップします。後のことを考えて列の並び順をジャンル、件数に変えています。END以後の処理はすべての行の処理が終わった後に実行されます。ここでは、for文を使って配列aのキー(ジャンル)毎の件数を出力しています。
このように、awkの配列や演算子は非常に柔軟であるのでワンライナーで威力を発揮します。

$ cat tweets_open.csv | awk -F, '{a[$2]+=1}END{for (i in a)print i,a[i]}'
10021 79733
10002 71058
10022 68609
10025 68468
10026 68490
10000 81021
10020 11664
10001 17492
10024 68428

以上の方法を応用してジャンル毎にデータ件数の割合を出してみます。先ほどの処理に加えて、データ全体の件数を数える処理を追加し、最後に割合を計算して出力しました。概ね10%以上ですが、10020と10001だけ極端に割合が小さそうです。

$ cat tweets_open.csv | awk -F, '{a[$2]+=1;n++}END{for (i in a)print i,a[i],a[i]/n}'
10021 79733 0.149044
10002 71058 0.132828
10022 68609 0.12825
10025 68468 0.127986
10026 68490 0.128028
10000 81021 0.151452
10020 11664 0.0218034
10001 17492 0.0326976
10024 68428 0.127912

ここまでで目的は達成されたのですが、並び順がいまいちなのでsortコマンドを使って割合の多い順に並べ替えてみました。sortコマンドの'-k'オプションでソートキーの列番号を指定することがきます。また、'-r' で逆順、'-n'で数値とみなすオプションを選択しています。以下では見やすくするためコマンドの途中で改行を入れていますが、もちろん改行なしでも実行できます。

$ cat tweets_open.csv | awk -F, '{a[$2]+=1;n++}END{for (i in a)print i,a[i],a[i]/n}' \
> | sort -k3 -r -n
10000 81021 0.151452
10021 79733 0.149044
10002 71058 0.132828
10022 68609 0.12825
10026 68490 0.128028
10025 68468 0.127986
10024 68428 0.127912
10001 17492 0.0326976
10020 11664 0.0218034

初期分析してみる2 (ポジネガ分析の準備)

今回のツイートデータには、対象となるジャンルについてポジティブな意見か、それともネガティブな意見であるか、というラベルが付与されています。いわゆるポジネガ分析に利用できる情報があるということですね。
これを調べていきます。

ラベルの付与状況を見てみる1 (shuf)
ポジネガに関する情報はデータの4-8行目に0,1フラグで記載されています。4列目はポジネガ両方、5列目はポジ、6列目はネガ、7列目はニュートラル、8列目はジャンルに無関係の場合にそれぞれ1となります。
先ほど分析したジャンルと違ってわざわざ列を分けていることが気になりました。もし各ツイートがポジネガ、ポジ、ネガ、ニュートラル、無関係のどれかにしか該当しないのであれば、列を分ける必要がないからです。
そこで、shufコマンドを使ってランダムにデータを見てみました。何度かやってみましたが、概ね各データに一つのフラグがたっているように見えます。しかし、果たして本当にそうでしょうか。

$ shuf -n 10 tweets_open.csv
479185,10000,668782568515371009,0,0,0,1,0
1725006,10026,702791707054645249,0,0,0,1,0
1171274,10025,557909213997838336,0,0,0,0,1
516936,10002,524133264735236096,0,0,0,0,1
20317,10000,525282941434200064,0,0,0,1,0
453197,10000,660448939817086976,0,1,0,0,0
581209,10002,592159362707099649,0,0,0,0,1
588616,10002,602396102168416257,0,0,0,0,1
2079888,10024,695206172115095552,0,0,0,0,1
462264,10000,663253742628696064,0,0,0,1,0

ラベルの付与状況を見てみる2 (awk)
次に、awkコマンドを使って各データのフラグ数を数えてみます。awkで4列目から8列目までの値を加算し、その結果をsortとuniqを使って数えてみました。フラグが0,1で表現されているのでこのような処理で簡単に見ることができました。
結果、全体の約95%は排他的にフラグがついていたのですが、残りの約5%は複数のフラグがついていることがわかりました。

$ cat tweets_open.csv | awk -F, '{print $4+$5+$6+$7+$8}' | sort | uniq -c
510683 1
 24191 2
    23 3
     2 4
    64 5

驚くべきことに、すべてのフラグが1となっているデータもあるようです。どんなデータなのか見てみたくなりますね。awkで条件を指定することでその条件に合う行のみを出力できます。確かに全部のフラグが1のデータがあるようです。

$ cat tweets_open.csv | awk -F, '$4+$5+$6+$7+$8==5' | head
10918,10000,522694845756760064,1,1,1,1,1
11701,10000,522938836171517952,1,1,1,1,1
12423,10000,523118581702082560,1,1,1,1,1
14820,10000,523920304305733632,1,1,1,1,1
15601,10000,524165586905870336,1,1,1,1,1
14207,10000,523747907841691648,1,1,1,1,1
27933,10000,527087891885748224,1,1,1,1,1
449480,10000,659307551817732096,1,1,1,1,1
448353,10000,658968057612603394,1,1,1,1,1
448894,10000,658778143583371264,1,1,1,1,1

ちなみに、フラグのうち3つが1となっているデータは全部で24件ありますが、フラグの立ち方のバリエーションはいくつかありそうです。

$ cat tweets_open.csv | awk -F, '$4+$5+$6+$7+$8==3' | head
10465,10000,522570390137106433,0,1,0,1,1
12470,10000,523126034321776640,0,0,1,1,1
13386,10000,523451698371518465,0,1,0,1,1
20187,10000,525269988072910849,0,0,1,1,1
450137,10000,659739109246676992,0,1,0,1,1
450568,10000,659698457288966144,0,0,1,1,1
450242,10000,659731347674546177,0,1,1,1,0
490697,10000,671866340626403328,0,1,0,1,1
481310,10000,669218545549443072,0,0,1,1,1
572521,10002,579257766595862528,0,0,1,1,1

ジャンルに関係のあるデータの量を見てみる (awk)
ジャンル毎にポジティブな意見とネガティブな意見がどのくらいあるのか、ということが興味の対象です。早速分析したいと思ったのですが、8列目の情報が気になりました。8列目が1となっているデータはジャンルと無関係ということですので、分析の邪魔になりそうです。そこで、8列目が1となっているデータの件数を数えてみました。
そうすると、282911件がジャンルと無関係ということが分かりました。つまり、ジャンルのポジネガ分析をする上では、本データのうち約53%は利用できないということになりますね。

$ cat tweets_open.csv | awk -F, '$8==1' | wc -l
282911

ジャンルに関係のあるデータだけを抽出する (grep)
ジャンル毎のポジネガ分析をするため、ジャンルに関係のあるデータだけを抽出します。今回の抽出条件は8列目で行末なので、grepコマンドで簡単に実現できます。

$ grep "0$" tweets_open.csv | wc -l
252051

awkコマンドでも同じようにできます。

$ cat tweets_open.csv | awk -F, '$8==0' | wc -l
252051

なお、grepコマンドはオプション'-v'を付けることで、条件に合致しないデータ行を抽出することができます。今回の例では、行の末尾が1以外という条件で処理すればよいことになりますね。

$ grep -v "1$" tweets_open.csv | wc -l
252052

しかし、実行してみると上の2つの実行結果よりも1件多く抽出されてしまいました。はじめの方で調べたとおり、データ最終行8列目の情報が欠損となっているからです。以後の分析では、ひとまず最終行は欠損として分析対象外とします。

初期分析してみる3 (ポジネガ分析)

準備が整いましたので、ジャンル毎のポジネガ分析に進みます。ここまでの分析で、ジャンルに関係のある252051件が分析対象となりました。本来、分析対象データのみを抽出して一度保存しておくのがベターですが、記事のタイトルに沿ってワンライナーで書いてみます。

関係のあるデータでジャンル毎のデータ件数を見てみる (sort, uniq)
絞り込んだデータに対してsort, uniq -c を使って集計してみます。随分件数が減った印象を受けました。

$ grep "1$" tweets_open.csv | cut -d',' -f2 | sort | uniq -c
 18818 10000
 10027 10001
 57611 10002
  4371 10020
 28622 10021
 54459 10022
 35235 10024
 47780 10025
 25988 10026

意見がはっきりしているデータを抽出する (awk)
4列目から7列目のポジネガ情報のうち、両方の意見を含む4列目やニュートラルな7列目が1となっているデータは、不明確な意見と言えそうです。必然的に、5列目と6列目に着目すれば分析ができそうですね。
しかし、5列目と6列目が同時に1となっているデータがあるとやっかいですので、早速しらべてみましょう。awkコマンドを使って5列目と6列目のフラグの様子を見てみました。5列目と6列目の値の合計をとってみて、もし2となるデータがあった場合、ポジネガがはっきりしないデータと言えます。
調べてみると67件ありました。全体と比較してデータの件数は少ないですが注意すべきですね。

$ grep "1$" tweets_open.csv | awk -F, '{print $5+$6}' | sort | uniq -c
279467 0
  3377 1
    67 2

ジャンル毎のポジネガの割合を見る1 (awk)
ジャンル毎にツイートをグループ化し、それぞれポジティブ・ネガティブな反応のあったツイート件数を集計してみます。awkコマンドで複数の連想配列を使い、5列目と6列目が1になっている件数を数え上げました。
実行例では、ジャンルの値、ポジティブ件数、ネガティブ件数、全体件数の順に表示しています。ポジティブな件数がネガティブな件数を上回っているジャンルは10020, 10022, 10025でした。

$ grep "1$" tweets_open.csv \
> | awk -F, '{p[$2]+=$5;n[$2]+=$6;a[$2]++}END{for (i in p)print i,p[i],n[i],a[i]}'
10021 159 312 28622
10002 140 275 57611
10022 168 118 54459
10025 107 101 47780
10026 92 339 25988
10000 225 412 18818
10020 56 15 4371
10001 170 254 10027
10024 216 352 35235

ところで、ジャンルに無関係なツイートを含めて分析してみるとどうなるでしょうか。実行例を見ると、ポジティブな件数がネガティブな件数を上回っているジャンルが10020, 10022, 10024となり、きちんと絞り込んだデータとは異なる結果となりました。やはり、適切なデータ標本を使わないとよくないということですね。

$ cat tweets_open.csv \
> | awk -F, '{p[$2]+=$5;n[$2]+=$6;a[$2]++}END{for (i in p)print i,p[i],n[i],a[i]}'
10021 3443 6074 79733
10002 909 2128 71058
10022 1499 933 68609
10025 949 1084 68468
10026 744 4420 68490
10000 5650 9526 81021
10020 741 311 11664
10001 1996 3060 17492
10024 3881 3482 68428

ジャンル毎のポジネガの割合を見る2 (awk)
上の例ではジャンル毎に5列目と6列目が1になっている件数を数え上げましたが、5列目と6列目が両方1となるデータがありました。これはポジネガがはっきりしないので、集計対象外としたいですね。
awkコマンドで集計するときにif文で条件を付けてみました。具体的には5列目の集計をするときには6列目の値が0であることを条件としています。また、6列名の集計をするときには5列目の値が0であることを条件としています。今回はジャンル毎の件数の集計は省略しました。
結果、ポジティブな件数がネガティブな件数を上回っているジャンルは10020, 10022, 10025のままで変わりませんでした。

$ grep "1$" tweets_open.csv \
> | awk -F, '{if($6==0)p[$2]+=$5;if($5==0)n[$2]+=$6}END{for (i in p)print i,p[i],n[i]}'
10021 146 299
10002 136 271
10022 168 118
10025 103 97
10026 88 335
10000 209 396
10020 55 14
10001 156 240
10024 205 341

ということで、簡単な分析ですがジャンル毎のポジティブ・ネガティブな発言の状況を可視化することができました。

まとめ

今回はサンプルデータを使い、bashでデータの簡単な確認や初期分析をやってみました。ワンライナーということでやや無理のある処理もあったかと思いますが、シェルでもできることが多いです。データをpandasやRのデータフレームに取り込む前に、さっと生データを確認することで思わぬミスを防ぐことができます。

一方、今回ご紹介したワンライナーはさっと実行するには便利なのですが、再現性の面で課題があります。データ分析で前処理を含めた処理の再現性は重要です。「このデータ、どうやって加工したっけ?」と、コードを思い出すのに苦労することになりかねません。
このため、ワンライナーではデータの外観を確認することに留めておくのが安全だと思います。もし、bashで複雑で長めの処理を行う場合は、シェルスクリプトとしてファイルに保存するのがよいでしょう。

以上のようにワンライナーに頼りすぎるのはよくないことですが、それでもこうした処理ができることを知っておくのは有用だと思います。短時間でログを解析しないといけないときや、Linuxの標準コンソールのみで作業をする必要があるときなどに慌てなくて済みますので。

また、今回取り上げたデータは大変興味深いものでした。Suzuki Lab.のみなさまに改めて御礼申し上げます。

最後まで読んでいただきありがとうございました。


この記事が気に入ったらサポートをしてみませんか?