母馬年齢と産駒の成績との関係

また今年も日本ダービーが近づいてきました。 というわけで、POGのデータ分析を少しやってみました。

今回は母馬年齢と産駒の成績の関係を調べてみました。

調査は以下の2シーズンのデータを使って行います。


やったことは単純に母馬年齢と産駒の獲得賞金をプロットしただけです

結果は以下のようになりました。

f:id:sanshonoki:20190502092211p:plain

f:id:sanshonoki:20190502092139p:plain

母年齢20歳以上になってくるとさすがにちょっと狙いづらくなってきますね

5000万以上を稼ぐ馬は重賞勝ちレベルの馬になってきますが重賞級のボリュームゾーンは母年齢~13歳ぐらいまでになるでしょうか


ちなみに母年齢20歳以上で産駒が5000万円以上稼いだ馬は以下の2頭でした

活躍した産駒が出たので狙いたくなりますがデータ的には狙いづらいですね。。

arXivの論文をチェックするbot

AIの最新の動向を追いたいならarXivをチェックしないわけにはいきません!

しかし、

自分の業界のキーワードにマッチする論文は数ヶ月に1回あるかないか...

毎日チェックするのは面倒

そこでボットを作ってチェックすることにしました

システム構成

システムは以下のような感じです。 今回も?Herokuを使います。無料バンザイです。

f:id:sanshonoki:20190302220149p:plain

arXivにはAPIが用意されているのでrubyスクリプトからarXivAPIを叩きます。

rubyスクリプトをHerokuにデプロイし、スケジューラアドオンを追加し定期実行させます。 通知済みの論文の履歴はDB(postgresql)に保存し、新しい論文だけを通知します。

arXivAPI

arXivAPIが用意されています。 GETだけでのシンプルなものなので使い方は簡単です。 Perl, Ruby, Python, PHPの簡単なサンプルも用意されています。 ただ、レスポンスがJSONではなくXMLなのでパースは若干面倒。

慣れないXMLに少しハマりかけましたがググって解決。

APIを叩いてパースするコアとなるコードはこちら

  def search(keywords, max_results = 3)
    query = build_query(keywords)
    url = URI.parse("http://export.arxiv.org/api/query?search_query=#{query}&start=0&max_results=#{max_results}&sortBy=submittedDate&sortOrder=descending")
    res = Net::HTTP.get_response(url)

    xml = res.body
    doc = REXML::Document.new(xml)

    # https://medium.com/tech-batoora/xml-50488ec69b20
    entries = REXML::XPath.match(doc, '//feed/entry').map do |entry|
      {
        id: entry.elements['id'].text,
        updated: Date.parse(entry.elements['updated'].text),
        published: Date.parse(entry.elements['published'].text),
        title: entry.elements['title'].text,
        summary: entry.elements['summary'].text
      }
    end
    entries
  end

  private

  def build_query(keywords)
    cat = 'all'
    keywords.inject('') do |param, kw|
      if param.empty?
        param = "#{cat}:#{kw}"
      elsif kw.start_with?('+')
        param = "#{param}+OR+#{cat}:#{kw[1..kw.size]}"
      elsif kw.start_with?('!')
        param = "#{param}+ANDNOT+#{cat}:#{kw[1..kw.size]}"
      else
        param = "#{param}+AND+#{cat}:#{kw}"
      end
      param
    end
  end

40行ちょっとのコードです。keywordのANDやORも指定できるので一部対応しました。 (自分の関心のある論文はキーワード1個で十分なので対応してなくもよかったけど)

通知

通知先はとりあえずとしてメールとSlackです。

Slackは前に使ったことのあるslack-notifierのgemを利用させてもらい、WebHook URL経由で送信します。

メールはGmailSMTPサーバーを使って送ります。

テストでGmailから送ってみると Net::SMTPAuthenticationError が出力されてしまいました..。

あれ?以前他のプログラムからメール送信していたときはこんなの出てなかった気がするけど

と思ったけどコードにコメントが残っていて実は対策をしていた。。

今回も同じ対策をします。

Gmail を使って Net::SMTPAuthenticationError が出力される場合の解決法 - 大学生からの Web 開発」の通りにやればok。

Googleアカウントのページにいって、セキュリティの変更をします

  • 二段階認証を設定し、その後アプリ用のパスワードを発行する
  • もしくは、「安全性の低いアプリのアクセスを許可」


1年ぶりのrubyのコーディングで少し手こずりましたが無事稼働するようになりました。 気長に論文を待とうと思います。

と思ってたら、こんな感じでいきなり通知がやってきました!

f:id:sanshonoki:20190301233651p:plain

やったー。想像してた以上に便利でうれしい

今回のコードはこちら。もし興味ある方がいたらのぞいてみてください github.com

今回までやったことなかったのですがHerokuはwebアプリじゃなくてもデプロイできちゃうんですね。 今後もお世話になろうと思います。

Flask + Flask-Scriptでgunicornをマニアックに起動する

HerokuでpythonのWebアプリを動かすとき

Flask-Scriptを使っている場合、

== manager.py ==

from flask import Flask
from flask_script import Manager

app = Flask(__name__)
manager = Manager(app)

if __name__ == "__main__":
    manager.run()

== Procfile ==

web: gunicorn manager:app

と書けば何ら問題なくデプロイできます。

