OpenShiftにRedmineを導入②

いよいよRedmineを入れる。

OpenShiftのGitHubにインストール用の一式があるのでそれを使う。
openshift-redmine-quickstart

README.mdに書かれていることをそのままトレースするだけの簡単なお仕事。
MySQLで入れるかPostgreSQLで入れるかが最大の悩み。PostgreSQLの方が使い慣れているけど、新しいことにチャレンジしようということでMySQLにしてみる。

C:\>rhc app create redmine ruby-1.9 mysql-5.1

これで一見Redmineが入ったようにログが見えるけれど、表示されたURLにアクセスしてもWelcomeページが表示されるだけ。続いてGitを使ってソースをアップする必要があるよう。

READMEを見るとredmineディレクトリがあるようだがローカルにない。
試しに次のgitコマンドを投げてみる。

$ git remote add upstream -m master git://github.com/openshift/openshift-redmine-quickstart.git
fatal: Not a git repository (or any of the parent directories): .git

やっぱりエラー。
よくよくrhcコマンドのログを見ると、

You do not have git installed, so your application's git repo will not be cloned

と。どうやらgitが入っていないからリポジトリをcloneできなかった様子。
Git for Windowsは入れていなくてSourceTreeでGitを使っているからgitコマンドにパスが通っていないのが原因か。

SourceTreeのターミナルならgitコマンドが使えるので、このターミナル上でアプリケーションのインストールをしてみることにする。
その前に、まずはできてしまった中途半端なredmineを削除する。rhcのヘルプを見ると「app delete xxx」で消せるよう。

$ rhc app delete redmine
This is a non-reversible action! Your application code and data will be
permanently deleted if you continue!

Are you sure you want to delete the application 'redmine'? (yes|no): yes

Deleting application 'redmine' ... deleted

$ rhc apps
No applications. Use 'rhc create-app'.

消えたようなので、気を取り直して。
前回の実行ログからするとカレントディレクトリの直下にredmineのリポジトリがcloneされるようなので、先にリポジトリを作ってもいいパスに移動して置いたほうがよさそう。(リポジトリの場所をきちんと管理したいので)

そこでまずGitのリポジトリ用のディレクトリをローカルに作る。
ここでは「C:\Users\Username\Documents\OpenShift」とする。

次にそのディレクトリに移動してから再度rchコマンドを実行。

$ cd /c/Users/Username/Documents/OpenShift/
$ rhc app create redmine ruby-1.9 mysql-5.1

やっぱりgitがないと言われてしまった。。。
ログをよく見ると最後のところにアプリケーションのgitのURLが書かれていることに気付いた。
仕方ないので、このURLを指定して手動でローカルにcloneをすることにする。

まず、SSHのキー登録が必要。先に登録しておかないとcloneするときに怒られる。
まずはPuTTY用のキーを生成する必要があるよう。SourceTreeの「ツール – SSHキーの作成/インポート」でPuTTYのキージェネレータを起動し、「Conversions – Import key」からOpenShiftのSSHキー(秘密鍵)を読み込む。SSHキーは「rhc setup」をした時に「C:\Users\Username\.ssh\id_rsa」としてできている。
に、「Save private key」で秘密鍵を保存する。元のキーと同じ場所に「id_rsa.ppk」というファイル名で保存。

続いて、PuTTYに上記で保存したキーを登録する。
タスクトレイに「Pageant」というPuTTYのエージェントがいるのでダブルクリックして起動し、「Add key」で先ほどの「id_rsa.ppk」を読み込み登録完了。

いよいよリポジトリのclone。SourceTreeなので「新規/クローンを作成する」から。「元のパス/URL」にログに出ていたURLを、「保存先のパス」に「上記で作ったリポジトリ用のディレクトリ/redmine」を指定して、「クローン」を。まだ何もないのですぐに完了。
(うっかりログを保存せずにコマンドプロンプトを閉じてしまった場合は「rhc apps」で確認できるのでご安心を。)

ようやくGitHubのリポジトリを持ってきてOpenShiftにアップする準備が整った。
気を取り直してopenshift-redmine-quickstartの続きを。ここからは手順にあるコマンドを打つのでGUIではなくSourceTreeのターミナルから。

