Plotlyを用いた2つの時系列データ比較

Story-telling with data 1/N

公開日: 2022-11-01
更新日: 2024-04-01

  Table of Contents

問題意識

storytelling with dataより

The more infomation you’re deealing with, the difficult it is to filter down to the most important bits.

When engaging in data storytelling, it is imperative to adhere to the following principles:

  • focus on the message
  • simple beats sexy

PlotlyやTableauなどのツールのおかげでデータの可視化は容易くできるようになった一方, 本来求められているものデータの可視化自体ではなく, データの可視化を通じてストーリーを伝えることです.

データの可視化で生成されるグラフやpngファイルにどのように分析ストーリーを組み込んでいくのかをシリーズを通して勉強していきます. 勉強するにあたって個人的に意識しているポイントとして以下です

可視化お作法を学ぶにあたって

  1. 可視化の技法がどのように使えるのか?
  2. それぞれの技法がどのような場合に用いられるか?
  3. なぜ特定の場面でとある手法が有効なのか?

今回のお題: 人員補充のリクエスト

意思決定者へ伝えたいこと

  • 2021年5月にバックオフィスチームより2人離職 & 追加人員補填なし
  • 2021年7月以降, 契約受注件数に対して処理件数遅れてしまっており, 人員追加が必要と役員に伝えたい

2021年の契約受注件数と処理件数の実績データを抽出したところ以下のような結果になった.

month received processed
0 2021-01-01 160 159
1 2021-02-01 138 147
2 2021-03-01 154 162
3 2021-04-01 178 180
4 2021-05-01 140 145
5 2021-06-01 156 145
6 2021-07-01 187 181
7 2021-08-01 198 165
8 2021-09-01 187 140
9 2021-10-01 199 160
10 2021-11-01 180 140
11 2021-12-01 173 140

これだけを見せても「2021年7月以降, 契約受注件数に対して処理件数遅れてしまってる」ということは読み解くことは難しいです.

分析フレームワーク

いきなり可視化に入る前に, そもそも分析とはなにかを簡単に振り返ります.

分析フレームワーク

  1. 分析の前提となる意思決定問題, クエッションを理解する = What is the matter?
  2. 意思決定を行うにあたって必要な情報はなにかを理解する = What answer do you want to tell?
  3. QuestionとAnswerのコンテクストを説明する = How do you tell your story?

そもそも分析や可視化はそれ自体に価値があるのではなく「意思決定をサポートする」という観点で価値があるものです. そのため, ゴールは「分析結果を構造化し, 分かりやすく & 一貫性のあるストーリーを意思決定者に伝える」ことであるべきです.

今回はすでに分析方針がお題で与えられてますが, それを整理すると以下のようになります

  • What is the matter?: 契約受注件数に対して処理件数遅れてしまっている
  • What answer?: 処理件数遅れはバックオフィスチームの人員不足に起因する
  • How do you tell your story?: バックオフィスチームの離職発生以後に処理件数遅れが発生するようになった

可視化

可視化のお作法

  1. gridlineは薄い色を用いること, 競合してしまう場合はなくしてしまうこと
  2. data markerは意図を持って使用すること
  3. axis labelはtidyであること
  4. 比較の際は, 基準ラインを薄く(細く), 比較対象を濃く(太く)すること
  5. データソースや重要な前提条件はfootnoteに記載すること

上述のポイントを踏まえて今回作成した分析可視化は以下となります

ポイントとしては

  • Titleには分析から導かれる意思決定内容 = Answerを明記
  • 比較対象時系列(=基準ライン)を細い & 薄いグレイ, 注目すべき時系列を太く & 濃いネイビーで表現
  • イベント発生月以前と以後をわかりやすくする
  • 着目してもらいたい数値のみをマーカーで強調
  • Footnoteにデータソース, 及びデータ前提条件を明記

Python Plotlyコード紹介

分析用データはすでにdfオブジェクトに格納されているとします. dfの中身については上で説明したとおりです.

時系列ごとの太さとカラーの調整

1
2
3
4
5
6
7
8
9
10
11
12
fig = px.line(
    df,
    x="month",
    y=["received", "processed"],
    labels={"month": "", "value": "Number of tickets"},
    color_discrete_map={"received": "#757C88", "processed": "#00004d"}, # Color調整
    title="<b>受注処理件数水準の回復には2 FTEの補充が必要</b><br><sup>月次受注件数及び処理件数推移</sup>",
)

# 太さ調整
fig.data[0]['line']['width'] = 2
fig.data[1]['line']['width'] = 4

