Simple gadget life programming diary

Simple gadget life の中の人によるプログラミングメモ

機械学習で競馬・フェブラリーステークスを予想してみた。

f:id:jtaka1012:20190222084740p:plain
何度かチャレンジして、ようやく形らしいものになったので、備忘録的に残しておこうと思います。
機械学習の初心者のやってみた記事です。

1着から5着までの馬が当たりました!(着順通りではないです)

問題の設定

1着から3着までに入りそうな馬を当てるというものです。
過去レースを用いて、3着以内にゴールしたものを答えとして学習し、それを用いて予想を行います。

問題設定時に考えたこと

競馬は出走頭数がレースによりバラバラです。
扱い方次第ですが、欠損値が多く発生します。そこで、タイムのような特性だと線形データとしてうまく扱えなそうだなと思い、単純な分類問題として扱うことにしました。

取得データ

学習に用いるデータはnetkeiba.comさんのデータを用いて学習を行いました。
また、クローリングの際は以下のコードを参考にすすめていきました。 github.com

ライブラリはBeautifulSoupを使います。

今回の取得対象はフェブラリーステークスと同一競馬場、同一距離のコース(東京競馬場、ダート1600m)かつ、1000万円以上のレースを300件分取得します。
このような条件にしたのは、賞金が低いと馬の過去の出走数が低かったりとデータの欠損値が多くなりそうだと推測したためです。(これならデータが割と取れそう) この条件の場合、10年前のレースも対象となってきます。

最終的に取得したデータは以下になります。 このあたりは、元となるプログラムを参考に少し改良したもので、これからも改良していきたい部分です。

①レースデータ

f:id:jtaka1012:20190222073450p:plain 対象レースで取得するデータが赤枠の内容です。 出走した馬と着順を教師データとして使用。斤量、馬場状態については、学習時に使用します。

②レースに出場した馬の過去5戦分の戦績

f:id:jtaka1012:20190222073533p:plain ①を元に各馬の過去5戦分のデータを取得します。

③ジョッキーの該当レースの年の成績

f:id:jtaka1012:20190222073705p:plain こちらも①からジョッキーを割り出し、その年の勝利数を取得していきます。

最終的なデータは、

学習データ:
②+③を各レース毎に1行にしたデータ

教師データ:
①を元に、馬番順に並び替え、1着〜3着の馬番に「1」、残りを「0」としたデータです。

前処理

東京ダート1600Mは最大16頭のレースなので、16頭に満たないときはデータを0埋め。 その他、学習データで欠損値がある場合も0埋めしています。 さらに、各データが0〜1の範囲となるように正規化しています。

MLP

Keras (TensorFlow) を用いて学習を行いました。

MLP実装

ほぼ、こちらの参考記事と同様になります。 傾向をみていると、やはりエポック数が30あたりから過学習をしているように見えたので、30で止めています。

# -*- coding: utf-8 -*-
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import Adam
from keras.layers.normalization import BatchNormalization
from keras.callbacks import EarlyStopping, TensorBoard, ModelCheckpoint
import matplotlib.pyplot as plt
import horse_data  # テストデータ、教師データの取得を行うプログラム
import pandas as pd

TRAIN_FILE = "train.txt"  # 教師データ名
TEST_FILE = "test.txt" # 学習データ名
ELEMENT_COUNT = 2417

# 学習履歴を表示
def plot_history(history):
    print(history.history.keys())

    # 精度の履歴をプロット
    plt.plot(history.history['acc'])
    plt.plot(history.history['val_acc'])
    plt.title('model accuracy')
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.legend(['acc', 'val_acc'], loc='lower right')
    plt.show()

    # 損失の履歴をプロット
    # plt.plot(history.history['loss'])
    # plt.plot(history.history['val_loss'])
    # plt.title('model loss')
    # plt.xlabel('epoch')
    # plt.ylabel('loss')
    # plt.legend(['loss', 'val_loss'], loc='lower right')
    # plt.show()

def pred(X):
    score = list(model.predict(X.reshape(1,ELEMENT_COUNT))[0])
    result = pd.DataFrame([], columns=['num','score'])
    result['score'] = score
    result['num'] = list(range(1,17))
    print(result.sort_values(by='score', ascending=False))

train_horse, train_label = horse_data.get_horse_and_label(TRAIN_FILE)

model = Sequential()
model.add(Dense(ELEMENT_COUNT, activation='relu', input_dim=ELEMENT_COUNT))
model.add(Dropout(0.8))
model.add(BatchNormalization())
model.add(Dense(1280, activation='relu'))
model.add(Dropout(0.5))
model.add(BatchNormalization())
model.add(Dense(16, activation='softmax'))

