情報の科学私記

量子アニーリング、情報統計力学、数理最適化に興味があります。普段の研究で学び得たことを発信しています。

Pythonライブラリ「plotly」でガントチャートを表示する

TL;DR

Pythonライブラリ「plotly」で扱えるガントチャートについて、ジョブショップスケジューリング問題を例に解説します。

plotly

ガントチャートとは、人や工場などのスケジュールを可視化したもので、棒グラフの一種です。具体的には、ソフトウェア開発において開発者の稼働状況や、工場の機械がこなすタスクの流れを可視化するものです。自分が働いていたWeb制作会社では、Backlogというwebサービスガントチャートを用いて空き状況や作業量を把握していました。

https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Gantt_it.png/800px-Gantt_it.png

plotlyはPythonの可視化ライブラリで、様々なグラフを扱うことができます。この記事ではその一つのガントチャートについて、実際にコードも交えて紹介していきたいと思います。

plot.ly

ちなみに、以前書いた記事では「Gantt-Chart」というjavascriptのライブラリを使用したのですが、Pythonのライブラリをよく調べていませんでした。普段のコーディングはだいたいPythonで行うので、こちらをメインで使っていこうと考えています。

maruzhang.hatenablog.com

ひとまず、pipでインストールしましょう。

pip install plotpy

サンプルコードを理解する

それでは、plotpyのガントチャートの使い方を調べていきたいと思います。上記のページには、サンプルコードがいくつか列挙されています。私が望む図のスタイルは、上から7番目のUsing Hours and Minutes in Timesで、以下のコードとなります。Jupyter Notebookに表示するために、コードを追加しています。

# plotlyをインポート
# import plotly.plotly as py
import plotly.figure_factory as ff

# Jupyter Notebookで表示する場合
import plotly.offline as pof
pof.init_notebook_mode(connected=False)


# 可視化する個々のデータをdictで表現し、listに入れる
# dictは直接、{'Task': 'Morning Sleep', ...}も可
df = [
    dict(Task='Morning Sleep', Start='2016-01-01', Finish='2016-01-01 6:00:00', Resource='Sleep'),
    dict(Task='Breakfast', Start='2016-01-01 7:00:00', Finish='2016-01-01 7:30:00', Resource='Food'),
    dict(Task='Work', Start='2016-01-01 9:00:00', Finish='2016-01-01 11:25:00', Resource='Brain'),
    dict(Task='Break', Start='2016-01-01 11:30:00', Finish='2016-01-01 12:00:00', Resource='Rest'),
    dict(Task='Lunch', Start='2016-01-01 12:00:00', Finish='2016-01-01 13:00:00', Resource='Food'),
    dict(Task='Work', Start='2016-01-01 13:00:00', Finish='2016-01-01 17:00:00', Resource='Brain'),
    dict(Task='Exercise', Start='2016-01-01 17:30:00', Finish='2016-01-01 18:30:00', Resource='Cardio'), 
    dict(Task='Post Workout Rest', Start='2016-01-01 18:30:00', Finish='2016-01-01 19:00:00', Resource='Rest'),
    dict(Task='Dinner', Start='2016-01-01 19:00:00', Finish='2016-01-01 20:00:00', Resource='Food'),
    dict(Task='Evening Sleep', Start='2016-01-01 21:00:00', Finish='2016-01-01 23:59:00', Resource='Sleep')
]

colors = dict(Cardio = 'rgb(46, 137, 205)',
              Food = 'rgb(114, 44, 121)',
              Sleep = 'rgb(198, 47, 105)',
              Brain = 'rgb(58, 149, 136)',
              Rest = 'rgb(107, 127, 135)')


fig = ff.create_gantt(df, colors=colors, index_col='Resource', title='Daily Schedule',
                      show_colorbar=True, bar_width=0.8, showgrid_x=True, showgrid_y=True)

# Jupyter Notebookで表示する場合
pof.iplot(fig, filename='gantt-hours-minutes')

# ブラウザなどで表示する場合
# py.iplot(fig, filename='gantt-hours-minutes', world_readable=True)

初めにploylyをインポートして、pyとffのように短い名前で呼び出せるようにしています。次に可視化する個々のデータ、一つ一つの横棒についての情報をdictにして、listに格納しています。dictの各要素は以下の通りです。

  • Task: ガントチャートの左側に表示される行の内容
  • Start / Finish: タスクの初めと終わりの時刻
  • Resource: 各タスクが属する分類

