競馬予測AIの作成㉕(順位予測モデル生成プログラムの改修)

Python
  1. はじめに
  2. 順位予測モデル作成のシステム概要
    1. システムの目的
    2. 主な機能
    3. 技術的特徴
    4. プロジェクト構造
  3. 実行環境構築・実行手順
    1. 実行環境要件
      1. ハードウェア要件
      2. ソフトウェア要件
    2. インストール手順
      1. Pythonライブラリのインストール
      2. GPU環境設定(オプション)
      3. プロジェクトセットアップ
    3. データ準備
      1. 入力データの配置
      2. データ形式確認
    4. 設定ファイル設定
      1. config.ini の主要設定項目
    5. 実行手順
      1. 事前確認
      2. システム実行
      3. 実行時の表示例
    6. 実行結果の確認
      1. 出力ディレクトリ構造
      2. 主要結果ファイル
      3. 結果の分析例
    7. トラブルシューティング
      1. よくあるエラーと対処法
      2. GPU関連のトラブルシューティング
      3. ログレベル変更(詳細確認用)
      4. メモリ不足時の設定調整
    8. 実行時間の目安
  4. システムアーキテクチャ
    1. 全体アーキテクチャ
  5. モジュール設計
    1. train_model.py(メインエントリーポイント)
      1. 概要
      2. 主要関数
      3. 処理フロー
      4. 出力データ
    2. model_creator.py(統合管理)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    3. data_processor.py(データ前処理)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    4. model_trainer.py(モデル学習・最適化)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    5. ensemble_methods.py(アンサンブル学習)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    6. evaluator.py(評価・可視化)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    7. utils.py(共通機能・的中率計算)
      1. 概要
      2. 主要関数
      3. 処理フロー(的中率計算)
      4. 出力データ
    8. score_calculator.py(総合スコア計算)
      1. 概要
      2. 主要クラス
      3. 処理フロー
      4. 出力データ
    9. generate_folder_list.py(フォルダ管理)
      1. 概要
      2. 主要関数
      3. 処理フロー
      4. 出力データ
  6. 設定管理(config.ini)
    1. 設定ファイル構造
  7. データ設計
    1. 入力データ仕様
    2. 出力データ構造
  8. 評価指標
    1. 統計的評価指標
    2. 競馬特有の的中率
    3. 総合スコア計算
  9. 処理時間とリソース要件
    1. 処理時間目標
    2. リソース要件
  10. エラー処理とログ
    1. ログレベル設定
    2. ログ出力内容
    3. エラー処理方針
  11. ハイパーパラメータ最適化設定
    1. XGBoostパラメータ最適化範囲
    2. LightGBMパラメータ最適化範囲
    3. CatBoostパラメータ最適化範囲
  12. アンサンブル手法詳細
    1. 重み付けアンサンブル
    2. スタッキングアンサンブル
    3. ブレンディングアンサンブル
  13. 全コード
    1. train_model.py(メインエントリーポイント)
    2. model_creator.py(統合管理)
    3. data_processor.py(データ前処理)
    4. model_trainer.py(モデル学習・最適化)
    5. ensemble_methods.py(アンサンブル学習)
    6. evaluator.py(評価・可視化)
    7. utils.py(共通機能・的中率計算)
    8. score_calculator.py(総合スコア計算)
    9. generate_folder_list.py(フォルダ管理)

はじめに

JRA日本中央競馬会の順位予測モデルを作成するプログラムについて改修しました。
下記のプログラムにハードコーディングしていた内容をconfig.iniで設定できるようにしました。

  • 学習・テストデータ分割日
  • 予測対象
  • 使用する特徴量
  • 予測モデル作成時の機械学習で使用するパラメータ

順位予測モデルの作成には、複数のプログラムを用いて作成します。
下記に設計書ベースで各プログラムの解説を記載しました。

順位予測モデル作成のシステム概要

システムの目的

競馬レース予測システムは、機械学習技術を活用して競馬レースの着順を予測するシステムです。
複数の機械学習モデルとアンサンブル手法を組み合わせることで、高精度な予測を実現します。

主な機能

  • データ前処理: レース結果データの前処理とエンコーディング
  • モデル学習: XGBoost、LightGBM、CatBoostによる機械学習
  • アンサンブル学習: 5種類のアンサンブル手法による予測精度向上
  • 性能評価: 統計的評価指標と競馬特有の的中率計算
  • 結果可視化: グラフによる予測結果と特徴量重要度の可視化

技術的特徴

  • 3種類の基本モデル: XGBoost, LightGBM, CatBoost
  • 5種類のアンサンブル手法: 平均、重み付け、スタッキング、投票、ブレンディング
  • Optunaによるハイパーパラメータ最適化
  • GPU対応による高速処理
  • 設定ファイルによる柔軟な設定管理
  • リソース監視機能: CPU/GPU/メモリ使用量の動的監視

プロジェクト構造

keiba_ai/
├── config.ini                      # 設定ファイル(プロジェクトルート)
├── make_model/                     # プログラム群ディレクトリ
│   ├── train_model.py              # メインエントリーポイント
│   ├── model_creator.py            # 統合管理
│   ├── data_processor.py           # データ前処理
│   ├── model_trainer.py            # モデル学習・最適化
│   ├── ensemble_methods.py         # アンサンブル学習
│   ├── evaluator.py                # 評価・可視化
│   ├── utils.py                    # 共通機能・的中率計算
│   ├── score_calculator.py         # 総合スコア計算
│   └── generate_folder_list.py     # フォルダ管理
├── data/                           # データディレクトリ
└── web_scraping/                   # Webスクレイピング関連

パス管理の仕組み:

  • utils.get_project_root(): make_modelディレクトリから親ディレクトリ(keiba_ai)を取得
  • utils.get_config_path(): プロジェクトルート + ‘config.ini’ で設定ファイルパスを構築
  • 相対パス基準: make_modelディレクトリ内のプログラムが基準点

実行環境構築・実行手順

実行環境要件

ハードウェア要件

項目 最小要件 推奨構成 最適構成
CPU 4コア(Intel i5以上) 8コア以上 16コア以上
メモリ 8GB 16GB 32GB以上
GPU なし(CPU実行) GTX 1060以上 RTX 3080以上
GPU メモリ 6GB以上 12GB以上
ディスク容量 5GB 20GB 50GB以上
OS Windows 10以降 同左 同左

ソフトウェア要件

ソフトウェア バージョン 必須/推奨 用途
Python 3.8以降 必須 メイン実行環境
CUDA 11.0以降 推奨 GPU加速
cuDNN 8.0以降 推奨 GPU最適化
Git 最新版 推奨 ソースコード管理

インストール手順

Pythonライブラリのインストール

Windows環境で仮想環境を構築する手順は下記となります。

# 仮想環境作成
python -m venv keiba_env
keiba_env\Scripts\activate

# 必須ライブラリのインストール
pip install pandas==1.5.3
pip install numpy==1.24.3
pip install scikit-learn==1.3.0
pip install xgboost==1.7.5
pip install lightgbm==3.3.5
pip install catboost==1.2.2
pip install optuna==3.2.0
pip install matplotlib==3.7.1
pip install japanize-matplotlib==1.1.3
pip install joblib==1.3.1
pip install configparser
pip install psutil==5.9.5

# 追加ライブラリ(分析・可視化用)
pip install seaborn==0.12.2
pip install plotly==5.15.0

GPU環境設定(オプション)

NVIDIA GPU使用時:

# CUDA Toolkitインストール確認
nvidia-smi
nvcc --version

# CUDAライブラリ用環境変数設定
set CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8
set PATH=%CUDA_PATH%\bin;%PATH%

注意事項:

  • XGBoost 1.7.5とLightGBM 3.3.5では、GPUサポートが標準で含まれています
  • 実際のGPU使用はconfig.iniuse_gpu = true設定で制御されます

プロジェクトセットアップ

# プロジェクトディレクトリに移動
cd keiba_ai

# ディレクトリ構造確認
dir
# 以下のファイル・ディレクトリが存在することを確認
# ├── config.ini
# ├── make_model/
# ├── data/
# └── web_scraping/

データ準備

入力データの配置

入力データはNetkeibaから取得したレース結果となります。
レース結果の取得は「get_raceResults.py」を実行して取得してください。
取得したレース結果を学習用データに変換するために「prepare_raceResults.py」を実行すると、
入力データを配置することができます。

get_raceResults.pyとprepare_raceResults.pyは下記を参照してください。

競馬予測AIの作成㉔(Netkeibaのレース結果をデータベースに保存)

データ形式確認

必須カラム(18項目):

time,horse,father,mother,age,rider_weight,rider,odds,popular,horse_weight,distance,weather,ground,condition,date,race_name,location,round

入力データのサンプル:

time,horse,father,mother,age,rider_weight,rider,odds,popular,horse_weight,distance,weather,ground,condition,date,race_name,location,round
77.2,ハーモニーソング,デクラレーションオブウォー,リルティングソング,牝3,55,戸崎圭太,1.2,1,452,1300,小雨, ダート , 重 ,2025/5/10,3歳未勝利,2回東京5日目,1 R
77.2,シルフレイ,フォーウィールドライブ,エレディア,牝3,55,横山武史,12.3,3,426,1300,小雨, ダート , 重 ,2025/5/10,3歳未勝利,2回東京5日目,1 R
77.6,ショウナンラリー,ナダル,ショウナンライム,牡3,57,吉田豊,5.4,2,480,1300,小雨, ダート , 重 ,2025/5/10,3歳未勝利,2回東京5日目,1 R
78.1,ベルレオーネ,アポロケンタッキー,ナムラマッチェリ,牡3,57,石川裕紀,24.6,4,462,1300,小雨, ダート , 重 ,2025/5/10,3歳未勝利,2回東京5日目,1 R

設定ファイル設定

config.ini の主要設定項目

[settings]
# 学習・テストデータ分割日
train_test_split_date = 2024-12-1

[features]
# 予測対象列
target = time

# 使用する特徴量(カンマ区切り)
# すべての特徴量: horse,father,mother,age,rider_weight,rider,odds,popular,horse_weight,distance,weather,ground,condition,date,race_name,location,round
use_features = horse,father,mother,age,rider,distance,weather,ground,condition,date,race_name,location,round

# 特徴量重要度の閾値
importance_threshold = 0.01

[paths]
# 入力データパス(keiba_aiからの相対パス)
input_path = ./data/raceresults/prepared_raceresults/
# 出力パス
output_path = ./data/model/

[model_params]
# Optuna最適化設定(実行時間に大きく影響)
optuna_trials = 10          # 試行回数(多いほど精度向上、時間増加)
optuna_timeout = 1800       # タイムアウト(秒)
use_gpu = true              # GPU使用(要CUDA環境)
early_stopping_rounds = 10
validation_split = 0.2
random_state = 0

 

実行手順

事前確認

# 1. 設定ファイル確認
type config.ini

# 2. 入力データ確認
dir data/raceresults/prepared_raceresults/

# 3. Python環境確認
python --version
pip list | findstr /I "pandas numpy xgboost lightgbm catboost optuna"

# 4. GPU環境確認(GPU使用時)
nvidia-smi                  # GPU認識確認

システム実行

# make_modelディレクトリに移動
cd make_model

# メインプログラム実行
python train_model.py

実行時の表示例

2025-06-25 08:36:57 - utils - INFO - ================================================================================
2025-06-25 08:36:57 - utils - INFO - 競馬レース予測システム - ログ設定完了
2025-06-25 08:36:57 - utils - INFO - ================================================================================
2025-06-25 08:36:57 - utils - INFO - ログファイル: /keiba_ai/data/model/20250625_083657\training_log_20250625_083657.log
2025-06-25 08:36:57 - utils - INFO - ログレベル: DEBUG
2025-06-25 08:36:57 - utils - INFO - 実行タイムスタンプ: 20250625_083657
2025-06-25 08:36:57 - utils - INFO - システム情報:
2025-06-25 08:36:57 - utils - INFO -   - OS: Windows 10
2025-06-25 08:36:57 - utils - INFO -   - Python: 3.10.6
2025-06-25 08:36:57 - utils - INFO -   - CPU: 24コア
2025-06-25 08:36:57 - utils - INFO -   - メモリ: 31.7GB
2025-06-25 08:36:57 - utils - INFO -   - GPU: GPUtilがインストールされていません
2025-06-25 08:36:57 - __main__ - INFO - プログラム開始
2025-06-25 08:36:57 - __main__ - INFO - 設定されたログレベル: DEBUG
...
2024-06-23 14:45:30 - INFO - 競馬レース予測システム - 処理完了
2024-06-23 14:45:30 - INFO - 実行時間: 17130.00秒 (285.50分)

実行結果の確認

出力ディレクトリ構造

# 実行完了後の確認
dir ../data/model/

# 例: ../data/model/20240623_100000/traindata_2020_2024/
├── xgboost/
│   ├── evaluation_results.csv
│   ├── xgb_model.pkl
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── lightgbm/
├── catboost/
├── average_ensemble/
├── weighted_ensemble/
├── stacking_ensemble/
├── voting_ensemble/
├── blending_ensemble/
├── feature_importance.csv
├── 的中率結果.csv
├── 的中率結果_with_score.csv
└── training_log_20240623_100000.log

主要結果ファイル

ファイル 内容 確認方法
的中率結果_with_score.csv 各モデルの的中率と総合スコア head 的中率結果_with_score.csv
evaluation_results.csv 統計的評価指標(R2, RMSE, MAE) 各モデルディレクトリ内
feature_importance.csv 特徴量重要度ランキング head feature_importance.csv
training_log_*.log 実行ログ(詳細) tail -n 50 training_log_*.log

結果の分析例

# PowerShellを起動してから実行

# 1. 最高スコアモデル確認
Get-Content 的中率結果_with_score.csv | ConvertFrom-Csv | Sort-Object {[double]$_.スコア} -Descending | Select-Object -First 1

# 2. 単勝的中率上位確認  
Get-Content 的中率結果_with_score.csv | ConvertFrom-Csv | Sort-Object {[double]($_.単勝 -replace '%','')} -Descending | Select-Object -First 5

トラブルシューティング

よくあるエラーと対処法

エラー 原因 対処法
ModuleNotFoundError ライブラリ未インストール pip install [ライブラリ名]
FileNotFoundError: config.ini 設定ファイル不存在 config.iniの配置確認
CUDA out of memory GPU メモリ不足 use_gpu = false に設定
Permission denied 実行権限なし chmod +x train_model.py
CSVファイルが存在しません 入力データなし データファイル配置確認
WARNING: xgboost does not provide the extra 'gpu' 新バージョンの正常動作 問題なし(GPUサポート標準装備)
WARNING: lightgbm does not provide the extra 'gpu' 新バージョンの正常動作 問題なし(GPUサポート標準装備)

GPU関連のトラブルシューティング

GPU認識確認:

# NVIDIA GPU確認
nvidia-smi

# CUDA確認
nvcc --version

GPU使用時の設定調整:

[model_params]
use_gpu = true              # GPU使用する場合
memory_limit_gb = 6         # GPU メモリに応じて調整

CPU環境での実行:

[model_params]
use_gpu = false             # GPU使用しない場合
cpu_cores = -1              # 全CPUコア使用

ログレベル変更(詳細確認用)

[logging]
log_level = DEBUG  # より詳細なログ出力

メモリ不足時の設定調整

[model_params]
optuna_trials = 5           # 試行回数削減
memory_limit_gb = 8         # メモリ制限設定

実行時間の目安

データサイズ 環境 処理時間目安
10万レコード CPU(8コア、16GB) 2-3時間
10万レコード GPU(RTX 3080) 1-1.5時間
50万レコード CPU(8コア、16GB) 8-12時間
50万レコード GPU(RTX 3080) 3-5時間
100万レコード GPU(RTX 3080) 6-10時間

注意: 処理時間はoptuna_trials設定値に大きく依存します。

システムアーキテクチャ

全体アーキテクチャ

モジュール設計

train_model.py(メインエントリーポイント)

概要

システム全体の実行エントリーポイントとなるプログラム。設定ファイル(config.ini)から動的に設定を読み込み、入力データの存在確認を行った後、ModelCreatorを通じて機械学習モデルの作成、学習、評価を実行する。

実行場所: keiba_ai/make_model/train_model.py
設定ファイル: keiba_ai/config.ini(utils経由でアクセス)

主要関数

関数名 引数 戻り値 説明
load_config() なし ConfigParser 設定ファイル読み込みとデフォルト値設定
get_log_level_from_config(config) ConfigParser int ログレベル文字列から定数変換
check_input_files() なし bool 入力CSVファイル存在確認
main() なし int メイン処理実行とステータス返却

処理フロー

出力データ

  • ログファイル: training_log_{timestamp}.log
  • フォルダリスト: folder_list.json
  • スコア付き結果: 的中率結果_with_score.csv

model_creator.py(統合管理)

概要

モデル作成・学習・評価の統合管理を行うクラス。設定ファイルに基づくデータの前処理から複数モデルの作成、学習、評価、アンサンブル学習、特徴量重要度分析までの一連の処理を統合的に管理する。

主要クラス

ModelCreator

メソッド名 種別 引数 戻り値 説明
__init__(input_path, output_path, train_test_split_date) コンストラクタ str, str, str None 初期化と各コンポーネント設定
run(current_date_time) パブリック str None メイン処理の統合実行
analyze_and_save_feature_importance(feature_importance_dict, model_dir) パブリック dict, str None 特徴量重要度分析・保存
plot_feature_importance(sorted_features, model_dir, top_n=20) パブリック list, str, int None 特徴量重要度可視化

処理フロー

出力データ

  • モデルディレクトリ: {timestamp}/traindata_{start_year}_{end_year}/
  • 統合特徴量重要度: feature_importance.csv, feature_importance.svg
  • 次回学習用設定: important_features.ini

data_processor.py(データ前処理)

概要

競馬予測のための機械学習に使用するデータの前処理を行うプログラム。設定ファイルに基づく特徴量とラベルの準備、時系列によるデータ分割、カテゴリカル変数のエンコーディングを実行する。

主要クラス

DataProcessor

メソッド名 種別 引数 戻り値 説明
__init__() コンストラクタ なし None 設定ファイル読み込みと初期化
prepare_features_and_labels(race_results) パブリック DataFrame tuple 特徴量とラベルの分離
encode_categorical_features(all_train_features, test_features, model_save_dir_path) パブリック DataFrame, DataFrame, str tuple カテゴリカル変数エンコーディング
split_data_into_train_test(features, labels, split_day) パブリック DataFrame, ndarray, str tuple 時系列データ分割

処理フロー

出力データ

  • エンコード済みデータ: DataFrame形式(カラム名保持)
  • エンコーダーファイル: oe_x.pkl(圧縮形式)

model_trainer.py(モデル学習・最適化)

概要

競馬レース予測のための機械学習モデル(XGBoost, LightGBM, CatBoost)のトレーニングと最適化を行うプログラム。Optunaを使用してハイパーパラメータを最適化し、学習済みモデルを保存する。config.iniからの設定読み込み、リソース監視機能、詳細ログ出力等の拡張機能も実装。

主要クラス

ModelTrainer

メソッド名 種別 引数 戻り値 説明
__init__() コンストラクタ なし None 設定読み込みとパラメータ設定
train_and_save_model(encoded_train_features, encoded_test_features, all_train_labels, test_labels, model_save_dir_path, model_type) パブリック DataFrame, DataFrame, ndarray, ndarray, str, str tuple モデル学習・保存の統括実行
optimize_xgboost_hyperparameters(trial, train_dmatrix, validation_dmatrix, test_dmatrix, test_labels) パブリック Trial, DMatrix, DMatrix, DMatrix, ndarray float XGBoostハイパーパラメータ最適化
optimize_lightgbm_hyperparameters(trial, train_dataset, validation_dataset) パブリック Trial, Dataset, Dataset float LightGBMハイパーパラメータ最適化
optimize_catboost_hyperparameters(trial, train_pool, validation_pool) パブリック Trial, Pool, Pool float CatBoostハイパーパラメータ最適化
print_config_summary() パブリック なし None 設定値の詳細表示
_start_resource_monitoring() プライベート なし None リソース監視開始
_stop_resource_monitoring() プライベート なし None リソース監視停止

処理フロー

出力データ

  • XGBoost: xgb_model.pkl, xgb_hyperparameters.txt
  • LightGBM: lightgbm_model.pkl, lgb_hyperparameters.txt
  • CatBoost: catboost_model.cbm, catboost_hyperparameters.txt

ensemble_methods.py(アンサンブル学習)

概要

複数の機械学習モデルの予測結果を様々なアンサンブル手法で統合し、予測精度の向上を図るプログラム。5種類のアンサンブル手法を実装し、特徴量重要度分析も提供する。

主要クラス

EnsembleMethods

メソッド名 種別 引数 戻り値 説明
__init__() コンストラクタ なし None 初期化
weighted_ensemble_predict(predictions, models, r2_scores, ensemble_save_dir_path) パブリック dict, dict, dict, str ndarray R2スコア重み付けアンサンブル
average_ensemble_predict(predictions, models, test_features, test_labels, ensemble_save_dir_path) パブリック dict, dict, DataFrame, ndarray, str ndarray 単純平均アンサンブル
stacking_ensemble_predict(train_features, test_features, train_labels, test_labels, base_models, ensemble_save_dir_path) パブリック DataFrame, DataFrame, ndarray, ndarray, dict, str ndarray スタッキングアンサンブル
voting_ensemble_predict(predictions, models, test_features, test_labels, ensemble_save_dir_path) パブリック dict, dict, DataFrame, ndarray, str ndarray 投票アンサンブル
blending_ensemble_predict(train_features, test_features, train_labels, test_labels, base_models, ensemble_save_dir_path) パブリック DataFrame, DataFrame, ndarray, ndarray, dict, str ndarray ブレンディングアンサンブル

処理フロー

出力データ

  • 重み付け: ensemble_weights.txt, weighted_ensemble_feature_importance.svg
  • スタッキング: stacking_meta_model.pkl, stacking_feature_importance.svg
  • ブレンディング: blending_meta_model.pkl, blending_feature_importance.svg
  • 投票: model_contributions.txt, voting_ensemble_feature_importance.svg
  • 平均: ensemble_feature_importance.svg

evaluator.py(評価・可視化)

概要

機械学習モデルの予測性能を評価し、結果を可視化・保存するプログラム。R2スコア、MAE、RMSEなどの評価指標の計算、予測結果の散布図作成、特徴量重要度の分析を行う。

主要クラス

Evaluator

メソッド名 種別 引数 戻り値 説明
__init__() コンストラクタ なし None 初期化
evaluate_model_performance(test_labels, predicted_race_times, model_save_dir_path) パブリック ndarray, ndarray, str float 性能評価指標計算・保存
plot_actual_predicted(test_labels, predicted_race_times, model_save_dir_path, r2) パブリック ndarray, ndarray, str, float None 実測値vs予測値散布図作成
plot_feature_importance(model, model_type, model_save_dir_path) パブリック object, str, str None 特徴量重要度可視化

