在数据分析这个圈子里,python+miniconda+jupyter这套组合几乎成了标配。很多人第一次接触时习惯直接装Anaconda,图省事,觉得一次性打包了几百个库,什么都有。但用过一段时间之后,大多数老手会悄悄回到Miniconda,原因很简单:Anaconda实在太臃肿了。就拿我自己的经历来说,装完Anaconda之后启动都要好几秒,磁盘占用动辄几个G,更别提不同项目之间的库版本互相"打架"有多头疼。
这篇文章不打算讲教科书式的概念,我直接带你把整个流程走一遍:从Miniconda创建干净环境开始,装好Jupyter Notebook,导入一份模拟的用户消费数据,依次完成数据清洗、分布查看、相关性分析,最后解读结果背后的业务信号。整个链路跑通之后你会发现,数据分析本身并不神秘,可复用的分析套路就那些,难的是在每一步里判断该做什么、为什么这么做。
这篇内容适合刚入门Python数据分析的人,也适合想系统地重新整理一遍分析流程的读者。看完之后,你可以直接把这套流程复制到自己的数据上,照猫画虎跑一遍,基本就能应付绝大多数探索性分析的需求。
1. 环境准备:把基础打好再谈分析
1.1 为什么我推荐Miniconda而不是Anaconda
其实这两者一脉相承,底层都是conda生态。Anaconda相当于一个全家桶,默认装上几百个包,安装包体积巨大;Miniconda则只保留conda包管理器、Python解释器和最基础的依赖,其他库需要什么装什么。很多人觉得Anaconda方便,是因为不用自己操心装库,但实际用起来,"版本冲突"的问题反而更容易出现。
举个例子。我某个项目用pandas 2.x跑得好好的,另一个旧项目还依赖pandas 1.5的某些行为。如果都装在一个环境里,升级一次可能全线崩溃。用Miniconda给每个项目单独建环境,想用哪个版本就用哪个版本,互不干扰。这种隔离能力对数据分析尤其重要,因为数据分析项目通常堆了一堆第三方库,库与库之间的兼容性往往是最容易被低估的隐性成本。
1.2 创建干净环境与核心库安装
Miniconda的安装包在官网上按操作系统选择即可,安装过程基本是下一步下一步。装完之后打开终端,执行以下命令:
conda create -n>conda install pandas numpy matplotlib seaborn jupyter scipy openpyxl这里稍微解释一下每个库的用途。pandas负责数据结构和表格操作,numpy提供底层数值计算,matplotlib是基础的绘图库,seaborn是建立在matplotlib之上的统计可视化库,scipy提供科学计算函数,openpyxl用来读写Excel文件。如果你所在的网络安装速度很慢,可以给conda配置国内镜像源,速度会有明显提升。
全部装完之后启动Notebook:
jupyter notebook浏览器会自动打开Jupyter页面。新建一个Notebook文件,重命名为类似"数据全流程分析.ipynb"的名字,接下来的所有操作都在这个文件里完成。
1.3 Jupyter Notebook的几个实用技巧
很多人把Jupyter Notebook当成一个"高级文本编辑器",其实它有几个技巧用好了效率翻倍。
- 单元格分两种:代码单元格(Code)和标记单元格(Markdown)。分析过程中的思路、结论用Markdown记录,代码放在Code里,这样整个分析过程带着思考痕迹,别人看你的Notebook能直接理解每一步在做什么。
- 快捷键:Shift+Enter运行当前单元格并跳到下一个,Ctrl+Enter只运行不跳转;A在单元格上方插入,B在下方插入;连续按两次D删除单元格。
- 在代码单元格里以感叹号开头可以执行shell命令,比如!pip list可以查看已安装的包。但我不建议在Notebook里直接安装包,环境管理最好还是在终端里用conda操作,保持环境状态清晰可控。
- DataFrame放在单元格最后一行,会以富文本表格形式展示,比print输出的纯文本清晰得多,建议养成"最后一行直接写变量名"的习惯。
2. 数据加载与第一眼观察
2.1 准备演示数据集
为了演示完整的数据分析流程,我们需要一份数据。现实中数据可能来自数据库导出、接口返回、Excel表格等,最常见的落地格式就是CSV文件。我这里直接用代码生成一份模拟数据:某零售平台的会员消费记录,共1000条,包含用户ID、年龄、注册天数、月消费金额、购买次数、平均订单金额、最近消费距今天数等字段。
生成数据的代码如下:
import numpy as np import pandas as pd np.random.seed(42) n = 1000 age = np.random.normal(35, 10, n).round().astype(int) age = np.clip(age, 18, 65) reg_days = np.random.randint(1, 800, n) purchase_count = np.random.poisson(8, n) avg_order_value = np.random.lognormal(4.0, 0.5, n) monthly_spend = (purchase_count * avg_order_value * np.random.uniform(0.8, 1.2, n)).round(2) recency = np.random.randint(0, 90, n) df = pd.DataFrame({ 'user_id': range(1, n + 1), 'age': age, 'reg_days': reg_days, 'purchase_count': purchase_count, 'avg_order_value': avg_order_value.round(2), 'monthly_spend': monthly_spend, 'recency': recency }) df.head()这里有几个关键点。np.random.seed(42)是固定随机种子,保证每次运行生成的数据一致,方便复现整个分析过程。purchase_count用泊松分布模拟,因为购买次数在现实里就是典型的计数分布;avg_order_value用对数正态分布生成,因为金额数据通常右偏,直接使用正态分布会生成负数,不符合业务直觉。
为了让演示覆盖数据处理环节,我在数据生成后人为加入一些缺失值:
df.loc[df.sample(50).index, 'age'] = np.nan df.loc[df.sample(30).index, 'recency'] = np.nan df.loc[df.sample(20).index, 'monthly_spend'] = np.nan这样后面处理缺失值才有实际素材可用。
2.2 用info和describe快速摸底
拿到数据之后,第一步不要急着画图,先站在全局看一眼。两个方法用起来:
df.info() df.describe().Tinfo会显示每一列的非空数量、数据类型。通过info我们可以很快发现age、recency、monthly_spend三列都有缺失值,而且字段类型大体正确,user_id是整数,金额字段是浮点数。describe输出统计信息,包括均值、标准差、最小值、四分位数、最大值。从这里往往就能看出端倪,比如某一列的max值远远超出合理范围,那大概率存在异常值,需要进一步排查。
用describe时有一点要特别注意:它默认只统计数值列。如果你的数据里有大量分类字段,先用df.dtypes或者df.select_dtypes确认哪些是数值列,别被常规统计结果误导。
2.3 先想清楚分析目标再看数据
这一步看似务虚,其实是最关键的。拿到一份数据之后,先问自己:我要回答什么问题?是"什么样的用户更爱消费",还是"消费金额受哪些因素影响"?不同目标决定了后续数据处理的侧重点。
如果目标是用户分层,那特征工程和聚类是重点;如果目标是预测月消费金额,那要严格做缺失值处理、异常值处理、特征相关性排查,防止模型被个别极端样本带偏。这篇文章走的是探索性分析的路子,侧重理解数据本身,所以后面的清洗、分布查看和相关性分析,都是围绕这个目标服务的。目标明确之后,每一步操作都有了判断依据,不会陷入"为了处理而处理"的误区。
3. 数据处理:别让脏数据毁了一切
3.1 缺失值先判断类型再决定补还是删
缺失值是数据分析遇到的第一个坎。处理缺失值之前,先搞清楚两个问题:缺失比例有多大?缺失机制是什么?
对只有几条缺失的列,比如monthly_spend缺了20条,直接删除这20条即可,对整体影响微乎其微。对缺失比例相对较高的列,比如age缺了50条,占比达到5%,直接删除会浪费样本信息,更好的做法是用中位数填充。
为什么推荐中位数而不是均值?因为均值对异常值敏感,数据里一旦有极端值,均值会被拉得偏离主体水平,而中位数几乎不受影响。代码实现:
df['age'] = df['age'].fillna(df['age'].median()) df['recency'] = df['recency'].fillna(df['recency'].median()) df = df.dropna(subset=['monthly_spend'])填完之后建议马上再跑一次df.info(),确认缺失值已经清零。这个习惯看着简单,实际能帮你省下大量排查问题的时间。
3.2 异常值识别:IQR和3σ法则
异常值指明显偏离正常范围的值。处理异常值不是因为它们"看起来奇怪",而是因为它们会扭曲统计结果。比如某个月消费金额高达几万,一个人的数据就能把均值拉得很高,后面计算相关系数时,这种极端值也可能把结果带偏。
常用的识别方法有两种。IQR法基于四分位数:
Q1 = df['monthly_spend'].quantile(0.25) Q3 = df['monthly_spend'].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR df_clean = df[(df['monthly_spend'] >= lower) & (df['monthly_spend'] <= upper)]3σ法则基于均值和标准差:
mean = df['monthly_spend'].mean() std = df['monthly_spend'].std() df_clean = df[(df['monthly_spend'] > mean - 3 * std) & (df['monthly_spend'] < mean + 3 * std)]这两种方法怎么选?IQR对偏态分布更稳健,因为四分位数本身对极端值不敏感;3σ法则的前提是数据近似正态分布。所以实际操作中,应该先看数据分布的形态,再决定用哪种方法。我个人的习惯是先用describe看最大值和四分位间距,再结合业务逻辑判断。比如monthly_spend如果出现上万的值,在这个平台的消费场景里就很不合理,应当剔除;反过来,如果数据本身是重尾分布,大量"看起来很大"的值其实是正常的,这时就要谨慎下手。
3.3 类型转换与特征衍生
数据处理还有一个重要环节:确保每一列的数据类型都正确。常见的问题有两个。
一是数字被读成了字符串。比如Excel导出的CSV文件里,某些数字列前面带着空格,或者数字列里混入了"暂无"这类文本,pandas读取后整列会变成object类型。处理方式:
df['avg_order_value'] = pd.to_numeric(df['avg_order_value'], errors='coerce')errors='coerce'的意思是转换失败的地方置为NaN,正好可以当作缺失值重新走一遍填充或删除流程。
二是日期列存的是字符串。用pd.to_datetime统一转换,转换失败的行会变成NaT,同样归入缺失值处理。
类型处理之外,特征衍生经常被忽略。在探索性分析阶段,可以基于现有字段创造一些有业务含义的新变量。比如把recency和purchase_count组合,定义一个"活跃度分层"之类的字段。这类衍生特征在后续的相关性分析里往往能带来新的洞察。
4. 数据分布:摸清每个字段的脾气
4.1 直方图一眼看出分布形态
分布分析的核心目的,是看数据长什么样:对称的、右偏的、还是双峰的?不同分布形态对应不同的后续处理方式。
用matplotlib或seaborn画直方图都很方便,seaborn的代码更短,默认图形也更好看。绘图之前需要先设置中文字体,否则图里的标题和轴标签会显示成方块:
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Micro Hei'] plt.rcParams['axes.unicode_minus'] = False cols = ['age', 'reg_days', 'purchase_count', 'avg_order_value', 'monthly_spend', 'recency'] fig, axes = plt.subplots(2, 3, figsize=(15, 8)) for ax, col in zip(axes.ravel(), cols): sns.histplot(df[col], kde=True, ax=ax) plt.tight_layout() plt.show()从图上能很直观地看到:purchase_count是泊松分布的形状,集中在均值附近;avg_order_value明显右偏,长尾拖在右边;monthly_spend也表现出右偏特征。这种直观印象,在后面的量化指标里会得到印证。
plt.rcParams[‘font.sans-serif’]设置字体时,我的经验是写一个列表,第一个字体不存在时matplotlib会自动尝试列表里的下一个。在Linux服务器上一般用[‘WenQuanYi Micro Hei’],Windows上SimHei基本能用。
4.2 箱线图里的四分位信息
箱线图是另一个看分布的工具,它把数据的核心信息压缩成几个关键统计量:最小值、下四分位数、中位数、上四分位数、最大值,以及被标记为离群点的散点。
fig, axes = plt.subplots(2, 3, figsize=(15, 8)) for ax, col in zip(axes.ravel(), cols): sns.boxplot(y=df[col], ax=ax) plt.tight_layout() plt.show()看箱线图重点看三件事。一是中位数在箱子里的位置,如果明显偏上或偏下,说明分布不对称;二是箱子的宽度,宽说明数据比较分散,窄说明集中;三是看有没有大量圆圈散点在须线外面,这些散点就是离群点。它们在前面的异常值处理中应该已经被清理过一遍,如果清理之后仍有大量离群点,要么是阈值设置太激进,要么这个字段本身就属于重尾分布,需要进一步用数据变换来处理。
4.3 偏度峰度与数据变换
直方图是"看感觉",偏度和峰度是"给数字"。偏度描述分布的不对称程度,正偏表示右偏,负偏表示左偏,绝对值超过0.5一般就认为有明显偏斜。峰度描述分布尾部的厚度,正态分布的峰度是0,正数表示尖峰厚尾,负数表示平坦散布。
df[cols].skew() df[cols].kurtosis()对明显右偏的字段,比如monthly_spend、avg_order_value,可以考虑做对数变换,压缩右尾,让数据更接近正态:
df['log_spend'] = np.log1p(df['monthly_spend']) df['log_aov'] = np.log1p(df['avg_order_value'])对数变换的意义在于,后续做相关性分析时,极端值的权重会被削弱,相关系数能更真实地反映主体数据的关系。不用担心变换会破坏业务可解释性,在相关性分析阶段,变换后的变量和原始变量的趋势基本一致,只是敏感度不同。实际项目里,原始字段和变换字段可以同时保留,按需取用。
5. 相关性分析:找到变量之间的秘密联系
5.1 皮尔逊相关系数怎么读
相关性分析回答的问题是:两个变量之间存在什么方向的关系、关系强度如何。最常用的是皮尔逊相关系数,取值范围在-1到1之间,正数代表正相关,负数代表负相关,绝对值越接近1代表线性关系越强。
df[['age', 'reg_days', 'purchase_count', 'avg_order_value', 'monthly_spend', 'recency']].corr()读相关矩阵有个顺序:先看和你要回答的问题直接相关的行或列。比如分析月消费金额的驱动因素,就看monthly_spend这一行与其他列的相关系数;然后再看变量之间的共线性关系,比如两个特征之间相关系数超过0.8,后续如果要做建模,这两个特征就不适合同时进入模型。
在演示数据中,我们大概能看到:monthly_spend与purchase_count的相关系数很高,因为月消费金额本身就是由购买次数和平均订单金额计算出来的;monthly_spend与avg_order_value也有明显的正相关;而purchase_count与avg_order_value之间往往呈现负相关——买得多的人通常单笔金额偏低,这和现实中的消费习惯完全一致。
5.2 用热力图把相关矩阵可视化
数字表格看多了容易抓不住重点,热力图是最直观的相关矩阵展示方式:
plt.figure(figsize=(10, 8)) sns.heatmap(df.corr(), annot=True, fmt='.2f', cmap='coolwarm', center=0) plt.show()annot=True会在每个格子里显示数值,fmt='.2f'指定保留两位小数,cmap控制配色,center=0让0值显示为中间色。看热力图时,一眼扫过去颜色深的地方就是关系密切的变量对。我习惯在热力图下面加两句注释,直接把业务结论写下来,比如"购买次数与月消费金额强相关,是驱动消费的核心变量"。这样的图形带注释,后续做汇报时可以直接引用,省得临时回忆。
有一个细节容易踩坑:corr()默认只计算数值列,如果你的DataFrame里包含字符串类型的字段,结果里自然不会出现那些列。使用之前先用df.select_dtypes(include=[‘number’])确认一下参与计算的字段列表。
5.3 皮尔逊与斯皮尔曼的分寸
皮尔逊相关系数衡量的是线性关系,对异常值和分布形态比较敏感。当数据明显偏态时,比如avg_order_value这种右偏严重的字段,斯皮尔曼秩相关系数会更加稳健,因为它只关心排序关系,不关心具体数值之间的间距。
df.corr(method='spearman')什么情况用斯皮尔曼?我的判断标准是:如果图形上两个变量的关系看起来单调但不是直线,或者数据里有明显的离群值而且我不想剔除,就用斯皮尔曼。把两种相关系数的结果放在一起对比,如果差异很大,说明变量之间的关系不是线性的,需要进一步画散点图确认形态。
sns.scatterplot(x='purchase_count', y='monthly_spend', data=df) plt.show()散点图能直接补上相关系数无法传达的细节:两个变量之间是否存在明显的分段关系、是否有成簇的数据点、是否存在非线性趋势。相关分析从来不是只看一个数字就能下结论的事情,图形和统计指标配合使用,结论才站得住脚。
6. 常见问题排查与实操心得
6.1 高频报错与快速解法
分享几个我实际项目中遇到过的报错,每一条都浪费过不少时间。
文件编码问题。pd.read_csv默认编码是utf-8,但很多业务系统导出的CSV实际是gbk编码,直接读会出现乱码甚至报错。解决办法是用encoding='gbk',或者encoding='utf-8', errors='ignore'强行忽略错误。我的习惯是读取之前先问清楚数据来源,或者在终端用file命令确认编码,别猜。
中文字体显示成方块。seaborn和matplotlib默认字体不含中文字符,设置方法在第4章已经提到,关键是以后每个Notebook开头都要写这两行配置,不然画出来的图没法直接用于汇报。
数据列全是object类型。通常是因为列里有文本或者缺失值表现得很特殊,比如空字符串。解决办法是pd.to_numeric配合errors='coerce',转换失败的地方自动变成NaN,再走缺失值流程处理。
Jupyter内核环境不对。在conda里创建了新环境,启动jupyter却还在用旧环境的库,这种问题经常出现。如果不想在conda环境里重新安装jupyter,可以通过下面命令把环境注册为Notebook内核:
python -m ipykernel install --user --name=data-analysis然后在Notebook页面的Kernel菜单里切换内核,问题立刻解决。
6.2 几个实实在在的建议
第一,建立统一的代码习惯。处理数据前先写df.copy(),避免在原始DataFrame上原地修改。数据清理过程来回调整很频繁,保留一份原始数据的副本,排查问题时能省很多心力。
第二,每一步操作后都回头看一眼验证结果。fillna之后马上跑df.info(),确认缺失值清零;dropna之后马上看行数,确认删除比例在合理范围内。分析链路上每一步都可能出错,越早发现越容易修正。把验证动作养成肌肉记忆,比记住所有API都管用。
第三,Notebook里的Markdown不是摆设。给每一段代码补一两句话说明意图,比如"这里检测缺失值并决定填充策略",两周之后回看这个文件还能立刻回忆起当时的想法。我自己就吃过这个亏,当时觉得理所当然的操作,隔了一段时间再看完全想不起为什么要这么做。
第四,相关性分析阶段要时刻提醒自己一句话:相关不等于因果。两个变量相关系数高,不等于一个变量导致另一个变量。在演示数据里,月消费金额和购买次数相关性很强,但这不一定是购买次数"决定"了月消费金额,可能两个变量同时受"用户活跃度"这个隐藏变量驱动。做业务判断时,这个分寸必须拿捏清楚。
最后说一个我个人感受很深的事:整个数据分析流程跑下来,真正花时间最多的往往不是写代码,而是不断回头看数据、确认每一个统计量是否符合业务直觉。分析这份模拟数据的时候,我第一次跑完相关性矩阵,看到purchase_count和avg_order_value是负相关,第一反应是奇怪,冷静下来想一想,这个平台买得频繁的用户多数是日常小额消费,正好解释了负相关的来源。数据本身不会说话,但如果你对业务有足够的理解,数据里的每个信号都会变得有迹可循。把流程跑通只是基本功,能在每个环节都形成自己的判断,才是真正拉开差距的地方。