编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

    • Python 从入门到实战
    • 入门与基础类型
    • 序列与集合类型
    • 流程控制与函数
    • 面向对象与工程
    • 爬虫全流程实战
    • 数据分析三件套
      • 6.1 场景引入
      • 6.2 NumPy 入门
        • 2.2.1 ndarray 创建与属性
        • 2.2.2 数组运算与广播
        • 2.2.3 切片与花式索引
        • 2.2.4 常用函数速查
        • 2.2.5 为什么 NumPy 这么快?
      • 6.3 Pandas 入门
        • 2.3.1 Series 与 DataFrame
        • 2.3.2 数据筛选与查询
        • 2.3.3 分组聚合与透视表
        • 2.3.4 缺失值与重复值处理
        • 2.3.5 合并与连接
      • 6.4 数据可视化
        • 2.4.1 折线图与柱状图
        • 2.4.2 散点图与饼图
        • 2.4.3 子图布局与样式定制
        • 2.4.4 seaborn 快速美化
      • 6.5 综合实战
        • 2.5.1 数据加载与清洗
        • 2.5.2 多维度分析
        • 2.5.3 可视化报告
      • 6.6 新手陷阱
      • 6.7 综合思考题
    • 办公自动化实战
    • 开发环境与规范
    • 调试与性能优化
    • 部署与并发实战
    • 函数高级特性剖析:装饰器 / 生成器 / 上下文管理器
    • 并发底层原理揭秘
    • 面向对象与类型系统:元类 / 描述符 / 鸭子类型
    • 解释器源码初探
  • Shell-Bash

  • 工具脚本

  • ScriptHub
  • Python
杨充
2017-12-15
目录

数据分析三件套

# 第 6 章 数据分析三件套

# 目录介绍

  • 2.1 问题引入:一份"看不懂"的销售数据
  • 2.2 NumPy:数值计算的"加速引擎"
    • 2.2.1 ndarray 创建与属性
    • 2.2.2 数组运算与广播
    • 2.2.3 切片与花式索引
    • 2.2.4 常用函数速查
    • 2.2.5 为什么 NumPy 这么快?
  • 2.3 Pandas:数据处理的"瑞士军刀"
    • 2.3.1 Series 与 DataFrame
    • 2.3.2 数据筛选与查询
    • 2.3.3 分组聚合与透视表
    • 2.3.4 缺失值与重复值处理
    • 2.3.5 合并与连接
  • 2.4 Matplotlib:数据可视化的"画布"
    • 2.4.1 折线图与柱状图
    • 2.4.2 散点图与饼图
    • 2.4.3 子图布局与样式定制
    • 2.4.4 seaborn 快速美化
  • 2.5 综合实战:电商销售分析全流程
    • 2.5.1 数据加载与清洗
    • 2.5.2 多维度分析
    • 2.5.3 可视化报告
  • 2.6 新手陷阱 Top 5
  • 2.7 综合思考题

# 6.1 场景引入

💬 场景:你是电商公司的数据分析师。老板丢给你一个 CSV 文件:"这是上半年的销售记录,帮我搞清楚三件事——哪个品类最赚钱?各地区的销售趋势怎样?下半年备货重点是什么?"

你打开文件,10 万行数据、20 列字段、夹杂缺失值和异常记录。用 Excel 打开已经卡顿了——更别提分析。

你需要 Python 的数据分析三件套:NumPy(底层计算)+ Pandas(表格处理)+ Matplotlib(可视化)。

本章按"加速引擎 → 瑞士军刀 → 画布 → 端到端实战"四步走:

阶段 工具 能力
① 引擎 NumPy 数值计算、数组广播、性能对比
② 军刀 Pandas 读写、筛选、分组、透视表、合并
③ 画布 Matplotlib 折线图、柱状图、散点图、饼图、子图、seaborn
④ 实战 三件套联用 10 万行电商数据:清洗→分析→可视化→报告

# 6.2 NumPy 入门

NumPy 让 Python 拥有了接近 C 语言速度的数组运算——Pandas、Matplotlib、Scikit-learn 等核心库全部构建在它之上。

# 2.2.1 ndarray 创建与属性

import numpy as np

