1. 这不是“Python入门课”,而是一套真实项目中反复验证过的机器学习前置工作流

你打开过多少本《Python机器学习实战》?翻到第三章就卡在数据加载报错,第四章的绘图代码跑出来全是空白图,第五章模型训练时突然发现特征里混着大量空值和异常日期——最后合上书,默默关掉Jupyter。这不是你学得不够努力,而是绝大多数教程把“机器学习”当成一个从模型开始的黑箱,却刻意跳过了真正决定项目成败的前80%: 数据准备、可信可视化与先验认知构建 。这个标题里的“Prior”不是“优先级”的Priority,而是统计学中的 Prior(先验) ——它指的是在接触任何算法之前,你对数据本身必须建立的直觉、判断与信任。Part 1不讲RandomForest或XGBoost,只聚焦两件事:用Python把原始数据变成可信赖的分析对象,再用可视化把它变成人能看懂的语言。它适合三类人:刚转行的数据新人(别急着调参,先学会“看”数据)、业务部门想自己验证报表逻辑的分析师(不用等工程师排期)、以及被线上模型效果波动折磨已久的算法工程师(你上次认真检查过训练集的分布偏移吗?)。核心关键词—— Python、机器学习前置、数据可视化、先验认知、分布诊断、特征可信度 ——每一个都对应一个你在真实项目里每天要面对的硬问题。

2. 为什么必须把“Prior”单独拆成Part 1?——来自12个失败项目的血泪教训

2.1 先验缺失是模型失效的第一推手,不是第三或第五

我带过三个信贷风控模型迭代项目,其中两个上线后AUC在两周内暴跌超15个百分点。回溯根因,技术团队第一反应是“特征工程不够深”“模型结构太简单”,花两周时间加了27个交叉特征、换了LightGBM的叶子节点数。结果呢?新模型在测试集上AUC涨了0.3%,但线上KS指标反而恶化。最终发现,问题出在Part 1:训练数据里有一批“已结清但未更新状态”的贷款记录,其逾期天数字段被错误填充为0,导致模型学到“结清=无风险”的虚假关联。这个错误在数据加载后的第一张分布直方图里就清晰可见——逾期天数=0的样本占比突兀地高达63%,远超业务常识的40%阈值。但当时没人画这张图,因为“数据清洗脚本跑通了就行”。 Prior阶段的核心任务,从来不是让代码跑通,而是让人的判断有依据 。当你跳过分布诊断、缺失值归因、时间序列断点检查这些步骤,后续所有模型优化都是在流沙上盖楼。

2.2 可视化不是“锦上添花”,而是唯一能暴露数据谎言的显微镜

去年帮一家电商公司诊断推荐系统冷启动问题,他们抱怨新用户点击率比老用户低40%。我们没碰模型代码,直接拉出新老用户的行为日志,用 seaborn.histplot 分组绘制“单日浏览商品数”分布。结果发现:老用户峰值在12-15件,符合运营预期;但新用户曲线在1-3件处出现诡异双峰,且第二峰紧贴0值。进一步下钻发现,这批“新用户”实为爬虫账号——它们注册后立即触发自动化脚本,批量访问首页、分类页、搜索页,但绝不点击商品详情。这种模式在原始日志里就是一串IP+UA+时间戳,人类肉眼根本无法识别。 可视化在这里的作用,是把高维、离散、无序的日志数据,强制映射到人类视觉系统最敏感的维度:形状、密度、位置 。没有这一步,你连问题是否存在都不知道,更别说定位。

2.3 “Python Prior”不是工具罗列,而是一套防御性工作流设计

很多教程教 pandas.read_csv 参数,却从不解释为什么 dtype 必须显式声明。我见过最惨的案例:某金融客户用默认 int64 读取身份证号,结果末尾几位数字因精度丢失全变0,导致千万级用户身份校验失败。这不是bug,是设计缺陷。真正的Prior工作流必须包含三层防御:

  • 输入层防御 :强制指定 dtype 、设置 na_values 、用 low_memory=False 避免类型自动推断错误;
  • 处理层防御 :所有清洗操作必须保留原始字段副本(如 df['age_raw'] ),新字段命名带 _clean 后缀,禁止覆盖原数据;
  • 输出层防御 :每次 df.describe() 后必须人工核对 count unique 值是否符合业务逻辑(例如“省份”字段 unique 值应为34而非340)。

