Kevin's Data Analytics Blog

データサイエンティスト、AIエンジニアを目指す方に向けて情報発信していきます。

[機械学習 実践]プロ野球選手の年俸を査定するAIを作成|LightGBM|Machine Learning|Python

機械学習の実践例として、プロ野球選手の成績と年俸のデータを使って、機械学習モデルの作成・予測実行してみました。

イメージは以下のとおりです。
f:id:dskevin:20220116104836p:plain:h320
f:id:dskevin:20220116104850p:plain:h320
f:id:dskevin:20220116104905p:plain:h320

準備

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選手(投手・野手)は、以下のとおりでした。

投手

選手名チーム背番号年齢年数防御率試合勝利敗北セーブホールド勝率打者投球回被安打本塁打与四球死球奪三振失点自責点WHIPDIPS年俸年俸(AI)
田中 将大E183383.012349000.308624155.21311729512654521.033.589000034551
大野 雄大D2233112.9522711000.389566143.11211226211848471.033.133000031805
平野 佳寿B1637132.346132930.2516343304903711110.913.241500031591
則本 昂大E143193.1723115000.688584144.21231835315256511.093.423000029512
マルティネスH373141.62194000.692553140.2108638513825251.042.631000027793

野手

選手名チーム背番号年齢年数打率試合打席数打数安打本塁打打点盗塁四球死球三振犠打併殺打出塁率長打率OPSRC27XR27年俸年俸(AI)
鈴木 誠也C12790.3171325334351383888987688070.4330.6391.07210.039.773100036256
柳田 悠岐H933110.314159351615528806696122060.3880.5410.9297.897.596100034681
島内 宏明E3531100.25714159948612521962978872150.3850.4770.8636.396.381200034395
丸 佳浩G832140.26511845739210423555630120050.3650.4950.866.56.494500033422
山田 哲人S129110.2721375814931343410147651000100.370.5150.8856.716.845000030991
※実際には、様々な要素が年俸に反映されていると思いますので、こちらの結果自体は参考値として捉えていただけると助かります。

まとめ

プロ野球選手の成績・年俸を題材として機械学習の使い方を紹介しました。Pythonで用意されているライブラリ関数を使うことで、複雑なプログラミング無しで機械学習を使った処理が実装できると感じていただけたと思います。今回扱ったLightGBM以外にも、たくさんの機械学習アルゴリズムPythonで用意されていますが、使い方は同様です。今後また紹介していきたいと思います。

今回の内容を動画でも紹介していますので、もしよければこちらもご覧ください。
youtu.be

本記事が皆様のお役に立てば幸いです。最後まで読んでいただき、ありがとうございました!

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」を右クリックしてファイルを保存します。
f:id:dskevin:20211220174226p:plain:h320

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ブラウザで表示したイメージです。
f:id:dskevin:20211220175305p:plain:h320

まとめ

jQueryを使うことで、自分でJavaScriptで複雑な処理を書かずに、簡単に実装できました。今後も便利なプラグインがあれば紹介していきたいと思います。
また、今回も環境の設定方法から実装までの様子を動画にまとめました。よければこちらもご確認ください。
youtu.be

本記事が皆様のお役に立てば幸いです。最後まで読んでいただき、ありがとうございました!

Webページ上でCSVデータを表示・検索するHTML/JavaScript

CSVファイルから読み込んだテーブルデータをWebページ上に表示して、テーブル内の文字を検索するための、HTML/JavaScriptを紹介します。
f:id:dskevin:20211128172158p:plain:h160

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;
}

 

表示イメージ

検索ボックスに値を入れるとその文字列を含む行が表示されます。




