Pythonで作るIoTデータのダッシュボード(第一回)

導入と目的

新連載:Pythonで作るIoTデータのダッシュボード

IoT(モノのインターネット)ビジネスの多くはスタートアップであり、リソースが限られています。しかし、大企業であっても慎重な支出戦略は決して損ではありません。

データの可視化やリアルタイムストリーミングのツールは、世の中にたくさんあります。しかし、実際には、2分ごとに更新されるべきいくつかのチャートが必要なだけであれば、そのようなものにお金を使う必要はありません。

この記事は、私たちが用意した3つのシリーズのうち、オープニングの1つです。このシリーズでは、GridDBからIoTデータを取り出し、3つの重要なIoTメトリクスを表示するシンプルなPythonアプリの作り方を紹介します。

提供するコードをもとに、好きなだけチャートを作成したり、カスタマイズしたりすることができます。

このシリーズは、以下のチュートリアルで構成されています。

  • ダッシュボード・リフレッシュのスケジューリング
  • Python kivyを使ったシンプルなアプリの構築
  • アプリの完成とデスクトップアイコンの追加

最終的には、小さくても独立したデスクトップ・アプリケーションができあがり、毎朝起動して日中はそれを使い続けることができます。

このアプリはタイムラグを最小限に抑え、ほぼリアルタイムでデータを表示します。しかもあなたは何もしなくてもいいのです。

ユースケースについて

この記事では、ランダムなデータを生成するPythonパッケージで作成されたテストデータセットを使用しています。このデータセットは、あらゆるIoTデバイスから送られてくる標準的な接続データを模倣しています。正確に言うと、そのようなデバイスの内部にインストールされたSIMカードがバックグラウンドで送信するデータです。

接続データは通常、サービスエンジニアやサポートマネージャがトラブルシューティングや異常検知に使用します。

目標について

この記事では、データを照会して、以下の3つのグラフを作成します。

  • データ使用量
  • イベントの数
  • アラートの数

ここでは、「PDPコンテキストの作成」イベントを例に挙げて説明します。PDPコンテキストの詳細については、こちらをご覧ください。

また、このクエリを10分ごとに実行し、それに応じてデータを更新します。

手法について

ここでは、Jupyter Notebookを使用します。最終的なコードをPythonのIDEで実行してもよいでしょう。

前提知識

こちらの記事では、Jupyter NotebookからGridDBを照会する方法を詳しく説明しているのでJupyter Notebookの使い方に詳しくない方はぜひご覧になってください。このチュートリアルでは、Dockerをインストールしてコンテナを作成し、その中でJupyterを実行することで、チュートリアルを進めることができます。

本編

事前準備

データベースからのデータ取得

データベースからデータを取得するために、Pythonパッケージ jaydebeapi を用いてデータベースとの接続を確立し、WHERE 句を用いたSQLベースのクエリを使用します。

ここでは、直近1時間のデータのみを取得するために、GridDBネイティブの関数 TIMESTAMP_ADD() を使用しています。GridDB SQLリファレンスに記載されているように、カスタマイズすることができます。


import pandas as pd
import jaydebeapi

conn = jaydebeapi.connect("com.toshiba.mwcloud.gs.sql.Driver",
                          "jdbc:gs://griddb:20001/defaultCluster/public?notificationMember:127.0.0.1:20001",
                          ["admin", "admin"],
                          "/usr/share/java/gridstore-jdbc-4.5.0.jar",)
iot = ('''SELECT TIMESTAMP(timestamp) as timestamp, event, simid, data_usage
          FROM IoT
          WHERE TIMESTAMP(timestamp) < TIMESTAMP_ADD(HOUR, NOW(), -1)''')