$ git remote add upstream -m master git://github.com/openshift/openshift-redmine-quickstart.git
$ git pull -s recursive -X theirs upstream master
warning: no common commits
remote: Counting objects: 3341, done.
remote: Total 3341 (delta 0), reused 0 (delta 0), pack-reused 3341
Receiving objects: 100% (3341/3341), 6.04 MiB | 250.00 KiB/s, done.
Resolving deltas: 100% (522/522), done.
From git://github.com/openshift/openshift-redmine-quickstart
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> upstream/master
(省略)
$ git push
Counting objects: 3350, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2701/2701), done.
Writing objects: 100% (3339/3339), 6.04 MiB | 742.00 KiB/s, done.
Total 3339 (delta 529), reused 3327 (delta 522)
(省略)

pushしたらインストールが動くのが不思議。いったいどうなっているのか謎。「.openshift」の中にいろいろスクリプトがあるようなので、これらがgitに更新されたら自動的に動く仕組みなんだろうか。気が向いたらいずれ調べる。

http://redmine-ドメイン.rhcloud.com/にアクセスするとちゃんとRedmineのトップページが表示される。adminのパスワードを変えてとりあえずインストール完了。

OpenShiftにRedmineを導入①

以前にホスティングサーバ上にRedmineを入れようとして失敗し、そのまま放置していたが、やっぱりRedmineが使いたくなったので再調査。

何かいい方法(できればお金をかけず)がないか探していたところ、Redhatが提供するOpenShiftというPaaS上に構築できることが判明。しかも無料。早速試してみる。

まずはOpenShift Onlineのアカウントを作る。

次にOpenShiftをいじくる環境を整備。rhcという独自のクライアントツールを使うようで、これにはrubyとgitが必要らしい。「Getting Started with OpenShift Online」を参考に環境を整える。

まずはRubyのインストール。Ruby Installerを使って最新版(投稿時2.2.2)を入れればOK。

次にGitのインストール。既にインストール済みなので割愛。

続いてRubyGemsを使ってrhcをインストール。

C:\>gem install rhc

続いてrhcのセットアップ。

C:\>rhc setup
C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- dl/import (LoadError)
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh/authentication/pageant.rb:1:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh/authentication/agent/socket.rb:5:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh/authentication/agent.rb:22:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh/authentication/key_manager.rb:4:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh/authentication/session.rb:4:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/net-ssh-2.9.2/lib/net/ssh.rb:11:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/ssh_helpers.rb:18:in `<top (required)>'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/wizard.rb:77:in `<class:Wizard>'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/wizard.rb:7:in `<module:RHC>'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/wizard.rb:6:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands/base.rb:4:in `<top (required)>'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands/account.rb:2:in `<module:Commands>'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands/account.rb:1:in `<top (required)>'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands.rb:189:in `block in load'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands.rb:188:in `each'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/commands.rb:188:in `load'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/lib/rhc/cli.rb:36:in `start'
        from C:/Ruby22/lib/ruby/gems/2.2.0/gems/rhc-1.36.4/bin/rhc:20:in `<top (required)>'
        from C:/Ruby22/bin/rhc:23:in `load'
        from C:/Ruby22/bin/rhc:23:in `<main>'

おーい。。。
調べてみると、どうやらnet-ssh-2.9.2が悪いのかnet-ssh-2.9.3beta1を入れろと書いてある。あるいはrubyを1.9.3にするか。
どちらにしようか悩んだが、beta版を入れるのはあまり好きではないし、Gettng Startedでは1.9.3を入れているようなので1.9.3に入れなおすことにする。

一旦2.2.2をアンインストールして1.9.3をインストール。
その後、rhcのインストールもやり直し。
先ほどは出なかったドキュメントのインストールエラーが出るが、ドキュメントなので放置。

気を取り直してrhcのセットアップを。

C:\>rhc setup
OpenShift Client Tools (RHC) Setup Wizard
(省略)
Your client tools are now configured.