# ===== 创建 ndarray =====
a = np.array([1, 2, 3, 4, 5])                # 从列表创建
b = np.zeros((3, 4))                          # 3×4 全零矩阵
c = np.ones((2, 3))                           # 2×3 全一矩阵
d = np.arange(0, 10, 2)                       # [0, 2, 4, 6, 8](类似 range)
e = np.linspace(0, 1, 5)                      # [0, 0.25, 0.5, 0.75, 1](等间距)
f = np.random.randn(3, 3)                     # 3×3 标准正态分布随机数
g = np.eye(3)                                 # 3×3 单位矩阵

# ===== 属性 =====
print(f"{a.ndim=}")       # 1(维度)
print(f"{a.shape=}")      # (5,)(形状)
print(f"{a.dtype=}")      # int64(数据类型)
print(f"{a.size=}")       # 5(元素总数)

# 修改形状
m = np.arange(12).reshape(3, 4)
print(m)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.2.2 数组运算与广播

NumPy 的向量化运算是整个数据科学生态的性能根基:

a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

# 逐元素运算——不需要写循环
print(a + b)      # [11 22 33 44]
print(a * b)      # [10 40 90 160]
print(a ** 2)     # [1 4 9 16]
print(np.sqrt(a)) # [1. 1.414 1.732 2.]

# 比较——返回布尔数组
print(a > 2)      # [False False True True]

# 聚合
print(a.sum())    # 10
print(a.mean())   # 2.5
print(a.max())    # 4
print(f"{a.std():.2f}")  # 1.12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🔑 广播(Broadcasting)——理解这个就理解了 NumPy 的核心:

# 不同形状的数组也能运算——NumPy 自动"广播"小数组去匹配大数组
a = np.array([[1, 2, 3],
              [4, 5, 6]])            # shape (2, 3)
b = np.array([10, 20, 30])           # shape (3,)

print(a + b)
# [[11 22 33]
#  [14 25 36]]                       # b 自动"复制"成 (2,3) 再相加
1
2
3
4
5
6
7
8

🖼️ 图解广播:

a (2,3):              b (3,)  广播为 (2,3):     结果:
[[1 2 3]    +   [10 20 30]  →  [[10 20 30]  =  [[11 22 33]
 [4 5 6]]                       [10 20 30]]      [14 25 36]]
1
2
3

# 2.2.3 切片与花式索引

a = np.arange(12).reshape(3, 4)

# 常规索引
print(a[0, 2])         # 2(第 0 行第 2 列)

# 切片
print(a[:2, 1:3])
# [[1 2]
#  [5 6]]

# 布尔索引——过滤数据的神器
print(a[a > 5])        # [6 7 8 9 10 11](所有 >5 的元素)

# 花式索引——用数组指定位置
print(a[[0, 2]])       # 取第 0 行和第 2 行
# [[0 1 2 3]
#  [8 9 10 11]]

print(a[:, [0, -1]])   # 取第一列和最后一列
# [[0 3]
#  [4 7]
#  [8 11]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.2.4 常用函数速查

a = np.array([[1, 2, 3], [4, 5, 6]])

# 统计
print(a.sum())              # 21(总和)
print(a.sum(axis=0))        # [5 7 9](按列——axis=0 沿行方向压缩)
print(a.sum(axis=1))        # [6 15](按行——axis=1 沿列方向压缩)

# 线性代数
x = np.array([[1, 2], [3, 4]])
y = np.array([[5, 6], [7, 8]])
print(np.dot(x, y))         # 矩阵乘法 [[19 22] [43 50]]
print(x @ y)                # Python 3.5+ 等价写法

# 条件替换
print(np.where(a > 3, a, 0))   # >3 保留,否则 0 → [[0 0 0] [4 5 6]]

# 拼接
print(np.concatenate([a, a], axis=0))   # 纵向拼接 (6,4)
print(np.concatenate([a, a], axis=1))   # 横向拼接 (3,8)

# 唯一值
vals, counts = np.unique([1, 2, 2, 3, 3, 3], return_counts=True)
print(vals, counts)          # [1 2 3] [1 2 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.2.5 为什么 NumPy 这么快?

import time

size = 10_000_000

# Python 原生循环
py_list = list(range(size))
start = time.time()
result_py = sum(x * 2 for x in py_list)
print(f"Python 循环:{time.time() - start:.3f}s")

# NumPy 向量化
np_arr = np.arange(size)
start = time.time()
result_np = np.sum(np_arr * 2)
print(f"NumPy 向量化:{time.time() - start:.3f}s")

