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,它就不认。
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 处理缺失值
缺失值在电池日志里太常见了。比如某个采样周期,电压传感器没响应,就留下一个空位。
处理策略有三种,我按优先级排序:
- 删除:如果缺失比例很小(<5%),直接删掉。用
df.dropna()。 - 填充:用前一个值填充(
method='ffill'),适合电压、电流这种缓变量。 - 插值:用前后值线性插值(
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(drop=True)。