以"智慧大棚环境传感器数据"为场景:PM2.5 与湿度正相关、与温度负相关,设备告警由高 PM2.5/高温触发,并人为注入了通信中断缺失值、网络重传重复行、故障异常值(999℃、负湿度)、混入字符串等真实脏数据。
脚本结构与覆盖的函数:
| 环节 | 用到的函数 |
|---|---|
| 数据构造与类型 | np.array、to_numeric、astype、shape、len() |
| 缺失值 | isnull、fillna(均值填充)、ffill/pad、bfill/backfill、dropna |
| 重复值 | duplicated、drop_duplicates |
| 异常值 | quantile(IQR 法)、np.where(打标记)、df.loc(截断修正)、between、query |
| 数据分析 | pd.cut(空气质量分箱)、value_counts、groupby + agg/transform/filter、crosstab、isin、contains、describe、apply(舒适度评价)、drop、concat |
| 可视化 | plot(温度趋势)、scatter(温度-PM2.5 散点) |
| 机器学习 | get_dummies、train_test_split、fit_transform、StandardScaler、Pipeline、LinearRegression、RandomForestRegressor、LogisticRegression、fit_resample(过采样,只在训练集上,避免泄漏)、predict、score、mean_squared_error、r2_score、mean、argmax、argsort、dump |
运行结果要点(均在注释中解释了原因):
- 清洗流程:203 行 → 去缺失 199 行 → 去重 196 行 → 剔除异常 195 行
- PM2.5 等级(优/良/污染)与区域交叉表清晰呈现
- 回归任务:线性回归 R²≈0.48,随机森林 R²≈0.49,并输出了特征重要性(湿度影响最大,与数据生成逻辑一致,可用来讲解"模型可解释性")
- 分类任务:过采样解决 176:18 的类别不平衡后,逻辑回归准确率约 0.97
运行后会同时生成两张图表和一个保存好的模型文件 pm25预测模型.pkl(演示 joblib.dump 的部署流程)。依赖:pandas numpy matplotlib scikit-learn imbalanced-learn joblib。
python
# -*- coding: utf-8 -*-
"""
=========================================================================
人工智能训练师(Python 语法)教学示例
场景:智慧大棚环境传感器数据处理与建模
流程:构造数据 -> 缺失值处理 -> 重复值处理 -> 异常值处理
-> 数据分析(分组/透视/筛选)-> 可视化 -> 机器学习建模
=========================================================================
"""
import numpy as np
import pandas as pd
import joblib # 用于 dump 保存模型
import matplotlib
matplotlib.use("Agg") # 无界面环境下只保存图片不弹窗
import matplotlib.pyplot as plt
# 设置中文字体,保证图中中文正常显示(按可用字体自动回退)
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "WenQuanYi Zen Hei",
"Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False # 正常显示负号
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
from imblearn.over_sampling import RandomOverSampler # 提供 fit_resample
# =====================================================================
# 0. 构造一组"带脏数据"的传感器数据(模拟真实采集问题)
# 字段:设备编号、区域、温度、湿度、PM2.5、光照、采集时间
# =====================================================================
np.random.seed(42)
n = 200
# 先生成基础气象数据
temperature = np.round(np.random.normal(25, 3, n), 1) # 温度(℃)
humidity = np.round(np.random.normal(60, 12, n), 1) # 湿度(%)
light = np.round(np.random.normal(800, 100, n), 0) # 光照(lux)
# PM2.5 与湿度正相关、与温度负相关、受光照轻微影响(符合物理直觉),叠加随机噪声
pm25 = np.round(15 + 0.6 * humidity + 0.02 * light - 0.5 * temperature
+ np.random.normal(0, 8, n), 1)
data = {
"device_id": np.random.choice(["S01", "S02", "S03", "S04"], n), # 传感器编号
"region": np.random.choice(["东区", "西区", "南区", "北区"], n), # 大棚分区
"temperature": temperature,
"humidity": humidity,
"pm25": pm25,
"light": light,
# 设备状态与数据逻辑相关:PM2.5 过高或温度过高时更可能告警
"status": np.where((pm25 > 70) | (temperature > 30), "告警", "正常")
}
df = pd.DataFrame(data)
# ---- 人为制造脏数据,模拟真实传感器采集问题 ----
df.loc[5, "temperature"] = np.nan # 缺失值1:通信中断导致温度未上报
df.loc[10, "humidity"] = np.nan # 缺失值2:湿度传感器瞬时故障
df.loc[20, "pm25"] = np.nan # 缺失值3
df.loc[30, "temperature"] = 999.0 # 异常值:传感器故障输出极大值
df.loc[40, "humidity"] = -5.0 # 异常值:湿度不可能为负
df = pd.concat([df, df.iloc[[0, 1, 2]]], ignore_index=True) # 重复值:网络重传导致重复记录
df["pm25"] = df["pm25"].astype(object) # 先转为 object 类型,才能混入字符串模拟脏数据
df.loc[50, "pm25"] = "未知" # 脏数据:混入非数值字符串
print("=" * 60)
print("原始数据预览:")
print(df.head())
# shape:查看数据的形状(行数, 列数),了解数据集规模
print("\n数据形状(行, 列):", df.shape)
# =====================================================================
# 1. 数据类型检查与清洗
# =====================================================================
# to_numeric:把某列强制转换为数值型,errors="coerce" 表示无法转换的(如"未知")变成 NaN
df["pm25"] = pd.to_numeric(df["pm25"], errors="coerce")
# astype:把列转换为指定类型(这里把光照列转为 int 整型)
df["light"] = df["light"].astype(int)
# np.array:把 DataFrame 的一列转成 NumPy 数组,便于向量化数值计算
temp_arr = np.array(df["temperature"])
print("\n温度列转为 NumPy 数组后的前5个元素:", temp_arr[:5])
# =====================================================================
# 2. 缺失值处理(isnull / dropna / fillna / ffill / bfill)
# =====================================================================
# isnull():检测每个元素是否缺失,返回布尔型 DataFrame;配合 sum() 统计每列缺失个数
print("\n各列缺失值数量:")
print(df.isnull().sum())
print("\n缺失的行数:", len(df[df.isnull().any(axis=1)])) # len():统计含缺失值的行数
# --- 策略一:fillna 用指定值/统计量填充 ---
# 用湿度列的均值(mean)填充湿度缺失值,数值型传感器常用均值/中位数填补
df["humidity_fill"] = df["humidity"].fillna(df["humidity"].mean())
# --- 策略二:ffill(前向填充,旧名 pad):用缺失位置"前一个"有效值填充 ---
# 适合时间序列数据:用上一时刻的读数近似当前缺失值
df["temperature_ffill"] = df["temperature"].ffill() # 等价于 df["temperature"].fillna(method="pad")
# --- 策略三:bfill(后向填充,旧名 backfill):用缺失位置"后一个"有效值填充 ---
df["pm25_bfill"] = df["pm25"].bfill() # 等价于 fillna(method="backfill")
# --- 策略四:dropna 删除含缺失值的行 ---
# 当缺失很少且删除不影响分析时,直接丢弃(subset 指定只参考某些列)
df_clean = df.dropna(subset=["temperature", "humidity", "pm25"]).copy()
print("\ndropna 删除缺失行后的形状:", df_clean.shape)
# =====================================================================
# 3. 重复值处理(duplicated / drop_duplicates)
# =====================================================================
# duplicated():标记每一行是否为重复行(完全相同的行),True 表示重复
dup_mask = df_clean.duplicated()
print("\n重复行数量:", dup_mask.sum())
# drop_duplicates():删除重复行,keep="first" 保留第一次出现的记录
df_clean = df_clean.drop_duplicates(keep="first").reset_index(drop=True)
print("去重后的形状:", df_clean.shape)
# =====================================================================
# 4. 异常值处理(quantile / between / np.where / df.loc / query)
# =====================================================================
# quantile():计算分位数,用 IQR(四分位距)法界定异常值边界
Q1 = df_clean["temperature"].quantile(0.25) # 下四分位数
Q3 = df_clean["temperature"].quantile(0.75) # 上四分位数
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
print(f"\n温度 IQR 异常值边界:[{lower:.1f}, {upper:.1f}]")
# np.where():条件判断,类似于三元表达式------满足条件取第一个值,否则取第二个
# 这里给温度打一个"是否异常"的标记列
df_clean["temp_flag"] = np.where(
(df_clean["temperature"] < lower) | (df_clean["temperature"] > upper),
"异常", "正常"
)
# df.loc[]:按"行条件 + 列名"定位并修改数据
# 把超出合理范围的温度异常值替换为上下边界值(截断法/winsorize)
df_clean.loc[df_clean["temperature"] > upper, "temperature"] = upper
df_clean.loc[df_clean["temperature"] < lower, "temperature"] = lower
# between():判断数值是否落在某个区间内(含边界),返回布尔 Series
valid_humidity = df_clean["humidity"].between(0, 100) # 湿度合理区间 0~100%
print("湿度在合理范围内的记录数:", valid_humidity.sum())
# query():用字符串表达式筛选数据,写法简洁直观
df_clean = df_clean.query("0 <= humidity <= 100").copy() # 剔除湿度为负的异常记录
print("query 过滤异常湿度后的形状:", df_clean.shape)
# =====================================================================
# 5. 数据分析:离散化、分组统计、透视表、条件筛选
# =====================================================================
# pd.cut():把连续变量分箱离散化------把 PM2.5 按空气质量标准分成不同等级
df_clean["pm25_level"] = pd.cut(
df_clean["pm25"],
bins=[0, 35, 75, 150], # 分箱边界
labels=["优", "良", "污染"] # 每个箱子的标签
)
# value_counts():统计某列各取值出现的次数,常用于查看分布
print("\nPM2.5 等级分布:")
print(df_clean["pm25_level"].value_counts())
# groupby():分组聚合------按区域分组,计算每组温度/湿度的均值
print("\n各区域平均温度与湿度:")
print(df_clean.groupby("region")[["temperature", "humidity"]].mean())
# groupby + agg():一次对多列应用多种聚合函数(更灵活)
print("\n各区域 PM2.5 的均值/最大值/最小值:")
print(df_clean.groupby("region")["pm25"].agg(["mean", "max", "min"]))
# groupby + transform():分组计算后把结果"广播"回原表的每一行(行数不变)
# 常用于构造特征:如"该记录温度 - 所在区域平均温度"= 区域温差
df_clean["temp_diff_region"] = (
df_clean["temperature"] - df_clean.groupby("region")["temperature"].transform("mean")
)
# groupby + filter():按"组的性质"整组筛选------只保留记录数 >= 45 的区域组
df_big_region = df_clean.groupby("region").filter(lambda g: len(g) >= 45)
print("\n记录数>=45 的大区域数据形状:", df_big_region.shape)
# crosstab():交叉列联表------统计两个分类变量的组合频数(区域 × PM2.5等级)
print("\n区域 × PM2.5 等级交叉表:")
print(pd.crosstab(df_clean["region"], df_clean["pm25_level"]))
# isin():判断列的值是否属于给定集合,常用于多值筛选
df_east_south = df_clean[df_clean["region"].isin(["东区", "南区"])]
print("\n东区或南区的记录数:", len(df_east_south))
# str.contains():字符串匹配------筛选 device_id 中包含 "S0" 的记录(模糊匹配)
df_s0 = df_clean[df_clean["device_id"].str.contains("S0")]
print("设备编号含 S0 的记录数:", len(df_s0))
# describe():一键输出数值列的描述性统计(计数/均值/标准差/分位数等)
print("\n数值列描述性统计:")
print(df_clean[["temperature", "humidity", "pm25", "light"]].describe().round(2))
# apply():对行或列应用自定义函数------根据温度返回舒适度评价
df_clean["comfort"] = df_clean["temperature"].apply(
lambda t: "适宜" if 20 <= t <= 28 else "不适宜"
)
# drop():删除不需要的列(axis=1 表示按列删除;删除行用 axis=0)
df_model = df_clean.drop(columns=["humidity_fill", "temperature_ffill", "pm25_bfill"])
# concat():把多个 DataFrame 按行或列拼接(这里演示把两个区域子集拼回去)
df_part1 = df_model[df_model["region"] == "东区"]
df_part2 = df_model[df_model["region"] == "西区"]
df_concat = pd.concat([df_part1, df_part2], axis=0) # axis=0 按行纵向拼接
print("\nconcat 拼接东区+西区后的形状:", df_concat.shape)
# =====================================================================
# 6. 可视化(plot / scatter)
# =====================================================================
plt.figure(figsize=(10, 4))
# plot():折线图------查看温度随样本序号的波动趋势
df_clean["temperature"].head(50).plot(title="前50条记录的温度变化趋势")
plt.xlabel("样本序号")
plt.ylabel("温度(℃)")
plt.tight_layout()
plt.savefig("/mnt/agents/output/温度趋势图.png", dpi=120)
plt.close()
plt.figure(figsize=(6, 5))
# scatter():散点图------观察温度与 PM2.5 的相关关系,按区域着色
regions = df_clean["region"].unique()
for r in regions:
sub = df_clean[df_clean["region"] == r]
plt.scatter(sub["temperature"], sub["pm25"], label=r, alpha=0.6)
plt.xlabel("温度(℃)")
plt.ylabel("PM2.5")
plt.title("温度与 PM2.5 散点图")
plt.legend()
plt.tight_layout()
plt.savefig("/mnt/agents/output/温度_PM25散点图.png", dpi=120)
plt.close()
# =====================================================================
# 7. 特征工程:独热编码(get_dummies)
# =====================================================================
# get_dummies():把类别变量转成 0/1 独热编码,机器学习模型只能接受数值输入
df_feat = pd.get_dummies(df_model, columns=["region", "device_id"], drop_first=True)
print("\n独热编码后的列名:", list(df_feat.columns))
# =====================================================================
# 8. 任务一(回归):预测 PM2.5 ------ LinearRegression / RandomForestRegressor
# =====================================================================
features = [c for c in df_feat.columns
if c not in ["pm25", "pm25_level", "temp_flag", "comfort", "status"]]
X = df_feat[features]
y = df_feat["pm25"]
# train_test_split():把数据集划分为训练集和测试集,test_size=0.2 表示20%作测试
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# fit_transform():fit 学习训练集的均值/标准差,transform 执行标准化(二者合一)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # 测试集只 transform,防止数据泄漏
# --- 模型1:线性回归(直接用标准化后的数据) ---
lr = LinearRegression()
lr.fit(X_train_scaled, y_train)
pred_lr = lr.predict(X_test_scaled) # predict():对测试集做预测
# --- 模型2:随机森林回归(用 Pipeline 串联 标准化 + 模型) ---
# Pipeline:把多个步骤打包成一个整体,避免手动逐步转换,也便于保存部署
pipe_rf = Pipeline([
("scaler", StandardScaler()), # 第一步:标准化
("rf", RandomForestRegressor(n_estimators=100, random_state=42)) # 第二步:随机森林
])
pipe_rf.fit(X_train, y_train)
pred_rf = pipe_rf.predict(X_test)
# score():模型自带评分,回归模型默认返回 R²(决定系数,越接近1越好)
print("\n线性回归 R²:", round(lr.score(X_test_scaled, y_test), 4))
print("随机森林 Pipeline R²:", round(pipe_rf.score(X_test, y_test), 4))
# mean_squared_error():均方误差,预测值与真实值误差平方的平均,越小越好
mse_lr = mean_squared_error(y_test, pred_lr)
mse_rf = mean_squared_error(y_test, pred_rf)
# r2_score():独立的 R² 计算函数(与 score 结果一致)
r2_lr = r2_score(y_test, pred_lr)
r2_rf = r2_score(y_test, pred_rf)
print(f"线性回归 MSE={mse_lr:.2f} R²={r2_lr:.4f}")
print(f"随机森林 MSE={mse_rf:.2f} R²={r2_rf:.4f}")
# mean():均值------对比两个模型的 MSE 均值,做整体评估
print("两个模型 MSE 的平均值:", round(np.mean([mse_lr, mse_rf]), 2))
# 用随机森林的特征重要性找出影响 PM2.5 的关键因素
importances = pipe_rf.named_steps["rf"].feature_importances_
# argsort():返回从小到大排序后的"索引"(不改变原数组),取最后几个即最重要
top_idx = np.argsort(importances)[-3:]
print("\n影响 PM2.5 最重要的3个特征:", [features[i] for i in top_idx])
# argmax():返回最大值的索引------找出最重要的那个特征
print("最重要特征:", features[np.argmax(importances)])
# dump():把训练好的 Pipeline 保存为文件,部署时 load 回来即可直接 predict
joblib.dump(pipe_rf, "/mnt/agents/output/pm25预测模型.pkl")
print("\n模型已保存:pm25预测模型.pkl")
# =====================================================================
# 9. 任务二(分类):预测设备是否"告警" ------ LogisticRegression + fit_resample
# =====================================================================
Xc = df_feat[features]
yc = (df_feat["status"] == "告警").astype(int) # 标签转为 0/1
print("\n过采样前类别分布:")
print(yc.value_counts())
# 先划分训练集/测试集,过采样只作用于训练集,避免数据泄漏
Xc_train, Xc_test, yc_train, yc_test = train_test_split(
Xc, yc, test_size=0.2, random_state=42
)
# fit_resample():对训练数据重采样(这里用随机过采样),解决类别不平衡问题
ros = RandomOverSampler(random_state=42)
X_res, y_res = ros.fit_resample(Xc_train, yc_train)
print("过采样后训练集类别分布:")
print(pd.Series(y_res).value_counts())
clf = LogisticRegression(max_iter=1000)
clf.fit(X_res, y_res)
print("\n逻辑回归分类准确率:", round(clf.score(Xc_test, yc_test), 4))
print("\n" + "=" * 60)
print("全部流程执行完毕!")