这套流程不增加代码量,但能把80%的数据事故挡在建模之前。它不追求炫技,只确保每一步操作都可逆、可审计、可归因。

3. 核心细节解析:从 pd.read_csv 到可信分布图的七道生死关

3.1 第一道关:数据加载——用 dtype converters 锁死数据基因

pandas.read_csv 默认的 infer_dtype 是最大隐患源。它会把“2023-01-01”和“2023/01/01”自动识别为不同类型,导致后续 pd.to_datetime 报错;会把含千分位符的“1,234.56”误判为字符串, astype(float) 直接崩溃。正确做法是 显式声明所有字段类型

# 错误示范:依赖自动推断
df = pd.read_csv("sales.csv")  # 10万行数据,3个字段类型错乱

# 正确示范:用字典精确控制
dtypes = {
    "order_id": "string",           # 强制字符串,避免ID被转为int丢失前导零
    "amount": "float32",            # float32足够精度,内存减半
    "province": "category",         # 分类变量用category节省70%内存
    "is_vip": "boolean"             # pandas 1.5+支持原生boolean,空值自动转<NA>
}

# 对于复杂格式,用converters替代dtype
converters = {
    "create_time": lambda x: pd.to_datetime(x, format="mixed", errors="coerce")
    # format="mixed"能同时处理"2023-01-01"和"01/Jan/2023",errors="coerce"把非法值转NaT
}

df = pd.read_csv("sales.csv", dtype=dtypes, converters=converters, low_memory=False)

提示: low_memory=False 必须开启。默认 True 会让pandas分块读取并分别推断类型,再合并时类型冲突导致 DtypeWarning 。这不是警告,是灾难预告。

3.2 第二道关:缺失值归因——区分“真缺失”与“假缺失”

df.isnull().sum() 只能告诉你“有多少空”,但解决不了“为什么空”。我在医疗项目中处理过一份患者检验报告, glucose_level 字段缺失率达38%。如果直接填均值,会掩盖关键业务逻辑:门诊患者空值多因未做空腹血糖检测,住院患者空值则多因检测时间未到。 缺失值模式本身就是强特征 。必须用交叉表定位:

# 按就诊类型分组统计缺失率
missing_by_type = pd.crosstab(
    df["visit_type"], 
    df["glucose_level"].isnull(),
    rownames=["visit_type"],
    colnames=["glucose_missing"]
).apply(lambda r: r/r.sum(), axis=1)  # 转换为百分比

# 输出:
# visit_type     False   True
# outpatient    0.92   0.08   # 门诊仅8%缺失,属正常漏检
# inpatient     0.41   0.59   # 住院59%缺失,说明检测流程未完成

此时填充策略完全不同:门诊缺失值可用同年龄段均值填充;住院缺失值则需标记为 "pending_test" 类别,后续建模时作为独立特征使用。 忽略缺失归因,等于主动向模型注入噪声

3.3 第三道关:数值型分布诊断——直方图只是起点,QQ图才是真相

df["age"].hist() 能看大概,但无法判断是否符合正态分布。而很多模型(如线性回归)对残差正态性敏感。必须用QQ图(Quantile-Quantile Plot):

import scipy.stats as stats
import matplotlib.pyplot as plt

# 创建QQ图
fig, ax = plt.subplots(figsize=(8, 5))
stats.probplot(df["age"].dropna(), dist="norm", plot=ax)
ax.set_title("Age Distribution Q-Q Plot")
ax.grid(True)
plt.show()
  • 如果点基本落在红线上,说明接近正态;
  • 如果左端点下弯、右端点上弯,说明右偏(如收入数据);
  • 如果两端都下弯,说明峰度高(如考试成绩)。

