☰
数据挖掘设计实战:数据预处理、特征工程与模型调参全流程
2026/10/8 19:42:56 网站建设 项目流程

简介:这是一套完整的航空公司客户价值分析数据挖掘课程设计项目,适合高校数据挖掘课程学生、毕业设计选题者以及希望掌握客户分析实战技能的初学者。项目以客户分群与价值预测为主线,完整覆盖数据清洗、标准化、探索性可视化、K-Means聚类与结果输出等关键流程。压缩包共23个文件,包含7个Python脚本,分别对应数据预处理、探索分析、聚类等环节;xls/csv数据集及属性说明用于支撑各阶段处理,原始数据与中间结果也一并提供;docx文档详述了设计方法与业务建议,doc文件则为课程考试要求,整体大小20.78MB。目前已有1254人学习。借助该资源,读者能获得一套可复现的完整代码与数据,参照文档理解每一步分析逻辑,并将其迁移至零售、金融等其他行业的客户价值分析场景,是课程设计或项目实战的实用参考。

1. 数据挖掘设计.zip:拆开这个压缩包之前,先想清楚它到底要交付什么

拿到一份“数据挖掘设计.zip”,不少人第一反应是双击解压,然后被里面一堆名字混乱的 CSV、几个没注释的 py 文件,还有一份写得含含糊糊的《设计报告模板》砸晕。这个压缩包本身不是答案,它只是把“数据挖掘设计”这个任务的所有原材料塞进了一个 zip 里。你要做的不是“跑通一段代码”,而是完成一条完整链路:把任务描述翻译成可验证的预测问题,把散乱数据整理成可建模的表,再把模型结果讲成一份别人愿意看、能复核的报告。这篇笔记适合正在做课程设计、毕业设计或第一个公司内部数据挖掘项目的读者,目标是让你少走两到三周的弯路,拿到包之后知道第一步该按哪里。

2. 从 zip 里的散乱文件到一张可建模的数据表:任务定义与数据盘点

拿到压缩包,先别急着写代码。数据挖掘设计最常见的翻车点,不是模型选错,而是连“到底要预测什么”都没定义清楚就开始跑数据。这一章先解决任务定义,再讲怎么盘点 zip 里的数据资产。

2.1 先回答三个问题:预测什么、用什么预测、怎么评分

一份合格的数据挖掘设计,任务描述通常能拆成三个明确问题:

第一,预测目标是什么。是二分类(比如用户是否会流失)、多分类(把商品按销量分成高中低三档),还是回归(预测销售额数值)?如果任务书里写的是“分析用户行为特征”,你需要在报告开头自己补一个“建模目标”段落,明确写出目标字段。第二,输入特征从哪里来。zip 里通常有原始数据表,也可能附带一份数据字典。你要做的是把“哪些表、哪些字段能进模型”列成清单,这一步决定了后面所有特征工程的范围。第三,结果怎么评价。分类问题常用准确率、F1、AUC;回归问题常用 MAE、RMSE。评分口径先定下来,后面建模时才能拿同一个尺子比较。

我一般会把这三个问题的答案直接写进报告的第一节,哪怕任务书没要求。因为评审方(老师或业务负责人)第一眼就要看这个:你做的“设计”,目标到底是谁。三个问题写不清楚,后面模型再漂亮也容易被判定为“跑了一堆数据但没做设计”。

2.2 盘点 zip 里的数据资产:数据字典、字段类型与缺失审计

目标定了之后,下一步是看清 zip 里到底有什么。常见的数据挖掘课程设计压缩包,内部结构大致是:一个data/目录存放原始数据,一个或多个*.py或*.ipynb为建模脚本,外加一份报告模板。先列目录,再逐个文件确认格式,这一步用命令行就好:

unzip -l 数据挖掘设计.zip