何とか成功したよう。
とりあえず今日はここまで。
次回こそRedmineのインストールを。

はじめてのGitHub

とあるコミュニティでGitHub使いそうなので、遅ればせながらアカウント作ってみた。
とりあえず本番で使う前にちょっと練習をしてみる。

まずは、新しいリポジトリを作成。
自分のアカウントのホーム画面にある「Repositories」タブの右の方にある「New」という緑色のボタンを押す。

「Repository name」にリポジトリ名を入力。今回は練習用なので「GitHub_Practice」とでもしておく。
「Description」は任意なので入れなくてもいいけれど、とりあえず簡単に説明を入れておく。
タイプは無料アカウントなので当然「Public」しか選べない。
READMEや.gitignoreやライセンスはとりあえず作らなくていいのでデフォルトのまま(チェックなし、None)。
最後に「Create Repository」を押せばできあがり。

次に、作ったリポジトリをローカルにcloneする。
解説サイトなどを見ていると、先にローカルにリポジトリを作ってからpushする方法がよく紹介されているが、cloneした方が手っ取り早いような感じがする。(何か不都合があるのかは現時点で不明。とりあえず、この方法でやってみる。)

$ git clone https://github.com/furipon/GitHub_Practice.git
Cloning into 'GitHub_Practice'...
warning: You appear to have cloned an empty repository.
Checking connectivity... done.

ちゃんとできたかステータスを見てみる。

$ cd GitHub_Practice/
$ git status
On branch master

最初のコミット

nothing to commit (create/copy files and use "git add" to track)

ちゃんとできた様子。

お次は適当にファイルを作ってリポジトリに登録。
ファイル作る→add→commit→pushの順。
まずはcommitまで。

$ echo "# GitHub_Practice" > README.md
$ git add README.md 
$ git commit -m "README.mdを追加"
[master (root-commit) 1812759] README.mdを追加
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

これでローカルリポジトリにcommitするところまでできた。
次はpushだが、pushする前にローカルリポジトリの内容でpushしていないものを確認するコマンドで見てみる。

$ git log --branches --not --remotes
commit 1812759eee2e630add3afa964a7cfc6ea6e35caf
Author: ***** <*****@take-f.net>
Date:   Thu May 28 23:06:56 2015 +0900

    README.mdを追加

先ほどcommitした内容が表示された。
続いて、これをpush。

$ git push -u origin master
Username for 'https://github.com': *****
Password for 'https://*****@github.com': 
Counting objects: 3, done.
Writing objects: 100% (3/3), 243 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/furipon/GitHub_Practice.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

無事にpushできた様子。
ブラウザでリポジトリのURLを見ると、ちゃんとREADME.mdができている。

pushするときにUsernameとPasswordを求められたが、解説サイトにはそのような記載はなし。もしかしてSSH鍵を登録しておけばいいのかな?次回試してみることにして、今日はここまで。

livedoorグルメのデータで機械学習してみる⑥

ようやく協調フィルタリングに再挑戦できる状況に。
Pythonで変換した入力ファイルをHDFSにputして「Item-Based Recommendations」を実行。

$ hadoop fs -put ldgourmet_ratings_for_ibr2.csv ldgourmet