iotdf = pd.read_sql_query(iot, conn)
iotdf.head()
timestamp event simid data_usage
0 2021-01-18 09:39:27.200000 Create PDP context 0003 0.00
1 2021-01-18 09:39:28.200000 data 0003 0.03
2 2021-01-18 09:39:29.200000 Delete PDP context 0003 0.00
3 2021-01-18 09:40:05.200000 Create PDP context 0003 0.00
4 2021-01-18 09:40:06.200000 data 0003 0.03

これは時系列データセットです。タイムスタンプ、このタイムスタンプに割り当てられたイベント、タイムスタンプを送信したSIMカードのID、そして必要に応じて、インターネット転送量(単位はkB)が含まれています。

データのピボット処理

Jupyterでは、データベースでは正しいタイムスタンプ形式であっても、データフレームではtimestampカラムが文字列カラムとして表示されることがあります。

タイムスタンプに変換します。

iotdf['timestamp']= iotdf['timestamp'].apply(pd.to_datetime)

カラム数の少ない、いわゆる「スリム」なデータベースがあります。まず、それを広げていく必要があります。イベントの列をいくつかの列に分割し、イベントの種類ごとに1つの列を作ります。イベントが発生した場合、それぞれの列には値1が入ります。

そして pivot_table() 関数を使って、iotデータフレームをpandasのピボットテーブルに変換します。


pivotdf = iotdf.pivot_table(index=['timestamp', 'simid', 'data_usage'],
                            columns='event', 
                            values= 'event',
                            aggfunc=lambda x: 1)
pivotdf.head()
event Create PDP context Delete PDP context alert data
timestamp simid data_usage
2021-01-18 09:39:27.200 0003 0.00 1.0 NaN NaN NaN
2021-01-18 09:39:28.200 0003 0.03 NaN NaN NaN 1.0
2021-01-18 09:39:29.200 0003 0.00 NaN 1.0 NaN NaN
2021-01-18 09:40:05.200 0003 0.00 1.0 NaN NaN NaN
2021-01-18 09:40:06.200 0003 0.03 NaN NaN NaN 1.0

データフレームには複雑な列名があり、3つの列がインデックスになっているので、これを磨いて通常のデータフレームのように戻します。reset_index() 関数でインデックスを削除します。

また、計算をしたり、実際に時系列データをプロットしたりするためには、NaNをゼロに置き換える必要があります。そこで、fillna() 関数を使います。


pivotdf = pivotdf.reset_index()
pivotdf = pivotdf.fillna(0)
pivotdf.head()
event timestamp simid data_usage Create PDP context Delete PDP context alert data
0 2021-01-18 09:39:27.200 0003 0.00 1.0 0.0 0.0 0.0
1 2021-01-18 09:39:28.200 0003 0.03 0.0 0.0 0.0 1.0
2 2021-01-18 09:39:29.200 0003 0.00 0.0 1.0 0.0 0.0
3 2021-01-18 09:40:05.200 0003 0.00 1.0 0.0 0.0 0.0
4 2021-01-18 09:40:06.200 0003 0.03 0.0 0.0 0.0 1.0

これで、データを可視化する準備が整いました。

グラフ化

デバイスやSIM IDごとに各指標を分割しています。それによって、行動の異常をキャッチすることができるのです。

グループ化されたチャートの作成に関しては、seaborn パッケージが最も高速であることが証明されています。数秒の差で勝負がつきます。これは他の状況ではそれほど重要ではなかったでしょう。しかし、リアルタイムのダッシュボードが必要な場合は、チャートを非常に速く生成する必要があります。

seabornパッケージは、グループ化されたチャートを作成するための複数の機会を提供しています。2つ目のオプションであるフェイスグリッドの代わりに リレーショナルプロット を使用していますが、これはラインチャートとの相性が良いからです。タイムスタンプごとに1つのイベントがあるにもかかわらず、データの連続性を示すには折れ線グラフが適しています。また、X軸の目盛りの回転など、チャートのパラメータをカスタマイズする必要がある場合にも適しています。

以下は、データの使用例です。


import seaborn as sns
import matplotlib.pyplot as plt