# 典型结果:Python 循环 ~1.2s vs NumPy ~0.02s——快 60 倍!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🔑 NumPy 快的三个原因:

  1. C 语言实现:运算在 C 层完成,不经过 Python 解释器
  2. 连续内存:ndarray 是一块连续内存,CPU 缓存友好
  3. SIMD 指令:现代 CPU 的向量指令集一次处理多个数据

# 6.3 Pandas 入门

# 2.3.1 Series 与 DataFrame

Pandas 有两个核心数据结构:

import pandas as pd
import numpy as np

# ===== Series:带索引的一维数组 =====
s = pd.Series([85, 92, 78, 90], index=["张三", "李四", "王五", "赵六"])
print(s)
# 张三    85
# 李四    92
# 王五    78
# 赵六    90
print(s["李四"])               # 92——按索引取值
print(s[s > 80].index.tolist())  # ['张三', '李四', '赵六']——布尔索引

# ===== DataFrame:带行列索引的二维表 =====
# 创建方式一:从字典
df = pd.DataFrame({
    "姓名": ["张三", "李四", "王五", "赵六"],
    "语文": [85, 92, 78, 90],
    "数学": [88, 95, 82, 87],
    "英语": [79, 91, 85, 93],
})
print(df)
#    姓名  语文  数学  英语
# 0  张三  85   88   79
# 1  李四  92   95   91
# 2  王五  78   82   85
# 3  赵六  90   87   93

# 创建方式二:从 CSV
# df = pd.read_csv("data.csv", encoding="utf-8")

# 创建方式三:从 NumPy
# df = pd.DataFrame(np.random.randn(100, 4), columns=list("ABCD"))

# ===== 基本属性 =====
print(df.shape)            # (4, 4)——行数, 列数
print(df.columns.tolist()) # ['姓名', '语文', '数学', '英语']
print(df.dtypes)           # 每列的数据类型
print(df.describe())       # 数值列的统计摘要(count/mean/std/min/四分位数/max)
print(df.head(2))          # 前 2 行
print(df["语文"])          # 取一列 → Series
print(df[["姓名", "语文"]]) # 取多列 → DataFrame

# 新增列——向量化运算
df["总分"] = df["语文"] + df["数学"] + df["英语"]
df["平均分"] = df["总分"] / 3
print(df)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 2.3.2 数据筛选与查询

# ===== loc:按标签索引 =====
# df.loc[行标签, 列标签]
print(df.loc[0])                       # 第 0 行(按索引标签)
print(df.loc[0:2, ["姓名", "总分"]])   # 索引 0~2 的姓名和总分
print(df.loc[df["总分"] > 260])        # 总分 > 260 的所有行

# ===== iloc:按位置索引 =====
print(df.iloc[0])                      # 第 0 行
print(df.iloc[0:2, 0:3])               # 前 2 行,前 3 列

# ===== query():SQL 风格的筛选 =====
print(df.query("总分 >= 260 and 语文 >= 85"))

# ===== 条件组合 =====
cond1 = df["语文"] >= 85
cond2 = df["数学"] >= 85
print(df[cond1 & cond2])               # & 是 and,| 是 or,~ 是 not
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2.3.3 分组聚合与透视表

这是 Pandas 最强大的功能——一行代码完成 Excel 里 10 分钟的数据透视:

# 模拟更真实的数据
data = {
    "日期": pd.date_range("2025-01-01", periods=100, freq="D"),
    "城市": np.random.choice(["北京", "上海", "深圳", "杭州"], 100),
    "品类": np.random.choice(["服装", "电子", "食品", "家居"], 100),
    "销售额": np.random.randint(500, 5000, 100),
    "数量": np.random.randint(1, 20, 100),
}
df = pd.DataFrame(data)
df["月份"] = df["日期"].dt.month

# ===== groupby():分组聚合 =====
# 按城市分组——看哪个城市卖得多
print(df.groupby("城市")["销售额"].agg(["count", "sum", "mean", "max"]))

# 按城市和品类——两级分组
print(df.groupby(["城市", "品类"])["销售额"].sum().unstack())

# 多列多聚合
result = df.groupby("城市").agg({
    "销售额": ["sum", "mean"],
    "数量": "sum",
})
print(result)

