
简介中国轻食行业消费者行为调查数据以文档形式呈现适合餐饮品牌市场人员、行业分析师以及健康食品方向的学生用于快速了解轻食消费市场。内容基于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:p和w: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, maxsplit1) if len(parts) 2: parsed_rows.append({ 题目标识: kw, 原始文本: text, 解析结果: parts[1].strip() }) break这个兜底解析的关键参数是maxsplit1。它保证答案内容里即使还有冒号比如价格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(indexseries.index) # 按长度降序优先匹配长选项避免子串误判 for opt in sorted(options, keylen, reverseTrue): 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], axis1)拆列后每个选项变成0/1变量可以做频次统计、做多选题响应占比也可以和前面编码好的周频次做分组均值比较。排序匹配是这段代码最值得说的细节健康营养和营养均衡如果同时出现在选项里按长度降序后健康营养优先被检查不容易被营养这种短词抢先命中。实际操作中还有一类问题是选项里带括号备注比如方便快捷外卖/自带匹配时如果直接拿完整字符串去in则大概率失败更好的方式是把括号内容单独处理成同义词表或者先对回答做括号剔除再做映射。多选题拆完之后每个受访者就同时拥有了用户属性、消费行为和态度倾向三组变量可以开始做交叉分析了。4. 从频次表到交叉分析轻食消费者画像怎么拆4.1 城市线级与消费频次的交叉透视轻食行业有明显的城市分层效应一线城市白领工作餐场景和高频轻食消费高度绑定新一线城市则是周末轻食居多下沉市场更多是尝鲜式消费。要看这种结构第一张表就应该是城市线级和消费频次档位的交叉透视表。用pivot_table做交叉表是标准做法不需要自己手写循环。pivot pd.pivot_table( df, index城市线级, columns频次档位, values用户ID, aggfunccount, fill_value0, marginsTrue, margins_name总计 ) print(pivot)values用户ID配合aggfunccount是为了稳定计数如果你选一个全为空值的列pivot会报错用户ID这种主键列是最安全的选择。marginsTrue会在行列末尾追加总计前三次跑数据透视时建议全部保留方便快速核对行总计和原始样本量是否一致。透视表跑完后要做一次加法校验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(fSpearman相关系数: {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, annotTrue, fmtd, cmapYlOrRd, linewidths0.5, axax) ax.set_title(中国轻食行业_城市与消费频次交叉分布) plt.tight_layout() plt.savefig(轻食消费者调查_交叉热力图.png, dpi300)annotTrue会在每个格子中央打印数值fmtd表示整数如果透视表里放的是占比fmt.1%更合适。cmapYlOrRd在黑白打印下仍有灰度区分度这个细节容易被忽略。热力图只能展示二维关系要看健康关注度和复购意愿的分组差异叠加一张箱线图或蜂群图效果更好sns.boxplot(datadf.dropna(subset[健康关注度]), x健康关注度, y复购意愿, paletteSet2) plt.title(健康关注度分组的复购意愿分布) plt.show()箱线图能同时呈现中位数、四分位距和离群点对于一百多份问卷的小样本尤其有用——可以直接看到某一组里是否存在极端低分用户把均值拉低。轻食消费者样本量普遍不大箱线图暴露离群点的能力比柱状图强很多分析时值得优先使用。5.2 导出结构化调查结果表清洗好的明细数据、透视结果和统计量需要分别落盘方便后续给BI团队或报告撰写方复用。导出的关键是把第3章生成的编码字段和原始文本同时保留因为业务同事可能还要回看具体原话。df.to_excel(轻食消费者行为_清洗后数据.xlsx, indexFalse) pivot.to_excel(轻食消费者_交叉透视表.xlsx)没有安装openpyxl的话先执行pip install openpyxl再跑导出。如果原始docx中开放题里带超链接或图片excel导出时这些对象不会被保留需要在原始文档里做一次备份。整个流程到这里已经形成一条从docx抽取、清洗编码、交叉分析到可视化导出的完整可复用链路下一次来新一期的轻食调查数据换文件名跑一遍脚本就能直接得到同样结构化的结果文件。本文还有配套的精品资源点击获取