news 2026/9/18 4:05:57

竞争分析与对策的数据工程实践:从指标建模到信号验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
竞争分析与对策的数据工程实践:从指标建模到信号验证

简介:这是长江商学院陶志刚教授的市场竞争与对策课件讲义,聚焦博弈论在管理经济学与战略决策中的应用,适合MBA、商科学生及企业管理者理解竞争动态、均衡与定价策略。压缩包为1个pptx文件,共676KB,以图文和案例页形式呈现,便于直接阅读与课堂演示。内容从捕虾游戏引入市场供需与产量决策,逐步讲解纳什均衡的优化性和稳定性,并以百事可乐与可口可乐的“竞争两难”说明价格战成因与共谋、差异化破局思路;还结合新旧世界葡萄酒商争夺900亿美元全球市场的案例,对比传统艺术化与科学创新两种生产营销体系。已有85人浏览学习。这套讲义既给出博弈论基础框架,又提供可迁移到现实竞争环境中的思考方式,适合作为战略课程补充材料或自学参考。

1. 把“讲义三:市场竞争和对策”的事,当作数据工程问题来做

一份“市场竞争和对策”的讲义,放到技术团队面前,通常不会被当成 PPT 看。大家更关心的是:这份材料里讲的竞争格局、份额变化、价格手段,能不能在我的数据环境里重新算出来。实际干过竞品分析的人都有同感,真正难的不是理解“竞争激烈”这个结论,而是把“激烈”拆成可观测、可预警、可回测的指标。

这篇文章不假设你已经拿到了长江商学院陶志刚老师那版讲义,只假设你手里有一个需要持续监控的市场,以及一台能跑 Python 和 SQL 的工作机。目标是把市场竞争和对策这件事,拆成一套完整的数据链路:先做指标建模,再做采集入库,接着计算份额、弹性、竞争强度,最后验证信号有没有被误报带偏。

适合的读者包括:做增长和策略的数据分析师、负责自家产品定价的产品经理、以及想给业务方搭一套竞争情报系统的后端工程师。读完你能直接搭出一个最小可用的竞争情报管道,而不是只记住几个概念。

2. 从“市场竞争”到数据模型:先定指标,再谈采集

2.1 把“五力”翻译成可观测字段

市场上主流的竞争分析框架,不管叫五力模型还是 SCP 范式,落到数据工程里都逃不开一个事实:理论只给维度,不给字段。你得自己决定,用哪些数据代表“供应商议价能力”,用哪些数据代表“替代品威胁”。

我一般会把五力模型映射成四张可计算的表:价格快照表、SKU 覆盖表、流量渠道表、渠道事件表。价格快照表记录每个竞品在不同时点的售价和促销折扣;SKU 覆盖表记录在售商品数量和缺货数;流量渠道表记录搜索热度、榜单排名、应用商店评论量;渠道事件表记录新品发布、价格调整、补贴活动这类离散事件。

这种映射的好处是,每个理论概念都有至少一个代理变量。比如“替代品威胁”可以用相近品类的新品上架速度和搜索热度来代理;“新进入者威胁”可以用一段时间内新增竞品的数量来代理。代理变量不一定完全准确,但它让分析从“感觉竞争变大了”变成了“上月替代品流量周增速超过 20%”。

2.2 设计竞争快照底表:时间、范围、字段都不可含糊

竞争分析最忌讳的是只存当前状态。你如果不在每个采集周期里把快照存下来,三个月后就无法回答“份额是怎么迁移到这个位置的”这个问题。所以表结构里一定有一个时间字段,而且必须是带时区的 TIMESTAMPTZ。

CREATE TABLE IF NOT EXISTS competitor_snapshot ( competitor_id VARCHAR, category VARCHAR, captured_at TIMESTAMPTZ, sku_count INTEGER, avg_price DECIMAL(12,2), promo_discount DECIMAL(5,4), stockout_sku INTEGER, traffic_rank INTEGER, PRIMARY KEY (competitor_id, category, captured_at) );