model.compile(loss='categorical_crossentropy',
                            optimizer=Adam(),
                            metrics=['accuracy'])

model.summary()

# 過学習に対する早期切り上げの設定
early_stopping = EarlyStopping(patience=0, verbose=1)

# バッチサイズ
batch_size = 5

# エポック数の定義
epoches = 30

# 学習
history = model.fit(train_horse, train_label,
                                        epochs=epoches,
                                        batch_size=batch_size,
                                        validation_split=0.1)

# 評価
test_horse, test_label = horse_data.get_horse_and_label(TEST_FILE)
pred(test_horse)

score = model.evaluate(test_horse, test_label, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])


# 学習履歴をプロット
plot_history(history)

出力結果とレース結果

出力結果

    num     score
7     8  0.120262
1     2  0.111644
5     6  0.092470
2     3  0.090559
10   11  0.083663
4     5  0.067724
3     4  0.059051
8     9  0.057948
11   12  0.056728
6     7  0.054262
9    10  0.046802
13   14  0.042381
12   13  0.041633
0     1  0.035678
15   16  0.023597
14   15  0.015599
Test loss: 7.9274187088012695
Test accuracy: 0.0

レース結果

f:id:jtaka1012:20190222075442p:plain

1〜4着までは当たっているので、3連複で4つの馬を買っていれば、当たったことになります。 15、16番の馬は存在していないため、傾向的にも正しそうです。(工夫の余地は多分にありますが。)

まだまだ勉強不足な中での結果で、まぐれ当たりなことは十分理解していますが、それでも結果が出たことはとても嬉しいものでしたし、可能性はあるのだなということがわかりました。 もう少し学習を深めていって、どういうことなら精度がでるのかなどを探ってみたいと思います。

参考にした記事

qiita.com

BacklogのAPIをたたいて、情報を集計してSlackへ通知するプログラムを作ったよ。

マネジメント業務を効率化したくて、BacklogのAPIをたたいて、必要な情報を集計してSlackへ通知するプログラムを作ってみました。
HerokuにPythonのBottleでWebサーバ立てて、定期的にプログラムを動かす感じです。

取得できる情報は以下のようなものです。

課題着手情報

今やるべきことに絞って情報を通知。
未着手、着手中、期限切れをユーザー単位にまとめて通知
アジャイルでいうところの各人がイテレーション内でやるべきタスクが見える感じです。

工数情報

今週、来週、今月、来月の工数情報を予定、実績それぞれ通知
毎週金曜日にはCSVファイルでのサマリ情報も合わせて通知

問い合せ情報

前日の問い合わせ件数、タイトルを通知
毎週月曜日には先週の問い合わせ件数をまとめて通知

Backlogの課題状況のサンプルが以下で、
f:id:jtaka1012:20171112160337p:plain

通知のイメージはこんな感じ。

課題着手情報通知

f:id:jtaka1012:20171112155406p:plain

問い合わせ情報通知

f:id:jtaka1012:20171112155358p:plain

プログラムはgithubにおいてあります。

github.com

READMEにプログラムの構成や役割がありますので、そちらをご参照ください。
Backlogは無料プランを使用してサンプルを作ったので、有料プランで開始日がいれれるともっとちゃんとでるようになります。

pythonの勉強がてらにかいたので、統一感とかはあまりないです。。
TypeHintってどうなのとか、確かめながら書いてたのですが、途中で
「軽めの言語ならない方が書きやすいなーオレオレプログラムだし」
などと考えてしまい、結局使わなくなりました。

生産性をとるのか、複数人での作業のしやすさをとるのかなどは、ケースバイケースですね。
気づけばそこそこの量を書いてました。
よろしければお使いください。

Swift事始め。サンプルプログラムを作ってGitHubへアップロードしてみた。

これまで、自分のアプリ作成(もう少しかかる。。)やサーバサイドの学習をしていた関係上、Swiftに手を出さないままここまで来てしまいました。
アプリ作成のモチベーションアップのためにもこの辺でSwiftにシフトするのもありかなと考えてちょっとGW中に触ってみました。

作った成果物はこちら

github.com

f:id:jtaka1012:20160509071729p:plain
f:id:jtaka1012:20160509071735p:plain

天気予報のAPIを叩いてパースして表示。せっかくなので過去の情報もDBへ保存という内容はともかく技術的には割りと多く求められるタイプのものだと思います。