$ mahout recommenditembased -s SIMILARITY_LOGLIKELIHOOD -i ldgourmet/ldgourmet_ratings_for_ibr2.csv -o ldgourmet/output --numRecommendations 3
MAHOUT_LOCAL is not set; adding HADOOP_CONF_DIR to classpath.
Running on hadoop, using /usr/lib/hadoop/bin/hadoop and HADOOP_CONF_DIR=/etc/hadoop/conf
MAHOUT-JOB: /usr/lib/mahout/mahout-examples-0.9-cdh5.3.1-job.jar
15/03/17 23:07:52 INFO common.AbstractJob: Command line arguments: {--booleanData=[false], --endPhase=[2147483647], --input=[ldgourmet/ldgourmet_ratings_for_ibr2.csv], --maxPrefsInItemSimilarity=[500], --maxPrefsPerUser=[10], --maxSimilaritiesPerItem=[100], --minPrefsPerUser=[1], --numRecommendations=[3], --output=[ldgourmet/output], --similarityClassname=[SIMILARITY_LOGLIKELIHOOD], --startPhase=[0], --tempDir=[temp]}
15/03/17 23:07:52 INFO common.AbstractJob: Command line arguments: {--booleanData=[false], --endPhase=[2147483647], --input=[ldgourmet/ldgourmet_ratings_for_ibr2.csv], --minPrefsPerUser=[1], --output=[temp/preparePreferenceMatrix], --ratingShift=[0.0], --startPhase=[0], --tempDir=[temp]}
15/03/17 23:07:53 INFO Configuration.deprecation: mapred.input.dir is deprecated. Instead, use mapreduce.input.fileinputformat.inputdir
15/03/17 23:07:53 INFO Configuration.deprecation: mapred.compress.map.output is deprecated. Instead, use mapreduce.map.output.compress
15/03/17 23:07:53 INFO Configuration.deprecation: mapred.output.dir is deprecated. Instead, use mapreduce.output.fileoutputformat.outputdir
15/03/17 23:07:54 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
15/03/17 23:07:55 WARN security.UserGroupInformation: PriviledgedActionException as:take (auth:SIMPLE) cause:org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory temp/preparePreferenceMatrix/itemIDIndex already exists
Exception in thread "main" org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory temp/preparePreferenceMatrix/itemIDIndex already exists
        at org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.checkOutputSpecs(FileOutputFormat.java:146)
        at org.apache.hadoop.mapreduce.JobSubmitter.checkSpecs(JobSubmitter.java:554)
        at org.apache.hadoop.mapreduce.JobSubmitter.submitJobInternal(JobSubmitter.java:430)
        at org.apache.hadoop.mapreduce.Job$10.run(Job.java:1295)
        at org.apache.hadoop.mapreduce.Job$10.run(Job.java:1292)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:415)
        at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1642)
        at org.apache.hadoop.mapreduce.Job.submit(Job.java:1292)
        at org.apache.hadoop.mapreduce.Job.waitForCompletion(Job.java:1313)
        at org.apache.mahout.cf.taste.hadoop.preparation.PreparePreferenceMatrixJob.run(PreparePreferenceMatrixJob.java:77)
        at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70)
        at org.apache.mahout.cf.taste.hadoop.item.RecommenderJob.run(RecommenderJob.java:164)
        at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70)
        at org.apache.mahout.cf.taste.hadoop.item.RecommenderJob.main(RecommenderJob.java:322)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at org.apache.hadoop.util.ProgramDriver$ProgramDescription.invoke(ProgramDriver.java:72)
        at org.apache.hadoop.util.ProgramDriver.run(ProgramDriver.java:145)
        at org.apache.hadoop.util.ProgramDriver.driver(ProgramDriver.java:153)
        at org.apache.mahout.driver.MahoutDriver.main(MahoutDriver.java:195)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at org.apache.hadoop.util.RunJar.main(RunJar.java:212)

あれ、、、またエラーに。
今度は中間データと思われるファイルの出力先が既に存在している、と。
恐らく前回エラーになったときに残ったのだと思われるが、それにしても指定したパス以外に勝手にディレクトリを作っておいて、残したままにしておくとは。もうちょっと後処理ちゃんとせえよ…。
とぼやいていても仕方ないのでディレクトリを消して再度。

$ hadoop fs -rmr temp/preparePreferenceMatrix

$ mahout recommenditembased -s SIMILARITY_LOGLIKELIHOOD -i ldgourmet/ldgourmet_ratings_for_ibr2.csv -o ldgourmet/output --numRecommendations 3
(省略)
15/03/17 23:18:37 INFO driver.MahoutDriver: Program took 347306 ms (Minutes: 5.788433333333334)

MapReduceが9回ほど動いて5分ちょいで無事終了。
出力データの中身を見てみる。

