🔍

Jupyter(matplotlib)でセンサデータの分析をする

2023/03/28に公開

自分よくセンサを使ったことする人でセンサを扱うにも得られるデータの分析をしないといけない
今回は自分がよくセンサデータの分析に使っているmatplotlibの使い方について載せておく

matplotlibで何ができるの?

グラフが作れる

データを見れるだけでなく,データ処理の結果も表示できる
言語はpythonを使用しているので,JupyterNotebookで実行する以外にもpython単体でも動きます
なんならvsCodeでJupyter環境構築できます
というかvsCodeでやるのおすすめです

JupyteNotebookで実行するメリット・デメリット

  • メリット

    • ファイル出力せずにグラフを確認できる
      python単体ではグラフの確認は毎回ファイルで出力する必要あるがJupyterNotebookでは不要
      画像で出力されるので右クリDLもできる
    • セルで実行区間を分けれる
      importセル,functionセル,などに分けられる
  • デメリット
    コード補完がない(つらい)
    web上のJupyterNotebookでの話なのでvsCodeのJupyter環境ならデメリットなし

Jupyter環境のインストール

大体3つの方法がある

vsCodeで環境構築

前提条件:VSCodeのインストール,python実行環境
してない人は今すぐインストールするのだ
環境構築
jupyterのインストール$pip install jupyter
できない場合は$pip3 install jupyterを試してみて
VSCodeの拡張機能を追加
↓この辺をインストール

インストールが終わったら適当なファイル名.ipynbを作成する
ターミナルで% python --versionで現在使用しているPythonのバージョンを確認する
①を押し,②で先ほど確認したバージョンと一致するものを選択

後はファイルに以下のコードを記述して実行しておく

import matplotlib

print(matplotlib.matplotlib_fname())
print(matplotlib.get_cachedir())

書く場所と実行方法

後2つはブラウザでやるのがいいのだ人向け

  • anacondaを利用
    anacondaをインストールしてJupyteNotebookを起動するだけ
    matplotlibなどの必要なライブラリが入っているpythonが付いてくる
    これでpythonインストールする方法もある
  • コマンドを使ってインストール・実行
    やったことないので自分で調べてもろて

ライブラリの追加

pip install **でインストール

  • numpy:n次元配列を利用可能
  • pandas:CSVのデータ操作用
  • matplotlib:グラフ関係
  • statistics:平均や分散などが利用可

日本語を利用できるようにする

デフォルト状態では表内に日本語を使おうとすると文字化けします
↓このように

おすすめフォント(IPAexGothic)
ダウンロードしたフォントはmatplotlib.matplotlib_fname()でmatplotlibrcのファイルパスを確認し,
フォントファイル(.ttf)をmatplotlibrcのあるフォルダ(mpl-data)内のfont/ttf内に入れる
フォントの変更方法2種
link

  1. コード内にplt.rcParams['font.family'] = "フォント名"を追加
    ファイル内のみで有効
  2. matplotlibrcの設定を変更
    link
    python自体の設定を変更する方法
    まずはmatplotlibrcを編集
    場所はprint(matplotlib.matplotlib_fname())の場所にある
    260行あたりにfont.family: 〜〜〜があるので,〜〜〜の部分を使いたいフォント名にする
    行の最初に#がある場合は消しておく
    print(matplotlib.get_cachedir())でキャッシュがあるフォルダの場所があるので,中のファイル〜〜〜.jsonを全て削除

matplotlibの基本

勉強用データ
今回使用するCSVファイル
階段(踊場あり)で1階から4階までを登った際の気圧と加速度(重力含まない)のデータ
ヘッダ情報
気圧(study_pressure.csv)

  • time: センサを取得した実時間[ms](ミリ秒,1000ms = 1s)
  • pressure: 気圧データ[hPa]

加速度(study_acc.csv)

  • time: センサを取得した実時間[ms]
  • x: x軸の加速度[m/s^2]
  • y: y軸の加速度[m/s^2]
  • z: z軸の加速度[m/s^2]

基本的なコード

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt

plt.rcParams["font.size"] = 28

fname = "study"
press = pd.read_csv(fname+"_pressure.csv", encoding = 'utf-8')
acc = pd.read_csv(fname+"_acc.csv",encoding = 'utf-8')

