时间序列数据集构建方案Pytorch

时间序列数据集构建方案

时间序列数据集TimeSeriesDataset
时间序列数据集增强EnhancedTimeSeriesDataset

时间序列数据集的构建不同于图像、传统面板数据,其需要满足多实体、动态窗口、时间连续等性质,且容易产生数据泄漏。本文介绍了一种时间序列数据集的构建方法,可以高效地产生训练数据。

一、应用场景

1.1 典型场景示例

(1)零售销量预测

  • 场景描述:同时预测某连锁零售企业1000家门店、5000种商品未来7天的每日销量

  • 数据特征:

    • 时间维度:3年历史数据(1095天)

    • 特征维度:销量、折扣力度、天气数据、节假日标记

    • 实体维度:store_id(1000个) × product_id(5000个)= 500万时间序列

(2)能源需求预测

  • 场景描述:预测某省级电网100个变电站未来24小时的电力负荷

  • 数据特征:

    • 时间粒度:每小时数据

    • 特征维度:历史负荷、温度、湿度、工作日标记

    • 实体维度:100个变电站 × 3种电压等级 = 300个时间序列

(3)服务器集群负载预测

  • 场景描述:预测某数据中心500台服务器未来30分钟的CPU使用率

  • 数据特征:

    • 时间粒度:5分钟间隔

    • 特征维度:CPU使用率、内存占用、网络流量

    • 实体维度:500台独立服务器

二、场景核心难点

2.1 多实体并行处理

  • 挑战:

    • 每个实体(store+product组合)形成独立时间序列

    • 需要保持各序列的独立性同时实现批量处理

    • 示例:500万时间序列需高效管理内存和计算资源

  • 解决方案:

    • 使用分组索引机制(对数据集按照实体id+time column排序,groups列表存储各实体的数据区间),通过预排序和建立索引可以快速定位每个实体的数据slice

    • 预计算实体边界(start/end索引),避免重复groupby操作

2.2 动态窗口切分

  • 挑战:

    • 不同序列长度差异大(新商品可能只有30天数据)

    • 需要同时满足:

      • 最小训练长度要求(min_encoder_length + min_decoder_length)

      • 数据增强需求(随机采样不同历史窗口),其他数据增强策略比如反事实推断/时序随机波动等,可以在__getitem__中实现

  • 解决方案:

    • 动态计算可用窗口范围:max_L = remaining - D

    • 双重随机采样机制:

    python 复制代码
    D = random.randint(min_decoder, max_decoder)  # decoder长度随机
    L = random.randint(min_encoder, max_L)        # encoder长度随机

2.3 变长序列处理

  • 挑战:

    • 不同样本的encoder/decoder长度不同

    • 需要统一为固定维度张量进行批量处理,并使用mask标记

  • 解决方案:

    • 双端独立padding机制:

      python 复制代码
      # Encoder padding
      padded_encoder = np.full((max_encoder_length, features), padding_value)
      padded_encoder[:L] = actual_data
      
      # Decoder padding
      padded_decoder = np.full((max_decoder_length, features), padding_value)
    • 配套MASK机制标识有效数据:

      python 复制代码
         encoder_mask = [1]*L + [0]*(max_length-L)

2.4 时间连续性保证

  • 挑战:

    • 随机采样可能破坏时间序列连续性,时序预测需要保证每个实体的每个时间片段是连续的,encoder代表历史,decoder代表未来

    • 需防止数据泄露(未来信息混入历史数据)

  • 解决方案:

    • 严格的时间窗口划分:
    python 复制代码
      |---- encoder ----|---- decoder ----|
      t               t+L              t+L+D
    • 三重验证机制:

      • remaining = T - t 检查剩余长度

      • t + L + D <= T 最终边界检查

      • 特征分离:确保decoder不使用未来特征

      原始数据 排序分组 静态特征编码 构建分组索引 特征索引映射

关键步骤:

  • 排序分组:

    • 按group_ids + time_col排序保证时间连续性

    • 示例:df.sort_values('store_id','product_id','target_date')

  • 静态特征编码:

    • 使用LabelEncoder对类别特征进行数字化

    • 存储编码器供后续使用:static_encoders字典

  • 分组索引构建:

    • 遍历数据记录实体变化点

    • 存储三元组(group_key, start_idx, end_idx)

  • 特征索引映射:

    • 建立特征名称到列索引的映射

    • 示例:encoder_features = 'sale','discount' → indices 0,1

  • 样本生成:

    • 在每个实体的时间序列上抽样时间切片slice
    • 格式group_id, start_idx, encoder_length, decoder_length,在group_id上切片-
      • encoder_slice=start_idx:start_idx+encoder_length
      • decoder_slice=start_idx+encoder_length:start_idx+encoder_length+decoder_length
    • 时间切片的个数就是样本数(len ),在 getitem 函数通过样本生成的切片索引切分出训练数据
    • 最后进行动态mask