処理フロー

出力データ

  • 評価結果: evaluation_results.csv
  • 散布図: 実タイムと予測タイムの散布図.svg
  • 特徴量重要度: 予測タイムの重要度分析_{model_type}.svg

utils.py(共通機能・的中率計算)

概要

競馬予測システムにおける多目的ユーティリティプログラム。プロジェクト全体で使用されるパス管理機能、レース予測結果の評価・分析機能、競馬特有の的中率計算機能、データ保存機能を統合的に提供する。

パス管理の重要性:

  • 設定ファイル(config.ini)はプロジェクトルート(keiba_ai/)に配置
  • プログラム群はmake_model/サブディレクトリに配置
  • 各プログラムから設定ファイルへの統一的なアクセスを提供

主要関数

関数名 引数 戻り値 説明
get_project_root() なし str プロジェクトルートディレクトリパス取得
get_config_path() なし str config.iniの絶対パス取得
save_predictions_to_csv(test_features, test_labels, predicted_race_times, model_save_dir_path) DataFrame, ndarray, ndarray, str None 予測結果CSV保存
analyze_and_save_win_rates(model_save_dir_path, train_test_split_date) str, str None 的中率計算・保存
calculate_win_accuracy(ranked_race_results) DataFrame int 単勝的中判定
calculate_place_accuracy(ranked_race_results) DataFrame int 複勝的中判定
calculate_quinella_accuracy(ranked_race_results) DataFrame int 馬連的中判定
calculate_quinella_place_accuracy(ranked_race_results) DataFrame int ワイド的中判定
calculate_trio_accuracy(ranked_race_results) DataFrame int 三連複的中判定

処理フロー(的中率計算)

出力データ

  • 予測結果: 予測結果.csv(cp932エンコーディング)
  • 的中率結果: 的中率結果.csv(累積追記形式)

score_calculator.py(総合スコア計算)

概要

モデル性能スコアを計算し、的中率結果のCSVファイルにスコアを追加するプログラム。単勝的中率、R2スコア、RMSE、MAEに重み付けを行い、総合的な性能評価スコアを算出する。

主要クラス

ScoreCalculator

メソッド名 種別 引数 戻り値 説明
calculate_model_score(win_rate, r2, rmse, mae) 静的 float, float, float, float float 重み付けスコア計算
add_scores_to_csv(file_path, output_path) 静的 str, str None CSVにスコア列追加・保存

処理フロー

出力データ

  • スコア付き結果: 的中率結果_with_score.csv

generate_folder_list.py(フォルダ管理)

概要

競馬予測システムで生成された学習結果フォルダを管理するプログラム。YYYYMMdd_HHMMSS形式のフォルダを検索し、一覧をJSON形式で保存する。

主要関数

関数名 引数 戻り値 説明
generate_folder_list(target_dir, output_path) str, str None フォルダ一覧生成・JSON保存
main() なし None コマンドライン引数処理

処理フロー

出力データ

  • フォルダリスト: folder_list.json(Web UI用)

設定管理(config.ini)

設定ファイル構造

ファイル配置: keiba_ai/config.ini(プロジェクトルート)
アクセス方法: utils.get_config_path()による統一的なパス取得

[settings]
# 学習・テストデータ分割日
train_test_split_date = 2025-2-1

[features]
# 予測対象(ラベル)
target = time

# 使用する特徴量(カンマ区切り)
use_features = horse,father,mother,age,rider,distance,weather,ground,condition,date,race_name,location,round

# 特徴量の重要度の閾値
importance_threshold = 0.01

[paths]
# 入出力パスの設定(プロジェクトルートからの相対パス)
input_path = ./data/raceresults/prepared_raceresults/
output_path = ./data/model/

[model_params]
# Optuna最適化設定
optuna_trials = 10
optuna_timeout = 1800
use_gpu = true
early_stopping_rounds = 10
validation_split = 0.2
random_state = 0

# 拡張パラメータ
max_training_time_per_model = 3600
memory_limit_gb = 24
cpu_cores = -1

# Optuna設定の詳細化
optuna_direction = minimize
optuna_sampler = TPESampler
optuna_pruner = MedianPruner
optuna_n_startup_trials = 5
optuna_n_warmup_steps = 5

# 評価指標設定
primary_metric = rmse
secondary_metrics = mae,r2
metric_direction = minimize

# デバッグ・ログ設定
verbose_training = false
save_intermediate_results = false
show_optimization_progress = true

# リソース監視設定
memory_monitoring = true
performance_monitoring = false

[xgboost]
# XGBoost最適化範囲
max_depth_range = 3,10
eta_range = 0.001,0.1
# 固定パラメータ
objective = reg:squarederror
eval_metric = rmse
num_boost_round = 10000

[lightgbm]
# LightGBM最適化範囲
max_depth_range = 3,10
num_leaves_range = 20,150
learning_rate_range = 0.001,0.1
# 固定パラメータ
objective = regression
metric = rmse

[catboost]
# CatBoost最適化範囲
iterations_range = 100,1000
depth_range = 4,10
learning_rate_range = 0.01,0.3
# 固定パラメータ
od_type = Iter
verbose = false

[logging]
log_level = DEBUG
detailed_logging = true
performance_logging = true

データ設計

入力データ仕様

データ配置: keiba_ai/data/raceresults/prepared_raceresults/
アクセス方法: config.iniのinput_path設定による動的パス指定

ファイル名形式: {開始年}_{終了年}.csv
エンコーディング: cp932
必須カラム(17項目):
- date: レース日付(YYYY-MM-DD形式)
- time: レースタイム(予測対象)
- horse: 馬名
- father: 父馬名
- mother: 母馬名
- age: 馬齢
- rider_weight: 騎手負担重量
- rider: 騎手名
- odds: オッズ
- popular: 人気
- horse_weight: 馬体重
- distance: 距離
- weather: 天候
- ground: 馬場状態
- condition: コンディション
- race_name: レース名
- location: 開催場所
- round: ラウンド

出力データ構造

出力ベースディレクトリ: keiba_ai/data/model/
アクセス方法: config.iniのoutput_path設定による動的パス指定

keiba_ai/data/model/{timestamp}/traindata_{start_year}_{end_year}/
├── xgboost/
│   ├── evaluation_results.csv          # R2, RMSE, MAE
│   ├── xgb_hyperparameters.txt         # 最適化パラメータ
│   ├── xgb_model.pkl                   # 学習済みモデル
│   ├── oe_x.pkl                        # エンコーダー
│   ├── 実タイムと予測タイムの散布図.svg
│   ├── 予測タイムの重要度分析_xgboost.svg
│   └── 予測結果.csv                    # テストデータ予測結果
├── lightgbm/
│   ├── evaluation_results.csv
│   ├── lgb_hyperparameters.txt
│   ├── lightgbm_model.pkl
│   ├── oe_x.pkl
│   ├── 実タイムと予測タイムの散布図.svg
│   ├── 予測タイムの重要度分析_lightgbm.svg
│   └── 予測結果.csv
├── catboost/
│   ├── evaluation_results.csv
│   ├── catboost_hyperparameters.txt
│   ├── catboost_model.cbm
│   ├── oe_x.pkl
│   ├── 実タイムと予測タイムの散布図.svg
│   ├── 予測タイムの重要度分析_catboost.svg
│   └── 予測結果.csv
├── average_ensemble/
│   ├── evaluation_results.csv
│   ├── ensemble_feature_importance.svg
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── weighted_ensemble/
│   ├── evaluation_results.csv
│   ├── ensemble_weights.txt            # モデル重み
│   ├── weighted_ensemble_feature_importance.svg
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── stacking_ensemble/
│   ├── evaluation_results.csv
│   ├── stacking_meta_model.pkl         # メタモデル
│   ├── stacking_feature_importance.svg
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── voting_ensemble/
│   ├── evaluation_results.csv
│   ├── model_contributions.txt         # モデル寄与度
│   ├── voting_ensemble_feature_importance.svg
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── blending_ensemble/
│   ├── evaluation_results.csv
│   ├── blending_meta_model.pkl         # メタモデル
│   ├── blending_feature_importance.svg
│   ├── 実タイムと予測タイムの散布図.svg
│   └── 予測結果.csv
├── feature_importance.csv              # 統合特徴量重要度
├── feature_importance.svg              # 重要度可視化
├── important_features.ini              # 次回学習用設定
├── 的中率結果.csv                       # 競馬的中率
├── 的中率結果_with_score.csv           # スコア付き結果
└── training_log_{timestamp}.log        # 実行ログ

評価指標

統計的評価指標

  • R2スコア(決定係数): モデルの説明力(0-1、1に近いほど良い)
  • RMSE(平均二乗誤差の平方根): 予測誤差の大きさ(0に近いほど良い)
  • MAE(平均絶対誤差): 予測誤差の平均(0に近いほど良い)

競馬特有の的中率

券種 的中条件 計算方法 実装詳細
単勝 1着の完全予測 実順位1位 == 予測順位1位 int(results.iloc[0]['rank_real'] == results.iloc[0]['rank_predict'])
複勝 3着以内の予測(8頭以上)
2着以内の予測(8頭未満)
着内に予測上位が含まれるか horses_num >= 8 で判定分岐
any(results.iloc[:N]['rank_predict'] <= M)
馬連 1-2着の組み合わせ予測 予測1位+2位の合計 == 3 int(sum(results.iloc[:2]['rank_predict']) == 3)
ワイド 3着以内に2頭以上の予測 3着以内に予測1-3位が2頭以上 int(sum(results.iloc[:3]['rank_predict'] <= 3) >= 2)
三連複 1-3着の完全予測 予測1-3位の合計 == 6 int(sum(results.iloc[:3]['rank_predict']) == 6)

総合スコア計算

総合スコア = 0.4 × 的中率 + 0.3 × R2 + 0.2 × (1/(1+RMSE)) + 0.1 × (1/(1+MAE))

処理時間とリソース要件

処理時間目標

処理フェーズ 実装基準時間 最大許容時間
データ前処理 5-15分 30分
XGBoost学習 30-60分 120分
LightGBM学習 20-40分 80分
CatBoost学習 40-80分 150分
アンサンブル学習 10-30分 60分
評価・可視化 5-15分 30分
全体処理時間 2-4時間 8時間

リソース要件

リソース 最小要件 推奨構成 最適構成
CPU 4コア 8コア以上 16コア以上
メモリ 8GB 16GB 32GB以上
GPU なし GTX 1060以上 RTX 3080以上
GPU メモリ 6GB以上 12GB以上
ディスク容量 5GB 20GB 50GB以上

エラー処理とログ

ログレベル設定

# utils.py - ログレベル変換
def get_log_level_from_string(level_string):
    level_mapping = {
        'DEBUG': logging.DEBUG,
        'INFO': logging.INFO,
        'WARNING': logging.WARNING,
        'ERROR': logging.ERROR,
        'CRITICAL': logging.CRITICAL
    }
    return level_mapping.get(level_string.upper(), logging.INFO)

ログ出力内容

ログレベル 出力内容 出力例
DEBUG 詳細なデバッグ情報 パラメータ値、中間結果
INFO 正常な処理進捗 処理開始・完了、データサイズ
WARNING 注意が必要な状況 データスキップ、設定値警告
ERROR エラー発生時の詳細 例外情報、スタックトレース
CRITICAL システム停止レベル 致命的エラー

エラー処理方針

  • Fail Fast: 早期エラー検出と適切な処理
  • Graceful Degradation: 部分的失敗でもシステム継続
  • Detailed Logging: エラー原因と対処法の詳細記録
  • リソース保護: メモリ・GPU使用量の監視と制限

ハイパーパラメータ最適化設定

XGBoostパラメータ最適化範囲

パラメータ 最適化範囲 スケール デフォルト
max_depth 3-10 uniform 6
min_child_weight 0-10 uniform 1
eta 0.001-0.1 log 0.3
subsample 0.5-1.0 uniform 1.0
colsample_bytree 0.5-1.0 uniform 1.0
alpha 0.01-10.0 log 0
lambda 0.01-10.0 log 1
gamma 0.01-10.0 log 0

LightGBMパラメータ最適化範囲

パラメータ 最適化範囲 スケール デフォルト
max_depth 3-10 uniform -1
num_leaves 20-150 uniform 31
learning_rate 0.001-0.1 log 0.1
feature_fraction 0.5-1.0 uniform 1.0
bagging_fraction 0.5-1.0 uniform 1.0
bagging_freq 1-10 uniform 0
lambda_l1 0.0001-10.0 log 0.0
lambda_l2 0.0001-10.0 log 0.0
min_child_weight 0.001-10.0 log 0.001

CatBoostパラメータ最適化範囲

パラメータ 最適化範囲 スケール デフォルト
iterations 100-1000 uniform 1000
depth 4-10 uniform 6
learning_rate 0.01-0.3 log 0.03
l2_leaf_reg 1e-8-10.0 log 3.0
border_count 32-255 uniform 254
bagging_temperature 0-1 uniform 1.0
random_strength 1e-8-10 log 1.0

アンサンブル手法詳細

重み付けアンサンブル

概要: R2スコアに基づいてモデルの重みを計算し、重み付け平均を行う

計算式:

weights[model] = r2_scores[model] / sum(r2_scores.values())
prediction = Σ(weights[model] × predictions[model])

特徴:

  • 最軽量・高速
  • 解釈性が高い
  • 実装が簡単

スタッキングアンサンブル

概要: ベースモデルの予測を特徴量としてメタモデル(XGBoost)で学習

処理手順:

  1. ベースモデルの予測生成(N×3行列)
  2. XGBoostメタモデル学習
  3. メタモデルで最終予測

メタモデルパラメータ:

meta_model = XGBRegressor(
    n_estimators=100,
    learning_rate=0.1,
    random_state=42
)

特徴:

  • 高精度が期待できる
  • 計算コストが高い
  • メタモデルが複雑になりがち

ブレンディングアンサンブル

概要: 元の特徴量とベースモデル予測を結合してメタモデル学習

処理手順:

  1. 元特徴量(M次元)+ ベース予測(3次元)を結合
  2. 結合特徴量(M+3次元)でXGBoostメタモデル学習
  3. メタモデルで最終予測

メタモデルパラメータ:

meta_model = XGBRegressor(
    n_estimators=100,
    learning_rate=0.1,
    random_state=42
)

特徴:

  • 最高精度が期待できる
  • 元特徴量の情報も活用
  • 最も計算コストが高い

全コード

train_model.py(メインエントリーポイント)

# ==============================================================================
# 競馬レース予測モデル作成・評価プログラム(実行エントリーポイント)
# ==============================================================================
# プログラムの概要:
#   競馬レース予測システムの実行エントリーポイントとなるプログラム。
#   設定ファイル(config.ini)から動的に設定を読み込み、入力データの
#   存在確認を行った後、ModelCreatorを通じて機械学習モデルの作成、
#   学習、評価を実行する。処理完了後はフォルダ管理とスコア計算を行い、
#   実行時間の測定・表示も提供する。
#
# プログラムの主な機能:
#   1. 設定管理機能
#      - config.iniからの動的設定読み込み
#      - デフォルト値の設定と上書き処理
#      - 相対パス→絶対パス変換
#   2. 入力データ検証機能
#      - 入力ディレクトリの存在確認
#      - CSVファイルの存在確認
#      - 現在ディレクトリとパス情報の表示
#   3. モデル作成制御機能
#      - ModelCreatorのインスタンス化と実行制御
#      - タイムスタンプ生成と処理時間測定
#      - 複数モデル・アンサンブル手法の統合実行
#   4. 後処理機能
#      - 学習結果フォルダ一覧のJSON生成
#      - 的中率結果へのスコア付与
#      - 実行時間の詳細表示
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install pandas numpy scikit-learn xgboost lightgbm catboost optuna
#    pip install matplotlib seaborn japanize_matplotlib joblib configparser
#
# 2. 設定ファイル(config.ini)の準備:
#    [paths]セクション:
#    - input_path: 入力データディレクトリ(デフォルト: './data/raceresults/prepared_raceresults/')
#    - output_path: 出力ディレクトリ(デフォルト: './data/model/')
#    [settings]セクション:
#    - train_test_split_date: 学習/テスト分割日(デフォルト: '2024-12-1')
#    その他のセクションも適切に設定
#
# 3. 入力データの準備:
#    - INPUT_PATHに前処理済みレース結果CSVファイルを配置
#    - prepare_raceResults.py等で前処理済みのデータが必要
#    - データ形式:CSV(エンコーディング:cp932)
#    - 必須カラム:date, time, その他特徴量
#
# 4. 実行方法:
#    python train_model.py
#    (コマンドライン引数は不要、すべて設定ファイルから読み込み)
#
# 5. 処理の流れ:
#    1) load_config(): 設定ファイルから値を読み込み、デフォルト値を上書き
#    2) check_input_files(): 入力ディレクトリとCSVファイルの存在確認
#    3) ModelCreator初期化: 読み込んだ設定値でインスタンス作成
#    4) creator.run(): モデル作成・学習・評価の全工程を実行
#    5) generate_folder_list(): 学習結果フォルダ一覧をJSONで生成
#    6) ScoreCalculator: 的中率結果にスコアを付与したCSVを作成
#    7) 実行時間の計算と表示
#
# ==============================================================================
# 設定ファイル(config.ini)仕様
# ==============================================================================
# 1. デフォルト設定値:
#    - INPUT_PATH = './data/raceresults/prepared_raceresults/'
#    - OUTPUT_PATH = './data/model/'
#    - TRAIN_TEST_SPLIT_DATE = '2024-12-1'
#
# 2. 設定ファイル構造:
#    [paths]
#    input_path = ./data/raceresults/prepared_raceresults/
#    output_path = ./data/model/
#
#    [settings]
#    train_test_split_date = 2024-12-1
#
# 3. パス処理:
#    - 相対パスは自動的に絶対パスに変換
#    - get_project_root()でプロジェクトルートを基準にパス解決
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. モデル学習結果(ModelCreator経由):
#    [OUTPUT_PATH]/[YYYYMMDD_HHMMSS]/traindata_[start_year]_[end_year]/
#    ├── 各モデルディレクトリ(xgboost, lightgbm, catboost)
#    ├── 各アンサンブルディレクトリ(5種類)
#    └── 特徴量重要度ファイル
#
# 2. フォルダ管理ファイル:
#    - [OUTPUT_PATH]/folder_list.json
#      └─ YYYYMMDD_HHMMSS形式のフォルダ一覧(降順ソート)
#      └─ Web UI(report2.html)でのフォルダ選択に使用
#
# 3. スコア付き評価ファイル:
#    - [OUTPUT_PATH]/[timestamp]/的中率結果.csv
#      └─ 各モデルの基本的中率データ
#    - [OUTPUT_PATH]/[timestamp]/的中率結果_with_score.csv
#      └─ 重み付けスコア付きの評価データ
#
# 4. 実行ログ情報:
#    - 処理開始・終了時刻
#    - 実行時間(秒・分単位)
#    - 現在ディレクトリとパス情報
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本プログラムが呼び出すモジュール:
#    - ModelCreator (model_creator.py)
#      └─ モデル作成・学習・評価の全工程を実行
#    - generate_folder_list (generate_folder_list.py)
#      └─ 学習結果フォルダのJSON一覧生成
#    - ScoreCalculator (score_calculator.py)
#      └─ 的中率結果への重み付けスコア付与
#    - utils (utils.py)
#      └─ 設定ファイルパス取得、プロジェクトルート取得
#
# 2. 間接的に実行されるモジュール(ModelCreator経由):
#    - DataProcessor: データ前処理とエンコーディング
#    - ModelTrainer: 個別モデルの学習と最適化
#    - EnsembleMethods: アンサンブル学習の実行
#    - Evaluator: モデル評価と可視化
#
# 3. Web UI連携:
#    - start_server_and_open_url.bat: 結果確認用Webサーバー起動
#    - report2.html: ブラウザでの結果表示
#    - folder_list.json: Web UIでのフォルダ選択データ
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行前の確認事項:
#    - config.iniファイルが存在し、適切に設定されていること
#    - 入力ディレクトリに前処理済みCSVファイルが存在すること
#    - 十分なディスク容量があること(数GB以上)
#    - GPU環境が利用可能であること(推奨)
#
# 2. 実行環境要件:
#    - Python 3.x(3.8以降推奨)
#    - GPU環境での実行を強く推奨(CUDA対応)
#    - メモリ容量:16GB以上推奨
#    - ディスク容量:学習データサイズの5-10倍程度
#
# 3. 実行時間の目安:
#    - 小規模データ(数万レコード):1-3時間
#    - 中規模データ(数十万レコード):3-8時間
#    - 大規模データ(数百万レコード):8時間以上
#    - GPU使用により大幅な時間短縮が可能
#
# 4. エラー対応:
#    - 設定ファイル読み込みエラー:デフォルト値で継続実行
#    - 入力ファイル不存在:エラーメッセージ表示後、sys.exit(1)
#    - ModelCreator内エラー:個別モデルはスキップ、他は継続
#    - フォルダ一覧生成エラー:警告表示、処理継続
#
# 5. 設定ファイルのカスタマイズ:
#    - パス設定は環境に応じて変更推奨
#    - 分割日は学習データの期間に応じて調整
#    - [features]セクションで特徴量選択のカスタマイズ可能
#    - GPU設定はハードウェア構成に応じて調整
#
# 6. 結果の確認方法:
#    - コンソール出力で実行状況を確認
#    - start_server_and_open_url.batで詳細結果をブラウザ表示
#    - folder_list.jsonでフォルダ一覧を確認
#    - 的中率結果_with_score.csvで総合評価を確認
#
# 7. 拡張・カスタマイズ:
#    - 新しいモデルタイプの追加はModelTrainerを修正
#    - アンサンブル手法の追加はEnsembleMethodsを修正
#    - 評価指標の追加はEvaluator・ScoreCalculatorを修正
#    - UI改善はreport2.htmlを修正
#

# ==============================================================================
# 競馬レース予測モデル作成・評価プログラム(実行エントリーポイント)
# ==============================================================================

# ==============================================================================
# 競馬レース予測モデル作成・評価プログラム(実行エントリーポイント)
# ==============================================================================

import os
import matplotlib

matplotlib.use('Agg')  # GUIを使わないバックエンドに設定

import time
import datetime
import sys
import configparser
import logging
import logging.handlers

from utils import get_config_path, get_project_root, setup_project_logging, log_system_info, close_logging
from model_creator import ModelCreator
from generate_folder_list import generate_folder_list
from score_calculator import ScoreCalculator


