PR

ロードマップ4: Djangoで作る競馬予想AIモデル分析ソフト part7

DevOps
この記事は約74分で読めます。
スポンサーリンク
スポンサーリンク

はじめに

私は競馬予想プログラムソフトの制作過程を動画で投稿している者です。

ここでは、モデル分析用のWEBアプリの開発手順を話していきます。

現在作成している競馬予想プログラムソフトの概要は以下を参照ください。

スポンサーリンク

本プログラムの前提

本プログラムでは以下の前提を置いています。

  • Windows 10
  • Python3.10.5
  • Django ver5.0.4
  • 競馬予想プログラムで作成したモデルを分析する目的で使います。
  • Bookersで公開中のモデル分析管理クラスと連携してモデル分析を行います。

とりわけ、最後の2つに関しては有料記事でソースを公開しているため、まったく同じ環境で競馬予想プログラムの作成とモデルの分析を行いたい場合は以下のBookers記事一覧から記事を購入ください。

また、競馬予想プログラムの制作過程については大まかな概要は以下の再生リストから参照ください。

詳細な解説は以下の記事一覧を参照ください。

競馬予想プログラム
「競馬予想プログラム」の記事一覧です。
スポンサーリンク

モデル分析画面:モデルの回収率と的中率のテーブル表示機能

今回も前回に続いてモデル分析画面の基礎分析画面の開発に入っていきます。基礎分析画面では、選択したモデルに対して収支グラフ、回収率と的中率、人気別ベット回数を可視化する画面です。

今回のパートでは、モデルを選択後、基礎分析画面へ遷移すると表示されるモデルの回収率と的中率のテーブル表示をする機能を作成します。

以下に完成イメージGIFをお見せします。

完成イメージ(GIF)

モデルを選択後、基礎分析画面の表示ボタンをクリックするのですが、モデルインポート時に登録した各種分析結果のCSVファイルを読み込むので、基礎分析画面が表示されるまで少し時間がかかります。

上記のように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>
  ┃      ┃      ┃      ┃      ┃     ┣ profit_loss_graph.py
  ┃      ┃      ┃      ┃      ┃     ┣ return_hit_rate_table.py
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ <model_info_list_page>
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ <ogs_page>
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ base_analyzer_dto.py
  ┃      ┃      ┃      ┃      ┣ analyze_viewer_operator.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_graph.html
  ┃      ┃      ┃    ┃     ┃            ┣ rtn_hit_rate_table.html
  ┃      ┃      ┃    ┃     ┃            ┣ table_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側)

それでは、実際に変更および追加されたソースを一つずつ見せていきます。基本はこの手順通りにソースを変更して頂ければ。

(更新) profit_loss_graph.py

収支グラフ描画用のクラスです。かなり複雑ですがこのクラスの構成と似たような形で今後開発する回収率と的中率、人気別ベット回数の可視化を行います。

(省略)
# class ProfitLossManager
    # 以下メソッドを丸っと置き換え
    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():
            if bet not in graphJsonMap.get_bet_columns():
                continue
            for target_map in [base_map]+target_map_list:
                if target_map is None:
                    continue

                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 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

(新規) return_hit_rate_table.py

回収率と的中率のテーブルを表示するためのメインプログラムです。

