时间序列中的缺失数据-机器学习技术-第二部分-
时间序列中的缺失数据?机器学习技术(第二部分)
原文:
towardsdatascience.com/missing-data-in-time-series-machine-learning-techniques-part-2-86fac838d1ce/
使用聚类算法处理缺失的时间序列数据

图片由作者提供。
(如果您还没有阅读第一部分,请在此处查看这里。)
时间序列分析中的缺失数据是一个反复出现的问题。
正如我们在第一部分中探讨的那样,简单的插补技术甚至基于回归的模型(如线性回归、决策树)可以让我们走得很远。
但如果我们需要处理更微妙的模式并捕捉复杂时间序列数据中的细微波动呢?
在本文中,我们将探讨 K-Nearest Neighbors(KNN)。该模型的优势在于对数据中非线性关系的假设很少;因此,它成为了一种灵活且健壮的缺失数据插补解决方案。
我们将使用与第一部分中相同的模拟能源生产数据集,其中 10%的值随机缺失。
我们将使用一个您可以轻松生成的数据集来插补缺失数据,让您可以跟随并实时应用技术,在逐步探索过程中应用这些技术!
这次,我们将看到 KNN 如何比简单方法更有效地填补空白。
你好!
我的名字是Sara Nóbrega,我是一名数据科学家,专注于 AI 工程。我拥有物理学硕士学位,后来转型到了令人兴奋的数据科学领域。
我撰写有关数据科学、人工智能以及数据科学职业建议的文章。请确保关注我并订阅,以便在发布下一篇文章时收到更新!
目录
-
数据笔记:模拟能源生产数据集
-
为什么和何时使用非线性机器学习进行插补?
-
第二部分:KNN 用于时间序列插补 3.1 统计比较 3.1.1 自相关、STL 分解和残差分析
-
KNN 的潜在局限性
-
何时使用 KNN 进行时间序列插补
数据笔记:模拟能源生产数据集
在第一部分中,我们使用了一个从 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:带有缺失值的模拟能源生产数据。| 图片由作者提供。
为什么和何时使用非线性机器学习进行插补?
K-Nearest Neighbors 利用数据点之间的邻近性。这些方法在以下情况下特别有用:
-
非线性模式存在:KNN 在处理异常或周期性趋势方面比简单的多项式或基本平滑方法更好。
-
更多数据特征:如果你有多个相关特征(温度、湿度、一周中的某一天指标),KNN 可以利用这些特征。
-
更大的差距:这些模型捕捉更广泛的背景,因此可以扩展到更长的缺失间隔。
处理时间序列数据?那么你需要掌握关键技术来精通其分析:
时间序列插补的 K-Nearest Neighbors
我们将使用来自scikit-learn的KNNImputer根据最近邻的已知观察值来填充缺失值。以下是如何操作的示例:
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
from datetime import datetime
# Assuming data_with_index is already defined as per your provided data code
# Columns: ['index', 'Datetime', 'Energy_Production', 'Time_Index']
# Step 1: Feature Engineering
data_knn = data_with_index.copy()
# Extract hour, day of week, and encode cyclical features
data_knn['Hour'] = data_knn['Datetime'].dt.hour
data_knn['DayOfWeek'] = data_knn['Datetime'].dt.dayofweek # 0=Monday, 6=Sunday
# Cyclical encoding for 'Hour'
data_knn['Hour_Sin'] = np.sin(2 * np.pi * data_knn['Hour'] / 24)
data_knn['Hour_Cos'] = np.cos(2 * np.pi * data_knn['Hour'] / 24)
# Cyclical encoding for 'DayOfWeek'
data_knn['Day_Sin'] = np.sin(2 * np.pi * data_knn['DayOfWeek'] / 7)
data_knn['Day_Cos'] = np.cos(2 * np.pi * data_knn['DayOfWeek'] / 7)
# Optional: Remove 'Time_Index' if not adding value
# You can comment out the next line if you want to keep 'Time_Index'
data_knn = data_knn.drop(columns=['Time_Index'])
# Step 2: Prepare Features and Target
# Select relevant features for imputation
feature_columns = ['Hour_Sin', 'Hour_Cos', 'Day_Sin', 'Day_Cos']
features = data_knn[feature_columns]
target = data_knn['Energy_Production']
# Combine features and target for imputation
combined_data = pd.concat([features, target], axis=1)
# Step 3: Feature Scaling
scaler = StandardScaler()
scaled_data = scaler.fit_transform(combined_data)
# Step 4: Apply KNNImputer
knn_imputer = KNNImputer(n_neighbors=5, weights='distance')
imputed_array = knn_imputer.fit_transform(scaled_data)
# Inverse transform the scaled data to original scale
imputed_data = scaler.inverse_transform(imputed_array)
# Step 5: Update the 'Energy_Production' column with imputed values
knn_filled_data = data_knn.copy()
knn_filled_data['Energy_Production'] = imputed_data[:, -1]
# Step 6: Visualization of Imputation Results
# Define the time range for visualization
start_month = datetime(2023, 1, 1)
end_month = datetime(2023, 1, 31)
# Extract original and imputed data for the specified range
original_month_data = mock_energy_data_with_missing[
(mock_energy_data_with_missing['Datetime'] >= start_month) &
(mock_energy_data_with_missing['Datetime'] <= end_month)
]
knn_month_data = knn_filled_data[
(knn_filled_data['Datetime'] >= start_month) &
(knn_filled_data['Datetime'] <= end_month)
]
# Plotting
plt.figure(figsize=(14, 7))
plt.plot(knn_month_data['Datetime'], knn_month_data['Energy_Production'],
label='Imputed Data (KNN)', color='purple', alpha=0.8)
plt.plot(original_month_data['Datetime'], original_month_data['Energy_Production'],
label='Original Data (with Missing)', color='red', alpha=0.5)
plt.scatter(original_month_data['Datetime'], original_month_data['Energy_Production'],
c=original_month_data['Energy_Production'].isna(), cmap='coolwarm',
label='Missing Values', s=10)
plt.title('Original vs. KNN-Imputed Energy Production Data (January 2023)')
plt.xlabel('Datetime')
plt.ylabel('Energy Production')
plt.legend()
plt.grid(True)
plt.show()