这段建表语句的核心,是把主键设成“竞品 ID + 品类 + 采集时间”三个字段的组合。这意味着同一次采集中,一个竞品在一个品类下只保留一条记录。下次再采数据,就写入一条新的时间戳记录,而不是覆盖旧数据。promo_discount用 DECIMAL(5,4) 存 0.15 这样的折扣率,避免用浮点类型产生精度问题。traffic_rank是排名快照,记录当天该竞品在目标类目下的搜索或销量排名,这个字段对后面计算份额迁移很有用。

如果还要接收事件型数据,比如某竞品突然降价、某新品上线,可以单独建一张事件表。事件表不要和快照表混在一起,因为快照是周期性写入的,事件是异步到达的,混在一起会让聚合查询变得非常别扭。

2.3 用“原始层 + 标签层”避免脏数据污染决策

很多人会把抓到的数据直接整理成最终要用的格式,然后写进一张表。这个习惯在竞争分析场景里很危险。昨天解析逻辑写错一个字段,今天整列数据就错了,而且因为旧数据已经被覆盖,根本没法回查。

我的做法是强制分两层。第一层叫 raw 层,原样保留采集回来的 HTML、JSON 或 CSV 内容,哪怕是带格式的完整文本也没关系。第二层叫 label 层,从 raw 层解析出结构化字段。任何解析规则变了,只影响 label 层,raw 层永远不动。

这个设计和数据湖的思路是一致的,只是竞争分析不需要那么重的基础设施。一个 DuckDB 文件,或者一张 PostgreSQL 表,按日期分区保存 raw 文本就够了。等解析逻辑稳定下来了,再回放 raw 层的数据,重新生成 label 层。关键是,这个回放过程必须能随时执行,所以采集端要留着原始报文体,而不是只存清洗后的价格数字。

3. 采集端落地:竞品价格、SKU 与渠道动态的抓取和入库

3.1 一个稳健的采集器,先过三关:频率、字段、异常

采集竞品数据,最常见的坑不是反爬,而是采集器本身不稳定。跑了两周才发现,某两天因为页面结构改版,所有价格字段解析出来都是空值,导致那两天的分析结果直接失真。

所以采集器上线前要过三关。第一关是频率:按页面更新速度决定采集间隔,价格页面可以每小时采,促销活动页可以每半小时采,而静态类目排名每天采一次就够了。第二关是字段校验:每次解析完后,检查非空率和数字格式,如果某字段空值率超过 5%,直接丢弃该批次并报警。第三关是异常隔离:解析失败的原始内容放进隔离区,绝不进仓库。

另外,在动手写爬虫之前,先看一眼目标站点的采集规范文件。curl 一下看看站点允许抓取哪些路径和访问频率,是比较稳妥的做法。

curl -s https://example.com/robots.txt | head -n 30

这段命令返回的是站点声明允许或禁止的路径列表。重点看 Disallow 规则和 Crawl-delay 字段,Crawl-delay 会告诉你每次请求之间至少要隔多少秒。需要注意,采集规范文件的允许范围不等于你可以无视站点服务条款,正式使用前还需要按目标站点的规则确认自己的用途和身份标识。

3.2 用 requests + BeautifulSoup 抓价格列表的可执行示例

拿到页面后,第一版采集器可以用最简单的组合实现:requests 负责请求,BeautifulSoup 负责解析。下面是一段可独立运行的示例,提取列表页上的所有价格文本。

