PR

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

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

はじめに

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

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

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

スポンサーリンク

本プログラムの前提

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

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

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

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

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

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

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

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

今回のパートでは、モデルを選択後、基礎分析画面へ遷移して馬券種別、データセットの種類、年度を選択後「描画」を押下で、人気別ベット回数の円グラフを表示する機能を作成します。

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

完成イメージ(GIF)

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

ベースラインモデルを選択した場合

2重円のグラフが描画されます。内側の円グラフがベースラインのモデルで、外側の円グラフがターゲットモデルです。

内側の円グラフと比較して、外側の円グラフだと人気別ベット回数がどう変わっているかを見ることができます。こうすることで、ターゲットモデルはベースラインのモデルと比べてベット回数にどのくらい変化があったかを確認できるようになります。

ベースラインのモデルを選択しなかった場合

ベースラインのモデルを選択しなかった場合は、ひとつの円グラフになります。

スポンサーリンク

フォルダ構成

今回の実装でもフォルダ構成を変えています

前回記事のフォルダ構成をベースに追加箇所を赤字にしています。注意して変更してください。

<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>
  ┃      ┃      ┃      ┃      ┃     ┗ 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>
  ┃      ┃      ┃    ┃     ┃            ┣ fav_bet_num_chart.html ※ 人気別ベット回数を描画するJavaScript
  ┃      ┃      ┃    ┃     ┃            ┣ graph_controller.html
  ┃      ┃      ┃    ┃     ┃            ┣ profit_loss_graph.html
  ┃      ┃      ┃    ┃     ┃            ┣ rtn_hit_rate_table.html
  ┃      ┃      ┃    ┃     ┃            ┗ table_controller.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側)

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

(新規) fav_bet_num_chart.py

人気別ベット回数グラフ描画用のクラスです。収支グラフの描画用クラスと同様の形式のクラスです。ここまで来るとベースのクラスを作って継承しろって話ですけど、もう面倒くさかったです。今後のメンテ考えればベースクラス用意した方が絶対に今後面倒にならないです。けど、面倒くさかったです。

