-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmatching.py
More file actions
152 lines (123 loc) · 5.96 KB
/
Copy pathmatching.py
File metadata and controls
152 lines (123 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
Fit prediction engine — v1
CONCEPT: A user owns items from brands they've already worn. For each, they
rate how it fits (tight / true / loose) per body region. From that, we back
out an ESTIMATE of their actual body measurement (garment measurement minus
an "ease" allowance implied by the fit rating). Averaging across their owned
items gives a body-measurement estimate that's brand-independent.
To predict a size in a NEW brand, we don't just find the size closest to
their raw body estimate — we also account for how much ease they personally
prefer (some people like tees snug, some baggy). We find the size in the
new brand whose (garment measurement - preferred ease) is closest to their
estimated body measurement.
WHY THIS COUNTS AS THE 'AI' PART (not just a database lookup):
A static size-chart lookup only works if the user already knows their exact
body measurements — most people don't. This engine instead learns a body
estimate FROM SUBJECTIVE FIT FEEDBACK across brands with different cuts,
which is a pattern-inference problem, not a direct lookup.
v2 UPGRADE PATH (documented for the case study, not built yet):
Once there's fit data from many users, this heuristic (fixed ease-per-rating)
should be replaced with a learned model — e.g. an embedding per brand/size
learned from aggregate fit outcomes, so ease assumptions aren't hand-picked
but inferred from real data. That's the difference between a v1 heuristic
and a real ML system, and it's worth being explicit about which one this is.
"""
import json
from pathlib import Path
DATA_PATH = Path(__file__).parent / "data" / "size_charts.json"
# Hand-picked ease assumptions (cm) per fit rating, per region.
# This is the part a v2 model would learn from real data instead.
EASE_CM = {
"tight": {"chest": 2, "shoulder": 0.5, "length": -1},
"true": {"chest": 6, "shoulder": 1.5, "length": 1},
"loose": {"chest": 10, "shoulder": 2.5, "length": 3},
}
REGION_WEIGHTS = {"chest": 0.5, "shoulder": 0.35, "length": 0.15}
def load_size_charts():
with open(DATA_PATH) as f:
return json.load(f)
def estimate_body_measurements(owned_items, charts):
"""
owned_items: list of dicts like
{"brand": "H&M", "size": "M", "fit": "true"}
Returns estimated body measurements dict, e.g. {"chest": 85.3, ...}
and the user's average preferred ease per region (for re-applying later).
"""
region_estimates = {"chest": [], "shoulder": [], "length": []}
ease_used = {"chest": [], "shoulder": [], "length": []}
for item in owned_items:
brand_chart = charts["brands"].get(item["brand"])
if not brand_chart:
continue
size_data = brand_chart["sizes"].get(item["size"])
if not size_data:
continue
fit_rating = item.get("fit", "true")
ease = EASE_CM.get(fit_rating, EASE_CM["true"])
for region in ("chest", "shoulder", "length"):
garment_measure = size_data[region]
body_estimate = garment_measure - ease[region]
region_estimates[region].append(body_estimate)
ease_used[region].append(ease[region])
def avg(lst):
return sum(lst) / len(lst) if lst else None
body = {r: avg(v) for r, v in region_estimates.items()}
preferred_ease = {r: avg(v) for r, v in ease_used.items()}
return body, preferred_ease
# Below this many owned items, the body estimate is too thin to trust,
# regardless of how tight the score gap looks. Matches the "add 3-5 owned
# items first" prompt in user-flows.md, so the code and the UX copy agree
# on what "enough" means.
MIN_ITEMS_FOR_FULL_CONFIDENCE = 3
# Confidence downgrades by one level when data is thin (see below).
CONFIDENCE_LEVELS = ["high", "medium", "low"]
def predict_size(target_brand, body_estimate, preferred_ease, charts, item_count):
"""
item_count: number of owned items that went into body_estimate/preferred_ease.
Used to keep a lucky score match on thin data from reading as "high
confidence" — see failure-modes.md ("Overconfident prediction from thin data").
Returns (predicted_size, confidence_note, scored_sizes)
"""
brand_chart = charts["brands"].get(target_brand)
if not brand_chart:
return None, f"No size chart on file for {target_brand}.", []
scored = []
for size, measures in brand_chart["sizes"].items():
error = 0
regions_used = 0
for region, weight in REGION_WEIGHTS.items():
if body_estimate.get(region) is None:
continue
target = body_estimate[region] + preferred_ease.get(region, 0)
diff = abs(measures[region] - target)
error += weight * diff
regions_used += 1
if regions_used > 0:
scored.append((size, error))
scored.sort(key=lambda x: x[1])
if not scored:
return None, "Not enough data to predict.", []
best_size, best_error = scored[0]
# Step 1: confidence from score gap alone, same as before.
if best_error < 1.5:
level = "high"
elif best_error < 3.5:
level = "medium"
else:
level = "low"
# Step 2: downgrade one level if the underlying data is thin. A great
# score match built on one owned item is still a guess dressed up as
# a good score — this keeps that from reading as "high confidence".
thin_data = item_count < MIN_ITEMS_FOR_FULL_CONFIDENCE
if thin_data and level != "low":
level = CONFIDENCE_LEVELS[CONFIDENCE_LEVELS.index(level) + 1]
# Build the message last, once we know the final level and *why* we
# landed there — thin data and a genuinely bad score match are different
# problems and deserve different explanations.
if level == "low" and thin_data:
confidence = f"low — only {item_count} owned item(s) logged, not enough to be confident yet"
elif level == "low":
confidence = "low — fit history is thin or this brand's cut is unusual for you"
else:
confidence = level
return best_size, confidence, scored