Flaskのベストプラクティスの研究

FlaskといえばpythonでさくっとWebアプリをつくれるフレームワークです。

ちょっとしたAPIを公開する分にはすべてを1ファイルに書いてしまって何ら問題ないのですが 複数ファイルに分けたくなるような規模のWebアプリを実装するときにプロジェクト構成がカオスになりがちです。 というか、自分の場合そうなってしまいました..。

そこでプロジェクト構成のベストプラクティス(的なもの)を調べてみました。

参考になった資料

以下がとても参考になりました!

成果物

github.com

今後Flaskアプリを作るときはこれをベースに作っていこうと思います

たどり着いたプロジェクト構成
~/ExampleApp
    |-- manage.py
    |-- config.py
    |-- /app
        |-- __init__.py
        |-- /models
            |-- __init__.py
            |-- entry.py  # model
            |-- user.py   # model
        |-- /views
            |-- __init__.py
            |-- error.py  
            |-- entry.py  # view for model
            |-- user.py   # view for model
        |-- /templates
            |-- layout.html
            |-- /entries
                |-- show_entries.html  # template for view
            |-- /errors
                |-- 400.html
                |-- 404.html
                |-- 500.html
        |-- /static
            |-- style.css
    |-- /tests
        |-- __init__.py
        |-- /models
            |-- test_entry.py
        |-- /views
            |-- test_api.py

ポイント

これさえ守っておけばきれいになると思います。

  • dbインスタンスapp/__init__.py作らない
    • app/models.py もしくは app/models/__init__.py に作る
  • ビューは Blueprint を使う

逆に、このポイントを守らないと次のようになってしまいます。

NGパターン

f:id:sanshonoki:20181108233328p:plain

一見動きそうですが、app/views.py を importしたとき app/views.py 内で参照する appdbインスタンスがこの時点ではまだ存在してないのでエラーになってしまいます。

いちおうOKパターン

f:id:sanshonoki:20181108233758p:plain

動作はします。 ただ、ファイルの最下部で import文を実行する必要があり違和感をぬぐえません。。

推奨パターン

以上を踏まえて推奨パターンを図示します。

f:id:sanshonoki:20181108234016p:plain

import文を慣習通りにファイル冒頭に書くことができ、もちろんエラーもなく動作します。

db が外部のapp/models.py の中に記述されているので app/views.py の最初のimport文でエラーになりません。

また、Blueprintを使っているので app/views.py において appの循環参照が発生しません。 Blueprintを使わず viewの中で app/__init__.pyapp を参照しようとすると(コードで書くとfrom app import app)、循環参照が発生してしまってダメです..

MeCab用のDockerfile

前回の内容の環境構築をDockerfileで作ろうとしてドハマりしました..

mecab dockerfile」とググれば何個も参考になる記事が出てくるので楽勝だろうと踏んで作業開始。

しかし、Dockerfileを作りビルドしたところ、、

opt/mecab-ipadic-neologd/bin/../libexec/make-mecab-ipadic-neologd.sh: 505 行:   759 強制終了            ${MECAB_LIBEXEC_DIR}/mecab-dict-index -f UTF8 -t UTF8
The command '/bin/sh -c git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git   && cd mecab-ipadic-neologd   && ./bin/install-mecab-ipadic-neologd -n -y   && cd ..   && rm -rf mecab-ipadic-neologd' returned a non-zero code: 137

なに?

${MECAB_LIBEXEC_DIR}/mecab-dict-index -f UTF8 -t UTF8 でエラーだと?

全く原因がわからん.. (ググってもヒントがない)

と原因解明に数日をロスしてしまいましたが結論から書くと、

なんと単にdockerのメモリ不足が原因でした

dockerのメモリを2G -> 3Gに増やしたら問題なくビルドできました。。

めでたし

Docker for Mac だと Preferences からメモリサイズ変更できます。 f:id:sanshonoki:20181009224611p:plain

メモリ要件に関しては mecab-ipadic-NEologdのオフィシャルページにちゃんと書いてました。

ちゃんと最初に一読せよということですね