import requests from bs4 import BeautifulSoup from time import sleep from random import uniform def fetch_price_list(url: str, selector: str, interval=(1.5, 3.0)) -> list[float]: session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (compatible; MarketIntelBot/1.0; +https://example.com/bot)" }) resp = session.get(url, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") prices = [] for node in soup.select(selector): raw = node.get_text(strip=True).replace("¥", "").replace(",", "") try: prices.append(float(raw)) except ValueError: continue sleep(uniform(*interval)) return prices

selector是 CSS 选择器,指向包含价格文本的 DOM 元素,例如li.product-priceinterval参数用随机区间控制请求间隔,避免每个请求都精确卡在同一个时间点。resp.raise_for_status()会在 HTTP 状态码为 4xx 或 5xx 时直接抛异常,避免把错误页面当成正常内容解析。

解析部分做了两件防御工作:第一,先删掉货币符号和千分位逗号;第二,float 转换失败就跳过,不中断整批采集。这种逐条容错在面对页面局部改版时非常重要,一个坏数据点最多影响一条记录,不会让整轮采集失败。

现在的电商页面大量使用 JavaScript 渲染,爬出来的 HTML 里可能没有价格节点。这种情况下,最快的替代方案是找页面底部的 API 接口,用浏览器开发者工具里的网络面板查看 XHR 请求,直接请求 JSON 数据源。解析逻辑不用改,只是把 HTML 选择器换成 JSON 的 key 路径。

3.3 把采集结果写进 DuckDB:去重、增量与留存

采集到的数据进入分析流程之前,需要一个能快速查询的本地存储。DuckDB 是合适的选择,它没有独立服务进程,一个文件就是一个数据库,非常适合单机分析。入库时要处理的核心问题,是避免同一时间点的重复记录。

import duckdb from datetime import datetime, timezone conn = duckdb.connect("competition.duckdb") conn.execute(""" CREATE TABLE IF NOT EXISTS price_snapshot ( product_id VARCHAR, competitor VARCHAR, price DECIMAL(12,2), captured_at TIMESTAMPTZ ); """) conn.execute(""" INSERT OR REPLACE INTO price_snapshot VALUES (?, ?, ?, ?) """, ("P-10086", "competitor_a", 129.00, datetime.now(timezone.utc)))

INSERT OR REPLACE的行为依赖表上的主键或唯一约束,如果同样的product_id + competitor + captured_at已经存在,就会用新值替换旧值。这比先查再插的写法少一次请求,而且在并发写入时更安全。captured_at统一用 UTC 时间,到了分析阶段再按业务时区转换,避免不同机器上的本地时区造成时间错位。

留存策略上,raw 层数据按月归档,label 层快照永久保留。价格快照这种数据占用的空间不大,一万个 SKU 每小时采一次,一年也才几千万行,完全在普通商业笔记本可以处理的范围内,没必要过早做聚合压缩。

4. 竞争态势的计算层:份额迁移、排名与价格弹性

4.1 SQL 计算周维度份额迁移与排名

有了快照表之后,第一个要算的通常是份额趋势。份额的统计口径不是唯一标准,可以用 SKU 数量算覆盖份额,也可以用销量算销售额份额。在只有价格和覆盖数据的情况下,SKU 覆盖份额是最容易得到的近似指标。

WITH weekly AS ( SELECT competitor, date_trunc('week', captured_at) AS week_start, count(DISTINCT product_id) AS live_skus, median(price) AS median_price FROM price_snapshot WHERE captured_at >= current_date - INTERVAL '90 days' GROUP BY 1, 2 ) SELECT competitor, week_start, live_skus, median_price, round( 100.0 * live_skus / sum(live_skus) OVER (PARTITION BY week_start), 2 ) AS sku_share, rank() OVER (PARTITION BY week_start ORDER BY live_skus DESC) AS rank_no FROM weekly ORDER BY week_start DESC, rank_no;

这段 SQL 的要点在窗口函数上。sum(live_skus) OVER (PARTITION BY week_start)计算的是同一周内所有竞品的 SKU 总数,然后用每个竞品的数量除以这个总数,就得到当周的覆盖份额。rank()按每周内 SKU 数量降序排名,rank_no = 1表示当周覆盖最全的竞品。

如果发现某周第一名和第二名的份额差距在缩小,但价格中位数差没有同步缩小,说明竞争正在从价格维度转向覆盖维度。此时再看上一周的营销事件表,往往能找到新品发布或渠道拓展的记录。

4.2 用对数回归估算价格弹性

价格弹性是竞争分析里被提到最多、但算得最少的一个指标。原因很简单,大部分团队没有干净的历史价格和销量数据。如果你已经积累了一段时间的快照,那可以做一个简化的对数线性回归。

import pandas as pd import numpy as np def price_elasticity(df: pd.DataFrame) -> float: sub = df[["price", "sales_qty"]].replace(0, np.nan).dropna() x = np.log(sub["price"]) y = np.log(sub["sales_qty"]) beta = np.polyfit(x, y, 1)[0] return round(beta, 3)

np.polyfit(x, y, 1)在双对数坐标下拟合一条直线,返回的第一个系数就是斜率,也就是价格弹性的近似值。典型结果是负值,比如 -1.8,表示价格每上涨 1%,销量大约下降 1.8%。

这里的sales_qty可以是自己的销量,也可以是竞品页面上的销量标签。如果你拿自己和某个竞品的数据交替做这个回归,可以对比不同品牌面对价格变动时的敏感程度。弹性绝对值大的品牌,价格战对它伤害更大;弹性绝对值小的,可以承受更长时间的折扣战。要注意,这个分析只能看到相关性,不能直接当成因果关系,价格之外的因素需要结合事件表辅助判断。

4.3 竞争强度计分卡与对策触发条件

份额和弹性都是单维度指标,真正推动团队行动的是综合评分。我会根据业务特点,做一个竞争强度计分卡,把所有可量化信号加权成一个分数。

信号门槛分值
竞品平均折扣率高于 20%+5
我方 SKU 覆盖率落后低于领先者 30 个百分点+3
替代品流量周增速超过 20%+2
新进入者上线过去 30 天出现新品牌+4
价格弹性绝对值大于 1.5+2
竞品缺货率超过 10%-3

这个计分卡的逻辑不是要预测什么,而是让“对策”有明确的触发条件。总分大于等于 10 时,进入重点关注名单,产研和运营团队每周过一遍;大于等于 15 时,触发专项应对,比如跟随调价或加快新品排期。分数小于 3 时,维持常规监控,不消耗团队注意力。

计分卡的阈值必须定期用历史数据回验。如果过去一个月里,触发重点关注的周次里有一半以上最后并没有发生实质性竞争恶化,就该把阈值往上调,减少空转。

5. 对策信号验证与误报收敛:把分析结果放进回放闭环

5.1 离线回放:让解析逻辑对旧数据负责

竞争分析系统跑起来之后,最怕的不是没信号,而是信号错了没人知道。所以我坚持每个解析脚本都要支持离线回放。回放的意思是,用当前的解析逻辑,重新处理过去 7 天的 raw 数据,然后把新解析结果和当天入库时的结果做对比。

对比的维度和注意点可以参考下面的 Python 片段,它统计每一轮解析后的空值数量和价格差:

import pandas as pd def diff_parsed(current: pd.DataFrame, replayed: pd.DataFrame) -> pd.DataFrame: diff = current.merge( replayed, on=["product_id", "captured_at"], suffixes=("_old", "_new") ) invalid = diff[ diff["price_new"].isna() | (abs(diff["price_new"] - diff["price_old"]) > 0.01) ] return invalid

这段代码通过merge把两轮解析结果对齐到同一对主键上,再找出价格缺失或差异大于 0.01 的记录。如果某一天的差异行数明显偏多,就说明当时页面结构变化了,而旧解析逻辑没有兼容。只有把这样 的差异控制到接近零,计分卡产生的信号才值得业务方相信。

5.2 用 MAD 替换 z-score,减少长尾误报

价格异常检测是竞争情报里最常用的警报,但用传统的均值加减三倍标准差,很容易被极端值带偏。比如某个 SKU 突然被下架,价格变成 0,均值会被拉低,导致正常价格反而被标记为异常。中位数绝对偏差在这个场景下更稳定。

from scipy.stats import median_abs_deviation def mad_based_alert(series, k=4.5): med = series.median() mad = median_abs_deviation(series) lower = med - k * 1.4826 * mad upper = med + k * 1.4826 * mad return series[(series < lower) | (series > upper)]

median_abs_deviation计算每个样本到中位数的绝对偏差的中位数,1.4826是把 MAD 调整到与正态分布标准差同一尺度的常数。k=4.5是比较保守的阈值,意味着只有偏离中位数 4.5 个 MAD 的值才报警。

z-score 对均值很敏感,而 MAD 看的是中位数,所以即便有少量极端值,正常数据的基线也不会被拉走,误报率会低很多。如果切换到 MAD 后警报数量大幅下降,说明原来的异常检测被极端值劫持了。

5.3 把对策信号固化到日调度

最后的技巧,是把验证好的采集、解析、计分、报警串成一个可重复执行的日调度。每天早上固定时间运行,结束后在日志里输出统计摘要,我主要看processedalertsquarantine三个计数器,分别代表本轮处理的产品数、触发的警报数和被隔离的可疑记录数。当quarantine回归到接近 0,业务方再看到来自这套管道的数据,就不会再把“竞品降价了”当成一句无法核实的话。

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

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

【ComfyUI】Wan2.2 Smooth Mix 首尾帧图像电影质感视频生成

今天给大家演示一个高质量、自动化的 ComfyUI 视频生成工作流,其亮点在于通过两张图像(视频首帧与尾帧)自动生成画面提示词,并融合大模型实现影视级的细腻镜头过渡。该流程还集成了视频放大、插帧、文本提示控制、双模型混合、自动构图等关键模块,实现从图像到动态影像的全…

作者头像 李华
网站建设 2026/9/18 4:03:24

AI Agent落地实战:Agent-Reach触达层设计全复盘

我最早做AI Agent相关项目的时候&#xff0c;踩过一个特别典型的坑&#xff1a;模型选的是当时最强的&#xff0c;Prompt也反复调了好几个版本&#xff0c;Demo演示的时候各种丝滑&#xff0c;结果一接到真实业务场景&#xff0c;Agent就开始"满嘴跑火车"——让它查一…

作者头像 李华
网站建设 2026/9/18 4:02:33

Shell命令实战进阶:从cd导航到脚本执行避坑指南

很多人学Shell都是从一张“常用命令清单”开始的&#xff0c;cd、ls、df、grep、find背得滚瓜烂熟&#xff0c;可真到了排查问题或者写部署脚本的时候&#xff0c;还是觉得命令不听使唤。这不是记性不好&#xff0c;而是你只是在背命令的拼写&#xff0c;没有理解它们在实际场景…

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

药品仓储巡检系统实战:双框架架构与批次效期管理

1. 药品仓储巡检到底要巡什么&#xff1a;需求调研阶段的关键发现去年年中我接手这个项目时&#xff0c;甲方提的需求特别简单——“做一个药品仓库的巡检系统”&#xff0c;听起来像是那种随手就能交付的管理小工具。但真正蹲到仓库现场待了两天之后&#xff0c;我才意识到这事…

作者头像 李华
网站建设 2026/9/18 4:01:20

Ant Design按钮点击文字位移问题:CSS根因分析与实用修复方案

做管理后台的开发者&#xff0c;十有八九被一个细节折磨过&#xff1a;Ant Design 的按钮样式写得再规整&#xff0c;鼠标按下的那一瞬间&#xff0c;按钮里的文字还是会像被什么东西推了一下&#xff0c;轻微地往右下角或上方挪动几个像素&#xff0c;松开手指又弹回来。反复试…

作者头像 李华