关注输出里的文件大小和路径。如果发现某个 CSV 只有几 KB,而其他数据有几百 MB,那大概率是样例文件或者空表,后面加载时要单独处理。解压后,我会先把所有数据文件按“原始表、中间表、结果表”分类。中间表通常是别人跑过的预处理结果,直接拿来用会有隐患,最好自己在代码里复现一遍,避免引入一个不知道改过什么的数据集。

接下来做字段审计。这一步的目标是搞清楚每张表的行数、列数、字段类型和缺失比例:

import pandas as pd df = pd.read_csv("data/users.csv", encoding="utf-8-sig") print("shape:", df.shape) print(df.dtypes) missing_ratio = df.isnull().mean() print("缺失率超过 20% 的字段:") print(missing_ratio[missing_ratio > 0.2])

encoding="utf-8-sig"是为了兼容从 Windows 导出的带 BOM 头的 CSV,课程设计数据里很常见,不加这个参数第一行列名会变成\ufeffuser_id,排查时浪费十分钟。df.isnull().mean()一行输出所有字段的缺失率,比数空格更可靠,因为 CSV 里空字符串和NA在isnull()眼里都算缺失。这条命令跑完,你对这份数据的“健康状况”就有了第一印象:哪些字段能直接用,哪些字段后面必须处理。

2.3 数据探查的最小脚本:用 pandas 十分钟看清全貌

数据字典描述的是“应该是什么”,真实数据往往是另一回事。我会跑一个最小探查脚本,把字段的类型、取值个数、唯一值样例一次性看全:

import pandas as pd df = pd.read_csv("data/orders.csv", encoding="utf-8-sig") for col in df.columns: nunique = df[col].nunique(dropna=True) if df[col].dtype == "object": sample = df[col].dropna().unique()[:3] print(f"[类别] {col}: 唯一值 {nunique} 个,样例 {sample}") else: print(f"[数值] {col}: 唯一值 {nunique} 个,范围 {df[col].min()} ~ {df[col].max()}")

这段代码的价值在于区分“真类别”和“伪类别”。nunique能直接暴露那些看起来像字符串、其实是数值编码的字段(比如用户 ID、订单编号)。这类字段绝对不能进模型,因为它们每个取值只出现一次,模型学到的只是“记住这个 ID 对应什么结果”,泛化能力为零。另外,如果某个数值字段的取值范围是 0 和 1,但dtype显示为 float,那多半是 0/1 标志位,后面可以按类别特征处理。

3. 数据预处理与特征工程:数据挖掘设计里决定上限的 70% 工作量

预处理和特征工程,是数据挖掘设计和“跑一个 sklearn 例子”最本质的区别。很多新手把 80% 时间花在调参上,但真正让模型结果产生质的飞跃的,是这一章的内容:缺失值怎么补、异常值怎么处理、类别怎么编码、数值要不要标准化。

3.1 缺失值和异常值:处理顺序与分组填充参数

先说我踩过的坑:有一次直接把缺失值全用全局均值填充,模型倒是跑通了,但特征重要性一出来,那个字段完全没被用到。后来发现原因是全局均值把大部分样本的值都压到了同一个点,方差几乎消失,模型当然学不到信息。

正确的做法是“分组填充”:先找一个和缺失字段相关性强的分组变量,在组内用中位数或众数填充。比如订单表里的customer_level字段有缺失,而vip_level是完好的,那就按vip_level分组,每组用各自的中位数补:

import pandas as pd df = pd.read_csv("data/orders.csv", encoding="utf-8-sig") not_null_mask = df["customer_level"].notna() # 先用完整组统计每组的均值,再 fillna group_med = df.groupby("vip_level")["customer_level"].transform("median") df["customer_level"] = df["customer_level"].fillna(group_med)

transform("median")返回的结果和原 DataFrame 行数一致,可以直接传给fillna,这是最稳妥的写法,不需要先构造映射表再 merge。为什么不选均值?因为中位数对离群值稳健,当字段分布偏斜时(比如消费金额、点击次数这类右偏分布),均值会被几个大值拉高,填充出来的值会系统性偏高。