我处理过一份销售数据, order_amount 直方图看着还行,但QQ图显示严重右偏。强行用线性模型预测,残差图出现明显喇叭形——这是异方差性的铁证。解决方案不是换模型,而是对 order_amount np.log1p 变换( log1p log 安全,能处理0值),变换后QQ图完美贴合直线,残差立刻均匀分布。

3.4 第四道关:分类型变量陷阱——“类别爆炸”与“长尾噪声”的双重绞杀

df["product_category"].nunique() 返回217,看似合理。但用 value_counts(normalize=True).cumsum() 看累计占比:

cat_dist = df["product_category"].value_counts(normalize=True)
cumsum_dist = cat_dist.cumsum()

# 找出覆盖95%样本的最少类别数
top_n = (cumsum_dist <= 0.95).sum()  # 结果是12
print(f"Top {top_n} categories cover 95% of data")  # Top 12 categories cover 95% of data

这意味着其余205个类别合计只占5%样本,全是噪声。若直接 pd.get_dummies ,会生成205个稀疏列,严重拖慢训练速度且引入过拟合风险。正确做法是 聚合长尾

# 保留Top 15,其余归为"Other"
top_categories = cat_dist.index[:15].tolist()
df["product_category_clean"] = df["product_category"].apply(
    lambda x: x if x in top_categories else "Other"
)

更狠的技巧:对 Other 组计算其 order_amount 均值,若显著低于Top组,则标记为 "LowValue_Other" ,反而成为有效特征。

3.5 第五道关:时间序列断点检测——用 diff() 揪出数据采集故障

时间类字段(如 event_time )的连续性是业务分析的生命线。某IoT项目中,设备上报数据突然在2023-08-15 14:22:03中断,2小时后恢复。但原始数据里只有时间戳,肉眼无法发现。用 diff() 秒级定位:

# 确保时间索引有序
df = df.sort_values("event_time").set_index("event_time")

# 计算相邻时间差(秒)
time_diffs = df.index.to_series().diff().dt.total_seconds()

# 找出间隔超300秒(5分钟)的断点
break_points = time_diffs[time_diffs > 300].index
print("Data breaks at:", break_points.tolist())
# 输出:Data breaks at: [Timestamp('2023-08-15 14:22:03'), Timestamp('2023-08-15 16:22:03')]

这类断点往往对应设备掉线、网络抖动或ETL任务失败。必须在建模前剔除断点前后各1小时数据,否则模型会学到“时间跳跃=业务激增”的虚假规律。

3.6 第六道关:相关性热力图的致命盲区——Spearman与Pearson必须并存

df.corr() 默认用Pearson,它只捕捉线性相关。但业务中大量是非线性关系:比如“用户年龄”与“APP使用时长”常呈U型(青少年和中老年用得久,中年用得少)。Pearson相关系数可能接近0,让人误判无关。必须同步计算Spearman(秩相关):

import seaborn as sns

# 计算两种相关系数
pearson_corr = df[numeric_cols].corr(method="pearson")
spearman_corr = df[numeric_cols].corr(method="spearman")

# 并排可视化
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
sns.heatmap(pearson_corr, annot=True, cmap="RdBu_r", center=0, ax=axes[0])
axes[0].set_title("Pearson Correlation (Linear)")

sns.heatmap(spearman_corr, annot=True, cmap="RdBu_r", center=0, ax=axes[1])
axes[1].set_title("Spearman Correlation (Monotonic)")

plt.show()

当Pearson值低(|r|<0.3)但Spearman值高(|r|>0.6)时,强烈提示存在非线性关系,应考虑添加二次项(如 age^2 )或分段特征。

3.7 第七道关:可视化可信度自检——三张图缺一不可

任何分析报告,必须包含以下三张图才能交付:

  • 原始分布图 histplot kdeplot ,确认数据范围与形态;
  • 缺失值矩阵图 :用 missingno.matrix(df) ,直观显示缺失模式(随机缺失?按行缺失?按列缺失?);
  • 特征-目标变量关系图 :对数值型目标用 scatterplot ,对分类型目标用 boxplot violinplot

