PR

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

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

はじめに

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

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

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

スポンサーリンク

本プログラムの前提

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

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

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

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

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

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

モデル分析画面:オッズグラフスコアの確認画面作成

今回はモデル分析画面の3つ目の機能であるモデルのオッズグラフスコアの確認画面の作成に入ります。

選択したモデルのオッズグラフ、オッズグラフスコア、オッズグラフスコアの素データを確認できる画面を作ります。

オッズグラフってなに?って方は先に以下の記事を参照ください。

オッズグラフスコアってなに?って方は先に以下の記事を参照ください。

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

完成イメージ(GIF)

モデルを選択後、オッズグラフスコアの確認画面の表示ボタンをクリックします。今回の記事を皮切りにロードマップ4で作成したコードを無料公開します(*条件有)。しかし、オッズグラフスコアの確認画面では有料ソース箇所も含めた内容がありますので、無料公開分はそこを省いた内容になります。

そのため、今回の記事は無料公開箇所のソースと有料公開箇所のソースの2部構成になります。

まずは無料公開箇所からの解説です。

無料公開範囲

以上のようにモデルを選択後、オッズグラフスコアの確認画面を表示すると、選択したモデルのオッズグラフを確認できるようにしました。

支持率オッズグラフと回収率オッズグラフも描画してますので、そのラインよりも高いか低いかでパフォーマンスを確認できるようになっています。

有料公開範囲

以上のようにモデル選択後、オッズグラフスコアの確認画面を表示すると、オッズグラフの描画だけでなくオッズグラフスコアの表とその素データのテーブルが表示されるようになっています。

これらのテーブルから選択したベースモデルとターゲットモデルのオッズグラフスコアやAonBオッズグラフスコアを確認できるようになっています。

このようにすることで、モデル同士の精度の比較を数値的に一目で確認できるようになっています。

スポンサーリンク

フォルダ構成

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

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

<any-dir>
  ┣ <app_keiba>
  ┃      ┣ <app_keiba>
  ┃      ┃      ┣ __init__.py
  ┃      ┃      ┣ settings.py
  ┃      ┃      ┣ urls.py
  ┃      ┃      ┣ asgi.py
  ┃      ┃      ┗ wsgi.py
  ┃      ┣ <model_analyzer>
  ┃      ┃      ┣ <migrations>
  ┃      ┃      ┃      ┗ __init__.py
  ┃      ┃      ┣ <tools>
  ┃      ┃      ┃      ┣ <form_control>
  ┃      ┃      ┃      ┃     ┗ model_manage_forms.py
  ┃      ┃      ┃      ┗ <model_control>
  ┃      ┃      ┃             ┣ model_analyze_models.py
  ┃      ┃      ┃             ┗ model_manage_models.py
  ┃      ┃      ┣ <main_viewer>
  ┃      ┃      ┃      ┣ <analyze_viewr>
  ┃      ┃      ┃      ┃      ┣ <base_analyze_page>
  ┃      ┃      ┃      ┃      ┃     ┣ fav_bet_num_chart.py
  ┃      ┃      ┃      ┃      ┃     ┣ profit_loss_graph.py
  ┃      ┃      ┃      ┃      ┃     ┣ return_hit_rate_table.py
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ <model_info_list_page>
  ┃      ┃      ┃      ┃      ┃     ┣ const.py
  ┃      ┃      ┃      ┃      ┃     ┣ model_item_manager.py
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ <ogs_page>
  ┃      ┃      ┃      ┃      ┃     ┣ const.py
  ┃      ┃      ┃      ┃      ┃     ┣ odds_graph_chart.py
  ┃      ┃      ┃      ┃      ┃     ┣ odds_graph_score_table.py
  ┃      ┃      ┃      ┃      ┃     ┗ views.py
  ┃      ┃      ┃      ┃      ┣ base_analyzer_dto.py
  ┃      ┃      ┃      ┃      ┗ analyze_viewer_operator.py
  ┃      ┃      ┃      ┗ views.py
  ┃      ┃      ┣ __init__.py
  ┃      ┃      ┣ admin.py
  ┃      ┃      ┣ apps.py
  ┃      ┃      ┣ models.py
  ┃      ┃      ┣ test.py
  ┃      ┃      ┣ urls.py
  ┃      ┃      ┗ views.py
  ┃      ┣ <static>
  ┃      ┃      ┣ <css>
  ┃      ┃      ┃    ┣ <model_analyze>
  ┃      ┃      ┃    ┃      ┗ style.css
  ┃      ┃      ┃    ┣ <model_manage>
  ┃      ┃      ┃    ┃      ┗ style.css
  ┃      ┃      ┃    ┗ base.css
  ┃      ┃      ┣ <images>
  ┃      ┃      ┃    ┣ caret-left-square.svg
  ┃      ┃      ┃    ┣ caret-right-square.svg
  ┃      ┃      ┃    ┣ favicon.png
  ┃      ┃      ┃    ┗ file-bar-graph-fill.svg
  ┃      ┃      ┗ <  js  >
  ┃      ┃            ┣ <base_layout>
  ┃      ┃            ┃     ┗ fade_popup.js
  ┃      ┃            ┣ <model_analyze>
  ┃      ┃            ┃     ┣ check_submit_mode.js
  ┃      ┃            ┃     ┣ sidebar_slide.js
  ┃      ┃            ┃     ┗ validate_form.js
  ┃      ┃            ┗ <model_manage>
  ┃      ┃                   ┗ update_form_initial.js
  ┃      ┣ <templates>
  ┃      ┃      ┣ <model_analyze>
  ┃      ┃      ┃    ┣ <analyze_pages>
  ┃      ┃      ┃    ┃     ┣ <scripts>
  ┃      ┃      ┃    ┃     ┃     ┣ <base_analyze>
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ delete_graph.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ fav_bet_num_chart.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ graph_controller.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ profit_loss_graph.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ rtn_hit_rate_table.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┗ table_controller.html
  ┃      ┃      ┃    ┃     ┃     ┣ <model_info_list>
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ <card_items>
  ┃      ┃      ┃    ┃     ┃     ┃     ┃     ┣ summary_fav_bet_num.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┃     ┣ summary_item.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┃     ┣ summary_profit_loss_graph.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┃     ┗ summary_rtn_hit_table.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┣ model_card_item.html
  ┃      ┃      ┃    ┃     ┃     ┃     ┗ scroll_models.html
  ┃      ┃      ┃    ┃     ┃     ┗ <odds_page>
  ┃      ┃      ┃    ┃     ┃            ┣ odds_score_chart.html
  ┃      ┃      ┃    ┃     ┃            ┗ ogs_table.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側)

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

