Spatiotemporal traffic data analysis is an emerging area in intelligent transportation systems. In the past few years, data-driven machine learning models have provided new dimensions for understanding real-world data, building data computing paradigm, and supporting real-world applications.
In this blog post, we plan to:
- introduce a publicly available traffic flow data in Seattle, USA,
- design an easy-to-use data set (as a toy example) for traffic flow analysis,
- perform dynamic mode decomposition on the toy example and discuss the interpretation of results.
Seattle Freeway Traffic Speed Data Set
This is a traffic speed data set collected by the inductive loop detectors deployed on freeways in Seattle, USA. The data set is publicly available on GitHub (see https://github.com/zhiyongc/Seattle-Loop-Data). As shown in Figure 1, the freeways include I-5, I-405, I-90, and SR-520. The speed information at a milepost is averaged from multiple loop detectors on the mainlanes in the same direction. In the data set, there are 323 loop detectors. The time resolution of speed information is 5 minutes, meaning that we have 288 time intervals per day or saying 288 data points per day for each loop detectors.

We can take a look at the data file as shown in Figure 2. The speed data is in the form of matrix. The row corresponds to each specific time interval, which is given by the time stamp like 2015–01–01 00:00:00 and 2015–01–01 00:05:00. The column refers to each loop detector ID.

In this blog post, we design a toy example data from this data set, and create a subset as toy_data.npy
. The subset is available at our GitHub repository:
https://github.com/xinychen/transdim/blob/master/datasets/Seattle-data-set/toy_data.npy

Figure 3 shows the speed heatmap of traffic speed data (subset), which is of size 75-by-72 (i.e., 75 loop detectors and 72 time intervals). The subset covers the traffic state of the I-5 freeway during the morning rush hours. As can be seen, from (roughly) loop detector 176 to (roughly) loop detector 196, it shows clear low traffic speeds referring to traffic congestion. In other loop detectors, traffic speeds are relatively high.
To draw this figure as you have prepared the subset, you can try the following Python code:
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
mat = np.load('toy_data.npy')
plt.style.use('bmh')
plt.rcParams['font.family'] = 'Arial'
fig = plt.figure(figsize = (4, 4))
sns.heatmap(mat, cmap = 'YlGnBu', cbar_kws={'label': 'Traffic speed'}, vmin = 0, vmax = 75)
plt.xticks(np.arange(0, 72 + 1, 12), np.arange(4 * 288 + 72, 4 * 288 + 144 + 1, 12), rotation = 0)
plt.yticks(np.arange(0.5, 75.5, 10), np.arange(166, 241, 10), rotation = 0)
plt.show()
Analyze Traffic Speed Time Series
In Figure 3, we take into account a 75-by-72 subset of traffic speed. We can also draw some time series curves of traffic speed. Figure 4 shows three traffic speed time series of loop detectors 186, 196, and 236, respectively. We can see that both loop detectors 186 and 196 with relatively lower traffic speed suffer from traffic congestion.

To draw this figure, please try the following Python code:
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (6, 6))
i = 1
for loc in [185, 195, 235]:
ax = fig.add_subplot(3, 1, i)
plt.plot(mat[loc - 166, :], color = "#006ea3", linewidth = 2.5)
plt.xticks(np.arange(0, 72 + 1, 12),
np.arange(4 * 288 + 72, 4 * 288 + 144 + 1, 12))
if i != 3:
plt.setp(ax.get_xticklabels(), visible = False)
plt.grid(axis = 'both', linestyle = "--", linewidth = 0.1, color = 'gray')
plt.ylabel('Speed')
plt.title('Loop detector {}'.format(loc + 1))
ax.tick_params(direction = "in")
ax.set_xlim([0, 72])
ax.set_ylim([0, 70])
i += 1
plt.show()
Reproduce Dynamic Mode Decomposition (DMD) on the Subset
DMD is a data-driven dimensionality reduction method for time series data. For any multivariate time series data, DMD can compute a set of dynamic modes in which each mode is associated with temporal behaviors. In particular, DMD allows one to interpret temporal behaviors of data with physically meaningful modes [1]. For an in-depth discussion of DMD, please check out [1]. We use the Python code in [1] for the following analysis.
import numpy as np
def DMD(data, r):
"""Dynamic Mode Decomposition (DMD) algorithm."""
## Build data matrices
X1 = data[:, : -1]
X2 = data[:, 1 :]
## Perform singular value decomposition on X1
u, s, v = np.linalg.svd(X1, full_matrices = False)
## Compute the Koopman matrix
A_tilde = u[:, : r].conj().T @ X2 @ v[: r, :].conj().T * np.reciprocal(s[: r])
## Perform eigenvalue decomposition on A_tilde
eigval, eigvec = np.linalg.eig(A_tilde)
## Compute the coefficient matrix
Psi = X2 @ v[: r, :].conj().T @ np.diag(np.reciprocal(s[: r])) @ eigvec
return eigval, eigvec, Psi
In the case, r
is the predefined low rank of DMD. eigval
and eigvec
correspond to the eigenvalues and the eigenvectors of Koopman matrix in DMD. In particular, eigval
can be interpreted as DMD spectrum, appearing as complex conjugate pairs. Psi
refers to the dynamic modes.
DMD Interpretation of the Results
Eigenvalues and DMD Spectrum
Here, we use the DMD function as mentioned above and evaluate the DMD model with rank 5. The Python code is given by
r = 5
eigval, eigvec, Psi = DMD(mat, r)
Then, we use the following Python code to draw the DMD spectrum:
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (4, 4))
ax = fig.add_subplot(1, 1, 1)
ax.set_aspect('equal', adjustable = 'box')
plt.plot(eigval.real, eigval.imag, 'o', color = 'red', markersize = 6)
circle = plt.Circle((0, 0), 1, color = 'blue', linewidth = 2.5, fill = False)
ax.add_patch(circle)
plt.grid(axis = 'both', linestyle = "--", linewidth = 0.1, color = 'gray')
ax.tick_params(direction = "in")
plt.xlabel('Real axis')
plt.ylabel('Imaginary axis')
plt.show()
Figure 5 shows the DMD spectrum of data. Each eigenvalue explains the dynamic behavior of its corresponding dynamic mode. We can interpret eigenvalues as follows [2],
- If the eigenvalue has a non-zero imaginary part, then there is oscillation in the corresponding dynamic mode.
- If the eigenvalue is inside the unit circle, then the dynamic mode is decaying.
- If the eigenvalue is outside the unit circle, then the dynamic mode is growing.
In Figure 5, the dots are close to or on the unit circle. There are four eigenvalues that are inside the unit circle, while one eigenvalue is on the unit circle whose imaginary part is 0. We can also see that two eigenvalue pairs are symmetric in the imaginary axis. The most dynamic modes are oscillatory and decaying. This result is consistent with spatiotemporal patterns of low traffic speeds in the morning rush hours.

