← 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()