無料ソース範囲

(新規) odds_graph_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 OddsGraphChartManager:
    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 = {
            "odds": ("number", "オッズ"),
            "support_os": ("number", "支持率OS"),
            "return_os": ("number", "回収率OS"),
        }

    def generate_odds_graph_json(self) -> GraphJsonMap:
        graphDataMap = self.generate_odds_graph_data()
        graphJsonMap = GraphJsonMap("odds_score_chart")

        target_map_list: list[BetDataItem] = graphDataMap["target"]
        base_map: BetDataItem = graphDataMap["base"]
        for bet in graphJsonMap.get_bet_columns():
            for mode in ["train", "valid", "test"]:
                for target_map in [base_map]+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
                    graph_item: JsonItem = graphJsonMap.__dict__[
                        bet].__dict__[mode]
                    for model_name, target_item in target_bet_mode_map.items():
                        graph_item.description |= self.description_temp.copy()
                        graph_item.description |= target_item.description
                        graph_item.data += [target_item.data]

            for mode in ["train", "valid", "test"]:
                graph_item: JsonItem = graphJsonMap.__dict__[
                    bet].__dict__[mode]
                if len(graph_item.data) == 0:
                    continue
                idf = pd.concat(graph_item.data,
                                axis=1).fillna(0).reset_index(names="odds")
                idf["support_os"] = (
                    80/idf["odds"]).apply(lambda d: (d, f"{d:.2f}%"))
                idf["return_os"] = (
                    100/idf["odds"]).apply(lambda d: (d, f"{d:.2f}%"))

                data_table = gviz_api.DataTable(graph_item.description)
                data_table.LoadData(idf.to_dict(orient="records"))

                columns_order = list(graph_item.description.keys())
                graph_item.json = data_table.ToJSon(
                    columns_order=columns_order, order_by=columns_order[0]).replace('{"v":NaN}', '{"v":null}')

        return graphJsonMap

    def generate_odds_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.__dict__[self.prop_key]:
            return DataTableItem()

        model_id = "base_" + tempMap.meta.model_id if base else tempMap.meta.model_id

        dfv: pd.DataFrame = dataMap.valid.rename(
            columns={"勝率": model_id}).set_index("odds")
        dft: pd.DataFrame = dataMap.test.rename(
            columns={"勝率": model_id}).set_index("odds")

        target_columns = [model_id]
        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]:

        item = TableItem()
        item.description[target_columns[0]] = (
            "number", target_columns[0], )
        item.description[f"{target_columns[0]}_labelsText"] = (
            "string", "labelsText", {"role": "tooltip", "html": True})

        idf.reset_index(inplace=True)
        idf[f"{target_columns[0]}_labelsText"] = idf.fillna(0).apply(
            lambda row: f'<table class="m-1"><tr><th>ODDS</th><th class="text-end">{row["odds"]}</th></tr>' +
            f'<tr><th>勝率</th><td class="text-end">{row[target_columns[0]]:.2f}%</td></tr>' +
            f'<tr><th>加重OS</th><td class="text-end">{row["weight"]*row["回収率100%超"]:.4f}</td></tr>' +
            f'<tr><th>件数</th><td class="text-end">{row["件数"]}</td></tr></table>',
            axis=1
        )

        item.data = idf.set_index(
            "odds")[[target_columns[0], f"{target_columns[0]}_labelsText"]]
        return {target_columns[0]: 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.analyze_viewer.analyze_viewer_operator import ModelListController
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models
from model_analyzer.models import ModelList
from model_analyzer.tools.model_control.model_manage_models import get_table_list
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms

from django.core.cache import cache
import pandas as pd
import pathlib
import datetime

THISPAGENAME = "ogs"


class OGSView(ModelAnalyzeView):

    def start_post(self, request: WSGIRequest):
        urls = super().start_post(request)
        if THISPAGENAME not in urls.split("?")[0].split("/"):
            self.pop_unnecessary()
        if len(urls.split("?")[0].split("/")) == 2:
            urls_params = urls.split("?")
            urls = "/".join(urls_params[0].split("/") +
                            [THISPAGENAME]) + "?"+urls_params[1]

        cache.clear()

        return urls

    def generate_graph(self):
        modelController = ModelListController(
            self.nav_params["model_query"], self.nav_params["base_model"])
        if "og_chart" not in self.nav_params or self.nav_params["og_chart"] is None:
            ogChartJsonData = modelController.generate_odds_graph()
            self.nav_params[
                "og_chart"] = ogChartJsonData.convert_odds_graph_chart_for_django()
            self.nav_params["target_bet_columns"] = ogChartJsonData.get_bet_columns()

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

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

(更新) analyze_viewer_operator.py

オッズグラフ描画用のメソッドを追加しました。

# 以下インポートの追加
from model_analyzer.main_viewer.analyze_viewer.ogs_page.odds_graph_chart import OddsGraphChartManager

# class ModelListController:
# 以下のメソッドを追加
    def generate_odds_graph(self) -> GraphJsonMap:
        targetModelMap, baseModelMap = self.load_dir_data("odds_graph_file")
        oddsGraphManager = OddsGraphChartManager(
            targetModelMap, baseModelMap, "odds_graph_file")
        return oddsGraphManager.generate_odds_graph_json()

(更新) base_analyzer_dto.py

オッズグラフ用のDTOを作成したのですが、差分を取るのが面倒だったので丸っと置き換えてください。

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

        self.target_model_list = []
        self.base_id = None

    def get_bet_columns(self):
        return ["tan", "fuku"]

    def convert_odds_graph_chart_for_django(self):
        return {
            bet: {
                mode: self.__dict__[bet].__dict__[mode].json if self.__dict__[
                    bet].__dict__[mode].json else ""
                for mode in ["valid", "test"]
            }
            for bet in self.get_bet_columns()
        }

    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 JsonListTableItem:
    def __init__(self) -> None:
        self.train: JsonItem = JsonItem()
        self.valid: JsonItem = JsonItem()
        self.test: JsonItem = 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.id = modellist.id
        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

有料ソース範囲

以下は有料ソース範囲ですBookers記事の第3弾記事の内容を使用していますので、そちらを購入してから以下のソースを反映してください。

(新規) const.py

オッズグラフスコアを描画するために色々と使いまわしたかったのでまとめました。

OGS_DICT = {
    'over_support_score': "超過SOS",
    'over_100per_score': "超過ROS",
    'over_base_score': "超過BOS",
    'weighted_over_support_score': "加重超過SOS",
    'weighted_over_100per_score': "加重超過ROS",
    'weighted_over_base_score': "加重超過BOS",
    'weight': "割合",
    'base_weight': "base割合",
    '件数': "Num",
    'base_件数': "baseNum"
}

OGS_COLUMNS = [
    "加重超過SOS",
    "加重超過ROS",
]

AONB_OGS_COLUMNS = [
    "加重超過BOS"
]

OGS_REPORT_DICT = {
    "加重超過SOS": "支持率OGS",
    "加重超過ROS": "回収率OGS",
    "加重超過BOS": "AonBOGS"
}

(新規) odds_graph_score_table.py

オッズグラフスコアを描画するのデータ管理クラスです。ここでベースのモデルを作っておけばと後悔しました。

from model_analyzer.main_viewer.analyze_viewer.ogs_page.const import OGS_DICT, OGS_COLUMNS, AONB_OGS_COLUMNS, OGS_REPORT_DICT
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, OGSTblJsonChartItem, OGSTblJsonItem, RtnHitRateJsonItem, 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
import sys
sys.path.append("..")
from src.model_manager.base_manager import BaseModelManager  # noqa


warnings.simplefilter(action="ignore", category=SettingWithCopyWarning)


class OddsGraphScoreTblManager:
    def __init__(self, targetModelMap: list[ModelInfoMap], baseModelMap: ModelInfoMap, prop_key: str) -> None:
        self.targetModelMap = targetModelMap
        self.baseModelMap = baseModelMap
        self.prop_key = prop_key
        self.ogs_report_description = {
            "model_id": ("string", "モデルID")
        } | {
            key: ("number", key)
            for key in OGS_REPORT_DICT.values()
        }

    def generate_odds_graph_score_json(self) -> GraphJsonMap:
        graphDataMap = self.generate_odds_graph_score_data()
        graphJsonMap = GraphJsonMap("odds_graph_score")

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

        ogs_report_map = {}
        ogs_report_c_map = {}
        for bet in graphJsonMap.get_bet_columns():
            ogs_report_map[bet] = {}
            ogs_report_c_map[bet] = {}
            for mode in ["valid", "test"]:
                ogs_report_map[bet][mode] = []
                ogs_report_c_map[bet][mode] = []
                for data_map in [{"mode": False, "data": d} for d in target_map_list] + [{"mode": True, "data": base_map}]:
                    target_map = data_map["data"]
                    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
                    graph_item: OGSTblJsonChartItem = graphJsonMap.__dict__[
                        bet].__dict__[mode]
                    for model_name, target_item in target_bet_mode_map.items():
                        ogs_item = OGSTblJsonItem()
                        ogs_item.description = target_item.description
                        ogs_item.data = target_item.data
                        data_table = gviz_api.DataTable(ogs_item.description)
                        data_table.LoadData(
                            ogs_item.data.to_dict(orient="records"))

                        columns_order = list(ogs_item.description.keys())
                        ogs_item.json = data_table.ToJSon(
                            columns_order=columns_order, order_by=columns_order[0]).replace('{"v":NaN}', '{"v":null}')
                        target_ogs_columns = [
                            col for col in target_item.data.columns if col in OGS_COLUMNS+AONB_OGS_COLUMNS]
                        if data_map["mode"]:
                            graph_item.base[model_name] = ogs_item
                            graph_item.base_id = model_name
                            ogs_report_map[bet][mode] += [
                                pd.DataFrame(
                                    target_item.data[target_ogs_columns].sum(), columns=[model_name])
                            ]
                        else:
                            graph_item.target[model_name] = ogs_item
                            ogs_report_map[bet][mode] += [
                                pd.DataFrame(
                                    target_item.data[target_ogs_columns].sum(), columns=[model_name])
                            ]
                if len(ogs_report_map[bet][mode]):
                    idf = pd.concat(ogs_report_map[bet][mode], axis=1).T
                    for col in idf.columns:
                        idf[col] = idf[col].apply(lambda d: (
                            d, f"{d:.5f}") if not pd.isna(d) else None)
                    idf = idf.rename(columns=OGS_REPORT_DICT).reset_index(
                        names="model_id")
                    data_table2 = gviz_api.DataTable(
                        self.ogs_report_description)
                    data_table2.LoadData(idf.to_dict(orient="records"))
                    ogs_report_map[bet][mode] = data_table2.ToJSon(
                        columns_order=list(self.ogs_report_description.keys())).replace('null', '{"v":null}')
        return graphJsonMap, ogs_report_map

    def generate_odds_graph_score_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, baseMap=self.baseModelMap)]

        graphDataMap["base"] = self.create_betdata_item(
            self.baseModelMap, True)
        return graphDataMap

    def create_betdata_item(self, tempMap: ModelInfoMap | None, base: bool = False, baseMap: ModelInfoMap | None = None) -> 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
            if baseMap is None or base:
                baseDataMap = None
            else:
                baseDataMap = baseMap.betData.__dict__[bet]
            dataitem: DataTableItem = self.create_datatable_item(
                bet, dataMap, tempMap, base, baseMap, baseDataMap)
            betdataitem.__dict__[bet] = dataitem

        return betdataitem

    def create_datatable_item(
        self,
        bet: str,
        dataMap: DataMap,
        tempMap: ModelInfoMap,
        base: bool = False,
        baseMap: ModelInfoMap = None,
        baseDataMap: DataMap = None,
    ) -> DataTableItem:
        if bet not in tempMap.meta.__dict__[self.prop_key]:
            return DataTableItem()

        remain_columns = ["勝率", "支持率", "回収率100%超"]

        if baseMap is None:
            tmp = DataMap(
                **BaseModelManager.calc_odds_graph_score(dataMap.__dict__))
            for mode, dfval in tmp.__dict__.items():
                dataMap.__dict__[mode] = pd.concat(
                    [dataMap.__dict__[mode][remain_columns], dfval], axis=1)
            target_columns = remain_columns + [val for val in OGS_DICT.values(
            ) if val not in ["超過BOS", "加重超過BOS", "base割合", "baseNum"]]
        else:
            tmp = DataMap(
                **BaseModelManager.calc_odds_graph_score(dataMap.__dict__, baseDataMap.__dict__))
            for mode, dfval in tmp.__dict__.items():
                dataMap.__dict__[mode] = pd.concat(
                    [dataMap.__dict__[mode][remain_columns], dfval], axis=1)
            target_columns = remain_columns + list(OGS_DICT.values())

        dfv: pd.DataFrame = dataMap.valid.rename(columns=OGS_DICT).fillna(0)
        dft: pd.DataFrame = dataMap.test.rename(columns=OGS_DICT).fillna(0)

        model_id = tempMap.meta.model_id + \
            " (baseline)" if base else tempMap.meta.model_id
        dataitem = DataTableItem()
        dataitem.valid = {model_id: self.create_data_map(dfv, target_columns)}
        dataitem.test = {model_id: self.create_data_map(dft, target_columns)}

        return dataitem

    def create_data_map(
        self,
        idf: pd.DataFrame,
        target_columns: list[str],
    ) -> dict[str, TableItem]:

        item = TableItem()
        item.description["odds"] = ("number", "odds")
        for key in target_columns:
            item.description[key] = ("number", key)
        item.data = idf[target_columns].reset_index(names="odds")
        return item