チーム背番号選手名防御率試合勝利敗北セーブホールド勝率打者投球回被安打本塁打与四球死球奪三振失点自責点WHIPDIPS
2021巨人11平内 龍太14.4030100.000245.082205882.007.52
2021巨人12デラロサ2.8346107131.00017541.13831223415131.213.43
2021巨人15サンチェス4.68145500.50031773.082132335440381.445.02
2021巨人17大竹 寛3.8640001.000112.150000112.143.12
2021巨人18菅野 智之3.19196700.462465115.2901525710241410.993.85
2021巨人19山崎 伊織------------------
2021巨人20戸郷 翔征4.27269800.529639151.21301958613875721.244.16
2021巨人21井納 翔一14.4050100.000265.0113105882.409.52
2021巨人23野上 亮磨1.6590114.0006316.1816015330.863.00
2021巨人26今村 信貴2.71173400.42927463.06652235921191.403.47
2021巨人30鍵谷 陽平3.1959301151.00018242.13951803019151.354.51
2021巨人33太田 龍------------------
2021巨人35桜井 俊貴5.402910061.00014533.13571612020201.536.09
2021巨人41中川 皓太2.475843125.57122054.24721054917151.042.57
2021巨人42メルセデス3.77177500.58336986.09652257436361.373.06
2021巨人45畠 世周3.075243111.57139096.283132549737331.123.73
2021巨人46鍬原 拓也------------------
2021巨人47髙橋 優貴3.392711900.550598140.2125186127657531.325.00
2021巨人49ビエイラ2.935603191.00022855.13832616418181.162.98
2021巨人50戸根 千明4.822920101.00016137.12542433620201.314.75
2021巨人53高梨 雄平3.695522120.50017139.03422154717161.413.07
2021巨人54直江 大輔4.9140110.0005111.0151424661.735.21
2021巨人56伊藤 優輔------------------
2021巨人57高木 京介4.421510011.0008118.12217015991.583.34
2021巨人58山本 一輝------------------
2021巨人59田中 豊樹2.84390002.00015238.02252013713121.114.38
2021巨人62横川 凱3.3820100.000378.092209431.384.87
2021巨人63古川 侑利18.0010000.000112.031310443.0014.12
2021巨人64大江 竜聖4.094700013.00014533.02921722316151.394.15
2021巨人90戸田 懐生0.0030000.000123.030100001.334.12
2021巨人91井上 温大------------------
2021巨人92沼田 翔平7.7120000.000112.131103221.716.12
2021巨人95堀岡 隼人------------------
2021巨人99山口 俊3.56152800.20033778.16663738334311.313.49


 
なお、 上記のデータは、以下のサイトから取得しました。
読売ジャイアンツ投手成績 - プロ野球データFreak

まとめ

JavaScriptを使うことで、Webページ上の表示をユーザの操作に合わせて動的に変えられます。このとき、別のページに遷移する場合に比べて、動作が軽いのが特徴です。実際の業務において、社内の情報共有サイトにデータを載せる際に、このような検索機能を付けたところ好評でした。

また、これからHTML/JavaScriptを使ってみたいという方に向けて、環境の設定方法から実装までの様子を動画にまとめました。よければこちらもご確認ください。
youtu.be


本記事が皆様のお役に立てば幸いです。最後まで読んでいただき、ありがとうございました!

確率分布の期待値・分散・母関数まとめ~離散分布~

今回は、統計検定1級/準1級の対策として、各確率分布の期待値・分散・母関数について整理しました。

1. 離散一様分布

定義

離散型の確率変数 𝑋 について、全ての事象の起こる確率が等しい分布のこと

確率関数

 P(X=x)=\dfrac{1}{n}

期待値

 E(X)=\dfrac{n+1}{2}

分散

 V(X)=\dfrac{n^{2}-1}{12}

確率母関数

 E(t^{X})=\dfrac{t(1-t^{n})}{n(1-t)}

積率母関数(モーメント母関数)

 E(t^{tX})=\dfrac{e^{t}(1-e^{nt})}{n(1-e^{t})}

2. ベルヌーイ分布

定義

確率変数が 2 つの値(0,1)しか取らない離散確率分布のこと。確率 𝑝 で 1 となり、確率 1−𝑝 で 0 となる。

