TabulaData 100 Reference Sheet
pandas 2.3 · sklearn 1.8
Tabula › API Reference

API Reference

Everything I looked up, got stuck on, or got wrong while preparing for the data science lab tests — pandas, plotting and scikit-learn, as runnable snippets.

Note
Snippets were run on pandas 2.3.3 · scikit-learn 1.8. Tags: tricky points I got stuck on, log #n entries in my mistake log, mock X questions from the mock exams. Card titles read what to do · function.

Exam Essentials

考场须知

常见报错与对策log #33

报错里有多半是改法
NameError: name 'X' is not defined没 import,或前面那格没运行补 import;从上往下「全部运行」
KeyError: 'GrlivArea'列名拼错(区分大小写)列名从 df.columns 复制,别手敲
has no attribute 'meadian'方法名拼错先查拼写
'Series' object is not callable该用 [ ] 取东西,写成了 ( )( ) 是调用函数,[ ] 是取东西
could not convert string to float表里有文字列:corr 没加 numeric_only=True,或 X 里没做 get_dummies见对应卡片
Expected a 2-dimensional containerX 用了单中括号(Series)X 要双中括号 df[['a']]
tuple index out of range对 Series 取了 .shape[1]Series 只有 shape[0]
SyntaxError比如 h.['col'] 多了个点;整格一行都不执行看报错指的那一行
'tuple' object has no attribute 'tolist'对元组用了 .tolist(),比如 stack() 后的 idxmax()sorted(...) 或 list(...)
unhashable type: 'Series'往 .count() 里塞了条件数个数用 (条件).sum()

Notebook 使用注意事项

  • 只有最后一行会自动显示,中间某行写 na 不显示;要么挪到最后一行,要么 print(na)。(错题 36)
  • 改了前面的格子要从头重跑(菜单「全部运行」),否则后面用的还是旧数据。(错题 37)
  • 同一变量赋值两次,留后面那次:题目的 x = None 占位要改掉,别在上面另插一行。
  • 函数后面要加括号:df.isna().sum(),写成 .sum 拿到的是函数本身。

查阅官方文档的方法

  1. 先想清楚要的结果长什么样:一个数?一列?一张表?一张图?
  2. 打开文档页,上面的参数说明先别看,直接拉到最底下 Examples,找最像的例子抄。
  3. 把例子里的数据名、列名换成自己的,跑一下,print 看形状对不对。
  4. pandas 链接地址里要有 version/2.3(本机是 2.3.3,官网默认 3.x)。链接汇总在 练习/1108测试2_API文档索引.md。

Pandas

Examples use the California housing data housing.csv unless noted. Every snippet was run in order on pandas 2.3.3.

数据概览

数据集概览:shape / dtypes / describetricky

shape 是 (行数, 列数),先行后列,和 loc[行, 列]、axis=0/1 是一个顺序。
df = pd.read_csv('housing.csv')
df.shape                                  # (20640, 10)
df.shape[0], len(df)                      # 行数:20640
df.shape[1]                               # 列数:10
df.select_dtypes('object').shape[1]       # 文字列有几列:1(挑完再取 shape[1],别接 .count())
df.duplicated().sum()                     # 完全重复的行:0
df.describe()                             # 描述统计:count/mean/std/min/25%/50%/75%/max

缺失值处理

缺失值统计:isna().sum()tricky

一次 .sum() 少一个维度:表 → 每列缺几个 → 全表一共缺几个。
na = df.isna().sum()                          # 每列缺几个(一列数)
na = na[na > 0].sort_values(ascending=False)   # 只留有缺失的,从多到少
na_table = pd.DataFrame({'缺失数': na, '缺失比例': na / len(df)})   # 字典拼成表
df.isna().sum().sum()                         # 全表一共缺几个:207

整列填充:fillnalog #31 / 32 / 34

先定填什么:数值偏态 → 中位数;文字 → 众数(不能用中位数);NaN 表示「没有」→ 0 或 'None'(见 答疑记录/1108缺失值怎么处理.md)。再定填哪些格:整列的空值都填 → fillna;只填满足条件的 → df.loc[条件, 列] = 值。
df2 = df.copy()
df2['total_bedrooms'] = df2['total_bedrooms'].fillna(df2['total_bedrooms'].median())
df2['ocean_proximity'].mode()[0]              # '<1H OCEAN'  众数要 [0],不取只填第 0 行且不报错
df3 = df.fillna({'total_bedrooms': 0})        # 一次填多列:传字典
t2 = pd.DataFrame({'YearBuilt': [1990, 2000], 'GarageYrBlt': [1995, np.nan]})
t2['GarageYrBlt'].fillna(t2['YearBuilt'])     # [1995, 2000]  括号里放一列 → 按行对位填
df2['total_bedrooms'] = df2['total_bedrooms'].fillna(
    df2.groupby('ocean_proximity')['total_bedrooms'].transform('median'))   # 按组填(见下一节)

按条件填充:df.loc[条件, 列] = 值mock A4

t = pd.DataFrame({'TotalBsmtSF': [800, 900, 0, 700],
                  'BsmtExposure': ['No', np.nan, np.nan, 'No']})
t['BsmtExposure'].fillna('No')                # ['No','No','No','No']  ❌ 没地下室的也填了
bad = (t['TotalBsmtSF'] > 0) & t['BsmtExposure'].isna()
t.loc[bad, 'BsmtExposure'] = 'No'             # ['No','No',nan,'No']   ✅ 只填真缺的

结构性缺失与真实缺失:loc + fillnamock A4

for col in bsmt:
    dc.loc[a & dc[col].isna(), col] = dc[col].mode()[0]
bad_mas = dc['MasVnrType'].isna() & (dc['MasVnrArea'] > 0)
dc.loc[bad_mas, 'MasVnrType'] = dc['MasVnrType'].mode()[0]
dc[none_cols] = dc[none_cols].fillna('None')
dc['MasVnrType'].value_counts()               # None 867 / BrkFace 450 …;漏了 bad_mas 那步是 872 / 445

布尔条件

构造布尔条件:== / > / isin / isnalog #38

每个比较加括号;用 & | ~,不能写 and or not,否则报 truth value of a Series is ambiguous。数「满足的有几个」用 .sum(),不用 .count()(它数的是非空值)。
t = pd.DataFrame({'Id': [1, 2, 3, 4, 5],
                  'GarageType': ['Attchd', np.nan, 'Detchd', np.nan, 'Attchd'],
                  'GarageArea': [500, 0, 300, 0, 0]})

# 写条件:每个比较 → 一列 True/False
a = t['GarageType'].isna()      # [F, T, F, T, F]
b = t['GarageArea'] == 0        # [F, T, F, T, T]
t['Id'].isin([2, 4])            # 在不在名单里
df[cols].isna().any(axis=1)     # 几列里至少一列空(每行横着看);.all(axis=1) 全空