fig = plt.figure(figsize=(20, 20))
ax1 = fig.add_subplot(2, 1, 1)
ax1.grid(color='k', linestyle='dotted', linewidth=1, alpha=0.5, zorder=2)
ax1.set_title("気圧値")
ax1.set_xlabel('data[num]')
ax1.set_ylabel('pressure[$hPa$]')
ax1.get_yaxis().get_major_formatter().set_useOffset(False)
ax1.plot(np.arange(0,len(press)),press["pressure"],label="生データ")
ax1.legend()

ax2 = fig.add_subplot(2, 1, 2)
ax2.grid(color='k', linestyle='dotted', linewidth=1, alpha=0.5, zorder=2)
ax2.set_title("加速度")
ax2.set_xlabel('data[num]')
ax2.set_ylabel('accelaration[$m/s^2$]')
x = ax4.plot(np.arange(0,len(acc)),acc["x"],label="x")
y = ax4.plot(np.arange(0,len(acc)),acc["y"],label="y")
z = ax4.plot(np.arange(0,len(acc)),acc["z"],label="z")
ax2.legend(handles = x + y +z)

出力結果

説明

  • importシリーズ
    import numpy as np
    import pandas as pd
    import matplotlib
    import matplotlib.pyplot as plt:グラフ表示用

  • plt.rcParams["font.size"] = フォントサイズ
    フォントサイズを変更
    後で説明するグラフサイズの場合は30くらい

  • csv = pd.read_csv("ファイル名(.csv)", encoding = 'utf-8')
    csvファイル(ヘッダ(項目名)あり)を読み込む
    読み込んだファイルはcsv["ヘッダ"][データ番号]で確認できる
    encoding = 'utf-8'はヘッダ名が日本語の場合は必須
    ヘッダなしの場合csv = pd.read_csv("ファイル名(.csv)", header=None, names=['user_id', 'name'])で可能
    namesの部分でヘッダ名を指定する

  • fig = plt.figure(figsize=(横サイズ, 縦サイズ))
    グラフのサイズを指定

  • ax1 = fig.add_subplot(行数, 列数, 場所番号)
    一つの画像内に複数のグラフを表示させる場合に使用
    変数にそれぞれのグラフを指定してあげる
    以降ax1.~~~が出てくるが,plt.~~~~でもできる(少し関数名が変わるけど)
    場所番号は左上を1として右優先で割り振られている

  • ax1.grid(color='k', linestyle='dotted', linewidth=1, alpha=0.5, zorder=2)
    グラフ内にグリッドを表示

  • ax1.set_title("タイトル")
    グラフのタイトルを指定

  • ax1.set_xlabel('ラベル名'), ax1.set_ylabel('ラベル名')
    x軸・y軸のラベル名を指定

  • ax1.get_yaxis().get_major_formatter().set_useOffset(False)
    y軸の数値のオフセット表示をさせないようにする
    ↓こういうの(y軸)

設定するとこうなる↓

x軸の場合はax1.get_xaxis().get_major_formatter().set_useOffset(False)

  • ax1.plot(x軸データ,y軸データ,label="ラベル名")
    グラフにデータを表示
    他の表示方法としてacc.plot(x="time", ax=ax4)もある
    こちらだとCSV内のデータを全て出力する
  • ax1.legend()
    データのラベルを表示
    ラベル(凡例)について(表示関係の引数の説明付き)
    引数handlesなしの場合はplot時に指定したlabelが表示される
    handlesは個別にラベルを指定する用
    handlesを使う場合をax2でやってる

コードにはないけど知っておくといいもの

  • ax1.set_xlim([x1,x2]), ax1.set_ylim([y1,y2])
    グラフのx1~x2(y1~y2)区間を表示

x軸を時間に変える

基本的なコードだとx軸はデータ番号になっている
そのため気圧と加速度でx軸の範囲が異なる
きちんと対応づけるためにx軸を時間で表示しよう
今回のデータには時間が入っているのでそれを使う

コードの変更

#気圧
ax1.plot(list(range(0,len(press))), press["pressure"], label="生データ")
-> ax1.plot((press["time"]-press["time"][0])/1000, press["pressure"], label="生データ")

#x軸加速度(y・zについても)
ax4.plot(list(range(0,len(acc))), acc["x"], label="x")
-> ax4.plot((acc["time"]-acc["time"][0])/1000, acc["x"], label="x")

#x軸ラベルも変えておこう(ax2も)
ax1.set_xlabel('data[num]') -> ax1.set_xlabel('time[s]')

x軸を最初のデータからの相対時間で表示させています

