时间序列缺失数据插补-处理缺失数据
时间序列缺失数据插补:处理缺失数据

来源:DALL-E。
时间序列分析中常见的一个问题是缺失数据。
正如我们在第一部分中看到的,简单的插补技术或基于回归的模型,如线性回归和决策树,可以让我们走得很远。
但如果我们需要处理更微妙模式并捕捉复杂时间序列数据中的细微波动呢?
在这篇文章中,我们将探讨如何使用神经网络来插补缺失值。
神经网络(NN)的优势在于其捕捉数据中非线性模式和交互的能力。尽管神经网络通常计算成本较高,但它们可以提供一种非常有效的方法来插补缺失的时间序列数据,在简单模型失败的情况下。
我们将与第一部分和第二部分相同的 dataset 进行工作,其中包含 10%的缺失值,这些缺失值随机引入到模拟能源生产 dataset 中。
不要错过本系列的第一部分:
以及第二部分,其中我们使用 KNN:
你好!
我的名字是Sara Nóbrega,我是一名数据科学家,专注于人工智能工程。我拥有物理学硕士学位,后来转型进入了令人兴奋的数据科学领域。
我写关于数据科学、人工智能和数据科学职业建议的文章。确保关注我和订阅,以便在发布下一篇文章时收到更新!
目录
-
数据笔记:模拟能源生产 dataset
-
为什么和何时使用非线性机器学习进行插补?
-
第二部分:时间序列插补的神经网络 3.1 统计比较 3.1.1 自相关、STL 分解和残差分析
-
神经网络的潜在局限性
-
何时不使用神经网络进行时间序列插补
数据笔记:模拟能源生产 dataset
在第一部分,我们使用了 2023 年 1 月 1 日至 2023 年 3 月 1 日之间每 10 分钟间隔的能源生产模拟数据集。为了使案例更真实,我们取了大约 10%的数据点作为缺失值进行估计。
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
# Generate the mock energy production data
start_date = datetime(2023, 1, 1)
end_date = datetime(2023, 3, 1)
datetime_index = pd.date_range(start=start_date, end=end_date, freq='10T')
# Create energy production values with day-night cycles
np.random.seed(42)
base_energy = []
for dt in datetime_index:
hour = dt.hour
if 6 <= hour <= 18:
energy = np.random.normal(loc=300, scale=30)
else:
energy = np.random.normal(loc=50, scale=15)
base_energy.append(energy)
energy_production = pd.Series(base_energy)
# Introduce missing values
num_missing = int(0.1 * len(energy_production))
missing_indices = np.random.choice(len(energy_production), num_missing, replace=False)
energy_production.iloc[missing_indices] = np.nan
mock_energy_data_with_missing = pd.DataFrame({
'Datetime': datetime_index,
'Energy_Production': energy_production
})
# Reset index for easier handling
data_with_index = mock_energy_data_with_missing.reset_index()
data_with_index['Time_Index'] = np.arange(len(data_with_index)) # Add time-based index
plt.figure(figsize=(14, 7))
plt.plot(mock_energy_data_with_missing['Datetime'], mock_energy_data_with_missing['Energy_Production'],
label='Energy Production (With Missing)', color='blue', alpha=0.7)
plt.scatter(mock_energy_data_with_missing['Datetime'], mock_energy_data_with_missing['Energy_Production'],
c=mock_energy_data_with_missing['Energy_Production'].isna(), cmap='coolwarm',
label='Missing Values', s=10) # Reduced size of the markers
plt.title('Mock Energy Production Data with Missing Values (10-Minute Intervals)')
plt.xlabel('Datetime')
plt.ylabel('Energy Production')
plt.legend()
plt.grid(True)
plt.show()

