時系列データの可視化#

生のCSVデータはただの数値の羅列であり、そのままでは直感的な理解は困難です。グラフに描画(可視化)することで、”上昇傾向にある”, ”周期性がある”, ”変化がない”などさまざまな情報が見えてきます。Pythonでは、PythonベースとJavascriptベースのライブラリがよく用いられます(ほかにもd3jsやOpenGLベースのものもあるようです)。

  • Pythonベース

    • Matplotlib

      • 最も基本的な可視化ライブラリです。ごちきかのコンテンツでもよく利用されています。

    • seaborn

      • matplotlibを拡張した可視化ライブラリで、特に統計的なグラフ描画が得意です。散布図やヒストグラム、箱ひげ図などを容易にプロットできます。

    • pandas

      • pandas自体は、主にデータの加工や集計、演算のためのライブラリですがバックエンドにmatplotlibを採用しているため、plot()メソッドで基本的な描画を担うことができます。

  • Javascriptベース

    • Plotly

      • インタラクティブなグラフをきれいにプロットできます。タグごとに表示を切り替えたり、グラフの値をマウスオーバーで動的に得られるため、1枚のグラフでさまざまな説明がしやすいのが利点です。注意点として、時系列データのようにデータ数が大きくなると描画が重くなってしまいます。

    • Bokeh

      • 同様にインタラクティブなグラフをプロットできます。

ここでは、基本的な描画ツールのmatplotlibをつかった可視化を行います。一般的な時系列センサーデータは値の時間変化に興味があるため、まず折れ線プロットを作図します。前章で前処理済みのデータを対象とします。

matplotlibによる可視化#

matplotlibにはグラフを作る方法が主に2つあります。

pyplotインターフェース#

plt.plot()で描画していくパターンです。

plt.figure()
plt.plot(x, y)
plt.show()

1枚のグラフを手早く作成する場合に便利です。

オブジェクト指向インターフェース#

plt.subplots().add_subplot()を用いてAxes オブジェクトを作成し、グラフを作成するパターンです。複数グラフを同時に描画したり、二軸グラフを作成する場合、細かいレイアウトを調整したいときに利用します。

FigureオブジェクトとAxesオブジェクトの関係は下図のように入れ子構造になっています(matplotlib公式チュートリアルより引用)。 Figureがグラフ全体を、Axesが実際にプロットする場所を指しています。

Hide code cell source
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib.patheffects import withStroke
from matplotlib.ticker import AutoMinorLocator, MultipleLocator

royal_blue = [0, 20/256, 82/256]


# make the figure

np.random.seed(19680801)

X = np.linspace(0.5, 3.5, 100)
Y1 = 3+np.cos(X)
Y2 = 1+np.cos(1+X/0.75)/2
Y3 = np.random.uniform(Y1, Y2, len(X))

fig = plt.figure(figsize=(7.5, 7.5))
ax = fig.add_axes([0.2, 0.17, 0.68, 0.7], aspect=1)

ax.xaxis.set_major_locator(MultipleLocator(1.000))
ax.xaxis.set_minor_locator(AutoMinorLocator(4))
ax.yaxis.set_major_locator(MultipleLocator(1.000))
ax.yaxis.set_minor_locator(AutoMinorLocator(4))
ax.xaxis.set_minor_formatter("{x:.2f}")

ax.set_xlim(0, 4)
ax.set_ylim(0, 4)

ax.tick_params(which='major', width=1.0, length=10, labelsize=14)
ax.tick_params(which='minor', width=1.0, length=5, labelsize=10,
               labelcolor='0.25')

ax.grid(linestyle="--", linewidth=0.5, color='.25', zorder=-10)

ax.plot(X, Y1, c='C0', lw=2.5, label="Blue signal", zorder=10)
ax.plot(X, Y2, c='C1', lw=2.5, label="Orange signal")
ax.plot(X[::3], Y3[::3], linewidth=0, markersize=9,
        marker='s', markerfacecolor='none', markeredgecolor='C4',
        markeredgewidth=2.5)

ax.set_title("Anatomy of a figure", fontsize=20, verticalalignment='bottom')
ax.set_xlabel("x Axis label", fontsize=14)
ax.set_ylabel("y Axis label", fontsize=14)
ax.legend(loc="upper right", fontsize=14)


# Annotate the figure

