{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Giai đoạn 1 — Dữ liệu v2\nChọn sản phẩm chỉ từ train; bỏ hai tuần biên không đầy đủ; dùng chung split theo ngày. Notebook này ghi bộ mới vào `processed_data/v2`. Cài `requirements.txt` nếu môi trường chưa đủ thư viện.","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nimport os\nimport sys\n\n# Set this only if more than one source-code dataset is attached.\nCODE_ROOT_OVERRIDE = None\nif CODE_ROOT_OVERRIDE:\n    CODE_ROOT = Path(CODE_ROOT_OVERRIDE).resolve()\nelse:\n    candidates = {p.resolve() for p in [Path.cwd(), Path.cwd().parent] if (p/'src/config.py').is_file()}\n    if Path('/kaggle/input').exists():\n        for root, dirs, files in os.walk('/kaggle/input'):\n            dirs[:] = [d for d in dirs if d not in {'images', '.git', '__pycache__'}]\n            candidate = Path(root)\n            if candidate.name == 'src' and 'config.py' in files and (candidate/'pipeline/train_baselines.py').is_file():\n                candidates.add(candidate.parent.resolve())\n    if len(candidates) != 1:\n        raise ValueError(f'Set CODE_ROOT_OVERRIDE to the folder containing src. Candidates: {sorted(candidates)}')\n    CODE_ROOT = next(iter(candidates))\nif not (CODE_ROOT/'src/config.py').is_file():\n    raise FileNotFoundError('The selected code folder is not source v2')\nsys.path.insert(0, str(CODE_ROOT))\nfrom src.utils.paths import resolve_data_dir, resolve_raw_dir, resolve_embeddings_dir\nWORK_ROOT = Path('/kaggle/working') if Path('/kaggle/input').exists() else CODE_ROOT\nprint('Code:', CODE_ROOT, 'Output:', WORK_ROOT)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-09-17T05:32:37.386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"RAW_DATA_OVERRIDE = None\nRAW_DIR = resolve_raw_dir(CODE_ROOT, RAW_DATA_OVERRIDE)\nPROCESSED_DIR = WORK_ROOT/'processed_data/v2'\nfrom src.config import DataConfig\nCONFIG = DataConfig()  # 80/12/12 complete weeks; train-only thresholds 20 weeks, 50 transactions; 8,000 items\nprint(CONFIG)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. EDA\nĐọc giao dịch theo từng khối để hạn chế RAM. Lượng bán quan sát là đại diện cho nhu cầu; không suy ra tồn kho từ số 0.","metadata":{}},{"cell_type":"code","source":"from src.data.eda import run_eda\neda = run_eda(RAW_DIR, output_dir=WORK_ROOT/'eda/v2')\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Tổng hợp và chọn mẫu\nLưu riêng `weekly_sales_all.csv` và `weekly_sales_selected.csv`. Giữ nguyên file toàn bộ sản phẩm để thử lại sampling.","metadata":{}},{"cell_type":"code","source":"from src.pipeline.run_stage1_stage2 import run_stage1_stage2\nsummary = run_stage1_stage2(RAW_DIR, PROCESSED_DIR, CONFIG)\ndisplay(summary)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Xuất bộ input cho Kaggle\nUpload toàn bộ archive này thành một dataset. Giữ nguyên nội dung các CSV để hash khớp. Không chỉ upload selected_articles.csv.","metadata":{}},{"cell_type":"code","source":"import shutil\nfrom IPython.display import FileLink, display\narchive = shutil.make_archive(str(WORK_ROOT/'hm-stage1-v2'), 'zip', root_dir=PROCESSED_DIR)\ndisplay(FileLink(archive))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Giai đoạn 2 — Trích xuất embedding từ ảnh thật\nGắn source code v2, bộ dữ liệu giai đoạn 1 v2 và H&M competition. Bật GPU và Internet cho lần tải encoder đầu tiên. Nếu thiếu thư viện, cài `requirements-extraction.txt`; không cần cài lại mỗi lần chạy.","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nimport os\nimport sys\n\n# Set this only if more than one source-code dataset is attached.\nCODE_ROOT_OVERRIDE = None\nif CODE_ROOT_OVERRIDE:\n    CODE_ROOT = Path(CODE_ROOT_OVERRIDE).resolve()\nelse:\n    candidates = {p.resolve() for p in [Path.cwd(), Path.cwd().parent] if (p/'src/config.py').is_file()}\n    if Path('/kaggle/input').exists():\n        for root, dirs, files in os.walk('/kaggle/input'):\n            dirs[:] = [d for d in dirs if d not in {'images', '.git', '__pycache__'}]\n            candidate = Path(root)\n            if candidate.name == 'src' and 'config.py' in files and (candidate/'pipeline/train_baselines.py').is_file():\n                candidates.add(candidate.parent.resolve())\n    if len(candidates) != 1:\n        raise ValueError(f'Set CODE_ROOT_OVERRIDE to the folder containing src. Candidates: {sorted(candidates)}')\n    CODE_ROOT = next(iter(candidates))\nif not (CODE_ROOT/'src/config.py').is_file():\n    raise FileNotFoundError('The selected code folder is not source v2')\nsys.path.insert(0, str(CODE_ROOT))\nfrom src.utils.paths import resolve_data_dir, resolve_raw_dir, resolve_embeddings_dir\nWORK_ROOT = Path('/kaggle/working') if Path('/kaggle/input').exists() else CODE_ROOT\nprint('Code:', CODE_ROOT, 'Output:', WORK_ROOT)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T17:19:29.918642Z","iopub.execute_input":"2026-09-16T17:19:29.918943Z","iopub.status.idle":"2026-09-16T17:19:30.034194Z","shell.execute_reply.started":"2026-09-16T17:19:29.918905Z","shell.execute_reply":"2026-09-16T17:19:30.033237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR_OVERRIDE = None\nRAW_DATA_OVERRIDE = None\nIMAGES_DIR_OVERRIDE = None\nDATA_DIR = resolve_data_dir(CODE_ROOT, DATA_DIR_OVERRIDE)\nRAW_DIR = resolve_raw_dir(CODE_ROOT, RAW_DATA_OVERRIDE)\nIMAGES_DIR = Path(IMAGES_DIR_OVERRIDE) if IMAGES_DIR_OVERRIDE else RAW_DIR/'images'\nOUTPUT_DIR = WORK_ROOT/'embeddings/v2'\nSELECTED_PATH = DATA_DIR/'selected_articles.csv'\nBATCH_SIZE = 64\nMAX_MISSING_IMAGE_FRACTION = 0.05  # cho phép một tỷ lệ nhỏ ảnh thiếu trong dữ liệu thật\nIMAGE_REVISION = 'main'  # use the resolved commit in metadata to reproduce a later extraction\nTEXT_REVISION = 'main'\nimport torch\nprint('Device:', 'cuda' if torch.cuda.is_available() else 'cpu')\nprint('Selection:', SELECTED_PATH, 'Images:', IMAGES_DIR, 'Output:', OUTPUT_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T17:55:36.003994Z","iopub.execute_input":"2026-09-16T17:55:36.005136Z","iopub.status.idle":"2026-09-16T17:55:36.065863Z","shell.execute_reply.started":"2026-09-16T17:55:36.005097Z","shell.execute_reply":"2026-09-16T17:55:36.064926Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Xem ảnh đầu vào\nĐây phải là ảnh sản phẩm thật, không phải ảnh đen thay thế. Pipeline tiếp theo sẽ kiểm tra toàn bộ ảnh và xuất image_audit.json.","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nfrom IPython.display import display\nfrom PIL import Image\nfrom src.features.extract_embeddings import image_path\nselected = pd.read_csv(SELECTED_PATH, dtype={'article_id': str})\nfor key in selected.article_id.sample(min(10, len(selected)), random_state=42):\n    path = image_path(IMAGES_DIR, key)\n    print(key, path)\n    with Image.open(path) as source:\n        preview = source.convert('RGB')\n        preview.thumbnail((224, 224))\n        display(preview)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T17:55:39.146062Z","iopub.execute_input":"2026-09-16T17:55:39.146402Z","iopub.status.idle":"2026-09-16T17:55:39.559936Z","shell.execute_reply.started":"2026-09-16T17:55:39.146369Z","shell.execute_reply":"2026-09-16T17:55:39.55908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Trích xuất và kiểm định\nẢnh 768d và text 384d được L2-normalize riêng. Hai encoder chưa có không gian ngữ nghĩa chung. Không tái tạo text theo công thức khác: dùng đúng text_for_embedding của Stage 1.","metadata":{}},{"cell_type":"code","source":"from src.features.extract_embeddings import extract_embeddings\nreport = extract_embeddings(SELECTED_PATH, IMAGES_DIR, OUTPUT_DIR, BATCH_SIZE,\n    max_missing_image_fraction=MAX_MISSING_IMAGE_FRACTION,\n    image_revision=IMAGE_REVISION, text_revision=TEXT_REVISION)\ndisplay(report)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T17:55:43.403994Z","iopub.execute_input":"2026-09-16T17:55:43.40433Z","iopub.status.idle":"2026-09-16T18:13:57.883889Z","shell.execute_reply.started":"2026-09-16T17:55:43.4043Z","shell.execute_reply":"2026-09-16T18:13:57.883115Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Giai đoạn 3 — Baseline và kiểm tra thiếu modality\nDùng bộ dữ liệu và embedding v2 cùng hash. Nếu chưa trích xuất lại embedding, đặt MODELS thành danh sách sales-only như hướng dẫn ở cell cấu hình. Kết quả cũ không dùng để kết luận về lợi ích ảnh.","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nimport os\nimport sys\n\n# Set this only if more than one source-code dataset is attached.\nCODE_ROOT_OVERRIDE = None\nif CODE_ROOT_OVERRIDE:\n    CODE_ROOT = Path(CODE_ROOT_OVERRIDE).resolve()\nelse:\n    candidates = {p.resolve() for p in [Path.cwd(), Path.cwd().parent] if (p/'src/config.py').is_file()}\n    if Path('/kaggle/input').exists():\n        for root, dirs, files in os.walk('/kaggle/input'):\n            dirs[:] = [d for d in dirs if d not in {'images', '.git', '__pycache__'}]\n            candidate = Path(root)\n            if candidate.name == 'src' and 'config.py' in files and (candidate/'pipeline/train_baselines.py').is_file():\n                candidates.add(candidate.parent.resolve())\n    if len(candidates) != 1:\n        raise ValueError(f'Set CODE_ROOT_OVERRIDE to the folder containing src. Candidates: {sorted(candidates)}')\n    CODE_ROOT = next(iter(candidates))\nif not (CODE_ROOT/'src/config.py').is_file():\n    raise FileNotFoundError('The selected code folder is not source v2')\nsys.path.insert(0, str(CODE_ROOT))\nfrom src.utils.paths import resolve_data_dir, resolve_raw_dir, resolve_embeddings_dir\nWORK_ROOT = Path('/kaggle/working') if Path('/kaggle/input').exists() else CODE_ROOT\nprint('Code:', CODE_ROOT, 'Output:', WORK_ROOT)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T14:31:09.724308Z","iopub.execute_input":"2026-09-17T14:31:09.724636Z","iopub.status.idle":"2026-09-17T14:31:09.854813Z","shell.execute_reply.started":"2026-09-17T14:31:09.724606Z","shell.execute_reply":"2026-09-17T14:31:09.853839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR_OVERRIDE = None\nEMBEDDINGS_DIR_OVERRIDE = None\nDATA_DIR = resolve_data_dir(CODE_ROOT, DATA_DIR_OVERRIDE)\nMODELS = ['zero', 'last_value', 'moving_average', 'linear', 'mlp', 'lstm', 'transformer', 'multimodal_mlp']\n# Before extraction: MODELS = ['zero', 'last_value', 'moving_average', 'linear', 'mlp', 'lstm', 'transformer']\nEMBEDDINGS_DIR = resolve_embeddings_dir(CODE_ROOT, EMBEDDINGS_DIR_OVERRIDE) if 'multimodal_mlp' in MODELS else CODE_ROOT/'embeddings/v2'\nSEEDS = [42, 43, 44]\nEPOCHS = 30\nPATIENCE = 5\nBATCH_SIZE = 256\nSEQ_LEN, PRED_LEN = 12, 2\nTARGET_TRANSFORM = 'log1p'  # all learning models share this; use 'none' for a controlled raw-sales comparison\nRETRAINED_ABLATION = True\n# If running sales-only, set RETRAINED_ABLATION = False.\nprint('Data:', DATA_DIR, 'Embeddings:', EMBEDDINGS_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T14:31:16.369067Z","iopub.execute_input":"2026-09-17T14:31:16.369945Z","iopub.status.idle":"2026-09-17T14:31:16.4225Z","shell.execute_reply.started":"2026-09-17T14:31:16.3699Z","shell.execute_reply":"2026-09-17T14:31:16.421465Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Chạy thực nghiệm\nCùng checkpoint full được đánh giá với 5 tình huống: full, mất ảnh, mất text, chỉ còn nửa lịch sử gần nhất, mất cả ảnh và text. Bảng retrained là thí nghiệm riêng, huấn luyện lại từng tổ hợp. Không diễn giải hai bảng như cùng một protocol.","metadata":{}},{"cell_type":"markdown","source":"### Lưu ý cell code dưới đây chỉ dùng trong kaggle này, không dùng trong local","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nimport os\n\n# 1. Xác định thư mục cha của dataset tùy theo môi trường\nif os.path.exists('/kaggle/input'):\n    # Chạy trên Kaggle: đường dẫn dataset mount vào\n    BASE_EMB = Path('/kaggle/input/datasets/alexnguyen147/hm-embeddings-8k')\n    \n    # Một số trường hợp Kaggle slug đường dẫn không có chữ 'datasets/'\n    if not BASE_EMB.exists():\n        BASE_EMB = Path('/kaggle/input/hm-embeddings-8k')\nelse:\n    # Chạy ở Local: đặt đường dẫn thư mục embeddings ở máy bạn\n    BASE_EMB = Path('embeddings')  # hoặc Path('data/embeddings') tùy theo máy bạn\n\n# 2. Tự động kiểm tra: nếu có thư mục con 'embeddings' thì trỏ vào bên trong\nif (BASE_EMB / 'embeddings').exists():\n    EMBEDDINGS_DIR = BASE_EMB / 'embeddings'\nelse:\n    EMBEDDINGS_DIR = BASE_EMB\n\nprint(f\"-> Using EMBEDDINGS_DIR: {EMBEDDINGS_DIR.resolve()}\")\nprint(f\"-> Metadata exists: {(EMBEDDINGS_DIR / 'metadata.json').exists()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T14:31:20.450472Z","iopub.execute_input":"2026-09-17T14:31:20.450815Z","iopub.status.idle":"2026-09-17T14:31:20.458473Z","shell.execute_reply.started":"2026-09-17T14:31:20.450783Z","shell.execute_reply":"2026-09-17T14:31:20.457615Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from src.pipeline.train_baselines import run_all_baselines\nRUN_DIR = run_all_baselines(epochs=EPOCHS, patience=PATIENCE, seeds=SEEDS,\n    batch_size=BATCH_SIZE, processed_data_dir=DATA_DIR, embeddings_dir=EMBEDDINGS_DIR,\n    output_dir=WORK_ROOT/'results/v2', models=MODELS, seq_len=SEQ_LEN, pred_len=PRED_LEN,\n    target_transform=TARGET_TRANSFORM, retrained_ablation=RETRAINED_ABLATION)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T14:31:25.025802Z","iopub.execute_input":"2026-09-17T14:31:25.026105Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Kết quả và độ suy giảm\nMAE/RMSE ở đơn vị lượt bán. wMAPE/sMAPE tính bằng %. Khi tổng actual bằng 0, wMAPE không xác định và được lưu trống. Không dùng test để chọn siêu tham số.","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nfrom IPython.display import display\ndisplay(pd.read_csv(RUN_DIR/'baseline_results.csv'))\ndisplay(pd.read_csv(RUN_DIR/'seed_summary.csv'))\nif (RUN_DIR/'robustness_comparison.csv').exists():\n    display(pd.read_csv(RUN_DIR/'robustness_comparison.csv'))\ndisplay(pd.read_csv(RUN_DIR/'detailed_metrics.csv').head(30))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T13:05:34.333616Z","iopub.execute_input":"2026-09-17T13:05:34.333962Z","iopub.status.idle":"2026-09-17T13:05:34.343001Z","shell.execute_reply.started":"2026-09-17T13:05:34.333934Z","shell.execute_reply":"2026-09-17T13:05:34.341879Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Lưu kết quả\nMỗi run có cấu hình, hash dữ liệu và source, checkpoint, learning curves dạng CSV, dự đoán theo sản phẩm/tuần/horizon, metric theo nhóm và summary seed.","metadata":{}},{"cell_type":"code","source":"import shutil\nfrom IPython.display import FileLink\narchive = shutil.make_archive(str(WORK_ROOT/('baseline-results-'+RUN_DIR.name)), 'zip', root_dir=RUN_DIR)\ndisplay(FileLink(archive))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}