PR

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

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

はじめに

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

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

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

スポンサーリンク

本プログラムの前提

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

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

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

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

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

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

モデル管理画面:モーダル化とレスポンス表示について

今回扱う項目は「モーダル化」と「レスポンス表示」です。前回Djangoで作成した以下画像のインポート画面ですが、かなり味気ないデザインになっていると思います。

そのため、この味気ないインポート画面を「モーダル化」して、インポートが成功したら成功したという旨の「レスポンス」を表示するようにします。

完成イメージ(gif)

part1~part2までの内容かつ今回の記事を手順通りに作れば、以下のようなWEBページができるようになります。

モーダルとは?

Bootstrap5の機能の一つで、完成イメージのgifの冒頭で「モデルインポート」ボタンを押した後に出てくる画面のことです。

Djangoだけでは実現できないことなので機能詳細は気になる人だけ調べてもらって、ここではHTMLソースまるごとコピペでこのモーダルの表示ができるようにしています。

レスポンスとは?

前回作成したインポート機能では「送信」ボタンを押すと何事もなく元のフォーム画面に戻っていたので実際にインポートできたのか良く分からなかったと思います。

そのため、インポートに成功したらそのようなメッセージをWEB画面上に表示させてあげることで、ユーザ側もモデルがインポートできたんだなと確認できるようになります。

完成イメージのgifでいうところの一番最後に出ている緑色のポップアップのことです。

これもこのポップアップ自体はBootstrap5とJavaScriptで表示非表示をコントロールしているため、HTMLソースとJavaScriptソースを丸写しで実現できます。(若干Djangoも触りますが、、)

以上が今回行うものです。

スポンサーリンク

フォルダ構成

今回の実装から、今後のプロジェクトを管理しやすくするためにフォルダの構成を少し変えます。

part1で示したフォルダ構成をベースに以下のように変更ください。赤文字が変更箇所です。

<any-dir>
  ┣ <app_keiba>
  ┃      ┣ <app_keiba>
  ┃      ┃      ┣ __init__.py
  ┃      ┃      ┣ settings.py
  ┃      ┃      ┣ urls.py
  ┃      ┃      ┣ asgi.py
  ┃      ┃      ┗ wsgi.py
  ┃      ┣ <app_keiba>
  ┃      ┃      ┣ <migrations>
  ┃      ┃      ┃      ┗ __init__.py
  ┃      ┃      ┣ <tools>
  ┃      ┃      ┃      ┗ <form_control> ※Formクラスの動きを制御
  ┃      ┃      ┃             ┗ model_manage_forms.py ※モデル管理画面で扱うFormのクラスを制御 
  ┃      ┃      ┣ __init__.py
  ┃      ┃      ┣ admin.py
  ┃      ┃      ┣ apps.py
  ┃      ┃      ┣ models.py
  ┃      ┃      ┣ test.py
  ┃      ┃      ┣ urls.py
  ┃      ┃      ┗ views.py
  ┃      ┣ <static>
  ┃      ┃      ┣ <css>
  ┃      ┃      ┣ <images>
  ┃      ┃      ┃    ┗ favicon.png
  ┃      ┃      ┗ <  js  >
  ┃      ┃             ┗ <base_layout> ※ base_layout.html画面上でインポートされるjsファイルを管理 
  ┃      ┃                  ┗ fade_popup.js ※ ポップアップの表示非表示の操作
  ┃      ┣ <templates>
  ┃      ┃      ┣ base_layout.html
  ┃      ┃      ┣ top_page.html
  ┃      ┃      ┣ model_analyze.html
  ┃      ┃      ┗ model_manage.html
  ┃      ┗ manage.py
  ┗ <src>
スポンサーリンク

モーダル化+レスポンス表示の開発手順(Django側)

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

settings.pyの編集

今回も追加のアプリケーションがあったので、それを追加しましょう。すでにpipでインストール済なので、settings.pyファイルだけ変更でOKです。

