You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CNN-LSTM预处理后NaN值残留及train_test_split匹配失败问题排查

CNN-LSTM数据预处理问题排查:NaN残留与train_test_split失败

问题背景

在CNN-LSTM时序数据预处理中,已尝试**插值、前向填充(ffill)、后向填充(bfill)**等操作,但NaN值仍未完全消除;后续执行train_test_split时,因数据样本数量不匹配无法完成。以下是预处理代码及流程:

# 빈 데이터프레임 생성 함수 정의
def create_empty_df(index_list, columns):
    return pd.DataFrame(index=index_list, columns=columns)

# 센서 데이터 처리 함수 정의
def process_sensor_data(label_data, cols, interpolation_method='linear'):
    # 최소 시간값 구하기
    base = np.min(label_data['Htime'])
    # 최소 시간부터 최대 시간까지 20밀리초 간격으로 시간 리스트 생성
    datetime_list = pd.date_range(np.min(label_data['Htime']), np.max(label_data['Htime']), freq='20L')
    # 빈 데이터프레임 생성
    test_df = create_empty_df(datetime_list, [cols])

    # 값이 있는 경우 해당 값을 채우고, 없는 경우 NaN으로 채움
    for tdf in range(len(test_df)):
        value = label_data[label_data['Htime'] == test_df.index[tdf]][cols]
        test_df.iloc[tdf, 0] = value.iloc[0] if not value.empty else np.nan

    # 센서 데이터가 포함된 컬럼에 대한 처리
    if cols in ['Gyro_x', 'Gyro_y', 'Gyro_z', 'Acc_x', 'Acc_y', 'Acc_z']:
        test_df[cols] = test_df[cols].astype(float)
        
        # NaN 값 채우기: forward fill (앞 방향으로 채우기)
        test_df.fillna(method='ffill', inplace=True)
        # NaN 값 채우기: backward fill (뒷 방향으로 채우기)
        test_df.fillna(method='bfill', inplace=True)
        
        # NaN 값을 보간법으로 채우기 (기본값은 선형 보간법)
        test_df.interpolate(method=interpolation_method, limit_direction='both', inplace=True)

    return test_df

# 시프트 및 필터링 함수 정의
def shift_and_filter(test_df, cols):
    # 데이터를 이동시키기 위한 리스트 생성
    shifted_dfs = [test_df[cols].shift(x) for x in range(-1, -150, -1)]
    # 이동된 데이터를 하나의 데이터프레임으로 합치기
    shifted_df = pd.concat(shifted_dfs, axis=1)
    shifted_df.columns = ['Shift_' + str(x) for x in range(-1, -150, -1)]
    test_df = pd.concat([test_df, shifted_df], axis=1)

    # 인덱스를 재설정하고 원하는 데이터만 선택하기
    test_df.reset_index(drop=True, inplace=True)
    test_df = test_df.iloc[test_df.index % 75 == 0]
    
    # NaN 값을 보간법으로 채우기 (기본값은 선형 보간법)
    test_df.interpolate(method='linear', limit_direction='both', inplace=True)
    
    # NaN 값을 포함하는 행 제거
    test_df = test_df.dropna(how='any', axis=0)

    return test_df


# 센서 데이터 및 메타데이터를 저장하기 위한 리스트 생성
for col in range(len(data.columns)):
    if 2 <= col <= 7:
        col_name = data.columns[col]
        globals()['{}_list'.format(col_name)] = []
    elif col == 8:
        col_name = data.columns[col]
        globals()['{}_list'.format(col_name)] = []

globals()['behavior_list'] = []


count = 0

# 각 행동 데이터에 대해 처리
for i in range(len(h_data)):
    
    count += 1
    if (count == 30) :
        break
        
    # 해당 시간 범위와 '이름' 컬럼 값이 일치하는 label_data 추출
    label_data = data[(data['Htime'] >= h_data['시작시간'].iloc[i]) & (data['Htime'] <= h_data['종료시간'].iloc[i])]
    label_data = label_data[label_data['Pname'] == h_data['이름'].iloc[i]]
    # h_data의 'total_behavior' 값을 label_data의 'behavior' 컬럼에 할당
    label_data['behavior'] = h_data['total_behavior'].iloc[i]
    
    if len(label_data) == 0:
        continue

    test_df_len = 0

    # label_data가 비어있지 않은 경우, 다음 작업을 수행
    for cols in label_data.columns:
        # 'Cnt', 'Htime', 'Pname', 'behavior' 컬럼은 건너뛴다
        if cols == 'Cnt' or cols == 'Htime' or cols == 'Pname' or cols == 'behavior':
            continue

        # 센서 데이터 처리
        test_df = process_sensor_data(label_data, cols)

        # 시프트 연산 및 필터링
        test_df = shift_and_filter(test_df, cols)

        # test_df의 길이를 계산하고, 해당 길이만큼 각 리스트에 값을 추가
        test_df_len = len(test_df)
        test_df.apply(lambda x: globals()["{}_list".format(cols)].append(list(x)), axis=1)

    # 각 리스트에 행동과 이름 정보 추가
    for l in range(test_df_len):
        globals()['behavior_list'].append(label_data['behavior'].iloc[0])
        globals()['Pname_list'].append(label_data['Pname'].iloc[0])