Memory requirements
Required: 1.5GB of RAM
Recommend: 5GB of RAM

とのことです。

Dockerfile

今回の成果物

FROM ubuntu:16.04

RUN apt-get update \
  && apt-get install -y python3 python3-pip git curl wget make xz-utils file sudo unzip \
  && apt-get install -y mecab libmecab-dev mecab-ipadic-utf8 \
  && apt-get install -y language-pack-ja \
  && apt clean \
  && update-locale LANG=ja_JP.UTF-8

# Set locale
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8

# Install mecab-ipadic-NEologd (Docker memory should be enough to compile)
WORKDIR /opt
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
  && cd mecab-ipadic-neologd \
  && ./bin/install-mecab-ipadic-neologd -n -y \
  && cd .. \
  && rm -rf mecab-ipadic-neologd

# Set mecab-ipadic-NEologd as default
RUN sed -i 's/dicdir = \/var\/lib\/mecab\/dic\/debian/dicdir = \/usr\/lib\/mecab\/dic\/mecab-ipadic-neologd/' /etc/mecabrc

# Install python packages
ADD requirements.txt /tmp/requirements.txt
RUN pip3 install -r /tmp/requirements.txt

# Install fonts
RUN wget -O IPAfont00303.zip https://ipafont.ipa.go.jp/old/ipafont/IPAfont00303.php \
  && unzip IPAfont00303.zip \
  && mv IPAfont00303 fonts \
  && rm IPAfont00303.zip

# Add scripts
ENV PYTHONPATH /opt
ADD . .

CMD ["bash"]

Pythonコードの日本語出力で UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: となるエラーにも遭遇しましたが これは locale周りの設定がdockerでできてなかったからでした

キャンプ場レビューからWordCloudを生成

趣味のキャンプに役立つ何かをということで キャンプ場のレビューコメントからWordCloudを生成するというのをやってみました。

やったこと

  1. キャンプ場のレビューを集める
  2. MeCabで単語に分割する
  3. WordCloudを生成する

キャンプ場のレビューを集める

なっぷ というキャンプ場検索・予約サイトのレビューページをスクレイピングしました。 www.nap-camp.com

なっぷ自体は使ってませんがなっぷの運営会社のスペースキーさんが運営しているキャンプ情報サイト、CAMP HACK はよく利用させてもらっています m( )m

スクレイピングPythonでお馴染みのBeautifulSoupを使います。

特に工夫はなくページのhtmlをインスペクタで調べながらゴリゴリ抽出していきます。

MeCabで単語に分割する

辞書はNEologdを使いました。 github.com

MeCab関連でハマった箇所が以下。 2行目の tagger.parse('') がないと UnicodeDecodeError: 'utf-8' codec can't decode byte 0x90 in position 0: invalid start byte というエラーが出てしまいました..

tagger = MeCab.Tagger('-Ochasen')
tagger.parse('')  # https://teratail.com/questions/88592
node = tagger.parseToNode(text)

WordCloudを生成する

よくお世話になっている西丹沢のバウアーハウスジャパンと そのお隣のウェルキャンプ西丹沢のWordCloudを生成し、比べてみます。

WordCloudは以下のライブラリを使って作ります。 github.com

pipでインストールでき、使い方も簡単です。

生成画像

バウアーハウスジャパン

f:id:sanshonoki:20180928225102p:plain

行ったことある人しか分からないかもしれなけど、

一言で言えば、「分かる」

ウェルキャンプ西丹沢

f:id:sanshonoki:20180928225126p:plain

こちらはやや残念な感じになってしまいました。。 隣接しているので雰囲気はなんとなく分かるのですが私の想像以上に満足度は低いようです.. 「狭い」印象は確かにあります

どちらも 「トイレ」の文字が大きく表示されており、キャンプ場の評価として大きなウェイトをもつということなのでしょう。

何だかキャンプ場の雰囲気はいくらか汲み取れてそうです。 思ったより面白かったので関心のある他のキャンプ場でもやってみようと思います。

今回使ったコードはこちらです。 github.com

ワールドカップ ユニフォームカラー

