简介:围绕大学生网络消费行为展开的调查报告文档,适用于电子商务、市场营销、社会学等专业的学生与教师,也可为校园消费调研或课程作业提供参考样本。全文以问卷调研为主线,依次覆盖调查背景与目的、提纲设计、样本选取与抽样方法,并对年级分布、店铺搜索路径、网店选择影响因素、常用购物平台、购买商品类别、购物频率、发展趋势与交易安全性等维度逐一展开数据统计与图表分析,其中随机抽取30个样本、回收有效问卷28份,可作为问卷设计与结果分析的实例参照。资源包共1个doc文件,体积约1.49MB,打开即可阅读完整正文与配套图表,无需额外安装工具。已有86人学习下载,适合需要了解大学生网购特征、撰写同类调查报告时对照参考。
1. 从一份《大学生网上购物情况调查报告.doc》说起:问卷到结论之间隔了什么
每年学期末,教务系统里总会出现一批文件名高度相似的文档:《大学生网上购物情况调查报告.doc》。常见版本是三五百份问卷、几张饼图、一句“可见大学生网购热情高涨”。但如果这份报告要用于课程设计答辩、校园渠道立项、或者电商平台的校园用户研究,评审第一句话往往是:“样本多少份、怎么抽的、月均消费这个数字怎么算出来的?”
真正要交付的并不是一篇 Word 文档,而是一条从问卷设计、字段编码、数据清洗、统计检验到自动排版的完整数据管线。它解决的是“数字站不住脚”这个具体问题:口径写清楚、脚本能重跑、样本量算得出来。适合看这份内容的人有三类——要交调研作业的学生、带学生做实证分析的老师,以及做校园用户研究的运营和数据分析新人。
下面按“问卷怎么设计、数据怎么洗、结论怎么算、文档怎么出”的顺序,把每一环的命令、参数和坑拆开讲。
2. 问卷设计与变量建模:把“大学生网上购物”拆成可统计字段
很多人是先写题目、后想分析,结果收回来才发现“你一般买什么”是多选,却按单选统计了。正确的顺序是先定字段名和取值,再写题面。字段名一旦确定,中途不要改,因为后面 Excel 透视、SPSS 变量视图、pandas 列名全靠它对齐。
2.1 变量分层:人口学、行为、态度三层结构
把整份问卷拆成三层,每层对应不同的统计方法:人口学变量做分组,行为变量做主分析,态度量表做信度和因子。下面这张表是校园网购调研里比较通用的一套字段,可以直接改成自己的版本。
| 字段名 | 题型 | 取值 / 编码 | 后续用途 |
|---|---|---|---|
| gender | 单选 | 1=男,2=女 | 分组变量,卡方交叉 |
| grade | 单选 | 1~4 对应大一至大四 | 分组变量、趋势分析 |
| monthly_cost | 区间单选 | 1~6 对应生活费档位 | 分箱后作定序变量 |
| platform | 多选 | 淘宝/京东/拼多多/抖音/唯品会/其他 | 拆哑变量算渗透率 |
| category | 多选 | 服饰/数码/食品/美妆/图书/虚拟服务 | 拆哑变量做品类结构 |
| freq | 单选 | 1=几乎不买,5=几乎每天 | 定序变量,映射月单量 |
| avg_ticket | 数值填空 | 单位:元 | 连续变量,做回归 |
| impulse | Likert 1~5 | 1=完全不同意,5=完全同意 | 量表题,算总分 |
| satis | Likert 1~5 | 同上 | 信度分析、因子分析 |
| duration | 平台自动记录 | 秒 | 清洗阶段剔除乱填 |
这里有两个容易被忽略的点。第一,生活费不要让人直接填数字,学生对这个数往往只有一个模糊印象,填出来的尾数全是 0 和 500,做区间单选反而更稳。第二,多选题的选项集合必须提前冻结,“其他”也要有独立列,否则每批数据的哑变量列数不一样,脚本第二次跑就会报列不匹配。
2.2 抽样与样本量:95% 置信度、±5% 误差要收多少份
样本量不是拍脑袋定的,用比例估计公式算:n = Z²p(1-p)/e²。p 取 0.5 是最保守的估计(方差最大),Z 在 95% 置信度下取 1.96,允许误差 e 取 0.05。算出来 385 份,但校园调研很少是简单随机抽样,整班发放会带来设计效应,再加上无效问卷,实际发放量要往上抬。
import math def calc_sample(z=1.96, p=0.5, e=0.05, deff=1.3, valid_rate=0.85): """ z: 置信水平对应的分位数,95% 用 1.96 p: 目标比例,未知时取 0.5 最保守 e: 允许误差,常规取 0.05 deff: 设计效应,整群/整班抽样取 1.2~1.5 valid_rate: 有效回收率,线上问卷一般 0.8~0.9 """ n0 = z ** 2 * p * (1 - p) / e ** 2 # 简单随机抽样所需样本 n = math.ceil(n0 * deff) # 乘设计效应 n_issue = math.ceil(n / valid_rate) # 反推需要发放的份数 return round(n0, 2), n, n_issue print(calc_sample()) # (384.16, 500, 589)按这组参数,发放 589 份才比较稳妥,实操里一般凑整到 600 份。发下去还要做配额,不能让某个年级占到七成:按年级 4 档、性别 2 类做交叉配额,专业大类做兜底。配额表要写进报告附录,这决定了“样本对学校有没有代表性”这一问能不能答上来。
2.3 用 openpyxl 生成带回填校验的回收模板
线上问卷平台大多能直接导出 xlsx,但如果调研是在教室里发纸质表、后面人工录入,或者需要让同学统一填一个表格,最好生成一个带下拉约束的模板,从源头减少脏数据。
from openpyxl import Workbook from openpyxl.worksheet.datavalidation import DataValidation from openpyxl.styles import Font, Alignment wb = Workbook() ws = wb.active ws.title = "responses" headers = ["student_id", "gender", "grade", "monthly_cost", "platform", "category", "freq", "avg_ticket", "impulse", "satis", "duration"] ws.append(headers) for c in ws[1]: c.font = Font(bold=True) c.alignment = Alignment(horizontal="center") ws.freeze_panes = "A2" # 冻结表头,录入时不易错行 # 性别、年级用下拉列表,避免出现"男生""男 "这类同义异形 dv_gender = DataValidation(type="list", formula1='"1,2"', allow_blank=False) dv_grade = DataValidation(type="list", formula1='"1,2,3,4"', allow_blank=False) ws.add_data_validation(dv_gender) ws.add_data_validation(dv_grade) dv_gender.add("B2:B2000") dv_grade.add("C2:C2000") # 客单价限制为 0~20000 的数值,挡住"一顿饭"这类文本 dv_ticket = DataValidation(type="decimal", operator="between", formula1=0, formula2=20000, allow_blank=True) ws.add_data_validation(dv_ticket) dv_ticket.add("H2:H2000") wb.save("survey_template.xlsx")三个参数值得说明:allow_blank=False用在关键分组字段上,逼着录入选值;freeze_panes="A2"是给人工录入减负,行数一多没有冻结表头很容易串行;数值型字段的范围约束比事后清洗便宜得多。
2.4 预调研:30 份试填要盯的三个指标
正式发放前先找 30 个人试填,看三件事:量表题的信度、填答时长分布、以及逻辑矛盾率。信度用 Cronbach's α,一般要求 0.7 以上,低于 0.6 说明题目在测不同的东西,要删题或改表述。
import pandas as pd def cronbach_alpha(df): """df 的每一列是一道量表题,行是样本""" df = df.dropna() k = df.shape[1] var_items = df.var(axis=0, ddof=1).sum() # 各题方差之和 var_total = df.sum(axis=1).var(ddof=1) # 总分方差 return k / (k - 1) * (1 - var_items / var_total) items = pd.read_excel("pilot_30.xlsx")[["impulse", "satis", "trust", "repurchase"]] print(round(cronbach_alpha(items), 3))填答时长这一项最容易被跳过。线上问卷平台会自动记录,低于 60 秒的样本基本可以判定为乱填;纸质录入没有时长,可以用“同一份表里前后矛盾的题目”替代,比如前面选了“几乎不网购”、后面客单价填了 300 元。
3. 数据清洗与字段编码:把回收表变成能跑的 DataFrame
清洗是整个流程里最耗时间的一步,通常占掉一半以上工时。原则是:所有处理都写成代码,不手工改单元格。手工改过的表没人能复现,被人追问一句“这几份为什么删了”就答不上来。
3.1 读入与类型统一
先用dtype=str全部按字符串读进来,再逐列转换,避免 pandas 把“1”和“1.0”混着解析,也避免学号被当成数字丢掉前导零。
import pandas as pd import numpy as np raw = pd.read_excel("survey_raw.xlsx", sheet_name="responses", dtype=str) raw.columns = [c.strip().lower() for c in raw.columns] # 统一列名格式 # 逐列声明目标类型,转换失败的一律变 NaN,不静默丢数据 num_cols = ["grade", "monthly_cost", "freq", "avg_ticket", "impulse", "satis", "duration"] for col in num_cols: raw[col] = pd.to_numeric(raw[col], errors="coerce") raw["gender"] = raw["gender"].astype("Int64") df = raw.copy() print(df.dtypes)errors="coerce"是关键参数:它把“未填”“无”这类文本变成 NaN,而不是抛异常中断脚本。这样后面能统一按缺失值规则处理,也能顺便统计出哪一列脏得最厉害。
3.2 缺失值、逻辑矛盾与异常值的三条规则
缺失值不能一律删。关键字段(性别、年级、网购频次)缺失的样本对分组分析没价值,直接剔除;非关键字段(客单价、满意度)缺失可以保留,做对应分析时再按列删。逻辑矛盾和异常值要按规则表处理,处理动作和影响样本量都要记录。
| 检查项 | 判定条件 | 处理动作 | 典型影响 |
|---|---|---|---|
| 填答过快 | duration < 60 秒 | 整条剔除 | 剔除 3%~8% |
| 重复提交 | student_id 重复 | 保留最早一条 | 剔除 1%~3% |
| 关键字段缺失 | gender/grade/freq 为空 | 整条剔除 | 剔除 2%~5% |
| 行为矛盾 | freq=1 且 avg_ticket>0 | 客单价置 0 | 修正而非剔除 |
| 数值越界 | avg_ticket>20000 或 <0 | 按缺失处理 | 影响 <1% |
| 量表直线作答 | impulse~satis 全部同值 | 标记但不删,做敏感性分析 | 5%~10% |
# 1) 填答时长过滤 df = df[df["duration"].fillna(0) >= 60] # 2) 同一学号重复提交,保留第一次 df = df.sort_values("duration", ascending=False).drop_duplicates("student_id", keep="first") # 3) 关键字段缺失剔除 df = df.dropna(subset=["gender", "grade", "freq"]) # 4) 行为矛盾修正 mask = (df["freq"] == 1) & (df["avg_ticket"].fillna(0) > 0) df.loc[mask, "avg_ticket"] = 0.0 # 5) 数值越界按缺失处理 df.loc[(df["avg_ticket"] > 20000) | (df["avg_ticket"] < 0), "avg_ticket"] = np.nan print("清洗后有效样本:", len(df))注意第 2 步的keep="first"是配合上一行的排序用的:先按时长降序排,再保留第一条,等于留下填得最认真的那份。如果直接按原顺序去重,留下的可能是随手点的那份。
3.3 多选题拆成哑变量矩阵
多选题导出的单元格内容是“淘宝,京东,拼多多”这种字符串,必须先冻结类别集合再拆列。用get_dummies直接拆的问题是:某批数据里没人选“唯品会”,列就消失了,两次运行的结果没法对齐。
def split_multi(value): if pd.isna(value): return [] text = str(value).replace(",", ",").replace("、", ",") return [x.strip() for x in text.split(",") if x.strip()] PLATFORMS = ["淘宝", "京东", "拼多多", "抖音", "唯品会", "其他"] CATEGORIES = ["服饰", "数码", "食品", "美妆", "图书", "虚拟服务"] for p in PLATFORMS: df[f"platform_{p}"] = df["platform"].apply(lambda s: int(p in split_multi(s))) for c in CATEGORIES: df[f"cat_{c}"] = df["category"].apply(lambda s: int(c in split_multi(s))) # 平台渗透率 = 选择人数 / 有效样本 penetration = df[[f"platform_{p}" for p in PLATFORMS]].mean().sort_values(ascending=False) print((penetration * 100).round(1))列名统一加前缀(platform_、cat_)是为了后面批量取列:df.filter(like="platform_")一行就能把整块矩阵拿出来做交叉分析,不用手写列名列表。
3.4 分箱与派生变量:月均消费金额怎么算
原始问卷里没有“月均消费金额”这个字段,它是算出来的。算法必须在报告里写明白,否则同一个数据集两个人能算出两个数。
# 生活费分箱:左闭右开,避免 2000 这种边界值落进两个箱 bins = [0, 1000, 1500, 2000, 2500, 3000, np.inf] labels = ["1000以下", "1000-1500", "1500-2000", "2000-2500", "2500-3000", "3000以上"] df["cost_bin"] = pd.cut(df["monthly_cost_num"], bins=bins, labels=labels, right=False) # 频次映射为月单量(经验系数,需在报告口径中说明) freq_to_orders = {1: 0.5, 2: 2, 3: 4, 4: 8, 5: 20} df["monthly_orders"] = df["freq"].map(freq_to_orders) df["monthly_amount"] = df["monthly_orders"] * df["avg_ticket"] print(df["monthly_amount"].describe().round(2))right=False表示左闭右开,边界值 2000 归到“2000-2500”而不是“1500-2000”,这样区间不重不漏。freq_to_orders里的系数是估计值,写报告时要注明“月单量由频次档位折算,属于近似口径”,这一句能挡掉一半的质疑。
4. 统计分析与显著性检验:从描述统计到客单价回归
分析部分要回答三个问题:大学生网购的结构长什么样、不同群体之间有没有真实差异、客单价受什么影响。前两个靠描述统计和卡方,第三个靠回归。
4.1 描述性统计与消费画像
先把核心指标的分布拉出来,中位数和分位数比均值更能说明问题,因为客单价是典型的右偏分布,均值会被少数高消费样本拉高。
core = ["avg_ticket", "monthly_amount", "impulse", "satis"] desc = df[core].describe(percentiles=[0.25, 0.5, 0.75]).T desc["cv"] = desc["std"] / desc["mean"] # 变异系数,衡量离散程度 print(desc.round(2))cv比标准差更好用,因为它去掉了量纲。客单价的变异系数超过 1 就说明个体差异极大,报告里只写均值会有误导性。
4.2 交叉表与卡方检验:性别、年级和网购频次有没有真差异
交叉表看分布,卡方检验判断差异是不是抽样波动。
from scipy.stats import chi2_contingency import numpy as np ct = pd.crosstab(df["grade"], df["freq"]) chi2, p, dof, expected = chi2_contingency(ct) n = ct.values.sum() cramers_v = np.sqrt(chi2 / (n * (min(ct.shape) - 1))) print(f"chi2={chi2:.2f}, p={p:.4f}, dof={dof}, V={cramers_v:.3f}") print("期望频数小于5的格子占比:", (expected < 5).mean().round(3))两个参数要点:p < 0.05 只说明“有差异”,差异有多大要看 Cramér's V,0.1 以下算弱相关;expected < 5的格子占比超过 20% 时卡方结果不可靠,要把年级合并成“低年级/高年级”再跑一遍。
4.3 客单价的多元线性回归与稳健标准误
import statsmodels.api as sm from statsmodels.stats.outliers_influence import variance_inflation_factor X = df[["monthly_cost_num", "freq", "impulse", "satis"]].astype(float) X = sm.add_constant(X) y = df["avg_ticket"].astype(float) model = sm.OLS(y, X, missing="drop").fit(cov_type="HC3") # HC3 稳健标准误 print(model.summary()) vif = pd.DataFrame({ "变量": X.columns, "VIF": [variance_inflation_factor(X.values, i) for i in range(X.shape[1])] }) print(vif.round(2))cov_type="HC3"处理异方差,客单价这类右偏数据几乎一定存在异方差,用普通标准误会把显著性说得过于乐观。VIF 超过 10 说明存在多重共线性,常见于把 “freq” 和折算出来的 “monthly_orders” 同时放进模型——这两个变量本质上是同一个东西,选一个就行。
4.4 可视化:中文字体与三个必调参数
图表在 Word 里显示成方框,九成是因为没设中文字体。
import matplotlib matplotlib.use("Agg") # 无显示环境下也能出图 import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False # 负号正常显示 plt.rcParams["figure.dpi"] = 150 # 插入 Word 不糊 fig, ax = plt.subplots(figsize=(7, 4)) df.boxplot(column="avg_ticket", by="cost_bin", ax=ax, grid=False) ax.set_title("不同生活费档位的客单价分布") ax.set_xlabel("月生活费档位") ax.set_ylabel("客单价(元)") plt.suptitle("") plt.tight_layout() plt.savefig("fig_ticket_box.png", bbox_inches="tight")| 图形类型 | 适合的变量组合 | 关键参数 | 常见误用 |
|---|---|---|---|
| 分组柱状图 | 分组变量 × 定序变量 | rot=0防标签倾斜 | 用堆叠柱比多选占比 |
| 箱线图 | 分组变量 × 连续变量 | showfliers=False看主体 | 样本量 < 30 时硬画 |
| 热力图 | 多选哑变量的共现矩阵 | annot=True标数值 | 拿它当相关系数图 |
| 散点加拟合线 | 两个连续变量 | 先看分布再加lowess | 直接线性拟合有异常点的数据 |
多选题的占比不能用堆叠柱状图表示,因为各选项之间有重叠,加起来会超过 100%,这类数据只适合做条形图。
5. 结果落地与复现:自动生成 .doc 与一键重跑
分析做完,最后一步是把数字和图塞进那份《大学生网上购物情况调查报告.doc》。手工复制粘贴的后果是,改一个清洗规则就要重新排版一遍。用 python-docx 从模板生成,能把排版成本压到接近零。
5.1 用模板文件保留封面与页眉样式
from docx import Document from docx.shared import Pt, Cm doc = Document("template.docx") # 预置封面、页眉、正文字体 doc.add_heading("大学生网上购物情况调查报告", level=1) doc.add_paragraph( f"本次调研共发放 {ISSUED} 份,回收有效问卷 {len(df)} 份," f"有效回收率 {len(df)/ISSUED:.1%};置信水平 95%,允许误差 ±5%。" ) doc.add_heading("一、样本结构与消费概况", level=2) doc.add_paragraph(f"有效样本中男生占比 {male_ratio:.1%}," f"月均网购金额中位数为 {median_amount:.0f} 元。") doc.add_picture("fig_ticket_box.png", width=Cm(14)) doc.add_heading("二、群体差异检验", level=2) doc.add_paragraph(f"年级与网购频次的卡方检验 p={p:.4f}," f"Cramér's V={cramers_v:.3f}。") doc.save("大学生网上购物情况调查报告.doc")Document("template.docx")而不是Document(),是让封面、页眉、正文样式沿用模板,add_heading的 level 只是逻辑层级,视觉样式由模板决定。图片宽度用Cm(14)而不是像素,是因为 A4 页边距 2.5 厘米时正文宽度约 16 厘米,14 厘米留出边距不会撑破版面。
5.2 一键重跑:把清洗规则和结果绑在同一个脚本里
import argparse parser = argparse.ArgumentParser() parser.add_argument("--input", default="survey_raw.xlsx") parser.add_argument("--issued", type=int, default=600, help="实际发放份数") parser.add_argument("--out", default="大学生网上购物情况调查报告.doc") parser.add_argument("--seed", type=int, default=42) args = parser.parse_args() # 清洗规则里的任何随机过程都固定种子,保证两次运行结果一致 import numpy as np rng = np.random.default_rng(args.seed)命令行是python run_report.py --input survey_raw.xlsx --issued 600 --out 报告.doc。把--issued做成参数而不是写死,是因为答辩时被问到“回收率怎么算的”,改一个数就能重新出表。下面这张自查表放在报告附录第一页,比任何解释都管用。
| 检查项 | 应当对上的位置 |
|---|---|
| 发放份数、有效份数、回收率 | 方法章节 与 附录脚本输出 |
| 样本量计算公式与参数 | 抽样设计小节 |
| 剔除规则与影响条数 | 清洗规则表 |
| 月均消费金额的折算系数 | 变量定义表 |
| 卡方、回归的 p 值与效应量 | 分析章节 与 图表标题 |
| 脚本、参数与随机种子 | 附录命令示例 |
把--seed、样本量对账表和清洗规则表一起写进报告附录,下一次有人质疑某个数字时,你能在三分钟内重跑出同一张表——这份报告的硬度,就体现在这一步。
本文还有配套的精品资源,点击获取