観測が時間的に等間隔になっていない場合の処理#
入力された時系列データによっては、タイムスタンプが均等な時間間隔になっていない場合があります。未来の予測を行ったり、時間方向の窓を使った分析を行う場合には、データが等間隔にサンプリングされている必要があります。本ページでは非等間隔な時刻インデクスを持つデータを等間隔に変換する方法について考えてみます。
均等でないパターン#
均等でないと言ってもいくつかのパターンが考えられます。
データ収集機機器の仕組み上生ずる細かな誤差が時刻ラベルに出てしまう。
データの抜けが存在する
上記1,2の組み合わせ
そもそもデータを等間隔(を目指して)で取得していない。(イベント発生時に収集するなど)
ここでは、1 ~ 3 を主な対象とします。 4. については、最終的に想定する解析手法が異なる(時系列としては扱わない、等)はずなので、対象外としますが、実際にデータを取得する間隔と、データ分析時に想定する時間間隔(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時刻のデータが抜けいている
データ間隔がわかっている場合#
pd.date_range() 関数を使って等間隔なインデクスを生成し、空のデータフレームを作成
もとのデータフレームの値を代入
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秒とみなすことにする。
時刻インデクスのきりが良くない場合#
時刻インデクスが、小数点以下無しで秒単位・分単位などでちょうどの時刻になっていない場合があります。もしデータを取得した機器の特性を知ることができれば、その情報をもとに等間隔データの作成を行います。
データ取得は等間隔になされていて時刻も正しく記録されていると考えて良い場合
データ取得間隔未満の端数(ミリ秒など)を切り捨てる
データ取得は等間隔になされているが、時刻情報の取得に誤差がある(レアケース)
データ取得間隔未満の端数(ミリ秒など)を四捨五入
データ取得間隔自体が等間隔になっていない
データ取得時刻が正確だとみなして、値の補間により一定間隔データを得る
データの準備#
# 不均等間隔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
ばかりになってしまったりする場合や元データと変換後のデータで時間間隔に乖離がある場合などは、そもそも時系列解析手法や時間窓を使った説明変数による回帰分析などの適用には再考が必要かもしれません。変換前後のデータの確認(必要に応じて、時刻インデクスのチェックやプロットによる可視化などを用いて)は実施した方が良さそうです。
参考文献#
Time series / date functionality (Pandas マニュアルページ) Pandasの時刻処理方法について詳述されています。