← back to Hollywood Price 2026
split_analysis.py
134 lines
#!/usr/bin/env python3
"""Measure the real split for the 1,577 Needs-Price Hollywood products via the
CORRECT join: Shopify sku-base -> col3 (Momentum number) -> LIVE Meilisearch price.
Buckets: LIVE-PRICED (current Momentum price) / GONE (col3 known, no live price) /
NO-COL3 (sku-base not in the momentum col3 map). Writes plan_correct.csv. $0."""
import sys
import os
import json
import csv
import urllib.request
import collections
sys.path.insert(0, "lib")
from shopify import gql
HOST = "https://ms-e886719d86e7-4256.sfo.meilisearch.io"; INDEX = "redesign-colors"
KEY = "95fe8376edc78d49e787db49edda68426b21649f6271f49e2f1412118612fbd6"
HI = os.path.expanduser("~/Projects/hollywood-import")
MARKUP = 1.448
def meili_index():
"""Page the whole index -> {number: {price, pattern, color, closeout, new}}."""
out = {}; off = 0
while True:
body = json.dumps({"q": "", "limit": 1000, "offset": off,
"attributesToRetrieve": ["number", "pattern_name", "preferred_color_name", "price", "new", "closeout"]}).encode()
req = urllib.request.Request(f"{HOST}/indexes/{INDEX}/search", data=body, method="POST",
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
hits = json.load(urllib.request.urlopen(req, timeout=40)).get("hits", [])
if not hits:
break
for h in hits:
num = h.get("number")
if num:
out[str(num)] = {"price": h.get("price"), "pattern": h.get("pattern_name"),
"color": h.get("preferred_color_name"), "closeout": h.get("closeout"), "new": h.get("new")}
off += len(hits)
if len(hits) < 1000 or off >= 20000:
break
return out
def col3_map():
"""mfr (Shopify sku-base) -> col3 (Momentum number), from staged + absent."""
m = {}
for fn in ["mom-col3-staged.json", "mom-col3-absent.json"]:
p = os.path.join(HI, fn)
if not os.path.exists(p):
continue
for r in json.load(open(p)):
mfr = r.get("mfr"); col3 = r.get("col3")
if mfr and col3:
m[str(mfr).strip()] = str(col3).strip()
return m
def shopify_needsprice():
Q = """query($q:String!,$after:String){ products(first:200, after:$after, query:$q){
pageInfo{hasNextPage endCursor}
edges{ node{ id legacyResourceId handle title
dwsku: metafield(namespace:"custom", key:"dw_sku"){ value }
variants(first:5){ edges{ node{ sku } } } } } } }"""
out, cur = [], None
while True:
d = gql(Q, {"q": "vendor:'Hollywood Wallcoverings' status:active tag:Needs-Price", "after": cur})
p = d["products"]; out += [e["node"] for e in p["edges"]]
if not p["pageInfo"]["hasNextPage"]:
break
cur = p["pageInfo"]["endCursor"]
return out
def sku_base(n):
for e in n["variants"]["edges"]:
sk = (e["node"]["sku"] or "")
if sk.lower().endswith("-sample"):
return sk[:-len("-sample")]
return None
def main():
print("pulling live Momentum Meilisearch index...")
meili = meili_index()
print(f" meili index: {len(meili)} priced/known numbers")
c3 = col3_map()
print(f" col3 map (sku-base->number): {len(c3)}")
prods = shopify_needsprice()
print(f" shopify Needs-Price Hollywood: {len(prods)}")
buckets = collections.Counter()
by_prefix = collections.defaultdict(lambda: collections.Counter())
priceable = []
for n in prods:
base = sku_base(n)
dw = (n.get("dwsku") or {}).get("value") or ""
pref = dw.split("-")[0] if dw else "NO-DW"
if not base:
buckets["no-sku"] += 1; by_prefix[pref]["no-sku"] += 1; continue
col3 = c3.get(base)
if not col3:
buckets["no-col3 (not in momentum map)"] += 1; by_prefix[pref]["no-col3"] += 1; continue
rec = meili.get(col3)
if rec and rec.get("price") not in (None, 0):
buckets["LIVE-PRICED"] += 1; by_prefix[pref]["live"] += 1
lp = float(rec["price"])
priceable.append({"product_id": n["legacyResourceId"], "gid": n["id"], "handle": n["handle"],
"title": n["title"], "dw_sku": dw, "sku_base": base, "col3": col3,
"mom_pattern": rec.get("pattern"), "mom_color": rec.get("color"),
"list_price": lp, "hw_retail": round(lp * MARKUP, 2),
"closeout": rec.get("closeout"), "new": rec.get("new")})
else:
buckets["GONE (col3 known, no live price)"] += 1; by_prefix[pref]["gone"] += 1
if priceable:
with open("plan_correct.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(priceable[0].keys())); w.writeheader(); w.writerows(priceable)
print("\n===== SPLIT (of %d) =====" % len(prods))
for k, v in buckets.most_common():
print(f" {k:38}: {v}")
print("\n by dw_sku prefix (live / gone / no-col3 / no-sku):")
for pref, c in sorted(by_prefix.items(), key=lambda x: -sum(x[1].values())):
print(f" {pref:8}: live={c['live']:4} gone={c['gone']:4} no-col3={c['no-col3']:4} no-sku={c['no-sku']:3}")
if priceable:
hws = sorted(p["hw_retail"] for p in priceable)
print(f"\n LIVE-PRICED -> plan_correct.csv ({len(priceable)}); hw_retail ${hws[0]:.2f}-${hws[-1]:.2f} median ${hws[len(hws)//2]:.2f}")
print(" samples:")
for p in priceable[:6]:
print(f" {p['sku_base']:12} {p['mom_pattern']!r:16}/{p['mom_color']!r:14} list ${p['list_price']} -> hw ${p['hw_retail']}")
if __name__ == "__main__":
main()