使用したライブラリは以下の通り(CocoaPodsにて導入)
- Alamofire (通信ライブラリ)
- Realm (DB)

ホントはもっと使いたかったけど、まずは完成させることが最優先なので、この辺で。
今後はRxSwiftも触ってMVVMモデルでもう少しちゃんと書きなおしてみる想定。

とここまではGitHubのREADMEに書いたことのまとめ。
ここからはSwiftを初めて触った感想を少々

文法的にはわりとすぐ慣れそう

iOSフレームワークという点では今までと同じなので、心配はしていなくて最初の難関(?)としては文法的な部分かのかなと。
いろんな言語を参考にされて作られているっぽいので、「みたことあるなぁ」的な感じにはなります。
個人的にどの言語にちかいのかというと、JSのようなJavaのようなPythonのようなみたいな感じです。
触る前はArray,Dictionaryとかの呼び名はPython的かなとおもってましたが、try-catch(do-catch)はJavaだし、メソッドチェーンみてるとJS思い出すしでなんだか不思議な感じです。
慣れるとObjective-Cに戻りたくなくなってきますね、これ。

ビルド遅い

そんなに遅いのって思ってましたが、作ったサンプルでさえ手持ちのMacBookAir(多分2013製)で1分ほどのビルド時間を要することがあります。ビルド時に型推論とかで時間かかってますかね。クラスのインポート行わないので、このタイミングで何かしらするのかなと。
ある意味CPUやメモリに依存する部分なので、マシンスペックが上がれば解決する問題なような気がしますが、個人用のPCではそんなに変わらないかな。(サーバで出来るようになれば全然話が変わりそうだけど)

厳密な型チェック

最初はよくわかりませんでした。!とか?とか。なんで怒られるのかと。。
要約仕組みを理解してきて書けるようにはなってきて受け入れられるようにはなりました。
基本文法はイマドキなフレンドリーさがある一方で、型チェックにはうるさいですね。「これJavaが嫌いな人とか無理だろうなぁ」と何となく思ってみたり。個人的には問題なしです。

と個人的なファーストインプレッションでした。
小さなサンプルだけど、設計的にいろいろ考えながらやってるので勉強になる。やっぱり手を動かさないとですね。
設計で考えたところはRxSwift対応した時に書いてみようかなと。
もっと触る頻度を増やしていって綺麗に早く書けるようになっていきたいです。

Mac+Pythonでフォルダ内のmp3ファイルをcafファイルに一括変換するスクリプト

お手軽スクリプトです。

フォルダ内の再帰検索やエラーハンドリングには対応していません。
指定したフォルダ内にすべてmp3ファイルが存在する前提です。

pathの部分にmp3ファイルを置いたフォルダの場所を指定してください。

すこしハマった所としてはglobの記述方法でしょうか。
他のサイトのマネでは動かなかった。。

環境はMac(El Capitan)+python 2.7.10 です。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import subprocess
from os.path import join, relpath, splitext
import glob

path = '/Users/ユーザー名/Desktop/sound/'

# ディレクトリから拡張子なしでファイル名を配列取得
files = [relpath(x, path) for x in glob.glob(join(path, '*'))]

# 配列分処理を繰り返す
for filename in files:
    # ファイル名と拡張子を分ける
    name, ext = splitext(filename)
    # ファイル変換コマンドを引数形式で実行
    cmd = "afconvert -f caff -d ima4 {0}.mp3 {1}.caf".format(path+name, path+name)
    # 実行
    subprocess.call(cmd, shell=True)

よろしければどうぞ。

iOSでセクションタップ方式のアコーディオン型TableViewライブラリを作ってみた。

UIKitのUITableViewだとどこかをクリックすると伸び縮みするようなジャバラの構造にはなっていません。 また、 実現するためにはそこそこのコードを書かなければなりません。
幾つかサンプルのコードがあったけれど、セルの1行目をセクションヘッダーに使っていているものがほとんど。コントローラーが汚れがちで直感的に取り扱えるものでもありません。

そこで、セクションヘッダーをタップすると伸び縮みし、標準のTableViewでコードを書くような間隔で使えるアコーディオン型のTableViewライブラリを作ってみました。(車輪の再発明だと思いますがいい勉強になりました。)

f:id:jtaka1012:20150315102023g:plain

コードはgithubに置いています。
SGLAccordionTableView

できること

