Python实现问卷数据统计:从数据清洗到交叉分析
简介面向问卷调查数据统计需求2024年Python实现资料包聚焦问卷数据的读取、清洗、统计分析与结果导出等环节适合具备一定Python基础的学生、数据分析人员及市场调研从业者参考。压缩包共129个文件约185.63MB内含PPT类演示文稿41个、Word文档25个、Markdown笔记15个、PDF文档11个以及Excel表格、Python脚本和MP4操作演示视频等类型覆盖代码、数据、文档与展示材料。尤为突出的是文档部分整合了ISO9000系列标准、测试用例编写规范、软件设计规格说明书、概要设计说明书、可行性研究报告等软件工程模板可辅助用户规范从项目计划到验收测试的完整开发流程。Markdown笔记与Word文档相互配合适合边阅读边动手Excel表格可替换为自有问卷数据直接运行代码加速落地。配合录屏讲解读者既能跟进实现细节也能利用PPT进行答辩或汇报掌握问卷统计项目的排错思路和成果展示技巧。目前已有208人学习适合作为课程设计、毕业设计或小型调研项目的直接参考资料。1. 问卷数据统计一门被低估的预处理课拿到“2024 Python实现调查问卷数据统计”这个标题很多人的第一反应是“不就是算算百分比、画几张图嘛”。但真正用 Python 跑过完整问卷数据的人都知道统计本身只占整个工作量的三成剩下七成都在和数据较劲单选题选项里的错别字、多选题被分成了好几列、量表题有人选了“其他”还填了备注、问卷系统导出的 Excel 自带合并单元格。这些问题不处理算出来的平均数、交叉表全是错的而且错得毫无声息。这篇文章想讲的就是用 pandas 和 matplotlib 这类常规武器把一份“原始得像刚从田野里挖出来”的问卷数据变成能直接写进报告、能支撑决策的统计结果。适合正在处理课程作业问卷、客户满意度调查或者产品反馈表单的从业者也适合想系统性梳理问卷数据清洗流程的初学者。先立一个结论问卷统计的代码难度不高但每一步选择都有代价读完全文你至少能避开十个常见的坑。2. 先用 pandas 把问卷数据读进来编码、类型和结构检查2.1 为什么是 pandas 而不是 Excel 或 SPSS问卷数据本质上是“行是样本、列是题目”的表格结构这种结构恰好是 pandas 的 DataFrame 最擅长的领域。Excel 能做透视表但遇到多选题拆分列、量表题反向计分、开放题文本清洗时操作会变得机械且容易出错SPSS 功能强大但对很多人来说不是随手就能装上的环境。pandas 的优势在于数据清洗、变换、聚合和可视化全部可以在同一个 Python 脚本里完成而整个过程是可以复现的——这意味着你今天处理完的结果三个月后有人问“这个百分比怎么算出来的”你可以直接把脚本丢给他看而不是翻聊天记录找当时的操作步骤。我一般会先建一个虚拟环境安装 pandas、openpyxl用于读写 Excel和 matplotlib。如果原始文件是 CSV注意指定encodingutf-8-sig这样可以避免 Windows 下打开 CSV 时中文乱码如果是 Excel 文件则用pd.read_excel()并配合sheet_name参数读入指定工作表。还有一点容易被忽略问卷平台导出的文件经常表头占两行第一行是题号、第二行是题目文本所以header1这种参数要灵活使用。import pandas as pd df pd.read_excel(survey_raw.xlsx, sheet_name0, header1) print(df.shape) print(df.columns.tolist()[:10])这段代码做了三件事读取第一个工作表、跳过默认表头改用第二行作为列名、输出数据的形状和前 10 列名。df.shape返回(行数, 列数)如果问卷回收了 500 份、题目有 20 道你会看到(500, 20)左右。如果列数比预期多出不少往往是因为多选题被拆成了多列或者是存在“其他请注明”这种开放填空列。df.columns.tolist()是用来检查列名的实际内容因为问卷平台的题号命名经常是“Q1”“Q1_2”这种风格不先看一眼后面写代码全靠猜。2.2 数据类型检查先看 dtypes再谈统计读入数据后不要急着算描述性统计先执行df.info()看每一列的数据类型。问卷数据里最典型的类型错乱是年龄列明明应该存数字结果因为有人填了“25岁”变成了字符串或者是五级量表题非常不满意到非常满意被读成了 object因为某个样本选了“3一般”这种带括号的文本。这些都会导致df[age].mean()直接报错或者在没有任何报错的情况下算出错误结果——比如把字符串做了拼接而不是求和。我的习惯是先写一个简单的循环把每一列的唯一值数量打印出来。唯一值数量少的列比如小于 10基本是单选题或量表题唯一值数量接近样本量的列要么是开放题要么是 ID 列。这个检查能帮你快速锁定需要处理的列也能发现异常值比如性别列出现了“男”“女”“Male”“m”四种取值说明问卷平台的选项映射没统一或者有人在填写时自行输入了内容。for col in df.columns: uniq_count df[col].nunique(dropnaTrue) print(f{col}: 唯一值数量 {uniq_count}, dtype {df[col].dtype})nunique(dropnaTrue)统计非空唯一值数量dtype 显示当前数据类型。这一步的输出要逐列核对不要只扫一眼就过。数据量大的时候可以只看 object 类型的列因为这些列最可能需要清洗。还有一个细节如果某一列的唯一值数量是 1说明这列是常量列比如问卷平台自动添加的“提交时间”列或者“渠道编号”列统计分析时可以直接 drop 掉避免后续交叉表里出现无意义的维度。2.3 缺失值处理不只有 dropna 一条路问卷数据的缺失有三种来源受访者跳过题目、逻辑跳转导致后续题目为空、数据导出时格式转换失败。df.isnull().sum()可以快速统计每列缺失数量但关键决策在于“怎么处理”。如果缺失比例低于 5%且该列是主要的分析变量直接删除缺失样本是常见做法如果缺失比例高比如超过 20%就要反思是不是题目设计有问题——可能是问题表述不清晰也可能是该题目涉及敏感话题导致大量拒答。我处理问卷缺失值的原则是先填空再删除。填空只用于连续型变量比如用中位数填充年龄列因为年龄分布通常偏态均值会被极端值拉高分类变量不做填充而是把它保留为“缺失”类别。实现上df[age].fillna(df[age].median())是常规操作。还要注意一点pandas 的dropna()默认删除只要含任一缺失值的行这在问卷数据里经常把有效样本“误杀”了所以要用subset参数指定只在核心题目列上检查缺失。core_cols [Q1, Q2, Q3_score] df_clean df.dropna(subsetcore_cols, howany) print(f清洗前样本量: {len(df)} - 清洗后: {len(df_clean)})subset只针对核心列做缺失检查howany表示这些列里只要有一个为空就删除该行。这样做的前提是你心里清楚哪些题目是必答的统计分析项哪些题目本来就设计了跳转逻辑。如果问卷数据里有大量“未填写”的空单元格但它们在原始平台上显示为“-”或者“跳过”导出后可能变成空字符串而不是 NaN所以清洗第一步还要做df.replace(, pd.NA)把空字符串统一转成缺失值。3. 单选题和多选题的编码与频次统计别让“其他”选项毁掉百分比3.1 单选题编码把文本映射成数值再算频次问卷平台导出的单选题通常是文本形式比如性别列是“男”“女”满意度列是“非常满意”“满意”“一般”“不满意”“非常不满意”。直接对这些文本做value_counts()是可以的但有两个问题一是排序混乱文本排序和量表等级顺序不一致画图时会出现“一般”在最前面、“非常满意”在最后面的尴尬二是后续如果要算平均分或者做相关性分析文本类型无法参与计算。常见做法是建立编码映射表用 pandas 的map()函数把文本转换成数值。对于量表题数值和选项含义是一一对应的比如 1 到 5 代表从“非常不满意”到“非常满意”对于无序分类变量如性别、职业编码只是为了给后续的交叉统计提供服务。我一般会把原始文本保留为单独一列把编码列加到数据框最右侧这样既能追溯原始答案又能直接参与计算算一种“后悔药”。score_map { 非常不满意: 1, 不满意: 2, 一般: 3, 满意: 4, 非常满意: 5 } df[Q3_score] df[Q3].map(score_map) print(df[Q3_score].value_counts().sort_index())map()遇到映射字典里没有的键时会返回 NaN。这既是好事也是坏事好事是能立刻暴露出数据里的异常取值比如有人填了“满意比较满意”坏事是该样本被静默置为缺失后续统计结果中该题的有效样本量会下降。所以 map 之后一定要执行一次df[Q3_score].isna().sum()和df.loc[df[Q3_score].isna(), Q3]把异常文本拉出来看通常是问卷平台选项文本和导出文本不一致。批量修改映射字典、把“满意比较满意”这类文本并入“满意”组的操作也是在这一步完成的。频次统计的核心代码是value_counts()但要注意两个参数normalizeTrue会输出占比而不是频数dropnaFalse会保留缺失值作为一个类别。问卷报告的“有效百分比”通常不把缺失计入分母但如果你直接用normalizeTrue分母是全部样本而不是有效样本两者可能差出几个百分点。我一般这样写freq df[Q3_score].value_counts(normalizeTrue).mul(100).round(1) freq_valid df[Q3_score].dropna().value_counts(normalizeTrue).mul(100).round(1) print(含缺失占比:) print(freq) print(\n有效样本占比:) print(freq_valid)mul(100)把比例转成百分比round(1)保留一位小数。两组对比能直观看出缺失值对结果的影响。多提一句在报告里写“有效百分比”时一定要把分母是谁写清楚否则读者可能对数字来源产生疑惑。3.2 多选题的“宽表”拆分一题多列怎么合并统计多选题是问卷数据清洗的重灾区。问卷平台通常会把一道多选题拆成多列每列对应一个选项值为 1 或 0选中则 1未选则 0列名类似Q4_1、Q4_2、Q4_3。这类数据不能直接做value_counts()因为每列只能反映一个选项的选中情况无法直接计算“选择人数最多的选项排名”。正确做法是先筛选出所有属于该题的列然后用sum()逐列统计选中次数再用max()判断每个样本是否至少选了一个选项。multi_cols [Q4_1, Q4_2, Q4_3, Q4_4] selected_counts df[multi_cols].sum() print(selected_counts.sort_values(ascendingFalse)) df[Q4_any] df[multi_cols].max(axis1) valid_n df[Q4_any].sum() print(f本题有效作答人数: {valid_n})selected_counts.sort_values(ascendingFalse)直接给出每个选项的选中次数排名max(axis1)在每行上取多列最大值只要选过任意一个选项就为 1。计算选项选中率时分母应该是有效作答人数而不是全体样本所以要先算valid_n。这里还有一个常见坑有些人把未作答的多选题列全部填了 0但这其实是“未选”和“未答”两种状态如果不加以区分选项选中率会被整体拉低。区分方法是在问卷平台导出时设置一个“未回答”的占位值或者在读入数据后用“是否所有多选列为空”来判定未作答。下面的代码演示了第二种方式df[Q4_is_empty] df[multi_cols].sum(axis1) 0 print(df[Q4_is_empty].sum())如果Q4_is_empty为 True 的行数较多说明许多人跳过了这道题。统计时用~df[Q4_is_empty]做行筛选只保留有效作答的样本去计算选项选中率。这在报告里要单独说明“选项选中率 选中该选项的人数 / 本题有效作答人数”。3.3 “其他请注明”的处理策略几乎每份问卷的最后都有一道“其他请注明”的开放填空题。这类题在统计上最麻烦因为文本答案五花八门直接算频次会得到几十个独立类别毫无意义。处理策略分两步先看文本长度太短的比如只有一两个字的可以直接归类为“其他”然后是人工阅读归类把相似语义的答案合并成一个新类别。如果开放题比较多而且你希望用 Python 做初步的自动归类最简单的方案是基于关键词匹配而不是上复杂的 NLP 模型。问卷开放题的文本量通常只有几十到几百条人工阅读完全可控自动分类反而容易因为同义词问题产生误判。我一般会把开放题的答案单独导出成一个 CSV通读一遍后手动建立“原始文本 - 标准化类别”的映射表然后用map()映射回数据框open_end_map { 功能不稳定: 稳定性问题, 经常闪退: 稳定性问题, 界面难看: 界面体验, 希望增加导出功能: 功能需求 } df[Q5_grouped] df[Q5].map(open_end_map).fillna(其他) df[Q5_grouped].value_counts()fillna(其他)把没匹配上的所有文本统一归入“其他”。这里的关键是映射表必须人工审核过不能用 Chat 类工具直接生成规则因为问卷答案是真实用户的表达很多说法不在你预定义的关键词里。把“其他”选项单独统计出来是有决策价值的——如果“其他”占比过高比如超过 20%说明这道题的选项设计没覆盖真实情况问卷本身需要迭代。4. 描述性统计与分组对比用交叉分析代替“只会算平均分”4.1 量表题的均值和标准差先分组再比较多份问卷统计报告的通病是只报告总体的平均满意度比如“总体满意度均值为 4.2 分”但这个数字掩盖了太多信息——不同年龄段的满意度可能差异极大不同使用频率的用户对某个功能的评价也可能完全不同。正确的做法是先按背景变量分组再计算各组的均值和标准差并做可视化对比。pandas 的groupby()配合agg()可以一次性算出多个指标。假设背景变量是年龄已分箱和性别核心量表是Q3_scoreage_bins [0, 18, 30, 45, 60, 100] age_labels [18, 18-30, 31-45, 46-60, 60] df[age_group] pd.cut(df[age], binsage_bins, labelsage_labels) group_stats ( df.groupby(age_group, observedTrue)[Q3_score] .agg([mean, std, count]) .round(2) ) print(group_stats)pd.cut()把连续年龄分成离散区间observedTrue是 pandas 2.x 里 groupby 分类列时的推荐写法避免警告信息。agg([mean, std, count])一次算均值、标准差、样本量。这里有个统计常识当组的样本量小于 30 时均值稳定性较差报告中要谨慎解读当标准差大于 1.5五级量表时说明组内分歧大均值代表性不足建议进一步看频次分布而不是只报告均值。4.2 交叉表用 crosstab 同时看两个维度交叉分析是问卷报告里最出效果的部分比如“不同年龄段的满意度分布”“不同职业的用户对功能 A 的选择率”。pd.crosstab()是专门干这个的能同时输出频次和占比。需要注意的是默认输出的是频次表通常还需要通过normalizeindex转换成行百分比这样每一行加起来是 100%方便横向比较。cross pd.crosstab(df[age_group], df[Q3_score]) cross_pct pd.crosstab(df[age_group], df[Q3_score], normalizeindex).mul(100).round(1) print(频次交叉表:) print(cross) print(\n行百分比交叉表:) print(cross_pct)normalizeindex表示按行标准化mul(100)转成百分数。看交叉表时有两个视角横向看某一组的偏好结构纵向看某个选项在不同组间的差异。这个差异是否显著则需要卡方检验来支撑后面第 6 章会讲具体做法。交叉表的“坑”在于边际分布如果某个年龄组样本量很小比如只有 5 个人百分比就是 0 或 100 的极端值画图后会误导阅读者所以最好在频次表里标注样本量。4.3 将统计结果批量导出到 Excel分析完成后需要把结果整理成报告素材。用 pandas 的ExcelWriter可以把多个统计结果写入同一个 Excel 文件的不同 sheet方便后续在 Word 或 PPT 里引用。一个小技巧是设置float_format保证小数位数统一并给每个 sheet 起有意义的名字。with pd.ExcelWriter(survey_summary.xlsx, engineopenpyxl) as writer: freq_table.to_excel(writer, sheet_name单题频次, indexTrue) cross_pct.to_excel(writer, sheet_name年龄交叉分析, indexTrue) group_stats.to_excel(writer, sheet_name分组均值, indexTrue)to_excel的indexTrue是因为交叉表的行名比如年龄组本身就是有意义的信息不能丢。这里有个隐藏问题Excel 单元格的格式比如列宽pandas 默认不会优化导出后列宽很窄中文字段显示不全。解决方法是导出后用 openpyxl 调整列宽或者直接在 PowerShell 里把 Excel 打开后手动调整一次。如果追求自动化可以在写完数据后遍历列设置宽度但这对需求不高的报告来说不是必需步骤。5. 问卷统计必踩的 6 个坑现象、原因与解决办法5.1 百分比加起来不等于 100%现象某个单选题的频次统计里各选项百分比总和是 99.8% 或 100.2%不是精确的 100%。原因四舍五入。每个选项的百分比单独保留一位小数舍入误差会累积。解决要么在报告中保留两位小数要么用round()后人工微调也可以写成“由于四舍五入百分比之和可能不等于 100%”。我的做法是在报告中直接写“n500有效百分比基于有效作答样本”再把表格里的百分比保留一位小数并不强制修正到 100%。读者能理解四舍五入误差。5.2 多选题选项选中率之和远超 100%现象多选题统计结果里各选项选中率加起来可能到 200% 甚至更高。原因多选题本来就可以选多个选项每个选项的选中率分母是有效作答人数所以各项占比相加没有物理意义不应强求等于 100%。解决报告中明确写“本题为多选题各项占比之和可能大于 100%”。这不是错误而是多选题统计的常态。5.3 反向计分题没处理高分反而代表差评现象量表里经常有一两道反向表述的题比如“该产品操作复杂”选“非常同意”应该是低分。如果直接计入总分均值会被拉高。原因没有做反向编码。解决把反向题的分数反转例如 5 分量表用6 - raw_score。操作上先确认哪些列是反向题然后单独处理最终和正向题统一方向后计算总分或均值。df[Q6_reversed] 6 - df[Q6_score]5.4 无效样本没有筛除有人 30 秒就提交了现象统计结果显示满意度极高但回收问卷数量巨大仔细一查里面有一半的问卷是同一台设备在短时间内提交的。原因问卷平台没有设置最低作答时间或者没有开启重复提交检测。解决利用问卷平台导出的“开始时间”“结束时间”列计算作答时长把低于合理阈值的样本删除。合理的阈值取决于问卷长度一般 10 题以上的问卷至少需要 60 秒低于 30 秒的样本基本可以直接剔除。如果时间列不存在可以用“所有题目答案完全一致”的规则来筛。5.5 多重共线性两个问题问的是同一件事现象交叉分析、回归分析时发现两个变量高度相关比如“满意度”和“愿意推荐程度”相关系数接近 0.9放进同一个回归模型导致系数不稳定。原因题目设计时两个概念没有区分开措辞高度相似。解决如果两个题的相关系数超过 0.8报告里只选择其中一个作为核心指标即可。用df[[Q3_score,Q7_score]].corr()可以快速检验。5.6 开放题归类不稳定同一个答案被分到不同类别现象同一条开放题文本在人工归类时第一次归到“功能问题”第二次归到“体验问题”自己都拿不准。原因分类标准没有事先定义清楚归类时凭直觉。解决先明确互斥的类别定义把分类规则写下来再逐条归类。比如“功能问题”指“某项功能不能正常工作”“体验问题”指“功能正常但使用不顺畅”这样大部分文本就能落位。6. 把“统计”变成“解读”交叉分析与假设检验的最小路径统计结果要支撑结论不能只停留在“频率分布”层面。你还需要回答这个差异是真实存在的还是抽样误差导致的此时需要引入假设检验。但问卷数据的检验代码其实不复杂常见的有两类卡方独立性检验用于分类变量和独立样本 t 检验用于两组均值比较。卡方检验做的是“两个分类变量是否独立”典型场景是“年龄组和满意度等级是否相关”。实现上直接用scipy.stats.chi2_contingencyfrom scipy.stats import chi2_contingency crosstab_result pd.crosstab(df[age_group], df[Q3_score]) chi2, p_value, dof, expected chi2_contingency(crosstab_result) print(f卡方值: {chi2:.3f}, p值: {p_value:.3f}) if p_value 0.05: print(差异显著年龄组和满意度存在关联) else: print(差异不显著年龄组和满意度可能是独立的)chi2_contingency返回的四个值分别是卡方统计量、p 值、自由度和期望频数表。如果某个单元格的期望频数小于 5卡方检验的结果不可靠需要检查expected表。问卷数据里常见的状况是某个年龄组样本太少此时应把相邻组合并或者改用 Fisher 精确检验。如果 p 值小于 0.05只能说明“有关联”不能说明“有因果关系”报告措辞要克制。独立样本 t 检验收缩到 7 行代码以内吗可以的。比如比较“男性和女性对满意度的均值是否有显著差异”from scipy.stats import ttest_ind male_scores df.loc[df[gender] 男, Q3_score] female_scores df.loc[df[gender] 女, Q3_score] t_stat, p_value ttest_ind(male_scores.dropna(), female_scores.dropna()) print(ft值: {t_stat:.3f}, p值: {p_value:.3f})ttest_ind默认假设两组方差相等。如果两组样本量差异大或方差异明显设置equal_varFalse运行 Welchs t 检验更稳妥。这里的经验是用了这个功能你手里的报告就从“数据一览”升级成“分析结论”。不过要提醒一句问卷数据大多不是随机抽样而是方便样本比如朋友圈转发收集来的显著性检验只能说“在这个样本里差异不大可能是偶然”不能直接推广到整个总体。报告里最好加一句“本研究样本来源为便利抽样结论外推需谨慎”。还有一个经常被忽略的习惯在交付统计结果时把两套东西一起给出去——一份是人类可读的 Excel/PDF 报告一份是完整的 Python 脚本和数据清洗日志。前者给人看后者给未来的自己看。问卷统计最怕的不是没有结论而是三个月后有人问“这个数怎么算出来的”你发现自己根本记不清当时是删了哪些行、填了哪些缺失值。所以从第一天开始就要把数据清洗 统计分析的每一步都写在脚本里而不是在交互式界面里东点一下西点一下。我自己的习惯是每个脚本开头用注释写明“数据源文件、清洗规则、生成日期”末尾输出一份清洗前后的样本量对照。希望这几个从数据到结论的路径能帮你真正把问卷数据用起来。本文还有配套的精品资源点击获取