def load_config():
    """
    設定ファイルから値を読み込む
    """
    config = configparser.ConfigParser()
    config_path = get_config_path()
    config.read(config_path, encoding='utf-8')

    global INPUT_PATH, OUTPUT_PATH, TRAIN_TEST_SPLIT_DATE

    # デフォルト値を指定
    INPUT_PATH = './data/raceresults/prepared_raceresults/'
    OUTPUT_PATH = './data/model/'
    TRAIN_TEST_SPLIT_DATE = '2024-12-1'

    # 設定ファイルから値を読み込む
    if 'paths' in config:
        INPUT_PATH = config['paths'].get('input_path', INPUT_PATH)
        OUTPUT_PATH = config['paths'].get('output_path', OUTPUT_PATH)

    if 'settings' in config:
        TRAIN_TEST_SPLIT_DATE = config['settings'].get('train_test_split_date', TRAIN_TEST_SPLIT_DATE)

    # 相対パスを絶対パスに変換(必要な場合)
    if not os.path.isabs(INPUT_PATH):
        INPUT_PATH = os.path.join(get_project_root(), INPUT_PATH)
    if not os.path.isabs(OUTPUT_PATH):
        OUTPUT_PATH = os.path.join(get_project_root(), OUTPUT_PATH)

    return config


def get_log_level_from_config(config):
    """
    設定ファイルからログレベルを取得する

    :param configparser.ConfigParser config: 設定オブジェクト
    :return: ログレベル定数
    :rtype: int
    """
    from utils import get_log_level_from_string

    default_level = 'INFO'

    if 'logging' in config:
        log_level_str = config['logging'].get('log_level', default_level)
    else:
        log_level_str = default_level

    return get_log_level_from_string(log_level_str)


def check_input_files():
    """
    入力パスにCSVファイルが存在するかを確認する
    :return: CSVファイルが存在する場合はTrue、存在しない場合はFalse
    """
    logger = logging.getLogger(__name__)

    # 現在のディレクトリを取得して表示
    current_directory = os.getcwd()
    logger.info(f"現在のディレクトリ: {current_directory}")
    logger.info(f"入力パス: {INPUT_PATH}")

    # 入力ディレクトリの存在確認
    if not os.path.exists(INPUT_PATH):
        logger.error(f"入力ディレクトリが存在しません: {INPUT_PATH}")
        return False

    # CSVファイルの存在確認
    csv_files = [f for f in os.listdir(INPUT_PATH) if f.endswith('.csv')]
    if not csv_files:
        logger.error(f"{INPUT_PATH} にレース結果のCSVファイルが存在しません。")
        return False

    logger.info(f"検出されたCSVファイル数: {len(csv_files)}")
    for csv_file in csv_files:
        logger.info(f"  - {csv_file}")

    return True