异常值方面,最常用的不是删除,而是“截断”。IQR 方法是计算四分位距,把超过Q3 + 1.5*IQR或低于Q1 - 1.5*IQR的值截断到边界:

Q1, Q3 = df["amount"].quantile(0.25), df["amount"].quantile(0.75) IQR = Q3 - Q1 lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR df["amount_clipped"] = df["amount"].clip(lower=lower, upper=upper)

删除异常值的缺点是改变样本量,而且被删的样本可能承载业务上的特殊含义(比如大客户的大额订单);clip则保留了样本,只是把极端值拉回合理区间。截断之后要重新看一眼分布,确认没有出现“所有值挤在一端”的新问题。

3.2 类别特征编码:OneHot 还是 Ordinal,按基数决定

类别特征的处理,核心只有一个判断标准:这个字段的“基数”有多大。基数就是唯一值个数。

基数小(比如性别、星期几,小于 10 个类别)用 OneHot 编码。基数十到几十(比如城市、商品类目)要分情况:如果类别之间有天然顺序(比如用户等级:普通、白银、黄金、钻石),用 OrdinalEncoder 转成有序整数;如果没有顺序,OneHot 会导致特征维度爆炸(100 个城市的 OneHot 就是 100 列),这时更好的做法是把低频类别合并成一个“其他”类,再 OneHot,或者直接用 OrdinalEncoder 让模型自己学。

用sklearn的ColumnTransformer可以一次性完成混合类型编码:

from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder from sklearn.compose import ColumnTransformer pre = ColumnTransformer( transformers=[ ("onehot", OneHotEncoder(handle_unknown="ignore"), ["gender", "weekday", "city_top"]), ("ordinal", OrdinalEncoder(categories=[["普通", "白银", "黄金", "钻石"]]), ["vip_level"]), ], remainder="passthrough", )

handle_unknown="ignore"这个参数务必加上。它的作用是:当预测时出现训练集里没见过的类别,不会报错,而是把该类别所有 OneHot 列置为 0。真实数据里测试集出现新类别是常态,不加这个参数,整个编码器会在预测阶段直接崩溃。categories=参数是给有序类别显式指定顺序,不指定的话 OrdinalEncoder 会按字母序排,黄金变 0 钻石变 1,顺序就错了。

3.3 数值特征标准化与相关性粗筛:哪些字段值得进模型

标准化不是所有模型都需要。决策树、随机森林、XGBoost 这类树模型对特征尺度不敏感,不标准化也能跑。但逻辑回归、SVM、KNN 这类基于距离或梯度的模型,必须做标准化,否则数值范围大的特征会主导模型。

from sklearn.preprocessing import StandardScaler num_cols = ["amount", "frequency", "recency"] scaler = StandardScaler() df_num_scaled = scaler.fit_transform(df[num_cols])

StandardScaler默认对每列计算均值和标准差,然后变成均值为 0、方差为 1 的分布。注意我在这里写的直接是fit_transform,是因为在预处理阶段我们还没切分训练集和测试集。到了建模阶段,千万不能对整份数据做标准化再切分,这是后面的避坑重点,这里先埋个伏笔。

相关性粗筛的做法很简单,用df[numeric_cols].corr()看相关系数矩阵。我关注的不是“高度相关”本身,而是两个特征之间相关系数绝对值超过 0.9 的情况。比如total_orders和order_count大概率是同义重复字段,留一个就行。留着两个高度相关特征进模型,在线性模型里会造成共线性,系数解释性变差;在树模型里则会让特征重要性在两个冗余字段之间被均分,误导你判断哪个特征真正重要。

4. 建模与调参:从基线模型到可交付的挖掘结果

数据准备好了,建模阶段最忌讳一上来就上 XGBoost。我见过太多人跳过基线模型直接堆集成,结果翻车了还不知道是数据问题还是模型问题。正确的路径是:先用一个简单模型跑通全流程,确认数据没有低级错误,再逐步增加模型复杂度。

