グラフ化する

目次
  • 解説
  • 1. 関数化する
  • 2. 2次元グラフにプロットする
  • 3. 2次元グラフの傾きを明確にする
  • 4. 3次元グラフにプロットする
問題に挑戦!
進捗を変更する




解説

1. 関数化する


学習用の教師データができましたが、データの計算処理が進み、表形式だけでは内容を把握しきれなくなってきましたので、グラフにして見てみましょう。まずは複雑化してきたここまでの計算をコンパクトに関数化していきます。

(1)Colab上で「step06.ipynb」を新規作成してください。
(2)下記のコードをそれぞれ新しいセルに入力して、
ランタイム」 > 「すべてのセルを実行」してください。
# インポート
import random
import pandas as pd
import altair as alt
import plotly.express as px

# 定数
DICE_AB='サイコロA/B'
DICE_MIN = 2
DICE_MAX = 20
ROLLS=100
C_DICE_A='diceA'
C_DICE_B='diceB'
C_DICE_A_add_B='diceA+B'
C_ROLLS_A_add_B_MEAN='rollsA+B_mean'
# 「サイコロを振る」のと同じ状態を再現する関数
# 引数1つ目はint型、dice_max=サイコロの最大値
# 引数2つ目はint型、dice_min=サイコロの最小値、省略時は1
# 戻り値はint型、サイコロを振った結果の目の値
def roll_dice(dice_max: int, dice_min: int = 1) -> int:
    return random.randint(dice_min, dice_max)
# step05のコードを簡略化して関数化
def create_dice_df(dice_min: int, dice_max: int, rolls: int) -> pd.DataFrame:
    dice_df = pd.DataFrame([[diceA_max, diceB_max]
        for diceA_max in range(dice_min, dice_max + 1)
        for diceB_max in range(dice_min, dice_max + 1)
    ], columns=[C_DICE_A, C_DICE_B])
    dice_df[C_DICE_A_add_B] = dice_df[C_DICE_A] + dice_df[C_DICE_B]
    # 仮合計列は列名も仮の0で良い
    dice_df[0] = 0
    for _ in range(rolls):
        # lambdaに続く引数の数と型が同じ関数ならば、関数を直接mapの引数に指定できる
        dice_df[0] += (dice_df[C_DICE_A].map(roll_dice)
                      + dice_df[C_DICE_B].map(roll_dice))
    dice_df[C_ROLLS_A_add_B_MEAN] = dice_df[0] / rolls
    # 仮合計列は削除してしまう
    dice_df.pop(0)
    return dice_df
# step05と同じデータを作成
dice_df = create_dice_df(DICE_MIN, DICE_MAX, ROLLS)
print('{}を{}回振って合計を平均'.format(DICE_AB, ROLLS))
dice_df

実行して次の結果が出力されればOKです。

サイコロA/Bを100回振って合計を平均
     diceA  diceB  diceA+B  rollsA+B_mean
0        2      2        4           3.13
1        2      3        5           3.46
2        2      4        6           3.76
3        2      5        7           4.47
4        2      6        8           5.04
..     ...    ...      ...            ...
356     20     16       36          17.85
357     20     17       37          19.52
358     20     18       38          19.50
359     20     19       39          19.28
360     20     20       40          20.21

2. 2次元グラフにプロットする


教師データ2次元グラフ点グラフとして表示してみましょう。グラフにデータを反映することをプロットと呼びます。

前項の出力結果の通り、2から20の目を持つサイコロA/Bの組み合わせ(厳密には順列)は361パターンです。そのパターン毎に100回サイコロを振った平均値となっていますので、表示される点は361個ということになります。

(1)下記のコードを新しいセルに入力して、
ランタイム」 > 「すべてのセルを実行」してください。
# 2dグラフ化
scatter_graph = alt.Chart(dice_df).mark_circle(size=40, color='rgba(255,0,0,0.5)').encode(
    x=alt.X(
        C_DICE_A_add_B,
        axis=alt.Axis(title='{}の目の最大値合算'.format(DICE_AB))),
    y=alt.Y(
        C_ROLLS_A_add_B_MEAN,
        axis=alt.Axis(title='{}回振った平均値'.format(ROLLS)))
).properties(
    width=500,
    height=400,
    title='サイコロの組合せと{}回振った平均値(2D)'.format(ROLLS)
).interactive()

# 表示
scatter_graph

実行して次の結果が出力されればOKです。

ポイント

グラフ生成ライブラリ:altair

この2次元(2D=2-Dimensions)グラフには、altair(アルタイル)というライブラリを使用しています。グラフの上でマウスホイールを操作するとズーム、ドラッグすると座標表示範囲を移動することができます。また、右上の「・・・」ボタンからは、画像として保存することが可能です。