# ===== pivot_table():数据透视表 =====
pivot = pd.pivot_table(
    df,
    values="销售额",
    index="城市",
    columns="品类",
    aggfunc="sum",
    margins=True,          # 加合计行列
)
print(pivot)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

🖼️ groupby 执行流程:

原始数据                 groupby("城市")         聚合结果
┌────┬────┬──────┐      分拆              合并
│城市│品类│销售额│  →  [北京: rows...]  →  sum/mean
│北京│电子│ 3000│      [上海: rows...]
│北京│服装│ 1500│      [深圳: rows...]
│上海│食品│ 2000│      [杭州: rows...]
│深圳│电子│ 4500│
│上海│家居│ 1800│
└────┴────┴──────┘
1
2
3
4
5
6
7
8
9

# 2.3.4 缺失值与重复值处理

真实世界的 CSV 几乎没有干净的——处理缺失值和脏数据是数据分析 80% 的工作量:

# 制造脏数据
df2 = df.copy()
df2.loc[5:8, "销售额"] = np.nan       # 缺失
df2.loc[10, "数量"] = 999              # 异常值
df2 = pd.concat([df2, df2.iloc[:3]])   # 重复

# ① 检测缺失
print(df2.isnull().sum())              # 每列缺失数

# ② 处理缺失
# 填固定值
df2["销售额"] = df2["销售额"].fillna(0)
# 填均值
df2["销售额"] = df2["销售额"].fillna(df2["销售额"].mean())
# 填前值(时间序列常用)
df2["销售额"] = df2["销售额"].ffill()
# 删除含缺失的行
df2 = df2.dropna(subset=["销售额"])

# ③ 异常值处理——用条件过滤
df2 = df2[df2["数量"].between(1, 50)]   # 只保留 1~50

# ④ 去重
df2 = df2.drop_duplicates()
print(f"去重后:{len(df2)} 行")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 2.3.5 合并与连接

# 模拟订单表 + 客户表
orders = pd.DataFrame({
    "order_id": [1, 2, 3, 4],
    "customer_id": ["C001", "C002", "C001", "C003"],
    "amount": [150, 200, 120, 300],
})
customers = pd.DataFrame({
    "customer_id": ["C001", "C002", "C003"],
    "name": ["张三", "李四", "王五"],
    "level": ["VIP", "普通", "普通"],
})

# merge()——SQL JOIN 的等价物
merged = pd.merge(orders, customers, on="customer_id", how="left")
print(merged)
#    order_id customer_id  amount name level
# 0         1        C001     150   张三   VIP
# 1         2        C002     200   李四   普通
# 2         3        C001     120   张三   VIP
# 3         4        C003     300   王五   普通

# concat()——上下/左右拼接
combined = pd.concat([orders, orders])  # 纵向堆叠(两份)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

🔑 merge 的四种 how 参数:

how 含义 类比 SQL
"inner" 交集 INNER JOIN
"left" 保留左表全部 LEFT JOIN
"right" 保留右表全部 RIGHT JOIN
"outer" 并集 FULL OUTER JOIN

# 6.4 数据可视化

# 2.4.1 折线图与柱状图

import matplotlib.pyplot as plt
import numpy as np

# 配置中文字体
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "SimHei", "Heiti SC"]
plt.rcParams["axes.unicode_minus"] = False

# ===== 折线图 =====
months = ["1月", "2月", "3月", "4月", "5月", "6月"]
sales = [12000, 15000, 13500, 18000, 16000, 21000]

plt.figure(figsize=(10, 5))
plt.plot(months, sales, marker="o", color="#2196F3",
         linewidth=2, markersize=8, label="销售额")
plt.title("上半年销售趋势", fontsize=16, fontweight="bold")
plt.xlabel("月份")
plt.ylabel("销售额(元)")
plt.grid(True, alpha=0.3)
plt.legend()

# 在数据点上标注数值
for i, (x, y) in enumerate(zip(months, sales)):
    plt.text(x, y + 300, f"{y:,}", ha="center")

plt.savefig("line_chart.png", dpi=150, bbox_inches="tight")

# ===== 柱状图 =====
categories = ["服装", "电子", "食品", "家居", "运动"]
revenue = [45000, 82000, 38000, 29000, 21000]
colors = ["#4CAF50", "#2196F3", "#FF9800", "#9C27B0", "#F44336"]