def annotate(x, y, text, code):
    # Circle marker
    c = Circle((x, y), radius=0.15, clip_on=False, zorder=10, linewidth=2.5,
               edgecolor=royal_blue + [0.6], facecolor='none',
               path_effects=[withStroke(linewidth=7, foreground='white')])
    ax.add_artist(c)

    # use path_effects as a background for the texts
    # draw the path_effects and the colored text separately so that the
    # path_effects cannot clip other texts
    for path_effects in [[withStroke(linewidth=7, foreground='white')], []]:
        color = 'white' if path_effects else royal_blue
        ax.text(x, y-0.2, text, zorder=100,
                ha='center', va='top', weight='bold', color=color,
                style='italic', fontfamily='Courier New',
                path_effects=path_effects)

        color = 'white' if path_effects else 'black'
        ax.text(x, y-0.33, code, zorder=100,
                ha='center', va='top', weight='normal', color=color,
                fontfamily='monospace', fontsize='medium',
                path_effects=path_effects)


annotate(3.5, -0.13, "Minor tick label", "ax.xaxis.set_minor_formatter")
annotate(-0.03, 1.0, "Major tick", "ax.yaxis.set_major_locator")
annotate(0.00, 3.75, "Minor tick", "ax.yaxis.set_minor_locator")
annotate(-0.15, 3.00, "Major tick label", "ax.yaxis.set_major_formatter")
annotate(1.68, -0.39, "xlabel", "ax.set_xlabel")
annotate(-0.38, 1.67, "ylabel", "ax.set_ylabel")
annotate(1.52, 4.15, "Title", "ax.set_title")
annotate(1.75, 2.80, "Line", "ax.plot")
annotate(2.25, 1.54, "Markers", "ax.scatter")
annotate(3.00, 3.00, "Grid", "ax.grid")
annotate(3.60, 3.58, "Legend", "ax.legend")
annotate(2.5, 0.55, "Axes", "fig.subplots")
annotate(4, 4.5, "Figure", "plt.figure")
annotate(0.65, 0.01, "x Axis", "ax.xaxis")
annotate(0, 0.36, "y Axis", "ax.yaxis")
annotate(4.0, 0.7, "Spine", "ax.spines")

# frame around figure
fig.patch.set(linewidth=4, edgecolor='0.5')
plt.show()
../_images/ebc39d362663a6ffb2a581c8fd32a3dbb626343bca10becaddf766d5a6077e0c.png

Axesの作成方法は前述の通り2パターンあります

plt.subplots#

FigureAxesを同時に作成する方法です。引数はsubplots(行数, 列数) のようになっています。

fig, ax = plt.subplots(1, 1)
ax.plot(x, y)
plt.show()
import matplotlib.pyplot as plt
%matplotlib inline

fig, axes = plt.subplots(2, 3, figsize=(6, 4))
fig.subplots_adjust(hspace=0.4, wspace=0.4)

for i in range(2):
    for j in range(3):
        # axesにはarray形式で複数のAxesが格納されている.
        # index指定でアクセス可能
        axes[i][j].text(0.5, 0.5, str((i, j)),
           fontsize=18, ha='center')

plt.show()
../_images/721f61bbf51cc61c51c5ade27b648988facdf0f23a624074e027db6ea8be4871.png

fig.add_subplot()#

plt.figure()Figureを作成してからAxesを追加する方法です。引数がadd_subplot(行数, 列数, 図番号) になっており、以下の図の様に左上から順番に図番号が振られています。

# 例2 
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(x, y)
plt.show()
fig = plt.figure(figsize=(6, 4))
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(1, 7):
    ax = fig.add_subplot(2, 3, i)
    ax.text(0.5, 0.5, str((2, 3, i)),
           fontsize=18, ha='center')
plt.show()
../_images/d1983bd412dd4670fb7090ac44b9c86bf44aa89e2eec5cd2b9ceeb7793abfcf3.png

時系列データの描画#

データを読み込みます。

import numpy as np
import pandas

fname = '../data/2020413.csv'
df = pandas.read_csv(fname, index_col=0, parse_dates=True, na_values=['None'], encoding='shift-jis')

matplotlib本体ではなく、その下のpyplotを呼び出します。横軸が時刻、縦軸がデータの一列目の図を出してみましょう。
pandas では、 iloc[行数, 列数]Dataframe からデータを抽出することができます。

import matplotlib.pyplot as plt

x = df.index        # x軸: 時刻
y = df.iloc[:, 0]   # データの一列目. 「:」で行すべて選択、「0」で1列目を選択