altairは、他の教材やサイトでよく使われているグラフ生成ライブラリではできない機能が色々ありますので、こちらを使うことに慣れておきましょう。

グラフ生成コードの構成

altairによってグラフを生成するには、以下のように設定を行います。

alt.Chartによってdice_dfのグラフのベースを作成
mark_circleでプロットするポイントの大きさや色を指定
encodeで掛け合わせる横=x縦=yの軸(axis)のデータ列やタイトルを指定
propertiesで表全体の大きさやタイトルを指定
interactiveでズームや座標範囲移動を可能化

その他にもいろいろな設定が可能ですので、以下のURLを見ておきましょう。

グラフの軸

生成されたグラフのに注目してみましょう。横方向に伸びるには「サイコロA/Bの目の最大値合算」とあり、縦方向に伸びるには「100回振った平均値」とあります。つまり、2つのサイコロの最大値を足した値が増えるに従って、そのサイコロを振った目の合計値が右肩上がりに増えていく、という相関関係が見えてきます。

一見すると当たり前のことを確認しているようではありますが、視覚的にも確認しておくことは、扱うデータが複雑化していくと大変重要になります。また、データの散らばり具合であったり、密集度など、表で見ていてもあまり目に付かないポイントも見えてきますので、積極的にグラフ化するようにしましょう。

3. 2次元グラフの傾きを明確にする


グラフ化したことで、2つのサイコロの最大値を足した値が増えるに従って、そのサイコロを振った目の合計値が右肩上がりに増えていく、という相関関係が見えてきました。

線グラフであればそれを傾きとして明確に表現できるのですが、今は点の重なり具合などによりぼやけています。一見真っ直ぐだと言い切ってしまっても良さそうですが、ズームするとまた違った分布になっているようにも見えます。

そこで、このグラフの中心線を表現してみましょう。

グラフの点の数は361個ですが、左下の端(横軸=4)は サイコロA=2 + サイコロB=2 のひとつだけで、同様に右上の端(横軸=40)も 20 + 20 のみとなります。反対に一番多いのは横軸が22となる場合で、19個の点=サイコロの組み合わせが存在します。

つまり、横軸毎に1〜19個ずつ存在する各点の縦軸の値の平均を繋いでいけば、中心線となりそうです。

(1)下記のコードを新しいセルに入力して、
ランタイム」 > 「すべてのセルを実行」してください。
# 2dグラフに平均値を表示
# 「サイコロA/Bの目の最大値合算」ごとに合計して、その平均値を算出
dice_df_mean = (dice_df.groupby(C_DICE_A_add_B, as_index=False)
                         .agg({ C_ROLLS_A_add_B_MEAN: 'mean' }))
# 線グラフ
line_graph = alt.Chart(dice_df_mean).mark_line(color='rgba(255,0,0,1)').encode(
    x=C_DICE_A_add_B,
    y=C_ROLLS_A_add_B_MEAN)

# 2つのグラフを合体して表示
scatter_graph + line_graph

実行して次の結果が出力されればOKです。

ポイント

DataFrame.groupby / .agg

中心線表示用のデータ作成には、pandas.DataFramegroupbyaggという集計関数を使用しています。

groupbyはSQLを触ったことがある人ならイメージが湧くと思いますが、同じ値でグルーピングする関数です。引数に指定している C_DICE_A_add_B の値が同じデータをグループ化、つまり、横軸が22の時には19個のデータがひとまとめにされるということです。ひとまとめにしただけでは、グラフに何を表示すべきかわかりません。そこで、aggの出番です。

aggはaggregate=集計の略です。引数には C_ROLLS_A_add_B_MEANmean するよう指定しています。つまり、groupbyによってひとまとめになったデータの縦軸の値の平均値をそのグループの値として置き換えているのです。

わかりにくければ、dice_df_meanもこれまでのように画面に表示させてみましょう。

グラフの傾きと線形グラフ

生成された赤い線グラフの傾きに注目してみましょう。

簡単のために軸が交差している箇所だけ抜き出すと、横軸が6の時に縦軸が410の時614の時8、というようになっています。横軸が2増える毎に縦軸は1増える、つまり傾きは0.5となり、ここまでを数式で表すと y = 0.5x となります。

また、このグラフを伸ばして横軸が0の時を想像すると、縦軸は0より上の位置にありそうです。右辺にxを当てはめて計算してみると、6 x 0.5 = 3、10 x 0.5 = 5、14 x 0.5 = 7、のように縦軸の値より1少ないようです。つまり、すべての縦軸の値は +1 されているようです。これをグラフの切片(せっぺん)と言います。

最終的にこれを数式で表すと、y = 0.5x + 1となり、真っ直ぐな線形グラフであると言えそうです。

サンプルサイズとばらつきと誤差