组合多个条件:& | ~

a & b;  a | b;  ~a           # 且 / 或 / 非
a == b                          # [T, T, T, T, F]  两边一致吗
# ❌ a.isna() == b == 0          连写比较 = (… == b) and (b == 0),报错

条件计数与汇总:.sum() / .mean() / .all()log #28 · mock A3

b.sum()                         # 3     几行(True 算 1)
b.mean()                        # 0.6   比例
(a == b).all()                  # False 全都满足吗(「是不是恰好」)
b.any()                         # True  有没有
t['GarageType'].count()         # 3     ⚠ 这是非空值个数,和条件无关

# 全真卷 1 A3
(df['GarageType'].isna() == (df['GarageArea'] == 0)).all()          # True
(df['MasVnrType'].isna() & (df['MasVnrArea'] > 0)).sum()           # 5

选择与删除

选择列:df['a'] / df[['a', 'b']]

t = pd.DataFrame({'Id': [1, 2, 3, 4], 'Area': [1200, 4500, 900, 5600],
                  'Type': ['A', 'B', 'A', 'C'], 'Price': [150, 160, 90, 180]})

t['Area']                               # 一列 → Series
t[['Area', 'Price']]                    # 几列 → DataFrame(里层是列名列表)

按条件筛选行:df[条件] / df.loc[条件, 列]

bad = (t['Area'] > 4000) & (t['Price'] < 170)
t[bad]                                  # Id [2]
t.loc[bad, 'Id']                        # 筛行 + 取列
t[t['Type'].isin(['A', 'C'])]           # 在列表里:Id [1, 3, 4]

删除行:~条件 / drop(index=) / dropnamock A5

删行 = 筛出「其余行」(~ 取反)或 drop 行标签。都返回新表,要存起来;后面要加列就 .copy()(免 SettingWithCopyWarning)。
t2 = t[~bad].copy()                     # ~ 取反 = 留下其余行:Id [1, 3, 4](常用)
t.drop(index=t[bad].index)              # drop 要行标签(index),不是 Id
t[~t['Id'].isin([2])]                   # 按 Id 删
t2.index.tolist()                       # [0, 2, 3]  删完标签有缺口
t2.reset_index(drop=True)               # 重编 0,1,2…;drop=True 不把旧标签留成列
t.dropna()                              # 删有空值的行;subset=['Area'] 只看某列
t.drop_duplicates()                     # 删完全重复的行

删除列:drop(columns=)

t.drop(columns=['Price'])               # 推荐
t.drop('Price', axis=1)                 # 同上;漏了 axis=1 会当行标签找 → KeyError

删除异常记录:df[~条件]mock A5

# 全真卷 1 A5
a5_logic = dc.loc[dc['YrSold'] < dc['YearRemodAdd'], 'Id'].tolist()     # [524]
big = (dc['GrLivArea'] > 4000) & (dc['SalePrice'] < 300000)
a5_big = sorted(dc.loc[big, 'Id'].tolist())                            # [524, 1299]
a5_cond = dc.loc[big, 'SaleCondition'].unique()[0]                    # 'Partial'
df2 = dc[~big].copy()
a5_n2 = len(df2)                                                       # 1458

结果提取

Series 转为列表:.tolist() / sorted()

loc 取一列得到的是 Series(左边带行标签),不是 list,也不是值。题目要什么,就在末尾转成什么。
big = (dc['GrLivArea'] > 4000) & (dc['SalePrice'] < 300000)
s = dc.loc[big, 'SaleCondition']        # Series
# 523     Partial
# 1298    Partial

# 要 list
s.tolist()                              # ['Partial', 'Partial']
sorted(dc.loc[big, 'Id'].tolist())      # [524, 1299]   排好序的新列表
dc.loc[big, 'Id'].sort_values().tolist()  # 先排再转,一样
# ❌ x = lst.sort()                      原地排,返回 None

提取单个值:.iloc[0] / .unique()[0]mock A5

s.iloc[0]                               # 'Partial'  按位置取第 1 个
s.unique()                              # array(['Partial'], dtype=object)  去重后仍是数组,套着一层
s.unique()[0]                           # 'Partial'  [0] 把值取出来
s.nunique()                             # 1          几种

定位最大值所在行:idxmax + loc

dc.loc[dc['SalePrice'].idxmax(), 'Id']  # 692   idxmax 给行标签,再用 loc 取列

条件变换

生成 0/1 指示变量:(条件).astype(int)mock B1

if 一次只判断一个值,对整列用会报 truth value of a Series is ambiguous。按要的结果选写法,从简单到通用:
t = pd.DataFrame({'YearBuilt': [1990, 2000, 1970], 'YearRemodAdd': [1990, 2005, 2008],
                  'YrSold': [2008, 2007, 2007], 'Qual': ['Gd', 'TA', 'Ex']})
d = t['YrSold'] - t['YearRemodAdd']                       # [18, 2, -1]

# ❌ t['x'] = d if d > 0 else 0                             if 不能判断一整列

# 1. 结果是 1/0:条件本身就是 True/False,转成 int
(t['YearRemodAdd'] != t['YearBuilt']).astype(int)        # [0, 1, 1]   翻修过记 1

截断上下限:clipmock B1

# 2. 截上下限:小于 0 的改成 0
d.clip(lower=0)                                           # [18, 2, 0]  上限用 upper=

条件赋值:np.where / np.select

# 3. 二选一:成立取 A,否则取 B(A、B 可以是值,也可以是一列)
np.where(d < 0, 0, d)                                     # [18, 2, 0]  和 clip 同效
np.where(t['YearRemodAdd'] > t['YearBuilt'], '翻修过', '原样')   # ['原样','翻修过','翻修过']

# 4. 多选一:条件按顺序判断,先满足先用
np.select([d < 0, d <= 5], ['异常', '新翻修'], default='旧')   # ['旧','新翻修','异常']
# 按数值区间分档也可以用 pd.cut(见「分档」)

按映射表编码:.map(字典)mock B2

# 5. 按取值一一对照:字典 map(有顺序的类别 → 数字)
t['Qual'].map({'Ex': 5, 'Gd': 4, 'TA': 3})               # [4, 3, 5]
t['Qual'].map({'Ex': 5, 'Gd': 4})                         # [4, nan, 5]  ⚠ 字典里没有的变 NaN

分箱

等距与等频分箱:pd.cut / pd.qcuttricky

cut 按你给的边界切,qcut 按分位数切成人数相同的几档。对切出来的列做 groupby / pivot_table 时加 observed=True(只要真的有数据的组,不加会弹 FutureWarning)。
df['inc_cat'] = pd.cut(df['median_income'], bins=[0, 1.5, 3, 4.5, 6, np.inf], labels=[1, 2, 3, 4, 5])
df['inc_q'] = pd.qcut(df['median_income'], q=4, labels=['低', '中低', '中高', '高'])
df.groupby('inc_cat', observed=True)['median_house_value'].median()