g = sns.relplot(
    data=pivotdf, x="timestamp", y="data_usage", col="simid", hue="simid",col_order = ['0001', '0002', '0003', '0004'],
    kind="line", palette="Set2", linewidth=4, zorder=5,
    col_wrap=1, height=2, aspect=7, legend=False,
    )

for simid, ax in g.axes_dict.items():
    ax.text(.8, .85, simid, transform=ax.transAxes, fontweight="bold")

    sns.lineplot(data=pivotdf, x="timestamp", y="data_usage", units="simid",
                 estimator=None, color=".7", linewidth=1, ax=ax,
                )

ax.set_xticks(ax.get_xticks()[::1])
g.set_xticklabels(rotation=90)

g.set_titles("")
g.fig.suptitle('DATA USAGE', horizontalalignment = 'right')
Text(0.5, 0.98, 'DATA USAGE')

自動更新機能の作成

whileループの紹介

そして、最もエキサイティングな部分に到達しました。

必要なのは、私たちの手を煩わせることなく、コードを何度も実行させることです。

この目的のために、whileループを使います。このループの中に、上で述べたことをすべて(さらに2つのグラフを)入れるだけです。そして、それを一度開始します。ほら! Jupyter NotebookまたはPython IDEが開いていて、スクリプトが一度実行されている間は、データを取得し続け、リフレッシュされたチャートを表示します。

最適化のための注意事項

完全なコードを公開する前に、知っておくべきいくつかのベストプラクティスを紹介します。

  • 同じチャートを何度もスクロールするのは絶対に避けたいですよね。この手間を省くために、 clear_output() をループの中で、何よりも先に追加します。
  • 存在しない出力をクリアすることはできませんので、 try/except/pass を使用して、スクリプトの初回実行時に失敗しないようにしてください。
  • オプション:データが少なすぎて適切にレンダリングできないチャートに try/except/pass を使用します。これにより、スクリプトが壊れてエラーメッセージを投げることなく、次のセクションにジャンプすることができます。
  • matplotlib パッケージの各プロットの後に plt.show() 関数を使用してください。そうしないと、コードの最後のプロットだけが表示されます。
  • sleep() 関数を各ループ実行の最後に使用して、「実際の」リアルタイムでのデータベースへの問い合わせが意味をなさない場合、隙間時間を作ります。データベースとCPUの両方を休ませてあげましょう。
  • 最後になりましたが、進行状況を確認したり、トラブルシューティングを行うために、コード全体にカスタムステータスメッセージを追加してください。例えば、print(“got new data”, datetime.now()) を追加して、新しいデータが正常に取得されたことを示すようにします。独自のタイムスタンプを作成するには、datetime パッケージを使用します。

さあ、ソースコードを見てみましょう!

最終版のソースコード


from datetime import datetime
from IPython.display import clear_output

import time
import seaborn as sns
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw
import pandas as pd
import jaydebeapi

conn = jaydebeapi.connect("com.toshiba.mwcloud.gs.sql.Driver",
                          "jdbc:gs://griddb:20001/defaultCluster/public?notificationMember:127.0.0.1:20001",
                          ["admin", "admin"],
                          "/usr/share/java/gridstore-jdbc-4.5.0.jar",)

curs = conn.cursor()