(更新) views.py

オッズグラフスコアの確認画面のViewクラスです。

# 以下のインポートを追加
import numpy as np

# class OGSView(ModelAnalyzeView):
# 以下のメソッドを置き換え
    def generate_graph(self):
        modelController = ModelListController(
            self.nav_params["model_query"], self.nav_params["base_model"])
        if "og_chart" not in self.nav_params or self.nav_params["og_chart"] is None:
            ogChartJsonData = modelController.generate_odds_graph()
            self.nav_params[
                "og_chart"] = ogChartJsonData.convert_odds_graph_chart_for_django()
            self.nav_params["target_bet_columns"] = ogChartJsonData.get_bet_columns()
            ogsJsonData, ogs_report = modelController.generate_odds_graph_score()
            ogs_table = ogsJsonData.convert_odds_graph_score_for_django()
            self.nav_params["ogs_table"] = ogs_table
            self.nav_params["ogs_report"] = ogs_report

            self.nav_params["ogs_target_list"] = {
                bet: [data for data in val["target"].keys()]
                for bet, val in self.nav_params["ogs_table"].items()
            }
            self.nav_params["ogs_base_name"] = {
                bet: [data["model_id"]
                      for data in val["base"]].pop(0) if len(val["base"]) else None
                for bet, val in self.nav_params["ogs_table"].items()
            }
            self.nav_params["display_base_ogs"] = np.any([
                True if len(val["base"]) else False
                for val in self.nav_params["ogs_table"].values()
            ])