我坚持一个原则: 如果某张图不能用一句话结论说服业务方,这张图就不该存在 。例如 violinplot 显示“VIP用户订单金额中位数比普通用户高2.3倍”,这就是有效结论;而“VIP用户分布更宽”,这种描述毫无业务价值。

4. 实操过程:以电商用户行为数据为例,完整走一遍Prior工作流

4.1 数据获取与初始探查——拒绝“一眼望尽”的幻觉

我们拿到一份脱敏的电商用户行为日志 user_behavior.csv ,共120万行,11个字段。第一步不是写代码,而是打开文件用 head -20 看前20行:

$ head -20 user_behavior.csv
user_id,event_type,product_id,category_id,brand_id,price,province,city,age,gender,timestamp
U123456,click,P789012,C345,B678,299.00,Beijing,Beijing,28,M,2023-01-01 08:23:45
U123456,buy,P789012,C345,B678,299.00,Beijing,Beijing,28,M,2023-01-01 08:25:12
...

关键发现:

  • user_id 含字母前缀,必须设为 string
  • price 有小数,但需检查是否含货币符号(此处无,安全);
  • timestamp 格式标准,可用 pd.to_datetime
  • age 为整数,但需警惕0值(可能是未填写)。

立即编写加载脚本:

dtypes = {
    "user_id": "string",
    "event_type": "category",
    "product_id": "string",
    "category_id": "category",
    "brand_id": "string",
    "price": "float32",
    "province": "category",
    "city": "string",
    "age": "Int64",  # pandas nullable integer,支持NaN
    "gender": "category",
    "timestamp": "string"  # 先读为string,再转换
}

df = pd.read_csv("user_behavior.csv", dtype=dtypes, low_memory=False)
df["timestamp"] = pd.to_datetime(df["timestamp"])

注意: "Int64" (大写I)是pandas的可空整数类型, "int64" 会把NaN转为 <NA> ,避免后续 astype(int) 报错。

4.2 缺失值深度归因——用交叉分析定位系统性问题

运行 df.isnull().sum() 发现 age 缺失12.7万行(10.6%), city 缺失8.3万行(6.9%)。粗暴填充?不。先做交叉分析:

# age缺失 vs province
age_missing_by_province = pd.crosstab(
    df["province"], 
    df["age"].isnull(),
    normalize="index"  # 按省份计算缺失率
).sort_values(by=True, ascending=False)

# 输出前5:
# province      False   True
# Tibet        0.998  0.002  # 西藏缺失率最低
# Xinjiang     0.995  0.005
# Beijing      0.892  0.108  # 北京缺失率10.8%
# Shanghai     0.871  0.129  # 上海12.9%
# Guangdong    0.783  0.217  # 广东21.7%!异常高

# 进一步查广东缺失用户的event_type分布
guangdong_missing = df[(df["province"]=="Guangdong") & (df["age"].isnull())]
print(guangdong_missing["event_type"].value_counts(normalize=True))
# buy      0.021
# click    0.979  # 97.9%是点击行为,几乎不购买

结论:广东缺失年龄的用户,极可能是爬虫或测试账号(只刷点击,不成交)。因此 age 缺失值不应填充,而应创建新特征 is_suspicious_clicker (广东+age缺失+event_type全为click)。

4.3 数值型特征可信度验证——价格分布的业务逻辑校验

price 字段直方图显示主峰在0-500元,但右尾拖得很长。用QQ图检查:

fig, ax = plt.subplots(1, 2, figsize=(12, 5))
# 左图:原始price
stats.probplot(df["price"].dropna(), dist="norm", plot=ax[0])
ax[0].set_title("Original Price Q-Q Plot")

# 右图:log1p(price)
stats.probplot(np.log1p(df["price"].dropna()), dist="norm", plot=ax[1])
ax[1].set_title("log1p(Price) Q-Q Plot")
plt.show()