・UITableViewと同じメソッド名でセクションの数や列の数、セルの生成、ヘッダーの生成が可能。
delegateとdataSourceを別ファイルに切り出すことも可能。(tableDalegate,tableDataSourceにて指定してください。)
・セクションヘッダーが残らずスクロールさせることが可能。
・ヘッダーがタップされた際にヘッダーのView情報と開閉情報を受け取ることが可能。(アイコンを付け替えたいときなどに使えます。)

できないこと (現在未対応。プルリクエストお待ちしております。。)

・編集モードの使用
・タップをヘッダーの一部分のみに限定する
・コードによる開閉操作

上の動画で使っているコントローラーのコードが以下になります。
スッキリでいつもの通りに使えると思います。
また、_tbl_sampleはstoryBoard上でSGLAccordionTableViewを指定しています。

ポイントはtableDalegate,tableDataSourceの部分。
ここでTableViewで使う情報の取り先を指定しています。名前が違うので注意してください。

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // ** tableDataSource設定後に実施 **/
    // 開閉の初期状態を格納
    NSMutableArray *esArray = [NSMutableArray array];
    
    [esArray insertObject:[NSNumber numberWithBool:YES] atIndex:0];
    [esArray insertObject:[NSNumber numberWithBool:NO] atIndex:1];
    [esArray insertObject:[NSNumber numberWithBool:YES] atIndex:2];
    [esArray insertObject:[NSNumber numberWithBool:NO] atIndex:3];
    [esArray insertObject:[NSNumber numberWithBool:YES] atIndex:4];
    [_tbl_sample setExpandStatus:esArray];

    // セクションヘッダーがスクロールするようにセクションヘッダーの最大値を設定
    _tbl_sample.scrollSectionHeaderHeight = 100;
    
    // delegate設定
    _tbl_sample.tableDelegate = self;
    _tbl_sample.tableDataSource = self;
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 5;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 6;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return 40;
}

-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    
    return 50;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{

    UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, _tbl_sample.frame.size.width, 100)];
    label.backgroundColor = [UIColor lightGrayColor];

    label.text = [NSString stringWithFormat:@"Section%ld", section];
    
    return label;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"myCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
    }
    
    cell.textLabel.text = [NSString stringWithFormat:@"Row%ld", indexPath.row];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    
    return cell;
}

-(void)tableViewSection:(NSInteger)section expanded:(BOOL)expanded headerView:(UIView *)view{
    
    NSLog(@"SectionHeader%ld Tapped!",section);
}

- (IBAction)pushedResetButton:(id)sender {
    
    // セクションの開閉状態を取得
    NSMutableArray *array = _tbl_sample.expandStatusArray;
    
    for (NSNumber *n in array) {
        NSLog(@"値は%d",[n boolValue]);
    }
    
}
@end

よろしければどうぞ。

UITableViewの上にヘッダー分の隙間が開くときの対処法

発生するタイミング

StoryboardでViewControllerに対してUITableViewを最初に貼り付けた場合。
ViewControllerのオプションで"Adjust Scroll View Inserts" がオン または "Under Top Bars"のチェックがオフになっている場合。

f:id:jtaka1012:20140522134826j:plain

上記は以前ScrollViewで起こった際のキャプチャ。UITableViewでも同じ事象が発生する。

原因

こちらの記事がくわしい。

やはりお前らのiOS7対応は間違っている(解説編) - Qiita

解決方法

上記の記事のnatsuさんのコメント部分。
他にViewやボタンを貼り付ける場合、TableViewの順番を下げれば事象は発生しなくなる。

忘れた頃にやってしまうのでメモ。

Google-Maps-for-Railsを使ってマップを表示する際、初期表示時の縮尺を変更する方法。

タイトルではrailsのライブラリの名前をあげていますが、javascriptの話です。

Google-Maps-for-Rails
このリンク先のgithubのREADMEに記載されているサンプルの通りにviewに対してコードを記載した場合、マークを1つだけ表示する場合、最大ズームになってしまう。

<div style='width: 600px;'>
  <div id="map" style='width: 600px; height: 400px;'></div>
</div>

<script type="text/javascript">
  handler = Gmaps.build('Google');

  handler.buildMap({
    provider: {
      
    },
    internal: {
      id: 'map'
    }
  },function(){
    markers = handler.addMarkers(<%=raw @hash.to_json %>);
    handler.bounds.extendWith(markers);
    handler.fitMapToBounds();
  });

</script>

オプションはproviderの部分に記載するはずだが、zoomオプションをつけても何ら影響しない。

もう少し調べると、functionの最後に以下の文を加えるとデフォルトの縮尺が変わってくれる。

handler.getMap().setZoom(15);

意外とこの情報が見つからなかったのでメモ。