つい先日終了したばかりのワールドカップですがユニフォームの色が気になり、 ロシア大会を含むここ7大会のベスト8のチームのユニフォームの色を調べてみました。

f:id:sanshonoki:20180723225714p:plain

(注) ホームのユニフォームを表示しています。ロシア大会に出場しているチームはロシア大会のを使いまわしているので昔の大会のものは正確でないかも..。また、5位以下の並びは適当です

【各画像の参照元

きっかけ

日本の初戦のコロンビア戦をTV観戦したとき、日本の選手が見にくいなーと思ったのがきっかけです。

日本のユニフォームは「サムライブルー」と言われる爽やかな青色ですが コロンビアのVividな黄色のカラーと比べると明らかに視認性が悪く(芝生の色と明度が近いので)、

あれ? 味方の位置の把握が(瞬間的に相対的に)しにくいんじゃない?

と思った次第です。

色のちから

視認性以外の側面から見てみます。

赤や黄色などの暖色系は 神経の興奮作用があり (程度のこそはあれ)相手の冷静さを失わせることができると考えられます。 また、サポーターは自国の赤や黄色を見て興奮し、熱烈な応援になりそうです。 スペインの闘牛も観客を興奮させるために赤い布を使ってるそうです。 他には、浦和レッズのファンは過激という話もあります。

逆に、青色の寒色系は神経の鎮静作用があり(程度のこそはあれ)相手を冷静させてしまうのではないかと考えられます。 サポーターも冷静になってcoolな応援になってしまいそうです。

白は膨張色なので相手選手に対して自分を大きく見せたりできそうです。また、「長時間見ると目が疲れる」効果もあるそうです。

ということから考えると やはり日本のサムライブルーは不利なんじゃないかと思うわけです。

【参考】

分析?

とりあえず、色ごとにカウントしてみます。

Count
19
赤 + 橙 18
12
7

白赤混合のクロアチアパラグアイは赤としてカウントしました

そもそも赤と白の総数が多いんで上位進出率として統計的に有意とかは全く主張できないですけど予想通りに白と赤+橙、黄が上位に来ました。 青は自分の予想ではもっと少ないかと思っていましたがフランスとイタリアの頑張りによりそこそこありました。 でも、パッと見、目立たないのは目立たないですね。。

ワールドカップ常連国とまでは言えないコスタリカ、ガーナ、韓国、ロシア、パラグアイウクライナセネガルはすべて赤、白、黄なのでひょっとしたら色彩の影響はいくらかあるのかもしれません。

ひとこと

悲願のベスト8進出の確率を少しでも上げるために、次回のカタール大会では日の丸カラーの白と赤を使ったユニフォームにしてはどうでしょうか。。

リモートのJupyter Notebookを自動でブラウザで開く方法

2ヶ月くらい前に「現場で使える! TensorFlow開発入門 Kerasによる深層学習モデル構築手法」という書籍を買ってみたのですが手をつけずに放置してました..。サンプルコードぐらい動かそうということで環境構築をAzure上で行いました。

この本の中では nvidia-docker を使ってコンテナの中でjupyter notebookを起動するので以前にfast.ai用に作ったAzureラッパースクリプトを少し修正しました。

github.com

サンプルプログラムを動かすための環境構築は基本的に翔泳社のサイトからダウンロードできる補足資料のpdfの通りに進めれば何も問題ないです。

が、

↑のラッパースクリプトを使うとjupyter notebookのトークン認証のトークンを毎回手動でコピペして入力する必要がなくなるので毎回の立ち上げのストレスが減ります :-)

個人的にはすごい便利です。

今回はリモートマシンのdockerコンテナで起動しているjupyter notebookのトークンを取得して自動でブラウザで開く方法について書きます。

起動中のjupyter notebookのトークンを取得する

$ jupyter notebook list

で起動しているjupyter notebookのtokenつきのURLが取得できます。

具体的には以下のような出力が得られます。

Currently running servers:
http://localhost:8888/?token=1864c580501c69e7a331707d7901bc2e4263bffd54c70538 :: /notebooks