图 2:比较线图,显示红色为原始能源生产数据(含缺失值),紫色为 KNN 插补数据。此图突出了插补值与 2023 年 1 月数据集的无缝集成 | 图片由作者提供。
在图 2 中,我们可以看到缺失值是如何根据数据中的模式用现实估计值替换的。
此代码使用数据中存在的时间相关模式来预处理数据集,以填充缺失的能源生产值。
为一天中的小时和一周中的某一天创建特征,并将它们编码以表示它们的自然周期,例如,午夜比晚上 11 点更接近。
周期性编码是一种转换,它将任何与时间相关的特征(如一天中的小时或一周中的某一天)转换为以这种方式表示该特征,从而捕捉这些变量中的自然周期。
例如,考虑一天中的小时从 0(代表午夜)到 23(代表晚上 11 点)的周期;这两个小时在时间上实际上非常接近。
这些特征有助于捕捉有意义的基于时间趋势。
在准备插补之前,所有特征都进行了标准化,以便它们在 KNN 算法中平等地贡献,该算法基于最相似的数据点填充缺失值。
观察结果
-
本地适应: KNN 算法基于点之间的距离,这些距离取决于诸如 Time_Index 和 Hour 等特征;只要这种循环在 Hour 特征中得到反映,它确实可以捕捉到昼夜循环。
-
非线性保持: 与直线不同,KNN 预测的形状与数据相似,如果邻近点显示出这种模式,它将保留峰值和谷值。
目前来看,KNN 插补方法似乎相当合理地恢复了缺失值,利用了周围的时间序列模式! 然而,我们需要的不仅仅是这种图表的更先进的方法。
接下来,让我们评估这种 KNN 插补的结果。
统计比较
正如第一部分所述,我们评估均值、标准差、最小值、最大值和四分位数:
original_stats = mock_energy_data_with_missing['Energy_Production'].describe()
knn_stats = knn_filled_data['Energy_Production'].describe()
stats_comparison_knn = pd.DataFrame({
'Metric': original_stats.index,
'Original Data': original_stats.values,
'Imputed Data (KNN)': knn_stats.values
})
stats_comparison_knn