三、代码

python 复制代码
import torch
from torch.utils.data import Dataset
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder
import random

class TimeSeriesDataset(Dataset):
    def __init__(
        self,
        data: pd.DataFrame,
        time_col: str = "target_date",
        target_col: str = "sale_amount",
        group_ids: list = ["store_id", "product_id"],
        encoder_features: list = ["sale_amount", "discount", "precipitation", "temperature"],
        decoder_features: list = ["discount", "precipitation", "temperature"],
        static_features: list = ["store_id", "product_id"],
        max_encoder_length: int = 35,
        min_encoder_length: int = 14,
        max_decoder_length: int = 7,
        min_decoder_length: int = 7,
        num_samples_per_step: int = 1,
        padding_value: float = 0.0,
    ):
        """
        优化版时间序列数据集类
        
        参数新增:
        num_samples_per_step: 每个时间步生成的样本数
        max_decoder_length: decoder最大长度
        min_decoder_length: decoder最小长度
        """
        super().__init__()

        # 参数校验
        assert max_encoder_length >= min_encoder_length, "encoder长度参数非法"
        assert max_decoder_length >= min_decoder_length, "decoder长度参数非法"
        assert num_samples_per_step >= 1, "样本数需≥1"

        # 存储参数
        self.time_col = time_col
        self.target_col = target_col
        self.group_ids = group_ids
        self.encoder_features = encoder_features
        self.decoder_features = decoder_features
        self.static_features = static_features
        self.max_encoder_length = max_encoder_length
        self.min_encoder_length = min_encoder_length
        self.max_decoder_length = max_decoder_length
        self.min_decoder_length = min_decoder_length
        self.num_samples_per_step = num_samples_per_step
        self.padding_value = padding_value

        # 预处理数据
        self._preprocess_data(data)
        
        # 生成样本索引
        self.samples = self._generate_samples()

    def _preprocess_data(self, data):
        """数据预处理"""
        # 排序并建立索引
        self.data = data.sort_values(self.group_ids + [self.time_col]).reset_index(drop=True)
        
        # 编码静态特征
        self.static_encoders = {}
        for col in self.static_features:
            le = LabelEncoder()
            self.data[col] = le.fit_transform(self.data[col])
            self.static_encoders[col] = le

        # 构建分组索引
        self.groups = []
        current_group = None
        start_idx = 0
        group_values = self.data[self.group_ids].values
        
        for idx in range(len(group_values)):
            group_key = tuple(group_values[idx])
            if group_key != current_group:
                if current_group is not None:
                    self.groups.append( (current_group, start_idx, idx) )
                current_group = group_key
                start_idx = idx
        self.groups.append( (current_group, start_idx, len(group_values)) )

        # 特征索引映射
        self.all_features = list(set(self.encoder_features + self.decoder_features))
        self.encoder_feature_indices = [self.all_features.index(col) for col in self.encoder_features]
        self.decoder_feature_indices = [self.all_features.index(col) for col in self.decoder_features]

    def _generate_samples(self):
        """样本生成"""
        samples = []
        
        # 按预存索引访问分组数据
        for group_key, start, end in self.groups:
            dynamic_values = self.data[self.all_features].iloc[start:end].values
            target_values = self.data[self.target_col].iloc[start:end].values
            static_values = self.data[self.static_features].iloc[start].values
            T = end - start  # 时间序列长度
            
            # 生成有效时间窗口
            min_required = self.min_encoder_length + self.min_decoder_length
            if T < min_required:
                continue
                
            # 滑动窗口生成多个样本
            for t in range(T - min_required + 1):
                # 每个时间步生成多个样本
                for _ in range(self.num_samples_per_step):
                    # 动态计算最大可用长度
                    remaining = T - t
                    
                    # 随机选择decoder长度
                    max_D = min(remaining - self.min_encoder_length, self.max_decoder_length)
                    if max_D < self.min_decoder_length:
                        continue
                    D = random.randint(self.min_decoder_length, max_D)
                    
                    # 根据D确定encoder长度范围
                    max_L = remaining - D
                    if max_L < self.min_encoder_length:
                        continue
                    L = random.randint(self.min_encoder_length, max_L)
                    
                    # 有效性验证
                    if t + L + D > T:
                        continue
                    
                    samples.append({
                        "group_id": group_key,
                        "start_idx": t,
                        "encoder_length": L,
                        "decoder_length": D,
                        "static": static_values,
                        "dynamics": dynamic_values,
                        "targets": target_values,
                    })
        return samples

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx):
        sample = self.samples[idx]
        L = sample["encoder_length"]
        D = sample["decoder_length"]
        t = sample["start_idx"]
        
        # Encoder部分处理
        encoder_data = sample["dynamics"][t:t+L, self.encoder_feature_indices]
        padded_encoder = np.full((self.max_encoder_length, len(self.encoder_features)), self.padding_value)
        padded_encoder[:L] = encoder_data
        encoder_mask = np.zeros(self.max_encoder_length)
        encoder_mask[:L] = 1.0
        
        # Decoder部分处理
        decoder_start = t + L
        decoder_end = decoder_start + D
        decoder_data = sample["dynamics"][decoder_start:decoder_end, self.decoder_feature_indices]
        padded_decoder = np.full((self.max_decoder_length, len(self.decoder_features)), self.padding_value)
        padded_decoder[:D] = decoder_data
        decoder_mask = np.zeros(self.max_decoder_length)
        decoder_mask[:D] = 1.0
        
        # 目标值处理
        target = sample["targets"][decoder_start:decoder_end]
        padded_target = np.full(self.max_decoder_length, self.padding_value)
        padded_target[:D] = target

        return {
            # Encoder部分
            "encoder_input": torch.FloatTensor(padded_encoder),
            "encoder_mask": torch.FloatTensor(encoder_mask),
            # Decoder部分
            "decoder_input": torch.FloatTensor(padded_decoder),
            "decoder_mask": torch.FloatTensor(decoder_mask),
            # 目标值
            "target": torch.FloatTensor(padded_target),
            # 静态特征
            "static_features": torch.LongTensor(sample["static"]),
            # 实际长度(可选)
            "actual_lengths": (L, D)
        }

