写在前面
上一篇讲怎么保证数据干净,这一篇讲干净之后怎么「看出东西」。
汇总表只回答「多少」,回答不了「为什么」和「什么和什么有关」。而分析报告的价值,往往就在后半句:哪些指标同向变化、哪类组合表现最好、哪个群体明显偏离。
这篇用四个工具回答三类问题:corr 看数值关系、crosstab 看分类交叉、groupby 看分组差异、热力图看整体格局。
先确认示例数据在手边:
python
import os
import mkdata
if not os.path.exists("data/sales.csv"):
mkdata.main()
print("示例数据就绪:", os.path.exists("data/sales.csv"))
这段会检查第 1 篇生成的 data/ 目录在不在,不在就自动补一次;已经有数据时只会打印一行 示例数据就绪: True,不影响后面的结果。
一、corr:两个数值列是同向还是反向
corr() 默认算 Pearson 相关系数,取值 −1 到 1:接近 1 同向、接近 −1 反向、接近 0 没关系。
python
import pandas as pd
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
# 只保留数值列(文本列算不了相关)
num = df.select_dtypes(include="number")
print("数值列:", num.columns.tolist())
print("----")
print("Pearson 相关系数:")
print(num.corr().round(3).to_string())
print("----")
print("Spearman(秩相关,抗极端值):")
print(num.corr(method="spearman").round(3).to_string())
text
数值列: ['数量', '单价', '金额']
----
Pearson 相关系数:
数量 单价 金额
数量 1.000 -0.039 0.245
单价 -0.039 1.000 0.448
金额 0.245 0.448 1.000
----
Spearman(秩相关,抗极端值):
数量 单价 金额
数量 1.000 -0.06 0.495
单价 -0.060 1.00 0.420
金额 0.495 0.42 1.000
两个参数要会用:
method="spearman":按排名算相关,不受极端值影响。数据里有-599这种脏值时,Spearman 比 Pearson 稳;min_periods:参与计算的最小样本数,小样本别硬算。
corrwith 更省事------一张表对另一列算相关:
python
import pandas as pd
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
num = df.select_dtypes(include="number")
print("各列与金额的相关性:")
print(num.corrwith(num["金额"]).round(3).sort_values(ascending=False).to_string())
text
各列与金额的相关性:
金额 1.000
单价 0.448
数量 0.245
二、crosstab:分类字段的交叉表
两张分类表交叉,看组合分布,crosstab 比 groupby 更直观。
python
import pandas as pd
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
# 城市 × 产品:每个组合有多少单
ct = pd.crosstab(df["城市"], df["产品"], margins=True, margins_name="合计")
print("订单数交叉表:")
print(ct.to_string())
print("----")
# normalize="index":每行归一化,看每个城市内部的产品结构
ct_pct = pd.crosstab(df["城市"], df["产品"], normalize="index").round(3)
print("按城市看产品结构(行占比):")
print(ct_pct.to_string())
text
订单数交叉表:
产品 A-笔记本 B-鼠标 C-键盘 D-显示器 合计
城市
上海 0 2 2 0 4
北京 1 0 1 1 3
广州 0 2 1 0 3
杭州 2 1 0 2 5
合计 3 5 4 3 15
----
按城市看产品结构(行占比):
产品 A-笔记本 B-鼠标 C-键盘 D-显示器
城市
上海 0.000 0.500 0.500 0.000
北京 0.333 0.000 0.333 0.333
广州 0.000 0.667 0.333 0.000
杭州 0.400 0.200 0.000 0.400
normalize 的三个取值很好记:"index" 按行、"columns" 按列、"all" 按全表。分析结构差异时用 index,比较不同城市的偏好正合适。
三、groupby 分组对比:把「差异」量化
分组对比的关键不是算均值,而是同时给几个指标 + 排序 + 算占比,让差异一眼可见。
python
import pandas as pd
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
df["金额"] = df["金额"].fillna((df["数量"] * df["单价"]).round(2))
clean = df[(df["金额"] > 0)].drop_duplicates()
g = (
clean.groupby("城市", dropna=False)
.agg(订单数=("金额", "size"), 销量=("数量", "sum"),
销售额=("金额", "sum"), 客单价=("金额", "mean"))
.round(2)
)
g["销售额占比"] = (g["销售额"] / g["销售额"].sum() * 100).round(1)
g = g.sort_values("销售额", ascending=False)
print(g.to_string())
print("----")
print("最高与最低城市销售额差:", round(g["销售额"].max() - g["销售额"].min(), 2), "元")
text
订单数 销量 销售额 客单价 销售额占比
城市
杭州 4 14 3404.1 851.02 54.1
北京 3 5 1425.9 475.30 22.7
上海 2 12 772.5 386.25 12.3
广州 2 10 535.4 267.70 8.5
NaN 1 3 149.7 149.70 2.4
----
最高与最低城市销售额差: 3254.4 元
一个实用习惯:分组结果一定加占比列。「杭州 6399 元」没人有感觉,「杭州占 67.6%」立刻就有结论了。
四、pd.cut 分段对比:连续变量怎么分组
金额、年龄、时长这类连续值,先分段再对比,比直接看原始值有洞察。
python
import numpy as np
import pandas as pd
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
df["金额"] = df["金额"].fillna((df["数量"] * df["单价"]).round(2))
clean = df[df["金额"] > 0].drop_duplicates()
clean["价格段"] = pd.cut(
clean["单价"], bins=[0, 50, 100, 600], labels=["低价(<50)", "中价(50-100)", "高价(>100)"]
)
seg = clean.groupby("价格段", observed=True).agg(
订单数=("金额", "size"), 销售额=("金额", "sum"), 客单价=("金额", "mean")
).round(2)
print(seg.to_string())
print("----")
print("各价格段订单占比:")
print((clean["价格段"].value_counts(normalize=True).sort_index() * 100).round(1).to_string())
text
订单数 销售额 客单价
价格段
低价(<50) 7 937.6 133.94
中价(50-100) 3 1157.0 385.67
高价(>100) 2 4193.0 2096.50
----
各价格段订单占比:
价格段
低价(<50) 58.3
中价(50-100) 25.0
高价(>100) 16.7
pd.cut 是按你给的边界 切(业务含义明确);pd.qcut 是按分位数切(每段样本数尽量相等)。前者适合汇报,后者适合建模。
五、画出来:相关系数热力图
数字看着不直观,画成热力图,哪里强哪里弱一眼就有感觉。

