本 Skill 为《数据挖掘与商务智能》课程设计,每一步都标注了对应知识点,让学生在操作中学习。
| 步骤 | 知识点 | 对应章节 |
| :--: | -------------------------------------------------------- | :------: |
| 1 | Python 环境管理 — pip、虚拟环境、清华源镜像 | §1 |
| 2 | 数据加载 — CSV/Excel 读取、编码问题、BOM 头 | §2 |
| 3 | 数据探查 — 形状、数据类型、缺失值、描述性统计 | §3 |
| 4 | 数据清洗 — 缺失值/重复值/异常值处理、类型转换 | §4 |
| 5 | 探索性分析(EDA) — 分布、相关性、分组聚合 | §5 |
| 6 | 特征工程 — 派生变量、编码、标准化 | §6 |
| 7 | 可视化 — matplotlib/seaborn 基础图表 | §7 |
| 8 | 建模入门 — 回归/分类/聚类基础 | §8 |
| 9 | 结果解释 — 如何读懂输出、撰写结论 | §9 |
学生需要理解:跑代码之前先检查环境,避免"代码没问题但跑不起来"。
# 1. 检查 Python 版本(≥ 3.11 即可)
python --version
# 2. 检查核心包是否已安装(;分隔,PowerShell 兼容)
python -c "import pandas; print('pandas', pandas.__version__)"
python -c "import matplotlib; print('matplotlib', matplotlib.__version__)"
python -c "import seaborn; print('seaborn', seaborn.__version__)"
python -c "import scipy; print('scipy', scipy.__version__)"
python -c "import sklearn; print('sklearn', sklearn.__version__)"
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas matplotlib seaborn scipy scikit-learn
&&,所以用分步执行
pip:Python 官方 包管理工具 ,专门用来安装 / 卸载 / 更新第三方库,是环境配置的核心命令
python --version提示不是内部命令:Python 未添加系统环境变量,重新安装 Python 时勾选「Add Python to PATH」
理解编码(UTF-8 vs GBK)对中文数据处理的影响,学会用 pathlib 管理路径。
# -*- coding: utf-8 -*-
import sys
sys.stdout.reconfigure(encoding='utf-8')
from pathlib import Path
import pandas as pd
# 使用 pathlib 管理路径(禁止硬编码绝对路径)
data_dir = Path('data') # 数据放在项目 data/ 目录下
file_path = data_dir / 'your_file.csv'
# 尝试不同编码读取
try:
df = pd.read_csv(file_path, encoding='utf-8')
except UnicodeDecodeError:
df = pd.read_csv(file_path, encoding='gbk')
print(f'数据加载完成: {df.shape[0]} 行 × {df.shape[1]} 列')
\ufeff,表现为 出现在第一列列名前
"先看数据,再动手" — 这是数据挖掘的第一铁律。
# ===== 基本结构 =====
print('数据形状:', df.shape)
print('列名列表:', df.columns.tolist())
# ===== 数据类型 =====
print('\n各列数据类型:')
print(df.dtypes)
# ===== 缺失值 =====
print('\n缺失值统计:')
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(2)
missing_df = pd.DataFrame({'缺失数': missing, '缺失率%': missing_pct})
print(missing_df[missing_df['缺失数'] > 0])
# ===== 重复值 =====
print(f'\n重复行数: {df.duplicated().sum()}')
# ===== 数值列统计 =====
print('\n数值列描述性统计:')
print(df.describe())
# ===== 分类列统计 =====
for col in df.select_dtypes(include='object').columns:
print(f'\n{col} 唯一值数: {df[col].nunique()}, 示例: {df[col].dropna().unique()[:5]}')
理解 Garbage In, Garbage Out — 脏数据产出的分析结论不可信。
clean = df.copy() # 保留原始数据
# ---- 4.1 列名规范化 ----
clean.columns = clean.columns.str.strip() # 去除首尾空格
# ---- 4.2 类型转换 ----
# 日期列
clean['日期列'] = pd.to_datetime(clean['日期列'], errors='coerce')
# 数值列(如果是字符串格式,如 "1,234")
clean['数值列'] = clean['数值列'].str.replace(',', '').astype(float)
# 科学计数法 ID(如 1.1582E+14)
clean['id'] = clean['id'].astype(str)
# ---- 4.3 缺失值处理 ----
# 策略 A:删除缺失率过高的行
clean = clean.dropna(subset=['关键列'])
# 策略 B:填充(均值/中位数/众数/固定值)
clean['数字列'] = clean['数字列'].fillna(clean['数字列'].median())
clean['分类列'] = clean['分类列'].fillna('未知')
# 策略 C:前向/后向填充(时间序列)
clean['数值列'] = clean['数值列'].ffill()
# ---- 4.4 重复值 ----
clean = clean.drop_duplicates()
# ---- 4.5 异常值 ----
# IQR 方法:Q1 - 1.5*IQR ~ Q3 + 1.5*IQR 之外为异常
Q1 = clean['数值列'].quantile(0.25)
Q3 = clean['数值列'].quantile(0.75)
IQR = Q3 - Q1
outliers = clean[(clean['数值列'] < Q1 - 1.5*IQR) | (clean['数值列'] > Q3 + 1.5*IQR)]
print(f'检测到 {len(outliers)} 个异常值')
print(f'清洗完成: {len(clean)} 行 (原始 {len(df)} 行)')
真实世界的数据永远有瑕疵 —— 传感器故障、人工录入错误、系统迁移丢失数据。跳过清洗直接建模,等同于用发霉的食材做菜:Garbage In, Garbage Out。清洗不是"可选的优化步骤",而是分析的前提条件。
| 缺失机制 | 定义 | 例子 | 推荐处理 |
| ------------------------------ | ------------------------------------------ | -------------------------------------------- | --------------------------------------- |
| MCAR(完全随机缺失) | 缺失与任何变量无关 | 问卷随机漏填 | 可删除,不会引入偏差 |
| MAR(随机缺失) | 缺失与其他变量有关,但与被缺失变量本身无关 | 高收入者更不愿填收入,但缺失只与收入高低无关 | 用均值/中位数/回归填充 |
| MNAR(非随机缺失) | 缺失与被缺失变量本身有关 | 收入越高越不愿填收入 | 需专门建模处理,单纯删除/填充会引入偏差 |
pd.to_datetime(..., errors='coerce') 中 coerce 的意思是"转不了的变成 NaT(Not a Time)",不要报错中断程序。
1.1582E+14 是浮点数,但在 Excel 中这是 ID(如 B站视频 ID),必须转成字符串 astype(str),否则后续匹配会失败。
学会用统计指标和分组聚合来发现数据中的规律。
EDA 第一准则:先识别 5 类商科常用数据类型,再匹配对应分析方法
先分型,再分析:掌握 5 类商科常用数据的分型规则,杜绝分析方法混用:
> ⚠️ 以下代码为一个完整脚本,直接复制运行即可,无需手动改列名。
# -*- coding: utf-8 -*-
# ============================================================
# §5 探索性数据分析 EDA — 5 类商科数据分型分析
# ============================================================
# ---- 第一步:自动识别数据类型(零介入,适配所有数据) ----
print("=" * 60)
print("第一步:数据集字段类型一览")
print("=" * 60)
print(clean.dtypes)
print()
# 自动筛选各类字段(学生无需手动修改列名)
numeric_cols = clean.select_dtypes(include=['int64', 'float64']).columns.tolist()
category_cols = clean.select_dtypes(include=['object', 'category']).columns.tolist()
time_cols = clean.select_dtypes(include=['datetime64']).columns.tolist()
# 文本型:从分类列中筛选唯一值多的列(>10种 → 更像文本而非分类)
text_cols = []
for col in category_cols:
try:
if clean[col].astype(str).nunique() > 10:
text_cols.append(col)
except Exception:
pass
print(f"✅ 数值型字段({len(numeric_cols)} 个):{numeric_cols}")
print(f"✅ 分类型字段({len(category_cols)} 个):{category_cols}")
print(f"✅ 文本型字段({len(text_cols)} 个):{text_cols if text_cols else '无'}")
print(f"✅ 时间型字段({len(time_cols)} 个):{time_cols if time_cols else '无'}")
print()
# ---- 一、数值型:描述性统计 + 汇总(统计年鉴核心) ----
# 知识点:均值、中位数、偏度、标准差、极值
print("=" * 60)
print("一、数值型分析 — 描述性统计(适配统计年鉴/面板数值)")
print("=" * 60)
if numeric_cols:
# 逐列展示关键指标(限前10列防止输出过长)
for col in numeric_cols[:10]:
vals = clean[col].dropna()
if len(vals) > 0:
print(f" {col}: 均值={vals.mean():.2f}, 中位数={vals.median():.2f}, "
f"偏度={vals.skew():.2f}, 标准差={vals.std():.2f}, "
f"min={vals.min():.2f}, max={vals.max():.2f}")
# 汇总统计表(宏观数据专属:GDP/营收等多指标汇总)
print("\n 数值型指标汇总表(count/mean/sum/std):")
print(clean[numeric_cols].agg(['count', 'mean', 'sum', 'std']).round(2))
else:
print(" ⚠️ 未检测到数值型字段,跳过数值分析")
# ---- 二、数值型:相关性矩阵(统计年鉴常用) ----
# 知识点:皮尔逊相关系数、变量关联关系
print()
print("=" * 60)
print("二、相关性分析 — 高相关变量筛选 (|r| > 0.5)")
print("=" * 60)
if len(numeric_cols) >= 2:
corr_matrix = clean[numeric_cols].corr()
high_corr = corr_matrix.where(abs(corr_matrix) > 0.5).stack().dropna()
found = False
for (c1, c2), val in high_corr.items():
if c1 < c2:
direction = "正" if val > 0 else "负"
print(f" {c1} ↔ {c2}: r = {val:.3f}({direction}相关)")
found = True
if not found:
print(" 未发现 |r| > 0.5 的变量对")
else:
print(" ⚠️ 数值型字段不足 2 个,无法计算相关性")
# ---- 三、分类型:分组聚合(等价 SQL GROUP BY) ----
# 知识点:分组统计、多维指标汇总
print()
print("=" * 60)
print("三、分类型分析 — 分组聚合统计(适配面板分类/统计年鉴分类)")
print("=" * 60)
if category_cols and numeric_cols:
group_col = category_cols[0]
num1 = numeric_cols[0]
num2 = numeric_cols[1] if len(numeric_cols) >= 2 else numeric_cols[0]
group_stats = clean.groupby(group_col).agg(
计数=(num1, 'count'),
均值=(num1, 'mean'),
中位数=(num1, 'median'),
总和=(num2, 'sum'),
标准差=(num2, 'std')
).round(2)
print(f" 按「{group_col}」分组统计:")
print(group_stats)
else:
print(" ⚠️ 缺少分类列或数值列,无法分组")
# ---- 四、数值型:Top N 排名(统计年鉴/面板常用) ----
# 知识点:数据排序、极值分析
print()
print("=" * 60)
print("四、Top N 排名分析 — 极值发现")
print("=" * 60)
if numeric_cols:
top_col = numeric_cols[0]
key_col = category_cols[0] if category_cols else numeric_cols[0]
top10 = clean.nlargest(10, top_col)[[key_col, top_col]]
print(f" 「{top_col}」Top 10:")
print(top10.to_string(index=False))
else:
print(" ⚠️ 缺少数值列,无法生成 Top N")
# ---- 五、文本型:基础频数分析(统计年鉴文本/调研文本) ----
# 知识点:文本分类统计、关键词频次(零基础入门)
print()
print("=" * 60)
print("五、文本数据分析 — 内容频次统计(适配统计年鉴说明/调研文本)")
print("=" * 60)
if text_cols:
text_col = text_cols[0]
text_freq = clean[text_col].astype(str).value_counts().head(10)
print(f" 「{text_col}」文本内容 Top 10 频次:")
print(text_freq)
else:
print(" ⚠️ 未检测到文本型字段,跳过文本分析")
# ---- 六、面板数据:时间趋势分析 ----
# 知识点:时间序列基础、多主体趋势对比(面板数据核心)
print()
print("=" * 60)
print("六、面板数据 — 时间趋势分析(多主体 × 多时间点)")
print("=" * 60)
if time_cols and numeric_cols and category_cols:
time_col = time_cols[0]
num_col = numeric_cols[0]
entity_col = category_cols[0]
panel_trend = clean.groupby([time_col, entity_col])[num_col].mean().unstack().round(2)
print(f" 「{entity_col}」在「{num_col}」上的时间趋势:")
print(panel_trend)
else:
print(" ⚠️ 缺少时间列/数值列/分类列,无法进行面板数据趋势分析")
print()
print("✅ §5 EDA 分析完成")
EDA 的目标不是"找到正确答案",而是发现值得深挖的方向。两者有本质区别:
学生常犯的错误:把 EDA 中发现的模式直接当结论 —— 比如看到"大城市薪资高"就说"去大城市就能高薪",忽略了选择偏差(大城市本身就吸引了更多高学历人才)。EDA 是起点,不是终点。
| 数据类型 | 用错方法的后果 |
| -------------------- | ----------------------------------------------------------- |
| 把分类列当数值列 | df['城市'].mean() → 报错,或算出无意义的"平均城市编码" |
| 把数值列当分类列 | 每个不同的数值被当成一个类别,导致分组数 = 样本数 |
| 把文本列当分类列 | 每行一个独特值,分组无意义 |
| 把截面数据当面板数据 | 错误地强加"时间趋势"解释,得出虚假结论 |
这就是本 Skill 在 EDA 开头强制执行 clean.dtypes 的原因 —— 先知道每一列是什么类型,才能决定用什么分析方法。
| 指标 | 告诉你的信息 | 局限 |
| -------------- | ------------------ | ------------------------- |
| 均值 | 整体水平 | 被极值严重拉偏 |
| 中位数 | "中间那个人"的水平 | 无法反映极端情况 |
| 偏度 | 数据往哪边"甩尾" | 需要配合均值/中位数一起看 |
薪资数据几乎总是正偏(右偏):大多数人拿中等薪资,少数人拿极高薪资。如果只汇报"平均薪资",学生会高估自己的收入预期。汇报中位数比汇报均值更接近"普通人能拿到的"。
相关系数把"这两个变量看起来有关系"变成了一个可比较的数字。但要注意:
GROUP BY 在 pandas 中的等价操作 —— 本质是将数据按某一列的类别切分,然后对每个切块做统计运算(计数、求和、均值等)
| 数据类型 | 能做什么 | 不能做什么 |
| ---------------------- | ------------------------------- | ---------------------- |
| 数值型(统计年鉴核心) | 均值、相关、TopN、趋势 | 不能用来分组分类 |
| 分类型(面板分类项) | 分组、计数 | 不能计算均值、相关系数 |
| 文本型(统计年鉴文本) | 频数统计(入门阶段) | 不做复杂文本挖掘 |
| 统计年鉴数据 | 汇总统计、地区/行业对比、相关性 | 照搬数值/分类代码 |
| 面板数据 | 时间趋势 + 主体对比 | 忽略时间维度单独分析 |
df.dtypes),再选择对应代码
理解"同样的数据,不同的表达方式会直接影响模型效果"。
# ---- 6.1 时间特征提取 ----
clean['年'] = clean['日期列'].dt.year
clean['月'] = clean['日期列'].dt.month
clean['星期'] = clean['日期列'].dt.dayofweek # 0=周一
clean['是否周末'] = (clean['星期'] >= 5).astype(int)
clean['季度'] = clean['日期列'].dt.quarter
# ---- 6.2 文本特征 —— 提取关键词/长度 ----
clean['文本长度'] = clean['文本列'].str.len()
clean['含关键词'] = clean['文本列'].str.contains('关键词', na=False).astype(int)
# ---- 6.3 比率特征 —— 两个数值列的比例 ----
clean['比率'] = (clean['分子列'] / clean['分母列']).replace([float('inf'), -float('inf')], 0)
# ⚠️ 除以零会产生 inf,必须处理
# ---- 6.4 分类变量编码 ----
# One-Hot 编码(适用于无序分类,如颜色:红/蓝/绿)
df_encoded = pd.get_dummies(clean, columns=['分类列'], prefix='cat')
# Label 编码(适用于有序分类,如等级:低/中/高)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
clean['等级_编码'] = le.fit_transform(clean['等级列'])
模型的数学引擎只能处理数字矩阵。而原始数据往往是混杂的 —— 日期、文本、类别、缺失值。特征工程就是把"杂乱的原始数据"翻译成"模型能理解的数值特征"。同样的原始数据,不同的特征表达方式,模型效果可以差 3~5 倍。
模型不认识"2022-03-15"这个字符串,也不理解"12月之后是1月"的周期性。把日期拆成年/月/星期/季度这些独立数字后,模型就能捕捉到季节性规律(如冬季招聘职位少)。
sin(月份/122π) 和 cos(月份/122π) 编码月份,就能完美体现"12月和1月距离近"的周期关系。但课程入门阶段用拆分即可。
| 编码方式 | 原理 | 什么时候用 | 用错的后果 |
| ----------------- | ------------------------- | ---------------------------- | --------------------------------------------- |
| One-Hot | 每个类别变成一列 0/1 | 无序分类(城市、颜色、品牌) | 如果类别太多(>100种),特征矩阵爆炸 |
| Label | 把类别替换成数字 1,2,3... | 有序分类(学历、等级、星级) | 用在无序分类上,模型会以为"上海=3" > "北京=1" |
掌握数据挖掘中最常用的 6 种图表及各自适用场景。
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
import seaborn as sns
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
# ① 直方图 — 看分布形态
axes[0, 0].hist(clean['数值列'], bins=30, color='#3498db', edgecolor='white')
axes[0, 0].set_title('数值列分布直方图')
axes[0, 0].set_xlabel('值')
axes[0, 0].set_ylabel('频数')
# ② 箱线图 — 看异常值与四分位数
sns.boxplot(data=clean, x='分类列', y='数值列', ax=axes[0, 1])
axes[0, 1].set_title('各类别的数值分布(箱线图)')
# ③ 柱状图 — 类别间比较
cat_counts = clean['分类列'].value_counts().head(10)
axes[0, 2].bar(range(len(cat_counts)), cat_counts.values, color='#e74c3c')
axes[0, 2].set_xticks(range(len(cat_counts)))
axes[0, 2].set_xticklabels(cat_counts.index, rotation=45, ha='right')
axes[0, 2].set_title('分类列 Top 10 频数')
# ④ 散点图 — 两变量关系
axes[1, 0].scatter(clean['数值列1'], clean['数值列2'], alpha=0.5, s=20)
axes[1, 0].set_xlabel('数值列1')
axes[1, 0].set_ylabel('数值列2')
axes[1, 0].set_title('数值列1 vs 数值列2')
# ⑤ 折线图 — 时间趋势
time_data = clean.groupby('日期列')['数值列'].mean()
axes[1, 1].plot(time_data.index, time_data.values, color='#2ecc71')
axes[1, 1].set_title('时间趋势')
axes[1, 1].tick_params(axis='x', rotation=45)
# ⑥ 热力图 — 相关性矩阵
num_cols = clean.select_dtypes(include='number').columns[:8]
sns.heatmap(clean[num_cols].corr(), annot=True, fmt='.2f', cmap='RdBu_r',
center=0, ax=axes[1, 2])
axes[1, 2].set_title('相关性热力图')
plt.tight_layout()
plt.savefig('eda_charts.png', dpi=150, bbox_inches='tight')
print('图表已保存至 eda_charts.png')
人眼是人类最强大的模式识别系统。肉眼扫一遍散点图,能瞬间发现:线性/曲线趋势、离群点、聚类结构、数据缺口。而直接丢进模型跑,这些信息全部丢失。可视化是建模前的"体检报告"。
图表本质上是用视觉属性(位置、长度、颜色、形状)来编码数据特征。选错图表 = 用错视觉属性,读者无法正确解码信息:
| 想看什么 | 用什么图 | 视觉编码 | 为什么选它 | 注意 |
| --------------- | ------------ | ---------------------------- | -------------------------------------------------- | ----------------- |
| 分布形态 | 直方图 / KDE | 柱高/面积 = 频数 | 单变量、看集中与分散 | bin 数量影响观感 |
| 异常值 + 四分位 | 箱线图 | 位置 = 四分位数,须外 = 异常 | 最适合多组比较,一张图能看多组的分布差异 | 点超出须线即异常 |
| 类别比较 | 柱状图 | 柱高 = 数量 | 分类 x 数值,简单直接 | 类别太多时取 TopN |
| 两变量关系 | 散点图 | 点的 x/y 位置 | 发现相关性、聚类、离群、非线性 | 加透明度避免重叠 |
| 时间趋势 | 折线图 | 斜率 = 变化率 | 强调先后顺序和变化速率 | 时间轴要排序 |
| 相关性 | 热力图 | 颜色深浅 = 相关强弱 | 同时展示几十个变量的两两关系 | 颜色深浅 = 强弱 |
alpha=0.5 解决。
理解监督学习(回归/分类)和无监督学习(聚类)的基本概念。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import r2_score, accuracy_score, classification_report
# ====== 准备特征和标签 ======
features = ['特征1', '特征2', '特征3', '特征4']
target = '目标列'
X = clean[features].copy()
y = clean[target].copy()
# 处理特征中的缺失值
X = X.fillna(X.median())
# ====== 划分训练集/测试集 (7:3) ======
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
print(f'训练集: {len(X_train)} 条, 测试集: {len(X_test)} 条')
# ====== 标准化 ======
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# ⚠️ fit_transform 用在训练集,transform 用在测试集
# ====== 场景 A:回归(预测连续值,如价格、播放量) ======
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
model_reg = RandomForestRegressor(n_estimators=100, random_state=42)
model_reg.fit(X_train, y_train)
y_pred_reg = model_reg.predict(X_test)
print(f'回归 R² 分数: {r2_score(y_test, y_pred_reg):.3f}')
# 特征重要性
importance = pd.DataFrame({'特征': features, '重要性': model_reg.feature_importances_})
print(importance.sort_values('重要性', ascending=False))
# ====== 场景 B:分类(预测类别,如是/否、高/中/低)=======
from sklearn.ensemble import RandomForestClassifier
# 如果目标是连续值,需要先分箱
if y.nunique() > 10:
y = pd.cut(y, bins=3, labels=['低', '中', '高'])
X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(
X, y, test_size=0.3, random_state=42
)
model_clf = RandomForestClassifier(n_estimators=100, random_state=42)
model_clf.fit(X_train_c, y_train_c)
y_pred_clf = model_clf.predict(X_test_c)
print(f'分类准确率: {accuracy_score(y_test_c, y_pred_clf):.3f}')
print(classification_report(y_test_c, y_pred_clf))
# ====== 场景 C:聚类(没有标签,发现自然分组)=======
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# 肘部法则 —— 选最佳 K 值
inertias = []
K_range = range(1, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X_train_scaled)
inertias.append(km.inertia_)
print(f'肘部法则 Inertia: {dict(zip(K_range, [round(i, 2) for i in inertias]))}')
# 聚类
k_best = 3
kmeans = KMeans(n_clusters=k_best, random_state=42, n_init=10)
clean['聚类标签'] = kmeans.fit_predict(X_train_scaled)
print(f'聚类轮廓系数: {silhouette_score(X_train_scaled, clean["聚类标签"]):.3f}')
| 任务 | 问题形式 | 典型场景 | 常用算法 | 评估指标 |
| -------------- | ---------------------- | ------------------ | ---------------------- | ---------------------- |
| 回归 | 预测一个连续数值 | 房价、薪资、销量 | 线性回归、随机森林回归 | R²(0~1,越高越好) |
| 分类 | 判断属于哪个类别 | 是否购买、信用评级 | 随机森林分类、逻辑回归 | 准确率、精确率、召回率 |
| 聚类 | 没有标签,自动发现分组 | 用户分群、岗位分层 | K-Means、DBSCAN | 轮廓系数、肘部法则 |
选择原则:如果你有 y(目标列)→ 监督学习(回归或分类);如果没有 y → 无监督学习(聚类)。
如果把所有数据都用来训练模型,模型会把数据"背下来"(包括噪声),但遇到新数据时表现极差 —— 这叫做过拟合。训练集/测试集分离的本质是模拟"期末考试":用 70% 的数据当练习题,用 30% 的未见过数据当考试题。考试分数才代表真实水平。
random_state=42 不是魔法数字,而是随机种子 —— 保证每次划分结果相同,实验可重复。
K-Means 的核心是欧氏距离。如果"薪资(万)"的量纲是 0.5~5,而"年龄"是 20~60,那么薪资对距离的贡献会被年龄完全淹没。StandardScaler 把所有特征拉到同一尺度上,让每个特征公平参与距离计算。
随着 K 增大,Inertia(簇内平方和)一定会下降 —— 极端情况 K = 样本数时 Inertia = 0。肘部法则找的是"下降速度突然放缓"的转折点,即继续增加 K 带来的收益急剧减少的那个位置。
知道每个数字代表什么含义,并能用通俗语言写出来。
生成一份可打印的 HTML 分析报告,排版行文按学术期刊论文格式(含标题、摘要、分析方法、图表、参考文献样式)。报告必须包含所用分析方法的简要介绍及选择理由。
# 生成 HTML 报告
html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>数据分析报告</title>
<style>
body {{ font-family: "SimSun", "宋体", serif; max-width: 900px; margin: 0 auto; padding: 40px; }}
h1 {{ text-align: center; font-size: 18pt; }}
h2 {{ font-size: 14pt; border-bottom: 1px solid #333; padding-bottom: 4px; }}
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
th, td {{ border: 1px solid #666; padding: 6px 10px; font-size: 10pt; }}
th {{ background: #f0f0f0; }}
.abstract {{ background: #fafafa; padding: 12px 16px; border-left: 4px solid #3498db; }}
.method {{ background: #fff8e1; padding: 10px 14px; border-left: 4px solid #f39c12; margin: 10px 0; font-size: 10pt; }}
</style>
</head>
<body>
<h1>{title}</h1>
<div class="abstract"><b>摘要:</b>{abstract_text}</div>
<h2>0. 分析方法</h2>
<div class="method">
{method_text}
</div>
<h2>1. 数据概况</h2>
<p>{overview}</p>
<h2>2. 核心发现</h2>
<p>{findings}</p>
<h2>3. 相关性分析</h2>
<p>{correlation_text}</p>
<h2>4. 模型效果</h2>
<p>{model_text}</p>
<h2>5. 局限与建议</h2>
<p>{limitations}</p>
</body>
</html>
"""
with open(output_dir / '分析报告.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print(f'✅ HTML 报告已保存至: {output_dir / "分析报告.html"}')
method_text)
<b>本报告使用的分析方法及选择理由:</b>
<ul>
<li><b>描述性统计分析</b>(均值、中位数、偏度):
用于刻画数据集中趋势与离散程度。选择均值+中位数双指标汇报,
因为薪资数据通常右偏,均值会被极端高薪拉高,中位数更能反映"普通人能拿到的"。</li>
<li><b>皮尔逊相关系数</b>(|r| > 0.5 为强相关):
用于量化变量之间的线性关联程度。选择皮尔逊而非斯皮尔曼,
因为需要测量的是线性关系而非单调关系(如"经验增加→薪资稳步上升")。</li>
<li><b>分组聚合分析</b>(GROUP BY 等价操作):
用于按类别(城市/学历/行业)比较薪资差异。选择分组均值而非分组总数,
因为目标是横向比较"哪个类别更高",而非"哪个类别岗位更多"。</li>
<li><b>K-Means 聚类</b>(轮廓系数验证 + 肘部法则选 K):
用于无监督地发现岗位的"自然分层"。选择 K-Means 而非 DBSCAN,
因为我们预期的岗位分层是球状分布(高/中/低三个梯队),
而非密度差异分布。标准化后再聚类是为了消除量纲差异。</li>
<li><b>可视化方法</b>(直方图/箱线图/柱状图/散点图/热力图):
每种图的选择基于视觉编码理论——用最合适的视觉属性(位置、长度、颜色)
编码对应的数据特征,详见 §7 图表选择指南。</li>
</ul>
1. 数据概况:本分析使用 {len(clean)} 条数据,涵盖 {时间范围}
的 {数据来源}。
2. 核心发现:
- {最突出的发现1}(用数字支撑,如:A类占比xx%,是B类的x倍)
- {最突出的发现2}(用数字支撑)
- {最突出的发现3}(用数字支撑)
3. 相关性分析:{变量A} 与 {变量B} 的相关系数为 {r},
表明 {强/中等/弱} {正/负}相关。
4. 模型效果:{模型名} 的 {指标名} 为 {值},
说明模型的 {预测/分类/聚类} 能力 {优秀/良好/一般}。
5. 局限与建议:
- 数据局限:{数据可能的问题(样本量、覆盖范围、时效性)}
- 后续建议:{可以进一步分析的方向(加入新变量、扩展时间范围等)}
| 错误 | 原因 | 解决方法 |
| --------------------------------- | ------------------ | ----------------------------------------- |
| KeyError: '列名' | 列名写错了 | 打印 df.columns 确认 |
| ValueError: could not convert | 类型不匹配 | 先 dtype 检查,用 errors='coerce' |
| 中文乱码 | 编码不对 | 试 gbk 或 utf-8-sig |
| 图表中文显示方块 | 字体缺失 | 设 rcParams['font.sans-serif'] |
| 相关系数 NaN | 列中有 NaN | 先 dropna() 再算相关 |
| 回归 R² 为负 | 模型比"猜均值"还差 | 检查特征选择,可能需要更多特征 |
| SettingWithCopyWarning | 在切片上赋值 | 用 .copy() 创建独立副本 |
共 1 个版本