[機械学習 実践]プロ野球選手の年俸を査定するAIを作成|LightGBM|Machine Learning|Python
機械学習の実践例として、プロ野球選手の成績と年俸のデータを使って、機械学習モデルの作成・予測実行してみました。
イメージは以下のとおりです。
準備
Pythonの設定
プログラムはPythonを使用し、機械学習のアルゴリズムは、Kaggle等のデータ分析コンペでも人気のLightGBMを使用します。
Pythonの設定方法・使い方については、必要に応じて過去の記事をご確認ください。
データサイエンティストを目指す方向け Pythonセットアップ方法(Windows版) - Kevin's Data Analytics Blog
Jupyter Notebook:データサイエンティストを目指す方にオススメのPython実行環境 - Kevin's Data Analytics Blog
【初心者向け】Pandas入門 これだけは押さえておくべき基本操作|データ分析で必須 - Kevin's Data Analytics Blog
lightgbmパッケージのインストール
LightGBMを使うためには、lightgbmパッケージが必要です。インストールしていない場合は、コマンドプロンプトを開いて、以下のコマンドを実行するとインストールできます。
pip install lightgbm
sweetvizパッケージのインストール
データの可視化で、Sweetvizを使用します。日本語データを扱うための設定が若干複雑でしたので、今回は説明は割愛します。(別の機会に紹介します。)
もし、インストール済でない場合、機械学習モデルの作成自体はできますので、スキップしても大丈夫です。
データの取得
機会学習に必要な数値を以下のサイトから取得し、CSVファイルに保存します。欠損値データの除去などの前処理も事前に行います。
プロ野球データFreak
実装
1. ライブラリのインポート
import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline # データ可視化用ライブラリ →未インストールの場合はインポート不要 import sweetviz as sv sv.config_parser.read('sweetviz_setting.ini') # 機械学習用ライブラリ LightGBM import lightgbm as lgb # テストデータ分割用ライブラリ from sklearn.model_selection import LeaveOneOut # 評価用ライブラリ from sklearn.metrics import mean_squared_error from sklearn.metrics import r2_score
2. データの読み込み・確認
pitcher = pd.read_csv('../data/npb_stats_salary_pitcher.csv', encoding='cp932')
pitcher
pitcher[pitcher['年']==2021]
# Sweetviz →未インストールの場合はスキップ sv_out = sv.analyze(pitcher) sv_out.show_html('../output/sv_report.html')
3. 機械学習の適用
# 説明変数用カラム X_columns = pitcher.columns[3:-1]
X_columns
# 説明変数
X = pitcher[X_columns].copy()
# 目的変数 y = pitcher['年俸']
X
y
# モデル学習、予測 # モデルのインスタンスの作成 model = lgb.LGBMRegressor() # Leave one out法で、モデルの学習と予測を繰り返す loo = LeaveOneOut() actual = [] pred = [] cnt = 0 for train_index, test_index in loo.split(X): # データの分割 x_train, x_test = X.iloc[train_index], X.iloc[test_index] y_train, y_test = y[train_index], y[test_index] # モデルの学習 model.fit(x_train, y_train) # モデルの学習 # テストデータの予測 y_pred = model.predict(x_test) actual.append(y_test.values[0]) pred.append(y_pred[0]) # 340人分(2021年データ)で終了 cnt+=1 if cnt == 340: break
# 散布図を描画(実値 vs 予測値) plt.plot(actual, actual, color = 'red', label = 'x=y') # 直線y = x plt.scatter(actual, pred) # 散布図のプロット plt.xlabel('y_actual') # x軸ラベル plt.ylabel('y_pred') # y軸ラベル plt.title('y_actual vs y_pred') # グラフタイトル
# モデル評価 # rmse: 平均二乗誤差の平方根 mse = mean_squared_error(actual, pred) rmse = np.sqrt(mse) print('RMSE :',rmse) # r2: 決定係数 r2 = r2_score(actual,pred) print('R2 :',r2)
4. 結果の出力
# 2021年の340人分に結合 pitcher_v2 = pitcher.head(340).copy() pitcher_v2['年俸(AI)'] = pred
pitcher_v2
# CSVファイルに出力 pitcher_v2.to_csv('../output/npb_stats_salary_pitcher_output.csv', index=False, encoding='cp932')
結果
機械学習で算出した年俸額の上位5選手(投手・野手)は、以下のとおりでした。
投手
選手名 | チーム | 背番号 | 年齢 | 年数 | 防御率 | 試合 | 勝利 | 敗北 | セーブ | ホールド | 勝率 | 打者 | 投球回 | 被安打 | 被本塁打 | 与四球 | 与死球 | 奪三振 | 失点 | 自責点 | WHIP | DIPS | 年俸 | 年俸(AI) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
田中 将大 | E | 18 | 33 | 8 | 3.01 | 23 | 4 | 9 | 0 | 0 | 0.308 | 624 | 155.2 | 131 | 17 | 29 | 5 | 126 | 54 | 52 | 1.03 | 3.58 | 90000 | 34551 |
大野 雄大 | D | 22 | 33 | 11 | 2.95 | 22 | 7 | 11 | 0 | 0 | 0.389 | 566 | 143.1 | 121 | 12 | 26 | 2 | 118 | 48 | 47 | 1.03 | 3.13 | 30000 | 31805 |
平野 佳寿 | B | 16 | 37 | 13 | 2.3 | 46 | 1 | 3 | 29 | 3 | 0.25 | 163 | 43 | 30 | 4 | 9 | 0 | 37 | 11 | 11 | 0.91 | 3.24 | 15000 | 31591 |
則本 昂大 | E | 14 | 31 | 9 | 3.17 | 23 | 11 | 5 | 0 | 0 | 0.688 | 584 | 144.2 | 123 | 18 | 35 | 3 | 152 | 56 | 51 | 1.09 | 3.42 | 30000 | 29512 |
マルティネス | H | 37 | 31 | 4 | 1.6 | 21 | 9 | 4 | 0 | 0 | 0.692 | 553 | 140.2 | 108 | 6 | 38 | 5 | 138 | 25 | 25 | 1.04 | 2.63 | 10000 | 27793 |
野手
選手名 | チーム | 背番号 | 年齢 | 年数 | 打率 | 試合 | 打席数 | 打数 | 安打 | 本塁打 | 打点 | 盗塁 | 四球 | 死球 | 三振 | 犠打 | 併殺打 | 出塁率 | 長打率 | OPS | RC27 | XR27 | 年俸 | 年俸(AI) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
鈴木 誠也 | C | 1 | 27 | 9 | 0.317 | 132 | 533 | 435 | 138 | 38 | 88 | 9 | 87 | 6 | 88 | 0 | 7 | 0.433 | 0.639 | 1.072 | 10.03 | 9.77 | 31000 | 36256 |
柳田 悠岐 | H | 9 | 33 | 11 | 0.3 | 141 | 593 | 516 | 155 | 28 | 80 | 6 | 69 | 6 | 122 | 0 | 6 | 0.388 | 0.541 | 0.929 | 7.89 | 7.59 | 61000 | 34681 |
島内 宏明 | E | 35 | 31 | 10 | 0.257 | 141 | 599 | 486 | 125 | 21 | 96 | 2 | 97 | 8 | 87 | 2 | 15 | 0.385 | 0.477 | 0.863 | 6.39 | 6.38 | 12000 | 34395 |
丸 佳浩 | G | 8 | 32 | 14 | 0.265 | 118 | 457 | 392 | 104 | 23 | 55 | 5 | 63 | 0 | 120 | 0 | 5 | 0.365 | 0.495 | 0.86 | 6.5 | 6.49 | 45000 | 33422 |
山田 哲人 | S | 1 | 29 | 11 | 0.272 | 137 | 581 | 493 | 134 | 34 | 101 | 4 | 76 | 5 | 100 | 0 | 10 | 0.37 | 0.515 | 0.885 | 6.71 | 6.84 | 50000 | 30991 |
jQuery csv2tableの使い方
今回は、csv2tableというjQueryプラグインを使って、CSVファイルをWebブラウザ上で表示する方法を紹介します。
JavaScript/jQuery
JavaScriptを使うと、Webページ上の表示をユーザの操作に合わせて動的に変えられます。さらに、jQueryというJavaScriptのライブラリを使用することで、複雑な処理を簡単に実装できます。
jQuery csv2tableの取得
以下のページからcsv2rableのjsファイルを取得します。
https://jsgt.org/lib/jquery/plugin/csv2table/v002/test.htm
「Source」を右クリックしてファイルを保存します。
HTMLの編集
csv2rableを使って、CSVファイルの読み込みと表示を行います。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="./jquery.csv2table-0.02-b-4.4.js" type="text/javascript" charset="utf-8"></script><!--jsファイルを指定--> </head> <body> <div id="view0"></div> <script> $(function(){ $('#view0').csv2table('./npb_stats_hitter_utf8.csv'); //CSVデータを指定 }); </script> </body> </html>
HTML表示
Webブラウザで表示したイメージです。
まとめ
jQueryを使うことで、自分でJavaScriptで複雑な処理を書かずに、簡単に実装できました。今後も便利なプラグインがあれば紹介していきたいと思います。
また、今回も環境の設定方法から実装までの様子を動画にまとめました。よければこちらもご確認ください。
youtu.be
本記事が皆様のお役に立てば幸いです。最後まで読んでいただき、ありがとうございました!
Webページ上でCSVデータを表示・検索するHTML/JavaScript
CSVファイルから読み込んだテーブルデータをWebページ上に表示して、テーブル内の文字を検索するための、HTML/JavaScriptを紹介します。
HTMLコード
以下の処理を行います。
- 検索ボックスで文字列を受取る
- JavaScriptを実行
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>NPB 投手成績一覧 2009年~2021年</title> </head> <body> <h1>NPB 投手成績一覧 2009年~2021年</h1> <!--検索入力--> <form action="#"> <input type="text" name="search" value="" id="search" /> </form> <!--テーブル表示--> <table border="1" id="result"> <tbody id="data_csv"></tbody> </table> <!--JavaScript--> <script type="text/javascript" src='./table_data_search.js'></script><!--JavaScriptを指定--> <script> getCsvData('http://127.0.0.1:5500/npb_stats_pitcher_utf8.csv');<!--CSVデータを指定--> </script> </body> </html>
JavaScriptコード
以下の処理を行います。
- CSVファイルの読み込み
- HTMLのテーブル形式に変換
- レコード検索機能
// レコード検索処理 window.addEventListener( "DOMContentLoaded", function(){ const search = document.forms[ 0 ].search; const table = document.querySelector( "table" ); const nohit = table.parentNode.insertBefore(document.createElement( "div" ), table.nextNode); nohit.textContent = "該当なし"; nohit.style.display = "none"; search.oninput = function(){ const key = search.value.replace( /([\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/])/g, "\\$1" ); let hit = 0; [].forEach.call( table.rows, function( row, index ){ if( index==0 ){ return false } row.style.display = [].some.call( row.cells, function( cell ){ return ( new RegExp( key ) ).test( cell.textContent ); } ) ? "table-row" : "none" ; row.style.display=="table-row" ? hit++ : hit ; } ); nohit.style.display = hit ? "none" : "block" ; } }, false ); const outputElement = document.getElementById('data_csv'); // CSVデータの読み込み function getCsvData(dataPath) { const request = new XMLHttpRequest(); request.addEventListener('load', (event) => { const response = event.target.responseText; convertArray(response); } ); request.open('GET', dataPath, true); request.send(); } // CSVデータをHTMLのテーブル形式に変換 function convertArray(data) { const dataArray = []; const dataString = data.split('\n'); for (let i = 0; i < dataString.length; i++) { dataArray[i] = dataString[i].split(','); } let insertElement = ''; dataArray.forEach( (element) => { insertElement += '<tr>'; element.forEach( (childElement) => { insertElement += `<td>${childElement}</td>` } ); insertElement += '</tr>'; } ); outputElement.innerHTML = insertElement; }
表示イメージ
検索ボックスに値を入れるとその文字列を含む行が表示されます。
年 | チーム | 背番号 | 選手名 | 防御率 | 試合 | 勝利 | 敗北 | セーブ | ホールド | 勝率 | 打者 | 投球回 | 被安打 | 被本塁打 | 与四球 | 与死球 | 奪三振 | 失点 | 自責点 | WHIP | DIPS |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2021 | 巨人 | 11 | 平内 龍太 | 14.40 | 3 | 0 | 1 | 0 | 0 | .000 | 24 | 5.0 | 8 | 2 | 2 | 0 | 5 | 8 | 8 | 2.00 | 7.52 |
2021 | 巨人 | 12 | デラロサ | 2.83 | 46 | 1 | 0 | 7 | 13 | 1.000 | 175 | 41.1 | 38 | 3 | 12 | 2 | 34 | 15 | 13 | 1.21 | 3.43 |
2021 | 巨人 | 15 | サンチェス | 4.68 | 14 | 5 | 5 | 0 | 0 | .500 | 317 | 73.0 | 82 | 13 | 23 | 3 | 54 | 40 | 38 | 1.44 | 5.02 |
2021 | 巨人 | 17 | 大竹 寛 | 3.86 | 4 | 0 | 0 | 0 | 1 | .000 | 11 | 2.1 | 5 | 0 | 0 | 0 | 0 | 1 | 1 | 2.14 | 3.12 |
2021 | 巨人 | 18 | 菅野 智之 | 3.19 | 19 | 6 | 7 | 0 | 0 | .462 | 465 | 115.2 | 90 | 15 | 25 | 7 | 102 | 41 | 41 | 0.99 | 3.85 |
2021 | 巨人 | 19 | 山崎 伊織 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 20 | 戸郷 翔征 | 4.27 | 26 | 9 | 8 | 0 | 0 | .529 | 639 | 151.2 | 130 | 19 | 58 | 6 | 138 | 75 | 72 | 1.24 | 4.16 |
2021 | 巨人 | 21 | 井納 翔一 | 14.40 | 5 | 0 | 1 | 0 | 0 | .000 | 26 | 5.0 | 11 | 3 | 1 | 0 | 5 | 8 | 8 | 2.40 | 9.52 |
2021 | 巨人 | 23 | 野上 亮磨 | 1.65 | 9 | 0 | 1 | 1 | 4 | .000 | 63 | 16.1 | 8 | 1 | 6 | 0 | 15 | 3 | 3 | 0.86 | 3.00 |
2021 | 巨人 | 26 | 今村 信貴 | 2.71 | 17 | 3 | 4 | 0 | 0 | .429 | 274 | 63.0 | 66 | 5 | 22 | 3 | 59 | 21 | 19 | 1.40 | 3.47 |
2021 | 巨人 | 30 | 鍵谷 陽平 | 3.19 | 59 | 3 | 0 | 1 | 15 | 1.000 | 182 | 42.1 | 39 | 5 | 18 | 0 | 30 | 19 | 15 | 1.35 | 4.51 |
2021 | 巨人 | 33 | 太田 龍 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 35 | 桜井 俊貴 | 5.40 | 29 | 1 | 0 | 0 | 6 | 1.000 | 145 | 33.1 | 35 | 7 | 16 | 1 | 20 | 20 | 20 | 1.53 | 6.09 |
2021 | 巨人 | 41 | 中川 皓太 | 2.47 | 58 | 4 | 3 | 1 | 25 | .571 | 220 | 54.2 | 47 | 2 | 10 | 5 | 49 | 17 | 15 | 1.04 | 2.57 |
2021 | 巨人 | 42 | メルセデス | 3.77 | 17 | 7 | 5 | 0 | 0 | .583 | 369 | 86.0 | 96 | 5 | 22 | 5 | 74 | 36 | 36 | 1.37 | 3.06 |
2021 | 巨人 | 45 | 畠 世周 | 3.07 | 52 | 4 | 3 | 1 | 11 | .571 | 390 | 96.2 | 83 | 13 | 25 | 4 | 97 | 37 | 33 | 1.12 | 3.73 |
2021 | 巨人 | 46 | 鍬原 拓也 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 47 | 髙橋 優貴 | 3.39 | 27 | 11 | 9 | 0 | 0 | .550 | 598 | 140.2 | 125 | 18 | 61 | 2 | 76 | 57 | 53 | 1.32 | 5.00 |
2021 | 巨人 | 49 | ビエイラ | 2.93 | 56 | 0 | 3 | 19 | 1 | .000 | 228 | 55.1 | 38 | 3 | 26 | 1 | 64 | 18 | 18 | 1.16 | 2.98 |
2021 | 巨人 | 50 | 戸根 千明 | 4.82 | 29 | 2 | 0 | 1 | 0 | 1.000 | 161 | 37.1 | 25 | 4 | 24 | 3 | 36 | 20 | 20 | 1.31 | 4.75 |
2021 | 巨人 | 53 | 高梨 雄平 | 3.69 | 55 | 2 | 2 | 1 | 20 | .500 | 171 | 39.0 | 34 | 2 | 21 | 5 | 47 | 17 | 16 | 1.41 | 3.07 |
2021 | 巨人 | 54 | 直江 大輔 | 4.91 | 4 | 0 | 1 | 1 | 0 | .000 | 51 | 11.0 | 15 | 1 | 4 | 2 | 4 | 6 | 6 | 1.73 | 5.21 |
2021 | 巨人 | 56 | 伊藤 優輔 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 57 | 高木 京介 | 4.42 | 15 | 1 | 0 | 0 | 1 | 1.000 | 81 | 18.1 | 22 | 1 | 7 | 0 | 15 | 9 | 9 | 1.58 | 3.34 |
2021 | 巨人 | 58 | 山本 一輝 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 59 | 田中 豊樹 | 2.84 | 39 | 0 | 0 | 0 | 2 | .000 | 152 | 38.0 | 22 | 5 | 20 | 1 | 37 | 13 | 12 | 1.11 | 4.38 |
2021 | 巨人 | 62 | 横川 凱 | 3.38 | 2 | 0 | 1 | 0 | 0 | .000 | 37 | 8.0 | 9 | 2 | 2 | 0 | 9 | 4 | 3 | 1.38 | 4.87 |
2021 | 巨人 | 63 | 古川 侑利 | 18.00 | 1 | 0 | 0 | 0 | 0 | .000 | 11 | 2.0 | 3 | 1 | 3 | 1 | 0 | 4 | 4 | 3.00 | 14.12 |
2021 | 巨人 | 64 | 大江 竜聖 | 4.09 | 47 | 0 | 0 | 0 | 13 | .000 | 145 | 33.0 | 29 | 2 | 17 | 2 | 23 | 16 | 15 | 1.39 | 4.15 |
2021 | 巨人 | 90 | 戸田 懐生 | 0.00 | 3 | 0 | 0 | 0 | 0 | .000 | 12 | 3.0 | 3 | 0 | 1 | 0 | 0 | 0 | 0 | 1.33 | 4.12 |
2021 | 巨人 | 91 | 井上 温大 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 92 | 沼田 翔平 | 7.71 | 2 | 0 | 0 | 0 | 0 | .000 | 11 | 2.1 | 3 | 1 | 1 | 0 | 3 | 2 | 2 | 1.71 | 6.12 |
2021 | 巨人 | 95 | 堀岡 隼人 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
2021 | 巨人 | 99 | 山口 俊 | 3.56 | 15 | 2 | 8 | 0 | 0 | .200 | 337 | 78.1 | 66 | 6 | 37 | 3 | 83 | 34 | 31 | 1.31 | 3.49 |
なお、 上記のデータは、以下のサイトから取得しました。
読売ジャイアンツ投手成績 - プロ野球データFreak
まとめ
JavaScriptを使うことで、Webページ上の表示をユーザの操作に合わせて動的に変えられます。このとき、別のページに遷移する場合に比べて、動作が軽いのが特徴です。実際の業務において、社内の情報共有サイトにデータを載せる際に、このような検索機能を付けたところ好評でした。
また、これからHTML/JavaScriptを使ってみたいという方に向けて、環境の設定方法から実装までの様子を動画にまとめました。よければこちらもご確認ください。
youtu.be
本記事が皆様のお役に立てば幸いです。最後まで読んでいただき、ありがとうございました!
確率分布の期待値・分散・母関数まとめ~離散分布~
今回は、統計検定1級/準1級の対策として、各確率分布の期待値・分散・母関数について整理しました。
1. 離散一様分布
2. ベルヌーイ分布
3. 二項分布
定義
確率pのベルヌーイ試行をn回行ったときに、ある事象が何回起こるかを表す確率分布のこと。 ※n=1のとき、ベルヌーイ分布になります。
確率関数
期待値
分散
確率母関数
積率母関数(モーメント母関数)
5. 超幾何分布
6. 幾何分布
定義
成功確率pのベルヌーイ試行を繰り返し実行し、初めて成功するまでの失敗回数をXとしたとき、Xは幾何分布に従う。
※初めて成功するまでの試行回数(失敗回数+1)と定義する場合もある
確率関数
期待値
分散
確率母関数
積率母関数(モーメント母関数)
7. 負の二項分布
定義
成功確率pのベルヌーイ試行を繰り返し実行し、r回成功するまでの失敗回数をXとしたとき、Xは負の二項分布に従う。※r=1のとき、幾何分布になります。
確率関数
期待値
分散
確率母関数
積率母関数(モーメント母関数)
導出方法
多くの参考書において、これらの導出は、数式の途中計算や公式等の前提知識の説明が省略されていることが多いため、理解に時間がかかると感じていました。
今回、自分用に整理したものを、動画にしてみました。途中の流れを細かく説明しています。必要に応じてご確認ください。
youtu.be
対策本
統計検定1級/準1級の対策本としては、以下の書籍があります。
こちらの書籍は、検定の範囲内のトピックが幅広く網羅されていますが、数式や解説が省略されている個所が多い印象です。あくまでも、出題範囲のトピックを確認するための用途として使用し、詳細の内容はインターネット等で確認し理解を深めるのが良いと思います。まとめ
確率分布の期待値・分散・母関数について整理しました。また、導出方法についてまとめた動画および、対策本について紹介しました。
本記事が、統計検定の対策を進める上で、お役に立てば幸いです。
Pythonでコイントスをするアプリを作ってみた|Pyinstaller
Pythonでコイントスをするアプリを作ってみましたので、画像イメージとソースコードをこちらに残します。
画面イメージ
ソースコード
cointoss.py
#!/usr/bin/env python # coding: utf-8 import os import sys import time import random title=''' ■ ■ ■ ■ ■■■■ ■ ■■■■ ■■ ■ ■■■ ■ ■■■■ ■ ■ ■■ ■■ ■ ■■ ■■ ■ ■■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■■ ■■ ■ ■ ■ ■ ■ ■ ■ ■■■■■■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■■ ■■ ■ ■■ ■■■■ ■ ■ ■ ■ ■■■■■ ■ ■■■■ ■ ■ ■ ■ ■ ■■■■ ■■■■ ■ ■ ■■■ ■■■■ ■■■■ ■■■■ ■■■■ ■■ ■ ■■ ■■ ■ ■■ ■■ ■ ■■ ■■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■■ ■■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■■ ■ ■■ ■■ ■ ■ ■ ■■ ■■ ■■ ■ ■ ■ ■ ■■■■ ■■■■ ■ ■ ■ ■■ ■■■■ ■■■■ ■■■■ ''' omote=''' ■■■■■■■■■■■■■■■ ■■■ ■■■ ■■ ■■ ■■ ■■ ■■ ■ ■■ ■ ■ ■■■■■■■■■■■■■■■■ ■ ■ ■■ ■ ■ ■■■■■■■■■■■■■■ ■ ■ ■■ ■ ■ ■■ ■ ■ ■■■■■■■■■■■■■■■■■■ ■ ■ ■■■■ ■ ■ ■■■ ■ ■■ ■ ■ ■■ ■■ ■■ ■ ■ ■■■■■ ■■■ ■ ■ ■■■ ■ ■■ ■ ■ ■ ■ ■■ ■ ■ ■■■■■■ ■■■ ■ ■ ■■■■■■ ■■■ ■ ■ ■ ■■ ■ ■■ ■■ ■■ ■■ ■■■ ■■■ ■■■■■■■■■■■■■■■ ''' ura=''' ■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■ ■■■■■■■■■■■ ■■■■■■■■■■ ■■■■■■■■■■■ ■■ ■■■ ■■■■■■■■■■■■■■■■■■■■■■■■■ ■■■■■ ■■■■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■■■■ ■■■■■■■■■■■ ■■■■■■■■■■■■ ■■■■ ■■■■■ ■■■■■■■■■■■ ■■■■■■■■■■■■ ■■■ ■■■■ ■■■■■■■■■ ■ ■■■ ■■■■■■ ■■■■■■■ ■■■ ■ ■■■■■■ ■■■ ■■■■■ ■■■■■■■■ ■■■ ■■■■ ■■ ■ ■■■■■■■ ■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■ ''' def toss(): for i in range(4): # print("コイントス中:") print(" ") print(omote) time.sleep(0.1) os.system('cls') print(" ") print(ura) time.sleep(0.1) os.system('cls') def main(): counter_total = 0 counter_omote = 0 counter_ura = 0 while(1): print(title) print('') print('Enterキーを押してコイントスを開始します。終了するときは、"q"キーを押します') x = input() os.system('cls') if x == 'q': break else: toss() counter_total += 1 result = random.randint(0, 1) if result == 0: counter_omote += 1 print("結果:") print(omote) else: counter_ura += 1 print("結果:") print(ura) print('実施回数:'+str(counter_total) + ' 表:'+str(counter_omote) + ' 裏:'+str(counter_ura)) x = input() os.system('cls') if __name__ == '__main__': main() sys.exit(0)
注意:文字をアスキーアート風に描いています。ブログ画面の表示上ズレていますが、テキストエディタにコピペすれば、以下の様にきれいに見えると思います。
exeファイルへの変換方法
Pythonプログラムをexeファイル化することで、Pythonをインストールしていない環境でも、アイコンをクリックするだけで簡単にPythonプログラムを実行できるようになります。
exeファイル化する際には、Pyinstallerを使用します。やり方は以下の動画で公開していますので、よければこちらをご確認ください。
youtu.be
また、Pyinstallerで作成したexeファイルのエラー発生時の対処方法について、別記事に公開していますので、こちらも必要に応じてご確認ください。
dskevin.hatenablog.com
Pythonでレトロ2Dゲームを作ってみた|Pyxel|ゲームプログラミング
PythonのPyxelというライブラリを使って、レトロゲームを作ってみました。
youtu.be
おにぎりくんが爆弾を避けながらビールを獲得していくというシンプルなゲームです。※おにぎりくんは、親戚の子供向けに作成した架空のキャラクターです。
Pyxel紹介
Pythonのライブラリの1つです。ゲームを作るために必要な基本的な機能が用意されているので、ゲームプログラミング初心者の方でも、短時間でゲームを実装できます。ゲーム内で使用するアイコンや音楽も自分で作ることができます。
Pyxelのサイト:
https://github.com/kitao/pyxel/blob/master/README.ja.md
ソースコード
サイトに公開されているサンプルコードを基に、見よう見まねで実装しました。オブジェクト指向プログラミングの練習にもなりました。
処理が冗長だったり、わかりづらかったりする部分はあるかと思いますが、一旦、作りたい仕様は実現できたので良しとします。
# -*- coding: utf-8 -*- from collections import deque, namedtuple from random import randint import pyxel import time Point = namedtuple("Point", ["w", "h"]) UP = Point(-16, 16) DOWN = Point(16, 16) RIGHT = Point(-16, 16) LEFT = Point(16, 16) class App: def __init__(self): pyxel.init(160, 120, caption="Onigiri-kun Loves Beer") pyxel.load("my_resource.pyxres") self.direction = RIGHT #START FLAG self.START = False #GAMEOVER FLAG self.GAMEOVER = False self.end_bgm_flg = 1 # Score self.score = 0 self.items_got = 0 self.bombs_got = 0 self.total_items = 0 # Starting Point self.player_x = 37 self.player_y = 38 self.player_vy = 0 self.item = [((i+4) * 60, randint(6, 104), True) for i in range(4)] self.sp_item = [((i+1) * 950, randint(6, 104), True) for i in range(2)] self.bomb = [((i+3) * 77, randint(6, 104), True) for i in range(3)] self.timebar = 99 pyxel.playm(0, loop=True) pyxel.run(self.update, self.draw) def update(self): if pyxel.btnp(pyxel.KEY_Q): pyxel.quit() # enter key to start if pyxel.btn(pyxel.KEY_ENTER) or pyxel.btn(pyxel.GAMEPAD_1_START): self.START = True if self.timebar < 0: self.GAMEOVER = True if self.GAMEOVER is True: pyxel.stop() if self.GAMEOVER and (pyxel.btn(pyxel.KEY_ENTER or pyxel.btn(pyxel.GAMEPAD_1_START))) : self.reset() if not self.START or self.GAMEOVER: return self.update_player() for i, v in enumerate(self.item): self.item[i] = self.update_item(*v) for i, v in enumerate(self.sp_item): self.sp_item[i] = self.update_sp_item(*v) for i, v in enumerate(self.bomb): self.bomb[i] = self.update_bomb(*v) self.timebar -= 0.2415 def update_player(self): if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD_1_LEFT): self.player_x = max(self.player_x - 4, 0) self.direction = LEFT if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.GAMEPAD_1_RIGHT): self.player_x = min(self.player_x + 4, pyxel.width - 16) self.direction = RIGHT if pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.GAMEPAD_1_UP): self.player_y = max(self.player_y - 4, 0) self.direction = UP if pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.GAMEPAD_1_DOWN): self.player_y = min(self.player_y + 4, pyxel.height - 16) self.direction = DOWN def draw(self): if self.GAMEOVER: if self.end_bgm_flg == 1: if (self.bomb[0][2] is False and abs(self.bomb[0][0] - self.player_x) < 48 and abs(self.bomb[0][1] - self.player_y) < 48) \ or (self.bomb[1][2] is False and abs(self.bomb[1][0] - self.player_x) < 48 and abs(self.bomb[1][1] - self.player_y) < 48) \ or (self.bomb[2][2] is False and abs(self.bomb[2][0] - self.player_x) < 48 and abs(self.bomb[2][1] - self.player_y) < 48): pyxel.play(3, 6) else: pyxel.play(3, 7) time.sleep(1) self.end_bgm_flg = 0 MESSAGE =\ """ FINISH PUSH ENTER RESTART """ pyxel.text(51, 40, MESSAGE, 1) pyxel.text(50, 40, MESSAGE, 7) return # bg color pyxel.cls(12) # time bar pyxel.rect( 50, 2, 90 if self.timebar > 90 else self.timebar, 4, 11 if self.timebar > 60 else 10 if self.timebar > 40 else 9 if self.timebar > 20 else 8 ) # draw item for x, y, is_active in self.item: if is_active: pyxel.blt(x, y, 0, 16, 0, 16, 16, 12) # draw special item for x, y, is_active in self.sp_item: if is_active: pyxel.blt(x, y, 0, 16, 16, 16, 16, 12) # draw bomb for x, y, is_active in self.bomb: if is_active: pyxel.blt(x, y, 0, 32, 0, 16, 16, 12) # draw player pyxel.blt( self.player_x, self.player_y, 0, 0, 16 if (self.item[0][2] is False and abs(self.item[0][0] - self.player_x) < 12 and abs(self.item[0][1] - self.player_y) < 12) or (self.item[1][2] is False and abs(self.item[1][0] - self.player_x) < 12 and abs(self.item[1][1] - self.player_y) < 12) or (self.item[2][2] is False and abs(self.item[2][0] - self.player_x) < 12 and abs(self.item[2][1] - self.player_y) < 12) or (self.item[3][2] is False and abs(self.item[3][0] - self.player_x) < 12 and abs(self.item[3][1] - self.player_y) < 12) else 32 if (self.bomb[0][2] is False and abs(self.bomb[0][0] - self.player_x) < 24 and abs(self.bomb[0][1] - self.player_y) < 24) or (self.bomb[1][2] is False and abs(self.bomb[1][0] - self.player_x) < 24 and abs(self.bomb[1][1] - self.player_y) < 24) or (self.bomb[2][2] is False and abs(self.bomb[2][0] - self.player_x) < 24 and abs(self.bomb[2][1] - self.player_y) < 24) else 48 if (self.sp_item[0][2] is False and abs(self.sp_item[0][0] - self.player_x) < 48 and abs(self.sp_item[0][1] - self.player_y) < 48) or (self.sp_item[1][2] is False and abs(self.sp_item[1][0] - self.player_x) < 48 and abs(self.sp_item[1][1] - self.player_y) < 48) else 0, self.direction[0], self.direction[1], 12, ) # print score s = "Score: {:>3}".format(self.score) pyxel.text(5, 4, s, 1) pyxel.text(4, 4, s, 7) s = "Beer: {:>1}".format(self.items_got) + " Bomb: {:>1}".format(self.bombs_got) pyxel.text(5, 110, s, 1) pyxel.text(4, 110, s, 7) if not self.START: START_TEXT1 ="PUSH ENTER KEY" START_TEXT2 =": PLAYER" START_TEXT3 =": 100 pt / time +++" START_TEXT4 =": 10 pt / time +" START_TEXT5 =": -50 pt / time ---" pyxel.text(50, 23, START_TEXT1, 1) pyxel.text(49, 23, START_TEXT1, 7) pyxel.text(56, 43, START_TEXT2, 1) pyxel.text(55, 43, START_TEXT2, 7) pyxel.blt(37, 55, 0, 16, 16, 16, 16, 5) pyxel.text(56, 61, START_TEXT3, 1) pyxel.text(55, 61, START_TEXT3, 7) pyxel.blt(37, 73, 0, 16, 0, 16, 16, 5) pyxel.text(56, 79, START_TEXT4, 1) pyxel.text(55, 79, START_TEXT4, 7) pyxel.blt(37, 91, 0, 32, 0, 16, 16, 5) pyxel.text(56, 97, START_TEXT5, 1) pyxel.text(55, 97, START_TEXT5, 7) return def update_item(self, x, y, is_active): if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12: is_active = False self.score += 10 self.items_got += 1 self.timebar += 3 self.player_vy = min(self.player_vy, -8) pyxel.play(3, 4) self.total_items += 1 if self.GAMEOVER is False: x -= 3 if x < -10: if is_active is True: self.total_items += 1 if self.timebar < 0: x = 999999999999999 else: x += 240 y = randint(6, 104) is_active = True return (x, y, is_active) def update_sp_item(self, x, y, is_active): if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12: is_active = False self.score += 100 self.timebar += 10 self.items_got += 1 self.player_vy = min(self.player_vy, -8) pyxel.play(3, 5) self.total_items += 1 if self.GAMEOVER is False: x -= 5 if x < -10: if is_active is True: self.total_items += 1 if self.timebar < 0: x = 999999999999999 else: x += 1900 y = randint(6, 104) is_active = True return (x, y, is_active) def update_bomb(self, x, y, is_active): if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12: is_active = False self.score -= 50 self.timebar -= 50 self.bombs_got += 1 self.player_vy = min(self.player_vy, -8) pyxel.play(3, 6) if self.GAMEOVER is False: x -= 2 if x < -10: if self.timebar < 0: x = 999999999999999 else: x += 231 y = randint(6, 104) is_active = True return (x, y, is_active) def reset(self): #GAMEOVER FLAG self.GAMEOVER = False # Score self.score = 0 self.items_got = 0 self.bombs_got = 0 self.total_items = 0 # Starting Point self.player_x = 42 self.player_y = 60 self.player_vy = 0 self.item = [((i+5) * 60, randint(6, 104), True) for i in range(4)] self.sp_item = [((i+1) * 950, randint(6, 104), True) for i in range(2)] self.bomb = [((i+4) * 77, randint(6, 104), True) for i in range(3)] self.timebar = 99 self.end_bgm_flg = 1 pyxel.playm(0, loop=True) App()
プログラム実行ファイル
実行ファイルは、Google ドライブ上にzipファイル形式で公開してます。以下のリンクから、zipファイルをダウンロードできます。
origiri-kun_loves_beer_v1.0.zip - Google ドライブ
ダウンロードの完了後、解凍ソフト等を使用して、zipファイルを展開します。
※Google ドライブなのでファイル改ざんの可能性は無いと思いますが、念のため、ウイルスソフトでファイルをスキャンすることをお勧めします。
exeファイルをダブルクリックすると、ゲームが起動されます。是非遊んでみてください。
まとめ
投稿が遅くなりましたが、夏休みの成果物として、プログラムのコードおよびプログラム実行ファイルを公開しました。
また、実際に作業している様子を動画にしていますので、Pyxelを使って実際にゲームを作ってみたいという方の参考になれば幸いです。
youtu.be
最後まで読んでいただき、ありがとうございました。
【Python/Pandas】Webページ上のデータを読み込んで、動く棒グラフを作成する方法|AKB48 シングル選抜回数
こんにちは。今回は、Web上に公開されているAKB48歴代シングルにおける選抜メンバーの一覧表を読み込んで、メンバーごとの選抜回数の推移を集計し、動く棒グラフ(Bar Chart Race)で可視化したいと思います。
AKB48のディスコグラフィー/選抜メンバーの一覧 - エケペディア
事前準備
今回も、ツールはPythonを使用します。Pythonのセットアップ方法は過去の記事を参照ください。
データサイエンティストを目指す方向け Pythonセットアップ方法(Windows版) - Kevin's Data Analytics Blog
また、Pythonを使って動く棒グラフを作成するためには、PCにいくつかのソフトウェアをインストールする必要があります。こちらも手順については、過去の記事でまとめていますので、併せてご参照ください。
Pythonで動く棒グラフを作成する方法|Bar Chart Race - Kevin's Data Analytics Blog
Webページ上のデータの読み込み
Pythonの実行環境を開いたら、まず、Pandasライブラリをインポートします。
import pandas as pd
Pandasライブラリが用意しているpd.io.html.read_html()という関数を使用すると、指定したURLから表形式のデータだけを抜き出してくれます。
target_url = 'https://48pedia.org/AKB48%E3%81%AE%E3%83%87%E3%82%A3%E3%82%B9%E3%82%B3%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%BC/%E9%81%B8%E6%8A%9C%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E3%81%AE%E4%B8%80%E8%A6%A7'
tables = pd.io.html.read_html(target_url)
実行すると、tablesという変数に、URL内の表データがリスト形式で保存されるので、1番目(リストの番号は0始まりなので0)を指定して、dfという変数に格納します。
df = tables[0]
変数名だけで実行すると、変数の中身をみることができます。
df
データの読み込みが出来ました。
データの加工
動く棒グラフを書くためにデータを加工します。
不要な列の削除
del 関数を使用して、列を削除します。今回は、一番左の「所属」列と一番右の「選抜回数」列は使用しないので、それぞれ、列番号を指定して削除します。
del df[df.columns[61]] del df[df.columns[0]]
不要な行の削除
「名前」もしくは「定員」から始まる行を削除します。
df = df[df[df.columns[0]]!='名前'] df = df[df[df.columns[0]]!='定員']
列名の加工
列名が3階層(発売年、枚数、曲名)になっているため、1階層にまとめます。また、元のサイトでは、曲名は縦書きですが、これを横書きにするために、記号文字を変換します。replace関数を使って、「|」→「ー」、「 `」→「、」とします。
df.columns = [df.columns[i][0]+'年\n'+df.columns[i][1]+'. '+df.columns[i][2] for i in range(len(df.columns))] df.columns = [df.columns[i].replace('|','ー').replace(' `','、') for i in range(len(df.columns))]
データ値の変換
欠損値は選抜されなかったことを表すため「0」に、「●」と「★」は選抜されたことを表すため「1」に変換します。欠損値の置き換えは、fillna関数を使用します。
df = df.fillna(0).replace('●',1).replace('★',1)
行と列の入れ替え
元のデータは、左から右の順番で古いシングルから新しいシングルのデータが格納されています。動く棒グラフを作成するために、上から下に時系列が進むように行と列を入れ替えます。このような操作を行列の転置といい、データの変数に「.T」をつけて実行すると転置されます。また、転置前の行インデックスが転置後の列名になりますので、set_index関数を使用して、転置前に1列目のメンバーの名前をインデックスに指定します。
df = df.set_index(df.columns[0]).T
ここまでのデータ加工のイメージは以下のとおりです。
累積和の計算
最後に、cumsum関数を使用して、列ごとに縦方向に累積和を計算します。これにより各データの値は、該当行のシングル曲の時点における、それまでの合計の選抜回数を表します。
これで、動く棒グラフを作成するためのデータ準備ができました。
動く棒グラフの作成
加工後のデータを利用して、動く棒グラフを作成します。
今回はデータに日本語を含むので、japanize_matplotlibというPythonライブラリをインストールして、Pythonのグラフ描画機能を日本語データに対応させる必要があります。もし、まだインストールしていない場合は、コマンドプロンプトから以下のコマンドを実行します。
pip install japanize_matplotlib
Pythonの実行環境に戻って、ライブラリをインポートします。
import bar_chart_race as bcr import japanize_matplotlib
bar_chart_race関数に、先ほど加工したデータ「df」を指定して、動く棒グラフを作成します。パラメータの値はお好みで調整ください。
bcr.bar_chart_race( df=df ,fixed_max=True ,title='AKB48 歴代メンバー シングル選抜回数 ランキング推移 2006.02-2021.06 ' ,n_bars=27 ,bar_size=.8 ,period_label={'x': .975, 'y': .075, 'ha': 'right', 'va': 'center', 'size': 13} ,filter_column_colors=True )
グラフのカスタマイズ
スマホ表示用に動画を縦長にする
fig_kwargsというパラメタで動画サイズを指定します。スマホ用の動画は、横と縦の比率が9:16が適していますので、文字サイズとのバランスを考慮して、それぞれ1/2にして4.5:8とします。
bcr.bar_chart_race( df=df ,fixed_max=True ,title='AKB48 歴代メンバー シングル選抜回数 ランキング推移 2006.02-2021.06 ' ,n_bars=27 ,bar_size=.8 ,period_label={'x': .975, 'y': .05, 'ha': 'right', 'va': 'center', 'size': 11} ,fig_kwargs={'figsize': (4.5, 8), 'dpi': 72} ,filter_column_colors=True )
棒の色を指定する
例として、メンバーの所属チームに応じて、棒の色を変えてみたいと思います。
まずは、前準備として、Excelファイルに各メンバーの所属チームをまとめます。複数所属している場合は、代表的なチームを選んでいます。なお、メンバーの並び順をウェブから取得したテーブルとExcelファイルで一致させる必要がある点にご注意ください。
pd.read_excel関数を使用して、こちらのExcelファイルを読み込みます。
member_team=pd.read_excel('../data/member_team.xlsx')
「color」という列を追加して、「team」列の値に応じて色を指定します。replace関数を使用して、'A'などのチーム名を'red'などの色の名称に変えていきます。
member_team['color']=member_team['team'].replace('A','red').replace('K','lime').replace('B','aqua').replace('SKE','orange').replace('NMB','gold').replace('HKT','dimgrey').replace(4,'skyblue') # チーム不明のメンバーはwhiteで補完 member_team['color']=member_team['color'].fillna('white') member_team
filter_column_colorsをFalseにして、colorsに先ほど作成したデータの「color」列の値を指定します。
bcr.bar_chart_race( df=df ,fixed_max=True ,title='AKB48 歴代メンバー シングル選抜回数 2006.2-2021.6 ' ,n_bars=27 ,bar_size=.8 ,period_label={'x': .975, 'y': .05, 'ha': 'right', 'va': 'center', 'size': 11} ,fig_kwargs={'figsize': (4.5, 8), 'dpi': 72} ,filter_column_colors=False ,colors=member_team['color'] )