# app_keiba/settings.pyの編集

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'model_analyzer',
    "markdownx",
    # 以下2行を追加
    'crispy_forms',
    "crispy_bootstrap5",
]
# 以下2行を追加
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

models.pyの編集

今のままの実装だと、変な警告文がでて面倒なので登録日のカラム情報を変更します。

以下の箇所を修正してください。

# model_analyzer/models.pyの編集



class ModelList(models.Model):
    model_id = models.CharField(
        verbose_name="モデルID", max_length=64)

~~~~~~~~~ 省略 ~~~~~~~~~

    memo = models.TextField(verbose_name="備考", max_length=1024, blank=True)
    # 以下を修正
    regist_date = models.DateTimeField(
        verbose_name="登録日", auto_now_add=True)
    # 以下を追加
    delete_flag = models.BooleanField(verbose_name="削除フラグ", default=False)

regist_dateカラムの内容を変更しています。また、delete_flagというカラムも追加しました。

また、models.pyを修正したため、今回もmigrationの実行をしましょう。

python manage.py makemigrations
python manage.py migrate

※上記の2コマンドを実行した際にエラーが出てしまう人は、以下のように対処してください。
 このやり方は環境を完全に作り直すやり方なので、開発初期のころには有効ですが普通に濫用はやめた方がいいです。

・db.sqlite3ファイルを削除
・「model_analyzer/__pycache__」フォルダを削除
・「model_analyzer/migrations/__pycache__」フォルダを削除

上記の3つを削除後、再度migrationコマンドを実行してください。

forms.pyの編集

インポート時に表示するフォームの「モデル説明」の欄に初期値を設定するようにしています。

~~~~~~~~~~~ 省略 ~~~~~~~~~~~

# 以下のModelImportFormsに丸っと置き換えてください
class ModelImportForms(forms.Form):
    model_name = forms.CharField(
        label="モデル名",
        required=True,
        widget=forms.TextInput(
            attrs={
                "placeholder": "分かりやすい名前にしましょう"
            }
        )
    )
    motivate = forms.CharField(
        label="モデルの説明",
        required=True,
        widget=forms.Textarea(
            attrs={
                "rows": 8
            }
        ),
        initial="""以下テンプレ、好きに変えてください。
【モデル作成の動機】

【モデルの目的】

【期待すること】

【特徴量】

【目的変数】
"""
    )
    memo = forms.CharField(
        widget=forms.Textarea(
            attrs={
                "rows": 2,
                'placeholder': '他に何か書いておきたいこと',
            }
        ),
        label="備考",
        required=False
    )
    model_info_json = forms.FileField(
        label="モデル情報 (モデル分析管理クラスでエクスポートしたmodel_info.json)",
        required=True,
        validators=[FileExtensionValidator(['json', ])],
        widget=forms.FileInput(
            attrs={
                "placeholder": "モデル分析管理クラスでエクスポートしたJsonファイルを指定"
            }
        )
    )

~~~~~~~~~~~ 省略 ~~~~~~~~~~~

views.pyの編集

views.pyファイルをこのタイミングで今後もモジュール管理しやすいようにコードを整理しました。

差分取るのが面倒だったので、丸ごとコピペしてください。

from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms

# Create your views here.


class TopPageView(View):
    nav_params = {
        "model_analyze": True,
        "model_manage": True
    }

    def get(self, request: WSGIRequest):

        return render(request, 'top_page.html', self.nav_params)


class ModelAnalyzeView(View):
    nav_params = {
        "model_analyze": False,
        "model_manage": True
    }

    def get(self, request: WSGIRequest):
        return render(request, 'model_analyze.html', self.nav_params)


class ModelManageView(View):
    nav_params = {
        "model_analyze": True,
        "model_manage": False
    }

    def start(self, request: WSGIRequest):
        generate_model_import_forms(self.nav_params, request)

    def get(self, request: WSGIRequest):
        self.start(request)
        return render(request, 'model_manage.html', self.nav_params)

    def post(self, request: WSGIRequest):
        flag = validation_model_import_forms(request, self.nav_params)
        if flag:
            return redirect("./model-manage")
        return render(request, 'model_manage.html', self.nav_params)

