天鳳個室の自動成績管理アプリをLINE botとPythonで作る　その③

# はじめに
今回は以下の投稿で作成した天鳳の成績自動管理アプリに__Rating機能__を追加したお話です．

- [天鳳個室の自動成績管理アプリをLINE botとPythonで作る その①](https://qiita.com/all_green/items/4c5f4bc1a2044b00d2f8)
- [天鳳個室の自動成績管理アプリをLINE botとPythonで作る その②](https://qiita.com/all_green/items/7d33ad7472a7a3d4aba6)

# Rating計算式
Ratingは以下の天鳳形式を採用します．天鳳では成績の完全順位制を踏まえてRatingも完全順位制になっています．私達の個室では比較的順位点のWeightが高いルールを採用しているので，天鳳式のRatingを採用しました．　　

- [天鳳公式マニュアル](https://tenhou.net/man/)　　

（以下一部抜粋）
> - 順位点(10-30)に同卓プレーヤの平均による補正を行ったもの
> - 初期値=R1500
> - 卓の平均Rが高いほど大きく上昇します  
> (Rateの変動) = (試合数補正) x ( 対戦結果 + 補正値 ) x (スケーリング係数)
> 試合数補正(400試合未満): 1 - 試合数 x 0.002
> 試合数補正(400試合以上): 0.2
> 対戦結果(段位戦3人打ち): 1位+30 2位0 3位-30  
> 補正値: ( 卓の平均R - 自分のR ) / 40  
> スケーリング係数(段位戦):1.0  

もしあなたの個室では順位点より素点の方を重視するルールならば，他のRating計算式を重視したほうが，成績との相関が高いかもしれません．以下のサイトではMリーガーのRatingを3種類の計算式で算出しているので，参考にしてみてください．

- [Mリーガーのレート・ランキングを天鳳・MaruJan・MJの3パターンで算出してみた！
](https://crittoo96.hatenablog.jp/entry/2019/03/17/205115)

# 実装

## プリアンブル
```python
import re
import matplotlib.pyplot as plt
```

## Rating/Rの初期化
`initial_file.txt`内の初期データ（Player名:player，初期R:init_rating，初期打荘数:init_games）によってレーティングデータを初期化します．基本はR=1500，games=0．

```python:calc_rating.py

def initialize_rating(initial_file):
    
    with open(initial_file) as f:
        init_ratings = f.readlines() 

    rating = {}
    games  = {}
    rating_history = {}
    
    for l in init_ratings:
        player      = l.split()[0]
        init_rating = l.split()[1] 
        init_games  = l.split()[2]
        rating[player] = float(init_rating) 
        games[player]  = int(init_games)
        rating_history[player] = [float(init_rating)]
        print(games)
    
    return rating,games,rating_history
```

```plaintext:initial_file.txt
Aさん 1500 0
Bさん 1500 0
Cさん 1500 0
Dさん 1500 0
```

## Rの計算
* ログファイルの対戦結果からR変動値を求め，初期RにR変動値を足していく．
* 引数は以下の５つ
    * 初期R
    * 初期Game数
    * それまでのR履歴
    * ログファイルのパス
    * チップの有無
* 返り値は以下の３つ
    * 最終R
    * 最終Game数
    * R履歴    
* 例えばログファイルが複数ある場合はこの関数を複数回実行すれば通算のRが計算できる．

```python  
def calc_rating(initial_rating,initial_games,initial_rating_history,logfile,tip=False):
    with open(logfile) as f:
        lines = f.readlines() # 1行毎にファイル終端まで全て読む(改行文字も含まれる)

    rating = initial_rating
    games  = initial_games
    rating_history = initial_rating_history

    for line in lines[1:]:
        # print(games)

        if len(line) > 10: #変な行は飛ばす
         
            roomid  = line.split("|")[0]
            time    = line.split("|")[1]
            rools   = line.split("|")[2]
            players = line.split("|")[3]
            
　　　　　　　# 祝儀なしの場合
            if tip == False:
                l = re.split('[ ()]', players)
                player1 = l[1]
                player2 = l[4]
                player3 = l[7]
                
            # 祝儀ありの場合
            if tip == True:
                l = re.split('[ (,)]', players)
                player1 = l[1]
                player2 = l[5]
                player3 = l[9]

            rate_average = (rating[player1]+rating[player2]+rating[player3])/3.0
            rate_average = round(rate_average,3)
            if rate_average < 1500.0:
                rate_average = 1500.0

            # 試合数補正
            if games[player1] < 400:
                games_correction1 = 1.0 - games[player1]*0.002
            if games[player1] >= 400:
                games_correction1 = 0.2
            if games[player2] < 400:
                games_correction2 = 1.0 - games[player2]*0.002
            if games[player2] >= 400:
                games_correction2 = 0.2
            if games[player3] < 400:
                games_correction3 = 1.0 - games[player3]*0.002
            if games[player3] >= 400:
                games_correction3 = 0.2

            # 平均R補正
            averageR_correction1 = (rate_average - rating[player1])/40.0
            averageR_correction2 = (rate_average - rating[player2])/40.0
            averageR_correction3 = (rate_average - rating[player3])/40.0

            # Rating変動
            rate_delta1 = round(games_correction1 * ( 30.0  + averageR_correction1 ) * 1.0, 3) # 1st
            rate_delta2 = round(games_correction2 * ( 0.0   + averageR_correction2 ) * 1.0, 3) # 2nd
            rate_delta3 = round(games_correction3 * ( -30.0 + averageR_correction3 ) * 1.0, 3) # 3rd
            print(rate_delta1,rate_delta2,rate_delta3)

            # Rating
            rating[player1] += rate_delta1
            rating[player2] += rate_delta2
            rating[player3] += rate_delta3

            # Rating History
            rating_history[player1].append(rating[player1]) 
            rating_history[player2].append(rating[player2]) 
            rating_history[player3].append(rating[player3])             

            # Games
            games[player1] += 1
            games[player2] += 1
            games[player3] += 1

    return rating,games,rating_history
```
## R変動のグラフ化
* 折れ線(plot)と最終Rの1点プロット(scatter)，最終R値の表示を行っています．

```python
def rating_plot(rating_history):

    plt.clf()
    names = {"Aさん":"a","Bさん":"b","Cさん":"c","Dさん":"d"}
    for player in rating_history.keys():
        x = [i for i in range(len(rating_history[player]))]
        y = rating_history[player]
        plt.plot(x,y,linewidth=0.5,alpha=0.5)
        plt.scatter(x[-1],y[-1],label=names[player])
        plt.text(x[-1],y[-1]+5,int(y[-1]))

    plt.legend()
    plt.savefig("rating.png")
```

## 実行（ローカル環境でのテスト）
```python  
if __name__ == "__main__":
    r,g,h = initialize_rating("rating.txt")
    r,g,h = calc_rating(r,g,h,"logvol1.txt",tip=False)
    r,g,h = calc_rating(r,g,h,"logvol2.txt",tip=True)
    r,g,h = calc_rating(r,g,h,"logvol3.txt",tip=True)

    rating_plot(h)
```

## 出力結果

* 私は9人で麻雀しているので9本のグラフが表示されています．
* 平均Rが1500以下の場合は切り上げているので平均的には1500より大きくなっています．
* Game数が増えていくとRの変動は小さくなっていきます（400戦目以降は変わらない）．
![出力結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/160678/16facb48-6480-98df-04ca-4a669b898bf0.png)

##　LINE bot への実装
* 先程のプログラムを`rating`フォルダに入れて，line botのプログラムから呼び出します．
* 前回のつづきで`postback`アクションがあったときにratingを計算してグラフを送信します．
* 一回計算したratingを残しておくようにすれば毎回計算しなくて済みそうですが，DBとか使うことになりそうなのでとりあえず毎回計算することにしました．
* シーズン3までログファイルがあるのでその通算Rを計算しています．
* これで毎日最新（前日までの分）のRをいつでもLINEから見られるようになりました．

```python:tenhoulinebot.py

(中略)
    import download4
    import rating.calc_rating as cr
(中略)
    elif postbackdata == "request_rating":
        download4.download("/logvol1.txt","rating/logvol1.txt")
        download4.download("/logvol2.txt","rating/logvol2.txt")
        download4.download("/logvol3.txt","rating/logvol3.txt")
        
        initial_rating,initial_games,initial_rating_history = cr.initialize_rating("rating/rating.txt")
        r,g,h = cr.calc_rating(initial_rating,initial_games,initial_rating_history,"rating/logvol1.txt",tip=False)
        r,g,h = cr.calc_rating(r,g,h,"rating/logvol2.txt",tip=True)
        r,g,h = cr.calc_rating(r,g,h,"rating/logvol3.txt",tip=True)
        cr.rating_plot(h)

        bucket.upload_file("rating.png", "rating.png")
        s3_image_url = s3_client.generate_presigned_url(
            ClientMethod = 'get_object',
            Params       = {'Bucket': aws_s3_bucket, 'Key': "rating.png"},
            ExpiresIn    = 600,
            HttpMethod   = 'GET'
        )

        line_bot_api.reply_message(
            event.reply_token,
            ImageSendMessage(
                original_content_url = s3_image_url,
                preview_image_url    = s3_image_url,
            )
        )
        download4.upload("rating.png","/rating.png")  


```

## P.S.

今季の麻雀はすごく調子がいいです．一人勝ち状態です．このまま優勝したいと思います．　　
天鳳の牌譜解析もやってみたいけど，mjlog形式（xml)が難しいです．．．誰か教えてください．．．