news 2026/9/18 19:12:35

从docx到可视化:用Python解析轻食消费者调查数据全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从docx到可视化:用Python解析轻食消费者调查数据全流程

简介:中国轻食行业消费者行为调查数据以文档形式呈现,适合餐饮品牌市场人员、行业分析师以及健康食品方向的学生,用于快速了解轻食消费市场。内容基于2023年艾媒咨询调查,完整记录了消费者食用轻食频率、喜欢的轻食类型、运动习惯、向他人推荐意愿以及认为目前轻食餐存在的问题等维度,包括75.8%消费者每周至少吃一次轻食、53.6%偏好沙拉蔬菜卷类、84.0%愿意推荐轻食等多项关键百分比,同时指出食品质量参差不齐、产品品类单一、定价过高、饱腹感不强、味道不佳等主要痛点,并延伸分析了高端化、品牌化、多元化的发展前景,可直接引用作为行业分析或报告素材。资源包内共1个文件,为docx格式,大小约331KB,便于携带和查阅。目前已有155人学习,对把握轻食消费人群画像、消费习惯与行业未来趋势具有实用参考价值。

1. 轻食行业消费者行为调查数据到底该怎么读

一份标题写着"中国轻食行业消费者行为调查数据.docx"的文件,落到不同人手里会走完全不同的路:有人当附件直接转发给下游,有人复制粘贴进Excel再手工整理三小时。做过消费品牌数据分析的人心里都清楚,docx格式的问卷调查报告恰恰是最容易压垮人的那类数据源——它经常同时包含普通选择统计表、量表题、多选拆分项和写在正文里的开放回答,这些结构在Word里看着整齐,一旦脱离原始文档就七零八落。这里不打算讲泛泛的"数据分析方法论",而是从解析docx这个容器开始,把轻食行业消费者行为数据从抽取、清洗、编码到交叉分析和可视化出图的全过程走一遍。适合手里正好有一份这类问卷文档、又不想每次靠手工救火的分析师或数据工程师。

2. 用 python-docx 把调查数据从 docx 里完整抽取出来

2.1 先看文档结构:调查数据为什么藏在多个容器里

处理过.docx的人都知道它本质上是一个ZIP压缩包,内部是若干XML文件描述段落、表格、样式和属性。python-docx这个库把这些XML包装成Document、Paragraph和Table这些高层对象,但它默认的读取方式有一个坑:doc.paragraphs只返回正文中的段落,doc.tables只返回文档层的表格,两者的顺序信息会丢失。轻食行业调查报告偏偏很容易出现"问题和结果混排"的情况,比如前半部分是段落式问卷说明,中间插入三张频次统计表,最后又是几十行开放填空题答案。一旦段落和表格的相对顺序断裂,后续要把回答和对应的题目标签对齐就非常麻烦。

另外还有一个容易踩的盲区:Word里的文本框、批注和表格嵌套。python-docx默认不解析文本框内容,批注里的修订记录也不会出现在paragraphs里。如果调查数据里出现过"备注:"或者"其他意见"这类信息被录入者塞进文本框的情况,直接用paragraphs会漏数据。稳妥的做法是先对文档做一次整体结构扫描,确认每个块是什么类型、大致有多少行,再决定后续用哪种方式抽取。

2.2 抽取表格型问卷数据的最小可跑脚本

我处理这类文档的惯例做法是:遍历document.element.body下的所有子元素,逐个判断是段落还是表格,再分别用Paragraph和Table包装。python-docx本身没有提供按顺序同时遍历段落和表格的公开接口,但通过底层XML节点的标签名可以精确区分w:pw:tbl

from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph doc = Document("中国轻食行业消费者行为调查数据.docx") def iter_block_items(parent): """按文档物理顺序依次返回段落和表格对象""" from docx.oxml.ns import qn body = parent.element.body for child in body.iterchildren(): if child.tag == qn('w:p'): yield Paragraph(child, parent) elif child.tag == qn('w:tbl'): yield Table(child, parent) records = [] current_section = "" for block in iter_block_items(doc): if isinstance(block, Paragraph): text = block.text.strip() # 短文本且不以数字编号开头,视为问卷分节标题 if text and len(text) < 30 and not text.startswith(("1.", "2.")): current_section = text elif isinstance(block, Table): for row in block.rows: cells = [cell.text.strip().replace("\n", " ") for cell in row.cells] records.append({ "分节": current_section, "表格行": cells })

