示范:时间倒退的处理
这一页用 demo_s2_dirty.csv 示范一个问题从发现、分类、修复到验证的全过程。选这个例子,是因为它说明了处理顺序为什么重要。
发现
S1 的规则 time_backwards 在文件第 5400 行报警:时间戳比上一行早。那里的原始数据是这样(只列几列):
Time | T1 | T2 | Sensors_Left | Sensors_Right | Tank_Level |
|---|---|---|---|---|---|
08:08:59.700 | 2 | 3 | 33 | 100 | 11 |
08:08:59.800 | 2 | 3 | 41 | 100 | 11 |
08:08:59.900 | 2 | 3 | 9 | 102 | 11 |
08:08:59.200 | 2 | 3 | 9 | 38 | 11 |
08:08:59.300 | 2 | 3 | 9 | 38 | 11 |
08:08:59.400 | 2 | 3 | 25 | 36 | 11 |
时间从 08:08:59.900 跳回 08:08:59.200,倒退了 0.8 秒。
分类
看其他列:Sensors_Left 和 Sensors_Right 的值按动作顺序正常推进,T1、T2、Tank_Level 没有异常。机器在继续工作,只是时间戳往回跳了。
一台机器不可能回到过去,而它的信号又是连续的,所以这是数据层的问题:记录时间的时钟被调整过。
先修时钟,还是先去重?
这份文件还有重复行。去重有两种做法,在这份文件上结果不同:
| 去重方法 | 删掉的行 |
|---|---|
| 整行完全相同才算重 复 | 177 |
| 时间戳相同就算重复 | 185 |
多出来的 8 行,正是时钟倒退之后、时间戳与前面重复的那 8 行。它们的时间戳和前面的行一样,内容却不同,是真实的数据。按时间戳去重会把它们当作重复删掉。
所以顺序是:先删整行完全相同的重复行,再修时钟。
修复
把跳变点之后的所有时间戳加上 0.8 秒,让它们接上前一行。
clean_log.py(节选)
def fix_clock_jump(df, step_s):
"""时间倒退时,把跳变点之后整段加上偏移量,使其接上前一行。"""
dt = df["t"].diff().dt.total_seconds()
jumps = df.index[dt < 0]
for i in jumps:
offset = pd.Timedelta(milliseconds=round(1000 * (step_s - dt[i])))
df.loc[i:, "t"] += offset
log_step("fix_clock_jump", rule="time_backwards", rows=f"{i}-{len(df) - 1}",
detail=f"offset {offset.total_seconds():+.1f} s")
return df
注意 log_step:清洗日志由修复函数自己写出,修复和记录是同一件事。
验证
修复后重跑全部规则:
| 检查 | 修复后 |
|---|---|
| 时间倒退 | 0 |
| 重复的时间戳 | 0 |
| 时间缺口 | 166 处,共缺 171 行 |
| 行数 | 17829 |
时间缺口还在,这是对的:缺的行补不回来,只能标出来。原始文件 18006 行,去掉重复、加上缺口,正好对应 30 分钟应有的 18000 行。
再看清洗改变了哪些数字:
| 数字 | 清洗前 | 清洗后 |
|---|---|---|
| 分拣的球数(由管内计数累加) | 613 | 613 |
| 满管停机次数 | 90 | 90 |
| 停机总时长,按时间戳计 | 360.2 s | 360.2 s |
| 停机总时长,按行数 × 0.1 s 计 | 359.6 s | 356.8 s |
| 气压均值(另一处修复:08:15:00 起单位改变) | 3299 | 6001 |
两点值得注意:
- 球数和停机次数没有变。管内计数是累计值,丢一行、重一行都不影响它。能用计数的地方,就别用"数行数"。
- 按行数算的时长,清洗前后不同;按时间戳算的时长则基本不变。时长应该由时间戳计算。
轮到你
这份文件里还有另外几类数据问题,以及几段缺料停机。用示范文件找出它们,判断每一个属于哪一层,修复数据层的问题,并写出完整的清洗日志。