DD 数据开发学习站DATA ENGINEERING
模块 01 · 开始

Git 与 Python 基础

Git 版本管理与 Python/pandas 基础,为 Spark 和项目交付做准备。

难度 · 入门 约 210 分钟 必修 练习 6 项 计划内
建议先学:Linux 与 Shell 基础
本页目录 · 29 节

本章目标:能用 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 的分母为什么是有效订单数。

下一章进入 SQL 基础。真实项目的字段和口径在完成项目线时再查项目数据字典。

本章任务0 / 6
本页目录