第7章 数据处理:pandas与numpy的测试数据流水线#
本章核心理念:嵌入式工程师的数据不是教科书里的”Iris鸢尾花数据集”,而是从串口、网络、传感器中滚滚而来的真实工程日志——它们带着噪声、缺失值、乱序时间戳和各种”脏数据”。本章教你用 pandas 建立一条从”原始日志”到”清晰报表”的自动化流水线。
7.1 场景与痛点:海量测试日志的”数据沼泽”#
7.1.1 一个真实的周末噩梦#
周五下午五点半,你刚准备收工,老板走过来说:“下周一批量生产之前,把这批 500 块板子的温升测试数据整理成报表,每个板子算一下最高温度、平均温度、温升速率超标的次数……”
你看了看文件夹:
test_logs/
├── board_001_temp.csv
├── board_002_temp.csv
├── board_003_temp.csv
├── ...(共500个文件)plaintext每个文件长这样:
timestamp,sensor_id,temperature,voltage,status
2024-05-20 08:00:01.123,NTC_1,25.34,3.302,OK
2024-05-20 08:00:01.123,NTC_1,25.34,3.302,OK ← 重复行
2024-05-20 08:00:01.456,NTC_2,25.67,3.298,OK
2024-05-20 08:00:01.789,NTC_1,,3.301,OK ← 温度缺失
2024-05-20 08:00:02.001,NTC_3,1256.80,3.300,OK ← 明显的传感器错误值
2024-05-20 08:00:02.345,NTC_1,25.89,3.302,
2024-05-20 08:00:02.890,NTC_2,26.01,3.297,OK
...(每个文件上万行)csv你的痛点清单:
| 痛点 | 具体表现 |
|---|---|
| 数据量大 | 500个文件 × 每个文件2万行 = 1000万行,Excel 打开就卡死 |
| 格式不规范 | 有重复行、空值、传感器异常尖峰、status列有时为空 |
| 需要聚合 | 要按板子编号分组、按传感器ID分组、按时间段统计,再汇总成一张总表 |
| 时间对齐 | 三个传感器的采样频率不同(100Hz、10Hz、1Hz),需要对齐到统一时间轴才能做关联分析 |
| 重复性劳动 | 每批货都要跑一遍,下周又来一批,你不想再手动搞一次 |
如果你的处理逻辑是”打开Excel → 复制粘贴 → 手动筛选 → 求平均值”,那你这个周末就交代在这了。
7.1.2 我们需要什么样的工具?#
理想的工具应该能做到:
- 一行代码读取整个文件夹的几百个CSV,并自动合并为一个大表。
- 几秒钟内完成上万行数据的筛选、去重、缺失值填补。
- 按任意维度(板子、传感器、时间段)分组聚合,一行代码出结果。
- 将不同采样率的时间序列对齐到统一时间轴。
- 整个流程可复用——下周来新数据,改个文件夹路径,重新运行即可。
这个工具,就是 pandas。
7.2 技术选型:原生 csv 模块 vs pandas#
7.2.1 Python 标准库 csv 模块能做什么?#
Python 自带的 csv 模块可以读写 CSV 文件,它轻量、零依赖,对于简单的逐行处理完全够用。
import csv
with open('board_001_temp.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if row['status'] == 'OK':
print(f"温度: {row['temperature']}°C")pythoncsv 模块的局限:它只是一个”逐行读取器”,读出来的每一行都是字符串(str),你需要自己做类型转换、自己写循环筛选、自己实现分组聚合。当你的需求从”读出来看看”升级到”清洗、分析、统计”时,csv 模块就像用螺丝刀盖房子——工具本身没问题,但你得累死。
7.2.2 什么时候该用 pandas 这把”牛刀”?#
决策阈值:当你的数据处理需求命中以下任意一条时,请毫不犹豫地切换到 pandas:
| 条件 | 说明 |
|---|---|
| 数据量 > 1万行 | pandas 基于 NumPy 的 C 语言底层实现,处理速度比纯 Python 循环快 10~100 倍 |
| 需要复杂筛选 | 多条件组合查询(如”温度>30 且 电压<3.2 且 status为OK”),pandas 一行搞定 |
| 需要分组聚合 | ”按板子编号分组,算每个板子的平均温度”——pandas 的 groupby() 是专门为此设计的 |
| 需要时间序列处理 | 按时间窗口重采样、对齐不同频率的时间序列——pandas 内置强大的时间序列引擎 |
| 需要合并多表 | 将500个CSV文件合并成一个大表,或者将两张表按某个键做 SQL 式的 JOIN |
| 需要输出报表 | 将结果导出为格式化的 Excel 文件(含多个 Sheet、合并单元格、条件格式) |
7.2.3 pandas 与 NumPy 的关系#
很多初学者会困惑:pandas 和 NumPy 是什么关系?我需要先学 NumPy 吗?
一句话回答:NumPy 是引擎,pandas 是车身。
| 维度 | NumPy | pandas |
|---|---|---|
| 核心数据结构 | ndarray(多维数组) | Series(一列)、DataFrame(一张表) |
| 适合的数据类型 | 同类型数值矩阵(全是浮点数、全是整数) | 异构表格数据(一列是时间戳、一列是浮点数、一列是字符串) |
| 典型用途 | 数学运算、矩阵计算、信号处理(配合 scipy) | 数据清洗、统计分析、报表生成 |
| 对嵌入式工程师 | 用于处理 ADC 原始波形数据、做 FFT 频谱分析 | 用于处理测试日志、生成自动化报表 |
实践建议:对于本章的数据处理任务,pandas 是主角,NumPy 是幕后英雄。你几乎不需要直接使用 NumPy 的 API——pandas 在底层已经帮你调用了。
7.2.4 安装#
# 推荐使用 pip 安装(pandas 内部已包含 numpy 依赖)
pip install pandas openpyxl
# openpyxl 是 pandas 读写 .xlsx 文件的后端引擎,强烈建议一并安装bash7.3 核心方法论:数据清洗三板斧与时间序列对齐#
7.3.1 第一板斧:去重——删除”幽灵行”#
问题来源:串口通信中偶尔出现的重复发送、数据采集软件的缓存回放、或者简单的人为复制粘贴错误。
识别重复的标准:你需要决定”什么算重复”——是所有列都一样算重复?还是某个关键列(如 timestamp + sensor_id)一样就算重复?
import pandas as pd
df = pd.read_csv('board_001_temp.csv')
# 方法1:删除所有列完全相同的行
df_unique = df.drop_duplicates()
# 方法2(更常用):删除关键字段相同的重复行,保留第一条
df_unique = df.drop_duplicates(subset=['timestamp', 'sensor_id'], keep='first')
# 查看删除了多少行
print(f"原始行数: {len(df)}, 去重后: {len(df_unique)}, 删除了 {len(df) - len(df_unique)} 行重复数据")pythonkeep 参数的选择:
keep='first':保留第一次出现的行(最常用,假设第一次是对的)。keep='last':保留最后一次出现的行(适用于”后来的修正覆盖前面的”场景)。keep=False:把所有重复的行都删掉(最严格,适用于你不想冒任何风险的场景)。
7.3.2 第二板斧:填补缺失值——别让”空洞”毁了你的统计#
缺失值的典型来源:
- 传感器通信瞬间丢失,某条数据的某个字段为空。
- CSV 文件中本身就缺少某些列(有的行有
status列,有的没有)。 - 数据采集软件崩溃重启后,中间有一段时间没有记录。
pandas 读取时会自动将空值识别为 NaN(Not a Number),这是它的一大便利。
# 快速查看缺失值情况
print(df.isnull().sum())
# 输出示例:
# timestamp 0
# sensor_id 0
# temperature 3 ← 有3行温度缺失
# voltage 0
# status 1 ← 有1行状态缺失python填补策略的选择:
| 策略 | 方法 | 适用场景 |
|---|---|---|
| 前向填充 | df['temperature'].fillna(method='ffill') | 传感器数据:温度不会突变,用前一个有效值填补(最常用) |
| 后向填充 | df['temperature'].fillna(method='bfill') | 需要用后面的值回填(如记录开始时的缺失) |
| 线性插值 | df['temperature'].interpolate(method='linear') | 需要更平滑的过渡,前后值之间按比例插值 |
| 固定值填充 | df['status'].fillna('UNKNOWN') | 分类字段,用一个明确的标记值代替(如 ‘UNKNOWN’、‘N/A’) |
| 直接删除 | df.dropna(subset=['temperature']) | 缺失比例极低(<1%)且该字段是关键数据,不能容忍任何猜测 |
# 实战:对不同字段用不同策略
df['temperature'] = df['temperature'].interpolate(method='linear') # 温度用线性插值
df['status'] = df['status'].fillna('UNKNOWN') # 状态用固定值python7.3.3 第三板斧:格式转换——让数据”说人话”#
CSV 文件读进来的所有东西都是字符串。pandas 会尝试自动推断类型,但你经常需要手动修正。
# 1. 时间列:转为 pandas 的 datetime 类型(这是后续一切时间操作的基础!)
df['timestamp'] = pd.to_datetime(df['timestamp'])
# 2. 数值列:强制转为浮点数,无法转换的标记为 NaN
df['temperature'] = pd.to_numeric(df['temperature'], errors='coerce')
df['voltage'] = pd.to_numeric(df['voltage'], errors='coerce')
# 3. 字符列:去除首尾空格(常见的"隐形杀手")
df['sensor_id'] = df['sensor_id'].str.strip()
# 4. 分类列:转为 category 类型(节省内存,加速 groupby)
df['status'] = df['status'].astype('category')python为什么要特别关注 to_datetime?
因为一旦 timestamp 列被正确识别为 datetime 类型,pandas 就会为你解锁一整套时间序列操作能力:按小时/天/月聚合、时间窗口滚动计算、时间差计算、时区转换……这些在下一节的”时间序列对齐”中至关重要。
7.3.4 去除异常值:给数据”修剪枯枝”#
工业传感器数据中,偶尔会出现明显不合理的尖峰。比如室温测试中突然出现一个 1256.80°C 的读数,那大概率是传感器通信错误,而不是真的着火了。
# 方法1:简单阈值过滤(适合你已经知道合理范围的场景)
df = df[(df['temperature'] >= -40) & (df['temperature'] <= 150)]
# 方法2:基于统计的异常检测(IQR 方法,适合不确定合理范围时)
Q1 = df['temperature'].quantile(0.25)
Q3 = df['temperature'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常但不直接删除,方便后续审查
df['is_outlier'] = (df['temperature'] < lower_bound) | (df['temperature'] > upper_bound)
outlier_count = df['is_outlier'].sum()
print(f"发现 {outlier_count} 个异常值,范围外: [{lower_bound:.2f}, {upper_bound:.2f}]")
# 将异常值替换为 NaN,再用插值填补
df.loc[df['is_outlier'], 'temperature'] = None
df['temperature'] = df['temperature'].interpolate(method='linear')
df = df.drop(columns=['is_outlier'])python7.3.5 进阶:时间序列重采样与对齐——不同频率传感器的”同步合奏”#
这是嵌入式数据处理中最具特色、也最容易踩坑的环节。
为什么需要对齐?#
一个真实的测试场景:你的待测设备上贴了三种传感器:
| 传感器 | 采样频率 | 数据含义 |
|---|---|---|
| 三轴加速度计 | 100 Hz(每10ms一条) | 振动分析 |
| 热电偶温度 | 10 Hz(每100ms一条) | 温升曲线 |
| 电池电压 | 1 Hz(每1秒一条) | 供电稳定性 |
它们各自输出的 CSV 文件,时间戳不可能完全对齐——加速度计可能在 08:00:00.010 采了一个点,温度传感器在 08:00:00.095 采了一个点,电压在 08:00:01.002 采了一个点。
如果你想分析”当电压骤降时,加速度和温度有什么变化”,就必须先把三个数据源对齐到同一个时间轴上。
策略一:降采样(高频率 → 低频率)#
把所有数据统一到最低频率(比如 1Hz),对高频数据做聚合。
# 假设 accel_df 是加速度数据,已设置 timestamp 为索引
# 将100Hz数据按1秒窗口聚合(取均值、最大值、标准差)
accel_resampled = accel_df.resample('1s').agg({
'accel_x': ['mean', 'max', 'std'],
'accel_y': ['mean', 'max', 'std'],
'accel_z': ['mean', 'max', 'std']
})
# 结果:每秒一行,包含该秒内加速度的均值、峰值、波动
temp_resampled = temp_df.resample('1s').mean() # 温度取1秒均值
voltage_1hz = voltage_df # 电压本身就是1Hz,无需处理python策略二:merge_asof——最近时间匹配#
当两个数据源的采样时间不完全一致时,merge_asof 是 pandas 提供的”杀手级”功能。它为左表中的每个时间戳,在右表中找到时间上最接近的那条记录进行匹配。
# 模拟数据
import pandas as pd
import numpy as np
# 加速度计数据:100Hz
accel_times = pd.date_range('2024-05-20 08:00:00', periods=500, freq='10ms')
accel_df = pd.DataFrame({
'timestamp': accel_times,
'accel_x': np.random.randn(500) * 0.5, # 模拟振动数据
'accel_y': np.random.randn(500) * 0.3,
'accel_z': np.random.randn(500) * 0.4 + 9.8 # Z轴含重力分量
})
# 温度数据:10Hz
temp_times = pd.date_range('2024-05-20 08:00:00.05', periods=50, freq='100ms')
temp_df = pd.DataFrame({
'timestamp': temp_times,
'temperature': np.linspace(25.0, 32.5, 50) # 模拟温升曲线
})
# 电压数据:1Hz
volt_times = pd.date_range('2024-05-20 08:00:00.5', periods=5, freq='1s')
volt_df = pd.DataFrame({
'timestamp': volt_times,
'voltage': [3.30, 3.29, 3.27, 3.25, 3.28]
})
# 使用 merge_asof 将温度"附着"到加速度计的时间轴上
# 方向='nearest' 表示找时间上最近的(无论前后)
merged = pd.merge_asof(
accel_df.sort_values('timestamp'),
temp_df.sort_values('timestamp'),
on='timestamp',
direction='nearest' # 关键参数:找最近的时间戳
)
# 继续将电压也合入
merged = pd.merge_asof(
merged.sort_values('timestamp'),
volt_df.sort_values('timestamp'),
on='timestamp',
direction='nearest',
tolerance=pd.Timedelta('2s') # 可选:超过2秒的差距就不匹配了,避免"张冠李戴"
)
print(merged.head(10))
print(f"合并后总行数: {len(merged)}") # 仍然是500行(以加速度计为基准)python输出示意:
timestamp accel_x accel_y accel_z temperature voltage
0 2024-05-20 08:00:00.000 0.342156 0.123456 9.812345 NaN NaN
1 2024-05-20 08:00:00.010 0.567890 0.234567 9.923456 NaN NaN
2 2024-05-20 08:00:00.020 -0.123456 -0.345678 9.678901 NaN NaN
...
9 2024-05-20 08:00:00.090 0.789012 0.456789 10.012345 25.25 NaNplaintext注意前几行的 temperature 和 voltage 是 NaN,因为加速度计最早的时间点(08:00:00.000)早于温度传感器的起始时间(08:00:00.050),tolerance 限制了匹配范围。
策略三:插值补齐——填补匹配后的空洞#
merge_asof 之后,可能仍有一些 NaN(时间差距超出 tolerance,或者刚好卡在两个采样点之间)。这时可以对合并后的数据做一次插值。
# 对合并后的数值列做线性插值
numeric_cols = ['temperature', 'voltage']
merged[numeric_cols] = merged[numeric_cols].interpolate(method='linear')
# 首尾的 NaN 可能插值不了(没有参考点),用前向/后向填充兜底
merged[numeric_cols] = merged[numeric_cols].fillna(method='ffill').fillna(method='bfill')
print(merged.isnull().sum()) # 确认没有缺失值了python完整对齐流程总结:
原始数据(不同频率)
│
▼
各数据源分别读取 → 转 datetime → 设置索引
│
▼
降采样到统一频率(可选,取决于分析粒度需求)
│
▼
merge_asof 逐个合入(以最高频数据为基准)
│
▼
interpolate + ffill/bfill 兜底填补
│
▼
对齐完成的统一 DataFrame ✅plaintext7.4 核心实战:分组聚合生成自动化测试报表#
现在,让我们回到 7.1 节的那个”周末噩梦”场景,用 pandas 建立一条完整的数据处理流水线。
7.4.1 目标#
输入:一个文件夹,里面放着 500 块板子的温升测试 CSV 文件。
输出:一个 Excel 报表,包含:
- 汇总表:每块板子一行,列出最高温度、平均温度、温升速率超标次数、测试结论(PASS/FAIL)。
- 异常明细表:所有温度异常数据点的详细记录,方便追溯。
7.4.2 完整代码#
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime
# ============================================================
# 配置区(只改这里!)
# ============================================================
INPUT_DIR = Path('./test_logs/') # CSV文件所在的文件夹
OUTPUT_FILE = 'test_report_20240520.xlsx' # 输出的Excel报表文件名
TEMP_MAX_LIMIT = 85.0 # 最高温度阈值(°C)
TEMP_RISE_RATE_LIMIT = 2.0 # 温升速率阈值(°C/min),超过即标记
EXPECTED_SAMPLES = 10000 # 预期每块板子的最低采样数(数据完整性检查)
# ============================================================
# 第一步:批量读取并合并所有 CSV 文件
# ============================================================
def load_all_csv(directory: Path) -> pd.DataFrame:
"""读取目录下所有CSV文件,添加'board_id'列,合并为一个大DataFrame"""
all_dfs = []
csv_files = sorted(directory.glob('*.csv'))
if not csv_files:
raise FileNotFoundError(f"在 {directory} 下未找到任何CSV文件")
for file_path in csv_files:
# 从文件名提取板子编号,例如 'board_001_temp.csv' → '001'
board_id = file_path.stem.split('_')[1]
try:
df = pd.read_csv(file_path)
df['board_id'] = board_id
all_dfs.append(df)
except Exception as e:
print(f"⚠️ 读取 {file_path.name} 失败: {e}")
continue
combined = pd.concat(all_dfs, ignore_index=True)
print(f"✅ 共读取 {len(csv_files)} 个文件, 合并后共 {len(combined)} 行数据")
return combined
# ============================================================
# 第二步:数据清洗流水线
# ============================================================
def clean_data(df: pd.DataFrame) -> pd.DataFrame:
"""标准化的数据清洗流水线"""
print("\n--- 数据清洗开始 ---")
original_count = len(df)
# 2.1 去重
df = df.drop_duplicates(subset=['timestamp', 'board_id', 'sensor_id'], keep='first')
print(f" 去重后: {original_count} → {len(df)} 行 (删除 {original_count - len(df)} 行重复)")
# 2.2 格式转换
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['temperature'] = pd.to_numeric(df['temperature'], errors='coerce')
df['voltage'] = pd.to_numeric(df['voltage'], errors='coerce')
df['sensor_id'] = df['sensor_id'].str.strip()
# 2.3 缺失值处理
missing_temp = df['temperature'].isnull().sum()
df['temperature'] = df['temperature'].interpolate(method='linear')
df['temperature'] = df['temperature'].fillna(method='ffill').fillna(method='bfill')
df['status'] = df['status'].fillna('UNKNOWN')
print(f" 温度缺失值: {missing_temp} 个(已插值填补)")
# 2.4 异常值标记与处理
df['is_temp_outlier'] = (df['temperature'] < -40) | (df['temperature'] > 150)
outlier_count = df['is_temp_outlier'].sum()
if outlier_count > 0:
print(f" 发现 {outlier_count} 个温度异常值(<-40°C 或 >150°C),已标记")
# 用前向填充替换异常值
df.loc[df['is_temp_outlier'], 'temperature'] = None
df['temperature'] = df['temperature'].fillna(method='ffill')
# 2.5 排序
df = df.sort_values(['board_id', 'sensor_id', 'timestamp']).reset_index(drop=True)
print(f"--- 清洗完成,最终 {len(df)} 行 ---\n")
return df
# ============================================================
# 第三步:按板子分组聚合,生成汇总指标
# ============================================================
def generate_summary(df: pd.DataFrame) -> pd.DataFrame:
"""对每块板子计算关键统计指标"""
# 按板子分组
grouped = df.groupby('board_id')
# 计算每个板子的统计量
summary = grouped.agg(
temp_max=('temperature', 'max'),
temp_min=('temperature', 'min'),
temp_mean=('temperature', 'mean'),
temp_std=('temperature', 'std'),
sample_count=('temperature', 'count'),
voltage_mean=('voltage', 'mean'),
).round(2)
# 计算温升速率:对每块板子的温度数据求差分,再除以时间差
def calc_max_rise_rate(group):
"""计算最大温升速率(°C/min)"""
group = group.sort_values('timestamp')
temp_diff = group['temperature'].diff()
time_diff = group['timestamp'].diff().dt.total_seconds() / 60 # 转为分钟
# 避免除以零
valid = time_diff > 0
if valid.any():
rates = temp_diff[valid] / time_diff[valid]
return round(rates.max(), 2)
return 0.0
rise_rates = grouped.apply(calc_max_rise_rate)
summary['max_rise_rate'] = rise_rates
# 计算超标次数
def count_exceeded(group):
"""计算温度超过阈值的数据点数量"""
return (group['temperature'] > TEMP_MAX_LIMIT).sum()
exceed_counts = grouped.apply(count_exceeded)
summary['exceed_count'] = exceed_counts
# 数据完整性检查
summary['data_complete'] = summary['sample_count'] >= EXPECTED_SAMPLES
# 最终判定
def judge(row):
if not row['data_complete']:
return 'INCOMPLETE'
if row['temp_max'] > TEMP_MAX_LIMIT:
return 'FAIL'
if row['max_rise_rate'] > TEMP_RISE_RATE_LIMIT:
return 'WARN'
return 'PASS'
summary['verdict'] = summary.apply(judge, axis=1)
# 重置索引,让 board_id 变回普通列
summary = summary.reset_index()
# 重新排列列顺序,让结论放最前面
cols = ['board_id', 'verdict', 'temp_max', 'temp_mean', 'temp_min',
'temp_std', 'max_rise_rate', 'exceed_count',
'sample_count', 'data_complete', 'voltage_mean']
summary = summary[cols]
return summary
# ============================================================
# 第四步:提取异常明细
# ============================================================
def extract_anomalies(df: pd.DataFrame) -> pd.DataFrame:
"""提取所有需要关注的异常数据点"""
# 条件1:温度超过阈值
high_temp = df[df['temperature'] > TEMP_MAX_LIMIT].copy()
high_temp['anomaly_type'] = 'HIGH_TEMP'
# 条件2:原数据中的异常值标记
flagged = df[df['is_temp_outlier']].copy()
flagged['anomaly_type'] = 'SENSOR_ERROR'
# 合并异常记录
anomalies = pd.concat([high_temp, flagged], ignore_index=True)
anomalies = anomalies.drop(columns=['is_temp_outlier'], errors='ignore')
if not anomalies.empty:
anomalies = anomalies.sort_values(['board_id', 'timestamp'])
return anomalies
# ============================================================
# 第五步:输出 Excel 报表
# ============================================================
def export_report(summary: pd.DataFrame, anomalies: pd.DataFrame, output_path: str):
"""将汇总表和异常明细写入同一个Excel文件的不同Sheet"""
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Sheet 1: 汇总表
summary.to_excel(writer, sheet_name='板子汇总', index=False)
# Sheet 2: 异常明细
if not anomalies.empty:
anomalies.to_excel(writer, sheet_name='异常明细', index=False)
else:
pd.DataFrame({'说明': ['未发现任何异常数据']}).to_excel(
writer, sheet_name='异常明细', index=False
)
print(f"✅ 报表已生成: {output_path}")
print(f" - 汇总: {len(summary)} 块板子")
verdict_counts = summary['verdict'].value_counts()
for v, c in verdict_counts.items():
print(f" - {v}: {c} 块")
# ============================================================
# 主程序入口
# ============================================================
if __name__ == '__main__':
# 1. 加载数据
raw_df = load_all_csv(INPUT_DIR)
# 2. 清洗数据
clean_df = clean_data(raw_df)
# 3. 生成汇总
summary_df = generate_summary(clean_df)
# 4. 提取异常
anomalies_df = extract_anomalies(clean_df)
# 5. 输出报表
export_report(summary_df, anomalies_df, OUTPUT_FILE)python7.4.3 代码解读与设计决策#
1. 为什么用函数拆分而不是写成一个大函数?
每个函数职责单一,方便:
- 单独测试:你可以只调用
clean_data()看看清洗效果,不影响其他步骤。 - 替换策略:如果换了一批数据,清洗规则不同,只需替换
clean_data()函数。 - AI 协作:把某个函数单独喂给 AI 审查或优化,上下文清晰。
2. 配置区为什么放在文件顶部?
这是工程脚本的黄金法则——所有需要根据项目调整的参数集中放在最前面,用大写字母常量命名。下次换一批数据、换一个阈值,你只需要改最上面的几行,而不是在代码深处翻找。
3. groupby().agg() vs groupby().apply()
agg()适合内置聚合函数(max, mean, std, count 等),速度快。apply()适合自定义复杂逻辑(如温升速率需要同时用到温度和时间的差分),更灵活但稍慢。
在我们的代码中,简单统计用 agg(),温升速率计算用 apply(),各取所长。
7.5 AI协作指南:用自然语言描述数据特征与期望输出#
7.5.1 核心理念:把你的数据”翻译”给AI听#
pandas 的 API 非常丰富,同一个需求可能有十种写法。AI 的优势在于:你用自然语言描述”我有什么数据、我想得到什么结果”,它就能帮你生成对应的 pandas 代码链。
但 AI 生成的代码能否直接用,取决于你给它的上下文质量。
7.5.2 万能 Prompt 模板#
【数据描述】
我的CSV文件路径是: xxx.csv
列名及含义:
- timestamp: 采样时间,格式 "2024-05-20 08:00:01.123"
- sensor_id: 传感器编号,值为 "NTC_1", "NTC_2", "NTC_3"
- temperature: 温度值(°C),浮点数,可能有缺失值
- voltage: 电压值(V),浮点数
- status: 状态标记,"OK" 或 "ERROR" 或空值
数据特征:
- 约20000行,单个传感器约6600行
- 采样频率约10Hz
- 存在重复行、缺失值、传感器偶发异常尖峰
【期望输出】
我想得到一份按 sensor_id 分组的统计报表:
1. 每个传感器的温度: 最大值、最小值、均值、标准差
2. 每个传感器温度超过 85°C 的次数
3. 每个传感器的数据完整性(实际行数 vs 预期行数)
4. 最后输出为 Excel 文件,包含两个 Sheet: "汇总" 和 "高温明细"
【附加要求】
- 代码要有清晰的函数拆分和中文注释
- 清洗步骤包括: 去重、缺失值插值、异常值过滤(<-40 或 >150)
- 输出代码要能直接运行plaintext7.5.3 AI 审查重点清单#
AI 生成的 pandas 代码,重点审查以下容易出错的地方:
| 审查项 | 常见问题 | 检查方法 |
|---|---|---|
| 类型转换 | 没有调用 pd.to_datetime() 或 pd.to_numeric(),导致后续时间操作或数值比较失败 | 在转换后加 print(df.dtypes) 确认 |
| 链式赋值陷阱 | df[condition]['col'] = value 不会修改原始 DataFrame(SettingWithCopyWarning) | 使用 .loc[] 进行赋值:df.loc[condition, 'col'] = value |
| 缺失值处理顺序 | 先插值再去重,可能导致插值出”虚假数据” | 严格按 去重 → 类型转换 → 缺失处理 → 异常过滤 的顺序 |
| 时间索引 | resample() 要求索引是 DatetimeIndex,AI 可能忘记 set_index('timestamp') | 检查 resample 前的索引类型 |
| 内存爆炸 | 读取500个CSV时全部存入列表再 concat,如果每个文件100MB 就会 OOM | 超大数据量时分批读取、处理后只保留结果 |
| groupby 排序 | groupby() 默认会排序,如果不需要排序(性能考虑),加 sort=False | 根据实际需求决定 |
7.5.4 进阶 Prompt:让AI帮你做数据探索#
当你拿到一份陌生的数据集,不确定里面有什么”坑”时,可以让 AI 帮你写一个数据探索脚本:
我有一份CSV文件: data.csv
请帮我写一个数据探索脚本,完成以下任务:
1. 读取文件,输出总行数、列名、各列数据类型
2. 输出每列的缺失值数量和缺失比例
3. 对数值列输出统计摘要(均值、标准差、最小值、25%/50%/75%分位数、最大值)
4. 对时间列输出最早和最晚时间戳,以及总时间跨度
5. 对分类列输出每个类别的出现次数
6. 检测是否存在完全重复的行,输出重复行数
7. 对数值列检测异常值(使用IQR方法),输出异常值数量
8. 将以上所有信息输出为一个结构清晰的文本报告
请使用 pandas,代码中加入中文注释。plaintext这个探索脚本跑一遍,你对数据的”全貌”就有数了,后续的数据清洗和分析就有了方向。
本章小结#
| 你学到了什么 | 解决了什么问题 |
|---|---|
| pandas 基础读写与类型转换 | 告别 Excel 打不开大文件的痛苦 |
| 数据清洗三板斧(去重、填补、转换) | 让”脏数据”变得干净可用 |
| 异常值检测与处理 | 自动识别传感器错误,而不是肉眼逐行检查 |
时间序列重采样与 merge_asof 对齐 | 让不同频率的传感器数据在统一时间轴上”合奏” |
groupby 分组聚合 | 500块板子的数据,一行代码算出每块的统计指标 |
| 自动化报表输出 | 跑一个脚本就出 Excel 报表,老板周一就能看到 |
| AI 协作 Prompt 模板 | 用自然语言描述需求,让 AI 帮你生成和审查 pandas 代码 |
下一章预告:数据处理完了,但”最高温度85.3°C”这样的数字远不如一张漂亮的温升曲线图来得直观。第8章我们将学习如何用 matplotlib 和 Plotly,把枯燥的数字变成工程师一看就懂的专业图表。