もしも「右上の方は線形じゃない・・・」と納得が行かないならば、DICE_MAXを倍の40にして再度実行してみてください。点の数が1521まで増え、グラフの範囲も右上に伸びました。

このグラフで先ほどの右上、つまり横軸が40のあたりをみると、先ほどの数式が綺麗に当てはまりそうですね。でもやはり、今のグラフの右上ではばらつきが見られます。平均からの距離を誤差と呼び、誤差の大きいデータが目立つ状態は一般的にばらつきと呼ばれます。

左下は、サンプルサイズ(点の数)が少ないですが、そもそもサイコロの目の最大値が小さく、取りうる値の範囲が小さいため誤差が小さいです。

真ん中あたりは、サンプルサイズが多くなっているため、平均値が想定通り算出できています。

右上は、サンプルサイズが少なく、サイコロの目の最大値も大きいため、誤差の大きさが平均に影響しやすくなってしまうのです。

4. 3次元グラフにプロットする


ここで、横軸についてあらためて考えてみると、「サイコロの目の最大値の組み合わせ」だったはずなのに合算してしまっています。これでは、特徴を増やした意味がありません。3次元グラフにして、軸をサイコロAとサイコロBに分割しましょう。

(1)下記のコードを新しいセルに入力して、
ランタイム」 > 「すべてのセルを実行」してください。
# 3dグラフ化
scatter_graph3d = px.scatter_3d(
    dice_df,
    x=C_DICE_A,
    y=C_DICE_B,
    z=C_ROLLS_A_add_B_MEAN,
    color_discrete_sequence=['rgba(255,0,0,0.5)'])
scatter_graph3d.update_traces(marker_size = 2)
scatter_graph3d.update_layout(
    title='サイコロの組合せと{}回振った平均値(3D)'.format(ROLLS),
    scene=dict(
        xaxis=dict(title='サイコロBの目の最大値'),
        yaxis=dict(title='サイコロAの目の最大値'),
        zaxis=dict(title='{}回振った平均値'.format(ROLLS))),
    margin=dict(l=0, r=0, b=0, t=30),
    scene_camera=dict(
        eye=dict(x=1.6, y=-1.7, z=1),
        center=dict(x=0, y=0, z=-0.2)),
    width=500,
    height=400,
    autosize=False)
scatter_graph3d.show()

実行して次の結果が出力されればOKです。

ポイント

グラフ生成ライブラリ:plotly.express

この3Dグラフには、plotly.expressというライブラリを使用しています。こちらもグラフの上でマウスホイールを操作するとズーム、ドラッグするとぐるぐると回すことができます。

グラフ生成コードの構成

plotly.expressによってグラフを生成するには、以下のように設定を行います。

px.scatter_3dによってdice_dfのグラフのベースを作成し各軸(x,y,z)と色を指定
update_tracesでプロットするポイントの大きさを指定
update_layoutで表全体の大きさやタイトルを指定
scene_cameraで初期表示時の回転や視点距離を指定

その他にもいろいろな設定が可能ですので、以下のURLを見ておきましょう。

4次元は無い

生成されたグラフのに注目してみましょう。左奥から中央手前方向に伸びるには「サイコロBの目の最大値」とあり、中央手前から右奥方向に伸びるには「サイコロAの目の最大値」とあります。そして、2Dの時と同じく、縦方向に伸びるには「100回振った平均値」とあります。

グラフにプロットされた点は右上に傾いた平行四辺形の形に見えますね。グラフの中央手前あたりをドラッグして、マウスを少し上に移動すると、2Dのグラフと同じように見えると思います。合算していたため線状に見えていましたが、分解するとこのような拡がりを持っていたということがわかります。

今回は2Dを3Dにすることでより詳しくみることができましたが、4D以降はあり得ません。さらにデータの次元数が増えた場合には、データを分割したり一部を取り出したりして、2Dまたは3Dに表現することで、データの把握に役立てましょう。

問題

実践問題


問題

step06.ipynbの各セルに必要なコードを入力して、グラフの点や線の色を変更してください。また、セル3create_dice_df関数内12〜13行目で行っている処理を加算+」から乗算*」に変更して、その結果をグラフで確認してください。

なお、色の指定方法は次を参考にして構いません。
rgba(255, 0, 0, 1) => 赤100%、不透明度100%
rgba(0, 255, 0, 1) => 緑100%不透明度100%
rgba(0, 0, 255, 1) => 青100%不透明度100%
rgba(255, 0, 0, 0.5) => 赤100%不透明度50%
rgba(255, 0, 0, 0.5) => 赤100%不透明度50%
※ r=red / g=green / b=blue は各色の比率を 0〜255 の範囲で表現
※ a=alpha は不透明度を 0〜1 の範囲で表現、0ならば透明

実行結果例を表示

正解は省略します。