オンライン家族麻雀をPythonを使って解析してみる(PART 1: DATAを取る)

#概要

最近家族とオンライン麻雀サイト[天鳳](http://tenhou.net/)で週に1回2時間ほど楽しんでいます。

今回は対戦成績の分析を家族が皆がウェブ上で見れるようにしたのでその解説です。

プロセスとしては：

1. Pythonで分析
1. Dashを使って可視化
1. HerokuとGithubを使ってデブロイ


こんな感じに仕上がりました↓
https://drmahjong.herokuapp.com/

![drmahjangsc1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666405/bfbda243-ea64-0c9c-2f5f-9218e311fa36.png)
![drmahjangsc2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666405/c5abdf6e-d727-9d23-fbc4-f5b42a99f678.png)
![drmahjangsc3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666405/9b68795f-2331-9cbf-6ccb-80af966c3867.png)

Codeは以下のGithubで見ることができます。
https://github.com/mottoki/mahjan-score

#データを取る
天鳳のデータは[ログ](https://tenhou.net/sc/raw)から取ることができます。requestというModuleを使用してデータを取ります。

```python:python.py
import requests
import datetime

new_date = datetime.datetime.now().strftime('%Y%m%d')
url = "https://tenhou.net/sc/raw/dat/"+f"sca{new_date}.log.gz"
filename = f"sca{new_date}.log.gz"

# Download gz file from the url
with open(filename, "wb") as f:
    r = requests.get(url)
    f.write(r.content)

```

#PythonとPandasを使ってデータ処理

Rawデータをプレイヤーの名前でフィルターをかけて、splitを使用してプレイヤーの名前とポイントだけのデータフレームを摘出します。

```python:new_data.py
import os
import pickle
import pandas as pd

# プレイヤーの名前
playercol = ['date', 'Mirataro', 'Shinwan', 'ToShiroh', 'yukoron']

# Pandasのデータフレームに変換
df = pd.read_csv(filename, usecols=[0], error_bad_lines=False, header=None)
df[len(df.columns)] = new_date

# プレイヤーの名前でフィルターをかける
df = df[(df[0].str.contains(playercol[1])) & 
    (df[0].str.contains(playercol[2])) & 
    (df[0].str.contains(playercol[3])) &
    (df[0].str.contains(playercol[4]))]

# データフレームの処理を行う
df[['one','two','three','four']] = df[0].str.split('|', 3, expand=True)
df.columns = ['original', 'date', 'room', 'time', 'type', 'name1']
df['date'] = pd.to_datetime(df['date'], format='%Y%m%d')
df[['empty', 'n1', 'n2', 'n3', 'n4']] = df.name1.str.split(" ", n=4, expand=True)
# 重要なコラムだけ使用
df = df[['date', 'n1', 'n2', 'n3', 'n4']]

# スコアと名前についているカギカッコを取り去りデータフレーム化する
new_score = pd.DataFrame(columns=playercol)
k=0
for i, j in df.iterrows():
   dd = j[0]
   new_score.loc[k, 'date'] = dd
   for name in df.columns[1:]:
       s = j[name]
       player = s.split('(')[0]
       score = [p.split(')')[0] for p in s.split('(') if ')' in p][0]
       score = int(float(score.replace('+', '')))
       new_score.loc[k, player] = score
   k += 1

# 古いデータをPickleから呼ぶ
current_dir = os.getcwd()
old_score = pd.read_pickle(f"{current_dir}/players_score.pkl")

# 新しいデータと古いデータを合わせる
concat_score = pd.concat([old_score, new_score], ignore_index=True)
concat_score.to_pickle(f"{current_dir}/players_score.pkl")

```

#Dashを使って可視化する
Dashというライブラリを使って素早くデータの可視化を行います。

Dashのチュートリアルが一番わかりやすいです。（参考：[Dash Documentation & User Guide](https://dash.plotly.com/))

Dashで引っかかるところのはCallbackという機能ですが、[Pythonの可視化ライブラリDashを使う　2　Callbackをみる](https://qiita.com/OgawaHideyuki/items/1eea435b3f7c90375848)など詳しく説明されている方がいますのでそちらを参照してください。

###1. Front側（ウェブに見えるもの）
全てのコードを説明すると長くなってしまうので、コアとなる部分を例として説明します。

基本的に最初の ` app.layout＝`  の中に書かれているものは全てウェブサイトで表示されるものです。

表示したくないもの（例えば何度も使うデータ「intermediate-values」）は `style={'display': 'none'}` と入れるとウェブ上で見えなくなります。

```python

# フロントエンドはこのなかに書く
app.layout = html.Div([
    # データに反映される日付をユーザーが選べるようにする
    html.Div([
        html.H2("DR.麻雀"),
        dcc.DatePickerRange(
            id='my-date-picker-range',
            min_date_allowed=dt(2020, 3, 1),
            max_date_allowed=dt.today(),
            end_date=dt.today()
        ),
        ], className="mytablestyle"),

    # 何度も使うデータ:style={'display': 'none'}で見えなくする
    html.Div(id='intermediate-value', style={'display': 'none'}),

    # ポイントの推移（グラフ）
    dcc.Graph(id='mygraph'),

    # 総合ポイント（テーブル）
    html.Div([
            html.Div(html.P('現在の総合ポイント')),
            html.Div(id='totalscore'),
        ], className="mytablestyle"),

])

```

###2. CallbackでデータをJson化する

データをpandasのread_pickleで読み込み、日付でフィルターをかけ、json化して戻します。

こうすることによって同じデータをグラフやテーブルに何度も使えるようになります。

```python
@app.callback(Output("intermediate-value", "children"),
    [Input("my-date-picker-range", "start_date"),
    Input("my-date-picker-range", "end_date")])
def update_output(start_date, end_date):
    players = pd.read_pickle('players_score.pkl')
    if start_date is not None:
        start_date = dt.strptime(re.split('T| ', start_date)[0], '%Y-%m-%d')
        players = players.loc[(players['date'] >= start_date)]
    if end_date is not None:
        end_date = dt.strptime(re.split('T| ', end_date)[0], '%Y-%m-%d')
        players = players.loc[(players['date'] <= end_date)]
    return players.to_json(date_format='iso', orient='split')
```

### 3. Callbackでデータをグラフ化＆テーブル化
json化したデータをPandasのデータフレームに戻し、グラフ化とテーブル化していきます。

グラフ化はPlotlyの様式に乗っ取り、go.Figure()で表します。

テーブル化はhtml.Tableで表しました。テーブルはdash_tableというライブラリもあるのですが、今回のテーブルは簡易なものだったので必要ないろ思いこのスタイルにしました。

```python
@app.callback([Output('mygraph', 'figure'),
    Output('totalscore', 'children')],
    [Input('intermediate-value', 'children'),
    Input('datatype', 'value')])
def update_fig(jsonified_df, data_type):
    # Json化したデータをPandasでもとに戻す。
    players = pd.read_json(jsonified_df, orient='split')

    # グラフ化
    fig = go.Figure()
    for i, name in enumerate(players.columns[1:]):
        fig.add_trace(go.Scatter(x=players.date, 
                            y=np.array(players[name]).cumsum(),
                            mode='lines',
                            name=name,
                            line=dict(color=colors[i], width=4)))

    fig.update_layout(plot_bgcolor='whitesmoke',
        title='合計ポイントの推移',
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1,)
    )

    # 総合ポイントを計算
    summed = players.sum()

    # グラフと表を返す
    return fig, html.Table([
        html.Thead(
            html.Tr([html.Th(col) for col in summed.index])
            ),
        html.Tbody(
            html.Tr([html.Td(val) for val in summed])
            ),
        ])
```

# HerokuとGithubを使ってデプロイ

最後にHerokuとGithubを使ってデプロイしていきます。

公式サイト（[Deploying Dash Apps](https://dash.plotly.com/deployment)）にはGitとHerokuのやりかたが詳しく載っているので、ほとんどやり方は一緒です。

プロセスはこんな感じです：

1. Githubのアカウントにサインアップする
2. Githubの新しいレポジトリを作成する
3. GithubにSSH接続する。（オプショナルですが、やったほうが楽。参照：[GitHubにssh接続できるようにする](https://qiita.com/0ta2/items/25c27d447378b13a1ac3))
4. デプロイに必要なファイル(`.ignore`, `Procfile`, `requirements.txt`)を作成する。gunicornも必要なので `pip install gunicorn`でインストールする。
5. Gitコマンドで上記のファイルと`app.py`や`players_score.pkl`のデータファイルをGithubにプッシュする。

    ```bash
    git init
    git add .
    git commit -m "メッセージ"
    git remote add origin git@github.com:<ユーザー名>/＜レポジトリ名＞
    git push origin master
    ```

6. Githubにプッシュされているのを確認したらHerokuのアカウントを作成して`New > create new app`のボタンで新しいアプリを作成（Regionは日本がないので、United Statesを選択）。
7. 作成されたアプリのDeployのタブをクリックし、`Deployment method`をGithubにし、2.で作成したレポジトリに接続します。
8. 最後に`Manual deploy`の`Deploy Branch`という黒いボタンを押すと勝手にデプロイしてくれます。

#最後に
いかかでしたでしょうか。

cronとHerokuのAutomatic Deployを使えば天鳳から取ってくる新しいデータのアップデートを自動化することも可能です。（参考：[cronでGithubにPushするプロセスを自動化する](https://qiita.com/tokishin/items/a2eeaf3a76e83269d960)）


#　参考

- [天鳳個室の自動成績管理アプリをLINE botとPythonで作る](https://qiita.com/all_green/items/4c5f4bc1a2044b00d2f8)
- [Dash Documentation & User Guide](https://dash.plotly.com/)
- [Deploying Dash Apps](https://dash.plotly.com/deployment)
- [Pythonの可視化ライブラリDashを使う　2　Callbackをみる](https://qiita.com/OgawaHideyuki/items/1eea435b3f7c90375848)
- [cronでGithubにPushするプロセスを自動化する](https://qiita.com/tokishin/items/a2eeaf3a76e83269d960)