两年前我还在用Excel给业务部门做统计报表,直到有一次处理40万行订单数据,Excel直接卡死,弹出"未响应",我被迫重启电脑,一小时的工作白干。也就是从那一刻起,我彻底转向了Python。后来我才发现,Python做数据统计,不只是"能处理大数据量"这么简单,它真正改变的是你的工作方式:清洗、建模、可视化、复现,一切都能在同一个脚本里完成。这篇文章,就是我两年下来从入门到实战的完整经验总结,包含环境搭建、数据清洗、描述性统计、推断统计、可视化和一个完整实战案例,适合零基础和数据量初步接触统计的读者。
1. 统计项目的第一步:Python环境与工具链搭建
很多新手学Python数据统计,第一个坑就踩在环境上:不是装不上Python,就是装上了NumPy跑不起来,再要么是import pandas直接报错。这些问题的根子,大多出在安装环节没处理干净。我见过太多人在这一步卡住,最后放弃了整个学习计划,实在可惜。
1.1 安装Python时的两个关键决策
第一是版本选择。目前主流是Python 3.10到3.12,我推荐直接选择3.11或3.12,原因很简单:NumPy、pandas、SciPy这些数据统计的核心库,对最新版本的支持通常需要一点时间,但3.11和3.12已经完全稳定,而且无论Windows、macOS还是Linux,都有预编译的wheel包,不需要自己编译源码。不要选择3.6、3.7这种老版本,有些新版本的库已经不再支持,会出现"找不到对应版本"的安装错误。
第二是Windows上安装时务必要勾选"Add Python to PATH"选项。这句话是每一篇教程都会提的,但每年依然有无数人在这一步翻车。如果你没有勾选,python命令在命令行里就是无法识别的,后面装什么库都要手动处理环境变量,非常麻烦。万一你没勾选,解决方案有两种:一个是重新运行安装程序进入"Modify"界面勾选,另一个是手动添加环境变量。我个人强烈推荐直接重装一遍,省事,新手别轻易手动改环境变量,路径写错的概率太高了。
注意:安装时还建议选择"Install Now"而不是"Customize installation",除非你有特殊需求,默认选项足够应对日常数据统计工作。
1.2 虚拟环境:避免库冲突的保命手段
数据统计项目不是一次性跑完就扔的,你可能会做多个项目,每个项目用到的库版本不一样。今天这个项目需要pandas 1.5,明天那个项目需要pandas 2.1,如果你全部装在全局环境里,版本冲突会让你崩溃。
我的做法是给每个项目建一个独立的虚拟环境。在Python 3.10以上版本中,创建和激活虚拟环境的命令如下:
bash复制# 创建虚拟环境
python -m venv my_stats_env
# 激活虚拟环境
# Windows:
my_stats_env\Scripts\activate
# macOS / Linux:
source my_stats_env/bin/activate
激活后你会看到命令行前面出现(my_stats_env)字样,这时候再安装库,就会装到这个独立环境中,和全局环境完全隔离。
在虚拟环境里安装核心库,使用pip一行命令搞定:
bash复制pip install numpy pandas scipy matplotlib seaborn statsmodels openpyxl
这些库的分工我得讲明白,因为很多新手装了不知道干嘛用:
- numpy:提供数组和矩阵运算,是所有数据操作的底层引擎
- pandas:专门处理结构化数据(表格),核心数据结构是DataFrame
- scipy:封装了大量统计检验函数(t检验、卡方检验、ANOVA等)
- matplotlib:最基础的绘图库,一切可视化的地基
- seaborn:基于matplotlib的进阶封装,用几行代码就能画出高质量的统计图
- statsmodels:更专业的统计建模库,线性回归、时间序列分析都用它
- openpyxl:让pandas能读取和写入Excel文件
1.3 开发环境和编辑器选型
写Python代码,我常用的编辑器有VS Code、PyCharm和Jupyter Notebook/Lab,三者的定位差异很大:
| 工具 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| Jupyter Notebook/Lab | 数据探索、学习阶段、写报告式代码 | 每个代码单元格独立运行,结果直接内嵌展示,非常适合看统计图表 | 代码重用时不如脚本方便,逻辑混乱时很难调试 |
| VS Code | 小中型脚本项目 | 轻量、插件丰富,有Python交互式窗口 | 数据探索时不如Jupyter直观 |
| PyCharm | 大型工程、多人协作 | 智能补全强大,调试体验好 | 启动慢、占用内存高 |
我的建议是:学统计、做分析用Jupyter Notebook/Lab,写自动化处理脚本用VS Code。我自己通常是两边同时开,Notebook里做探索性分析,确定流程后在VS Code里整合成可复用的.py脚本。
安装Jupyter非常简单:
bash复制pip install jupyterlab
jupyter lab
跑起来之后,浏览器会自动打开Jupyter界面,你可以在里面创建Notebook文件,边写代码边看结果,这种即时反馈对学习统计概念非常有帮助。
1.4 验证环境是否安装成功
装好之后别急着开跑,先做一个快速验证,确保所有库都可用。在命令行或Notebook里输入以下代码:
python复制import numpy as np
import pandas as pd
import scipy
import matplotlib.pyplot as plt
import seaborn as sns
print("numpy版本:", np.__version__)
print("pandas版本:", pd.__version__)
print("scipy版本:", scipy.__version__)
如果每一行都能正常输出版本号,说明你的环境已经准备好了。如果某个库报错ModuleNotFoundError,不要慌张,用pip install 库名补装即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据加载与清洗:统计工作的地基
我在带新人做项目时,最常说的一句话是:真实的统计数据永远不会是干净的。你拿到的数据可能从CRM系统导出,可能从数据库查询得到,可能自动采集,但大概率有缺失值、重复值、异常值、类型不对的问题。如果你跳过清洗直接做统计,算出来的每一个数字都可能是错的,而且错得很隐蔽,并不报错,这是最可怕的。
2.1 从不同数据源读取数据
第一步是把数据读进来。最常见的数据源就是CSV和Excel,pandas都提供了非常方便的读取接口。
python复制import pandas as pd
# 读取CSV文件
df = pd.read_csv('sales_data.csv', encoding='utf-8')
# 读取Excel文件,指定工作表
df = pd.read_excel('sales_data.xlsx', sheet_name='Sheet1', engine='openpyxl')
读取Excel时有个细节:如果你的Excel文件有多个sheet,不指定sheet_name时只读取第一个。另外,如果文件里有公式,openpyxl默认读取的是公式计算后的缓存值,一般不用额外处理。
读取数据之后,第一时间应该做的是观察数据整体情况:
python复制# 查看前5行
print(df.head())
# 查看数据维度
print(df.shape)
# 查看每列的数据类型和缺失情况
print(df.info())
# 生成描述性统计预览(这个后面会详细讲)
print(df.describe())
这几个操作能让你迅速建立起对数据的整体印象:有多少行、多少列、每列是什么类型、有没有缺失值。
2.2 缺失值处理:不能删、不能填,先判断再处理
缺失值是数据清洗中遇到最多的问题之一。pandas用NaN表示缺失值,判断缺失情况的标准做法:
python复制# 每列缺失值的数量
print(df.isnull().sum())
# 缺失值占比
print(df.isnull().mean() * 100)
缺失值怎么处理,取决于缺失的比例和业务含义。我总结了一个经验规则:
- 缺失比例低于5%,并且该列不是关键字段:可以直接删除有缺失值的行
- 缺失比例在5%到30%之间:考虑用均值、中位数或众数填充
- 缺失比例超过30%:别硬填,先想清楚这个字段对你的统计目标是否重要,如果不重要直接丢弃该列
实际处理代码如下:
python复制# 删除包含缺失值的行
df_cleaned = df.dropna()
# 数值列用中位数填充(推荐,避免被异常值拉偏)
df['salary'] = df['salary'].fillna(df['salary'].median())
# 分类列用众数填充
df['department'] = df['department'].fillna(df['department'].mode()[0])
# 时间序列数据可以用前向填充
df['sales'] = df['sales'].fillna(method='ffill')
我用均值还是中位数填充数值列,这个选择本身就有讲究。均值受极端值影响很大,如果你的数据分布偏斜(比如收入分布),用均值填充会扭曲数据,而中位数具有较强的稳健性,在偏态分布中更推荐。
2.3 异常值检测:统计的"隐形杀手"
异常值不一定是"错误数据",也可能是真实存在但极其罕见的极端情况,比如电商大促期间的千倍销量。但如果你不识别异常值,后面算均值、标准差、相关系数,全都会被它们带偏。
最常用的异常值检测方法有两个:
IQR方法(四分位距法):
python复制# 计算四分位数
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
# 定义异常值边界
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 找出异常值
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]
Z-score方法(标准化):
python复制from scipy import stats
import numpy as np
# 计算每个数据的Z分数
z_scores = np.abs(stats.zscore(df['price']))
# 通常认为Z分数超过3的点是异常值
outliers = df[z_scores > 3]
注意:IQR方法对偏态分布更稳健,Z-score方法假定数据近似正态。如果你的数据明显偏态,优先使用IQR。
找到异常值之后,并不是一删了之,你要去理解它出现的原因:是录入错误、是真实极端值、还是特殊场景下的合理数据?必要的时候可以和业务方沟通确认。我见过有人把"店铺周年庆促销当天的销量"当异常值删了,后来发现那是业务的核心价值点,直接导致统计结论失真。
2.4 数据类型转换与重复值处理
pandas读取数据后,经常会出现类型不对的情况,最常见的是:数值列被读成了object类型(字符串),日期列读成了object类型。处理方式如下:
python复制# 强制转换为数值类型,无法转换的值变为NaN
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# 字符串转日期
df['order_date'] = pd.to_datetime(df['order_date'], format='%Y-%m-%d')
# 将分类文本转成category类型,减少内存占用
df['city'] = df['city'].astype('category')
errors='coerce'这个参数非常实用,意思是遇到无法转换的值时不报错,而是置为NaN。这样你既得到了一个可用的数值列,又知道了哪些数据是有问题的。
重复值处理相对简单:
python复制# 检查重复行
print(df.duplicated().sum())
# 删除重复行(保留第一次出现的那条)
df = df.drop_duplicates()
但注意,删除重复需要根据业务判断。如果一行数据包含订单ID,订单ID本身就应该是唯一的,重复了肯定有问题;但如果是客户行为日志,可能同一个用户在同一天有多次记录,这些是合法重复,不能删。所以删除之前先确认"重复"的标准是什么。
数据清洗这个环节,我花了大量篇幅讲,是因为它真的太重要了。我做过的一个实际案例中,原始数据有20万行,清洗完后只剩14万行,但那14万行做出的统计结果,比之前直接用原始数据做的结果要可靠得多。清洗永远是最费时间、最不讨喜、却最值得做的环节。
3. 描述性统计:先看懂你的数据再谈分析
清洗完数据,接下来就要真正开始统计了。描述性统计是统计学中最基础也最常用的部分,它的核心任务就是:用几个数字和图表,把一大坨数据的特征讲清楚。你不需要看每个订单的明细,只需要知道平均订单是多少、订单金额波动大不大、大多数订单集中在什么价位,你就对业务心里有数了。
3.1 集中趋势:均值、中位数、众数的不同视角
集中趋势回答的是"数据集中在什么位置"这个问题,常用的指标有三个:均值、中位数、众数。
python复制import pandas as pd
# 均值
mean_value = df['order_amount'].mean()
# 中位数
median_value = df['order_amount'].median()
# 众数
mode_value = df['order_amount'].mode()[0] # mode()可能返回多个值
print(f"均值: {mean_value:.2f}")
print(f"中位数: {median_value:.2f}")
print(f"众数: {mode_value:.2f}")
三个指标中,均值是最常用的,但也是最容易被"平均"这句话误导的指标。举一个实际例子:假设你统计一个部门的月薪,部门共有10个人,其中9个人月薪8000元,1个高管月薪80000元。均值是15200元,中位数是8000元。如果你用均值向外界汇报说"该部门平均月薪15200元",那就严重高估了绝大多数员工的薪资水平。而中位数8000元,反映出"一半人低于8000,一半人高于8000"的位置,更真实地体现了普通人的感受。
所以我判断数据的分布形态时,如果均值远大于中位数,说明数据右偏,存在一些特别大的值在拉高均值,这种情况实际分析中要高度警惕。
3.2 离散程度:方差、标准差、四分位距
集中趋势只告诉你数据集中在哪,离散程度告诉你数据"散不散"。两个班的平均分都是80分,可能一个班所有人都在75到85分之间,另一个班有的30分有的100分,显然第二个班的情况完全不同。
核心指标如下:
python复制# 方差(所有值与均值差值的平方的平均)
variance = df['order_amount'].var()
# 标准差(方差的平方根,与原始数据同单位)
std_dev = df['order_amount'].std()
# 极差(最大值 - 最小值)
range_val = df['order_amount'].max() - df['order_amount'].min()
# 四分位距(第75百分位 - 第25百分位)
q1 = df['order_amount'].quantile(0.25)
q3 = df['order_amount'].quantile(0.75)
iqr = q3 - q1
print(f"标准差: {std_dev:.2f}")
print(f"极差: {range_val:.2f}")
print(f"四分位距: {iqr:.2f}")
标准差和方差是统计中最重要的离散度指标,它们对每一个数据的波动都很敏感。标准差尤其有用,因为它的单位和原始数据一致。在近似正态分布的数据中,大约68%的数据落在均值±1个标准差之内,95%的数据落在均值±2个标准差之内,这是经验法则,也是后面做置信区间的基础。
四分位距则更稳健,它只看数据的中间50%落在多宽的区间内,不受极端值影响。如果一个人跟你说"数据的四分位距很大",他的意思是超过50%的观测值分布特别分散,稳定性差。
3.3 分布形态:偏度和峰度
偏度(skewness)和峰度(kurtosis)用来描述数据分布的形状。
python复制from scipy.stats import skew, kurtosis
skewness = skew(df['order_amount'])
kurt = kurtosis(df['order_amount'])
print(f"偏度: {skewness:.2f}")
print(f"峰度: {kurt:.2f}")
偏度描述的是分布的不对称程度:
- 偏度接近0:分布近似对称
- 偏度大于0(正偏/右偏):分布右侧有长尾,存在较大的极端值
- 偏度小于0(负偏/左偏):分布左侧有长尾,存在较小的极端值
峰度描述的是分布"尖陡"程度:
- 峰度接近0:类似正态分布的陡峭程度
- 峰度大于0:分布更尖,中间集中、尾部厚(也叫尖峰厚尾)
- 峰度小于0:分布更平坦,数据更加分散
偏度和峰度不只是学术指标,它们直接影响你后面能否使用需要"正态性假设"的统计方法。如果数据严重偏态,使用t检验等参数检验方法可能不适用,需要考虑对数据进行变换(如取对数)或使用非参数检验。
3.4 pandas中的describe()与groupby聚合
describe()函数是pandas里最被低估的一个函数,一行代码就能输出描述性统计的核心指标:
python复制# 对数值列做描述性统计
print(df.describe())
# 输出示例:
# order_amount quantity
# count 140000.0000 140000.000000
# mean 186.5000 2.300000
# std 256.3000 1.800000
# min 0.0100 1.000000
# 25% 45.0000 1.000000
# 50% 109.0000 2.000000
# 75% 210.0000 3.000000
# max 52000.0000 50.000000
count、mean、std、min、25%、50%、75%、max这一套输出,已经覆盖了集中趋势、离散程度、极值和中位数,几秒钟就能对数据有个初步判断。我拿到任何新数据集,第一件事永远是跑df.describe(include='all')。
groupby则是分组统计的核心工具,在统计实战中极其常用。比如你想按城市和商品类别分别统计销售额的平均值和总和:
python复制# 按城市分组,统计订单金额的均值、中位数、总和
city_stats = df.groupby('city')['order_amount'].agg(['mean', 'median', 'sum', 'count'])
print(city_stats)
# 按城市+品类两个维度分组
multi_stats = df.groupby(['city', 'category'])['order_amount'].agg(['mean', 'sum'])
groupby的本质逻辑是"拆分-应用-合并":先按指定列拆分成组,再对每组应用聚合函数,最后把结果合并成新表。理解了这一步,很多复杂统计需求都能通过组合groupby和agg来实现。
4. 推断统计实战:从样本推断总体
描述性统计只是在描述你已有的数据,但现实中的统计任务往往是:我只拿到了1000个用户的样本数据,却要推断平台所有100万用户的特征,这就是推断统计的范畴。它的核心由三部分组成:置信区间、假设检验、相关性分析。
4.1 置信区间:用样本估计总体的"靠谱区间"
假设你抽样调查了100个用户的平均消费金额是200元,但你不能说全平台所有用户的平均消费就是精确的200元。更合理的说法是:全用户的平均消费有95%的可能性落在某个区间内,比如[180, 220]元。这个区间就是置信区间。
用Python计算均值的95%置信区间:
python复制import numpy as np
from scipy import stats
sample = df['order_amount'].sample(1000, random_state=42)
mean = sample.mean()
std = sample.std(ddof=1) # 样本标准差
n = len(sample)
se = std / np.sqrt(n) # 标准误
# 95%置信区间(t分布临界值)
t_critical = stats.t.ppf(0.975, df=n-1)
ci_lower = mean - t_critical * se
ci_upper = mean + t_critical * se
print(f"95%置信区间: [{ci_lower:.2f}, {ci_upper:.2f}]")
这里有个关键点:std参数ddof=1表示计算样本标准差(除以n-1),而不是总体标准差(除以n)。初学者最容易在这一步出错。
标准误(Standard Error)是理解置信区间的关键概念。标准误=标准差/√样本量,所以样本量越大,标准误越小,置信区间越窄,估计越精确。这就是为什么大样本抽样比小样本抽样更让人放心。
4.2 假设检验的基本框架与p值误区
假设检验是推断统计的核心武器。它的基本思路是:先提一个"原假设"(H0),然后看数据是否支持拒绝这个假设。
以"某地区用户平均消费是否不等于200元"为例:
python复制from scipy import stats
# 单样本t检验
t_stat, p_value = stats.ttest_1samp(sample, 200)
print(f"t统计量: {t_stat:.4f}, p值: {p_value:.4f}")
# 判断
alpha = 0.05
if p_value < alpha:
print("拒绝原假设:该地区用户平均消费显著不等于200元")
else:
print("不能拒绝原假设:没有证据表明平均消费显著偏离200元")
p值永远是统计学习中最容易懵的概念。我的解释方式是:p值是在原假设成立的前提下,观察到当前样本数据或更极端数据的概率。如果p值很小(小于0.05),说明在当前假设下,出现这样的数据非常罕见,那我们有理由怀疑原假设本身有问题,于是拒绝它。
但这里有个常见的误区:p值不是"原假设成立的概率",更不是"结果重要的程度"。p值很小只说明"统计显著",不代表"实际显著"。一个拥有50万样本的A/B测试,即使两组转化率的差异只有0.1%,也会被判定为统计显著,但这个差异在商业上可能毫无意义。判断实用性要看效应量(effect size),而不是只看p值。
还有一点我必须提:p值受样本量影响极大。样本量很大时,任何细小的差异都会被检验为显著;样本量很小时,即使真实差异很大也可能不显著。所以在下结论前,先确认你的样本量是否足够支撑你想做的推断。
4.3 独立样本t检验:两组数据有没有差异
做A/B测试或比较两个独立群体时,用的是独立样本t检验。比如比较"北京用户"和"上海用户"的平均订单金额是否有显著差异:
python复制beijing = df[df['city'] == '北京']['order_amount']
shanghai = df[df['city'] == '上海']['order_amount']
t_stat, p_value = stats.ttest_ind(beijing, shanghai, equal_var=False)
print(f"t统计量: {t_stat:.4f}, p值: {p_value:.4f}")
equal_var=False代表使用Welch's t-test,即不假设两组方差相等。我在实战中默认使用这个参数,因为真实数据很难保证方差齐性,而Welch方法在绝大多数情况下都是稳健的。
如果比较的是同一组对象在不同时间点的变化(比如用户注册前后的消费),就要用配对样本t检验:
python复制# 注意:两个样本长度必须一致,且按个体一一对应
before = df['before_amount']
after = df['after_amount']
t_stat, p_value = stats.ttest_rel(before, after)
配对检验的本质是计算每个个体前后的差值,再检验这些差值的均值是否显著不为0。由于去掉了个体间差异的影响,检验灵敏度更高,更容易检测出微小变化。
4.4 卡方检验:分类变量之间有没有关系
前面的t检验适用于连续型数值变量,但统计中大量数据是分类变量,比如"用户的性别"和"用户是否购买"这两个类别型变量之间有没有关系?这时候用卡方检验。
python复制from scipy.stats import chi2_contingency
# 构建列联表(交叉表)
contingency_table = pd.crosstab(df['gender'], df['is_purchase'])
print(contingency_table)
# 卡方检验
chi2, p_value, dof, expected = chi2_contingency(contingency_table)
print(f"卡方值: {chi2:.4f}, p值: {p_value:.4f}")
卡方检验的原理是:比较实际观察到的频数和在原假设(两个变量独立)下期望的频数之间的差距。如果差距很大,说明两个变量很可能不独立。
p值的解读和前面一样。如果p < 0.05,说明性别和购买行为之间存在显著关联,接下来可以进一步看列联表的百分比,判断具体是哪一类偏好更强。
4.5 方差分析(ANOVA):多组均值比较
如果要比较三组及以上的均值是否有显著差异,比如三个不同商品品类的订单金额是否相同,不能两两做t检验(那样会造成多重比较问题,增加假阳性概率),而应该用方差分析。
python复制from scipy.stats import f_oneway
group_a = df[df['category'] == 'A']['order_amount']
group_b = df[df['category'] == 'B']['order_amount']
group_c = df[df['category'] == 'C']['order_amount']
f_stat, p_value = f_oneway(group_a, group_b, group_c)
print(f"F统计量: {f_stat:.4f}, p值: {p_value:.4f}")
方差分析的思想是:将总的变异拆分成"组内变异"和"组间变异"两部分。如果组间变异相对组内变异足够大,说明组均值之间存在显著差异。如果ANOVA检验结果显著,说明至少有一组和其他组不同,但要具体知道是哪一组不同,还需要做事后检验(post-hoc test),比如Tukey HSD检验,这一步在scipy中没有直接实现,需要用到statsmodels库:
python复制from statsmodels.stats.multicomp import pairwise_tukeyhsd
# 构造长格式数据
data = df[['order_amount', 'category']]
tukey = pairwise_tukeyhsd(endog=data['order_amount'], groups=data['category'], alpha=0.05)
print(tukey)
4.6 相关性分析:相关不等于因果
最后是相关性分析。它用来回答"两个变量之间是否存在某种关联趋势"的问题,比如"订单金额和用户注册天数之间有没有关系"。
python复制from scipy.stats import pearsonr, spearmanr
# 皮尔逊相关系数(适用于线性关系、近似正态分布)
r_pearson, p_pearson = pearsonr(df['order_amount'], df['user_days'])
# 斯皮尔曼等级相关系数(适用于单调关系,对异常值不敏感)
r_spearman, p_spearman = spearmanr(df['order_amount'], df['user_days'])
print(f"皮尔逊相关系数: {r_pearson:.4f}, p值: {p_pearson:.4f}")
print(f"斯皮尔曼相关系数: {r_spearman:.4f}, p值: {p_spearman:.4f}")
相关系数的取值在-1到1之间,正数代表正相关,负数代表负相关,绝对值越接近1代表线性关系越强。但判断"强不强"要看具体领域,物理实验里0.99算稀松平常,社会科学里0.3已经算重要发现,所以不能只用绝对数值一刀切。
我特别想强调一句老话:相关不等于因果。冰淇淋销量和游泳溺水人数高度正相关,但没有人会认为卖冰淇淋导致了溺水。它们背后的共同驱动力是天气热。在做实际业务分析时,千万别看到两个指标相关系数高就急着下"一个影响另一个"的结论,先想想有没有隐藏的混淆变量。
5. 统计可视化:用图表辅助判断,而不只是展示
很多教程把可视化放在最后一步,纯粹当"画图展示",但在我看来,统计可视化的核心价值是辅助你在分析过程中快速理解数据,甚至发现出乎意料的模式。一张图,往往胜过几十行统计指标。
5.1 直方图:一眼看穿分布形态
直方图是统计中最基础也最重要的图形,它把数据分成若干个区间,展示每个区间里有多少个观测值。通过直方图,你能直观看到数据是近似正态分布、偏态分布、还是多峰的。
python复制import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10, 6))
sns.histplot(df['order_amount'], bins=50, kde=True, color='steelblue')
plt.xlabel('订单金额')
plt.ylabel('频数')
plt.title('订单金额分布直方图')
plt.show()
bins参数控制直方图的分箱数量,这个参数对图形效果影响很大。bins太少,信息被过度压缩;bins太多,则噪声太大。我一般从30到50起步,根据数据的取值范围和样本量调整。kde=True会在直方图上叠加一条核密度估计曲线,相当于一个平滑的分布形态参考线,非常直观。
5.2 箱线图:看分布、识异常一把好手
箱线图是另一个信息密度极高的统计图。一张箱线图,包含了中位数、四分位数、上下须和异常值,相当于把关键的描述性统计指标画在了一张图里。
python复制plt.figure(figsize=(10, 6))
sns.boxplot(x='city', y='order_amount', data=df)
plt.title('不同城市的订单金额分布')
plt.xticks(rotation=45)
plt.show()
箱线图特别适合做多个组间的对比。你不需要记住每个组的均值和标准差,只要看箱子的位置和长度,就能判断哪组金额更高、哪组波动更大、哪组有更多极端值。我在做多城市对比时,往往先画箱线图,再决定要不要做ANOVA检验。如果箱线图显示组间差异明显,ANOVA大概率也会显著;如果箱线图里各组的箱子高度重合,那ANOVA就别抱太大期望。
5.3 QQ图:判断正态性的利器
很多统计方法(如t检验、方差分析、线性回归)都假定数据近似正态分布。怎么检验这个假设?除了偏度峰度指标,更直观的方式是画QQ图(分位数-分位数图)。
python复制from scipy import stats
plt.figure(figsize=(8, 6))
stats.probplot(df['order_amount'], dist='norm', plot=plt)
plt.title('订单金额的QQ图')
plt.show()
QQ图的判断逻辑是:如果数据近似正态分布,散点会大致沿着45度参考线排列;如果数据偏离直线明显弯曲,说明分布不是正态的。看到明显的"S形"或者"U形"弯曲,就不要强行使用假定正态的方法,考虑对数据做对数变换、平方根变换,或者改用非参数检验。
5.4 散点图与联合分布:观察变量关系
在算相关系数之前,先画一张散点图非常有必要。因为相关系数只能捕捉线性或单调的关系,如果两个变量之间是"倒U型"关系(比如年龄和某些消费行为),相关系数可能接近0,但图上一眼就能看出来。
python复制plt.figure(figsize=(10, 6))
sns.scatterplot(x='user_days', y='order_amount', data=df, alpha=0.3)
plt.xlabel('用户注册天数')
plt.ylabel('订单金额')
plt.title('注册天数与订单金额关系')
plt.show()
alpha=0.3设置透明度,数据点很多时避免严重重叠。如果需要进一步检查条件关系,可以加hue参数按类别着色,比如不同城市的用户用不同颜色,这样可以同时查看三四个维度的关系。
6. 综合实战:从零完成一份电商订单统计报告
前面讲的方法论已经很完整了,现在把它们串起来,做一个完整的实战项目。假设我们手里有一份电商订单数据sales_data.csv,包含字段:订单ID、用户ID、城市、商品品类、订单金额、下单日期、用户注册天数。我们的目标是回答三个业务问题:
- 各城市的订单金额有无显著差异?
- 订单金额是否随用户注册天数变化?
- 不同品类的用户消费画像有何不同?
6.1 数据加载与清洗
python复制import pandas as pd
import numpy as np
from scipy import stats
# 1. 读取数据
df = pd.read_csv('sales_data.csv')
print("原始数据量:", df.shape)
print(df.info())
# 2. 删除完全重复的行
df = df.drop_duplicates()
# 3. 处理缺失值
# 订单金额缺失用中位数填充
df['order_amount'] = df['order_amount'].fillna(df['order_amount'].median())
# 城市缺失:如果占比很小,直接删除
df = df.dropna(subset=['city'])
# 4. 类型转换
df['order_amount'] = pd.to_numeric(df['order_amount'], errors='coerce')
df['order_date'] = pd.to_datetime(df['order_date'])
# 5. 异常值处理(使用IQR方法,仅标记不删除,业务侧确认后再决定)
Q1 = df['order_amount'].quantile(0.25)
Q3 = df['order_amount'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df['is_outlier'] = (df['order_amount'] < lower) | (df['order_amount'] > upper)
print("标记异常值数量:", df['is_outlier'].sum())
这里我把异常值先标记而不直接删除,因为在电商场景中,高额订单可能是真实的大客户采购,不一定是错误数据。标记后可以先统计一次总体数据、再基于非异常值统计一次,对比两次结论差异。如果差异不大,说明异常值影响有限,可以保留;如果差异很大,需要找到异常值来源并单独分析。
6.2 描述性统计与分组对比
python复制# 总体描述统计
print(df.describe())
# 按城市分组统计
city_st = df.groupby('city')['order_amount'].agg(['count', 'mean', 'median', 'std'])
print(city_st.round(2))
# 按品类分组统计
cat_st = df.groupby('category')['order_amount'].agg(['count', 'mean', 'median'])
print(cat_st.round(2))
从分组统计结果中,我们可能看到不同城市的平均订单金额差异明显,比如一线城市均值300元,其他城市均值150元。但这只是样本内的描述,还不能断定"总体上也存在这差异",需要用方差分析验证。
6.3 假设检验
python复制# 各城市订单金额差异检验(ANOVA)
groups = [group['order_amount'].values for name, group in df.groupby('city')]
f_stat, p_value = stats.f_oneway(*groups)
print(f"ANOVA: F={f_stat:.4f}, p={p_value:.6f}")
# 相关分析:注册天数与订单金额
r, p = stats.pearsonr(df['user_days'], df['order_amount'])
print(f"皮尔逊相关: r={r:.4f}, p={p:.4f}")
如果ANOVA的p值小于0.05,我们可以在报告中写明"不同城市之间的订单金额存在统计显著差异"。如果相关分析的p值显著,但r的绝对值只有0.1,虽然"显著",但实际解释力很弱,应该如实写明"注册天数与订单金额存在微弱的正相关关系,但影响力度有限"。
6.4 可视化输出与结论整合
python复制import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文乱码
plt.rcParams['axes.unicode_minus'] = False
# 绘制三个图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 直方图
sns.histplot(df['order_amount'], bins=50, kde=True, ax=axes[0][0])
axes[0][0].set_title('订单金额分布')
# 城市箱线图
sns.boxplot(x='city', y='order_amount', data=df, ax=axes[0][1])
axes[0][1].set_title('各城市订单金额分布')
axes[0][1].set_xticklabels(axes[0][1].get_xticklabels(), rotation=45)
# 品类柱状图(均值对比)
cat_mean = df.groupby('category')['order_amount'].mean().sort_values()
cat_mean.plot(kind='bar', ax=axes[1][0], color='steelblue')
axes[1][0].set_title('各品类平均订单金额')
# 散点图
sns.scatterplot(x='user_days', y='order_amount', data=df.sample(2000), alpha=0.3, ax=axes[1][1])
axes[1][1].set_title('注册天数和订单金额的关系')
plt.tight_layout()
plt.savefig('report_charts.png', dpi=150)
plt.show()
在整合结论时,我建议遵循这个结构:先写核心发现(用一句话点明三个业务问题的答案),再用统计数据作为支撑(给出具体的统计量和p值),最后写出业务建议。比如:"订单金额在城市维度上存在显著差异(ANOVA,F=25.31,p<0.001),一线城市的平均订单金额显著高于其他城市,建议针对高客单价城市加强会员运营"。这样写出来的统计报告,业务部门一看就懂,分析的价值也真正落地了。
6.5 一键复用:把分析流程封装成脚本
实际工作中,数据是每周更新的,你不可能每次拿到新数据都重新手写一遍分析流程。我的做法是把整个分析流程封装成一个函数或脚本,输入数据文件路径,输出统计结果和图表。
python复制def run_sales_report(file_path):
# 清洗
df = pd.read_csv(file_path)
df = df.drop_duplicates()
df['order_amount'] = pd.to_numeric(df['order_amount'], errors='coerce')
df = df.dropna(subset=['order_amount'])
# 统计
city_stats = df.groupby('city')['order_amount'].agg(['count', 'mean', 'median'])
f_stat, p_value = stats.f_oneway(*[g['order_amount'].values for _, g in df.groupby('city')])
# 输出
print(city_stats.round(2))
print(f"ANOVA检验: F={f_stat:.4f}, p={p_value:.6f}")
return city_stats, p_value
这样每周新数据来了,一行代码run_sales_report('new_data.csv')就能跑完整套分析。这也是Python数据统计相比Excel的核心优势之一:你的分析流程是可复现、可维护、可扩展的,而不是每次手动重新操作。
到了这个阶段,你已经不再是"会用几个Python函数画图"的入门者了,而是可以独立完成"加载数据-清洗-描述-推断-可视化-结论"完整统计链路的分析者。我自己就是从这条路上一步步走过来的,最大的体会是:统计学绝对不仅是一堆公式,它是一套帮助你在不确定性中做判断的思维框架,而Python只是将这套框架付诸实施的最好工具。数据清洗的耐心、样本量的把控、p值的使用边界、相关与因果的区分,这些才是统计实战中最容易出错、也最体现功力的地方。