$ hadoop fs -ls ldgourmet/output
Found 2 items
-rw-r--r--   1 take supergroup          0 2015-03-17 23:18 ldgourmet/output/_SUCCESS
-rw-r--r--   1 take supergroup     245633 2015-03-17 23:18 ldgourmet/output/part-r-00000
$ hadoop fs -tail ldgourmet/output/part-r-00000
9923823]
4278205127      [1050:4.0119233,138:4.007087,586:4.007003]
4278971698      [277:1.0,1637:1.0]
4279952571      [6645:2.50514,581:1.0,4244:1.0]
4281265025      [311518:4.0,370162:4.0,332770:4.0]
4281634113      [300933:4.4904943,335499:4.0,373547:4.0]
4281870486      [11824:4.500969,21348:4.488718,3729:4.487594]
4282442439      [2358:4.0,313:4.0]
4282703212      [384403:4.2505198,334805:4.20008,380188:4.20008]
4283170779      [7910:5.0,8796:5.0]
4283926049      [12555:4.4908667]
4284165778      [8728:5.0,2701:5.0,2302:5.0]
4285085889      [300933:5.0,301515:5.0,323026:4.673149]
4288106213      [406041:4.0870314,396687:4.087023,359265:4.083425]
4288708981      [2945:4.504145,4365:4.502466,8206:4.502196]
4289424931      [2297:5.0,11250:4.5070863,20130:4.498961]
4289532598      [308708:5.0,300489:5.0,308715:5.0]
4291952257      [5343:4.0,5433:4.0,2044:4.0]
4291973773      [2855:5.0,891:5.0,3574:5.0]
4292027381      [352331:4.0,374319:4.0,341967:4.0]
4292733388      [3240:4.514431,10312:4.4899635,20040:4.0136404]
4293060625      [4836:5.0,25064:5.0,3074:5.0]
4294950334      [7207:4.494212,6595:4.493965,12624:4.0]

それっぽく出ている感じ。
データはユーザーIDに対しておすすめレストランのIDと点数のセットが3つ(パラメータで3つレコメンドするようにしたので)出ているよう。元の評価データが5点満点だからか見た感じ5点満点で点数が出ている。

ちょっと気になるのは、3つとも満点で出ているユーザーが結構いること。そんなにお勧めなのか、あるいは自分が評価して満点を付けたレストランが出ているのか。

試しに元データを追ってみる。

$ grep 4293060625 ldgourmet_ratings_for_ibr2.csv
4293060625,433,5
4293060625,6839,5
4293060625,302354,5
$ grep 4291973773 ldgourmet_ratings_for_ibr2.csv
4291973773,4243,4
4291973773,6404,5
4291973773,6925,5
4291973773,11719,4
4291973773,13890,5
4291973773,17502,4
4291973773,19616,3

どうやら、自分が評価したのとは違うレストランがちゃんと選ばれている様子。
ほんとに適切なレコメンドがされているのか気になるが、今日はここまで。
次回、レコメンド結果を定性的に評価してみよ。

livedoorグルメのデータで機械学習してみる⑤

データクレンジングにあたり必要な機能のPythonでの実装方法の確認が一通り終わったので組み合わせてデータ変換プログラムを完成させる。

convert_ratings.py

# -*- coding: utf-8 -*-
 
import sys
 
if __name__ == "__main__":
    arg = sys.argv
 
    input_file = arg[1]
    output_file = arg[2]
 
    with open(input_file, 'r') as in_file:
        with open(output_file, 'w') as out_file:
            rows = 0
            for line in in_file:
                rows += 1

                # 先頭行はヘッダなのでスキップ
                if rows == 1:
                    continue

                list = line.split(",")

                restaurant_id = list[1]

                # ユーザーIDは16進->10進変換
                user_id = long(list[2], 16)

                total = list[3]

                out_file.write(str(user_id) + "," + restaurant_id + "," + total + "\n")

                if rows % 1000 == 0:
                    print(str(rows) + "行変換しました。")

            print(str(rows) + "行変換しました。")

まあ、こんな感じで。
では、ようやく変換を。

$ python convert_ratings.py ratings.csv ldgourmet_ratings_for_ibr2.csv
1000行変換しました。
2000行変換しました。
(省略)
205833行変換しました。