# 使用示例
if __name__ == "__main__":
    # 生成测试数据
    dates = pd.date_range(start="2023-01-01", periods=90, name="target_date")
    example_data = pd.DataFrame({
        "store_id": np.repeat([1, 2], 45),
        "product_id": np.repeat([101, 102], 45),
        "target_date": np.tile(dates, 2),
        "sale_amount": np.random.randint(0, 100, 180),
        "discount": np.random.rand(180),
        "precipitation": np.random.rand(180),
        "temperature": np.random.rand(180),
    })

    # 初始化数据集
    dataset = TimeSeriesDataset(
        data=example_data,
        max_encoder_length=35,
        min_encoder_length=14,
        max_decoder_length=14,  # 扩展decoder长度范围
        min_decoder_length=7,
        num_samples_per_step=3,  # 每个时间步生成3个样本
    )
    
    # 验证样本
    sample = dataset[0]
    print(f"Encoder输入形状: {sample['encoder_input'].shape}")
    print(f"Decoder输入形状: {sample['decoder_input'].shape}")
    print(f"有效Decoder长度: {sample['actual_lengths'][1]}")
    print(f"Mask矩阵示例:\nEncoder: {sample['encoder_mask'][:5]}\nDecoder: {sample['decoder_mask'][:5]}")
相关推荐
凡达Ai派1 小时前
AI画布交付文件总是混在一起?用源文件、预览图和发布图三层目录分开管理
图像处理·深度学习·神经网络·自然语言处理·知识图谱
归秋1421 小时前
2026企业AI办公工具选型指南:从场景匹配到平台评估
大数据·运维·人工智能
2601_968900771 小时前
Agent沙盒基础设施拆解:三层镜像与按需加载的工程取舍
人工智能
楚楚2511 小时前
2026实测:智能体办公平台三周上手真实体验
人工智能
智圣新创011 小时前
教育数据要素流通刚需下 智圣新创高校数据中台解决方案的全域建设落地框架
大数据·人工智能·物联网
xianghongtao01162 小时前
如何选择一台适合你的本地 AI 硬件设备?Lucy AI Studio——从本地 AI 办公到家庭影音的完整指南
人工智能·端脑科技·本地ai硬件设备·kickstarter众筹项目·lucy ai studio·lucy aios
ZhangJun952 小时前
Mobike 共享单车分析项目
人工智能·python·算法·kmeans·聚类·knn
互联网资讯2 小时前
本地创业者入局 AI 短剧,本地化部署怎么选更省钱
大数据·人工智能
唐维康2 小时前
昆工计算机408考研2027招285人,比去年少6人
人工智能·考研·昆明理工大学
Martina_03212 小时前
AI生成的过山车轨道导入后频繁脱轨?用5步检查样条、轨距与碰撞
图像处理·人工智能·游戏·3d·材质·游戏策划·关卡设计