左图严重右偏,右图接近直线。但业务上需验证: log1p 是否扭曲业务含义?计算 price 在各分位数的实际值:

price_q = df["price"].quantile([0.25, 0.5, 0.75, 0.9, 0.95, 0.99])
print(price_q.round(2))
# 0.25    49.00
# 0.50    99.00
# 0.75   199.00
# 0.90   399.00
# 0.95   799.00
# 0.99  1999.00

发现95%的商品价格≤799元,符合常识(手机配件、服饰为主)。但99%达1999元,暗示存在高价商品(如手机、电脑)。此时 log1p 是合理选择——它压缩了极端值影响,又保留了低价商品的区分度。

4.4 分类型特征降维——用信息增益筛选有效类别

category_id 有189个值,但业务方说核心品类只有20个。用信息增益(IG)量化每个类别的预测价值:

from sklearn.metrics import mutual_info_score

# 以event_type为预测目标(buy vs click)
target = (df["event_type"] == "buy").astype(int)

ig_scores = {}
for cat in df["category_id"].unique():
    subset = df[df["category_id"] == cat]["event_type"]
    if len(subset) > 100:  # 过滤小样本
        ig = mutual_info_score(target[df["category_id"] == cat], subset)
        ig_scores[cat] = ig

# 取IG最高的15个
top_15_cat = sorted(ig_scores.items(), key=lambda x: x[1], reverse=True)[:15]
top_cat_ids = [cat for cat, ig in top_15_cat]
print("Top 15 category_ids by info gain:", top_cat_ids)

结果中 C123 (手机)、 C456 (笔记本)稳居前二,而 C789 (宠物食品)IG极低——因其购买转化率与点击转化率几乎一致(0.12 vs 0.11),无法区分用户意图。果断将其归入 Other

4.5 时间序列健康度扫描——用滑动窗口检测数据漂移

用户行为具有强时间周期性(工作日/周末、白天/夜间)。用滑动窗口计算每小时 buy 事件占比,观察是否稳定:

# 按小时分组
df_hourly = df.set_index("timestamp").resample("H").agg({
    "user_id": "nunique",  # 去重用户数
    "event_type": lambda x: (x == "buy").mean()  # 购买转化率
}).rename(columns={"event_type": "buy_rate"})

# 计算7天滑动平均
df_hourly["buy_rate_ma7"] = df_hourly["buy_rate"].rolling(window=168, min_periods=1).mean()

# 绘制
fig, ax = plt.subplots(figsize=(12, 6))
df_hourly["buy_rate"].plot(ax=ax, alpha=0.6, label="Hourly Buy Rate")
df_hourly["buy_rate_ma7"].plot(ax=ax, linewidth=2, label="7-Day Moving Avg")
ax.set_ylabel("Buy Rate")
ax.set_title("Buy Rate Stability Check (7-Day Window)")
ax.legend()
plt.show()

图中若出现 buy_rate 持续偏离 ma7 超2个标准差,即为数据漂移信号。某次检测到连续3天周末 buy_rate 下降15%,经查是第三方支付接口升级导致部分订单未回传,及时修复避免了GMV误判。

4.6 最终交付物——一份可执行的Prior检查清单

所有分析完成后,生成标准化交付物:

  • data_profile.html :用 pandas-profiling (现为 ydata-profiling )生成的交互式报告,含缺失率、唯一值、描述统计;
  • visualization_dashboard.ipynb :含7张核心图的Jupyter Notebook,每张图配一句业务结论;
  • prior_checklist.md :文本版检查清单,供后续项目复用:
## Python Prior Checklist v1.0
- [x] 数据加载:dtype显式声明,low_memory=False,timestamp格式校验
- [x] 缺失值:按业务维度(province/event_type)交叉分析,区分真/假缺失
- [x] 数值型:QQ图验证分布,log1p等变换需业务可解释
- [x] 分类型:信息增益筛选Top N,长尾归为Other并评估其业务含义
- [x] 时间序列:滑动窗口检测漂移,断点前后数据剔除
- [x] 可视化:原始分布、缺失矩阵、特征-目标关系图三张必有