图 1:相关系数热力图。数量与金额 0.24、单价与金额 0.45------都不算强,说明销售额更多由「单价高」而不是「买得多」驱动。
python
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
df = pd.read_csv("data/sales.csv", encoding="utf-8-sig")
num = df.select_dtypes(include="number")
corr = num.corr().round(2)
fig, ax = plt.subplots(figsize=(6.2, 5))
im = ax.imshow(corr, cmap="RdBu_r", vmin=-1, vmax=1)
ax.set_xticks(range(len(corr)))
ax.set_yticks(range(len(corr)))
ax.set_xticklabels(corr.columns, rotation=30)
ax.set_yticklabels(corr.index)
for i in range(len(corr)):
for j in range(len(corr)):
v = corr.iloc[i, j]
ax.text(j, i, f"{v:.2f}", ha="center", va="center", fontsize=10,
color="white" if abs(v) > 0.6 else "#1f2937")
ax.set_title("数值列相关系数热力图", fontsize=13, pad=12)
fig.colorbar(im, ax=ax, shrink=0.85, label="Pearson 相关系数")
fig.tight_layout()
os.makedirs("output", exist_ok=True)
fig.savefig("output/corr_heatmap.png", dpi=200)
plt.close(fig)
print("已保存 output/corr_heatmap.png")
print("相关系数最大的一对:", corr.where(~np.eye(len(corr), dtype=bool)).stack().idxmax())
text
已保存 output/corr_heatmap.png
相关系数最大的一对: ('单价', '金额')
六、相关不等于因果:一个反例
假设你发现「冰淇淋销量」和「溺水人数」强相关(r≈0.9)。结论不是「吃冰淇淋导致溺水」,而是两者都受第三个因素------气温影响。
分析报告里最容易翻车的就是这一句。三个自检问题:
- 有没有第三个变量同时影响两者(混淆变量)?
- 因果关系方向对吗(A 导致 B,还是 B 导致 A)?
- 样本量够吗?小样本的高相关很容易是巧合。
常见问题
Q1:corr() 报 could not convert string to float。
表里有文本列。先 df.select_dtypes(include="number") 过滤;如果某个「数字列」其实是文本(带单位、空格),先 pd.to_numeric(errors="coerce")。
Q2:crosstab 结果里少了 NaN 那一行。
crosstab 默认丢弃缺失。想保留:pd.crosstab(df["城市"].fillna("未知"), df["产品"], dropna=False)。
Q3:热力图上中文全是方块。
没设中文字体。plt.rcParams["font.sans-serif"] = ["SimHei"],Mac 换 PingFang SC,并且要写在画图之前------这和第 11 篇是同一个坑。
小结
这一篇的四个工具各管一段:corr 管数值关系、crosstab 管分类交叉、groupby + 占比 管分组差异、pd.cut 管连续值分段。把它们组合起来,一张表就能回答「谁和谁有关、哪类最好、差多少」。但结论出口前一定要过一遍「相关不等于因果」的三问------这比任何代码都重要。
下一篇进入第二批的实战篇:用一份模拟数据,把用户行为分析从指标定义做到结论输出。