この出力から "http://localhost:8888/?token=xxx" の部分を抜き出し、localhostをリモートマシンのIPアドレスに置き換えたURLをopenコマンドで開けばokです(Macの場合)。

ちなみに、複数のjupyter notebookインスタンスを起動しているときはhttp://localhost:xxxx/?token=xxx が複数行出力されます。

dockerコンテナでjupyter notebookを動かしているとき

dockerコンテナでjupyter notebookが起動しているときはnvidia-dockerコマンドを使って

$ sudo nvidia-docker exec YOUR_CONTAINER_NAME jupyter notebook list

でjupyter notebookのtokenつきのURLが取得できます。

ここで "YOUR_CONTAINER_NAME" はjuypter notebookを起動しているdockerコンテナの名前です。

本のサンプルの場合だと、

sudo nvidia-docker run --rm --name tfbook -p 80:8888 -p 6006:6006 -v /notebooks:/notebooks tfbook

というようにtfbookというコンテナ名で jupyter notebookを起動しています。 (--nameで指定しているtfbookがコンテナ名で末尾のtfbookはdockerイメージ名)

このコマンドだと明示的にjupyter notebookを起動していませんが 大元のDockerfileの中で定義されています。

CMD ["/run_jupyter.sh", "--allow-root"] # 最終行

この1行があるのでコンテナ生成時にjupyter notebookが起動します。

ちなみに、nvidia-docker runだと空の出力になってしまいます。

nvidia-docker execを使って既存のコンテナ上でjupyter listコマンドを実行することがポイントです。

リモートマシンのjupyter notebookのトークンを取得

リモートマシンで先のコマンドを実行するにはsshコマンドを使います。

最近まで知らなかったのですが(汗)、ssh ユーザー名@リモート端末のIPアドレス 任意のコマンド とするとリモートマシン上で任意のコマンドを実行できます。

なので、今回の場合、以下のようなコマンドになります。