区间端点的开闭:right= / include_lowest=mock B4

# 区间开闭:默认 right=True → (左, 右],左开右闭
s = pd.Series([0, 10, 11, 30])
pd.cut(s, [0, 10, 30])                        # [NaN, (0,10], (10,30], (10,30]]   0 不在 (0,10] 里 → NaN
pd.cut(s, [0, 10, 30], right=False)           # [[0,10), [10,30), [10,30), NaN]   改成左闭右开
pd.cut(s, [0, 10, 30], include_lowest=True)   # 第一档把左端点也算进去,0 不再是 NaN
pd.cut(s, [-1, 10, 30])                       # 或者把左边界往下放一点(全真卷 B4 用 -1)

分布与异常值

IQR 法识别异常值:quantile · IQRtricky

Q1、Q3 是箱子两条边,边界还要再往外扩 1.5 个 IQR。超出 = 低于下界 或 高于上界:| 是或,& 是且,每个条件套括号。
q1 = df['median_house_value'].quantile(0.25)
q3 = df['median_house_value'].quantile(0.75)
iqr = q3 - q1
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
((df['median_house_value'] < low) | (df['median_house_value'] > high)).sum()   # 超出的个数

偏度:.skew() / stats.skewtricky

右偏 = 右边拖长尾巴,均值 > 中位数,skew() > 0。取 log 把「差多少」变成「差几倍」,长尾被压回去。
s = df['median_house_value']
s.mean(), s.median(), s.skew()       # 206856 > 179700,偏度 0.98

# 偏度两种算法(Ames 的 SalePrice)
s.skew()                             # 1.8829  pandas 自带,不用另外 import
from scipy import stats              # 卷子开头一般已 import
stats.skew(s)                        # 1.8809  题目说用 scipy 就用这个;默认公式略不同,差一点正常
stats.skew(s, bias=False)            # 1.8829  加 bias=False 就和 pandas 一样

对数变换与还原:np.log1p / np.expm1

np.log(s)        # 自然对数 ln;数据里没有 0 时用(np 是 numpy)
np.log1p(s)      # ln(1 + x);有 0 时用
np.expm1(np.log1p(s))                # 反函数,还原回原值

分组与汇总

分组聚合:groupby().agg([...])log #29

行 = 分了几组(组名变成索引);列 = 算了几样。一次算几样要装进列表,写成 .agg('median', 'count') 会悄悄只算第一个。
g = (df.groupby('ocean_proximity')['median_house_value']
       .agg(['mean', 'median', 'count'])            # 列表!
       .sort_values('median', ascending=False))     # 从大到小
g.index[0]                                           # 第一名的组名(组名在索引上)
g.loc['INLAND', 'mean']                              # 取某组某列
g['median'].idxmax()                                 # 最大值对应的组名

组内统计量回填:groupby().transform()

groupby(...).median() 每组一行(5 行),对不上原表 20640 行;.transform('median') 把每组的值写回该组每一行,长度和原表一样,所以能直接拿去填或比较。
df.groupby('ocean_proximity')['median_house_value'].median()            # 5 行
df.groupby('ocean_proximity')['median_house_value'].transform('median').shape   # (20640,)

类别频数:value_counts / nunique

填完缺失值用它核对:每种值几个、还有没有 NaN。
s = pd.Series(['None', 'BrkFace', 'None', 'Stone', 'None', np.nan])
s.value_counts()                 # None 3 / BrkFace 1 / Stone 1(不含 NaN,从多到少)
s.value_counts(normalize=True)   # 占比:0.6 / 0.2 / 0.2
s.value_counts(dropna=False)     # 多一行 NaN 1
s.nunique()                      # 3    几种
s.unique()                       # ['None','BrkFace','Stone',nan]  哪几种
s.count()                        # 5    非空几个

交叉频数表:pd.crosstab

格子里是人数;normalize='index' 让每一行加起来等于 1(每类在各档上怎么分布)。
ct = pd.crosstab(df['ocean_proximity'], df['inc_q'], normalize='index')
ct.round(2)

透视表:pivot_tabletricky

groupby 按一个东西分,结果是一列;透视表一个放行、一个放列,每格算一个数。四个参数 = 四个问题:行按什么分、列按什么分、格子里算哪列、怎么算。
pt = df.pivot_table(index='inc_q', columns='ocean_proximity',
                    values='median_house_value', aggfunc='median', observed=True)
pt.round(0)

相关分析

相关系数:corrlog #33

corr 要 numeric_only=True(有文字列);负相关也是强相关,排序前取绝对值。只要两列时直接 a.corr(b)。
r = df.corr(numeric_only=True)['median_house_value']   # 每个变量和房价的 r
r = r.drop(['median_house_value'])                     # 去掉自己
r.abs().sort_values(ascending=False).head(3)           # 按绝对值排,取前 3
df['median_income'].corr(df['median_house_value'])     # 只算两列:0.688

相关系数与显著性:stats.pearsonr / spearmanr

pandas 的 corr 只给 r;要 p 值(显著不显著)用 scipy。p < 0.05 = 显著。样本大时很弱的相关也显著,结论两件事都说:「显著,但 r 只有 0.11,影响很小」。⚠ 有 NaN 返回 nan,先 dropna。
from scipy import stats              # import 这一行,之后都写 stats.xxx

a, b = df['median_income'], df['median_house_value']

# 皮尔森:线性相关
r, p = stats.pearsonr(a, b)          # 返回两个:r、p 值
r                                    # 0.688
p < 0.05                             # True   显著

# 斯皮尔曼:按排名算,只看「同增同减」,不怕离群点、弯曲
rho, p2 = stats.spearmanr(a, b)      # rho = 0.677
a.corr(b, method='spearman')         # 0.677  pandas 也能算 rho,但没有 p 值

# 一个变量接住,按名字取
res = stats.pearsonr(df['housing_median_age'], b)
res.statistic, res.pvalue < 0.05     # 0.106, True

# 有缺失值:先把两列一起 dropna(同一行一起删)
stats.pearsonr(df['total_bedrooms'], b)             # nan, nan  ✗
d = df[['total_bedrooms', 'median_house_value']].dropna()
stats.pearsonr(d['total_bedrooms'], d['median_house_value'])   # 0.050, p = 1.2e-12

相关性最强的变量对:where + stack + idxmaxlog #43mock C2

方表没法直接问「最大值在哪一格」,所以三步:对角线变空 → 摊成一列 → 找最大。⚠ stack() 后 idxmax() 给的是元组,不能接 .tolist(),用 sorted(...)。
m = pd.DataFrame([[1, .6, .9], [.6, 1, .3], [.9, .3, 1]],
                 index=['A', 'B', 'C'], columns=['A', 'B', 'C'])   # 假装这是 corr() 的结果

