fix(growth): stabilize comparison coverage cohort
This commit is contained in:
@@ -414,7 +414,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 系統版本與路徑
|
# 系統版本與路徑
|
||||||
# ==========================================
|
# ==========================================
|
||||||
SYSTEM_VERSION = "V10.798"
|
SYSTEM_VERSION = "V10.799"
|
||||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||||
public_url = PUBLIC_URL # 用於模板顯示
|
public_url = PUBLIC_URL # 用於模板顯示
|
||||||
|
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ def build_coverage_snapshot(payload: dict[str, Any]) -> dict[str, Any]:
|
|||||||
contract = stats.get("comparison_metric_contract") or {}
|
contract = stats.get("comparison_metric_contract") or {}
|
||||||
partition = contract.get("state_partition") or {}
|
partition = contract.get("state_partition") or {}
|
||||||
return {
|
return {
|
||||||
"scope": contract.get("scope") or f"top_{len(opportunities)}_revenue_opportunities",
|
"scope": contract.get("scope") or f"top_{len(opportunities)}_revenue_products",
|
||||||
"comparison_ready_count": int(
|
"comparison_ready_count": int(
|
||||||
partition.get("comparison_ready") or stats.get("mapped_count") or 0
|
partition.get("comparison_ready") or stats.get("mapped_count") or 0
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
@@ -202,7 +203,7 @@ def _fetch_sales_rows(conn, limit: int) -> tuple[list[dict[str, Any]], str | Non
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM sales
|
FROM sales
|
||||||
WHERE sales_7d > 0 OR sales_prev_7d > 0
|
WHERE sales_7d > 0 OR sales_prev_7d > 0
|
||||||
ORDER BY {order_metric} DESC, qty_7d DESC
|
ORDER BY {order_metric} DESC, qty_7d DESC, pchome_product_id ASC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
"""), {"limit": candidate_limit}).mappings().all()
|
"""), {"limit": candidate_limit}).mappings().all()
|
||||||
|
|
||||||
@@ -1178,6 +1179,9 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
|||||||
"opportunities": [],
|
"opportunities": [],
|
||||||
"message": str(exc),
|
"message": str(exc),
|
||||||
}
|
}
|
||||||
|
# Coverage must use a stable revenue-ranked cohort. Mapping status may
|
||||||
|
# reorder work inside the cohort, but must never remove a success from it.
|
||||||
|
sales_rows = sales_rows[:limit]
|
||||||
sales_ids = [str(row.get("pchome_product_id") or "") for row in sales_rows]
|
sales_ids = [str(row.get("pchome_product_id") or "") for row in sales_rows]
|
||||||
external_map, market_offer_map = _fetch_external_offer_maps(
|
external_map, market_offer_map = _fetch_external_offer_maps(
|
||||||
conn,
|
conn,
|
||||||
@@ -1281,13 +1285,24 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
|||||||
if runtime_external_source_count < registered_external_source_count:
|
if runtime_external_source_count < registered_external_source_count:
|
||||||
blocked_reason_codes.append("platform_runtime_coverage_gap")
|
blocked_reason_codes.append("platform_runtime_coverage_gap")
|
||||||
|
|
||||||
|
scope_ids = [str(row.get("pchome_product_id") or "") for row in sales_rows]
|
||||||
|
scope_fingerprint = hashlib.sha256(
|
||||||
|
json.dumps(scope_ids, ensure_ascii=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
).hexdigest()[:24]
|
||||||
comparison_metric_contract = {
|
comparison_metric_contract = {
|
||||||
"version": "pchome_comparison_coverage_v2",
|
"version": "pchome_comparison_coverage_v3",
|
||||||
"scope": f"top_{len(opportunities)}_revenue_opportunities",
|
"scope": f"top_{len(opportunities)}_revenue_products",
|
||||||
|
"scope_membership": {
|
||||||
|
"selection_stage": "before_action_priority_scoring",
|
||||||
|
"ranking": "max_current_or_previous_7d_revenue_desc_then_qty_desc_then_product_id_asc",
|
||||||
|
"member_count": len(scope_ids),
|
||||||
|
"fingerprint_sha256": scope_fingerprint,
|
||||||
|
"mapping_status_can_change_membership": False,
|
||||||
|
},
|
||||||
"numerator": mapped_count,
|
"numerator": mapped_count,
|
||||||
"denominator": len(opportunities),
|
"denominator": len(opportunities),
|
||||||
"rate": mapping_rate,
|
"rate": mapping_rate,
|
||||||
"meaning": "通過同款與價格品質門檻、具正式外部價格證據的高優先商品",
|
"meaning": "營收排名母體中,通過同款與價格品質門檻且具正式外部價格證據的商品",
|
||||||
"state_partition": {
|
"state_partition": {
|
||||||
"comparison_ready": mapped_count,
|
"comparison_ready": mapped_count,
|
||||||
"candidate_validation": candidate_validation_count,
|
"candidate_validation": candidate_validation_count,
|
||||||
@@ -1314,7 +1329,7 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
|||||||
"numerator": round(comparison_ready_sales_7d, 2),
|
"numerator": round(comparison_ready_sales_7d, 2),
|
||||||
"denominator": round(total_opportunity_sales_7d, 2),
|
"denominator": round(total_opportunity_sales_7d, 2),
|
||||||
"rate": comparison_revenue_coverage_rate,
|
"rate": comparison_revenue_coverage_rate,
|
||||||
"meaning": "高優先商品近 7 天業績中,已有正式同商品外部價格證據的占比",
|
"meaning": "營收排名母體近 7 天業績中,已有正式同商品外部價格證據的占比",
|
||||||
"state_partition": {
|
"state_partition": {
|
||||||
"comparison_ready": round(comparison_ready_sales_7d, 2),
|
"comparison_ready": round(comparison_ready_sales_7d, 2),
|
||||||
"candidate_validation": round(candidate_validation_sales_7d, 2),
|
"candidate_validation": round(candidate_validation_sales_7d, 2),
|
||||||
|
|||||||
@@ -330,6 +330,45 @@ def test_pchome_growth_coverage_states_are_mutually_exclusive():
|
|||||||
assert stats["comparison_metric_contract"]["state_partition"]["mutually_exclusive"] is True
|
assert stats["comparison_metric_contract"]["state_partition"]["mutually_exclusive"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_pchome_growth_coverage_scope_is_fixed_before_mapping_priority():
|
||||||
|
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
_seed_growth_tables(engine)
|
||||||
|
with engine.begin() as conn:
|
||||||
|
conn.execute(text("""
|
||||||
|
INSERT INTO daily_sales_snapshot
|
||||||
|
("商品ID", "商品名稱", snapshot_date, "總業績", "數量", "商品館")
|
||||||
|
VALUES
|
||||||
|
('PCH-3', '營收第三名', '2026-06-14', 150000, 30, '保養'),
|
||||||
|
('PCH-4', '營收第四名', '2026-06-14', 145000, 29, '保養'),
|
||||||
|
('PCH-5', '營收第五名', '2026-06-14', 140000, 28, '保養'),
|
||||||
|
('PCH-6', '營收第六名', '2026-06-14', 135000, 27, '保養')
|
||||||
|
"""))
|
||||||
|
|
||||||
|
payload = build_pchome_growth_opportunities(engine, limit=5)
|
||||||
|
product_ids = {
|
||||||
|
item["pchome_product_id"] for item in payload["opportunities"]
|
||||||
|
}
|
||||||
|
contract = payload["stats"]["comparison_metric_contract"]
|
||||||
|
|
||||||
|
assert "PCH-1" in product_ids
|
||||||
|
assert "PCH-2" not in product_ids
|
||||||
|
assert contract["version"] == "pchome_comparison_coverage_v3"
|
||||||
|
assert contract["scope"] == "top_5_revenue_products"
|
||||||
|
assert contract["numerator"] == 1
|
||||||
|
assert contract["denominator"] == 5
|
||||||
|
assert contract["rate"] == 20.0
|
||||||
|
assert contract["scope_membership"] == {
|
||||||
|
"selection_stage": "before_action_priority_scoring",
|
||||||
|
"ranking": "max_current_or_previous_7d_revenue_desc_then_qty_desc_then_product_id_asc",
|
||||||
|
"member_count": 5,
|
||||||
|
"fingerprint_sha256": contract["scope_membership"]["fingerprint_sha256"],
|
||||||
|
"mapping_status_can_change_membership": False,
|
||||||
|
}
|
||||||
|
assert len(contract["scope_membership"]["fingerprint_sha256"]) == 24
|
||||||
|
|
||||||
|
|
||||||
def test_momo_review_candidates_return_dual_store_links_and_plain_reasons():
|
def test_momo_review_candidates_return_dual_store_links_and_plain_reasons():
|
||||||
from services.external_market_offer_service import list_momo_review_candidates
|
from services.external_market_offer_service import list_momo_review_candidates
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user