这段代码的核心是iter_block_items这个生成器。qn('w:p')qn('w:tbl')是python-docx内部的XML命名空间常量,用来判断body子元素的类型;body.iterchildren()按文档顺序逐一遍历,保证输出的块不会乱序。段落判断里的startswith(("1.", "2."))是用来排除问卷编号本身,如果文档里题干也是短文本,这个条件要根据实际文档调整。表格行抽取时我把单元格里的换行符替换成空格,防止后续写CSV时字段被拆裂。

容器类型python-docx 对象位置常见调查内容
正文段落Paragraphbody 直接子元素问卷说明、分节标题、开放题答案
固定表格Tablebody 直接子元素选择题频次统计、交叉表
嵌套表格Table单元格内部极少见,出现时需递归遍历
文本框无默认接口画布或浮动层备注、补充说明

如果文档里存在合并单元格,row.cells对同一行重复单元格会返回相同文本,实际抽取后要做一次相邻去重。具体做法是记录上一个单元格的坐标,如果当前单元格的行号和列号与上一个重合就跳过,或者直接用table._tbl解析更精确的坐标范围。合并单元格在问卷统计表里很常见,尤其是"总体样本"这一行经常跨列合并,不去重的话后续统计行数会虚高。

2.3 问卷数据混在正文段落里时的兜底解析

轻食问卷的开放题答案经常不在表格里,而是以"Q7:您选择轻食时最看重什么?回答:方便快捷"这种形式写在段落中。这时候不能只靠表格解析,需要追加一种按关键词匹配正文段落的策略。实际处理时我一般维护一个题目关键词列表,逐段查找冒号或制表符分隔的答案。

import re question_keywords = ["频次", "价格", "因素", "复购", "性别", "城市", "收入"] parsed_rows = [] for block in iter_block_items(doc): if isinstance(block, Paragraph): text = block.text.strip() for kw in question_keywords: if kw in text and (":" in text or ":" in text or "\t" in text): parts = re.split(r":|:|\t", text, maxsplit=1) if len(parts) == 2: parsed_rows.append({ "题目标识": kw, "原始文本": text, "解析结果": parts[1].strip() }) break

这个兜底解析的关键参数是maxsplit=1。它保证答案内容里即使还有冒号(比如"价格:30元:这个价位还算合理"),分隔也只发生在第一个冒号处,答案字段保留完整信息。关键词匹配本身比较粗糙,但轻食行业问卷里的常见词就那几个,覆盖八成场景没问题。遇到全角冒号和半角冒号混杂时,正则里的:|:已经同时兼容;如果是空格分隔的键值对,把正则改成\s{2,}更合适。解析结果如果出现乱码,先检查docx是否来自WPS或Mac版Office,旧版本导出时Unicode规范化方式不同,必要时先做一次unicodedata.normalize("NFKC", text)

至此,原始docx里的表格和段落数据已经被抽成结构化的dict列表,接下来才能进入真正让数据变干净、可计算的清洗编码环节。

3. 轻食消费者行为的核心字段清洗与编码

3.1 频次和价格区间必须重新编码的原因

从docx直接抽出的数据,大概率长这样:"每周2-3次"、"偶尔吃"、"每月1-2次"、"30元左右"。这些字符串人眼能看懂,但pandas算不了——字符串不能直接求均值、做相关系数或聚类。轻食消费者行为分析里,消费频次是最核心的因变量,它决定用户可以被划分为轻度尝试者、习惯型消费和重度依赖人群;价格接受度则直接关联产品定价和促销策略。这两列不做结构化编码,后面所有分析都无从谈起。

另一个需要重新编码的原因是轻食问卷版本的差异。有的品牌会做两期追踪调研,第一期频次选项是"每周1次"、"每周2-3次",到了第二期变成"每周1-2次"、"每周3-5次",如果不统一映射到一个连续的周频次数值轴上,两期数据无法对比,后续的时间趋势分析直接失效。

3.2 用 pandas 把原始表格转成结构化编码表

拿到表格数据后,先转成DataFrame,再根据实际字段做映射。下面是轻食问卷最常见的两套映射:消费频次和价格带。频次映射成"每周次数"是为了让变量具备连续属性,价格带映射成1到5的等距整数则是为了在报告里直接计算平均价格带指数。

import pandas as pd import numpy as np df = pd.DataFrame(records) # records来自第2章的抽取结果 freq_map = { "每天": 7, "每周4次以上": 5, "每周2-3次": 2.5, "每周1次": 1, "每月2-3次": 0.6, "每月1次": 0.25, "偶尔吃": 0.1, "几乎不吃": 0 } price_map = { "15元以下": 1, "15-25元": 2, "25-35元": 3, "35-50元": 4, "50元以上": 5 } def encode_freq(value): if pd.isna(value): return np.nan text = str(value).strip() for k in freq_map.keys(): if k in text: return freq_map[k] return np.nan def encode_price(value): if pd.isna(value): return np.nan text = str(value).strip() for k in price_map.keys(): if k in text: return price_map[k] return np.nan df["周频次_编码"] = df["回答"].apply(encode_freq) df["价格带_编码"] = df["回答"].apply(encode_price)

substring in text这种匹配方式对"每周2-3次"和"每周1次"这两个值会出问题——因为"每周1次"是"每周2-3次"的子串吗?不是,字符串"周2-3次"里并不包含连在一起的"每周1次",但反过来如果问卷选项是"每周1-2次",它同时包含"每周1"开头,不包含完整的"每周1次","每周1次" in "每周1-2次"结果是False,所以这个映射表暂时安全。真正要警惕的是"每月1次"和"每月1-2次":"每月1次" in "每月1-2次"同样是False,因为中间有短横线;但"偶尔吃"和"偶尔(每月1次)"这种组合就可能误命中。稳妥做法是把freq_map的键先按长度降序排序再匹配,或者用精确的==判断加一个模糊归一化步骤。

原始回答编码值编码类型业务含义
每天7周频次重度高频消费者
每周2-3次2.5周频次习惯型消费者
每月1次0.25周频次低频尝试者
偶尔吃0.1周频次极低意愿用户
15元以下1价格带价格敏感群体
25-35元3价格带主流消费带
50元以上5价格带高端健康餐人群

缺失值处理上有一个轻食行业特有的坑:问卷里"未作答"和空字符串不应该一概而论。如果你看到某条记录频次为空但价格带已填,这可能是对方跳题了;如果整行都空,那基本是无效问卷。区分办法是增加一列is_valid = df["回答"].notna() & (df["回答"] != ""),把全空样本先摘出去,不要直接dropna把有效部分也删掉。对编码后仍为NaN的频次,不要简单填中位数,后续分析可以按空值单独分一组,对比有效样本和缺失样本的分布差异。

3.3 多选题的一题变多列技巧

"您选择轻食时最关注哪些因素"是轻食问卷里的必考题,选项一般包含健康营养、减脂瘦身、方便快捷、口感口味、价格实惠、社交分享。这种多选题在docx表格里通常存在同一单元格,用顿号分隔。分析时必须拆成多个布尔列,后续才能和频次、城市、价格带做交叉分析。

option_cols = ["健康营养", "减脂瘦身", "方便快捷", "口感口味", "价格实惠", "社交分享"] def split_multi_choice(series, options): result = pd.DataFrame(index=series.index) # 按长度降序,优先匹配长选项,避免子串误判 for opt in sorted(options, key=len, reverse=True): result[opt] = series.apply( lambda x: 1 if isinstance(x, str) and opt in x else 0 ) return result multi_df = split_multi_choice(df["回答"], option_cols) df = pd.concat([df, multi_df], axis=1)