from django.db.models.query import QuerySet
from model_analyzer.main_viewer.analyze_viewer.base_analyzer_dto import (
    FavBetNumJsonChartItem, FavBetNumJsonItem, 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 FavBetNumManager:
    def __init__(self, targetModelMap: list[ModelInfoMap], baseModelMap: ModelInfoMap, prop_key: str) -> None:
        self.targetModelMap = targetModelMap
        self.baseModelMap = baseModelMap
        self.prop_key = prop_key
        self.description_temp = {
            "betFav": ("string", "人気"),
            "betNum": ("number", "bet数")
        }

    def generate_fav_bet_num_chart_json(self) -> GraphJsonMap:
        graphDataMap = self.generate_fav_bet_num_chart_data()
        graphJsonMap = GraphJsonMap("fav_bet_num")

        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 mode in ["valid", "test"]:

                graph_item_dict: dict[str, FavBetNumJsonChartItem] = graphJsonMap.__dict__[
                    bet].__dict__[mode]
                for target_map in target_map_list:
                    if target_map is None:
                        continue

                    target_bet_map: DataTableItem = target_map.__dict__[bet]
                    target_bet_mode_map: dict[
                        str, TableItem] = target_bet_map.__dict__[mode]
                    if target_bet_mode_map is None:
                        continue

                    for year, target_item in target_bet_mode_map.items():
                        if year not in graph_item_dict:
                            graph_item_dict[year] = FavBetNumJsonChartItem()
                        chartItem = graph_item_dict[year]

                        jsonItem = FavBetNumJsonItem()
                        jsonItem.description |= self.description_temp.copy()
                        jsonItem.data = target_item.data

                        data_table = gviz_api.DataTable(jsonItem.description)
                        data_table.LoadData(
                            jsonItem.data.to_dict(orient="records"))
                        columns_order = list(jsonItem.description.keys())
                        jsonItem.json = data_table.ToJSon(
                            columns_order=columns_order, order_by=columns_order[0])

                        chartItem.target |= {
                            target_map.model_id: jsonItem}
                if base_map is not None:
                    if base_map is None:
                        continue

                    target_bet_map: DataTableItem = base_map.__dict__[bet]
                    target_bet_mode_map: dict[
                        str, TableItem] = target_bet_map.__dict__[mode]
                    if target_bet_mode_map is None:
                        continue

                    for year, target_item in target_bet_mode_map.items():
                        chartItem = graph_item_dict[year]

                        jsonItem = FavBetNumJsonItem()
                        jsonItem.description |= self.description_temp.copy()
                        jsonItem.data = target_item.data

                        data_table = gviz_api.DataTable(jsonItem.description)
                        data_table.LoadData(
                            jsonItem.data.to_dict(orient="records"))

                        columns_order = list(jsonItem.description.keys())
                        jsonItem.json = data_table.ToJSon(
                            columns_order=columns_order, order_by=columns_order[0])

                        chartItem.base |= {
                            base_map.model_id: jsonItem}
                        chartItem.base_id = base_map.model_id

        return graphJsonMap

    def generate_fav_bet_num_chart_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
        model_id = "base_" + tempMap.meta.model_id if base else tempMap.meta.model_id
        betdataitem = BetDataItem(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)
            betdataitem.__dict__[bet] = dataitem

        return betdataitem

    def create_datatable_item(
        self,
        bet: str,
        dataMap: DataMap,
        tempMap: ModelInfoMap
    ) -> DataTableItem:
        if bet not in tempMap.meta.__dict__[self.prop_key]:
            return DataTableItem()

        dfv: pd.DataFrame = dataMap.valid.set_index("model")
        dfv["year"] = dfv.index.str[:4].astype(int)
        dfv["year"] = dfv["year"].shift().fillna(
            f"{dfv['year'].min()-1}").astype(str).str[:4]
        dfv = dfv.groupby("year").sum()

        dft: pd.DataFrame = dataMap.test.set_index("model")
        dft["year"] = dft.index.str[:4]
        dft = dft.groupby("year").sum()

        dataitem = DataTableItem()
        dataitem.valid = self.create_data_map(dfv)
        dataitem.test = self.create_data_map(dft)

        return dataitem

    def create_data_map(
        self,
        idf: pd.DataFrame,
    ) -> dict[str, TableItem]:
        data_map = {}
        for year in idf.index:
            item = TableItem()
            dftmp = idf.loc[year]
            srs = pd.Series(data=0, index={f"{num}人気" for num in range(1, 19)})
            srs.loc[dftmp.index] = dftmp
            dftmp = pd.DataFrame(
                srs, columns=["betNum"]).reset_index(names="betFav")
            item.data = dftmp.copy()
            data_map[year] = item
        return data_map

(更新) views.py

実はDjangoサーバ、ページ遷移(例えばモデル分析画面のページ)したタイミングでルートに対応するViewクラスが実行されるのですが、その後別のURLルート(例えばモデル管理画面)へ遷移したときに、遷移前のページで実行されたViewクラスはインスタンスが削除されずに残ったままになっています。

つまり、再度モデル管理画面からモデル分析画面にページを移動したとき、前回実行済のViewクラスが呼び出されるみたいです。

この何が困るかって言うと、HTMLソースにレンダリング用のパラメータ(nav_params)をメンバ変数としていたので、最初にモデル分析画面から基礎分析画面を表示した際に、選択したモデルのCSVデータをすべて読み込んだものを「nav_params」変数へ格納しています。

つまり、その後モデル管理画面へ移動して、モデル分析画面へ戻ってきた際に「nav_params」の中に既に前回選択していたモデルのCSVデータが残ったままになっています。

なので、最初に「ベースモデル」を選択して基礎分析画面を表示 → モデル管理画面へ移動 → 再度モデル分析画面で「ベースモデル」を選択せずに基礎分析画面を表示 → なぜかベースモデルが選択された状態になっているという不可解なバグに陥ります。(多分パート7までのソースだとそうなってるはずです。再現性があるかは各自で確認ください。)

上記のようなバグが起きないように、今回の修正ではモデル分析画面関連のViewクラスが呼び出された際に、ページ移動前のURL情報を使ってモデル分析画面外のページから遷移してきた場合は、事前にnav_paramsに残っている不要な変数を削除する処理を追加しました。

こうすることで、不可解なバグもなくなり完全にメモリひっ迫の現象に陥らなくなったと思います。

# import文のすぐ下にモジュール内変数として以下を追加ください。
THISPAGENAME = "base-analyze"

# class BaseAnalyzeView
    # 以下メソッドを丸っと置き換え
    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 generate_graph(self):
        modelController = ModelListController(
            self.nav_params["model_query"], self.nav_params["base_model"])
        if "gchart_all" not in self.nav_params or self.nav_params["gchart_all"] is None:
            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:
            rtnHitRateJsonData = modelController.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}
        if "favBetNumChart" not in self.nav_params or self.nav_params["favBetNumChart"] is None:
            favBetNumJsonData = modelController.generate_fav_bet_num_table()
            self.nav_params["favBetNumChart"] = favBetNumJsonData.convert_fav_bet_num_for_django(
            )

        cache.clear()

(更新) analyze_viewer_operator.py

基礎分析画面を描画するためのコントロールクラス。人気別ベット回数に関する描画データを持ってくるメソッドを追加しています。

# 以下import追加
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.fav_bet_num_chart import FavBetNumManager

# class ModelListController:
    # 以下メソッドを追加
    def generate_fav_bet_num_table(self) -> GraphJsonMap:
        targetModelMap, baseModelMap = self.load_dir_data("fav_bet_num_dir")
        favBetNumManager = FavBetNumManager(
            targetModelMap, baseModelMap, "fav_bet_num_dir")
        return favBetNumManager.generate_fav_bet_num_chart_json()

(更新) base_analyzer_dto.py

