ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

辅导班市场调研实践:数据清洗与归一化实战方案

辅导班市场调研实践:数据清洗与归一化实战方案 最近因为家里孩子要选辅导班我开始认真研究辅导班市场。原本以为这件事很简单打开几个平台搜一搜机构名字再对比一下价格和评价很快就能得出“哪家值得报”。真正动手之后才发现我低估了这件事。同一个机构在不同页面上的名称不一样有的带“教育”有的带“培训”有的干脆是缩写同一门课程的价格有时按“节”算有时按“期”算还有的页面只展示“低至99元”却看不到总价更麻烦的是信息分散在大量网页里靠手动逐条登记既容易遗漏也很难做横向比较。于是我换了思路把这次市场调查当成一个数据处理项目来做。用脚本采集公开信息用 pandas 清洗归一化再按区域、科目、价格区间做聚合分析。这篇文章就想把这套完整的“市场数据调查”流程拆给你看包括采集、清洗、分析和验证。如果你也遇到过信息混乱、无法比较的问题或者想掌握一套可以复用的数据调研方法那这篇文章应该能帮到你。读完这篇文章你会得到一套可复制的技术方案也会明白辅导班市场看起来复杂本质上是“信息口径不统一”的问题而这类问题恰恰是结构化数据处理擅长的领域。1. 用数据去摸清辅导班市场真正的难点在哪先说结论辅导班市场的复杂度不在信息量本身而在信息口径极度不统一。如果只看表面你可能会以为“不就是在网上找机构信息吗”。但实际操作时会遇到四类问题。第一数据源分散。机构信息分布在各式各样的平台和页面上有综合生活服务平台有垂直教育平台也有机构自己的官网。不同平台展示的字段不一样有的有评分有的没有有的只有电话号码有的页面则包含完整的课程表。把这些来源的数据合并到一张表里本身就是一件脏活。第二实体识别难。同一个机构在不同平台可能叫“某某教育”、“某某培训中心”、“某某学校某某校区”。如果不做名称归一化统计结果会非常失真。比如你想统计“某区域一共有多少家机构”同一个机构被算了三次这份统计就没有意义。第三价格口径混乱。这是最让人头疼的地方。同样是一对一辅导A 机构按“每小时”报价B 机构按“每节课”报价C 机构按“一学期”报价。表面上看都是价格字段实际上完全不可比。第四评价数据主观性强。评分高不一定代表教学好可能与机构主动邀请评价有关也可能与评价基数太小有关。只看评分很容易被误导。所以真正难的不是“收集信息”而是“把信息整理成能比较、能统计的结构化数据”。这正好是数据采集、清洗、归一化技术的用武之地。对普通家长来说这个调查过程是体力活对数据分析师和开发者来说它是一个完整的数据工程问题。这篇文章后续的流程就把“调查辅导班市场”这件事转成了一个从采集到可视化的技术实践。你可以直接复用这套流程去调查任何一个需要横向比较的行业。2. 核心概念与整体流程设计在动手写代码之前先把这次调查涉及的核心概念和数据流程讲清楚。这个概念并不复杂但理解了它后面的代码才有依据。2.1 市场调研数据链路这次调查的数据链路可以拆成四个阶段采集、清洗、分析、验证。阶段一采集。从公开网页中提取机构名称、区域、科目、价格、评分等原始字段。这个阶段的关键是合法合规和限速不能给对方服务器造成压力。阶段二清洗。把采集到的原始数据标准化包括去重、填充缺失值、统一价格单位、拆分区域字段、归一化科目名称。这个阶段是最耗时间的也是数据质量的关键。阶段三分析。在清洗后的数据上进行聚合统计比如“每个区域的机构数量分布”、“不同科目的平均课单价”、“价格区间与评分的交叉分析”。阶段四验证。用抽样回查原网页的方式验证采集和清洗结果是否有误。这一步容易被忽略但非常必要因为自动化流程不能保证 100% 准确。2.2 数据字段设计采集之前先把需要的数据字段定义清楚。这个动作决定了后续分析的边界。我最终使用的是下面这套字段。字段名类型说明示例org_namestring机构名称采集后需归一化某某教育XX校区regionstring所属区域海淀区subjectstring科目需映射到统一分类英语 / 雅思 / 数学course_typestring课程类型一对一 / 小班 / 大班price_valuefloat价格数值300price_unitstring价格单位节 / 期 / 小时ratingfloat评分0-54.8review_countint评论数量126sourcestring来源平台platform_a这套字段的设计原则是原始字段尽量保留归一化的结果用新字段保存避免清洗过程中丢失原始信息。比如价格字段我会保留用户填写的原始价格和单位同时新增一个“每课时折算价格”用于横向比较。2.3 为什么不能直接使用网络原始数据很多人会把“采集到数据”等同于“调查完成”这是最常见的一个误区。原始网页数据至少有三个问题。第一重复项无法避免同一家机构的多个分校区、多个目录页都可能被采集到第二字段类型不固定价格可能带“元”、带“起”、带“约”评分可能缺失第三科目体系不统一不同机构对同一门课的命名差异很大。如果不做清洗后续的所有统计都是不可靠的。这也是为什么我说“辅导班市场比想象中复杂”——复杂不在于采集不到数据而在于采集到的数据无法直接用于判断。3. 环境准备与依赖安装这部分我们开始动手。先准备好运行环境再安装 Python 依赖。3.1 运行环境说明本文演示的代码基于 Python 3。版本建议使用 3.9 或以上。操作系统不限Windows、macOS、Linux 都可以。建议使用虚拟环境避免把依赖装到全局环境里出现版本冲突。3.2 创建项目目录和虚拟环境mkdir education_market_research cd education_market_research python -m venv venv创建完成后激活虚拟环境。Windowsvenv\Scripts\activatemacOS / Linuxsource venv/bin/activate激活后命令行提示符前会出现(venv)说明已经进入虚拟环境。3.3 安装依赖需要安装以下 Python 库库名用途requests发送 HTTP 请求采集页面beautifulsoup4解析 HTML 页面pandas数据清洗、去重、聚合分析openpyxl导出 Excel 文件pyecharts生成可视化图表 HTML安装命令如下pip install requests beautifulsoup4 pandas openpyxl pyecharts安装完成后可以执行下面的命令确认版本python -c import pandas as pd; print(pd.__version__)如果正常输出版本号说明环境准备完成。后续代码都基于这个虚拟环境运行。4. 数据采集把公开信息变成结构化表格采集阶段的任务是把网页上的公开机构信息转换成结构化的 CSV 文件。4.1 采集前的合规提醒这部分非常重要。采集公开数据时必须遵守以下边界仅采集公开可访问的页面信息不登录、不破解、不绕过反爬机制。采集前查看目标网站的 robots.txt 和用户协议确认允许的范围。控制请求频率不要对服务器造成压力。采集结果仅用于个人学习、研究等非商用用途。合法合规地采集数据是这篇文章能够成立的前提。下面的代码只是一个简化演示目标 URL 也使用占位地址请在实际使用时替换为合规的数据源。4.2 编写采集脚本新建文件collector.py代码如下。# 文件路径collector.py import csv import time import requests from bs4 import BeautifulSoup # 目标页面地址这里使用示例占位地址 # 实际使用时请替换为合规的公开页面 BASE_URL https://example.com/edu-list?page{} HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_page(page_num): 请求第 page_num 页的公开列表页返回 HTML 文本。 url BASE_URL.format(page_num) try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f请求第 {page_num} 页失败: {e}) return def parse_page(html): 解析 HTML 中的机构条目返回字段字典列表。 soup BeautifulSoup(html, html.parser) results [] # 下面的选择器仅作示例实际页面需要按真实 HTML 结构调整 for item in soup.select(.edu-item): org_name item.select_one(.org-name).get_text(stripTrue) region item.select_one(.org-region).get_text(stripTrue) subject item.select_one(.org-subject).get_text(stripTrue) price_text item.select_one(.org-price).get_text(stripTrue) rating item.select_one(.org-rating).get_text(stripTrue) review_count item.select_one(.org-review).get_text(stripTrue) results.append({ org_name: org_name, region: region, subject: subject, price_text: price_text, rating: rating, review_count: review_count, source: platform_a }) return results def save_to_csv(data, pathraw_data.csv): 把解析结果写入 CSV编码使用 UTF-8。 if not data: print(没有数据可保存) return fieldnames [org_name, region, subject, price_text, rating, review_count, source] with open(path, modew, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data) print(f已保存 {len(data)} 条数据到 {path}) def main(): all_data [] # 这里只爬取前 5 页演示实际采集建议分批执行 for page in range(1, 6): html fetch_page(page) if html: items parse_page(html) all_data.extend(items) print(f第 {page} 页解析到 {len(items)} 条数据) # 控制请求间隔避免对服务器造成压力 time.sleep(2) save_to_csv(all_data) if __name__ __main__: main()这段代码的核心逻辑是通过requests请求列表页用BeautifulSoup解析页面上的机构条目再按字段写入 CSV 文件。关键在于解析后保留原始文本比如价格字段先存成price_text不急着转数字。运行方式python collector.py如果只是想要一个能直接演示全流程的数据集也可以先手写一份raw_data.csv跳过“真实采集”这一步把重点放在清洗和分析上。清洗之后的代码并不依赖采集方式。5. 数据清洗统一口径才是复杂度的核心这一章是整个调查流程中最关键的部分也是“辅导班市场比想象中复杂”最直接的体现。5.1 清洗思路原始 CSV 中有几个典型的脏数据问题需要处理机构名称重复或名称不统一。价格字段混合了“300元/节”“1200元/期”“低至160/小时”等不同格式。科目字段混杂比如“小学数学”“奥数”“思维训练”其实指向同一类需求。区域字段可能是“北京海淀区”也可能是“海淀”需要统一。清洗的目标是生成一份新表每一行都是可以参与统计的规范数据。5.2 编写清洗脚本新建文件clean.py。# 文件路径clean.py import re import pandas as pd def normalize_org_name(name): 机构名称归一化去空格、去掉常见后缀。 if not isinstance(name, str): return name name.strip() name re.sub(r[\s\u3000], , name) # 这里只处理一种常见情况实际项目中按需扩展 name re.sub(r(教育|培训|学校)$, , name) return name def normalize_subject(subject): 科目归一化把口语化名称映射到标准科目。 if not isinstance(subject, str): return 其他 subject subject.strip() mapping { 数学: [数学, 奥数, 思维训练, 数理化], 英语: [英语, 雅思, 托福, 口语], 语文: [语文, 阅读写作, 大语文], 编程: [编程, 少儿编程, python, 信息学], 物理: [物理], 化学: [化学], } for standard, keys in mapping.items(): for key in keys: if key in subject: return standard return 其他 def parse_price(price_text): 从价格字段中解析出数值和单位。 if not isinstance(price_text, str): return None, None text price_text.replace( , ) match re.search(r(\d\.?\d*), text) if not match: return None, None value float(match.group(1)) unit 未知 if 小时 in text or 小时 in text: unit 小时 elif 节 in text or 课时 in text: unit 节 elif 期 in text or 学期 in text: unit 期 elif 月 in text: unit 月 return value, unit def standardize_region(region): 区域标准化提取区级名称。 if not isinstance(region, str): return 其他 match re.search(r([\u4e00-\u9fa5]区), region) if match: return match.group(1) return region.strip() def main(): df pd.read_csv(raw_data.csv) # 1. 删除完全重复的行 df_before len(df) df df.drop_duplicates() print(f去重前 {df_before} 行去重后 {len(df)} 行) # 2. 机构名称归一化同时保留原始名称 df[org_name_orig] df[org_name] df[org_name_norm] df[org_name].map(normalize_org_name) # 3. 区域标准化 df[region_norm] df[region].map(standardize_region) # 4. 科目归一化 df[subject_norm] df[subject].map(normalize_subject) # 5. 价格解析 price_results df[price_text].map(parse_price) df[price_value] [x[0] for x in price_results] df[price_unit] [x[1] for x in price_results] # 6. 评分和评论数转数值转换失败置为 NaN df[rating_num] pd.to_numeric(df[rating], errorscoerce) df[review_count_num] pd.to_numeric(df[review_count], errorscoerce) # 7. 保存清洗后的数据 output_cols [org_name_orig, org_name_norm, region_norm, subject_norm, price_value, price_unit, rating_num, review_count_num, source] df[output_cols].to_csv(cleaned_data.csv, indexFalse, encodingutf-8-sig) print(清洗完成已保存到 cleaned_data.csv) if __name__ __main__: main()清洗脚本的关键点是“归一化”而不是“删除”。机构名称归一化不一定能把所有实体都识别出来但至少可以去重时避免大量重复计数科目归一化需要把业务认知写进映射表后续如果要扩展其他科目直接在映射表里加内容即可。价格解析是所有清洗步骤中最容易出错的。因为原始文本的格式千变万化最好在运行后抽样检查遇到解析异常的行及时调整正则表达式。5.3 价格口径统一价格解析完成之后仍然存在“节、期、小时、未知”等多个单位。要横向比较还得把这些单位统一换算成“每课时参考价格”。这一步不能完全自动化需要结合业务知识做换算规则。比如如果知道“一对一”课程通常一次课 2 小时那么按“节”计价就可以折算成“每小时价格”按“期”计价需要知道一期的总课时数这个信息往往不在列表页需要进入详情页或人工核对。更稳妥的做法是在分析阶段不强行换算所有字段而是先用“价格区间价格单位”两个维度分组先看数据分布再决定是否换算。要避免造出一个看起来精确、实际是拍脑袋的折算价格。6. 分析与可视化从表格到洞察清洗完之后数据终于可以参与统计了。这一章用一个分析脚本完成三件事区域机构数量分布、科目分布、价格区间分析。6.1 编写分析脚本新建文件analysis.py。# 文件路径analysis.py import pandas as pd from pyecharts.charts import Bar, Pie from pyecharts import options as opts def main(): df pd.read_csv(cleaned_data.csv) # 1. 区域机构数量分布 region_dist df[region_norm].value_counts() bar_region ( Bar() .add_xaxis(region_dist.index.tolist()) .add_yaxis(机构数量, region_dist.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title各区域辅导机构数量分布), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate15)), ) ) bar_region.render(output_region_bar.html) # 2. 科目分布 subject_dist df[subject_norm].value_counts() pie_subject ( Pie() .add(, [list(item) for item in subject_dist.items()]) .set_global_opts(title_optsopts.TitleOpts(title辅导科目分布)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c})) ) pie_subject.render(output_subject_pie.html) # 3. 价格分析只分析单位不为“未知”的数据 price_df df[df[price_unit] ! 未知].copy() if price_df.empty: print(没有可用的价格数据) return # 按科目和单位分组查看价格中位数 grouped price_df.groupby([subject_norm, price_unit])[price_value].median() print(按科目、单位统计的价格中位数) print(grouped) # 输出一个价格明细表供人工核对 price_df.to_excel(price_detail.xlsx, indexFalse, encodingutf-8-sig) print(价格明细已导出到 price_detail.xlsx) if __name__ __main__: main()分析脚本的输出有两类一是 HTML 图表可以直接用浏览器打开二是price_detail.xlsx方便人工核对。这里的核心思路是自动化的图表负责发现问题人工核对负责确认问题。6.2 更深入的分析思路除了基础分布还可以做以下分析评分与评论数量的散点分析评论数很少但评分很高的机构可能需要重点核查。同一机构在不同区域的定价对比找出价格差异较大的区域进一步判断原因。课程类型一对一、小班、大班的价格对比不同课程类型单价差异比较大分类统计更合理。词频分析从机构名称、课程名称中提取关键词观察市场宣传热点。这些分析不是必须的但能把“调查市场”从“汇总数据”升级为“形成判断”。在实际操作中我会先跑基础分布再根据兴趣点做交叉分析。7. 运行结果、效果验证与问题排查代码写完之后需要按顺序运行并验证结果是否合理。7.1 运行流程建议按以下顺序执行python collector.py python clean.py python analysis.py如果collector.py因为网络问题没有采集到数据可以先准备一份手写的raw_data.csv把后续流程跑通再回来补采集逻辑。7.2 如何判断结果正确判断标准不是“代码跑完没有报错”而是“数据是否符合常识”。我建议检查三个点。第一样本量是否合理。如果某个区域只采集到 3 条记录不能认为这个区域的市场已经被充分覆盖只能作为线索。第二价格字段是否有明显异常。比如同一科目的课时价格从 30 元到 3000 元都有那么很可能是不同课程类型混在一起也可能是采集到了非正常数据需要进一步拆分维度。第三抽样回查。随机抽取清洗结果中的 10 到 20 条记录回到原始页面核对机构名称、价格、评分是否一致。如果抽样错误率超过 5%说明清洗规则需要调整。7.3 常见问题与排查思路问题现象可能原因排查方式解决方案collector.py 请求返回空内容页面结构变化或请求被拦截打印返回状态码检查页面 HTML更新选择器调整 headers检查 robots 规则parse_page 解析到 0 条数据CSS 选择器与页面不匹配在浏览器中检查页面真实结构修正select中的选择器价格字段解析为 None正则不匹配或字段为空打印原始 price_text扩展正则规则增加异常值记录drop_duplicates 去重效果差机构名称未充分归一化查看去重前后的行数提高名称归一化规则覆盖率pyecharts 图表中文乱码环境字体或输出方式问题检查浏览器渲染结果更新浏览器字体设置或导出图片运行时报 pandas 列不存在原始 CSV 字段名不一致打印 df.columns统一字段命名增加列名校验如果你遇到的是采集层面的反爬问题第一原则是“停止并检查合规性”。不要尝试绕过访问限制。公开信息不等于可以无限抓取采集节奏和数据使用边界都需要谨慎。8. 最佳实践与工程建议这一章把调查过程中沉淀下来的经验整理成可复用的建议供你在自己的项目里参考。8.1 数据合规优先对公开网页数据做采集和分析必须先确认合规。这里再强调一次不越权、不绕过、不滥用不要用采集数据做商业决策之外的敏感用途。实际项目中可以把数据源的授权范围写在调研文档里。8.2 原始数据与清洗数据分层保存不要直接修改原始采集文件。保存raw_data.csv和cleaned_data.csv两份数据清洗规则要保证可重复执行。这样当清洗逻辑优化时不需要重新采集数据只需要重跑清洗脚本。8.3 清洗规则配置化科目映射、名称后缀、价格单位等规则不要硬编码在脚本里可以抽到配置文件或 Python 字典中统一管理。这样维护成本更低而且业务调整时不需要改代码逻辑。8.4 抽样核对形成固定流程每次清洗规则变更后都做一次抽样核对。把“回查原始页面”当成清洗流程的一部分而不是事后补救。抽样核对的比例可以根据数据量动态调整数据量小就多查一些数据量大也可以随机抽查 5%。8.5 自动化与人工判断结合数据流程负责提高效率人工判断负责处理异常。特别是价格折算、机构主体识别这类需要业务知识的环节自动化无法保证完全准确。比较可靠的方式是自动化生成候选结果人工抽检并修正把修正后的规则再回填到脚本里。8.6 数据增量更新如果市场调研需要持续跟踪可以给数据表增加collected_at时间戳字段增量采集时只更新新增和变化的数据。避免每次全量重爬既减少对目标站点的压力也让趋势分析成为可能。9. 总结回到最开始的问题辅导班市场为什么比想象中复杂根本原因不是机构太多也不是信息太少而是信息分散、格式混乱、口径不一。如果你只靠人眼去看很难形成全局判断但如果你愿意把它当成一个数据问题用采集、清洗、分析的技术手段去处理复杂度就会下降很多。这篇文章提供了一套从采集到可视化再到验证的完整流程你可以直接套用到其他需要横向比较的调研场景里。真正想做好这件事精力要多花在“清洗规则”和“抽样核对”上而不是一味增加采集量。只要数据口径统一了市场的样子会清晰很多。
返回列表