stop = 1
while stop > 0:
    #delete the previous charts if any
    try:
        clear_output(wait=True)
    except:
        pass

    #querying the data for the last hour
    iot = ('''SELECT TIMESTAMP(timestamp) as timestamp, event, simid, data_usage
              FROM IoT 
              WHERE TIMESTAMP(timestamp) > TIMESTAMP_ADD(HOUR, NOW(), -1)''')
    iotdf = pd.read_sql_query(iot, conn)
    print('got new data')
    print(datetime.now())

    #data preparation
    iotdf['timestamp']= iotdf['timestamp'].apply(pd.to_datetime)
    pivotdf = iotdf.pivot_table(index=['timestamp', 'simid', 'data_usage'],
                                columns='event', 
                                values= 'event',
                                aggfunc=lambda x: 1)
    pivotdf = pivotdf.reset_index()    
    pivotdf = pivotdf.fillna(0)

    #data visualization
    order = ['0001', '0002', '0003', '0004']

    #data usage
    g = sns.relplot(
        data=pivotdf,
        x="timestamp", y="data_usage", col="simid", hue="simid",col_order = ['0001', '0002', '0003', '0004'],
        kind="line", palette="Set2", linewidth=4, zorder=5,
        col_wrap=1, height=2, aspect=7, legend=False,
        )

    for simid, ax in g.axes_dict.items():
        ax.text(.8, .85, simid, transform=ax.transAxes, fontweight="bold")
        sns.lineplot(
            data=pivotdf, x="timestamp", y="data_usage", units="simid",
            estimator=None, color=".7", linewidth=1, ax=ax,
            )

    ax.set_xticks(ax.get_xticks()[::1])
    g.set_xticklabels(rotation=90)

    g.set_titles("")
    g.fig.suptitle('DATA USAGE                   ', horizontalalignment = 'right')

    plt.show()

    #number of pdp events
    g = sns.relplot(
        data=pivotdf,
        x="timestamp", y="Create PDP context", col="simid", hue="simid",col_order = ['0001', '0002', '0003', '0004'],
        kind="line", palette="Set2", linewidth=4, zorder=5,
        col_wrap=1, height=2, aspect=7, legend=False,
        )

    for simid, ax in g.axes_dict.items():
        ax.text(.8, .85, simid, transform=ax.transAxes, fontweight="bold")
        sns.lineplot(
            data=pivotdf, x="timestamp", y="Create PDP context", units="simid",
            estimator=None, color=".7", linewidth=1, ax=ax,
        )

    ax.set_xticks(ax.get_xticks()[::1])
    g.set_xticklabels(rotation=90)

    g.set_titles("")
    g.fig.suptitle('NUMBER OF PDP EVENTS                   ', horizontalalignment = 'right')

    plt.show()

    #alerts
    g = sns.relplot(
        data=pivotdf,
        x="timestamp", y="alert", col="simid", hue="simid",col_order = ['0001', '0002', '0003', '0004'],
        kind="line", palette="Set2", linewidth=4, zorder=5,
        col_wrap=1, height=2, aspect=7, legend=False,
        )

    for simid, ax in g.axes_dict.items():
        ax.text(.8, .85, simid, transform=ax.transAxes, fontweight="bold")
        sns.lineplot(
            data=pivotdf, x="timestamp", y="alert", units="simid",
            estimator=None, color=".7", linewidth=1, ax=ax,
            )

    ax.set_xticks(ax.get_xticks()[::1])
    g.set_xticklabels(rotation=90)

    g.set_titles("")
    g.fig.suptitle('NUMBER OF ALERTS                      ', horizontalalignment = 'right')

    plt.show()

    #putting it into sleep for 10 seconds
    print('falling asleep')
    print(datetime.now())
    time.sleep(60)

got new data
2021-01-19 11:46:05.329856

falling asleep
2021-01-19 11:46:18.786249

備考

Jupyterを使いたい場合は、出力部分をマウスでクリックした後、CMD + O(アルファベットのo)で展開することができます。

ちょっと待って!

whileループの終了条件はどうするの? stop の値を減らさなくて良いのでしょうか?ところで、stopはどこから来たのでしょうか?

いいえ、stopの値は減らしません。ループを停止させたいわけではないからですJupyter Notebookを閉じればアプリケーションは停止します。

この条件は任意に決めたものです。ランダムな変数が必要だっただけです。

次回に向けて

IoT接続データを表示するダッシュボードを構築し、10秒ごとに更新するようにしました。次回は、これを小さな独立したアプリケーションに統合してみます。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *