4、数据读取与清洗:使用Pandas读取CSV/JSON日志,处理缺失值与异常值

好,咱们进入实战环节。

上一章我们聊了日志格式设计,这一章就真刀真枪地干——把日志数据读进来,洗干净。

你想想看,从嵌入式设备里拿到的原始日志,那叫一个「脏」。缺字段、乱码、时间戳格式不统一、甚至还有传感器抽风输出的负值……我刚开始做功耗分析那会儿,就吃过这个亏。有一次拿着一个月的电池数据做分析,结果因为没处理异常值,算出来的续航时间比实际多了整整两天。嗯,从那以后,数据清洗就成了我流程里雷打不动的第一步。

4.1 用Pandas读取CSV日志

CSV是最常见的日志格式。嵌入式设备通常以逗号分隔,每行一条记录。

我个人习惯用 pd.read_csv(),但有几个参数必须设对。

import pandas as pd

# 读取CSV,指定分隔符,处理时间戳
df = pd.read_csv(
    'battery_log_2024.csv',
    sep=',',
    parse_dates=['timestamp'],
    infer_datetime_format=True,
    na_values=['NA', 'NULL', '']
)

print(df.head())
print(df.info())

这里我重点说两个坑:

  • parse_dates:一定要指定时间列。不指定的话,Pandas会当成字符串,后面做时间序列分析就麻烦了。
  • na_values:设备日志里经常用 'NA' 或空字符串表示缺失。你不告诉Pandas,它就不认。
我的小技巧:如果日志文件很大(比如超过100MB),加一个 chunksize=10000 参数,分块读取。嵌入式日志动辄几百万行,一次性读入内存会崩。

4.2 读取JSON日志

JSON格式在较新的设备上越来越常见。尤其是那些带结构化数据的传感器日志。

# 读取JSON,自动解析嵌套结构
df_json = pd.read_json('sensor_log_2024.json', lines=True)

# 如果JSON是嵌套的,用json_normalize展开
from pandas import json_normalize
import json

with open('nested_log.json', 'r') as f:
    data = json.load(f)

df_nested = json_normalize(data, record_path='samples', meta=['device_id', 'timestamp'])

这里要注意:lines=True 表示每行是一个独立的JSON对象。很多嵌入式设备就是这么写的——每行一个JSON,而不是一个巨大的JSON数组。我遇到过有人用错了参数,结果读进来全是NaN,排查了半天。

4.3 处理缺失值

缺失值在电池日志里太常见了。比如某个采样周期,电压传感器没响应,就留下一个空位。

处理策略有三种,我按优先级排序:

  1. 删除:如果缺失比例很小(<5%),直接删掉。用 df.dropna()
  2. 填充:用前一个值填充(method='ffill'),适合电压、电流这种缓变量。
  3. 插值:用前后值线性插值(df.interpolate()),适合温度、电量百分比。
# 查看缺失情况
print(df.isnull().sum())

# 策略1:删除缺失严重的行
df_clean = df.dropna(subset=['timestamp', 'voltage'])

# 策略2:前向填充(适合电流)
df_clean['current'] = df_clean['current'].fillna(method='ffill')

# 策略3:线性插值(适合温度)
df_clean['temperature'] = df_clean['temperature'].interpolate()
注意:千万不要对时间戳做插值!时间戳缺失说明那次采样根本没发生,插值会伪造一个不存在的时间点。我曾经见过有人这么干,结果续航曲线出现了「时间倒流」的诡异现象。

4.4 异常值检测与处理

异常值比缺失值更隐蔽。设备可能正常上报数据,但数值明显不合理。

常见的电池日志异常值:

  • 电压为负值(传感器故障)
  • 电流超过电池最大放电能力(比如3.7V电池报出10A电流)
  • 温度超过-40°C~85°C范围(工业级芯片极限)
  • 电量百分比突然跳变(比如从80%直接跳到50%)

我一般用两种方法:

4.4.1 基于业务规则的过滤

