每年这个时节,各大厂的秋招战鼓一响,数据分析岗笔试就成了许多同学早上起来第一件发愁的事。尤其是某头部电商平台2021年的数据分析岗笔试编程题,在求职圈里流传度极高,几乎每个准备面大厂数分的同学都绕不开它。原因也很简单:它不是LeetCode那种纯算法题,而是把SQL、Python处理和业务场景揉在一起考,出题风格相当典型,做题过程特别像一段浓缩版的取数、清洗、算指标的真实工作。这篇文章我就把这套题里最有代表性的三类题型拆开讲透,每一道都从读题、拆解、编码到避坑顺一遍,顺便聊聊我当时带学弟学妹刷这类题时反复强调的答题节奏和容易翻车的细节。无论你是正在准备秋招、打算转岗数分,还是已经在做数据相关的工作想补一补笔试短板,这套复盘思路应该都能直接用在下一场考试里。
1. 笔试题目构成与出题意图剖析
1.1 三道题的经典题型分布
说实话,2021年那会儿流出的笔试题目版本有好几个,但只要把靠谱的几份放在一起看,题型规律非常清楚:整套笔试题基本稳定在三道编程大题左右,时间限制通常在90到120分钟之间。第一道几乎肯定是SQL题,场景跑不掉电商经典业务,什么订单表、用户表、商品表,让你算复购、留存、GMV、品类top榜这类指标;第二道是Python数据处理题,Pandas用的最多,考的是分组聚合、时间筛选、多表拼接这类日常分析高频动作;第三道略有点变化,可能是统计概率类的编程题,比如抽奖事件的期望、分布模拟,也可能是简单的算法思想题,但整体都不太难,核心是看你有没有用代码解决问题的能力。
这里要提醒一下,很多同学备考的时候把精力押在LeetCode刷题上,刷了几百道动态规划、二叉树,结果打开数分笔试题发现完全不是一回事。这类笔试考察的不是复杂的算法设计,而是你处理“不那么干净的数据”的能力。你以为面试官在考你代码写得妙不妙,其实他考的是你拿到业务需求之后,能不能用最直接的方式把结果算出来。
1.2 出题人真正想考察的三件事
把题目还原到工作场景,你会发现每个考点背后都有明确的指向。
第一是对业务指标定义的理解。比如“复购用户”是当月购买次数大于等于2的用户,还是历史购买两次以上的用户?定义不同,SQL写法天差地别。很多人代码基础没问题,但卡在参数口径上,白白丢分。
第二是对数据质量的敏感度。真实业务表里永远有退款单、重复记录、空值、格式不统一的时间字段。笔试题目虽然经过简化,但依然会埋几个这样的坑:表里可能有重复行需要去重,时间字段可能是字符串需要转化。这一条考察的是你有没有处理脏数据的意识,而不是只会在干净环境里写标准答案。
第三是对代码效率和可读性的把握。笔试时间短、数据量没让你真去跑集群,但出题人会故意在题面上写“订单表记录数过亿”,看你会不会在高开销的join、子查询上做优化。话句话说,你写出来的东西不仅要结果对,还要让别人(面试官)觉得你有工程直觉。
2. SQL窗口函数复购题:从读题到写出AC答案的完整拆解
2.1 题目还原与口径确认
先说第一道SQL题。网上流传的版本很多,核心场景是电商复购率统计。我这里用一道同考点的还原模拟题来讲,原题的数据结构一模一样。
表 t_order(订单明细):order_id(订单号)、user_id(用户ID)、order_date(下单日期,格式为 'yyyy-MM-dd HH:mm:ss')、pay_amount(实付金额)。 需求:统计2020年每个自然月内,下单次数大于等于2次的用户数量,输出月份和对应的复购用户数。
读题时第一个要圈出来的信息就是“当月复购”。这就是一道典型的窗口函数题,但难点不在于窗口函数本身,而在于你有没有把“按用户-月份分组”的思路拆出来。很多同学一上来就对订单表直接开窗排序,写了一堆row_number(),最后算出来的根本不是用户数,而是订单数,完全跑偏。
2.2 从业务到代码的映射过程
这道题的正确思考顺序分三步。
第一步,先把订单数据按“月份 + 用户”粒度聚合,统计每个用户每个月下了多少单。注意必须先把order_date格式化到年月,不能直接用原始日期分组,否则同一个人1号和15号各下一单会被分成两行。
第二步,在分组结果里筛选计数大于等于2的用户,这一步代表“复购用户”的业务刻画。
第三步,按月份统计筛选后的用户个数,得到最终输出。
SQL直接写出来就是这样:
select month_id, count(user_id) as repurchase_user_cnt from ( select user_id, date_format(order_date, '%Y-%m') as month_id, count(1) as order_cnt from t_order where order_date >= '2020-01-01' and order_date < '2021-01-01' group by user_id, date_format(order_date, '%Y-%m') ) t where order_cnt >= 2 group by month_id order by month_id;有同学会问,为什么内层先用了group by,外层又用group by,能不能直接在having count()>=2那一步完成?当然可以,把where order_cnt换成having count()>=2效果一样。这里写成子查询+where,主要是让逻辑更清晰,面试讲思路的时候更容易表达。
2.3 窗口函数变体与常见进阶追问
那道笔试原题其实还有第二问,只是流传的版本经常断掉,我当年面试复盘的时候特意找人确认过,确实存在。第二问是这样的:统计每个用户相邻两次下单的时间间隔,并计算全站用户的平均下单间隔。
这种题目就真正暴露窗口函数功底了,需要你准确理解lead()函数的用法:
select user_id, order_date, lead(order_date, 1) over(partition by user_id order by order_date) as next_order_date from ( select user_id, order_date from t_order where order_date >= '2020-01-01' and order_date < '2021-01-01' group by user_id, order_date ) t;拿到相邻日期后,datediff(next_order_date, order_date)就是两次下单间隔。这里关键的一步是先去重再开窗:同一用户同一天下了好几单,如果不先去重,计算出来的间隔会有一堆0,业务上毫无意义。这种细节就是笔试和工作中拉开差距的地方,因为没人会告诉你原始表里有重复数据,你得自己发现并处理。
2.4 这道题最容易栽的五个坑
把SQL题做完之后,我建议你对照一下这五个点,笔试的时候任何一点踩中都会导致结果错误。
- 日期格式化函数:不同数据库写法不同,MySQL用date_format,Hive用date_format,SQL Server用format。笔试环境如果是线上SQL,先看它提示的数据库类型再动笔。
- 时间边界条件:我见过有人写order_date between '2020-01-01' and '2020-12-31',这样会把2020-12-31当天之后的数据漏掉,应该使用左闭右开写法。
- 复购口径:题目说“购买两次以上”,到底含不含两次?这类题默认含等于,但我建议写SQL前先把口径在草稿纸上写明。
- 是否先过滤退款单:笔试题里表结构有时不包含退款标记字段,但题面备注里会说“不包括已退款订单”,这时你得在where里主动加过滤条件,别等着数据干净。
- 聚合后字段引用:内层子查询取别名的字段,外层引用时不会报错,但如果你图省事把count(1)直接写到外层,会把自己绕晕。宁可多写一层子查询,也不要压缩成一长串。
3. Pandas分组聚合题:GMV计算与用户分层实操
3.1 题目还原与实际数据形态
第二道题是Python题,Pandas的出场率几乎百分之百。这里还是一道经过我重构的还原模拟题,考点和原题一致。
有两个csv文件: orders.csv:order_id(订单号)、user_id(用户ID)、pay_amount(支付金额)、pay_time(支付时间,字符串格式 '2020-10-12 14:23:11')、is_refund(是否退款,0/1)。 users.csv:user_id(用户ID)、register_date(注册日期,字符串格式 '2020-09-01')。 需求:计算2020年第四季度(10月、11月、12月)每个用户的累计支付金额(GMV),剔除退款订单,再去重订单,输出消费金额最高的前100个用户的user_id、gmv、register_date,按gmv降序排列。
看题第一眼会觉得简单,不就是groupby加个sum吗。但只要动手写,很快就会发现三个魔鬼细节:退款记录、重复订单、日期筛选。
3.2 正确解题顺序与核心代码
我习惯的解题顺序是先处理脏数据,再筛选时间窗口,最后聚合拼接。这个顺序背后的逻辑其实和SQL题是一样的:先把数据洗干净,再算业务指标,减少后续步骤出错的可能。
import pandas as pd orders = pd.read_csv('orders.csv') users = pd.read_csv('users.csv') # 第一步:剔除退款单 orders = orders[orders['is_refund'] == 0].copy() # 第二步:去除重复订单,保留第一条 orders = orders.drop_duplicates(subset=['order_id'], keep='first').copy() # 第三步:时间字段解析并筛选Q4 orders['pay_time'] = pd.to_datetime(orders['pay_time']) orders['pay_month'] = orders['pay_time'].dt.to_period('M') q4_orders = orders[orders['pay_month'].isin(['2020-10', '2020-11', '2020-12'])] # 第四步:按用户聚合GMV user_gmv = ( q4_orders.groupby('user_id')['pay_amount'] .sum() .reset_index() .rename(columns={'pay_amount': 'gmv'}) ) # 第五步:降序取前100 top100 = user_gmv.sort_values('gmv', ascending=False).head(100) # 第六步:关联用户注册日期 result = top100.merge(users, on='user_id', how='left') result = result[['user_id', 'gmv', 'register_date']]这段代码看起来平平无奇,但每一行都有讲究。第二行的drop_duplicates用的是order_id做去重键,不是user_id也不是全部字段,为什么?因为同一笔订单可能在明细表里冗余多条记录,但order_id是唯一标识。如果按全部字段去重,碰到同一订单恰好所有字段都一样的情况,就达不到去重效果了,但业务上一般不至于出现完全相同字段的两行,所以按order_id是稳妥的。另外,在groupby之前做去重,和在groupby之后做去重,结果完全不同,必须先洗后聚。
3.3 为什么很多人在这里丢分
这道题的丢分点,一来是不清楚pandas中dt.to_period和dt.month的区别,二来是对merge逻辑理解不透。
先说时间筛选。很多人写成这样:
q4_orders = orders[pd.to_datetime(orders['pay_time']).dt.month.isin([10, 11, 12])]这个写法在两个层面出错:第一,跨年了,2021年10月的数据也会被算进去,但题目要求的是2020年Q4;第二,pd.to_datetime是对整个Series操作,这里没有覆盖赋值,后面再用orders['pay_time']还是字符串,容易在后续报错。正确的是先解析一遍,转成datetime类型,再提取年月。
再说merge。users表里user_id如果存在重复,top100关联时就会出现一对多,行数膨胀,输出结果比100行多。真实环境里用户注册表一般不会重复,但笔试为了严谨,事先在草稿纸上确认主键永远是加分项。我在代码里用了how='left',就是为了保住top100的完整性,哪怕有个别用户在users表里找不到,也不该被丢掉。
3.4 这个场景还能怎么扩展考
原题后面有没有隐藏追问?我之前推测是有的,后来在和一起教研的前辈讨论中发现确实存在一个扩展问:如果希望知道每个用户的GMV在全体用户中的排名百分比排名,怎么做?
这个用rank方法就能解决。
user_gmv['gmv_pct_rank'] = user_gmv['gmv'].rank(pct=True)补充这个点是想提醒大家,Pandas题的考察重点从来不是“会不会调API”,而是“数据处理的顺序意识”和“对异常情况的预判”。如果笔试时间富余,你可以顺手加一行注释说明自己为什么先过滤退款、为什么按order_id去重,这种习惯非常加分。
4. 统计概率编程题:期望公式推导与蒙特卡洛验证
4.1 一道让人"摸不着头脑"的概率题
第三道题在整套卷子里风格最跳跃,很多备战同学会在它上面卡住。题面大意是(还原模拟题):
某电商平台做抽奖活动,用户每次点击抽奖按钮,中奖概率为0.1,每次抽奖相互独立。现在有10万用户参与,每人抽10次。请估算中奖次数大于等于3次的用户数。
乍一看像是概率论考试,但这是笔试题,你需要在限定时间内写出代码来解答。很多同学第一反应是套二项分布公式,写出一大堆阶乘、组合数,结果算到一半发现环境里根本没有科学计算库,或者压根忘了公式。这时候可以换个思路:用Python直接模拟,几行代码就能给出足够精确的估算值。
4.2 二项分布公式推导
说句实话,公式法也是完全可行的,毕竟每次抽奖独立、中奖概率固定,每人抽10次就是典型的二项分布。单人中奖次数X符合二项分布B(10, 0.1),要求的是P(X>=3),那么先用补事件公式简化:
P(X>=3) = 1 - P(X=0) - P(X=1) - P(X=2)
具体算的话:
- P(X=0) = C(10,0) * 0.1^0 * 0.9^10 = 0.9^10 ≈ 0.348678
- P(X=1) = C(10,1) * 0.1^1 * 0.9^9 ≈ 0.387420
- P(X=2) = C(10,2) * 0.1^2 * 0.9^8 ≈ 0.193710
所以P(X>=3) ≈ 1 - 0.348678 - 0.387420 - 0.193710 ≈ 0.0702。最后乘10万用户,估算中奖次数大于等于3次的用户数约为7020人。
这个推导没有任何难度,但笔试题要求的是代码。在手写公式的同时,用代码完成一次验证可以让你的答案更加无懈可击,这就来到了蒙特卡洛模拟。
4.3 蒙特卡洛模拟的代码实现
import random def estimate_probability(trials=100000, users=100000): """ 蒙特卡洛模拟:模拟 users 个用户,每人抽 10 次 统计中奖次数 >= 3 的用户比例 """ hit_count = 0 p = 0.1 n_draws = 10 for _ in range(users): prize_times = 0 for _ in range(n_draws): if random.random() < p: prize_times += 1 if prize_times >= 3: hit_count += 1 prob = hit_count / users expected_users = prob * users return prob, expected_users random.seed(42) prob, expected_users = estimate_probability() print(f"P(X>=3) ≈ {prob:.4f}") print(f"估算人数 ≈ {expected_users:.0f}")跑出来的结果会在0.069到0.071之间浮动,和公式算出来的0.0702非常接近。随机种子固定之后,结果可以复现,这一点在笔试里很加分。蒙特卡洛的核心逻辑就是:每次抽奖用random.random()生成一个0到1之间的均匀随机数,小于0.1就代表中奖。把10次抽奖作为一组,判断这组里中奖次数是否达到3次,最后统计占比。
有人会问,直接套公式不是更快吗?为什么还要大费周章写模拟?其实你只要真实笔试现场待过就知道,紧张状态下公式经常忘,但random库几乎是本能反应级别的东西,而且模拟代码的每一步都有直观含义,就算结果有微小偏差,面试官也能一眼看懂你的思路。正因如此,如果你想提前验证自己推导的公式是否正确,写个模拟永远是最稳的兜底手段。
4.4 概率统计笔试题的两个进阶方向
去年开始,一些大厂开始在概率题上增加“用代码验证结论”的要求,不再满足于背公式。比如追问:如果中奖概率不是0.1而是按照用户等级变化(普通用户0.05,VIP用户0.2),在只知道总用户中VIP占比20%的情况下,估算结果应该怎么改?这时公式法要先做全概率公式展开,而模拟法只需根据用户等级生成不同概率的随机数,代码几乎不用重写。这种灵活性是模拟法最大的优势,我建议大家在准备阶段一定要彻底掌握。
5. 笔试高频踩坑点与答题时间分配实战
5.1 最常见的四类翻车现场
刷题量上去了之后,你会发现大家犯的错高度集中,总结起来就四大类。
第一类是“审题太快,口径跑偏”。“复购用户”到底是当月买两次还是历史买过两次以上?季度GMV包不包含退款前数据?题目里已经写清楚了,但考场上人一急就按自己脑补的来,结果答案和样例输出对不上。这种分丢得最冤。
第二类是“拿到了数据先不观察,拿起键盘就写”。数据分析岗位的笔试题在牛客这类平台上一般会提供测试数据,你先打印前几行,看下字段类型和空值情况,成本极低收益极高。我见过直接对字符串时间做groupby,本来以为稳了,结果输出一坨乱码的例子,就是典型的不做数据探查。
第三类是“不会用窗口函数,遇到取每个分类topN的题还在join自己”。在SQL里取每个月的GMV top10商品,是最常见的需求,但实现方式选择直接影响正确率和代码可读性。row_number() over(partition by month order by gmv desc)一行解决的事,有人用自连接绕了七八行,还绕出个笛卡尔积。
第四类是“Python题不写注释,变量名全是a、b、c”。笔试环境没法跟面试官实时解释,注释就是你表达思路的唯一途径。我每次批改模拟卷,看到这种代码都头大,就算结果对了,也会觉得候选人代码交付习惯不好。
5.2 时间分配的一个可靠策略
90分钟做三道题,怎么分配?我给你一套经过多人验证的节奏:
- 前5分钟:通读三道题,快速判断每道题的难度和熟悉度。
- 70分钟:按题目顺序做,控制在每道20到25分钟。先保证SQL、Pandas这两道必拿分的题稳稳过样例。
- 最后15分钟:统一检查。重点看日期边界、退款标记、输出列名顺序、数值精度。
这套节奏的关键点是“不要在一道题上恋战”。如果统计概率题公式忘了,模拟代码一时也卡住,可以先跳过,回头再看,保证前面两道题拿到80%的分数,比最后一道题拿满分更有价值。
5.3 提交前必做的五步自查
我根据自己多年的笔试经验,整理了一个提交前检查清单,你可以直接背下来:
- [ ] 输出字段名、字段数量、字段顺序是否和题目要求一致,很多Oj系统按列名匹配,多一列都会报错。
- [ ] 是否有硬编码了题目里没有出现的数值。
- [ ] 日期筛选是否包含正确的时间跨度,有没有出现“2020-12-31”当右边界的问题。
- [ ] join之后行数是否膨胀,去重逻辑是否在正确位置执行了。
- [ ] 是否处理了潜在的NaN值,尤其是在用户关联不到注册信息时。
养成这套检查习惯之后,我帮不同基础的同学模拟测试过,平均能减少20%左右的低级错误,效果相当明显。
6. 备战路径与复盘技巧
6.1 刷题的直接价值不在量,而在覆盖度
数据分析岗的笔试题目,说来说去就那么几个业务场景:订单、用户、商品、流量。刷题的时候不建议海量刷乱七八糟的题,而是把几个核心场景刷透:每个场景下的SQL题至少会写三四遍不同的解法。本周能把复购率用三种方案做出来,下周考试时无论题目换个什么说法,你都能一眼看到本质。
对于某电商平台2021这套笔试题,我建议你把复购、留存、GMV分层、二项分布模拟这四个考点作为必修模块,每个模块都做到“不看笔记能独立写出代码”的程度。
6.2 模拟真实环境比单纯看题重要十倍
我陪人备战的时候经常说一句话:“看题看懂了,和能上手写出来之间,隔着一道巨大的墙。”建议每周至少抽两个完整时间段,按照笔试的时间限制和题型分布完整模拟一次。环境方面,SQL就找一个支持窗口函数的在线环境,Python就用本地的notebook。重点不是得分,而是要习惯限时状态下“写完代码立刻自查”的节奏感。
还有一个非常有效的办法:把每次模拟时犯错的地方记成错题本,不用记题目本身,只要记“我在哪个环节出了问题”。比如“日期字段忘了先to_datetime”写下来,考前翻一遍,能起到很好的警示作用。很多时候你笔试翻车不是因为不会,而是因为犯过的错又犯了一遍。
6.3 复盘的时候要还原"审题动作"
题目改完不是终点,复盘才是涨分的关键环节。我要求学生复盘时回答三个问题:这题审题时哪几个词最关键?我的第一步为什么放在那儿而不是别的位置?如果数据量扩大100倍,我的代码还能跑吗?
第一个问题训练的是抓口径能力。比如“2020年每个自然月内”和“最近12个月滚动计算”是两种完全不同的需求,审题时看不出来,代码就是白写。第二个问题训练的是工程思维,很多人写代码是“想到了就往上堆”,复盘时还原一下决策过程,你会发现绝大多数步骤都有更优顺序。第三个问题虽然笔试一般不直接考,但从长远来看,决定了你是否具备解决真实问题的水平。
结尾
写到这儿,这套2021年某电商平台数据分析岗笔试题的核心内容就算复盘完了。我个人在实际带人刷题的过程中最大的体会是,笔试真正筛掉的往往不是技术差的人,而是准备方向错的人。SQL、Pandas、统计模拟这三个板块看似彼此独立,底层共通的能力却是相通的:把一个模糊的业务问题翻译成清晰的数据处理步骤。很多同学刷题时只盯着“这道题的答案是什么”,从没想过“出题人想让我展示什么”,这种习惯带到考场上最容易在变动题型面前乱了阵脚。如果你手头正好在备大厂数分岗,我建议你把这篇文章里的三道题按原题时间限制完整做一遍,然后再去做一套其它年份的真题,对比一下自己的错题类型是否重叠。最后再分享一个小技巧:不管笔试结果如何,尽量把当天写过的代码保存下来,过一周回头用“如果数据量大十倍”的标准重写一遍,坚持三个月,你的处理数据思路会比别人刷两百道题还要清晰。