各データを横棒として可視化する際の色をdictで指定します。dictの各要素は、上で述べたResourceをkey、rgbを表す文字列をvalueです。そして、これまで指定した内容からガントチャートを作成するのがplotly.figure_factory.create_gantt()です。先ほど作成したデータと色の情報を引数として渡します。それ以外の引数は名前の通りなので、説明は割愛します。

最後に、作成したガントチャートのオブジェクトをplotly.plotly.iplot()に渡すことで可視化されます。jupyter notebookでも埋め込まれた状態で可視化でき、また画像として保存することもできるので、現場においても有用かと思います。

具体例: ジョブショップスケジューリング問題

次に具体的な問題に対して、どのようにデータを渡せばよいか、またその結果を紹介していきます。この記事では、ジョブショップスケジューリング問題(以下、JSP)という組合せ最適化問題について話を始めていきます。JSPは以下のような問題です。

サンプルコードの出力結果
サンプルコードの出力結果

仕事(ジョブ)を機械に効率的に割り振り、完了時刻、納期遅れなどを最小化する問題

例題として、以下のページを参考にします。

www.msi.co.jp

  仕事a 仕事b 仕事c
作業1(機械1) 1(i=0) 1(i=1) 2(i=2)
作業2(機械2) 3(i=3) 2(i=4) 1(i=5)
作業3(機械3) 2(i=6) 3(i=7) 3(i=8)

表のように、それぞれのジョブにインデックスiを振ります。ぞれぞれのジョブをいつ開始するかを表す2値変数として、x{i,t}を定義します。ジョブiを時刻tに行うときにx{i,t}=1、そうでないときにx_{i,t}=0となります。

次に、例題についてのデータを用意します。上記ページと同様の解x_{i,t}が得られたとしましょう。行はi、列はtとしています。ちなみにD-Waveマシンなどのアニーリングマシンでは、出力がこのような多次元配列で得られます。

job_list = ['jobA', 'jobB', 'jobC']
machine_list = ['machine1', 'machine2', 'machine3']
n_job = len(job_list)
n_machine = len(machine_list)

jobshop_info = [[1, 1, 2], [3, 2, 1], [2, 3, 3]]

solution = \
    [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]]

可視化するデータをcreate_gantt()に渡す形式に整えます。上記の解を行ごとに取り出し、開始時刻や終了時刻、割り当てる機械名やジョブ名を参照します。

# 可視化する個々のデータをdictで表現し、listに入れる
# dictは直接、{'Task': 'Morning Sleep', ...}も可
df = []
for i, sol in enumerate(solution):
    machine_i = i // n_machine
    job_i = machine_i % n_job
    for t, s in enumerate(sol):
        if s == 1:
            df.append(
                dict(Task=machine_list[machine_i],
                     Start='2019-05-01 {}:00:00'.format(t),
                     Finish='2019-05-01 {}:00:00'.format(
                         t + jobshop_info[machine_i][machine_i]),
                     Resource=job_list[job_i]))

後は、サンプルコードと同様にガントチャートを出力します。group_tasks=Trueとすることで、異なるラベル(ここではジョブ)に対する同じ行(ここでは機械)をまとめることができます。

# Resourseをkey、rgbを表す文字列をvalueとしたdictで色の情報を表す
colors = {'jobA': 'rgb(46, 137, 205)',
          'jobB': 'rgb(114, 44, 121)',
          'jobC': 'rgb(198, 47, 105)'}

# 可視化するデータと色の情報をplotly.figure_factory.create_gantt()に渡す
# group_tasks=Trueとすることで、異なるラベル(ここではジョブ)に対する同じ行(ここでは機械)をまとめることができる
fig = ff.create_gantt(df, colors=colors, index_col='Resource', title='Job Shop Schedule',
                      show_colorbar=True, showgrid_x=True, showgrid_y=True, group_tasks=True)
pof.iplot(fig)

出力結果は以下のようになりました。javascriptが動作するようで拡大などもでき、プレゼンの場面でそのまま使えるかもしれません。

ジョブショップスケジューリング問題の出力結果
ジョブショップスケジューリング問題の出力結果

上記のコードをgistに公開しているので、参考にしてください。

まとめ

Pythonのライブラリplotlyを用いて、ガントチャートを表示する方法を紹介しました。可視化ライブラリの中でも使いやすい方だと思うので、機械学習や組合せ最適化など様々な場面で活用できるかと思います。

今回は、これで以上となります。