(更新) analyze_viewer_operator.py

オッズグラフ描画用のメソッドを追加しました。丸っと置き換えてください。

from django.core.handlers.wsgi import WSGIRequest
from django.db.models.query import QuerySet
from model_analyzer.main_viewer.analyze_viewer.ogs_page.odds_graph_score_table import OddsGraphScoreTblManager
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
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.fav_bet_num_chart import FavBetNumManager
from model_analyzer.main_viewer.analyze_viewer.ogs_page.odds_graph_chart import OddsGraphChartManager


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

    def generate_odds_graph(self) -> GraphJsonMap:
        targetModelMap, baseModelMap = self.load_dir_data("odds_graph_file")
        oddsGraphManager = OddsGraphChartManager(
            targetModelMap, baseModelMap, "odds_graph_file")
        return oddsGraphManager.generate_odds_graph_json()

    def generate_odds_graph_score(self) -> tuple[GraphJsonMap, dict]:
        targetModelMap, baseModelMap = self.load_dir_data(
            "odds_graph_file", "odds")
        oddsGraphManager = OddsGraphScoreTblManager(
            targetModelMap, baseModelMap, "odds_graph_file")
        return oddsGraphManager.generate_odds_graph_score_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, set_col: str | None = None) -> 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"):
                    if set_col is not None:
                        dfmap[fpath.stem] = pd.read_csv(
                            fpath).set_index(set_col)
                    else:
                        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"):
                    if set_col is not None:
                        dfmap[fpath.stem] = pd.read_csv(
                            fpath).set_index(set_col)
                    else:
                        dfmap[fpath.stem] = pd.read_csv(fpath)
                baseModelInfoMap.betData.__dict__[bet] = DataMap(**dfmap)
                baseModelInfoMap.meta = self.basemodel

        return modelInfoMapList, baseModelInfoMap