from django.db.models.query import QuerySet
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
    BetFileDataItem, DataFile, ModelListProp, BetDataItem, ModelInfoFileMap, DataTableItem, DataMap, RtnHitRateJsonItem, RtnHitRateJsonTableItem, 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 ReturnHitRateManager:
    def __init__(self, targetModelMap: list[ModelInfoFileMap], baseModelMap: ModelInfoFileMap | None, prop_key: str) -> None:
        self.targetModelMap = targetModelMap
        self.baseModelMap = baseModelMap
        self.prop_key = prop_key
        dataset_name = ["test", "valid", "train"]
        self.description_temp = {
            "model": ("string", "model")
        } | {
            f"回収率_{mode}": ("number", f"回収率_{mode}")
            for mode in dataset_name
        } | {
            f"的中率_{mode}": ("number", f"的中率_{mode}")
            for mode in dataset_name
        }

    def generate_return_hit_rate_table_json(self) -> GraphJsonMap:
        graphDataMap = self.generate_return_hit_rate_table_data()
        graphJsonMap = GraphJsonMap(mode="return_hit_rate")

        target_map_list: list[BetFileDataItem] = graphDataMap["target"]
        base_map: BetFileDataItem = graphDataMap["base"]

        for bet in graphJsonMap.get_bet_columns():
            betHitRateJsonTableItem = RtnHitRateJsonTableItem()
            for target_map in target_map_list:
                target_bet_map: TableItem = target_map.__dict__[bet]
                if target_bet_map.data is None:
                    continue
                jsonItem = RtnHitRateJsonItem()
                jsonItem.description |= target_bet_map.description
                jsonItem.data = target_bet_map.data.to_dict(orient="records")

                data_table = gviz_api.DataTable(jsonItem.description)
                data_table.LoadData(jsonItem.data)

                columns_order = list(jsonItem.description.keys())
                jsonItem.json = data_table.ToJSon(
                    columns_order=columns_order, order_by=columns_order[0])
                betHitRateJsonTableItem.target |= {
                    target_map.model_id: jsonItem}
            if base_map is not None and base_map.__dict__[bet]:
                base_bet_map: TableItem = base_map.__dict__[bet]
                if base_bet_map.data is None:
                    continue
                jsonItem = RtnHitRateJsonItem()
                jsonItem.description |= base_bet_map.description
                jsonItem.data = base_bet_map.data.to_dict(orient="records")

                data_table = gviz_api.DataTable(jsonItem.description)
                data_table.LoadData(jsonItem.data)

                columns_order = list(jsonItem.description.keys())
                jsonItem.json = data_table.ToJSon(
                    columns_order=columns_order, order_by=columns_order[0])
                betHitRateJsonTableItem.base |= {base_map.model_id: jsonItem}
                betHitRateJsonTableItem.base_id = base_map.model_id
            graphJsonMap.__dict__[bet] = betHitRateJsonTableItem

        return graphJsonMap

    def generate_return_hit_rate_table_data(self) -> dict[str, BetFileDataItem | list[BetFileDataItem] | None]:

        graphDataMap: dict[str, BetFileDataItem | list[BetFileDataItem] | 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: ModelInfoFileMap | None, base: bool = False) -> BetFileDataItem | None:
        if tempMap is None:
            return tempMap
        model_id = "base_" + tempMap.meta.model_id if base else tempMap.meta.model_id
        betdataitem = BetFileDataItem(model_id, tempMap.meta.model_name)
        for bet, dataMap in tempMap.betData.__dict__.items():
            if dataMap is None:
                continue
            dataitem: TableItem = self.create_datatable_item(
                bet, dataMap, tempMap)
            betdataitem.__dict__[bet] = dataitem

        return betdataitem

    def create_datatable_item(
        self,
        bet: str,
        dataMap: DataFile,
        tempMap: ModelInfoFileMap
    ) -> TableItem:
        if bet not in tempMap.meta.__dict__[self.prop_key]:
            return TableItem()

        idf: pd.DataFrame = dataMap.data

        return self.create_data_map(idf,)

    def create_data_map(
        self,
        idf: pd.DataFrame
    ) -> TableItem:

        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.description = self.description_temp.copy()
        item.data = idf.copy()
        for col in list(item.description.keys())[1:]:
            item.data[col] = item.data[col].apply(
                lambda d: (d, f'{round(100*d, 3)}%'))
        return item

(更新) views.py

基礎分析画面のViewクラスです。変更箇所が多いので丸っと置き換えてください。

from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models, get_model_from_modelId
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 model_analyzer.main_viewer.analyze_viewer.analyze_viewer_operator import ModelListController
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
    ModelListProp, BetDataItem, ModelInfoMap, DataTableItem, DataMap, TableItem)
from django.core.cache import cache
import pandas as pd
import pathlib
import datetime


