PlotlyDashで携帯の通信料金プラン比較

# PlotlyDashで携帯の通信料金プラン比較
各社から携帯の新料金プランが出ているので、Pythonの可視化ライブラリ[Plotly](https://plotly.com/python/)とダッシュボード作成ライブラリ[Dash](https://plotly.com/dash/)を使って、データ通信量や通話料で変わる各プランの通信料金を比較して、自分にあったプランを選びます。

# 準備
各社の通信プランを調べて、一覧化したCSVを用意します。ここは自分で調べてください。以下はあくまで例です。

```csvs:mobleplan.csv
プラン,基本料金(円/月),データ通信量料金(円/GB),無料データ通信量(GB),通信量無制限(円/月),通話料金(円/30秒),無料通話時間(分),無料通話追加料金(円/月),通話無制限(円/月)
Aプラン,2700,500,20,99999,20,5,0,1000
Bプラン,2480,500,20,99999,20,5,500,1000
Cプラン,2480,500,20,99999,20,5,500,1500
Dプラン,5150,1000,3,1500,20,5,700,1700
Eプラン,980,1000,0,5600,20,5,800,1800
Fプラン,5080,9999,3,1500,20,5,800,1800
```

列項目のオプションがないプランは「99999」などありえない数値を入れておきます。

# Pythonのコード

コード内のCSVの保存先を編集します。

```python:cell_phone_plan.py
import numpy as np
import pandas as pd

import plotly.graph_objects as go
import plotly.express as px

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

plan_path = '/xxxx/mobleplan.csv'  # CSVの保存先を修正
plantable = pd.read_csv(plan_path)
plantable = plantable.set_index('プラン')

def monthly_cost(plantable, plan, gb, num_of_calls, mean_call_seconds):
    # 基本料金
    main_cost = plantable.loc[plan,  '基本料金(円/月)']
    # 通信量料金
    gb_variable = gb * plantable.loc[plan, 'データ通信量料金(円/GB)']
    gb_fixed = plantable.loc[plan, '通信量無制限(円/月)']
    gb0 = plantable.loc[plan, '無料データ通信量(GB)']
    if gb > gb0:
        gb_hybrid = plantable.loc[plan,  'データ通信量料金(円/GB)'] * (gb - gb0)
    else:
        gb_hybrid = 0
    gb_variable_cost = main_cost + gb_variable
    gb_hybrid_cost = main_cost + gb_hybrid
    gb_fixed_cost = main_cost + gb_fixed
    gb_cost = min([gb_variable_cost, gb_hybrid_cost, gb_fixed_cost])
    gb_argmin_cost = np.argmin([gb_variable_cost, gb_hybrid_cost, gb_fixed_cost])
    # 通話料金
    call_variable_cost = num_of_calls * mean_call_seconds * plantable.loc[plan, '通話料金(円/30秒)'] // 30
    call_fixed_cost = plantable.loc[plan, '通話無制限(円/月)']
    if mean_call_seconds > plantable.loc[plan, '無料通話時間(分)'] * 60:
        call_hybrid_cost = plantable.loc[plan,  '無料通話追加料金(円/月)'] + num_of_calls * (mean_call_seconds -  plantable.loc[plan, '無料通話時間(分)'] * 60) * plantable.loc[plan, '通話料金(円/30秒)'] / 30
    else:
        call_hybrid_cost = plantable.loc[plan,  '無料通話追加料金(円/月)']
    call_hybrid_cost = call_hybrid_cost // 1
    call_cost = min([call_variable_cost, call_hybrid_cost, call_fixed_cost])
    call_argmin_cost = np.argmin([call_variable_cost, call_hybrid_cost, call_fixed_cost])
    # トータル費用
    cost = gb_cost + call_cost

    if gb_argmin_cost == 1:
        select_plan = str(plantable.loc[plan, '無料データ通信量(GB)']) + 'GB通信無料プラン'
    elif gb_argmin_cost == 2:
        select_plan = '通信量制限なしプラン'
    else:
        select_plan = '通信オプションなしプラン'
    
    if call_argmin_cost == 1:
        call_select_plan = str(plantable.loc[plan, '無料通話時間(分)']) + '分通話無料プラン'
    elif call_argmin_cost == 2:
        call_select_plan = '通話無制限プラン'
    else:
        call_select_plan = '通話オプションなしプラン'
    return cost, select_plan, call_select_plan

def create_fig(plantable, slide1=0, slide2=0):
    fig = go.Figure()
    for plan in plantable.index:
        y = []
        hovertext = []
        x = range(0, 41, 1)
        for i in x:
            cost, select_plan, call_select_plan = monthly_cost(plantable, plan, i, slide1, slide2)
            y += [cost]
            hovertext += ['通信プラン:' + select_plan + ', 通話プラン:' + call_select_plan]
        xticksuffix = 'GB'
        fig.add_trace(
            go.Scatter(
                x=list(x),
                y=y,
                mode='lines',
                name=plan,
                hovertext=hovertext,
                hoverinfo='x+y+text'
            )
        )
        fig.update_layout(
            xaxis = dict(ticksuffix=xticksuffix),
            yaxis_tickformat=',',
            yaxis = dict(ticksuffix='円'),
        )
    return fig

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, suppress_callback_exceptions=True, external_stylesheets=external_stylesheets)

server = app.server

app.layout = html.Div(
    children=[
        html.H1(
            id='title',
            children='携帯の通信料金プラン比較'
        ),
        html.P(
            id='p_num_of_calls',
            children='通話回数'
        ),
        dcc.Slider(
            id='num_of_calls',
            min=0,
            max=40,
            step=1,
            value=5,
            marks={i: {'label': str(i)} for i in range(0, 51, 1)},
        ),
        html.P(
            id='p_mean_call_seconds',
            children='平均通話時間'
        ),
        dcc.Slider(
            id='mean_call_seconds',
            min=0,
            max=1800,
            step=30,
            value=300,
            marks={i*60: {'label': str(i)+'分'} for i in range(0, 61, 5)}
        ),
        dcc.Graph(
            id='graph',
            figure=create_fig(plantable, slide1=0, slide2=0),
        ),
    ],  # layoutDivのchildrenの閉じ括弧
    id="layout_div",
)

@app.callback(
    Output(component_id="graph", component_property="figure"),
    Input(component_id="num_of_calls", component_property="value"),
    Input(component_id="mean_call_seconds", component_property="value"),
)
def culc(num_of_calls, mean_call_seconds):
    fig = create_fig(plantable, slide1=num_of_calls, slide2=mean_call_seconds)
    return fig

if __name__ == "__main__":
    app.run_server(debug=True)
```

通話料金は通話回数と平均通話時間のみの入力に簡略化していますのでご注意ください。
例えば、1回5分以内無料プランでは、7分の電話を3回かけた場合と1分の電話を2回と19分の電話を1回書けた場合とでは、通話回数はいずれも3回で平均通話時間もいずれも7分ですが、前者は6分間分に追加料金が発生し、後者は14分間分に追加料金が発生します。通話回数と平均通話時間のみの入力にすると、後者のような電話の傾向がある人も前者の方法で比較の計算がされてしまいます。


# Dashを実行
ターミナル等で以下を実行します。

```
python cell_phone_plan.py
```

ブラウザで以下localhostにアクセスします。
http://127.0.0.1:8050/

こんな感じに表示されます。

![mobileplan.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193486/03778030-20ae-04e3-e5bd-f4705235a8c7.gif)

自分の毎月のデータ通信量や通話時間を参考にして、最も安いプランを選ぶことができます。
なお、セット割引やキャンペーン、解約費用などを考慮する場合には、Pythonのコードに追加する必要があります。個人的には、複雑なわかりにくいプランは利用者の検討時間を余計に奪うものなので、考慮から外してもよいと思いますが。
また、当然ですが、通信速度やエリアなど料金以外の要素については、別途で比較・検討が必要です。

# 念のため、免責事項等

- この記事は特定の料金プランを推奨するものではありません。
- この記事の数値や計算は仮定のものであり、その結果に一切責任は負いません。
- この記事の内容は、所属組織とは一切関係ありません。
- もし誤った記載がありましたら、修正・削除しますので、ご指摘ください。