"""Project 109: likes by feature for #gamedev posts, compared WITHIN follower bands. Reads posts.json, writes counts.json. Main metric: median likes, plus share of posts with 5+ likes. Also an author-balanced check (each author counts once: their median), so a few accounts posting 20x a day can't drive the result.""" import json, re, statistics as st from collections import Counter, defaultdict from datetime import datetime import sys METRIC = sys.argv[1] if len(sys.argv) > 1 else "others" # "others" = likes minus the 7 mass-likers (masslikes.py) D = json.load(open("posts.json", encoding="utf-8")); P = D["posts"] BANDS = ["<100", "100-999", "1k-9.9k", "10k+"] def band(f): return "<100" if f < 100 else "100-999" if f < 1000 else "1k-9.9k" if f < 10000 else "10k+" def ntags(p): return len(p["tags"]) def tagband(n): return "1-2 tags" if n <= 2 else "3-5 tags" if n <= 5 else "6+ tags" def hour(p): return datetime.fromisoformat(p["time"].replace("Z", "+00:00")).hour def hblock(p): h = hour(p); return f"{h//6*6:02d}-{h//6*6+5:02d} UTC" def steam(p): return any("store.steampowered.com" in l or "s.team" in l for l in p["links"]) def itch(p): return any("itch.io" in l for l in p["links"]) def lang(p): return (p["langs"] or ["?"])[0][:2] def length(p): n = len(p["text"]); return "short <100" if n < 100 else "mid 100-250" if n <= 250 else "long >250" WISH = re.compile(r"\bwishlist", re.I) AI = re.compile(r"\b(ai|genai|gen ai|ai-generated|midjourney|stable diffusion|chatgpt|llm)\b", re.I) NOAI = re.compile(r"(#?no ?ai|#noai|not ai|ai[- ]free|human[- ]made|#humanmade|handmade)", re.I) Q = re.compile(r"\?") FEATS = { "media": lambda p: p["media"], "hashtags": lambda p: tagband(ntags(p)), "time posted (UTC)": hblock, "length": length, "Steam link": lambda p: "yes" if steam(p) else "no", "itch.io link": lambda p: "yes" if itch(p) else "no", "says 'wishlist'": lambda p: "yes" if WISH.search(p["text"]) else "no", "asks a question (?)": lambda p: "yes" if Q.search(p["text"]) else "no", "says it's human-made / no AI": lambda p: "yes" if NOAI.search(p["text"]) else "no", "language": lambda p: lang(p) if lang(p) in ("en", "ja", "es", "pt", "fr", "de") else "other", "bot label": lambda p: "yes" if p["bot"] else "no", } def stats(s): if not s: return None L = [p[METRIC] for p in s] return {"n": len(s), "median": st.median(L), "p5": round(sum(x >= 5 for x in L) / len(L), 3), "zero": round(sum(x == 0 for x in L) / len(L), 3), "mean": round(st.mean(L), 1)} def author_median(s): by = defaultdict(list) for p in s: by[p["did"]].append(p[METRIC]) return {"authors": len(by), "median_of_author_medians": st.median([st.median(v) for v in by.values()]) if by else None} out = {"metric": METRIC, "window": D["window"], "n": len(P), "authors": len({p["did"] for p in P}), "base": {}, "features": {}} print(f"N={len(P)} posts, {out['authors']} authors, window {D['window']}") for b in BANDS: s = [p for p in P if band(p["followers"]) == b]; out["base"][b] = (stats(s) or {}) | author_median(s) print(b, out["base"][b]) c = Counter(p["handle"] for p in P); out["top_posters"] = c.most_common(10) print("top posters", c.most_common(10)) print("share of posts from top 10 accounts", round(sum(n for _, n in c.most_common(10)) / len(P), 3)) for name, fn in FEATS.items(): vals = sorted({fn(p) for p in P}) out["features"][name] = {} print(f"\n== {name}") for v in vals: row = {"all": stats([p for p in P if fn(p) == v]) | author_median([p for p in P if fn(p) == v])} for b in BANDS: row[b] = stats([p for p in P if fn(p) == v and band(p["followers"]) == b]) out["features"][name][v] = row cells = " ".join(f"{b}: med {row[b]['median']:g} 5+ {row[b]['p5']:.0%} (n{row[b]['n']})" if row[b] else f"{b}: -" for b in BANDS) print(f" {v:14} all n{row['all']['n']:4} med {row['all']['median']:g} authmed {row['all']['median_of_author_medians']:g} 5+ {row['all']['p5']:.0%} | {cells}") json.dump(out, open(f"counts_{METRIC}.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False)