拆列后每个选项变成0/1变量,可以做频次统计、做多选题响应占比,也可以和前面编码好的周频次做分组均值比较。排序匹配是这段代码最值得说的细节:"健康营养"和"营养均衡"如果同时出现在选项里,按长度降序后"健康营养"优先被检查,不容易被"营养"这种短词抢先命中。实际操作中还有一类问题是选项里带括号备注,比如"方便快捷(外卖/自带)",匹配时如果直接拿完整字符串去in,则大概率失败;更好的方式是把括号内容单独处理成同义词表,或者先对回答做括号剔除再做映射。

多选题拆完之后,每个受访者就同时拥有了用户属性、消费行为和态度倾向三组变量,可以开始做交叉分析了。

4. 从频次表到交叉分析:轻食消费者画像怎么拆

4.1 城市线级与消费频次的交叉透视

轻食行业有明显的城市分层效应:一线城市白领工作餐场景和高频轻食消费高度绑定,新一线城市则是"周末轻食"居多,下沉市场更多是尝鲜式消费。要看这种结构,第一张表就应该是城市线级和消费频次档位的交叉透视表。用pivot_table做交叉表是标准做法,不需要自己手写循环。

pivot = pd.pivot_table( df, index="城市线级", columns="频次档位", values="用户ID", aggfunc="count", fill_value=0, margins=True, margins_name="总计" ) print(pivot)

values="用户ID"配合aggfunc="count"是为了稳定计数,如果你选一个全为空值的列,pivot会报错,用户ID这种主键列是最安全的选择。margins=True会在行列末尾追加总计,前三次跑数据透视时建议全部保留,方便快速核对行总计和原始样本量是否一致。透视表跑完后要做一次加法校验:pivot.loc["总计", :].sum()应该等于总样本数,如果对不上就回头查docx抽取环节是否有重复行或丢行。频次档位我一般是把周频次编码切成三档:0到0.5为低频、0.5到2.5为中频、2.5以上为高频,分档逻辑放在pivot之前用pd.cut完成,这样透视表不会因为原始值太分散而变得稀疏。

城市线级低频中频高频总计
一线8261852
新一线1222842
二线及以下219333
总计415729127

上面这张表是我用模拟数据跑出来的示意,从行占比就能读出一个结论:一线城市高频占比为18/52约35%,二线及以下则只有3/33约9%。这就是交叉透视表的价值——不靠描述性统计的均值,直接看结构差异。

4.2 健康关注度与复购意愿的相关性计算

轻食消费者和普通餐饮消费者最大的差别在"健康关注度"这个变量。问卷里通常表现为量表题:1分是完全不关注,5分是极其关注。复购意愿也是同度量表。两个有序变量之间的相关分析不适合用Pearson,因为Pearson默认假设是线性关系且数据近似正态;而量表的步进是离散的,更适合用Spearman秩相关。

from scipy.stats import spearmanr valid = df[["健康关注度", "复购意愿"]].dropna() rho, p_value = spearmanr(valid["健康关注度"], valid["复购意愿"]) print(f"Spearman相关系数: {rho:.3f}, p值: {p_value:.4f}") # 按健康关注度分组,看复购意愿的中位数阶梯 grouped = df.groupby("健康关注度")["复购意愿"].median() print(grouped)

输出里的rho取值范围在-1到1之间,正数代表正相关,接近0则无明显线性关系。p_value是显著性检验的p值,样本量少于100时尤其要盯住它,如果p值大于0.05即使rho看起来有0.4也不能下结论。轻食样本通常只有一百多份,误读p值的概率不低,稳妥做法是把两期数据合并后再算一次,或用bootstrap抽样看rho的置信区间。

关于相关强度如何解读,行业里一般参考以下区间:

rho 绝对值范围强度在轻食调查里的参考含义
0.0 - 0.2极弱两变量几乎独立
0.2 - 0.4趋势存在但被其他因素干扰
0.4 - 0.6中等可以支撑初步业务判断
0.6 以上需要重点分析其因果链

分组中位数表和相关系数配合看更有业务洞察。比如我做过的一个案例里,健康关注度从3分升到4分的用户,复购意愿中位数直接从3跳到了4.5,而4分到5分之间却几乎没有变化。这说明市场投入的重点应该放在"把用户的健康意识从普通提升到关注"这一层,而不是去争抢那批已经极其关注健康的人群。相关分析做完后,剩下的工作就是把结论可视化,让业务同事拿过去就能用。