这份清单已在我们团队12个项目中复用,平均减少数据相关返工时间67%。

5. 常见问题与排查技巧实录——那些文档里不会写的坑

5.1 问题: pd.read_csv MemoryError ,10GB文件打不开

现象 df = pd.read_csv("big_data.csv") 直接崩溃,内存占用飙升至95%。
根因 :pandas默认将所有字符串读为 object 类型,每个字符串对象额外占用40+字节内存。
解法

  1. chunksize 分块读取,逐块处理:
chunks = []
for chunk in pd.read_csv("big_data.csv", chunksize=50000):
    # 对每块做清洗
    chunk = clean_chunk(chunk)
    chunks.append(chunk)
df = pd.concat(chunks, ignore_index=True)
  1. 更优方案:用 dask.dataframe 替代pandas:
import dask.dataframe as dd
df = dd.read_csv("big_data.csv", dtype=dtypes)  # 支持lazy evaluation
result = df.groupby("province")["price"].mean().compute()  # 仅在需要时计算

实测:12GB日志文件,pandas耗时48分钟+内存溢出;dask耗时11分钟,内存稳定在3.2GB。

5.2 问题: seaborn.boxplot 显示异常值过多,但业务说“都是正常数据”

现象 :箱线图里70%的点被标为异常值(outlier),显然算法误判。
根因 :箱线图默认用 Q1-1.5*IQR Q3+1.5*IQR 判定异常,但对右偏分布(如价格)过于敏感。
解法

  • 改用 violinplot 看整体分布形态;
  • 或调整异常值阈值:
# 自定义异常值范围:用Q1-3*IQR和Q3+3*IQR(更宽松)
q1 = df["price"].quantile(0.25)
q3 = df["price"].quantile(0.75)
iqr = q3 - q1
lower_bound = q1 - 3 * iqr
upper_bound = q3 + 3 * iqr
df["is_outlier"] = (df["price"] < lower_bound) | (df["price"] > upper_bound)

经验:电商价格数据, 3*IQR 1.5*IQR 更符合业务实际。

5.3 问题: pd.to_datetime OutOfBoundsDatetime ,时间超出范围

现象 pd.to_datetime(df["date_str"]) 报错 Out of bounds nanosecond timestamp
根因 :pandas datetime64[ns]支持范围为1677-2262年,但数据中混有 0000-00-00 9999-99-99 等非法值。
解法

# 用errors="coerce"转为NaT,再用fillna填充合理默认值
df["date_clean"] = pd.to_datetime(df["date_str"], errors="coerce")
# 将NaT替换为业务默认时间(如数据采集起始日)
default_date = pd.Timestamp("2020-01-01")
df["date_clean"] = df["date_clean"].fillna(default_date)

注意: fillna 必须在 to_datetime 之后,否则 fillna("2020-01-01") 会把字符串填进去,破坏datetime类型。

5.4 问题: value_counts() 返回的类别顺序混乱,影响图表可读性

现象 sns.countplot(x="province") 显示省份顺序是随机的,北京不在开头。
解法

  • 方法1:用 category 类型并设置顺序:
provinces_ordered = ["Beijing", "Shanghai", "Guangdong", "Zhejiang", ...]
df["province"] = pd.Categorical(df["province"], categories=provinces_ordered, ordered=True)
  • 方法2: countplot 中用 order 参数:
sns.countplot(data=df, x="province", order=df["province"].value_counts().index)

推荐方法1,因为 Categorical 在内存和计算上都更高效。

5.5 问题: missingno.matrix 图中缺失模式不明显,看不出规律

现象 :缺失矩阵图一片灰,无法分辨缺失是随机还是成块。
解法

  • 启用 sparkline=False 关闭右侧小图,专注主矩阵;
  • filter 参数聚焦关键字段:
import missingno as msno
msno.matrix(
    df[["age", "city", "province", "price", "timestamp"]], 
    sparkline=False,
    figsize=(10, 5)
)
  • 更强技巧:用 msno.heatmap(df) 看字段间缺失相关性——若 age city 缺失高度相关(相关系数0.92),说明是同一数据源问题。

5.6 问题: sns.scatterplot 点太多,图变成一块黑斑

现象 :100万用户 age vs price 散点图,密密麻麻全是黑点。
解法

  • alpha 降低透明度: sns.scatterplot(..., alpha=0.05)
  • 改用 hexbin 图:
plt.hexbin(df["age"], df["price"], gridsize=30, cmap="Blues")
plt.colorbar(label="Count")
  • 或用2D直方图:
plt.hist2d(df["age"], df["price"], bins=[30, 50], cmap="Blues")
plt.colorbar(label="Frequency")

实测: hexbin 在百万级数据下渲染速度比 scatterplot 快12倍,且能清晰显示密度中心。

5.7 问题: df.corr() 结果中出现 inf nan ,无法解读

现象 :相关系数矩阵里某些格子是 inf
根因 :某字段标准差为0(所有值相同),导致除零。
解法

# 计算前先过滤标准差为0的列
std_cols = df[numeric_cols].std()
valid_cols = std_cols[std_cols > 0].index.tolist()
corr_matrix = df[valid_cols].corr()

提示: std() 返回 Series std_cols > 0 生成布尔索引,比 df[numeric_cols].nunique() > 1 更准确(能处理浮点精度问题)。

6. 实操心得:那些让我少熬200小时夜的硬核技巧

6.1 技巧1:用 df.memory_usage(deep=True).sum() 实时监控内存,比 top 命令更准

pandas的 memory_usage(deep=True) 会计算字符串内容实际内存,而 deep=False 只算指针。我曾用此发现一个隐藏炸弹: city 字段有10万唯一值,每个字符串平均长12字符, deep=True 显示占内存1.8GB,而 deep=False 只报24MB。果断改用 category 类型,内存直降1.7GB。 监控内存不是为了炫技,而是预判下一步操作是否可行 ——比如知道剩余内存仅2GB,就不会尝试 df.merge 一个5GB的表。

6.2 技巧2: query() 方法比布尔索引快3倍,且代码可读性爆炸提升

# 慢且难读
mask = (df["age"] >= 18) & (df["age"] <= 65) & (df["province"] == "Beijing") & (df["price"] > 0)
result = df[mask]

# 快且像说话
result = df.query("18 <= age <= 65 and province == 'Beijing' and price > 0")

query() 底层用 numexpr 引擎,对大表加速明显。更重要的是,业务方能直接看懂代码逻辑,减少沟通成本。

6.3 技巧3: pd.cut() pd.qcut() 不是选哪个,而是按场景组合用

  • pd.cut(x, bins=5) :等宽分箱(如年龄分0-20、20-40...),适合物理量;
  • pd.qcut(x, q=5) :等频分箱(每箱样本数相等),适合偏态分布;
  • 组合技 :先 qcut 分5组,再对每组 cut 细分:
# 先按价格分5等频组
df["price_group"] = pd.qcut(df["price"], q=5, labels=False, duplicates="drop")

# 再对高价组(group=4)细分为3档
high_price_mask = df["price_group"] == 4
df.loc[high_price_mask, "price_subgroup"] = pd.cut(
    df.loc[high_price_mask, "price"], 
    bins=3, 
    labels=["High", "Higher", "Highest"]
)

这样既保证了各档业务意义明确,又避免了低价商品被过度切分。

6.4 技巧4: sns.pairplot() 不是万能钥匙,超10个数值字段必须降维

pairplot 画10个字段会生成100张小图,根本没法看。正确姿势:

  • 先用 df.corr().abs().unstack().sort_values(ascending=False) 找Top 5强相关对;
  • 再用 PCA 降维到2D,画 scatterplot
from sklearn.decomposition import PCA
p
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