☰
Python数据分析入门:从数据清洗到可视化报告实战指南
2026/9/26 7:58:40 网站建设 项目流程

一直有一个现象让我挺困惑的:很多人学 Python 数据分析,买书、看视频、收藏一堆教程,最后打开电脑还是不知道从哪下手。问题不是出在某个函数不会写,而是缺一条完整的链路——从拿到一份原始数据,到中间怎么清洗、怎么分析、怎么画图,再到最后怎么把结果整理成一份能交出去的报告。这条链路没人带着走一遍,知识就永远是散的。这篇内容就是想补上这个缺口。

这篇文章特别适合刚学完 Python 基础语法、想往数据分析方向走的新手,也适合那些在 Excel 里处理数据已经觉得吃力、想换个工具提高效率的职场人。我会用一套模拟的订单数据,带着你完整走一遍“数据 → 清洗 → 分析 → 可视化 → 报告”的流程。每个步骤我都会解释为什么这样做,而不只是丢给你一段能跑的代码。

1. 开工之前:环境装对了,后面能少踩一半坑

1.1 别盲目装最新版 Python:版本选择的现实逻辑

很多新手的第一反应是去官网下载最新的 Python 3.13,觉得越新越好。我的建议是:如果你只是学数据分析,装 Python 3.11 或 3.12 就足够,别追求最新。

原因有两点。第一,数据分析生态里最核心的 pandas、numpy、scikit-learn 这些库,对新版本 Python 的适配通常有滞后期。你装了 3.13,结果某个库还没有对应版本的预编译包,pip 安装的时候就会现场编译,轻则等半天,重则报一堆看不懂的红色错误。第二,很多教程和第三方工具默认环境还是 3.10 或者 3.11,版本差太多,照着敲代码容易踩到莫名其妙的兼容问题。

如果你电脑里已经装了新版本,也不用推倒重来,后面直接用虚拟环境锁定 Python 版本就行。

1.2 用 Anaconda 还是纯 Python + pip?

这个选择题我纠结过很久,也折腾过两条路。现在给新手的建议非常明确:直接装 Anaconda。

我知道有人会说 Anaconda 体积大、占用空间多,这个批评没错,但它给新手带来的好处远大于代价。Anaconda 自带 conda 包管理器,可以一句命令创建一个独立的环境,把 Python 版本、所有数据分析相关的库一次性装好,不用自己一个个 pip install,也不会把系统自带的 Python 环境弄乱。更重要的是,它自带 Jupyter Notebook,这是数据分析场景最好用的交互式编程环境,后面几乎天天要用。

如果你实在不想用 Anaconda,那纯 Python + pip 的路子也能走通,需要手动安装以下几样东西:

  1. Python(建议 3.11 版本)
  2. Jupyter Notebook 或 Jupyter Lab
  3. pandas、numpy、matplotlib、seaborn、openpyxl 这几个核心库

但我还是要说一句,新手阶段别在环境配置上消耗太多热情。我见过太多人第一天学 Python,装了三天环境,第四天放弃了。Anaconda 的价值就是帮你把这一步压缩到十分钟以内。

1.3 创建独立的分词环境:一个值得养成的习惯

装好 Anaconda 之后,我建议你打开 Anaconda Prompt(Windows)或终端(Mac/Linux),先创建一个独立环境,不要直接在 base 环境里写分析代码。原因和 1.1 说的一样:数据分析项目之间经常会有库版本冲突,今天这个项目需要 pandas 2.0,明天那个项目可能只支持 pandas 1.5,分开建环境最省心。

conda create -n data_analysis python=3.11 conda activate data_analysis conda install pandas numpy matplotlib seaborn jupyter openpyxl

等你运行完这几条命令,环境就备好了。以后每次做数据分析,先打开终端激活环境,再启动 Jupyter:

conda activate data_analysis jupyter notebook

这一步很多人容易漏掉,直接双击桌面图标打开 Jupyter,结果发现自己装的库全都不认识,原因就是激活错了环境。

2. 拿到数据先别急着跑代码:三步看清数据的“长相”

2.1 没有真实数据怎么办:给自己造一份可复现的样例数据

