結論:欠測を残したままNaNかmaskで線を切る
観測していない区間を線で結びたくないときは、時刻の並びを保ったまま値をNaNにするか、マスク付き配列を使う。欠測行を丸ごと除いてから折れ線を描くと、残った点が隣同士になり、停止前後を一本の線で結んでしまう。直線で補間したつもりがなくても、図を見た人には連続して測定したように見えるので注意したい。
例えば1秒ごとの観測で4・5・6秒のデータがないなら、その3つの時刻を欠測として残す。0で埋めると「ゼロを観測した」という別の意味になり、平均や最小値にも影響する。線を切る処理は欠測の表示であり、値の推定ではない。補間して解析する必要がある場合は、元の観測点と推定した点が区別できるように別の系列として扱うとよい。
そのまま動かせる例
以下では10個の予定時刻のうち3点を欠測扱いにする。同じデータから、欠測点を削除した図、NaNを代入した図、マスクで隠した図を作る。灰色の帯は欠測時刻の範囲を示す補助表示である。元の配列は説明のため値を持っているが、マスクで指定した値を測定値として解釈したり解析へ使ったりしない。
例全体を保存して実行するとmissing-gaps.pngを作る。NumPyとMatplotlibが必要である。出力先は現在の作業フォルダーで、同名ファイルは上書きされる。3パネルは同じ縦軸と横軸を共有しているため、表示範囲の違いではなく欠測の扱いだけを比較できる。
import numpy as np
import matplotlib.pyplot as plt
t = np.arange(10)
y = np.array([1., 2., 3., 4., 5., 6., 7., 6., 5., 4.])
missing = (t >= 4) & (t <= 6)
y_nan = y.copy()
y_nan[missing] = np.nan
y_mask = np.ma.array(y, mask=missing)
fig, axs = plt.subplots(3, 1, figsize=(7.2, 6.4), sharex=True,
sharey=True, layout="constrained")
try:
axs[0].plot(t[~missing], y[~missing], "o-", color="#ac5435")
axs[0].set_title("Removed rows: a line crosses the gap")
nan_line, = axs[1].plot(t, y_nan, "o-", color="#215f91")
axs[1].set_title("NaN: the line is broken")
axs[2].plot(t, y_mask, "o-", color="#3b8068")
axs[2].set_title("Mask: the line is broken")
for ax in axs:
ax.axvspan(3.5, 6.5, color="0.9")
ax.set(ylabel="Signal (a.u.)", ylim=(0, 8))
ax.grid(alpha=0.2)
axs[-1].set(xlabel="Time (s)", xticks=t)
assert len(nan_line.get_xdata()) == len(t)
assert np.isnan(nan_line.get_ydata()).sum() == 3
assert np.ma.getmaskarray(y_mask).sum() == 3
print("scheduled points:", len(t))
print("observed:", int((~missing).sum()))
print("missing:", int(missing.sum()))
fig.savefig("missing-gaps.png", dpi=160)
finally:
plt.close(fig)
print("saved: missing-gaps.png")
実行結果
scheduled points: 10
observed: 7
missing: 3
saved: missing-gaps.png

削除と欠測表示の違い
上段では3秒と7秒が直接つながる。中段・下段は欠測部の前後で線が切れ、そこでマーカーも描かれない。出力の予定点数10に対して観測済みが7、欠測が3となることも確認できる。折れ線のx座標を10個のまま保持しているので、時刻と配列位置との対応が途中でずれない。欠測箇所を含む他の測定チャネルとの比較にも都合がよい。
NaNを代入するには通常は浮動小数点型の配列が必要である。整数配列へそのまま代入せず、必要に応じて浮動小数点へ変換する。一方、maskは元の値と有効・無効の情報を別に持つ。元データの保持に便利だが、後で通常の配列へ変換したり別ライブラリへ渡したりするとマスクの意味が引き継がれるとは限らない。解析側でも欠測条件を明示しておきたい。
処理用データと表示用データを分ける
この例でy.copy()を作ってからNaNを代入しているのは、元の配列を後の処理で意図せず変更しないためである。作図だけを目的とするマスクと、解析から除外する品質フラグは、必要に応じて別の変数として管理できる。異常値を隠す場合も、どの規則で除外したかを記録しよう。図から外した点の件数を表示しておけば、前処理条件を変えた際に急にデータが減ったことへ気付きやすい。読みやすい図と追跡できる除外条件の両方を残すのが基本である。
実際の時系列で見落としやすい点
ログに欠測行そのものが存在しない場合、単に読み込んだ値をNaNへ変えるだけでは空白を作れない。想定される時間間隔の格子へそろえるか、大きな時間差の位置に区切りを追加する必要がある。ただし不規則サンプリングでは、間隔が広いだけで欠測と断定できない。装置の取得周期、停止記録、許容する最大間隔をもとに判定しよう。
欠測の直前と直後を線で結ぶこと自体が目的に合う図もある。離れた観測を補助線でつないで傾向を示す場合などである。そのときは線が観測を保証するものではないと説明する。独立な観測値しか示す必要がなければ、線のない散布図も選べる。描画方法はデータの取り方と読み手に伝えたい主張へ合わせる。
欠測点が末尾だけにあると、線を切っても横軸の自動範囲から期間が読み取りにくい場合がある。観測予定期間を軸の範囲として明示したり、停止区間を帯で示したりすると分かりやすい。ただし帯の開始・終了境界は推定で決めず、記録や取得間隔に合わせる。この例の3.5〜6.5秒は整数時刻のセル境界であり、装置の実際の停止時刻を表すものではない。
確認環境と参考資料
例はLinux・CPython 3.12.14・NumPy 2.3.5・Matplotlib 3.10.8(Agg)で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。