全体のテイストの調整

フォントとバックグラウンドカラーの白色への変更

1
fig.update_layout(template="simple_white", font={"family": "Meiryo"})

y軸とx軸の範囲と表記の調整

1
2
3
4
5
6
7
8
9
fig.update_yaxes(range=[0, 300], showgrid=True)
fig.update_xaxes(
    dtick="M1",
    ticklen=0,
    tickformat="%m月\n%Y",
    showticklabels=True,
    visible=True,
    showgrid=False,
)

Markerとtextの追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# -------------------
# Marker用データの作成
# -------------------

x_scatter = df.loc[df["month"] >= "2021-08-01", "month"]
y_scatter_recieved = df.loc[df["month"] >= "2021-08-01", "received"]
y_scatter_processed = df.loc[df["month"] >= "2021-08-01", "processed"]

# -------------------
# Markerの追加
# -------------------
# 比較対象時系列
fig.add_traces(
    go.Scatter(
        x=x_scatter,
        y=y_scatter_recieved,
        mode="markers+text",
        text=y_scatter_recieved,
        textposition="top center",
        marker_color="gray",
        hoverinfo="skip",
        showlegend=False,
        marker=dict(
            size=10,
        ),
    )
)

# メイン時系列
fig.add_traces(
    go.Scatter(
        x=x_scatter,
        y=y_scatter_processed,
        mode="markers+text",
        text=y_scatter_processed,
        textposition="bottom center",
        marker_color="#00004d",
        hoverinfo="skip",
        showlegend=False,
        marker=dict(
            size=10,
        ),
    )
)

イベント発生タイミングの追加とテクスト

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fig.add_vline(x=pd.to_datetime("2021-05-01"), line_width=1, opacity=1, line_dash="dash")
fig.add_shape(
    type="rect",
    fillcolor=fig.layout["template"]["layout"]["plot_bgcolor"],
    opacity=1,
    x0="2021-05-03",
    y0=262,
    x1="2021-12-31",
    line=dict(color=fig.layout["template"]["layout"]["plot_bgcolor"]),
    y1=300,
    label=dict(
        text=(
            "<b>5月にて従業員2名の離職</b><br>"
            "従業員離職後,2ヶ月間は受注オーダー件数に"
            "応じた処理が<br>できていたが, 8月より処理件数の"
            "遅れが恒常的に発生"
        ),
        textposition="top left",
        font=dict(size=11),
    ),
)

Footnoteの追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fig.add_annotation(
    x=-0.1,
    y=-0.2,
    xref="paper",
    yref="paper",
    xanchor="left",
    yanchor="bottom",
    font=dict(size=12),
    xshift=-1,
    yshift=-5,
    text=(
        "Data source: corporate.order_table, as of 2022-01-15"
        " | 受注件数はキャンセルを除外して計算"
    ),
    showarrow=False,
)

Appendix I: RAPフレームワーク

項目 説明
Research Question 意思決定者が直面している問題について, 手元の分析でどの範囲を答えるのか(= スコープ設定)を簡易に表現した文章
Answer Research Question に対する分析結果, Research Question の文章に対応した返答になっていること
Positioning Research Question が「なぜ重要か?」「意思決定問題とどのように関連しているのか」「なぜ実施する必要があるのか」というコンテクストを説明する

なお, RAP フレームワークは分析アウトプットが満たすべきフォーマットを示唆するだけで, PDCA サイクルみたいな進め方を意味するものではありません.

Appendix II: Data Generating Process

今回のサンプルデータの生成コードです.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ------------------
# data generating
# ------------------
import pandas as pd
import numpy as np

np.random.seed(42)

mu = 150
std = 20
effect_pre = 10
effect_main = 30

start_month = "2021-01-01"
end_month = "2021-12-01"
before_treatment_month = pd.to_datetime("2021-05-01")
treatment_actual_month = pd.to_datetime("2021-08-01")

# make columes
time_range = pd.date_range(start=start_month, end=end_month, freq="MS")
effect_array = np.where(time_range > before_treatment_month, effect_pre, 0)
effect_array = effect_array + np.where(
    time_range >= treatment_actual_month, effect_main, 0
)

processed = np.int64(np.random.normal(mu, std, len(time_range)))
received = np.int64(processed + effect_array) + np.int64(
    np.random.normal(0, 5, len(time_range))
)

df = pd.DataFrame({"month": time_range, "received": received, "processed": processed})
df.head()

References



Share Buttons
Share on:

Feature Tags
Leave a Comment
(注意:GitHub Accountが必要となります)