@petercjl/taobao-ai-ops 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +30 -22
  2. package/package.json +2 -7
  3. package/requirements.txt +1 -0
  4. package/skills/taobao-ai-operations/SKILL.md +8 -1
  5. package/src/cli.mjs +16 -6
  6. package/src/components.mjs +73 -0
  7. package/src/core.mjs +5 -1
  8. package/src/runtime.mjs +18 -16
  9. package/src/skill-manager.mjs +4 -4
  10. package/src/update.mjs +5 -5
  11. package/src/workflow.mjs +3 -3
  12. package/suite-manifest.json +186 -10
  13. package/skills/taobao-search-product-form/SKILL.md +0 -74
  14. package/skills/taobao-search-product-form/adapters/codex.json +0 -139
  15. package/skills/taobao-search-product-form/adapters/sealseek.json +0 -24
  16. package/skills/taobao-search-product-form/agents/openai.yaml +0 -3
  17. package/skills/taobao-search-product-form/capabilities.json +0 -148
  18. package/skills/taobao-search-product-form/references/SCHEMA.md +0 -3
  19. package/skills/taobao-search-product-form/references/index.md +0 -10
  20. package/skills/taobao-search-product-form/references/log.md +0 -35
  21. package/skills/taobao-search-product-form/references/queries/evaluate-white-label-forms.md +0 -3
  22. package/skills/taobao-search-product-form/references/queries/identify-advertisers.md +0 -5
  23. package/skills/taobao-search-product-form/references/queries/identify-geography.md +0 -5
  24. package/skills/taobao-search-product-form/references/queries/identify-price-bands.md +0 -3
  25. package/skills/taobao-search-product-form/references/queries/identify-product-forms.md +0 -3
  26. package/skills/taobao-search-product-form/references/queries/identify-title-roots.md +0 -5
  27. package/skills/taobao-search-product-form/references/report-output.md +0 -23
  28. package/skills/taobao-search-product-form/references/source-manifest.md +0 -37
  29. package/skills/taobao-search-product-form/references/title-lexicon.json +0 -55
  30. package/skills/taobao-search-product-form/references/topics/advertiser-analysis-method.md +0 -43
  31. package/skills/taobao-search-product-form/references/topics/geography-analysis-method.md +0 -23
  32. package/skills/taobao-search-product-form/references/topics/price-band-analysis-method.md +0 -27
  33. package/skills/taobao-search-product-form/references/topics/title-root-analysis-method.md +0 -37
  34. package/skills/taobao-search-product-form/references/topics/visual-form-method.md +0 -31
  35. package/skills/taobao-search-product-form/references/topics/white-label-evidence.md +0 -38
  36. package/skills/taobao-search-product-form/references/workflow-ledger.md +0 -28
  37. package/skills/taobao-search-product-form/report_runtime/VERSION +0 -1
  38. package/skills/taobao-search-product-form/report_runtime/renderer.py +0 -485
  39. package/skills/taobao-search-product-form/report_runtime/templates/workbench.html +0 -67
  40. package/skills/taobao-search-product-form/report_runtime/validator.py +0 -117
  41. package/skills/taobao-search-product-form/scripts/analyze.py +0 -119
  42. package/skills/taobao-search-product-form/scripts/analyze_advertisers.py +0 -352
  43. package/skills/taobao-search-product-form/scripts/analyze_geography.py +0 -316
  44. package/skills/taobao-search-product-form/scripts/analyze_price_bands.py +0 -257
  45. package/skills/taobao-search-product-form/scripts/analyze_title_roots.py +0 -327
  46. package/skills/taobao-search-product-form/scripts/assemble_opportunity_evidence.py +0 -322
  47. package/skills/taobao-search-product-form/scripts/build_report_viewmodel.py +0 -822
  48. package/skills/taobao-search-product-form/scripts/build_visual_review_viewmodel.py +0 -177
  49. package/skills/taobao-search-product-form/scripts/clean_search_export.mjs +0 -61
  50. package/skills/taobao-search-product-form/scripts/cluster_visual_forms.py +0 -193
  51. package/skills/taobao-search-product-form/scripts/compile_style_prototypes.py +0 -115
  52. package/skills/taobao-search-product-form/scripts/compile_visual_review.py +0 -60
  53. package/skills/taobao-search-product-form/scripts/compile_visual_selection.py +0 -148
  54. package/skills/taobao-search-product-form/scripts/prepare.py +0 -180
  55. package/skills/taobao-search-product-form/scripts/render_report.py +0 -31
  56. package/skills/taobao-search-product-form/scripts/validate_opportunity_cards.py +0 -84
  57. package/skills/taobao-search-product-form/scripts/validate_report_viewmodel.py +0 -152
  58. package/skills/taobao-search-product-form/scripts/visual_form_pipeline.py +0 -247
  59. package/skills/taobao-search-product-form/scripts/visual_observation_batches.py +0 -117
  60. package/skills/taobao-search-product-form/scripts/workflow_ledger.py +0 -177
  61. package/skills/yuce-category-opportunity-report/SKILL.md +0 -151
  62. package/skills/yuce-category-opportunity-report/adapters/codex.json +0 -113
  63. package/skills/yuce-category-opportunity-report/adapters/sealseek.json +0 -39
  64. package/skills/yuce-category-opportunity-report/agents/openai.yaml +0 -3
  65. package/skills/yuce-category-opportunity-report/assets/chart-views.js +0 -80
  66. package/skills/yuce-category-opportunity-report/assets/package.json +0 -1
  67. package/skills/yuce-category-opportunity-report/assets/report-template.html +0 -62
  68. package/skills/yuce-category-opportunity-report/capabilities.json +0 -48
  69. package/skills/yuce-category-opportunity-report/references/SCHEMA.md +0 -5
  70. package/skills/yuce-category-opportunity-report/references/data-contract.md +0 -40
  71. package/skills/yuce-category-opportunity-report/references/index.md +0 -6
  72. package/skills/yuce-category-opportunity-report/references/log.md +0 -29
  73. package/skills/yuce-category-opportunity-report/references/queries/assess-category-opportunity.md +0 -7
  74. package/skills/yuce-category-opportunity-report/references/source-manifest.md +0 -14
  75. package/skills/yuce-category-opportunity-report/references/topics/category-opportunity-method.md +0 -39
  76. package/skills/yuce-category-opportunity-report/scripts/build_report.py +0 -779
  77. package/skills/yuce-category-opportunity-report/scripts/market_routes.py +0 -139