plt.figure(figsize=(10, 5))
bars = plt.bar(categories, revenue, color=colors, edgecolor="white")
plt.title("各品类销售额", fontsize=16, fontweight="bold")
plt.ylabel("销售额(元)")

# 柱顶标注
for bar, val in zip(bars, revenue):
    plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 500,
             f"¥{val:,}", ha="center", fontweight="bold")

plt.savefig("bar_chart.png", dpi=150, bbox_inches="tight")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 2.4.2 散点图与饼图

# ===== 散点图——看两变量相关性 =====
np.random.seed(42)
n = 200
x = np.random.randn(n) * 15 + 100        # 广告投放(元)
y = x * 2.5 + np.random.randn(n) * 30    # 销售额(元)——与 x 正相关

plt.figure(figsize=(10, 6))
scatter = plt.scatter(x, y, c=y, cmap="viridis",
                      s=x/2, alpha=0.6, edgecolors="white")
plt.colorbar(scatter, label="销售额")
plt.title("广告投放 vs 销售额", fontsize=16, fontweight="bold")
plt.xlabel("广告投放(元)")
plt.ylabel("销售额(元)")

# 趋势线
z = np.polyfit(x, y, 1)
p = np.poly1d(z)
plt.plot(sorted(x), p(sorted(x)), "r--", linewidth=2, label="趋势线")
plt.legend()
plt.savefig("scatter_chart.png", dpi=150, bbox_inches="tight")

# ===== 饼图 =====
categories = ["服装", "电子", "食品", "家居", "其他"]
shares = [28, 35, 18, 12, 7]
explode = (0, 0.05, 0, 0, 0)   # 突出电子品类

plt.figure(figsize=(8, 8))
wedges, texts, autotexts = plt.pie(
    shares, labels=categories, autopct="%1.1f%%",
    explode=explode, colors=plt.cm.Paired.colors,
    startangle=90, textprops={"fontsize": 12},
)
plt.title("各品类销售占比", fontsize=16, fontweight="bold")
plt.savefig("pie_chart.png", dpi=150, bbox_inches="tight")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2.4.3 子图布局与样式定制

# 2×2 子图——一屏展示四维分析
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("销售数据多维分析面板", fontsize=18, fontweight="bold")

# 子图 1:月度趋势
ax1 = axes[0, 0]
ax1.plot(months, sales, "o-", color="#2196F3", linewidth=2)
ax1.set_title("月度销售趋势")
ax1.grid(alpha=0.3)

# 子图 2:城市对比
ax2 = axes[0, 1]
cities = ["北京", "上海", "深圳", "杭州"]
city_sales = [35000, 42000, 28000, 22000]
ax2.barh(cities, city_sales, color=["#4CAF50", "#2196F3", "#FF9800", "#9C27B0"])
ax2.set_title("各城市销售额")
for i, v in enumerate(city_sales):
    ax2.text(v + 500, i, f"¥{v:,}", va="center")

# 子图 3:品类分布(饼图)
ax3 = axes[0, 3]   # 占两个位置——这个不太对,应该用专门的位置
# 实际用 axes[1, 0/1] 来画别的

# 子图 3 & 4 使用同一个图——画分布直方图
# ...(在实战环节完整演示)

plt.tight_layout()
plt.savefig("dashboard.png", dpi=150, bbox_inches="tight")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 2.4.4 seaborn 快速美化

seaborn 基于 matplotlib,一行代码让图表变得专业:

import seaborn as sns
sns.set_style("whitegrid")
sns.set_palette("husl")

# 加载示例数据集
tips = sns.load_dataset("tips")
# 或自定义:df = pd.read_csv("sales.csv")

# ① 箱线图——看数据分布
plt.figure(figsize=(10, 5))
sns.boxplot(data=tips, x="day", y="total_bill", hue="smoker")
plt.title("不同日期的账单分布(按是否吸烟)", fontsize=14)

# ② 小提琴图——分布 + 密度
plt.figure(figsize=(10, 5))
sns.violinplot(data=tips, x="day", y="total_bill", inner="quartile")

# ③ 热力图——相关性矩阵
plt.figure(figsize=(8, 6))
# sns.heatmap(df.corr(), annot=True, cmap="coolwarm", fmt=".2f")
# 实战中会大量用到——见 §2.5

# ④ pairplot——散点图矩阵(看所有数值列两两关系)
# sns.pairplot(df, hue="品类")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