图 1:带有缺失值的模拟能源生产数据。| 图像由作者提供。
处理时间序列数据?那么你需要掌握关键技术来精通其分析:
时间序列估计的神经网络
将实现一个简单的前馈神经网络来预测和估计缺失值。网络使用输入周期性特征,如小时和星期几,来学习时间相关趋势中的时间模式。
步骤:
-
特征工程:形成神经网络可以从中学习时间模式的有关特征。
-
数据分割:准备训练数据,排除带有缺失值的行。
-
模型训练:训练一个前馈神经网络来预测能源生产。
-
估计:使用上一步训练的模型来估计缺失值。
让我们开始:
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from tensorflow.keras.callbacks import EarlyStopping
import random
# Set seed for reproducibility
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
tf.random.set_seed(SEED)
# Step 1: Feature Engineering
data_nn = data_with_index.copy()
# Extract hour, day of week, and encode cyclical features
data_nn['Hour'] = data_nn['Datetime'].dt.hour
data_nn['DayOfWeek'] = data_nn['Datetime'].dt.dayofweek
# Cyclical encoding for 'Hour'
data_nn['Hour_Sin'] = np.sin(2 * np.pi * data_nn['Hour'] / 24)
data_nn['Hour_Cos'] = np.cos(2 * np.pi * data_nn['Hour'] / 24)
# Cyclical encoding for 'DayOfWeek'
data_nn['Day_Sin'] = np.sin(2 * np.pi * data_nn['DayOfWeek'] / 7)
data_nn['Day_Cos'] = np.cos(2 * np.pi * data_nn['DayOfWeek'] / 7)
# Step 2: Prepare Features and Target
feature_columns = ['Hour_Sin', 'Hour_Cos', 'Day_Sin', 'Day_Cos']
data_nn = data_nn.dropna() # Remove rows with missing target values
features = data_nn[feature_columns]
target = data_nn['Energy_Production']
# Standardize the features
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
# Step 3: Split Data
X_train, X_test, y_train, y_test = train_test_split(features_scaled, target, test_size=0.2, random_state=42)
# Step 4: Define Neural Network Model
model = tf.keras.Sequential([
tf.keras.layers.Dense(64, activation='relu', input_shape=(X_train.shape[1],)),
tf.keras.layers.Dense(32, activation='relu'),
tf.keras.layers.Dense(1) # Single output
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
# Step 5: Train the Model
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=30, batch_size=32, validation_data=(X_test, y_test), callbacks=[early_stopping])
# Step 6: Imputation
# Predict missing values using the trained model
data_with_missing = data_with_index.copy()
# Extract hour and day of week (redo feature engineering)
data_with_missing['Hour'] = data_with_missing['Datetime'].dt.hour
data_with_missing['DayOfWeek'] = data_with_missing['Datetime'].dt.dayofweek
# Cyclical encoding for 'Hour'
data_with_missing['Hour_Sin'] = np.sin(2 * np.pi * data_with_missing['Hour'] / 24)
data_with_missing['Hour_Cos'] = np.cos(2 * np.pi * data_with_missing['Hour'] / 24)
# Cyclical encoding for 'DayOfWeek'
data_with_missing['Day_Sin'] = np.sin(2 * np.pi * data_with_missing['DayOfWeek'] / 7)
data_with_missing['Day_Cos'] = np.cos(2 * np.pi * data_with_missing['DayOfWeek'] / 7)
# Select missing features for prediction
missing_features = data_with_missing.loc[data_with_missing['Energy_Production'].isna(), feature_columns]
missing_features_scaled = scaler.transform(missing_features)
# Predict missing values
predicted_values = model.predict(missing_features_scaled)
data_with_missing.loc[data_with_missing['Energy_Production'].isna(), 'Energy_Production'] = predicted_values
Epoch 1/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 47289.5938 - mae: 177.2104 - val_loss: 31623.6465 - val_mae: 141.5553
Epoch 2/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 17872.7188 - mae: 100.2324 - val_loss: 4078.6731 - val_mae: 50.8379
Epoch 3/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 3836.3140 - mae: 49.4058 - val_loss: 3911.0200 - val_mae: 49.2663
Epoch 4/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 3654.9775 - mae: 47.6395 - val_loss: 3804.6765 - val_mae: 48.3873
Epoch 5/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 3534.4617 - mae: 46.6192 - val_loss: 3656.6111 - val_mae: 47.2733
Epoch 6/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 3378.8108 - mae: 45.3782 - val_loss: 3461.5427 - val_mae: 45.8035
Epoch 7/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 3182.7766 - mae: 43.7996 - val_loss: 3222.8591 - val_mae: 43.9219
Epoch 8/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 2943.5549 - mae: 41.8146 - val_loss: 2940.5742 - val_mae: 41.6142
Epoch 9/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 2667.7310 - mae: 39.4235 - val_loss: 2621.4626 - val_mae: 38.9475
Epoch 10/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 2357.8921 - mae: 36.7026 - val_loss: 2270.7549 - val_mae: 35.9953
Epoch 11/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 2022.3892 - mae: 33.7252 - val_loss: 1904.5137 - val_mae: 32.9176
Epoch 12/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 1672.1134 - mae: 30.6480 - val_loss: 1536.0582 - val_mae: 29.7249
Epoch 13/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 1329.0890 - mae: 27.5651 - val_loss: 1227.2889 - val_mae: 26.9015
Epoch 14/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 1055.4308 - mae: 24.8727 - val_loss: 1003.9343 - val_mae: 24.5620
Epoch 15/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 865.8824 - mae: 22.7478 - val_loss: 867.3121 - val_mae: 22.8818
Epoch 16/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 756.9211 - mae: 21.3564 - val_loss: 802.0678 - val_mae: 21.9702
Epoch 17/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 706.9869 - mae: 20.6465 - val_loss: 770.4452 - val_mae: 21.4972
Epoch 18/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 680.2199 - mae: 20.2274 - val_loss: 750.3719 - val_mae: 21.1738
Epoch 19/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 662.4779 - mae: 19.9081 - val_loss: 736.6984 - val_mae: 20.9330
Epoch 20/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 648.1115 - mae: 19.6183 - val_loss: 725.9727 - val_mae: 20.7376
Epoch 21/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 635.9745 - mae: 19.4054 - val_loss: 715.5459 - val_mae: 20.5475
Epoch 22/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 627.1302 - mae: 19.2404 - val_loss: 708.2336 - val_mae: 20.4228
Epoch 23/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 620.2661 - mae: 19.1281 - val_loss: 701.4677 - val_mae: 20.3177
Epoch 24/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 614.5076 - mae: 19.0311 - val_loss: 695.7640 - val_mae: 20.2231
Epoch 25/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 609.0852 - mae: 18.9352 - val_loss: 689.9358 - val_mae: 20.1252
Epoch 26/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 603.5502 - mae: 18.8279 - val_loss: 684.2870 - val_mae: 20.0308
Epoch 27/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 598.4345 - mae: 18.7273 - val_loss: 679.2006 - val_mae: 19.9456
Epoch 28/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 591.7822 - mae: 18.6190 - val_loss: 675.0480 - val_mae: 19.8816
Epoch 29/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 590.4954 - mae: 18.5747 - val_loss: 671.3280 - val_mae: 19.8238
Epoch 30/30
192/192 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 587.6835 - mae: 18.5242 - val_loss: 668.3542 - val_mae: 19.7750
27/27 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
# Plot the first 100 data points for comparison
plt.figure(figsize=(14, 7))
# Original data with missing values
plt.plot(
data_with_missing['Datetime'][:200],
data_with_missing['Energy_Production'][:200],
label='Imputed Data (NN)',
linestyle='-',
color='blue',
alpha=0.8,
)
# Original data with missing values (plotted last to appear in the foreground)
plt.plot(
mock_energy_data_with_missing['Datetime'][:200],
mock_energy_data_with_missing['Energy_Production'][:200],
label='Original Data (With Missing)',
linestyle='--',
color='red',
alpha=0.7,
)
plt.title('Comparison of Original and NN-Imputed Data (First 100 Data Points)')
plt.xlabel('Datetime')
plt.ylabel('Energy Production')
plt.legend()
plt.grid(True)
plt.show()

比较线图显示原始能源生产数据(带有缺失值)用红色表示,NN 估计数据用蓝色表示 | 图像由作者提供。
在这里,我选择展示估计数据的第一个 100 个点。
注意蓝色估计数据中的间隙连续性是如何与红色虚线表示的原始数据平滑地填充的:缺失点和现有点顺利地结合在一起;估计值与观测值很好地拟合在一起。
这是一个好迹象:这种详细的比较验证了估计质量,尤其是在数据的小部分中。
统计比较
让我们比较它们的统计数据:
original_stats = mock_energy_data_with_missing['Energy_Production'].describe()
nn_stats = data_with_missing['Energy_Production'].describe()
stats_comparison_nn = pd.DataFrame({
'Metric': original_stats.index,
'Original Data': original_stats.values,
'Imputed Data (NN)': nn_stats.values
})
stats_comparison_nn

统计比较 | 图像由作者提供
估计数据的平均值和标准差非常接近原始数据;这意味着在中心趋势和变异性方面存在一致性。
值的范围保持不变,暗示没有引入不切实际的反常极端值。
这些统计数据的相似性意味着 NN估计的数据与原始数据相比在分布上高度相似,因此不会破坏数据集的完整性。
自相关、STL 分解和残差分析
我们将应用自相关函数(ACF)、STL 分解来评估估计数据在潜在的季节模式、趋势和自相关中的表现。
ACF 比较函数
import statsmodels.api as sm
from statsmodels.tsa.seasonal import seasonal_decompose
def plot_acf_comparison(original_series, imputed_series, lags=50):
plt.figure(figsize=(14, 5))
# Original Data ACF (using linear interpolation to handle missing values)
original_interpolated = original_series.interpolate(method='linear')
plt.subplot(1, 2, 1)
sm.graphics.tsa.plot_acf(original_interpolated, lags=lags, ax=plt.gca(),
title="ACF of Original Data (Interpolated)")
plt.grid(True)
# Imputed Data ACF
plt.subplot(1, 2, 2)
sm.graphics.tsa.plot_acf(imputed_series, lags=lags, ax=plt.gca(),
title="ACF of NN-Imputed Data")
plt.grid(True)
plt.tight_layout()
plt.show()
# Perform ACF Comparison
plot_acf_comparison(
mock_energy_data_with_missing['Energy_Production'],
data_with_missing['Energy_Production']
)

并排显示比较原始(插值)和 NN 补全数据集的时间依赖性的 ACF 图。两个图表都显示了相似的周期性模式,验证了时间依赖性的保留。| 图像由作者提供
我们可以看到,两个图表显示的图案非常相似,证实了补全确实保留了时间依赖性!
从这些图表中,我们可以得出结论,补全的 NN 数据集很好地捕捉了原始数据的周期性。
# Define the seasonal period
# Since data is at 10-minute intervals, and there are 144 intervals in a day (144 * 10 minutes = 1440 minutes = 24 hours)
seasonal_period = 144
# Handle missing values in original data by linear interpolation for decomposition
original_interpolated = mock_energy_data_with_missing['Energy_Production'].interpolate(method='linear')
# Perform STL Decomposition
original_decompose = seasonal_decompose(original_interpolated, model='additive', period=seasonal_period)
nn_decompose = seasonal_decompose(data_with_missing['Energy_Production'], model='additive', period=seasonal_period)
# Visualization of STL Decomposition Components
# Plot Trend Comparison
plt.figure(figsize=(14, 5))
plt.plot(nn_decompose.trend, label='NN-Imputed Trend', color='green')
plt.plot(original_decompose.trend, label='Original Interpolated Trend', color='orange', alpha=0.7)
plt.title('Trend Comparison: Original vs. NN-Imputed Data')
plt.xlabel('Datetime')
plt.ylabel('Energy Production Trend')
plt.legend()
plt.grid(True)
plt.show()
# Plot Seasonal Comparison
plt.figure(figsize=(14, 5))
plt.plot(original_decompose.seasonal, label='Original Interpolated Seasonality', color='orange', alpha=0.7)
plt.plot(nn_decompose.seasonal, label='NN-Imputed Seasonality', color='green')
plt.title('Seasonality Comparison: Original vs. NN-Imputed Data')
plt.xlabel('Datetime')
plt.xlim(0, 1000)
plt.ylabel('Energy Production Seasonality')
plt.legend()
plt.grid(True)
plt.show()
# Optional: Plot Residuals Comparison
plt.figure(figsize=(14, 5))
plt.plot(original_decompose.resid, label='Original Interpolated Residuals', color='orange', alpha=0.7)
plt.plot(nn_decompose.resid, label='NN-Imputed Residuals', color='green')
plt.title('Residuals Comparison: Original vs. NN-Imputed Data')
plt.xlabel('Datetime')
plt.ylabel('Residuals')
plt.legend()
plt.grid(True)
plt.show()

比较原始(插值)和 NN 补全数据集的 STL 分解趋势的趋势分析图。图表显示最小差异,表明补全数据保持了长期趋势。| 图像由作者提供
原始插值数据和 NN 补全数据集的趋势非常相似。两者都遵循平滑的图案,这表明插值过程很好地保留了数据中的长期趋势!
这证实了神经网络已经学会了时间序列中的潜在动态,而没有对趋势成分引入显著的扭曲。

比较原始和补全数据集的季节成分图。| 图像由作者提供
最后,原始插值和 NN 补全数据集的季节成分交点非常好。这个能源生产数据集(日夜周期)的周期性得到了保留。
这表明模型很好地捕捉了数据的内在周期性,这对于下游预测任务中准确保留季节性非常重要。
残差分析

图 7:展示原始(插值)和 NN 补全数据集的季节成分和残差的两种可视化。季节模式几乎相同,而残差显示了两个数据集一致的噪声水平。| 图像由作者提供
在两个数据集的残差中都可以看到类似的白噪声模式。神经网络的插补没有引入许多异常。人们可以注意到由于存在缺失值,原始数据中存在一些小的偏差。
神经网络的潜在局限性
让我们从显而易见的事情开始:训练神经网络可能非常计算密集,可能需要过多的计算资源(CPU/GPU),尤其是在大型数据集或深层架构的情况下。
神经网络无法处理非平稳时间序列数据,这意味着趋势或季节性可能会随时间变化。模型通常假设平稳性,除非明确处理。
神经网络需要充足且高质量的数据进行训练。如果数据集中包含太多缺失值,可能难以学习到有意义的模式,从而导致插补效果不佳。
神经网络包含许多超参数,需要大量调整,包括层数/神经元数量、学习率和激活函数。
神经网络通常被认为是“黑盒”模型,因为几乎不可能理解网络用于插补缺失值的逻辑过程。
在数据集较小或噪声较强的数据集中,神经网络容易过拟合。模型可能记住模式而不是泛化它。
何时避免使用神经网络进行时间序列插补
-
缺失值比例高,如>50%,且训练数据不足。
-
无论是时间限制还是资源限制,都阻碍了模型训练或超参数调整。
-
数据集要么很小,要么缺乏复杂模式——更简单的方法就足够了。
-
应用需要高度可解释的插补过程。
结论
在这篇文章中,我们应用了一个简单的神经网络模型来插补模拟能源数据中的时间序列缺失数据。
我们看到,神经网络的插补方法保留了原始数据的重要统计和时序属性,包括时间趋势、季节性模式和自相关性。
我们由此推断,神经网络模型为时间序列数据集中的缺失值问题提供了一个良好的解决方案。
显然,在真实数据集中,我们面临一些困难;这些问题大多与数据质量、其黑盒性质和所需的计算资源有关。
感谢您阅读😉。您还使用了哪些插补方法?请在评论中告诉我!
如果您觉得这篇文章有价值,我将非常感激您的支持,给我点赞。您也可以在Medium上关注我,阅读类似的文章!
在此预订与我通话,向我提问或发送您的简历:

浙公网安备 33010602011771号