fig = plt.figure()  # figure オブジェクトの作成
ax = fig.add_subplot(1, 1, 1)   # Axesオブジェクトの作成
ax.plot(x, y)
plt.show()
../_images/f61adb0d4feefa142fedde99e88ae5a5c3eab11b728c3a918c64c4cbf832ef89.png

上で示したMatplolibの公式チュートリアルの図のように、Axesオブジェクトは軸や目盛りを持っているため、それぞれにアクセスすることで細かい調整が可能となります。ここでは、図の解像度や色、日本語フォントの埋め込み、時系列データに頻出な時刻目盛りなどを調整します。

# matplotlibで日付のフォーマットを取り扱うモジュールを追加で読み込む
from matplotlib.dates import DateFormatter

# matplotlibでは日本語にデフォルトで対応していないので、日本語対応フォントを読み込む
plt.rcParams['font.family'] = 'BIZ UDGothic' # Windows
# plt.rcParams['font.family'] = 'Hiragino Maru Gothic Pro' # Mac 

# その他使えるフォント一覧は以下のコードで取得できます。
# import matplotlib.font_manager
# [f.name for f in matplotlib.font_manager.fontManager.ttflist]

# figureの引数; figsizeで画像のサイズ、dpiで解像度を設定できます。
fig = plt.figure(figsize=(7, 4), dpi=100)
ax = fig.add_subplot(1, 1, 1)

# 線の色や、太さを指定
ax.plot(x, y, color='#02A8F3', linewidth=1.0)

# x軸(xaxis)にアクセスしてフォーマットを調整
ax.xaxis.set_major_formatter(DateFormatter('%m/%d\n%H:%M')) #日付を2段組に

# プロットをグラフの限界まで広げる(余計な空白を作らない)
ax.set_xlim(x[0], x[-1])

# タイトルをつける
ax.set_title('時系列プロット')

plt.show()
../_images/3fa2d02724f669ce753e6381a50f757eb3a2f57f1834a542f87ff61b06745e5e.png

再利用可能とするため、グラフのプロットをまとめる関数を作成します。

import itertools


def plot_figs(df):
    '''
    うけとったデータをすべてプロットする関数
    :param df: plotしたいpandas.Dataframe
    :return: 
    '''
    color_iter = itertools.cycle(['#02A8F3', '#33B490', '#FF5151', '#B967C7'])
    n_figs = df.columns.size
    fig = plt.figure(figsize=(6, 2.0*n_figs), dpi=100)
    x = df.index
    for i in range(n_figs):
        y = df.iloc[:, i]
        title = df.columns[i]
        ax = fig.add_subplot(n_figs, 1, i+1)
        ax.plot(x, y, color=next(color_iter), linewidth=1.0)
        ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
        ax.set_xlim(x[0], x[-1])
        ax.set_title(title, fontsize=12)
    fig.tight_layout()
    plt.show()
    plt.close()

# 全部をプロットする余白がないので、ここでは一部をプロット
plot_list = ['コンプレッサー出口温度_PV', 'コンプレッサー出口温度_SV',
             '気化器液面レベル_PV', '気化器液面レベル_SV', '気化器液面レベル_MV']
plot_figs(df.loc[:, plot_list])
../_images/5a9757fb035f7cc3101d8c893d8a4a6543d11ac08e3b106ea8408fdd98d3b7dd.png

まとめて可視化してみると、これだけでもさまざまな特徴に気づくことができます。例えば、

  • コンプレッサー出口温度_SV は目標値(SV)なので、出力値(PVコンプレッサー出口温度_PV が追従して動いている

  • 気化器液面レベル_PV は8時30分ごろと17時ごろの挙動に変化がある

    • 目標値(SV)である 気化器液面レベル_SV は変化していないが、操作量(MV)である 気化器液面レベル_MV は変化していることから、同時刻に他の目標値(SV)が変更され、その影響を吸収するために操作が行われたことが推察される

などがわかります。データ分析をする際はどのようなデータに対しても可視化は必要不可欠です。

小括#

時系列データの基本的なプロット方法を紹介しました。さまざまある可視化ライブラリの中でも、Pythonにおいてはmatplotlibがデファクトスタンダードになっています。紹介した折れ線グラフ以外にもさまざまなプロットに対応しているため、必要に応じて参照してください。
データの可視化によって、数値データからは読み取れなかったさまざまな特徴や情報を得ることができます。これらの解釈にはドメイン知識が必要となりますが、傾向を把握しておくだけも、解析の際の大きな助けになります。

参考#

  • matplotlib公式

    • Tutorials

    • Examples: 多数の作図例が掲載されており、参考になります。