🔑 seaborn vs matplotlib 的定位:

Matplotlib Seaborn
底层引擎——完全控制 高层封装——默认好看
代码多但自由度高 一行出图但定制有限
plt.plot() plt.bar() sns.lineplot() sns.barplot()
适合定制化报表 适合探索性数据分析

# 6.5 综合实战

现在把三件套串联起来——10 万行电商 CSV → 清洗 → 多维度分析 → 可视化报告。

# 2.5.1 数据加载与清洗

"""
模块一:数据加载 + 清洗
"""

import pandas as pd
import numpy as np

# 1. 生成模拟的电商数据(实际使用时替换为 pd.read_csv("sales.csv"))
np.random.seed(42)

n = 100_000
dates = pd.date_range("2025-01-01", periods=n, freq="h")
df = pd.DataFrame({
    "order_id": range(1, n + 1),
    "date": dates,
    "category": np.random.choice(["服装", "电子", "食品", "家居", "运动", "美妆"], n,
                                  p=[0.20, 0.25, 0.20, 0.15, 0.10, 0.10]),
    "city": np.random.choice(["北京", "上海", "深圳", "杭州", "广州", "成都"], n),
    "amount": np.random.lognormal(mean=6, sigma=0.8, size=n).astype(int),
    "quantity": np.random.randint(1, 20, n),
    "member_level": np.random.choice(["普通", "银卡", "金卡", "钻石"], n,
                                      p=[0.50, 0.30, 0.15, 0.05]),
})

# 注入一些脏数据(模拟真实场景)
df.loc[np.random.choice(n, 200), "amount"] = np.nan      # 缺失 sales
df.loc[np.random.choice(n, 100), "quantity"] = 999        # 异常数量
df = pd.concat([df, df.iloc[:50]])                         # 重复 50 条

print(f"原始数据:{len(df)} 行")
print(f"缺失值数:{df.isnull().sum().sum()}")
print(f"数据类型:\n{df.dtypes}")
print(df.head())

# 2. 清洗
print("\n=== 开始清洗 ===")

# ① 填充缺失值——用品类中位数
df["amount"] = df.groupby("category")["amount"].transform(
    lambda x: x.fillna(x.median())
)

# ② 异常值——数量 > 50 视为异常
before = len(df)
df = df[df["quantity"].between(1, 50)]
print(f"异常值过滤:{before} → {len(df)} 行")

# ③ 去重
df = df.drop_duplicates(subset=["order_id"])
print(f"去重后:{len(df)} 行")

# ④ 日期拆分——方便后续分析
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["weekday"] = df["date"].dt.weekday     # 0=周一
df["hour"] = df["date"].dt.hour

# ⑤ 客单价计算
df["unit_price"] = (df["amount"] / df["quantity"]).round(0)

print(f"\n清洗完成:{len(df)} 行 × {len(df.columns)} 列")
print(df.describe())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 2.5.2 多维度分析

"""
模块二:多维度分析
"""

print("\n" + "=" * 60)
print("一、各品类销售概览")
print("=" * 60)
cat_stats = df.groupby("category").agg(
    总销售额=("amount", "sum"),
    订单数=("order_id", "count"),
    平均客单价=("amount", "mean"),
    平均数量=("quantity", "mean"),
).round(0)
cat_stats["销售额占比"] = (cat_stats["总销售额"] / cat_stats["总销售额"].sum() * 100).round(1)
print(cat_stats.sort_values("总销售额", ascending=False))

print("\n" + "=" * 60)
print("二、月度销售趋势")
print("=" * 60)
monthly = df.groupby("month").agg(
    销售额=("amount", "sum"),
    订单数=("order_id", "count"),
).round(0)
monthly["月环比"] = (monthly["销售额"].pct_change() * 100).round(1)
print(monthly)

print("\n" + "=" * 60)
print("三、城市 × 品类交叉分析")
print("=" * 60)
pivot = pd.pivot_table(
    df, values="amount", index="city", columns="category",
    aggfunc="sum", margins=True, margins_name="合计"
).round(0)
print(pivot)

print("\n" + "=" * 60)
print("四、会员等级分析")
print("=" * 60)
member_stats = df.groupby("member_level").agg(
    人数=("order_id", "count"),
    总消费=("amount", "sum"),
    人均消费=("amount", "mean"),
).round(0)
print(member_stats)

