はじめに
私は競馬予想プログラムソフトの制作過程を動画で投稿している者です。
ここでは、モデル分析用のWEBアプリの開発手順を話していきます。
現在作成している競馬予想プログラムソフトの概要は以下を参照ください。
本プログラムの前提
本プログラムでは以下の前提を置いています。
- Windows 10
- Python3.10.5
- Django ver5.0.4
- 競馬予想プログラムで作成したモデルを分析する目的で使います。
- Bookersで公開中のモデル分析管理クラスと連携してモデル分析を行います。
とりわけ、最後の2つに関しては有料記事でソースを公開しているため、まったく同じ環境で競馬予想プログラムの作成とモデルの分析を行いたい場合は以下のBookers記事一覧から記事を購入ください。
また、競馬予想プログラムの制作過程については大まかな概要は以下の再生リストから参照ください。
詳細な解説は以下の記事一覧を参照ください。
モデル分析画面:モデル情報一覧画面作成
今回はモデル分析画面の2つ目の機能であるモデル情報一覧画面の作成に入ります。
選択したモデルの「収支グラフ」「回収率と的中率」「人気別ベット回数」の概要を確認でき、モデルのメタ情報を確認できる、そういった画面を作ります。
以下に完成イメージGIFをお見せします。
完成イメージ(GIF)
モデルを選択後、モデル情報一覧確認画面の表示ボタンをクリックします。この画面ではCSVファイルの読み込みをしますが、重たい処理をしていないのでそれなりに表示が早いです。
モデルサマリ画面の表示
3つモデルを選択すると、Target ModelsとBase Modelとブロックが分かれて、モデルのサマリ画面が表示されます。
サマリ画面には、モデルの全期間の月ごとの収支グラフ、全期間の人気別ベット回数、全期間の回収率と的中率をそれぞれ出しています。
モデル情報画面の表示
こちらはモデル管理画面のモデル一覧画面から閲覧できる「モデル詳細」で表示されるモーダルの内容を持ってきました。
あのモーダル画面はいくらかよくできていたので、使いまわしです。
フォルダ構成
今回の実装でもフォルダ構成を変えています。
前回記事のフォルダ構成をベースに追加箇所を赤字にしています。注意して変更してください。
<any-dir>
┣ <app_keiba>
┃ ┣ <app_keiba>
┃ ┃ ┣ __init__.py
┃ ┃ ┣ settings.py
┃ ┃ ┣ urls.py
┃ ┃ ┣ asgi.py
┃ ┃ ┗ wsgi.py
┃ ┣ <model_analyzer>
┃ ┃ ┣ <migrations>
┃ ┃ ┃ ┗ __init__.py
┃ ┃ ┣ <tools>
┃ ┃ ┃ ┣ <form_control>
┃ ┃ ┃ ┃ ┗ model_manage_forms.py
┃ ┃ ┃ ┗ <model_control>
┃ ┃ ┃ ┣ model_analyze_models.py
┃ ┃ ┃ ┗ model_manage_models.py
┃ ┃ ┣ <main_viewer>
┃ ┃ ┃ ┣ <analyze_viewr>
┃ ┃ ┃ ┃ ┣ <base_analyze_page>
┃ ┃ ┃ ┃ ┃ ┣ fav_bet_num_chart.py
┃ ┃ ┃ ┃ ┃ ┣ profit_loss_graph.py
┃ ┃ ┃ ┃ ┃ ┣ return_hit_rate_table.py
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ <model_info_list_page>
┃ ┃ ┃ ┃ ┃ ┣ const.py
┃ ┃ ┃ ┃ ┃ ┣ model_item_manager.py
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ <ogs_page>
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ base_analyzer_dto.py
┃ ┃ ┃ ┃ ┗ analyze_viewer_operator.py
┃ ┃ ┃ ┗ views.py
┃ ┃ ┣ __init__.py
┃ ┃ ┣ admin.py
┃ ┃ ┣ apps.py
┃ ┃ ┣ models.py
┃ ┃ ┣ test.py
┃ ┃ ┣ urls.py
┃ ┃ ┗ views.py
┃ ┣ <static>
┃ ┃ ┣ <css>
┃ ┃ ┃ ┣ <model_analyze>
┃ ┃ ┃ ┃ ┗ style.css
┃ ┃ ┃ ┣ <model_manage>
┃ ┃ ┃ ┃ ┗ style.css
┃ ┃ ┃ ┗ base.css
┃ ┃ ┣ <images>
┃ ┃ ┃ ┣ caret-left-square.svg
┃ ┃ ┃ ┣ caret-right-square.svg
┃ ┃ ┃ ┣ favicon.png
┃ ┃ ┃ ┗ file-bar-graph-fill.svg
┃ ┃ ┗ < js >
┃ ┃ ┣ <base_layout>
┃ ┃ ┃ ┗ fade_popup.js
┃ ┃ ┣ <model_analyze>
┃ ┃ ┃ ┣ check_submit_mode.js
┃ ┃ ┃ ┣ sidebar_slide.js
┃ ┃ ┃ ┗ validate_form.js
┃ ┃ ┗ <model_manage>
┃ ┃ ┗ update_form_initial.js
┃ ┣ <templates>
┃ ┃ ┣ <model_analyze>
┃ ┃ ┃ ┣ <analyze_pages>
┃ ┃ ┃ ┃ ┣ <scripts>
┃ ┃ ┃ ┃ ┃ ┣ <base_analyze>
┃ ┃ ┃ ┃ ┃ ┃ ┣ delete_graph.html ※ バグ対応
┃ ┃ ┃ ┃ ┃ ┃ ┣ fav_bet_num_chart.html
┃ ┃ ┃ ┃ ┃ ┃ ┣ graph_controller.html
┃ ┃ ┃ ┃ ┃ ┃ ┣ profit_loss_graph.html
┃ ┃ ┃ ┃ ┃ ┃ ┣ rtn_hit_rate_table.html
┃ ┃ ┃ ┃ ┃ ┃ ┗ table_controller.html
┃ ┃ ┃ ┃ ┃ ┗ <model_info_list>
┃ ┃ ┃ ┃ ┃ ┣ <card_items>
┃ ┃ ┃ ┃ ┃ ┃ ┣ summary_fav_bet_num.html
┃ ┃ ┃ ┃ ┃ ┃ ┣ summary_item.html
┃ ┃ ┃ ┃ ┃ ┃ ┣ summary_profit_loss_graph.html
┃ ┃ ┃ ┃ ┃ ┃ ┗ summary_rtn_hit_table.html
┃ ┃ ┃ ┃ ┃ ┣ model_card_item.html
┃ ┃ ┃ ┃ ┃ ┗ scroll_models.html
┃ ┃ ┃ ┃ ┣ base_analyze_page.html
┃ ┃ ┃ ┃ ┣ model_info_list_page.html
┃ ┃ ┃ ┃ ┗ ogs_page.html
┃ ┃ ┃ ┣ <parts>
┃ ┃ ┃ ┃ ┣ <accordion_items>
┃ ┃ ┃ ┃ ┃ ┣ base_analyze.html
┃ ┃ ┃ ┃ ┃ ┣ display_model_info.html
┃ ┃ ┃ ┃ ┃ ┗ display_OGS.html
┃ ┃ ┃ ┃ ┣ offcanvas_analyze_list.html
┃ ┃ ┃ ┃ ┗ select_models.html
┃ ┃ ┃ ┗ model_manage.html
┃ ┃ ┣ <model_manage>
┃ ┃ ┃ ┣ <modal>
┃ ┃ ┃ ┃ ┣ modelDeleteModal.html
┃ ┃ ┃ ┃ ┣ modelImportModal.html
┃ ┃ ┃ ┃ ┗ modelInfoModal.html
┃ ┃ ┃ ┗ model_manage.html
┃ ┃ ┣ base_layout.html
┃ ┃ ┣ top_page.html
┃ ┃ ┗ model_analyze.html
┃ ┗ manage.py
┗ <src>
モデル情報一覧画面の開発手順(Django側)
それでは、実際に変更および追加されたソースを一つずつ見せていきます。基本はこの手順通りにソースを変更して頂ければ。
(新規) const.py
馬券種の一覧をひとつのモジュールから参照できるようにしました。他にもいろいろ増えるかな~と思ってましたが、今のところ一行だけのプログラムになってます。
BET_LIST = ["tan", "fuku", "uren", "utan", "wide", "sanfuku", "santan"]
(新規) model_item_manager.py
モデル情報一覧確認画面で表示するGoogle Chartsのグラフを管理するクラスです。基礎分析画面とは違い、収支グラフ・人気別ベット回数・回収率と的中率のグラフをすべてここで管理します。
from django.db.models.query import QuerySet
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import ModelListProp
from model_analyzer.models import ModelList
from model_analyzer.main_viewer.analyze_viewer.model_info_list_page.const import BET_LIST
from pandas import concat, read_csv, DataFrame
import gviz_api
class ModelItemManager:
def __init__(self, model_query: QuerySet, base_model: ModelList) -> None:
self.model_query = model_query
self.base_model = None
if base_model is not None:
self.base_model = base_model
self.table_idname_fmt = "summary-{mode}-table{id_}"
self.graph_idname_fmt = "summary-{mode}-graph{id_}"
self.bar_idname_fmt = "summary-{mode}-bar{id_}"
def get_model_cards(self):
model_query_cards = []
for model in self.model_query:
modelitem = ModelItem(model)
modelitem.setup()
model_query_cards += [modelitem]
if self.base_model:
base_model_card = ModelItem(self.base_model, True)
base_model_card.setup()
else:
base_model_card = None
return model_query_cards, base_model_card
def get_table_code(self, model_cards_list: list['ModelItem']):
return [{
"idname": model.base + self.table_idname_fmt.format(mode="valid", id_=model.id),
"data": model.summary["valid"]
} for model in model_cards_list] + [{
"idname": model.base + self.table_idname_fmt.format(mode="test", id_=model.id),
"data": model.summary["test"]
} for model in model_cards_list]
def get_graph_code(self, model_cards_list: list['ModelItem']):
return [{
"idname": model.base + self.graph_idname_fmt.format(mode="valid", id_=model.id),
"data": model.graph_summary["valid"],
"mode": "valid"
} for model in model_cards_list] + [{
"idname": model.base + self.graph_idname_fmt.format(mode="test", id_=model.id),
"data": model.graph_summary["test"],
"mode": "test"
} for model in model_cards_list]
def get_bar_code(self, model_cards_list: list['ModelItem']):
return [{
"idname": model.base + self.bar_idname_fmt.format(mode="valid", id_=model.id),
"data": model.bar_summary["valid"],
"mode": "valid"
} for model in model_cards_list] + [{
"idname": model.base + self.bar_idname_fmt.format(mode="test", id_=model.id),
"data": model.bar_summary["test"],
"mode": "test"
} for model in model_cards_list]
class ModelItem(ModelListProp):
def __init__(self, model: ModelList, base: bool = False) -> None:
super().__init__(model)
self.motivate = model.get_motivate_markdown()
self.horse_ticket = model.target_horse_ticket()
self.BET_LIST = BET_LIST
if base:
self.base = "base"
else:
self.base = ""
def setup(self):
self.generate_summary()
self.generate_bar_summary()
def generate_summary(self):
descriptions = {
"馬券種": ("string", "馬券種"),
} | {
bet: ("number", bet)
for bet in self.BET_LIST
}
json_dict = {
"test": None,
"valid": None
}
graph_json_dict = {
"test": None,
"valid": None
}
for mode in ["valid", "test"]:
rtn_data_list = {
"馬券種": "回収率",
}
hit_data_list = {
"馬券種": "的中率"
}
pl_graph_list = []
for pl_bet in self.BET_LIST:
if pl_bet not in self.bet_columns_map:
rtn_data_list[pl_bet] = (-1, "-")
hit_data_list[pl_bet] = (-1, "-")
continue
fval = self.pl_dir[pl_bet]
dfprofit = concat([read_csv(fpath)
for fpath in fval.glob(f"{mode}.csv")])
pl_col = self.pl_column_map[pl_bet]
bet_col = self.bet_columns_map[pl_bet]
profit = (100+dfprofit[dfprofit[bet_col].isin([1])][pl_col]
).sum()/len(dfprofit[dfprofit[bet_col].isin([1])])
hit = 100*(dfprofit[dfprofit[bet_col].isin([1])][pl_col] > 0).sum(
)/len(dfprofit[dfprofit[bet_col].isin([1])])
rtn_data_list[pl_bet] = (profit, f"{profit:.2f}%")
hit_data_list[pl_bet] = (hit*2, f"{hit:.2f}%")
# rtn_data_list[pl_bet] = profit
# hit_data_list[pl_bet] = hit
dfprofit["year-month"] = (dfprofit["raceDate"].str[:-3] + "-01"
).astype("datetime64[ns]")
graph_data = dfprofit[dfprofit[bet_col].isin([1])].groupby("year-month")[
pl_col].sum().rename(pl_bet)
pl_graph_list += [graph_data]
data_table = gviz_api.DataTable(descriptions)
data_table.LoadData([rtn_data_list, hit_data_list])
json_dict[mode] = data_table.ToJSon(
columns_order=list(descriptions.keys()))
pl_graph_data: DataFrame = concat(pl_graph_list, axis=1)
data_graph = gviz_api.DataTable(
{"year-month": ("date", "期間")} | {col: ("number", col) for col in pl_graph_data.columns})
data_graph.LoadData(
pl_graph_data.reset_index().to_dict(orient="records"))
graph_json_dict[mode] = data_graph.ToJSon(
columns_order=list(pl_graph_data.reset_index().columns), order_by="year-month")
self.summary = json_dict.copy()
self.graph_summary = graph_json_dict.copy()
def generate_bar_summary(self):
descriptions = {
"bet": ("string", "馬券種"),
} | {
f"{num}人気": ("number", f"{num}人気")
for num in range(1, 19)
}
json_dict = {
"test": None,
"valid": None
}
for mode in ["valid", "test"]:
fav_bet_num_list = []
for pl_bet in self.BET_LIST:
fav_bet_num_dict = {
"bet": pl_bet,
} | {
f"{num}人気": 0
for num in range(1, 19)
}
if pl_bet not in self.bet_columns_map:
continue
fval = self.fav_bet_num_dir[pl_bet]
dffavnum = concat([read_csv(fpath)
for fpath in fval.glob(f"{mode}.csv")])
dffavnum = dffavnum[dffavnum.columns[1:]].sum()
fav_bet_num_dict.update(dffavnum.to_dict())
fav_bet_num_list += [fav_bet_num_dict.copy()]
data_table = gviz_api.DataTable(descriptions)
data_table.LoadData(fav_bet_num_list)
json_dict[mode] = data_table.ToJSon(
columns_order=list(descriptions.keys()))
self.bar_summary = json_dict.copy()
(更新) views.py
モデル情報一覧確認画面のHTMLソースを管理するViewクラスです。
変更箇所が多いので、丸っとコピペでOKです。
from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.main_viewer.analyze_viewer.model_info_list_page.model_item_manager import ModelItemManager
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models
from model_analyzer.models import ModelList
from model_analyzer.tools.model_control.model_manage_models import get_table_list
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms
from django.core.cache import cache
THISPAGENAME = "models-info-list"
class ModelInfoListView(ModelAnalyzeView):
def start_post(self, request: WSGIRequest):
urls = super().start_post(request)
if THISPAGENAME not in urls.split("?")[0].split("/"):
self.pop_unnecessary()
if len(urls.split("?")[0].split("/")) == 2:
urls_params = urls.split("?")
urls = "/".join(urls_params[0].split("/") +
[THISPAGENAME]) + "?"+urls_params[1]
cache.clear()
return urls
def get(self, request: WSGIRequest):
self.start(request)
modelItemMng = ModelItemManager(
self.nav_params["model_query"], self.nav_params["base_model"])
self.nav_params["model_items"], self.nav_params["base_model_item"] = modelItemMng.get_model_cards()
target_model_cards = self.nav_params["model_items"] if self.nav_params[
"base_model_item"] is None else self.nav_params["model_items"] + [self.nav_params["base_model_item"]]
self.nav_params["rtn_hit_table"] = modelItemMng.get_table_code(
target_model_cards)
self.nav_params["profit_loss_graph"] = modelItemMng.get_graph_code(
target_model_cards)
self.nav_params["fav_bet_num_bar"] = modelItemMng.get_bar_code(
target_model_cards)
return render(request, 'model_analyze/analyze_pages/model_info_list_page.html', self.nav_params)
def post(self, request: WSGIRequest):
urls = self.start_post(request)
if urls:
return redirect(urls)
return render(request, 'model_analyze/analyze_pages/model_info_list_page.html', self.nav_params)
(更新) views.py
モデル分析画面のHTMLソースを管理するViewクラスです。
画面遷移時に不要なパラメータを削除する際の条件を変更しました。
(省略)
# Create your views here.
THISPAGENAME = "model-analyze"
(省略)
# class ModelAnalyzeView(View):
# 以下のメンバ変数を変更
DELETE_ATTR = [
"gchart_all", "target_bet_columns", "year_dict",
"rtnHitTable", "rtnHitTableName", "favBetNumChart", "og_chart",
"model_items", "base_model_items", "rtn_hit_table",
"profit_loss_graph", "fav_bet_num_bar",
"display_base_ogs", "ogs_base_name", "ogs_target_list", "ogs_table",
]
# 以下のメソッドを変更
def start(self, request: WSGIRequest):
if "HTTP_REFERER" not in request.META.keys() or THISPAGENAME != request.META["HTTP_REFERER"].split("?")[0].split("/")[-1]:
self.pop_unnecessary()
self.nav_params["model_table"] = get_import_models()
self.nav_params["model_num"] = len(self.nav_params["model_table"])
self.nav_params["analyze_list_disable"] = True
if request.GET.get("tm"):
self.nav_params["analyze_list_disable"] = False
self.nav_params = get_select_models(
self.nav_params,
request.GET.getlist("tm"),
request.GET.get("bm")
)
cache.clear()
モデル情報一覧画面の開発手順(WEB画面側)
以下のCSSファイル、JavaScriptファイル、HTMLファイルを新規作成・編集をしてください。
CSSファイル
モデル情報一覧確認画面を表示して「モデル選択」を押下して表示されるサイドバーと、モデル情報一覧の目次箇所の内容と被ってしまっていたので、z座標の位置をかなり前に変更しました。
#select-models {
position: fixed;
height: auto;
min-width: 70%;
max-width: 70%;
left: 0%;
z-index: 10000000;
margin-top: 37px;
}
#select-models.SlideNormal {
transform: translateX(-101%);
}
#select-models.SlideOut {
animation: SlideOut 0.25s forwards;
}
@keyframes SlideOut {
0% {
transform: translateX(-1px);
}
100% {
transform: translateX(-101%);
}
}
#select-models.SlideIN {
animation: SlideIN 0.25s forwards;
}
@keyframes SlideIN {
0% {
transform: translateX(-101%);
}
100% {
transform: translateX(-1px);
}
}
.in-shadow {
box-shadow: inset 0 0 3px rgb(120, 120, 120, .75);
}
JavaScriptファイル
今回は変更箇所なしです。
HTMLファイル
バグ対応
実はしばらく気づいてなかったのですが、Google Chartsの表示が複雑すぎたときにBootstrap5のオフキャンバスを実行すると、依存関係の問題か画面がフリーズしてしまう状況に陥りました。
再現方法は「基礎分析画面」で「収支グラフ」を適当に描画したのち、「分析モード」を押下してオフキャンバスが表示されるとそのまま画面がフリーズします。
いくらか調べたのですが、Google Chartsの悪いところで全然Tipsが共有されておらず原因不明でした。そのため、苦肉の策として「分析モード」を押下した際に描画中のグラフを全て削除するという処理を挟むことで、フリーズを回避するようにしました。
以下に示すHTMLソースたちがバグ対応後のものになります。
<script type="text/javascript">
const analyzeBtn = document.getElementById("analyze-list-btn");
analyzeBtn.addEventListener("click", (e) => {
{% autoescape off %}
let targetPieModels = JSON.parse(JSON.stringify({{rtnHitTableName.target}}));
{% endautoescape %}
let targetIdList = ['plGraphChart'];
for (let idx in targetPieModels){
targetIdList.push(`favBetNumChart-valid-${idx}`);
targetIdList.push(`favBetNumChart-test-${idx}`);
}
console.log(targetIdList);
for (let targetId of targetIdList){
var chartElem = document.getElementById(targetId);
while (chartElem.firstChild) {
chartElem.removeChild(chartElem.firstChild);
}
}
})
</script>
<script type="text/javascript">
const listenPlChart = document.getElementById("changeGraphBtn");
listenPlChart.addEventListener("click", (e) => FavBetNumChart())
const listenNextPlChart = document.getElementById("graphRightBtn");
listenNextPlChart.addEventListener("click", (e) => FavBetNumChart())
const listenPrevPlChart = document.getElementById("graphLeftBtn");
listenPrevPlChart.addEventListener("click", (e) => FavBetNumChart())
function FavBetNumChart() {
let slctList = getSelected();
let betMode = slctList[0];
let yearMode = slctList[1];
if (yearMode == null) {
return;
}
google.charts.load('current', { packages: ['corechart'] });
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
{% autoescape off %}
target_model_name_list = JSON.parse(JSON.stringify({{ rtnHitTableName.target }}));
let favBetNumChart = JSON.parse(JSON.stringify({{ favBetNumChart }}));
{% endautoescape %}
target_model_name_list.forEach(function (model_name, idx) {
baseModelName = "{{ rtnHitTableName.base }}";
var options = {
legend: 'none',
pieSliceText: 'label',
backgroundColor: {
fill: "transparent"
}
};
for (let dataset of ["test", "valid"]) {
if (Object.hasOwn(favBetNumChart[betMode][dataset], yearMode)) {
let targetJsonStr = JSON.parse(favBetNumChart[betMode][dataset][yearMode]["target"][model_name])
targetJsonCode = new google.visualization.DataTable(targetJsonStr);
let idname = "favBetNumChart-"+dataset+"-"+String(idx);
const chartElem = document.getElementById(idname);
if(chartElem.innerText != ""){
while (chartElem.firstChild) {
chartElem.removeChild(chartElem.firstChild);
}
}
const targetChart = new google.visualization.PieChart(document.getElementById(idname));
if (baseModelName != "None") {
let baseJsonStr = JSON.parse(favBetNumChart[betMode][dataset][yearMode]["base"][baseModelName])
baseJsonCode = new google.visualization.DataTable(baseJsonStr);
targetJsonCode = targetChart.computeDiff(baseJsonCode, targetJsonCode);
options["diff"] = {
oldData: {
tooltip: {
prefix: "baseline"
}
},
newData: {
tooltip: {
prefix: model_name
}
}
}
}
targetChart.draw(targetJsonCode, options);
}
}
})
}
}
function getSelected() {
selectedBet = null;
selectedYear = null;
mode = document.getElementById("selectBetMode");
Array.from(mode.options).forEach(function (option) {
if (option.selected) {
selectedBet = option.innerHTML;
}
});
mode = document.getElementById("selectYear");
Array.from(mode.options).forEach(function (option) {
if (option.selected) {
selectedYear = option.innerHTML;
}
});
return [selectedBet, selectedYear];
}
</script>
<script type="text/javascript">
function changeChart(num = 0) {
var targetIdx = changeSelectDatas(num);
google.charts.load('current', { 'packages': ['corechart', 'table',] });
google.charts.setOnLoadCallback(updateChart);
setSelectDatas(targetIdx);
function updateChart() {
const slist = getSelectDatas();
{% autoescape off %}
let graph_data_obj = JSON.parse(JSON.stringify({{ gchart_all }}));
let jscode_data = new google.visualization.DataTable(graph_data_obj[slist[0]][slist[1]][slist[2]]);
{% endautoescape %}
var options = {
legend: { position: 'top' },
hAxis: { title: 'Year', titleTextStyle: { color: '#333' } },
// width: "90%",
// height: "80%",
backgroundColor: {
fill: "transparent"
},
tooltip: { isHtml: true },
chartArea: {
width: '80%',
height: '80%',
top: 30
},
};
const chartElem = document.getElementById('plGraphChart');
if(chartElem.innerText != ""){
while (chartElem.firstChild) {
chartElem.removeChild(chartElem.firstChild);
}
}
const chart = new google.visualization.LineChart(document.getElementById('plGraphChart'));
chart.draw(jscode_data, options);
google.visualization.events.addListener(chart, 'select', selectHandler);
function selectHandler(e) {
console.log(chart.getSelection()[0]);
const getPosition = chart.getSelection()[0];
if (getPosition.row === null) {
return
}
const raceId = chart.Z.Wf[getPosition.row].c[getPosition.column + 1].v;
console.log(raceId);
const race_url = `https://db.netkeiba.com/race/${raceId}/`;
console.log(race_url);
window.open(race_url, '_blank');
}
}
}
function getSelectDatas() {
selectedList = [];
for (let idname of ["selectBetMode", "selectDataSet", "selectYear"]) {
mode = document.getElementById(idname);
Array.from(mode.options).forEach(function (option) {
if (option.selected) {
selectedList.push(option.innerHTML);
}
});
}
return selectedList;
}
function changeSelectDatas(num) {
if (num === 0) {
return;
}
targetIdx = null;
mode = document.getElementById("selectYear");
const predBtn = document.querySelector("#graphLeftBtn");
predBtn.disabled = true;
const nextBtn = document.querySelector("#graphRightBtn");
nextBtn.disabled = true;
Array.from(mode.options).forEach(function (option, idx) {
if (option.selected) {
targetIdx = idx + num;
option.selected = false;
}
});
return targetIdx;
}
function setSelectDatas(targetIdx) {
mode = document.getElementById("selectYear");
Array.from(mode.options).forEach(function (option, idx, array) {
if (idx === targetIdx) {
option.selected = true;
const predBtn = document.querySelector("#graphLeftBtn");
predBtn.disabled = idx === 0;
const nextBtn = document.querySelector("#graphRightBtn");
nextBtn.disabled = idx === array.length - 1;
}
});
}
</script>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", (e) => {
changeRtnHitTable(0);
});
const listenChangeBtn = document.getElementById("changeGraphBtn");
listenChangeBtn.addEventListener("click", (e) => {
changeRtnHitTable(0);
})
function changeRtnHitTable(num) {
{% autoescape off %}
target_model_name_list = JSON.parse(JSON.stringify({{ rtnHitTableName.target }}));
{% endautoescape %}
target_model_idx = get_target_model_id(target_model_name_list);
next_model_idx = target_model_idx + num;
const leftBtn = document.querySelector("#tableLeftBtn");
const rightBtn = document.querySelector("#tableRightBtn");
if (next_model_idx == 0 & target_model_name_list.length > 1) {
rightBtn.disabled = false;
leftBtn.disabled = true;
} else if (next_model_idx == target_model_name_list.length - 1 & target_model_name_list.length > 1) {
rightBtn.disabled = true;
leftBtn.disabled = false;
} else {
rightBtn.disabled = false;
leftBtn.disabled = false;
}
changeName(target_model_name_list[next_model_idx]);
google.charts.load('current', { 'packages': ['table',] });
google.charts.setOnLoadCallback(updateTable);
}
function changeName(modelName) {
const targetElem = document.getElementById("targetRtnHitTableName");
targetElem.innerText = modelName;
}
function updateTable() {
{% autoescape off %}
target_model_name_list = JSON.parse(JSON.stringify({{ rtnHitTableName.target }}));
rtnHitTableData = JSON.parse(JSON.stringify({{ rtnHitTable }}));
{% endautoescape %}
var betMode = getBetMode();
target_model_idx = get_target_model_id(target_model_name_list);
let targetTableCode = JSON.parse(rtnHitTableData[betMode]["target"][target_model_name_list[target_model_idx]]);
targetTableCode = new google.visualization.DataTable(targetTableCode);
const targetTable = new google.visualization.Table(document.getElementById('targetRtnHitTableChart'));
targetTable.draw(targetTableCode, {});
const baseTableElem = document.getElementById('baseRtnHitTableChart');
if ("{{ rtnHitTableName.base }}" !== "None") {
let baseRtnHitTableName = document.getElementById('baseRtnHitTableName').innerText
baseTableCode = new google.visualization.DataTable(JSON.parse(rtnHitTableData[betMode]["base"][baseRtnHitTableName]));
const baseTable = new google.visualization.Table(document.getElementById('baseRtnHitTableChart'));
baseTable.draw(baseTableCode, {});
}
}
function get_target_model_id(target_model_name_list) {
const targetElem = document.getElementById("targetRtnHitTableName");
target_model_idx = null;
target_model_name_list.forEach(function (elem, idx,) {
if (elem === targetElem.innerText) {
target_model_idx = idx;
}
});
return target_model_idx;
}
function getBetMode() {
selectedBet = null;
mode = document.getElementById("selectBetMode");
Array.from(mode.options).forEach(function (option) {
if (option.selected) {
selectedBet = option.innerHTML;
}
});
return selectedBet;
}
</script>
{% extends "model_analyze/model_analyze.html" %}
{% load static %}
{% block ext-cssblock %}
{% endblock %}
{% block gCharts %}
{% endblock %}
{% block analyze-title %}
<h2 class="d-flex container justify-content-center">
<span class="me-5 pt-4">
モデルの基礎分析
</span>
<span class="me-5 ms-5"></span>
</h2>
{% endblock %}
{% block analyze-content %}
<div style="width: 90%; height: 1460px; min-width: 1400px;"
class="d-flex container flex-column border border-dark-subtle rounded pt-3 ps-4 mt-3 mb-5 shadow bg-light bg-gradient">
<div class="row">
<div class="fs-4 text-center fw-bold">
収支グラフ
</div>
</div>
<div class="row me-1 border in-shadow border-dark-subtle rounded bg-white" style="height: 450px;">
<div id="plGraphChart"></div>
</div>
<div class="row col-nowrap mt-3 row-cols-lg-auto align-items-center justify-content-center" id="selectGraphMode">
<div class="col col-nowrap">
<button type="button" class="btn btn-outline-secondary text-nowrap" id="graphLeftBtn" disabled
onclick="changeChart(-1)">
<img src="/static/images/caret-left-square.svg">
前へ
</button>
</div>
<div class="col col-nowrap">
<label for="selectBetMode" class="form-label text-nowrap">Select Bet Mode</label>
<select class="form-select form-select-sm" aria-label="select bet mode" id="selectBetMode">
{% for item in target_bet_columns %}
<option value="{{item}}" {% if forloop.first %} selected{% endif %}>{{item}}</option>
{% endfor %}
</select>
</div>
<div class="col col-nowrap">
<label for="selectDataSet" class="form-label text-nowrap">Select Data Mode</label>
<select class="form-select form-select-sm" aria-label="select dataset" id="selectDataSet">
<option selected></option>
<option value="test">test</option>
<option value="valid">valid</option>
</select>
</div>
<div class="col col-nowrap">
<label for="selectYear" class="form-label text-nowrap">Select Year</label>
<select class="form-select form-select-sm" aria-label="select year" id="selectYear" required>
<option selected></option>
</select>
</div>
<div class="col col-nowrap">
<button id="changeGraphBtn" type="button" class="btn btn-primary" disabled onclick="changeChart()">描画</button>
</div>
<div class="col col-nowrap">
<button type="button" class="btn btn-outline-secondary text-nowrap" id="graphRightBtn" disabled
onclick="changeChart(1)">
次へ
<img src="/static/images/caret-right-square.svg">
</button>
</div>
</div>
<div class="row mt-3" id="return_hit_rate">
<div class="fs-4 text-center fw-bold">
人気別ベット回数
</div>
</div>
<div class="row me-1 border in-shadow border-dark-subtle rounded col-nowrap bg-white" style="height: 350px;">
{% for targetName in rtnHitTableName.target %}
<div class="col col-nowrap">
<div class="row col-nowrap w-100 ms-1 me-1" id="model_name{{ forloop.counter0 }}">
<div class="col fs-6 text-end fw-bold mt-3">valid</div>
<div class="col fs-5 text-center fw-bold mt-2">
{{ targetName }}
</div>
<div class="col fs-6 text-start fw-bold mt-3">test</div>
</div>
<div class="row col-nowrap w-100 ms-1 me-1 in-shadow border border-dark-subtle rounded" style="height: 300px;">
<div class="col" id="favBetNumChart-valid-{{ forloop.counter0 }}"></div>
<div class="col" id="favBetNumChart-test-{{ forloop.counter0 }}"></div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-3" id="return_hit_rate">
<div class="fs-4 text-center fw-bold">
モデルの回収率と的中率
</div>
</div>
<div style="height: 410px;" class="d-flex flex-row row in-shadow border border-dark-subtle rounded me-1 p-2 bg-white">
<div class="col">
<div class="d-flex flex-column row ms-1 me-1 pt-1" style="height: 30px; width: 100%;">
<div class="col w-25 d-flex justify-content-end">
<button type="button" class="btn btn-outline-secondary btn-sm border border-0" id="tableLeftBtn" disabled
onclick="changeRtnHitTable(-1)">
<img src="/static/images/caret-left-square.svg">
</button>
</div>
<div class="col w-50 d-flex justify-content-center">
<div class="fs-5 text-center fw-bold">
Target: <span id="targetRtnHitTableName">{{rtnHitTableName.target.0}}</span>
</div>
</div>
<div class="col w-25 d-flex justify-content-start">
<button type="button" class="btn btn-outline-secondary btn-sm border border-0" id="tableRightBtn" disabled
onclick="changeRtnHitTable(1)">
<img src="/static/images/caret-right-square.svg">
</button>
</div>
</div>
<div class="row w-100 ms-1 me-1" style="height: 370px;">
<div id="targetRtnHitTableChart" class="d-flex justify-content-center align-items-center"></div>
</div>
</div>
{% if rtnHitTableName.base is not None %}
<div class="col">
<div class="row w-100 ms-1 me-1" style="height: 30px;">
<div class="fs-5 text-center fw-bold">
Baseline: <span id="baseRtnHitTableName">{{rtnHitTableName.base}}</span>
</div>
</div>
<div class="row w-100 ms-1 me-1" style="height: 370px;">
<div id="baseRtnHitTableChart" class="d-flex justify-content-center align-items-center"></div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block ext-jsblock %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{% include 'model_analyze/analyze_pages/scripts/base_analyze/graph_controller.html' %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/table_controller.html' %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/profit_loss_graph.html' %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/rtn_hit_rate_table.html' %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/fav_bet_num_chart.html' %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/delete_graph.html' %}
{% endblock %}
以上の修正が終わったら「基礎分析画面」を表示して「収支グラフ」を描画後に「分析モード」を押下してみてください。
オフキャンバスの表示とともに収支グラフと人気別ベット回数の円グラフが削除されていればOKです。
本題
以下のHMTLソースを追加・変更ください。すべて丸ごとコピーでOKです。
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", (e) => {
{% autoescape off %}
jsmapobj = JSON.parse(JSON.stringify({{ fav_bet_num_bar }}));
{% endautoescape %}
for (jsobj of jsmapobj) {
displayBar(jsobj.idname, jsobj.data, jsobj.mode);
}
});
function displayBar(idname, jscode, modename) {
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawBar);
function drawBar() {
targetGraphCode = new google.visualization.DataTable(JSON.parse(jscode));
const targetGraph = new google.visualization.BarChart(document.getElementById(idname));
var options = {
vAxis: { title: `Fav BET Num ( ${modename} )`, titleTextStyle: { color: '#333', fontSize: 14} },
width: 600,
height: 230,
isStacked: 'percent',
legend: {position: 'bottom',},
hAxis: {
minValue: 0,
ticks: [0, .3, .6, .9, 1]
}
};
targetGraph.draw(targetGraphCode, options);
}
}
</script>
<div class="row flex-nowrap align-items-center" style="height: 260px">
<div class="col-6">
<div class="row" id="{{ base }}summary-valid-graph{{ modelItem.id }}"></div>
</div>
<div class="col-5">
<div class="row" id="{{ base }}summary-test-graph{{ modelItem.id }}"></div>
</div>
</div>
<div class="row flex-nowrap align-items-top" style="height: 230px">
<div class="col-6">
<div class="row" id="{{ base }}summary-valid-bar{{ modelItem.id }}"></div>
</div>
<div class="col-5">
<div class="row" id="{{ base }}summary-test-bar{{ modelItem.id }}"></div>
</div>
</div>
<div class="row" style="height: 200px">
<div class="fw-bold text-center">valid</div>
<div id="{{ base }}summary-valid-table{{ modelItem.id }}"></div>
<div></div>
<div class="fw-bold text-center">test</div>
<div id="{{ base }}summary-test-table{{ modelItem.id }}"></div>
</div>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", (e) => {
{% autoescape off %}
jsmapobj = JSON.parse(JSON.stringify({{ profit_loss_graph }}));
{% endautoescape %}
for (jsobj of jsmapobj) {
displayGraph(jsobj.idname, jsobj.data, jsobj.mode);
}
});
function displayGraph(idname, jscode, modename) {
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawGraph);
function drawGraph() {
targetGraphCode = new google.visualization.DataTable(JSON.parse(jscode));
const targetGraph = new google.visualization.AreaChart(document.getElementById(idname));
var options = {
width: 550,
height: 250,
legend: { position: 'bottom' },
hAxis: { title: `Profit Loss Graph by Month ( ${modename} )`, titleTextStyle: { color: '#333', fontSize: 16} },
backgroundColor: {
fill: "transparent"
},
chartArea: {
width: '80%',
height: '70%',
bottom: 50,
},
};
targetGraph.draw(targetGraphCode, options);
}
}
</script>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", (e) => {
{% autoescape off %}
jsmapobj = JSON.parse(JSON.stringify({{ rtn_hit_table }}));
{% endautoescape %}
for (jsobj of jsmapobj) {
displayTable(jsobj.idname, jsobj.data);
}
});
function displayTable(idname, jscode) {
google.charts.load('current', { 'packages': ['table',] });
google.charts.setOnLoadCallback(drawTable);
function drawTable() {
targetTableCode = new google.visualization.DataTable(JSON.parse(jscode));
const targetTable = new google.visualization.Table(document.getElementById(idname));
var formatter = new google.visualization.PatternFormat(
'<span class="fw-bold mx-auto text-center" style="display: block;">{0}</span>');
for (let idx=1 ; idx < 8; idx++) {
var c_formatter = new google.visualization.ColorFormat();
c_formatter.addGradientRange(0, 100, '#000000', '#ff8b8b', '#ffffff');
c_formatter.addGradientRange(100, 200, '#000000', '#ffffff', '#a397ff');
c_formatter.format(targetTableCode, idx);
}
formatter.format(targetTableCode, [0]);
targetTable.draw(targetTableCode, {allowHtml: true, width: "100%"});
}
}
</script>
<h5 id="model-name" class="fw-bold text-center pt-2">{{ modelItem.model_name }}</h5>
<ul class="nav nav-tabs">
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#{{base}}model-info-{{ modelItem.id }}"
type="button">
Info
</button>
</li>
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#{{base}}model-summary-{{ modelItem.id }}"
type="button">
Summary
</button>
</li>
{% comment %} <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#{{base}}model-details-{{ modelItem.id }}"
type="button">
Details
</button>
</li> {% endcomment %}
</ul>
<!-- Infoタブ -->
<div class="tab-content rounded bg-white overflow-auto" style="height: 710px;">
<div id="{{base}}model-info-{{ modelItem.id }}" class="tab-pane p-2 mt-4">
<table class="w-100 in-shadow rounded mt-4">
<tbody class="p-2 rounded">
<tr>
<th scope="row" class="border-bottom text-start p-2 bg-secondary text-white text-nowrap">モデルID</th>
<td class="border-start border-bottom text-start p-3">{{modelItem.model_id}}</td>
</tr>
<tr>
<th scope="row" class="border-bottom text-start p-2 bg-secondary text-white text-nowrap">モデル種別</th>
<td class="border-start border-bottom text-start p-3">{{modelItem.model_type}}</td>
</tr>
<tr>
<th scope="row" class="border-bottom text-start p-2 bg-secondary text-white text-nowrap">対象馬券</th>
<td class="border-bottom border-start text-start p-3">{{modelItem.horse_ticket}}</td>
</tr>
<tr>
<th scope="row" class="border-bottom text-start p-2 bg-secondary text-white text-nowrap">モデル説明</th>
<td class="border-start border-bottom text-start p-3">{{modelItem.motivate | safe}}</td>
</tr>
<tr>
<th scope="row" class="border-bottom text-start p-2 bg-secondary text-white text-nowrap">モデルパス</th>
<td class="border-start border-bottom text-start p-3">{{modelItem.model_dir}}</td>
</tr>
<tr>
<th scope="row" class="text-start p-2 bg-secondary text-white text-nowrap">備考</th>
<td class="border-start text-start p-3">{{modelItem.memo}}</td>
</tr>
</tbody>
</table>
</div>
<!-- Summaryタブ -->
<div id="{{base}}model-summary-{{ modelItem.id }}" class="tab-pane active p-2 container">
{% include 'model_analyze/analyze_pages/scripts/model_info_list/card_items/summary_item.html' with modelItem=modelItem base=base %}
</div>
<!-- Detailsタブ -->
{% comment %} <div id="{{base}}model-details-{{ modelItem.id }}" class="tab-pane p-2">
</div> {% endcomment %}
</div>
<script>
var offset = 60;
$('.nav-scroll').click(function (event) {
event.preventDefault();
scrollTo(0, $($(this).attr('href')).offset().top - offset);
});
$('.model-scroll').click(function (event) {
event.preventDefault();
scrollTo(0, $('.model-header').offset().top - offset);
});
$('.model-scroll-base').click(function (event) {
event.preventDefault();
scrollTo(0, $('.model-header-base').offset().top - offset);
});
</script>
{% extends "model_analyze/model_analyze.html" %}
{% block ext-cssblock %}
{% endblock %}
{% block gCharts %}
{% endblock %}
{% block analyze-title %}
<h2 class="d-flex container justify-content-center">
<span class="pt-3">
モデル一覧
</span>
</h2>
{% endblock %}
{% block analyze-content %}
<div id="navbar-mode-info-list"
class="navbar navbar-light flex-column justify-content-start bg-light border border-dark-subtle btn-sm rounded in-shadow mb-5"
style="margin-top: 42px;">
<div style="top: 60px;left: 0;" class="text-center pt-2 sticky-top">
<button class="text-nowrap fs-6 fw-bold model-scroll btn btn-outline-dark border-0 border">Target
Models</button>
<nav class="nav flex-column text-start">
{% for imodel in model_items %}
<a class="nav-link nav-scroll" href="#model-{{imodel.id}}">{{imodel.model_id}}</a>
{% endfor %}
</nav>
{% if base_model_item is not None %}
<button class="mt-2 text-nowrap fs-6 fw-bold model-scroll-base btn btn-outline-dark btn-sm border-0 border">Base
Model</button>
<nav class="nav flex-column text-start">
<a class="nav-link nav-scroll" href="#base-model-{{base_model_item.id}}">{{base_model_item.model_id}}</a>
</nav>
{% endif %}
</div>
</div>
<div class="pb-3 ps-3 flex-column w-100">
<h3 class="row fw-bold text-center model-header w-100 justify-content-center">Target Models</h3>
{% for imodel in model_items %}
<div class="row border border-dark-subtle rounded in-shadow w-100 bg-light p-1" id="model-{{imodel.id}}"
style="height: 800px;">
{% include 'model_analyze/analyze_pages/scripts/model_info_list/model_card_item.html' with modelItem=imodel base="" %}
</div>
{% endfor %}
{% if base_model_item is not None %}
<h3 class="row fw-bold text-center model-header-base pt-4 w-100 justify-content-center">Base Model</h3>
<div class="row border border-dark-subtle rounded in-shadow w-100 bg-light p-1" id="base-model-{{base_model_item.id}}"
style="height: 800px;">
{% include 'model_analyze/analyze_pages/scripts/model_info_list/model_card_item.html' with modelItem=base_model_item base="base" %}
</div>
{% endif %}
</div>
{% endblock %}
{% block ext-jsblock %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{% include 'model_analyze/analyze_pages/scripts/model_info_list/scroll_models.html' %}
{% include 'model_analyze/analyze_pages/scripts/model_info_list/card_items/summary_rtn_hit_table.html' %}
{% include 'model_analyze/analyze_pages/scripts/model_info_list/card_items/summary_profit_loss_graph.html' %}
{% include 'model_analyze/analyze_pages/scripts/model_info_list/card_items/summary_fav_bet_num.html' %}
{% endblock %}
サーバの起動と画面の確認
それでは、モデル情報一覧確認画面のページができたので、サーバを起動しましょう。
manage.pyファイルがあるフォルダがカレントディレクトリになっていることを確認して、コマンドプロンプトで以下のコマンドを実行
python manage.py runserver
http://localhost:8000/model-analyze へアクセスしましょう。
その後適当なモデルを選択して「分析モード」ボタンより表示されるサイドバーから「モデル情報一覧確認画面」の画面を表示してください。
以下のモデル情報一覧確認画面が表示されればOKです。
簡単な動作確認
今回はとりわけ追加の隠し機能はないので、動作確認は完成イメージ(GIF)通りになっていればOKです。
以上でモデル情報一覧確認画面の開発は終了です。お疲れさまでした。
いよいよ次回オッズグラフスコアの確認画面の開発に入ります。
モデル分析画面の開発も終盤に入ります。
ソース公開しました!
Bookersでロードマップ4で解説したソースを公開しました!
以下のリンクへ飛んでいただき、BookersとYouTube連携して私のチャンネルを登録すると無料でWEBアプリのソースが手に入ります。
良ければ、実際に触って遊んでみてください!
コメント