確率関数

 P(X=x)=p^{x}(1-p)^{1-x}

期待値

 E(X)=p

分散

 V(X)=p(1-p)

確率母関数

 E(t^{X})=1-p+tp

積率母関数(モーメント母関数)

 E(t^{tX})=1-p+pe^{t}

3. 二項分布

定義

確率pのベルヌーイ試行をn回行ったときに、ある事象が何回起こるかを表す確率分布のこと。 ※n=1のとき、ベルヌーイ分布になります。

確率関数

 P(X=x)={}_n C_x p^{x} (1-p)^{n-x}

期待値

 E(X)=np

分散

 V(X)=np(1-p)

確率母関数

 E(t^{X})=(pt+1-p)^{n}

積率母関数(モーメント母関数)

 E(t^{tX})=(pe^{t}+1-p)^{n}

4. ポアソン分布

定義

単位時間あたりに平均 λ 回起こる事象が、単位時間あたりに 何回起こるかを表す確率分布のこと

確率関数

 P(X=x)=e^{-\lambda} \dfrac{\lambda ^x}{x!}

期待値

 E(X)=\lambda

分散

 V(X)=\lambda

確率母関数

 E(t^{X})=e^{\lambda(t-1)}

積率母関数(モーメント母関数)

 E(t^{tX})=e^{\lambda(e^{t}-1)}

5. 超幾何分布

定義

成功状態をもつ母集団から非復元抽出したときに成功状態がいくつあるかという確率を与える離散確率分布の一種である。(引用:Wikipedia

確率関数

 P(X=x)= \dfrac{{}_M C_x \cdot {}_{N-M} C_{n-x}} {{}_N C_n}

期待値

 E(X)= n\dfrac{M}{N}

分散

 V(X)= n\dfrac{M}{N}\biggl(\dfrac{(N-n)(N-M)}{N(N-1)}\biggr)

確率母関数

簡単な式で表せない

積率母関数(モーメント母関数)

簡単な式で表せない

6. 幾何分布

定義

成功確率pのベルヌーイ試行を繰り返し実行し、初めて成功するまでの失敗回数をXとしたとき、Xは幾何分布に従う。
※初めて成功するまでの試行回数(失敗回数+1)と定義する場合もある

確率関数

 P(X=x)=p(1-p)^{x}

期待値

 E(X)=\dfrac{1-p}{p}

分散

 V(X)=\dfrac{1-p}{p^{2}}

確率母関数

 E(t^{X})=\dfrac{p}{1-t(1-p)}

積率母関数(モーメント母関数)

 E(t^{tX})=\dfrac{p}{1-e^{t}(1-p)}

7. 負の二項分布

定義

成功確率pのベルヌーイ試行を繰り返し実行し、r回成功するまでの失敗回数をXとしたとき、Xは負の二項分布に従う。※r=1のとき、幾何分布になります。

確率関数

 P(X=x)={}_{x+r-1} C_x p^{r}(1-p)^{x}

期待値

 E(X)=\dfrac{r(1-p)}{p}

分散

 V(X)=\dfrac{r(1-p)}{p^{2}}

確率母関数

 E(t^{X})=\biggl(\dfrac{p}{1-t(1-p)}\biggr) ^{r}

積率母関数(モーメント母関数)

 E(t^{tX})=\biggl(\dfrac{p}{1-e^{t}(1-p)} \biggr) ^{r}

導出方法

多くの参考書において、これらの導出は、数式の途中計算や公式等の前提知識の説明が省略されていることが多いため、理解に時間がかかると感じていました。
今回、自分用に整理したものを、動画にしてみました。途中の流れを細かく説明しています。必要に応じてご確認ください。
youtu.be

対策本

統計検定1級/準1級の対策本としては、以下の書籍があります。

こちらの書籍は、検定の範囲内のトピックが幅広く網羅されていますが、数式や解説が省略されている個所が多い印象です。あくまでも、出題範囲のトピックを確認するための用途として使用し、詳細の内容はインターネット等で確認し理解を深めるのが良いと思います。

まとめ

確率分布の期待値・分散・母関数について整理しました。また、導出方法についてまとめた動画および、対策本について紹介しました。
本記事が、統計検定の対策を進める上で、お役に立てば幸いです。

Pythonでコイントスをするアプリを作ってみた|Pyinstaller

Pythonコイントスをするアプリを作ってみましたので、画像イメージとソースコードをこちらに残します。

画面イメージ

f:id:dskevin:20210929215023p:plain
f:id:dskevin:20210929215044p:plain
f:id:dskevin:20210929215058p:plain

ソースコード

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)