フォーム管理および操作する関数の作成

views.pyのファイル内をスパゲッティにしないように、別のpyファイルをスパゲッティにしましょう。

上記で示していた「フォルダ構成」通りになっている前提で話します。

model_analyzer/tools/form_control」フォルダ配下に「model_manage_forms.py」ファイルを新規作成して、以下の内容を丸っとコピペしましょう。

from model_analyzer.forms import ModelImportForms, ModelListForms
from model_analyzer.models import ModelList
from django.contrib import messages
import json
from django.core.handlers.wsgi import WSGIRequest
import uuid


def set_submit_token(request: WSGIRequest):
    request_token = request.session.get("csrfmiddlewaretoken")
    if request_token == "":
        request.session["csrfmiddlewaretoken"] = str(uuid.uuid4())


def check_submit_token(request: WSGIRequest,) -> bool:
    request_token = request.session.get("csrfmiddlewaretoken")
    submit_token = request.POST.get('csrfmiddlewaretoken')
    if not submit_token:
        return False
    if request_token != submit_token:
        request.session["csrfmiddlewaretoken"] = submit_token
        return True
    else:
        return False


def generate_model_import_forms(nav_params: dict, request: WSGIRequest):
    form = ModelImportForms()
    if "model_form" in nav_params:
        nav_params.pop("model_form")
    if "form" in nav_params:
        nav_params.pop("form")
    nav_params |= {"form": form}
    set_submit_token(request)


def validation_model_import_forms(request: WSGIRequest, nav_params: dict):
    form = ModelImportForms(request.POST, request.FILES)
    if form.is_valid():
        model_info = json.loads(
            form.cleaned_data.pop("model_info_json").read())
        form.cleaned_data |= model_info
        model_form = ModelListForms(form.cleaned_data)
        if model_form.is_valid():
            if check_submit_token(request):
                model_form.save()
                messages.success(
                    request,
                    "モデルのインポートに成功しました。" +
                    f"モデル名: {model_form.cleaned_data['model_name']}, " +
                    f"モデル種別: {model_form.cleaned_data['model_type']}"
                )
                nav_params |= {"form": ModelImportForms()}
            else:
                return True
        else:
            nav_params |= {"model_form": model_form, "form": form}

    return False

ちなみに、このモジュールの中でモデルのインポートが成功したらレスポンスとしてメッセージを出すように処理しています。

気になる方だけソースを確認してみてください。これからも同じようなメッセージをいくつか追加する機会が出てくるので、知ってて損はないかと思います。

スポンサーリンク

モーダル化+レスポンス表示の開発手順(WEB画面側)

以下の3ファイルを丸ごとコピペしましょう。

$(document).ready(function () {
    $.each(
        $("[id=success-alert]"),
        function (i, elem) {
            $(elem).fadeTo(
                3000 + 1000 * i,
                1000
            ).slideUp(
                1000, function () {
                    $(elem).slideUp(500);
                }
            );
        }
    );
});
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="icon" type="image/x-icon" href="/static/images/favicon.png">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
  <title>競馬予想ソフト</title>
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
    crossorigin="anonymous"></script>
  <header>
    <nav class="navbar navbar-expand bg-body-secondary" data-bs-theme="dark">
      <div class="container-fluid">
        <a class="navbar-brand" href="./index.html">um-AI</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
          aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbar">
          <ul class="navbar-nav me-auto">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="./index.html">Home</a>
            </li>
            <li class="nav-item">
              {% if model_analyze %}
              <a class="nav-link active" href="./model-analyze">モデル分析</a>
              {% else %}
              <a class="nav-link disabled" href="./model-analyze" tabindex="-1" aria-disabled="true">モデル分析</a>
              {% endif %}
            </li>
            <li class="nav-item">
              {% if model_manage %}
              <a class="nav-link active" href="./model-manage">モデル管理</a>
              {% else %}
              <a class="nav-link disabled" href="./model-manage" tabindex="-1" aria-disabled="true">モデル管理</a>
              {% endif %}
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>

  {% if messages %}
  <div class="container-fluid" style="margin-top:1rem;">
    <div class="notification is-info">
      {% for message in messages %}
      <div {% if message.tags %}class="alert alert-{{ message.tags }}" {% endif %} id="success-alert"
        style="font-size: small;">
        {{ message }}
      </div>
      {% endfor %}
    </div>
  </div>
  {% endif %}
  <main>
    <div class="d-flex container-fluid">
      {% block content %}
      <!-- ここに個別のhtmlが入る -->
      {% endblock %}
    </div>
  </main>
  <script src="/static/js/base_layout/fade_popup.js"></script>