4.1 模型选型:为什么先跑逻辑回归和决策树

基线模型的意义不是拿到好结果,而是验证“数据管道是否通畅”。我会先用逻辑回归跑一遍完整流程,理由有三:训练快,几十秒出结果;对特征尺度和缺失值敏感,能暴露预处理遗漏;输出概率可以被解释,方便检查预测分布是否合理。

决策树作为第二个基线,用来和逻辑回归做对比。如果逻辑回归的 AUC 是 0.72,决策树是 0.85,说明特征和目标之间存在明显的非线性关系,后面值得上随机森林或梯度提升;如果两个基线都卡在 0.70 附近,问题很可能不在模型复杂度,而在特征工程——比如缺了一个关键字段,或者目标变量定义有歧义。

from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score X_train, X_valid, y_train, y_valid = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) lr = LogisticRegression(max_iter=1000) lr.fit(X_train, y_train) print("LR AUC:", roc_auc_score(y_valid, lr.predict_proba(X_valid)[:, 1])) dt = DecisionTreeClassifier(max_depth=4, random_state=42) dt.fit(X_train, y_train) print("DT AUC:", roc_auc_score(y_valid, dt.predict_proba(X_valid)[:, 1]))

stratify=y是分类问题切分时的必选参数,它让训练集和验证集中正负样本的比例保持一致。如果数据集里正样本只占 10%,不加这个参数,随机切分可能让验证集里只出现几个正样本,AUC 的波动会大到无法判断模型好坏。random_state=42的作用是固定随机种子,保证每次运行得到同一份切分结果——没有它,你每一次跑完实验的指标都不一样,没法区分“模型改进了”还是“恰好切到了好数据”。

4.2 交叉验证:单次切分不可信,至少看 5 折的平均值

单次划分训练集和验证集,结果受切分随机性影响很大。同一份数据,换一个 random_state,AUC 可能从 0.82 掉到 0.78。所以基线模型确认没问题之后,我会立刻切到交叉验证:

from sklearn.model_selection import cross_val_score scores = cross_val_score( lr, X, y, cv=5, scoring="roc_auc", n_jobs=-1, ) print("AUC: %.3f ± %.3f" % (scores.mean(), scores.std()))

cv=5表示把数据分成 5 份,每次用 4 份训练、1 份验证,轮转 5 次,最后输出 5 个指标的均值和标准差。标准差比均值更能说明问题:如果 5 折 AUC 是 0.83 ± 0.12,说明模型在不同数据子集上表现波动巨大,这不是调参能解决的,要回头检查数据是否有序时间依赖、是否存在少量异常样本主导结果。n_jobs=-1是让交叉验证并行跑满所有 CPU 核心,5 折小数据集上收益明显。

交叉验证的另一个用法是“模型对比”:分别把逻辑回归、决策树、随机森林丢进cross_val_score,同一把尺子量下来,谁的平均分高选谁。这一步做完再谈调参,顺序才正确。

4.3 网格搜索的搜索空间与代价控制:先粗后细,别一次铺满

调参是这个阶段最像“玄学”的地方,但网格搜索本身是可以被控制的。我的习惯是先粗后细:第一轮搜索空间设大一点,步长粗一点,目的是确定参数的大致方向;第二轮在最优值附近缩小范围,步长减半。

以随机森林为例,两个最常见参数是树的深度max_depth和弱分类器数量n_estimators:

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV rf = RandomForestClassifier(random_state=42, n_jobs=-1) param_grid = { "max_depth": [4, 6, 8, 10], "n_estimators": [100, 200, 300], } grid = GridSearchCV( rf, param_grid, cv=3, scoring="roc_auc", n_jobs=-1, verbose=1, ) grid.fit(X_train, y_train) print("best params:", grid.best_params_)