ssh -i ${privateKeyPath} ubuntu@${instanceId} 'sudo nvidia-docker exec tfbook jupyter notebook list'`

あとは このコマンド出力からtokenつきのURLの箇所を抜き出して、localhost をリモートマシンのIPアドレスに置換してブラウザに渡せばokです。

完成形スクリプト

最終的には以下のシェルスクリプトを実行することによってリモートマシン上で起動しているjupyter notebookを自動的にブラウザで開くことができます。

list=`ssh -i ${privateKeyPath} ubuntu@${instanceId} 'sudo nvidia-docker exec tfbook jupyter notebook list'`
url=`echo $list | grep http | sed -e "s/localhost/${instanceId}/g" | sed -e "s/:8888//g" | awk '{print $4}'`
echo $url
open $url

また、リモートマシンに明示的にログインせずにリモートマシン上でjupyter notebookを起動するには、同様にして

ssh -i ${privateKeyPath} -o "StrictHostKeyChecking no" ubuntu@${instanceId} "sudo nvidia-docker run --rm --name tfbook -p 80:8888 -p 6006:6006 -v /notebooks:/notebooks tfbook"

となります。

docker使わない場合は、もっとシンプルに

ssh -i ${privateKeyPath} -o "StrictHostKeyChecking no" ubuntu@${instanceId} "cd /path/to/notebook_dir; jupyter notebook"

です。

変動係数でPOGデータの分析

せっかく集めたデータなので変動係数(Coefficient of Variance)を使って粘って解析してみます。 データは5/3時点で集計したものを引き続き使っています。

変動係数(Coefficient of Variance)とは

定義

CV = 標準偏差 / 平均値

何を意味する?

相対的なばらつきを表します。 平均値が異なる二つの集団のばらつきを比較する場合に用いられます。

具体例

例えば、りんごが5個、いちごが5個ありそれぞれの重さ(g)が以下であったとします

りんご いちご
100 10
100 10
110 15
110 15
100 10

それぞれの標準偏差は次のようになります。

りんごの標準偏差=4.90、いちごの標準偏差=2.45

りんごの標準偏差の方が大きいので一見、りんごの方がバラつきが大きいように見えますがこれはりんごの1個1個の重さがいちごに比べて大きいためであり、

変動係数CVを計算すると

りんごの変動係数= 4.90(標準偏差) / 104(平均値) = 0.047

いちごの変動係数= 2.45(標準偏差) / 12(平均値) = 0.20 >> 0.047

となり、いちごの方がバラつきが大きいことがわかります。

POGのデータで解析

それでは、POGのデータでいろいろ見てみます。

見方としては、変動係数が大きい = リスクが高い ということになりますので 「平均値が高く、変動係数が小さい」ところを狙うのが統計的には良い戦略となります。

牡馬・牝馬

f:id:sanshonoki:20180530215652p:plain

やはりというか牡馬が平均値が高くリスクも低いという結果でした。 素直に牡馬の割合を多くしたほうが良さそうです。

種牡馬

リーディング上位の15頭をプロットしています。

f:id:sanshonoki:20180530215953p:plain

ディープインパクトが断トツなことがわかります。これはもう逆らえないですね。。

ダイワメジャーよりキングカメハメハハーツクライルーラーシップのほうに行きたくなりますが産駒全体としてはダイワメジャーのほうが平均的に稼ぐようです。なるほど、そうなのか。

キンシャサノキセキが健闘していて下位指名で妙味がありそうです。

ロードカナロアオルフェーヴルの新種牡馬は育成ノウハウがたまってくる来年以降、変動係数も下がってくるのではないでしょうか。注目したいです

BMS(母父)

リーディング上位の15頭をプロットしています。

f:id:sanshonoki:20180530220144p:plain

Storm Catキングカメハメハが抜けています。 なお、フォーティナイナー皐月賞馬のエポカドーロ効果です。

記憶に新しいところでは今年のダービー馬 ワグネリアンは母父キングカメハメハ、 2013年のダービー馬 キズナは母父Storm Catでした。 どちらも父はディープインパクト

生産者

リーディング上位20牧場をプロットしています。

f:id:sanshonoki:20180530221208p:plain

(注:エポカドーロ効果で生産者の田上徹さんは表外に突き抜けてプロットされています)

ノーザンファーム社台ファーム白老ファームの社台系はさすがに外せないですが

ケイアイファームはロードホースクラブやダノンとの結びつきがあり、社台系以外で狙うならこれではないでしょうか。

それ以外では、グラフから

も健闘していることが読み取れます。

下位で遊びつつも賞金も狙いたいというときによいかもしれません。

調教師

リーディング上位の40人をプロットしました。

f:id:sanshonoki:20180530223458p:plain

桜花賞馬アーモンドアイを出した国枝厩舎はさすがに変動係数が大きくなっています。 それを考えるとエポカドーロの藤原英厩舎、ダノンプレミアムの中内田厩舎はすごい気がします。

友道厩舎も重賞馬出していてこの変動係数の低さはすごいと思います。

あとはリーディング上位常連の藤沢和、池江厩舎、音無厩舎、角居厩舎、手塚厩舎と若手の木村厩舎ですか (角居と手塚はプロットが重なっています)

藤岡厩舎、南井厩舎、浅見厩舎はデータにすると私のイメージ以上の活躍でした。侮っていてすみません。。

須貝、矢作、石坂厩舎は毎年のように活躍馬を出しているイメージですが0勝に終わる馬も多いようで平均値が低くなっていることがわかりました。

勝ち上がり馬率

その、登録されている3歳馬のうち1勝以上あげている馬の割合です。

藤沢厩舎はすごいですね。 堀厩舎の安定感はイメージ通り。

このデータからは庄野厩舎、高野厩舎もマークしておきたいです。

f:id:sanshonoki:20180530223642p:plain

栗東美浦

f:id:sanshonoki:20180530222105p:plain

素直に栗東の割合を多くしましょう

誕生日

f:id:sanshonoki:20180530221718p:plain

このグラフと次のグラフは縦軸が賞金です。

ある程度以上の活躍する馬は1/1から数えて120日ぐらいが目安でしょうか。 つまり5月生まれはやはり不利なことがわかります。

デビュー時点での馬体重

f:id:sanshonoki:20180530221832p:plain

450-500Kgが良さそうですね。 本当に稼ぐ馬は470-480Kg前後ぐらいでしょうか。

使ったコード

今回の分析で使ったjupyter notebookはこちらになります。 csvファイルもあります。

github.com

「やはり」を連発してしまいましたが、自分で数字出せるとなかなか面白いです。 来年度、またやってみようと思います。(もうちょっと深い分析ができるとよいな)

RandomForestでPOGの賞金予測をする

結論から書けば玉砕でした。。

そりぁそうですよ

全兄弟で同じ厩舎でも一方はG1馬、他方は未勝利馬ということもあるわけですから。

数千サンプルのデータと二十次元程度の特徴量から予測するなんておこがましかったわけです。

いちおうトライしたのでやったことを書いておこうと思います。

データ

先日実装したnetkeibaスクレイパーを使って現3歳馬のデータを抽出しました。

  • 2018/5/3時点での皐月賞後が終わったあとデータ
  • 5962頭から 地方調教師に所属する馬を除いて 全3923頭

他に調教師、生産者、種牡馬、母父、母馬のデータも抽出し、結合して使います

特徴量ベクトル(説明変数)

調教師、生産者、種牡馬、母父、母馬のデータを結合し、 19次元のベクトルを入力として用います。

  • debut_weight:デビュー戦の体重
  • birth_date_from_beginning_of_year:1/1を基準にした誕生日までの日数
  • crop_win_count:母馬の産駒の合計勝利数
  • crop_grade_horse_count:母馬の産駒の重賞馬の合計頭数
  • crop_grade_win_count:母馬の産駒の重賞勝ちの合計数
  • win_count_trainer:調教師の勝利数
  • prize_trainer:調教師の獲得賞金額
  • win_count_breeder:生産者の勝利数
  • prize_breeder:生産者の獲得賞金額
  • win_ratio:種牡馬の勝率
  • earning_index:種牡馬のearning index
  • prize_sire:種牡馬の獲得賞金額
  • win_ratio_bms:母父の勝率
  • earning_index_bms:母父のearning index
  • sex_セ、sex_牝、sex_牡: 性別(sexをone-hot encoding)
  • stable_trainer_栗東、stable_trainer_美浦: 所属(stable_trainerをone-hot encoding)

今年デビュー予定馬のデビュー時体重のデータを得るのは難しいかもしれませんが 大型とか小型とかある程度はPOG本から情報仕入れられるのと予測に役に立ちそうなファクターな気がしたので入れています。

目的変数

もちろん獲得賞金額になりますがlogをとって対数スケールにしました。

使った機械学習アルゴリズム

手っ取り早い RandomForest です。 パラメータチューニングなしで学習させました。

学習データと検証データの比率は 7:3 でランダムに分割しました

学習結果

学習データでの予測結果のグラフです (logスケールなので軸の目盛りは0-10です)

f:id:sanshonoki:20180523223843p:plain

R2スコアは 0.83です

このグラフを見ると淡い期待が。。

競馬で言えば、最後の直線に入り「そのまま、そのまま」と叫ぶのに似た気持ちです

検証データでの予測結果

f:id:sanshonoki:20180523223919p:plain

R2スコアは 0.07 ...

全部正解だったときのスコアが1.0、ランダムに当てずっぽうに答えたときのスコアが0.0なので非常に辛い結果です

心の目で見るとわずかに右上がりの傾向が見えないこともないけど?(強がり)

Feature Importance

夢は潰え終戦しました

が、最後にFeature Importanceを出力し、各特徴量の学習への寄与度を見てみます。

f:id:sanshonoki:20180523225001p:plain

上位8位の特徴量
  1. 調教師の獲得賞金額
  2. 1/1を基準にした誕生日までの日数
  3. 母馬の産駒の合計勝利数
  4. デビュー戦の体重
  5. 母父のearning index
  6. 生産者の獲得賞金額
  7. 種牡馬の勝率
  8. 母父の勝率

これはPOGをやったことある人ならうなずける結果ではないでしょうか

このデータを頭に入れてドラフト会議に臨みたいと思います