観測が時間的に等間隔になっていない場合の処理#

入力された時系列データによっては、タイムスタンプが均等な時間間隔になっていない場合があります。未来の予測を行ったり、時間方向の窓を使った分析を行う場合には、データが等間隔にサンプリングされている必要があります。本ページでは非等間隔な時刻インデクスを持つデータを等間隔に変換する方法について考えてみます。

均等でないパターン#

均等でないと言ってもいくつかのパターンが考えられます。

  1. データ収集機機器の仕組み上生ずる細かな誤差が時刻ラベルに出てしまう。

  2. データの抜けが存在する

  3. 上記1,2の組み合わせ

  4. そもそもデータを等間隔(を目指して)で取得していない。(イベント発生時に収集するなど)

ここでは、1 ~ 3 を主な対象とします。 4. については、最終的に想定する解析手法が異なる(時系列としては扱わない、等)はずなので、対象外としますが、実際にデータを取得する間隔と、データ分析時に想定する時間間隔(1分ごとなど)がだいたい同じ場合には使えると思います。

以下、以下の2段階で処理を述べます。

  1. データの抜けだけが存在する場合(基本的に等間隔データ)

    • データサンプリング間隔が分かっている場合

    • データサンプリング間隔が不明の場合

  2. 時刻インデクスのきりが良くない場合

データの抜けだけが存在する場合#

取得したデータは基本的に等間隔であるが、一部の時刻インデクスがかけている場合

  • インデクスを等間隔に作成し直して、抜けの部分に欠測値 NaN を入れる。

# まず、抜けの存在するデータを作成
import random
import pandas as pd
import numpy as np

random.seed(10)
# サンプルデータの作成(長さn)
n_org = 10
n_missing = 3
idx_org = pd.date_range(start="2019-08-01 00:00:00", periods=n_org, freq="S")
idx = random.sample(list(idx_org), n_org - n_missing)
idx = sorted([pd.to_datetime(x) for x in idx])
cols = ["hoge", "fuga"]

nrow = len(idx)
ncol = len(cols)
df = pd.DataFrame(np.random.rand(nrow, ncol), index=idx, columns=cols)

print("オリジナルのデータ")
display(df)
オリジナルのデータ
hoge fuga
2019-08-01 00:00:00 0.359570 0.829707
2019-08-01 00:00:01 0.663091 0.047745
2019-08-01 00:00:03 0.159244 0.411434
2019-08-01 00:00:04 0.497031 0.905746
2019-08-01 00:00:06 0.533238 0.970455
2019-08-01 00:00:08 0.161287 0.675499
2019-08-01 00:00:09 0.064457 0.631000

00:00:02, 00:00:05, 00:00:07 の3時刻のデータが抜けいている

データ間隔がわかっている場合#

  1. pd.date_range() 関数を使って等間隔なインデクスを生成し、空のデータフレームを作成

  2. もとのデータフレームの値を代入

idx_new = pd.date_range(start=df.index[0], end=df.index[-1], freq="S")
nrow, ncol = df.shape

df_new = pd.DataFrame(index=idx_new, columns=df.columns)
df_new.loc[df.index, :] = df
print("整形後データ")
display(df_new)
整形後データ
hoge fuga
2019-08-01 00:00:00 0.35957 0.829707
2019-08-01 00:00:01 0.663091 0.0477446
2019-08-01 00:00:02 NaN NaN
2019-08-01 00:00:03 0.159244 0.411434
2019-08-01 00:00:04 0.497031 0.905746
2019-08-01 00:00:05 NaN NaN
2019-08-01 00:00:06 0.533238 0.970455
2019-08-01 00:00:07 NaN NaN
2019-08-01 00:00:08 0.161287 0.675499
2019-08-01 00:00:09 0.0644568 0.631

pd.date_range()freq 引数で、さまざまな長さの時間間隔を指定することができる。Offsetの指定を参照。 freq='2h20min' などと指定することができます。

データ取得間隔が不明の場合#