出力結果

グラフに点をつける

ax1.plot(x, y, marker='o', markersize=10, color='#00FF00')
ax1.scatter(x, y,marker='o', s=100, color = '#00FF00') でも可(少し変わるので注意)
(x, y)に指定した形(marker)の点をつける
markersize(scatterの場合は's',起点の大きさが違うので注意)で大きさ,colorで色を指定
markerの種類

x,yには配列も渡せるけど,plotの場合だと点と点を折れ線グラフで表示してしまう(元々グラフ表示用だから)
scatterのほうはなぜかplotで出した線の下に描画される(なぜ?)
自分は線の上に出したいのでplotを複数回繰り返す方法してる(求:もっといい方法)

背景に色をつける(ラベリング)

ax1.axvspan(x1, x2, color="#009000", alpha=0.3)
x1からx2までの背景を色color・透明度alphaにする
colorはカラーコード指定以外の方法もある link
y軸で指定する場合はax1.axhspan(y1, y2, color="#009000", alpha=0.3)

背景色や点のラベルを作成する

背景色

背景色のラベルを入れた変数を用意してlegendの引数handlesに入れる

from matplotlib.patches import Patch
from matplotlib import patches

legend_marker = [Patch(facecolor='#009000', alpha=0.3, label='緑'), Patch(facecolor='#000090', alpha=0.3, label='青'), Patch(facecolor='#900000', alpha=0.3, label='赤')]

fig.legend(handles=legend_marker)

scatterを生成して引数にlabelを指定してあげるだけ
plotだとグラフのラベルになる

point = ax1.scatter(x, y,marker='o', s=100, color = '#00FF00', label="点")

# 背景色のラベルとの併用方法
fig.legend(handles=legend_marker + [Point])

実際にデータ処理をしてみる

階段を登っている区間をラベリングしてみよう
加速度データでは極値を走査して点で表示して,さらに閾値以上の極大値に別の点をつける

やること

  • 気圧
    • ノイズ除去する
    • 推定・結果表示
  • 加速度
    • 加速度をノルムにする
    • ノイズ除去する
    • 極値走査
    • 閾値処理

加速度:加速度をノルムにする

加速度を使う際に軸方向を考慮しない場合はノルムを使うのが多い.
ノルムとは加速度全体の大きさを表すもの.
ベクトルの合成結果の大きさでもある.
大きさなので値は正になる.
ノルムの式norm = √(x^2 + y^2 + z^2)
pythonだと(x**2 + y**2 + z**2)**0.5で出せる

気圧・加速度:ノイズ除去する

センサデータはたとえ静止状態でも値が微小に変化する(この微小な変化がノイズ).
ノイズが推定に影響を与える場合があるのでノイズをできるだけ除去する.
ノイズ除去にはフィルタを使う.
フィルタはさまざまな種類がある.
今回は移動平均フィルタ(p.20以降)を使う.
pythonではrolling関数がある.
press["pressure"].rolling(window=5).mean()

結果としてほぼ水平部分が滑らかな線になっている(window = 気圧:5,加速度:3).

一部拡大した図

振幅が減衰してるね

気圧:推定・結果表示

気圧センサから階移動区間推定・背景色でラベリング
高さが1m変化すると気圧は0.1hPa変化する.
変化の部分を捉えようとする方法もあるけど,
変化していない部分を先に捉えて,それ以外の部分を階段使用中とする方法で行う.

変化していない部分をラベリングした結果(閾値:0.05hPa,3s)

後はラベリングしていない区間を階段としてやれば完了

最終的な階移動区間推定のラベリング結果

コード例

pressFlat = []# 変化していない部分の保存用
iBefore = 0# 比較するデータ番号

for i in range(5,len(lowPass)):
    if i == 5: iBefore = i # 比較するデータ番号を保存
    if abs(lowPass[i]-lowPass[iBefore]) > 0.05:
        # 現在のデータと比較し,気圧が0.05hPa以上の時
        # 図にラベリングする x軸の区間
        first = (press["time"][iBefore] - firstPressTime)/1000
        final = (press["time"][i] - firstPressTime)/1000
        if final - first > 3:
            # x軸の区間が3秒以上の時
            pressFlat.append([first,final])# 変化していない部分を保存
            ax1.axvspan(first,final, color="#009000", alpha=0.3)# 変化していない部分をラベリング
        iBefore = i# 比較するデータ番号を変更