预处理流程

  • 创建空DataFrame:create_empty_df函数根据给定索引列表和列名生成空DataFrame。
  • 传感器数据处理:process_sensor_data函数为各传感器数据列生成20ms间隔的时间索引,通过ffill、bfill及线性插值处理NaN值。
  • 移位与过滤:shift_and_filter函数对DataFrame执行移位操作(生成149个移位特征),筛选索引为75倍数的行,再插值并移除含NaN的行。
  • 数据存储:遍历数据集,创建全局列表存储各传感器数据及元数据(行为、名称)。
  • 行为数据批量处理:针对指定时间范围的行为数据,完成传感器数据处理、移位过滤后,将行为及名称信息存入对应列表。

一、NaN值残留原因分析

  1. 时间索引匹配精度问题
    在process_sensor_data中,通过label_data[label_data['Htime'] == test_df.index[tdf]]匹配时间戳,但pd.date_range生成的datetime64类型索引,可能与原始label_data['Htime']的时间精度(比如毫秒后的小数位)不一致,导致大量位置被填充NaN。后续的ffill/bfill/插值无法处理整段无有效数据的区域(比如某段时间内完全没有传感器数据)。

  2. 移位操作引入新NaN
    shift_and_filter中执行shift(x)(x从-1到-149),会在数据头部生成大量NaN。虽然后续执行了interpolate和dropna,但如果移位后的特征列中存在连续NaN(比如原始数据开头部分移位后无前置数据),插值无法完全覆盖,最终仍会残留NaN。

  3. 全NaN列未处理
    若某段传感器数据完全缺失,生成的test_df整列都是NaN,ffill/bfill和插值都无法填充,后续dropna也会跳过整行,但如果所有列都存在这类情况,最终会残留空数据或NaN。

二、train_test_split数据量不匹配原因

  1. 各传感器列表长度不一致
    在批量处理循环中,test_df_len = len(test_df)是每次处理单个传感器列后更新的,若不同传感器列处理后的test_df长度不同(比如某列因NaN被drop后行数更少),最终各传感器列表(如Gyro_x_list、Acc_x_list)的长度会不一致,导致无法组合成特征矩阵,进而train_test_split因特征与标签长度不匹配失败。

  2. 行为/名称列表的长度计算逻辑错误
    循环中for l in range(test_df_len)是基于最后一个传感器列的test_df_len添加行为和名称数据,若前面的传感器列处理后的行数更多,就会导致特征列表长度大于标签列表长度,触发数据量不匹配。

  3. 全局变量存储的不可控性
    使用globals()动态创建列表,无法直观检查各列表的长度一致性,容易出现某列数据存储时的异常(比如apply执行失败导致列表未追加数据),进一步加剧长度不匹配问题。


修复建议

针对NaN残留

  1. 优化时间匹配逻辑
    替换精确匹配为最近时间匹配,避免精度问题:
    # 替换process_sensor_data中的循环填充部分
    label_data_sorted = label_data.sort_values('Htime')
    test_df = pd.DataFrame(index=datetime_list, columns=[cols])
    merged = pd.merge_asof(test_df.reset_index(), label_data_sorted[['Htime', cols]], 
                           left_on='index', right_on='Htime', direction='nearest')
    test_df[cols] = merged[cols].values
    
  2. 提前处理全NaN列
    在process_sensor_data中添加全NaN检查,用均值或中位数填充:
    if test_df[cols].isna().all():
        test_df[cols] = label_data[cols].mean() if not label_data.empty else 0.0
    
  3. 调整移位后的NaN处理顺序
    先执行双向填充,再插值,最后删除含NaN的行:
    # 修改shift_and_filter中的NaN处理部分
    test_df.fillna(method='ffill', inplace=True)
    test_df.fillna(method='bfill', inplace=True)
    test_df.interpolate(method='linear', limit_direction='both', inplace=True)
    test_df = test_df.dropna(how='any', axis=0)
    

针对数据量不匹配

  1. 统一各传感器列的处理后长度
    先处理所有传感器列,取最小长度作为统一标准,截断所有列到该长度:
    # 替换原循环中的传感器处理部分
    processed_dfs = {}
    for cols in label_data.columns:
        if cols in ['Cnt', 'Htime', 'Pname', 'behavior']:
            continue
        test_df = process_sensor_data(label_data, cols)
        test_df = shift_and_filter(test_df, cols)
        processed_dfs[cols] = test_df
    # 统一长度
    min_len = min([len(df) for df in processed_dfs.values()])
    for cols, df in processed_dfs.items():
        processed_dfs[cols] = df.iloc[:min_len]
        df.apply(lambda x: globals()["{}_list".format(cols)].append(list(x)), axis=1)
    # 更新test_df_len为统一长度
    test_df_len = min_len
    
  2. 改用字典存储数据
    避免全局变量的不可控性,用字典统一管理所有特征和标签:
    # 替换全局列表初始化
    data_container = {col: [] for col in data.columns[2:8]}
    data_container['behavior'] = []
    data_container['Pname'] = []
    # 后续存储时直接用data_container[cols].append(...)
    
  3. 添加长度校验
    在批量处理结束后,检查所有列表的长度是否一致:
    lengths = [len(v) for v in data_container.values()]
    if len(set(lengths)) != 1:
        raise ValueError(f"数据长度不匹配: {dict(zip(data_container.keys(), lengths))}")
    

内容的提问来源于stack exchange,提问作者soon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 22:47:07