大抵の場合は、データ取得間隔の情報が提供されるので不要ですが、情報が不確かな場合は、データの時間間隔の統計量などを見て、何秒(何分)おきに取られているかを特定してから、上記の処理を実施します。

# DatatimeIndex型は差分を計算する diff() が適用できないので、 pd.Series に変換してから処理を行う。
# 度数分布のインデクスが正しく表示されないので `astype('category') でカテゴリ変数化しておく
intervals = pd.Series(df.index).diff().dropna().astype("category")
print("時間間隔のユニーク数")
print(intervals.unique(), "\n")
print("時間間隔の度数分布")
print(intervals.value_counts())
時間間隔のユニーク数
[00:00:01, 00:00:02]
Categories (2, timedelta64[ns]): [00:00:01, 00:00:02] 

時間間隔の度数分布
00:00:02    3
00:00:01    3
dtype: int64

時間間隔が1秒と2秒なので、オリジナルのデータでは、時間間隔1秒でデータが収集されているが、たまに抜けがあり、2秒のものが存在している、などと考える。ここでは、時間間隔=1秒とみなすことにする。

時刻インデクスのきりが良くない場合#

時刻インデクスが、小数点以下無しで秒単位・分単位などでちょうどの時刻になっていない場合があります。もしデータを取得した機器の特性を知ることができれば、その情報をもとに等間隔データの作成を行います。

  1. データ取得は等間隔になされていて時刻も正しく記録されていると考えて良い場合

    • データ取得間隔未満の端数(ミリ秒など)を切り捨てる

  2. データ取得は等間隔になされているが、時刻情報の取得に誤差がある(レアケース)

    • データ取得間隔未満の端数(ミリ秒など)を四捨五入

  3. データ取得間隔自体が等間隔になっていない

    • データ取得時刻が正確だとみなして、値の補間により一定間隔データを得る

データの準備#

# 不均等間隔Timestampサンプルデータ作成用コード

from datetime import timedelta
import pandas as pd
import numpy as np

np.random.seed(20)
n = 10
cols = ["hoge", "fuga"]
ncols = len(cols)
# まず均等間隔
idx_org = pd.date_range(start="2019-08-01 00:00:00", periods=n, freq="S")
# 時刻にランダムな時間間隔を加算・減算して不均等化
idx = [x + timedelta(seconds=0.3 * np.random.randn()) for x in idx_org]
df = pd.DataFrame(np.random.rand(n, ncols), index=idx, columns=cols)
df.index.name = "時刻"
print("不均等timestamp")
display(df)
不均等timestamp
hoge fuga
時刻
2019-08-01 00:00:00.265168 0.775245 0.036664
2019-08-01 00:00:01.058760 0.116694 0.751281
2019-08-01 00:00:02.107261 0.239218 0.254806
2019-08-01 00:00:02.297021 0.857626 0.949779
2019-08-01 00:00:03.674550 0.561687 0.178781
2019-08-01 00:00:05.167909 0.770252 0.492381
2019-08-01 00:00:06.281841 0.631253 0.839498
2019-08-01 00:00:06.706456 0.461039 0.497940
2019-08-01 00:00:08.150929 0.679411 0.650786
2019-08-01 00:00:09.121924 0.268795 0.067325

データ取得間隔の特定(オプション:不明の場合)#

必要に応じて、時間間隔の統計量などを見て、何秒(何分)おきに取られているかを特定します。

# DatatimeIndex型は差分を計算する diff() が適用できないので、 pd.Series に変換してから処理を行う。
# ユニーク数が多く、一定間隔ではない(離散値でない)ので、`value_counts()` は実施しないので、 `astype('category')`も行わない
intervals = pd.Series(df.index).diff().dropna()
print("時間間隔のユニーク数")
print(len(intervals.unique()), "\n")
print(intervals.describe())
時間間隔のユニーク数
9 

count                         9
mean     0 days 00:00:00.984084
std      0 days 00:00:00.451046
min      0 days 00:00:00.189760
25%      0 days 00:00:00.793592
50%      0 days 00:00:01.048501
75%      0 days 00:00:01.377529
max      0 days 00:00:01.493359
Name: 時刻, dtype: object