我带过不少新人,最常听到的困难是“我手头没有数据,练什么”。这里建议自己造一份模拟数据,模拟的规则贴近真实业务场景就行。下面这段代码就是我用随机数生成的订单表,用来模拟一家小型零售电商的销售记录:

import pandas as pd import numpy as np np.random.seed(42) n = 2000 cities = ['北京', '上海', '广州', '深圳', '杭州'] categories = ['数码', '服饰', '家居', '食品', '图书'] pay_types = ['支付宝', '微信', '银行卡', '货到付款'] df = pd.DataFrame({ '订单号': ['ORD' + str(i).zfill(6) for i in range(1, n+1)], '日期': pd.date_range('2024-01-01', periods=n, freq='h').strftime('%Y-%m-%d %H:%M'), '城市': np.random.choice(cities, n), '品类': np.random.choice(categories, n), '销售额': np.round(np.random.uniform(20, 500, n), 2), '成本': np.round(np.random.uniform(10, 300, n), 2), '支付方式': np.random.choice(pay_types, n, p=[0.4, 0.3, 0.2, 0.1]) }) df.to_csv('orders.csv', index=False, encoding='utf-8-sig') print('订单表已生成,行数:', len(df))

造数据这件事本身也有讲究。我不想一开始就弄一个干干净净的完美表格,那样你后面学不到清洗的本领。但第一版先让你跑通全流程,所以这份模拟数据故意没有做太多污染。等你走完一遍流程,我建议你动手往里面塞点重复行、缺失值、格式错乱的字段,再重新跑一遍清洗环节,那才是真正的实战。

2.2 read_csv 没你想象的那么简单:编码和路径是两个大坑

读取数据是分析的第一步,也是新手翻车频率最高的环节。read_csv 第一参数填文件路径,这个谁都知道,但有两个细节你没注意到就会卡很久。

第一个是编码。我生成数据时用了utf-8-sig,这是为了考虑 Excel 打开 CSV 不乱码,同时也兼容 pandas 读取。但你从其他地方拿到的 CSV 可能是gbk或gb2312编码,直接读会报一个让人摸不着头脑的错误:UnicodeDecodeError: 'utf-8' codec can't decode byte。

解决办法很简单,读取时手动指定编码:

df = pd.read_csv('orders.csv', encoding='utf-8-sig')

如果报错,就换成encoding='gbk'再试。实在不行,用errors='ignore'先读进来再说:

df = pd.read_csv('orders.csv', encoding='gbk', errors='ignore')

第二个坑是相对路径。新手经常在 Jupyter 里写pd.read_csv('orders.csv'),结果报“文件不存在”。Jupyter 的工作目录默认是它启动时所在的文件夹,如果你的 CSV 放在别的目录,直接写文件名当然找不到。我这里处理的笨办法是:把 CSV 和 Notebook 放在同一个目录下,省心;或者用绝对路径:

df = pd.read_csv(r'D:\项目\学习笔记\orders.csv')

Windows 路径里的反斜杠要加r前缀,表示原始字符串,否则会被转义。很多人在这里报错,就是漏了这个r。

2.3 用三行代码快速摸清数据的长相

数据读进来了,先别急着print(df)看全部。数据量大时这样刷屏不说,你也看不出什么结构。我习惯的节奏是三个函数连用:

print(df.head()) # 看前5行,了解字段长什么样 print(df.info()) # 看字段类型和非空数量,判断缺失 print(df.describe()) # 看数值型字段的统计分布

这三个函数各自回答一个问题。head()解决“数据大概长什么样”,让你直观感受每一列是文本、数字还是日期;info()解决“数据有没有缺”,行数和非空数量一旦对不上,就说明数据存在缺失;describe()解决“数值范围是不是正常”,比如销售额最小 20、最大 500,符合模拟数据的设定,如果出现负数或者一个离谱的极大值,那你就要留个心眼了。

这里我特别强调一个新手容易忽略的点:info()里的object类型并不代表数据是“错的”。比如“日期”这一列在模拟数据里读进来是object,因为 CSV 里的时间被当成字符串处理了。这部分后面讲类型转换时会专门处理。

3. pandas 核心操作:清洗、筛选、聚合就是分析的主战场

