というわけで早速使ってみる。
Windows上での環境構築については前回の記事を参照のこと。
環境
Windows 10(64bit)
CPU: Intel Core i7-7500U
メモリ: 16GB
Bash on Windows(Ubuntu 16.04.4 LTS (Xenial Xerus))
データ
MovieLens ml-20M(Harper and Konstan,2015*)を使う。
映画の評価情報が入っているデータセットである。
同梱のREADMEには
This dataset (ml-20m) describes 5-star rating and free-text tagging activity from [MovieLens](http://movielens.org), a movie recommendation service. It contains 20000263 ratings and 465564 tag applications across 27278 movies. These data were created by 138493 users between January 09, 1995 and March 31, 2015. This dataset was generated on March 31, 2015, and updated on October 17, 2016 to update links.csv and add genome-* files.
(邦訳筆者:)このデータセット(ml-20m)は映画推薦サービスMovieLens上の5段階評価のレイティングおよび自由記述によるタグ付け情報からなる。2,0000,263件の評価、27,278件の映画とそれらにタグ付けされた465,564のタグ情報を含む。データは1995/01/09~2015/03/31の期間、138,493人のユーザーのものを収集した。データセットは2015/03/31に出力され、2016/10/17にアップデートがなされた(links.csvおよびgenome-で始まるファイル群が付加された)
比較的大きめのデータセットで、解凍すると全体で約800MBにもなる。
nysolを試すのにはちょうどよいだろう。
ファイル構成はこんな感じ。
$ ls
genome-scores.csv genome-tags.csv links.csv movies.csv ratings.csv README.txt tags.csv
今回はこのうちmovie.csv(約1.3MB)およびratings.csv(約500MB)を使ってみる。
ちょっと中身を見てみると、moviesは各映画のIDとタイトル、ジャンルといった基本情報、
$ head movies.csv
movieId,title,genres
1,Toy Story (1995),Adventure|Animation|Children|Comedy|Fantasy
2,Jumanji (1995),Adventure|Children|Fantasy
3,Grumpier Old Men (1995),Comedy|Romance
4,Waiting to Exhale (1995),Comedy|Drama|Romance
5,Father of the Bride Part II (1995),Comedy
6,Heat (1995),Action|Crime|Thriller
7,Sabrina (1995),Comedy|Romance
8,Tom and Huck (1995),Adventure|Children
9,Sudden Death (1995),Action
ratingsは評価したユーザのIDと映画のID、評点、および評価した時刻(unix秒)、
$ head ratings.csv
userId,movieId,rating,timestamp
1,2,3.5,1112486027
1,29,3.5,1112484676
1,32,3.5,1112484819
1,47,3.5,1112484727
1,50,3.5,1112484580
1,112,3.5,1094785740
1,151,4.0,1094785734
1,223,4.0,1112485573
1,253,4.0,1112484940
がそれぞれ入っている。
処理
nysolは複数のコマンド群からなる。
このうち基本的な処理をつかさどるのがMCMD(Mコマンド)パッケージである。
Mコマンドというだけあって、すべてのコマンドの頭にmの字がついている(ちなみにMは考案者の苗字にちなんでいるそうだ。)
各コマンドはパラメータiで入力ファイル名、パラメータoで出力ファイル名を指定する。
特に入出力ファイルの指定がなければ標準入出力を使う。
そのためパイプ処理的に次々とつなげて使うことができる。
$ {コマンド1} i={入力ファイル名} | {コマンド2} | ... | {コマンドn} o={出力ファイル名}
もちろん、パイプ処理で普通のunixコマンドに渡すこともできる。
パイプを活用し、並行処理をすることによって、CPUやメモリを効率的に扱える…らしい。
今回はml-20Mデータセットを使いつつ、MCMDを簡単に使ってみる。
mcut: 列を選択する
mcutは行から選択した列を抜き出すコマンド。$ mcut f={項目名1},{項目名2}
※コンマの後には空白を入れない。パラメータfで必要な列を選択する(fは何の略だろう…focus?)。
パイプラインに逐次データを流しこむ都合上、早い段階で必要な項目だけに絞り込んで流すデータ量を減らしたい。
movies内のIDおよびタイトルのみを切り出し、headコマンドに渡して先頭10行だけ出力してみる。
$ time mcut f=movieId,title i=movies.csv | head
movieId,title
1,Toy Story (1995)
2,Jumanji (1995)
3,Grumpier Old Men (1995)
4,Waiting to Exhale (1995)
5,Father of the Bride Part II (1995)
6,Heat (1995)
7,Sabrina (1995)
8,Tom and Huck (1995)
9,Sudden Death (1995)
#END# kgcut f=movieId,title i=movies.csv; IN=27278 OUT=27278; 2018/06/30 12:48:14
real 0m0.130s
user 0m0.000s
sys 0m0.047s
※行頭のtimeは時間計測用のコマンド1MB程度のファイルだと一瞬で処理が終わるのがわかる。
mcount: 行を数える
mcountは行を数えるコマンド。$ mcount a={新しい列名} [k=数える対象の列]
パラメータaで集約した列の名前、パラメータkで数える対象の列を指定する(kはkeyとして、aはas?)。
kを指定しない場合は全部の行を数える。
なお、kで指定していない列の値は不定(適当なデータが選ばれる)なので当てにしない。
ユーザーが映画を評価したデータ(ratings.csv)を用いて、各映画の評価がどれだけ投稿されているかの回数を調べたい。そういうときにmcountが役に立つ。
$ time mcount a=count k=movieId i=ratings.csv | head
,count,movieId%0,rating,timestamp
,496951,4.5,1379744413
,2900510,4.0,843239650
,4115,100,3.0,942233644
,203,1000,4.0,868380324
,348,100003,2.0,1402337250
,1026,100006,2.5,1358891058
,6398,100008,4.0,1358823364
,2217,100010,3.0,1395061487
,50827,100013,2.5,1381595359
#END# kgcount a=count i=ratings.csv k=movieId; IN=20000263 OUT=26744; 2018/06/30 13:25:28
real 0m21.086s
user 0m19.391s
sys 0m4.297s
なんだか表示がおかしい…と思ったら、改行コードが悪さをしていた。
nysolで扱うcsvの改行コードはunix式(LF)のみとのこと。
このデータセットの改行コードはdos式(CRLF)なので、出力時に不具合が生じていたらしい。
そこでnkfなどのツールを用いて改行コードを変えてやる必要がある。
前回構築した環境ではnkfはデフォルトでは入っていなかったのでとりあえずインストールしてやる。
$ sudo apt-get install nkf
$ nkf -Lu ratings.csv > _ratings.csv
$ nkf -Lu movies.csv > _movies.csv
この新しいファイルでやってみると
$ time mcount a=count k=movieId i=_ratings.csv | head
userId,movieId%0,rating,timestamp,count
65891,1,4.0,965331158,49695
31323,10,4.0,1121207588,29005
91696,100,4.0,833287336,4115
21055,1000,4.0,940160814,203
19153,100003,5.0,1364238516,3
73026,100006,2.5,1358891058,1
21398,100008,4.0,1358823364,6
53478,100010,4.0,1394292232,22
134567,100013,3.5,1418289706,5
#END# kgcount a=count i=_ratings.csv k=movieId; IN=20000263 OUT=26744; 2018/06/30 13:27:06
real 0m27.944s
user 0m24.281s
sys 0m5.203s
綺麗に出力された。
さてここでID(movieId)および評価回数(count)以外の列の値は不定なため意味のある値でない。そこでmcutと組み合わせてやれば、
$ time mcut f=movieId i=_ratings.csv | mcount a=count k=movieId | head
#END# kgcut f=movieId i=_ratings.csv; IN=20000263 OUT=20000263; 2018/06/30 16:09:02
movieId%0,count
1,49695
10,29005
100,4115
1000,203
100003,3
100006,1
100008,6
100010,22
100013,5
#END# kgcount a=count k=movieId; IN=20000263 OUT=26744; 2018/06/30 16:09:15
real 0m22.724s
user 0m21.688s
sys 0m4.109s
となり不要な列を削除して表示できる。
mbest: ソートして指定の行数を返す
せっかく集計したので評価回数の多い映画のリストを出力したい。そこでmbestコマンドを使って上位のレコードを取得する。
$ mbest s={ソートする列名}[並べ替えオプション] size={取得する行数}
パラメータsでソートする列名を指定する(sはsortだろうか)、
並べ替えは、デフォルトでは文字列・昇順で行われるが、
項目名の後にオプションをつけることで方式を指定できる。
%r ...文字列・降順
%n ...数値・昇順
%nr ...数値・降順
(n がnumericとして、r はreversed?)
ここでは評価回数について降順(大きい方から順に)でデータを取得したいので、並べ替えオプションには%nrと指定してやる
$ time mcut f=movieId i=_ratings.csv | mcount a=count k=movieId | mbest s=count%nr size=10
#END# kgcut f=movieId i=_ratings.csv; IN=20000263 OUT=20000263; 2018/06/30 16:13:13
#END# kgcount a=count k=movieId; IN=20000263 OUT=26744; 2018/06/30 16:13:25
movieId,count%0nr
296,67310
356,66172
318,63366
593,63299
480,59715
260,54502
110,53769
589,52244
2571,51334
527,50054
#END# kgbest s=count%nr size=10; IN=11 OUT=10; 2018/06/30 16:13:25
real 0m22.447s
user 0m19.797s
sys 0m3.594s
(mbestで出力するサイズを指定できるためheadに渡す必要がなくなった。)mjoin: データを結合する
さて、上位の件数が求まったところで、映画のIDだけではこの映画が何かを知ることはできない。そこでmjoinを使い、映画のIDとタイトルが紐付いたファイル(movies.csv)からタイトルのデータを引っ張ってくることにする。mjoinコマンドはデータを結合するコマンド。
$ (何らかの処理) | mjoin k={統合に使うキー} i={読み込むファイル名}
受け取ったデータをキーをもとに統合する。これを使えば、
$ time mcut f=movieId i=_ratings.csv |mcount a=count k=movieId |mbest s=count%nr size=10|mjoin k=movieId i=_movies.csv
#END# kgcut f=movieId i=_ratings.csv; IN=20000263 OUT=20000263; 2018/06/30 16:23:32
#END# kgcount a=count k=movieId; IN=20000263 OUT=26744; 2018/06/30 16:23:43
#END# kgbest s=count%nr size=10; IN=11 OUT=10; 2018/06/30 16:23:43
movieId%0,title,genres,count
110,Braveheart (1995),Action|Drama|War,53769
2571,"Matrix, The (1999)",Action|Sci-Fi|Thriller,51334
260,Star Wars: Episode IV - A New Hope (1977),Action|Adventure|Sci-Fi,54502
296,Pulp Fiction (1994),Comedy|Crime|Drama|Thriller,67310
318,"Shawshank Redemption, The (1994)",Crime|Drama,63366
356,Forrest Gump (1994),Comedy|Drama|Romance|War,66172
480,Jurassic Park (1993),Action|Adventure|Sci-Fi|Thriller,59715
527,Schindler's List (1993),Drama|War,50054
589,Terminator 2: Judgment Day (1991),Action|Sci-Fi,52244
593,"Silence of the Lambs, The (1991)",Crime|Horror|Thriller,63299
#END# kgjoin i=_movies.csv k=movieId; IN=27278 OUT=10; 2018/06/30 16:23:43
real 0m20.631s
user 0m17.438s
sys 0m3.391s
タイトルを引っ張ってくることができる。
こいつを多少整形してやると、
$ time mcut f=movieId i=_ratings.csv |mcount a=count k=movieId |mbest s=count%nr size=10|mjoin k=movieId i=_movies.csv | mcut f=count,title | msortf f=count%nr
#END# kgcut f=movieId i=_ratings.csv; IN=20000263 OUT=20000263; 2018/06/30 16:34:39
#END# kgcount a=count k=movieId; IN=20000263 OUT=26744; 2018/06/30 16:34:50
#END# kgbest s=count%nr size=10; IN=11 OUT=10; 2018/06/30 16:34:50
#END# kgjoin i=_movies.csv k=movieId; IN=27278 OUT=10; 2018/06/30 16:34:51
#END# kgcut f=count,title; IN=10 OUT=10; 2018/06/30 16:34:51
count%0nr,title
67310,Pulp Fiction (1994)
66172,Forrest Gump (1994)
63366,"Shawshank Redemption, The (1994)"
63299,"Silence of the Lambs, The (1991)"
59715,Jurassic Park (1993)
54502,Star Wars: Episode IV - A New Hope (1977)
53769,Braveheart (1995)
52244,Terminator 2: Judgment Day (1991)
51334,"Matrix, The (1999)"
50054,Schindler's List (1993)
#END# kgsortf f=count%nr; IN=10 OUT=10; 2018/06/30 16:34:51
real 0m21.086s
user 0m17.641s
sys 0m3.766s
これでサイト上で評価回数の多かった映画10件の評価数、ならびにそのタイトルが出力できた。
こんな感じで比較的高速にデータを処理できる。
後記
今回はデモだったので中間処理ファイルを保存せずにいちいちコマンドを書いていたが、ある程度何度も使うような処理結果に関しては途中経過を適宜ファイルに保存しておくとよい。
また、コマンドの処理結果を試すのにいちいち全ファイルを読み込むのも煩雑なので、
色々試したいときは少量の行数を読み込んで作業するといいだろう。
例えばheadコマンドでデータ読み込んでパイプで渡してやるとサクサク動くのでよい。
$ head {ファイル名} | {mコマンド}
ハマった点は、改行コード以外ではmcountコマンド。mcountではソートされた結果に基づいて行をカウントしているので、コマンド実行時にqオプション(自動ソートにしない)をつけると正常にカウントされない。特に警告も出ないので、原因解明に時間がかかった。
今のところWeb上にそんなに情報もないので、利用者が増えて知見が蓄積されていくことを期待したいところ。
引用文献
* F. Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4, Article 19 (December 2015), 19 pages. DOI=<http://dx.doi.org/10.1145/2827872>