注意:文字をアスキーアート風に描いています。ブログ画面の表示上ズレていますが、テキストエディタにコピペすれば、以下の様にきれいに見えると思います。

f:id:dskevin:20210929222026p:plain

実行方法

Pythonがインストール済の環境であれば、コマンドプロンプトなどの実行環境から以下のように実行できます。

python cointoss.py

exeファイルへの変換方法

Pythonプログラムをexeファイル化することで、Pythonをインストールしていない環境でも、アイコンをクリックするだけで簡単にPythonプログラムを実行できるようになります。
f:id:dskevin:20210929222314p:plain

exeファイル化する際には、Pyinstallerを使用します。やり方は以下の動画で公開していますので、よければこちらをご確認ください。
youtu.be

また、Pyinstallerで作成したexeファイルのエラー発生時の対処方法について、別記事に公開していますので、こちらも必要に応じてご確認ください。
dskevin.hatenablog.com

まとめ

コイントスをするPythonプログラムのソースコードを公開しました。
本記事が、Pythonを使ったプログラミングを学習する際の参考になれば幸いです。
最後まで読んでいただき、ありがとうございました。

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)で可視化したいと思います。

f:id:dskevin:20210626161830p:plain:h320
AKB48のディスコグラフィー/選抜メンバーの一覧 - エケペディア

youtu.be

事前準備

今回も、ツールは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

f:id:dskevin:20210626170646p:plain

データの読み込みが出来ました。

データの加工

動く棒グラフを書くためにデータを加工します。

不要な列の削除

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

  
ここまでのデータ加工のイメージは以下のとおりです。
f:id:dskevin:20210627155113p:plain

累積和の計算

最後に、cumsum関数を使用して、列ごとに縦方向に累積和を計算します。これにより各データの値は、該当行のシングル曲の時点における、それまでの合計の選抜回数を表します。

f:id:dskevin:20210627160133p:plain

これで、動く棒グラフを作成するためのデータ準備ができました。

動く棒グラフの作成

加工後のデータを利用して、動く棒グラフを作成します。
 
今回はデータに日本語を含むので、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
)

f:id:dskevin:20210627162927p:plain

グラフのカスタマイズ

スマホ表示用に動画を縦長にする

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
)

f:id:dskevin:20210627171556p:plain

棒の色を指定する

例として、メンバーの所属チームに応じて、棒の色を変えてみたいと思います。
まずは、前準備として、Excelファイルに各メンバーの所属チームをまとめます。複数所属している場合は、代表的なチームを選んでいます。なお、メンバーの並び順をウェブから取得したテーブルとExcelファイルで一致させる必要がある点にご注意ください。
f:id:dskevin:20210627164352p:plain:h320

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

f:id:dskevin:20210627165435p:plain:h320

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']
)

f:id:dskevin:20210627171845p:plain

まとめ

Webページからデータ取得、データ加工、動く棒グラフの作成、という一連のプロセスを全てPythonで実装できました。このように、様々な処理を実装できる事がPythonの大きな魅力の1つです。今回の記事が皆様のPython活用の参考になれば幸いです。

今回も、記事の内容を実演した様子を動画にしてアップしています。文章で分かりづらい点は、こちらの動画もあわせてご確認ください。
youtu.be

最後まで読んでいただき、ありがとうございました。