saraNour commited on
Commit
a1d862a
·
verified ·
1 Parent(s): d3aefaa

Upload outputs/phase1_executed.ipynb with huggingface_hub

Browse files
Files changed (1) hide show
  1. outputs/phase1_executed.ipynb +1400 -0
outputs/phase1_executed.ipynb ADDED
@@ -0,0 +1,1400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Phase 1 — Load / Validate / Provenance\n",
8
+ "## Compliments Reference DB Pipeline\n",
9
+ "\n",
10
+ "**Authoritative Input:**\n",
11
+ "`saraNour/compliments-brand/source_of_truth/products.parquet`\n",
12
+ "\n",
13
+ "**This phase does:**\n",
14
+ "1. Downloads the authoritative products.parquet from HuggingFace\n",
15
+ "2. Validates schema, row count, nulls, duplicates\n",
16
+ "3. Analyzes UPC patterns, brand values, size fields\n",
17
+ "4. Documents provenance of every column\n",
18
+ "5. Drops 100% null columns with explicit documentation\n",
19
+ "6. Produces clean Phase 1 output + validation + statistics"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": 1,
25
+ "metadata": {
26
+ "execution": {
27
+ "iopub.execute_input": "2026-07-30T14:10:05.327344Z",
28
+ "iopub.status.busy": "2026-07-30T14:10:05.326892Z",
29
+ "iopub.status.idle": "2026-07-30T14:10:06.411761Z",
30
+ "shell.execute_reply": "2026-07-30T14:10:06.407953Z"
31
+ }
32
+ },
33
+ "outputs": [
34
+ {
35
+ "name": "stdout",
36
+ "output_type": "stream",
37
+ "text": [
38
+ "Python: 3.13.7 (main, Mar 3 2026, 12:19:54) [GCC 15.2.0]\n",
39
+ "pandas: 3.0.5\n",
40
+ "numpy: 2.5.1\n",
41
+ "Timestamp: 2026-07-30T14:10:06.402163+00:00\n"
42
+ ]
43
+ }
44
+ ],
45
+ "source": [
46
+ "import json, os, sys\n",
47
+ "from datetime import datetime, timezone\n",
48
+ "from pathlib import Path\n",
49
+ "\n",
50
+ "import pandas as pd\n",
51
+ "import numpy as np\n",
52
+ "from huggingface_hub import hf_hub_download\n",
53
+ "\n",
54
+ "print(f\"Python: {sys.version}\")\n",
55
+ "print(f\"pandas: {pd.__version__}\")\n",
56
+ "print(f\"numpy: {np.__version__}\")\n",
57
+ "print(f\"Timestamp: {datetime.now(timezone.utc).isoformat()}\")"
58
+ ]
59
+ },
60
+ {
61
+ "cell_type": "markdown",
62
+ "metadata": {},
63
+ "source": [
64
+ "## 1. Configuration"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": 2,
70
+ "metadata": {
71
+ "execution": {
72
+ "iopub.execute_input": "2026-07-30T14:10:06.511086Z",
73
+ "iopub.status.busy": "2026-07-30T14:10:06.510327Z",
74
+ "iopub.status.idle": "2026-07-30T14:10:06.525994Z",
75
+ "shell.execute_reply": "2026-07-30T14:10:06.522561Z"
76
+ }
77
+ },
78
+ "outputs": [
79
+ {
80
+ "name": "stdout",
81
+ "output_type": "stream",
82
+ "text": [
83
+ "Source: saraNour/compliments-brand/source_of_truth/products.parquet\n",
84
+ "Version: 1.0.0\n"
85
+ ]
86
+ }
87
+ ],
88
+ "source": [
89
+ "HF_REPO = \"saraNour/compliments-brand\"\n",
90
+ "HF_FILE = \"source_of_truth/products.parquet\"\n",
91
+ "HF_REPO_TYPE = \"dataset\"\n",
92
+ "\n",
93
+ "VERSION = \"1.0.0\"\n",
94
+ "TIMESTAMP = datetime.now(timezone.utc).isoformat()\n",
95
+ "\n",
96
+ "print(f\"Source: {HF_REPO}/{HF_FILE}\")\n",
97
+ "print(f\"Version: {VERSION}\")"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "markdown",
102
+ "metadata": {},
103
+ "source": [
104
+ "## 2. Load Authoritative Dataset"
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "code",
109
+ "execution_count": 3,
110
+ "metadata": {
111
+ "execution": {
112
+ "iopub.execute_input": "2026-07-30T14:10:06.532965Z",
113
+ "iopub.status.busy": "2026-07-30T14:10:06.530781Z",
114
+ "iopub.status.idle": "2026-07-30T14:10:07.287661Z",
115
+ "shell.execute_reply": "2026-07-30T14:10:07.285564Z"
116
+ }
117
+ },
118
+ "outputs": [
119
+ {
120
+ "name": "stdout",
121
+ "output_type": "stream",
122
+ "text": [
123
+ "Downloaded to: /home/sara/.cache/huggingface/hub/datasets--saraNour--compliments-brand/snapshots/3d9e0ded3dba963c24c623d623b07dc94592c198/source_of_truth/products.parquet\n",
124
+ "Shape: 4440 rows x 16 columns\n"
125
+ ]
126
+ }
127
+ ],
128
+ "source": [
129
+ "path = hf_hub_download(HF_REPO, HF_FILE, repo_type=HF_REPO_TYPE)\n",
130
+ "print(f\"Downloaded to: {path}\")\n",
131
+ "\n",
132
+ "df = pd.read_parquet(path)\n",
133
+ "print(f\"Shape: {df.shape[0]} rows x {df.shape[1]} columns\")"
134
+ ]
135
+ },
136
+ {
137
+ "cell_type": "markdown",
138
+ "metadata": {},
139
+ "source": [
140
+ "## 3. Schema Inspection"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "code",
145
+ "execution_count": 4,
146
+ "metadata": {
147
+ "execution": {
148
+ "iopub.execute_input": "2026-07-30T14:10:07.294052Z",
149
+ "iopub.status.busy": "2026-07-30T14:10:07.292241Z",
150
+ "iopub.status.idle": "2026-07-30T14:10:07.351713Z",
151
+ "shell.execute_reply": "2026-07-30T14:10:07.347077Z"
152
+ }
153
+ },
154
+ "outputs": [
155
+ {
156
+ "name": "stdout",
157
+ "output_type": "stream",
158
+ "text": [
159
+ "=== Column Names ===\n",
160
+ " 1. upc str 3271 unique, 1 null\n",
161
+ " 2. external_id str 4440 unique, 0 null\n",
162
+ " 3. brand str 11 unique, 0 null\n",
163
+ " 4. title str 4375 unique, 0 null\n",
164
+ " 5. price float64 247 unique, 0 null\n",
165
+ " 6. price_currency str 1 unique, 0 null\n",
166
+ " 7. size str 631 unique, 0 null\n",
167
+ " 8. size_amount float64 387 unique, 307 null\n",
168
+ " 9. size_unit str 5 unique, 307 null\n",
169
+ " 10. size_qty int64 17 unique, 0 null\n",
170
+ " 11. size_per_unit object 0 unique, 4440 null\n",
171
+ " 12. size_unit_norm str 5 unique, 307 null\n",
172
+ " 13. size_total object 0 unique, 4440 null\n",
173
+ " 14. image_url str 4440 unique, 0 null\n",
174
+ " 15. source str 1 unique, 0 null\n",
175
+ " 16. source_url str 4440 unique, 0 null\n"
176
+ ]
177
+ }
178
+ ],
179
+ "source": [
180
+ "print(\"=== Column Names ===\")\n",
181
+ "for i, col in enumerate(df.columns):\n",
182
+ " print(f\" {i+1:2d}. {col:20s} {str(df[col].dtype):10s} {df[col].nunique():5d} unique, {df[col].isna().sum():5d} null\")"
183
+ ]
184
+ },
185
+ {
186
+ "cell_type": "code",
187
+ "execution_count": 5,
188
+ "metadata": {
189
+ "execution": {
190
+ "iopub.execute_input": "2026-07-30T14:10:07.366335Z",
191
+ "iopub.status.busy": "2026-07-30T14:10:07.365726Z",
192
+ "iopub.status.idle": "2026-07-30T14:10:07.420853Z",
193
+ "shell.execute_reply": "2026-07-30T14:10:07.418826Z"
194
+ }
195
+ },
196
+ "outputs": [
197
+ {
198
+ "name": "stdout",
199
+ "output_type": "stream",
200
+ "text": [
201
+ "=== First 3 Rows ===\n"
202
+ ]
203
+ },
204
+ {
205
+ "data": {
206
+ "text/html": [
207
+ "<div>\n",
208
+ "<style scoped>\n",
209
+ " .dataframe tbody tr th:only-of-type {\n",
210
+ " vertical-align: middle;\n",
211
+ " }\n",
212
+ "\n",
213
+ " .dataframe tbody tr th {\n",
214
+ " vertical-align: top;\n",
215
+ " }\n",
216
+ "\n",
217
+ " .dataframe thead th {\n",
218
+ " text-align: right;\n",
219
+ " }\n",
220
+ "</style>\n",
221
+ "<table border=\"1\" class=\"dataframe\">\n",
222
+ " <thead>\n",
223
+ " <tr style=\"text-align: right;\">\n",
224
+ " <th></th>\n",
225
+ " <th>upc</th>\n",
226
+ " <th>external_id</th>\n",
227
+ " <th>brand</th>\n",
228
+ " <th>title</th>\n",
229
+ " <th>price</th>\n",
230
+ " <th>price_currency</th>\n",
231
+ " <th>size</th>\n",
232
+ " <th>size_amount</th>\n",
233
+ " <th>size_unit</th>\n",
234
+ " <th>size_qty</th>\n",
235
+ " <th>size_per_unit</th>\n",
236
+ " <th>size_unit_norm</th>\n",
237
+ " <th>size_total</th>\n",
238
+ " <th>image_url</th>\n",
239
+ " <th>source</th>\n",
240
+ " <th>source_url</th>\n",
241
+ " </tr>\n",
242
+ " </thead>\n",
243
+ " <tbody>\n",
244
+ " <tr>\n",
245
+ " <th>0</th>\n",
246
+ " <td>055742522167</td>\n",
247
+ " <td>297608EA</td>\n",
248
+ " <td>Compliments</td>\n",
249
+ " <td>Compliments Cold And Sinus Plus 20 Count</td>\n",
250
+ " <td>9.99</td>\n",
251
+ " <td>CAD</td>\n",
252
+ " <td>20 per pack</td>\n",
253
+ " <td>20.000</td>\n",
254
+ " <td>count</td>\n",
255
+ " <td>1</td>\n",
256
+ " <td>None</td>\n",
257
+ " <td>count</td>\n",
258
+ " <td>None</td>\n",
259
+ " <td>https://voila.ca/images-v3/2d92d19c-0354-49c0-...</td>\n",
260
+ " <td>voila</td>\n",
261
+ " <td>https://voila.ca/products/compliments-cold-and...</td>\n",
262
+ " </tr>\n",
263
+ " <tr>\n",
264
+ " <th>1</th>\n",
265
+ " <td>055742335200</td>\n",
266
+ " <td>464324EA</td>\n",
267
+ " <td>Compliments</td>\n",
268
+ " <td>Compliments Twister T2 Light Bulbs 23W Soft Wh...</td>\n",
269
+ " <td>11.99</td>\n",
270
+ " <td>CAD</td>\n",
271
+ " <td>2 per pack</td>\n",
272
+ " <td>2.000</td>\n",
273
+ " <td>count</td>\n",
274
+ " <td>1</td>\n",
275
+ " <td>None</td>\n",
276
+ " <td>count</td>\n",
277
+ " <td>None</td>\n",
278
+ " <td>https://voila.ca/images-v3/2d92d19c-0354-49c0-...</td>\n",
279
+ " <td>voila</td>\n",
280
+ " <td>https://voila.ca/products/compliments-twister-...</td>\n",
281
+ " </tr>\n",
282
+ " <tr>\n",
283
+ " <th>2</th>\n",
284
+ " <td>990002600400</td>\n",
285
+ " <td>144288CS</td>\n",
286
+ " <td>Compliments</td>\n",
287
+ " <td>Compliments Zero Calorie Soft Drink 12 x 355ml</td>\n",
288
+ " <td>5.49</td>\n",
289
+ " <td>CAD</td>\n",
290
+ " <td>12 x 29.583ml</td>\n",
291
+ " <td>29.583</td>\n",
292
+ " <td>ml</td>\n",
293
+ " <td>12</td>\n",
294
+ " <td>None</td>\n",
295
+ " <td>ml</td>\n",
296
+ " <td>None</td>\n",
297
+ " <td>https://voila.ca/images-v3/2d92d19c-0354-49c0-...</td>\n",
298
+ " <td>voila</td>\n",
299
+ " <td>https://voila.ca/products/compliments-zero-cal...</td>\n",
300
+ " </tr>\n",
301
+ " </tbody>\n",
302
+ "</table>\n",
303
+ "</div>"
304
+ ],
305
+ "text/plain": [
306
+ " upc external_id brand \\\n",
307
+ "0 055742522167 297608EA Compliments \n",
308
+ "1 055742335200 464324EA Compliments \n",
309
+ "2 990002600400 144288CS Compliments \n",
310
+ "\n",
311
+ " title price price_currency \\\n",
312
+ "0 Compliments Cold And Sinus Plus 20 Count 9.99 CAD \n",
313
+ "1 Compliments Twister T2 Light Bulbs 23W Soft Wh... 11.99 CAD \n",
314
+ "2 Compliments Zero Calorie Soft Drink 12 x 355ml 5.49 CAD \n",
315
+ "\n",
316
+ " size size_amount size_unit size_qty size_per_unit \\\n",
317
+ "0 20 per pack 20.000 count 1 None \n",
318
+ "1 2 per pack 2.000 count 1 None \n",
319
+ "2 12 x 29.583ml 29.583 ml 12 None \n",
320
+ "\n",
321
+ " size_unit_norm size_total \\\n",
322
+ "0 count None \n",
323
+ "1 count None \n",
324
+ "2 ml None \n",
325
+ "\n",
326
+ " image_url source \\\n",
327
+ "0 https://voila.ca/images-v3/2d92d19c-0354-49c0-... voila \n",
328
+ "1 https://voila.ca/images-v3/2d92d19c-0354-49c0-... voila \n",
329
+ "2 https://voila.ca/images-v3/2d92d19c-0354-49c0-... voila \n",
330
+ "\n",
331
+ " source_url \n",
332
+ "0 https://voila.ca/products/compliments-cold-and... \n",
333
+ "1 https://voila.ca/products/compliments-twister-... \n",
334
+ "2 https://voila.ca/products/compliments-zero-cal... "
335
+ ]
336
+ },
337
+ "execution_count": 5,
338
+ "metadata": {},
339
+ "output_type": "execute_result"
340
+ }
341
+ ],
342
+ "source": [
343
+ "print(\"=== First 3 Rows ===\")\n",
344
+ "df.head(3)"
345
+ ]
346
+ },
347
+ {
348
+ "cell_type": "markdown",
349
+ "metadata": {},
350
+ "source": [
351
+ "## 4. Schema Validation"
352
+ ]
353
+ },
354
+ {
355
+ "cell_type": "code",
356
+ "execution_count": 6,
357
+ "metadata": {
358
+ "execution": {
359
+ "iopub.execute_input": "2026-07-30T14:10:07.429459Z",
360
+ "iopub.status.busy": "2026-07-30T14:10:07.427323Z",
361
+ "iopub.status.idle": "2026-07-30T14:10:07.447665Z",
362
+ "shell.execute_reply": "2026-07-30T14:10:07.443941Z"
363
+ }
364
+ },
365
+ "outputs": [
366
+ {
367
+ "name": "stdout",
368
+ "output_type": "stream",
369
+ "text": [
370
+ "Row count: 4440 (expected 4440) -> PASS\n",
371
+ "Columns: 16 (expected 16) -> PASS\n",
372
+ "Column order matches -> PASS\n"
373
+ ]
374
+ }
375
+ ],
376
+ "source": [
377
+ "EXPECTED_COLUMNS = [\n",
378
+ " \"upc\", \"external_id\", \"brand\", \"title\", \"price\", \"price_currency\",\n",
379
+ " \"size\", \"size_amount\", \"size_unit\", \"size_qty\", \"size_per_unit\",\n",
380
+ " \"size_unit_norm\", \"size_total\", \"image_url\", \"source\", \"source_url\",\n",
381
+ "]\n",
382
+ "\n",
383
+ "EXPECTED_ROW_COUNT = 4440\n",
384
+ "\n",
385
+ "# Check row count\n",
386
+ "row_check = len(df) == EXPECTED_ROW_COUNT\n",
387
+ "print(f\"Row count: {len(df)} (expected {EXPECTED_ROW_COUNT}) -> {'PASS' if row_check else 'FAIL'}\")\n",
388
+ "\n",
389
+ "# Check columns\n",
390
+ "missing = [c for c in EXPECTED_COLUMNS if c not in df.columns]\n",
391
+ "extra = [c for c in df.columns if c not in EXPECTED_COLUMNS]\n",
392
+ "col_check = len(missing) == 0\n",
393
+ "print(f\"Columns: {len(df.columns)} (expected {len(EXPECTED_COLUMNS)}) -> {'PASS' if col_check else 'FAIL'}\")\n",
394
+ "if missing:\n",
395
+ " print(f\" MISSING: {missing}\")\n",
396
+ "if extra:\n",
397
+ " print(f\" EXTRA: {extra}\")\n",
398
+ "\n",
399
+ "# Check column order\n",
400
+ "order_check = list(df.columns) == EXPECTED_COLUMNS\n",
401
+ "print(f\"Column order matches -> {'PASS' if order_check else 'FAIL'}\")"
402
+ ]
403
+ },
404
+ {
405
+ "cell_type": "markdown",
406
+ "metadata": {},
407
+ "source": [
408
+ "## 5. Null Analysis"
409
+ ]
410
+ },
411
+ {
412
+ "cell_type": "code",
413
+ "execution_count": 7,
414
+ "metadata": {
415
+ "execution": {
416
+ "iopub.execute_input": "2026-07-30T14:10:07.455038Z",
417
+ "iopub.status.busy": "2026-07-30T14:10:07.452399Z",
418
+ "iopub.status.idle": "2026-07-30T14:10:07.499238Z",
419
+ "shell.execute_reply": "2026-07-30T14:10:07.496950Z"
420
+ }
421
+ },
422
+ "outputs": [
423
+ {
424
+ "name": "stdout",
425
+ "output_type": "stream",
426
+ "text": [
427
+ "=== Null Analysis ===\n",
428
+ " upc : 1 nulls ( 0.0%)\n",
429
+ " size_amount : 307 nulls ( 6.9%)\n",
430
+ " size_unit : 307 nulls ( 6.9%)\n",
431
+ " size_per_unit : 4440 nulls (100.0%) *** 100% NULL ***\n",
432
+ " size_unit_norm : 307 nulls ( 6.9%)\n",
433
+ " size_total : 4440 nulls (100.0%) *** 100% NULL ***\n"
434
+ ]
435
+ },
436
+ {
437
+ "data": {
438
+ "text/html": [
439
+ "<div>\n",
440
+ "<style scoped>\n",
441
+ " .dataframe tbody tr th:only-of-type {\n",
442
+ " vertical-align: middle;\n",
443
+ " }\n",
444
+ "\n",
445
+ " .dataframe tbody tr th {\n",
446
+ " vertical-align: top;\n",
447
+ " }\n",
448
+ "\n",
449
+ " .dataframe thead th {\n",
450
+ " text-align: right;\n",
451
+ " }\n",
452
+ "</style>\n",
453
+ "<table border=\"1\" class=\"dataframe\">\n",
454
+ " <thead>\n",
455
+ " <tr style=\"text-align: right;\">\n",
456
+ " <th></th>\n",
457
+ " <th>column</th>\n",
458
+ " <th>null_count</th>\n",
459
+ " <th>null_pct</th>\n",
460
+ " </tr>\n",
461
+ " </thead>\n",
462
+ " <tbody>\n",
463
+ " <tr>\n",
464
+ " <th>0</th>\n",
465
+ " <td>upc</td>\n",
466
+ " <td>1</td>\n",
467
+ " <td>0.02</td>\n",
468
+ " </tr>\n",
469
+ " <tr>\n",
470
+ " <th>7</th>\n",
471
+ " <td>size_amount</td>\n",
472
+ " <td>307</td>\n",
473
+ " <td>6.91</td>\n",
474
+ " </tr>\n",
475
+ " <tr>\n",
476
+ " <th>8</th>\n",
477
+ " <td>size_unit</td>\n",
478
+ " <td>307</td>\n",
479
+ " <td>6.91</td>\n",
480
+ " </tr>\n",
481
+ " <tr>\n",
482
+ " <th>10</th>\n",
483
+ " <td>size_per_unit</td>\n",
484
+ " <td>4440</td>\n",
485
+ " <td>100.00</td>\n",
486
+ " </tr>\n",
487
+ " <tr>\n",
488
+ " <th>11</th>\n",
489
+ " <td>size_unit_norm</td>\n",
490
+ " <td>307</td>\n",
491
+ " <td>6.91</td>\n",
492
+ " </tr>\n",
493
+ " <tr>\n",
494
+ " <th>12</th>\n",
495
+ " <td>size_total</td>\n",
496
+ " <td>4440</td>\n",
497
+ " <td>100.00</td>\n",
498
+ " </tr>\n",
499
+ " </tbody>\n",
500
+ "</table>\n",
501
+ "</div>"
502
+ ],
503
+ "text/plain": [
504
+ " column null_count null_pct\n",
505
+ "0 upc 1 0.02\n",
506
+ "7 size_amount 307 6.91\n",
507
+ "8 size_unit 307 6.91\n",
508
+ "10 size_per_unit 4440 100.00\n",
509
+ "11 size_unit_norm 307 6.91\n",
510
+ "12 size_total 4440 100.00"
511
+ ]
512
+ },
513
+ "execution_count": 7,
514
+ "metadata": {},
515
+ "output_type": "execute_result"
516
+ }
517
+ ],
518
+ "source": [
519
+ "print(\"=== Null Analysis ===\")\n",
520
+ "null_data = []\n",
521
+ "for col in df.columns:\n",
522
+ " n = df[col].isna().sum()\n",
523
+ " pct = round(n / len(df) * 100, 2)\n",
524
+ " flag = \" *** 100% NULL ***\" if n == len(df) else \"\"\n",
525
+ " null_data.append({\"column\": col, \"null_count\": n, \"null_pct\": pct})\n",
526
+ " if n > 0:\n",
527
+ " print(f\" {col:20s}: {n:5d} nulls ({pct:5.1f}%){flag}\")\n",
528
+ "\n",
529
+ "null_df = pd.DataFrame(null_data)\n",
530
+ "null_df[null_df[\"null_count\"] > 0]"
531
+ ]
532
+ },
533
+ {
534
+ "cell_type": "code",
535
+ "execution_count": 8,
536
+ "metadata": {
537
+ "execution": {
538
+ "iopub.execute_input": "2026-07-30T14:10:07.502778Z",
539
+ "iopub.status.busy": "2026-07-30T14:10:07.502373Z",
540
+ "iopub.status.idle": "2026-07-30T14:10:07.509460Z",
541
+ "shell.execute_reply": "2026-07-30T14:10:07.507432Z"
542
+ }
543
+ },
544
+ "outputs": [
545
+ {
546
+ "name": "stdout",
547
+ "output_type": "stream",
548
+ "text": [
549
+ "Columns that are 100% null: ['size_per_unit', 'size_total']\n",
550
+ "These will be DROPPED in Phase 1 output.\n"
551
+ ]
552
+ }
553
+ ],
554
+ "source": [
555
+ "# Identify 100% null columns\n",
556
+ "cols_100pct_null = [row[\"column\"] for row in null_data if row[\"null_count\"] == len(df)]\n",
557
+ "print(f\"Columns that are 100% null: {cols_100pct_null}\")\n",
558
+ "print(f\"These will be DROPPED in Phase 1 output.\")"
559
+ ]
560
+ },
561
+ {
562
+ "cell_type": "markdown",
563
+ "metadata": {},
564
+ "source": [
565
+ "## 6. Duplicate Analysis"
566
+ ]
567
+ },
568
+ {
569
+ "cell_type": "code",
570
+ "execution_count": 9,
571
+ "metadata": {
572
+ "execution": {
573
+ "iopub.execute_input": "2026-07-30T14:10:07.514613Z",
574
+ "iopub.status.busy": "2026-07-30T14:10:07.514058Z",
575
+ "iopub.status.idle": "2026-07-30T14:10:07.555136Z",
576
+ "shell.execute_reply": "2026-07-30T14:10:07.553337Z"
577
+ }
578
+ },
579
+ "outputs": [
580
+ {
581
+ "name": "stdout",
582
+ "output_type": "stream",
583
+ "text": [
584
+ "=== Duplicate Analysis ===\n",
585
+ "Full row duplicates: 0 -> PASS\n",
586
+ "UPC duplicates: 1168 (expected: some UPCs reused across variants)\n",
587
+ "external_id dupes: 0 -> PASS\n"
588
+ ]
589
+ }
590
+ ],
591
+ "source": [
592
+ "print(\"=== Duplicate Analysis ===\")\n",
593
+ "full_dupes = df.duplicated().sum()\n",
594
+ "upc_dupes = df[\"upc\"].duplicated().sum()\n",
595
+ "ext_dupes = df[\"external_id\"].duplicated().sum()\n",
596
+ "\n",
597
+ "print(f\"Full row duplicates: {full_dupes} -> {'PASS' if full_dupes == 0 else 'FAIL'}\")\n",
598
+ "print(f\"UPC duplicates: {upc_dupes} (expected: some UPCs reused across variants)\")\n",
599
+ "print(f\"external_id dupes: {ext_dupes} -> {'PASS' if ext_dupes == 0 else 'FAIL'}\")"
600
+ ]
601
+ },
602
+ {
603
+ "cell_type": "code",
604
+ "execution_count": 10,
605
+ "metadata": {
606
+ "execution": {
607
+ "iopub.execute_input": "2026-07-30T14:10:07.568353Z",
608
+ "iopub.status.busy": "2026-07-30T14:10:07.567601Z",
609
+ "iopub.status.idle": "2026-07-30T14:10:07.706138Z",
610
+ "shell.execute_reply": "2026-07-30T14:10:07.704546Z"
611
+ }
612
+ },
613
+ "outputs": [
614
+ {
615
+ "name": "stdout",
616
+ "output_type": "stream",
617
+ "text": [
618
+ "=== Reused UPCs (top 10) ==="
619
+ ]
620
+ },
621
+ {
622
+ "name": "stdout",
623
+ "output_type": "stream",
624
+ "text": [
625
+ "\n",
626
+ "Total reused UPCs: 774\n",
627
+ "\n",
628
+ "UPC 055742560770 (10 rows):\n",
629
+ " 403517EA | Compliments Naturally Simple Turkey Jerky 80 g | Compliments Naturally Simple\n",
630
+ " 416081EA | Compliments Naturally Simple Crisp Crackers Multigrain 150 g | Compliments\n",
631
+ " 591909EA | Compliments Naturally Simple Yogurt Tzatziki Cucumber Dip 22 | Compliments Naturally Simple\n",
632
+ " 498164EA | Compliments Naturally Simple Oven Roasted Chicken Breast 175 | Compliments Naturally Simple\n",
633
+ " 645109EA | Compliments Naturally Simple Rice Pilaf Wild Mushroom & Herb | Compliments\n",
634
+ " 403516EA | Compliments Naturally Simple Beef Jerky Teriyaki 80 g | Compliments Naturally Simple\n",
635
+ " 417169EA | Compliments Naturally Simple Tortilla Chips Beet And Corn 19 | Compliments\n",
636
+ " 680994EA | Compliments Naturally Simple Baking Mix Scone 500 g | Compliments Naturally Simple\n",
637
+ " 483867EA | Compliments Naturally Simple Crystal Mountain Trail Mix 400 | Compliments Naturally Simple\n",
638
+ " 403515EA | Compliments Naturally Simple Beef Jerky Original 80 g | Compliments Naturally Simple\n",
639
+ "\n",
640
+ "UPC 055742501926 (9 rows):\n",
641
+ " 316959EA | Compliments Gift Wrap Roll 40 Inch 90 Feet | Compliments\n",
642
+ " 316979EA | Compliments Foil Gift Wrap 30 Inch 12 Feet | Compliments\n",
643
+ " 657871EA | Compliments 12 Inch Pizza Pan | Compliments\n",
644
+ " 24142EA | Compliments Foil Containers With Lids 4 lb 3 Pack | Compliments\n",
645
+ " 229086EA | Compliments Aluminum Foil 12 Inch x 50 Feet | Compliments\n",
646
+ " 20712EA | Compliments Trendy Gift Wrap 30-Inch x 57.5-Inch 1 Count | Compliments\n",
647
+ " 24107EA | Compliments Foil Containers with Lids 3-lb 5 Pack | Compliments\n",
648
+ " 24066EA | Compliments Foil Containers with Lids 2-lb 7 Pack | Compliments\n",
649
+ " 316963EA | Compliments Paper Gift Wrap 30 Inch 45 Feet | Compliments\n",
650
+ "\n",
651
+ "UPC 055742562125 (8 rows):\n",
652
+ " 539754EA | Compliments JuJubes 350 g | Compliments\n",
653
+ " 957046EA | Compliments Jujubes 700 g | Compliments\n",
654
+ " 590556EA | Compliments Candy Jujubes 750 g | Compliments\n",
655
+ " 990350EA | Compliments Candy Jujubes Bunnies 325 g | Compliments\n",
656
+ " 647037EA | Compliments Candy Jujubes 800 g | Compliments\n",
657
+ " 590552EA | Compliments Candy Jujubes 175 g | Compliments\n",
658
+ " 647035EA | Compliments Candy Jujubes 200 g | Compliments\n",
659
+ " 587575EA | Compliments Candy Jujubes 350 g | Compliments\n",
660
+ "\n",
661
+ "UPC 055742567861 (8 rows):\n",
662
+ " 528968EA | Compliments Terry Kitchen Towels Yellow 2 Count | Compliments\n",
663
+ " 528414EA | Compliments Charcoal Kitchen Towels 2 EA | Compliments\n",
664
+ " 528962EA | Compliments Terry Kitchen Towels Mint 2 EA | Compliments\n",
665
+ " 528901EA | Compliments Terry Kitchen Towels Charcoal 2 Count | Compliments\n",
666
+ " 851246EA | Compliments Paper Towels Greencare Recycled 6 x 105 Sheets R | Compliments\n",
667
+ " 528448EA | Compliments Yellow Kitchen Towels 2 EA | Compliments\n",
668
+ " 528372EA | Compliments Beige Kitchen Towels 2 EA | Compliments\n",
669
+ " 322530EA | Compliments Paper Towels Green Care Half Size Sheets 90 x 6 | Compliments\n",
670
+ "\n",
671
+ "UPC 055742375534 (7 rows):\n",
672
+ " 158294EA | Compliments Juice Strawberry 2 L (bottle) | Compliments\n",
673
+ " 545616EA | Compliments Juice Peach & Passion Fruit With Coconut Water 1 | Compliments\n"
674
+ ]
675
+ },
676
+ {
677
+ "name": "stdout",
678
+ "output_type": "stream",
679
+ "text": [
680
+ " 521832EA | Compliments Juice Apples & Greens 1.65 L | Compliments\n",
681
+ " 158317EA | Compliments Juice Peach Drink 2 L (bottle) | Compliments\n",
682
+ " 656001EA | Compliments Juice Pineapple With Coconut Water 1.54 L (Bottl | Compliments\n",
683
+ " 656003EA | Compliments Juice Peach Passion Fruit Coconut Blend 1.54 L ( | Compliments\n",
684
+ " 521839EA | Compliments Juice Beet & Cherry 1.65 L | Compliments\n",
685
+ "\n",
686
+ "UPC 055742516111 (7 rows):\n",
687
+ " 351251EA | Compliments Plastic Bandages 100 EA | Compliments\n",
688
+ " 298558EA | Compliments Plastic Beer Cup Red 16-Ounce 50 Pack | Compliments\n",
689
+ " 485028EA | Compliments Plastic Forks White Full Size 24 Pack | Compliments\n",
690
+ " 265142EA | Compliments Plastic Wrap 90 m | Compliments\n",
691
+ " 485150EA | Compliments Plastic Wine Glasses 5 Ounce 10 Pack | Compliments\n",
692
+ " 486882EA | Compliments Plastic Beverage Glasses 7 Ounce Small 50 Pack | Compliments\n",
693
+ " 280774EA | Compliments Plastic Lice Combs 3 EA | Compliments\n",
694
+ "\n"
695
+ ]
696
+ },
697
+ {
698
+ "name": "stdout",
699
+ "output_type": "stream",
700
+ "text": [
701
+ "UPC 055742335729 (7 rows):\n",
702
+ " 613615EA | Compliments Soft Drink Grape 355 ml | Compliments\n",
703
+ " 281039EA | Compliments Soft Drink Cola 355 ml | Compliments\n",
704
+ " 613604CS | Compliments Soft Drink Cola Blue 12 x 355 ml | Compliments\n",
705
+ " 613615CS | Compliments Soft Drink Grape 12 x 355 ml (cans) | Compliments\n",
706
+ " 503191EA | Compliments Soft Drink Cola 2 L (bottle) | Compliments\n",
707
+ " 613604EA | Compliments Soft Drink Blue Cola 355 ml (can) | Compliments\n",
708
+ " 281039CS | Compliments Soft Drink Cola 12 x 355 ml (cans) | Compliments\n",
709
+ "\n",
710
+ "UPC 055742502756 (6 rows):\n",
711
+ " 587512EA | Compliments Party Bites Snack Mix 150 g | Compliments\n",
712
+ " 259864EA | Compliments Party Bites Snack Mix 225 g | Compliments\n",
713
+ " 231864EA | Compliments Snack Party Mix 300 g | Compliments\n",
714
+ " 587555EA | Compliments Manhattan Snack Mix 375 g | Compliments\n",
715
+ " 259901EA | Compliments Manhattan Snack Mix 500 g | Compliments\n",
716
+ " 958118EA | Compliments Snack Cheddar Cheese Corn 482 g | Compliments\n",
717
+ "\n",
718
+ "UPC 055742581607 (6 rows):\n",
719
+ " 259875EA | Compliments Walnut Pieces 250 g | Compliments\n",
720
+ " 584540EA | Compliments Walnut Pieces 150 g | Compliments\n",
721
+ " 421729EA | Compliments Walnut Pieces 400 g | Compliments\n",
722
+ " 259934EA | Compliments Dried Mango Slices 325 g | Compliments\n",
723
+ " 480211EA | Compliments Walnut Pieces 750 g | Compliments\n",
724
+ " 1341149EA | Compliments Walnut Pieces 350 g | Compliments\n",
725
+ "\n",
726
+ "UPC 055742575361 (6 rows):\n",
727
+ " 287328EA | Compliments Apple Caramel Coffee Cake 850 g (frozen) | Compliments\n",
728
+ " 636356EA | Compliments Cinnamon Coffee Cake 500 g | Compliments\n",
729
+ " 27846EA | Compliments Muffin Cinnamon Coffee Cake 400 g | Compliments\n",
730
+ " 408285EA | Compliments Muffins Cinnamon Swirl Coffee Cake 440 g | Compliments\n",
731
+ " 657878EA | Compliments Muffin Pan 12 Cup 1 Pack | Compliments\n",
732
+ " 636353EA | Compliments Coffee Cake Cinnamon 500 g | Compliments\n",
733
+ "\n"
734
+ ]
735
+ }
736
+ ],
737
+ "source": [
738
+ "# Show reused UPCs (products sharing the same barcode)\n",
739
+ "print(\"=== Reused UPCs (top 10) ===\")\n",
740
+ "upc_counts = df[\"upc\"].value_counts()\n",
741
+ "reused = upc_counts[upc_counts > 1]\n",
742
+ "print(f\"Total reused UPCs: {len(reused)}\")\n",
743
+ "print()\n",
744
+ "for upc, count in reused.head(10).items():\n",
745
+ " subset = df[df[\"upc\"] == upc][[\"external_id\", \"title\", \"brand\"]]\n",
746
+ " print(f\"UPC {upc} ({count} rows):\")\n",
747
+ " for _, row in subset.iterrows():\n",
748
+ " print(f\" {row['external_id']:12s} | {row['title'][:60]:60s} | {row['brand']}\")\n",
749
+ " print()"
750
+ ]
751
+ },
752
+ {
753
+ "cell_type": "markdown",
754
+ "metadata": {},
755
+ "source": [
756
+ "## 7. UPC Analysis"
757
+ ]
758
+ },
759
+ {
760
+ "cell_type": "code",
761
+ "execution_count": 11,
762
+ "metadata": {
763
+ "execution": {
764
+ "iopub.execute_input": "2026-07-30T14:10:07.715374Z",
765
+ "iopub.status.busy": "2026-07-30T14:10:07.714883Z",
766
+ "iopub.status.idle": "2026-07-30T14:10:07.739920Z",
767
+ "shell.execute_reply": "2026-07-30T14:10:07.736865Z"
768
+ }
769
+ },
770
+ "outputs": [
771
+ {
772
+ "name": "stdout",
773
+ "output_type": "stream",
774
+ "text": [
775
+ "=== UPC Analysis ==="
776
+ ]
777
+ },
778
+ {
779
+ "name": "stdout",
780
+ "output_type": "stream",
781
+ "text": [
782
+ "\n",
783
+ "Total rows: 4440\n",
784
+ "Null UPCs: 1\n",
785
+ "Unique UPCs: 3271\n",
786
+ "Reused UPCs: 774\n",
787
+ "\n",
788
+ "Rows with null UPC:\n",
789
+ " external_id title brand\n",
790
+ "406 24061EA Compliments Foil Containers with Lids 5-lb 2 Pack Compliments\n"
791
+ ]
792
+ }
793
+ ],
794
+ "source": [
795
+ "print(\"=== UPC Analysis ===\")\n",
796
+ "print(f\"Total rows: {len(df)}\")\n",
797
+ "print(f\"Null UPCs: {df['upc'].isna().sum()}\")\n",
798
+ "print(f\"Unique UPCs: {df['upc'].nunique()}\")\n",
799
+ "print(f\"Reused UPCs: {len(reused)}\")\n",
800
+ "print()\n",
801
+ "\n",
802
+ "# Show rows with null UPC\n",
803
+ "null_upc = df[df[\"upc\"].isna()]\n",
804
+ "if len(null_upc) > 0:\n",
805
+ " print(\"Rows with null UPC:\")\n",
806
+ " print(null_upc[[\"external_id\", \"title\", \"brand\"]].to_string())"
807
+ ]
808
+ },
809
+ {
810
+ "cell_type": "markdown",
811
+ "metadata": {},
812
+ "source": [
813
+ "## 8. Brand Analysis"
814
+ ]
815
+ },
816
+ {
817
+ "cell_type": "code",
818
+ "execution_count": 12,
819
+ "metadata": {
820
+ "execution": {
821
+ "iopub.execute_input": "2026-07-30T14:10:07.746876Z",
822
+ "iopub.status.busy": "2026-07-30T14:10:07.744331Z",
823
+ "iopub.status.idle": "2026-07-30T14:10:07.762909Z",
824
+ "shell.execute_reply": "2026-07-30T14:10:07.759922Z"
825
+ }
826
+ },
827
+ "outputs": [
828
+ {
829
+ "name": "stdout",
830
+ "output_type": "stream",
831
+ "text": [
832
+ "=== Brand Distribution ===\n",
833
+ "Unique brands: 11\n",
834
+ "\n",
835
+ " Compliments : 4170 ( 93.9%)\n",
836
+ " Compliments Organic : 90 ( 2.0%)\n",
837
+ " Compliments : 87 ( 2.0%)\n",
838
+ " Compliments Balance : 42 ( 0.9%)\n",
839
+ " Compliments Naturally Simple : 25 ( 0.6%)\n",
840
+ " Sensations : 8 ( 0.2%)\n",
841
+ " Compliments Little Ones : 7 ( 0.2%)\n",
842
+ " Compliments Green Care : 5 ( 0.1%)\n",
843
+ " Compliments Green : 4 ( 0.1%)\n",
844
+ " COMPLIMENTS : 1 ( 0.0%)\n",
845
+ " Compliments : 1 ( 0.0%)\n"
846
+ ]
847
+ }
848
+ ],
849
+ "source": [
850
+ "print(\"=== Brand Distribution ===\")\n",
851
+ "brand_counts = df[\"brand\"].value_counts()\n",
852
+ "print(f\"Unique brands: {len(brand_counts)}\")\n",
853
+ "print()\n",
854
+ "for brand, count in brand_counts.items():\n",
855
+ " print(f\" {brand:35s}: {count:5d} ({count/len(df)*100:5.1f}%)\")"
856
+ ]
857
+ },
858
+ {
859
+ "cell_type": "markdown",
860
+ "metadata": {},
861
+ "source": [
862
+ "## 9. Size Analysis"
863
+ ]
864
+ },
865
+ {
866
+ "cell_type": "code",
867
+ "execution_count": 13,
868
+ "metadata": {
869
+ "execution": {
870
+ "iopub.execute_input": "2026-07-30T14:10:07.771584Z",
871
+ "iopub.status.busy": "2026-07-30T14:10:07.769398Z",
872
+ "iopub.status.idle": "2026-07-30T14:10:07.814398Z",
873
+ "shell.execute_reply": "2026-07-30T14:10:07.801004Z"
874
+ }
875
+ },
876
+ "outputs": [
877
+ {
878
+ "name": "stdout",
879
+ "output_type": "stream",
880
+ "text": [
881
+ "=== Size String Analysis ===\n",
882
+ "Unique size strings: 631\n",
883
+ "Null size strings: 0\n",
884
+ "\n",
885
+ "Top 20 size strings:\n",
886
+ " : 215\n",
887
+ " 400g : 133\n",
888
+ " 200g : 117\n",
889
+ " 500g : 111\n",
890
+ " 300g : 92\n",
891
+ " 454g : 69\n",
892
+ " 250g : 67\n",
893
+ " 1kg : 65\n",
894
+ " 450g : 64\n",
895
+ " 1L : 61\n",
896
+ " 340g : 59\n",
897
+ " 398ml : 59\n",
898
+ " 600g : 58\n",
899
+ " 12 per pack : 57\n",
900
+ " 100g : 56\n",
901
+ " 900g : 55\n",
902
+ " 375g : 54\n",
903
+ " 350g : 50\n",
904
+ " 680g : 50\n",
905
+ " 175g : 46\n"
906
+ ]
907
+ }
908
+ ],
909
+ "source": [
910
+ "print(\"=== Size String Analysis ===\")\n",
911
+ "print(f\"Unique size strings: {df['size'].nunique()}\")\n",
912
+ "print(f\"Null size strings: {df['size'].isna().sum()}\")\n",
913
+ "print()\n",
914
+ "print(\"Top 20 size strings:\")\n",
915
+ "for size, count in df[\"size\"].value_counts().head(20).items():\n",
916
+ " print(f\" {size:20s}: {count:5d}\")"
917
+ ]
918
+ },
919
+ {
920
+ "cell_type": "code",
921
+ "execution_count": 14,
922
+ "metadata": {
923
+ "execution": {
924
+ "iopub.execute_input": "2026-07-30T14:10:07.830225Z",
925
+ "iopub.status.busy": "2026-07-30T14:10:07.829314Z",
926
+ "iopub.status.idle": "2026-07-30T14:10:07.857284Z",
927
+ "shell.execute_reply": "2026-07-30T14:10:07.853647Z"
928
+ }
929
+ },
930
+ "outputs": [
931
+ {
932
+ "name": "stdout",
933
+ "output_type": "stream",
934
+ "text": [
935
+ "=== Size Amount Analysis ===\n",
936
+ "Null count: 307 (6.9%)\n",
937
+ "Non-null: 4133\n",
938
+ "Min: 0.27\n",
939
+ "Max: 980.0\n",
940
+ "Mean: 283.52\n",
941
+ "Median: 250.00\n",
942
+ "\n",
943
+ "=== Size Unit Distribution ===\n"
944
+ ]
945
+ },
946
+ {
947
+ "name": "stdout",
948
+ "output_type": "stream",
949
+ "text": [
950
+ "Null count: 307 (6.9%)\n",
951
+ " g : 2312\n",
952
+ " ml : 737\n",
953
+ " count : 568\n",
954
+ " L : 261\n",
955
+ " kg : 255\n"
956
+ ]
957
+ }
958
+ ],
959
+ "source": [
960
+ "print(\"=== Size Amount Analysis ===\")\n",
961
+ "sa = df[\"size_amount\"]\n",
962
+ "print(f\"Null count: {sa.isna().sum()} ({sa.isna().sum()/len(df)*100:.1f}%)\")\n",
963
+ "print(f\"Non-null: {sa.notna().sum()}\")\n",
964
+ "if sa.notna().any():\n",
965
+ " print(f\"Min: {sa.min()}\")\n",
966
+ " print(f\"Max: {sa.max()}\")\n",
967
+ " print(f\"Mean: {sa.mean():.2f}\")\n",
968
+ " print(f\"Median: {sa.median():.2f}\")\n",
969
+ "print()\n",
970
+ "print(\"=== Size Unit Distribution ===\")\n",
971
+ "su = df[\"size_unit\"]\n",
972
+ "print(f\"Null count: {su.isna().sum()} ({su.isna().sum()/len(df)*100:.1f}%)\")\n",
973
+ "for unit, count in su.value_counts().items():\n",
974
+ " print(f\" {unit:10s}: {count:5d}\")"
975
+ ]
976
+ },
977
+ {
978
+ "cell_type": "markdown",
979
+ "metadata": {},
980
+ "source": [
981
+ "## 10. Price Analysis"
982
+ ]
983
+ },
984
+ {
985
+ "cell_type": "code",
986
+ "execution_count": 15,
987
+ "metadata": {
988
+ "execution": {
989
+ "iopub.execute_input": "2026-07-30T14:10:07.864737Z",
990
+ "iopub.status.busy": "2026-07-30T14:10:07.862359Z",
991
+ "iopub.status.idle": "2026-07-30T14:10:07.884735Z",
992
+ "shell.execute_reply": "2026-07-30T14:10:07.881977Z"
993
+ }
994
+ },
995
+ "outputs": [
996
+ {
997
+ "name": "stdout",
998
+ "output_type": "stream",
999
+ "text": [
1000
+ "=== Price Analysis ==="
1001
+ ]
1002
+ },
1003
+ {
1004
+ "name": "stdout",
1005
+ "output_type": "stream",
1006
+ "text": [
1007
+ "\n",
1008
+ "Null count: 0\n",
1009
+ "Min: $0.01\n",
1010
+ "Max: $2152.80\n",
1011
+ "Mean: $7.56\n",
1012
+ "Median: $5.79\n",
1013
+ "Unique: 247\n",
1014
+ "Currency: <ArrowStringArray>\n",
1015
+ "['CAD']\n",
1016
+ "Length: 1, dtype: str\n"
1017
+ ]
1018
+ }
1019
+ ],
1020
+ "source": [
1021
+ "print(\"=== Price Analysis ===\")\n",
1022
+ "p = df[\"price\"]\n",
1023
+ "print(f\"Null count: {p.isna().sum()}\")\n",
1024
+ "print(f\"Min: ${p.min():.2f}\")\n",
1025
+ "print(f\"Max: ${p.max():.2f}\")\n",
1026
+ "print(f\"Mean: ${p.mean():.2f}\")\n",
1027
+ "print(f\"Median: ${p.median():.2f}\")\n",
1028
+ "print(f\"Unique: {p.nunique()}\")\n",
1029
+ "print(f\"Currency: {df['price_currency'].unique()}\")"
1030
+ ]
1031
+ },
1032
+ {
1033
+ "cell_type": "markdown",
1034
+ "metadata": {},
1035
+ "source": [
1036
+ "## 11. Provenance Documentation"
1037
+ ]
1038
+ },
1039
+ {
1040
+ "cell_type": "code",
1041
+ "execution_count": 16,
1042
+ "metadata": {
1043
+ "execution": {
1044
+ "iopub.execute_input": "2026-07-30T14:10:07.891601Z",
1045
+ "iopub.status.busy": "2026-07-30T14:10:07.889144Z",
1046
+ "iopub.status.idle": "2026-07-30T14:10:07.904981Z",
1047
+ "shell.execute_reply": "2026-07-30T14:10:07.902154Z"
1048
+ }
1049
+ },
1050
+ "outputs": [
1051
+ {
1052
+ "name": "stdout",
1053
+ "output_type": "stream",
1054
+ "text": [
1055
+ "{\n",
1056
+ " \"source_dataset\": \"saraNour/compliments-brand/source_of_truth/products.parquet\",\n",
1057
+ " \"source_url\": \"https://huggingface.co/datasets/saraNour/compliments-brand/blob/main/source_of_truth/products.parquet\",\n",
1058
+ " \"source_type\": \"HuggingFace dataset (private)\",\n",
1059
+ " \"original_source\": \"Voila.ca (Loblaw) Compliments private-label products\",\n",
1060
+ " \"columns\": {\n",
1061
+ " \"upc\": \"Universal Product Code. Some reused across variants.\",\n",
1062
+ " \"external_id\": \"Voila retailer product ID. Unique per row.\",\n",
1063
+ " \"brand\": \"Product brand. 11 variants of Compliments/Sensations.\",\n",
1064
+ " \"title\": \"Raw product title from Voila.\",\n",
1065
+ " \"price\": \"Price in CAD.\",\n",
1066
+ " \"price_currency\": \"Always 'CAD'.\",\n",
1067
+ " \"size\": \"Raw size string from Voila.\",\n",
1068
+ " \"size_amount\": \"Parsed numeric size amount.\",\n",
1069
+ " \"size_unit\": \"Parsed size unit (g, kg, ml, l, count).\",\n",
1070
+ " \"size_qty\": \"Size quantity multiplier. Usually 1.\",\n",
1071
+ " \"size_per_unit\": \"100% null. DROPPED.\",\n",
1072
+ " \"size_unit_norm\": \"Normalized size unit.\",\n",
1073
+ " \"size_total\": \"100% null. DROPPED.\",\n",
1074
+ " \"image_url\": \"Product image URL from Voila.\",\n",
1075
+ " \"source\": \"Always 'voila'.\",\n",
1076
+ " \"source_url\": \"Product page URL on Voila.\"\n",
1077
+ " },\n",
1078
+ " \"dropped_columns\": [\n",
1079
+ " {\n",
1080
+ " \"column\": \"size_per_unit\",\n",
1081
+ " \"reason\": \"100% null (4,440/4,440 values are NaN).\"\n",
1082
+ " },\n",
1083
+ " {\n",
1084
+ " \"column\": \"size_total\",\n",
1085
+ " \"reason\": \"100% null (4,440/4,440 values are NaN).\"\n",
1086
+ " }\n",
1087
+ " ]\n",
1088
+ "}\n"
1089
+ ]
1090
+ }
1091
+ ],
1092
+ "source": [
1093
+ "provenance = {\n",
1094
+ " \"source_dataset\": f\"{HF_REPO}/{HF_FILE}\",\n",
1095
+ " \"source_url\": f\"https://huggingface.co/datasets/{HF_REPO}/blob/main/{HF_FILE}\",\n",
1096
+ " \"source_type\": \"HuggingFace dataset (private)\",\n",
1097
+ " \"original_source\": \"Voila.ca (Loblaw) Compliments private-label products\",\n",
1098
+ " \"columns\": {\n",
1099
+ " \"upc\": \"Universal Product Code. Some reused across variants.\",\n",
1100
+ " \"external_id\": \"Voila retailer product ID. Unique per row.\",\n",
1101
+ " \"brand\": \"Product brand. 11 variants of Compliments/Sensations.\",\n",
1102
+ " \"title\": \"Raw product title from Voila.\",\n",
1103
+ " \"price\": \"Price in CAD.\",\n",
1104
+ " \"price_currency\": \"Always 'CAD'.\",\n",
1105
+ " \"size\": \"Raw size string from Voila.\",\n",
1106
+ " \"size_amount\": \"Parsed numeric size amount.\",\n",
1107
+ " \"size_unit\": \"Parsed size unit (g, kg, ml, l, count).\",\n",
1108
+ " \"size_qty\": \"Size quantity multiplier. Usually 1.\",\n",
1109
+ " \"size_per_unit\": \"100% null. DROPPED.\",\n",
1110
+ " \"size_unit_norm\": \"Normalized size unit.\",\n",
1111
+ " \"size_total\": \"100% null. DROPPED.\",\n",
1112
+ " \"image_url\": \"Product image URL from Voila.\",\n",
1113
+ " \"source\": \"Always 'voila'.\",\n",
1114
+ " \"source_url\": \"Product page URL on Voila.\",\n",
1115
+ " },\n",
1116
+ " \"dropped_columns\": [\n",
1117
+ " {\"column\": \"size_per_unit\", \"reason\": \"100% null (4,440/4,440 values are NaN).\"},\n",
1118
+ " {\"column\": \"size_total\", \"reason\": \"100% null (4,440/4,440 values are NaN).\"},\n",
1119
+ " ],\n",
1120
+ "}\n",
1121
+ "print(json.dumps(provenance, indent=2))"
1122
+ ]
1123
+ },
1124
+ {
1125
+ "cell_type": "markdown",
1126
+ "metadata": {},
1127
+ "source": [
1128
+ "## 12. Build Outputs"
1129
+ ]
1130
+ },
1131
+ {
1132
+ "cell_type": "code",
1133
+ "execution_count": 17,
1134
+ "metadata": {
1135
+ "execution": {
1136
+ "iopub.execute_input": "2026-07-30T14:10:07.909755Z",
1137
+ "iopub.status.busy": "2026-07-30T14:10:07.909177Z",
1138
+ "iopub.status.idle": "2026-07-30T14:10:07.928659Z",
1139
+ "shell.execute_reply": "2026-07-30T14:10:07.925801Z"
1140
+ }
1141
+ },
1142
+ "outputs": [
1143
+ {
1144
+ "name": "stdout",
1145
+ "output_type": "stream",
1146
+ "text": [
1147
+ "Output shape: 4440 rows x 14 columns\n",
1148
+ "Columns: ['upc', 'external_id', 'brand', 'title', 'price', 'price_currency', 'size', 'size_amount', 'size_unit', 'size_qty', 'size_unit_norm', 'image_url', 'source', 'source_url']\n"
1149
+ ]
1150
+ }
1151
+ ],
1152
+ "source": [
1153
+ "# Drop 100% null columns\n",
1154
+ "df_out = df.drop(columns=cols_100pct_null)\n",
1155
+ "print(f\"Output shape: {df_out.shape[0]} rows x {df_out.shape[1]} columns\")\n",
1156
+ "print(f\"Columns: {list(df_out.columns)}\")"
1157
+ ]
1158
+ },
1159
+ {
1160
+ "cell_type": "code",
1161
+ "execution_count": 18,
1162
+ "metadata": {
1163
+ "execution": {
1164
+ "iopub.execute_input": "2026-07-30T14:10:07.935063Z",
1165
+ "iopub.status.busy": "2026-07-30T14:10:07.933034Z",
1166
+ "iopub.status.idle": "2026-07-30T14:10:07.948749Z",
1167
+ "shell.execute_reply": "2026-07-30T14:10:07.946896Z"
1168
+ }
1169
+ },
1170
+ "outputs": [
1171
+ {
1172
+ "name": "stdout",
1173
+ "output_type": "stream",
1174
+ "text": [
1175
+ "Validation result: PASS\n"
1176
+ ]
1177
+ }
1178
+ ],
1179
+ "source": [
1180
+ "# Build validation report\n",
1181
+ "validation = {\n",
1182
+ " \"version\": VERSION,\n",
1183
+ " \"timestamp\": TIMESTAMP,\n",
1184
+ " \"result\": \"PASS\",\n",
1185
+ " \"failures\": [],\n",
1186
+ " \"checks\": {\n",
1187
+ " \"row_count\": {\"expected\": 4440, \"actual\": len(df), \"pass\": len(df) == 4440},\n",
1188
+ " \"column_count\": {\"expected\": 16, \"actual\": len(df.columns), \"pass\": len(df.columns) == 16},\n",
1189
+ " \"columns_dropped\": cols_100pct_null,\n",
1190
+ " \"full_row_duplicates\": {\"count\": int(full_dupes), \"pass\": full_dupes == 0},\n",
1191
+ " \"external_id_duplicates\": {\"count\": int(ext_dupes), \"pass\": ext_dupes == 0},\n",
1192
+ " },\n",
1193
+ "}\n",
1194
+ "print(f\"Validation result: {validation['result']}\")"
1195
+ ]
1196
+ },
1197
+ {
1198
+ "cell_type": "code",
1199
+ "execution_count": 19,
1200
+ "metadata": {
1201
+ "execution": {
1202
+ "iopub.execute_input": "2026-07-30T14:10:07.954161Z",
1203
+ "iopub.status.busy": "2026-07-30T14:10:07.953613Z",
1204
+ "iopub.status.idle": "2026-07-30T14:10:07.966699Z",
1205
+ "shell.execute_reply": "2026-07-30T14:10:07.965433Z"
1206
+ }
1207
+ },
1208
+ "outputs": [
1209
+ {
1210
+ "name": "stdout",
1211
+ "output_type": "stream",
1212
+ "text": [
1213
+ "{\n",
1214
+ " \"version\": \"1.0.0\",\n",
1215
+ " \"timestamp\": \"2026-07-30T14:10:06.514655+00:00\",\n",
1216
+ " \"input\": {\n",
1217
+ " \"source\": \"saraNour/compliments-brand/source_of_truth/products.parquet\",\n",
1218
+ " \"row_count\": 4440,\n",
1219
+ " \"column_count\": 16\n",
1220
+ " },\n",
1221
+ " \"output\": {\n",
1222
+ " \"row_count\": 4440,\n",
1223
+ " \"column_count\": 14,\n",
1224
+ " \"columns_dropped\": [\n",
1225
+ " \"size_per_unit\",\n",
1226
+ " \"size_total\"\n",
1227
+ " ]\n",
1228
+ " },\n",
1229
+ " \"summary\": {\n",
1230
+ " \"unique_upcs\": 3271,\n",
1231
+ " \"reused_upcs\": 774,\n",
1232
+ " \"null_upcs\": 1,\n",
1233
+ " \"unique_brands\": 11,\n",
1234
+ " \"unique_size_strings\": 631\n",
1235
+ " }\n",
1236
+ "}\n"
1237
+ ]
1238
+ }
1239
+ ],
1240
+ "source": [
1241
+ "# Build statistics\n",
1242
+ "statistics = {\n",
1243
+ " \"version\": VERSION,\n",
1244
+ " \"timestamp\": TIMESTAMP,\n",
1245
+ " \"input\": {\"source\": f\"{HF_REPO}/{HF_FILE}\", \"row_count\": len(df), \"column_count\": len(df.columns)},\n",
1246
+ " \"output\": {\"row_count\": len(df_out), \"column_count\": len(df_out.columns), \"columns_dropped\": cols_100pct_null},\n",
1247
+ " \"summary\": {\n",
1248
+ " \"unique_upcs\": int(df[\"upc\"].nunique()),\n",
1249
+ " \"reused_upcs\": int(len(reused)),\n",
1250
+ " \"null_upcs\": int(df[\"upc\"].isna().sum()),\n",
1251
+ " \"unique_brands\": int(brand_counts.shape[0]),\n",
1252
+ " \"unique_size_strings\": int(df[\"size\"].nunique()),\n",
1253
+ " },\n",
1254
+ "}\n",
1255
+ "print(json.dumps(statistics, indent=2))"
1256
+ ]
1257
+ },
1258
+ {
1259
+ "cell_type": "markdown",
1260
+ "metadata": {},
1261
+ "source": [
1262
+ "## 13. Save Outputs"
1263
+ ]
1264
+ },
1265
+ {
1266
+ "cell_type": "code",
1267
+ "execution_count": 20,
1268
+ "metadata": {
1269
+ "execution": {
1270
+ "iopub.execute_input": "2026-07-30T14:10:07.992090Z",
1271
+ "iopub.status.busy": "2026-07-30T14:10:07.989963Z",
1272
+ "iopub.status.idle": "2026-07-30T14:10:08.085599Z",
1273
+ "shell.execute_reply": "2026-07-30T14:10:08.083881Z"
1274
+ }
1275
+ },
1276
+ "outputs": [
1277
+ {
1278
+ "name": "stdout",
1279
+ "output_type": "stream",
1280
+ "text": [
1281
+ "Saved: outputs/phase1_output.parquet\n",
1282
+ "Saved: validation/phase1_validation.json\n",
1283
+ "Saved: statistics/phase1_statistics.json\n",
1284
+ "Saved: outputs/phase1_provenance.json\n"
1285
+ ]
1286
+ }
1287
+ ],
1288
+ "source": [
1289
+ "OUTPUT_DIR = Path(\"outputs\")\n",
1290
+ "VALIDATION_DIR = Path(\"validation\")\n",
1291
+ "STATISTICS_DIR = Path(\"statistics\")\n",
1292
+ "\n",
1293
+ "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
1294
+ "VALIDATION_DIR.mkdir(parents=True, exist_ok=True)\n",
1295
+ "STATISTICS_DIR.mkdir(parents=True, exist_ok=True)\n",
1296
+ "\n",
1297
+ "df_out.to_parquet(OUTPUT_DIR / \"phase1_output.parquet\", index=False)\n",
1298
+ "print(f\"Saved: {OUTPUT_DIR / 'phase1_output.parquet'}\")\n",
1299
+ "\n",
1300
+ "with open(VALIDATION_DIR / \"phase1_validation.json\", \"w\") as f:\n",
1301
+ " json.dump(validation, f, indent=2, default=str)\n",
1302
+ "print(f\"Saved: {VALIDATION_DIR / 'phase1_validation.json'}\")\n",
1303
+ "\n",
1304
+ "with open(STATISTICS_DIR / \"phase1_statistics.json\", \"w\") as f:\n",
1305
+ " json.dump(statistics, f, indent=2, default=str)\n",
1306
+ "print(f\"Saved: {STATISTICS_DIR / 'phase1_statistics.json'}\")\n",
1307
+ "\n",
1308
+ "with open(OUTPUT_DIR / \"phase1_provenance.json\", \"w\") as f:\n",
1309
+ " json.dump(provenance, f, indent=2, default=str)\n",
1310
+ "print(f\"Saved: {OUTPUT_DIR / 'phase1_provenance.json'}\")"
1311
+ ]
1312
+ },
1313
+ {
1314
+ "cell_type": "markdown",
1315
+ "metadata": {},
1316
+ "source": [
1317
+ "## 14. Summary"
1318
+ ]
1319
+ },
1320
+ {
1321
+ "cell_type": "code",
1322
+ "execution_count": 21,
1323
+ "metadata": {
1324
+ "execution": {
1325
+ "iopub.execute_input": "2026-07-30T14:10:08.108443Z",
1326
+ "iopub.status.busy": "2026-07-30T14:10:08.102152Z",
1327
+ "iopub.status.idle": "2026-07-30T14:10:08.124049Z",
1328
+ "shell.execute_reply": "2026-07-30T14:10:08.119790Z"
1329
+ }
1330
+ },
1331
+ "outputs": [
1332
+ {
1333
+ "name": "stdout",
1334
+ "output_type": "stream",
1335
+ "text": [
1336
+ "============================================================\n",
1337
+ "PHASE 1 COMPLETE\n",
1338
+ "============================================================\n",
1339
+ "Input: 4440 rows, 16 columns\n",
1340
+ "Output: 4440 rows, 14 columns\n",
1341
+ "Dropped: ['size_per_unit', 'size_total']\n",
1342
+ "Validation: PASS\n",
1343
+ "============================================================\n",
1344
+ "\n",
1345
+ "Output columns:\n",
1346
+ " 1. upc\n",
1347
+ " 2. external_id\n",
1348
+ " 3. brand\n",
1349
+ " 4. title\n",
1350
+ " 5. price\n",
1351
+ " 6. price_currency\n",
1352
+ " 7. size\n",
1353
+ " 8. size_amount\n",
1354
+ " 9. size_unit\n",
1355
+ " 10. size_qty\n",
1356
+ " 11. size_unit_norm\n",
1357
+ " 12. image_url\n",
1358
+ " 13. source\n",
1359
+ " 14. source_url\n"
1360
+ ]
1361
+ }
1362
+ ],
1363
+ "source": [
1364
+ "print(\"=\" * 60)\n",
1365
+ "print(\"PHASE 1 COMPLETE\")\n",
1366
+ "print(\"=\" * 60)\n",
1367
+ "print(f\"Input: {df.shape[0]} rows, {df.shape[1]} columns\")\n",
1368
+ "print(f\"Output: {df_out.shape[0]} rows, {df_out.shape[1]} columns\")\n",
1369
+ "print(f\"Dropped: {cols_100pct_null}\")\n",
1370
+ "print(f\"Validation: {validation['result']}\")\n",
1371
+ "print(\"=\" * 60)\n",
1372
+ "print()\n",
1373
+ "print(\"Output columns:\")\n",
1374
+ "for i, col in enumerate(df_out.columns):\n",
1375
+ " print(f\" {i+1:2d}. {col}\")"
1376
+ ]
1377
+ }
1378
+ ],
1379
+ "metadata": {
1380
+ "kernelspec": {
1381
+ "display_name": "Python 3",
1382
+ "language": "python",
1383
+ "name": "python3"
1384
+ },
1385
+ "language_info": {
1386
+ "codemirror_mode": {
1387
+ "name": "ipython",
1388
+ "version": 3
1389
+ },
1390
+ "file_extension": ".py",
1391
+ "mimetype": "text/x-python",
1392
+ "name": "python",
1393
+ "nbconvert_exporter": "python",
1394
+ "pygments_lexer": "ipython3",
1395
+ "version": "3.13.7"
1396
+ }
1397
+ },
1398
+ "nbformat": 4,
1399
+ "nbformat_minor": 4
1400
+ }