本章目标:能用 Git 管理代码和回滚错误,能用 Python 读写文件,能用 pandas 完成筛选、分组、关联和导出。 前置知识:第 02 章环境已跑通;会一点命令行。 预计用时:3–4 小时,Git 和 pandas 都要动手敲。
1. 为什么需要它
数据开发的项目不只有 SQL。建表脚本、清洗程序、调度配置、指标文档都要版本管理;临时处理 CSV、调用接口、跑 PySpark,也离不开 Python。
面试常追问两句:“你的项目代码在哪,能看提交记录吗?”和“这段清洗逻辑用 Python 怎么写?”前者考 Git,后者考 Python/pandas。不需要开发工程师的深度,但要能独立完成日常操作。
2. 核心概念
2.1 Git 在解决什么问题
Git 记录每次提交的快照:谁改了什么、为什么改。改错能回滚,多人能各开分支再合并。
- 工作区:正在编辑的文件。
- 暂存区:
git add后等待提交的内容。 - 本地仓库:
git commit后的历史,存在.git目录。 - 远程仓库:GitHub 等平台上的副本,用
push/pull同步。
2.2 一个数仓项目的目录
shop-warehouse/
├── .gitignore
├── README.md
├── requirements.txt
├── src/ # Python 脚本
├── sql/ # 建表、清洗、汇总 SQL
├── data/raw/ # 原始 CSV,通常不提交
└── output/ # 运行结果,通常不提交
原则:代码、SQL、文档进 Git;原始数据、虚拟环境、运行结果不进 Git。
2.3 日常 Git 流程
在空目录初始化练习项目:
mkdir shop-warehouse && cd shop-warehouse
git init -b main
git config user.name "你的名字"
git config user.email "you@example.com"
mkdir -p src
printf "# 电商数仓练习\n" > README.md
git status
git add README.md
git commit -m "docs: 初始化项目说明"
git log --oneline
git config 只对当前仓库生效。最常用的命令:
| 命令 | 作用 | 使用时机 |
|---|---|---|
git status |
看改动和暂存状态 | 每次 add/commit 前后 |
git diff |
看未暂存改动 | 提交前检查 |
git diff --staged |
看已暂存改动 | git add 之后 |
git add <文件> |
放进暂存区 | 提交前 |
git commit -m "说明" |
生成一次提交 | 完成一个独立小改动 |
git log --oneline |
看提交历史 | 回顾和排查 |
提交信息写“做了什么”,例如 feat: 增加品类 GMV 汇总、fix: 修正空用户过滤,不要写“update”。
2.4 分支与合并
git switch -c feature/category-gmv
# 编辑 src/category_gmv.py
git add src/category_gmv.py
git commit -m "feat: 增加品类 GMV 统计"
git switch main
git merge feature/category-gmv
git branch -d feature/category-gmv
冲突时文件会出现 <<<<<<<、=======、>>>>>>> 三行标记。手工改成最终版本,删掉标记,再 git add、git commit。不要直接删掉对方的改动。
2.5 远程仓库与 Pull Request
在 GitHub 新建空仓库,不要自动生成 README,然后:
git remote add origin https://github.com/<你的用户名>/shop-warehouse.git
git push -u origin main
之后用 git pull --rebase origin main 同步、git push 上传。PR 流程:从 main 开功能分支,提交并推送;在 GitHub 点 “Compare & pull request”;自己先看 diff,确认没有密钥和大文件;合并后切回 main 执行 git pull。一个人做项目也建议走一次 PR,面试时能讲清流程。
2.6 撤销与回滚
| 场景 | 命令 | 说明 |
|---|---|---|
| 改了还没 add | git restore README.md |
丢弃未暂存改动 |
| add 了还没 commit | git restore --staged README.md |
退回工作区,改动还在 |
| commit 信息写错,未 push | git commit --amend -m "新说明" |
修改最近一次提交 |
| 撤销最近一次 commit,保留改动 | git reset --soft HEAD~1 |
改动回到暂存区 |
| 已 push 的提交要撤销 | git revert <commit> |
生成反向提交,历史保留 |
| 彻底丢弃本地改动和提交 | git reset --hard HEAD~1 |
危险,先确认 |
| 误删分支或提交 | git reflog |
找到记录后恢复 |
git reset --hard 会删除未提交内容;已推送的提交不要用 reset 改写历史,用 revert。
2.7 .gitignore
项目根目录创建 .gitignore:
.venv/
__pycache__/
*.pyc
output/
data/raw/
.env
验证:git check-ignore -v data/raw/example.csv。如果数据已提交过,先取消跟踪:
git rm -r --cached data/raw
git add .gitignore
git commit -m "chore: 忽略原始数据"
本地文件还在,但历史提交里仍有旧文件。提交过密钥时要立刻更换密钥,再清理历史。
2.8 Python 最小语法
from pathlib import Path
order_id = "O500000"
amounts = [981.13, 450.00]
order = {"order_id": order_id, "amount": 981.13, "status": "delivered"}
for row in [order]:
if row["status"] != "canceled":
print(row["order_id"], row["amount"])
def total(rows):
return sum(r["amount"] for r in rows if r["status"] != "canceled")
print(total([order]))
path = Path("output/summary.txt")
path.parent.mkdir(exist_ok=True)
try:
with open(path, "w", encoding="utf-8") as f:
f.write("GMV=15740216.93\n")
except OSError as e:
print(f"写入失败:{e}")
要点:列表用 [],字典用 {};循环和函数用缩进表示代码块;with open(...) 自动关闭文件;encoding="utf-8" 防乱码;try/except 让程序在文件或权限出错时给出可读提示。
2.9 venv 与 pip
每个项目用独立虚拟环境,避免包装到系统 Python:
| 动作 | Windows CMD | Git Bash |
|---|---|---|
| 创建 | python -m venv .venv |
python -m venv .venv |
| 激活 | .venv\Scripts\activate |
source .venv/Scripts/activate |
| 安装 | python -m pip install pandas |
python -m pip install pandas |
| 退出 | deactivate |
deactivate |
激活后命令行前会出现 (.venv)。导出和恢复:python -m pip freeze > requirements.txt、python -m pip install -r requirements.txt。PowerShell 提示禁止运行脚本时,改用 Git Bash 或调整当前用户执行策略。
2.10 pandas 五个动作
| 动作 | 代码 | 说明 |
|---|---|---|
| 读取 | pd.read_csv("data/sample/orders_demo.csv") |
可加 parse_dates=["order_date"] |
| 筛选 | df[df["status"] == "canceled"] |
多条件用 &,每个条件加括号 |
| 分组 | df.groupby("status")["order_id"].count() |
计数、求和、平均 |
| 关联 | df.merge(other, on="order_id", how="left") |
类似 SQL JOIN |
| 导出 | df.to_csv("output/result.csv", index=False) |
给 Excel 看用 utf-8-sig |
先看 df.shape、df.head()、df.isna().sum(),再写复杂逻辑。
2.11 pandas 处理脏数据的四个动作
真实 CSV 不会像示例一样干净。下面四个动作覆盖实习中最常见的清洗场景,每一行都给出 SQL 对照。
动作一:看空值和重复
orders = pd.read_csv("data/sample/orders_demo.csv", parse_dates=["order_date"])
print(orders.isna().sum()) # 每列空值数
print(orders.duplicated().sum()) # 完全重复行数
print(orders["status"].value_counts(dropna=False))
-- SQL 对照
SELECT COUNT(*) - COUNT(amount) AS amount_null_cnt FROM orders;
SELECT COUNT(*) - COUNT(DISTINCT order_id) AS dup_order_cnt FROM orders;
示例表没有空值和完全重复行。真实数据不会这么干净:要逐列看空值数,并区分业务空值和缺陷;主键重复通常意味着上游同步或 JOIN 出了问题。
动作二:保留或填充空值
# 业务空值:保留,分析某种状态时再排除
shipped = orders[orders["status"] == "shipped"]
# 可填充空值:品类缺失补 unknown,并保留一个标记列
items = pd.read_csv("data/sample/order_items_demo.csv")
items["category_filled"] = items["category"].fillna("unknown")
SELECT COALESCE(category, 'unknown') AS category_filled FROM order_items;
空值不能一律 fillna(0) 或 dropna()。先问业务含义:shipped 订单没有签收时间,是业务事实;商品品类为空可以补 unknown 并单列展示;主键为空不能伪造一个值。
动作三:去重和过滤
orders = orders.drop_duplicates(subset="order_id")
items = pd.read_csv("data/sample/order_items_demo.csv")
items = items[items["price"] >= 0] # 示例没有负价,规则仍要保留
print(items.shape)
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY order_id
ORDER BY order_date) AS rn
FROM orders
) t
WHERE rn = 1;
动作四:类型转换和字符串处理
orders["order_date"] = pd.to_datetime(orders["order_date"], errors="coerce")
orders["status_upper"] = orders["status"].str.upper()
orders["order_month"] = orders["order_date"].dt.to_period("M")
print(orders[["order_id", "status_upper", "order_month"]].head())
SELECT CAST(order_date AS DATE) AS order_date,
UPPER(status) AS status_upper,
DATE_TRUNC('month', order_date) AS order_month
FROM orders;
errors="coerce" 会把无法解析的日期变成 NaT,而不是让整个脚本报错。转换后要统计 NaT 数量,确认有多少脏数据被吞掉:
bad_dates = orders["order_date"].isna().sum()
print("无法解析的日期:", bad_dates)
与 PySpark 的对应:pandas 的
groupby、merge、fillna、drop_duplicates在 PySpark DataFrame 里几乎同名;差别是 Spark 是分布式、惰性执行,show()才会真正计算。先把 pandas 写顺,后面学 PySpark 只需换 API。
3. 动手示例
以下命令都在教程项目根目录执行。
3.1 示例一:订单状态盘点
创建 src/order_status.py:
import pandas as pd
orders = pd.read_csv("data/sample/orders_demo.csv", parse_dates=["order_date"])
print(orders.shape)
print(orders["status"].value_counts().to_string())
print("canceled:", (orders["status"] == "canceled").sum())
print(orders.loc[orders["status"] == "canceled",
["order_id", "order_date"]].to_string(index=False))
运行 python src/order_status.py,预期 shape 为 (12, 5),状态分布是 paid 8、canceled 2、shipped 1、unavailable 1,取消单 2 行。
3.2 示例二:清洗后的 GMV 与品类 Top5
创建 src/category_gmv.py:
from pathlib import Path
import pandas as pd
raw = Path("data/sample")
orders = pd.read_csv(raw / "orders_demo.csv", parse_dates=["order_date"])
items = pd.read_csv(raw / "order_items_demo.csv")
valid = orders[~orders["status"].isin(["canceled", "unavailable"])]
items["category"] = items["category"].fillna("unknown")
d = items.merge(valid[["order_id"]], on="order_id")
d["gmv"] = d["price"]
d["gmv_freight"] = d["price"] + d["freight"]
print("有效明细:", len(d))
print("GMV(含运费):", round(d["gmv_freight"].sum(), 2))
print("AOV:", round(d.groupby("order_id")["gmv_freight"].sum().mean(), 2))
r = d.groupby("category")["gmv"].sum().sort_values(ascending=False)
print(r.head().round(2).to_string())
Path("output").mkdir(exist_ok=True)
r.to_csv("output/category_gmv.csv", encoding="utf-8-sig")
运行 python src/category_gmv.py,关键输出:
有效明细: 9
GMV(含运费): 1509.0
AOV: 167.67
category
数码 740.0
家居 310.0
服饰 200.0
四步口径:只保留 canceled、unavailable 以外的有效订单;明细用 merge 关联有效订单,天然过滤掉取消单明细;品类缺失补 unknown;GMV 含运费,品类排名用不含运费的 gmv。真实项目只是把 12 行换成 55 万行,口径和检查步骤完全一样。
3.3 把代码提交到 Git
git status
git add src/category_gmv.py
git diff --staged
git commit -m "feat: 增加品类 GMV 统计"
git log --oneline
提交前看 git diff --staged,确认没有输出 CSV、虚拟环境或密钥。
4. 常见坑
| 坑 | 表现 | 处理 |
|---|---|---|
| 在错误目录运行 | FileNotFoundError: data/raw/... |
从项目根目录执行 |
data/raw 被提交 |
仓库突然变大 | git rm --cached 后写 .gitignore |
直接 git reset --hard |
本地改动全没了 | 先看 status/diff,必要时 git reflog |
| pandas 多条件报错 | & 两侧没括号 |
每个条件加括号,用 & 不用 and |
| merge 后行数暴涨 | 关联键有重复 | 先检查右表键是否唯一 |
| 中文 CSV 乱码 | 编码不是 BOM | to_csv(..., encoding="utf-8-sig") |
5. 练习
练习 1:完成第一次提交
在空目录初始化 Git,创建 README 和 .gitignore,完成第一次提交,并验证 data/raw 下的 CSV 不会被跟踪。
参考答案
git init -b main
printf "# 数仓练习\n" > README.md
printf "data/raw/\noutput/\n.venv/\n" > .gitignore
git add README.md .gitignore
git commit -m "docs: 初始化项目"
git check-ignore -v data/raw/example.csv
git status 应显示工作区干净;check-ignore 能命中规则。
练习 2:开分支并合并
新建 feature/order-status 分支,添加 src/order_status.py,提交后合并回 main。
参考答案
git switch -c feature/order-status
git add src/order_status.py
git commit -m "feat: 增加订单状态统计"
git switch main
git merge feature/order-status
git log --oneline --graph
合并成功后用 git branch -d feature/order-status 删除分支。
练习 3:误提交数据文件
不小心提交了 data/raw/example.csv,后来才加 .gitignore。写出取消跟踪并保持本地文件不动的命令。
参考答案
git rm -r --cached data/raw
git add .gitignore
git commit -m "chore: 停止跟踪原始数据"
本地文件还在。文件里有密钥时还要更换密钥并清理历史。
练习 4:用 Python 过滤并汇总
给定订单字典列表,写函数计算有效订单金额总和:取消单、负金额、零数量都不算。
参考答案
orders = [
{"price": 100, "quantity": 2, "freight": 10, "status": "delivered"},
{"price": -5, "quantity": 1, "freight": 0, "status": "delivered"},
{"price": 50, "quantity": 1, "freight": 5, "status": "canceled"},
]
def total(rows):
return sum(row["price"] * row["quantity"] + row["freight"]
for row in rows
if row["status"] != "canceled"
and row["price"] > 0 and row["quantity"] > 0)
print(total(orders))
结果是 210。
练习 5:统计有效订单最多的城市
用 pandas 统计有效订单数最多的 5 个城市(city),有效订单指 canceled、unavailable 以外的订单。
参考答案
import pandas as pd
orders = pd.read_csv("data/sample/orders_demo.csv")
valid = orders[~orders["status"].isin(["canceled", "unavailable"])]
result = (valid.groupby("city")["order_id"].nunique()
.sort_values(ascending=False))
print(result.head())
结果:北京 3、上海 2、广州 2、深圳 1、杭州 1。有效订单共 9 条。
练习 6:复现 AOV
用 pandas 关联订单和明细,计算清洗后的客单价,目标值 167.67,并说明分母是多少。
参考答案
import pandas as pd
orders = pd.read_csv("data/sample/orders_demo.csv")
items = pd.read_csv("data/sample/order_items_demo.csv")
valid = orders[~orders["status"].isin(["canceled", "unavailable"])]
detail = items.merge(valid[["order_id"]], on="order_id")
detail["gmv"] = detail["price"] + detail["freight"]
print(round(detail.groupby("order_id")["gmv"].sum().mean(), 2))
输出 167.67。分母是有明细的有效订单数 9,不是原始订单行数 12。一张订单有多条明细时,要先把金额按订单汇总再求平均。
6. 面试延伸
面试题:Git 的 add 和 commit 有什么区别?
是什么:add 把工作区改动放进暂存区,commit 把暂存区保存成一次带说明的历史快照。
解决什么问题:分两步可以只提交本次相关的改动,避免把调试代码和临时文件带进去。git diff 看工作区,git diff --staged 看暂存区。
我的实操边界:我在本地项目里用 add/commit/branch/merge 管理 SQL 和 Python 脚本,走过一次 PR 流程;多人协作的冲突处理和发布回滚没有在生产项目里做过。
面试题:pandas 的 merge 和 SQL 的 JOIN 有什么关系?
是什么:两者都按关联键拼表,how="inner/left/right/outer" 对应 SQL 的 INNER/LEFT/RIGHT/FULL JOIN。
解决什么问题:明细表要关联维表补分类。关联前确认右表键唯一,否则一对多会让行数膨胀、指标重复计算。
我的实操边界:我在本地用 merge 关联过订单、明细、商品和用户表,并通过对账行数确认没有膨胀;没有处理过亿级表的关联性能问题。
7. 本章验收
- 能独立完成
git init、add、commit、status、log、diff六个操作。 - 能开分支、合并分支,并说清
.gitignore为什么不能提交data/raw、output、.venv。 - 能用
git restore、git reset --soft、git revert分别处理三种撤销场景。 - 能写带列表、字典、循环、函数、文件读写和
try/except的 Python 脚本。 - 能运行本章两个 pandas 示例,输出 GMV 1,509.0、AOV 167.67 和品类 Top3。
- 能完成至少 6 个练习中的 5 个,并解释 AOV 的分母为什么是有效订单数。