为什么数据科学从 NumPy 开始
Python 之所以成为数据科学的主流语言,不是因为语法优雅,而是因为它有一套层次分明的计算生态:NumPy 负责同构数值数组,Pandas 负责带标签的表格数据,Matplotlib 负责出图。三者叠加,覆盖了从读取到分析再到呈现的完整链路。
很多人跳过 NumPy 直接学 Pandas,结果遇到性能问题或维度问题时无从下手。实际上 Pandas 的底层就是 NumPy,理解 ndarray 的形状、广播与向量化,是后面一切的基础。
数据分析的效率,取决于你能不能把循环换成向量化运算——这是 NumPy 教给你的第一课,也是最重要的一课。
如果你的环境还没装好,一句 pip install numpy pandas matplotlib 就够用了。接下来我们按“数组 → 表格 → 清洗 → 聚合 → 出图”的顺序推进。
ndarray:向量化运算的威力
ndarray 的核心特征是同构(所有元素类型一致)和定长(形状固定),这让它可以连续存储、批量运算。把 Python 的 for 循环换成数组运算,速度常常有几十倍的差距。
- 形状shape 表示各维长度,reshape 与 [:, None] 是构造多维数据最常用的手法。
- 向量化用 arr * 2 替代逐元素循环,由底层 C 代码执行。
- 广播形状不同的数组按规则自动对齐,避免手工复制数据。
- 布尔索引arr[arr > 0] 直接完成筛选,是 Pandas 条件筛选的底层机制。
一个常见陷阱是原地操作:arr += 1 会修改原数组,而 arr = arr + 1 会新建对象。在函数里传参时要特别小心,必要时用 arr.copy() 明确复制。
Pandas 的两大数据结构
Pandas 提供了两个结构:Series(带索引的一维数组)和 DataFrame(带行列标签的二维表)。可以把 DataFrame 理解为“多个共享索引的 Series”,这也是它按列运算高效、按行遍历缓慢的原因。
import pandas as pd
import numpy as np
df = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004, 1005],
"city": ["佛山", "广州", "佛山", "深圳", "广州"],
"amount": [128.0, 89.5, np.nan, 320.0, 45.0],
"qty": [2, 1, 3, 5, 1],
})
print(df.dtypes) # 逐列类型
print(df.head(3)) # 前 3 行
print("平均客单价:", round(df["amount"].mean(), 2)) # nan 被自动跳过 → 145.62
选择数据时优先用 loc(按标签)和 iloc(按位置),避免链式索引 df[df.a > 1]["b"],因为它可能返回视图而非副本,赋值时会静默失效——这是最常见也最难排查的 Pandas 陷阱。
数据清洗:缺失值、重复与类型
真实数据一定脏。清洗通常占整个分析流程六成以上的时间,标准动作有四个。
- 缺失值:先判断是“随机缺失”还是“系统性缺失”。数值用中位数填充,类别用众数或单列“未知”,缺失比例过高的列直接删除。
- 重复值:用 subset 指定判定列做去重,保留最新一条。
- 类型与格式:金额、日期、ID 常被读成字符串,需要显式转换并处理异常值。
- 异常值:用分位数或业务规则识别,不要无脑删除——异常往往才是分析的重点。
# 清洗:类型转换、去重、派生列
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df = df.dropna(subset=["amount"]).drop_duplicates(subset=["order_id"])
df["city"] = df["city"].astype("category")
df["unit_price"] = df["amount"] / df["qty"]
print(df.isna().sum()) # 逐列统计缺失数量
print(df.describe()) # 数值列的分布概况
建议把每一步清洗都写成独立函数并加注释:数据管道要被别人读懂,也要能在三个月后重新跑一遍。把清洗逻辑留在 Notebook 的临时单元格里,是团队协作中最容易埋雷的做法。
分组聚合:用 groupby 回答问题
groupby 是回答业务问题的主力:把数据按某个维度切分,再对每个分组做聚合。它的心智模型是“拆分 → 应用 → 合并”。
常见聚合包括 sum、mean、count、nunique(去重计数)以及自定义函数。多列聚合用 agg 传字典或命名元组;分组结果要保留原表宽度时用 transform 而不是 agg——这是最容易踩的坑之一,用错了会得到形状不匹配的报错。
透视表 pivot_table 可以理解为 groupby 的表格化表达,把行、列、值、聚合方式一次配齐,输出直接可读。做月度 × 品类的交叉分析时,它比手写多层 groupby 清晰得多。
可视化:把结论画出来
图表的价值是让结论在一秒内被理解。三个基本原则:一图一结论,不要把五条曲线塞进同一张图;坐标轴从零开始(除非有充分理由截断);标注单位与样本量。
常用图形与用途:折线图看趋势,柱状图比大小,箱线图看分布与离群点,散点图看相关性与聚类倾向,热力图看二维交叉强度。Matplotlib 负责精细控制,Seaborn 负责统计图形的快速出图。
作图前先明确你想让读者记住什么,再选图形;而不是先选一个好看的图,再想办法塞数据。另外记得在服务器或无界面环境里把后端设为 Agg,否则会因为缺少图形界面直接报错。
一个完整的订单分析案例
把前面的内容串起来,用一份模拟订单数据回答三个问题:各城市销售额排名、客单价分布、以及销售额与数量的关系。
import pandas as pd, numpy as np
import matplotlib
matplotlib.use("Agg") # 无界面环境保存图片
import matplotlib.pyplot as plt
rng = np.random.default_rng(7)
raw = pd.DataFrame({
"order_id": range(1, 201),
"city": rng.choice(["佛山", "广州", "深圳", "东莞"], 200),
"qty": rng.integers(1, 6, 200),
})
raw["amount"] = raw["qty"] * rng.normal(60, 15, 200).round(1)
raw.loc[rng.choice(raw.index, 15), "amount"] = np.nan # 制造缺失
# 1) 清洗
d = raw.dropna(subset=["amount"]).copy()
d["unit_price"] = (d["amount"] / d["qty"]).round(2)
# 2) 各城市销售额排名(命名聚合,输出列名可读)
by_city = (d.groupby("city")
.agg(orders=("order_id", "count"),
sales=("amount", "sum"),
avg_price=("unit_price", "mean"))
.round(2)
.sort_values("sales", ascending=False))
print(by_city)
# 3) 出图
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(by_city.index, by_city["sales"], color="#4A9DF2")
ax.set_title("City Sales Ranking")
ax.set_ylabel("Sales")
fig.tight_layout()
fig.savefig("city_sales.png", dpi=140)
几点说明:模拟数据让示例可以独立运行,真实项目里把读取换成 pd.read_csv 即可;groupby 用命名聚合能让输出列名可读;matplotlib 在服务器上必须设置 Agg 后端。继续扩展的方向包括把 unit_price 的分布改用箱线图呈现、按月份分组看趋势、或把结果导出为 Excel 报表交给业务方。
性能与习惯:让代码跑得更快
当数据量从几万行涨到几百万行,几处习惯的差别会放大成几十倍的性能差距。
- 避免 apply 逐行处理,优先用向量化或内置聚合;确实需要循环时,用 itertuples 而不是 iterrows。
- 读大文件时用 usecols 只取需要的列,用 dtype 指定类型;分类字段用 category 类型可显著减小内存。
- 把重复使用的中间结果缓存到 Parquet 而不是 CSV,读写更快且保留数据类型。
- 超过单机内存规模时,考虑 Polars、Dask 或直接下沉到数据库做聚合,而不是硬扛。
先让结果正确,再让代码变快;但不要等到数据长了十倍才想起性能。
下一步如果你要做机器学习,可以读本模块的《机器学习基础》,把这里清洗好的 DataFrame 直接喂给 scikit-learn 的模型。