# ① 对角线是自己和自己(= 1),不算,变成 NaN
m = m.where(m < 1)          # where:条件成立的留下,其余变 NaN
#      A    B    C
# A  NaN  0.6  0.9
# B  0.6  NaN  0.3
# C  0.9  0.3  NaN

# ② stack:把方表摊成一长列,每格变成一行,「行名 + 列名」一起当索引(NaN 自动丢掉)
s = m.stack()
# A  B    0.6
#    C    0.9      ← 最大
# B  A    0.6
#    C    0.3
# C  A    0.9      (同一对出现两次,A-C 和 C-A,不影响)
#    B    0.3

# ③ 找最大
s.idxmax()                  # ('A', 'C')  最大值的索引 = 哪两列(元组)
s.max()                     # 0.9         最大值本身
sorted(s.idxmax())          # ['A', 'C']  题目要字母序的 list
# s.idxmax().tolist()       # ✗ AttributeError: 'tuple' object has no attribute 'tolist'
# list(s.idxmax())          # ['A', 'C']  能转 list,但不排序

# 全真卷 C2:只用 8 个特征(cm 里带着 LogPrice 时先 cm.loc[c1_top, c1_top])
m = df2[c1_top].corr()
m = m.where(m < 1)
c2_pair = sorted(m.stack().idxmax())     # ['GarageArea', 'GarageCars']
c2_pair_r = m.stack().max()              # 0.887

其他

常用小语法

na.index[:3].tolist()          # 索引转成 Python 列表(题目要「列表」时加 .tolist())
x = 3
'正' if x > 0 else '负'         # 一行 if:条件成立取前面,否则取后面
df.sample(2000, random_state=0).shape   # 随机抽 2000 行(只在画散点图时用,统计量用全量)

Visualization

TA focus: scatter, bar and heatmap. Examples use df2 (Ames housing) from the mock exam. Run the setup cell first.

绘图准备

字体与配色设置:rcParams

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import glob, matplotlib
from matplotlib import font_manager as fm
for f in glob.glob(r'F:\2026fall_skd\练习\画图字体\NotoSerifSC-*.ttf'):
    fm.fontManager.addfont(f)              # 本机字体;别的电脑没有这个文件夹,自动跳过
have = {f.name for f in fm.fontManager.ttflist}