(更新) base_analyzer_dto.py

オッズグラフ用のDTOを作成したのですが、差分を取るのが面倒だったので丸っと置き換えてください。

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()
        if mode == "odds_score_chart":
            self.tan: JsonListTableItem = JsonListTableItem()
            self.fuku: JsonListTableItem = JsonListTableItem()
        if mode == "odds_graph_score":
            self.tan: OGSTblJsonTableItem = OGSTblJsonTableItem()
            self.fuku: OGSTblJsonTableItem = OGSTblJsonTableItem()

        self.target_model_list = []
        self.base_id = None

    def get_bet_columns(self):
        return ["tan", "fuku"]

    def convert_odds_graph_chart_for_django(self):
        return {
            bet: {
                mode: self.__dict__[bet].__dict__[mode].json if self.__dict__[
                    bet].__dict__[mode].json else ""
                for mode in ["valid", "test"]
            }
            for bet in self.get_bet_columns()
        }

    def convert_odds_graph_score_for_django(self):
        return {
            bet: {
                "target": {
                    key: {
                        "test": val.json,
                        "valid": self.__dict__[bet].__dict__["valid"].target[key].json
                    }
                    for key, val in self.__dict__[bet].__dict__["test"].target.items()
                },
                "base": [{
                    "model_id": key,
                    "data": {
                        "test": val.json,
                        "valid": self.__dict__[bet].__dict__["valid"].base[key].json
                    }
                } for key, val in self.__dict__[bet].__dict__["test"].base.items()]
                if self.__dict__[bet].__dict__["test"].base else []
            }
            for bet in self.get_bet_columns()
        }

    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 OGSTblJsonItem:
    def __init__(self) -> None:
        self.description: dict[str, object] = {}
        self.data: pd.DataFrame = None
        self.json: str = None


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


class OGSTblJsonTableItem:
    def __init__(self) -> None:
        self.train: OGSTblJsonChartItem = OGSTblJsonChartItem()
        self.valid: OGSTblJsonChartItem = OGSTblJsonChartItem()
        self.test: OGSTblJsonChartItem = OGSTblJsonChartItem()


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 JsonListTableItem:
    def __init__(self) -> None:
        self.train: JsonItem = JsonItem()
        self.valid: JsonItem = JsonItem()
        self.test: JsonItem = 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.id = modellist.id
        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
スポンサーリンク

オッズグラフスコアの確認画面の開発手順(WEB画面側)

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

CSSとJavaScriptは無料公開、有料公開どちらも共通です。

CSSファイル

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

JavaScriptファイル

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

無料公開範囲HTMLファイル