@@ -1,352 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Profile advertisers in a cleaned two-sheet Taobao search workbook."""
3
-
4
- import argparse
5
- import bisect
6
- import hashlib
7
- import json
8
- from collections import Counter
9
- from pathlib import Path
10
-
11
- import pandas as pd
12
-
13
-
14
- REQUIRED = {"商品ID", "店铺名称", "占位类型"}
15
- SYSTEM_DISPLAY_SHOP_LABELS = {"百亿补贴品牌优选"}
16
-
17
-
18
- def text(value):
19
- return "" if pd.isna(value) else str(value).strip()
20
-
21
-
22
- def position(value):
23
- if pd.isna(value):
24
- return None
25
- number = float(value)
26
- return int(number) if number.is_integer() else number
27
-
28
-
29
- def number(value):
30
- if pd.isna(value):
31
- return None
32
- return float(value)
33
-
34
-
35
- def total_or_none(values):
36
- numeric = pd.to_numeric(pd.Series(values), errors="coerce")
37
- return int(numeric.sum()) if numeric.notna().any() else None
38
-
39
-
40
- def median_or_none(values):
41
- numeric = pd.to_numeric(pd.Series(values), errors="coerce")
42
- return float(numeric.median()) if numeric.notna().any() else None
43
-
44
-
45
- def price_band(value, edges):
46
- return None if value is None else bisect.bisect_right(edges, value)
47
-
48
-
49
- def price_band_labels(edges):
50
- fmt = lambda value: f"¥{value:g}"
51
- return [f"<{fmt(edges[0])}"] + [f"{fmt(a)}–<{fmt(b)}" for a, b in zip(edges, edges[1:])] + [f"≥{fmt(edges[-1])}"]
52
-
53
-
54
- def load_sheet(source, sheet_name, placement):
55
- frame = pd.read_excel(source, sheet_name=sheet_name, dtype={"商品ID": str})
56
- missing = REQUIRED - set(frame.columns)
57
- if missing:
58
- raise ValueError(f"{sheet_name}: missing columns {sorted(missing)}")
59
- frame["商品ID"] = frame["商品ID"].map(text)
60
- frame["店铺名称"] = frame["店铺名称"].map(text)
61
- if (frame["商品ID"] == "").any() or (frame["店铺名称"] == "").any():
62
- raise ValueError(f"{sheet_name}: blank product ID or shop name")
63
- if not frame["占位类型"].map(text).eq(placement).all():
64
- raise ValueError(f"{sheet_name}: unexpected placement label")
65
- return frame
66
-
67
-
68
- def resolve_advertiser_shops(ads):
69
- """Keep source display labels while resolving corroborated platform aliases."""
70
- ads = ads.copy()
71
- ads["_resolved_shop_name"] = ads["店铺名称"]
72
- ads["_shop_resolution"] = "exact_display_name"
73
- aliases = []
74
- for product_id, group in ads.groupby("商品ID", sort=False):
75
- names = set(group["店铺名称"])
76
- aliases_for_product = names & SYSTEM_DISPLAY_SHOP_LABELS
77
- if not aliases_for_product:
78
- if len(names) != 1:
79
- raise ValueError(f"广告位 product {product_id} has unresolved multiple shop names: {sorted(names)}")
80
- continue
81
- actual_names = names - SYSTEM_DISPLAY_SHOP_LABELS
82
- sellers = {text(value) for value in group.get("掌柜名", pd.Series(dtype=str)) if text(value)}
83
- if len(actual_names) != 1 or sellers != actual_names:
84
- raise ValueError(
85
- f"广告位 product {product_id} has an unresolved display shop label: "
86
- f"display names={sorted(names)}, seller names={sorted(sellers)}"
87
- )
88
- actual_shop = next(iter(actual_names))
89
- alias_mask = (ads["商品ID"] == product_id) & ads["店铺名称"].isin(aliases_for_product)
90
- ads.loc[alias_mask, "_resolved_shop_name"] = actual_shop
91
- ads.loc[alias_mask, "_shop_resolution"] = "system_display_label_corroborated_by_seller_and_same_id_shop"
92
- aliases.append({
93
- "product_id": product_id,
94
- "resolved_shop_name": actual_shop,
95
- "display_shop_names": sorted(names),
96
- "alias_appearance_count": int(alias_mask.sum()),
97
- "basis": "same product ID has an actual shop display name matching every seller name",
98
- })
99
- return ads, aliases
100
-
101
-
102
- def analyze(source, supplied_price_bands=None):
103
- natural = load_sheet(source, "自然位", "自然位")
104
- ads = load_sheet(source, "广告位", "广告位")
105
- if natural["商品ID"].duplicated().any():
106
- raise ValueError("自然位 must contain one first-seen row per product ID")
107
- if ads.empty:
108
- raise ValueError("广告位 is empty; no advertiser profile can be calculated")
109
- if "现价" not in ads or "现价" not in natural or "付款人数" not in natural:
110
- raise ValueError("price comparison needs 现价 in both sheets and 付款人数 in 自然位")
111
-
112
- natural_by_id = natural.set_index("商品ID")
113
- natural_ids = set(natural["商品ID"])
114
- natural_shop_counts = natural.groupby("店铺名称")["商品ID"].nunique().to_dict()
115
- ads, alias_resolutions = resolve_advertiser_shops(ads)
116
- ads["_resolved_shop_type"] = ads.get("店铺类型", pd.Series([None] * len(ads)))
117
- for entry in alias_resolutions:
118
- actual_rows = ads[(ads["商品ID"] == entry["product_id"]) & (ads["店铺名称"] == entry["resolved_shop_name"])]
119
- actual_types = [text(value) for value in actual_rows.get("店铺类型", []) if text(value)]
120
- if actual_types:
121
- alias_mask = (ads["商品ID"] == entry["product_id"]) & (ads["店铺名称"] != entry["resolved_shop_name"])
122
- ads.loc[alias_mask, "_resolved_shop_type"] = actual_types[0]
123
- ad_product_counts = ads["商品ID"].value_counts()
124
- ad_unique = ads.drop_duplicates("商品ID", keep="first")
125
- ad_unique_prices = pd.to_numeric(ad_unique["现价"], errors="coerce")
126
- if supplied_price_bands is None:
127
- edges = sorted(set(round(float(value), 2) for value in ad_unique_prices.quantile([0.25, 0.5, 0.75]).dropna()))
128
- band_source = "advertised-product price quartiles"
129
- else:
130
- edges = supplied_price_bands
131
- band_source = "run-supplied boundaries after inspecting the source prices"
132
- if not edges or any(edge <= 0 for edge in edges) or edges != sorted(set(edges)):
133
- raise ValueError("price band boundaries must be positive, unique and ascending")
134
- labels = price_band_labels(edges)
135
-
136
- products = []
137
- for product_id, group in ads.groupby("商品ID", sort=False):
138
- shop_names = group["_resolved_shop_name"].unique().tolist()
139
- if len(shop_names) != 1:
140
- raise ValueError(f"广告位 product {product_id} has unresolved multiple shop names: {shop_names}")
141
- first = group.iloc[0]
142
- actual_rows = group[group["店铺名称"] == shop_names[0]]
143
- shop_type_row = actual_rows.iloc[0] if not actual_rows.empty else first
144
- natural_row = natural_by_id.loc[product_id] if product_id in natural_ids else None
145
- natural_shop = text(natural_row["店铺名称"]) if natural_row is not None else None
146
- ad_prices = sorted(set(float(value) for value in pd.to_numeric(group["现价"], errors="coerce").dropna()))
147
- first_ad_price = number(pd.to_numeric(first["现价"], errors="coerce"))
148
- products.append({
149
- "product_id": product_id,
150
- "title": text(first.get("商品名称")),
151
- "shop_name": shop_names[0],
152
- "shop_type": text(shop_type_row.get("店铺类型")) or None,
153
- "display_shop_names_in_source": list(dict.fromkeys(group["店铺名称"].tolist())),
154
- "system_display_alias_appearances": int(group["_shop_resolution"].ne("exact_display_name").sum()),
155
- "ad_appearances": len(group),
156
- "ad_positions": [position(value) for value in group.get("序号", pd.Series([None] * len(group)))],
157
- "ad_price_first_appearance": first_ad_price,
158
- "ad_price_band": labels[price_band(first_ad_price, edges)] if first_ad_price is not None else None,
159
- "ad_price_values_in_source": ad_prices,
160
- "ad_price_varies_across_appearances": len(ad_prices) > 1,
161
- "in_natural": natural_row is not None,
162
- "natural_position": position(natural_row.get("序号")) if natural_row is not None else None,
163
- "natural_shop_name": natural_shop,
164
- "natural_price": number(pd.to_numeric(natural_row.get("现价"), errors="coerce")) if natural_row is not None else None,
165
- "natural_payment_people": number(pd.to_numeric(natural_row.get("付款人数"), errors="coerce")) if natural_row is not None else None,
166
- "cross_placement_shop_name_differs": natural_shop is not None and natural_shop != shop_names[0],
167
- })
168
- product_by_id = {row["product_id"]: row for row in products}
169
-
170
- shops = []
171
- for shop_name, group in ads.groupby("_resolved_shop_name", sort=False):
172
- product_ids = list(dict.fromkeys(group["商品ID"].tolist()))
173
- shop_types = sorted({text(value) for value in group["_resolved_shop_type"] if text(value)})
174
- overlap_count = sum(product_by_id[pid]["in_natural"] for pid in product_ids)
175
- shop_natural = natural[natural["店铺名称"] == shop_name]
176
- natural_advertised = shop_natural[shop_natural["商品ID"].isin(product_ids)]
177
- natural_unadvertised = shop_natural[~shop_natural["商品ID"].isin(product_ids)]
178
- matching_ad_natural = natural[natural["商品ID"].isin(product_ids)]
179
- top_natural = shop_natural.sort_values("付款人数", ascending=False).iloc[0] if not shop_natural.empty else None
180
- first_ad_prices = [product_by_id[pid]["ad_price_first_appearance"] for pid in product_ids]
181
- shops.append({
182
- "shop_name": shop_name,
183
- "shop_types": shop_types,
184
- "ad_product_count": len(product_ids),
185
- "ad_appearance_count": len(group),
186
- "ad_products_in_natural": overlap_count,
187
- "ad_only_products": len(product_ids) - overlap_count,
188
- "natural_product_count_by_exact_shop_name": natural_shop_counts.get(shop_name, 0),
189
- "shop_name_present_in_natural": shop_name in natural_shop_counts,
190
- "ad_product_ids": product_ids,
191
- "ad_price_median_by_unique_product": median_or_none(first_ad_prices),
192
- "natural_shop_payment_people_sum": total_or_none(shop_natural["付款人数"]),
193
- "natural_shop_payment_people_median_per_product": median_or_none(shop_natural["付款人数"]),
194
- "natural_advertised_product_count_by_same_shop_name": len(natural_advertised),
195
- "natural_advertised_product_payment_people_sum_by_same_shop_name": total_or_none(natural_advertised["付款人数"]),
196
- "natural_unadvertised_product_count_by_same_shop_name": len(natural_unadvertised),
197
- "natural_unadvertised_product_payment_people_sum_by_same_shop_name": total_or_none(natural_unadvertised["付款人数"]),
198
- "matching_ad_product_payment_people_sum_by_id": total_or_none(matching_ad_natural["付款人数"]),
199
- "highest_payment_natural_product": None if top_natural is None else {
200
- "product_id": text(top_natural["商品ID"]),
201
- "title": text(top_natural.get("商品名称")),
202
- "payment_people": number(top_natural["付款人数"]),
203
- "advertised_by_this_shop": text(top_natural["商品ID"]) in product_ids,
204
- },
205
- "natural_unadvertised_product_ids_by_same_shop_name": natural_unadvertised["商品ID"].tolist(),
206
- })
207
- product_ranks = {row["shop_name"]: rank for rank, row in enumerate(sorted(shops, key=lambda row: (-row["ad_product_count"], -row["ad_appearance_count"], row["shop_name"])), start=1)}
208
- shops.sort(key=lambda row: (-row["ad_appearance_count"], -row["ad_product_count"], row["shop_name"]))
209
- cumulative_products = cumulative_appearances = 0
210
- for rank, row in enumerate(shops, start=1):
211
- cumulative_products += row["ad_product_count"]
212
- cumulative_appearances += row["ad_appearance_count"]
213
- row["rank_by_ad_appearances"] = rank
214
- row["rank_by_ad_products"] = product_ranks[row["shop_name"]]
215
- row["ad_product_share"] = round(row["ad_product_count"] / len(products), 6)
216
- row["ad_appearance_share"] = round(row["ad_appearance_count"] / len(ads), 6)
217
- row["cumulative_ad_product_share_in_appearance_rank"] = round(cumulative_products / len(products), 6)
218
- row["cumulative_ad_appearance_share"] = round(cumulative_appearances / len(ads), 6)
219
-
220
- def type_breakdown(frame, grain):
221
- if "店铺类型" not in frame.columns:
222
- return {}
223
- result = {}
224
- for shop_type, group in frame.groupby(frame["店铺类型"].map(lambda x: text(x) or "未标注")):
225
- result[shop_type] = {
226
- "shop_count": group["_resolved_shop_name"].nunique() if grain == "ads" else group["店铺名称"].nunique(),
227
- "product_count": group["商品ID"].nunique(),
228
- }
229
- if grain == "ads":
230
- result[shop_type]["appearance_count"] = len(group)
231
- return result
232
-
233
- overlap_products = sum(row["in_natural"] for row in products)
234
- overlap_shops = sum(row["shop_name_present_in_natural"] for row in shops)
235
- repeat_distribution = dict(sorted(Counter(ad_product_counts.tolist()).items()))
236
- advertiser_shop_names = {row["shop_name"] for row in shops}
237
- natural_layout = [
238
- {
239
- "product_id": text(row["商品ID"]),
240
- "shop_name": text(row["店铺名称"]),
241
- "title": text(row.get("商品名称")),
242
- "natural_price": number(pd.to_numeric(row["现价"], errors="coerce")),
243
- "natural_payment_people": number(pd.to_numeric(row["付款人数"], errors="coerce")),
244
- "advertised_by_same_shop_name": text(row["商品ID"]) in product_by_id and product_by_id[text(row["商品ID"])]["shop_name"] == text(row["店铺名称"]),
245
- "advertised_by_any_shop_name": text(row["商品ID"]) in product_by_id,
246
- }
247
- for _, row in natural[natural["店铺名称"].isin(advertiser_shop_names)].iterrows()
248
- ]
249
-
250
- def price_distribution(rows, key, shop_key=None):
251
- result = []
252
- valid = [row for row in rows if row[key] is not None]
253
- for index, label in enumerate(labels):
254
- selected = [row for row in valid if price_band(row[key], edges) == index]
255
- entry = {"band": label, "product_count": len(selected), "share_of_priced_products": round(len(selected) / len(valid), 6) if valid else None}
256
- if shop_key:
257
- entry["shop_count"] = len({row[shop_key] for row in selected})
258
- result.append(entry)
259
- return {"priced_product_count": len(valid), "missing_price_count": len(rows) - len(valid), "bands": result}
260
-
261
- ad_price_distribution = price_distribution(products, "ad_price_first_appearance", "shop_name")
262
- natural_price_rows = [{"price": number(pd.to_numeric(row["现价"], errors="coerce"))} for _, row in natural.iterrows()]
263
- natural_price_distribution = price_distribution(natural_price_rows, "price")
264
- non_advertised_layout = [row for row in natural_layout if not row["advertised_by_same_shop_name"]]
265
- natural_non_ad_price_distribution = price_distribution(non_advertised_layout, "natural_price", "shop_name")
266
- counts = {
267
- "natural_products": len(natural),
268
- "natural_shops_by_exact_name": natural["店铺名称"].nunique(),
269
- "ad_appearances": len(ads),
270
- "ad_products": len(products),
271
- "advertiser_shops_by_exact_name": len(shops),
272
- "ad_appearances_with_resolved_system_display_label": sum(row["alias_appearance_count"] for row in alias_resolutions),
273
- "ad_products_with_resolved_system_display_label": len(alias_resolutions),
274
- "ad_products_in_natural": overlap_products,
275
- "ad_only_products": len(products) - overlap_products,
276
- "advertiser_shops_in_natural_by_exact_name": overlap_shops,
277
- "advertiser_shops_not_in_natural_by_exact_name": len(shops) - overlap_shops,
278
- "ad_products_repeated_in_ad_slots": sum(count > 1 for count in ad_product_counts),
279
- "ad_products_with_cross_placement_shop_name_difference": sum(row["cross_placement_shop_name_differs"] for row in products),
280
- "advertiser_shops_with_one_ad_product": sum(row["ad_product_count"] == 1 for row in shops),
281
- "natural_products_in_advertiser_shops_by_exact_name": len(natural_layout),
282
- "natural_products_not_advertised_by_same_shop_name": len(non_advertised_layout),
283
- "advertised_products_with_ad_price_variation": sum(row["ad_price_varies_across_appearances"] for row in products),
284
- "ad_rows_with_zero_displayed_payment_people": int(pd.to_numeric(ads["付款人数"], errors="coerce").eq(0).sum()) if "付款人数" in ads else None,
285
- }
286
- if sum(row["ad_product_count"] for row in shops) != len(products):
287
- raise ValueError("QA failed: shop-level product counts do not reconcile")
288
- if sum(row["ad_appearance_count"] for row in shops) != len(ads):
289
- raise ValueError("QA failed: shop-level appearance counts do not reconcile")
290
-
291
- return {
292
- "schema_version": "1.1",
293
- "source": {"file_name": source.name, "sha256": hashlib.sha256(source.read_bytes()).hexdigest(), "sheets": ["自然位", "广告位"]},
294
- "definitions": {
295
- "ad_appearance": "One row in the advertising-position sheet; not an impression, click, spend, or campaign count.",
296
- "ad_product": "One unique product ID in advertising positions, regardless of repeated appearances.",
297
- "advertiser_shop": "One resolved shop name in advertising positions; a known platform display label is assigned to the same-ID actual shop only when every seller name corroborates it. Matching names are not proof of a shared legal entity or brand.",
298
- "product_natural_overlap": "The same product ID appears in the natural-position sheet.",
299
- "shop_natural_overlap": "The same exact shop name appears in the natural-position sheet.",
300
- "natural_payment_people": "The source field 付款人数 on first-seen natural-position product rows; its time window is unspecified. A shop-level sum adds displayed product values and is not storewide sales or unique shop buyers.",
301
- "natural_shop_layout": "Natural-position products under the same exact shop name, split by whether that shop advertises the same product ID.",
302
- "ad_price": "The first advertising appearance's 现价 for each product ID; all observed ad prices and any variation remain in the product detail.",
303
- },
304
- "counts": counts,
305
- "ad_appearances_per_product_distribution": {str(k): v for k, v in repeat_distribution.items()},
306
- "shop_type_breakdown": {"advertising": type_breakdown(ads.assign(店铺类型=ads["_resolved_shop_type"]), "ads"), "natural": type_breakdown(natural, "natural")},
307
- "system_display_label_resolutions": alias_resolutions,
308
- "price_bands": {
309
- "boundaries_yuan": edges,
310
- "boundary_source": band_source,
311
- "interval_rule": "left-inclusive, right-exclusive",
312
- "advertised_unique_products": ad_price_distribution,
313
- "all_natural_unique_products": natural_price_distribution,
314
- "natural_products_in_advertiser_shops_not_advertised_by_same_shop": natural_non_ad_price_distribution,
315
- },
316
- "advertiser_shops": shops,
317
- "ad_products": products,
318
- "natural_products_of_advertiser_shops": natural_layout,
319
- "ad_appearance_records": [
320
- {"product_id": text(row["商品ID"]), "shop_name": text(row["_resolved_shop_name"]), "display_shop_name": text(row["店铺名称"]), "shop_resolution": text(row["_shop_resolution"]), "position": position(row.get("序号"))}
321
- for _, row in ads.iterrows()
322
- ],
323
- "quality": {"shop_identity_key": "resolved_shop_name_with_source_display_preserved", "product_identity_key": "product_id", "reconciled_shop_product_count": True, "reconciled_shop_appearance_count": True, "natural_payment_field": "付款人数", "ad_payment_field_used_for_sales_conclusion": False},
324
- }
325
-
326
-
327
- def main():
328
- parser = argparse.ArgumentParser(description=__doc__)
329
- parser.add_argument("--input", required=True, help="Cleaned XLSX with 自然位 and 广告位 sheets")
330
- parser.add_argument("--output", required=True, help="New JSON file; existing files are refused")
331
- parser.add_argument("--price-bands", help="Optional ascending comma-separated price boundaries in yuan; defaults to advertised-product quartiles")
332
- args = parser.parse_args()
333
- source = Path(args.input).expanduser().resolve()
334
- output = Path(args.output).expanduser().resolve()
335
- if not source.is_file() or source.suffix.lower() != ".xlsx":
336
- parser.error("input must be an existing XLSX file")
337
- if output.exists():
338
- parser.error("output already exists")
339
- try:
340
- price_bands = [float(value.strip()) for value in args.price_bands.split(",")] if args.price_bands else None
341
- except ValueError as error:
342
- parser.error(f"invalid --price-bands: {error}")
343
- result = analyze(source, price_bands)
344
- output.parent.mkdir(parents=True, exist_ok=True)
345
- with output.open("x", encoding="utf-8") as handle:
346
- json.dump(result, handle, ensure_ascii=False, indent=2)
347
- handle.write("\n")
348
- print(json.dumps({"output": str(output), "counts": result["counts"]}, ensure_ascii=False))
349
-
350
-
351
- if __name__ == "__main__":
352
- main()