5. 用 seaborn 把轻食消费者结论画成可直接放报告的热力图

5.1 交叉热力图与分组对比图

透视表算完之后,用seaborn.heatmap直接渲染成交叉热力图,颜色深浅代表消费密度,是调查报告里最直观的呈现方式。画图前先处理中文字体,否则标题和图例在大多数Linux服务器或macOS上会渲染成方块。

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"] plt.rcParams["axes.unicode_minus"] = False # 透视表去掉总计行列后再画,否则颜色标尺会被总计值压扁 plot_data = pivot.drop(index="总计", columns="总计") fig, ax = plt.subplots(figsize=(8, 6)) sns.heatmap(plot_data, annot=True, fmt="d", cmap="YlOrRd", linewidths=0.5, ax=ax) ax.set_title("中国轻食行业_城市与消费频次交叉分布") plt.tight_layout() plt.savefig("轻食消费者调查_交叉热力图.png", dpi=300)

annot=True会在每个格子中央打印数值,fmt="d"表示整数;如果透视表里放的是占比,fmt=".1%"更合适。cmap="YlOrRd"在黑白打印下仍有灰度区分度,这个细节容易被忽略。热力图只能展示二维关系,要看健康关注度和复购意愿的分组差异,叠加一张箱线图或蜂群图效果更好:

sns.boxplot(data=df.dropna(subset=["健康关注度"]), x="健康关注度", y="复购意愿", palette="Set2") plt.title("健康关注度分组的复购意愿分布") plt.show()

箱线图能同时呈现中位数、四分位距和离群点,对于一百多份问卷的小样本尤其有用——可以直接看到某一组里是否存在极端低分用户把均值拉低。轻食消费者样本量普遍不大,箱线图暴露离群点的能力比柱状图强很多,分析时值得优先使用。

5.2 导出结构化调查结果表

清洗好的明细数据、透视结果和统计量需要分别落盘,方便后续给BI团队或报告撰写方复用。导出的关键是把第3章生成的编码字段和原始文本同时保留,因为业务同事可能还要回看具体原话。

df.to_excel("轻食消费者行为_清洗后数据.xlsx", index=False) pivot.to_excel("轻食消费者_交叉透视表.xlsx")

没有安装openpyxl的话先执行pip install openpyxl再跑导出。如果原始docx中开放题里带超链接或图片,excel导出时这些对象不会被保留,需要在原始文档里做一次备份。整个流程到这里已经形成一条从docx抽取、清洗编码、交叉分析到可视化导出的完整可复用链路,下一次来新一期的轻食调查数据,换文件名跑一遍脚本就能直接得到同样结构化的结果文件。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 19:12:15

STM32CubeMX2生成代码在Keil µVision5中编译调试全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:10:30

300节点大数据平台验收:TPC-DS、性能压测与量收迁移拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:10:12

正交试验设计原理与工业实战:从数学构造到注塑工艺优化

简介&#xff1a;本资源是一份面向高校统计学、工业工程及实验设计相关专业师生的《正交试验设计原理及实例》教学课件&#xff0c;系统讲解多因素优化试验的核心方法论&#xff0c;解决传统全面试验成本高、周期长、实施难等实际问题。课件以PPT格式呈现&#xff0c;共1个文件…

作者头像 李华
网站建设 2026/9/18 19:09:31

PyCharm 安装与更新全流程:解释器、虚拟环境与依赖管理避坑指南

Python IDE 这个说法听起来挺正式&#xff0c;实际落到日常开发里&#xff0c;它就是我们每天开门第一件事要面对的东西。我装 PyCharm 的经历算不上顺利&#xff0c;最早那台 4G 内存的笔记本&#xff0c;装完打开一个稍大的项目&#xff0c;索引转了十几分钟&#xff0c;风扇…

作者头像 李华
网站建设 2026/9/18 19:07:15

C语言结构体尾部填充与__attribute__((packed))实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华