平均間隔 0.98秒 (サンプル数が少ないので正確で無いが)なので、1秒間隔データ と判断

切り捨て#

1秒未満 を切り捨てる

idx_new = df.index.floor("1S")
df_new = df.copy()
df_new.index = idx_new
print("均一化後(切り捨て:途中経過)")
display(df_new)
均一化後(切り捨て:途中経過)
hoge fuga
時刻
2019-08-01 00:00:00 0.775245 0.036664
2019-08-01 00:00:01 0.116694 0.751281
2019-08-01 00:00:02 0.239218 0.254806
2019-08-01 00:00:02 0.857626 0.949779
2019-08-01 00:00:03 0.561687 0.178781
2019-08-01 00:00:05 0.770252 0.492381
2019-08-01 00:00:06 0.631253 0.839498
2019-08-01 00:00:06 0.461039 0.497940
2019-08-01 00:00:08 0.679411 0.650786
2019-08-01 00:00:09 0.268795 0.067325

この場合、 00:00:02, 00:00:06 が重複しており、00:00:04, 00:00:07 が欠損しています。まず、上記 2. データ取得は等間隔になされているが、時刻情報の取得に誤差がある(レアケース) を疑い、この可能性が考えられれば、切り捨てではなく四捨五入を選んでみます。

四捨五入#

idx_new = df.index.round("1S")
df_new = df.copy()
df_new.index = idx_new
print("均一化後(四捨五入:途中経過)")
display(df_new)
均一化後(四捨五入:途中経過)
hoge fuga
時刻
2019-08-01 00:00:00 0.775245 0.036664
2019-08-01 00:00:01 0.116694 0.751281
2019-08-01 00:00:02 0.239218 0.254806
2019-08-01 00:00:02 0.857626 0.949779
2019-08-01 00:00:04 0.561687 0.178781
2019-08-01 00:00:05 0.770252 0.492381
2019-08-01 00:00:06 0.631253 0.839498
2019-08-01 00:00:07 0.461039 0.497940
2019-08-01 00:00:08 0.679411 0.650786
2019-08-01 00:00:09 0.268795 0.067325

重複・欠損の処理#

上記四捨五入の例では、 00:00:02 が重複、 00:00:03 が欠損となっています。少なくとも重複は都合が悪いので、解消しておきます。 方法としては、

  • オリジナルの時刻が近い方を採用

  • 複数ある値の平均を計算

などが考えられますが、前者は少しコーディングが必要(稿を改めます)なので、ここでは後者を紹介します。欠損については、上記同様 NaN で埋めます。時刻インデクス作成に切り捨てを選んだ場合も必要に応じて本処理を行います。

# 重複インデクスをの平均を計算
tmp_df = df_new.groupby(level=0).mean()

# 欠損行をNaNで埋める(上記と同様)
idx_new = pd.date_range(start=df_new.index[0], end=df_new.index[-1], freq="S")
nrow, ncol = tmp_df.shape
df_new = pd.DataFrame(index=idx_new, columns=tmp_df.columns)
df_new.loc[tmp_df.index, :] = tmp_df
print("均一化後(四捨五入)")
display(df_new)
均一化後(四捨五入)
hoge fuga
2019-08-01 00:00:00 0.775245 0.0366643
2019-08-01 00:00:01 0.116694 0.751281
2019-08-01 00:00:02 0.548422 0.602293
2019-08-01 00:00:03 NaN NaN
2019-08-01 00:00:04 0.561687 0.178781
2019-08-01 00:00:05 0.770252 0.492381
2019-08-01 00:00:06 0.631253 0.839498
2019-08-01 00:00:07 0.461039 0.49794
2019-08-01 00:00:08 0.679411 0.650786
2019-08-01 00:00:09 0.268795 0.0673247

このデータの場合は、00:00:02 のうちの片方は 00:00:03 に入るべきと考えるのが自然ですが、一般の大きなサイズのデータですべての重複について判断するのは困難です。重複行の割合が大きい場合などは、オリジナルの時刻が近い方を採用 等の処理が必要と判断した方が良いかもしれません。この場合の具体的なPythonコードについて稿を改めたいと思います。