def main():
    # タイムスタンプの生成
    current_date_time = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')

    # 設定ファイルからの読み込み
    config = load_config()

    # ログディレクトリの設定(OUTPUT_PATHの下にタイムスタンプディレクトリを作成)
    log_directory = os.path.join(OUTPUT_PATH, current_date_time)

    # 設定ファイルからログレベルを取得
    log_level = get_log_level_from_config(config)

    # ログ設定の初期化(設定ファイルから読み取ったログレベルを使用)
    log_file_path = setup_project_logging(log_directory, current_date_time, log_level)

    # システム情報のログ出力
    log_system_info()

    logger = logging.getLogger(__name__)
    logger.info("プログラム開始")
    logger.info(f"設定されたログレベル: {logging.getLevelName(log_level)}")

    # 入力ファイルの存在確認
    if not check_input_files():
        logger.error("入力ファイルの確認に失敗しました。プログラムを終了します。")
        sys.exit(1)

    start_time = time.time()
    logger.info(f"処理開始時刻: {datetime.datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S')}")

    # ModelCreator インスタンスの作成
    creator = ModelCreator(INPUT_PATH, OUTPUT_PATH, TRAIN_TEST_SPLIT_DATE)

    # 設定値確認
    logger.info("実行前設定値確認:")
    logger.info(f"  - 入力パス: {INPUT_PATH}")
    logger.info(f"  - 出力パス: {OUTPUT_PATH}")
    logger.info(f"  - 分割日: {TRAIN_TEST_SPLIT_DATE}")
    logger.info(f"  - ログファイル: {log_file_path}")

    # config.iniの内容を部分的に表示
    if creator.model_trainer.config.has_section('model_params'):
        logger.info(f"  - Optuna試行回数: {creator.model_trainer.optuna_trials}")
        logger.info(f"  - GPU使用: {creator.model_trainer.use_gpu}")

    # モデルの作成と評価の実行
    logger.info(f"実行タイムスタンプ: {current_date_time}")
    logger.info("ModelCreator.run() 開始")

    try:
        creator.run(current_date_time)
        logger.info("ModelCreator.run() 正常完了")
    except Exception as e:
        logger.error(f"ModelCreator.run() でエラーが発生: {str(e)}")
        raise

    # 実行時間の表示
    run_time = time.time() - start_time
    logger.info(f'処理終了時刻: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    logger.info('実行時間: {:.2f}秒 ({:.2f}分)'.format(run_time, run_time / 60))

    # モデルを作成したフォルダのリストを作成
    target_path = OUTPUT_PATH
    output_file = f"{OUTPUT_PATH}folder_list.json"
    logger.info("フォルダリスト生成開始")

    try:
        generate_folder_list(target_path, output_file)
        logger.info(f"フォルダリスト生成完了: {output_file}")
    except Exception as e:
        logger.error(f"フォルダリスト生成でエラー: {str(e)}")

    # 的中率結果にスコアを追加
    input_csv_path = f"{target_path}{current_date_time}/的中率結果.csv"
    output_csv_path = f"{target_path}{current_date_time}/的中率結果_with_score.csv"

    logger.info("スコア計算開始")
    try:
        ScoreCalculator.add_scores_to_csv(input_csv_path, output_csv_path)
        logger.info(f"スコア計算完了: {output_csv_path}")
    except Exception as e:
        logger.error(f"スコア計算でエラー: {str(e)}")

    logger.info("=" * 80)
    logger.info("競馬レース予測システム - 処理完了")
    logger.info("=" * 80)
    logger.info(f"結果ディレクトリ: {log_directory}")
    logger.info(f"ログファイル: {log_file_path}")
    logger.info("プログラム終了")

    # ログシステムの適切な終了
    close_logging()


if __name__ == "__main__":
    main()

model_creator.py(統合管理)

# ==============================================================================
# モデル作成・学習・評価の統合管理プログラム
# ==============================================================================
# プログラムの概要:
#   競馬予測システムの中核となるプログラム。設定ファイルに基づく
#   データの前処理から複数モデルの作成、学習、評価、アンサンブル学習、
#   特徴量重要度分析までの一連の処理を統合的に管理する。各種コンポーネントを
#   連携させ、予測モデルの作成から性能評価、次回学習用設定ファイル生成まで
#   を一貫して実行する。
#
# プログラムの主な機能:
#   1. 統合処理制御
#      - 設定ファイル(config.ini)の読み込みと管理
#      - 入力ディレクトリ内の全CSVファイルの順次処理
#      - タイムスタンプ付きディレクトリ構造の自動生成
#   2. モデル作成と学習の制御
#      - 基本モデル(XGBoost/LightGBM/CatBoost)の作成と最適化
#      - 5つのアンサンブル手法の統合実行
#        * 単純平均アンサンブル
#        * 重み付けアンサンブル
#        * スタッキングアンサンブル
#        * ボーティングアンサンブル
#        * ブレンディングアンサンブル
#   3. 評価・分析・保存機能
#      - 各モデルの性能評価(R2, RMSE, MAE)
#      - 競馬特有の的中率計算(単勝、複勝、馬連、ワイド、三連複)
#      - 特徴量重要度の総合分析と可視化
#      - 次回学習用の重要特徴量設定ファイル生成
#   4. 詳細ログ出力機能(新規追加)
#      - 各処理段階での詳細ログ記録
#      - エラー・警告の包括的記録
#      - 処理時間・進捗状況の記録
#      - デバッグ・トラブルシューティング支援
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install pandas scikit-learn xgboost lightgbm catboost optuna
#    pip install matplotlib japanize_matplotlib joblib configparser
#
# 2. 設定ファイル(config.ini)の準備:
#    [features]セクション:
#    - target: 予測対象列名(デフォルト: 'time')
#    - importance_threshold: 特徴量重要度閾値(デフォルト: 0.0)
#    その他の設定項目も適切に設定
#
# 3. 入力データの準備:
#    - 入力パスに前処理済みレース結果CSVファイルを配置
#      必須カラム:
#      - date: レース日付(YYYY-MM-DD形式文字列)
#      - time(または設定したターゲット列): 予測対象
#      - その他の特徴量カラム
#
# 4. 実行方法:
#    - train_model.pyから呼び出される(直接実行は想定していない)
#    - ModelCreator(input_path, output_path, split_date)でインスタンス化
#    - run(current_date_time)で全処理を実行
#
# 5. 処理の流れ:
#    1) 設定ファイルの読み込みと初期化
#    2) 入力ディレクトリ内の各CSVファイルに対して以下を実行:
#       a) レース結果の読み込みと日付型変換
#       b) タイムスタンプ付きディレクトリの作成
#       c) DataProcessorによる前処理とエンコーディング
#       d) 3つの基本モデルの学習・評価・保存
#       e) 5つのアンサンブル手法の実行・評価・保存
#       f) 各手法の予測結果保存と的中率計算
#    3) 全モデルの特徴量重要度を統合分析
#    4) 重要特徴量設定ファイル(important_features.ini)の生成
#    5) 特徴量重要度の可視化グラフ作成
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. ディレクトリ構造:
#    [output_path]/[timestamp]/traindata_[start_year]_[end_year]/
#    ├── xgboost/
#    ├── lightgbm/
#    ├── catboost/
#    ├── average_ensemble/
#    ├── weighted_ensemble/
#    ├── stacking_ensemble/
#    ├── voting_ensemble/
#    ├── blending_ensemble/
#    ├── feature_importance.csv
#    ├── feature_importance.svg
#    ├── important_features.ini
#    └── [model_type]_feature_importance.csv (各モデル別)
#
# 2. 各モデルディレクトリ内の共通ファイル:
#    - evaluation_results.csv: 評価指標(R2, RMSE, MAE)
#    - [model]_hyperparameters.txt: 最適化されたハイパーパラメータ
#    - [model]_model.pkl: 学習済みモデル(圧縮形式)
#    - 実タイムと予測タイムの散布図.svg: 予測精度可視化
#    - 予測タイムの重要度分析_[model].svg: 特徴量重要度グラフ
#    - 予測結果.csv: テストデータ予測結果
#    - oe_x.pkl: エンコーダーモデル(基本モデルのみ)
#
# 3. アンサンブル特有のファイル:
#    - ensemble_weights.txt: 重み付けアンサンブルの重み
#    - stacking_meta_model.pkl: スタッキング用メタモデル
#    - blending_meta_model.pkl: ブレンディング用メタモデル
#    - model_contributions.txt: 各モデルの寄与度
#    - 各アンサンブル手法の特徴量重要度グラフ
#
# 4. 特徴量分析ファイル:
#    - feature_importance.csv: 全モデル統合重要度(相対重要度付き)
#    - important_features.ini: 次回学習用設定ファイル
#    - feature_importance.svg: 重要度可視化グラフ(上位20件)
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - train_model.py
#      └─ ModelCreatorインスタンス化と実行制御
#
# 2. 呼び出すコンポーネント:
#    - DataProcessor: データ前処理とエンコーディング
#    - ModelTrainer: 個別モデルの学習と最適化
#    - EnsembleMethods: アンサンブル学習の実行
#    - Evaluator: モデル評価と可視化
#    - utils: 予測結果保存と的中率計算
#
# 3. 設定・ユーティリティ:
#    - utils.get_config_path(): 設定ファイルパス取得
#    - configparser: 設定ファイル読み込み
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行環境:
#    - GPU環境での実行を推奨(CUDA対応)
#    - 十分なディスク容量(数GB以上、データサイズに依存)
#    - 大容量メモリ(16GB以上推奨、データサイズに依存)
#
# 2. データ要件:
#    - 入力CSVは適切にフォーマットされていること
#    - date列は文字列形式(pandas.to_datetime()で変換可能)
#    - 必須カラムが全て存在し、欠損値が適切に処理されていること
#    - ファイル名は[start_year]_[end_year].csvの形式を推奨
#
# 3. 処理時間とリソース:
#    - 全体の処理に数時間から半日かかる可能性
#    - Optunaによるハイパーパラメータ最適化が時間の大部分を占める
#    - GPU使用時はメモリ使用量の監視が必要
#    - アンサンブル手法は基本モデルよりも計算コストが高い
#
# 4. 設定ファイル依存:
#    - config.iniの[features]セクションが重要特徴量選択に影響
#    - importance_threshold設定で次回学習の特徴量が決定
#    - 設定ファイルの破損や不正な値はデフォルト値で処理
#
# 5. エラー処理と継続実行:
#    - データ分割で適切なサイズが確保できない場合はスキップ
#    - 個別モデルでエラーが発生しても他のモデルは継続実行
#    - ファイル保存エラーでも処理は継続
#
# 6. 特徴量重要度分析:
#    - 全モデルの重要度を統合して相対重要度を計算
#    - importance_threshold以上の特徴量のみを次回学習用に選択
#    - important_features.iniは手動での設定変更も可能
#

import datetime
import os
import utils
import pandas as pd
import configparser
import logging

from utils import get_config_path

from model_trainer import ModelTrainer
from ensemble_methods import EnsembleMethods
from data_processor import DataProcessor
from evaluator import Evaluator

from utils import get_logger

class ModelCreator:
    """
    機械学習モデルの作成、訓練、評価を行うクラス。
    """

    def __init__(self, input_path, output_path, train_test_split_date):
        """
        :param str input_path: 入力データのパス
        :param str output_path: 出力(モデルや結果)を保存するパス
        :param str train_test_split_date: 訓練データとテストデータを分割する日付
        """
        self.logger = get_logger(__name__)
        self.logger.info("ModelCreator初期化開始")

        self.input_path = input_path
        self.output_path = output_path
        self.train_test_split_date = train_test_split_date

        self.logger.info(f"入力パス設定: {self.input_path}")
        self.logger.info(f"出力パス設定: {self.output_path}")
        self.logger.info(f"データ分割日設定: {self.train_test_split_date}")

        # 各コンポーネントの初期化
        self.logger.info("各コンポーネントの初期化開始")
        try:
            self.model_trainer = ModelTrainer()
            self.logger.info("ModelTrainer初期化完了")
        except Exception as e:
            self.logger.error(f"ModelTrainer初期化エラー: {str(e)}")
            raise

        try:
            self.ensemble_methods = EnsembleMethods()
            self.logger.info("EnsembleMethods初期化完了")
        except Exception as e:
            self.logger.error(f"EnsembleMethods初期化エラー: {str(e)}")
            raise

        try:
            self.data_processor = DataProcessor()
            self.logger.info("DataProcessor初期化完了")
        except Exception as e:
            self.logger.error(f"DataProcessor初期化エラー: {str(e)}")
            raise

        try:
            self.evaluator = Evaluator()
            self.logger.info("Evaluator初期化完了")
        except Exception as e:
            self.logger.error(f"Evaluator初期化エラー: {str(e)}")
            raise

        # 設定ファイルの読み込み
        self.logger.info("設定ファイル読み込み開始")
        self.config = configparser.ConfigParser()
        config_path = get_config_path()
        self.config.read(config_path, encoding='utf-8')
        self.logger.info(f"設定ファイル読み込み完了: {config_path}")

        self.logger.info("ModelCreator初期化完了")

    def run(self, current_date_time):
        """
        モデルの作成、訓練、評価、およびアンサンブル手法の適用を実行するメインメソッド。
        このメソッドは以下の手順を実行する:
        1. 入力データの読み込み
        2. データの前処理
        3. 複数のモデル(XGBoost, LightGBM, CatBoost)の訓練と評価
        4. 各種アンサンブル手法(平均、重み付け、スタッキング、ボーティング、ブレンディング)の適用
        5. 結果の保存と可視化
        6. 特徴量の重要度分析と保存

        :param str current_date_time: 実行日時のタイムスタンプ(フォルダ名に使用)
        :return: None
        """
        self.logger.info("ModelCreator.run() メイン処理開始")
        self.logger.info(f"実行タイムスタンプ: {current_date_time}")

        # 設定値の表示(デバッグ用)
        self.logger.info("モデル学習開始 - 設定値確認")
        self.model_trainer.print_config_summary()

        # 特徴量の重要度を追跡するディクショナリ
        all_feature_importance = {}
        self.logger.info("特徴量重要度追跡辞書を初期化")

        # 入力ディレクトリ内のCSVファイルを取得
        csv_files = [f for f in os.listdir(self.input_path) if f.endswith(".csv")]
        self.logger.info(f"処理対象CSVファイル数: {len(csv_files)}")

        for file_index, file in enumerate(csv_files, 1):
            self.logger.info(f"ファイル処理開始 [{file_index}/{len(csv_files)}]: {file}")

            try:
                # CSVファイルの読み込みと日付の変換
                self.logger.info(f"CSVファイル読み込み開始: {file}")
                race_results = pd.read_csv(os.path.join(self.input_path, file), encoding='cp932')
                self.logger.info(f"データ読み込み完了: {len(race_results)}行, {len(race_results.columns)}列")

                race_results['date'] = pd.to_datetime(race_results['date'])
                self.logger.info("日付カラムの変換完了")

                # 出力ディレクトリの作成
                file_split = file.split('_')
                dir_path = os.path.join(self.output_path,
                                        f'{current_date_time}/traindata_{file_split[0]}_{file_split[1]}/')
                os.makedirs(dir_path, exist_ok=True)
                self.logger.info(f"出力ディレクトリ作成: {dir_path}")

                # 特徴量とラベルの準備
                self.logger.info("データ前処理開始")
                features, labels = self.data_processor.prepare_features_and_labels(race_results)
                self.logger.info(f"特徴量・ラベル分離完了: 特徴量 {features.shape}, ラベル {labels.shape}")

                (all_train_features,
                 test_features,
                 all_train_labels,
                 test_labels) = self.data_processor.split_data_into_train_test(features,
                                                                               labels,
                                                                               self.train_test_split_date)

                if all_train_features is None:
                    self.logger.warning(f"データ分割に失敗: {file} - スキップします")
                    continue

                self.logger.info(
                    f"データ分割完了: 学習データ {all_train_features.shape}, テストデータ {test_features.shape}")

                # カテゴリカル変数のエンコーディング
                self.logger.info("カテゴリカル変数エンコーディング開始")
                (encoded_train_features,
                 encoded_test_features) = self.data_processor.encode_categorical_features(all_train_features,
                                                                                          test_features,
                                                                                          dir_path)
                self.logger.info(
                    f"エンコーディング完了: 学習データ {encoded_train_features.shape}, テストデータ {encoded_test_features.shape}")

                # モデル予測結果と評価スコアを格納する辞書
                predictions = {}
                models = {}
                r2_scores = {}
                feature_importance_by_model = {}

                # 各モデル(XGBoost, LightGBM, CatBoost)の訓練と評価
                model_types = ['xgboost', 'lightgbm', 'catboost']
                self.logger.info(f"基本モデル学習開始: {model_types}")

                for model_index, model_type in enumerate(model_types, 1):
                    self.logger.info(f"モデル学習開始 [{model_index}/{len(model_types)}]: {model_type}")

                    try:
                        model_dir = os.path.join(dir_path, model_type)
                        os.makedirs(model_dir, exist_ok=True)
                        self.logger.info(f"モデルディレクトリ作成: {model_dir}")

                        # モデル学習
                        (predicted_race_times, model) = self.model_trainer.train_and_save_model(encoded_train_features,
                                                                                                encoded_test_features,
                                                                                                all_train_labels,
                                                                                                test_labels,
                                                                                                model_dir, model_type)
                        self.logger.info(f"{model_type}モデル学習完了")

                        # モデルの評価
                        predictions[model_type] = predicted_race_times
                        models[model_type] = model
                        r2_scores[model_type] = self.evaluator.evaluate_model_performance(test_labels,
                                                                                          predicted_race_times,
                                                                                          model_dir)
                        self.logger.info(f"{model_type}モデル評価完了: R2 = {r2_scores[model_type]:.6f}")

                        # 散布図と特徴量重要度のプロット
                        self.evaluator.plot_actual_predicted(test_labels,
                                                             predicted_race_times,
                                                             model_dir,
                                                             r2_scores[model_type])
                        self.evaluator.plot_feature_importance(model, model_type, model_dir)
                        self.logger.info(f"{model_type}可視化完了")

                        # 予測結果の保存
                        utils.save_predictions_to_csv(test_features, test_labels, predicted_race_times, model_dir)
                        self.logger.info(f"{model_type}予測結果保存完了")

                        # 予測結果から的中率を計算
                        utils.analyze_and_save_win_rates(model_dir, self.train_test_split_date)
                        self.logger.info(f"{model_type}的中率計算完了")

                        # 特徴量の重要度を収集
                        if model_type == 'xgboost':
                            importance = model.get_score(importance_type='gain')
                        elif model_type == 'lightgbm':
                            importance = dict(
                                zip(model.feature_name(), model.feature_importance(importance_type='gain')))
                        elif model_type == 'catboost':
                            importance = dict(zip(model.feature_names_, model.get_feature_importance()))

                        feature_importance_by_model[model_type] = importance
                        self.logger.info(f"{model_type}特徴量重要度収集完了: {len(importance)}個の特徴量")

                        # 全体の特徴量重要度に追加
                        for feature, value in importance.items():
                            if feature in all_feature_importance:
                                all_feature_importance[feature] += value
                            else:
                                all_feature_importance[feature] = value

                    except Exception as e:
                        self.logger.error(f"{model_type}モデル処理でエラー: {str(e)}")
                        continue

                self.logger.info("基本モデル学習完了")

                # アンサンブル手法の実行
                ensemble_methods = [
                    'average_ensemble',
                    'weighted_ensemble',
                    'stacking_ensemble',
                    'voting_ensemble',
                    'blending_ensemble'
                ]
                self.logger.info(f"アンサンブル学習開始: {ensemble_methods}")

                for ensemble_index, ensemble_method in enumerate(ensemble_methods, 1):
                    self.logger.info(
                        f"アンサンブル手法実行 [{ensemble_index}/{len(ensemble_methods)}]: {ensemble_method}")

                    try:
                        ensemble_dir = os.path.join(dir_path, ensemble_method)
                        os.makedirs(ensemble_dir, exist_ok=True)

                        if ensemble_method == 'average_ensemble':
                            ensemble_predictions = self.ensemble_methods.average_ensemble_predict(predictions, models,
                                                                                                  test_features,
                                                                                                  test_labels,
                                                                                                  ensemble_dir)
                        elif ensemble_method == 'weighted_ensemble':
                            ensemble_predictions = self.ensemble_methods.weighted_ensemble_predict(predictions, models,
                                                                                                   r2_scores,
                                                                                                   ensemble_dir)
                        elif ensemble_method == 'stacking_ensemble':
                            ensemble_predictions = self.ensemble_methods.stacking_ensemble_predict(
                                encoded_train_features,
                                encoded_test_features,
                                all_train_labels,
                                test_labels, models,
                                ensemble_dir)
                        elif ensemble_method == 'voting_ensemble':
                            ensemble_predictions = self.ensemble_methods.voting_ensemble_predict(predictions,
                                                                                                 models,
                                                                                                 test_features,
                                                                                                 test_labels,
                                                                                                 ensemble_dir)
                        elif ensemble_method == 'blending_ensemble':
                            ensemble_predictions = self.ensemble_methods.blending_ensemble_predict(
                                encoded_train_features,
                                encoded_test_features,
                                all_train_labels,
                                test_labels, models,
                                ensemble_dir)

                        self.logger.info(f"{ensemble_method}予測完了")

                        # アンサンブルモデルの評価
                        r2 = self.evaluator.evaluate_model_performance(test_labels,
                                                                       ensemble_predictions,
                                                                       ensemble_dir)
                        self.logger.info(f"{ensemble_method}評価完了: R2 = {r2:.6f}")

                        self.evaluator.plot_actual_predicted(test_labels,
                                                             ensemble_predictions,
                                                             ensemble_dir,
                                                             r2)

                        # 予測結果の保存
                        utils.save_predictions_to_csv(test_features,
                                                      test_labels,
                                                      ensemble_predictions,
                                                      ensemble_dir)

                        # 予測結果から的中率を計算
                        utils.analyze_and_save_win_rates(ensemble_dir, self.train_test_split_date)
                        self.logger.info(f"{ensemble_method}完了")

                    except Exception as e:
                        self.logger.error(f"{ensemble_method}処理でエラー: {str(e)}")
                        continue

                self.logger.info("アンサンブル学習完了")

                # 特徴量の重要度分析と保存
                self.logger.info("特徴量重要度分析開始")
                try:
                    self.analyze_and_save_feature_importance(all_feature_importance, dir_path)
                    self.logger.info("特徴量重要度分析完了")
                except Exception as e:
                    self.logger.error(f"特徴量重要度分析でエラー: {str(e)}")

                # 各モデルの特徴量重要度を保存
                self.logger.info("個別モデル特徴量重要度保存開始")
                for model_type, importance in feature_importance_by_model.items():
                    try:
                        model_importance_file = os.path.join(dir_path, f'{model_type}_feature_importance.csv')
                        with open(model_importance_file, 'w') as f:
                            f.write("Feature,Importance,RelativeImportance\n")
                            total_imp = sum(importance.values())
                            sorted_importance = sorted(importance.items(), key=lambda x: x[1], reverse=True)
                            for feature, imp in sorted_importance:
                                rel_imp = imp / total_imp
                                f.write(f"{feature},{imp:.6f},{rel_imp:.6f}\n")
                        self.logger.info(f"{model_type}特徴量重要度保存完了: {model_importance_file}")
                    except Exception as e:
                        self.logger.error(f"{model_type}特徴量重要度保存でエラー: {str(e)}")

                self.logger.info(f"ファイル処理完了 [{file_index}/{len(csv_files)}]: {file}")

            except Exception as e:
                self.logger.error(f"ファイル処理でエラー: {file} - {str(e)}")
                continue

        self.logger.info("ModelCreator.run() メイン処理完了")

    def analyze_and_save_feature_importance(self, feature_importance_dict, model_dir):
        """
        特徴量の重要度を分析し、結果を保存する

        :param dict feature_importance_dict: 特徴量名をキー、重要度を値とする辞書
        :param str model_dir: 結果を保存するディレクトリパス
        """
        self.logger.info("特徴量重要度分析開始")

        if not feature_importance_dict:
            self.logger.warning("特徴量重要度データが空です")
            return

        # 重要度に基づいて特徴量をソート
        sorted_features = sorted(feature_importance_dict.items(), key=lambda x: x[1], reverse=True)
        total_importance = sum(value for _, value in sorted_features)
        self.logger.info(f"特徴量総数: {len(sorted_features)}, 総重要度: {total_importance:.6f}")

        # 特徴量の重要度をCSVファイルに保存
        importance_file = os.path.join(model_dir, 'feature_importance.csv')
        try:
            with open(importance_file, 'w') as f:
                f.write("Feature,Importance,RelativeImportance\n")
                for feature, importance in sorted_features:
                    relative_importance = importance / total_importance
                    f.write(f"{feature},{importance:.6f},{relative_importance:.6f}\n")
            self.logger.info(f"特徴量重要度CSV保存完了: {importance_file}")
        except Exception as e:
            self.logger.error(f"特徴量重要度CSV保存エラー: {str(e)}")

        # 重要な特徴量を選択 (importance_threshold以上のもの)
        threshold = self.data_processor.importance_threshold
        important_features = []
        for feature, importance in sorted_features:
            relative_importance = importance / total_importance
            if relative_importance >= threshold:
                important_features.append(feature)

        self.logger.info(f"重要特徴量選択完了: 閾値 {threshold}, 選択数 {len(important_features)}")

        # 次回の学習のための設定ファイルを作成
        try:
            important_config = configparser.ConfigParser()

            # 新しい設定ファイル用のセクションを作成
            important_config.add_section('features')

            # 予測対象(ラベル)の設定を保持
            if 'features' in self.config and 'target' in self.config['features']:
                important_config['features']['target'] = self.config['features']['target']
            else:
                important_config['features']['target'] = 'time'  # デフォルト値

            # 重要な特徴量を設定
            important_config['features']['use_features'] = ','.join(important_features)
            important_config['features']['importance_threshold'] = str(threshold)

            # 元の設定の他の項目も保持
            if 'features' in self.config:
                for key, value in self.config['features'].items():
                    if key not in ['use_features', 'importance_threshold', 'target']:
                        important_config['features'][key] = value

            # 新しい設定ファイルをモデルディレクトリに保存
            important_config_path = os.path.join(model_dir, 'important_features.ini')
            with open(important_config_path, 'w', encoding='utf-8') as configfile:
                important_config.write(configfile)
            self.logger.info(f"重要特徴量設定ファイル保存完了: {important_config_path}")
        except Exception as e:
            self.logger.error(f"重要特徴量設定ファイル保存エラー: {str(e)}")

        # 特徴量重要度のグラフを作成
        try:
            self.plot_feature_importance(sorted_features, model_dir)
            self.logger.info("特徴量重要度グラフ作成完了")
        except Exception as e:
            self.logger.error(f"特徴量重要度グラフ作成エラー: {str(e)}")

    def plot_feature_importance(self, sorted_features, model_dir, top_n=20):
        """
        特徴量の重要度をグラフとして可視化して保存する

        :param list sorted_features: (特徴量名, 重要度)のタプルのソート済みリスト
        :param str model_dir: グラフを保存するディレクトリパス
        :param int top_n: グラフに表示する上位N個の特徴量
        """
        self.logger.info(f"特徴量重要度グラフ作成開始: 上位{top_n}件")

        try:
            import matplotlib.pyplot as plt
            import japanize_matplotlib  # 日本語表示のため

            plt.figure(figsize=(10, 8))

            # 上位N個の特徴量を取得
            top_features = sorted_features[:top_n]
            features, values = zip(*top_features)

            # 総合重要度を計算
            total = sum(values)
            relative_values = [v / total for v in values]

            # 水平棒グラフを作成
            plt.barh(range(len(features)), relative_values, align='center')
            plt.yticks(range(len(features)), features)
            plt.xlabel('相対的重要度')
            plt.ylabel('特徴量')
            plt.title(f'特徴量の重要度(上位{top_n}件)')
            plt.tight_layout()

            # グラフを保存
            graph_path = os.path.join(model_dir, 'feature_importance.svg')
            plt.savefig(graph_path)
            plt.close()
            self.logger.info(f"特徴量重要度グラフ保存完了: {graph_path}")

        except Exception as e:
            self.logger.error(f"特徴量重要度グラフ作成エラー: {str(e)}")

data_processor.py(データ前処理)

# ==============================================================================
# レース結果データの前処理プログラム
# ==============================================================================
# プログラムの概要:
#   競馬レース予測のための機械学習に使用するデータの前処理を行うプログラム。
#   設定ファイルに基づく特徴量とラベルの準備、時系列によるデータ分割、
#   カテゴリカル変数のエンコーディングを実行する。前処理されたデータは
#   各種モデルの学習に使用される。
#
# プログラムの主な機能:
#   1. 設定ファイル読み込み機能
#      - config.iniから重要度閾値とターゲット列を読み込み
#      - 特徴量選択の設定値取得
#   2. データの準備と分割
#      - 特徴量とラベル(設定可能なターゲット列)の抽出
#      - 日付による時系列学習/テストデータの分割
#      - 分割基準日の妥当性チェック
#   3. カテゴリカル変数の処理
#      - OrdinalEncoderによる全カテゴリカル変数のエンコーディング
#      - エンコーダーモデルの保存(再利用可能)
#      - DataFrame形式の維持とカラム名の保持
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install pandas numpy scikit-learn joblib configparser
#
# 2. 設定ファイル(config.ini)の準備:
#    [features]セクションで以下を設定:
#    - target: 予測対象のカラム名(デフォルト: 'time')
#    - importance_threshold: 特徴量重要度の閾値(デフォルト: 0.0)
#
# 3. 入力データの準備:
#    - レース結果のDataFrame
#      必須カラム:
#      - date: レース日付(pandas.datetime形式)
#      - time(またはconfig.iniで指定したターゲット列): 予測対象
#      - その他の特徴量カラム(カテゴリカル変数含む)
#
# 4. 実行方法:
#    - model_creator.pyから呼び出される(直接実行は想定していない)
#    - DataProcessorクラスをインスタンス化して各メソッドを呼び出し
#
# 5. 処理の流れ:
#    1) DataProcessorクラスの初期化(設定ファイル読み込み)
#    2) prepare_features_and_labels()でターゲット列を分離
#    3) split_data_into_train_test()で日付基準による分割
#    4) encode_categorical_features()でカテゴリカル変数をエンコード
#    5) エンコーダーの保存とエンコード済みデータの返却
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. エンコードされたデータ:
#    - 学習用特徴量(DataFrame形式、カラム名保持)
#    - テスト用特徴量(DataFrame形式、カラム名保持)
#    - 学習用ラベル(numpy.ndarray)
#    - テスト用ラベル(numpy.ndarray)
#
# 2. 保存ファイル:
#    - oe_x.pkl: OrdinalEncoderモデル(joblib形式、圧縮レベル3)
#      └─ 予測時の新しいデータ処理で再利用可能
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - model_creator.py(ModelCreatorクラス)
#      └─ データの前処理とエンコーディングを実行
#
# 2. 設定ファイル依存:
#    - utils.py: get_config_path()でconfig.iniのパス取得
#    - config.ini: [features]セクションから設定値を読み込み
#
# 3. データの受け渡し:
#    - ModelTrainer: エンコードされたデータを提供
#    - EnsembleMethods: 前処理済みデータを提供
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データ要件:
#    - 入力DataFrameに欠損値が含まれていないこと
#    - date列がpandas.datetime型であること
#    - ターゲット列(デフォルト: 'time')が存在すること
#    - 全てのカテゴリカル変数がOrdinalEncoderで処理可能であること
#
# 2. 設定ファイル(config.ini):
#    - [features]セクションが存在しない場合はデフォルト値を使用
#    - target設定が無効な場合は'time'を使用
#    - importance_threshold設定が無効な場合は0.0を使用
#
# 3. エンコーディング仕様:
#    - 全ての非数値カラムがカテゴリカル変数として処理される
#    - エンコード後もDataFrame形式を維持(特徴量分析のため)
#    - カラム名は元の特徴量名が保持される
#    - エンコーダーは学習データとテストデータで個別にfit_transform実行
#
# 4. データ分割の条件:
#    - 分割基準日が学習データの日付範囲内にない場合はNoneを返す
#    - 学習データまたはテストデータのサイズが0の場合はNoneを返す
#    - 時系列順序を保持した分割(リークなし)
#
# 5. メモリ使用:
#    - 大規模データセット処理時はメモリ使用量に注意
#    - エンコード処理時に一時的にメモリ使用量が増加
#    - DataFrame→ndarray→DataFrame変換でメモリコピーが発生
#
# 6. 保存データ:
#    - エンコーダーファイルは自動的に上書きされる
#    - joblib圧縮レベル3でファイルサイズを最適化
#    - 予測時に同じエンコーダーを使用してデータ整合性を保つ
#

import pandas as pd
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
import joblib
import configparser
import os
from utils import get_config_path


class DataProcessor:
    def __init__(self):
        """
        初期化時に設定ファイルを読み込む
        """
        self.config = configparser.ConfigParser()
        config_path = get_config_path()
        self.config.read(config_path, encoding='utf-8')

        # 特徴量の重要度閾値を設定から読み込む(デフォルトは0.0)
        self.importance_threshold = 0.0
        if 'features' in self.config and 'importance_threshold' in self.config['features']:
            try:
                self.importance_threshold = float(self.config['features']['importance_threshold'])
            except ValueError:
                # 変換できない場合はデフォルト値を使用
                pass

    def prepare_features_and_labels(self, race_results):
        """
        レース結果のデータから特徴量とラベルを抽出する。
        設定ファイルから取得したtarget_columnをラベルとして使用し、
        use_featuresで指定された特徴量のみを使用する。
        """
        # 設定ファイルから予測対象(ラベル)を取得(デフォルトは'time')
        target_column = 'time'
        if 'features' in self.config and 'target' in self.config['features']:
            target_column = self.config['features']['target']

        # 設定ファイルから使用する特徴量を取得
        use_features = None
        if 'features' in self.config and 'use_features' in self.config['features']:
            use_features_str = self.config['features']['use_features']
            if use_features_str.strip():  # 空文字でない場合
                use_features = [feature.strip() for feature in use_features_str.split(',')]

        # 特徴量の選択
        if use_features:
            # 指定された特徴量のみを使用(存在するもののみ)
            available_features = [feature for feature in use_features if feature in race_results.columns]
            if not available_features:
                print(f"警告: 指定された特徴量がデータに存在しません。全ての特徴量を使用します。")
                features = race_results.drop([target_column], axis=1)
            else:
                features = race_results[available_features]
                print(f"使用する特徴量: {available_features}")
        else:
            # use_featuresが設定されていない場合は全ての特徴量を使用
            features = race_results.drop([target_column], axis=1)
            print("use_featuresが設定されていないため、全ての特徴量を使用します。")

        labels = race_results[target_column].values  # target_columnをラベルとして使用

        return features, labels

    def encode_categorical_features(self, all_train_features, test_features, model_save_dir_path):
        """
        OrdinalEncoderを使用して、訓練データセットとテストデータセットのカテゴリされた特徴量をエンコードし、
        その後エンコーダーを指定されたディレクトリに保存する。
        :param DataFrame all_train_features: 特徴量を含む訓練セット。
        :param DataFrame test_features: 特徴量を含むテストセット。
        :param str model_save_dir_path: エンコーダーを保存するディレクトリパス。
        :return: エンコードされた訓練セット、テストセット。
        :rtype: pd.DataFrame, pd.DataFrame
        """
        # 訓練データのラベルエンコード
        oe_x = OrdinalEncoder()
        encoded_train_features = oe_x.fit_transform(all_train_features)  # Dataframeからndarrayに変わるとカラムがなくなり、
        encoded_train_features = pd.DataFrame(encoded_train_features)  # 特徴量分析で特徴量名がf0,f1,f2,,となるのでndarrayからDataframeに戻す
        encoded_train_features.columns = list(all_train_features.columns.values)  # カラムを付ける

        encoded_test_features = oe_x.fit_transform(test_features)  # Dataframeからndarrayに変わるとカラムがなくなり、
        encoded_test_features = pd.DataFrame(encoded_test_features)  # 特徴量分析で特徴量名がf0,f1,f2,,となるのでndarrayからDataframeに戻す
        encoded_test_features.columns = list(test_features.columns.values)  # カラムを付ける

        # エンコーダーを保存する
        encoder_save_path = model_save_dir_path + '/oe_x.pkl'
        joblib.dump(oe_x, encoder_save_path, compress=3)

        return encoded_train_features, encoded_test_features

    def split_data_into_train_test(self, features, labels, split_day):
        """
        指定した日付で、特徴量とラベルのレース結果を訓練セットとテストセットに分割する。
        指定した日付の前のレース結果データは訓練セット、それ以降のレース結果はテストセットに分類する。
        split_dayがfeatures['date']に含まれない場合はNoneを返す。
        :param DataFrame features: 特徴量('time'列が含まれていないレース結果)
        :param np.ndarray labels: ラベル('time'列のみ)
        :param str split_day: 訓練セットとテストセットで分割する基準日。YYYY-MM-DD形式の文字列。
        :return: 訓練用特徴量セット、テスト用特徴量セット、訓練用ラベルセット、テスト用ラベルセット
        :rtype: pd.DataFrame, pd.DataFrame, np.ndarray, np.ndarray
        """
        mday = pd.to_datetime(split_day)

        # split_dayがfeatures['date']の範囲内にない場合、Noneを返す
        if not ((features['date'] < mday).any() and (features['date'] >= mday).any()):
            return None, None, None, None

        train_index = features['date'] < mday  # 基準日より前のデータのインデックス(訓練セット)
        test_index = features['date'] >= mday  # 基準日以降のデータインデックス(テストセット)

        return features[train_index], features[test_index], labels[train_index], labels[test_index]

model_trainer.py(モデル学習・最適化)

# ==============================================================================
# 機械学習モデルのトレーニングと最適化プログラム
# ==============================================================================
# プログラムの概要:
#   競馬レース予測のための機械学習モデル(XGBoost, LightGBM, CatBoost)の
#   トレーニングと最適化を行うプログラム。Optunaを使用して各モデルの
#   ハイパーパラメータを最適化し、学習済みモデルを保存する。
#
# プログラムの主な機能:
#   1. モデルのトレーニング
#      - XGBoostモデルのトレーニング
#      - LightGBMモデルのトレーニング
#      - CatBoostモデルのトレーニング
#   2. ハイパーパラメータの最適化
#      - Optunaによる自動パラメータチューニング
#      - モデルごとの最適パラメータ探索
#   3. モデルの評価と保存
#      - 最適化されたモデルの保存
#      - 予測実行と評価
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install numpy xgboost lightgbm catboost optuna joblib
#
# 2. 設定値の確認:
#    - Optuna最適化設定
#      - 試行回数: 10回
#      - タイムアウト: 1800秒(30分)
#    - GPU設定(必要に応じて)
#      - tree_method: 'gpu_hist'
#      - predictor: 'gpu_predictor'
#
# 3. 入力データの準備:
#    - DataProcessorで前処理された特徴量とラベル
#    - トレーニングデータとテストデータの分割済みデータ
#
# 4. 実行方法:
#    - model_creator.pyから呼び出される(直接実行は想定していない)
#
# 5. 処理の流れ:
#    1) データをモデル固有の形式に変換(DMatrix, Dataset等)
#    2) ハイパーパラメータの最適化をOptunaで実行
#    3) 最適化されたパラメータでモデルを学習
#    4) モデルの保存と予測の実行
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. モデルファイル:
#    - xgboost: xgb_model.pkl
#    - lightgbm: lightgbm_model.pkl
#    - catboost: catboost_model.cbm
#
# 2. 設定ファイル:
#    - xgb_hyperparameters.txt
#    - lightgbm_hyperparameters.txt
#    - catboost_hyperparameters.txt
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - model_creator.py
#      └─ train_and_save_model()メソッドを通じて呼び出し
#
# 2. データの受け渡し:
#    - DataProcessor: 前処理済みデータを受け取る
#    - EnsembleMethods: 学習済みモデルを提供
#    - Evaluator: モデル評価用のデータを提供
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行環境:
#    - GPU環境での実行を推奨
#    - 必要メモリ量: モデルごとに2-6GB程度
#    - 十分なディスク容量(モデル保存用)
#
# 2. 最適化設定:
#    - 試行回数とタイムアウト値は要調整
#    - GPU使用時はメモリ使用量に注意
#    - 早期終了条件の設定確認
#
# 3. モデル固有の注意:
#    - XGBoost: DMatrix形式への変換が必要
#    - LightGBM: Dataset形式への変換が必要
#    - CatBoost: カテゴリカル変数の指定方法に注意
#
# 4. 保存データ:
#    - モデルファイルは圧縮形式で保存
#    - ハイパーパラメータは可読形式で保存
#    - 既存のファイルは上書きされる
#
# ==============================================================================
# 機械学習モデルのトレーニングと最適化プログラム(config.ini対応版)
# ==============================================================================
# プログラムの概要:
#   競馬レース予測のための機械学習モデル(XGBoost, LightGBM, CatBoost)の
#   トレーニングと最適化を行うプログラム。Optunaを使用して各モデルの
#   ハイパーパラメータを最適化し、学習済みモデルを保存する。
#   config.iniファイルからパラメータを動的に読み込み、柔軟な設定管理を実現。
#
# 主な変更点:
#   - config.iniからの設定読み込み機能追加
#   - ハードコードされたパラメータの設定ファイル化
#   - GPU/CPU設定の動的切り替え機能
#   - パラメータ範囲の柔軟な設定機能
# ==============================================================================
# ==============================================================================
# 機械学習モデルのトレーニングと最適化プログラム(拡張版)
# ==============================================================================
# プログラムの概要:
#   競馬レース予測のための機械学習モデル(XGBoost, LightGBM, CatBoost)の
#   トレーニングと最適化を行うプログラム。Optunaを使用して各モデルの
#   ハイパーパラメータを最適化し、学習済みモデルを保存する。
#   config.iniファイルからパラメータを動的に読み込み、柔軟な設定管理を実現。
#
# 主な機能:
#   - config.iniからの設定読み込み機能
#   - ハードコードされたパラメータの設定ファイル化
#   - GPU/CPU設定の動的切り替え機能
#   - パラメータ範囲の柔軟な設定機能
#   - リソース監視機能
#   - 拡張パラメータ対応
# ==============================================================================

import numpy as np
import xgboost as xgb
import lightgbm as lgb
import catboost as cb
import optuna
import joblib
import os
import configparser
import matplotlib
matplotlib.use('Agg')  # GUIバックエンドを無効化

from catboost import Pool
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from utils import get_config_path

import psutil
import threading
import time
import logging
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from optuna.samplers import TPESampler, RandomSampler
from optuna.pruners import MedianPruner, SuccessiveHalvingPruner

from utils import get_logger

class ModelTrainer:
    def __init__(self):
        """
        初期化時に設定ファイルを読み込み、各モデルのパラメータを準備する
        """
        # ログ設定を最初に初期化
        self.logger = get_logger(__name__)
        self.logger.info("ModelTrainer初期化開始")

        # 設定ファイル読み込み
        self.config = configparser.ConfigParser()
        config_path = get_config_path()
        self.config.read(config_path, encoding='utf-8')

        # リソース監視の初期化
        self._init_resource_monitoring()

        # 共通パラメータの読み込み
        self._load_common_params()

        # モデル別パラメータの読み込み
        self._load_xgboost_params()
        self._load_lightgbm_params()
        self._load_catboost_params()

        self.logger.info("ModelTrainer初期化完了")

    def _init_resource_monitoring(self):
        """リソース監視機能の初期化"""
        self.resource_monitor_active = False
        self.resource_monitor_thread = None
        self.resource_stats = {
            'peak_memory_gb': 0.0,
            'avg_cpu_percent': 0.0,
            'monitoring_duration': 0.0
        }

    def _load_common_params(self):
        """共通パラメータを設定ファイルから読み込む(拡張版)"""
        # デフォルト値
        self.optuna_trials = 10
        self.optuna_timeout = 1800
        self.use_gpu = True
        self.early_stopping_rounds = 10
        self.validation_split = 0.2
        self.random_state = 0

        # 新規追加のデフォルト値
        self.max_training_time_per_model = 3600
        self.memory_limit_gb = 8
        self.cpu_cores = -1

        # Optuna設定
        self.optuna_direction = 'minimize'
        self.optuna_sampler = 'TPESampler'
        self.optuna_pruner = 'MedianPruner'
        self.optuna_n_startup_trials = 5
        self.optuna_n_warmup_steps = 5

        # 評価指標設定
        self.primary_metric = 'rmse'
        self.secondary_metrics = ['mae', 'r2']
        self.metric_direction = 'minimize'

        # ログ・デバッグ設定
        self.verbose_training = False
        self.save_intermediate_results = False
        self.show_optimization_progress = True

        # リソース監視設定
        self.memory_monitoring = True
        self.performance_monitoring = False

        # 設定ファイルから読み込み
        if 'model_params' in self.config:
            section = self.config['model_params']

            # 既存設定
            self.optuna_trials = int(section.get('optuna_trials', self.optuna_trials))
            self.optuna_timeout = int(section.get('optuna_timeout', self.optuna_timeout))
            self.use_gpu = section.getboolean('use_gpu', self.use_gpu)
            self.early_stopping_rounds = int(section.get('early_stopping_rounds', self.early_stopping_rounds))
            self.validation_split = float(section.get('validation_split', self.validation_split))
            self.random_state = int(section.get('random_state', self.random_state))

            # 新規追加設定
            self.max_training_time_per_model = int(
                section.get('max_training_time_per_model', self.max_training_time_per_model))
            self.memory_limit_gb = float(section.get('memory_limit_gb', self.memory_limit_gb))
            self.cpu_cores = int(section.get('cpu_cores', self.cpu_cores))

            # Optuna設定
            self.optuna_direction = section.get('optuna_direction', self.optuna_direction)
            self.optuna_sampler = section.get('optuna_sampler', self.optuna_sampler)
            self.optuna_pruner = section.get('optuna_pruner', self.optuna_pruner)
            self.optuna_n_startup_trials = int(section.get('optuna_n_startup_trials', self.optuna_n_startup_trials))
            self.optuna_n_warmup_steps = int(section.get('optuna_n_warmup_steps', self.optuna_n_warmup_steps))

            # 評価指標設定
            self.primary_metric = section.get('primary_metric', self.primary_metric)
            secondary_metrics_str = section.get('secondary_metrics', ','.join(self.secondary_metrics))
            self.secondary_metrics = [metric.strip() for metric in secondary_metrics_str.split(',')]
            self.metric_direction = section.get('metric_direction', self.metric_direction)

            # ログ・デバッグ設定
            self.verbose_training = section.getboolean('verbose_training', self.verbose_training)
            self.save_intermediate_results = section.getboolean('save_intermediate_results',
                                                                self.save_intermediate_results)
            self.show_optimization_progress = section.getboolean('show_optimization_progress',
                                                                 self.show_optimization_progress)

            # リソース監視設定
            self.memory_monitoring = section.getboolean('memory_monitoring', self.memory_monitoring)
            self.performance_monitoring = section.getboolean('performance_monitoring', self.performance_monitoring)

        # CPU cores設定の調整
        if self.cpu_cores == -1:
            self.cpu_cores = psutil.cpu_count()
        elif self.cpu_cores <= 0:
            self.cpu_cores = max(1, psutil.cpu_count() + self.cpu_cores)

        # ログ出力(self.loggerが確実に初期化されている状態で)
        if hasattr(self, 'logger'):
            self.logger.info(
                f"ModelTrainer共通パラメータ読み込み完了 - CPU cores: {self.cpu_cores}, Memory limit: {self.memory_limit_gb}GB")

    def _parse_range(self, range_str, default_min, default_max, is_int=False, is_log=False):
        """
        範囲文字列('min,max'形式)をパースして最適化用のタプルを返す

        :param str range_str: 範囲文字列(例:'3,10')
        :param float default_min: デフォルト最小値
        :param float default_max: デフォルト最大値
        :param bool is_int: 整数型かどうか
        :param bool is_log: 対数スケールかどうか
        :return: (最小値, 最大値, スケールタイプ)のタプル
        :rtype: tuple
        """
        try:
            min_val, max_val = map(float, range_str.split(','))
            if is_int:
                min_val, max_val = int(min_val), int(max_val)
            return (min_val, max_val, 'log' if is_log else 'uniform')
        except (ValueError, AttributeError):
            return (default_min, default_max, 'log' if is_log else 'uniform')

    def _load_xgboost_params(self):
        """XGBoostパラメータを設定ファイルから読み込む"""
        # デフォルト範囲設定
        self.xgb_ranges = {
            'max_depth': (3, 10, 'uniform'),
            'min_child_weight': (0, 10, 'uniform'),
            'eta': (0.001, 0.1, 'log'),
            'subsample': (0.5, 1.0, 'uniform'),
            'colsample_bytree': (0.5, 1.0, 'uniform'),
            'alpha': (0.01, 10.0, 'log'),
            'lambda': (0.01, 10.0, 'log'),
            'gamma': (0.01, 10.0, 'log')
        }

        # デフォルト固定パラメータ
        self.xgb_fixed = {
            'objective': 'reg:squarederror',
            'eval_metric': 'rmse',
            'num_boost_round': 10000
        }

        # 設定ファイルから読み込み
        if 'xgboost' in self.config:
            section = self.config['xgboost']

            # 範囲パラメータの読み込み
            self.xgb_ranges['max_depth'] = self._parse_range(
                section.get('max_depth_range', '3,10'), 3, 10, is_int=True)
            self.xgb_ranges['min_child_weight'] = self._parse_range(
                section.get('min_child_weight_range', '0,10'), 0, 10, is_int=True)
            self.xgb_ranges['eta'] = self._parse_range(
                section.get('eta_range', '0.001,0.1'), 0.001, 0.1, is_log=True)
            self.xgb_ranges['subsample'] = self._parse_range(
                section.get('subsample_range', '0.5,1.0'), 0.5, 1.0)
            self.xgb_ranges['colsample_bytree'] = self._parse_range(
                section.get('colsample_bytree_range', '0.5,1.0'), 0.5, 1.0)
            self.xgb_ranges['alpha'] = self._parse_range(
                section.get('alpha_range', '0.01,10.0'), 0.01, 10.0, is_log=True)
            self.xgb_ranges['lambda'] = self._parse_range(
                section.get('lambda_range', '0.01,10.0'), 0.01, 10.0, is_log=True)
            self.xgb_ranges['gamma'] = self._parse_range(
                section.get('gamma_range', '0.01,10.0'), 0.01, 10.0, is_log=True)

            # 固定パラメータの読み込み
            self.xgb_fixed['objective'] = section.get('objective', 'reg:squarederror')
            self.xgb_fixed['eval_metric'] = section.get('eval_metric', 'rmse')
            self.xgb_fixed['num_boost_round'] = int(section.get('num_boost_round', 10000))

    def _load_lightgbm_params(self):
        """LightGBMパラメータを設定ファイルから読み込む"""
        # デフォルト範囲設定
        self.lgb_ranges = {
            'max_depth': (3, 10, 'uniform'),
            'num_leaves': (20, 150, 'uniform'),
            'learning_rate': (0.001, 0.1, 'log'),
            'feature_fraction': (0.5, 1.0, 'uniform'),
            'bagging_fraction': (0.5, 1.0, 'uniform'),
            'bagging_freq': (1, 10, 'uniform'),
            'lambda_l1': (0.0001, 10.0, 'log'),
            'lambda_l2': (0.0001, 10.0, 'log'),
            'min_child_weight': (0.001, 10.0, 'log')
        }

        # デフォルト固定パラメータ
        self.lgb_fixed = {
            'objective': 'regression',
            'metric': 'rmse',
            'boosting_type': 'gbdt'
        }

        # 設定ファイルから読み込み
        if 'lightgbm' in self.config:
            section = self.config['lightgbm']

            # 範囲パラメータの読み込み
            self.lgb_ranges['max_depth'] = self._parse_range(
                section.get('max_depth_range', '3,10'), 3, 10, is_int=True)
            self.lgb_ranges['num_leaves'] = self._parse_range(
                section.get('num_leaves_range', '20,150'), 20, 150, is_int=True)
            self.lgb_ranges['learning_rate'] = self._parse_range(
                section.get('learning_rate_range', '0.001,0.1'), 0.001, 0.1, is_log=True)
            self.lgb_ranges['feature_fraction'] = self._parse_range(
                section.get('feature_fraction_range', '0.5,1.0'), 0.5, 1.0)
            self.lgb_ranges['bagging_fraction'] = self._parse_range(
                section.get('bagging_fraction_range', '0.5,1.0'), 0.5, 1.0)
            self.lgb_ranges['bagging_freq'] = self._parse_range(
                section.get('bagging_freq_range', '1,10'), 1, 10, is_int=True)
            self.lgb_ranges['lambda_l1'] = self._parse_range(
                section.get('lambda_l1_range', '0.0001,10.0'), 0.0001, 10.0, is_log=True)
            self.lgb_ranges['lambda_l2'] = self._parse_range(
                section.get('lambda_l2_range', '0.0001,10.0'), 0.0001, 10.0, is_log=True)
            self.lgb_ranges['min_child_weight'] = self._parse_range(
                section.get('min_child_weight_range', '0.001,10.0'), 0.001, 10.0, is_log=True)

            # 固定パラメータの読み込み
            self.lgb_fixed['objective'] = section.get('objective', 'regression')
            self.lgb_fixed['metric'] = section.get('metric', 'rmse')
            self.lgb_fixed['boosting_type'] = section.get('boosting_type', 'gbdt')

    def _load_catboost_params(self):
        """CatBoostパラメータを設定ファイルから読み込む"""
        # デフォルト範囲設定
        self.cb_ranges = {
            'iterations': (100, 1000, 'uniform'),
            'depth': (4, 10, 'uniform'),
            'learning_rate': (0.01, 0.3, 'log'),
            'l2_leaf_reg': (1e-8, 10.0, 'log'),
            'border_count': (32, 255, 'uniform'),
            'bagging_temperature': (0, 1, 'uniform'),
            'random_strength': (1e-8, 10, 'log')
        }

        # デフォルト固定パラメータ
        self.cb_fixed = {
            'od_type': 'Iter',
            'verbose': False,
            'iterations_fixed': 1000
        }

        # 設定ファイルから読み込み
        if 'catboost' in self.config:
            section = self.config['catboost']

            # 範囲パラメータの読み込み
            self.cb_ranges['iterations'] = self._parse_range(
                section.get('iterations_range', '100,1000'), 100, 1000, is_int=True)
            self.cb_ranges['depth'] = self._parse_range(
                section.get('depth_range', '4,10'), 4, 10, is_int=True)
            self.cb_ranges['learning_rate'] = self._parse_range(
                section.get('learning_rate_range', '0.01,0.3'), 0.01, 0.3, is_log=True)
            self.cb_ranges['l2_leaf_reg'] = self._parse_range(
                section.get('l2_leaf_reg_range', '1e-8,10.0'), 1e-8, 10.0, is_log=True)
            self.cb_ranges['border_count'] = self._parse_range(
                section.get('border_count_range', '32,255'), 32, 255, is_int=True)
            self.cb_ranges['bagging_temperature'] = self._parse_range(
                section.get('bagging_temperature_range', '0,1'), 0, 1)
            self.cb_ranges['random_strength'] = self._parse_range(
                section.get('random_strength_range', '1e-8,10'), 1e-8, 10, is_log=True)

            # 固定パラメータの読み込み
            self.cb_fixed['od_type'] = section.get('od_type', 'Iter')
            self.cb_fixed['verbose'] = section.getboolean('verbose', False)
            self.cb_fixed['iterations_fixed'] = int(section.get('iterations_fixed', 1000))

    def print_config_summary(self):
        """
        読み込んだ設定値を表示する(拡張版)
        コンソールとログファイルの両方に出力
        """

        def print_and_log(message):
            """コンソールとログファイルの両方に出力するヘルパー関数"""
            print(message)
            if message.strip():  # 空行でない場合のみログ出力
                self.logger.info(message)

        print_and_log("=" * 80)
        print_and_log("ModelTrainer 設定値サマリー(拡張版)")
        print_and_log("=" * 80)

        # 共通パラメータ
        print_and_log("共通パラメータ:")
        print_and_log(f"  - Optuna試行回数: {self.optuna_trials}")
        print_and_log(f"  - Optunaタイムアウト: {self.optuna_timeout}秒")
        print_and_log(f"  - GPU使用: {self.use_gpu}")
        print_and_log(f"  - 早期停止ラウンド: {self.early_stopping_rounds}")
        print_and_log(f"  - 検証データ分割比率: {self.validation_split}")
        print_and_log(f"  - 乱数シード: {self.random_state}")

        # 新規追加パラメータ
        print_and_log("")
        print_and_log("拡張パラメータ:")
        print_and_log(f"  - モデル別最大学習時間: {self.max_training_time_per_model}秒")
        print_and_log(f"  - メモリ制限: {self.memory_limit_gb}GB")
        print_and_log(f"  - CPU cores: {self.cpu_cores}")
        print_and_log(f"  - Optunaサンプラー: {self.optuna_sampler}")
        print_and_log(f"  - Optunaプルーナー: {self.optuna_pruner}")
        print_and_log(f"  - 主要評価指標: {self.primary_metric}")
        print_and_log(f"  - 副次評価指標: {', '.join(self.secondary_metrics)}")
        print_and_log(f"  - 詳細ログ: {self.verbose_training}")
        print_and_log(f"  - 中間結果保存: {self.save_intermediate_results}")
        print_and_log(f"  - メモリ監視: {self.memory_monitoring}")
        print_and_log(f"  - パフォーマンス監視: {self.performance_monitoring}")

        # XGBoostパラメータ
        print_and_log("")
        print_and_log("XGBoost設定:")
        print_and_log("  最適化範囲:")
        for param, (min_val, max_val, scale) in self.xgb_ranges.items():
            print_and_log(f"    - {param}: {min_val} ~ {max_val} ({scale})")
        print_and_log("  固定パラメータ:")
        for param, value in self.xgb_fixed.items():
            print_and_log(f"    - {param}: {value}")

        # LightGBMパラメータ
        print_and_log("")
        print_and_log("LightGBM設定:")
        print_and_log("  最適化範囲:")
        for param, (min_val, max_val, scale) in self.lgb_ranges.items():
            print_and_log(f"    - {param}: {min_val} ~ {max_val} ({scale})")
        print_and_log("  固定パラメータ:")
        for param, value in self.lgb_fixed.items():
            print_and_log(f"    - {param}: {value}")

        # CatBoostパラメータ
        print_and_log("")
        print_and_log("CatBoost設定:")
        print_and_log("  最適化範囲:")
        for param, (min_val, max_val, scale) in self.cb_ranges.items():
            print_and_log(f"    - {param}: {min_val} ~ {max_val} ({scale})")
        print_and_log("  固定パラメータ:")
        for param, value in self.cb_fixed.items():
            print_and_log(f"    - {param}: {value}")

        print_and_log("=" * 80)

    def create_dmatrix_for_xgboost(self, train_features, validation_features, encoded_test_features, train_labels,
                                   validation_labels, test_labels):
        """
        XGBoostモデルのトレーニングに使用するDMatrix形式のデータセットを作成する。
        """
        train_dmatrix = xgb.DMatrix(train_features, label=train_labels)
        validation_dmatrix = xgb.DMatrix(validation_features, label=validation_labels)
        test_dmatrix = xgb.DMatrix(encoded_test_features, label=test_labels)
        return train_dmatrix, validation_dmatrix, test_dmatrix

    def create_dataset_for_lightgbm(self, train_features, validation_features, test_features, train_labels,
                                    validation_labels, test_labels):
        """
        LightGBMモデルのトレーニングに使用するDataset形式のデータセットを作成する。
        """
        train_dataset = lgb.Dataset(train_features, label=train_labels)
        validation_dataset = lgb.Dataset(validation_features, label=validation_labels)
        test_dataset = lgb.Dataset(test_features, label=test_labels, reference=train_dataset)
        return train_dataset, validation_dataset, test_dataset

    def optimize_xgboost_hyperparamters(self, trial, train_dmatrix, validation_dmatrix, test_dmatrix, test_labels):
        """
        設定ファイルから読み込んだ範囲でXGBoostのハイパーパラメータを最適化する。
        """
        xgb_hyperparams = {}

        # 設定ファイルから読み込んだ範囲で動的にパラメータを設定
        for param_name, (min_val, max_val, suggest_type) in self.xgb_ranges.items():
            if suggest_type == 'log':
                if isinstance(min_val, int):
                    xgb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val, log=True)
                else:
                    xgb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val, log=True)
            else:
                if isinstance(min_val, int):
                    xgb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val)
                else:
                    xgb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val)

        # 固定パラメータを追加
        xgb_hyperparams.update({
            'objective': self.xgb_fixed['objective'],
            'eval_metric': self.xgb_fixed['eval_metric']
        })

        # GPU設定を動的に追加
        if self.use_gpu:
            xgb_hyperparams['tree_method'] = 'gpu_hist'
            xgb_hyperparams['device'] = 'cuda'
        else:
            xgb_hyperparams['tree_method'] = 'hist'
            xgb_hyperparams['device'] = 'cpu'

        xgb_model = xgb.train(
            xgb_hyperparams,
            train_dmatrix,
            num_boost_round=self.xgb_fixed['num_boost_round'],
            early_stopping_rounds=self.early_stopping_rounds,
            evals=[(validation_dmatrix, 'eval')],
            verbose_eval=False
        )

        preds = xgb_model.predict(test_dmatrix)
        rmse = np.sqrt(mean_squared_error(test_labels, preds))
        return rmse

    def optimize_lightgbm_hyperparamters(self, trial, train_dataset, validation_dataset):
        """
        設定ファイルから読み込んだ範囲でLightGBMのハイパーパラメータを最適化する。
        """
        lgb_hyperparams = {}

        # 設定ファイルから読み込んだ範囲で動的にパラメータを設定
        for param_name, (min_val, max_val, suggest_type) in self.lgb_ranges.items():
            if suggest_type == 'log':
                if isinstance(min_val, int):
                    lgb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val, log=True)
                else:
                    lgb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val, log=True)
            else:
                if isinstance(min_val, int):
                    lgb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val)
                else:
                    lgb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val)

        # 固定パラメータを追加
        lgb_hyperparams.update({
            'objective': self.lgb_fixed['objective'],
            'metric': self.lgb_fixed['metric'],
            'boosting_type': self.lgb_fixed['boosting_type']
        })

        # GPU設定を動的に追加(tree_methodは削除)
        if self.use_gpu:
            lgb_hyperparams['device_type'] = 'gpu'
        else:
            lgb_hyperparams['device_type'] = 'cpu'

        lgb_model = lgb.train(
            lgb_hyperparams,
            train_dataset,
            valid_sets=[validation_dataset],
            callbacks=[lgb.early_stopping(stopping_rounds=self.early_stopping_rounds, verbose=False)]
        )

        val_rmse = lgb_model.best_score['valid_0']['rmse']
        return val_rmse

    def optimize_catboost_hyperparameters(self, trial, train_pool, validation_pool):
        """
        設定ファイルから読み込んだ範囲でCatBoostのハイパーパラメータを最適化する。
        """
        cb_hyperparams = {}

        # 設定ファイルから読み込んだ範囲で動的にパラメータを設定
        for param_name, (min_val, max_val, suggest_type) in self.cb_ranges.items():
            if suggest_type == 'log':
                if isinstance(min_val, int):
                    cb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val, log=True)
                else:
                    cb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val, log=True)
            else:
                if isinstance(min_val, int):
                    cb_hyperparams[param_name] = trial.suggest_int(param_name, min_val, max_val)
                else:
                    cb_hyperparams[param_name] = trial.suggest_float(param_name, min_val, max_val)

        # 固定パラメータを追加
        cb_hyperparams.update({
            'od_type': self.cb_fixed['od_type'],
            'od_wait': self.early_stopping_rounds,
            'verbose': self.cb_fixed['verbose']
        })

        # GPU設定を動的に追加
        if self.use_gpu:
            cb_hyperparams['task_type'] = 'GPU'
        else:
            cb_hyperparams['task_type'] = 'CPU'

        cb_model = cb.CatBoostRegressor(**cb_hyperparams)
        cb_model.fit(
            train_pool,
            eval_set=validation_pool,
            early_stopping_rounds=self.early_stopping_rounds,
            verbose=False
        )

        return cb_model.get_best_score()['validation']['RMSE']

    def predict_and_save_xgboost_model(self, study, train_dmatrix, validation_dmatrix, test_dmatrix,
                                       model_save_dir_path):
        """
        最適化されたハイパーパラメータと設定ファイルの値を使用してXGBoostモデルを学習し、予測を行い、モデルを保存する。
        """
        xgb_hyperparams = study.best_params

        # 固定パラメータを追加
        xgb_hyperparams.update({
            'objective': self.xgb_fixed['objective'],
            'eval_metric': self.xgb_fixed['eval_metric']
        })

        # GPU設定を動的に追加
        if self.use_gpu:
            xgb_hyperparams['tree_method'] = 'gpu_hist'
            xgb_hyperparams['device'] = 'cuda'
        else:
            xgb_hyperparams['tree_method'] = 'hist'
            xgb_hyperparams['device'] = 'cpu'

        xgb_model = xgb.train(
            xgb_hyperparams,
            train_dmatrix,
            num_boost_round=self.xgb_fixed['num_boost_round'],
            early_stopping_rounds=self.early_stopping_rounds,
            evals=[(validation_dmatrix, 'eval')],
            verbose_eval=False
        )

        predicted_race_times = xgb_model.predict(test_dmatrix)
        joblib.dump(xgb_model, os.path.join(model_save_dir_path, 'xgb_model.pkl'), compress=3)

        with open(os.path.join(model_save_dir_path, 'xgb_hyperparameters.txt'), 'w') as f:
            f.write(str(xgb_hyperparams))

        return predicted_race_times, xgb_model

    def predict_and_save_lightgbm_model(self, study, train_dataset, validation_dataset, test_features,
                                        model_save_dir_path):
        """
        最適化されたハイパーパラメータと設定ファイルの値を使用してLightGBMモデルを学習し、予測を行い、モデルを保存する。
        """
        lgb_hyperparams = study.best_params

        # 固定パラメータを追加
        lgb_hyperparams.update({
            'objective': self.lgb_fixed['objective'],
            'metric': self.lgb_fixed['metric'],
            'boosting_type': self.lgb_fixed['boosting_type']
        })

        # GPU設定を動的に追加(tree_methodは削除)
        if self.use_gpu:
            lgb_hyperparams['device_type'] = 'gpu'
        else:
            lgb_hyperparams['device_type'] = 'cpu'

        lgb_model = lgb.train(
            lgb_hyperparams,
            train_dataset,
            valid_sets=[validation_dataset],
            callbacks=[lgb.early_stopping(stopping_rounds=self.early_stopping_rounds, verbose=False)]
        )

        predicted_race_times = lgb_model.predict(test_features)
        joblib.dump(lgb_model, os.path.join(model_save_dir_path, 'lightgbm_model.pkl'), compress=3)

        with open(os.path.join(model_save_dir_path, 'lgb_hyperparameters.txt'), 'w') as f:
            f.write(str(lgb_hyperparams))

        return predicted_race_times, lgb_model

    def predict_and_save_catboost_model(self, study, train_pool, validation_pool, test_pool, model_save_dir_path):
        """
        最適化されたハイパーパラメータと設定ファイルの値を使用してCatBoostモデルを学習し、予測を行い、モデルを保存する。
        """
        cb_hyperparams = study.best_params

        # 固定パラメータを上書き
        cb_hyperparams.update({
            'iterations': self.cb_fixed['iterations_fixed'],
            'od_type': self.cb_fixed['od_type'],
            'od_wait': self.early_stopping_rounds,
            'verbose': self.cb_fixed['verbose']
        })

        # GPU設定を動的に追加
        if self.use_gpu:
            cb_hyperparams['task_type'] = 'GPU'
        else:
            cb_hyperparams['task_type'] = 'CPU'

        cb_model = cb.CatBoostRegressor(**cb_hyperparams)
        cb_model.fit(
            train_pool,
            eval_set=validation_pool,
            early_stopping_rounds=self.early_stopping_rounds,
            verbose=False
        )

        predicted_race_times = cb_model.predict(test_pool)
        cb_model.save_model(os.path.join(model_save_dir_path, 'catboost_model.cbm'))

        with open(os.path.join(model_save_dir_path, 'catboost_hyperparameters.txt'), 'w') as f:
            f.write(str(cb_hyperparams))

        return predicted_race_times, cb_model

    def _create_optuna_study(self):
        """Optuna studyを設定に基づいて作成する"""
        # サンプラーの設定
        if self.optuna_sampler.lower() == 'tpesampler':
            sampler = TPESampler(
                n_startup_trials=self.optuna_n_startup_trials,
                n_ei_candidates=24,
                seed=self.random_state
            )
        elif self.optuna_sampler.lower() == 'randomsampler':
            sampler = RandomSampler(seed=self.random_state)
        else:
            sampler = None
            if hasattr(self, 'logger'):
                self.logger.warning(f"Unknown sampler: {self.optuna_sampler}, using default")

        # プルーナーの設定
        if self.optuna_pruner.lower() == 'medianpruner':
            pruner = MedianPruner(
                n_startup_trials=self.optuna_n_startup_trials,
                n_warmup_steps=self.optuna_n_warmup_steps
            )
        elif self.optuna_pruner.lower() == 'successiveharvingpruner':
            pruner = SuccessiveHalvingPruner()
        else:
            pruner = None
            if hasattr(self, 'logger'):
                self.logger.warning(f"Unknown pruner: {self.optuna_pruner}, using default")

        # Study作成
        study = optuna.create_study(
            direction=self.optuna_direction,
            sampler=sampler,
            pruner=pruner
        )

        if hasattr(self, 'logger'):
            self.logger.info(f"Optuna study作成完了 - Sampler: {self.optuna_sampler}, Pruner: {self.optuna_pruner}")
        return study

    def _start_resource_monitoring(self):
        """リソース監視を開始する"""
        if not self.memory_monitoring and not self.performance_monitoring:
            return

        self.resource_monitor_active = True
        self.resource_monitor_thread = threading.Thread(target=self._monitor_resources)
        self.resource_monitor_thread.daemon = True
        self.resource_monitor_thread.start()
        if hasattr(self, 'logger'):
            self.logger.info("リソース監視開始")

    def _stop_resource_monitoring(self):
        """リソース監視を停止する"""
        if self.resource_monitor_thread:
            self.resource_monitor_active = False
            self.resource_monitor_thread.join(timeout=5.0)
            if hasattr(self, 'logger'):
                self.logger.info("リソース監視停止")

    def _monitor_resources(self):
        """リソース監視のメインループ"""
        start_time = time.time()
        cpu_samples = []

        while self.resource_monitor_active:
            try:
                # メモリ使用量監視
                if self.memory_monitoring:
                    memory_info = psutil.virtual_memory()
                    current_memory_gb = memory_info.used / (1024 ** 3)
                    self.resource_stats['peak_memory_gb'] = max(
                        self.resource_stats['peak_memory_gb'],
                        current_memory_gb
                    )

                    # メモリ制限チェック
                    if current_memory_gb > self.memory_limit_gb:
                        if hasattr(self, 'logger'):
                            self.logger.warning(
                                f"メモリ使用量が制限を超過: {current_memory_gb:.2f}GB > {self.memory_limit_gb}GB")

                # CPU使用率監視
                if self.performance_monitoring:
                    cpu_percent = psutil.cpu_percent(interval=1)
                    cpu_samples.append(cpu_percent)

                time.sleep(5)  # 5秒間隔で監視

            except Exception as e:
                if hasattr(self, 'logger'):
                    self.logger.error(f"リソース監視エラー: {str(e)}")
                break

        # 統計情報の計算
        if cpu_samples:
            self.resource_stats['avg_cpu_percent'] = sum(cpu_samples) / len(cpu_samples)
        self.resource_stats['monitoring_duration'] = time.time() - start_time

    def _check_memory_limit(self):
        """メモリ制限をチェックし、必要に応じてガベージコレクションを実行"""
        if not self.memory_monitoring:
            return True

        memory_info = psutil.virtual_memory()
        current_memory_gb = memory_info.used / (1024 ** 3)

        if current_memory_gb > self.memory_limit_gb * 0.9:  # 90%到達時に警告
            if hasattr(self, 'logger'):
                self.logger.warning(f"メモリ使用量が警告レベル: {current_memory_gb:.2f}GB")
            import gc
            gc.collect()
            return current_memory_gb <= self.memory_limit_gb

        return True

    def _save_intermediate_results(self, model_type, trial_number, best_params, best_score):
        """中間結果を保存する"""
        if not self.save_intermediate_results:
            return

        try:
            intermediate_data = {
                'model_type': model_type,
                'trial_number': trial_number,
                'timestamp': time.time(),
                'best_params': best_params,
                'best_score': best_score,
                'resource_stats': self.resource_stats.copy()
            }

            # JSON形式で保存
            import json
            filename = f"intermediate_results_{model_type}_{trial_number}.json"
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(intermediate_data, f, indent=2, ensure_ascii=False)

            if hasattr(self, 'logger'):
                self.logger.info(f"中間結果保存: {filename}")

        except Exception as e:
            if hasattr(self, 'logger'):
                self.logger.error(f"中間結果保存エラー: {str(e)}")

    def train_and_save_model(self, encoded_train_features, encoded_test_features, all_train_labels, test_labels,
                             model_save_dir_path, model_type):
        """
        指定されたモデルタイプに応じて、モデルのトレーニング、ハイパーパラメータの最適化、予測、保存を行う。
        拡張版:タイムアウト制御、リソース監視、詳細ログ対応
        """
        if hasattr(self, 'logger'):
            self.logger.info(f"{model_type}モデル学習開始")
        training_start_time = time.time()

        # リソース監視開始
        self._start_resource_monitoring()

        try:
            # 設定から読み込んだ分割比率と乱数シードを使用
            train_features, validation_features, train_labels, validation_labels = train_test_split(
                encoded_train_features, all_train_labels,
                test_size=self.validation_split,
                random_state=self.random_state
            )

            if model_type == 'xgboost':
                train_data, validation_data, test_data = self.create_dmatrix_for_xgboost(
                    train_features, validation_features, encoded_test_features, train_labels, validation_labels,
                    test_labels)
                optimize_func = lambda trial: self.optimize_xgboost_hyperparamters(
                    trial, train_data, validation_data, test_data, test_labels)
                predict_and_save_func = lambda study: self.predict_and_save_xgboost_model(
                    study, train_data, validation_data, test_data, model_save_dir_path)
            elif model_type == 'lightgbm':
                train_data, validation_data, test_data = self.create_dataset_for_lightgbm(
                    train_features, validation_features, encoded_test_features, train_labels, validation_labels,
                    test_labels)
                optimize_func = lambda trial: self.optimize_lightgbm_hyperparamters(trial, train_data, validation_data)
                predict_and_save_func = lambda study: self.predict_and_save_lightgbm_model(
                    study, train_data, validation_data, encoded_test_features, model_save_dir_path)
            else:  # CatBoost
                cat_features = [col for col in encoded_train_features.columns if
                                encoded_train_features[col].dtype == 'object']
                train_data = Pool(train_features, train_labels, cat_features=cat_features)
                validation_data = Pool(validation_features, validation_labels, cat_features=cat_features)
                test_data = Pool(encoded_test_features, cat_features=cat_features)
                optimize_func = lambda trial: self.optimize_catboost_hyperparameters(trial, train_data, validation_data)
                predict_and_save_func = lambda study: self.predict_and_save_catboost_model(
                    study, train_data, validation_data, test_data, model_save_dir_path)

            # Optuna最適化を実行
            study = self._create_optuna_study()

            # コールバック関数の設定
            callback_func = None
            if self.show_optimization_progress or self.save_intermediate_results:
                def optimization_callback(study, trial):
                    if self.show_optimization_progress:
                        print(f"Trial {trial.number}: {self.primary_metric} = {trial.value:.6f}")

                    if self.save_intermediate_results and trial.number % 5 == 0:  # 5試行ごとに保存
                        self._save_intermediate_results(model_type, trial.number, study.best_params, study.best_value)

                    # メモリチェック
                    if not self._check_memory_limit():
                        raise optuna.TrialPruned("Memory limit exceeded")

                callback_func = optimization_callback

            # Optuna最適化を実行
            try:
                study.optimize(
                    optimize_func,
                    n_trials=self.optuna_trials,
                    timeout=self.optuna_timeout,
                    callbacks=[callback_func] if callback_func else None
                )
            except Exception as e:
                if hasattr(self, 'logger'):
                    self.logger.warning(f"{model_type}の最適化中にエラー: {str(e)}")
                # 最低限の試行が完了していれば継続
                if len(study.trials) == 0:
                    raise

            # 最適化結果の詳細ログ
            if self.verbose_training and hasattr(self, 'logger'):
                self.logger.info(f"{model_type}最適化完了:")
                self.logger.info(f"  - 最適試行数: {len(study.trials)}")
                self.logger.info(f"  - 最適スコア: {study.best_value:.6f}")
                self.logger.info(f"  - 最適パラメータ: {study.best_params}")

            # 最終モデルの学習と予測
            predicted_race_times, model = predict_and_save_func(study)

            # 学習時間の記録
            training_duration = time.time() - training_start_time
            if hasattr(self, 'logger'):
                self.logger.info(f"{model_type}学習完了 - 実行時間: {training_duration:.2f}秒")

            # リソース統計の記録
            if (self.memory_monitoring or self.performance_monitoring) and hasattr(self, 'logger'):
                self.logger.info(f"{model_type}リソース統計:")
                self.logger.info(f"  - ピークメモリ使用量: {self.resource_stats['peak_memory_gb']:.2f}GB")
                if self.performance_monitoring:
                    self.logger.info(f"  - 平均CPU使用率: {self.resource_stats['avg_cpu_percent']:.1f}%")

            return predicted_race_times, model

        except Exception as e:
            if hasattr(self, 'logger'):
                self.logger.error(f"{model_type}モデル学習エラー: {str(e)}")
            raise
        finally:
            # リソース監視停止
            self._stop_resource_monitoring()