3.1 缺失值和重复值:先处理它们,而不是急着算平均数

很多新手一上来就做平均、求和,得到的结果自己都不知道可不可靠。数据有缺失和有重复的时候,任何统计指标都是有偏的。所以我的固定顺序是:先检查缺失和重复,再进入分析。

检查缺失用isna(),配合sum()看每一列缺几个:

print(df.isna().sum())

处理缺失值的选择不是唯一的,取决于业务场景。常见就三种策略:

  • 删除缺失行:df.dropna()适合缺失比例很低、缺了就缺了不影响大局的情况;
  • 填充固定值:df['列名'].fillna('未知')适合文本类字段,比如支付方式缺失,填“未知”比删行合理;
  • 填充统计值:df['列名'].fillna(df['列名'].median())适合数值型字段,用中位数而不建议用平均数,是因为平均数容易被极端值带偏。

重复值的判断要提醒一句:不要用df.duplicated()看到有重复就删。重复分两种,一种是整行完全重复,这种删除没毛病;另一种是关键字段重复,比如同一订单号出现两次,可能一行的数据是错的,这时需要用subset=['订单号']指定判断依据再检查:

df.drop_duplicates(subset=['订单号'], keep='first', inplace=True)

这里inplace=True的意思是直接修改原数据框。很多教程不推荐这个参数,因为它在某些链式操作里容易出问题,但对新手来说,直接修改比反复赋值更直观。

3.2 类型转换:让日期真正变成“日期”,而不是一串文本

我前面提过,CSV 读进来的“日期”列大概率是字符串。字符串状态下的日期没法做按月份筛选、没法算持续天数、没法按照时间先后排序。把字符串转成 pandas 的 datetime 类型,是这个环节最核心的一件事:

df['日期'] = pd.to_datetime(df['日期']) print(df.dtypes)

pd.to_datetime()会智能识别常见的日期格式,比如2024-01-01 15:30、2024/01/01、2024年1月1日都能处理。万一遇到特别奇怪的格式,它识别不了会报错,这时可以加参数format告诉它解析规则。比如:

df['日期'] = pd.to_datetime(df['日期'], format='%Y/%m/%d')

这个%Y/%m/%d的写法对应的是“年/月/日”。日期格式里%Y是四位年份,%m是两位月份,%d是两位日期,%H是小时,%M是分钟。你手头的数据是什么格式,就按顺序把对应的占位符拼出来。

转完类型之后,就能做一件非常高频的操作:从日期里提取年、月、日、周几等维度:

df['月份'] = df['日期'].dt.month df['小时'] = df['日期'].dt.hour df['星期几'] = df['日期'].dt.dayofweek # 0=周一,6=周日

“提取月份”听起来很基础,但它直接决定了你能不能做“按月汇总”“按小时分析销售峰值”这类分析。很多看起来复杂的分析需求,拆到最后无非就是“把时间字段拆出维度,再分组求和”。

3.3 条件筛选和分组聚合:一个顶 Excel 透视表的组合拳

数据清洗完,就可以开始真正“问问题”了。比如你想知道“上海的数码类销售额是多少”,SQL 是where筛选加group by分组,pandas 的逻辑完全一致:

df_sh = df[df['城市'] == '上海'] df_sh_ds = df_sh[df_sh['品类'] == '数码'] total = df_sh_ds['销售额'].sum() print('上海数码类销售额:', round(total, 2))

这里df[df['城市'] == '上海']是一个“布尔索引”,通过一个真假序列来挑选符合条件的行。多个条件同时满足时,注意要用&而不是and:

df_sh_ds = df[(df['城市'] == '上海') & (df['品类'] == '数码')]

新手经常在这里踩坑,写成and会报错:ValueError: The truth value of a Series is ambiguous。这个报错的本质是,pandas 的 Series 是一个包含很多值的数组,中间用&表示逐个元素进行逻辑运算,而and要求的是单一布尔值。

接下来出场的是数据分析中使用频率最高的一组方法:groupby()加agg()。Excel 里用透视表能做的事情,这套组合拳基本都能做,而且代码写一次,以后跑任何数据都能复用:

result = df.groupby(['城市', '品类']).agg({ '订单号': 'count', # 订单数 '销售额': 'sum', # 总销售额 '成本': 'mean' # 平均成本 }).rename(columns={'订单号': '订单数'}) print(result)

这段代码的逻辑:按“城市”和“品类”两个字段分组,然后在每一个分组内分别对销售额做求和、对成本做平均、对订单号做计数。agg()的操作符和代码一样,sum()会按整列将所有值相加,这里的“sum”区别于 Python 内置的sum()函数。

如果更进一步,还可以用agg()在一个字段上同时计算多个统计值:

result = df.groupby('城市')['销售额'].agg(['sum', 'mean', 'count', 'max']) print(result)

这种写法返回的结果里,列名就是sum、mean、count、max,一目了然。分组聚合这东西,看起来简单,但它的思路是整个数据分析的核心骨架。后面无论你是做用户分层、销售漏斗还是库存周转分析,本质都是这个套路:先分组,再聚合,然后把结果丢给可视化。

4. 可视化:让结论自己“跳出来”,不只是画图

4.1 先想清楚要回答什么问题,再选图表类型

不少人学可视化的误区是:把所有图表类型函数背一遍,然后拿到数据挨个画,看看哪个好看用哪个。这是本末倒置。正确的顺序是:你先有一个问题,然后根据问题的性质去匹配图表类型。

我自己整理了一个对应关系,新手可以直接拿来用:

你想回答的问题合适的图表
某种商品的销售额在一年里怎么变化折线图
不同城市卖了多少东西柱状图
各种支付方式占比多少饼图
销售额和成本有没有关系散点图
不同品类的销售额分布差异箱线图

问题的类型决定了图表的类型,而图表类型又决定了你调用哪个库的哪个函数。先有问题,后有图表,这个习惯越早建立越好。

4.2 seaborn 让你少写 80% 的绘图代码

matplotlib 功能强大,但新手用它画图有个痛苦点:代码又长又绕,设置标题、设置坐标轴、调整字体,每一样都得自己写。我的建议是直接用一个更高级的封装库 seaborn 作为主力,它底层本身基于 matplotlib,画出来的图默认也更好看。

import matplotlib.pyplot as plt import seaborn as sns # 先把字体设置好,中文明细见4.3 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False # 柱状图:各城市销售额 city_sales = df.groupby('城市')['销售额'].sum().reset_index() sns.barplot(data=city_sales, x='城市', y='销售额') plt.title('各城市总销售额对比') plt.show()

这个例子只用三行核心代码就完成了从数据到图表的全部工作。相比 matplotlib 原生的写法,这已经是相当友好的体验了。

再看一个更有代表性的场景:按月份和品类画销售额折线图,展示趋势变化:

# 先按月份和品类做聚合 月度数据 = df.groupby(['月份', '品类'])['销售额'].sum().reset_index() sns.lineplot(data=月度数据, x='月份', y='销售额', hue='品类', marker='o') plt.title('各品类月度销售趋势') plt.show()

hue='品类'这个参数很重要,它让 seaborn 自动用不同颜色区分不同品类,并且自动加图例。你不需要自己循环画线,一次调用全部搞定。

箱线图特别适合看分布情况,比如“各品类销售额的分布差异”,一句话就能发现问题——哪个品类波动大,哪个品类更稳定:

sns.boxplot(data=df, x='品类', y='销售额') plt.title('各品类销售额分布对比') plt.show()

看到箱体上下沿的距离,你就能直观判断品类的离散程度。如果你只看平均数,很容易被极端值掩盖;箱线图把这个信息补回来了。

4.3 中文字体和坐标轴标签:新手最容易卡住的细节

画图的代码写完了,一运行,中文全部变成了方块。这是所有中文用户都会遇到的事,而且解决方式特别简单,就两行配置:

plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False

第一行指定了字符的字体,第二行解决负号显示的问题。如果你的系统是 macOS,字体名需要改成['Arial Unicode MS', 'Heiti TC'];Linux 则可能需要['WenQuanYi Zen Hei', 'Noto Sans CJK SC']。不确定系统字体叫什么,就先跑下面这段代码查看可用字体:

from matplotlib.font_manager import fontManager print([f.name for f in fontManager.ttflist if 'Hei' in f.name or 'CJK' in f.name])

这个坑让我想起一件事:有次我在演示环境里画好了图,换到客户的电脑上重新跑一遍,中文乱码了。原因就是两台机器装的中文字体不一样。所以上半场我建议你在脚本开头统一放这两行配置,下半场要部署到别人机器上,还得多一手字体检测。

另一个细节是横坐标标签太密时容易重叠,比如按小时分析销售额,x 轴有 24 个刻度,会挤成一团。解决办法是旋转角度或设置显示间隔密度:

plt.xticks(rotation=45, ha='right')

这段代码让标签斜着排,避免互相遮盖。

5. 从分析到报告:把代码、图表和结论整理成能交付的东西

5.1 Jupyter Notebook 本身就是一份“草稿报告”

很多人做分析的习惯是:在 Jupyter 里写代码,截图几张图表放进 Word 里,再写一段文字,生成一份报告。这种流程不是说不行,但效率太低了。Jupyter Notebook 本身就能把代码、运行结果、图表、文字说明组织在同一个文档里,每一段代码前面用 Markdown 写一句分析结论,后面直接是图表输出,读起来就是一份完完整整的报告。

所以我的建议是,从一开始就用“报告思维”来写 Notebook:

  • 每个分析问题用 Markdown 标题写出来:# 1. 各城市销售额对比
  • 代码块下面紧跟一句“结论”:比如“上海的销售额最高,占总额的 24.6%”
  • 图表不要随手画,要补充图注和关键信息

这样一来,你分析完的那一刻,报告已经完成了一半。剩下的工作就是整理和导出。

有些数据结论不应该只留在 Notebook 里。如果你要发给完全不懂代码的同事看,需要一份“干净”的文档,那就要用导出功能。

Jupyter 自带导出成 HTML 的功能,在界面上选择File → Download as → HTML就能一键生成。如果你在终端环境里,也可以直接用命令:

jupyter nbconvert --to html 数据分析报告.ipynb

导出之后有个细节需要注意:默认导出的 HTML 包含所有代码块。如果你不想让对方看到代码,可以加参数隐藏代码块:

jupyter nbconvert --to html 数据分析报告.ipynb --no-input

--no-input的意思是只保留输出结果,也就是图标、表格、文字结论,不显示代码。这是交付给业务人员最常见的姿势。

如果对方是一般需要 Word 的人,HTML 反而是更好用的交付方式,因为 Word 直接打开 HTML 也能看,排版问题少。PDF 也可以导出,但中文支持的问题更复杂,新手阶段优先级靠后。

5.3 最后一公里:写总结,而不是写过程

报告最后一定要有一个“结论和建议”部分,这也是拿数据说话的核心——你前面做了那么多图表,目的就是回答一个业务问题。如果你只是把图表贴出来,读者还要自己解读,那报告就失去了一半的价值。

我常用的模板是:

  1. 发现的核心事实:哪个品类销售额最高、哪个月增长最快
  2. 潜在的异常或风险:某个城市成本倒挂,利润率为负
  3. 基于数据给建议:下个月应该加大哪个品类的库存备货

比如你分析完这份模拟数据,可以用这段代码计算出按城市和品类的利润,然后挑出利润最高的组合:

df['利润'] = df['销售额'] - df['成本'] profit_summary = df.groupby(['城市', '品类'])['利润'].sum().reset_index() top3 = profit_summary.sort_values('利润', ascending=False).head(3) print(top3)

这个结果放进去,比单纯写“我们分析了各城市的销售情况”有说服力得多。


说回开头那个话题。学 Python 数据分析,卡住你的往往不是复杂的算法,而是缺一条完整的路线。我从环境配置讲到数据读取,从清洗讲到聚合,从画图讲到导出报告,核心就一个想法:整个链路走通了,你对“数据分析”这四个字的理解才算真正落地。我自己的体会是,第一次完整地从一个 CSV 文件做出带图和结论的报告,那种成就感比看多少节视频课都来得实在。如果你手头有真实数据,哪怕是几十行的表格,也建议今晚就按这个流程走一遍。碰到问题很正常,也许你会比教程里更容易迷路,但迷路之后走通的路,才是你自己的地图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询