verbose=1会在终端打印每轮搜索进度,方便你估算剩余时间。cv=3在这里是为了省时间——网格搜索内部会对每个参数组合再跑 3 折交叉验证,16 个组合就是 48 次训练,5 折就是 80 次。调参阶段用 3 折足够判断相对好坏,确定最终模型后再用 5 折验证一次。

搜索空间不是越大越好。n_estimators超过 300 之后,随机森林的效果提升非常缓慢,但训练时间线性增长,性价比很低。我一般第一轮固定在 100 和 300 两个档位,看趋势;第二轮在表现好的那个值附近取 50 的步长。max_depth的搜索范围要看特征数量,特征少(比如 20 个以内)从 3 搜到 8 就够,特征上百则要往 10 以上探。

网格搜索跑完后,最重要的一步是用最优参数在完整训练集上重新训练,然后在留出的验证集上做一次最终评估。注意:GridSearchCV返回的best_estimator_是在交叉验证内部重新训练过的模型,它已经看过训练集所有数据,但不能代表它在没见过的数据上的表现——那一次最终评估,必须单独做。

5. 数据挖掘设计避坑指南:五个我反复踩过的坑

这一章没有公式,全是血泪经验。每一条都是我在实际项目里遇到过的,按“现象、原因、解决”三个步骤写,照着核对你自己的流程就能避掉大部分坑。

5.1 数据泄露:验证集指标好得离谱,上真实数据就崩

这是一个课程设计里最经典的翻车现场。现象是:交叉验证 AUC 高达 0.96,模型看起来完美,但拿到“未来时间段”的数据上一测,AUC 掉到 0.70。原因是:预处理阶段对整份数据做了标准化或填充,而标准化的均值和标准差是用全量数据(包括验证集)计算出来的。验证集的信息在训练前就被模型“看见”了,相当于考试时把答案夹带进了考场。

解决的核心只有一句话:所有fit操作只允许在训练集上做,验证集和测试集只允许transform。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_valid_scaled = scaler.transform(X_valid)

同样的原则适用于缺失值填充、OneHot 编码、异常值截断。凡是需要“从数据里统计出参数”的步骤,都必须拆成 fit 和 transform 两步。如果嫌麻烦,就统一用 sklearn 的 Pipeline 把预处理和模型包在一起,让交叉验证自己处理训练集和验证集的隔离。

5.2 压缩包导入即报错:Invalid zip archive,还没开始就结束

现象是:解压 zip 时直接报caused by: invalid zip archive: could not find eocd,或者解压到一半说文件损坏。原因是下载不完整,或者传输过程中文件被截断。EOCD(End Of Central Directory)是 zip 文件末尾的结束标记,这个标记缺失,说明文件本身的尾部丢了,不是解压工具的锅。

解决办法和代码无关,首先用ls -l对比文件大小和下载页标注的大小,差几个字节都不行。其次用unzip -t 数据挖掘设计.zip做完整性测试,它会逐个文件校验 CRC 校验码。如果确认是坏包,重新下载,然后立刻用zipfile.is_zipfile()验证:

import zipfile if zipfile.is_zipfile("数据挖掘设计.zip"): print("zip 文件完整") else: print("文件已损坏,请重新下载")

这个检查写进代码的第一行,可以帮你避免在后面的数据加载阶段反复排查一个根本不存在的路径问题。

5.3 目标变量不平衡:准确率 99%,但少数类全错

现象是:二分类任务里正样本只占 3%,模型全预测为负样本,准确率 97%,但 AUC 只有 0.5。原因是模型被“多数类”主导,学到的最优策略就是全部预测多数类,因为这样损失最小。准确率在这一场景下完全失去意义。

解决的第一个动作是把评价指标换成 AUC、F1 或召回率,别只看准确率。第二个动作是在切分时用stratify=y保证训练和验证集中正样本比例一致。如果正样本实在太少,考虑对多数类降采样或对少数类升采样,或者直接换用带class_weight="balanced"的模型:

lr_balanced = LogisticRegression(max_iter=1000, class_weight="balanced")

class_weight="balanced"会自动根据类别频率调整损失权重,让少数类的分类错误付出更高代价。这是最简单的处理方式,不需要手动构造采样器。

5.4 缺失值用全局均值填充,模型特征重要性直接归零

现象是:某个字段缺失率很高,用df[col].fillna(df[col].mean())填充后,模型跑出来这个特征的重要性排名倒数第一。原因是大量样本被填成同一个值,这个特征的分布几乎变成一根直线,没有可分性,模型当然不拿它做判断。

解决的办法是分组填充,我在 3.1 中用groupby(...).transform("median")写过一个方案。另一个思路是:给这个字段新增一个“是否缺失”的二值特征,把缺失本身作为信息交给模型。有时候“这个用户没填收入”本身就暗示用户特征,让模型自己判断。

5.5 时间序列数据随机切分,预测未来时模型集体失效

现象是:数据是按时间记录的(比如交易流水),但建模时用了默认的随机切分,训练集和验证集的时间段交错重叠,模型相当于偷看了“未来”。实际投入使用时要预测下一个月的交易,模型输出完全乱套。

解决的唯一正确方式是按时间顺序切分:

X_train, X_valid = X[cutoff_idx:], X[:cutoff_idx]

按时间切分不需要shuffle,也不需要stratify,因为数据顺序本身就有时间意义。切分位置通常取时间轴的 80% 处。如果你的压缩包数据带有日期字段,先按日期排序,再取前面的 80% 当训练集,后面 20% 当验证集,这是一个能让模型真正可用的关键习惯。

6. 最后一步不是调参,是验证口径和一份能讲出来的报告

模型调完了,网格搜索也跑完了,数据挖掘设计这个交付物要做的事还剩最后两件:确定最终的验证口径,把所有过程组织成别人能复盘的报告。

先说验证口径。交叉验证的最优得分只能说明你在验证集上调参调得好,不能证明模型在没见过的数据上也能稳定。我会在建模开始时专门切出一份“留白测试集”——它不参与任何预处理参数拟合、不参与交叉验证、不参与网格搜索,直到最终模型完全确定后才碰一次。最后通行的验证代码是:

from sklearn.metrics import classification_report, confusion_matrix y_pred = final_model.predict(X_test_final) print(classification_report(y_test_final, y_pred)) print(confusion_matrix(y_test_final, y_pred))

这份测试集上的结果,才是报告里真正能写的“模型效果”。之前中间过程所有的得分,都只能算开发过程中的参考,因为调参本身就是在验证集上做优化,优化轮数越多,验证集上的分数虚高越严重。只碰一次的最后测试,是防止“调参过拟合”的最后一道保险。

报告写作我自己的习惯是“结论先行”。先写建模目标、数据概况、最终模型和最终指标,把特征重要性排名和业务含义对应写成一张小表,比如“订单金额是预测流失最重要的特征,其次是最近一次下单时间”;再把预处理、特征工程、模型对比这些细节放到后面的章节。不要按时间顺序从数据盘点写起,评审方没耐心看你探索的过程,他们想先看到你的判断。

这个习惯来自一次教训。我第一次做类似的数据挖掘设计时,把报告按“数据清洗、建模、调参”的顺序写了三十多页,最后被问了一句“所以你的设计对业务到底有什么结论”,我瞬间意识到自己写了一份跑代码记录,而不是一份设计报告。后来我先把结论写在第一页,所有技术细节降级为支撑材料,同一个项目再评审时对方只翻了十分钟就点头了。数据挖掘设计的最终交付物不是代码 zip,是别人读完报告后,能复述你做了什么、为什么这么做、结果有多可信。能把这个讲清楚,这份设计才算真正闭环。希望这篇笔记能帮你在自己的数据挖掘设计里少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询