$ head ldgourmet_ratings_for_ibr2.csv
3993170538,310595,5
4240577537,10237,1
104934135,3334,2
104934135,15163,5
1290717597,567,3
1290717597,1026,5
1290717597,1058,5
1290717597,2569,3
1290717597,3309,4
1290717597,3648,4

バッチリ。
とりあえず今日はここまでにして、次回いよいよ協調フィルタリングに再挑戦。

カンマ区切り文字列の分割

ファイルの読み書きもできたし、16進を10進に変換できたし、いよいよMahout用データを作るプログラムを書こうと思ったら、カンマ区切りの入力データを分割する方法が未確認だということが発覚。調べて試してみる。

comma_split.py

# -*- coding: utf-8 -*-
 
if __name__ == "__main__":
    comma_del_str = "aaa,bbb,ccc,ddd"

    list = comma_del_str.split(",")

    for column in list:
        print(column)

文字列型のオブジェクトに対してsplit(“区切り文字”)するだけだった。

$ python comma_split.py
aaa
bbb
ccc
ddd

16進表記の文字列を数値に変換

いよいよメインイベント。
16進表記されたユーザーIDを数値型に変換する。

ちょっとGoogle先生に聞いてみたところ、なんとlong(“文字列”, 基数)だけでOKっぽい。
とりあえず対話型のインタプリタで試してみる。

>>> print(long("ee02f26a", 16))
3993170538
>>> print(long("fcc21401", 16))
4240577537

できちゃった。恐るべしPython。
メインイベントなのにHello Worldより短い。(笑)

Pythonでファイル入出力

続いてファイルの読み込みと書き出し。
とりあえずは入力データをそのまま書き出してみる。

fileio_sample.py

# -*- coding: utf-8 -*-

import sys

if __name__ == "__main__":
    arg = sys.argv

    input_file = arg[1]
    output_file = arg[2]

    with open(input_file, 'r') as in_file:
        with open(output_file, 'w') as out_file:
            for line in in_file:
                out_file.write(line)

ファイルを開くにはopen()という関数を使えばいいらしい。引数はいくつかあって、今回は「ファイル名,読み書きモード」というパターンを使用。ファイル名だけならモードが’r’扱いになるよう。あと、3番目にエンコーディングも指定可能。

11行目と12行目のwith文は説明が難しいけど初期処理とか終了処理とかを自動的にやってくれるものらしい。どんな初期処理や終了処理が動くかは関数によって違い、例えばファイルを開くopen関数の場合だと、withを抜ける時に終了処理として自動的にcloseしてくれるよう。

今回は1行読みながら1行書くので入力と出力の2つのファイルを同時にopenする。順番はどちらが先でもいいと思うが、何となく読む方を先に。

では動かしてみる。
livedoorグルメのデータはサイズが大きいので、テスト用に抜粋して使用。