# ---- WYX Only:粉蓝马卡龙 ----
CN = [f for f in ['Noto Serif SC', 'Source Han Serif SC', 'Songti SC', 'STSong', 'SimSun', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei'] if f in have]   # 宋体优先,没有再退到黑体
MAIN, MAIN_D, MAIN_L = '#a9c8ea', '#6f93c6', '#c9dcf2'   # 主色:浅蓝 / 深蓝 / 很浅的蓝
ACCENT, ACCENT_D = '#f4b6ca', '#e0819f'                 # 强调色:浅粉 / 深粉
GRAY, BG = '#a89dab', '#fffbfc'                         # 注释文字 / 带粉的白底
CMAP = sns.blend_palette([ACCENT_D, '#fbe1ea', BG, MAIN_L, '#86abdb', '#4f7fb8', '#2d4a78'], as_cmap=True)  # 负粉 → 白 → 正蓝
sns.set_theme(style='white')
plt.rcParams.update({
    'font.family': CN + ['DejaVu Sans'], 'axes.unicode_minus': False,
    'figure.facecolor': BG, 'axes.facecolor': BG, 'savefig.facecolor': BG,
    'figure.dpi': 110, 'figure.constrained_layout.use': True,
    'axes.spines.top': False, 'axes.spines.right': False,
    'axes.edgecolor': '#e3d6dc', 'axes.linewidth': 0.9,
    'axes.grid': True, 'axes.grid.axis': 'y', 'axes.axisbelow': True,
    'grid.color': '#f3e9ee', 'grid.linewidth': 0.8,
    'axes.titlesize': 14, 'axes.titleweight': 'bold', 'axes.titlepad': 12, 'axes.titlecolor': '#4a4150',
    'axes.labelsize': 11, 'axes.labelcolor': '#7a6f80',
    'xtick.color': GRAY, 'ytick.color': GRAY, 'xtick.labelsize': 10, 'ytick.labelsize': 10,
    'xtick.major.size': 0, 'ytick.major.size': 0,
    'legend.frameon': False, 'legend.fontsize': 10, 'legend.labelcolor': '#6b6070',
    'axes.prop_cycle': matplotlib.cycler(color=[MAIN, ACCENT, MAIN_D, ACCENT_D]),
})

图表类型的选择tricky

# 一个数值        → 直方图 histplot(先 describe / skew)
# 一个类别        → 计数图 countplot(先 value_counts)
# 数值 × 数值     → 散点图 scatterplot + regplot(先 corr)
# 类别 × 数值     → 箱线图 boxplot、柱状图 barplot(先 groupby.agg)
# 类别 × 类别     → 堆叠柱状图、热力图(先 crosstab)
# 三个变量        → 加 hue=,或 pivot_table 后画热力图
# 很多数值列      → 相关系数热力图
# 随时间变化      → 折线图

seaborn 与 pandas 绘图的分工:sns.xxx / df.plot

# sns.xxx(data=原始表, x=列名, y=列名)   会帮你算(分组、计数、求平均)
# 算好的表.plot(kind='bar')              不算,表里是什么画什么:索引 → 横轴,每列一组
# ⚠ 算好的表别再交给 sns.barplot:它会再平均一次

# x / y 只传现成的列,图自己能算的不传:
#   直方图、计数图只给 x(高度是数出来的);箱线图 x=分组、y=数值;散点图 x、y 都给;热力图给一张表

画布与子图:plt.subplots / ax=log #30

fig, ax = plt.subplots(1, 2, figsize=(11, 4))     # 1 行 2 列:fig 是整张纸,ax[0]、ax[1] 是两个格子
sns.histplot(data=df2, x='SalePrice', ax=ax[0])    # ✅ ax= 写在括号里
# ❌ ax[0] = sns.histplot(...)                       等号是贴标签,图会画到最后一格
# 只有一个格子:fig, ax = plt.subplots(figsize=(7, 4)),后面写 ax=ax,不加 [0]

标题、坐标轴与参考线:set_title / axvline / textlog #35

ax[0].set_title('成交价分布')                                     # 标题写图名
ax[0].set_xlabel('成交价(美元)'); ax[0].set_ylabel('房子数')      # 中文 + 单位
ax[0].xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')    # 200000 → 200k;纵轴用 ax.yaxis
ax[0].tick_params(axis='x', rotation=45)                          # 横轴文字转方向
ax[0].axvline(180000, color=ACCENT_D, ls='--', label='某个值')       # 竖线给「数」;横线 axhline
ax[0].legend()                                                    # label 只是起名,legend() 才显示
ax[0].text(0.97, 0.9, '偏度 1.88', transform=ax[0].transAxes,      # 角上写字:坐标 0–1(右上角 ≈ 0.97, 0.9)
           ha='right', color=ACCENT_D)
fig.suptitle('整张图的总标题')

直方图

单变量分布:sns.histplotmock B3

fig, ax = plt.subplots(1, 2, figsize=(11, 4.2))
sns.histplot(data=df2, x='SalePrice', bins=40, kde=True, color=MAIN_D, ax=ax[0])   # kde=True 柱子自动半透明,给深色
med, mean = df2['SalePrice'].median(), df2['SalePrice'].mean()
ax[0].axvline(med, color=ACCENT_D, lw=2.2, label=f'中位数 {med/1000:.0f}k')            # 粉色实线
ax[0].axvline(mean, color=MAIN_D, lw=2.2, ls='--', label=f'均值 {mean/1000:.0f}k')    # 蓝色虚线
ax[0].set_title('成交价分布')
ax[0].set_xlabel('成交价(美元)'); ax[0].set_ylabel('房子数')
ax[0].xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax[0].legend(loc='upper right')
ax[0].text(0.97, 0.66, f"偏度 {stats.skew(df2['SalePrice']):.2f}", transform=ax[0].transAxes, ha='right', color=ACCENT_D)

sns.histplot(data=df2, x='LogPrice', bins=40, kde=True, color=ACCENT_D, ax=ax[1])
ax[1].set_title('log(1+成交价) 分布')
ax[1].set_xlabel('log(1+成交价)'); ax[1].set_ylabel('')
ax[1].text(0.97, 0.9, f"偏度 {stats.skew(df2['LogPrice']):.2f}", transform=ax[1].transAxes, ha='right', color=ACCENT_D)
# 纵轴想看占比:stat='density'(柱子面积加起来 = 1)

箱线图

分组分布比较:sns.boxplot

fig, ax = plt.subplots(figsize=(8, 4.2))
sns.boxplot(data=df2, x='OverallQual', y='SalePrice', color=MAIN, width=0.6, linewidth=1,
            medianprops={'color': ACCENT_D, 'linewidth': 2},                                  # 中位数线粉色
            flierprops={'marker': 'o', 'markersize': 3, 'markerfacecolor': ACCENT_D,
                        'markeredgecolor': 'none', 'alpha': 0.6}, ax=ax)                    # 离群点小粉点
ax.set_title('不同质量等级的成交价')
ax.set_xlabel('整体质量(1–10)'); ax.set_ylabel('成交价(美元)')
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
# 顺序:order=列表(如 groupby 后 sort_values 的 .index);再分一层:hue='CentralAir';名字长:tick_params 转 45 度

散点图

两个数值变量的关系:sns.scatterplot + regplottricky

fig, ax = plt.subplots(figsize=(7.5, 4.8))
sns.scatterplot(data=df2, x='GrLivArea', y='SalePrice', hue='CentralAir', hue_order=['Y', 'N'],
                palette=[MAIN_D, ACCENT_D], s=24, alpha=0.55, edgecolor='white', linewidth=0.4, ax=ax)   # 点多:alpha 调透明
sns.regplot(data=df2, x='GrLivArea', y='SalePrice', scatter=False, ci=None,                         # 只画回归线
            color=GRAY, line_kws={'lw': 1.4, 'ls': '--'}, ax=ax)
r, p = stats.pearsonr(df2['GrLivArea'], df2['SalePrice'])
ax.set_title('地上面积与成交价')
ax.set_xlabel('地上面积(平方英尺)'); ax.set_ylabel('成交价(美元)')
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.grid(axis='x')                                    # 散点图横竖网格都要
ax.legend(title='中央空调', loc='upper left')
ax.text(0.97, 0.06, f'r = {r:.2f}', transform=ax.transAxes, ha='right', color=ACCENT_D)
# 纵轴对数刻度:ax.set_yscale('log');hue 给数值列(如 OverallQual)会变成渐变色:palette='Blues'

柱状图

分组汇总:sns.barplot

柱子高度由 seaborn 按组算(estimator)。标数值:ax.bar_label(ax.containers[0]),有 hue 时每种颜色一组、要 for 循环;1×2 子图写 ax[0]。下面三张卡同理。
# 原始数据直接画:seaborn 自己按 x 分组、每组算一个数
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', estimator='median', errorbar=None, color=MAIN, ax=ax)
ax.set_title('各质量等级的成交价中位数')
ax.set_xlabel('整体质量'); ax.set_ylabel('成交价中位数(美元)')
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', padding=2, fontsize=8, color=GRAY)   # 柱顶标数值:50k …
# estimator 不写 = 均值;errorbar 不写 = 柱顶黑线(95% 置信区间)
# 只标数字不改格式:ax.bar_label(ax.containers[0])          # 50150 …;千分位 fmt='{:,.0f}'

# —— 想按高低排序:order 给一串类别 ——
order = df2.groupby('OverallQual')['SalePrice'].median().sort_values(ascending=False).index
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', estimator='median', errorbar=None, color=MAIN, order=order, ax=ax)

# —— 类别多 / 名字长,想横着放:x、y 对调 ——
fig, ax = plt.subplots(figsize=(7, 7))
sns.barplot(data=df2, x='SalePrice', y='Neighborhood', estimator='median', errorbar=None, color=MAIN, ax=ax)
ax.xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')        # 数值轴变成了 x 轴

# —— 想再分一层:hue;每种颜色是一组柱子,标数值要逐组 ——
fig, ax = plt.subplots(figsize=(9, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', hue='CentralAir', estimator='median', errorbar=None,
            palette=[ACCENT, MAIN], ax=ax)
for c in ax.containers:
    ax.bar_label(c, fmt=lambda v: f'{v/1000:.0f}k', fontsize=7, color=GRAY)
ax.legend(title='中央空调')

类别计数:sns.countplot

# 只给 x:柱子高度 = 每类几行
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.countplot(data=df2, x='OverallQual', color=ACCENT, ax=ax)
ax.set_title('各质量等级的房子数')
ax.set_xlabel('整体质量'); ax.set_ylabel('房子数')
ax.bar_label(ax.containers[0], padding=2, fontsize=8, color=GRAY)   # 柱顶标个数:2, 3, 20 …

# —— 想看占比:stat='percent'(seaborn 0.13 起) ——
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.countplot(data=df2, x='OverallQual', stat='percent', color=ACCENT, ax=ax)
ax.bar_label(ax.containers[0], fmt='{:.1f}%', padding=2, fontsize=8, color=GRAY)   # 0.1%, 0.2%, 1.4% …

# —— 想按多少排序 + 横着放 ——
fig, ax = plt.subplots(figsize=(7, 7))
sns.countplot(data=df2, y='Neighborhood', order=df2['Neighborhood'].value_counts().index, color=ACCENT, ax=ax)
ax.bar_label(ax.containers[0], padding=2, fontsize=8, color=GRAY)

# —— 想再分一层:hue,逐组标 ——
fig, ax = plt.subplots(figsize=(9, 4))
sns.countplot(data=df2, x='OverallQual', hue='CentralAir', palette=[ACCENT, MAIN], ax=ax)
for c in ax.containers:
    ax.bar_label(c, fontsize=7, color=GRAY)

已算好的表:ax.barh / Series.plot

# 已经 groupby 算好、排好序的 Series:index → 柱子,值 → 长度
med = df2.groupby('Neighborhood')['SalePrice'].median().sort_values()
fig, ax = plt.subplots(figsize=(7.5, 6.8))
ax.barh(med.index, med.values, color=MAIN, height=0.68)           # 竖着放:ax.bar
ax.set_title('各街区成交价中位数')
ax.set_xlabel('成交价中位数(美元)'); ax.set_ylabel('')
ax.xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.grid(axis='y', visible=False); ax.grid(axis='x')               # 横柱子配竖网格
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', padding=3, fontsize=8, color=GRAY)

# —— 想突出前 5 名:color 给一串颜色,和柱子一一对应 ——
colors = [ACCENT_D if v >= med.iloc[-5] else MAIN for v in med]   # med 升序,最后 5 个最大
fig, ax = plt.subplots(figsize=(7.5, 6.8))
ax.barh(med.index, med.values, color=colors, height=0.68)

# —— 不想写这么多:pandas 一行 ——
fig, ax = plt.subplots(figsize=(7.5, 6.8))
med.plot(kind='barh', color=MAIN, ax=ax)                           # 竖着放:kind='bar'
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', fontsize=8, color=GRAY)

堆叠柱状图:plot(kind='barh', stacked=True)tricky

# crosstab 的结果:index → 一根柱子,列 → 柱子里的一段颜色
tier = pd.cut(df2['OverallQual'], bins=[0, 5, 7, 10], labels=['低', '中', '高'])
ct = pd.crosstab(df2['MSZoning'], tier, normalize='index').sort_values('高')   # 每行加起来 = 1
fig, ax = plt.subplots(figsize=(7.5, 3.8))
ct.plot(kind='barh', stacked=True, color=[ACCENT, MAIN_L, MAIN_D], width=0.65, ax=ax)   # 竖着放:kind='bar'
ax.set_title('各用地类型的房屋质量构成')
ax.set_xlabel('占比'); ax.set_ylabel('用地类型')
ax.xaxis.set_major_formatter(lambda x, _: f'{x:.0%}')                 # 0.5 → 50%
ax.grid(axis='y', visible=False); ax.grid(axis='x')
ax.legend(title='质量', bbox_to_anchor=(1, 1), loc='upper left')      # 图例放到图外右边
for c in ax.containers:                                                # 每种颜色一组,数字写在段中间
    ax.bar_label(c, labels=[f'{v:.0%}' if v > 0.08 else '' for v in c.datavalues],   # 太窄的段不写
                 label_type='center', fontsize=8)
# 看法:比各根柱子里同一种颜色的长短;ct.T 转置换个角度看

# —— 想看人数不看占比:去掉 normalize ——
ct_n = pd.crosstab(df2['MSZoning'], tier)
fig, ax = plt.subplots(figsize=(7.5, 3.8))
ct_n.plot(kind='barh', stacked=True, color=[ACCENT, MAIN_L, MAIN_D], width=0.65, ax=ax)
for c in ax.containers:
    ax.bar_label(c, labels=[f'{v:.0f}' if v > 0 else '' for v in c.datavalues], label_type='center', fontsize=8)   # 0 不写

热力图

相关矩阵与列联表:sns.heatmapmock C2

cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'YearBuilt', 'FullBath']
corr = df2[cols].corr().iloc[1:, :-1]                     # 去掉对角线那一行一列
mask = np.triu(np.ones_like(corr, dtype=bool), k=1)        # 遮住右上角,只留下三角
fig, ax = plt.subplots(figsize=(7, 5.6))
sns.heatmap(corr, mask=mask, annot=True, fmt='.2f', cmap=CMAP, center=0, vmin=-1, vmax=1,   # 相关系数:−1 到 1
            square=True, linewidths=2, linecolor=BG, cbar_kws={'shrink': 0.7},
            annot_kws={'fontsize': 9.5}, ax=ax)          # 字色不指定:深底自动白字
ax.set_title('数值变量的相关系数')
ax.grid(False); ax.tick_params(axis='x', rotation=30); ax.tick_params(axis='y', rotation=0)
# 别的表照样喂(都只有正数 → 单色渐变):
# sns.heatmap(pd.crosstab(a, b, normalize='index'), annot=True, fmt='.0%', cmap=sns.light_palette(MAIN_D, as_cmap=True))
# sns.heatmap(confusion_matrix(y_test, pred), annot=True, fmt='d', cmap=sns.light_palette(MAIN_D, as_cmap=True))   # 整数 fmt='d'

折线图

时间趋势:ax.plot

yr = df2.groupby('YrSold')['SalePrice'].median()
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(yr.index, yr.values, color=MAIN_D, lw=2, marker='o', ms=7, mfc='white', mew=2)   # 白心圆点
ax.set_title('各年成交价中位数')
ax.set_xlabel('成交年份'); ax.set_ylabel('成交价中位数(美元)')
ax.set_xticks(yr.index)                                    # 横轴只标整数年份
ax.margins(y=0.2)                                          # 上下留白,给数字腾地方
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
for x, y in yr.items():                                   # 每个点上方标数值:往上挪 8 个点,不随刻度变
    ax.annotate(f'{y/1000:.0f}k', (x, y), xytext=(0, 8), textcoords='offset points',
                ha='center', fontsize=9, color=GRAY)
# 也可以:sns.lineplot(data=df2, x='YrSold', y='SalePrice', estimator='median', errorbar=None, ax=ax)

Modeling

X and y are prepared first, as in Modeling Mock 1.

数据准备

特征与目标变量tricky

X 是 DataFrame(每行一条记录,好几列特征,只能有数字);y 是 Series(每行一个答案)。两者行数相同、一一对应。X 不放答案列。
df = pd.read_csv('housing.csv')
df['rooms_per_hh'] = df['total_rooms'] / df['households']
df['bedroom_ratio'] = (df['total_bedrooms'] / df['total_rooms'])
df['bedroom_ratio'] = df['bedroom_ratio'].fillna(df['bedroom_ratio'].median())
num_cols = ['median_income', 'housing_median_age', 'rooms_per_hh', 'bedroom_ratio', 'population']
X = pd.get_dummies(df[num_cols + ['ocean_proximity']], columns=['ocean_proximity'], drop_first=True, dtype=int)
y = df['median_house_value']                              # 回归目标
y_hi = (df['median_house_value'] >= 300000).astype(int)   # 分类目标 0/1
type(X).__name__, X.shape, type(y).__name__, y.shape     # DataFrame (20640, 9) Series (20640,)

独热编码:pd.get_dummiestricky

不能编成 1、2、3(模型会以为有大小)。pd.get_dummies 是 pd. 开头,不是 df.。drop_first=True 扔掉排第一的类别当参照组(不然几列加起来恒为 1,和截距撞车);dtype=int 输出 0/1(默认 True/False)。哑变量系数读作「比参照组高/低多少」。
pd.get_dummies(df['ocean_proximity'], dtype=int).head(2)          # 只拆一列
pd.get_dummies(df[['median_income', 'ocean_proximity']], columns=['ocean_proximity'],
               drop_first=True, dtype=int).columns.tolist()        # 参照组是 '<1H OCEAN'

划分训练集与测试集:train_test_splittricky

一次返回 4 个,先 X 后 y,每对先 train 后 test。random_state 固定才能每次一样。分类且正例少时加 stratify=y,让两边正例比例一样。分类和回归的划分用不同名字,别互相覆盖。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
Xc_train, Xc_test, yc_train, yc_test = train_test_split(X, y_hi, test_size=0.2, random_state=42, stratify=y_hi)
X_train.shape, X_test.shape                 # (16512, 9) (4128, 9)

标准化:StandardScalertricky

fit = 量尺子(算每列均值、标准差);transform = 用尺子换算 (x−均值)/标准差;fit_transform = 两步一起。训练集 fit_transform,测试集只 transform:测试集要当成「将来才来的数据」,用它自己的均值就是数据泄漏。y 不用标准化。之后模型都用标准化后的 X。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
Xc_train_s = scaler.fit_transform(Xc_train)
Xc_test_s = scaler.transform(Xc_test)

建模流程

通用四步流程:fit / predicttricky

fit 给训练集 X 和 y;predict 只给 X;评估函数一律 (真实值, 预测值),写反不报错但结果错(recall 会变成 precision)。括号里 LinearRegression(...) 传的是设置,数据只在 fit 里传。模型要存成变量(如 lr),学到的系数存在它里面。
from sklearn.linear_model import LinearRegression
from sklearn.metrics import root_mean_squared_error, r2_score
lr = LinearRegression()                  # ① 建模型(带括号)
lr.fit(X_train, y_train)                 # ② 用训练集学
y_pred = lr.predict(X_test)              # ③ 对测试集预测(只给 X)
root_mean_squared_error(y_test, y_pred)  # ④ 评估:(真实, 预测) → 73072
lr2 = LinearRegression().fit(X_train, y_train)   # fit 返回模型本身,可以连着写

常用模型对照tricky

模型任务import常用设置要不要标准化学出来的东西 / 怎么解读
线性回归
LinearRegression()
回归(预测连续数:房价)from sklearn.linear_model import LinearRegression基本不用设不用(预测不变,只是系数数值变)coef_(一维,每个特征一个)、intercept_。系数 = 其他不变时,特征多 1 单位,y 多多少。单位不同不能直接比大小
逻辑回归
LogisticRegression()
分类(0/1:违约、高价)from sklearn.linear_model import LogisticRegression基本不用设要(不标准化可能不收敛)coef_ 是二维 (1, 特征数),取 [0]。系数为正 → 越大越可能是 1。标准化后可比大小。系数 = 特征多 1,log-odds 多多少
KNN
KNeighborsClassifier(n_neighbors=5)
分类(回归用 KNeighborsRegressor)from sklearn.neighbors import KNeighborsClassifiern_neighbors:看几个邻居;越小越容易过拟合要(算距离,大数列会压住小数列)没有系数。非参数模型:fit 只是把训练集存起来,预测时现找最近的 K 个投票
决策树
DecisionTreeClassifier(max_depth=5, random_state=42)
分类(回归用 DecisionTreeRegressor)from sklearn.tree import DecisionTreeClassifiermax_depth 最多问几层,不限就过拟合;random_state 固定结果不用(按阈值切)feature_importances_:每个特征帮了多大忙,加起来 = 1。自己学出一串「收入 ≤ 6.2 吗?」的问题

回归

模型评估与系数解读:score / RMSE / coef_tricky

.score:回归模型给 R²,分类模型给准确率。coef_ 只是一串没名字的数,顺序和 X.columns 一一对应,用 pd.Series(数, index=名字) 配起来再按名字取。mean_squared_error(..., squared=False) 在 sklearn 1.6 已删,用 root_mean_squared_error 或 np.sqrt(mean_squared_error(...))。
lr.score(X_train, y_train)               # 训练集 R²:0.614
lr.score(X_test, y_test)                 # 测试集 R²:0.593(两者接近 → 没过拟合)
r2_score(y_test, y_pred)                 # 同上
coef = pd.Series(lr.coef_, index=X.columns)
coef['median_income']                    # 44154:收入每多 1 万美元,房价多约 4.4 万(其他不变)
lr.intercept_                            # 截距

对数目标变量:np.log1p / np.expm1tricky

模型学的是 log 房价,预测出来也是 log 房价,先 expm1 还原再和 y_test 算 RMSE。取 log 不一定更好,以测试集数字为准(建模卷里反而变差)。
lr_log = LinearRegression().fit(X_train, np.log1p(y_train))
pred = np.expm1(lr_log.predict(X_test))
root_mean_squared_error(y_test, pred)     # 98731,比 73072 差

分类

逻辑回归与分类指标:LogisticRegression / recall_scoretricky

指标都在 sklearn.metrics,写法一样:precision_score、recall_score、f1_score、accuracy_score。0/1 列的 .mean() 就是 1 的比例。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
logit = LogisticRegression()
logit.fit(Xc_train_s, yc_train)
yc_pred = logit.predict(Xc_test_s)
yc_test.mean()                            # 测试集里 1 的比例:0.187
accuracy_score(yc_test, yc_pred)          # 0.886(全猜 0 也有 0.81,别只看它)
recall_score(yc_test, yc_pred)            # 0.524

混淆矩阵的行列约定:confusion_matrixtricky

            预测0   预测1
真实0  [[  TN      FP ]
真实1   [  FN      TP ]]
行 = 真实,列 = 预测,0 在前,TP 在右下角。很多课件把 TP 画在左上角,正好反过来。Recall 看行(真实为 1 的那行),Precision 看列(预测为 1 的那列)。画热力图:sns.heatmap(cm, annot=True, fmt='d', cmap='Blues'),坐标轴标「预测」「真实」。

混淆矩阵取值:confusion_matrix().ravel()

cm = confusion_matrix(yc_test, yc_pred)
tn, fp, fn, tp = cm.ravel()               # 取四个数最稳的写法

逻辑回归系数解读:coef_[0]

c2 = pd.Series(logit.coef_[0], index=X.columns)   # 二维要取 [0]
c2.idxmax()                               # 系数最大的特征名

分类阈值调整:predict_probatricky

proba = probability。predict_proba 返回两列:属于 0 的概率、属于 1 的概率,取 [:, 1]([行, 列],: 是所有行)。predict() 内部就是 ≥ 0.5 判 1。阈值调低 → 判成 1 的变多 → 召回率升、精确率降。漏掉代价大(风控抓违约、找大客户)就调低。
prob = logit.predict_proba(Xc_test_s)[:, 1]   # 每条记录是 1 的概率
pred_03 = (prob >= 0.3).astype(int)            # 自己按 0.3 判:True/False → 1/0
recall_score(yc_test, pred_03)                 # 0.706,比 0.5 时的 0.524 高

树模型与近邻

决策树与过拟合:DecisionTreeClassifier(max_depth=)tricky

训练集准确率远高于测试集 = 过拟合(不限深度的树:1.0 对 0.83)。限制 max_depth 反而测试集更好。准确率可以直接 .score;召回率等其他指标要先 predict 再传给 metrics 函数。
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
full = DecisionTreeClassifier(random_state=42)
full.fit(Xc_train_s, yc_train)
full.score(Xc_train_s, yc_train), full.score(Xc_test_s, yc_test)   # 1.0, 0.835 → 过拟合
d5 = DecisionTreeClassifier(max_depth=5, random_state=42)
d5.fit(Xc_train_s, yc_train)
d5.score(Xc_test_s, yc_test)                                       # 0.877

特征重要性:feature_importances_

imp = pd.Series(d5.feature_importances_, index=X.columns)
imp.idxmax()                                                       # 'median_income'

K 近邻分类:KNeighborsClassifier

knn = KNeighborsClassifier(n_neighbors=15)
knn.fit(Xc_train_s, yc_train)
knn_pred = knn.predict(Xc_test_s)
recall_score(yc_test, knn_pred)

模型选择

多模型比较:dict + for

models = {'逻辑回归': LogisticRegression(),
          'KNN': KNeighborsClassifier(n_neighbors=5),
          '决策树': DecisionTreeClassifier(max_depth=5, random_state=42)}
rows = []
for name, m in models.items():
    m.fit(Xc_train_s, yc_train)
    p = m.predict(Xc_test_s)
    rows.append([name, m.score(Xc_train_s, yc_train), accuracy_score(yc_test, p), recall_score(yc_test, p)])
pd.DataFrame(rows, columns=['模型', '训练准确率', '测试准确率', '测试召回率']).set_index('模型').round(3)

交叉验证调参:cross_val_scoretricky

挑参数不能看测试集(等于偷看考题),只在训练集里切:5 折 = 切 5 份,轮流拿 1 份当验证,平均 5 次的分数。传进去的是没 fit 的模型,它自己 fit。选好后用这个参数在整个训练集上重新 fit,再到测试集评估一次。
from sklearn.model_selection import cross_val_score
res = {}
for d in [3, 5, 8, 12]:
    tree = DecisionTreeClassifier(max_depth=d, random_state=42)
    res[d] = cross_val_score(tree, Xc_train_s, yc_train, cv=5).mean()
scores = pd.Series(res)          # 字典 → Series:键变索引
scores.idxmax()                  # 5

评价指标的选择tricky

任务看什么写法
回归RMSE(平均差多少,单位同 y)、R²(解释了多少波动)root_mean_squared_error、r2_score / .score
分类准确率、混淆矩阵accuracy_score / .score、confusion_matrix
分类,正例少或错误代价不对称精确率、召回率、F1precision_score、recall_score、f1_score
三条规矩:① 在测试集上算;② 训练集也算一次,差得多就是过拟合;③ 漏掉更贵看召回率(风控抓违约),误判更贵看精确率(判了就拒贷)。

Concepts

术语

核心概念速记

词意思
残差 residual真实 − 预测,点到线的竖直距离;有正有负,直接加会抵消,所以平方
损失 / 经验风险损失 = 一个样本错多少;经验风险 = 所有样本损失的平均。平方损失时就是 MSE。fit 就是找让它最小的参数
参数模型先定好式子的样子,训练只是找式子里的几个数(线性回归、逻辑回归);KNN 不算参数,要留着全部数据
R² 图解把残差画成正方形:R² = (常数模型的总面积 − 线性模型的总面积) ÷ 常数模型的总面积。1 = 全在线上,0 = 和猜均值一样,负数 = 比猜均值还差(只在测试集出现)。只有一个 x 时 R² = r²
标准化 vs 归一化Z-score:(x − 均值)/标准差,有正有负,StandardScaler,默认用它;Min-max:(x − min)/(max − min),落在 [0, 1],怕离群点,MinMaxScaler。pandas 的 std 除以 n−1,StandardScaler 除以 n,手算会差一点
数据泄漏训练时用到了测试集的信息(整体标准化、用测试集挑参数),测试分数偏乐观
过拟合把训练数据背下来:训练集很好、测试集差
逻辑回归先算分数 z = θ₀ + θ₁x,再 sigmoid 压成概率。z 就是 log(赔率);系数正 → 越大越可能是 1;阈值 0.5 ⇔ z = 0,决策边界是一条直线
赔率 oddsp / (1 − p),发生的可能是不发生的几倍
交叉验证训练集切 K 份轮流验证,用来挑参数
混杂变量同时影响 A 和 B 的第三个变量。按它分档再比:差距不变 → A 本身影响大;变小 → 部分是它的作用;方向反了 → 辛普森悖论

Writing

How to turn numbers and charts into conclusions.

结论撰写

三段式结论

  1. 看到了什么,带数字:收入与房价的相关系数为 0.69,散点图呈明显正相关。
  2. 说明了什么:收入是影响房价最大的因素。
  3. 局限:房价在 50 万截顶;相关不等于因果;某组样本太少(ISLAND 只有 5 个)。

分布图的解读

① 大部分数据在哪?② 对称吗,尾巴在哪边?③ 均值和中位数谁大,为什么?④ 所以呢(比如:比较各组用中位数)。第 ④ 句才是结论,前面是描述。

模型结果的报告

模型准不准(R² / RMSE 用人话说,或准确率 + 召回率)、哪个变量影响最大(系数 / 重要性,带方向)、局限(截顶、过拟合、样本少)、在金融里能干什么(抵押估值、核定额度;违约预测用于风控审批,更看重召回率;高价客户筛选用于营销)。
Tabula · Data 100 Reference Sheet · Introduction to Data Science, Fall 2026