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值残留原因分析
时间索引匹配精度问题
在process_sensor_data中,通过label_data[label_data['Htime'] == test_df.index[tdf]]匹配时间戳,但pd.date_range生成的datetime64类型索引,可能与原始label_data['Htime']的时间精度(比如毫秒后的小数位)不一致,导致大量位置被填充NaN。后续的ffill/bfill/插值无法处理整段无有效数据的区域(比如某段时间内完全没有传感器数据)。移位操作引入新NaN
shift_and_filter中执行shift(x)(x从-1到-149),会在数据头部生成大量NaN。虽然后续执行了interpolate和dropna,但如果移位后的特征列中存在连续NaN(比如原始数据开头部分移位后无前置数据),插值无法完全覆盖,最终仍会残留NaN。全NaN列未处理
若某段传感器数据完全缺失,生成的test_df整列都是NaN,ffill/bfill和插值都无法填充,后续dropna也会跳过整行,但如果所有列都存在这类情况,最终会残留空数据或NaN。
二、train_test_split数据量不匹配原因
各传感器列表长度不一致
在批量处理循环中,test_df_len = len(test_df)是每次处理单个传感器列后更新的,若不同传感器列处理后的test_df长度不同(比如某列因NaN被drop后行数更少),最终各传感器列表(如Gyro_x_list、Acc_x_list)的长度会不一致,导致无法组合成特征矩阵,进而train_test_split因特征与标签长度不匹配失败。行为/名称列表的长度计算逻辑错误
循环中for l in range(test_df_len)是基于最后一个传感器列的test_df_len添加行为和名称数据,若前面的传感器列处理后的行数更多,就会导致特征列表长度大于标签列表长度,触发数据量不匹配。全局变量存储的不可控性
使用globals()动态创建列表,无法直观检查各列表的长度一致性,容易出现某列数据存储时的异常(比如apply执行失败导致列表未追加数据),进一步加剧长度不匹配问题。
修复建议
针对NaN残留
- 优化时间匹配逻辑
替换精确匹配为最近时间匹配,避免精度问题:# 替换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 - 提前处理全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 - 调整移位后的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)
针对数据量不匹配
- 统一各传感器列的处理后长度
先处理所有传感器列,取最小长度作为统一标准,截断所有列到该长度:# 替换原循环中的传感器处理部分 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 - 改用字典存储数据
避免全局变量的不可控性,用字典统一管理所有特征和标签:# 替换全局列表初始化 data_container = {col: [] for col in data.columns[2:8]} data_container['behavior'] = [] data_container['Pname'] = [] # 后续存储时直接用data_container[cols].append(...) - 添加长度校验
在批量处理结束后,检查所有列表的长度是否一致: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