class BaseAnalyzeView(ModelAnalyzeView):

    def start(self, request: WSGIRequest):
        super().start(request)

    def start_post(self, request: WSGIRequest):
        urls = super().start_post(request)
        if len(urls.split("?")[0].split("/")) > 1 and urls.split("?")[0].split("/")[-1] != "base-analyze":
            for p in ["gchart_all", "target_bet_columns", "year_dict", "rtnHitTable", "rtnHitTableName"]:
                if p in self.nav_params:
                    self.nav_params.pop(p)
        return urls

    def generate_graph(self):
        if "gchart_all" not in self.nav_params or self.nav_params["gchart_all"] 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["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())

        if "rtnHitTable" not in self.nav_params or self.nav_params["rtnHitTable"] is None:
            rtnHitController = ModelListController(
                self.nav_params["model_query"], self.nav_params["base_model"])
            rtnHitRateJsonData = rtnHitController.generate_return_hit_rate_table()
            self.nav_params[
                "rtnHitTable"] = rtnHitRateJsonData.convert_rtn_hit_rate_for_django()

            self.nav_params["rtnHitTableName"] = {
                "target": rtnHitRateJsonData.target_model_list, "base": rtnHitRateJsonData.base_id}

        cache.clear()

    def get(self, request: WSGIRequest):
        self.start(request)
        self.generate_graph()

        return render(
            request,
            'model_analyze/analyze_pages/base_analyze_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/base_analyze_page.html', self.nav_params)

(新規) analyze_viewer_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,
    ModelInfoFileMap, BetFileMap, DataFile)
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.profit_loss_graph import ProfitLossManager
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.return_hit_rate_table import ReturnHitRateManager


class ModelListController:
    def __init__(self, modelquery: QuerySet, basemodel: ModelList | None) -> None:
        self.model_set = [ModelListProp(modellist) for modellist in modelquery]
        self.basemodel = None
        if basemodel is not None:
            self.basemodel = ModelListProp(basemodel)

    def generate_pl_graph_data(self) -> GraphJsonMap:
        targetModelMap, baseModelMap = self.load_dir_data("pl_dir")
        profitLossManager = ProfitLossManager(targetModelMap, baseModelMap)
        return profitLossManager.generate_pl_graph_json()

    def generate_return_hit_rate_table(self) -> GraphJsonMap:
        targetModelMap, baseModelMap = self.load_file_data("rtn_hit_rate_file")
        returnHitRateManager = ReturnHitRateManager(
            targetModelMap, baseModelMap, "rtn_hit_rate_file")
        return returnHitRateManager.generate_return_hit_rate_table_json()

    def load_file_data(self, target_prop: str) -> tuple[ModelInfoFileMap | list[ModelInfoFileMap] | None]:
        modelInfoMapList: list[ModelInfoFileMap] = []
        for model_prop in self.model_set:
            modelInfoMap = ModelInfoFileMap()
            for bet, fpath in model_prop.__dict__[target_prop].items():
                modelInfoMap.betData.__dict__[
                    bet] = DataFile(data=pd.read_csv(fpath))
                modelInfoMap.meta = model_prop
            modelInfoMapList += [modelInfoMap]

        baseModelInfoMap = None
        if self.basemodel:
            baseModelInfoMap = ModelInfoFileMap()
            for bet, fpath in self.basemodel.__dict__[target_prop].items():
                baseModelInfoMap.betData.__dict__[
                    bet] = DataFile(data=pd.read_csv(fpath))
                baseModelInfoMap.meta = self.basemodel

        return modelInfoMapList, baseModelInfoMap

    def load_dir_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

(更新) analyze_viewer_operator.py

モデル分析画面で描画するデータのData To Objectsクラスです。丸っと置き換えてください。

from model_analyzer.models import ModelList
import pandas as pd
import pathlib