ensemble_methods.py(アンサンブル学習)

# ==============================================================================
# アンサンブル学習による予測モデル統合プログラム
# ==============================================================================
# プログラムの概要:
#   複数の機械学習モデル(XGBoost, LightGBM, CatBoost)の予測結果を
#   様々なアンサンブル手法で統合し、予測精度の向上を図るプログラム。
#   各アンサンブル手法の結果を評価・可視化する機能も提供する。
#
# プログラムの主な機能:
#   1. アンサンブル手法の実装
#      - weighted_ensemble_predict(): 重み付けアンサンブル
#      - average_ensemble_predict(): 平均アンサンブル
#      - stacking_ensemble_predict(): スタッキングアンサンブル
#      - voting_ensemble_predict(): 投票アンサンブル
#      - blending_ensemble_predict(): ブレンディングアンサンブル
#   2. 特徴量重要度の分析
#      - 各アンサンブル手法の特徴量重要度計算
#      - 重要度の可視化とグラフ保存
#   3. モデル寄与度の分析
#      - 各モデルの予測への寄与度計算
#      - 寄与度の保存と可視化
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install numpy xgboost lightgbm matplotlib joblib
#
# 2. 設定値の確認:
#    - メタモデルのパラメータ設定
#      - n_estimators: 100
#      - learning_rate: 0.1
#      - random_state: 42
#    - 特徴量重要度の表示設定
#      - 上位表示件数: 20件
#
# 3. 入力データの準備:
#    - 各モデルの予測結果
#    - 学習済みモデル
#    - R2スコア
#    - 特徴量データ
#    - 正解ラベル
#
# 4. 実行方法:
#    - model_creator.pyから呼び出される(直接実行は想定していない)
#
# 5. 処理の流れ:
#    1) 各アンサンブル手法での予測実行
#    2) 特徴量重要度の計算と可視化
#    3) モデル寄与度の計算
#    4) 結果の保存
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. アンサンブル予測結果:
#    - 各手法での予測値(numpy array)
#
# 2. モデル関連ファイル:
#    - stacking_meta_model.pkl: スタッキング用メタモデル
#    - blending_meta_model.pkl: ブレンディング用メタモデル
#    - ensemble_weights.txt: 重み付けアンサンブルの重み
#    - model_contributions.txt: 各モデルの寄与度
#
# 3. 可視化ファイル:
#    - weighted_ensemble_feature_importance.svg
#    - ensemble_feature_importance.svg
#    - stacking_feature_importance.svg
#    - voting_ensemble_feature_importance.svg
#    - blending_feature_importance.svg
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - model_creator.py
#      └─ アンサンブル学習の実行を制御
#
# 2. データの受け取り:
#    - ModelTrainer: 基本モデルの予測結果
#    - DataProcessor: 前処理済みデータ
#
# 3. データの受け渡し:
#    - Evaluator: アンサンブル予測結果の評価
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行環境:
#    - GPU環境での実行を推奨
#    - メモリ使用量に注意(特にスタッキング/ブレンディング時)
#
# 2. データ要件:
#    - 全てのモデルの予測結果が必要
#    - 予測値のシェイプが一致していること
#    - 欠損値が含まれていないこと
#
# 3. アンサンブル手法の特徴:
#    - 重み付け: モデルのR2スコアに基づく重み付け
#    - スタッキング: メタモデルにXGBoostを使用
#    - ブレンディング: 元の特徴量も利用
#    - 投票: 予測値の平均化と寄与度計算
#
# 4. 保存データ:
#    - SVGファイルは上書きされる
#    - メタモデルは圧縮形式で保存
#    - 重み設定は可読形式で保存
#
# 5. 性能に関する注意:
#    - スタッキング/ブレンディングは計算コストが高い
#    - 大規模データセットでは処理時間が長くなる可能性
#    - メモリ使用量の監視が必要
#