print("\n" + "=" * 60)
print("五、时段分析(按小时)")
print("=" * 60)
hourly = df.groupby("hour").agg(
    订单数=("order_id", "count"),
    销售额=("amount", "sum"),
).round(0)
# 找高峰
peak_hour = hourly["订单数"].idxmax()
print(f"下单高峰:{peak_hour}:00({hourly.loc[peak_hour, '订单数']} 单)")
print(hourly.sort_values("订单数", ascending=False).head(6))

print("\n" + "=" * 60)
print("六、品类备货建议")
print("=" * 60)
# 假设:过去 30 天的日均销量 × 品类重要性权重
recent_30 = df[df["date"] >= df["date"].max() - pd.Timedelta(days=30)]
stock_advice = recent_30.groupby("category").agg(
    日均销量=("quantity", "sum"),
).reset_index()
stock_advice["日均销量"] = (stock_advice["日均销量"] / 30).round(0)
stock_advice["建议备货量"] = (stock_advice["日均销量"] * 7).astype(int)  # 周备货
print(stock_advice.sort_values("日均销量", ascending=False))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 2.5.3 可视化报告

"""
模块三:可视化 + 导出报告
"""
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("Agg")
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False

fig = plt.figure(figsize=(16, 12))
fig.suptitle("📊 电商销售数据分析报告", fontsize=20, fontweight="bold", y=0.98)

# ============ 子图1:品类销售额(柱状图+占比) ============
ax1 = plt.subplot(2, 3, 1)
cats = cat_stats.sort_values("总销售额", ascending=True)
ax1.barh(cats.index, cats["总销售额"] / 10000, color=plt.cm.viridis(np.linspace(0, 1, len(cats))))
ax1.set_title("各品类销售额(万元)")
for i, (idx, row) in enumerate(cats.iterrows()):
    ax1.text(row["总销售额"] / 10000 + 10, i - 0.1,
             f"{row['总销售额']/10000:.0f}万 ({row['销售额占比']}%)",
             fontsize=8)

# ============ 子图2:月度趋势 ============
ax2 = plt.subplot(2, 3, 2)
ax2.plot(monthly.index, monthly["销售额"] / 10000, "o-", color="#E91E63", linewidth=2, markersize=8)
ax2.set_title("月度销售趋势(万元)")
ax2.set_xticks(monthly.index)
ax2.grid(alpha=0.3)
for m, v in zip(monthly.index, monthly["销售额"] / 10000):
    ax2.text(m, v + 3, f"{v:.0f}万", ha="center", fontsize=8)

# ============ 子图3:城市分布(饼图) ============
ax3 = plt.subplot(2, 3, 3)
city_data = df.groupby("city")["amount"].sum().sort_values(ascending=False)
ax3.pie(city_data.values, labels=city_data.index, autopct="%1.1f%%",
        startangle=90, colors=plt.cm.Set3.colors)
ax3.set_title("销售额城市分布")

# ============ 子图4:时段热力图替代——时段折线 ============
ax4 = plt.subplot(2, 3, 4)
ax4.fill_between(hourly.index, hourly["订单数"] / 1000, alpha=0.4, color="#4CAF50")
ax4.plot(hourly.index, hourly["订单数"] / 1000, color="#2E7D32", linewidth=2)
ax4.set_title("24 小时下单分布(千单)")
ax4.set_xlabel("小时")
ax4.axvline(peak_hour, color="red", linestyle="--", alpha=0.7)
ax4.text(peak_hour + 0.5, ax4.get_ylim()[1] * 0.9, f"高峰 {peak_hour}:00", color="red")

# ============ 子图5:会员等级消费(柱状图) ============
ax5 = plt.subplot(2, 3, 5)
level_order = ["普通", "银卡", "金卡", "钻石"]
member_data = member_stats.reindex(level_order)
colors_bar = ["#BDBDBD", "#9E9E9E", "#FFC107", "#FF5722"]
ax5.bar(level_order, member_data["人均消费"] / 1000, color=colors_bar, edgecolor="white")
ax5.set_title("各等级会员人均消费(千元)")
for i, (idx, row) in enumerate(member_data.iterrows()):
    ax5.text(i, row["人均消费"] / 1000 + 0.2, f"¥{row['人均消费']/1000:.1f}K",
             ha="center", fontweight="bold")