基礎分析画面を描画するためのオブジェクト to データクラスです。変更箇所は多くないですが差分を出すの面倒なので、丸写しで大丈夫です。サービスです。

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()
        if mode == "fav_bet_num":
            self.tan: FavBetNumJsonTableItem = FavBetNumJsonTableItem()
            self.fuku: FavBetNumJsonTableItem = FavBetNumJsonTableItem()

        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_fav_bet_num_for_django(self):
        return {
            bet: {
                mode: {
                    key: {
                        "target": {
                            modelName: modelJson.json
                            for modelName, modelJson in val.target.items()
                        },
                        "base": {
                            modelName: modelJson.json
                            for modelName, modelJson in val.base.items()
                        }
                    }
                    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 FavBetNumJsonItem:
    def __init__(self) -> None:
        self.description: dict[str, object] = {}
        self.data: pd.DataFrame = None
        self.json: str = None


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


class FavBetNumJsonTableItem:
    def __init__(self) -> None:
        self.train: dict[str, FavBetNumJsonChartItem] = {}
        self.valid: dict[str, FavBetNumJsonChartItem] = {}
        self.test: dict[str, FavBetNumJsonChartItem] = {}


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クラスでもキャッシュクリアの処理を入れました。ここまで過剰にする必要ないのかもしれませんが、nav_params関係のバグの原因に気づかなかった際に全Viewクラスで怒りのキャッシュ削除処理を入れたみたいです。

別になくてもメモリひっ迫しないのかもしれません。

(省略)

# class ModelAnalyzeView(View):
    # 以下のメンバ変数追加
    DELETE_ATTR = ["gchart_all", "target_bet_columns", "year_dict",
                   "rtnHitTable", "rtnHitTableName", "favBetNumChart"]
    # 以下メソッド追加
    def pop_unnecessary(self):
        for p in self.DELETE_ATTR:
            if p in self.nav_params:
                self.nav_params.pop(p)

    # 以下メソッドを丸っと置き換え
    def start(self, request: WSGIRequest):

        if "HTTP_REFERER" not in request.META.keys() or "model-analyze" not in request.META["HTTP_REFERER"].split("?")[0].split("/"):
            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()

    # 以下メソッドを丸っと置き換え
    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

        cache.clear()
        return self.redirect_analyze(request)

# class ModelManageView(View):
    # 以下メソッドを丸っと置き換え
    def start(self, request: WSGIRequest):
        generate_model_import_forms(self.nav_params, request)
        get_table_list(self.nav_params)
        cache.clear()

# 以下のモデル削除用のページ画面関数を丸っと置き換え
def modelDeleteView(request: WSGIRequest, number: int):
    model = ModelList.objects.get(id=number)
    target_model_list = ModelList.objects.filter(model_id=model.model_id)
    for imodel in target_model_list:
        imodel.delete()
    cache.clear()
    return redirect("/model-manage")
スポンサーリンク

モデルの人気別ベット回数の円グラフ表示機能の開発手順(WEB画面側)

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

CSSファイル

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

JavaScriptファイル

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

HTMLファイル

<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 }}";
        console.log(favBetNumChart);
        var options = { 
          legend: 'none',
          pieSliceText: 'label',
          backgroundColor: {
            fill: "transparent"
          }
        };
        for (let dataset of ["test", "valid"]) {

          let targetJsonStr = JSON.parse(favBetNumChart[betMode][dataset][yearMode]["target"][model_name])
          targetJsonCode = new google.visualization.DataTable(targetJsonStr);
          let idname = "favBetNumChart-"+dataset+"-"+String(idx);
          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">
  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;
  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>
{% 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' %}

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

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

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

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

python manage.py runserver

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

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

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

簡単な動作確認

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

ただし、Google Chartsで描画する2重円グラフの機能ですが、これはcomputeDiffメソッドを用いて作成しています。

この円グラフはマウスオーバーでデータの内容を確認できるのですが、なぜかcomputeDiffメソッドだと、以下の画像のように内側の円グラフのラベル名がバグって表示されます。おそらく不良だと思うのですが、報告の仕方が分からず放置です。

また、差分グラフの内側の円グラフと外側の円グラフのラベル名を変えたい場合は、optionsの引数に以下を指定するとできるようです。バグが修正されるまで待ちましょう。

            options["diff"] = {
              oldData: {
                tooltip: {
                  prefix: "baseline"
                }
              },
              newData: {
                tooltip: {
                  prefix: model_name
                }
              }
            }
スポンサーリンク

ソース公開しました!

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

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

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

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

スポンサーリンク

前回記事

モデルの回収率と的中率のテーブル表示機能

スポンサーリンク

次回記事

モデル情報一覧画面の開発

コメント

  1. おきく より:

    お世話になっております。

    fav_bet_num_chart.pyのコード部分が違うコードになってそうです。
    今回これは追加されているコードではありませんでしょうか?

    確認よろしくお願いします。

    • oyakata より:

      すみません、今確認しました。報告ありがとうございます!
      fav_bet_num_chart.pyコード差し替えました!

      毎度前回記事をコピペして投稿を作成しているため、前回分の内容のままになってしまっていたようです。運用を見直す必要がありますね、、
      ご迷惑をおかけしました。いつもありがとうございます。

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