import numpy as np
import xgboost as xgb
import lightgbm as lgb
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
import joblib
import os


class EnsembleMethods:
    def __init__(self):
        pass

    def weighted_ensemble_predict(self, predictions, models, r2_scores, ensemble_save_dir_path):
        """
        各モデルのR2スコアに基づいて重み付けを行い、アンサンブル予測を実行する。
        重みの高いモデルの予測がより大きく影響を与える。
        重み付けの結果とモデルの特徴量重要度も保存する。

        :param dict predictions: 各モデルの予測結果。キーはモデル名、値は予測値のnp.ndarray。
        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param dict r2_scores: 各モデルのR2スコア。キーはモデル名、値はスコア。
        :param str ensemble_save_dir_path: アンサンブル結果を保存するディレクトリパス。
        :return: 重み付けアンサンブルによる予測結果。
        :rtype: np.ndarray
        """
        # R2スコアに基づいて重みを計算
        total_r2 = sum(r2_scores.values())
        weights = {model: score / total_r2 for model, score in r2_scores.items()}

        # 重み付けアンサンブル予測
        weighted_ensemble_predictions = np.zeros_like(list(predictions.values())[0])
        for model, pred in predictions.items():
            weighted_ensemble_predictions += weights[model] * pred

        # アンサンブルモデルの特徴量重要度(重み付け平均)
        self.plot_weighted_ensemble_feature_importance(models, weights, ensemble_save_dir_path)

        # 重みの保存
        with open(os.path.join(ensemble_save_dir_path, 'ensemble_weights.txt'), 'w') as f:
            for model, weight in weights.items():
                f.write(f"{model}: {weight:.4f}\n")

        return weighted_ensemble_predictions

    def average_ensemble_predict(self, predictions, models, test_features, test_labels, ensemble_save_dir_path):
        """
        各モデルの予測結果の単純平均を計算してアンサンブル予測を行う。
        すべてのモデルの予測に対して同じ重みを適用する。
        アンサンブルモデルの特徴量重要度も計算して保存する。

        :param dict predictions: 各モデルの予測結果。キーはモデル名、値は予測値のnp.ndarray。
        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param DataFrame test_features: テストデータの特徴量。
        :param np.ndarray test_labels: テストデータの正解ラベル。
        :param str ensemble_save_dir_path: アンサンブル結果を保存するディレクトリパス。
        :return: 平均アンサンブルによる予測結果。
        :rtype: np.ndarray
        """
        # アンサンブル予測(単純平均)
        average_ensemble_predictions = np.mean(list(predictions.values()), axis=0)

        # アンサンブルモデルの特徴量重要度(個々のモデルの重要度の平均)
        self.plot_average_ensemble_feature_importance(models, ensemble_save_dir_path)

        return average_ensemble_predictions

    def stacking_ensemble_predict(self, train_features, test_features, train_labels, test_labels, base_models,
                                  ensemble_save_dir_path):
        """
        スタッキングアンサンブルを実行する。
        ベースモデルの予測を特徴量として使用し、メタモデル(XGBoost)で最終予測を行う。
        メタモデルとその特徴量重要度を保存する。

        :param DataFrame train_features: トレーニングデータの特徴量。
        :param DataFrame test_features: テストデータの特徴量。
        :param np.ndarray train_labels: トレーニングデータのラベル。
        :param np.ndarray test_labels: テストデータのラベル。
        :param dict base_models: ベースモデルの辞書。キーはモデル名、値はモデルオブジェクト。
        :param str ensemble_save_dir_path: アンサンブル結果を保存するディレクトリパス。
        :return: スタッキングアンサンブルによる予測結果。
        :rtype: np.ndarray
        """
        # ベースモデルの予測を生成
        base_train_predictions = np.zeros((len(train_labels), len(base_models)))
        base_test_predictions = np.zeros((len(test_labels), len(base_models)))

        for i, (model_name, model) in enumerate(base_models.items()):
            if model_name == 'xgboost':
                dtrain = xgb.DMatrix(train_features)
                dtest = xgb.DMatrix(test_features)
                base_train_predictions[:, i] = model.predict(dtrain)
                base_test_predictions[:, i] = model.predict(dtest)
            elif model_name == 'lightgbm':
                base_train_predictions[:, i] = model.predict(train_features)
                base_test_predictions[:, i] = model.predict(test_features)
            elif model_name == 'catboost':
                base_train_predictions[:, i] = model.predict(train_features)
                base_test_predictions[:, i] = model.predict(test_features)

        # メタモデル(XGBoost)のトレーニング
        meta_model = xgb.XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
        meta_model.fit(base_train_predictions, train_labels)

        # メタモデルによる最終予測
        stacking_ensemble_predictions = meta_model.predict(base_test_predictions)

        # スタッキングモデルの特徴量重要度
        self.plot_stacking_feature_importance(meta_model, list(base_models.keys()), ensemble_save_dir_path)

        # メタモデルの保存
        joblib.dump(meta_model, os.path.join(ensemble_save_dir_path, 'stacking_meta_model.pkl'))

        return stacking_ensemble_predictions

    def voting_ensemble_predict(self, predictions, models, test_features, test_labels, ensemble_save_dir_path):
        """
        ボルティングアンサンブルを実行する。
        各モデルの予測を平均化し、各モデルの寄与度も計算する。
        モデルの特徴量重要度と寄与度情報を保存する。

        :param dict predictions: 各モデルの予測結果。キーはモデル名、値は予測値のnp.ndarray。
        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param DataFrame test_features: テストデータの特徴量。
        :param np.ndarray test_labels: テストデータのラベル。
        :param str ensemble_save_dir_path: アンサンブル結果を保存するディレクトリパス。
        :return: ボルティングアンサンブルによる予測結果。
        :rtype: np.ndarray
        """
        # ボルティングアンサンブル予測(平均)
        voting_ensemble_predictions = np.mean(list(predictions.values()), axis=0)

        # ボルティングアンサンブルモデルの特徴量重要度(個々のモデルの重要度の平均)
        self.plot_voting_ensemble_feature_importance(models, ensemble_save_dir_path)

        # 各モデルの寄与度を保存
        self.save_model_contributions(predictions, test_labels, ensemble_save_dir_path)

        return voting_ensemble_predictions

    def blending_ensemble_predict(self, train_features, test_features, train_labels, test_labels, base_models,
                                  ensemble_save_dir_path):
        """
        ブレンディングアンサンブルを実行する。
        元の特徴量とベースモデルの予測を結合し、メタモデル(XGBoost)で最終予測を行う。
        メタモデルと特徴量重要度を保存する。

        :param DataFrame train_features: トレーニングデータの特徴量。
        :param DataFrame test_features: テストデータの特徴量。
        :param np.ndarray train_labels: トレーニングデータのラベル。
        :param np.ndarray test_labels: テストデータのラベル。
        :param dict base_models: ベースモデルの辞書。キーはモデル名、値はモデルオブジェクト。
        :param str ensemble_save_dir_path: アンサンブル結果を保存するディレクトリパス。
        :return: ブレンディングアンサンブルによる予測結果。
        :rtype: np.ndarray
        """
        # ベースモデルの予測を生成
        base_train_predictions = np.zeros((len(train_labels), len(base_models)))
        base_test_predictions = np.zeros((len(test_labels), len(base_models)))

        for i, (model_name, model) in enumerate(base_models.items()):
            if model_name == 'xgboost':
                dtrain = xgb.DMatrix(train_features)
                dtest = xgb.DMatrix(test_features)
                base_train_predictions[:, i] = model.predict(dtrain)
                base_test_predictions[:, i] = model.predict(dtest)
            elif model_name == 'lightgbm':
                base_train_predictions[:, i] = model.predict(train_features)
                base_test_predictions[:, i] = model.predict(test_features)
            elif model_name == 'catboost':
                base_train_predictions[:, i] = model.predict(train_features)
                base_test_predictions[:, i] = model.predict(test_features)

        # 元の特徴量とベースモデルの予測を結合
        blended_train_features = np.hstack((train_features, base_train_predictions))
        blended_test_features = np.hstack((test_features, base_test_predictions))

        # メタモデル(XGBoost)のトレーニング
        meta_model = xgb.XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
        meta_model.fit(blended_train_features, train_labels)

        # メタモデルによる最終予測
        blending_ensemble_predictions = meta_model.predict(blended_test_features)

        # ブレンディングモデルの特徴量重要度
        self.plot_blending_feature_importance(meta_model, train_features.columns, base_models.keys(),
                                              ensemble_save_dir_path)

        # メタモデルの保存
        joblib.dump(meta_model, os.path.join(ensemble_save_dir_path, 'blending_meta_model.pkl'))

        return blending_ensemble_predictions

    def plot_weighted_ensemble_feature_importance(self, models, weights, ensemble_save_dir_path):
        """
        重み付きアンサンブルモデルの特徴量重要度を計算し、可視化する。
        各モデルの特徴量重要度に重みを適用して集計する。

        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param dict weights: 各モデルの重み。キーはモデル名、値は重み。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(10, 8))

        feature_importance = {}
        for model_type, model in models.items():
            if model_type == 'xgboost':
                importance = model.get_score(importance_type='gain')
            elif model_type == 'lightgbm':
                importance = dict(zip(model.feature_name(), model.feature_importance(importance_type='gain')))
            elif model_type == 'catboost':
                importance = dict(zip(model.feature_names_, model.get_feature_importance()))

            for feature, value in importance.items():
                if feature in feature_importance:
                    feature_importance[feature] += value * weights[model_type]
                else:
                    feature_importance[feature] = value * weights[model_type]

        # ソートして上位20個の特徴量を表示
        sorted_importance = sorted(feature_importance.items(), key=lambda x: x[1], reverse=True)[:20]
        features, values = zip(*sorted_importance)

        plt.barh(range(len(features)), values)
        plt.yticks(range(len(features)), features)
        plt.title('Weighted Ensemble Feature Importance (Top 20)')
        plt.xlabel('Weighted Average Feature Importance')
        plt.ylabel('Features')
        plt.tight_layout()
        plt.savefig(os.path.join(ensemble_save_dir_path, 'weighted_ensemble_feature_importance.svg'))
        plt.close()

    def plot_average_ensemble_feature_importance(self, models, ensemble_save_dir_path):
        """
        平均アンサンブルモデルの特徴量重要度を計算し、可視化する。
        各モデルの特徴量重要度の平均を計算する。

        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(10, 8))

        feature_importance = {}
        for model_type, model in models.items():
            if model_type == 'xgboost':
                importance = model.get_score(importance_type='gain')
            elif model_type == 'lightgbm':
                importance = dict(zip(model.feature_name(), model.feature_importance(importance_type='gain')))
            elif model_type == 'catboost':
                importance = dict(zip(model.feature_names_, model.get_feature_importance()))

            for feature, value in importance.items():
                if feature in feature_importance:
                    feature_importance[feature] += value
                else:
                    feature_importance[feature] = value

        # 平均化
        for feature in feature_importance:
            feature_importance[feature] /= len(models)

        # ソートして上位20個の特徴量を表示
        sorted_importance = sorted(feature_importance.items(), key=lambda x: x[1], reverse=True)[:20]
        features, values = zip(*sorted_importance)

        plt.barh(range(len(features)), values)
        plt.yticks(range(len(features)), features)
        plt.title('Ensemble Feature Importance (Top 20)')
        plt.xlabel('Average Feature Importance')
        plt.ylabel('Features')
        plt.tight_layout()
        plt.savefig(os.path.join(ensemble_save_dir_path, 'ensemble_feature_importance.svg'))
        plt.close()

    def plot_stacking_feature_importance(self, meta_model, base_model_names, ensemble_save_dir_path):
        """
        スタッキングアンサンブルのメタモデルにおける各ベースモデルの重要度を可視化する。

        :param XGBRegressor meta_model: 学習済みメタモデル。
        :param list base_model_names: ベースモデルの名前リスト。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(10, 6))
        importance = meta_model.feature_importances_
        indices = np.argsort(importance)[::-1]

        plt.title('Stacking Ensemble Feature Importance')
        plt.bar(range(len(importance)), importance[indices])
        plt.xticks(range(len(importance)), [base_model_names[i] for i in indices], rotation=45)
        plt.xlabel('Base Models')
        plt.ylabel('Feature Importance')
        plt.tight_layout()
        plt.savefig(os.path.join(ensemble_save_dir_path, 'stacking_feature_importance.svg'))
        plt.close()

    def plot_voting_ensemble_feature_importance(self, models, ensemble_save_dir_path):
        """
        投票アンサンブルモデルの特徴量重要度を計算し、可視化する。
        各モデルの特徴量重要度の平均を計算する。

        :param dict models: 訓練済みモデル。キーはモデル名、値はモデルオブジェクト。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(10, 8))

        feature_importance = {}
        for model_type, model in models.items():
            if model_type == 'xgboost':
                importance = model.get_score(importance_type='gain')
            elif model_type == 'lightgbm':
                importance = dict(zip(model.feature_name(), model.feature_importance(importance_type='gain')))
            elif model_type == 'catboost':
                importance = dict(zip(model.feature_names_, model.get_feature_importance()))

            for feature, value in importance.items():
                if feature in feature_importance:
                    feature_importance[feature] += value
                else:
                    feature_importance[feature] = value

        # 平均化
        for feature in feature_importance:
            feature_importance[feature] /= len(models)

        # ソートして上位20個の特徴量を表示
        sorted_importance = sorted(feature_importance.items(), key=lambda x: x[1], reverse=True)[:20]
        features, values = zip(*sorted_importance)

        plt.barh(range(len(features)), values)
        plt.yticks(range(len(features)), features)
        plt.title('Voting Ensemble Feature Importance (Top 20)')
        plt.xlabel('Average Feature Importance')
        plt.ylabel('Features')
        plt.tight_layout()
        plt.savefig(os.path.join(ensemble_save_dir_path, 'voting_ensemble_feature_importance.svg'))
        plt.close()

    def plot_blending_feature_importance(self, meta_model, original_features, base_model_names, ensemble_save_dir_path):
        """
        ブレンディングアンサンブルの特徴量重要度を可視化する。
        元の特徴量とベースモデルの予測の両方に対する重要度を表示する。

        :param XGBRegressor meta_model: 学習済みメタモデル。
        :param list original_features: 元の特徴量の名前リスト。
        :param list base_model_names: ベースモデルの名前リスト。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(12, 8))
        importance = meta_model.feature_importances_
        feature_names = list(original_features) + list(base_model_names)
        indices = np.argsort(importance)[::-1]

        plt.title('Blending Ensemble Feature Importance')
        plt.bar(range(len(importance)), importance[indices])
        plt.xticks(range(len(importance)), [feature_names[i] for i in indices], rotation=90)
        plt.xlabel('Features')
        plt.ylabel('Feature Importance')
        plt.tight_layout()
        plt.savefig(os.path.join(ensemble_save_dir_path, 'blending_feature_importance.svg'))
        plt.close()

    def save_model_contributions(self, predictions, test_labels, ensemble_save_dir_path):
        """
        各モデルの予測精度(R2スコア)に基づく寄与度を計算し、保存する。

        :param dict predictions: 各モデルの予測結果。キーはモデル名、値は予測値のnp.ndarray。
        :param np.ndarray test_labels: テストデータの正解ラベル。
        :param str ensemble_save_dir_path: 結果を保存するディレクトリパス。
        :return: なし
        """
        model_contributions = {}
        for model_name, model_predictions in predictions.items():
            model_contributions[model_name] = r2_score(test_labels, model_predictions)

        total_contribution = sum(model_contributions.values())
        normalized_contributions = {model: contrib / total_contribution for model, contrib in
                                    model_contributions.items()}

        with open(os.path.join(ensemble_save_dir_path, 'model_contributions.txt'), 'w') as f:
            for model, contribution in normalized_contributions.items():
                f.write(f"{model}: {contribution:.4f}\n")

evaluator.py(評価・可視化)

# ==============================================================================
# モデル評価・性能分析プログラム
# ==============================================================================
# プログラムの概要:
#   機械学習モデルの予測性能を評価し、結果を可視化・保存するプログラム。
#   R2スコア、MAE、RMSEなどの評価指標の計算、予測結果の散布図作成、
#   特徴量重要度の分析を行う。各種評価結果はCSVおよびSVG形式で保存される。
#
# プログラムの主な機能:
#   1. 予測性能の評価
#      - R2スコア(決定係数)の計算
#      - MAE(平均絶対誤差)の計算
#      - RMSE(平均二乗誤差の平方根)の計算
#   2. 結果の可視化
#      - 実タイムと予測タイムの散布図作成
#      - 特徴量重要度のグラフ作成
#   3. 評価結果の保存
#      - 評価指標のCSVファイル保存
#      - 可視化結果のSVGファイル保存
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install numpy matplotlib scikit-learn japanize_matplotlib pandas
#
# 2. 設定値の確認:
#    - グラフサイズ設定
#      - 散布図: figsize=(6, 6)
#      - 特徴量重要度: figsize=(10, 8)
#    - 日本語フォント設定
#    - 保存形式設定(SVG)
#
# 3. 入力データの準備:
#    - テストデータの正解ラベル
#    - モデルの予測結果
#    - 評価対象のモデル
#    - 結果保存用のディレクトリパス
#
# 4. 実行方法:
#    - model_creator.pyから呼び出される(直接実行は想定していない)
#
# 5. 処理の流れ:
#    1) モデルの予測性能を評価(R2, MAE, RMSE)
#    2) 評価指標をCSVファイルに保存
#    3) 実タイムと予測タイムの散布図を作成
#    4) 特徴量重要度のグラフを作成
#    5) 可視化結果をSVGファイルとして保存
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. 評価指標ファイル:
#    - evaluation_results.csv
#      - R2スコア
#      - RMSE
#      - MAE
#
# 2. 可視化ファイル:
#    - 実タイムと予測タイムの散布図.svg
#    - 予測タイムの重要度分析_[モデル名].svg
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - model_creator.py
#      └─ モデル評価の実行を制御
#
# 2. データの受け取り:
#    - ModelTrainer: モデルと予測結果
#    - EnsembleMethods: アンサンブル予測結果
#
# 3. データの利用:
#    - ScoreCalculator: 評価指標を利用
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データ要件:
#    - 予測値と正解値のシェイプが一致していること
#    - 欠損値が含まれていないこと
#    - 入力データが数値型であること
#
# 2. 可視化設定:
#    - 日本語フォントが正しく設定されていること
#    - グラフのサイズが適切であること
#    - 軸ラベルが正しく表示されること
#
# 3. 保存データ:
#    - ファイルは自動的に上書きされる
#    - SVGファイルは高解像度で保存
#    - 保存先ディレクトリが存在しない場合は自動作成
#
# 4. メモリ使用:
#    - 大規模データセットでの可視化時はメモリ使用量に注意
#    - グラフは適切なタイミングでクローズする
#
# 5. モデル固有の注意:
#    - XGBoost: feature_importanceはgainタイプを使用
#    - LightGBM: feature_importanceは独自の計算方法
#    - CatBoost: feature_namesの指定が必要
#

import numpy as np
import matplotlib
matplotlib.use('Agg')  # GUIバックエンドを無効化
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
import os
import xgboost as xgb
import japanize_matplotlib
import pandas as pd

class Evaluator:
    def __init__(self):
        pass

    def evaluate_model_performance(self, test_labels, predicted_race_times, model_save_dir_path):
        """
        テストセットに対するモデルの予測性能を評価し、R^2スコア、MAE(平均絶対誤差)、RMSE(平均二乗誤差の平方根)を計算する。
        計算した評価は指定したディレクトリにテキストファイルとして保存する。
        :param np.ndarray test_labels: テストセットの正解ラベル。
        :param np.ndarray predicted_race_times: モデルの予測結果。
        :param str model_save_dir_path:評価結果を保存するディレクトリパス。
        :return: R^2スコア。
        :rtype: float
        """
        # 評価指標を計算
        r2 = r2_score(test_labels, predicted_race_times)
        mae = mean_absolute_error(test_labels, predicted_race_times)
        rmse = np.sqrt(mean_squared_error(test_labels, predicted_race_times))

        # 保存用のCSVパス
        csv_path = os.path.join(model_save_dir_path, 'evaluation_results.csv')

        # CSVに書き込むデータを準備
        evaluation_data = {
            'R2': [r2],
            'RMSE': [rmse],
            'MAE': [mae]
        }

        # データフレームを作成
        df = pd.DataFrame(evaluation_data)

        # CSVとして保存
        df.to_csv(csv_path, index=False, encoding='utf-8-sig')

        return r2

    def plot_actual_predicted(self, test_labels, predicted_race_times, model_save_dir_path, r2):
        """
        テストセットの実際のラベルと予測されたラベルで散布図を作成する。
        R2スコアをグラフのタイトルに表示し、グラフをSVG形式で指定されたディレクトリに保存する。
        :param np.ndarray test_labels: テストセットの正解ラベル。
        :param np.ndarray predicted_race_times: モデルの予測結果。
        :param model_save_dir_path:  グラフを保存するディレクトリパス。
        :param float r2: モデルのR^2スコア。
        :return: 無し
        """
        plt.figure(figsize=(6, 6))
        plt.plot([test_labels.min(), test_labels.max()], [test_labels.min(), test_labels.max()], 'k--', lw=4)
        plt.scatter(test_labels, predicted_race_times, color='darkorange')
        plt.xlabel('実タイム')
        plt.ylabel('予測タイム')
        plt.title(f'実タイムと予測タイムの散布図 R2={r2:.4f}')
        plt.legend()
        plt.savefig(os.path.join(model_save_dir_path, '実タイムと予測タイムの散布図.svg'))
        plt.close()

    def plot_feature_importance(self, model, model_type, model_save_dir_path):
        """
        モデルの特徴量重要度をプロットし、SVGファイルとして保存します。
        :param model: 特徴量の重要度を表示するモデル(XGBoost、LightGBM、またはCatBoost)。
        :param str model_type: モデルの種類('xgboost'、'lightgbm'、または'catboost')。
        :param str model_save_dir_path: グラフを保存するディレクトリパス。
        :return: なし
        """
        plt.figure(figsize=(10, 8))

        if model_type == 'xgboost':
            xgb.plot_importance(model, ax=plt.gca(), importance_type='gain')
        elif model_type == 'lightgbm':
            importance = model.feature_importance(importance_type='gain')
            feature_names = model.feature_name()
            indices = np.argsort(importance)
            plt.barh(range(len(indices)), importance[indices])
            plt.yticks(range(len(indices)), [feature_names[i] for i in indices])
        elif model_type == 'catboost':
            feature_importance = model.get_feature_importance()
            feature_names = model.feature_names_
            sorted_idx = np.argsort(feature_importance)
            pos = np.arange(sorted_idx.shape[0]) + .5
            plt.barh(pos, feature_importance[sorted_idx], align='center')
            plt.yticks(pos, np.array(feature_names)[sorted_idx])
        else:
            raise ValueError(f"Unsupported model type: {model_type}")

        plt.title(f'予測タイムの重要度分析 ({model_type.capitalize()})')
        plt.xlabel('Feature Importance')
        plt.ylabel('Features')
        plt.tight_layout()
        plt.savefig(os.path.join(model_save_dir_path, f'予測タイムの重要度分析_{model_type}.svg'))
        plt.close()

utils.py(共通機能・的中率計算)

# ==============================================================================
# 予測結果評価・保存・パス管理ユーティリティプログラム
# ==============================================================================
# プログラムの概要:
#   競馬予測システムにおける多目的ユーティリティプログラム。プロジェクト
#   全体で使用されるパス管理機能、レース予測結果の評価・分析機能、
#   競馬特有の的中率計算機能、データ保存機能を統合的に提供する。
#   各種モジュールから呼び出されるサポート機能の中核を担う。
#
# プログラムの主な機能:
#   1. パス管理機能
#      - get_project_root(): プロジェクトルートディレクトリの絶対パス取得
#      - get_config_path(): config.iniファイルの絶対パス取得
#      - get_web_scraping_dir(): web_scrapingディレクトリのパス取得
#   2. 競馬特有の的中率計算機能
#      - 単勝的中率計算(1着の完全一致)
#      - 複勝的中率計算(頭数による着順範囲の動的判定)
#      - 馬連的中率計算(1-2着の組み合わせ)
#      - ワイド的中率計算(3着以内の2頭以上の予測的中)
#      - 三連複的中率計算(1-3着の完全予測)
#   3. レース結果の順位付け・分析機能
#      - 実際のタイムによる実順位付与
#      - 予測タイムによる予測順位付与
#      - レース単位でのグループ化処理
#   4. データ保存・統合機能
#      - 予測結果のCSV保存(cp932エンコーディング)
#      - 評価指標の統合(R2, RMSE, MAE)
#      - 的中率結果の累積保存(追記モード)
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install pandas numpy os csv datetime
#
# 2. プロジェクト構造の前提:
#    project_root/
#    ├── config.ini (設定ファイル)
#    ├── make_model/ (このutils.pyが配置されるディレクトリ)
#    ├── web_scraping/ (Webスクレイピング関連)
#    └── その他のディレクトリ
#
# 3. 入力データの前提:
#    - 予測結果データ(DataFrame)
#      必須カラム: race_name, location, round, ActualTime, PredictedTime
#    - evaluation_results.csv(各モデルディレクトリ内)
#      必須カラム: R2, RMSE, MAE
#
# 4. 実行方法:
#    - 他のモジュールから関数を個別にインポートして使用
#    - 直接実行は想定していない(ユーティリティライブラリ)
#
# 5. 主要な呼び出しパターン:
#    from utils import get_config_path, save_predictions_to_csv
#    config_path = get_config_path()
#    save_predictions_to_csv(features, labels, predictions, save_dir)
#
# ==============================================================================
# 競馬的中率計算の詳細仕様
# ==============================================================================
# 1. 複勝的中率の頭数による判定:
#    - 8頭以上出走: 3着以内に予測順位1~3位が含まれるか判定
#    - 8頭未満出走: 2着以内に予測順位1~2位が含まれるか判定
#    - 判定結果: 的中=1, 外れ=0
#
# 2. 馬連的中率の計算:
#    - 実際の1-2着と予測の1-2着が完全一致
#    - 判定条件: 予測順位1-2位の合計が3(1+2)
#
# 3. ワイド的中率の計算:
#    - 実際の3着以内に予測順位1~3位が2頭以上含まれる
#    - より柔軟な的中判定(馬連より当たりやすい)
#
# 4. 三連複的中率の計算:
#    - 実際の1-3着と予測の1-3着が完全一致
#    - 判定条件: 予測順位1-3位の合計が6(1+2+3)
#
# 5. レースグループ化処理:
#    - (race_name, location, round)の組み合わせで個別レースを識別
#    - レース単位で的中率を計算後、全体の平均を算出
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. 予測結果ファイル:
#    - 予測結果.csv(各モデルディレクトリ内)
#      - 元の特徴量データ
#      - ActualTime: 実際のレースタイム
#      - PredictedTime: モデル予測タイム
#      - エンコーディング: cp932
#
# 2. 的中率結果ファイル:
#    - 的中率結果.csv(出力ルートディレクトリ)
#      - ヘッダー: 作成日,単勝,複勝,馬連,ワイド,三連複,モデルパス,分割日,R2,RMSE,MAE
#      - 各モデルの結果を累積追記
#      - パーセント形式での的中率表示
#      - 評価指標の統合表示
#
# 3. 順位付けデータ:
#    - rank_real: 実際のタイムに基づく順位(1位から連番)
#    - rank_predict: 予測タイムに基づく順位(1位から連番)
#    - ソート処理により順位を動的に計算
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元(パス管理機能):
#    - train_model.py: get_config_path(), get_project_root()
#    - data_processor.py: get_config_path()
#    - model_creator.py: get_config_path()
#    - その他多数のモジュール
#
# 2. 呼び出し元(的中率計算機能):
#    - model_creator.py
#      └─ save_predictions_to_csv(), analyze_and_save_win_rates()
#      └─ 各モデルの予測結果保存と的中率計算
#
# 3. データの統合処理:
#    - Evaluator: evaluation_results.csvから評価指標を取得
#    - ModelCreator: 各モデルの予測結果を順次処理
#    - ScoreCalculator: 的中率結果をスコア計算に利用
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. パス管理の前提条件:
#    - utils.pyはmake_modelディレクトリ内に配置されること
#    - config.iniはプロジェクトルートに配置されること
#    - 相対パス計算は__file__の位置を基準に実行
#
# 2. データ要件:
#    - race_name, location, roundの組み合わせでレースを一意識別
#    - ActualTime, PredictedTimeは数値型(タイム)であること
#    - evaluation_results.csvが各モデルディレクトリに存在すること
#
# 3. 的中率計算の制約:
#    - 5頭未満のレースは一般的でないため、処理対象外とする場合がある
#    - 同着や失格等の特殊ケースは考慮されていない
#    - 予測順位は連続した整数値(1,2,3...)を前提
#
# 4. ファイル操作の注意:
#    - CSV保存時は'cp932'エンコーディング固定
#    - エラー発生時は'ignore'オプションで処理継続
#    - 的中率結果.csvは追記モード('a')で累積保存
#    - ヘッダーはファイル不存在またはサイズ0の場合のみ書き込み
#
# 5. 順位付け処理の仕様:
#    - add_real_and_predicted_ranks()で元のDataFrameを変更
#    - sort_values()による破壊的ソート操作
#    - rank_real, rank_predictカラムを新規追加
#
# 6. 評価指標の統合:
#    - R2, RMSE, MAEはevaluation_results.csvから自動取得
#    - ファイル不存在時は'N/A'で保存
#    - 小数点以下5桁での高精度保存
#
# 7. 日付処理:
#    - 作成日は処理実行時の日付を自動取得(YYYYMMDD形式)
#    - train_test_split_dateは呼び出し元から引数で受け取り
#
# 8. エラー処理とログ:
#    - ファイル読み書きエラーは'ignore'で継続
#    - 存在しないCSVファイルは警告なしでスキップ
#    - 数値変換エラーは個別に処理
#

import pandas as pd
import os
import csv
import datetime
import logging
import logging.handlers
import sys

def get_project_root():
    """
    プロジェクトのルートディレクトリのパスを取得する
    """
    # 現在のファイルのパス
    current_file = os.path.abspath(__file__)

    # make_modelディレクトリ
    make_model_dir = os.path.dirname(current_file)

    # プロジェクトルート(config.iniがある場所)
    project_root = os.path.dirname(make_model_dir)

    return project_root


def get_config_path():
    """
    config.iniの絶対パスを取得する
    """
    return os.path.join(get_project_root(), 'config.ini')


def get_web_scraping_dir():
    """
    web_scrapingディレクトリのパスを取得する
    """
    return os.path.join(get_project_root(), 'web_scraping')


def add_real_and_predicted_ranks(race_results):
    """
    実際のタイムと予測したタイムに基づいて、レース結果データに実際の順位(rank_real)と予測した順位(rank_predict)を追加する。
    :param race_results: レース結果データ。
    :return: rank_realとrank_predict列が追加されたレース結果データ。
    :rtype: pd.DataFrame
    """
    race_results['rank_real'] = range(1, len(race_results.index) + 1)  # 実際の順位を割り当てる
    race_results.sort_values('PredictedTime', inplace=True)  # 予測したタイムでソード
    race_results['rank_predict'] = range(1, len(race_results.index) + 1)  # 予測の順位を割り当てる
    race_results.sort_values('rank_real', inplace=True)  # 実際の順位で再ソート

    return race_results


def calculate_win_accuracy(ranked_race_results):
    """
    単勝(英訳:win)の的中を確認する。
    実順位の1位と予測順位の1位を比較し、一致の場合は1を返し、不一致の場合は0を返す。
    :param ranked_race_results:
    :return:
    """
    return int(ranked_race_results.iloc[0]['rank_real'] == ranked_race_results.iloc[0]['rank_predict'])


def calculate_place_accuracy(ranked_race_results):
    """
    複勝(英訳:place)の的中を確認する。
    8頭以上の出走レースでは、3着以内に予測順位1~3の有無を確認する。
    8頭未満が出走しているレースでは、2着以内に予測順位1~2の有無を確認する。
    予想の当たり1/外れ0を返す
    :param ranked_race_results:
    :return:
    """
    horses_num = len(ranked_race_results)
    if horses_num >= 8:
        return int(any(ranked_race_results.iloc[:3]['rank_predict'] <= 3))
    else:
        return int(any(ranked_race_results.iloc[:2]['rank_predict'] <= 2))


def calculate_quinella_accuracy(ranked_race_results):
    """
    馬連(英訳:quinella)の的中を確認する。
    実順位の1位と2位、予測順位の1位,2位が一致するかを確認する。
    予測順位の1行目と2行目の合計が3(1位+2位)だった場合は当たり、それ以外は外れ。
    予想の当たり1/外れ0を返す。
    :param ranked_race_results:
    :return:
    """
    return int(sum(ranked_race_results.iloc[:2]['rank_predict']) == 3)


def calculate_quinella_place_accuracy(ranked_race_results):
    """
    ワイド(英訳:quinella place)の的中を確認する。
    実順位の3着以内に予測順位1~3が2つ以上あるかを確認する。
    予想の当たり1/外れ0を返す。
    :param ranked_race_results:
    :return:
    """
    return int(sum(ranked_race_results.iloc[:3]['rank_predict'] <= 3) >= 2)


def calculate_trio_accuracy(ranked_race_results):
    """
    三連複(英訳:trio)の的中を確認する。
    予測順位の1~3行目の合計が6(1位+2位+3位)だった場合は当たり、それ以外は外れ。
    予想の当たり1/外れ0を返す。
    :param ranked_race_results:
    :return:
    """
    return int(sum(ranked_race_results.iloc[:3]['rank_predict']) == 6)


def save_predictions_to_csv(test_features, test_labels, predicted_race_times, model_save_dir_path):
    """
    テストセットの特徴量、実際のラベル、予測したラベルを含むCSVファイルを保存する。
    :param DataFrame test_features: 特徴量を含むテストセット。
    :param np.ndarray test_labels: テストセットの正解ラベル。
    :param np.ndarray predicted_race_times: モデルの予測結果。
    :param str model_save_dir_path: 予測結果を保存するディレクトリパス。
    :return: 無し
    """
    results = test_features.copy()
    results['ActualTime'] = test_labels
    results['PredictedTime'] = predicted_race_times
    results.to_csv(os.path.join(model_save_dir_path, '予測結果.csv'), encoding='cp932', index=False,
                   errors='ignore')


def analyze_and_save_win_rates(model_save_dir_path, train_test_split_date):
    """
    予測結果に基づいて単勝、複勝、馬連、ワイド、三連複の的中率を計算し、CSVファイルに保存する。
    :param model_save_dir_path: 予測結果を含むCSVファイルが保存されるディレクトリパス。
    :param train_test_split_date: トレーニングデータとテストデータを分割する日付。
    :return: 無し
    """

    race_results = pd.read_csv(f'{model_save_dir_path}/予測結果.csv', encoding='cp932')

    # 各的中率の初期値を設定する
    num_hits_win = 0
    num_hits_place = 0
    num_hits_quinella = 0
    num_hits_quinella_place = 0
    num_hits_trio = 0
    total_race = 0

    # レース結果ごとに的中率を計算する
    for (race_name, location, round), group_race_result in race_results.groupby(['race_name', 'location', 'round']):
        race_result_assign_rank = add_real_and_predicted_ranks(group_race_result)
        num_hits_win += calculate_win_accuracy(race_result_assign_rank)
        num_hits_place += calculate_place_accuracy(race_result_assign_rank)
        num_hits_quinella += calculate_quinella_accuracy(race_result_assign_rank)
        num_hits_quinella_place += calculate_quinella_place_accuracy(race_result_assign_rank)
        num_hits_trio += calculate_trio_accuracy(race_result_assign_rank)
        total_race += 1

    current_date = datetime.datetime.now().strftime('%Y%m%d')

    # evaluation_results.csvの読み込み
    eval_csv_path = os.path.join(model_save_dir_path, 'evaluation_results.csv')
    if os.path.exists(eval_csv_path):
        eval_results = pd.read_csv(eval_csv_path)
        r2 = eval_results['R2'].iloc[0]
        rmse = eval_results['RMSE'].iloc[0]
        mae = eval_results['MAE'].iloc[0]
    else:
        r2 = None
        rmse = None
        mae = None

    # CSVファイルにヘッダーを書き込むかを判定する
    # CSVファイルが存在しないあるいはファイルサイズが0の場合、ヘッダーを書き込むをTrueにする
    csv_columns = ['作成日', '単勝', '複勝', '馬連', 'ワイド', '三連複', 'モデルパス', '分割日', 'R2', 'RMSE', 'MAE']
    csv_file_path = f'{model_save_dir_path}/../../的中率結果.csv'
    write_header = not os.path.exists(csv_file_path) or os.stat(csv_file_path).st_size == 0

    # CSVファイルにデータを追記する
    with open(csv_file_path, 'a', newline='', encoding='cp932') as csvfile:
        writer = csv.writer(csvfile)

        # ヘッダの書き込み
        if write_header:
            writer.writerow(csv_columns)

        # 計算した的中率をパーセント形式で書き込む
        writer.writerow([
            current_date,
            f'{num_hits_win / total_race:.2%}',
            f'{num_hits_place / total_race:.2%}',
            f'{num_hits_quinella / total_race:.2%}',
            f'{num_hits_quinella_place / total_race:.2%}',
            f'{num_hits_trio / total_race:.2%}',
            model_save_dir_path,
            train_test_split_date,
            f'{r2:.5f}' if r2 is not None else 'N/A',
            f'{rmse:.5f}' if rmse is not None else 'N/A',
            f'{mae:.5f}' if mae is not None else 'N/A'
        ])
        # writer.writerow([current_date, f'{num_hits_win / total_race:.2%}', f'{num_hits_place / total_race:.2%}',
        #                  f'{num_hits_quinella / total_race:.2%}', f'{num_hits_quinella_place / total_race:.2%}',
        #                  f'{num_hits_trio / total_race:.2%}', model_save_dir_path, train_test_split_date])


def get_log_level_from_string(level_string):
    """
    文字列のログレベルをloggingモジュールのレベル定数に変換する

    :param str level_string: ログレベルの文字列(DEBUG, INFO, WARNING, ERROR, CRITICAL)
    :return: loggingモジュールのレベル定数
    :rtype: int
    """
    level_mapping = {
        'DEBUG': logging.DEBUG,
        'INFO': logging.INFO,
        'WARNING': logging.WARNING,
        'WARN': logging.WARNING,  # 省略形も対応
        'ERROR': logging.ERROR,
        'CRITICAL': logging.CRITICAL,
        'FATAL': logging.CRITICAL  # 省略形も対応
    }

    level_upper = level_string.upper() if level_string else 'INFO'
    return level_mapping.get(level_upper, logging.INFO)


def setup_project_logging(log_directory, current_date_time, log_level=logging.INFO):
    """
    プロジェクト全体のログ設定を初期化する

    :param str log_directory: ログファイルを保存するディレクトリ
    :param str current_date_time: 実行時のタイムスタンプ
    :param int log_level: ログレベル(デフォルト: INFO)
    :return: ログファイルのパス
    :rtype: str
    """
    # ログディレクトリの作成
    os.makedirs(log_directory, exist_ok=True)

    # ログファイルパス
    log_file_path = os.path.join(log_directory, f'training_log_{current_date_time}.log')

    # 既存のハンドラーをクリア
    root_logger = logging.getLogger()
    for handler in root_logger.handlers[:]:
        root_logger.removeHandler(handler)

    # ログフォーマットの設定
    log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    date_format = '%Y-%m-%d %H:%M:%S'
    formatter = logging.Formatter(log_format, date_format)

    # ファイルハンドラーの設定
    file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
    file_handler.setLevel(log_level)
    file_handler.setFormatter(formatter)

    # コンソールハンドラーの設定
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(log_level)
    console_handler.setFormatter(formatter)

    # ルートロガーに追加
    root_logger.setLevel(log_level)
    root_logger.addHandler(file_handler)
    root_logger.addHandler(console_handler)

    # ログ設定の確認
    logger = logging.getLogger(__name__)
    logger.info("=" * 80)
    logger.info("競馬レース予測システム - ログ設定完了")
    logger.info("=" * 80)
    logger.info(f"ログファイル: {log_file_path}")
    logger.info(f"ログレベル: {logging.getLevelName(log_level)}")
    logger.info(f"実行タイムスタンプ: {current_date_time}")

    return log_file_path

def get_logger(module_name):
    """
    指定されたモジュール名でロガーを取得する

    :param str module_name: モジュール名(通常は__name__を渡す)
    :return: ロガーインスタンス
    :rtype: logging.Logger
    """
    return logging.getLogger(module_name)


def log_system_info():
    """
    システム情報をログに出力する
    """
    import platform
    import psutil

    logger = logging.getLogger(__name__)

    logger.info("システム情報:")
    logger.info(f"  - OS: {platform.system()} {platform.release()}")
    logger.info(f"  - Python: {platform.python_version()}")
    logger.info(f"  - CPU: {psutil.cpu_count()}コア")
    logger.info(f"  - メモリ: {psutil.virtual_memory().total / (1024 ** 3):.1f}GB")

    # GPU情報(利用可能な場合)
    try:
        import GPUtil
        gpus = GPUtil.getGPUs()
        if gpus:
            for i, gpu in enumerate(gpus):
                logger.info(f"  - GPU{i}: {gpu.name} ({gpu.memoryTotal}MB)")
        else:
            logger.info("  - GPU: 検出されませんでした")
    except ImportError:
        logger.info("  - GPU: GPUtilがインストールされていません")
    except Exception as e:
        logger.info(f"  - GPU: 情報取得エラー ({str(e)})")


def close_logging():
    """
    ログハンドラーを適切に閉じる
    """
    logger = logging.getLogger(__name__)
    logger.info("ログシステムを終了します")

    # 全てのハンドラーを閉じる
    root_logger = logging.getLogger()
    for handler in root_logger.handlers[:]:
        handler.close()
        root_logger.removeHandler(handler)

score_calculator.py(総合スコア計算)

# ==============================================================================
# モデル性能スコア計算プログラム
# ==============================================================================
# プログラムの概要:
#   競馬予測モデルの評価スコアを計算し、的中率結果のCSVファイルに
#   スコアを追加するプログラム。単勝的中率、R2スコア、RMSE、MAEに
#   重み付けを行い、総合的な性能評価スコアを算出する。
#
# プログラムの主な機能:
#   1. スコア計算機能
#      - 単勝的中率への重み付け (0.4)
#      - R2スコアへの重み付け (0.3)
#      - RMSEへの重み付け (0.2)
#      - MAEへの重み付け (0.1)
#   2. スコア正規化
#      - RMSEの正規化(1/(1+RMSE))
#      - MAEの正規化(1/(1+MAE))
#   3. 結果の更新
#      - 的中率CSVへのスコア追加
#      - 新規CSVファイルの生成
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    pip install pandas numpy
#
# 2. 設定値の確認:
#    - スコア重み
#      - 単勝的中率: 0.4
#      - R2スコア: 0.3
#      - RMSE: 0.2
#      - MAE: 0.1
#    - CSVエンコーディング設定
#
# 3. 入力データの準備:
#    - 的中率結果.csv
#      必須カラム:
#      - 単勝: 的中率(%表示)
#      - R2: R2スコア
#      - RMSE: 平均二乗誤差の平方根
#      - MAE: 平均絶対誤差
#
# 4. 実行方法:
#    - train_model.pyから呼び出される(直接実行は想定していない)
#
# 5. 処理の流れ:
#    1) 的中率CSVファイルの読み込み
#    2) 各指標の正規化処理
#    3) 重み付けスコアの計算
#    4) 結果を新しいCSVファイルとして保存
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. スコア付き結果ファイル:
#    - 的中率結果_with_score.csv
#      - 元の的中率データ
#      + スコア列(0-1の範囲)
#
# 2. スコアの計算式:
#    score = (0.4 * win_rate +
#             0.3 * r2 +
#             0.2 * normalized_rmse +
#             0.1 * normalized_mae)
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - train_model.py
#      └─ スコア計算の実行を制御
#
# 2. データの受け取り:
#    - Evaluator: 評価指標を提供
#    - utils: 的中率データを提供
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データ要件:
#    - 入力CSVファイルの必須カラム確認
#    - パーセント表示の値の処理
#    - 数値型への変換確認
#
# 2. スコア計算:
#    - 重みの合計は1になるように設定
#    - 各指標の正規化範囲の確認
#    - ゼロ除算の防止
#
# 3. ファイル操作:
#    - CSVファイルは'cp932'エンコーディングで保存
#    - 既存ファイルは上書きされる
#    - 出力パスの存在確認
#
# 4. エラー処理:
#    - 欠損値の処理
#    - 不正なデータ形式の処理
#    - ファイル読み書きエラーの処理
#
# 5. 拡張性:
#    - 新しい評価指標の追加が可能
#    - 重み付けの調整が可能
#    - 正規化方法の変更が可能
#

import pandas as pd
import numpy as np


class ScoreCalculator:
    def calculate_model_score(win_rate, r2, rmse, mae):
        """
        モデルのスコアを計算するメソッド。
        :param float win_rate: 的中率 (0-1スケール)。
        :param float r2: R²スコア。
        :param float rmse: 平均二乗誤差平方根。
        :param float mae: 平均絶対誤差。
        :return: スコア。
        """
        try:
            # RMSEとMAEは小さいほど良いので逆数を計算
            normalized_rmse = 1 / (1 + rmse)
            normalized_mae = 1 / (1 + mae)

            # 重み付けスコアを計算
            score = (0.4 * win_rate +
                     0.3 * r2 +
                     0.2 * normalized_rmse +
                     0.1 * normalized_mae)
            return score
        except Exception as e:
            print(f"スコア計算中にエラーが発生しました: {e}")
            return 0.0

    def add_scores_to_csv(file_path, output_path):
        """
        的中率結果.csvにスコアを追加し、新しいCSVとして保存する。
        :param str file_path: 入力CSVファイルのパス。
        :param str output_path: 出力CSVファイルのパス。
        """
        # ファイルを読み込む
        df = pd.read_csv(file_path, encoding='cp932')

        # スコア列を計算して追加
        def calculate_row_score(row):
            win_rate = float(row['単勝'].strip('%')) / 100
            r2 = float(row['R2']) if 'R2' in row and not pd.isna(row['R2']) else 0
            rmse = float(row['RMSE']) if 'RMSE' in row and not pd.isna(row['RMSE']) else 0
            mae = float(row['MAE']) if 'MAE' in row and not pd.isna(row['MAE']) else 0
            return ScoreCalculator.calculate_model_score(win_rate, r2, rmse, mae)

        df['スコア'] = df.apply(calculate_row_score, axis=1)

        # 保存
        df.to_csv(output_path, index=False, encoding='cp932')
        print(f"スコアを追加したCSVファイルを保存しました: {output_path}")

generate_folder_list.py(フォルダ管理)

# ==============================================================================
# モデル保存フォルダ管理プログラム
# ==============================================================================
# プログラムの概要:
#   競馬予測システムで生成された学習結果フォルダを管理するプログラム。
#   YYYYMMdd_HHMMSS形式のフォルダを検索し、一覧をJSON形式で保存する。
#   生成されたJSONファイルは、Web UI(report2.html)でのフォルダ選択に
#   使用される。
#
# プログラムの主な機能:
#   1. フォルダ検索機能
#      - 指定ディレクトリ内のフォルダ検索
#      - YYYYMMdd_HHMMSS形式の検証
#      - 日付順での降順ソート
#   2. JSON生成機能
#      - フォルダ一覧のJSON形式変換
#      - 自動的なディレクトリ作成
#      - 既存JSONファイルの更新
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
#    - pythonの標準ライブラリのみを使用(追加のインストール不要)
#
# 2. 設定値の確認:
#    target_dir: 検索対象のディレクトリパス
#    output_path: 出力するJSONファイルのパス
#
# 3. 入力データの準備:
#    - target_dirに検索対象のフォルダが存在することを確認
#    - フォルダ名がYYYYMMdd_HHMMSS形式であることを確認
#
# 4. 実行方法:
#    python generate_folder_list.py <対象ディレクトリ> <出力JSONパス>
#    例: python generate_folder_list.py . ./data/folder_list.json
#
# 5. 処理の流れ:
#    1) 指定ディレクトリ内のフォルダを検索
#    2) YYYYMMdd_HHMMSS形式のフォルダを抽出
#    3) 日付順に降順ソート
#    4) JSONファイルとして保存
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. JSONファイル:
#    - folder_list.json
#      - フォルダ名の配列
#      - 日付降順でソート済み
#
# 2. フォーマット例:
#    [
#      "20240119_235959",
#      "20240119_225858",
#      "20240119_215757"
#    ]
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 呼び出し元:
#    - train_model.py
#      └─ モデル作成完了後にフォルダリストを更新
#
# 2. 出力の利用:
#    - report2.html
#      └─ フォルダリストをドロップダウンメニューに表示
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. フォルダ名の要件:
#    - 厳密にYYYYMMdd_HHMMSS形式である必要がある
#    - 15文字の固定長
#    - 日付部分が8桁の数字
#    - アンダースコアで区切られている
#    - 時刻部分が6桁の数字
#
# 2. ファイル操作:
#    - 出力ディレクトリが存在しない場合は自動的に作成
#    - 既存のJSONファイルは上書きされる
#    - ファイルパーミッションに注意
#
# 3. エラー処理:
#    - 不適切なフォルダ名は自動的に除外
#    - ディレクトリ権限エラーの可能性
#    - JSON保存時のエラー処理
#
# 4. パフォーマンス:
#    - 大量のフォルダが存在する場合の処理時間
#    - メモリ使用量は通常小さい
#
# 5. 拡張性:
#    - フォルダ名フォーマットの変更が可能
#    - JSON以外の出力形式への対応が可能
#    - ソート順の変更が可能
#

import os
import json
import sys
from pathlib import Path


def generate_folder_list(target_dir, output_path):
    """
    指定されたディレクトリ内のフォルダ一覧を生成しJSONファイルとして保存する。
    :param str target_dir: 検索対象のディレクトリパス。
    :param str output_path: 出力するJSONファイルのパス。
    :return: なし
    :rtype: None
    """
    try:
        # 対象ディレクトリの存在確認
        if not os.path.exists(target_dir):
            raise FileNotFoundError(f"指定されたディレクトリが存在しません: {target_dir}")

        # YYYYMMdd_HHMMSS形式のフォルダを取得
        folders = [d for d in os.listdir(target_dir)
                   if os.path.isdir(os.path.join(target_dir, d)) and
                   len(d) == 15 and
                   d[:8].isdigit() and
                   d[8] == '_' and
                   d[9:].isdigit()]

        # 日付順に降順ソート
        folders.sort(reverse=True)

        # 出力ディレクトリが存在しない場合は作成
        os.makedirs(os.path.dirname(output_path), exist_ok=True)

        # JSONファイルとして保存
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(folders, f, ensure_ascii=False, indent=2)

        print(f"フォルダ一覧を生成しました: {output_path}")
        print(f"フォルダ数: {len(folders)}")

    except Exception as e:
        print(f"エラーが発生しました: {str(e)}", file=sys.stderr)
        sys.exit(1)


def main():
    """
    コマンドライン引数を処理する。
    使用例:
    python generate_folder_list.py /path/to/target/dir /path/to/output/folder_list.json
    """
    if len(sys.argv) != 3:
        print("使用方法: python generate_folder_list.py <対象ディレクトリ> <出力JSONパス>")
        print("例: python generate_folder_list.py . ./data/folder_list.json")
        sys.exit(1)

    target_dir = sys.argv[1]
    output_path = sys.argv[2]

    generate_folder_list(target_dir, output_path)


if __name__ == '__main__':
    main()

以上です。

コメント

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