【人工智能训练师】python语法二

以"智慧大棚环境传感器数据"为场景: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("全部流程执行完毕!")
相关推荐
木易 士心4 小时前
JavaScript 闭包原理和实践深度解析
开发语言·javascript·ecmascript
小羊没烦恼!4 小时前
系统内部模块(子系统)之间的耦合以及模块(子系统)划分
java·开发语言·前端·数据库·算法·c#
Chase_______4 小时前
【杂项知识点】一文搞懂 JVM、JRE 与 JDK:从概念混淆到生产部署
java·开发语言·jvm
Nebula_g4 小时前
JavaSE拓展:Arrays和Collections工具类
java·开发语言·算法·排序算法·工具类·javase·技术栈
菜鸟~noob2334 小时前
【电子战】 第22篇:误差椭圆与置信区间【含matlab代码】
开发语言·算法·matlab
老王爱玩车4 小时前
自定义类型:结构体
c语言·开发语言·学习
潼心1412o4 小时前
C++初阶(长期更新)第5讲:类和对象(中)
开发语言·c++
weixin_307779134 小时前
C++代码实现MATLAB中的normalize函数功能
开发语言·c++·算法·matlab
谢亮_vipxieliang4 小时前
Go WaitGroup与Once——并发同步的基石
开发语言·后端·golang