<script type="text/javascript">

  const changeSelect = document.getElementById("selectBetMode");

  window.addEventListener("DOMContentLoaded", (e) => {
    changeChart();
  });

  changeSelect.addEventListener("change", (e) => {
    changeChart();
  });

  function changeChart() {
    
    bet = null;
    mode = document.getElementById("selectBetMode");
    Array.from(mode.options).forEach(function (option) {
      if (option.selected) {
        bet = option.innerHTML;
      }
    });

    {% autoescape off %}
    let og_chart = JSON.parse(JSON.stringify({{ og_chart }}));
    {% endautoescape %}
    
    if (!og_chart[bet]["test"]) {
      empty_data = {
        cols: [{id: "odds"}],
        rows: []
      };
      chart = new google.visualization.LineChart(document.getElementById('testOddsGraphChart'));
      chart.draw( new google.visualization.DataTable(empty_data));
      chart = new google.visualization.LineChart(document.getElementById('validOddsGraphChart'));
      chart.draw( new google.visualization.DataTable(empty_data));
      return;
    }

    google.charts.load('current', { 'packages': ['corechart',] });
    google.charts.setOnLoadCallback(drawOddsScoreChart);
    function drawOddsScoreChart() {
      var options = {
        legend: { position: 'top' },
        // width: "90%",
        // height: "80%",
        backgroundColor: {
          fill: "transparent"
        },
        tooltip: { isHtml: true },
        chartArea: {
          // top: 40
        },
      };
      json_data = new google.visualization.DataTable(JSON.parse(og_chart[bet]["test"]));
      const chart = new google.visualization.LineChart(document.getElementById('testOddsGraphChart'));
      options["hAxis"] = { title: 'odds', titleTextStyle: { color: '#333' } }
      options["title"] = "test data"
      chart.draw(json_data, options);

      json_data_valid = new google.visualization.DataTable(JSON.parse(og_chart[bet]["valid"]));
      const valid_chart = new google.visualization.LineChart(document.getElementById('validOddsGraphChart'));
      options["hAxis"] = { title: 'odds', titleTextStyle: { color: '#333' } }
      options["title"] = "valid data"
      valid_chart.draw(json_data_valid, options);

    }
  }
</script>
{% 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 %}
<h2 class="d-flex container justify-content-center">
  <span class="me-5 pt-4">
    OGSレポート
  </span>
  <span class="me-5 ms-5"></span>
</h2>
{% endblock %}

{% block analyze-content %}

<div style="width: 90%; height: 1460px;"
  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 col-nowrap mb-3 row-cols-lg-auto align-items-center justify-content-center" id="selectGraphMode">
    <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>
  <div class="row ps-3">
    <div class="fs-4 text-center fw-bold">
      オッズグラフ
    </div>
    <div class="row border border border-2 in-shadow border-dark-subtle rounded bg-white">
      <div class="row" style="height: 250px;">
        <div id="testOddsGraphChart"></div>
      </div>
      <div class="row" style="height: 250px;">
        <div id="validOddsGraphChart"></div>
      </div>
    </div>

  </div>
  {% endblock %}
  {% block ext-jsblock %}
  {% include 'model_analyze/analyze_pages/scripts/odds_page/odds_score_chart.html' %}
  {% endblock %}

有料公開範囲HTMLファイル

以下は有料ソース範囲ですBookers記事の第3弾記事の内容を使用していますので、そちらを購入してから以下のソースを反映してください。

<script type="text/javascript">

  const changeSelect = document.getElementById("selectBetMode");

  window.addEventListener("DOMContentLoaded", (e) => {
    changeChart();
  });

  changeSelect.addEventListener("change", (e) => {
    changeChart();
  });

  function changeChart() {
    
    bet = null;
    mode = document.getElementById("selectBetMode");
    Array.from(mode.options).forEach(function (option) {
      if (option.selected) {
        bet = option.innerHTML;
      }
    });

    {% autoescape off %}
    let og_chart = JSON.parse(JSON.stringify({{ og_chart }}));
    {% endautoescape %}
    
    if (!og_chart[bet]["test"]) {
      for (let targetId of ['testOddsGraphChart', 'validOddsGraphChart']){
        var chartElem = document.getElementById(targetId);
        while (chartElem.firstChild) {
            chartElem.removeChild(chartElem.firstChild);
        }
      }
      return;
    }

    google.charts.load('current', { 'packages': ['corechart',] });
    google.charts.setOnLoadCallback(drawOddsScoreChart);
    function drawOddsScoreChart() {
      var options = {
        legend: { position: 'top' },
        // width: "90%",
        // height: "80%",
        backgroundColor: {
          fill: "transparent"
        },
        tooltip: { isHtml: true },
        chartArea: {
          // top: 40
        },
      };
      json_data = new google.visualization.DataTable(JSON.parse(og_chart[bet]["test"]));
      const chart = new google.visualization.LineChart(document.getElementById('testOddsGraphChart'));
      options["hAxis"] = { title: 'odds', titleTextStyle: { color: '#333' } }
      options["title"] = "test data"
      chart.draw(json_data, options);

      json_data_valid = new google.visualization.DataTable(JSON.parse(og_chart[bet]["valid"]));
      const valid_chart = new google.visualization.LineChart(document.getElementById('validOddsGraphChart'));
      options["hAxis"] = { title: 'odds', titleTextStyle: { color: '#333' } }
      options["title"] = "valid data"
      valid_chart.draw(json_data_valid, options);

    }
  }
