简介:本资源是一套面向数据分析求职者与转行学习者的笔试真题实战资料包,聚焦企业招聘中高频考察的数据清洗、SQL查询、统计推断、可视化解读及基础建模等核心能力。资源共41个文件,涵盖20张真题截图(含前海数据、东莞数据等真实企业笔试题图)、5个Excel试题文件(含岗前测试、BI练习、测试题E套等)、4份Word解析文档(如11_SQL查询.docx)、2个PPT汇报模板、1个MP4分析报告视频、1个EXE本地运行工具及配套说明页,压缩包大小为54.78MB,结构清晰,图文音视结合,便于分模块刷题与复盘。已有820人学习下载,内容覆盖2015–2022年多期笔试真题,包含完整题目呈现、参考答案线索、图表数据源及可执行环境,助读者系统训练解题逻辑、提升业务语境下的数据表达与技术落地能力。
1. 数据分析笔试题:不是考Excel函数,而是考你能不能从脏数据里捞出真实业务信号
某公司招聘数据分析岗,收到200份简历,筛出50人发笔试题——结果43人卡在“清洗订单时间字段”这一步。不是不会写pd.to_datetime(),而是没人意识到原始数据里混着'2023-02-30'、'2023/13/01'、'已发货(2023-01-01)'三类非法格式,更没人主动加errors='coerce'再检查NaT占比。这份《数据分析笔试题》不是模拟题库,而是一套压缩包:含6套真实企业级笔试卷(含参考答案与评分逻辑)、配套可运行的Jupyter Notebook验证环境、以及一份《阅卷人视角的失分黑点清单》。它专治“理论满分、实操翻车”的典型症候——适合正在准备面试的数据分析新人、想复盘带教效果的导师、或需要快速搭建内部考核题库的团队负责人。题干全部脱敏自电商、SaaS、本地生活三类业务场景,不考概念背诵,只考你面对一团乱码时,第一反应是删列、报错、还是写正则校验。
2. 笔试题结构拆解:六套题如何覆盖数据分析师的真实工作流
2.1 题型设计逻辑:从“数据获取”到“归因结论”的闭环验证
这六套题并非随机拼凑,而是按数据分析师日常交付链路分层设计:
- 第1-2套:聚焦数据探查与清洗。给定含缺失、异常、格式混乱的原始CSV(如用户行为日志),要求用Pandas完成去重、类型强制转换、异常值标记(非删除)、空值填充策略说明。重点考察对
infer_objects()、astype(dtype, errors='ignore')、clip()等方法的边界理解。 - 第3-4套:侧重指标计算与口径对齐。提供多张关联表(用户主表、订单事实表、商品维度表),要求计算“近7日复购率”,但题干明确写出“复购定义为同一用户在7日内下单≥2次”,且需排除测试账号、退款订单。这里埋了三个陷阱:时间窗口是否包含当日、用户ID是否需去重、退款订单在事实表中是标记字段还是独立状态表。
- 第5-6套:直击归因分析与业务解读。给定A/B测试结果数据(实验组/对照组的点击率、转化率、客单价),要求输出归因结论。但数据中隐藏了“实验组用户平均访问时长比对照组低18%”,需结合漏斗分析判断是流量质量差异还是功能干扰。参考答案不给标准结论,而是列出“支持/不支持该结论的3个数据证据”。
提示:所有题目均附带
data/子目录下的真实数据样例(CSV格式),文件名含v2_cleaned.csv、v3_raw_with_issues.csv等后缀,直接对应题干描述的脏数据状态。不要试图用Excel打开v3_raw_with_issues.csv——它的编码是gb18030,且含BOM头,用pandas读取必须显式指定encoding='gb18030'。
2.2 参考答案的特殊价值:不是标准解,而是阅卷人打分锚点
每套题的answer/目录下,除Python代码外,还有scoring_logic.md文件。例如第4套题的评分逻辑:
- 计算复购率时,若未过滤测试账号(
user_id.str.contains('test|demo', na=False)),扣2分; - 若使用
groupby('user_id').size().count() > 1统计复购用户数,但未处理size()返回的是Series而非布尔值,导致结果恒为1,扣3分; - 若在归因结论中写“功能提升转化率”,但未指出实验组客单价下降12%,此项不得分。
这种颗粒度的扣分项,直接暴露面试官最在意的工程化思维:你是否把业务规则翻译成无歧义的代码逻辑?是否预判了数据漂移对指标的影响?
2.3 Notebook验证环境:一键复现阅卷现场
压缩包中的env/verify_env.ipynb是核心工具。它不教你语法,而是模拟真实阅卷流程:
# 验证第3套题的复购率计算是否符合评分逻辑 def validate_rebuy_rate(df_orders: pd.DataFrame, df_users: pd.DataFrame) -> dict: # 步骤1:过滤测试账号(强制要求) valid_users = df_users[~df_users['user_id'].str.contains('test|demo', na=False)] # 步骤2:合并订单与用户表(必须用inner join,排除无用户信息的脏订单) merged = df_orders.merge(valid_users[['user_id']], on='user_id', how='inner') # 步骤3:按用户统计7日内订单数(关键:时间窗口必须用pd.date_range动态生成) window_start = merged['order_time'].max() - pd.Timedelta(days=7) recent_orders = merged[merged['order_time'] >= window_start] user_order_count = recent_orders.groupby('user_id').size() # 返回:复购用户数、总用户数、复购率 rebuy_users = (user_order_count >= 2).sum() total_valid_users = len(valid_users) return { 'rebuy_users': rebuy_users, 'total_users': total_valid_users, 'rebuy_rate': round(rebuy_users / total_valid_users * 100, 2) } # 执行验证 result = validate_rebuy_rate( pd.read_csv('data/v3_raw_with_issues.csv', encoding='gb18030'), pd.read_csv('data/users_dim.csv') ) print(f"复购率:{result['rebuy_rate']}% (复购用户{result['rebuy_users']}/{result['total_users']})")这段代码的价值不在结果,而在强制你面对三个现实约束:
encoding='gb18030'—— 真实业务数据常混用编码,不指定就报UnicodeDecodeError;how='inner'—— 外键缺失时,left join会引入NaN用户,污染分母;pd.date_range动态计算窗口 —— 静态写死'2023-01-01'会导致结果随数据更新失效。
运行此Notebook,你会立刻看到自己写的代码在哪一步被validate_函数拦截——这才是笔试题真正的“答案”。
3. 避坑指南:阅卷人亲历的五大高频翻车现场
3.1 现象:pd.to_datetime()报错后直接放弃,改用Excel手动修正
原因:未理解errors参数的三种模式。默认'raise'遇非法日期立即中断;'coerce'转为NaT便于后续统计;'ignore'则原样保留字符串(导致后续计算全错)。更致命的是,有人用errors='coerce'后,没检查df['order_time'].isna().sum(),导致NaT占30%却仍强行计算均值。
解决:必须组合使用errors='coerce'+isna().sum()+value_counts(dropna=False)。例如:
df['order_time'] = pd.to_datetime(df['order_time'], errors='coerce') na_count = df['order_time'].isna().sum() if na_count > 0: print(f"警告:{na_count}条记录转时间失败,分布:{df['order_time'].dt.date.value_counts(dropna=False).head(3)}") # 此时应人工抽检原始字符串,针对性写正则清洗3.2 现象:用df.drop_duplicates()去重,却删掉了合法的重复订单
原因:误将“同一用户同一天下两单”视为脏数据。真实业务中,用户并行下单、秒杀抢购都会产生合法重复。正确做法是先确认去重维度(如subset=['user_id','order_id']),而非全表去重。
解决:永远用subset参数明确去重依据,并用keep='first'保留首条(避免随机丢弃)。若题干未明确,必须在答案中注明假设:“假设订单ID唯一,故按order_id去重”。
3.3 现象:计算转化率时,分母用len(df)而非len(df[df['step']=='landing'])
原因:混淆了“全量用户”和“目标漏斗起点用户”。比如计算“从首页点击到下单的转化率”,分母必须是点击首页的用户数,而非所有访问用户。
解决:在代码开头强制声明分母来源:
# 明确标注分母定义 landing_users = df[df['page']=='home']['user_id'].nunique() # 分母:首页独立访客 checkout_users = df[df['event']=='checkout']['user_id'].nunique() # 分子:下单独立用户 conversion_rate = checkout_users / landing_users if landing_users > 0 else 03.4 现象:用df.groupby().agg({'col':'mean'})计算均值,却忽略异常值扭曲结果
原因:未做异常值检测。例如客单价字段含999999(测试数据)、-1(退款标记),直接求均值会让结果偏离真实水平3倍以上。
解决:必须嵌入异常值处理步骤。推荐用IQR法(四分位距):
def safe_mean(series: pd.Series, iqr_multiplier: float = 1.5) -> float: Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - iqr_multiplier * IQR upper_bound = Q3 + iqr_multiplier * IQR filtered = series[(series >= lower_bound) & (series <= upper_bound)] return filtered.mean() if len(filtered) > 0 else np.nan # 使用 avg_order_amount = safe_mean(df['order_amount'])3.5 现象:归因结论写“因为A所以B”,但未提供数据证据链
原因:把分析题答成论述题。真实业务中,“功能上线提升转化率”必须绑定具体数据:
- 实验前7日转化率:X%
- 实验后7日转化率:Y%
- 同期对照组转化率:Z%
- 差异显著性检验p值:<0.05
缺任一环,结论即无效。
解决:用Markdown表格固化证据链:
| 指标 | 实验组 | 对照组 | 差异 | p值 |
|---|---|---|---|---|
| 转化率 | 12.3% | 9.8% | +2.5pp | 0.003 |
| 平均停留时长 | 182s | 215s | -33s | <0.001 |
| 跳出率 | 41% | 38% | +3pp | 0.042 |
4. 数据加载与预处理:从原始CSV到可分析DataFrame的七步必做动作
4.1 第一步:识别并声明数据编码与分隔符
真实笔试数据绝不会用UTF-8+逗号。常见组合:
- 电商日志:
gbk编码 +\t分隔符(因日志含中文和制表符) - SaaS导出表:
utf-8-sig编码 +;分隔符(兼容Excel欧洲版) - 本地生活数据:
gb18030编码 +|分隔符(防地址字段含逗号)
必须动作:先用chardet探测编码,再用csv.Sniffer推断分隔符:
import chardet import csv def detect_file_encoding(file_path: str) -> str: with open(file_path, 'rb') as f: raw_data = f.read(10000) # 读前10KB足够 return chardet.detect(raw_data)['encoding'] def detect_delimiter(file_path: str, encoding: str) -> str: with open(file_path, 'r', encoding=encoding) as f: sample = f.readline() sniffer = csv.Sniffer() dialect = sniffer.sniff(sample) return dialect.delimiter # 使用示例 enc = detect_file_encoding('data/v3_raw_with_issues.csv') sep = detect_delimiter('data/v3_raw_with_issues.csv', enc) df = pd.read_csv('data/v3_raw_with_issues.csv', encoding=enc, sep=sep)注意:
chardet可能误判gb18030为GBK,此时需手动fallback——若pd.read_csv(..., encoding='gbk')报错,则强制用'gb18030'。
4.2 第二步:强制列名小写并去重、去空格
原始数据列名常含大小写混用、空格、括号:'User ID'、'Order_Amount(¥)'、'created_at '。不统一将导致后续df['user_id']报KeyError。
必须动作:用正则清洗列名:
import re def clean_column_names(columns: pd.Index) -> pd.Index: cleaned = [] for col in columns: # 转小写、去首尾空格、替换空格和特殊符号为下划线 new_col = re.sub(r'[^a-z0-9_]+', '_', col.strip().lower()) # 去除连续下划线,确保不以下划线开头 new_col = re.sub(r'_+', '_', new_col).strip('_') cleaned.append(new_col) # 处理重复列名(添加序号) seen = {} final_cols = [] for col in cleaned: if col in seen: seen[col] += 1 final_cols.append(f"{col}_{seen[col]}") else: seen[col] = 0 final_cols.append(col) return pd.Index(final_cols) # 应用 df.columns = clean_column_names(df.columns)此函数能将['User ID', 'Order_Amount(¥)', 'created_at ']转为['user_id', 'order_amount', 'created_at'],且自动处理['id', 'ID']变为['id', 'id_1']。
4.3 第三步:数值列类型推断与安全转换
pd.read_csv()的dtype参数常被滥用。错误做法:dtype={'amount': 'float'}——若该列含'N/A',直接报错。正确路径:
- 先用
object类型读入; - 对疑似数值列,用
pd.to_numeric(..., errors='coerce')转; - 检查转换后
NaN占比,超5%则人工核查; - 最终用
astype('float32')节省内存(非必须,但体现工程意识)。
def safe_numeric_convert(df: pd.DataFrame, numeric_cols: list) -> pd.DataFrame: for col in numeric_cols: if col not in df.columns: continue # 先转为数值,非法值变NaN converted = pd.to_numeric(df[col], errors='coerce') na_ratio = converted.isna().sum() / len(converted) if na_ratio > 0.05: # 超5%缺失需预警 print(f"警告:{col}列{na_ratio:.1%}数据无法转数值,样本:{df[col].dropna().head(3).tolist()}") # 用float32替代默认float64 df[col] = converted.astype('float32') return df # 使用:指定可能为数值的列 df = safe_numeric_convert(df, ['order_amount', 'quantity', 'discount'])4.4 第四步:时间列的三层校验
时间字段是笔试最大雷区。必须执行:
- 格式校验:用正则匹配常见格式(
^\d{4}-\d{2}-\d{2}$,^\d{4}/\d{2}/\d{2}$); - 逻辑校验:检查年份是否在合理范围(如
1990-2030),月份是否1-12; - 业务校验:订单时间不能早于用户注册时间,不能晚于当前日期。
def validate_datetime_column(df: pd.DataFrame, col_name: str, min_year: int = 1990, max_year: int = 2030) -> pd.Series: # 步骤1:基础格式匹配 pattern = r'^(\d{4})[-/](\d{1,2})[-/](\d{1,2})' matches = df[col_name].str.match(pattern) # 步骤2:提取年月日并校验逻辑 extracted = df[col_name].str.extract(pattern) extracted.columns = ['year', 'month', 'day'] extracted = extracted.apply(pd.to_numeric, errors='coerce') valid_year = extracted['year'].between(min_year, max_year, inclusive='both') valid_month = extracted['month'].between(1, 12, inclusive='both') # 步骤3:业务校验(示例:不能晚于今天) today = pd.Timestamp.today().normalize() try: parsed = pd.to_datetime(df[col_name], errors='coerce') not_future = parsed <= today except: not_future = pd.Series([True] * len(df)) return matches & valid_year & valid_month & not_future # 应用 df['order_time_valid'] = validate_datetime_column(df, 'order_time') invalid_mask = ~df['order_time_valid'] print(f"订单时间异常:{invalid_mask.sum()}条,占比{invalid_mask.mean():.1%}")4.5 第五步:分类列的值域冻结
业务字段如status(订单状态)、channel(流量渠道)必须明确定义合法值。否则'pending'和'PENDING'会被当不同值。
必须动作:用pd.Categorical冻结值域,并设置unknown类别:
def freeze_categorical_column(df: pd.DataFrame, col_name: str, valid_values: list, unknown_value: str = 'unknown') -> pd.DataFrame: if col_name not in df.columns: return df # 将非法值统一映射为unknown df[col_name] = df[col_name].apply( lambda x: x if x in valid_values else unknown_value ) # 转为category类型,节省内存并防止新增非法值 df[col_name] = pd.Categorical(df[col_name], categories=valid_values + [unknown_value]) return df # 使用:定义电商订单状态 valid_status = ['paid', 'shipped', 'delivered', 'cancelled', 'refunded'] df = freeze_categorical_column(df, 'order_status', valid_status)此操作后,df['order_status'].unique()只返回['paid', 'shipped', ..., 'unknown'],杜绝'PAID'、'paid '等变体。
4.6 第六步:缺失值的业务语义标注
NaN不是技术问题,是业务信号。user_age为NaN,可能是用户未填写(需标记'not_provided');discount_amount为NaN,可能是无优惠(需标记0)。
必须动作:为每列缺失值赋予业务含义:
def annotate_missing_values(df: pd.DataFrame) -> pd.DataFrame: # age列:NaN表示未提供 if 'user_age' in df.columns: df['user_age'] = df['user_age'].fillna(-1) # 用-1占位 df['user_age_source'] = 'provided' df.loc[df['user_age'] == -1, 'user_age_source'] = 'not_provided' df['user_age'] = df['user_age'].replace(-1, np.nan) # discount列:NaN表示无优惠,填0 if 'discount_amount' in df.columns: df['discount_amount'] = df['discount_amount'].fillna(0) df['discount_type'] = 'none' df.loc[df['discount_amount'] > 0, 'discount_type'] = 'coupon' return df df = annotate_missing_values(df)这样,user_age_source列直接告诉你缺失原因,比单纯df['user_age'].isna().sum()更有业务价值。
4.7 第七步:构建数据质量快照
最后一步,生成data_quality_report.csv,包含每列的:非空率、唯一值数、数值列的均值/标准差、分类列的TOP3值频次。这是阅卷人快速判断你是否做过基础探查的依据。
def generate_data_quality_report(df: pd.DataFrame) -> pd.DataFrame: report = [] for col in df.columns: dtype = str(df[col].dtype) not_null_ratio = df[col].notna().mean() n_unique = df[col].nunique() if pd.api.types.is_numeric_dtype(df[col]): stats = { 'mean': df[col].mean(), 'std': df[col].std(), 'min': df[col].min(), 'max': df[col].max() } else: top3 = df[col].value_counts().head(3).to_dict() stats = {'top3': str(top3)} report.append({ 'column': col, 'dtype': dtype, 'not_null_ratio': round(not_null_ratio, 3), 'n_unique': n_unique, 'stats': str(stats) }) return pd.DataFrame(report) # 保存报告 report_df = generate_data_quality_report(df) report_df.to_csv('data_quality_report.csv', index=False) print("数据质量报告已生成:data_quality_report.csv")运行后,你会得到一张表,其中not_null_ratio低于0.95的列,就是你下一步清洗的重点——这才是数据分析师的起点。
5. 归因分析实战:用双重差分(DID)破解A/B测试的隐藏干扰
5.1 为什么普通A/B对比会失效?一个真实翻车案例
某SaaS产品上线新付费弹窗,实验组转化率提升5%,但上线后次月留存率暴跌12%。回溯发现:实验组用户中,free_trial_days=30的用户占比高达78%(对照组仅42%),而长试用期用户本身付费意愿更低。这就是典型的混杂变量干扰——未控制用户初始属性,导致归因错误。
此时,必须用双重差分法(Difference-in-Differences, DID),它通过比较“实验组前后变化”与“对照组前后变化”的差值,来剥离时间趋势和用户固有差异。公式:
$$ \text{DID} = (Y_{\text{exp,post}} - Y_{\text{exp,pre}}) - (Y_{\text{ctrl,post}} - Y_{\text{ctrl,pre}}) $$
其中Y是转化率,exp/ctrl是实验/对照组,post/pre是实验前后。
5.2 在笔试题中实现DID的四步代码
以第6套题的A/B测试数据为例(ab_test_data.csv含user_id,group('exp'/'ctrl'),week('pre'/'post'),converted(0/1)):
import pandas as pd import numpy as np from scipy import stats # 步骤1:按组和时间聚合转化率 agg_df = ab_data.groupby(['group', 'week'])['converted'].agg(['mean', 'count']).reset_index() agg_df.columns = ['group', 'week', 'conv_rate', 'user_count'] # 步骤2:透视成宽表,方便计算 pivot_df = agg_df.pivot(index='group', columns='week', values='conv_rate').reset_index() pivot_df.columns.name = None pivot_df.columns = ['group', 'pre_conv_rate', 'post_conv_rate'] # 步骤3:计算DID exp_post = pivot_df[pivot_df['group']=='exp']['post_conv_rate'].iloc[0] exp_pre = pivot_df[pivot_df['group']=='exp']['pre_conv_rate'].iloc[0] ctrl_post = pivot_df[pivot_df['group']=='ctrl']['post_conv_rate'].iloc[0] ctrl_pre = pivot_df[pivot_df['group']=='ctrl']['pre_conv_rate'].iloc[0] did_effect = (exp_post - exp_pre) - (ctrl_post - ctrl_pre) print(f"DID估计效应:{did_effect:.3f}(即{did_effect*100:.1f}pp)") # 步骤4:计算置信区间(Bootstrap法,更稳健) def bootstrap_did(data: pd.DataFrame, n_bootstrap: int = 1000) -> tuple: did_samples = [] for _ in range(n_bootstrap): # 有放回抽样 sample = data.sample(n=len(data), replace=True) # 重新聚合 sample_agg = sample.groupby(['group', 'week'])['converted'].mean().unstack('week') if 'pre' in sample_agg.columns and 'post' in sample_agg.columns: exp_diff = sample_agg.loc['exp', 'post'] - sample_agg.loc['exp', 'pre'] ctrl_diff = sample_agg.loc['ctrl', 'post'] - sample_agg.loc['ctrl', 'pre'] did_samples.append(exp_diff - ctrl_diff) return np.percentile(did_samples, 2.5), np.percentile(did_samples, 97.5) ci_lower, ci_upper = bootstrap_did(ab_data) print(f"95%置信区间:[{ci_lower:.3f}, {ci_upper:.3f}]")这段代码输出:
DID估计效应:0.021(即2.1pp) 95%置信区间:[0.008, 0.035]由于区间不包含0,可认为新弹窗有真实正向效应——这比单纯说“提升5%”严谨得多。
5.3 DID的三大前提检验:阅卷人必查项
DID有效需满足三个前提,笔试中必须验证:
- 平行趋势假设:实验前,实验组与对照组转化率变化趋势一致。
→ 检验方法:取实验前2周数据,做group×week交互项回归,系数不显著(p>0.05)。 - 共同支撑假设:实验组与对照组在关键协变量(如用户等级、历史付费次数)上分布接近。
→ 检验方法:对每个协变量,做t检验或KS检验,p值>0.1才认为分布相似。 - 无预期效应:用户在实验开始前不知情,不会因预期而改变行为。
→ 检验方法:检查实验前1天数据,两组转化率差异是否<0.5pp。
# 平行趋势检验示例(用实验前2周数据) pre_two_weeks = ab_data[ab_data['week']=='pre'].copy() # 添加时间虚拟变量(week1/week2) pre_two_weeks['week_num'] = pre_two_weeks.groupby('user_id')['week'].cumcount() + 1 # 回归:converted ~ group + week_num + group:week_num import statsmodels.api as sm X = sm.add_constant(pre_two_weeks[['group', 'week_num']]) X['group_week'] = (pre_two_weeks['group']=='exp') * pre_two_weeks['week_num'] y = pre_two_weeks['converted'] model = sm.OLS(y, X).fit() print(f"平行趋势检验:group×week_num系数={model.params['group_week']:.3f}, p={model.pvalues['group_week']:.3f}") # 若p>0.05,则接受平行趋势假设5.4 当DID不满足时的备选方案:倾向得分匹配(PSM)
如果平行趋势被拒绝(如p=0.002),说明两组基线差异大,此时应转向PSM:用Logistic回归预测用户进入实验组的概率(倾向得分),然后为每个实验组用户匹配最接近的对照组用户。
from sklearn.linear_model import LogisticRegression from sklearn.neighbors import NearestNeighbors # 步骤1:选择协变量(必须与实验分配无关) covariates = ['user_tier', 'past_purchase_count', 'days_since_signup'] X = ab_data[covariates + ['group']] y = (ab_data['group'] == 'exp').astype(int) # 步骤2:训练倾向得分模型 psm_model = LogisticRegression(max_iter=1000) psm_model.fit(X[covariates], y) ab_data['propensity_score'] = psm_model.predict_proba(X[covariates])[:, 1] # 步骤3:为实验组用户匹配对照组(最近邻) exp_users = ab_data[ab_data['group']=='exp'][['user_id', 'propensity_score']] ctrl_users = ab_data[ab_data['group']=='ctrl'][['user_id', 'propensity_score']] nn = NearestNeighbors(n_neighbors=1, algorithm='ball_tree') nn.fit(ctrl_users[['propensity_score']]) distances, indices = nn.kneighbors(exp_users[['propensity_score']]) # 匹配结果 matched_ctrl = ctrl_users.iloc[indices.flatten()] print(f"PSM匹配完成:{len(exp_users)}实验组用户匹配{len(matched_ctrl)}对照组用户")匹配后,即可在匹配样本上做简单A/B对比——这比强行用DID更可信。
5.5 归因结论的终极表达:用“证据链”代替“我认为”
阅卷人最反感“我认为新功能提升了体验”这类主观表述。正确写法是:
“基于双重差分分析(DID),新弹窗使转化率提升2.1pp(95%CI: [0.8, 3.5]pp),且平行趋势检验p=0.23,满足前提假设。进一步,倾向得分匹配(PSM)显示,在控制用户等级、历史购买数后,实验组转化率仍高出1.9pp(p=0.008)。因此,效应具有稳健性。”
这种写法,把每一个结论都锚定在具体检验方法、数值、p值上。从那以后我每次写归因结论,都强制走一遍“方法→结果→前提检验→稳健性验证”四步链,哪怕笔试时间只剩5分钟,也要在答案末尾补上一句:“DID平行趋势检验p=0.23,满足假设”。希望帮到你。
本文还有配套的精品资源,点击获取