$ head ratings.csv > test_input.csv
$ python fileio_sample.py test_input.csv test_output.csv
$ cat test_output.csv
id,restaurant_id,user_id,total,food,service,atmosphere,cost_performance,title,body,purpose,created_on
156445,310595,ee02f26a,5,0,0,0,0,,"名前は忘れましたが、札幌で食べたお店よりも、全然こっちの方が美味 しかったので、載せました。お店も綺麗(新規オープン・・)でランチは結構混んでいます。個人的にはゆったりと食事できるので夜の方がオススメです。  辛さが0倍から50倍まで選べるのもGOOD!、スープも2種類みたいで、友達は黄色がオススメと言っていましたが、自分は赤の方を食べました。かなり美味しかったです。店長も好感のもてるお兄さんでした。  駅近くなので一度お試しあれです!",0,"2006-10-07 05:06:09"
3842,10237,fcc21401,1,0,0,0,0,,"味的には別に取り立てておいしいと言うこともない。けどまずくもない。  ギリシア料理といもの自体があまり特徴がないということもあると思う。  でも★1を付けてしまったのはサービスの悪さから。  夜に行ったのですけど、何の説明もなくパンを出された。  本当はおかわり自由らしいけど、それについても何も言ってくれなかったので  当然こちらは催促しないし、「パンのおかわりいかがですか?」の一言もなかった。  女二人で料理3品とドリンク1つづつで¥7000ってかなり高いんじゃない?  場所はいいけど、かなり後悔。せめて笑顔と最低限の説明は欲しいところ。",0,"2004-10-20 00:34:28"
144379,3334,06412af7,2,0,0,0,0,,"意外と昔からあるお店で  近江牛、特選黒毛和牛など高級肉を扱ってるお 肉屋さん  ランチも、きっとおいしいと思い駐車場もあるので行ってみました  ステーキ焼ランチ1050円。とてもリーズナブル  炭火かなと思ったら、ガスでした    でてきたランチのお肉は、ロース肉  サシが入ってなく、見た目オージービーフっぽい  食べてみたら、うーんこれはやっぱ。。。  外国産のお肉みたいな味がする  高くておいしいのは当たり前  1000円ランチといえども肉の質は落として欲しくないな~と思った    他にもお客さん(サラリーマン系)が来てたけど  ランチでも、和牛サーロインステーキとか高い値段のを注文してました  ↑  焼き方を聞いていたので、和牛サーロインステーキ等の場合は  焼いてもらえるみたいですね    ",0,"2006-06-03 16:07:43"
144377,15163,06412af7,5,0,0,0,0,,"ランチがおすすめです  お肉自体の旨み、そしてあまみが  お口にじゅわぁ~と広がります。  あのお値段で、ここまでおいしかったお店は初めてです  駐車場は30台くらいとめられて  周りに別のお店はナイので、近くに看板もあるし場所もわかりやすい",0,"2006-06-03 15:14:45"
75967,567,4ceec99d,3,0,0,0,0,,"おいしいと言う友達のすすめでケーキを2種類、プリン系を2種類買ってみ ました。  ケーキはちょっと当たりハズレがあるのかな?  二つ買ったうちの一つ(苺がのったケーキ)はおいしかった。    プリンはクリームブリュレ風のと洋ナシがのったプリン。  両方とも超美味しい!!プリンはリピーター間違いなし。",0,"2004-12-01 23:12:29"
104898,1026,4ceec99d,5,0,0,0,0,,"ランチに利用しました。1,000円のカレー2種+ナンのセット。飲み物付   カレーがのったお皿にミニサラダとチキンも付いてました。    チキンカレーと野菜カレーを頼みました。カレーはスパイスが効いています。  でもだいぶ日本人向けに作ってあるように感じます。  私が頼んだチキン、野菜カレーはさらさらのタイプのカレーでは無く、少しとろみがあるタイプでした。  ナンが大きくて最初は食べきれるかな?と思ったのですが、全然いらぬ心配でした。  パリパリで、中はふっくらなナンはおかわりしたいぐらい美味しかったです。  夜のメニューを見せてもらったら、カレーはかなり種類があるみたいです。  激辛からマイルドと種類も豊富なので、辛いもの苦手な方も行ってみる価値ありです。",0,"2005-01-04 03:57:02"
86073,1058,4ceec99d,5,0,0,0,0,,"神楽坂付近に行く時は必ず寄る店です。  私はいつも抹茶ババロアを食べます。  甘さ控えめなババロア、上品な味の餡、濃厚な生クリーム  この3つがとってもいいバランス!!    注文してから待っている間に出るおせんべいも美味しい。  お茶をすすりながら待っている時間も堪能できる。  いつ行っても混んでいる理由は分かります。純粋においしいですもんね。    納得のお値段なんだけど‥  ただ持ち帰りにすると少し割高感を感じるんですよね。",0,"2004-11-09 00:34:17"
13968,2569,4ceec99d,3,0,0,0,0,,"タンタン麺を食べました。味は濃いめでしっかりとした味。  とっても美味しかったです。  お客さんはみんな地元の常連さんが多いと思いました。  店員の方も対応すごくいいです。",0,"2004-09-22 23:29:57"
97833,3309,4ceec99d,4,0,0,0,0,,"土曜日のランチで利用しました。  席が隣の人と近くてちょっとドキドキ。煙草吸う人来たら嫌だな~と。    ランチセットはメインが数種類から選べて、ピラフ、サラダ、デザート、コーヒーで1000円。  今回は+500円増のメインサイコロステーキを頂きました。  マスタードのソースとすごく合っていて美味しい。  連れのお皿からマッシュポテトを頂戴する。少し甘めで美味し~い!!    私が行った時は常に満員状態でした。  ‥にも関わらず、店員の方はよく気のきく感じでしたね。  コーヒーも気兼ねなくおかわりできますし。  また是非行きたいですね。",0,"2005-05-28 23:17:16"