class GraphJsonMap:
    def __init__(self, mode: str = "profit_loss") -> None:
        if mode == "return_hit_rate":
            self.tan: RtnHitRateJsonTableItem = RtnHitRateJsonTableItem()
            self.fuku: RtnHitRateJsonTableItem = RtnHitRateJsonTableItem()
        if mode == "profit_loss":
            self.tan: JsonTableItem = JsonTableItem()
            self.fuku: JsonTableItem = JsonTableItem()

        self.target_model_list = []
        self.base_id = None

    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()
        }

    def convert_rtn_hit_rate_for_django(self):

        for bet in self.get_bet_columns():
            self.target_model_list += list(self.__dict__[bet].target.keys())
        self.target_model_list = list(set(self.target_model_list))

        for bet in self.get_bet_columns():
            if self.__dict__[bet].base_id is None:
                continue
            else:
                self.base_id = self.__dict__[bet].base_id
                break
        return {
            bet: {
                "target": {
                    key: val.json
                    for key, val in self.__dict__[bet].__dict__["target"].items()
                },
                "base": {
                    key: val.json
                    for key, val in self.__dict__[bet].__dict__["base"].items()
                }
            }
            for bet in self.get_bet_columns()
        }


class RtnHitRateJsonItem:
    def __init__(self) -> None:
        self.description: dict[str, object] = {}
        self.data: pd.DataFrame = None
        self.json: str = None


class RtnHitRateJsonTableItem:
    def __init__(self) -> None:
        self.target: dict[str, RtnHitRateJsonItem] = {}
        self.base: dict[str, RtnHitRateJsonItem] = {}
        self.base_id: str | None = None


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 BetFileDataItem:
    def __init__(self, model_id: str, model_name: str) -> None:
        self.model_id = model_id
        self.model_name = model_name
        self.tan: TableItem = TableItem()
        self.fuku: TableItem = TableItem()


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(
            pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),)
        self.fuku: DataMap = DataMap(
            pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),)


class ModelInfoMap:
    def __init__(self) -> None:
        self.betData: BetMap = BetMap()
        self.meta: ModelListProp = None


class DataFile:
    def __init__(self, data: pd.DataFrame) -> None:
        self.data: pd.DataFrame = data


class BetFileMap:
    def __init__(self) -> None:
        self.tan: DataFile = DataFile(pd.DataFrame())
        self.fuku: DataFile = DataFile(pd.DataFrame())


class ModelInfoFileMap:
    def __init__(self) -> None:
        self.betData: BetFileMap = BetFileMap()
        self.meta: ModelListProp = None

(更新) views.py

メインのViewクラスです。メモリひっ迫問題をここで解決しています。

# 追加インポート
from django.core.cache import cache

# class ModelAnalyzeView
    # 以下のメソッドを置き換え
    def start(self, request: WSGIRequest):
        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()  # 追加箇所です:キャッシュクリアしてます

    # 以下のメソッドを置き換え
    def start_post(self, request: WSGIRequest):
        self.start(request)
        self.nav_params = get_select_models(
            self.nav_params,
            request.POST.getlist("target-model"),
            request.POST.get("base-model")
        )
        self.nav_params["analyze_list_disable"] = False
        return self.redirect_analyze(request)
スポンサーリンク

モデルの回収率と的中率のテーブル表示機能の開発手順(WEB画面側)

以下のCSSファイル、JavaScriptファイル、HTMLファイルを新規作成・編集をしてください。

CSSファイル

今回は変更箇所なしです。

JavaScriptファイル

今回は変更箇所なしです。

HTMLファイル


