DTW(Dynamic Time Warping、動的時間伸縮法)は、速さや長さが揃わない2つの時系列を、時間軸を伸び縮みさせながら対応づけて距離を測るアルゴリズムです。この記事では、累積コスト行列を数値で埋める例でDTW距離の求め方を確かめ、numpyでの実装、PythonのDTWライブラリ5種の使い方と違いまで扱います。5種は同じ2系列を渡しても2.0・1.414・4.0と別の値を返すため、どの値が何を意味するかも実行結果で示します。なお「DTW」はデトロイト・メトロポリタン・ウェイン郡空港の空港コードでもありますが、ここで扱うのは時系列解析のDTWです。
まとめ:DTWの意味とPythonでの選び方
- DTWは、点どうしの局所コストを動的計画法で累積し、最小になる対応経路(ワーピングパス)の合計を距離とする手法です。長さの違う系列も比べられます。
- 局所コストと集計はライブラリや設定によって異なります。以下は1次元系列を既定設定で比較する場合の説明です。
fastdtwとlibrosaは差の絶対値の和、dtaidistanceとtslearnは二乗差の和の平方根、dtw-pythonの既定は対角の移動を2倍に数えます。距離を比べるときは、局所コスト・ステップの重み・窓の定義を揃えます。 - 新しく書くコードで
fastdtwは選びません。近似のうえ、手元の計測では長さ1,000の系列で厳密計算のdtaidistanceより約15倍遅く、最終リリースは2019年の0.3.4です。 - 速度が要るなら
dtaidistance、機械学習や多変量系列ならtslearn、音声・音楽ならlibrosa、ステップパターンを細かく選びたい解析ならdtw-python(GPL-3.0)を使います。 - 厳密なDTWの計算量はO(NM)です。Sakoe-Chiba bandで探索を対角線の近くに絞りますが、窓幅の数値の意味は
dtaidistanceとtslearnで1つずれます。
DTW(動的時間伸縮法)とは:ユークリッド距離との違い
DTWは、2つの時系列の各点を「同じ時刻どうし」ではなく「形が対応する点どうし」で結び、その対応の取り方を最適化してから距離を測ります。1つの点が相手の複数の点に対応してよいので、テンポの違う同じ動作や、発話速度の違う同じ単語を近いと判定できます。原型はSakoeとChibaが1978年にIEEE Transactions on Acoustics, Speech, and Signal Processingの第26巻第1号で発表した、音声の単語認識のための動的計画法です。
ユークリッド距離は、同じ位置の点どうしを1対1で引き算するため、2系列の長さが同じでなければ計算できず、波形が1点ずれただけでも距離が大きくなります。この記事で使うa = [1, 3, 4, 2, 1](5点)とb = [1, 1, 2, 4, 3, 1](6点)は、bがaより1拍遅れて始まる似た山形ですが、長さが違うのでユークリッド距離はそもそも定義できません。DTWではこの2系列の距離が2.0になります。
形の似方ではなく向きの近さを測りたい場合は、ベクトルの角度を使うコサイン類似度とは?計算式・仕組みと類似検索での実装を実装目線で解説のほうが合います。DTWは時間方向のずれを許して形を比べたいときの手法です。
DTW距離の計算手順:累積コスト行列の数値例
漸化式とワーピングパスの3条件
まずaの各点とbの各点の組み合わせごとに局所コストを求めます。1次元なら差の絶対値が基本です。次に、左上から右下へ累積コスト行列Dを埋めます。
D(i, j) = 局所コスト(i, j) + min( D(i-1, j), D(i, j-1), D(i-1, j-1) )
上・左・左上の3つのうち累積コストが最小のセルから進み、右下の値D(n, m)がDTW距離です。右下から最小のセルをたどり直すと、どの点とどの点を対応づけたかを表すワーピングパスが得られます。パスは、両端どうしを必ず対応させる(境界条件)、時間を逆行しない(単調性)、隣のセルにしか進まない(連続性)の3条件を満たします。
5点と6点の系列で累積コスト行列を埋める例
a = [1, 3, 4, 2, 1]を行、b = [1, 1, 2, 4, 3, 1]を列にして、局所コストを差の絶対値として埋めた累積コスト行列です。太字がワーピングパスです。
| a\b | 1 | 1 | 2 | 4 | 3 | 1 |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 1 | 4 | 6 | 6 |
| 3 | 2 | 2 | 1 | 2 | 2 | 4 |
| 4 | 5 | 5 | 3 | 1 | 2 | 5 |
| 2 | 6 | 6 | 3 | 3 | 2 | 3 |
| 1 | 6 | 6 | 4 | 6 | 4 | 2 |
パスは(0,0)→(0,1)→(1,2)→(2,3)→(3,4)→(4,5)です。aの先頭の1がbの先頭2点にまとめて対応し、これで1拍の遅れを吸収しています。対応した点どうしの差は0・0・1・0・1・0で、合計2がDTW距離です。山の頂点(4と4)は差0で結ばれており、時刻をずらして形を合わせていることが数値から読めます。
PythonによるDTW実装:NumPyでの距離計算とワーピングパス
numpyによるDTW距離の関数
上の漸化式をそのまま書くと次のとおりです。0行目と0列目を無限大にしておくと、境界の場合分けが要りません。
import numpy as np
def dtw_distance(a, b):
n, m = len(a), len(b)
# 0行目と0列目は無限大の番兵、左上だけ0にする
acc = np.full((n + 1, m + 1), np.inf)
acc[0, 0] = 0.0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(a[i - 1] - b[j - 1]) # 局所コスト(差の絶対値)
acc[i, j] = cost + min(acc[i - 1, j], acc[i, j - 1], acc[i - 1, j - 1])
return acc[n, m], acc
a = np.array([1, 3, 4, 2, 1], dtype=float)
b = np.array([1, 1, 2, 4, 3, 1], dtype=float)
distance, acc = dtw_distance(a, b)
print(distance) # 2.0
二重ループなので、長さ1,000どうしでも100万回の計算になります。仕組みの確認用と割り切り、実データには後述のライブラリを使います。NumPyの配列操作に不慣れな場合はNumPyとは?読み方・インストール・基本的な使い方をPythonで解説(NumPy 2.5対応)で基本を確認できます。
ワーピングパスの取り出しと可視化
累積コスト行列の右下から、左上・上・左のうち最小のセルへ戻っていくとパスが取れます。2つの波形を上下にずらして描き、対応点を線で結ぶと伸縮の様子が見えます。
import matplotlib.pyplot as plt
def warping_path(acc):
i, j = acc.shape[0] - 1, acc.shape[1] - 1
path = [(i - 1, j - 1)]
while i > 1 or j > 1:
step = np.argmin([acc[i - 1, j - 1], acc[i - 1, j], acc[i, j - 1]])
if step == 0:
i, j = i - 1, j - 1 # 左上
elif step == 1:
i = i - 1 # 上
else:
j = j - 1 # 左
path.append((i - 1, j - 1))
return path[::-1]
path = warping_path(acc)
print(path) # [(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
offset = 5 # 線が重ならないようにbを上へずらす
plt.plot(a, marker="o", label="a")
plt.plot(b + offset, marker="o", label="b(+5)")
for i, j in path:
plt.plot([i, j], [a[i], b[j] + offset], color="gray", linestyle=":")
plt.legend()
plt.show()
斜めの線は、異なる時点どうしが対応したことを表します。時間方向のずれの大きさは、線の本数だけでなく、対応点の横方向の隔たりで確認します。グラフの体裁や日本語の表示はMatplotlibとは?使い方・インストール・日本語化を3.11対応で解説にまとめています。なお累積コストが同点の経路が複数あるとき、どれを返すかは実装ごとに違います。経路を比べるときは同じライブラリで揃えます。
PythonのDTWライブラリ5種:同じ入力で距離の値が違う理由
前の章のaとbを各ライブラリの既定設定に渡した結果です(2026年10月時点の最新版で実行)。
| ライブラリ | 版 | 局所コスト | 距離の集計 | a, bの結果 | ライセンス |
|---|---|---|---|---|---|
| fastdtw | 0.3.4(2019年) | 差の絶対値 | 和(近似) | 2.0 | MIT |
| dtaidistance | 2.5.1 | 差の二乗 | 和の平方根 | 1.4142 | Apache-2.0 |
| tslearn | 0.10.0 | 差の二乗 | 和の平方根 | 1.4142 | BSD-2-Clause |
| dtw-python | 1.9.0 | 差の絶対値 | 対角を2倍した和 | 4.0 | GPL-3.0以降 |
| librosa | 1.0.0 | 差の絶対値 | 和 | 2.0 | ISC |
経路は同じでも、局所コストを二乗して最後に平方根を取ると√(1²+1²)=1.4142になり、dtw-pythonの既定のステップパターンsymmetric2は斜めの移動の局所コストを2倍に数えるので4.0になります。どれも正しいDTW距離で、定義が違うだけです。したがって、論文やブログの「DTW距離0.5以下を類似とする」といったしきい値を、別のライブラリの出力にそのまま当てはめてはいけません。値を比べるには、前処理・局所コスト・ステップ重み・探索制約・集計方法を揃えます。これらが一致する厳密計算なら、異なるライブラリの出力も比較できます。
SciPyにはDTWの関数がありません。scipy.spatial.distanceが提供するのは点どうしの距離(euclideanやcdistなど)で、DTWでは局所コストの計算に使われます。SciPy全体の位置づけはSciPyとは?読み方・使い方・NumPyとの違いをPythonで解説を参照してください。
fastdtw:dist引数と1次元配列の落とし穴
1次元の系列ならdistを省略します。既定で差の絶対値が局所コストになります。
from fastdtw import fastdtw
from scipy.spatial.distance import euclidean
distance, path = fastdtw(a, b) # 1次元はdistを省略
print(distance) # 2.0
# 各時点がベクトルの系列は2次元配列にしてからdistを渡す
X, Y = a.reshape(-1, 1), b.reshape(-1, 1)
print(fastdtw(X, Y, dist=euclidean)[0]) # 2.0
1次元配列のままdist=euclideanを渡すと、SciPy 1.18.1ではAxisError: axis -1 is out of bounds for array of dimension 0で止まります。古い解説記事に多い書き方なので、エラーが出たらreshape(-1, 1)で2次元にするか、distを外します。
dtaidistance:二乗差の平方根とinner_dist
dtaidistanceの公式ドキュメントでは、dtw.distanceの既定の局所コストは差の二乗で、最後にsqrt(wps[-1, -1])を返すと書かれています。差の絶対値の和で揃えたいときはinner_distを変えます。
from dtaidistance import dtw
print(dtw.distance(a, b)) # 1.4142...
print(dtw.distance(a, b, inner_dist="euclidean")) # 2.0
print(dtw.warping_path(a, b)) # [(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
# distance_fastにはC拡張が必要。利用できない環境ではdtw.distance(a, b)を使う
print(dtw.distance_fast(a, b)) # C実装の高速版。1.4142...
distance_fastはC拡張で動く版で、後の章の計測で最も速かった関数です。こちらはuse_pruning=Trueが既定で、ユークリッド距離を上限にして見込みのない経路を打ち切ります。結果の距離は変わりません。
tslearn:多変量系列と制約の指定
tslearnのdtwは、各時点がベクトルの多変量系列をそのまま受け取ります(時点数×次元の配列)。2系列の長さは違ってよく、次元数だけ揃える必要があります。
from tslearn.metrics import dtw as ts_dtw, dtw_path
print(ts_dtw(a, b)) # 1.4142...
path, score = dtw_path(a, b)
print(path) # [(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
# 2次元の系列(時点数×2)も同じ関数で比べられる
X = np.array([[0, 0], [1, 1], [2, 2]], dtype=float)
Y = np.array([[0, 0], [0, 0], [1, 1], [2, 2]], dtype=float)
print(ts_dtw(X, Y)) # 0.0
k-meansやk近傍法でDTWを使うTimeSeriesKMeansなどの推定器もtslearnにあり、クラスタリングや分類に組み込むならこのライブラリが近道です。距離行列から階層的クラスタリングをする場合の読み方はデンドログラムとは?階層的クラスタリングの見方とクラスタ数の決め方で扱っています。
dtw-python:symmetric2とnormalizedDistance
dtw-pythonはRのdtwパッケージの移植で、ステップパターンや開始・終了点を開放する部分一致などを細かく選べます。import名がdtwなので、dtaidistanceのfrom dtaidistance import dtwと同じファイルで使うと名前が衝突します。
from dtw import dtw, symmetric1
alignment = dtw(a, b)
print(alignment.distance) # 4.0(既定のsymmetric2)
print(alignment.normalizedDistance) # 0.3636...(4.0 ÷ (5 + 6))
print(dtw(a, b, step_pattern=symmetric1).distance) # 2.0
公式のStepPatternの説明どおり、symmetric2の距離は2系列の長さの和N+Mで割って正規化され、normalizedDistanceに入ります。symmetric1を指定すると、ほかのライブラリと同じ差の絶対値の和になります。ライセンスはGPL-3.0以降なので、配布する製品に組み込む場合はソース開示の義務を確認してから採用します。
librosa.sequence.dtw:特徴量行列の向きと逆順のパス
librosaは音声・音楽の特徴量どうしを並べるための関数としてlibrosa.sequence.dtwを持っています。入力は「特徴量の次元×時点数」の行列で、ほかのライブラリと縦横が逆です。
import librosa
D, wp = librosa.sequence.dtw(X=a.reshape(1, -1), Y=b.reshape(1, -1))
print(D[-1, -1]) # 2.0(累積コスト行列の右下)
print(wp[::-1].tolist()) # 終点から始点の順で返るので反転する
戻り値は累積コスト行列Dと経路wpの組で、wpは終点から始点へ並んでいます。MFCCやクロマ特徴量を渡すと、テンポの違う2つの演奏の対応づけに使えます。
fastdtwは速いのか:近似誤差と実行時間の実測
fastdtwの原論文であるSalvadorとChanのToward Accurate Dynamic Time Warping in Linear Time and Space(Intelligent Data Analysis、2007年)は、粗い解像度で求めた経路を細かい解像度で修正する多段階の近似で、半径rが小さな定数ならO(N)になると示しています。一方、WuとKeoghは2020年のFastDTW is approximate and Generally Slower than the Algorithm it Approximatesで、現実的なデータマイニング用途ではfastdtwは厳密なDTWよりかなり遅いと結論づけました。窓で絞った厳密DTWと比べれば、速さでも精度でも利点が無いという主張です。
手元でも確かめました。乱数で作ったランダムウォーク2本を比べ、3回実行した中の最短時間です。条件は、局所コストをどちらも差の絶対値に揃え(dtaidistanceはinner_dist="euclidean")、dtaidistanceは枝刈りを切って距離だけを計算、fastdtwは既定どおり経路も返す、の3点です。環境はcolima上のLinuxコンテナ(Python 3.12・4CPU)で、pipで入ったfastdtw 0.3.4はfastdtw.fastdtwモジュールのPython実装(Cython拡張は未ビルド)、dtaidistance 2.5.1はC拡張が有効でした。
| 関数(計算方式) | 長さ1,000 | 長さ5,000 |
|---|---|---|
| fastdtw radius=1(近似) | 95.9ms | 555.5ms |
| dtaidistance distance_fast 窓なし(厳密) | 6.4ms | 130.0ms |
| dtaidistance distance_fast 窓10%(厳密) | 1.3ms | 25.2ms |
長さ1,000では、窓なしの厳密計算でもdtaidistanceはfastdtwの約15倍速く、窓を10%に絞ると約70倍速くなりました。長さ5,000では窓なしで約4倍、窓10%で約22倍です。fastdtwにはCython版のソースもありますが、この環境のようにPython実装で動くと、近似で計算量を減らしてもC実装の厳密計算に追いつきません。精度も見ておきます。長さ500の系列100組で、厳密な距離(差の絶対値の和)とのずれを測ると次の結果でした。
- radius=1(既定):ずれの中央値4.27%、最大72.11%。厳密値と一致したのは0組。
- radius=10:ずれの中央値0.00%、最大9.63%。73組が一致。
fastdtwの近似は最短でない経路を返すことがあるので、ずれは常に距離が大きくなる方向に出ます。既定の半径では100組すべてで厳密値より大きく、最大で7割増しだったため、近傍探索では近い順の並びが入れ替わる可能性があります。ただし、この計測では順位の入れ替わり自体は検証していません。既存のコードがfastdtwを使っている場合は、dtaidistance.dtw.distance_fast(a, b, inner_dist="euclidean")に置き換えると、同じ差の絶対値の和で厳密な値が速く得られます。Pythonの数値計算をC並みに速くする仕組みに興味があれば、tslearnが内部で使っているNumbaとは?Pythonを高速化するJITコンパイラの使い方とインストールも参考になります。
Sakoe-Chiba bandによる探索範囲の制限とライブラリ別の窓幅
厳密なDTWは、長さNとMの系列でN×M個のセルを埋めるため計算量がO(NM)です。Sakoe-Chiba bandは、ワーピングパスが対角線から一定の幅を超えて離れないように探索範囲を帯状に絞る制約で、計算量はO(N·w)に下がります。「時間のずれは現実的な範囲に収まる」という前提を入れることにもなり、無関係な点どうしの過剰な対応も防げます。
以下の窓幅の式は、2系列の長さが等しい場合の説明です。長さが異なる場合は長さの差に応じて許容帯が広がるため、この式をそのまま使えません。dtaidistanceのwindow=wは|i−j|<w、tslearnのsakoe_chiba_radius=rは|i−j|≦rで、同じ帯を作るにはdtaidistance側を1大きくします。同じ長さ300の2系列で確かめると、tslearnの半径1とdtaidistanceのwindow=2がどちらも450.4606で一致し、dtaidistanceのwindow=1は対角線しか通らないため、ただのユークリッド距離(450.7901)と同じ値になりました。
import numpy as np
from dtaidistance import dtw
from tslearn.metrics import dtw as ts_dtw
rng = np.random.default_rng(0)
x = np.cumsum(rng.normal(size=1000))
y = np.cumsum(rng.normal(size=1000))
d1 = dtw.distance_fast(x, y, window=101) # dtaidistance:|i-j| < 101
d2 = ts_dtw(x, y, global_constraint="sakoe_chiba", sakoe_chiba_radius=100) # tslearn:|i-j| <= 100
print(d1, d2, np.isclose(d1, d2)) # 同じ帯なので一致する(True)
tslearnではglobal_constraint="itakura"とitakura_max_slopeで、平行四辺形の範囲に絞るItakuraの制約も選べます。窓の幅は、想定する最大のずれ(たとえば1拍の遅れが最大0.5秒で、サンプリングが100Hzなら50点)から決めます。系列ごとに値の大きさが違う場合は、DTWの前に平均0・分散1へ揃える正規化をかけないと、形ではなく水準の差を測ってしまいます。正規化や欠損の扱いは時系列データの前処理|欠損補間・リサンプリング・特徴量生成とリーク回避を実装手順で解説で詳しく扱っています。
DTWの応用と使うべきでない場面
DTWの主な応用分野
- 音声認識:発話速度の違う同じ単語の照合。DTWが生まれた分野です。
- 動作・ジェスチャ認識:加速度センサや骨格座標の系列から、テンポの違う同じ動きを見分けます。カメラ映像から手や姿勢の座標を取る方法はMediaPipe(メディアパイプ)の使い方|Python版1.1.0で手・姿勢・顔を検出する手順とmp.solutions移行で解説しています。
- センサーデータの異常検知:正常時の波形との距離が大きい区間を検出します。しきい値の決め方は時系列データの異常検知|手法の選び分けとしきい値・評価・アラート運用を実装目線で解説が参考になります。
- 時系列のクラスタリング:売上や需要の推移を形で分類し、予測モデルを分ける前処理に使います。
DTWは形の似方を測る手法で、将来の値を予測するものではありません。予測そのものが目的なら、Prophet(Meta)とは|時系列予測ライブラリの仕組み・使い方と2026年の開発状況のような予測用のライブラリを使います。DTWでの類型化から需要予測のモデル開発までを業務で進める場合は、AI予測分析開発・需要予測開発としてご相談いただけます。
DTWが向かない場面
次の3つの条件では、DTWそのもの、またはDTWの前処理を見直します。
- 発生時刻そのものに意味があるデータ。ログの時刻照合や、月曜と金曜の違いを見たい売上では、時間を伸縮させて合わせると見たい差が消えます。
- 数万点を超える系列を総当たりで大量に比べる処理。窓を入れても系列の組数×N×wが積み上がるため、特徴量に要約してからユークリッド距離で比べたほうが速く済みます。
- 振幅や水準の違いを区別したい場合の、系列ごとに標準化したDTW。平均0・分散1への標準化が比較したい差を消すため、この前処理は避けます。大きさも比べたいなら、標準化しないDTWと平均値の差を別々に見ます。
よくある質問
「DTW」は何の略ですか?空港のDTWとは違いますか?
時系列解析や機械学習の文脈では、Dynamic Time Warping(動的時間伸縮法)の略です。航空・旅行の文脈では、米国ミシガン州のデトロイト・メトロポリタン・ウェイン郡空港の空港コードがDTWです。この記事は前者の、時系列の類似度を測るアルゴリズムを扱っています。
scipyにDTWの関数はありますか?
ありません。SciPy 1.18のscipy.spatial.distanceが提供するのはeuclideanやcdistなどの点どうしの距離で、DTWでは局所コストの計算に使います。DTW全体を計算するには、dtaidistance・tslearn・dtw-python・librosaなどを使います。
DTW距離は系列の長さで正規化すべきですか?
長さの違う系列を混ぜて比べるなら正規化します。長い系列ほど足し合わせる点が増え、距離が大きく出るためです。dtw-pythonではnormalizedDistance(既定のsymmetric2では距離÷(N+M))を使い、ほかのライブラリでは距離の定義に合わせて正規化を設計します。絶対差の和を経路長Lで割ると、その経路上の平均絶対差になりますが、平均コストを最小化した経路とは限りません。二乗和の平方根を返す場合、経路上のRMS誤差は距離を√Lで割って求めます。この記事のaとbなら、経路上の平均絶対差は2.0÷6点で約0.33です。
fastdtw・dtw-python・tslearn・dtaidistanceのどれを使えばよいですか?
距離を速く大量に計算するならdtaidistance、クラスタリングや分類まで続けるならtslearn、ステップパターンや部分一致を細かく指定したい解析ならdtw-python、音声・音楽ならlibrosaです。fastdtwは近似で、厳密な実装より遅い結果が論文と手元の計測の両方で出ているため、新しいコードでは選びません。
DTW距離がいくつ以下なら「似ている」と判断できますか?
決まった基準値はありません。距離の値はライブラリの定義・系列の長さ・正規化の有無で何倍も変わるためです。比べたい系列の集合の中で距離を計算し、近い順に並べるか、正常データどうしの距離の分布(たとえば95パーセンタイル、上側5%の境界)から候補のしきい値を決め、検証データで誤検知率と見逃し率を確認します。