# 电压合理范围:2.5V ~ 4.5V(单节锂电池)
df_valid = df_clean[(df_clean['voltage'] >= 2.5) & (df_clean['voltage'] <= 4.5)]

# 电流合理范围:-5A ~ 5A(根据设备功耗设定)
df_valid = df_valid[(df_valid['current'] >= -5) & (df_valid['current'] <= 5)]

# 温度合理范围:-20°C ~ 60°C
df_valid = df_valid[(df_valid['temperature'] >= -20) & (df_valid['temperature'] <= 60)]

4.4.2 基于统计的异常检测

有些异常值在业务规则范围内,但明显偏离正常趋势。比如电压在3.7V附近波动,突然出现一个3.2V又马上恢复。

# 用Z-score检测离群点
from scipy import stats

z_scores = stats.zscore(df_valid['voltage'])
df_no_outlier = df_valid[(abs(z_scores) < 3)]

# 或者用IQR方法
Q1 = df_valid['voltage'].quantile(0.25)
Q3 = df_valid['voltage'].quantile(0.75)
IQR = Q3 - Q1

df_no_outlier = df_valid[
    (df_valid['voltage'] >= Q1 - 1.5 * IQR) &
    (df_valid['voltage'] <= Q3 + 1.5 * IQR)
]

核心原则:先业务规则过滤,再统计方法检测。业务规则能干掉90%的明显错误,统计方法用来抓那些「看起来对但实际不对」的异常。

4.5 实战:完整的数据清洗流水线

把上面所有步骤串起来,写成一个函数。这样每次拿到新日志,跑一遍就行。

def clean_battery_log(filepath, file_type='csv'):
    """电池日志清洗流水线"""
    # 1. 读取
    if file_type == 'csv':
        df = pd.read_csv(filepath, parse_dates=['timestamp'], na_values=['NA', 'NULL', ''])
    elif file_type == 'json':
        df = pd.read_json(filepath, lines=True)
    else:
        raise ValueError("不支持的文件格式")
    
    # 2. 删除缺失严重的时间戳和电压
    df = df.dropna(subset=['timestamp', 'voltage'])
    
    # 3. 填充其他缺失值
    df['current'] = df['current'].fillna(method='ffill')
    df['temperature'] = df['temperature'].interpolate()
    
    # 4. 业务规则过滤
    df = df[(df['voltage'] >= 2.5) & (df['voltage'] <= 4.5)]
    df = df[(df['current'] >= -5) & (df['current'] <= 5)]
    df = df[(df['temperature'] >= -20) & (df['temperature'] <= 60)]
    
    # 5. 统计异常检测(只对电压做)
    Q1 = df['voltage'].quantile(0.25)
    Q3 = df['voltage'].quantile(0.75)
    IQR = Q3 - Q1
    df = df[(df['voltage'] >= Q1 - 1.5 * IQR) & (df['voltage'] <= Q3 + 1.5 * IQR)]
    
    # 6. 按时间排序
    df = df.sort_values('timestamp').reset_index(drop=True)
    
    print(f"清洗完成:原始{len(df)}行,保留{len(df)}行")
    return df

# 使用
df_clean = clean_battery_log('battery_log_2024.csv')

这个流水线我用了好几年,在十几个项目里验证过。你拿过去改改阈值就能用。

4.6 清洗后的数据质量检查

清洗完了别急着分析。先做几个快速检查:

检查项 方法 预期结果
缺失值 df.isnull().sum() 全部为0
时间连续性 df['timestamp'].diff().value_counts() 采样间隔一致
数值范围 df.describe() min/max在合理区间
重复行 df.duplicated().sum() 为0

嗯,到这里数据就洗干净了。下一章我们会用这些干净数据做可视化分析,看看电池到底是怎么耗电的。

避坑指南:我曾经在清洗时忘了重置索引(reset_index),结果后面做时间窗口聚合时,索引和实际行对不上,算出来的平均电流全是错的。所以记得加 reset_index(drop=True)