# 階段利用区間をラベリング
for i in range(1,len(pressFlat)):
    ax1.axvspan(pressFlat[i-1][1],pressFlat[i][0], color="#900000", alpha=0.3)

加速度:極値走査

極値かの確認方法はある値が前後より大きいor小さい値がなければ極値になる

結果

コード例

maxd = [] #極大値リスト, [時間(x軸), 加速度(y軸)]
mind = [] #極小値リスト
n = 1 #極値走査時に見る前後のデータ数
#極値走査
for i in range(3+n,len(lowNorm)-n): 
    time = (acc["time"][i] - firstAccTime)/1000 #現在データの時間情報(x軸)
    maxdF = True #極大値かどうか
    mindF = True #極小値かどうか
    #現在データが極値かどうか
    for j in range(i-n, i+n+1):
        if lowNorm[i] < lowNorm[j]: maxdF = False #現在データより値が大きいデータがあるので極大値でない
        if lowNorm[i] > lowNorm[j]: mindF = False #現在データより値が小さいデータがあるので極小値でない
    if maxdF: maxd.append([time, lowNorm[i]]) #極大値ならリストに追加
    if mindF: mind.append([time, lowNorm[i]]) #極小値ならリストに追加

for i in maxd: #極大値を表示
    ax2.plot(i[0], i[1], marker='^', markersize=10, color='#FF0000')
for i in mind: #極小値を表示
    ax2.plot(i[0], i[1], marker='v', markersize=10, color='#0000FF')

#凡例用
maxdP = ax2.scatter(maxd[0][0], maxd[0][1], marker='^', s=100, color='#FF0000', label="極大値")
mindP = ax2.scatter(mind[0][0], mind[0][1], marker='v', s=100, color='#0000FF', label="極小値")
ax2.legend(handles=[maxdP, mindP])

加速度:閾値処理

極大値のリストから閾値処理するだけ

結果

コード例

for i in maxd: #極大値を表示
    ax2.plot(i[0], i[1], marker='^', markersize=10, color='#FF0000')
    if i[1] >= 5.0: ax2.plot(i[0], i[1], marker='o', markersize=10, color='#00FF00') #追加
    
#凡例用
thP = ax2.scatter(stepPoint[0][0], stepPoint[0][1], marker='o', s=100, color='#00FF00', label="5m/s^2以上の極大値") #追加
ax2.legend(handles=[maxdP, mindP, thP]) #変更

おまけ

グラフアニメーションを作成する方法
音のグラフを見たいときに使う・使った
ffmpegのインストール必要

fig = plt.figure(figsize=(20, 15))
ax1 = fig.add_subplot(1, 1, 1)
ax1.grid(color='k', linestyle='dotted', linewidth=1, alpha=0.5, zorder=2)
ax1.get_yaxis().get_major_formatter().set_useOffset(False)
ax1.set_facecolor("#EEEEEE")

ax1.set_title(f"audio: time={time[0]}")
ax1.set_xlabel('data[$num$]')
ax1.set_ylabel('amplitude')
ax1.set_xlim([-1.0,len(buffer[0])+1.0])
ax1.set_ylim([-30000,30000])
line1, = ax1.plot(range(len(buffer[0])), buffer[0])
def update1(i):
    ax1.set_title(f"audio: time={time[i]}")  # グラフタイトル
    line1.set_data(range(len(buffer[i])), buffer[i])   # グラフ線の設定と描画
    # 処理状況の表示
    print('\r {}/{}'.format(str(i+1), str(len(buffer))), end='')

ani = animation.FuncAnimation(fig, update1, interval=40, frames=len(buffer))
ani.save(f"{fname}.mp4", writer="ffmpeg", dpi=200)

csvの書き出し・追加

いろいろなグラフの書き方

棒グラフの書き方
箱ひげ図の書き方
エラーバー付きの点
エラーバー付きの棒グラフ
エラーバーは標準偏差を表すのに使ったりする

グラフをコード内で保存させる

fig.savefig("ファイル名.png")
拡張子は画像系やpdfを指定できる
この方法で保存したpngは背景が透明になっている
(VSCodeの結果表示から保存すると背景が白になる)

複数のグラフを1つのpdfファイルにまとめたい場合

from matplotlib.backends.backend_pdf import PdfPages

pdf = PdfPages("ファイル名.pdf")
pdf.savefig(fig1)
pdf.savefig(fig2)
pdf.close()

Discussion