</script>
<script type="text/javascript">
	const changeBetSelect = document.getElementById("selectBetMode");
	const changeDatasetSelect = document.getElementById("selectDataSetMode");

	window.addEventListener("DOMContentLoaded", (e) => {
		changeOGSTable();
		changeReportOGSTable();
	});

	changeBetSelect.addEventListener("change", (e) => {
		changeOGSTable();
		changeReportOGSTable();
	});

	changeDatasetSelect.addEventListener("change", (e) => {
		changeOGSTable();
		changeReportOGSTable();
	});


	function changeReportOGSTable() {
		
		bet_mode = getBetMode();
		dataset_mode = getDatasetMode();

    {% autoescape off %}
    reportOGSData = JSON.parse(JSON.stringify({{ ogs_report }}));
    {% endautoescape %}

		if (reportOGSData[bet_mode][dataset_mode].length == 0) {
			for (let targetId of ["reportOGSTable"]){
				var chartElem = document.getElementById(targetId);
				while (chartElem.firstChild) {
					chartElem.removeChild(chartElem.firstChild);
				}
			}
			return;
		}
    google.charts.load('current', { 'packages': ['table',] });
    google.charts.setOnLoadCallback(updateReportTable);
	}

	function updateReportTable() {
		bet_mode = getBetMode();
		dataset_mode = getDatasetMode();

    {% autoescape off %}
    reportOGSData = JSON.parse(JSON.stringify({{ ogs_report }}));
    {% endautoescape %}
		
    let targetReportTableCode = JSON.parse(reportOGSData[bet_mode][dataset_mode]);
    targetReportTableCode = new google.visualization.DataTable(targetReportTableCode);
		
		var formatter = new google.visualization.PatternFormat(
			'<span class="fw-bold text-center">{0}</span>');
		formatter.format(targetReportTableCode, [0]);

    const targetReportTable = new google.visualization.Table(document.getElementById('reportOGSTable'));
    targetReportTable.draw(targetReportTableCode, {showRowNumber: true, allowHtml: true, width: "100%"});
	}

	function changeOGSTable(num=0) {
		bet_mode = getBetMode();
		dataset_mode = getDatasetMode();
		
		{% autoescape off %}
		target_model_name_list = JSON.parse(JSON.stringify({{ ogs_target_list }}))[bet_mode];
		{% endautoescape %}
		
		if (target_model_name_list.length == 0) {
			for (let targetId of ["targetOGSTableChart", "baseOGSTableChart"]){
				var chartElem = document.getElementById(targetId);
				while (chartElem.firstChild) {
					chartElem.removeChild(chartElem.firstChild);
				}
			}
			return;
		}

		target_model_idx = get_target_model_id(target_model_name_list);
		next_model_idx = target_model_idx + num;
		const rightBtn = document.querySelector("#tableRightBtn");
		const leftBtn = document.querySelector("#tableLeftBtn");
		if (next_model_idx == 0 & target_model_name_list.length > 1) {
			rightBtn.disabled = false;
			leftBtn.disabled = true;
		} else if (next_model_idx == target_model_name_list.length - 1 & target_model_name_list.length > 1) {
			rightBtn.disabled = true;
			leftBtn.disabled = false;
		} else {
			rightBtn.disabled = false;
			leftBtn.disabled = false;
		}
		changeName(target_model_name_list[next_model_idx]);
    google.charts.load('current', { 'packages': ['table',] });
    google.charts.setOnLoadCallback(updateTable);
  }

  function updateTable() {

    var betMode = getBetMode();
    var datasetMode = getDatasetMode();

    {% autoescape off %}
    target_model_name_list = JSON.parse(JSON.stringify({{ ogs_target_list }}))[bet_mode];
    OGSData = JSON.parse(JSON.stringify({{ ogs_table }}));
    {% endautoescape %}


    target_model_idx = get_target_model_id(target_model_name_list);
    let targetTableCode = JSON.parse(OGSData[betMode]["target"][target_model_name_list[target_model_idx]][datasetMode]);
    targetTableCode = new google.visualization.DataTable(targetTableCode);

		if ("{{display_base_ogs}}" == "True") {
			for (let idx=7 ; idx < 10; idx++) {
				var c_formatter = new google.visualization.ColorFormat();
				c_formatter.addGradientRange(from=0.0000000000000000001, to=1, color='#000000', fromBgColor='#ffffff', toBgColor='#bbbbff');
				c_formatter.addGradientRange(from=-5, to=0.0000000000000000001, color='#000000', fromBgColor='#ffbbbb', toBgColor='#ffffff');
				c_formatter.addGradientRange(from=1, to=10, color='#000000', fromBgColor='#bbbbff', toBgColor='#f0f0ff');
				c_formatter.addGradientRange(from=-20, to=-5, color='#000000', fromBgColor='#fff0f0', toBgColor='#ffbbbb');
				c_formatter.format(targetTableCode, idx);
			}
		} else {
			for (let idx=6 ; idx < 8; idx++) {
				var c_formatter = new google.visualization.ColorFormat();
				c_formatter.addGradientRange(from=0.0000000000000000001, to=1, color='#000000', fromBgColor='#ffffff', toBgColor='#bbbbff');
				c_formatter.addGradientRange(from=-5, to=0.0000000000000000001, color='#000000', fromBgColor='#ffbbbb', toBgColor='#ffffff');
				c_formatter.addGradientRange(from=1, to=10, color='#000000', fromBgColor='#bbbbff', toBgColor='#f0f0ff');
				c_formatter.addGradientRange(from=-20, to=-5, color='#000000', fromBgColor='#fff0f0', toBgColor='#ffbbbb');
				c_formatter.format(targetTableCode, idx);
			}
		}

    const targetTable = new google.visualization.Table(document.getElementById('targetOGSTableChart'));
    targetTable.draw(targetTableCode, {showRowNumber: true, allowHtml: true, width: "100%"});

		
    if ("{{display_base_ogs}}" == "True") {
			const baseTableElem = document.getElementById('baseOGSTableChart');
      baseTableCode = new google.visualization.DataTable(JSON.parse(OGSData[betMode]["base"][0]["data"][dataset_mode]));
      const baseTable = new google.visualization.Table(baseTableElem);
			for (let idx=6 ; idx < 8; idx++) {
				var c_formatter = new google.visualization.ColorFormat();
				c_formatter.addGradientRange(from=0.0000000000000000001, to=1, color='#000000', fromBgColor='#ffffff', toBgColor='#bbbbff');
				c_formatter.addGradientRange(from=-5, to=0.0000000000000000001, color='#000000', fromBgColor='#ffbbbb', toBgColor='#ffffff');
				c_formatter.addGradientRange(from=1, to=10, color='#000000', fromBgColor='#bbbbff', toBgColor='#f0f0ff');
				c_formatter.addGradientRange(from=-20, to=-5, color='#000000', fromBgColor='#fff0f0', toBgColor='#ffbbbb');
				c_formatter.format(baseTableCode, idx);
			}
      baseTable.draw(baseTableCode, {showRowNumber: true, allowHtml: true, width: "100%"});
    }

  }

	
	function get_target_model_id(target_model_name_list) {

		const targetElem = document.getElementById("targetOGSTableName");
		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 changeName(modelName) {
    const targetElem = document.getElementById("targetOGSTableName");
    targetElem.innerText = modelName;
  }
	
	function getBetMode() {
		selectedBet = null;
		mode = document.getElementById("selectBetMode");
		Array.from(mode.options).forEach(function (option) {
			if (option.selected) {
			selectedBet = option.innerHTML;
			}
		});
		return selectedBet;
	}
	function getDatasetMode() {
		selectedDataSet = null;
		mode = document.getElementById("selectDataSetMode");
		Array.from(mode.options).forEach(function (option) {
			if (option.selected) {
					selectedDataSet = option.innerHTML;
			}
		});
		return selectedDataSet;
	}
</script>
{% 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 %}
<h2 class="d-flex container justify-content-center">
  <span class="me-5 pt-4">
    OGSレポート
  </span>
  <span class="me-5 ms-5"></span>
</h2>
{% endblock %}

{% block analyze-content %}

<div style="width: 90%;"
  class="d-flex container flex-column border border-dark-subtle rounded pb-3 pt-3 ps-4 mt-3 mb-5 shadow bg-light bg-gradient">

  <div class="row ps-3">
    <div class="fs-4 text-center fw-bold">
      オッズグラフ
    </div>
    <div class="row border border-2 in-shadow border-dark-subtle rounded bg-white">
      <div class="row" style="height: 250px;">
        <div id="testOddsGraphChart"></div>
      </div>
      <div class="row" style="height: 250px;">
        <div id="validOddsGraphChart"></div>
      </div>
    </div>

  </div>
  <div class="row col-nowrap mb-3 row-cols-lg-auto align-items-center justify-content-center" id="selectGraphMode">
    <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="selectDataSetMode" class="form-label text-nowrap">Select Dataset</label>
      <select class="form-select form-select-sm" aria-label="select dataset" id="selectDataSetMode">
        <option value="test" selected>test</option>
        <option value="valid">valid</option>
      </select>
    </div>
  </div>
  <div class="row ps-3 mb-3">
    <div class="fs-4 text-center fw-bold">
      OGS 比較
    </div>
    <div class="row ms-1 me-1">
      <div id="reportOGSTable" class="d-flex w-100 justify-content-center align-items-center"></div>
    </div>
  </div>

  <div class="row ps-3">
    <div class="fs-4 text-center fw-bold">
      OGS 素テーブル
    </div>
    <div class="d-flex flex-row row border border-2 in-shadow border-dark-subtle rounded me-1 p-2 bg-white overflow-auto">
      <div class="col">
        <div class="d-flex flex-column row ms-1 me-1 pt-1 mb-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="changeOGSTable(-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="targetOGSTableName">{{ogs_target_list.tan.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="changeOGSTable(1)">
              <img src="/static/images/caret-right-square.svg">
            </button>
          </div>
        </div>
        <div class="row w-100 ms-1 me-1">
          <div id="targetOGSTableChart" class="d-flex justify-content-center align-items-center"></div>
        </div>
      </div>

      {% if display_base_ogs %}
      <div class="col mt-2">
        <div class="d-flex flex-column row ms-1 me-1 pt-1 justify-content-center mb-1" style="height: 30px; width: 100%;">
          <div class="fs-5 text-center fw-bold">
            <span id="baseOGSTableName">{{ogs_base_name.tan}}</span>
          </div>
        </div>
        <div class="row w-100 ms-1 me-1 overflow-auto">
          <div id="baseOGSTableChart" class="d-flex justify-content-center align-items-center"></div>
        </div>
      </div>
      {% endif %}
    </div>
  </div>
</div>
{% endblock %}
{% block ext-jsblock %}
{% include 'model_analyze/analyze_pages/scripts/odds_page/odds_score_chart.html' %}
{% include 'model_analyze/analyze_pages/scripts/odds_page/ogs_table.html' %}
{% endblock %}
スポンサーリンク

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

それでは、オッズグラフスコアの確認画面のページができたので、サーバを起動しましょう。

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

python manage.py runserver

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

その後適当なモデルを選択して「分析モード」ボタンより表示されるサイドバーから「オッズグラフスコアの確認」の画面を表示してください。

以下のオッズグラフスコアの確認画面が表示されればOKです。

簡単な動作確認

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

以上でオッズグラフスコアの確認画面の作成完了です。お疲れ様でした。

これにてモデル分析画面の開発も終了です。解説動画を投稿し次第、Bookersの第四弾記事を投稿します。ロードマップ4で作成したパート1からパート10までの内容を一まとめにしたソースを公開します。公開しました!

サンプルデータも提供しますので、モデル管理画面からモデルインポート → モデル分析画面からモデルのパフォーマンスを確認などいろいろ遊べます。

スポンサーリンク

ソース公開しました!

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

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

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

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

スポンサーリンク

前回記事

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

コメント

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