今日のところはこのへんで。

はじめてのPython

Javaのpublic static void main()的なやつすら知らないので、まずはHelloWorldでも。
最終的にそこそこのステップがあるプログラムを作るので対話型ではなくモジュール(Pythonではプログラムを記述したファイルをこう呼ぶらしい)を作って実行。

helloworld.py

# -*- coding: utf-8 -*-

if __name__ == "__main__":
    print "Hello World."

1行目の「# -*- coding: utf-8 -*-」はソースのエンコード指定。上記ソースでは特に非ASCII文字が記述されているわけではないが、日本語を記述するときのためにおまじないのように書いておけばよさそう。

3行目の「if __name__ == “__main__”:」がJavaでいうところのmain()メソッドのようなもの。「__name__」という特殊な変数にはモジュール名(通常はファイル名の.pyの前の部分)が格納されるのだが、コマンドラインから実行された場合のみ「__main__」となり、このifブロックの中はコマンドラインから実行された場合のみ処理されることになる。

上記ソースの場合は他のモジュールにimportされるようなものではなくスクリプトとして実行(コマンドラインから動かすことをそう言うらしい)するためのものなので全ての処理をトップの階層に記述しても動きは同じだが、勉強なので何となく明示的に記述してみた。(mainなしでベタに書いたらbashのシェルスクリプトのようなイメージ。お気軽にやるならそれで全然いいと思う。)

実行してみる。

$ python helloworld.py
Hello World.

次に、コマンドライン引数を受け取る方法を試す。
とりあえず受け取った文字列をそのまま標準出力に表示。

cmdarg_sample.py

# -*- coding: utf-8 -*-

import sys

if __name__ == "__main__":
    arg = sys.argv

    print "input file = " + arg[1]
    print "output file = " + arg[2]

コマンドライン引数はsysというモジュールのargvという配列に格納されるらしい。sysを使うために3行目でimport。Javaの場合Systemクラスなどはimport不要だが、Pythonは何でもimportが必要なのかな。
引数の配列はarg[0]から始めたくなるところだが、arg[0]にはモジュールのファイル名が入ってくるよう。よって実質的にarg[1]から。(確かにpythonコマンドから見たらモジュールファイル名が第一引数か…)

$ python cmdarg_sample.py input.csv output.csv
input file = input.csv
output file = output.csv

とりあえず今日はここまで。

livedoorグルメのデータで機械学習してみる④

Mahoutに読み込ませるデータのユーザーIDをlong型にしなければならない。
Hiveの関数を見てみたが文字列を数値に変換できそうなものがなかったので別の手段を使うことに。
簡単なプログラム書くだけなので何でもいいのだけれど、せっかくなので勉強してみようと思っていたPythonを使ってみる。

開発環境でコマンドを叩いてみると幸いデフォルトでインストール済みのよう。CentOS6のデフォルトで2.6.6とちょっと古めだが、大したことをするわけではないので特に問題はないだろうと思ってバージョンアップはしないまま使う。

さて、必要なプログラムの要件はとりあえず以下のような感じ。
・コマンドライン引数で入力ファイルと出力ファイルのパスを受け取る
・入力ファイルを1行ずつ読み込み、必要な項目を抜き出してレイアウトを整え、出力ファイルに出力する
・ユーザーID(データを見た感じ16進表記の数値っぽい)はJavaのlong型に変換可能な数値にして出力

Python初体験で構文も全く知らないド素人なので一歩ずつ進める。
参考にしたのは↓こちら。(微妙にバージョン違うけど…)

Python 2.7ja1 documentation
Python-ism

続きは別記事にて。

次のページ →