<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 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)
  });

  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;
  if (next_model_idx == 0 & target_model_name_list.length > 1) {
    const rightBtn = document.querySelector("#tableRightBtn");
    rightBtn.disabled = false;
    const leftBtn = document.querySelector("#tableLeftBtn");
    leftBtn.disabled = true;
  } else if (next_model_idx == target_model_name_list.length - 1 & target_model_name_list.length > 1) {
    const rightBtn = document.querySelector("#tableRightBtn");
    rightBtn.disabled = true;
    const leftBtn = document.querySelector("#tableLeftBtn");
    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>
<script text="text/javascript">

  {% autoescape off %}
  target_model_name_list = JSON.parse(JSON.stringify({{ rtnHitTableName.target }}));
  {% endautoescape %}
  const displayTargetName = document.querySelector("#targetRtnHitTableChart");
  displayTargetName.innerHTML = target_model_name_list[0];

  if (target_model_name_list.length > 1) {
    const rightBtn = document.querySelector("#tableRightBtn");
    rightBtn.disabled = false;
  }
</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 {% if rtnHitTableName.base is not None %} style="width: 90%; height: 2005px;" {% else %}
  style="width: 90%; height: 1610px;" {% endif %}
  class="d-flex container-fluid flex-column border border-dark-subtle rounded pt-3 ps-4 mt-3 mb-5 shadow">
  <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 text-nowrap" 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 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">
      <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">
      <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">
      <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 text-nowrap" 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 me-1 border in-shadow border-dark-subtle rounded" style="height: 600px;">
    <div id="plGraphChart"></div>
  </div>
  <div class="row mt-3" id="return_hit_rate">
    <div class="fs-4 text-center fw-bold">
      モデルの回収率と的中率
    </div>
  </div>
  <div {% if rtnHitTableName.base is not None %} style="height: 800px;" {% else %}style="height: 405px;" {% endif %}
    class="d-flex flex-row row in-shadow border border-dark-subtle rounded me-1 p-2">
    <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: 365px;">
      <div id="targetRtnHitTableChart" class="d-flex justify-content-center align-items-center"></div>
    </div>
    {% if rtnHitTableName.base is not None %}
    <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: 365px;">
      <div id="baseRtnHitTableChart" class="d-flex justify-content-center align-items-center"></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' %}

{% endblock %}
スポンサーリンク

サーバの起動と画面の確認

それでは、モデル選択機能と分析画面ページ作成ができたので、サーバを起動しましょう。

manage.pyファイルがあるフォルダがカレントディレクトリになっていることを確認して、コマンドプロンプトで以下のコマンドを実行

python manage.py runserver

http://localhost:8000/model-analyze へアクセスしましょう。

その後適当なモデルを選択して「分析モード」ボタンより表示されるサイドバーから「基礎分析」の画面を表示してください。

しばらく時間を要しますが、以下のモデルの基礎分析画面が表示されればOKです。

簡単な動作確認

今回はとりわけ追加の隠し機能はないので、動作確認は完成イメージ(GIF)通りになっていればOKです。

スポンサーリンク

ソース公開しました!

Bookersでロードマップ4で解説したソースを公開しました!

以下のリンクへ飛んでいただき、BookersとYouTube連携して私のチャンネルを登録すると無料でWEBアプリのソースが手に入ります。

無料!競馬予想AIモデル分析基盤um-AI
競馬予想プログラムソフトの開発をしている者です。今回は第一弾から第三弾記事を使って作った競馬予想モデルを分析できるWEBアプリを公開します…

良ければ、実際に触って遊んでみてください!

スポンサーリンク

前回記事

モデルの収支グラフ描画機能

スポンサーリンク

次回記事

モデルの人気別ベット回数の円グラフ表示機能

コメント

  1. おきく より:

    いつも勉強させて頂いております。

    前回記事のモデル収支描画までは、正常に動作していたのですが今回の記事ソースを反映したところ、GIFのようにグラフの表示ボタンを押下したところ「django ‘dict_filter’ is not a registered tag library. must be one of:」とエラーが出てしまいます。
    原因がわからず、苦戦しております。
    もし解決方法ご存知でしたらご教授頂けませんでしょうか。
    よろしくお願いします

    • oyakata より:

      お世話になっております。
      本シリーズを追っていただいてること大変うれしいです!ありがとうございます。
      本題についてですが、’dict_filter’というテンプレートタグのフィルタを自作していた時期があり、
      それがソースに残ってしまっていたみたいです。

      記事の方も更新しておきましたが、
      対応策として以下のHTMLの先頭行にある「{% load dict_filter %}」を削除してDjangoサーバを再起動してみてください。
      app_keiba/templates/model_analyze/analyze_pages/scripts/base_analyze/profit_loss_graph.html

      それでも解決しなかった場合は、お手数ですがエラー内容も含めて再度ご連絡ください。

      • おきく より:

        回答いただきありがとうございました。
        無事事象も解決して、GIF通り正常に動作するようになりました。

        これからも、たくさん勉強させていただきます!
        ありがとうございました!

タイトルとURLをコピーしました