MATLABでボートレースを解析する8番目くらいの記事です。
時間が空いたら、去年に何のプログラムを作ったのかすっかり忘れてしまった。。
とりあえず 2019 年のデータも揃ったし、ボートの選手データでも見る方法をやってみましょう。
みんな大好きモーターボートファン手帳を読み込もう。
蒲郡のタオル売り場とか、児島のガァーコの部屋とかに置いてありますよね。
ウェブサイトにデータもあるので、ダウンロードしてみましょう。
https://www.boatrace.jp/owpc/pc/extra/data/download.html
今やほとんど見かけない LZH ですが、ついでに展開しておきましょう。
MATLABで読み込んでみよう。
たまには Text Analytics Toolbox を使わないで、System コマンドで読んでみよう。
DOS の FIND コマンドを MATLAB から呼べばテキストで取れます。
fname = 'fan1910.txt';
[~,cmdout] = system(['find /c /v "" < ', fname]);
n = str2double(cmdout);
Person = table('Size',[n,44],...
'VariableTypes',{'double','string','string','string','string',...
'string','double','double','double','double',...
'double','string','double','double','double',...
'double','double','double','double','double',...
'double','double','double','double',...
'double','double','double','double',...
'double','double','double','double',...
'double','double','double','double',...
'double','double','double','double',...
'double','double','double','double'},...
'VariableNames',{'NameNo','Name','NameKana','Area','Class',...
'Era','Born','MF','Age','Height',...
'Weight','Blood','WinRate','DoubleWinRate','Win',...
'DoubleWin','NumRun','NumFinal','NumFinalWin','StartTiming',...
'NumEntry1','DWR1','ST1','SR1',...
'NumEntry2','DWR2','ST2','SR2',...
'NumEntry3','DWR3','ST3','SR3',...
'NumEntry4','DWR4','ST4','SR4',...
'NumEntry5','DWR5','ST5','SR5',...
'NumEntry6','DWR6','ST6','SR6'});
fid = fopen(fname);
for x = 1:n
txt = fgetl(fid);
Person{x,1} = str2double(txt(1:4));
Person{x,2} = erase(string(txt(5:12)),' ');
Person{x,3} = erase(string(txt(13:27)),' ');
Person{x,4} = string(txt(28:29));
Person{x,5} = string(txt(30:31));
Person{x,6} = string(txt(32));
Person{x,7} = str2double(txt(33:38));
Person{x,8} = str2double(txt(39));
Person{x,9} = str2double(txt(40:41));
Person{x,10} = str2double(txt(42:44));
Person{x,11} = str2double(txt(45:46));
Person{x,12} = string(txt(47:48));
Person{x,13} = str2double(txt(49:52))/100;
Person{x,14} = str2double(txt(53:56))/10;
Person{x,15} = str2double(txt(57:59));
Person{x,16} = str2double(txt(60:62));
Person{x,17} = str2double(txt(63:65));
Person{x,18} = str2double(txt(66:67));
Person{x,19} = str2double(txt(68:69));
Person{x,20} = str2double(txt(70:72))/100;
Person{x,21} = str2double(txt(73:75));
Person{x,22} = str2double(txt(76:79))/10;
Person{x,23} = str2double(txt(80:82))/100;
Person{x,24} = str2double(txt(83:85))/100;
Person{x,25} = str2double(txt(86:88));
Person{x,26} = str2double(txt(89:92))/10;
Person{x,27} = str2double(txt(93:95))/100;
Person{x,28} = str2double(txt(96:98))/100;
Person{x,29} = str2double(txt(99:101));
Person{x,30} = str2double(txt(102:105))/10;
Person{x,31} = str2double(txt(106:108))/100;
Person{x,32} = str2double(txt(109:111))/100;
Person{x,33} = str2double(txt(112:114));
Person{x,34} = str2double(txt(115:118))/10;
Person{x,35} = str2double(txt(119:121))/100;
Person{x,36} = str2double(txt(122:124))/100;
Person{x,37} = str2double(txt(125:127));
Person{x,38} = str2double(txt(128:131))/10;
Person{x,39} = str2double(txt(132:134))/100;
Person{x,40} = str2double(txt(135:137))/100;
Person{x,41} = str2double(txt(138:140));
Person{x,42} = str2double(txt(141:144))/10;
Person{x,43} = str2double(txt(145:147))/100;
Person{x,44} = str2double(txt(148:150))/100;
end
fid = fclose(fid);
・・・面倒なので44項目でやめた。。
多分143項目あったので、あと99項目も同じようにすれば読めます(省略)。
[項目一覧]
https://www.boatrace.jp/owpc/pc/extra/data/layout.html
試しに何か表示して確認してみよう。
血液型
figure(1)
histogram(categorical(Person.Blood))
選手にはA型が多い。(要らない情報)
身長
figure(1)
histogram(categorical(Person.Height))
165cm くらいが多い。(要らない情報・・・)
ファン手帳で勝てたら苦労しないので、別データで解析しよう。
ということで、、
選手データはレース結果から取れそう。あと、ファン手帳は 5月から10月のデータを元に計算してるけど、MATLAB ならその辺は自在ですね。とりあえず 2019年全部でやってみましょう。
ここから下準備3連発。
ダウンロードしよう。
何個か前の記事で実施したダウンロードプログラムなんですが、どこでやったか忘れたので再掲しますね。
スクリプトを実行しておきましょう。
% 自動でごっそりダウンロードシステム
%% こんな感じの URL 先を保存しよう。
% http://www1.mbrace.or.jp/od2/K/201904/k190404.lzh
% http://www1.mbrace.or.jp/od2/B/201904/b190404.lzh
since = "20190101";
to = "20191231";
for ix = datenum(since,'yyyymmdd'):datenum(to,'yyyymmdd')
if exist(['K',datestr(ix,'yymmdd'),'.TXT'],'file') ~= 2
url = "http://www1.mbrace.or.jp/od2/K/"+datestr(ix,'yyyymm')+"/k"+datestr(ix,'yymmdd')+".lzh";
web(url,'-browser')
end
if exist(['B',datestr(ix,'yymmdd'),'.TXT'],'file') ~= 2
url = "http://www1.mbrace.or.jp/od2/B/"+datestr(ix,'yyyymm')+"/b"+datestr(ix,'yymmdd')+".lzh";
web(url,'-browser')
end
end
%% ダウンロードしたもの(lzh)を C:\work\boat に展開
dl = [getenv('USERPROFILE'),'\Downloads\'];
for ix = datenum(since,'yyyymmdd'):datenum(to,'yyyymmdd')
if exist(['K',datestr(ix,'yymmdd'),'.TXT'],'file') ~= 2
zipname = ['k',datestr(ix,'yymmdd'),'.lzh'];
dos(['"C:\Program Files (x86)\Lhaplus\Lhaplus.exe" /o:C:\work\boat ',dl,zipname,' &']);
end
if exist(['B',datestr(ix,'yymmdd'),'.TXT'],'file') ~= 2
zipname = ['b',datestr(ix,'yymmdd'),'.lzh'];
dos(['"C:\Program Files (x86)\Lhaplus\Lhaplus.exe" /o:C:\work\boat ',dl,zipname,' &']);
end
end
Lhaplus が入ってて、ネットワークに繋がってれば自動ダウンロードが動くはず!
ダウンロードしたファイルをMATLABで読み込もう。
これもどっかでやったけど再掲。こんな関数を作って保存しておきましょう。
function T_Result = boat_allresult_scan(since,to)
addpath('C:\work\boat')
tic
%% テーブルを作ろう
T_Result = table;
for n = datenum(since,'YYYYmmDD'):datenum(to,'YYYYmmDD')
%% レース結果の読み込み
fname = ['K',datestr(n,'YYmmDD'),'.TXT'];
fid = fopen(fname);
S = textscan(fid,'%s','delimiter','\n');
C = S{1};
fclose(fid);
%% BGN/ENDを探す
KBGN = find(contains(C,'BGN'));
KEND = find(contains(C,'END'));
%% BGN-END Loop
for BEloop = 1:length(KBGN)
%% BGN-ENDで切り取る
res = C(KBGN(BEloop)+1:KEND(BEloop)-1);
res(cellfun(@isempty,res)) = [];
% 場所
place = res{1}(1:3);
place(place == 12288) = [];
% 開催日
ymd = datestr(n,'YYYYmmDD');
start_idx = startsWith(res,'着');
x = find(start_idx)-1;
end_x = [x(2:end)-1;length(res)];
% ここからレースのループ
for Rloop = 1:length(x)
% レース番号
race = res(x(Rloop):end_x(Rloop));
R = regexp(race{1},'\d+R','match');
R = str2double(R{1}(1:end-1));
for fn = 1:6
fin{fn} = race{fn+3}(1:2);
fin_boatnum{fn} = str2double(race{fn+3}(5));
fin_name{fn} = str2double(race{fn+3}(7:10));
fin_motor{fn} = str2double(race{fn+3}(20:22));
fin_boat{fn} = str2double(race{fn+3}(25:27));
fin_extime{fn} = str2double(race{fn+3}(30:33));
fin_entry{fn} = str2double(race{fn+3}(37));
fin_timing{fn} = str2double(race{fn+3}(42:45));
if isnan(race{fn+3}(51))
fin_time{fn} = NaN;
else
fin_time{fn} = str2double(race{fn+3}(51))*60+str2double(race{fn+3}(53:54))+str2double(race{fn+3}(56))*0.1;
end
end
ix = contains(race,'単勝');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) == 1 % 誰も買ってない場合の特払い70円 (2019/01/02 の 620行目の若松2Rとか)
win = NaN;
win_d = str2double(z{1}{1});
elseif isempty(z{1}) % 2013/01/14 の 児島7R に単勝が書いてない
win = NaN;
win_d = NaN;
else
win = str2double(z{1}{1});
win_d = str2double(z{1}{2});
end
end
ix = contains(race,'複勝');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) == 1 % 特払い70円
show = [NaN NaN];
show_d = [70 70];
elseif length(z{1}) < 4 % 2019/8/1 の 383行目みたいに、複勝なのに1個しか書いてないのが結構ある。
show = [NaN NaN];
show_d = [NaN NaN];
else
show = [str2double(z{1}{1}),str2double(z{1}{3})];
show_d = [str2double(z{1}{2}),str2double(z{1}{4})];
end
end
ix = contains(race,'2連単');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) <= 1 % 不成立
exacta = NaN;
exacta_d = NaN;
elseif length(z{1}) == 2 % 特払い70円
exacta = NaN;
exacta_d = str2double(z{1}{2});
else
exacta = str2double([z{1}{2},z{1}{3}]);
exacta_d = str2double(z{1}{4});
end
end
ix = contains(race,'2連複');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) <= 1 % 不成立
quinella = NaN;
quinella_d = NaN;
elseif length(z{1}) == 2 % 特払い70円
quinella = NaN;
quinella_d = str2double(z{1}{2});
else
quinella = str2double([z{1}{2},z{1}{3}]);
quinella_d = str2double(z{1}{4});
end
end
ix = contains(race,'拡連複');
ix = ix | circshift(ix,1) | circshift(ix,2);
rx = race(ix);
if any(ix)
z = regexp(rx,'\d+','match');
if length(z{1}) <= 1 % 不成立
wide = [NaN NaN NaN];
wide_d = [NaN NaN NaN];
elseif ~all(ismember(rx{2}(1:3),'123456-')) % フォーマットがおかしい (2013/04/04 三国9Rとか)
wide = [NaN NaN NaN];
wide_d = [NaN NaN NaN];
else
wide = [str2double([z{1}{1},z{1}{2}]) str2double([z{2}{1},z{2}{2}]) str2double([z{3}{1},z{3}{2}])];
wide_d = [str2double(z{1}{3}) str2double(z{2}{3}) str2double(z{3}{3})];
end
end
ix = contains(race,'3連単');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) <= 1 % 不成立
trifecta = NaN;
trifecta_d = NaN;
elseif length(z{1}) == 2 % 特払い70円
trifecta = NaN;
trifecta_d = str2double(z{1}{2});
else
trifecta = str2double([z{1}{2},z{1}{3},z{1}{4}]);
trifecta_d = str2double(z{1}{5});
end
end
ix = contains(race,'3連複');
if any(ix)
z = regexp(race(ix),'\d+','match');
if length(z{1}) <= 1 % 不成立
trio = NaN;
trio_d = NaN;
elseif length(z{1}) == 2 % 特払い70円
trio = NaN;
trio_d = str2double(z{1}{2});
else
trio = str2double([z{1}{2},z{1}{3},z{1}{4}]);
trio_d = str2double(z{1}{5});
end
end
T_Result(end+1,:) = {place,ymd,R,...
fin,cell2mat(fin_boatnum),cell2mat(fin_name),cell2mat(fin_motor),cell2mat(fin_boat),cell2mat(fin_extime),cell2mat(fin_entry),cell2mat(fin_timing),...
win,win_d,show,show_d,exacta,exacta_d,quinella,quinella_d,wide,wide_d,trifecta,trifecta_d,trio,trio_d};
end
end
stc = toc;
disp([datestr(n,'yyyymmdd'),': ',num2str(stc),'秒経過'])
end
T_Result.Properties.VariableNames = {'Place','YYYYMMDD','R',...
'fin','fin_boatnum','fin_name','fin_motor','fin_boat','fin_extime','fin_entry','fin_timing',...
'win','win_d','show','show_d','exacta','exacta_d','quinella','quinella_d','wide','wide_d','trifecta','trifecta_d','trio','trio_d'};
% 数種類に分類できる項目はカテゴリカル。
T_Result.win = categorical(T_Result.win);
T_Result.show = categorical(T_Result.show);
T_Result.quinella = categorical(T_Result.quinella);
T_Result.wide = categorical(T_Result.wide);
T_Result.exacta = categorical(T_Result.exacta);
T_Result.trifecta = categorical(T_Result.trifecta);
T_Result.trio = categorical(T_Result.trio);
toc
関数を実行しよう。
とりあえず 2019 年全部でやってみよう。
TR_2019 = boat_allresult_scan("20190101","20191231");
下準備ができたら、解析しよう。
変数は Person と TR_2019 があればいいね。
人が多すぎるので、A1 に絞ろう。
簡単簡単。
A1 = Person(Person.Class == "A1",:)
↓↓↓
A1 =
325×44 table
NameNo Name NameKana Area Class Era Born MF Age Height Weight Blood WinRate DoubleWinRate Win DoubleWin NumRun NumFinal NumFinalWin StartTiming NumEntry1 DWR1 ST1 SR1 NumEntry2 DWR2 ST2 SR2 NumEntry3 DWR3 ST3 SR3 NumEntry4 DWR4 ST4 SR4 NumEntry5 DWR5 ST5 SR5 NumEntry6 DWR6 ST6 SR6
______ ___________ ______________ ______ _____ ___ __________ __ ___ ______ ______ _____ _______ _____________ ___ _________ ______ ________ ___________ ___________ _________ ____ ____ ___ _________ ____ ____ ___ _________ ____ ____ ___ _________ ____ ____ ___ _________ ____ ____ ___ _________ ____ ____ ___
2876 "鈴木幸夫" "スズキユキオ" "愛知" "A1" "S" 3.2081e+05 1 62 163 51 "AB" 6.45 51.4 37 18 107 4 1 0.16 48 66.7 0.14 2.1 57 36.8 0.17 3.4 3 66.7 0.24 3.3 0 0 0 0 0 0 0 0 0 0 0 0
2992 "今村豊" "イマムラユタカ" "山口" "A1" "S" 3.6062e+05 1 58 162 50 "A " 6.67 39.2 20 24 112 2 0 0.14 25 68 0.14 3.4 23 52.2 0.13 3.1 15 33.3 0.14 2.9 22 31.8 0.13 2.5 14 14.3 0.14 3.3 13 7.7 0.12 2.1
3022 "西山昇一" "ニシヤマシヨウイチ" "愛知" "A1" "S" 3.5101e+05 1 59 168 54 "B " 6.53 49.6 37 29 133 5 1 0.16 32 90.6 0.15 3.1 21 61.9 0.16 3.2 20 40 0.15 2.3 27 29.6 0.18 3.6 23 26.1 0.18 3.6 10 20 0.13 3.6
3024 "西島義則" "ニシジマヨシノリ" "広島" "A1" "S" 3.6103e+05 1 58 166 58 "AB" 6.63 50.3 33 39 143 2 0 0.15 52 65.4 0.16 3.2 78 46.2 0.15 3.1 9 22.2 0.18 3.9 1 0 0.21 4 1 0 0.35 5 2 0 0.1 3.5
3070 "山室展弘" "ヤマムロノブヒロ" "岡山" "A1" "S" 3.6061e+05 1 58 168 51 "A " 7.21 58.8 38 25 107 3 1 0.14 26 80.8 0.13 2.2 32 59.4 0.14 3 26 46.2 0.18 2.5 23 43.5 0.13 2.7 2 0 0.12 3 1 100 0.01 1
3159 "江口晃生" "エグチアキオ" "群馬" "A1" "S" 4.0021e+05 1 54 164 54 "AB" 7.48 64 52 37 139 8 3 0.14 49 83.7 0.14 2.8 86 53.5 0.14 2.9 4 50 0.13 2.8 0 0 0 0 0 0 0 0 0 0 0 0
...
...
...
この人が出るだけで配当が荒れたA1選手ランキング2019
この人が出たら、その人が勝っても勝たなくても配当が荒れる人を探してみよう。こんな感じのスクリプトですね。
A1 = Person(Person.Class == "A1",:); % 325人いるよ。
m = zeros(height(A1),1);
d = m;
for n = 1:height(A1)
idx = any(ismember(TR_2019.fin_name,A1.NameNo(n)),2);
m(n) = nanmean(TR_2019.trifecta_d(idx));
d(n) = nanstd(TR_2019.trifecta_d(idx));
end
figure(1)
bar(A1.NameNo,m)
横軸が選手番号で縦軸が3連単の平均配当ですけど見にくい。。
配当が高い順に並び替えて、横軸を名前にしよう。
[ms,idx] = sort(m);
disp([A1.NameNo(idx), ms])
figure(1)
barh(ms);
yticks(1:325);
yticklabels(A1.Name(idx))
300人もいると、どうしても見にくい。。。TOP20 くらいにしよう。
figure(1)
barh(ms(end-19:end));
yticks(1:20);
yticklabels(A1.Name(idx(end-19:end)))
石川選手が出るレースの平均配当が10000円超え!
なんか分かる気がしますね。
低い方はどうだろう。安い方から20件を見てみよう。
figure(1)
barh(ms(1:20));
yticks(1:20);
yticklabels(A1.Name(idx(1:20)))
特に中谷選手の出るレースは荒れない!順当!ということですね。
石川選手 3473 を追ってみよう(プログラムで)
ベテランのイン屋で、特殊な石川ペラでスーパーピット離れ!の選手なので、スタートから荒れそうっていうのはなんとなくわかりますね。(先入観?)
2019年の着順
idx = any(ismember(TR_2019.fin_name,3473),2) ;
P_3473 = TR_2019(idx,:);
figure(1)
histogram(categorical(P_3473.fin(P_3473.fin_name == 3473))); % 着順
1着多め。F はフライング、K1 は選手責任の事前欠場、S1 は選手責任の失格です。
12000円以上ついたレースのどこに3473がいたか。
12000円は全通り買っても浮く金額で。
idx = P_3473.trifecta_d > 12000;
sum(P_3473(idx,:).fin_name == 3473)
↓こんな結果。(左から順に1,2,3,4,5,6着の回数)
ans =
10 10 11 13 9 11
まんべんなくいる。
3473 がインを取りに行った割合 (枠番 > 進入)
idx = P_3473.frame(P_3473.fin_name == 3473) > P_3473.fin_entry(P_3473.fin_name == 3473);
innum = nnz(idx)
inper = nnz(idx)/numel(idx)
↓↓↓
innum =
160
inper =
0.5735
57% くらいインに行ってますね。
3473 がインを取りに行ったレースの平均配当と、取らなかった時の平均配当
インに行ったとき
idx = P_3473.frame(P_3473.fin_name == 3473) > P_3473.fin_entry(P_3473.fin_name == 3473);
mean(P_3473(idx,:).trifecta_d)
↓↓↓
ans =
12477.63
125倍くらい。
インに行かなかったり行けなかったりしたとき
idx = [P_3473.frame(P_3473.fin_name == 3473) <= P_3473.fin_entry(P_3473.fin_name == 3473)];
mean(P_3473(idx,:).trifecta_d)
↓↓↓
ans =
7565.59
75倍。
3473 が、いくつインを取ったら配当が高いのか。
pidx = P_3473.fin_name' == 3473;
frame = P_3473.frame';
entry = P_3473.fin_entry';
mn = [];
for n = 1:5
idx = frame(pidx) == (entry(pidx) + n); % 枠 == 進入+(1|2|3|4|5) のケース
mn(n) = mean(P_3473(idx,:).trifecta_d);
end
figure(1)
bar(1:5,mn)
1つか4つ。
3473 がインを4つ取ったときの順位。
1つだけインを取ったときは配当が高いけど、あんまり特徴がなかったので省略して4つ前に行ったときを見てみましょう。
P_3473_in4 = P_3473(frame(pidx) == entry(pidx)+4,:);
sum(P_3473_in4.fin_name == 3473)
↓↓↓(左から順に1,2,3,4,5,6着の回数)
ans =
11 5 4 2 0 2
3着以内には入ることがほとんど。
インを4つ取った場合の、各着順の配当を個別に見てみる。
5着はなかったので省略。
pidx = P_3473_in4.fin_name' == 3473;
fin = P_3473_in4.fin';
X1 = P_3473_in4.trifecta_d(fin(pidx) == "01")
X2 = P_3473_in4.trifecta_d(fin(pidx) == "02")
X3 = P_3473_in4.trifecta_d(fin(pidx) == "03")
X4 = P_3473_in4.trifecta_d(fin(pidx) == "04")
X6 = P_3473_in4.trifecta_d(fin(pidx) == "06")
↓↓↓
X1 =
5300.00
11650.00
950.00
2540.00
3760.00
1750.00
9540.00
13140.00
1370.00
2680.00
53210.00
X2 =
2290.00
2320.00
72440.00
7060.00
2020.00
X3 =
8020.00
47370.00
750.00
1570.00
X4 =
25990.00
1200.00
X6 =
14300.00
32150.00
4, 6着が意外に高いのと、1,2,3着でも時々とんでもない倍率が出てる。
ページが縦に超長くなったので、とりあえずここまで。。
選手データをMATLABで見る方法がわかりました!スタ展で石川選手が4つ前に行きそうだったらチャンスと見ていいのかな。
とりあえず今回は選手をベースに簡単な解析をするやり方をやってみましたが、MATLAB は自分のアイデアを数行で書けるので、みなさんもやってみてくださいね。
次は何にしようかな。