Dynamic Modes and Time Dynamics
In DMD model, Psi
refers to the dynamic modes. We can visualize it as in Figure 6. As can be seen, dynamic modes shows the spatial patterns of the example data, and there are significant changes in some loop detectors like from 176 to 196. This is also consistent with the traffic congestion (with low traffic speed) monitored by these loop detectors.

The Python code for drawing this figure is given by
fig = plt.figure(figsize = (2, 4))
sns.heatmap(Psi.real)
plt.xticks(np.arange(0.5, 5.5), np.arange(1, 6), rotation = 0)
plt.yticks(np.arange(0.5, 75.5, 10), np.arange(166, 241, 10), rotation = 0)
plt.show()
To understand the time evolution of the example data, we can reconstruct data by using dynamic modes as shown in Figure 7. Here we define the DMD reconstruction as follows,
def reconstruct_DMD_system(data, r):
T = data.shape[1]
## Build data matrices
X1 = data[:, : -1]
X2 = data[:, 1 :]
## Perform singular value decomposition on X1
u, s, v = np.linalg.svd(X1, full_matrices = False)
## Compute the Koopman matrix
A_tilde = u[:, : r].conj().T @ X2 @ v[: r, :].conj().T * np.reciprocal(s[: r])
## Perform eigenvalue decomposition on A_tilde
eigval, eigvec = np.linalg.eig(A_tilde)
## Compute the coefficient matrix
Psi = X2 @ v[: r, :].conj().T @ np.diag(np.reciprocal(s[: r])) @ eigvec
time_dynamics = np.zeros((r, T), dtype = 'complex')
b = np.linalg.pinv(Psi) @ data[:, 0]
for t in range(T):
time_dynamics[:, t] = np.power(eigval, t + 1) * b
return (Psi @ time_dynamics).real, time_dynamics.real

To draw Figure 7, please use the following Python code:
r = 5
rec_system, time_dynamics = reconstruct_DMD_system(mat, r)
fig = plt.figure(figsize = (4, 4))
sns.heatmap(rec_system, cmap = 'YlGnBu', cbar_kws={'label': 'Traffic speed'}, vmin = 0, vmax = 75)
plt.xticks(np.arange(0, 72 + 1, 12), np.arange(4 * 288 + 72, 4 * 288 + 144 + 1, 12), rotation = 0)
plt.yticks(np.arange(0.5, 75.5, 10), np.arange(166, 241, 10), rotation = 0)
plt.show()
Here, time dynamics corresponding to each dynamic mode is given by Figure 8.

To draw Figure 8, please use the following Python code:
fig = plt.figure(figsize = (10, 6))
for loc in range(1, 6):
ax = fig.add_subplot(2, 3, loc)
plt.plot(time_dynamics[loc - 1, :], color = "#006ea3", linewidth = 2.5)
plt.xticks(np.arange(0, 72 + 1, 12),
np.arange(4 * 288 + 72, 4 * 288 + 144 + 1, 12))
if loc == 1 or loc == 2:
plt.setp(ax.get_xticklabels(), visible = False)
plt.grid(axis = 'both', linestyle = "--", linewidth = 0.1, color = 'gray')
plt.title('Time dynamics of mode {}'.format(loc))
ax.tick_params(direction = "in")
ax.set_xlim([0, 72])
plt.show()
Conclusion
In this blog post, we introduce a toy example of DMD model to the application of spatiotemporal traffic data analysis. DMD should have many interesting applications in spatiotemporal data analysis due to its meaningful interpretation. If you have any suggestion, please let me know! Thank you for your reading this post!
The full Python implementation of this blog post is available at:
https://github.com/xinychen/transdim/blob/master/datasets/Seattle-data-set/toy-examples.ipynb
References
[1] Xinyu Chen (2021). Dynamic mode decomposition for multivariate time series forecasting. Blog post on Medium. https://towardsdatascience.com/dynamic-mode-decomposition-for-multivariate-time-series-forecasting-415d30086b4b
[2] Dynamic mode decomposition in Python. 2016. Blog post. http://www.pyrunner.com/weblog/2016/07/25/dmd-python/