4. 表格数据清洗与缺失值处理
真实世界的表格数据几乎不可能“开箱即用”。传感器采集的数值可能因通信中断而空缺,人工录入的字段可能混入空格或单位,多个系统合并后可能出现完全重复的记录,而极端值又常常掩盖真实的数据分布。MATLAB 的 table 数据类型为这些清洗任务提供了统一的容器:每一列有明确的变量名和类型,行可以带行标签,缺失值用 missing 统一表示。本节围绕“识别—处理—规范化—去重—标记”这条主线,讲清楚每一步在 MATLAB 中具体怎么做、结果如何判断。
一、缺失值的识别:先看清缺口在哪里
MATLAB 中不同数据类型的缺失表示并不相同:数值型默认用 NaN,字符串用 <missing>,分类变量用 <undefined>,datetime 用 NaT。判断缺失最稳妥的方式是 ismissing,它对上述类型都能正确识别,而 isnan 只适用于数值数组。对整张表调用 ismissing(T) 会返回同尺寸的逻辑矩阵,再配合 sum 即可得到每列缺失数量:
T = readtable("sensor_log.csv", TextType="string");
missCount = sum(ismissing(T), 1); % 每列缺失个数
missRatio = missCount / height(T); % 每列缺失比例
disp(table(T.Properties.VariableNames', missCount', missRatio', ...
VariableNames=["变量","缺失数","缺失比例"]))
识别阶段的关键判断是“缺失是否有规律”。如果某一列的缺失比例超过 40%,或者缺失集中在某一段时间、某一台设备上,那么简单填充可能引入系统性偏差,此时更应考虑删除该列或回溯数据源。可以用 ismissing 的结果按行求和,观察缺失是否成簇出现:rowMiss = sum(ismissing(T), 2); histogram(rowMiss),若大量行同时缺多个字段,往往说明整条记录采集失败。
二、填充还是删除:让策略可解释
缺失处理没有万能公式,但有一条原则:处理方式必须能说清楚理由。数值型变量的常用填充方式包括均值、中位数、前一个有效值(适用于时间序列)以及按分组统计量填充。fillmissing 的 Method 参数支持 "mean"、"median"、"previous"、"linear" 等。对于传感器时序数据,线性插值通常比均值更合理,因为它保留了变化趋势:
T.temp = fillmissing(T.temp, "linear"); % 温度按时间线性插值
T.status = fillmissing(T.status, "previous"); % 状态量沿用上一有效值
T.pressure = fillmissing(T.pressure, "median"); % 压力用中位数抗极端值
当缺失行占比很小(例如低于 5%)且缺失分散时,T = rmmissing(T) 直接删除含缺失的行是简单可靠的选择;若只想在特定变量上判断,可用 rmmissing(T, DataVariables=["temp","pressure"])。需要强调的是,删除会改变样本量,若后续要做分组统计,应记录删除前后的行数对比,避免结论建立在被悄悄削减的数据上。一个实用习惯是新增一列 wasFilled 逻辑标记,记录哪些单元格被填充过,便于后续敏感性分析。
三、标准化变量类型:让每列“名副其实”
readtable 读入 CSV 时,若某列混有数字和文本,往往会被推断为字符串或字符向量,导致后续无法直接计算。类型标准化的目标是:数值列是 double,类别列是 categorical,时间列是 datetime,标识列是 string。常用手段有三种:
convertvars批量转换指定变量,适合一次处理多列;str2double处理带千分位或空格的数值字符串,转换失败会得到NaN,正好与缺失识别衔接;datetime配合InputFormat明确时间格式,避免“03/04/2024”被误解析为月/日或日/月。
T.unit = categorical(T.unit); % 单位转为分类变量
T.time = datetime(T.time, InputFormat="yyyy-MM-dd HH:mm:ss");
T.flow = str2double(erase(string(T.flow), ",")); % 去掉千分位再转数值
T = convertvars(T, ["deviceID","batch"], "string");
转换后应做一次“类型体检”:summary(T) 会列出每列的最小值、最大值、缺失数和类别计数,能快速暴露“本应是数值却仍是字符串”的问题。分类变量还有一个额外好处:categories(T.unit) 可以直接看出是否存在“kg”“KG”“千克”这类同义异写,为后续统一口径提供依据。
四、重复行处理:先定义“重复”
去重前必须先明确按哪些列判重。整行完全一致用 unique(T, "rows") 即可;但更常见的情形是“同一设备同一时刻被重复上报”,此时应只按关键列判重并保留一条:
[~, idx] = unique(T(:, ["deviceID","time"]), "rows", "stable");
T = T(idx, :); % stable 保留首次出现顺序
dupCount = height(T_before) - height(T);
"stable" 参数很重要,它保证保留的是原始顺序中第一次出现的记录,而不是排序后的任意一条。若重复记录的数值字段不一致,说明存在冲突,此时不应盲目保留第一条,而应按业务规则取舍,例如保留时间戳最新的一条,或对数值取平均并单独记录冲突条数。
五、异常值标记:标记优先于剔除
异常值不一定是错误值,也可能是真实工况。稳妥做法是先标记、后决策。isoutlier 支持 "median"(基于中位数绝对偏差)、"mean"、"quartiles" 等方法,返回逻辑向量:
isOut = isoutlier(T.pressure, "quartiles");
T.pressureFlag = string(repmat("正常", height(T), 1));
T.pressureFlag(isOut) = "疑似异常";
fprintf("疑似异常记录:%d 条(占 %.1f%%)\n", sum(isOut), 100*mean(isOut));
标记之后,可以结合业务阈值二次判断:例如压力超过量程上限的必然是采集错误,应置为缺失再填充;而落在量程内但偏离分布的,可能只是设备启停瞬间的真实波动,应保留并备注。把“统计异常”和“物理越界”分开处理,是避免误删有效数据的关键。
rng 固定随机种子(若涉及随机抽样)。每次运行输出“原始行数、删除行数、填充单元格数、重复行数、异常标记数”五项统计,形成可追溯的清洗日志。把上述步骤串起来,一个典型的清洗流程是:读入后先统计缺失比例,对高缺失列决定删除或回溯;对保留列按变量特性选择填充方法并留下标记;统一数值、分类、时间三类变量的类型;按业务主键去重;最后用 isoutlier 生成异常标记列。整个过程不追求“一步到位”,而追求每一步都有据可查、可回退、可复现——这正是表格数据清洗从“能跑”走向“可信”的分界线。