# 数据分析 · 第 08 篇|工具使用:Excel、SQL、Python、BI > 系列:project / 数据分析(共 12 篇)|AI生成|2026-10-27|codebuddy **这一篇讲什么** 上一篇讲方法选择,最后留了一句话:方法不由工具定义。这一篇把这句话落地,回答四个更具体的问题:Excel、SQL、Python、BI 各自最强的地方在哪,什么活不该交给它们;一个从零开始的人该按什么顺序学;每种工具最少要会哪几招,才能独立完成北辰优选的渠道分析;以及几个人协作时,怎么让结果不烂在自己电脑里。文中给出两段完整 SQL、三段 Python 和一组 Excel 公式,都可以直接换成北辰的表名跑起来。读完的目标不是"四种都会",而是"拿到一个任务,知道该打开哪个窗口"。 ## 一、工具地图:四种工具各自最强的地方 先把结论放前面:Excel 强在 exploratory(边看边想),SQL 强在取数与聚合,Python 强在清洗、建模与自动化,BI 强在持续监控与共享。四者不是替代关系,而是一条流水线上的四道工序。 | 工具 | 最强场景 | 典型任务 | 不该用的场景 | | --- | --- | --- | --- | | Excel / Sheets | 数据量小、要边看边改、要快速对齐口径 | 透视、临时核算、函数查找、图表草稿 | 十万行以上明细、多人同时改、需要复现 | | SQL | 数据在库里、要按口径聚合、要交给别人复用 | 取数、关联、分组汇总、窗口函数算首单与留存 | 复杂统计建模、画图、非结构化文本处理 | | Python | 清洗规则复杂、要建模、要定时跑 | 缺失值处理、特征工程、回归分类、批量出图 | 一次性小表、需要业务方自己改的场景 | | BI | 同一套指标要天天看、要给不同角色看 | 指标卡、趋势看板、筛选下钻、权限分发 | 探索性分析、临时口径试验、复杂建模 | | Notebook | 分析过程要留痕、要交付可复现的证据 | 探索记录、模型实验、图文报告 | 生产调度、多人协作的稳定报表 | 这张表最容易被误读成"高级工具更好"。实际相反:北辰华东站 3 月只有 5,150 笔支付订单,用 Python 写三十行做渠道汇总,比 Excel 透视表慢,而且业务方改不动。工具的"高级"体现在它能处理更复杂的约束,不体现在它更专业。 另一个常见误解是"BI 就是画图工具"。BI 真正的价值是**把口径固定下来**:同一个 GMV 指标,在五个人的 Excel 里有五种算法,在 BI 里只有一个定义。这件事的价值远高于图表好看。 ## 二、学习顺序:从业务感到自动化监控 学习顺序不该按工具难度排,该按"你能独立回答的问题"排。下面四个阶段,每个阶段都有一个可验收的产出。 | 阶段 | 目标 | 主要工具 | 验收产出 | 大致投入 | | --- | --- | --- | --- | --- | | 一、建立业务感 | 知道一张表里哪些字段有业务含义 | Excel | 能用透视表回答"哪个渠道卖得好" | 一到两周 | | 二、会取数 | 不依赖别人给现成表 | SQL | 能独立写出月度 GMV 与首单查询 | 三到四周 | | 三、会处理与建模 | 脏数据能洗干净,能跑通一个模型 | Python | 能交付一份带清洗和验证的分析脚本 | 四到八周 | | 四、会监控与协作 | 结果能被别人持续使用 | BI 加 Git | 有一张每天自动刷新的看板和一个版本库 | 持续 | 顺序不能倒。先学 Python 再学 SQL 的人,常见问题是"拿到数据不知道口径对不对",因为没在库里见过原始表;先学 BI 的人,常见问题是"只会拖字段,遇到脏数据就卡住"。Excel 阶段看似最土,但它训练的是对数字的直觉:同样的 GMV,拆成订单数和客单价,你能立刻判断哪个数不合理。这种直觉在 SQL 和 Python 里同样管用。 每个阶段都不要追求学完。Excel 学到透视表加四个函数就够用很久;SQL 学到窗口函数就能覆盖八成取数需求;Python 学到 pandas 的读写、筛选、分组、合并、透视,加一个画图库,就能做绝大多数分析。剩下的按需查。 ## 三、Excel:透视表、查找与条件聚合 北辰华东站的订单表导出后有 12,000 行,其中 5,150 行是 paid;这是用于工具演示的区域样本,与第 01—06 篇的全站案例不是同一口径。要做一张"分渠道 GMV"的表,标准动作是:先用 user_id 把渠道带进订单表,再按渠道和状态聚合。带渠道用 XLOOKUP,聚合用 SUMIFS 或透视表。 | 渠道 | 支付订单数 | GMV(元) | 客单价(元) | GMV 占比 | | --- | --- | --- | --- | --- | | 信息流 | 1,980 | 496,000 | 250.5 | 32.1% | | 搜索 | 1,150 | 393,300 | 342.0 | 25.5% | | 老客推荐 | 880 | 339,700 | 386.0 | 22.0% | | 自然访问 | 1,140 | 315,000 | 276.3 | 20.4% | | 合计 | 5,150 | 1,544,000 | 299.8 | 100% | 这张表有两个细节值得注意。第一,占比合计要正好 100%,差 0.1 个百分点通常是四舍五入,差更多就是有订单没匹配上渠道,必须查。第二,客单价不能对四个渠道的客单价再取平均,那会得到 313.7 元,而整体客单价是 299.8 元——平均的平均不等于整体平均,除非各渠道订单数相同。 常用公式如下,把表名和列号换成自己的即可。 ```text ' 1. 用 user_id 带出渠道;找不到时返回"未匹配",避免出现 #N/A 污染透视 =XLOOKUP($B2, users!$A:$A, users!$D:$D, "未匹配") ' 2. 不支持 XLOOKUP 的版本,用 INDEX + MATCH,效果相同且不依赖列顺序 =INDEX(users!$D:$D, MATCH($B2, users!$A:$A, 0)) ' 3. 分渠道支付 GMV:SUMIFS 支持多条件,这里同时限定状态与渠道 =SUMIFS(orders!$D:$D, orders!$F:$F, "paid", orders!$B:$B, $B2) ' 4. 分渠道支付订单数 =COUNTIFS(orders!$F:$F, "paid", orders!$B:$B, $B2) ' 5. 客单价,分母为 0 时留空而不是报错 =IFERROR(C2/D2, "") ' 6. 去重取渠道清单;动态数组版本可用 UNIQUE,老版本用"数据 - 删除重复项" =UNIQUE(users!D2:D10001) ``` 透视表的操作路径是:选中订单明细,插入数据透视表,行放 channel,值放 amount(求和)和 order_id(计数),筛选器放 status 并勾选 paid。要用客单价,就在透视表里加"计算字段",公式写 amount 除以 order_id 的计数,而不是在外面另起一列——后者一刷新就错位。 透视表和公式各有位置。透视表适合"先看结构、再决定看什么",拖一下就是一个新视角,代价是结果不能直接引用、刷新后单元格位置会变;SUMIFS 适合"结构已经定了、要嵌进固定模板的报表",结果稳定,但每加一个维度就要加一列公式。北辰的月度渠道报表用 SUMIFS,因为模板每月一致;临时想看"城市乘渠道"是什么样,用透视表,拖完就删。 判断该用哪个,看这张表要活多久。活不过一天的,透视表;要交给别人每月填一次的,公式加固定区域;要跑十万行以上的,直接去 SQL,别在 Excel 里硬撑。 Excel 的坑集中在四类。**数字存成文本**:单元格左对齐、左上角有绿色三角,SUM 会返回 0,用"分列"或乘 1 转换。**日期是文本**:排序会按字符串排,"2026-2-1"排在"2026-10-1"后面,用 DATEVALUE 转换。**合并单元格**:筛选和透视都会漏数据,分析前先取消合并并填充。**隐藏行**:SUM 会计入隐藏行而 SUBTOTAL 不会,交接时要说清用的是哪个。 再往上一层是"分析工具库"加载项,里面有描述统计、直方图、相关系数、回归和 t 检验。它适合快速看方向,不适合交付:输出是一张静态表,没有置信区间以外的诊断信息。用它做一次回归感受系数方向可以,写进报告要换 Python 复核。 ## 四、SQL:从单表聚合到窗口函数 SQL 的价值在于"口径写在代码里"。同一段 SQL 谁跑都是同一个数,这是 Excel 手工步骤做不到的。北辰的取数需求八成集中在三件事:按月聚合、按维度聚合、算用户级首次行为。 第一段,月度 GMV 与客单价。 ```sql WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, COUNT(DISTINCT user_id) AS pay_users, COUNT(*) AS pay_orders, SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND order_date >= DATE '2026-01-01' AND order_date < DATE '2026-10-01' GROUP BY 1 ) SELECT month, pay_users, pay_orders, gmv, ROUND(gmv / NULLIF(pay_orders, 0), 2) AS aov FROM monthly ORDER BY month; ``` 这段 SQL 有四个值得说的点。日期用半开区间而不是 BETWEEN,避免时间戳带时分秒时漏掉月末最后一天。NULLIF 防止除零。COUNT(DISTINCT user_id) 与 COUNT(*) 的区别是"支付用户数"和"支付订单数",这两个数不能混。DATE_TRUNC 是 PostgreSQL 的写法,MySQL 用 DATE_FORMAT(order_date, '%Y-%m-01'),Hive 用 TRUNC(order_date, 'MM')——换数据库要改这一处。 第二段,用户首单及其渠道。这是典型的窗口函数场景:先按用户分组、按下单时间排序,取第一行。 ```sql WITH ranked AS ( SELECT o.user_id, o.order_id, o.order_date, o.amount, ROW_NUMBER() OVER ( PARTITION BY o.user_id ORDER BY o.order_date, o.order_id ) AS rn FROM orders o WHERE o.status = 'paid' ), first_order AS ( SELECT * FROM ranked WHERE rn = 1 ) SELECT u.channel, COUNT(*) AS first_buyers, ROUND(AVG(f.amount), 2) AS first_aov, ROUND(SUM(f.amount), 2) AS first_gmv FROM first_order f JOIN users u ON u.user_id = f.user_id GROUP BY u.channel ORDER BY first_gmv DESC; ``` ORDER BY 里加了 order_id 作为第二排序键,是为了处理同一用户同一天下两单的情况。没有这个键,结果会随执行计划变化,今天跑和明天跑可能不一样——这类不确定性在交付里是硬伤。 写完就要验证,三步就够。**第一步对总数**:这段 SQL 的 first_buyers 合计应等于"支付用户数"而不是"支付订单数",北辰华东站 3 月是 4,300 对 5,150;如果相等,说明去重没生效。**第二步对分布**:各渠道 first_buyers 相加要等于总数,GMV 相加要等于全量 154.4 万元。**第三步抽样看明细**:随机抽三个 user_id,把他们的订单按时间排出来,人工确认首单日期。第三步最费时间也最有用,它抓的是逻辑错误,前两步只能抓算术错误。 再往下走是留存和复购。SQL 能算出来的口径是"注册后第 N 天是否有支付",用 datediff 或 date_add 生成日期骨架,再左连接订单。但要注意:只算"有访问"还是"有支付",分母是当期注册还是当期活跃,这两个选择会让留存率差出一倍。口径先定,SQL 后写。 举个具体的例子。北辰的 30 日复购率,分子可以取"注册后 30 天内第二次支付"或"首单后 30 天内第二次支付",分母可以取"当期注册用户"或"当期首单用户",四种组合在同一份数据上算出来分别是 11.8%、14.2%、19.4%、23.1%。没有哪个绝对正确:看拉新效率用注册口径,看产品粘性用首单口径。但必须选定一个写进指标字典,并在报告里注明——四个数放在一起,读者会以为数据出错了。 验证 SQL 还有一个便宜的办法:把结果和 BI 上已经上线的同一指标对一遍。BI 的数是别人按另一个口径算的,两者差在哪,往往就是你自己没意识到的假设。对不上不一定是错,但必须能解释。 ## 五、Python:清洗、分析与建模的流水线 Python 的定位是"规则复杂、要重复跑"的活。北辰的月度渠道分析,如果每月都要做一次、每次都要处理缺失渠道和异常金额,写成脚本比每次手工强。 第一段,清洗。核心是四件事:类型、重复、异常、缺失。 ```python import pandas as pd orders = pd.read_csv("orders.csv", parse_dates=["order_date"]) users = pd.read_csv("users.csv", parse_dates=["signup_date"]) # 类型:金额可能是字符串,带千分位或空值 orders["amount"] = pd.to_numeric( orders["amount"].astype(str).str.replace(",", ""), errors="coerce" ) # 重复:同一 order_id 只保留一条 before = len(orders) orders = orders.drop_duplicates(subset="order_id") print("去重删除行数:", before - len(orders)) # 异常:金额为空或小于等于 0 的记录不进入分析 orders = orders[orders["amount"].notna() & (orders["amount"] > 0)] # 缺失:coupon 缺失按"未用券"处理,并要求 status 取值在字典内 orders["coupon"] = orders["coupon"].fillna(0).astype(int) valid_status = {"paid", "refunded", "cancelled"} orders["status"] = orders["status"].str.strip().str.lower() print("异常状态:", set(orders["status"]) - valid_status) # 派生:月份用 Period,便于按月分组且不丢顺序 orders["order_month"] = orders["order_date"].dt.to_period("M") print(orders["amount"].describe(percentiles=[0.5, 0.9, 0.99])) ``` 缺失值怎么填,取决于缺失的含义,不能一律填零。下单渠道缺失可能是埋点没上报,填"未知"比填众数诚实;金额缺失大概率是数据错误,应该剔除而不是补均值,因为补均值会人为制造一批"平均订单",拉低方差。 | 缺失情况 | 建议处理 | 理由 | | --- | --- | --- | | 渠道、来源等标签缺失 | 单独归为"未知"并统计占比 | 缺失本身可能是信息,占比高说明埋点有问题 | | 金额、数量等度量缺失 | 剔除,并记录剔除比例 | 填补会制造不存在的观测 | | 用券标记缺失 | 按业务默认值处理(如未用券) | 缺省即默认,但要在口径里写明 | | 用户属性缺失 | 保留,模型里用缺失指示变量 | 直接删会损失样本并引入选择偏差 | 第二段,渠道分析。merge 之后 groupby,再用 assign 派生客单价,比先算再拼更不容易错。 ```python paid = orders.query("status == 'paid'") detail = paid.merge(users[["user_id", "channel"]], on="user_id", how="left") detail["channel"] = detail["channel"].fillna("未匹配") print("未匹配渠道占比:", round((detail["channel"] == "未匹配").mean(), 4)) channel = ( detail.groupby("channel") .agg(gmv=("amount", "sum"), orders=("order_id", "nunique"), users=("user_id", "nunique")) .assign(aov=lambda d: d["gmv"] / d["orders"], arpu=lambda d: d["gmv"] / d["users"], share=lambda d: d["gmv"] / d["gmv"].sum()) .sort_values("gmv", ascending=False) ) print(channel.round(2)) # 月度乘渠道的透视,用来判断结构变化 monthly_channel = detail.pivot_table( index="order_month", columns="channel", values="amount", aggfunc="sum", fill_value=0 ) print(monthly_channel) ``` 这段代码的输出应该和第三节的 Excel 透视表对得上:信息流 49.6 万元、占比 32.1%。对不上就是口径不同,通常是 status 过滤或去重步骤不一致——这种"两套工具互相校验"是发现口径问题最有效的方法。 第三段,逻辑回归流程示范。这里不追求模型效果,只示范一条能跑的流水线:切分、标准化、训练、评估。北辰的流失标签定义为 60 天无下单,特征是近 30 天访问次数、近 90 天订单数、客单价、用券次数、注册天数。 ```python from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import roc_auc_score, classification_report X = features.drop(columns=["user_id", "churn"]) y = features["churn"] # 流失问题应按时间切分:用 1-6 月样本训练,7-9 月样本验证 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) model = make_pipeline( StandardScaler(), LogisticRegression(max_iter=1000, class_weight="balanced") ) model.fit(X_train, y_train) proba = model.predict_proba(X_test)[:, 1] print("AUC:", round(roc_auc_score(y_test, proba), 3)) print(classification_report(y_test, (proba >= 0.35).astype(int))) ``` 两个提醒。class_weight 处理正例稀少,否则模型会倾向于全判负。threshold 0.35 不是默认值,是按业务成本调的——漏掉一个流失用户的损失,高于多发一张券的成本,阈值就该压低。代码里的随机切分只是示范;真实流失预测要用时间切分,否则同一用户的前后行为会泄漏到测试集,AUC 会虚高。 画图用 matplotlib 或 seaborn 都行,原则只有一条:图是给结论用的,不是给代码用的。渠道对比用横向条形图,趋势用折线,分布用直方图或箱线图。第 11 篇会专门讲怎么选图。 ## 六、BI:数据模型、指标卡与看板治理 BI 的第一件事不是拖图,是建模型。北辰的看板用最朴素的星型模型就够:一张事实表,三张维度表。 | 表 | 类型 | 粒度 | 关键字段 | | --- | --- | --- | --- | | fact_orders | 事实表 | 一行一个订单 | order_id、user_key、date_key、channel_key、amount、status | | dim_user | 维度表 | 一行一个用户 | user_key、signup_date、city、channel、is_new | | dim_date | 维度表 | 一行一天 | date_key、年、月、周、是否促销、是否节假日 | | dim_channel | 维度表 | 一行一个渠道 | channel_key、渠道名、是否付费、归因规则 | 事实表的粒度必须是"一行一个订单",不能是"一行一个订单商品",否则 GMV 会重复计算。维度表里不要放会变的属性,用户的城市如果会迁移,要么保留历史快照,要么明确用注册时城市——这是 BI 里最常见的口径争议来源。 看板要分层,一屏解决一个问题。 | 层级 | 内容 | 刷新频率 | 主要受众 | | --- | --- | --- | --- | | 第一屏 指标卡 | 当日 GMV、月累计 GMV、环比、目标完成率 | 每日 | 管理层、全员 | | 第二屏 趋势 | 日 GMV 与 7 日移动平均、大促日标注 | 每日 | 业务负责人 | | 第三屏 结构 | 渠道、品类、城市的 GMV 占比与环比 | 每日 | 运营、投放 | | 第四屏 漏斗 | 访问、详情、加购、支付四层转化 | 每小时 | 产品、增长 | | 第五屏 明细 | 可下钻的订单表,按角色控制可见范围 | 实时 | 分析师、客服主管 | 指标卡是最容易被做坏的一屏。一张好的卡至少要有三个东西:当期值、对比基准、目标值。只写"今日 GMV 52.3 万元",读者没有参照系;写成"今日 GMV 52.3 万元,环比下降 6.1%,月度目标完成 78%",读者立刻知道要不要管。对比基准要固定,今天比昨天、本周比上周、本月比上月,三种混用会让人反复误判。 治理有四条底线。**指标唯一**:GMV 只有一个定义,任何人要新口径就新建指标,不能改旧指标。**维度一致**:渠道名称在四张表里必须同一套编码,不能一处写"信息流"一处写"feed"。**权限最小化**:明细层按城市或渠道做行级权限,避免全量用户手机号暴露。**刷新可监控**:定时任务失败要告警,看板显示"数据截至"时间,否则有人会拿昨天的数做今天的决策。 BI 不适合做的事也要说清:临时想看的切分、没验证过的口径、需要建模的预测,都不该直接进看板。看板是"已经确定的结论",不是"正在探索的过程"。把探索放进看板,等于把草稿贴在公告栏。 ## 七、协作与工程化:让结果离开你的电脑 分析结果只存在个人电脑里,是团队最常见的浪费。下个月换人接手,SQL 找不到、口径问不到、数字对不上,一切重来。工程化不需要多复杂,五件事做到就够。 **命名可读。** SQL 文件用"层级_主题_粒度_频率"命名,例如 dws_gmv_channel_daily.sql,一看就知道是汇总层、渠道维度、按天。临时探索放 tmp_ 前缀,超过两周没人用就删。 **口径成字典。** 一个指标一行,写清定义、分子、分母、过滤条件、负责人。这份表比任何文档都重要,因为它是争议的裁判。 | 指标 | 口径 | 分母 | 负责人 | 更新 | | --- | --- | --- | --- | --- | | GMV | status 为 paid 的 amount 之和 | 无 | 数据组 | 每日 | | 支付订单数 | status 为 paid 的订单去重计数 | 无 | 数据组 | 每日 | | 客单价 | GMV 除以支付订单数 | 支付订单数 | 数据组 | 每日 | | 支付转化率 | 支付用户数除以访问用户数 | 访问 UV | 增长组 | 每日 | | 30 日留存 | 注册后第 30 天有访问的用户除以当期注册用户 | 当期注册用户 | 增长组 | 每周 | **Notebook 进版本库。** 分析脚本提交到 Git,提交信息写清"改了什么口径",不要写"update"。Notebook 输出要清空后再提交,否则仓库里全是图片和几兆的表格。 **结果带日期和来源。** 任何一张交付的表,表头写清数据截止时间、口径版本、SQL 文件路径。这三样齐了,别人才能复核。 **权限按需申请。** 能看聚合不看明细,能看本部门不看全公司。这不是不信任,是减少误用和泄露的机会。 ## 八、选型建议:小团队、个人与大团队 工具选型没有标准答案,只有匹配。判断维度是三个:数据量、复用频率、协作人数。成本也不只看订阅费,维护人力、口径治理和迁移成本往往更贵。 | 场景 | 推荐组合 | 理由 | 代价 | | --- | --- | --- | --- | | 个人做一次性分析 | Excel 加 SQL 取数 | 最快,改起来不用写代码 | 不可复现,超过十万行会卡 | | 三到五人小团队 | SQL 加轻量 BI | 口径能固定,看板能共享 | 建模能力弱,复杂清洗要另找工具 | | 有建模需求的团队 | SQL 加 Python 加定时调度 | 清洗和模型都能覆盖 | 维护成本高,需要有人懂代码 | | 多业务线大团队 | 数仓加 BI 加语义层 | 口径统一,权限清晰 | 建设周期长,改口径要走流程 | | 对外数据产品 | BI 只读账号 | 权限可控,交付稳定 | 灵活性低,临时需求要另开通道 | 小团队最容易犯的错是"一步到位":三个人就上完整数仓,结果半年都在搭管道,没人做分析。大团队最容易犯的错是"各自为战":每个部门一套 Excel,月度会上三个 GMV。前者是过度建设,后者是治理缺位,两者都不省成本。 一个实用的判断:如果同一张表一个月内被三个人分别手工做过,就该把它固化成 SQL 加看板;如果一张看板三个月没人打开,就该下线。工具是为人服务的,不做这两件事,工具越多越乱。 ## 本篇要点回顾 1. 四种工具是流水线上的四道工序:Excel 探索、SQL 取数、Python 清洗建模、BI 监控共享,用"高级"排序会选错工具。 2. Excel 的关键不是函数多,而是口径稳:北辰华东站 5,150 笔订单用透视表三十秒出渠道 GMV,但客单价不能对四个渠道再取平均。 3. SQL 的价值是把口径写进代码:首单用 ROW_NUMBER 加 order_id 作第二排序键,避免同一天两单导致结果不稳定。 4. Python 的流水线是"类型、重复、异常、缺失"四步,缺失值不能一律填零,金额缺失应剔除而非补均值。 5. BI 的底座是星型模型和指标字典:事实表粒度必须是"一行一个订单",否则 GMV 会重复计算。 ## 延伸阅读 - 第 02 篇《数据准备与 SQL 取数》:本篇第四节的表连接与粒度,在那篇有更完整的口径与清洗讨论,是写 SQL 前的必读。 - 第 03 篇《描述性分析与指标体系》:本篇 Excel 透视和 BI 指标卡背后的指标体系,由那篇定义。 - 第 06 篇《预测分析与建模入门》:本篇第五节的逻辑回归只是流程示范,模型选择、评估与过拟合在那篇展开。 - 第 07 篇《分析方法全景与选择》:先定方法再选工具,那篇给出方法地图,本篇是它的执行层。 - 第 11 篇《分析报告与沟通》:工具产出的图表和数字如何变成一页结论,由那篇接手。