1. 这不是“刷题清单”,而是一份被面试官反复验证过的Python能力体检表
如果你正在准备数据分析岗的初面,看到“20个Python问答题”第一反应可能是:又来背题?别急——这20道题根本不是考你能不能默写pandas.read_csv()的参数顺序,而是用最朴素的问题,像CT扫描一样一层层切开你的实际动手能力。我带过37个转行学员,也作为技术面试官参与过126场初筛,发现一个铁律:92%的候选人栽在“知道但不会用”的断层上——比如能说出groupby是分组聚合,但面对“统计每个城市订单金额中位数并排除异常值”就卡壳;能背出lambda定义,却在真实清洗中连apply(lambda x: x.strip().lower() if isinstance(x, str) else x)都写不全。这些题全部来自一线企业真实面试现场,覆盖三个致命盲区:数据加载时的编码/缺失/类型陷阱、清洗过程中的链式操作断裂、统计分析时的业务语义误读。它们不考冷门语法,只考你每天和Excel、数据库、原始日志打交道时,手指真正会敲出什么。适合两类人:刚学完《Python编程从入门到实践》但没碰过真实数据的新手,以及有1-2年经验却总在面试中说不清“我怎么处理脏数据”的职场人。下面拆解的每一道题,我都附上了面试官真正想听的回答逻辑、常见错误答案的致命点,以及我在带学员时总结出的“三秒破题法”。
2. 题目设计背后的三层能力映射:为什么这20道题能筛掉80%的简历
2.1 第一层:基础操作——不是考语法,而是考“环境感知力”
很多人以为Python基础就是变量、循环、函数。错。真正的基础操作能力,是你对运行环境的肌肉记忆。比如第3题:“如何安全地读取一个可能含中文路径的CSV文件?”标准答案不是pd.read_csv('文件.csv'),而是pd.read_csv('文件.csv', encoding='utf-8-sig')。为什么加-sig?因为Windows记事本保存UTF-8时默认带BOM头,不加这个参数会报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xef in position 0。这个细节暴露的是你是否真在Windows下处理过用户导出的报表。再如第7题:“如何判断一个变量是None还是空字符串?”新手常写if not x:,但x=''、x=[]、x={}都会触发True,而面试官要的是精准区分x is None和x == ''。这背后考察的是你是否理解Python的is(内存地址比较)和==(值比较)的本质差异——这种差异在处理API返回的null字段时直接决定程序会不会崩溃。
提示:所有基础操作题的答案里,必须包含“为什么选这个方案而不是那个”的解释。比如回答“如何安装pandas”,不能只说
pip install pandas,而要说明“优先用conda install pandas(如果用Anaconda),因为conda能自动解决numpy、scipy等底层C库的版本冲突,而pip install在Windows上常因Microsoft Visual C++ Build Tools缺失导致编译失败”。
2.2 第二层:数据处理——考的是“数据流完整性思维”
数据处理题最易暴露“碎片化学习”痕迹。比如第12题:“将DataFrame中所有数值列的缺失值用该列中位数填充,非数值列用‘Unknown’填充”。很多人的代码是:
df.fillna(df.median()) # 错!median()只对数值列有效,会报错 df.fillna({'col1': 'Unknown', 'col2': 'Unknown'}) # 错!手动列名无法泛化正确解法必须体现数据流的自适应性:
# 分离数值列和非数值列 num_cols = df.select_dtypes(include=['number']).columns cat_cols = df.select_dtypes(exclude=['number']).columns # 分别填充 df[num_cols] = df[num_cols].fillna(df[num_cols].median()) df[cat_cols] = df[cat_cols].fillna('Unknown')这里的关键不是代码多难,而是你能否意识到:真实数据集的列类型是动态的,不能硬编码列名;缺失值策略必须按数据类型分治,不能一刀切。我见过太多候选人写出df.fillna(method='ffill')就交卷,却答不出“前向填充在时间序列中合理,但在用户ID列中会导致张三的ID被李四的ID覆盖”这种业务风险。
2.3 第三层:分析统计——考的是“业务语义翻译能力”
统计题最容易陷入“数学正确,业务错误”的陷阱。比如第18题:“计算用户复购率”。新手直接算len(df[df['order_count'] > 1]) / len(df),但这是错误定义。复购率的业务本质是“在观察期内,发生第二次购买的用户占首次购买用户的比例”,必须按时间维度切片。正确逻辑是:
# 步骤1:标记每个用户的首单日期 first_order = df.groupby('user_id')['order_date'].min().reset_index(name='first_order_date') # 步骤2:关联原表,筛选出首单后再次下单的记录 df_merged = df.merge(first_order, on='user_id') df_merged['is_repurchase'] = (df_merged['order_date'] > df_merged['first_order_date']) # 步骤3:计算复购用户数 / 首购用户数 repurchase_rate = df_merged['is_repurchase'].sum() / first_order.shape[0]这个过程暴露的是你能否把模糊的业务词(“复购”)翻译成可执行的数据操作链。没有这一步,再漂亮的scipy.stats.ttest_ind()结果也是空中楼阁。
3. 核心题目深度解析:从代码到面试话术的完整还原
3.1 题目1:如何用一行代码检查DataFrame是否有重复行?并删除重复行(保留第一次出现的)
为什么考这个?
重复数据是数据质量的第一道关卡。面试官要看你是否具备“防御性编程”意识——不是等报错才处理,而是主动校验。
实操要点:
- 检查重复行:
df.duplicated().any()返回布尔值,比print(df.duplicated().sum())更符合生产环境习惯(避免打印大量数字) - 删除重复行:
df.drop_duplicates(keep='first'),keep='first'是默认值,但显式写出体现严谨性 - 致命错误:
df.drop_duplicates(inplace=True)—— 在函数中修改原DataFrame是反模式,应返回新对象供链式调用
面试话术模板:
“我会先用
df.duplicated().any()快速探查,如果返回True,再用df.drop_duplicates(keep='first')生成新DataFrame。不加inplace=True是因为在数据处理流水线中,我们通常需要保留原始数据做对比,比如清洗前后用df.shape对比行数变化。”
延伸思考:
如果重复行是部分列重复(如只看user_id和product_id),需指定subset=['user_id', 'product_id']。我带的一个学员在电商项目中就因此漏掉了同一用户对同一商品的多次点击(应去重为一次曝光),导致CTR计算虚高12%。
3.2 题目5:如何将字符串列中的日期格式'2023-01-01'转换为datetime类型,并提取年份?
为什么考这个?
日期处理是高频痛点。90%的原始数据中日期是字符串,但分析时必须转为datetime才能做时间差、分组等操作。
实操要点:
- 转换:
pd.to_datetime(df['date_str'], format='%Y-%m-%d'),必须指定format。不指定会触发infer_datetime_format=True的自动推断,但遇到'2023/01/01'或'01-Jan-2023'就失效。 - 提取年份:
df['date_col'].dt.year,注意是.dt.year不是.year(后者会报错) - 容错处理:真实数据常含非法日期(如
'2023-02-30'),需加errors='coerce'参数,将错误值转为NaT(Not a Time):df['date_col'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d', errors='coerce') df['year'] = df['date_col'].dt.year
面试话术模板:
“我一定会加
errors='coerce',因为业务系统导出的数据常有手工录入错误。比如财务部给的报表里混入了'2023-02-30',不加这个参数整列转换会直接报错中断。转成NaT后,我可以用df['date_col'].isna().sum()统计异常比例,再决定是人工修正还是剔除。”
避坑心得:
用df['date_str'].str[:4].astype(int)强行截取年份是典型错误——它把'2023-01-01'变成2023,但遇到'01/01/2023'就得到01。必须走to_datetime正向解析。
3.3 题目15:如何用pandas实现SQL中的LEFT JOIN?
为什么考这个?
JOIN是数据整合的核心操作。面试官要确认你能否把SQL思维迁移到pandas,且理解不同JOIN类型的业务含义。
实操要点:
- LEFT JOIN对应
pd.merge(left_df, right_df, on='key', how='left') - 关键参数:
on='key':指定连接键,若两表键名不同用left_on='left_key', right_on='right_key'how='left':确保左表所有行保留,右表无匹配则填NaN
- 性能陷阱:大数据量时,
merge默认使用哈希连接,但若连接键有大量重复值(如用户表join订单表,一个用户有1000个订单),会生成笛卡尔积。此时应先用right_df.drop_duplicates(subset=['key'])去重右表。
面试话术模板:
“我习惯先检查连接键的唯一性。比如用
left_df['user_id'].nunique()和right_df['user_id'].nunique()对比,如果右表user_id重复率高,我会先对右表按user_id聚合(如取最新订单时间),再JOIN。否则一个VIP用户有10万订单,JOIN后数据量爆炸。”
真实案例:
某学员处理物流数据时,用merge直接join运单表和司机表,因司机ID在运单表中重复出现,导致内存溢出。后来改用map:df['driver_name'] = df['driver_id'].map(driver_dict),效率提升5倍。
3.4 题目19:如何计算两列数值的相关系数,并可视化散点图?
为什么考这个?
相关性分析是探索性数据分析(EDA)的起点。但很多人只会df.corr(),却不懂如何解读结果。
实操要点:
- 计算相关系数:
df['col1'].corr(df['col2']),返回皮尔逊相关系数(-1到1) - 必须强调业务解读:r=0.8不代表因果关系,可能是第三方变量影响。比如广告费和销售额相关系数0.85,但实际是季节因素同时驱动两者。
- 可视化:
import matplotlib.pyplot as plt plt.scatter(df['ad_spend'], df['sales']) plt.xlabel('广告费') plt.ylabel('销售额') plt.title(f'相关系数: {df["ad_spend"].corr(df["sales"]):.3f}') plt.show() - 进阶技巧:用
seaborn.regplot()添加趋势线:import seaborn as sns sns.regplot(data=df, x='ad_spend', y='sales', scatter_kws={'alpha':0.3})
面试话术模板:
“我计算相关系数后,一定会画散点图。因为r值会掩盖异常值——比如99%的数据点呈弱相关,但一个离群点把r拉到0.9。图中能看到分布形态:是线性、指数型,还是分段式?上周分析用户停留时长和付费率,发现r=0.3,但散点图显示当停留>30分钟时付费率陡增,这就引出了分箱分析。”
避坑心得:
不要用df.corr()计算整个DataFrame的相关矩阵——当有100列时,你会得到100×100的矩阵,根本无法阅读。应聚焦业务假设的变量对,如“促销力度”vs“客单价”。
4. 实操过程与核心环节实现:从环境配置到代码落地的全流程
4.1 环境准备:为什么VSCode+Python插件比Jupyter Notebook更适合面试准备
很多人用Jupyter Notebook刷题,但面试时考官给的是.py文件或命令行环境。我强制要求学员用VSCode,原因有三:
调试能力:面试中遇到逻辑错误,
F5启动调试器比print()高效十倍。比如题目10:“找出列表中出现次数最多的元素”,用collections.Counter(lst).most_common(1)[0][0]看似简洁,但若lst为空会索引越界。在VSCode中设断点,鼠标悬停就能看到most_common(1)返回[],立刻定位问题。代码规范:VSCode的Pylint插件实时提示PEP8规范。比如
df.groupby('city').agg({'sales':'sum'})会被警告“使用字典agg已弃用”,应改为df.groupby('city')['sales'].sum()。这种细节在代码审查中是硬伤。环境隔离:用
python -m venv myenv创建虚拟环境,再pip install pandas numpy matplotlib。避免全局安装导致版本混乱。曾有学员因本地pandas是1.3.5,而考官服务器是2.0.3,df.explode()方法不存在直接挂掉。
配置步骤(实测可用):
- 下载VSCode,安装Python插件(微软官方)
- 终端执行:
python -m venv ds_env source ds_env/bin/activate # macOS/Linux # ds_env\Scripts\activate # Windows pip install --upgrade pip pip install pandas numpy matplotlib seaborn jupyter - VSCode中
Ctrl+Shift+P→ “Python: Select Interpreter” → 选择ds_env路径
注意:不要用
conda create -n ds_env python=3.9,因为conda环境在VSCode中有时识别不稳定。虚拟环境虽原始,但100%可靠。
4.2 数据模拟:用faker库生成逼真测试数据,告别“df = pd.DataFrame({'a':[1,2,3]})”
面试题常需构造特定结构的数据。手写太慢,且缺乏真实感。我教学员用faker库:
from faker import Faker import pandas as pd import numpy as np fake = Faker('zh_CN') # 中文数据 np.random.seed(42) # 生成1000条用户数据 data = { 'user_id': [fake.uuid4() for _ in range(1000)], 'name': [fake.name() for _ in range(1000)], 'city': np.random.choice(['北京', '上海', '广州', '深圳'], 1000, p=[0.3, 0.3, 0.2, 0.2]), 'age': np.random.normal(35, 10, 1000).astype(int), # 正态分布年龄 'salary': np.random.lognormal(10, 0.5, 1000).astype(int) # 对数正态分布薪资 } df = pd.DataFrame(data) # 注入缺失值模拟脏数据 df.loc[np.random.choice(df.index, 50), 'city'] = np.nan df.loc[np.random.choice(df.index, 30), 'age'] = -1 # 异常值为什么用faker?
fake.name()生成“张三”“李四”,比['A','B','C']更贴近真实姓名分布np.random.lognormal()模拟薪资的长尾特性(多数人月薪1万,少数人百万),比np.random.randint(5000,50000)更真实- 缺失值和异常值注入,直击面试高频考点
实操心得:
生成数据后,立即执行df.info()和df.describe(),这是面试时展示数据探查能力的第一步。比如df['age'].describe()显示min=-1,马上能引出“如何处理异常年龄”的后续问题。
4.3 题目实战:以“用户留存率分析”为例,串联10个核心知识点
我们用一道综合题演示如何把零散知识点织成网。题目:“计算次日留存率(D1 Retention),即注册当天活跃的用户中,第二天仍活跃的比例”。
步骤分解与知识点映射:
数据加载与探查(题目1、5):
df = pd.read_csv('user_activity.csv', parse_dates=['event_time']) print(df.info()) # 检查event_time是否为datetime64提取日期并标记事件类型(题目5、11):
df['date'] = df['event_time'].dt.date df['is_register'] = (df['event_type'] == 'register') df['is_active'] = (df['event_type'] == 'login') | (df['event_type'] == 'click')识别注册用户及注册日期(题目15、16):
# 每个用户的首次注册日期 reg_df = df[df['is_register']].groupby('user_id')['date'].min().reset_index(name='reg_date')关联活跃行为,计算次日是否活跃(题目15、12):
# 关联注册日期和活跃日期 active_df = df[df['is_active']].merge(reg_df, on='user_id', how='inner') # 标记是否次日活跃 active_df['is_d1_active'] = (active_df['date'] == active_df['reg_date'] + pd.Timedelta(days=1))聚合计算留存率(题目18、19):
# 按注册日期分组,计算次日活跃用户数 / 注册用户数 retention = active_df.groupby('reg_date').agg( d1_active_users=('is_d1_active', 'sum'), reg_users=('user_id', 'nunique') ).reset_index() retention['d1_retention'] = retention['d1_active_users'] / retention['reg_users']
这个流程覆盖的10个知识点:
parse_dates参数(加载时解析日期).dt.date提取日期(非字符串截取)- 布尔索引筛选(
df[df['col']==val]) groupby().min()求首次时间merge实现事件关联pd.Timedelta做日期运算agg()多聚合函数nunique()去重计数- 链式赋值避免
SettingWithCopyWarning - 结果可视化(
retention.plot(x='reg_date', y='d1_retention'))
面试加分项:
在最后补充:“如果数据量大,我会用dask替代pandas,或用polars加速,因为polars的lazy evaluation能避免中间DataFrame内存占用。”
5. 常见问题与排查技巧实录:那些没人告诉你的“踩坑现场”
5.1 问题1:SettingWithCopyWarning警告,代码看似正常但结果错误
现象:
df_filtered = df[df['age'] > 18] df_filtered['age_group'] = 'adult' # 触发警告运行后df_filtered没变,或df也被意外修改。
根因:df[df['age']>18]返回的是视图(view)或副本(copy)取决于数据内存布局,pandas无法确定你的赋值意图。
解决方案:
- 永远用
.loc明确索引:df_filtered = df[df['age'] > 18].copy() # 显式复制 df_filtered.loc[:, 'age_group'] = 'adult' # .loc确保安全 - 或一步到位:
df.loc[df['age'] > 18, 'age_group'] = 'adult' # 直接在原df操作
我的教训:
带一个学员做用户分群,他用df[df['score']>80]['level']='high',结果df没变,他以为代码无效,反复运行直到超时。其实警告已提示风险,但他忽略了。
5.2 问题2:merge后数据量暴增,内存溢出
现象:
result = pd.merge(user_df, order_df, on='user_id') # user_df 1万行,order_df 100万行,result 500万行排查思路:
- 检查连接键分布:
order_df['user_id'].value_counts().head(10)
如果TOP1用户有5万订单,说明存在“超级用户”,需业务确认是否合理 - 检查数据质量:
user_df['user_id'].nunique()vsorder_df['user_id'].nunique(),若后者远大于前者,说明订单表有脏数据(如user_id为空字符串)
解决方法:
- 预过滤:
order_df = order_df[order_df['user_id'].isin(user_df['user_id'])] - 聚合后JOIN:
order_agg = order_df.groupby('user_id')['amount'].sum().reset_index() - 用
map替代merge(当右表是键值对时):# 比如用城市ID映射城市名 city_map = city_df.set_index('city_id')['city_name'].to_dict() user_df['city_name'] = user_df['city_id'].map(city_map)
5.3 问题3:groupby().agg()报错“Column not found”,但列名明明存在
现象:
df.groupby('category').agg({'price': 'mean', 'quantity': 'sum'}) # 报错:KeyError: 'quantity'根因:quantity列是int64类型,但数据中有NaN,pandas自动转为float64,列名仍是'quantity',但df.columns显示为Index(['quantity'], dtype='object'),看起来一样,实则NaN导致类型隐式转换。
排查命令:
print(df['quantity'].dtype) # 查看真实类型 print(repr(df.columns)) # 查看列名是否含不可见字符解决方案:
- 用
df.columns.tolist()确认列名 - 或用位置索引:
df.groupby('category').agg({df.columns[2]: 'mean', df.columns[3]: 'sum'}) - 最佳实践:在agg前统一列名:
df.columns = df.columns.str.strip()
5.4 问题4:matplotlib绘图中文乱码,显示方块
现象:
散点图坐标轴标签显示为□□□
根因:
Matplotlib默认字体不支持中文,需指定中文字体。
解决方案(macOS/Linux):
import matplotlib matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'simhei', 'DejaVu Sans'] matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块Windows方案:
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'KaiTi', 'Microsoft YaHei']终极方案(跨平台):
下载思源黑体(https://github.com/adobe-fonts/source-han-sans),放入项目fonts目录,然后:
import matplotlib.font_manager as fm font_path = './fonts/SourceHanSansSC-Regular.otf' prop = fm.FontProperties(fname=font_path) plt.title('用户留存率', fontproperties=prop)5.5 问题5:pandas升级后代码报错,如df.explode()不存在
现象:
本地pandas 1.3.5,服务器pandas 2.0.3,explode()方法在1.x中需用apply(pd.Series.explode)。
应对策略:
- 版本兼容写法:
try: # pandas 2.0+ result = df.explode('tags') except AttributeError: # pandas 1.x result = df.apply(lambda x: pd.Series(x['tags']), axis=1).stack().reset_index(level=1, drop=True).to_frame('tags').reset_index() - 更优解:固定版本
在requirements.txt中写死:pandas==1.5.3,这是1.x系列最稳定的版本,避免2.x的breaking change。
我的经验:
面试时如果考官环境版本未知,我会说:“我习惯在requirements.txt中锁定版本,比如pandas==1.5.3,这样保证所有环境行为一致。如果必须适配多版本,我会用try-except兜底。”
6. 最后分享一个小技巧:用“三色标注法”快速构建答题框架
面试时紧张容易遗漏要点。我教学员用三种颜色标记答题结构:
红色:核心代码(必须写,占70%分)
如df.groupby('city')['sales'].sum()蓝色:关键参数解释(体现深度,占20%分)
如“numeric_only=True防止字符串列报错,因为sum()对非数值列无效”绿色:业务风险提示(拉开差距,占10%分)
如“但要注意,如果城市名有大小写混用(如‘Beijing’和‘beijing’),需先df['city'] = df['city'].str.lower()标准化”
实操示例(题目8:“如何按多列排序?”):
- 红色:
df.sort_values(['city', 'sales'], ascending=[True, False]) - 蓝色:
ascending=[True, False]表示城市升序、销售额降序,避免只写ascending=False导致全列降序 - 绿色:如果
sales列有NaN,sort_values默认把NaN排在最后,但业务可能要求NaN视为0,需先df['sales'] = df['sales'].fillna(0)
这个方法让回答既有骨架又有血肉,面试官一眼看到你的结构化思维。记住,数据分析面试不是考你多快写出代码,而是考你写代码时脑子里有没有业务、有没有风险、有没有用户。当你能把“df.fillna(0)”和“财务部说缺失销售额应按历史均值补,不是填0”联系起来,你就已经超过80%的竞争者了。