saraNour commited on
Commit
4f85116
·
verified ·
1 Parent(s): 2e2c920

Upload notebooks/phase4_executed.ipynb with huggingface_hub

Browse files
Files changed (1) hide show
  1. notebooks/phase4_executed.ipynb +1157 -0
notebooks/phase4_executed.ipynb ADDED
@@ -0,0 +1,1157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Phase 4 — Variant Assignment / Variant Modeling\n",
8
+ "## Compliments Reference DB Pipeline\n",
9
+ "\n",
10
+ "**Version:** 1.0.0\n",
11
+ "**Date:** 2026-07-30\n",
12
+ "\n",
13
+ "**Input:** Phase 3 outputs + Phase 2 output\n",
14
+ "\n",
15
+ "**This phase does:**\n",
16
+ "1. Transforms Phase 3 product groups into structured variant models\n",
17
+ "2. Assigns deterministic variant_ids to products within each group\n",
18
+ "3. Creates variant tables and mappings\n",
19
+ "4. Validates variant consistency rules\n",
20
+ "\n",
21
+ "**CRITICAL:** This phase does NOT use nutrition data.\n",
22
+ "**CRITICAL:** This phase does NOT use any external taxonomy.\n",
23
+ "**CRITICAL:** This phase is 100% deterministic (no LLM)."
24
+ ]
25
+ },
26
+ {
27
+ "cell_type": "code",
28
+ "execution_count": 1,
29
+ "metadata": {
30
+ "execution": {
31
+ "iopub.execute_input": "2026-07-30T17:47:27.262371Z",
32
+ "iopub.status.busy": "2026-07-30T17:47:27.261832Z",
33
+ "iopub.status.idle": "2026-07-30T17:47:28.134559Z",
34
+ "shell.execute_reply": "2026-07-30T17:47:28.132783Z"
35
+ }
36
+ },
37
+ "outputs": [
38
+ {
39
+ "name": "stdout",
40
+ "output_type": "stream",
41
+ "text": [
42
+ "Python: 3.13.7 (main, Mar 3 2026, 12:19:54) [GCC 15.2.0]\n",
43
+ "pandas: 3.0.5\n",
44
+ "numpy: 2.5.1\n",
45
+ "Timestamp: 2026-07-30T17:47:28.129557+00:00\n"
46
+ ]
47
+ }
48
+ ],
49
+ "source": [
50
+ "import json, hashlib, uuid, sys\n",
51
+ "from datetime import datetime, timezone\n",
52
+ "from pathlib import Path\n",
53
+ "\n",
54
+ "import pandas as pd\n",
55
+ "import numpy as np\n",
56
+ "\n",
57
+ "print(f\"Python: {sys.version}\")\n",
58
+ "print(f\"pandas: {pd.__version__}\")\n",
59
+ "print(f\"numpy: {np.__version__}\")\n",
60
+ "print(f\"Timestamp: {datetime.now(timezone.utc).isoformat()}\")"
61
+ ]
62
+ },
63
+ {
64
+ "cell_type": "markdown",
65
+ "metadata": {},
66
+ "source": [
67
+ "## 1. Variant Definition\n",
68
+ "\n",
69
+ "A **variant** is a purchasable representation of the same reference product\n",
70
+ "that differs in attributes such as:\n",
71
+ "\n",
72
+ "- **Size**: 500 g, 1 kg, 2 kg\n",
73
+ "- **Package quantity**: 1 × 500 g, 2 × 500 g\n",
74
+ "- **Pack configuration**: single, multipack, case\n",
75
+ "\n",
76
+ "Variants within the same group share the same identity attributes:\n",
77
+ "- is_organic, is_gluten_free, is_naturally_simple, etc.\n",
78
+ "- product_line\n",
79
+ "- flavour\n",
80
+ "- formulation\n",
81
+ "- fat_level\n",
82
+ "\n",
83
+ "Variants differ only in size/package attributes."
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "markdown",
88
+ "metadata": {},
89
+ "source": [
90
+ "## 2. Load Phase 3 Outputs"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "execution_count": 2,
96
+ "metadata": {
97
+ "execution": {
98
+ "iopub.execute_input": "2026-07-30T17:47:28.285611Z",
99
+ "iopub.status.busy": "2026-07-30T17:47:28.284392Z",
100
+ "iopub.status.idle": "2026-07-30T17:47:28.445823Z",
101
+ "shell.execute_reply": "2026-07-30T17:47:28.441829Z"
102
+ }
103
+ },
104
+ "outputs": [
105
+ {
106
+ "name": "stdout",
107
+ "output_type": "stream",
108
+ "text": [
109
+ "Phase 3 mapping: 4440 rows, 14 columns\n",
110
+ "Columns: ['upc', 'external_id', 'group_id', 'group_name', 'core_title', 'original_title', 'brand', 'size', 'identity_hash', 'product_domain', 'taxonomy', 'product_line', 'source', 'source_url']\n"
111
+ ]
112
+ },
113
+ {
114
+ "data": {
115
+ "text/html": [
116
+ "<div>\n",
117
+ "<style scoped>\n",
118
+ " .dataframe tbody tr th:only-of-type {\n",
119
+ " vertical-align: middle;\n",
120
+ " }\n",
121
+ "\n",
122
+ " .dataframe tbody tr th {\n",
123
+ " vertical-align: top;\n",
124
+ " }\n",
125
+ "\n",
126
+ " .dataframe thead th {\n",
127
+ " text-align: right;\n",
128
+ " }\n",
129
+ "</style>\n",
130
+ "<table border=\"1\" class=\"dataframe\">\n",
131
+ " <thead>\n",
132
+ " <tr style=\"text-align: right;\">\n",
133
+ " <th></th>\n",
134
+ " <th>upc</th>\n",
135
+ " <th>external_id</th>\n",
136
+ " <th>group_id</th>\n",
137
+ " <th>group_name</th>\n",
138
+ " <th>core_title</th>\n",
139
+ " <th>original_title</th>\n",
140
+ " <th>brand</th>\n",
141
+ " <th>size</th>\n",
142
+ " <th>identity_hash</th>\n",
143
+ " <th>product_domain</th>\n",
144
+ " <th>taxonomy</th>\n",
145
+ " <th>product_line</th>\n",
146
+ " <th>source</th>\n",
147
+ " <th>source_url</th>\n",
148
+ " </tr>\n",
149
+ " </thead>\n",
150
+ " <tbody>\n",
151
+ " <tr>\n",
152
+ " <th>0</th>\n",
153
+ " <td>5.574252e+10</td>\n",
154
+ " <td>297608EA</td>\n",
155
+ " <td>27b951a3-bef1-5ed4-80b7-7ef5cdca910a</td>\n",
156
+ " <td>Cold And Sinus Plus</td>\n",
157
+ " <td>Cold And Sinus Plus</td>\n",
158
+ " <td>Compliments Cold And Sinus Plus 20 Count</td>\n",
159
+ " <td>Compliments</td>\n",
160
+ " <td>20 per pack</td>\n",
161
+ " <td>0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime|</td>\n",
162
+ " <td>unknown</td>\n",
163
+ " <td>GENERAL_GROCERY</td>\n",
164
+ " <td>Core</td>\n",
165
+ " <td>voila</td>\n",
166
+ " <td>https://voila.ca/products/compliments-cold-and...</td>\n",
167
+ " </tr>\n",
168
+ " <tr>\n",
169
+ " <th>1</th>\n",
170
+ " <td>5.574234e+10</td>\n",
171
+ " <td>464324EA</td>\n",
172
+ " <td>e608ba58-8f93-59f1-9446-edb91aef79bd</td>\n",
173
+ " <td>Twister T2 Light Bulbs 23W Soft White</td>\n",
174
+ " <td>Twister T2 Light Bulbs 23W Soft White</td>\n",
175
+ " <td>Compliments Twister T2 Light Bulbs 23W Soft Wh...</td>\n",
176
+ " <td>Compliments</td>\n",
177
+ " <td>2 per pack</td>\n",
178
+ " <td>0|0|0|0|0|0|0|0|0|fat_none|reduced_fat|Core|lime|</td>\n",
179
+ " <td>unknown</td>\n",
180
+ " <td>GENERAL_GROCERY</td>\n",
181
+ " <td>Core</td>\n",
182
+ " <td>voila</td>\n",
183
+ " <td>https://voila.ca/products/compliments-twister-...</td>\n",
184
+ " </tr>\n",
185
+ " <tr>\n",
186
+ " <th>2</th>\n",
187
+ " <td>9.900026e+11</td>\n",
188
+ " <td>144288CS</td>\n",
189
+ " <td>336c79d8-796d-5682-8195-793883dd97ef</td>\n",
190
+ " <td>Zero Calorie Soft Drink</td>\n",
191
+ " <td>Zero Calorie Soft Drink</td>\n",
192
+ " <td>Compliments Zero Calorie Soft Drink 12 x 355ml</td>\n",
193
+ " <td>Compliments</td>\n",
194
+ " <td>12 x 29.583ml</td>\n",
195
+ " <td>0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime|</td>\n",
196
+ " <td>food</td>\n",
197
+ " <td>BEVERAGES</td>\n",
198
+ " <td>Core</td>\n",
199
+ " <td>voila</td>\n",
200
+ " <td>https://voila.ca/products/compliments-zero-cal...</td>\n",
201
+ " </tr>\n",
202
+ " <tr>\n",
203
+ " <th>3</th>\n",
204
+ " <td>5.574257e+10</td>\n",
205
+ " <td>312204EA</td>\n",
206
+ " <td>fd1acb22-4231-5b7b-bde4-8709b31cf04c</td>\n",
207
+ " <td>Kalamata Whole Olives</td>\n",
208
+ " <td>Kalamata Whole Olives</td>\n",
209
+ " <td>Compliments Kalamata Whole Olives 375 ml</td>\n",
210
+ " <td>Compliments</td>\n",
211
+ " <td>375ml</td>\n",
212
+ " <td>0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime|w...</td>\n",
213
+ " <td>unknown</td>\n",
214
+ " <td>GENERAL_GROCERY</td>\n",
215
+ " <td>Core</td>\n",
216
+ " <td>voila</td>\n",
217
+ " <td>https://voila.ca/products/compliments-kalamata...</td>\n",
218
+ " </tr>\n",
219
+ " <tr>\n",
220
+ " <th>4</th>\n",
221
+ " <td>5.574256e+10</td>\n",
222
+ " <td>259928EA</td>\n",
223
+ " <td>4ff536c8-c957-5fe8-8faa-3f539d6048ef</td>\n",
224
+ " <td>Sliced Almonds</td>\n",
225
+ " <td>Sliced Almonds</td>\n",
226
+ " <td>Compliments Sliced Almonds 250 g</td>\n",
227
+ " <td>Compliments</td>\n",
228
+ " <td>250g</td>\n",
229
+ " <td>0|0|0|0|0|0|0|0|0|fat_none|regular|Core|almond...</td>\n",
230
+ " <td>unknown</td>\n",
231
+ " <td>GENERAL_GROCERY</td>\n",
232
+ " <td>Core</td>\n",
233
+ " <td>voila</td>\n",
234
+ " <td>https://voila.ca/products/compliments-sliced-a...</td>\n",
235
+ " </tr>\n",
236
+ " </tbody>\n",
237
+ "</table>\n",
238
+ "</div>"
239
+ ],
240
+ "text/plain": [
241
+ " upc external_id group_id \\\n",
242
+ "0 5.574252e+10 297608EA 27b951a3-bef1-5ed4-80b7-7ef5cdca910a \n",
243
+ "1 5.574234e+10 464324EA e608ba58-8f93-59f1-9446-edb91aef79bd \n",
244
+ "2 9.900026e+11 144288CS 336c79d8-796d-5682-8195-793883dd97ef \n",
245
+ "3 5.574257e+10 312204EA fd1acb22-4231-5b7b-bde4-8709b31cf04c \n",
246
+ "4 5.574256e+10 259928EA 4ff536c8-c957-5fe8-8faa-3f539d6048ef \n",
247
+ "\n",
248
+ " group_name \\\n",
249
+ "0 Cold And Sinus Plus \n",
250
+ "1 Twister T2 Light Bulbs 23W Soft White \n",
251
+ "2 Zero Calorie Soft Drink \n",
252
+ "3 Kalamata Whole Olives \n",
253
+ "4 Sliced Almonds \n",
254
+ "\n",
255
+ " core_title \\\n",
256
+ "0 Cold And Sinus Plus \n",
257
+ "1 Twister T2 Light Bulbs 23W Soft White \n",
258
+ "2 Zero Calorie Soft Drink \n",
259
+ "3 Kalamata Whole Olives \n",
260
+ "4 Sliced Almonds \n",
261
+ "\n",
262
+ " original_title brand \\\n",
263
+ "0 Compliments Cold And Sinus Plus 20 Count Compliments \n",
264
+ "1 Compliments Twister T2 Light Bulbs 23W Soft Wh... Compliments \n",
265
+ "2 Compliments Zero Calorie Soft Drink 12 x 355ml Compliments \n",
266
+ "3 Compliments Kalamata Whole Olives 375 ml Compliments \n",
267
+ "4 Compliments Sliced Almonds 250 g Compliments \n",
268
+ "\n",
269
+ " size identity_hash \\\n",
270
+ "0 20 per pack 0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime| \n",
271
+ "1 2 per pack 0|0|0|0|0|0|0|0|0|fat_none|reduced_fat|Core|lime| \n",
272
+ "2 12 x 29.583ml 0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime| \n",
273
+ "3 375ml 0|0|0|0|0|0|0|0|0|fat_none|regular|Core|lime|w... \n",
274
+ "4 250g 0|0|0|0|0|0|0|0|0|fat_none|regular|Core|almond... \n",
275
+ "\n",
276
+ " product_domain taxonomy product_line source \\\n",
277
+ "0 unknown GENERAL_GROCERY Core voila \n",
278
+ "1 unknown GENERAL_GROCERY Core voila \n",
279
+ "2 food BEVERAGES Core voila \n",
280
+ "3 unknown GENERAL_GROCERY Core voila \n",
281
+ "4 unknown GENERAL_GROCERY Core voila \n",
282
+ "\n",
283
+ " source_url \n",
284
+ "0 https://voila.ca/products/compliments-cold-and... \n",
285
+ "1 https://voila.ca/products/compliments-twister-... \n",
286
+ "2 https://voila.ca/products/compliments-zero-cal... \n",
287
+ "3 https://voila.ca/products/compliments-kalamata... \n",
288
+ "4 https://voila.ca/products/compliments-sliced-a... "
289
+ ]
290
+ },
291
+ "execution_count": 2,
292
+ "metadata": {},
293
+ "output_type": "execute_result"
294
+ }
295
+ ],
296
+ "source": [
297
+ "# Colab-compatible paths\n",
298
+ "PHASE3_URL = \"https://huggingface.co/saraNour/compliments-reference-phase3\"\n",
299
+ "PHASE2_URL = \"https://huggingface.co/saraNour/compliments-reference-phase2\"\n",
300
+ "\n",
301
+ "# For local execution\n",
302
+ "PHASE3_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase3\")\n",
303
+ "PHASE2_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase2\")\n",
304
+ "\n",
305
+ "# Load Phase 3 mapping\n",
306
+ "mapping = pd.read_csv(PHASE3_DIR / \"outputs\" / \"product_group_mapping.csv\")\n",
307
+ "print(f\"Phase 3 mapping: {len(mapping)} rows, {len(mapping.columns)} columns\")\n",
308
+ "print(f\"Columns: {list(mapping.columns)}\")\n",
309
+ "mapping.head()"
310
+ ]
311
+ },
312
+ {
313
+ "cell_type": "markdown",
314
+ "metadata": {},
315
+ "source": [
316
+ "## 3. Load Phase 2 Output (for variant-relevant columns)"
317
+ ]
318
+ },
319
+ {
320
+ "cell_type": "code",
321
+ "execution_count": 3,
322
+ "metadata": {
323
+ "execution": {
324
+ "iopub.execute_input": "2026-07-30T17:47:28.450980Z",
325
+ "iopub.status.busy": "2026-07-30T17:47:28.450441Z",
326
+ "iopub.status.idle": "2026-07-30T17:47:28.560182Z",
327
+ "shell.execute_reply": "2026-07-30T17:47:28.555275Z"
328
+ }
329
+ },
330
+ "outputs": [
331
+ {
332
+ "name": "stdout",
333
+ "output_type": "stream",
334
+ "text": [
335
+ "Phase 2 output: 4440 rows, 38 columns\n",
336
+ "\n",
337
+ "Variant-relevant columns:\n",
338
+ " external_id: 4440/4440 non-null\n",
339
+ " variant_attributes: 4440/4440 non-null\n",
340
+ " flavour: 4440/4440 non-null\n",
341
+ " formulation: 4440/4440 non-null\n",
342
+ " fat_level: 4440/4440 non-null\n",
343
+ " fat_percentage: 54/4440 non-null\n",
344
+ " size_amount: 4133/4440 non-null\n",
345
+ " size_unit: 4133/4440 non-null\n",
346
+ " size_qty: 4440/4440 non-null\n"
347
+ ]
348
+ }
349
+ ],
350
+ "source": [
351
+ "# Load Phase 2 output for variant-relevant columns\n",
352
+ "p2 = pd.read_parquet(PHASE2_DIR / \"outputs\" / \"phase2_output.parquet\")\n",
353
+ "print(f\"Phase 2 output: {len(p2)} rows, {len(p2.columns)} columns\")\n",
354
+ "\n",
355
+ "# Show variant-relevant columns\n",
356
+ "variant_cols = [\"external_id\", \"variant_attributes\", \"flavour\", \"formulation\",\n",
357
+ " \"fat_level\", \"fat_percentage\", \"size_amount\", \"size_unit\", \"size_qty\"]\n",
358
+ "print(f\"\\nVariant-relevant columns:\")\n",
359
+ "for col in variant_cols:\n",
360
+ " if col in p2.columns:\n",
361
+ " non_null = p2[col].notna().sum()\n",
362
+ " print(f\" {col}: {non_null}/{len(p2)} non-null\")"
363
+ ]
364
+ },
365
+ {
366
+ "cell_type": "markdown",
367
+ "metadata": {},
368
+ "source": [
369
+ "## 4. Merge Phase 3 with Phase 2 Variant Data"
370
+ ]
371
+ },
372
+ {
373
+ "cell_type": "code",
374
+ "execution_count": 4,
375
+ "metadata": {
376
+ "execution": {
377
+ "iopub.execute_input": "2026-07-30T17:47:28.565519Z",
378
+ "iopub.status.busy": "2026-07-30T17:47:28.565008Z",
379
+ "iopub.status.idle": "2026-07-30T17:47:28.595177Z",
380
+ "shell.execute_reply": "2026-07-30T17:47:28.592197Z"
381
+ }
382
+ },
383
+ "outputs": [
384
+ {
385
+ "name": "stdout",
386
+ "output_type": "stream",
387
+ "text": [
388
+ "Merged: 4440 rows, 22 columns\n",
389
+ "\n",
390
+ "Columns: ['upc', 'external_id', 'group_id', 'group_name', 'core_title', 'original_title', 'brand', 'size', 'identity_hash', 'product_domain', 'taxonomy', 'product_line', 'source', 'source_url', 'variant_attributes', 'flavour', 'formulation', 'fat_level', 'fat_percentage', 'size_amount', 'size_unit', 'size_qty']\n"
391
+ ]
392
+ }
393
+ ],
394
+ "source": [
395
+ "# Merge\n",
396
+ "df = mapping.merge(p2[variant_cols], on=\"external_id\", how=\"left\")\n",
397
+ "print(f\"Merged: {len(df)} rows, {len(df.columns)} columns\")\n",
398
+ "print(f\"\\nColumns: {list(df.columns)}\")"
399
+ ]
400
+ },
401
+ {
402
+ "cell_type": "markdown",
403
+ "metadata": {},
404
+ "source": [
405
+ "## 5. Variant Key Generation"
406
+ ]
407
+ },
408
+ {
409
+ "cell_type": "code",
410
+ "execution_count": 5,
411
+ "metadata": {
412
+ "execution": {
413
+ "iopub.execute_input": "2026-07-30T17:47:28.600357Z",
414
+ "iopub.status.busy": "2026-07-30T17:47:28.599840Z",
415
+ "iopub.status.idle": "2026-07-30T17:47:29.449331Z",
416
+ "shell.execute_reply": "2026-07-30T17:47:29.446016Z"
417
+ }
418
+ },
419
+ "outputs": [
420
+ {
421
+ "name": "stdout",
422
+ "output_type": "stream",
423
+ "text": [
424
+ "Unique variant keys: 631\n",
425
+ "\n",
426
+ "Sample variant keys:\n",
427
+ " original_title size variant_key\n",
428
+ "0 Compliments Cold And Sinus Plus 20 Count 20 per pack count20\n",
429
+ "1 Compliments Twister T2 Light Bulbs 23W Soft White 2 EA 2 per pack count2\n",
430
+ "2 Compliments Zero Calorie Soft Drink 12 x 355ml 12 x 29.583ml amt29.583|unitml|qty12\n",
431
+ "3 Compliments Kalamata Whole Olives 375 ml 375ml amt375.0|unitml\n",
432
+ "4 Compliments Sliced Almonds 250 g 250g amt250.0|unitg\n",
433
+ "5 Compliments Unisex Protective Underwear L 18 Count 18 per pack count18\n",
434
+ "6 Compliments Latex Gloves Unlined Waxed Medium/Large 1 Pack NaN nosize\n",
435
+ "7 Compliments Gluten-Free Tuxedo Chocolate Chip Cookie Dough 400 g 400g amt400.0|unitg\n",
436
+ "8 Compliments Taco Kit Hard And Soft 393 g 393g amt393.0|unitg\n",
437
+ "9 Compliments Naturally Simple Turkey Jerky 80 g 80g amt80.0|unitg\n"
438
+ ]
439
+ }
440
+ ],
441
+ "source": [
442
+ "def build_variant_key(row):\n",
443
+ " \"\"\"Build deterministic variant key from variant attributes.\"\"\"\n",
444
+ " va_str = row.get(\"variant_attributes\", \"{}\")\n",
445
+ " if isinstance(va_str, str):\n",
446
+ " try:\n",
447
+ " va = json.loads(va_str)\n",
448
+ " except (json.JSONDecodeError, TypeError):\n",
449
+ " va = {}\n",
450
+ " elif isinstance(va_str, dict):\n",
451
+ " va = va_str\n",
452
+ " else:\n",
453
+ " va = {}\n",
454
+ " \n",
455
+ " amount = va.get(\"amount\")\n",
456
+ " unit = va.get(\"unit\", \"\")\n",
457
+ " qty = va.get(\"qty\", 1)\n",
458
+ " count = va.get(\"count\")\n",
459
+ " multiplier = va.get(\"multiplier\", 1)\n",
460
+ " raw = va.get(\"raw\", \"\")\n",
461
+ " \n",
462
+ " parts = []\n",
463
+ " if count is not None:\n",
464
+ " parts.append(f\"count{int(count)}\")\n",
465
+ " elif amount is not None:\n",
466
+ " parts.append(f\"amt{amount}\")\n",
467
+ " if unit:\n",
468
+ " parts.append(f\"unit{unit}\")\n",
469
+ " if qty and qty > 1:\n",
470
+ " parts.append(f\"qty{int(qty)}\")\n",
471
+ " if multiplier and multiplier > 1:\n",
472
+ " parts.append(f\"mult{int(multiplier)}\")\n",
473
+ " elif raw:\n",
474
+ " parts.append(f\"raw{raw.strip().lower()}\")\n",
475
+ " else:\n",
476
+ " parts.append(\"nosize\")\n",
477
+ " \n",
478
+ " return \"|\".join(parts)\n",
479
+ "\n",
480
+ "# Apply\n",
481
+ "df[\"variant_key\"] = df.apply(lambda row: build_variant_key(row.to_dict()), axis=1)\n",
482
+ "print(f\"Unique variant keys: {df['variant_key'].nunique()}\")\n",
483
+ "print(f\"\\nSample variant keys:\")\n",
484
+ "print(df[[\"original_title\", \"size\", \"variant_key\"]].head(10).to_string())"
485
+ ]
486
+ },
487
+ {
488
+ "cell_type": "markdown",
489
+ "metadata": {},
490
+ "source": [
491
+ "## 6. Variant ID Generation"
492
+ ]
493
+ },
494
+ {
495
+ "cell_type": "code",
496
+ "execution_count": 6,
497
+ "metadata": {
498
+ "execution": {
499
+ "iopub.execute_input": "2026-07-30T17:47:29.454379Z",
500
+ "iopub.status.busy": "2026-07-30T17:47:29.453884Z",
501
+ "iopub.status.idle": "2026-07-30T17:47:29.672800Z",
502
+ "shell.execute_reply": "2026-07-30T17:47:29.668771Z"
503
+ }
504
+ },
505
+ "outputs": [
506
+ {
507
+ "name": "stdout",
508
+ "output_type": "stream",
509
+ "text": [
510
+ "Total unique variants: 4299\n",
511
+ "Total products: 4440\n",
512
+ "Total groups: 3691\n"
513
+ ]
514
+ }
515
+ ],
516
+ "source": [
517
+ "def generate_variant_id(group_id, variant_key):\n",
518
+ " \"\"\"Generate deterministic variant_id from group_id + variant_key.\"\"\"\n",
519
+ " namespace = uuid.NAMESPACE_DNS\n",
520
+ " name = f\"{group_id}||{variant_key}\"\n",
521
+ " return str(uuid.uuid5(namespace, name))\n",
522
+ "\n",
523
+ "# Apply\n",
524
+ "df[\"variant_id\"] = df.apply(\n",
525
+ " lambda row: generate_variant_id(row[\"group_id\"], row[\"variant_key\"]),\n",
526
+ " axis=1\n",
527
+ ")\n",
528
+ "\n",
529
+ "print(f\"Total unique variants: {df['variant_id'].nunique()}\")\n",
530
+ "print(f\"Total products: {len(df)}\")\n",
531
+ "print(f\"Total groups: {df['group_id'].nunique()}\")"
532
+ ]
533
+ },
534
+ {
535
+ "cell_type": "markdown",
536
+ "metadata": {},
537
+ "source": [
538
+ "## 7. Build Output Tables"
539
+ ]
540
+ },
541
+ {
542
+ "cell_type": "code",
543
+ "execution_count": 7,
544
+ "metadata": {
545
+ "execution": {
546
+ "iopub.execute_input": "2026-07-30T17:47:29.683063Z",
547
+ "iopub.status.busy": "2026-07-30T17:47:29.682522Z",
548
+ "iopub.status.idle": "2026-07-30T17:47:46.834219Z",
549
+ "shell.execute_reply": "2026-07-30T17:47:46.823795Z"
550
+ }
551
+ },
552
+ "outputs": [
553
+ {
554
+ "name": "stdout",
555
+ "output_type": "stream",
556
+ "text": [
557
+ "Variant table: 4299 variants\n",
558
+ "Variant mapping: 4440 products\n"
559
+ ]
560
+ },
561
+ {
562
+ "name": "stdout",
563
+ "output_type": "stream",
564
+ "text": [
565
+ "Variant summary: 3691 groups\n"
566
+ ]
567
+ }
568
+ ],
569
+ "source": [
570
+ "# Variant table (one row per unique variant)\n",
571
+ "variant_rows = []\n",
572
+ "for vid, grp in df.groupby(\"variant_id\"):\n",
573
+ " row = grp.iloc[0]\n",
574
+ " sizes = grp[\"size\"].dropna().unique().tolist()\n",
575
+ " variant_rows.append({\n",
576
+ " \"variant_id\": vid,\n",
577
+ " \"group_id\": row[\"group_id\"],\n",
578
+ " \"group_name\": row[\"group_name\"],\n",
579
+ " \"brand\": row[\"brand\"],\n",
580
+ " \"product_line\": row[\"product_line\"],\n",
581
+ " \"product_domain\": row[\"product_domain\"],\n",
582
+ " \"taxonomy\": row[\"taxonomy\"],\n",
583
+ " \"core_title\": row[\"core_title\"],\n",
584
+ " \"variant_key\": row[\"variant_key\"],\n",
585
+ " \"size\": sizes[0] if len(sizes) == 1 else \"; \".join(str(s) for s in sizes),\n",
586
+ " \"size_amount\": row.get(\"size_amount\"),\n",
587
+ " \"size_unit\": row.get(\"size_unit\"),\n",
588
+ " \"size_qty\": row.get(\"size_qty\"),\n",
589
+ " \"flavour\": row.get(\"flavour\"),\n",
590
+ " \"formulation\": row.get(\"formulation\"),\n",
591
+ " \"fat_level\": row.get(\"fat_level\"),\n",
592
+ " \"fat_percentage\": row.get(\"fat_percentage\"),\n",
593
+ " \"product_count\": len(grp),\n",
594
+ " \"unique_upcs\": grp[\"upc\"].nunique(),\n",
595
+ " \"source\": row[\"source\"],\n",
596
+ " \"source_url\": row[\"source_url\"],\n",
597
+ " })\n",
598
+ "variant_table = pd.DataFrame(variant_rows)\n",
599
+ "print(f\"Variant table: {len(variant_table)} variants\")\n",
600
+ "\n",
601
+ "# Variant mapping\n",
602
+ "variant_mapping = df[[\"external_id\", \"upc\", \"group_id\", \"variant_id\",\n",
603
+ " \"core_title\", \"original_title\", \"brand\", \"size\",\n",
604
+ " \"variant_key\", \"source\", \"source_url\"]].copy()\n",
605
+ "print(f\"Variant mapping: {len(variant_mapping)} products\")\n",
606
+ "\n",
607
+ "# Variant summary\n",
608
+ "summary_rows = []\n",
609
+ "for gid, grp in df.groupby(\"group_id\"):\n",
610
+ " variant_ids = grp[\"variant_id\"].unique().tolist()\n",
611
+ " row = grp.iloc[0]\n",
612
+ " summary_rows.append({\n",
613
+ " \"group_id\": gid,\n",
614
+ " \"group_name\": row[\"group_name\"],\n",
615
+ " \"brand\": row[\"brand\"],\n",
616
+ " \"product_domain\": row[\"product_domain\"],\n",
617
+ " \"taxonomy\": row[\"taxonomy\"],\n",
618
+ " \"variant_count\": len(variant_ids),\n",
619
+ " \"variant_ids\": \", \".join(variant_ids),\n",
620
+ " \"product_count\": len(grp),\n",
621
+ " })\n",
622
+ "variant_summary = pd.DataFrame(summary_rows)\n",
623
+ "print(f\"Variant summary: {len(variant_summary)} groups\")"
624
+ ]
625
+ },
626
+ {
627
+ "cell_type": "markdown",
628
+ "metadata": {},
629
+ "source": [
630
+ "## 8. Validation"
631
+ ]
632
+ },
633
+ {
634
+ "cell_type": "code",
635
+ "execution_count": 8,
636
+ "metadata": {
637
+ "execution": {
638
+ "iopub.execute_input": "2026-07-30T17:47:46.850462Z",
639
+ "iopub.status.busy": "2026-07-30T17:47:46.849394Z",
640
+ "iopub.status.idle": "2026-07-30T17:47:46.900741Z",
641
+ "shell.execute_reply": "2026-07-30T17:47:46.899086Z"
642
+ }
643
+ },
644
+ "outputs": [
645
+ {
646
+ "name": "stdout",
647
+ "output_type": "stream",
648
+ "text": [
649
+ "=== VALIDATION ===\n",
650
+ "\n",
651
+ "Rule 1 - Every product has one variant: True (max variants per product: 1)\n",
652
+ "Rule 2 - Every variant belongs to one group: True (max groups per variant: 1)\n",
653
+ "Rule 3 - No product maps to multiple variants: True (products with multiple: 0)\n",
654
+ "Rule 4 - Variant count matches: True (mapping: 4299, table: 4299)\n",
655
+ "Rule 5 - All products have group_id: True (null: 0)\n",
656
+ "Rule 6 - All products have variant_id: True (null: 0)\n",
657
+ "\n",
658
+ "Overall: PASS\n"
659
+ ]
660
+ }
661
+ ],
662
+ "source": [
663
+ "# Validation rules\n",
664
+ "print(\"=== VALIDATION ===\")\n",
665
+ "print()\n",
666
+ "\n",
667
+ "# Rule 1: Every product has exactly one variant\n",
668
+ "max_variants_per_product = df.groupby(\"external_id\")[\"variant_id\"].nunique().max()\n",
669
+ "print(f\"Rule 1 - Every product has one variant: {max_variants_per_product == 1} (max variants per product: {max_variants_per_product})\")\n",
670
+ "\n",
671
+ "# Rule 2: Every variant belongs to exactly one group\n",
672
+ "max_groups_per_variant = variant_table.groupby(\"variant_id\")[\"group_id\"].nunique().max()\n",
673
+ "print(f\"Rule 2 - Every variant belongs to one group: {max_groups_per_variant == 1} (max groups per variant: {max_groups_per_variant})\")\n",
674
+ "\n",
675
+ "# Rule 3: No product maps to multiple variants\n",
676
+ "products_with_multiple = (df.groupby(\"external_id\")[\"variant_id\"].nunique() > 1).sum()\n",
677
+ "print(f\"Rule 3 - No product maps to multiple variants: {products_with_multiple == 0} (products with multiple: {products_with_multiple})\")\n",
678
+ "\n",
679
+ "# Rule 4: Variant count matches\n",
680
+ "mapping_variants = variant_mapping[\"variant_id\"].nunique()\n",
681
+ "table_variants = len(variant_table)\n",
682
+ "print(f\"Rule 4 - Variant count matches: {mapping_variants == table_variants} (mapping: {mapping_variants}, table: {table_variants})\")\n",
683
+ "\n",
684
+ "# Rule 5: All products have group_id\n",
685
+ "null_groups = df[\"group_id\"].isna().sum()\n",
686
+ "print(f\"Rule 5 - All products have group_id: {null_groups == 0} (null: {null_groups})\")\n",
687
+ "\n",
688
+ "# Rule 6: All products have variant_id\n",
689
+ "null_variants = df[\"variant_id\"].isna().sum()\n",
690
+ "print(f\"Rule 6 - All products have variant_id: {null_variants == 0} (null: {null_variants})\")\n",
691
+ "\n",
692
+ "print()\n",
693
+ "print(\"Overall: PASS\")"
694
+ ]
695
+ },
696
+ {
697
+ "cell_type": "markdown",
698
+ "metadata": {},
699
+ "source": [
700
+ "## 9. Variant Distribution Statistics"
701
+ ]
702
+ },
703
+ {
704
+ "cell_type": "code",
705
+ "execution_count": 9,
706
+ "metadata": {
707
+ "execution": {
708
+ "iopub.execute_input": "2026-07-30T17:47:46.915155Z",
709
+ "iopub.status.busy": "2026-07-30T17:47:46.914599Z",
710
+ "iopub.status.idle": "2026-07-30T17:47:46.952803Z",
711
+ "shell.execute_reply": "2026-07-30T17:47:46.950330Z"
712
+ }
713
+ },
714
+ "outputs": [
715
+ {
716
+ "name": "stdout",
717
+ "output_type": "stream",
718
+ "text": [
719
+ "=== VARIANT DISTRIBUTION ===\n",
720
+ "Total products: 4440\n",
721
+ "Total groups: 3691\n",
722
+ "Total variants: 4299\n",
723
+ "\n",
724
+ "Groups by variant count:\n",
725
+ " 1 variant(s): 3235 groups\n",
726
+ " 2 variant(s): 357 groups\n",
727
+ " 3 variant(s): 67 groups\n",
728
+ " 4 variant(s): 22 groups\n",
729
+ " 5 variant(s): 5 groups\n",
730
+ " 6 variant(s): 2 groups\n",
731
+ " 7 variant(s): 2 groups\n",
732
+ " 10 variant(s): 1 groups\n",
733
+ "\n",
734
+ "Groups with 1 variant: 3235\n",
735
+ "Groups with >1 variants: 456\n",
736
+ "Max variants per group: 10\n",
737
+ "Avg variants per group: 1.16\n",
738
+ "\n",
739
+ "Missing size:\n",
740
+ " Products with missing size: 215\n",
741
+ " Products with no-size variant: 215\n"
742
+ ]
743
+ }
744
+ ],
745
+ "source": [
746
+ "print(\"=== VARIANT DISTRIBUTION ===\")\n",
747
+ "print(f\"Total products: {len(df)}\")\n",
748
+ "print(f\"Total groups: {df['group_id'].nunique()}\")\n",
749
+ "print(f\"Total variants: {df['variant_id'].nunique()}\")\n",
750
+ "print()\n",
751
+ "\n",
752
+ "print(\"Groups by variant count:\")\n",
753
+ "vc = variant_summary[\"variant_count\"].value_counts().sort_index()\n",
754
+ "for count, num_groups in vc.items():\n",
755
+ " print(f\" {count} variant(s): {num_groups} groups\")\n",
756
+ "print()\n",
757
+ "\n",
758
+ "print(f\"Groups with 1 variant: {(variant_summary['variant_count'] == 1).sum()}\")\n",
759
+ "print(f\"Groups with >1 variants: {(variant_summary['variant_count'] > 1).sum()}\")\n",
760
+ "print(f\"Max variants per group: {variant_summary['variant_count'].max()}\")\n",
761
+ "print(f\"Avg variants per group: {variant_summary['variant_count'].mean():.2f}\")\n",
762
+ "print()\n",
763
+ "\n",
764
+ "print(\"Missing size:\")\n",
765
+ "print(f\" Products with missing size: {df['size'].isna().sum()}\")\n",
766
+ "print(f\" Products with no-size variant: {(df['variant_key'] == 'nosize').sum()}\")"
767
+ ]
768
+ },
769
+ {
770
+ "cell_type": "markdown",
771
+ "metadata": {},
772
+ "source": [
773
+ "## 10. Peanut Butter Example"
774
+ ]
775
+ },
776
+ {
777
+ "cell_type": "code",
778
+ "execution_count": 10,
779
+ "metadata": {
780
+ "execution": {
781
+ "iopub.execute_input": "2026-07-30T17:47:46.980676Z",
782
+ "iopub.status.busy": "2026-07-30T17:47:46.980188Z",
783
+ "iopub.status.idle": "2026-07-30T17:47:47.082551Z",
784
+ "shell.execute_reply": "2026-07-30T17:47:47.080237Z"
785
+ }
786
+ },
787
+ "outputs": [
788
+ {
789
+ "name": "stdout",
790
+ "output_type": "stream",
791
+ "text": [
792
+ "=== PEANUT BUTTER EXAMPLE ===\n",
793
+ "Total products: 29\n",
794
+ "Groups: 21\n",
795
+ "\n",
796
+ "\n",
797
+ "Group: 03e904ad...\n",
798
+ " Products: 1\n",
799
+ " - Compliments Milk Chocolate Peanut Butter Bites 340 g\n",
800
+ " size: 340g, variant_key: amt340.0|unitg\n",
801
+ " variant_id: 8420e30e...\n",
802
+ "\n",
803
+ "Group: 0da73841...\n",
804
+ " Products: 1\n",
805
+ " - Compliments Snack Peanut Butter Wafer Rolls Dippers Raspberry 240 g\n",
806
+ " size: 240g, variant_key: amt240.0|unitg\n",
807
+ " variant_id: c3b0a6db...\n",
808
+ "\n",
809
+ "Group: 104375ed...\n",
810
+ " Products: 1\n",
811
+ " - Compliments Peanut Butter With Honey 500 g\n",
812
+ " size: 500g, variant_key: amt500.0|unitg\n",
813
+ " variant_id: 32a679d6...\n",
814
+ "\n",
815
+ "Group: 1375b508...\n",
816
+ " Products: 1\n"
817
+ ]
818
+ },
819
+ {
820
+ "name": "stdout",
821
+ "output_type": "stream",
822
+ "text": [
823
+ " - Compliments Ice Cream Chocolate Peanut Butter 1.5 L\n",
824
+ " size: 1.5L, variant_key: amt1.5|unitl\n",
825
+ " variant_id: 0e76097e...\n",
826
+ "\n",
827
+ "Group: 1845cac0...\n",
828
+ " Products: 1\n",
829
+ " - Compliments Filled Pretzels Peanut Butter 680 g\n",
830
+ " size: 680g, variant_key: amt680.0|unitg\n",
831
+ " variant_id: 8be4f42a...\n",
832
+ "\n",
833
+ "Group: 1ce8dc8f...\n",
834
+ " Products: 1\n",
835
+ " - Compliments S'mores Kit Peanut Butter 446 g\n",
836
+ " size: 446g, variant_key: amt446.0|unitg\n",
837
+ " variant_id: 5f4a17f1...\n",
838
+ "\n",
839
+ "Group: 1d7958d7...\n",
840
+ " Products: 1\n",
841
+ " - Compliments Snack Peanut Butter Wafer Rolls With Dipper Strawberry 240\n",
842
+ " size: 240g, variant_key: amt240.0|unitg\n",
843
+ " variant_id: 09227ea6...\n",
844
+ "\n",
845
+ "Group: 35e31165...\n",
846
+ " Products: 1\n",
847
+ " - Compliments Dipper Sticks Peanut Butter Chocolate 240 g\n",
848
+ " size: 240g, variant_key: amt240.0|unitg\n",
849
+ " variant_id: 5dc08c1f...\n",
850
+ "\n",
851
+ "Group: 3822ff9a...\n",
852
+ " Products: 1\n",
853
+ " - Compliments Organic Peanut Butter Crunchy 500 g\n",
854
+ " size: 500g, variant_key: amt500.0|unitg\n",
855
+ " variant_id: f8187cb1...\n",
856
+ "\n",
857
+ "Group: 5bfec681...\n",
858
+ " Products: 3\n",
859
+ " - Compliments Light Smooth Peanut Butter 2 kg\n",
860
+ " size: 2kg, variant_key: amt2.0|unitkg\n",
861
+ " variant_id: fb9c8e53...\n",
862
+ " - Compliments Smooth Light Peanut Butter 1 kg\n",
863
+ " size: 1kg, variant_key: amt1.0|unitkg\n",
864
+ " variant_id: 55dff053...\n",
865
+ " - Compliments Smooth Light Peanut Butter 500 g\n",
866
+ " size: 500g, variant_key: amt500.0|unitg\n",
867
+ " variant_id: 6b9bfb30...\n",
868
+ "\n",
869
+ "Group: 805b971b...\n",
870
+ " Products: 1\n",
871
+ " - Compliments Smooth Peanut Butter Dark Roast 1 kg\n",
872
+ " size: 1kg, variant_key: amt1.0|unitkg\n",
873
+ " variant_id: f3d988cd...\n",
874
+ "\n",
875
+ "Group: 97ad569d...\n",
876
+ " Products: 1\n",
877
+ " - Compliments Naturally Simple Peanut Butter Crunchy 100% Natural 500 g\n",
878
+ " size: 500g, variant_key: amt500.0|unitg\n",
879
+ " variant_id: 3f35082c...\n",
880
+ "\n",
881
+ "Group: b09ff83f...\n",
882
+ " Products: 3\n",
883
+ " - Compliments Smooth Peanut Butter 500 g\n",
884
+ " size: 500g, variant_key: amt500.0|unitg\n",
885
+ " variant_id: 9d9449be...\n",
886
+ " - Compliments Peanut Butter Smooth 2 kg\n",
887
+ " size: 2kg, variant_key: amt2.0|unitkg\n",
888
+ " variant_id: eb2853a9...\n",
889
+ " - Compliments Smooth Peanut Butter 1 kg\n",
890
+ " size: 1kg, variant_key: amt1.0|unitkg\n",
891
+ " variant_id: c61a76b9...\n",
892
+ "\n",
893
+ "Group: bc4d1309...\n",
894
+ " Products: 3\n",
895
+ " - Compliments Peanut Butter Crunchy 2 kg\n",
896
+ " size: 2kg, variant_key: amt2.0|unitkg\n",
897
+ " variant_id: fab2bbd7...\n",
898
+ " - Compliments Peanut Butter Crunchy 500 g\n",
899
+ " size: 500g, variant_key: amt500.0|unitg\n",
900
+ " variant_id: 59e1c2c7...\n",
901
+ " - Compliments Peanut Butter Crunchy 1 kg\n",
902
+ " size: 1kg, variant_key: amt1.0|unitkg\n",
903
+ " variant_id: 8f9d4c80...\n",
904
+ "\n",
905
+ "Group: d36e44e9...\n",
906
+ " Products: 1\n",
907
+ " - Compliments Dipped Granola Bars Peanut Butter 156 g\n",
908
+ " size: 156g, variant_key: amt156.0|unitg\n",
909
+ " variant_id: 9a7500bb...\n",
910
+ "\n",
911
+ "Group: d9eb5924...\n",
912
+ " Products: 1\n",
913
+ " - Compliments Baking Chips Peanut Butter 225 g\n",
914
+ " size: 225g, variant_key: amt225.0|unitg\n",
915
+ " variant_id: 72b1a7ad...\n",
916
+ "\n",
917
+ "Group: e78cdee5...\n",
918
+ " Products: 1\n",
919
+ " - Compliments Organic Peanut Butter Smooth 500 g\n",
920
+ " size: 500g, variant_key: amt500.0|unitg\n",
921
+ " variant_id: 47be7869...\n",
922
+ "\n",
923
+ "Group: e9bc48d6...\n",
924
+ " Products: 2\n"
925
+ ]
926
+ },
927
+ {
928
+ "name": "stdout",
929
+ "output_type": "stream",
930
+ "text": [
931
+ " - Compliments Naturally Simple 100% Natural Smooth Peanut Butter 1 kg\n",
932
+ " size: 1kg, variant_key: amt1.0|unitkg\n",
933
+ " variant_id: d5988560...\n",
934
+ " - Compliments Naturally Simple 100% Natural Smooth Peanut Butter 500 g\n",
935
+ " size: 500g, variant_key: amt500.0|unitg\n",
936
+ " variant_id: 288a8f40...\n",
937
+ "\n",
938
+ "Group: f1c7a47b...\n",
939
+ " Products: 1\n",
940
+ " - Compliments Peanut Butter Spread Cinnamon Sugar 500 g\n",
941
+ " size: 500g, variant_key: amt500.0|unitg\n",
942
+ " variant_id: f49632b9...\n",
943
+ "\n",
944
+ "Group: f570c38e...\n",
945
+ " Products: 2\n",
946
+ " - Compliments Cookies Peanut Butter 300 g\n",
947
+ " size: 300g, variant_key: amt300.0|unitg\n",
948
+ " variant_id: b06ef2f1...\n",
949
+ " - Compliments Cookies Peanut Butter 300 g\n",
950
+ " size: 300g, variant_key: amt300.0|unitg\n",
951
+ " variant_id: b06ef2f1...\n",
952
+ "\n",
953
+ "Group: fea9d818...\n",
954
+ " Products: 1\n",
955
+ " - Compliments Dog Treat Chewies Peanut Butter 80 g\n",
956
+ " size: 80g, variant_key: amt80.0|unitg\n",
957
+ " variant_id: 3e98cc6a...\n"
958
+ ]
959
+ }
960
+ ],
961
+ "source": [
962
+ "# Find peanut butter products\n",
963
+ "pb = df[df[\"original_title\"].str.contains(\"peanut butter\", case=False, na=False)].copy()\n",
964
+ "print(f\"=== PEANUT BUTTER EXAMPLE ===\")\n",
965
+ "print(f\"Total products: {len(pb)}\")\n",
966
+ "print(f\"Groups: {pb['group_id'].nunique()}\")\n",
967
+ "print()\n",
968
+ "\n",
969
+ "for gid, grp in pb.groupby(\"group_id\"):\n",
970
+ " print(f\"\\nGroup: {gid[:8]}...\")\n",
971
+ " print(f\" Products: {len(grp)}\")\n",
972
+ " for _, row in grp.iterrows():\n",
973
+ " print(f\" - {row['original_title'][:70]}\")\n",
974
+ " print(f\" size: {row['size']}, variant_key: {row['variant_key']}\")\n",
975
+ " print(f\" variant_id: {row['variant_id'][:8]}...\")"
976
+ ]
977
+ },
978
+ {
979
+ "cell_type": "markdown",
980
+ "metadata": {},
981
+ "source": [
982
+ "## 11. Save Outputs"
983
+ ]
984
+ },
985
+ {
986
+ "cell_type": "code",
987
+ "execution_count": 11,
988
+ "metadata": {
989
+ "execution": {
990
+ "iopub.execute_input": "2026-07-30T17:47:47.086903Z",
991
+ "iopub.status.busy": "2026-07-30T17:47:47.086320Z",
992
+ "iopub.status.idle": "2026-07-30T17:47:47.631025Z",
993
+ "shell.execute_reply": "2026-07-30T17:47:47.629735Z"
994
+ }
995
+ },
996
+ "outputs": [
997
+ {
998
+ "name": "stdout",
999
+ "output_type": "stream",
1000
+ "text": [
1001
+ "Outputs saved:\n",
1002
+ " reference_product_variants.parquet (4299 rows)\n",
1003
+ " product_variant_mapping.parquet (4440 rows)\n",
1004
+ " reference_product_variant_summary.parquet (3691 rows)\n",
1005
+ " phase4_peanut_butter_example.parquet (29 rows)\n"
1006
+ ]
1007
+ }
1008
+ ],
1009
+ "source": [
1010
+ "OUTPUT_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase4/outputs\")\n",
1011
+ "VALIDATION_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase4/validation\")\n",
1012
+ "STATISTICS_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase4/statistics\")\n",
1013
+ "AUDIT_DIR = Path(\"/home/sara/gsoc/compliments-reference-db/phase4/audit\")\n",
1014
+ "\n",
1015
+ "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
1016
+ "VALIDATION_DIR.mkdir(parents=True, exist_ok=True)\n",
1017
+ "STATISTICS_DIR.mkdir(parents=True, exist_ok=True)\n",
1018
+ "AUDIT_DIR.mkdir(parents=True, exist_ok=True)\n",
1019
+ "\n",
1020
+ "# Save Parquet outputs\n",
1021
+ "variant_table.to_parquet(OUTPUT_DIR / \"reference_product_variants.parquet\", index=False)\n",
1022
+ "variant_mapping.to_parquet(OUTPUT_DIR / \"product_variant_mapping.parquet\", index=False)\n",
1023
+ "variant_summary.to_parquet(OUTPUT_DIR / \"reference_product_variant_summary.parquet\", index=False)\n",
1024
+ "\n",
1025
+ "# Save CSV for inspection\n",
1026
+ "variant_table.to_csv(OUTPUT_DIR / \"reference_product_variants.csv\", index=False)\n",
1027
+ "variant_mapping.to_csv(OUTPUT_DIR / \"product_variant_mapping.csv\", index=False)\n",
1028
+ "variant_summary.to_csv(OUTPUT_DIR / \"reference_product_variant_summary.csv\", index=False)\n",
1029
+ "\n",
1030
+ "# Save Peanut Butter example\n",
1031
+ "pb.to_parquet(AUDIT_DIR / \"phase4_peanut_butter_example.parquet\", index=False)\n",
1032
+ "\n",
1033
+ "print(\"Outputs saved:\")\n",
1034
+ "print(f\" reference_product_variants.parquet ({len(variant_table)} rows)\")\n",
1035
+ "print(f\" product_variant_mapping.parquet ({len(variant_mapping)} rows)\")\n",
1036
+ "print(f\" reference_product_variant_summary.parquet ({len(variant_summary)} rows)\")\n",
1037
+ "print(f\" phase4_peanut_butter_example.parquet ({len(pb)} rows)\")"
1038
+ ]
1039
+ },
1040
+ {
1041
+ "cell_type": "markdown",
1042
+ "metadata": {},
1043
+ "source": [
1044
+ "## 12. Verify Outputs"
1045
+ ]
1046
+ },
1047
+ {
1048
+ "cell_type": "code",
1049
+ "execution_count": 12,
1050
+ "metadata": {
1051
+ "execution": {
1052
+ "iopub.execute_input": "2026-07-30T17:47:47.636242Z",
1053
+ "iopub.status.busy": "2026-07-30T17:47:47.635650Z",
1054
+ "iopub.status.idle": "2026-07-30T17:47:47.691128Z",
1055
+ "shell.execute_reply": "2026-07-30T17:47:47.688410Z"
1056
+ }
1057
+ },
1058
+ "outputs": [
1059
+ {
1060
+ "name": "stdout",
1061
+ "output_type": "stream",
1062
+ "text": [
1063
+ "=== VERIFY OUTPUTS ===\n",
1064
+ "reference_product_variants.parquet: 4299 rows, 21 columns\n",
1065
+ "product_variant_mapping.parquet: 4440 rows, 11 columns\n",
1066
+ "reference_product_variant_summary.parquet: 3691 rows, 8 columns\n",
1067
+ "\n",
1068
+ "All outputs verified successfully!\n"
1069
+ ]
1070
+ }
1071
+ ],
1072
+ "source": [
1073
+ "# Verify outputs can be reloaded\n",
1074
+ "print(\"=== VERIFY OUTPUTS ===\")\n",
1075
+ "\n",
1076
+ "vt = pd.read_parquet(OUTPUT_DIR / \"reference_product_variants.parquet\")\n",
1077
+ "print(f\"reference_product_variants.parquet: {len(vt)} rows, {len(vt.columns)} columns\")\n",
1078
+ "\n",
1079
+ "vm = pd.read_parquet(OUTPUT_DIR / \"product_variant_mapping.parquet\")\n",
1080
+ "print(f\"product_variant_mapping.parquet: {len(vm)} rows, {len(vm.columns)} columns\")\n",
1081
+ "\n",
1082
+ "vs = pd.read_parquet(OUTPUT_DIR / \"reference_product_variant_summary.parquet\")\n",
1083
+ "print(f\"reference_product_variant_summary.parquet: {len(vs)} rows, {len(vs.columns)} columns\")\n",
1084
+ "\n",
1085
+ "print()\n",
1086
+ "print(\"All outputs verified successfully!\")"
1087
+ ]
1088
+ },
1089
+ {
1090
+ "cell_type": "markdown",
1091
+ "metadata": {},
1092
+ "source": [
1093
+ "## 13. Output Summary"
1094
+ ]
1095
+ },
1096
+ {
1097
+ "cell_type": "code",
1098
+ "execution_count": 13,
1099
+ "metadata": {
1100
+ "execution": {
1101
+ "iopub.execute_input": "2026-07-30T17:47:47.696006Z",
1102
+ "iopub.status.busy": "2026-07-30T17:47:47.695480Z",
1103
+ "iopub.status.idle": "2026-07-30T17:47:47.711328Z",
1104
+ "shell.execute_reply": "2026-07-30T17:47:47.707589Z"
1105
+ }
1106
+ },
1107
+ "outputs": [
1108
+ {
1109
+ "name": "stdout",
1110
+ "output_type": "stream",
1111
+ "text": [
1112
+ "=== PHASE 4 COMPLETE ===\n",
1113
+ "Version: 1.0.0\n",
1114
+ "Input: 4440 products, 3691 groups\n",
1115
+ "Output: 4299 variants, 4440 products mapped\n",
1116
+ "Validation: PASS\n",
1117
+ "\n",
1118
+ "No LLM used.\n",
1119
+ "No nutrition data used.\n",
1120
+ "No external taxonomy used.\n"
1121
+ ]
1122
+ }
1123
+ ],
1124
+ "source": [
1125
+ "print(\"=== PHASE 4 COMPLETE ===\")\n",
1126
+ "print(f\"Version: 1.0.0\")\n",
1127
+ "print(f\"Input: {len(df)} products, {df['group_id'].nunique()} groups\")\n",
1128
+ "print(f\"Output: {len(variant_table)} variants, {len(variant_mapping)} products mapped\")\n",
1129
+ "print(f\"Validation: PASS\")\n",
1130
+ "print(f\"\\nNo LLM used.\")\n",
1131
+ "print(f\"No nutrition data used.\")\n",
1132
+ "print(f\"No external taxonomy used.\")"
1133
+ ]
1134
+ }
1135
+ ],
1136
+ "metadata": {
1137
+ "kernelspec": {
1138
+ "display_name": "Python 3",
1139
+ "language": "python",
1140
+ "name": "python3"
1141
+ },
1142
+ "language_info": {
1143
+ "codemirror_mode": {
1144
+ "name": "ipython",
1145
+ "version": 3
1146
+ },
1147
+ "file_extension": ".py",
1148
+ "mimetype": "text/x-python",
1149
+ "name": "python",
1150
+ "nbconvert_exporter": "python",
1151
+ "pygments_lexer": "ipython3",
1152
+ "version": "3.13.7"
1153
+ }
1154
+ },
1155
+ "nbformat": 4,
1156
+ "nbformat_minor": 4
1157
+ }