しかし、

Flask-Script を使って開発サーバーを動かすときの

$ python manage.py runserver

のような python manage.py XXX という書式のマニアックな起動をしたいときはどうすればよいのでしょう?

python manage.py XXX 形式でのWebサーバーの起動方法

python - How to use Flask-Script and Gunicorn - Stack Overflow の記事を参考に所望の起動ができました

== manage.py ==

import os
from flask import Flask
from flask_script import Manager, Server, Command, Option

app = Flask(__name__)

class GunicornServer(Command):
    def __init__(self, host='127.0.0.1', port=8000, workers=1):
        self.port = port
        self.host = host
        self.workers = workers

    def get_options(self):
        return (
            Option('-H', '--host',
                   dest='host',
                   default=self.host),

            Option('-p', '--port',
                   dest='port',
                   type=int,
                   default=self.port),

            Option('-w', '--workers',
                   dest='workers',
                   type=int,
                   default=self.workers),
        )

    def __call__(self, app, host, port, workers):

        from gunicorn import version_info

        if version_info < (0, 9, 0):
            from gunicorn.arbiter import Arbiter
            from gunicorn.config import Config
            arbiter = Arbiter(Config({'bind': "%s:%d" % (host, int(port)),'workers': workers}), app)
            arbiter.run()
        else:
            from gunicorn.app.base import Application

            class FlaskApplication(Application):
                def init(self, parser, opts, args):
                    return {
                        'bind': '{0}:{1}'.format(host, port),
                        'workers': workers
                    }

                def load(self):
                    return app

            FlaskApplication().run()


manager = Manager(app)
manager.add_command('gunicorn', GunicornServer(host='0.0.0.0', port=os.environ.get('PORT', 5000)))


if __name__ == "__main__":
    manager.run()

== Procfile ==

python manage.py gunicorn

で無事にHeroku上で python manage.py XXX 書式でWebアプリが動きます。

あと、Herokuにデプロイするときはポート番号を環境変数PORTの値にしておかないといけません (PORTの値はデプロイするたびに変わるので)

とりあえず、マニアックなやり方でWebアプリを起動できましたが自己満足できるということ以外にこちらの方法を取るメリットが今のところ思いつかないです。。

キャンプ場レビューのWordCloud画像の検索サービスを作る

WordCloud画像は以前の記事で作れるようになったので今回、全キャンプ場分の画像を作成し検索できるようにしました。

こんな感じです。

https://campsite-wordcloud.herokuapp.com/

  1. 検索ボックスにキーワードを入力してキャンプ場を検索 f:id:sanshonoki:20190109224710p:plain

  2. 候補が複数あるときは候補を選択する f:id:sanshonoki:20190109224737p:plain

  3. 選択したキャンプ場のWordCloud画像が表示される f:id:sanshonoki:20190121222415p:plain

これだけなので検索サービスというのは少々大げさです。ただ、いい感じのタイトルがすぐに思いつかなかったのでお許しください。。

苦労したところ

苦労したことを一つあげるとすればストップワードの設定です。

ストップワードをどう設定するかによってキャンプ場同士の特徴の違いをうまく浮き出せられるかが変わってきます。

以下の3パターンを試しました

  1. 結果を見ながら除外する単語をストップワードに追加していく
  2. 頻度を算出し、カウント上位とカウント下位の単語をストップワードとする
  3. TF-IDFを計算し、閾値以下をストップワードとする

が、一番良さそうだったのは一番目の人力チューニングでした..。

TF-IDFを使っていい感じになってくれればブログの記事のネタが増えてくれて良かったのですが。。w

ちなみに、最終的なストップワードは以下になりました。

STOP_WORDS = ['あり', 'ある', 'いる', 'する', 'こと', 'それ', 'ない', 'の', 'し', 'さ', 'れ', 'い', 'サイト', 'キャンプ場', '思い', 'キャンプ', 'でき', 'よう', 'とても', 'ところ', '出来', 'なり', 'あっ', 'おり', 'なっ', 'テント', 'キャビン', 'さん', 'なく', 'られ', 'オートキャンプ', 'トイレ', '利用']

キャンプに行くときに利用しつつ、引き続きチューニングをしていこうと思います。

形態素解析(単語への分割)は通常のMeCabを使用しましたがmecab-ipadic-NEologdなど他の辞書を使えば結果もまた変わってくると思います。これも時間があれば試してみたい

Kerasの学習の再現性を担保する

学習のたびに精度(結果)が違ってしまい、後になって資料のためのグラフを作ろうとしたときなどたまに困るので再現性を担保する方法を調べました。

参考になった記事

結論

基本的に1つ目のqiitaの記事に書いてあるとおりでokでした。 自分の実験ではコメントアウトしている2行はコメントアウトした状態でも結果は再現されていました。

import numpy as np
import random
import keras.backend as K
import tensorflow as tf

np.random.seed(seed=0)

# os.environ['PYTHONHASHSEED'] = '0'
# random.seed(0)

session_conf = tf.ConfigProto(
    intra_op_parallelism_threads=1,
    inter_op_parallelism_threads=1
)

tf.set_random_seed(0)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)

後からグラフを作るとき以外にも精度向上を求めて無駄?に同じ学習パラメータでの学習を繰り返すことを強制的に諦めることができ助かりますw

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でできてなかったからでした