値の補間#

上記、 3. データ取得間隔自体が等間隔になっていない が想定される場合、オリジナルの不均等間隔のデータから、等間隔に再配置したインデクスにおける値を(線形)補間により推定する方法もあります。

元のデータに対して、最終的に得たいデータのインデクスを決める。具体的には、開始時刻と長さ、時間間隔。 上記例の場合は、

  • 開始時刻: '2019-08-01 00:00:00'

  • 長さ: 10 (もしくは終了時刻 2019-08-01 00:00:09

  • 時間間隔: 1秒

from datetime import timedelta
import pandas as pd
import numpy as np

n = 10
# 出力データ(1秒ごと)
idx_out = pd.date_range(start="2019-08-01 00:00:00", periods=n, freq="S")
# または、
idx_out = pd.date_range(
    start="2019-08-01 00:00:00", end="2019-08-01 00:00:09", freq="S"
)

# オリジナル・整形後それぞれ、最初の時刻からの経過時間のデータを x, x_out に入れる
# x, y(各カラム) を元に線形補間等によあり x_out における各カラムの値 y_out を推定
idx = df.index
dlt = idx - idx[0]  # オリジナルデータのデルタ
dlt_out = idx_out - idx_out[0]  # 出力データのデルタ
df_new = pd.DataFrame(index=idx_out, columns=df.columns)
x_out = [x.seconds + x.microseconds * 1.0e-6 + x.nanoseconds * 10e-9 for x in dlt_out]
x = [x.seconds + x.microseconds * 1.0e-6 + x.nanoseconds * 10e-9 for x in dlt]

# 列ごとに補間を実施
for c in df.columns:
    y = df[c].values
    y_out = np.interp(x_out, x, y)
    df_new[c] = y_out
print("均一化後(補間)")
display(df_new)
print("オリジナルデータ(参考)")
display(df)
均一化後(補間)
hoge fuga
2019-08-01 00:00:00 0.775245 0.036664
2019-08-01 00:00:01 0.140814 0.653545
2019-08-01 00:00:02 0.753820 0.833121
2019-08-01 00:00:03 0.649636 0.407910
2019-08-01 00:00:04 0.644174 0.302808
2019-08-01 00:00:05 0.758116 0.522688
2019-08-01 00:00:06 0.633334 0.834302
2019-08-01 00:00:07 0.545504 0.557060
2019-08-01 00:00:08 0.631102 0.582141
2019-08-01 00:00:09 0.268795 0.067325
オリジナルデータ(参考)
hoge fuga
時刻
2019-08-01 00:00:00.265168 0.775245 0.036664
2019-08-01 00:00:01.058760 0.116694 0.751281
2019-08-01 00:00:02.107261 0.239218 0.254806
2019-08-01 00:00:02.297021 0.857626 0.949779
2019-08-01 00:00:03.674550 0.561687 0.178781
2019-08-01 00:00:05.167909 0.770252 0.492381
2019-08-01 00:00:06.281841 0.631253 0.839498
2019-08-01 00:00:06.706456 0.461039 0.497940
2019-08-01 00:00:08.150929 0.679411 0.650786
2019-08-01 00:00:09.121924 0.268795 0.067325

補間を用いると、元データの値からは変化してしまうので、特に元データの時刻の精度やデータのインデクス時間間隔の関係には注意が必要です。

小括#

本項では、分析に使う元データ(時系列データ)が、時間的に等間隔になっていない場合に等間隔のデータに変換する方法について扱いました。変換した結果 NaN ばかりになってしまったりする場合や元データと変換後のデータで時間間隔に乖離がある場合などは、そもそも時系列解析手法や時間窓を使った説明変数による回帰分析などの適用には再考が必要かもしれません。変換前後のデータの確認(必要に応じて、時刻インデクスのチェックやプロットによる可視化などを用いて)は実施した方が良さそうです。

参考文献#