图 3:统计比较| 作者图片
我们可以看到,插补数据的描述性统计与原始数据的分布非常相似,整体特征得到保留。这是一个好消息:插补数据保持了能源生产值的现实变异性!
自相关、STL 分解和残差分析
我们将创建自相关函数(ACF)图并执行 STL 分解,就像第一部分一样,以评估时间序列数据中的潜在季节性模式、趋势和自相关,这为插补结果提供了更深入的理解。
# ACF Comparison Function
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 KNN-Imputed Data")
plt.grid(True)
plt.tight_layout()
plt.show()
# Perform ACF Comparison
plot_acf_comparison(
mock_energy_data_with_missing['Energy_Production'],
knn_filled_data['Energy_Production']
)
# STL Decomposition Function
def perform_stl_decomposition(series, period, title_suffix=''):
decomposition = seasonal_decompose(series, model='additive', period=period)
return decomposition
# 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 on Original Data
original_decompose = perform_stl_decomposition(original_interpolated, period=seasonal_period, title_suffix='Original')
# Perform STL Decomposition on KNN-Imputed Data
knn_decompose = perform_stl_decomposition(knn_filled_data['Energy_Production'], period=seasonal_period, title_suffix='KNN-Imputed')
# Visualization of STL Decomposition Components
# Plot Trend Comparison
plt.figure(figsize=(14, 5))
plt.plot(knn_decompose.trend, label='KNN-Imputed Trend', color='green')
plt.plot(original_decompose.trend, label='Original Interpolated Trend', color='blue', alpha=0.7)
plt.title('Trend Comparison: Original vs. KNN-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='blue', alpha=0.7)
plt.plot(knn_decompose.seasonal, label='KNN-Imputed Seasonality', color='green')
plt.title('Seasonality Comparison: Original vs. KNN-Imputed Data')
plt.xlabel('Datetime')
plt.xlim(0, 2000)
plt.ylabel('Energy Production Seasonality')
plt.legend()
plt.grid(True)
plt.show()
# Optional: Plot Residuals Comparison
plt.figure(figsize=(14, 5))
plt.plot(knn_decompose.resid, label='KNN-Imputed Residuals', color='green')
plt.plot(original_decompose.resid, label='Original Interpolated Residuals', color='blue', alpha=0.7)
plt.title('Residuals Comparison: Original vs. KNN-Imputed Data')
plt.xlabel('Datetime')
plt.ylabel('Residuals')
plt.legend()
plt.grid(True)
plt.show()

图 4:比较原始(插值)和 KNN 插补数据集的时间依赖性的 ACF 图。两个图表都显示出相似的周期性模式,验证了时间依赖性的保持。| 作者图片
图 4 显示了原始和插补数据集的ACF 图。我们可以看到,两个图表都显示出非常相似的图案,证实了插补确实保留了时间依赖性!
从这些图中我们可以得出结论,插补后的数据集很好地捕捉了原始数据的周期性。

图 5:比较原始(插值)和 KNN 插补数据集的 STL 分解趋势分析图。图表显示最小差异,表明插补数据保持了长期趋势。 | 作者图片

图 6:比较原始和插补数据的季节成分图 | 作者图片
-
图 5:STL 分解从 KNN 插补数据中提取的趋势成分与原始数据非常接近,反映出几乎没有偏差。这证实了在插补过程中很好地保留了能源生产的长期模式!
-
图 6:同样,我们甚至无法从视觉上看到原始和插补数据集之间在季节成分上的任何差异。这表明能源生产周期中固有的日和周周期性得到了很好的保留。实际上,从时间序列分析和预测任务的角度来看,这一点非常重要,尤其是在需要进一步下游工作的情况下。
残差分析

图 7:展示原始(插值)和 KNN 插补数据集的季节成分和残差的对比可视化。季节模式几乎相同,而残差在两个数据集中都表现出一致的噪声水平 | 作者图片
事实上,KNN 插补数据的 STL 分解残差确实显示出与用于此分析插值的原始数据集相似的噪声模式,这表明在插补过程中向数据集中引入的额外不规则性或伪影非常少。
由于残差方差在两个数据集中保持相似,这表明插补方法保留了原始时间序列数据中固有的随机性。
如何处理时间序列数据中的异常值?将这篇文章保存起来供以后参考:
KNN 的潜在局限性
我们已经看到 KNN 实际上是一个有用的模型来执行时间序列插补。当然,每个模型都有其局限性:
-
可能变得计算复杂:KNN 需要计算所有观察值的距离,如果你正在处理大型数据集或高维特征空间,这当然会变得计算昂贵。
-
对参数敏感:
n_neighbors的选择和加权方案会显著影响结果。 -
假设相似性: KNN 假设可以通过在特征空间中接近其他观测值来推断缺失值。当数据中相似性太低,或者太嘈杂时,这可能会失败。
-
平稳性假设: KNN 本身不支持非平稳数据 – 具有变化趋势或季节性的时间序列数据 – 除了可能需要额外的预处理。我们这里使用的数据是平稳的。
-
可扩展性: KNN 对于非常大的数据集或时间序列流数据扩展性不好。
当何时使用 KNN 进行时间序列数据插补
当时间序列数据满足某些标准时,KNN 插补很有用:
适度的缺失数据水平:
- 当缺失值的比例适中(例如,5-20%)时,KNN 表现良好。 如果缺失数据超过这些水平,模型可能会遇到困难。
周期性或周期性数据:
- 它最好应用于具有明显周期性模式的那些数据集(例如,每日、每周或年度周期)。能源生产、交通流量或季节性销售数据是此类数据的例子。通过这种周期性特征工程,KNN 的性能得到提高。
有限的非平稳性:
- 当数据集相对平稳或已预处理以去除非平稳性(如去趋势或去季节性)时,KNN 表现良好。
中小型数据集大小:
- 它在计算可行性和模型精度之间提供了良好的权衡,适用于非过度大的时间序列数据集。
缺失非随机(MNAR):
- 如果缺失数据与某些时间模式或周期性事件的存在相关,那么 KNN 可以有效地利用这些关系,尤其是在特征包含时间索引或周期性成分时。
将 KNN 应用于时间序列数据中的缺失值插补非常有效。
插补保留了统计数据的本质 – 中心趋势、变异性、分布形状,以及主要的时间特征如趋势、季节性和自相关模式。残差分析确认,在插补过程中没有引入显著的异常或不规则性。
我们看到,制造新的周期性特征(如果数据集中尚未存在)对于 KNN 插补器能够保留能源生产数据集固有的周期性性质非常重要,这样新的插补值才能很好地融入数据。这些结果证明,KNN 可以是一种稳健且可靠的方法,用于处理时间序列数据集中的缺失数据。
如果您觉得这篇文章有价值,我会很感激您通过点赞来支持我。您也可以在这里Medium关注我,阅读类似的文章!
与我预约通话,向我提问或在这里发送您的简历:
参考文献
-
Géron, A. (2019). 《使用 Scikit-Learn 和 TensorFlow 进行机器学习实践》. O’Reilly.
-
scikit-learn 文档:KNNImputer

浙公网安备 33010602011771号