1. 先搞明白:Python统计不是"高级计算器",而是一套流程
如果你问一个做了几年数据分析的人,Python 数据统计和 Excel 到底差在哪,他大概率会甩给你一句话:Excel 把统计做成了手工活,Python 把统计做成了流水线。我刚开始学的时候也不信,觉得一个平均值、一个图表,Excel 点两下就出来了,何必写代码?直到我真正拿到一份十几万行的业务数据,要按城市、按时段、按用户分层反复切分组做对比,还要保证每一步别人都能复核,Excel 那套操作就彻底撑不住了。
Python 做数据统计,真正值钱的不是"算得快",而是"整套流程可复现、可审计、可扩展"。同一个脚本,这个月跑一遍,下个月换个数据文件再跑一遍,逻辑完全一致;十个人拿同一份数据能得出同一个结果,而不是有人筛错了行、有人看错了列。这篇内容我打算从零开始,把统计分析的完整链路走一遍:环境准备、数据加载与清洗、描述性统计、推断统计、相关与回归,最后用一个业务场景把前面的东西全部串起来。适合刚接触 Python、想用数据分析解决实际问题的人,也适合那些已经会用 Pandas 但面对"统计"两个字依然发怵的同学。
很多人学了 NumPy 学 Pandas,学了 Pandas 又去学可视化,结果越来越迷茫,因为每一步都是孤立知识点,不知道怎么串成一条分析线。问题出在缺一个坐标系:你学的东西分别放在统计流程的哪个环节?我们先把这条流程画出来,再往每个环节里填工具。
统计思维的主干其实特别简单:先提出业务问题,再收集数据,然后清洗数据,接着描述数据长什么样,再根据样本推断总体,最后建模找关系。落到 Python 生态里,每个环节都有专门的库:
| 流程环节 | 主要工具 | 定位 |
|---|---|---|
| 数值计算 | NumPy | 底层数组运算,几乎所有库的基石 |
| 表格处理 | Pandas | 加载、清洗、切片、分组,日常的最主要战场 |
| 统计函数与检验 | SciPy | 假设检验、分布函数、各种统计量的计算 |
| 统计建模 | statsmodels | 回归、时间序列、统计推断,比 Scikit-learn 更侧重"解释" |
| 可视化 | Matplotlib / Seaborn | 画直方图、箱线图、相关系数矩阵,靠"眼睛"验证思路 |
现在的安装很简单,一个虚拟环境里一次性装齐:
python -m venv stats-env source stats-env/bin/activate # Windows 下用 stats-env\Scripts\activate pip install numpy pandas scipy statsmodels matplotlib seaborn很多新手在安装这一步就翻车,十有八九是国内网络导致的下载超时,或者某依赖版本冲突。遇到超时就用清华或阿里云的镜像源,比如pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple。至于版本冲突,尽量不要图省事一把梭乱装,虚拟环境隔离好,基本不会有大问题。装完跑一句python -c "import pandas; print(pandas.__version__)",看到版本号说明环境通了,可以开始干活。
2. 造一份"脏数据":加载与准备阶段的真实细节
真实项目里的数据永远不是教学文档里那种干干净净的表。我印象很深的一次,某公司给的订单表里有三分之一的金额字段是字符串,日期格式混了三种,还有人把"未知"写进了性别列。当时的感觉就是:统计根本还没开始,人先疯了。所以这一节我们先主动把数据搞脏,再一步步处理,模拟真实场景。
为了后面所有章节都能复现,我用一段代码生成一份模拟数据,假装是某个电商平台某天的用户访问记录:
import numpy as np import pandas as pd np.random.seed(42) n = 500 df = pd.DataFrame({ "user_id": np.arange(1, n + 1), "city": np.random.choice(["北京", "上海", "广州", "成都", "杭州"], n, p=[0.3, 0.25, 0.2, 0.15, 0.1]), "page_version": np.random.choice(["old", "new"], n), "visit_duration": np.round(np.random.gamma(shape=6, scale=18, size=n), 1), "pages_viewed": np.random.poisson(lam=4, size=n).astype(int), "converted": np.random.binomial(n=1, p=0.12, size=n), }) # 人为制造脏数据:缺失、异常值、类型问题 df.loc[3, "visit_duration"] = np.nan df.loc[120, "visit_duration"] = 3500.0 df.loc[200, "pages_viewed"] = "9" print(df.head())这个数据集有 500 条记录,字段包括用户 ID、城市、访问的页面版本(old/new)、访问时长、浏览页面数、是否转化。np.random.seed(42)很重要,保证每次跑出来都一样,你的结果和我文章里写的能对上。后面我会故意留一个 NaN、一个 3500 秒的超常时长异常值,还有一个字符串数字混在整数列里,都是为了还原真实清洗场景。
加载数据这一步,很多人只会写一句pd.read_csv("data.csv"),然后就在字段类型上吃了暗亏。我平时至少有四个参数是高频使用的:
df = pd.read_csv( "data.csv", encoding="utf-8-sig", # 避免 Excel 导出的 GBK 乱码 dtype={"user_id": str}, # 防止长 ID 被读成科学计数法 parse_dates=["date"], # 指定日期列 na_values=["", "NA", "unknown"] # 统一缺失值标记 )encoding这个点特别想强调:很多中文数据来自 Excel,编码是 GBK,直接读会乱码,用utf-8-sig可以兼顾 UTF-8 和带 BOM 的格式。而 ID 这种看起来是数字的字段,一旦超过一定长度就会被读成浮点数,后面 join 的时候才发现对不上,所以干脆在读取时就按字符串处理。
读进来之后的第一件事是"体检",不是直接算统计量。我会固定跑这几行:
print(df.shape) # 有几行几列 print(df.dtypes) # 每列的类型,一眼看出类型异常 print(df.info()) # 列名、非空数量、内存占用 print(df.describe()) # 数值列的统计摘要 print(df.isnull().sum()) # 每列缺失个数从 dtypes 就能看到,pages_viewed那一列因为混入字符串 "9" 被读成了 object;visit_duration有缺失;converted虽然看起来像布尔值,但被存成了 int64。体检的目的就是把这种"表面正常、内核有坑"的问题暴露出来,而不是等统计时得到奇怪的报错才回头查。
3. 统计前 90% 的工作:缺失值与异常值处理
有人统计完发现均值高得离谱,第一反应是怀疑公式算错了,其实很可能是数据里躺着一个 3500 秒的访问时长。这种极端值把平均值直接拉高,于是分布真相被一个异常点掩盖了。数据清洗这部分,是很多教程最不爱写、但实际项目里最花时间的内容。
3.1 缺失值:先判断机制,再决定策略
缺失值不是见一个删一个。在动手之前,我需要判断缺失是怎么产生的。统计上一般把缺失机制分三类:完全随机缺失(MCAR),比如系统抽风随机丢了一条记录;随机缺失(MAR),比如男性用户更倾向不填收入字段;非随机缺失(MNAR),比如收入越高的人越不愿意填收入。后两类如果直接删除,就会引入偏差,本来想做一个无偏估计,结果样本变了味。
实操层面,如果缺失比例小于 5%,且确认是随机的,直接扔掉问题不大;超过 10% 就要认真对待了。我处理visit_duration的方式是看分布,如果偏态严重就用中位数填充,比均值更抗异常值干扰:
duration_median = df["visit_duration"].median() df["visit_duration"] = df["visit_duration"].fillna(duration_median)提示:用均值填充会压缩整体方差,导致后续统计检验更容易出现"显著假象"。用中位数填充、用前向填充、还是用模型预测填充,本质都是你给缺失值赋予了一个"合理的猜测",没有绝对正确,只有符不符合业务逻辑。
3.2 异常值:不是所有偏大数都要删
3500 秒的访问时长是明显的异常值。但这个数可能是真实存在的吗?可能是一个用户挂机了,也可能数据采集 bug。处理异常值不能只看分布,要先问业务:这个值本身是否合法。如果合法,它是重要信息;如果不合法,才需要处理。对于时长这类右偏严重的变量,常见做法是把极端值截尾(winsorize),也就是把超过 P99 的值压到 P99 的取值上,而不是粗暴删除。
我用 IQR 方法定位异常值时是这样写的:
q1 = df["visit_duration"].quantile(0.25) q3 = df["visit_duration"].quantile(0.75) iqr = q3 - q1 condition = (df["visit_duration"] < q1 - 1.5 * iqr) | (df["visit_duration"] > q3 + 1.5 * iqr) print(df.loc[condition, "visit_duration"])这个规则的逻辑是假设正常数据集中分布在中间 50% 区域,超出上下边界太多就认为可疑。它的问题在于对样本量小或分布本身偏态的数据过于敏感,所以更稳妥的做法是把 IQR 当成"初筛"工具,筛出来逐条看业务含义。我的固定动作是:先生成可疑名单,再找业务方确认,最后决定删除、截尾还是保留。尤其是金额、时长这类字段,异常往往是最有价值的信号。
还有一个很容易翻车的地方:pages_viewed里的字符串"9"。如果直接跑统计,Pandas 会报错,或者静默跳过。这种问题的解法是先转类型、再处理失败值:
df["pages_viewed"] = pd.to_numeric(df["pages_viewed"], errors="coerce") df["pages_viewed"] = df["pages_viewed"].fillna(df["pages_viewed"].median())errors="coerce"的意思是把不能转成数字的统统变成 NaN,然后再走缺失值填充。这一步就是处理"数据类型不干净"的标准姿势。
4. 描述性统计:用数字说清一组数据的长相
清洗完毕,才进入许多人理解的"统计":算描述性统计量。这里的目标是把数据的集中趋势、离散程度、分布形态用数字归纳出来。怎么看这几个数字,其实比怎么算更重要。
4.1 集中趋势:均值、中位数、众数的使用时机
对对称分布来说,均值是很好的代表;对偏态分布,均值会被极端值拉偏,中位数更稳健。最经典的例子是收入:某公司员工平均年薪 30 万,听起来很高,但仔细看 90% 的人年薪不到 10 万,剩下 10% 的高管把均值拉起来了。这时候说"中位年薪 7 万"才更贴近普通人的感受。
在 Python 里几个值的计算非常直白:
mean_val = df["visit_duration"].mean() median_val = df["visit_duration"].median() mode_val = df["city"].mode()[0] q25 = df["visit_duration"].quantile(0.25) q75 = df["visit_duration"].quantile(0.75)我还特别建议看四分位数。P25、P50、P75 三个点基本能描述数据的分布骨架,比单独的均值加标准差更直观。describe()默认已经给出了这四个核心值(25% 、50%、75% 、max),很多新手忽略了这个现成的信息源。
4.2 离散程度与分布形态:答案是右侧长尾
方差和标准差衡量数据"有多散",但光看数字不够,我还会算偏度和峰度来确认形态:
skewness = df["visit_duration"].skew() kurtosis = df["visit_duration"].kurtosis() print("偏度:", skewness, "峰度:", kurtosis)偏度大于 0 代表右偏,意味着少数极大值把尾巴拖到了右边;峰度描述的是分布"尖"还是"平",相比标准正态,峰度大于 0 一般说明有重尾。对visit_duration这种访时长的数据,偏度和峰度通常显著大于 0,这提醒我们后续做 t 检验时不能无脑套正态假设,要么用大样本近似,要么考虑对数变换。
这里有一个特别多初学者踩的坑:Pandas 的std()默认计算的是样本标准差(分母是 n-1),而 NumPy 的std()默认是总体标准差(分母是 n)。同一个数组,两个函数结果不一样。刚入门时我用np.std(x)跟文档对答案,怎么都对不上,后来才发现是自由度的问题。在用统计学公式核对结果之前,先想清楚你调用的是样本口径还是总体口径。
4.3 分组对比:统计的日常主战场
描述统计不会只看整体,通常要按维度切组。比如对比新版页面和旧版页面的用户时长差异:
grouped = df.groupby("page_version")["visit_duration"].agg(["count", "mean", "median", "std"]) print(grouped)groupby之后接agg,可以一次算出多列,非常高效。如果维度多,还可以换成pivot_table:
pivot = pd.pivot_table( df, values="visit_duration", index="city", columns="page_version", aggfunc="mean" ) print(pivot)分组统计的价值在于"对比"。一个转化率 12% 放在那里没有意义,比一比旧版是多少、新版是多少、各城市差异多大,才能看出问题。也正是从分组对比开始,描述性统计走向推断性统计——组间的差异,是真的存在,还是抽样波动造成的偶然?
5. 从样本推断总体:置信区间与假设检验
拿到一组数据,算了均值,下一步就面临一个灵魂问题:这个均值可靠吗?你手里只有 500 条样本,真正关心的是平台上所有用户的总体情况。这就要靠推断统计,用量很少的样本反推总体的结论,并告诉别人这个结论有多不确定。
5.1 置信区间:给估计值一个"可信范围"
中心极限定理说,当样本量足够大时(通常 n 大于 30),样本均值的抽样分布近似正态。基于这个原理,我可以算出总体均值的 95% 置信区间。手工公式是"样本均值 ± 1.96 × 标准误",标准误等于样本标准差除以根号 n。用代码一行就搞定:
from scipy import stats n = len(df["visit_duration"]) mean_val = df["visit_duration"].mean() se = df["visit_duration"].std(ddof=1) / np.sqrt(n) ci = stats.t.interval(0.95, df=n - 1, loc=mean_val, scale=se) print("95% 置信区间:", ci)这里用的是 t 分布不是正态分布,因为样本量 500 其实已经接近正态,但为了严谨还是用t.interval更规范。置信区间比点估计靠谱得多:如果某个运营方案把用户时长提升了 10 秒,但置信区间是[-5, 25],跨度包含 0,那你说不清是真提升还是运气;如果区间是[8, 12],那结论就稳很多。
5.2 假设检验的完整流程:一种可以复用的套路
很多教材把假设检验写得很玄,其实做项目的步骤是固定的:先提原假设和备择假设,再选检验方法,然后看 p 值是否小于显著性水平(通常 0.05),最后下结论。我习惯把显著性水平取 0.05 理解为"我愿意承担 5% 的错误风险说'有差异',但实际没有差异"。
落实到代码里,最常见的是 t 检验,分三种场景:
| 检验类型 | 适用场景 | SciPy 函数 |
|---|---|---|
| 单样本 t 检验 | 判断样本均值是否等于某特定值 | scipy.stats.ttest_1samp |
| 独立样本 t 检验 | 比较两组独立数据均值 | scipy.stats.ttest_ind |
| 配对样本 t 检验 | 比较同一组对象前后两次结果 | scipy.stats.ttest_rel |
在我们的数据里,新版和旧版是两个独立分组,所以用ttest_ind。写代码之前要注意一个前提:t 检验对异常值敏感,前面已经做了清洗,这样才放心。另外,两组方差可能不相等,我通常加上equal_var=False,也就是 Welch 检验,更稳健:
new_duration = df.loc[df["page_version"] == "new", "visit_duration"] old_duration = df.loc[df["page_version"] == "old", "visit_duration"] t_stat, p_value = stats.ttest_ind(new_duration, old_duration, equal_var=False) print("t 统计量:", t_stat, "p 值:", p_value)p 值如果小于 0.05,说明新版和旧版的访问时长差异不太可能只是随机波动,这时才敢说"新页面的时长显著高于旧页面"。但"显著"不等于"幅度大"——p 值受样本量影响很大,样本多的时候 0.5 秒的差异也能显著。所以看结果的时候我会把效应量也关注一下,用 Cohens d 之类的方式量化差异大小,很多分析报告恰恰把这一步漏掉了。
5.3 分类变量的独立性检验:卡方检验
如果两个变量都是类别型,比如"页面版本"和"是否转化",要判断它们是否独立,就用卡方检验。它的逻辑是:先假设两变量独立,算出每个格子"期望频数",再看实际频数和期望频数差多远。
contingency_table = pd.crosstab(df["page_version"], df["converted"]) print(contingency_table) chi2, p_value, dof, expected = stats.chi2_contingency(contingency_table) print("卡方值:", chi2, "p 值:", p_value)需要注意卡方检验对单元格期望频数有要求,一般期望值不能小于 5,如果数据太稀疏,结果不够可信。对这张 2×2 列联表来说,样本量够大,可以正常解读。
关于 p 值,一个必须纠正的误解是:"p 值小于 0.05 说明原假设为假的概率小于 5%"是错的。p 值的真正含义是:在"原假设为真"这个前提下,看到当前或更极端数据的概率。它是个条件概率,不是"假设为假的概率"。有了这层认知,才不容易被"显著"两个字忽悠。
6. 从关联到预测:相关分析与回归建模
统计做到推断阶段,已经能回答"有没有差异"。但业务往往得寸进尺:既然访问时长和转化率都有差异,那它们之间有什么关系?能不能用一个字段预测另一个?这就是相关与回归的舞台。
6.1 相关系数:先弄清楚你该用哪一种
相关分析看两个连续变量的线性关联强度。最常用的是 Pearson 相关系数,但它的前提是两变量都近似正态且关系是线性的。很多新手不看前提,直接df.corr()一把梭,遇到非线性关系就得出一个接近 0 的错误结论。此时可以考虑 Spearman 秩相关,它不要求正态,只关心秩次上的单调关系。
| 相关系数 | 前提条件 | 适合场景 |
|---|---|---|
| Pearson | 连续变量、线性关系、近似正态 | 身高与体重、时长与浏览页数等 |
| Spearman | 连续/有序变量、单调关系 | 排名数据、偏态明显的数据 |
实际代码里可以对比看:
pearson = df["visit_duration"].corr(df["pages_viewed"], method="pearson") spearman = df["visit_duration"].corr(df["pages_viewed"], method="spearman") print("Pearson:", pearson, "Spearman:", spearman)如果两个值差距很大,说明线性关系不好,或者有极端值干扰,先回去检查散点图。相关系数的计算很简单,难的是解释:相关不等于因果。冰淇淋销量和溺水人数高度正相关,是因为夏天这个共同原因在背后起着作用,而不是吃冰淇淋导致溺水。业务分析里,看到相关关系只是线索,因果推断还需要实验设计或更高级的因果推断方法。
6.2 线性回归:从相关关系走向解释模型
回归模型的价值在于,它能帮你同时控制多个变量,看你关心的那个变量到底有多大影响。比如想看访问时长和浏览页面数的关系,顺便把城市差异一起考虑进来:
import statsmodels.api as sm X = df[["visit_duration", "city"]] X = pd.get_dummies(X, columns=["city"], drop_first=True) X = sm.add_constant(X) y = df["pages_viewed"] model = sm.OLS(y, X).fit() print(model.summary())pd.get_dummies把城市这种类别变量变成哑变量,drop_first=True是为了避免完全共线性。sm.add_constant是加截距项,新手最容易漏。看回归结果时我按顺序看四件事:整体显著性 F 检验、R 方、每个系数的 p 值、以及系数符号和业务逻辑对不对得上。R 方太低说明模型解释力不怎么样,但不代表变量没关系,可能只是非线性或缺了关键变量;系数 p 值大于 0.05 说明控制其他变量后,这个变量的独立贡献不够显著。
建模做完一定要检查残差。残差是预测值和真实值的差,理想情况下应该随机分布在 0 附近,且没有明显的模式。更严格的正规做法会画残差图、做正态性检验,不过在入门场景下,至少做到"看一眼散点图别出现漏斗形"。
7. 完整业务案例:从原始表到分析结论的闭环
前面讲了很多零件,这一节我用一个完整的业务问题把它们组装起来。假设某电商平台改版了落地页,想判断新版页面是否真的比旧版强。手里就是上面生成的那份数据:500 个用户,字段有城市、版本、访问时长、浏览页面数、是否转化。
7.1 先定指标,再跑代码
业务问题最怕一上来就画图、跑模型,先要定义清楚"新版比旧版强"体现在哪。我们选了三个可量化指标:转化率是否提高、平均访问时长是否增加、平均浏览页面数是否增加。定义清楚了,分析才有靶子。
7.2 第一步:清洗数据,建立分析基线
我先沿着第三节的流程把数据洗干净:中位数填充visit_duration的缺失值、处理掉 3500 秒的异常值、把pages_viewed的类型转正。这一步是后续所有结论的地基。为了保持数据完整性,异常值不删,而是用 P99 做截尾处理:
p99 = df["visit_duration"].quantile(0.99) df["visit_duration"] = df["visit_duration"].clip(upper=p99) df["pages_viewed"] = pd.to_numeric(df["pages_viewed"], errors="coerce") df["pages_viewed"] = df["pages_viewed"].fillna(df["pages_viewed"].median())7.3 第二步:描述性统计看差异方向
分版本做一次分组统计,先看表面差异方向:
result = df.groupby("page_version").agg( users=("user_id", "count"), conv_rate=("converted", "mean"), avg_duration=("visit_duration", "mean"), avg_pages=("pages_viewed", "mean") ).round(3) print(result)输出大概会是这样:旧版和新版用户数相当,新版转化率略高,平均访问时长也略长。但注意,这只是样本现象,不能拍板说结论。样本里看到的差异可能来自抽样运气,下一步要做假设检验。
7.4 第三步:假设检验判断差异是否可信
转化率是类别变量的对比,用卡方检验:
table = pd.crosstab(df["page_version"], df["converted"]) chi2, p_value, dof, expected = stats.chi2_contingency(table) print("转化率卡方检验 p 值:", p_value)访问时长是连续变量,用独立样本 t 检验:
new_duration = df.loc[df["page_version"] == "new", "visit_duration"] old_duration = df.loc[df["page_version"] == "old", "visit_duration"] t_stat, p_duration = stats.ttest_ind(new_duration, old_duration, equal_var=False) print("访问时长 t 检验 p 值:", p_duration)如果两个 p 值都小于 0.05,说明"差异是真实存在"的证据比较足。注意这里涉及两个检验,我还会顺手做个 Bonferroni 校正,把显著性水平降到 0.025,因为检验做得越多,碰巧发现显著的概率越高。这个细节很多报告都不做,实操中却非常重要。
7.5 第四步:建模找关系,并把结果可视化
最后用回归分析量化访问时长和页面浏览数之间的关系,看新版是否在"提升用户深度浏览"上也有贡献。原理解还是sm.OLS,只是这次我加入城市哑变量做分层控制:
X = df[["visit_duration", "page_version", "city"]] X = pd.get_dummies(X, columns=["city"], drop_first=True) X["page_version_new"] = (X["page_version"] == "new").astype(int) X = X.drop(columns=["page_version"]) X = sm.add_constant(X) model = sm.OLS(df["pages_viewed"], X).fit() print(model.summary())看回归输出里page_version_new的系数和 p 值,如果系数显著为正,说明在控制了访问时长和城市之后,新版页面仍然跟更多的浏览页面数挂钩。这一步能让结论更扎实:新版的优势不是单一指标,而是多个链路一起变好。
可视化是给结论加印象分的环节。用一张箱线图展示访问时长按版本分布的差异:
import seaborn as sns import matplotlib.pyplot as plt sns.boxplot(data=df, x="page_version", y="visit_duration") plt.title("页面版本与访问时长分布") plt.show()到这一步,整个分析闭环就走完了:业务问题 → 数据清洗 → 描述统计 → 推断检验 → 建模佐证 → 可视化沟通。每一步都能用脚本复现,而不是靠截图和手工操作。
8. 我踩过的坑和现在的固定动作
回顾整个分析流程,有几个教训是我真金白银换来的,新手尤其值得提前知道。
第一个坑是 Pandas 的std()默认是样本标准差,而 NumPy 的std()默认是总体标准差。同一个数组,两个函数结果不一样,如果不注意,做置信区间、做检验的时候,前后数据就对不上。我的习惯是在脚本开头统一写ddof=1,把样本标准差铭记于心。
第二个坑是填充缺失值的方法选不对。刚开始我图省事,所有缺失列一律用均值填充,结果把很多变量的方差大幅压缩,后续做出来的检验全部"显著"。后来我学会先看缺失比例和业务含义,再决定用中位数、众数、插值还是模型预测。均值填充只适合近似对称分布、缺失比例很低的列。
第三个坑是只看 p 值,忽略效应量和样本量。样本量一大,0.5 秒的时长差异都能检验出显著,但业务上这个差异可能毫无意义。p 值告诉你"有没有差异",效应量告诉你"差异有多大",两个一起看才完整。我在报告里现在会同时写置信区间和效应量,避免自己又被"显著"带跑。
第四个坑是多重比较不加校正。当你要对比的组别很多,比如五个城市两两比较,就会出现 10 次检验,哪怕所有城市其实没有差别,纯靠运气至少出现一次"显著"的概率接近 40%。我现在的固定动作是,任何超过三次的组间比较,都考虑用 Bonferroni 校正,或者直接改用 ANOVA 先做整体检验,显著后再做两两比较。
除了避坑,我还有一个工作习惯值得分享:每个分析任务尽量拆成"加载 → 清洗 → 探索 → 推断 → 建模"五个固定步骤,写成多个函数或脚本文件。这样做的唯一目的就是提升可维护性。哪怕过了三个月,回头重新跑,也只需要替换数据路径,流程不用重新捋。写代码时会随时用df.shape和df.dtypes确认每一步没有改变数据结构;每次清洗操作都在单独的数据副本上进行,绝不覆写原始数据。这个习惯帮我减少了很多"跑完发现原始数据被改了"的尴尬。
最后再说一个我特别想强调的认知:统计软件只是工具,统计判断才是核心。Python 能把所有计算在几毫秒内完成,但"这个缺失值应该怎么处理""这个异常值是不是业务信号""p 值显著这个结论能不能对外说",这些判断永远需要人来做。你能做的,是尽量把流程做规范、把假设写明、把不确定性讲清楚,让每个结论都经得起追问。如果你也在学 Python 数据统计,建议别盯着教程看,拿一份自己手头真正关心的数据,按这五步走一遍。遇到问题会比刷一百道题学到的东西更多。