</body>

</html>
{% extends "base_layout.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
  <div class="row my-1">
    {% if form.errors %}
    <ul>
      {% for error in form.errors %}
      <li>{{ error }}</li><!--form内にエラーがある際に表示させる-->
      {% endfor %}
    </ul>
    {% endif %}
    {% if model_form.errors %}
    <h4>Upload Json File Error!!</h4>
    {{ model_form.errors }}
    {% endif %}
  </div>
  <div class="row my-2">
    <div class="col-sm-12 col-xs-12 col-md-6">
      <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modelImportForm">
        モデルインポート
      </button>
    </div>

    <div id="modelImportForm" class="modal fade" tabindex="-1" aria-labelledby="modelImportLabel" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <form class="" action="" method="post" enctype="multipart/form-data">
            <div class="modal-header">
              <h1 class="modal-title fs-5" id="exampleModalLabel">Model Import Form</h1>
              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
              {% csrf_token %}
              {{ form|crispy }}
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
              <button type="submit" class="btn btn-primary">送信</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
  <div class="row">

  </div>
</div>
{% endblock %}
スポンサーリンク

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

それでは、インポート機能のモーダル化とレスポンス表示ができたので、サーバを起動しましょう。

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

python manage.py runserver

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

以下の画面が表示されればOKです!

簡単な動作確認

正常にインポートできるパターンは、完成イメージのgifで示した通りになっていればOKです。

逆に敢えてインポートするものを間違えてみましょう。

以下のmodel_info_error.jsonファイルを準備してください。コピペして貰っていいです。

{
  "model_dir": "E:\\keiba_dev\\keiba_ai\\models\\first_model",
  "model_analyze_dir": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze",
  "model_predict_dir": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze\\00_predict",
  "bet_columns_map": {
    "tan": "bet_tan"
  },
  "pl_column_map": {
    "tan": "pl_tan"
  },
  "return_hit_rate_file": {
    "tan": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze\\tan\\hit_and_return_rate.csv"
  },
  "fav_bet_num_dir": {
    "tan": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze\\tan\\fav_bet_num"
  },
  "profit_loss_dir": {
    "tan": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze\\tan\\profit_loss"
  },
  "odds_graph_file": {
    "tan": "E:\\keiba_dev\\keiba_ai\\models\\first_model\\analyze\\tan\\odds_graph"
  },
  "confidence_column": "pred_prob",
  "confidence_rank_column": "pred_rank"
}

普通のmodel_info.jsonとの違いは、Jsonの項目から「model_id」と「model_type」の二つを削除しています。

以下のgifの通りにモデルをインポートしてみましょう。最後の「Upload Json File Error!!」と出ていればOKです。

以上で、モデルをインポート機能のモーダル化とレスポンス表示の手順完了です。お疲れさまでした。

スポンサーリンク

ソース公開しました!

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

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

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

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

スポンサーリンク

前回記事

モデルの作成+フォームの作成

スポンサーリンク

次回記事

モデルのリスト表示

コメント

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