# ============ 子图6:品类客单价对比(箱线图概化——用均值) ============
ax6 = plt.subplot(2, 3, 6)
unit_data = df.groupby("category")["unit_price"].mean().sort_values()
ax6.bar(unit_data.index, unit_data.values, color=plt.cm.plasma(np.linspace(0, 1, len(unit_data))))
ax6.set_title("各品类平均客单价(元)")
ax6.set_ylabel("元/件")
for i, (name, val) in enumerate(unit_data.items()):
    ax6.text(i, val + 2, f"¥{val:.0f}", ha="center", fontsize=8)

plt.tight_layout(pad=3)
plt.savefig("ecommerce_report.png", dpi=150, bbox_inches="tight")
print("\n✅ 报告已保存:ecommerce_report.png")


# ============ 导出 Excel 报告(Pandas 原生的 ExcelWriter) ============
with pd.ExcelWriter("ecommerce_report.xlsx", engine="openpyxl") as writer:
    cat_stats.to_excel(writer, sheet_name="品类概览")
    monthly.to_excel(writer, sheet_name="月度趋势")
    pivot.to_excel(writer, sheet_name="城市×品类")
    member_stats.to_excel(writer, sheet_name="会员分析")
    hourly.to_excel(writer, sheet_name="时段分析")
    stock_advice.to_excel(writer, sheet_name="备货建议", index=False)
print("✅ Excel 报告已保存:ecommerce_report.xlsx")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

运行后得到:

  • ecommerce_report.png:6 合 1 可视化仪表板
  • ecommerce_report.xlsx:6 个 Sheet 的完整数据表——可直接发给老板

# 6.6 新手陷阱

# 陷阱 说明
1 DataFrame 修改返回 None df = df.dropna() 而不是 df.dropna()——Pandas 大部分操作返回新对象
2 SettingWithCopyWarning df[df["a"]>0]["b"] = 0 修改的是临时副本——用 df.loc[df["a"]>0, "b"] = 0
3 中文显示方块 plt.rcParams["font.sans-serif"] 没有设置——Mac 用 "Arial Unicode MS",Win 用 "SimHei"
4 groupby 后忘记 reset_index df.groupby(...).sum() 返回多级索引——加 .reset_index() 回到平表
5 NaN 参与比较永远 False np.nan == np.nan → False——用 pd.isna() 或 df.dropna() 处理

陷阱 2 详解——Pandas 链式赋值:

# ❌ 错误——触发 SettingWithCopyWarning
df[df["销售额"] > 1000]["品类"] = "高价值"
# 问题:df[df["销售额"] > 1000] 返回的是副本还是视图?不确定!

# ✅ 正确——用 loc
df.loc[df["销售额"] > 1000, "品类"] = "高价值"   # 明确索引,一步到位
1
2
3
4
5
6

# 6.7 综合思考题

  1. NumPy 广播 vs Python 循环:(a[:, np.newaxis] + b) 和 [[x+y for y in b] for x in a] 结果一样——前者每秒处理千万级数据,后者百万级就卡。广播机制在什么情况下不能使用?形状不兼容时会怎样?

  2. groupby().agg() 的性能:1000 万行数据按城市分组求销售额和——Pandas 的 groupby 需要几十秒。如果每天都要跑这个查询,你会怎么做?有哪些加速方案?(提示:Dask、Polars、数据库物化视图)

  3. Matplotlib 的面向对象 vs pyplot:plt.plot() 和 fig, ax = plt.subplots(); ax.plot() 都可以画图——它们的本质区别是什么?为什么生产级代码推荐后一种?

  4. Pandas 链式操作 vs SQL:df.query("category == '电子'").groupby("city")["amount"].mean() 这行代码,如果数据在 PostgreSQL 里用 SQL 写,哪个更快?什么场景该用 SQL,什么场景该用 Pandas?

  5. DataFrame 的内存优化:100 万行数据的 int64 列占 ~8MB——如果这一列实际只有 0/1/2/3 四个值,用 astype("int8") 可以压缩到 1MB。但如果是字符串列(object dtype),每个字符串都是一个 Python 对象——内存可能暴增到 100MB。Pandas 2.0 的 StringDtype 和 CategoricalDtype 是怎么解决这个问题的?

#Python#实战
上次更新: 2026/06/17, 12:47:39
爬虫全流程实战
办公自动化实战

← 爬虫全流程实战 办公自动化实战→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式