mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
refactor(crawler): 价盘促销分析迁入 competitor_report.price_promo
- 抽出 _analyze_price_promotions 与 _markdown_price_promotion_section,依赖 constants 与 csv_io。 - jd_competitor_report 显式 import,对外行为不变。 Made-with: Cursor
This commit is contained in:
parent
4687bcb907
commit
64dd1b149d
@ -0,0 +1,147 @@
|
|||||||
|
"""列表/合并行上的标价与券后价差统计,及第六章 6.1 Markdown 片段。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import statistics
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .constants import (
|
||||||
|
_COUPON_SHOW_PRICE_KEY,
|
||||||
|
_JD_LIST_PRICE_KEY,
|
||||||
|
_LEGACY_COUPON_SHOW_PRICE_KEY,
|
||||||
|
_LEGACY_JD_LIST_PRICE_KEY,
|
||||||
|
_LEGACY_RANK_TAGLINE_KEY,
|
||||||
|
_LEGACY_SELLING_POINT_KEY,
|
||||||
|
_ORIGINAL_LIST_PRICE_KEY,
|
||||||
|
_RANK_TAGLINE_KEY,
|
||||||
|
_SELLING_POINT_KEY,
|
||||||
|
)
|
||||||
|
from .csv_io import _cell, _float_price
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_price_promotions(rows: list[dict[str, str]]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
从列表或合并行中归纳「标价 vs 券后/到手」等价差信号,
|
||||||
|
供第六章第一节与结构化摘要使用(按**页面展示价**字段归纳,非结算实付)。
|
||||||
|
"""
|
||||||
|
n = len(rows)
|
||||||
|
with_jd = with_cp = with_both = 0
|
||||||
|
coupon_below = 0
|
||||||
|
pct_offs: list[float] = []
|
||||||
|
ori_above_list = 0
|
||||||
|
for row in rows:
|
||||||
|
jd = _float_price(_cell(row, _JD_LIST_PRICE_KEY, _LEGACY_JD_LIST_PRICE_KEY))
|
||||||
|
cp = _float_price(
|
||||||
|
_cell(row, _COUPON_SHOW_PRICE_KEY, _LEGACY_COUPON_SHOW_PRICE_KEY)
|
||||||
|
)
|
||||||
|
ori = _float_price(_cell(row, _ORIGINAL_LIST_PRICE_KEY))
|
||||||
|
if jd is not None and jd > 0:
|
||||||
|
with_jd += 1
|
||||||
|
if cp is not None and cp > 0:
|
||||||
|
with_cp += 1
|
||||||
|
if jd is not None and cp is not None and jd > 0 and cp > 0:
|
||||||
|
with_both += 1
|
||||||
|
if cp + 1e-6 < jd:
|
||||||
|
coupon_below += 1
|
||||||
|
pct_offs.append((jd - cp) / jd * 100.0)
|
||||||
|
if (
|
||||||
|
ori is not None
|
||||||
|
and jd is not None
|
||||||
|
and ori > 0
|
||||||
|
and jd > 0
|
||||||
|
and ori > jd + 1e-6
|
||||||
|
):
|
||||||
|
ori_above_list += 1
|
||||||
|
|
||||||
|
selling_nonempty = sum(
|
||||||
|
1
|
||||||
|
for r in rows
|
||||||
|
if _cell(r, _SELLING_POINT_KEY, _LEGACY_SELLING_POINT_KEY).strip()
|
||||||
|
)
|
||||||
|
rank_nonempty = sum(
|
||||||
|
1
|
||||||
|
for r in rows
|
||||||
|
if _cell(r, _RANK_TAGLINE_KEY, _LEGACY_RANK_TAGLINE_KEY).strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
median_pct = statistics.median(pct_offs) if pct_offs else None
|
||||||
|
mean_pct = statistics.mean(pct_offs) if pct_offs else None
|
||||||
|
share_below = (
|
||||||
|
(coupon_below / with_both) if with_both else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"row_count": n,
|
||||||
|
"rows_with_list_price": with_jd,
|
||||||
|
"rows_with_coupon_price": with_cp,
|
||||||
|
"rows_with_both_list_and_coupon": with_both,
|
||||||
|
"rows_coupon_below_list_price": coupon_below,
|
||||||
|
"share_coupon_below_list_when_both": share_below,
|
||||||
|
"median_discount_pct_when_coupon_below": median_pct,
|
||||||
|
"mean_discount_pct_when_coupon_below": mean_pct,
|
||||||
|
"rows_original_price_above_list_price": ori_above_list,
|
||||||
|
"rows_selling_point_nonempty": selling_nonempty,
|
||||||
|
"rows_rank_tagline_nonempty": rank_nonempty,
|
||||||
|
"promo_keyword_row_hits_top": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _markdown_price_promotion_section(p: dict[str, Any]) -> list[str]:
|
||||||
|
"""第六章第一节:优惠活动与价差信号(Markdown 行列表)。"""
|
||||||
|
lines: list[str] = [
|
||||||
|
"### 6.1 优惠活动与价差信号(页面展示摘录)",
|
||||||
|
"",
|
||||||
|
"- **统计范围**:与上节价量统计**同一批行**;比较的是列表/合并表中的**展示标价**与**展示券后/到手价**(字段见表头),"
|
||||||
|
"反映页面呈现的活动与券信息,**不等于**用户结算实付或历史最低价。",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
wb = int(p.get("rows_with_both_list_and_coupon") or 0)
|
||||||
|
if wb <= 0:
|
||||||
|
lines.append(
|
||||||
|
"- **标价与券后价可对齐比较**的有效行不足,本节以展示价与券后/到手字段的可得信息为主。"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
else:
|
||||||
|
cb = int(p.get("rows_coupon_below_list_price") or 0)
|
||||||
|
sh = p.get("share_coupon_below_list_when_both")
|
||||||
|
med = p.get("median_discount_pct_when_coupon_below")
|
||||||
|
mean = p.get("mean_discount_pct_when_coupon_below")
|
||||||
|
lines.append(
|
||||||
|
f"- **同时解析到标价与券后/到手价** 的行:**{wb}**;其中展示「到手/券后」**严格低于**「标价」的行:**{cb}**"
|
||||||
|
+ (
|
||||||
|
f"(占可对齐行的 **{100.0 * float(sh):.1f}%**)"
|
||||||
|
if isinstance(sh, (int, float))
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ "。"
|
||||||
|
)
|
||||||
|
if med is not None:
|
||||||
|
frag_mean = (
|
||||||
|
f",平均价差约 **{float(mean):.1f}%**" if mean is not None else ""
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"- **价差力度(仅「券后低于标价」子集)**:展示价差的中位数约 **{float(med):.1f}%**(相对标价){frag_mean};"
|
||||||
|
"通常对应满减、券、限时价等在列表上的叠加呈现。"
|
||||||
|
)
|
||||||
|
elif cb > 0:
|
||||||
|
lines.append(
|
||||||
|
"- **价差**:存在「券后低于标价」样本,但条数较少,未给出稳健分位数;建议结合第五章矩阵中的单品对照。"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
oa = int(p.get("rows_original_price_above_list_price") or 0)
|
||||||
|
if oa > 0:
|
||||||
|
lines.append(
|
||||||
|
f"- **划线原价高于当前标价** 的行约 **{oa}** 条(常见「划线价 + 当前价」促销陈列,具体以页面为准)。"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
"- **说明**:本节**不对**列表「卖点/腰带」等字段做预设促销关键词行级统计;"
|
||||||
|
"活动形态归纳以第六章「细类促销与活动要点归纳」为准(若已生成)。"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_analyze_price_promotions",
|
||||||
|
"_markdown_price_promotion_section",
|
||||||
|
]
|
||||||
@ -53,6 +53,10 @@ from pipeline.csv_schema import ( # noqa: E402
|
|||||||
from competitor_report.config import * # noqa: F403
|
from competitor_report.config import * # noqa: F403
|
||||||
from competitor_report.constants import * # noqa: F403
|
from competitor_report.constants import * # noqa: F403
|
||||||
from competitor_report.csv_io import * # noqa: F403
|
from competitor_report.csv_io import * # noqa: F403
|
||||||
|
from competitor_report.price_promo import ( # noqa: E402
|
||||||
|
_analyze_price_promotions,
|
||||||
|
_markdown_price_promotion_section,
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 运行配置(按需改这里;与 competitor_report.constants 中默认关注词等配合使用)
|
# 运行配置(按需改这里;与 competitor_report.constants 中默认关注词等配合使用)
|
||||||
@ -68,129 +72,6 @@ OVERRIDE_PAGE_START: int | None = None
|
|||||||
OVERRIDE_PAGE_TO: int | None = None
|
OVERRIDE_PAGE_TO: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def _analyze_price_promotions(rows: list[dict[str, str]]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
从列表或合并行中归纳「标价 vs 券后/到手」等价差信号,
|
|
||||||
供第六章第一节与结构化摘要使用(按**页面展示价**字段归纳,非结算实付)。
|
|
||||||
"""
|
|
||||||
n = len(rows)
|
|
||||||
with_jd = with_cp = with_both = 0
|
|
||||||
coupon_below = 0
|
|
||||||
pct_offs: list[float] = []
|
|
||||||
ori_above_list = 0
|
|
||||||
for row in rows:
|
|
||||||
jd = _float_price(_cell(row, _JD_LIST_PRICE_KEY, _LEGACY_JD_LIST_PRICE_KEY))
|
|
||||||
cp = _float_price(
|
|
||||||
_cell(row, _COUPON_SHOW_PRICE_KEY, _LEGACY_COUPON_SHOW_PRICE_KEY)
|
|
||||||
)
|
|
||||||
ori = _float_price(_cell(row, _ORIGINAL_LIST_PRICE_KEY))
|
|
||||||
if jd is not None and jd > 0:
|
|
||||||
with_jd += 1
|
|
||||||
if cp is not None and cp > 0:
|
|
||||||
with_cp += 1
|
|
||||||
if jd is not None and cp is not None and jd > 0 and cp > 0:
|
|
||||||
with_both += 1
|
|
||||||
if cp + 1e-6 < jd:
|
|
||||||
coupon_below += 1
|
|
||||||
pct_offs.append((jd - cp) / jd * 100.0)
|
|
||||||
if (
|
|
||||||
ori is not None
|
|
||||||
and jd is not None
|
|
||||||
and ori > 0
|
|
||||||
and jd > 0
|
|
||||||
and ori > jd + 1e-6
|
|
||||||
):
|
|
||||||
ori_above_list += 1
|
|
||||||
|
|
||||||
selling_nonempty = sum(
|
|
||||||
1
|
|
||||||
for r in rows
|
|
||||||
if _cell(r, _SELLING_POINT_KEY, _LEGACY_SELLING_POINT_KEY).strip()
|
|
||||||
)
|
|
||||||
rank_nonempty = sum(
|
|
||||||
1
|
|
||||||
for r in rows
|
|
||||||
if _cell(r, _RANK_TAGLINE_KEY, _LEGACY_RANK_TAGLINE_KEY).strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
median_pct = statistics.median(pct_offs) if pct_offs else None
|
|
||||||
mean_pct = statistics.mean(pct_offs) if pct_offs else None
|
|
||||||
share_below = (
|
|
||||||
(coupon_below / with_both) if with_both else None
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"row_count": n,
|
|
||||||
"rows_with_list_price": with_jd,
|
|
||||||
"rows_with_coupon_price": with_cp,
|
|
||||||
"rows_with_both_list_and_coupon": with_both,
|
|
||||||
"rows_coupon_below_list_price": coupon_below,
|
|
||||||
"share_coupon_below_list_when_both": share_below,
|
|
||||||
"median_discount_pct_when_coupon_below": median_pct,
|
|
||||||
"mean_discount_pct_when_coupon_below": mean_pct,
|
|
||||||
"rows_original_price_above_list_price": ori_above_list,
|
|
||||||
"rows_selling_point_nonempty": selling_nonempty,
|
|
||||||
"rows_rank_tagline_nonempty": rank_nonempty,
|
|
||||||
"promo_keyword_row_hits_top": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _markdown_price_promotion_section(p: dict[str, Any]) -> list[str]:
|
|
||||||
"""第六章第一节:优惠活动与价差信号(Markdown 行列表)。"""
|
|
||||||
lines: list[str] = [
|
|
||||||
"### 6.1 优惠活动与价差信号(页面展示摘录)",
|
|
||||||
"",
|
|
||||||
"- **统计范围**:与上节价量统计**同一批行**;比较的是列表/合并表中的**展示标价**与**展示券后/到手价**(字段见表头),"
|
|
||||||
"反映页面呈现的活动与券信息,**不等于**用户结算实付或历史最低价。",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
wb = int(p.get("rows_with_both_list_and_coupon") or 0)
|
|
||||||
if wb <= 0:
|
|
||||||
lines.append(
|
|
||||||
"- **标价与券后价可对齐比较**的有效行不足,本节以展示价与券后/到手字段的可得信息为主。"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
else:
|
|
||||||
cb = int(p.get("rows_coupon_below_list_price") or 0)
|
|
||||||
sh = p.get("share_coupon_below_list_when_both")
|
|
||||||
med = p.get("median_discount_pct_when_coupon_below")
|
|
||||||
mean = p.get("mean_discount_pct_when_coupon_below")
|
|
||||||
lines.append(
|
|
||||||
f"- **同时解析到标价与券后/到手价** 的行:**{wb}**;其中展示「到手/券后」**严格低于**「标价」的行:**{cb}**"
|
|
||||||
+ (
|
|
||||||
f"(占可对齐行的 **{100.0 * float(sh):.1f}%**)"
|
|
||||||
if isinstance(sh, (int, float))
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ "。"
|
|
||||||
)
|
|
||||||
if med is not None:
|
|
||||||
frag_mean = (
|
|
||||||
f",平均价差约 **{float(mean):.1f}%**" if mean is not None else ""
|
|
||||||
)
|
|
||||||
lines.append(
|
|
||||||
f"- **价差力度(仅「券后低于标价」子集)**:展示价差的中位数约 **{float(med):.1f}%**(相对标价){frag_mean};"
|
|
||||||
"通常对应满减、券、限时价等在列表上的叠加呈现。"
|
|
||||||
)
|
|
||||||
elif cb > 0:
|
|
||||||
lines.append(
|
|
||||||
"- **价差**:存在「券后低于标价」样本,但条数较少,未给出稳健分位数;建议结合第五章矩阵中的单品对照。"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
oa = int(p.get("rows_original_price_above_list_price") or 0)
|
|
||||||
if oa > 0:
|
|
||||||
lines.append(
|
|
||||||
f"- **划线原价高于当前标价** 的行约 **{oa}** 条(常见「划线价 + 当前价」促销陈列,具体以页面为准)。"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
lines.append(
|
|
||||||
"- **说明**:本节**不对**列表「卖点/腰带」等字段做预设促销关键词行级统计;"
|
|
||||||
"活动形态归纳以第六章「细类促销与活动要点归纳」为准(若已生成)。"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def _comment_keyword_hits(
|
def _comment_keyword_hits(
|
||||||
rows: list[dict[str, str]],
|
rows: list[dict[str, str]],
|
||||||
focus_words: tuple[str, ...],
|
focus_words: tuple[str, ...],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user