はじめに
私は競馬予想プログラムソフトの制作過程を動画で投稿している者です。
ここでは、モデル分析用のWEBアプリの開発手順を話していきます。
現在作成している競馬予想プログラムソフトの概要は以下を参照ください。
本プログラムの前提
本プログラムでは以下の前提を置いています。
- Windows 10
- Python3.10.5
- Django ver5.0.4
- 競馬予想プログラムで作成したモデルを分析する目的で使います。
- Bookersで公開中のモデル分析管理クラスと連携してモデル分析を行います。
とりわけ、最後の2つに関しては有料記事でソースを公開しているため、まったく同じ環境で競馬予想プログラムの作成とモデルの分析を行いたい場合は以下のBookers記事一覧から記事を購入ください。
また、競馬予想プログラムの制作過程については大まかな概要は以下の再生リストから参照ください。
詳細な解説は以下の記事一覧を参照ください。
モデル分析画面:モデルの収支グラフをGoogle Chartsで表示する機能
今回も前回に続いてモデル分析画面の基礎分析画面の開発に入っていきます。基礎分析画面では、選択したモデルに対して収支グラフ、回収率と的中率、人気別ベット回数を可視化する画面です。
今回のパートでは、モデルを選択後、基礎分析画面へ遷移すると、年度別の収支グラフを描画する機能を作成します。
グラフをブラウザ上で描画するツールとして、有名なのはChart.jsですが今回描画するグラフは少なくとも3,500件のデータがあります。
Chart.jsで一度表示してみたのですが、そこまでの規模のデータを描画すると動作が凄まじく重くなってしまい、分析どころの話ではなくなりました。
他にいい方法がないか調べたところ、Google Chartsを使うのがもっとも有効だという結論に至りました。Google Chartsで描画したグラフは確認する限り20,000データ程度であればある程度サクサク動く印象でした。
Chart.jsはcanvas要素に埋め込んでいるのが原因なのかデータ数が多いと使い物にならなかったのですが、Google ChartsではCanvas要素に埋め込まずにちゃんと要素を生成しているように思います。
そのあたりの差分が性能差に出ているのかと・・・HTMLソースを覗く限りそのような印象を受けました。結論なんで動作が軽いのかは明確に分かってません。
以下に完成イメージGIFをお見せします。
完成イメージ(GIF)
モデルを選択後、基礎分析画面の表示ボタンをクリックするのですが、モデルインポートで各種分析結果のCSVファイルを読み込むので、基礎分析画面が表示されるまで少し時間がかかります。
馬券別・データセット別・年度別に選択したモデルの収支グラフを描画する機能
セレクトボックスで、馬券種、データセットの種類、年度を選択できるようになっており 「描画」ボタンでグラフを描画できます。
年度を選択すれば「前へ」「次へ」ボタンが活性化し、セレクトボックスで年度を選びなおさずに、次や前の年度のグラフを描画できます。
グラフ上のデータ点から対象のレース情報を参照できる機能
描画したグラフは、各データ点を選択できるようになっています。
Chart.jsにもありますが、Google Chartsにはクリックイベントという機能があります。つまり、グラフ上のポイントをクリックするとブラウザ上で画面をクリックしたぞというイベントをキャッチします。
そのイベントからグラフの各データが持つraceIDの情報を抜き出して対応するレースURLへ自動でアクセスできるようにしました。
こうすることで、グラフ上の点で利益が伸びた瞬間のデータ点があった場合に、それがどのようなレースだったのかレース情報を確認したくなった際に、すぐに確認できるようにしました。
フォルダ構成
今回の実装でもフォルダ構成を変えています。
前回記事のフォルダ構成をベースに追加箇所を赤字にしています。注意して変更してください。
<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>
┃ ┃ ┃ ┃ ┃ ┣ profit_loss_graph.py
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ <model_info_list_page>
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ <ogs_page>
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ base_analyzer_dto.py
┃ ┃ ┃ ┃ ┗ baseanalyzer_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>
┃ ┃ ┃ ┃ ┃ ┣ graph_controller.html
┃ ┃ ┃ ┃ ┃ ┗ profit_loss.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側)
それでは、実際に変更および追加されたソースを一つずつ見せていきます。基本はこの手順通りにソースを変更して頂ければ、完成するはずです。
今回も変更および新規作成されたファイルが非常に多いので注意ください。
追加パッケージインストール
Google Chartsの描画をするためにpython用の支援ライブラリがありましたので追加パッケージとしてpipでインストールします。
pip install gviz_api==1.10.0
(新規) profit_loss_graph.py
収支グラフ描画用のクラスです。かなり複雑ですがこのクラスの構成と似たような形で今後開発する回収率と的中率、人気別ベット回数の可視化を行います。
from django.db.models.query import QuerySet
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
ModelListProp, BetDataItem, ModelInfoMap, DataTableItem, DataMap, TableItem, GraphJsonMap, JsonItem)
from model_analyzer.models import ModelList
import pandas as pd
import gviz_api
from app_keiba.settings import TEMPLATES
import datetime
import warnings
from pandas.errors import SettingWithCopyWarning
warnings.simplefilter(action="ignore", category=SettingWithCopyWarning)
class ProfitLossManager:
def __init__(self, targetModelMap: list[ModelInfoMap], baseModelMap: ModelInfoMap) -> None:
self.targetModelMap = targetModelMap
self.baseModelMap = baseModelMap
self.description_temp = {"raceDate": ("date", "raceDate")}
def generate_pl_graph_json(self) -> GraphJsonMap:
graphDataMap = self.generate_pl_graph_data()
graphJsonMap = GraphJsonMap()
target_map_list: list[BetDataItem] = graphDataMap["target"]
base_map: BetDataItem = graphDataMap["base"]
for bet in graphJsonMap.__dict__.keys():
for target_map in [base_map]+target_map_list:
target_bet_map: DataTableItem = target_map.__dict__[bet]
for mode in ["train", "valid", "test"]:
target_bet_mode_map: dict[
str, TableItem] = target_bet_map.__dict__[mode]
if target_bet_mode_map is None:
continue
graph_item_dict: dict[int, JsonItem] = graphJsonMap.__dict__[
bet].__dict__[mode]
for year, target_item in target_bet_mode_map.items():
if year not in graph_item_dict:
jsonItem = JsonItem()
jsonItem.description |= self.description_temp.copy()
jsonItem.description |= target_item.description
jsonItem.data += [target_item.data]
graph_item_dict[year] = jsonItem
else:
graph_item_dict[year].description |= target_item.description
graph_item_dict[year].data += [target_item.data]
for mode in ["train", "valid", "test"]:
graph_item_dict: dict[
int, JsonItem] = graphJsonMap.__dict__[bet].__dict__[mode]
if graph_item_dict is None:
continue
for year, target_item in graph_item_dict.items():
target_item.data = pd.concat(
target_item.data, axis=1).reset_index().to_dict(orient="records")
data_table = gviz_api.DataTable(target_item.description)
data_table.LoadData(target_item.data)
columns_order = list(target_item.description.keys())
target_item.json = data_table.ToJSon(
columns_order=columns_order, order_by=columns_order[0])
return graphJsonMap
def generate_pl_graph_data(self) -> dict[str, BetDataItem | list[BetDataItem] | None]:
graphDataMap: dict[str, BetDataItem | list[BetDataItem] | None] = {
"target": [], "base": None}
for tmMap in self.targetModelMap:
graphDataMap["target"] += [self.create_betdata_item(tmMap)]
graphDataMap["base"] = self.create_betdata_item(
self.baseModelMap, True)
return graphDataMap
def create_betdata_item(self, tempMap: ModelInfoMap | None, base: bool = False) -> BetDataItem | None:
if tempMap is None:
return tempMap
betdataitem = BetDataItem(
tempMap.meta.model_id, tempMap.meta.model_name)
for bet, dataMap in tempMap.betData.__dict__.items():
if dataMap is None:
continue
dataitem: DataTableItem = self.create_datatable_item(
bet, dataMap, tempMap, base)
betdataitem.__dict__[bet] = dataitem
return betdataitem
def create_datatable_item(
self,
bet: str,
dataMap: DataMap,
tempMap: ModelInfoMap,
base: bool = False
) -> DataTableItem:
if bet not in tempMap.meta.pl_column_map:
return DataTableItem()
pl_col = tempMap.meta.pl_column_map[bet]
pl_label = "base_" + tempMap.meta.model_id if base else tempMap.meta.model_id
raceId_col = "base_" + \
f"{tempMap.meta.model_id}_raceId" if base else f"{tempMap.meta.model_id}_raceId"
bet_col = tempMap.meta.bet_columns_map[bet]
dfv: pd.DataFrame = dataMap.valid.rename(
columns={pl_col: pl_label, "raceId": raceId_col})
dft: pd.DataFrame = dataMap.test.rename(
columns={pl_col: pl_label, "raceId": raceId_col})
dfv = dfv[dfv[bet_col].isin([1])]
dft = dft[dft[bet_col].isin([1])]
# dfv["year"] = dfv["raceDate"].astype("datetime64[ns]").apply(
# lambda d: f"{d.year}" + "f" if d.month < 7 else f"{d.year}" + "s")
# dft["year"] = dft["raceDate"].astype("datetime64[ns]").apply(
# lambda d: f"{d.year}" + "f" if d.month < 7 else f"{d.year}" + "s")
dfv["year"] = dfv["raceDate"].astype(
"datetime64[ns]").dt.year.astype(str)
dft["year"] = dft["raceDate"].astype(
"datetime64[ns]").dt.year.astype(str)
dfv["raceDate"] = dfv["raceDate"].astype("datetime64[ns]") + dfv[raceId_col].astype(
str).str[-4:].apply(lambda d: datetime.timedelta(minutes=int(d)))
dft["raceDate"] = dft["raceDate"].astype("datetime64[ns]") + dft[raceId_col].astype(
str).str[-4:].apply(lambda d: datetime.timedelta(minutes=int(d)))
target_columns = [pl_label, raceId_col]
dataitem = DataTableItem()
dataitem.valid = self.create_data_map(dfv, target_columns)
dataitem.test = self.create_data_map(dft, target_columns)
return dataitem
def create_data_map(
self,
idf: pd.DataFrame,
target_columns: list[str],
) -> dict[str, TableItem]:
data_map = {}
dfg = idf.set_index("raceDate").groupby('year')[target_columns]
for year in idf["year"].unique():
item = TableItem()
item.description[target_columns[0]] = ("number", target_columns[0])
item.description[target_columns[1]] = (
"string", "raceId", {"role": "annotationText"})
item.description[f"{target_columns[0]}_labelsText"] = (
"string", "labelsText", {"role": "tooltip", "html": True})
item.data = dfg.get_group(year)
item.data.loc[:, target_columns[0] +
"_row"] = item.data[target_columns[0]].round().astype(int).copy()
item.data.loc[:, target_columns[0]
] = item.data[target_columns[0]].cumsum().round().astype(int)
item.data.reset_index(inplace=True)
item.data.loc[:, f"{target_columns[0]}_labelsText"] = item.data.apply(
lambda row: f'<table class="m-1"><tr><th colspan="2">{str(row["raceDate"]).split()[0]}</th></tr>' +
f'<tr><th>profit</th><td class="text-end">{row[target_columns[0]+"_row"]}円</td></tr>' +
f'<tr><th>sum profit</th><td class="text-end">{int(row[target_columns[0]])}円</td></tr>' +
f'<tr><th>modelId</th><td class="text-end">{target_columns[0]}</td></tr>' +
f'<tr><th>raceId</th><td class="text-end">{row[target_columns[1]]}</td></tr></table>',
axis=1
)
item.data = item.data.set_index(
"raceDate")[target_columns+[f"{target_columns[0]}_labelsText"]]
data_map[year] = item
return data_map
(更新) views.py
収支グラフ描画用にViewクラスが返すレンダーにパラメータを与えています。
差分を示します。
# 以下を追加インポート
# (一部使わないものもあったような気がしますが、まだメンテできてないです。)
from model_analyzer.main_viewer.analyze_viewer.baseanalyzer_operator import ModelListController
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
ModelListProp, BetDataItem, ModelInfoMap, DataTableItem, DataMap, TableItem)
from model_analyzer.tools.model_control.model_analyze_models import get_select_models, get_model_from_modelId
import pandas as pd
import pathlib
# startメソッドをオーバーライド
class BaseAnalyzeView(ModelAnalyzeView):
# ここのメソッド追加
def start(self, request: WSGIRequest):
super().start(request)
if "gchart" not in self.nav_params or self.nav_params["gchart"] is None:
modelController = ModelListController(
self.nav_params["model_query"], self.nav_params["base_model"])
graphJsonData = modelController.generate_pl_graph_data()
self.nav_params["gchart_all"] = graphJsonData.convert_for_django()
# self.nav_params["gchart"] = self.nav_params[
# "gchart_all"][request.GET.get("bet")]
self.nav_params["target_bet_columns"] = graphJsonData.get_bet_columns()
self.nav_params["year_dict"] = {
col: {"test": [], "valid": []} for col in graphJsonData.get_bet_columns()}
for key, val in self.nav_params["year_dict"].items():
for mode, v in val.items():
v += list(graphJsonData.__dict__[
key].__dict__[mode].keys())
# getメソッドを修正
def get(self, request: WSGIRequest):
self.start(request)
return render(
request,
'model_analyze/analyze_pages/base_analyze_page.html',
self.nav_params
)
(新規) base_analyzer_dto.py
基礎分析用のデータtoオブジェクトクラスを管理するモジュール
from model_analyzer.models import ModelList
import pandas as pd
import pathlib
class GraphJsonMap:
def __init__(self) -> None:
self.tan: JsonTableItem = JsonTableItem()
self.fuku: JsonTableItem = JsonTableItem()
def get_bet_columns(self):
return ["tan", "fuku"]
def convert_for_django(self):
return {
bet: {
mode: {
key: val.json
for key, val in self.__dict__[bet].__dict__[mode].items()
}
for mode in ["valid", "test"]
}
for bet in self.get_bet_columns()
}
class JsonItem:
def __init__(self) -> None:
self.description: dict[str, object] = {}
self.data: list[pd.DataFrame] = []
self.json: str = None
class JsonTableItem:
def __init__(self) -> None:
self.train: dict[str, JsonItem] = {}
self.valid: dict[str, JsonItem] = {}
self.test: dict[str, JsonItem] = {}
class TableItem:
def __init__(self) -> None:
self.description: dict[str, object] = {}
self.data: pd.DataFrame = None
class DataTableItem:
def __init__(self) -> None:
self.train: dict[str, TableItem] = {}
self.valid: dict[str, TableItem] = {}
self.test: dict[str, TableItem] = {}
class BetDataItem:
def __init__(self, model_id: str, model_name: str) -> None:
self.model_id = model_id
self.model_name = model_name
self.tan: DataTableItem = DataTableItem()
self.fuku: DataTableItem = DataTableItem()
class ModelListProp:
def __init__(self, modellist: ModelList) -> None:
self.model_id = modellist.model_id
self.model_name = modellist.model_name
self.model_type = modellist.model_type
self.model_dir = pathlib.Path(modellist.model_dir)
self.model_analyze_dir = pathlib.Path(modellist.model_analyze_dir)
self.model_predict_dir = pathlib.Path(modellist.model_predict_dir)
self.bet_columns_map: dict[str, str] = modellist.bet_columns_map
self.pl_column_map: dict[str, str] = modellist.pl_column_map
self.rtn_hit_rate_file: dict[str, pathlib.Path] = {
k: pathlib.Path(v) for k, v in modellist.return_hit_rate_file.items()}
self.fav_bet_num_dir = {k: pathlib.Path(
v) for k, v in modellist.fav_bet_num_dir.items()}
self.pl_dir = {k: pathlib.Path(v)
for k, v in modellist.profit_loss_dir.items()}
self.odds_graph_file = {k: pathlib.Path(
v) for k, v in modellist.odds_graph_file.items()}
self.motivate = modellist.motivate
self.memo = modellist.memo
class DataMap:
def __init__(self, train, valid, test) -> None:
self.train: pd.DataFrame = train
self.valid: pd.DataFrame = valid
self.test: pd.DataFrame = test
class BetMap:
def __init__(self) -> None:
self.tan: DataMap = DataMap
self.fuku: DataMap = DataMap
class ModelInfoMap:
def __init__(self) -> None:
self.betData: BetMap = BetMap()
self.meta: ModelListProp = None
(新規) baseanalyzer_operator.py
モデル分析画面用のグラフ生成を管理するクラス
from django.core.handlers.wsgi import WSGIRequest
from django.db.models.query import QuerySet
from model_analyzer.models import ModelList
import pandas as pd
import pathlib
import gviz_api
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
ModelListProp, BetDataItem, ModelInfoMap, DataTableItem, DataMap, TableItem, GraphJsonMap)
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.profit_loss_graph import ProfitLossManager
class ModelListController:
def __init__(self, modelquery: QuerySet, basemodel: ModelList | None) -> None:
self.model_set = [ModelListProp(modellist) for modellist in modelquery]
self.basemodel = ModelListProp(basemodel)
def generate_pl_graph_data(self) -> GraphJsonMap:
targetModelMap, baseModelMap = self.load_data("pl_dir")
profitLossManager = ProfitLossManager(targetModelMap, baseModelMap)
return profitLossManager.generate_pl_graph_json()
def load_data(self, target_prop: str) -> tuple[ModelInfoMap | list[ModelInfoMap] | None]:
modelInfoMapList: list[ModelInfoMap] = []
for model_prop in self.model_set:
modelInfoMap = ModelInfoMap()
for bet, dpath in model_prop.__dict__[target_prop].items():
dfmap = {"train": None, "valid": None, "test": None}
for fpath in dpath.glob("*.csv"):
dfmap[fpath.stem] = pd.read_csv(fpath)
modelInfoMap.betData.__dict__[bet] = DataMap(**dfmap)
modelInfoMap.meta = model_prop
modelInfoMapList += [modelInfoMap]
baseModelInfoMap = None
if self.basemodel:
baseModelInfoMap = ModelInfoMap()
for bet, dpath in self.basemodel.__dict__[target_prop].items():
dfmap = {"train": None, "valid": None, "test": None}
for fpath in dpath.glob("*.csv"):
dfmap[fpath.stem] = pd.read_csv(fpath)
baseModelInfoMap.betData.__dict__[bet] = DataMap(**dfmap)
baseModelInfoMap.meta = self.basemodel
return modelInfoMapList, baseModelInfoMap
(更新) model_analyze_models.py
モデル情報を取得するための関数を追加
差分を表示します。
# 以下の関数を追加
def get_model_from_modelId(model_id) -> ModelList:
return ModelList.objects.filter(model_id=model_id).latest("regist_date")
モデルの収支グラフ表示機能の開発手順(WEB画面側)
以下のimageファイル、CSSファイル、JavaScriptファイル、HTMLファイルを新規作成・編集をしてください。
はじめに
ココだけの話、DjangoとGoogle Chartsを連携するとなるとかなり面倒でした。
DjangoにはテンプレートタグというHTMLソースへ情報を渡せる機能があるので、gviz_apiで作成した描画用のデータをレンダーのパラメタとして与えることで、ブラウザに情報を連携しています。
しかし、グラフ描画するにはJavaScriptでGoogle Chartsのライブラリを読み込んで、JavaScriptのスクリプトの中で、テンプレートタグで渡したデータを展開しGoogle Chartsのツールで読み込むといった処理を書かないといけません。
通常JavaScriptであれば、jsファイルを作ってそこにGoogle Chartsでの処理を書くのが普通だと思うのですが、そうした場合HTMLソース内で以下のようにJavaScriptのソースを読み込むコードを記載しないといけません。
<script src="/static/js/model_analyze/check_submit_mode.js"></script>
そのため、当初はこのJavaScriptのファイルにテンプレートタグを記載してGoogle Chartsのグラフ描画機能を実装していたのですが、大変困ったことにHTMLソースで上記のようなコードでJavaScriptのソースを読み込んでも、HTML内はそのjsファイル内のテンプレートタグを認識してくれませんでした。
つまり、jsファイルを読み込むやり方では、DjangoとGoogle Chartsの連携は不可能となります。(他にうまいやり方があるのかもしれませんが、調べた限り有効な手段がありませんでした。)
なので、テンプレートタグの問題はテンプレートタグで解決しようという考えの元、Djangoのincludeというテンプレートタグを用いて、HTMLソースをHTMLソース内で展開するようにします。
つまり、以下のコードをHTMLソース内に記載することで、別のHTMLソースをHTMLソース内に展開することができるというDjangoのテンプレートタグの素敵機能を使います。
こうすることで、HTMLソースをJavaScriptのソースとみなしてグラフ描画機能のソース管理を行います。
{% include 'model_analyze/analyze_pages/scripts/base_analyze/profit_loss_graph.html' %}
かなりの力技ですが、DjangoとGoogle Chartsの連携をするには、HTMLソース内にJavaScriptのソースを直接記載するしか手はありません。頑張りましょう。
イメージファイルの追加
今回もイメージファイルを追加しました。グラフ描画を楽に行える「前へ」ボタンと「次へ」ボタンにある変な矢印マークがそうです。そのSVGファイルを以下のリンクからダウンロードして頂いて、それぞれ「caret-left-square.svg
」と「caret-left-square.svg
」ファイルとして「static/images
」フォルダに設置してください。
https://icons.getbootstrap.jp/assets/icons/caret-left-square.svg
https://icons.getbootstrap.jp/assets/icons/caret-right-square.svg
CSSファイル
#select-models {
position: fixed;
height: auto;
min-width: 70%;
max-width: 70%;
left: 0%;
z-index: 10;
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ファイル
最初の2つがHTMLファイルと見せかけたJavaScriptソースです。
<script text="text/javascript">
const setYearList = (yearlist) => {
const yearElem = document.getElementById("selectYear");
while (yearElem.firstChild) {
yearElem.removeChild(yearElem.firstChild);
}
yearlist.forEach((year) => {
const option = document.createElement('option');
option.value = year;
option.innerHTML = year;
yearElem.appendChild(option);
});
}
function listenGraphSelect(e) {
selectedBet = null;
betMode = document.getElementById("selectBetMode");
Array.from(betMode.options).forEach(function (option) {
if (option.selected) {
selectedBet = option.innerHTML;
}
})
selectedDataset = null;
datasetMode = document.getElementById("selectDataSet");
Array.from(datasetMode.options).forEach(function (option) {
if (option.selected) {
selectedDataset = option.innerHTML;
}
})
{% autoescape off %}
var year_dict = JSON.parse(JSON.stringify({{ year_dict }}));
{% endautoescape %}
if (selectedBet == "" || selectedDataset == "") {
var yearlist = [];
}
else {
var yearlist = year_dict[selectedBet][selectedDataset];
}
setYearList(yearlist);
}
const betElem = document.getElementById("selectBetMode");
betElem.addEventListener("change", (e) => listenGraphSelect(e));
const datasetElem = document.getElementById("selectDataSet");
datasetElem.addEventListener("change", (e) => listenGraphSelect(e));
const graphModeElem = document.getElementById("selectGraphMode");
graphModeElem.addEventListener("change", (e) => {
selectedYear = null;
predFlag = true;
nextFlag = true;
yearMode = document.getElementById("selectYear");
Array.from(yearMode.options).forEach(function (option, idx, array) {
if (option.selected) {
selectedYear = option.innerHTML;
if (idx !== 0) {
predFlag = false;
}
if (idx !== array.length - 1) {
nextFlag = false;
}
}
});
const changeBtn = document.querySelector("#changeGraphBtn");
if (selectedYear) {
changeBtn.disabled = false;
} else {
changeBtn.disabled = true;
}
const predBtn = document.querySelector("#graphLeftBtn");
predBtn.disabled = predFlag;
const nextBtn = document.querySelector("#graphRightBtn");
nextBtn.disabled = nextFlag;
});
</script>
<script type="text/javascript">
function changeChart(num = 0) {
var targetIdx = changeSelectDatas(num);
google.charts.load('current', { 'packages': ['corechart', 'annotationchart'] });
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 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>
{% 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 class="d-flex container-fluid flex-column border border-dark-subtle rounded pt-3 ps-4 mt-3 mb-5"
style="width: 90%; height: 1800px;">
<div class="row row-cols-lg-auto align-items-center justify-content-center" id="selectGraphMode">
<div class="col">
<button type="button" class="btn btn-outline-secondary" id="graphLeftBtn" disabled onclick="changeChart(-1)">
<img src="/static/images/caret-left-square.svg">
前へ
</button>
</div>
<div class="col">
<label for="selectBetMode" class="form-label">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">
<label for="selectDataSet" class="form-label">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">
<label for="selectYear" class="form-label">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">
<button id="changeGraphBtn" type="button" class="btn btn-primary" disabled onclick="changeChart()">描画</button>
</div>
<div class="col">
<button type="button" class="btn btn-outline-secondary" id="graphRightBtn" disabled onclick="changeChart(1)">
次へ
<img src="/static/images/caret-right-square.svg">
</button>
</div>
</div>
<div class="row mt-3">
<div class="fs-4 text-center fw-bold">
収支グラフ
</div>
</div>
<div class="row pe-2" style="height: 30%;">
<div id="plGraphChart" class="border border-dark-subtle rounded"></div>
</div>
<div class="row">
<div id="return_hit_rate"></div>
</div>
</div>
{% endblock %}
{% block ext-jsblock %}
{% include 'model_analyze/analyze_pages/scripts/base_analyze/graph_controller.html' %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{% include 'model_analyze/analyze_pages/scripts/base_analyze/profit_loss.html' %}
{% endblock %}
{% extends "model_analyze/model_analyze.html" %}
{% block ext-cssblock %}
{% endblock %}
{% block gCharts %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{% endblock %}
{% block analyze-title %}
{% endblock %}
{% block analyze-content %}
<div class="row border border-dark-subtle rounded p-1 mt-3"></div>
{% endblock %}
{% block ext-jsblock %}
{% endblock %}
{% extends "model_analyze/model_analyze.html" %}
{% block ext-cssblock %}
{% endblock %}
{% block gCharts %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{% endblock %}
{% block analyze-title %}
{% endblock %}
{% block analyze-content %}
<div class="row border border-dark-subtle rounded p-1 mt-3"></div>
{% endblock %}
{% block ext-jsblock %}
{% endblock %}
{% extends "base_layout.html" %}
{% block cssblock %}
<link rel="stylesheet" href="/static/css/model_analyze/style.css">
{% block ext-cssblock %}
{% endblock %}
{% endblock %}
{% block content %}
<div class="d-flex flex-column w-100">
<div class="col d-flex">
{% include 'model_analyze/parts/select_models.html' %}
{% include 'model_analyze/parts/offcanvas_analyze_list.html' %}
{% block analyze-title %}{% endblock %}
</div>
<div class="col d-flex justify-content-center">
{% block analyze-content %}
{% endblock %}
</div>
</div>
{% endblock %}
{% block jsblock %}
<script src="/static/js/model_analyze/sidebar_slide.js"></script>
<script src="/static/js/model_analyze/validate_form.js"></script>
<script src="/static/js/model_analyze/check_submit_mode.js"></script>
{% block ext-jsblock %}
{% endblock %}
{% endblock %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{% block cssblock %}
{% endblock %}
{% block gCharts %}
{% endblock %}
<title>競馬予想ソフト</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<header>
<nav class="navbar navbar-expand bg-body-secondary fixed-top" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="/index.html">um-AI</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
</li>
<li class="nav-item">
{% if model_analyze %}
<a class="nav-link active" href="/model-analyze">モデル分析</a>
{% else %}
<a class="nav-link disabled" href="/model-analyze" tabindex="-1" aria-disabled="true">モデル分析</a>
{% endif %}
</li>
<li class="nav-item">
{% if model_manage %}
<a class="nav-link active" href="/model-manage">モデル管理</a>
{% else %}
<a class="nav-link disabled" href="/model-manage" tabindex="-1" aria-disabled="true">モデル管理</a>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="mt-3"></div>
{% if messages %}
<div class="container-fluid">
<div class="notification is-info">
{% for message in messages %}
{% if message.tags != 'error' %}
<div {% if message.tags %}class="alert alert-{{ message.tags }}" {% endif %} id="success-alert"
style="font-size: small;">
{% else %}
<div {% if message.tags %}class="alert alert-danger" {% endif %} id="success-alert" style="font-size: small;">
{% endif %}
{{ message }}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<main class="d-flex container-fluid h-100" style="margin-top: 66px;">
{% block content %}
<!-- ここに個別のhtmlが入る -->
{% endblock %}
</main>
{% block jsblock %}
{% endblock %}
<script src="/static/js/base_layout/fade_popup.js"></script>
</body>
</html>
サーバの起動と画面の確認
それでは、モデル選択機能と分析画面ページ作成ができたので、サーバを起動しましょう。
manage.pyファイルがあるフォルダがカレントディレクトリになっていることを確認して、コマンドプロンプトで以下のコマンドを実行
python manage.py runserver
http://localhost:8000/model-analyze へアクセスしましょう。
その後適当なモデルを選択して「分析モード」ボタンより表示されるサイドバーから「基礎分析」の画面を表示してください。
しばらく時間を要しますが、以下のモデルの基礎分析画面が表示されればOKです。
簡単な動作確認
今回はとりわけ追加の隠し機能はないので、動作確認は完成イメージ(GIF)通りになっていればOKです。
ソース公開しました!
Bookersでロードマップ4で解説したソースを公開しました!
以下のリンクへ飛んでいただき、BookersとYouTube連携して私のチャンネルを登録すると無料でWEBアプリのソースが手に入ります。
良ければ、実際に触って遊んでみてください!
コメント