{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":7391,"databundleVersionId":44328},{"sourceType":"datasetVersion","sourceId":15260092,"datasetId":9762313,"databundleVersionId":16158966}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"64f49c31","cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport plotly.express as px\nimport plotly.graph_objects as go\nimport plotly.io as pio\nfrom pathlib import Path\nimport torch\n\npio.renderers.default = \"iframe\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:31:17.912994Z","iopub.execute_input":"2026-03-18T18:31:17.913770Z","iopub.status.idle":"2026-03-18T18:31:17.918180Z","shell.execute_reply.started":"2026-03-18T18:31:17.913729Z","shell.execute_reply":"2026-03-18T18:31:17.917395Z"}},"outputs":[],"execution_count":null},{"id":"07a3d4f4-1211-45dd-b323-253b947dd712","cell_type":"markdown","source":"Импортируем необходимые библиотеки, ниже по ходу ноутбука объясним почему что используем","metadata":{}},{"id":"7885c234-bf2c-4b51-89aa-3b634e1f3380","cell_type":"code","source":"from statsforecast import StatsForecast\nfrom statsforecast.models import Naive, SeasonalNaive, AutoTheta, AutoETS","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:38:53.883576Z","iopub.execute_input":"2026-03-18T16:38:53.884297Z","iopub.status.idle":"2026-03-18T16:38:57.831740Z","shell.execute_reply.started":"2026-03-18T16:38:53.884267Z","shell.execute_reply":"2026-03-18T16:38:57.831105Z"}},"outputs":[],"execution_count":null},{"id":"5a9f7461-d6aa-446a-98f7-e2371f259f24","cell_type":"code","source":"from catboost import CatBoostRegressor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:39:05.092622Z","iopub.execute_input":"2026-03-18T16:39:05.093593Z","iopub.status.idle":"2026-03-18T16:39:05.885222Z","shell.execute_reply.started":"2026-03-18T16:39:05.093557Z","shell.execute_reply":"2026-03-18T16:39:05.884648Z"}},"outputs":[],"execution_count":null},{"id":"b20d58a6-f11c-4159-b405-ba06f544cc3c","cell_type":"code","source":"!pip install -q pytorch_forecasting","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:47:44.091676Z","iopub.execute_input":"2026-03-18T16:47:44.092497Z","iopub.status.idle":"2026-03-18T16:47:49.234232Z","shell.execute_reply.started":"2026-03-18T16:47:44.092468Z","shell.execute_reply":"2026-03-18T16:47:49.233273Z"}},"outputs":[],"execution_count":null},{"id":"67e0333b-2979-4bfa-bd25-a439ff66a2d5","cell_type":"code","source":"import pytorch_forecasting as pf                                                                   \nimport lightning.pytorch as pl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:47:51.591866Z","iopub.execute_input":"2026-03-18T16:47:51.592685Z","iopub.status.idle":"2026-03-18T16:48:10.241657Z","shell.execute_reply.started":"2026-03-18T16:47:51.592645Z","shell.execute_reply":"2026-03-18T16:48:10.240740Z"}},"outputs":[],"execution_count":null},{"id":"ef56f80c-8ea6-489c-9e90-cb0c8271770f","cell_type":"code","source":"DATA_DIR = Path(\"/kaggle/input/datasets/markterris/datasets-favorita\")\n\nTRAIN_PATH = DATA_DIR / \"train.csv\"\nTEST_PATH = DATA_DIR / \"test.csv\"\nSTORES_PATH = DATA_DIR / \"stores.csv\"\nITEMS_PATH = DATA_DIR / \"items.csv\"\nOIL_PATH = DATA_DIR / \"oil.csv\"\nHOLIDAYS_PATH = DATA_DIR / \"holidays_events.csv\"\nTRANSACTIONS_PATH = DATA_DIR / \"transactions.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:23.049108Z","iopub.execute_input":"2026-03-18T16:48:23.049570Z","iopub.status.idle":"2026-03-18T16:48:23.054077Z","shell.execute_reply.started":"2026-03-18T16:48:23.049537Z","shell.execute_reply":"2026-03-18T16:48:23.053154Z"}},"outputs":[],"execution_count":null},{"id":"6196c029-852d-46ce-a631-0b245ab5e29a","cell_type":"code","source":"def load_train(nrows = None) -> pd.DataFrame:\n    return pd.read_csv(TRAIN_PATH, parse_dates=[\"date\"], nrows=nrows)\n\n\ndef load_test() -> pd.DataFrame:\n    return pd.read_csv(TEST_PATH, parse_dates=[\"date\"])\n\n\ndef load_stores() -> pd.DataFrame:\n    return pd.read_csv(STORES_PATH)\n\n\ndef load_items() -> pd.DataFrame:\n    return pd.read_csv(ITEMS_PATH)\n\n\ndef load_oil() -> pd.DataFrame:\n    return pd.read_csv(OIL_PATH, parse_dates=[\"date\"])\n\n\ndef load_holidays() -> pd.DataFrame:\n    return pd.read_csv(HOLIDAYS_PATH, parse_dates=[\"date\"])\n\n\ndef load_transactions() -> pd.DataFrame:\n    return pd.read_csv(TRANSACTIONS_PATH, parse_dates=[\"date\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:25.304493Z","iopub.execute_input":"2026-03-18T16:48:25.305215Z","iopub.status.idle":"2026-03-18T16:48:25.311381Z","shell.execute_reply.started":"2026-03-18T16:48:25.305164Z","shell.execute_reply":"2026-03-18T16:48:25.310464Z"}},"outputs":[],"execution_count":null},{"id":"a3e825cd","cell_type":"code","source":"stores = load_stores()\nstores.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:28.149867Z","iopub.execute_input":"2026-03-18T16:48:28.150719Z","iopub.status.idle":"2026-03-18T16:48:28.166829Z","shell.execute_reply.started":"2026-03-18T16:48:28.150685Z","shell.execute_reply":"2026-03-18T16:48:28.166052Z"}},"outputs":[],"execution_count":null},{"id":"3c2ce83e","cell_type":"code","source":"items = load_items()\nitems.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:30.341966Z","iopub.execute_input":"2026-03-18T16:48:30.342885Z","iopub.status.idle":"2026-03-18T16:48:30.358224Z","shell.execute_reply.started":"2026-03-18T16:48:30.342833Z","shell.execute_reply":"2026-03-18T16:48:30.357436Z"}},"outputs":[],"execution_count":null},{"id":"04fac004","cell_type":"code","source":"oil = load_oil()\noil.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:32.830071Z","iopub.execute_input":"2026-03-18T16:48:32.830669Z","iopub.status.idle":"2026-03-18T16:48:32.845875Z","shell.execute_reply.started":"2026-03-18T16:48:32.830634Z","shell.execute_reply":"2026-03-18T16:48:32.845271Z"}},"outputs":[],"execution_count":null},{"id":"d5acbff2","cell_type":"code","source":"# Для цен на нефть пропуски заполним через линейную интерполяцию\noil[\"dcoilwtico\"] = oil[\"dcoilwtico\"].interpolate(method=\"linear\").bfill()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:36.166724Z","iopub.execute_input":"2026-03-18T16:48:36.167345Z","iopub.status.idle":"2026-03-18T16:48:36.173772Z","shell.execute_reply.started":"2026-03-18T16:48:36.167311Z","shell.execute_reply":"2026-03-18T16:48:36.172942Z"}},"outputs":[],"execution_count":null},{"id":"66ea885b","cell_type":"code","source":"oil.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:38.623450Z","iopub.execute_input":"2026-03-18T16:48:38.624111Z","iopub.status.idle":"2026-03-18T16:48:38.632433Z","shell.execute_reply.started":"2026-03-18T16:48:38.624077Z","shell.execute_reply":"2026-03-18T16:48:38.631515Z"}},"outputs":[],"execution_count":null},{"id":"6b03716f","cell_type":"code","source":"holidays = load_holidays()\nholidays.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:41.600463Z","iopub.execute_input":"2026-03-18T16:48:41.601208Z","iopub.status.idle":"2026-03-18T16:48:41.615247Z","shell.execute_reply.started":"2026-03-18T16:48:41.601162Z","shell.execute_reply":"2026-03-18T16:48:41.614392Z"}},"outputs":[],"execution_count":null},{"id":"02ddab1d","cell_type":"code","source":"holidays.locale.value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:45.466280Z","iopub.execute_input":"2026-03-18T16:48:45.466552Z","iopub.status.idle":"2026-03-18T16:48:45.474193Z","shell.execute_reply.started":"2026-03-18T16:48:45.466528Z","shell.execute_reply":"2026-03-18T16:48:45.473309Z"}},"outputs":[],"execution_count":null},{"id":"64782cb1","cell_type":"code","source":"transactions = load_transactions()\ntransactions.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:47.607621Z","iopub.execute_input":"2026-03-18T16:48:47.607919Z","iopub.status.idle":"2026-03-18T16:48:47.664876Z","shell.execute_reply.started":"2026-03-18T16:48:47.607892Z","shell.execute_reply":"2026-03-18T16:48:47.663952Z"}},"outputs":[],"execution_count":null},{"id":"1d6ac43c","cell_type":"code","source":"train = load_train()\ntrain.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:48:49.720318Z","iopub.execute_input":"2026-03-18T16:48:49.721123Z","iopub.status.idle":"2026-03-18T16:50:05.649515Z","shell.execute_reply.started":"2026-03-18T16:48:49.721089Z","shell.execute_reply":"2026-03-18T16:50:05.648695Z"}},"outputs":[],"execution_count":null},{"id":"df4016b0","cell_type":"code","source":"train.isna().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:50:05.651038Z","iopub.execute_input":"2026-03-18T16:50:05.651377Z","iopub.status.idle":"2026-03-18T16:50:11.725620Z","shell.execute_reply.started":"2026-03-18T16:50:05.651350Z","shell.execute_reply":"2026-03-18T16:50:11.724967Z"}},"outputs":[],"execution_count":null},{"id":"093bd27f","cell_type":"code","source":"train[\"onpromotion\"] = train[\"onpromotion\"].fillna(False)\ntrain.onpromotion.value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:50:11.726539Z","iopub.execute_input":"2026-03-18T16:50:11.726974Z","iopub.status.idle":"2026-03-18T16:50:24.746554Z","shell.execute_reply.started":"2026-03-18T16:50:11.726946Z","shell.execute_reply":"2026-03-18T16:50:24.745890Z"}},"outputs":[],"execution_count":null},{"id":"d184ea7d","cell_type":"code","source":"train.date.min(), train.date.max()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:50:24.748129Z","iopub.execute_input":"2026-03-18T16:50:24.748501Z","iopub.status.idle":"2026-03-18T16:50:25.299601Z","shell.execute_reply.started":"2026-03-18T16:50:24.748474Z","shell.execute_reply":"2026-03-18T16:50:25.298714Z"}},"outputs":[],"execution_count":null},{"id":"de0e8cf5","cell_type":"code","source":"test = load_test()\ntest.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:50:25.300720Z","iopub.execute_input":"2026-03-18T16:50:25.301108Z","iopub.status.idle":"2026-03-18T16:50:27.097337Z","shell.execute_reply.started":"2026-03-18T16:50:25.301080Z","shell.execute_reply":"2026-03-18T16:50:27.096526Z"}},"outputs":[],"execution_count":null},{"id":"6d4cbf2c","cell_type":"code","source":"test.date.max() - train.date.max()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:53:45.697487Z","iopub.execute_input":"2026-03-18T16:53:45.698175Z","iopub.status.idle":"2026-03-18T16:53:45.984431Z","shell.execute_reply.started":"2026-03-18T16:53:45.698144Z","shell.execute_reply":"2026-03-18T16:53:45.983693Z"}},"outputs":[],"execution_count":null},{"id":"4ddeb750-6811-4a3c-8f68-20008d53d552","cell_type":"markdown","source":"В тесте представлены данные на 16 дней вперед\n\nТеперь посмотрим на агрегированный график по всем продажам:","metadata":{}},{"id":"3ebf32fe","cell_type":"code","source":"daily_sales = train.groupby(\"date\")[\"unit_sales\"].sum().reset_index()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:50:27.118907Z","iopub.execute_input":"2026-03-18T16:50:27.119363Z","iopub.status.idle":"2026-03-18T16:50:29.803358Z","shell.execute_reply.started":"2026-03-18T16:50:27.119334Z","shell.execute_reply":"2026-03-18T16:50:29.802731Z"}},"outputs":[],"execution_count":null},{"id":"3d62f109-f6cc-408f-a867-b37bdbc084fe","cell_type":"code","source":"px.line(daily_sales, x=\"date\", y=\"unit_sales\", title=\"Продажи в День\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:26.797531Z","iopub.execute_input":"2026-03-18T16:51:26.797823Z","iopub.status.idle":"2026-03-18T16:51:27.379527Z","shell.execute_reply.started":"2026-03-18T16:51:26.797797Z","shell.execute_reply":"2026-03-18T16:51:27.378648Z"}},"outputs":[],"execution_count":null},{"id":"7214f1e0","cell_type":"markdown","source":"На всем периоде виден растущий тренд, сезонность надо на меньшем отрезке смотреть","metadata":{}},{"id":"75aee4cf","cell_type":"code","source":"last_daily_sales = daily_sales[daily_sales.date > daily_sales.date.max() - pd.Timedelta(days=90)]\npx.line(last_daily_sales, x=\"date\", y=\"unit_sales\", title=\"Продажи в День (последние 90 дней)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:30.416918Z","iopub.execute_input":"2026-03-18T16:51:30.417401Z","iopub.status.idle":"2026-03-18T16:51:30.476691Z","shell.execute_reply.started":"2026-03-18T16:51:30.417367Z","shell.execute_reply":"2026-03-18T16:51:30.476014Z"}},"outputs":[],"execution_count":null},{"id":"d20159ad","cell_type":"markdown","source":"Что видно на более детальном приближении:\n- на этом отрезке тренда нет\n- сезонность - однозначно (пробил по календарю, рост приходится на выходные дни и праздники), паттерн повторяется каждую неделю","metadata":{}},{"id":"b9f68a83","cell_type":"code","source":"series_len = train.groupby([\"store_nbr\", \"item_nbr\"]).size()\nlen(series_len)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:34.589032Z","iopub.execute_input":"2026-03-18T16:51:34.589414Z","iopub.status.idle":"2026-03-18T16:51:41.788004Z","shell.execute_reply.started":"2026-03-18T16:51:34.589383Z","shell.execute_reply":"2026-03-18T16:51:41.787315Z"}},"outputs":[],"execution_count":null},{"id":"c42b9b38","cell_type":"code","source":"items.item_nbr.nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:41.789436Z","iopub.execute_input":"2026-03-18T16:51:41.789755Z","iopub.status.idle":"2026-03-18T16:51:41.794925Z","shell.execute_reply.started":"2026-03-18T16:51:41.789728Z","shell.execute_reply":"2026-03-18T16:51:41.794343Z"}},"outputs":[],"execution_count":null},{"id":"78d881c9","cell_type":"code","source":"items.family.nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:41.795656Z","iopub.execute_input":"2026-03-18T16:51:41.795880Z","iopub.status.idle":"2026-03-18T16:51:41.808584Z","shell.execute_reply.started":"2026-03-18T16:51:41.795857Z","shell.execute_reply":"2026-03-18T16:51:41.807734Z"}},"outputs":[],"execution_count":null},{"id":"fa9736a5","cell_type":"markdown","source":"Рядов при группировке item на store очень много, поэтому попробуем сначала агрегировать по family и решать задачу предсказания относительно store_nbr и family, а не конкретного item_nbr","metadata":{}},{"id":"608609ff","cell_type":"code","source":"train_rn = train.rename(columns={\"date\": \"ds\", \"unit_sales\": \"y\"}).copy()\nmerged = train_rn.merge(items[[\"item_nbr\", \"family\"]], on=\"item_nbr\", how=\"left\")\nagg = (\n    merged.groupby([\"ds\", \"store_nbr\", \"family\"], observed=True)\n    .agg(y=(\"y\", \"sum\"))\n    .reset_index()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:51:45.685799Z","iopub.execute_input":"2026-03-18T16:51:45.686659Z","iopub.status.idle":"2026-03-18T16:52:20.837683Z","shell.execute_reply.started":"2026-03-18T16:51:45.686625Z","shell.execute_reply":"2026-03-18T16:52:20.836924Z"}},"outputs":[],"execution_count":null},{"id":"f44a7a26-7c09-453a-8cbf-3b57dff99ce0","cell_type":"code","source":"agg.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:52:20.838817Z","iopub.execute_input":"2026-03-18T16:52:20.839133Z","iopub.status.idle":"2026-03-18T16:52:20.864920Z","shell.execute_reply.started":"2026-03-18T16:52:20.839084Z","shell.execute_reply":"2026-03-18T16:52:20.864394Z"}},"outputs":[],"execution_count":null},{"id":"9a6d44f8","cell_type":"code","source":"agg.groupby([\"store_nbr\", \"family\"]).ngroups","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:52:20.865769Z","iopub.execute_input":"2026-03-18T16:52:20.866056Z","iopub.status.idle":"2026-03-18T16:52:21.048473Z","shell.execute_reply.started":"2026-03-18T16:52:20.866020Z","shell.execute_reply":"2026-03-18T16:52:21.047677Z"}},"outputs":[],"execution_count":null},{"id":"b5764b42","cell_type":"markdown","source":"Таким образом для бейзлайна мы сократили в десятки раз кол-во рядов и попробуем обучиться на этом.\n\nДисклеймер: метрика в submissions на kaggle считается по каждому store x item. Здесь мы берем store x family а значит и метрику буде честно считать по агрегату.","metadata":{}},{"id":"06ecccdc","cell_type":"code","source":"sample_store = 1\nsample_family = \"GROCERY I\"\nsample_series = agg[(agg.store_nbr == sample_store)].copy()\npx.line(\n    sample_series[sample_series.family == sample_family],\n    x=\"ds\",\n    y=\"y\",\n    title=\"Пример ряда store 1, family GROCERY I\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:58:35.411385Z","iopub.execute_input":"2026-03-18T16:58:35.412139Z","iopub.status.idle":"2026-03-18T16:58:35.499597Z","shell.execute_reply.started":"2026-03-18T16:58:35.412092Z","shell.execute_reply":"2026-03-18T16:58:35.498983Z"}},"outputs":[],"execution_count":null},{"id":"758904a0","cell_type":"markdown","source":"Построим бейзлайн на statsforecast, для этого сделаем уникальные айдишники {store}_{family}","metadata":{}},{"id":"de2807e5","cell_type":"code","source":"df = agg.copy()\n\ndf[\"unique_id\"] = df[\"store_nbr\"].astype(str) + \"_\" + df[\"family\"]\n\n# Обрезка слишком коротких рядов\n# (сезонность недельная, поэтому нужно как минимум 2 цикла взять для обучения)\nseries_len = df.groupby(\"unique_id\")[\"ds\"].count()\nlong_series = series_len[series_len >= 14].index\ndf = df[df.unique_id.isin(long_series)]\n\ndf_full = (\n    df[[\"unique_id\", \"ds\", \"y\"]]\n    .sort_values([\"unique_id\", \"ds\"])\n    .reset_index(drop=True)\n)\ndf_sample = (\n    df.loc[df.store_nbr == sample_store, [\"family\", \"ds\", \"y\"]]\n    .rename(columns={\"family\": \"unique_id\"})\n    .sort_values([\"unique_id\", \"ds\"])\n    .reset_index(drop=True)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:59:50.700740Z","iopub.execute_input":"2026-03-18T16:59:50.701020Z","iopub.status.idle":"2026-03-18T16:59:52.663440Z","shell.execute_reply.started":"2026-03-18T16:59:50.700993Z","shell.execute_reply":"2026-03-18T16:59:52.662425Z"}},"outputs":[],"execution_count":null},{"id":"84a2a97b","cell_type":"code","source":"len(df_full), len(df_sample)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:59:54.488562Z","iopub.execute_input":"2026-03-18T16:59:54.489170Z","iopub.status.idle":"2026-03-18T16:59:54.494056Z","shell.execute_reply.started":"2026-03-18T16:59:54.489137Z","shell.execute_reply":"2026-03-18T16:59:54.493258Z"}},"outputs":[],"execution_count":null},{"id":"934fa89b-80f0-4875-be61-fc2978aee27a","cell_type":"markdown","source":"Протокол валидациии - так как в тесте представлены будущие значения на 16 дней вперед","metadata":{}},{"id":"7f3fb9cc","cell_type":"code","source":"# Разобьем на test и val (как в трейне)\nval_size = 16\ncutoff = df.ds.max() - pd.Timedelta(days=val_size)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:00:11.098825Z","iopub.execute_input":"2026-03-18T17:00:11.099426Z","iopub.status.idle":"2026-03-18T17:00:11.108391Z","shell.execute_reply.started":"2026-03-18T17:00:11.099393Z","shell.execute_reply":"2026-03-18T17:00:11.107575Z"}},"outputs":[],"execution_count":null},{"id":"b74cce35","cell_type":"code","source":"sf_train = df_sample[df_sample.ds <= cutoff]\nsf_val = df_sample[df_sample.ds > cutoff]\n\nsf_train.shape, sf_val.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:00:12.440570Z","iopub.execute_input":"2026-03-18T17:00:12.441186Z","iopub.status.idle":"2026-03-18T17:00:12.450513Z","shell.execute_reply.started":"2026-03-18T17:00:12.441153Z","shell.execute_reply":"2026-03-18T17:00:12.449680Z"}},"outputs":[],"execution_count":null},{"id":"8d950e4e","cell_type":"markdown","source":"Для StatsForecast можем взять:\n- Naive - всегда применим\n- SeasonalNaive - есть сезонность, значит применим\n- AutoTheta - тоже почти всегда применим\n- AutoETS - ряды взяли достаточно длинные и есть сезонность, применим","metadata":{}},{"id":"b7d7c740","cell_type":"code","source":"sf_sample = StatsForecast(\n    models=[\n        Naive(),\n        SeasonalNaive(season_length=7),\n        AutoTheta(season_length=7),\n        AutoETS(season_length=7)\n    ],\n    freq=\"D\",\n    n_jobs=-1,\n).fit(sf_train)\n\nforecasts_sample = sf_sample.predict(h=val_size)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:01:14.260992Z","iopub.execute_input":"2026-03-18T17:01:14.261680Z","iopub.status.idle":"2026-03-18T17:01:46.311803Z","shell.execute_reply.started":"2026-03-18T17:01:14.261642Z","shell.execute_reply":"2026-03-18T17:01:46.310640Z"}},"outputs":[],"execution_count":null},{"id":"156100f4","cell_type":"code","source":"# Реализуем метрику из описания в каггле, но без весов (нет айтемов)\ndef rmsle(y_true, y_pred):\n    y_pred = np.clip(y_pred, 0, None)\n    return np.sqrt(np.mean((np.log1p(y_pred) - np.log1p(y_true)) ** 2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:01:46.313818Z","iopub.execute_input":"2026-03-18T17:01:46.314252Z","iopub.status.idle":"2026-03-18T17:01:46.319882Z","shell.execute_reply.started":"2026-03-18T17:01:46.314180Z","shell.execute_reply":"2026-03-18T17:01:46.319008Z"}},"outputs":[],"execution_count":null},{"id":"ebe70a08","cell_type":"code","source":"eval_df = sf_val.merge(forecasts_sample.reset_index(), on=[\"unique_id\", \"ds\"])\n\nmodels = [\"Naive\", \"SeasonalNaive\", \"AutoTheta\", \"AutoETS\"]\nfor m in models:\n    score = rmsle(eval_df[\"y\"].values, eval_df[m].values)\n    print(f\"{m} - {score}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:01:47.617858Z","iopub.execute_input":"2026-03-18T17:01:47.618466Z","iopub.status.idle":"2026-03-18T17:01:47.629491Z","shell.execute_reply.started":"2026-03-18T17:01:47.618433Z","shell.execute_reply":"2026-03-18T17:01:47.628713Z"}},"outputs":[],"execution_count":null},{"id":"0b3d7fe8","cell_type":"code","source":"actual = df_sample[df_sample.unique_id == sample_family].copy()\n\nforecasts_reset = forecasts_sample.reset_index()\npreds = forecasts_reset[forecasts_reset.unique_id == sample_family].copy()\n\nfig = go.Figure()\n\ntrain_part = actual[actual.ds <= cutoff]\nfig.add_trace(\n    go.Scatter(x=train_part.ds, y=train_part.y, name=\"train\", line=dict(color=\"blue\"))\n)\n\nval_part = actual[actual.ds > cutoff]\nfig.add_trace(\n    go.Scatter(x=val_part.ds, y=val_part.y, name=\"val\", line=dict(color=\"green\"))\n)\n\nfor m in models:\n    fig.add_trace(\n        go.Scatter(x=preds.ds, y=preds[m], name=m, line=dict(dash=\"dash\"))\n    )\n\nfig.update_layout(\n    title=\"Предсказания для store 1, family GROCERY I\",\n    xaxis_range=[cutoff - pd.Timedelta(days=60), actual.ds.max()]\n)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:02:48.341049Z","iopub.execute_input":"2026-03-18T17:02:48.341917Z","iopub.status.idle":"2026-03-18T17:02:48.410013Z","shell.execute_reply.started":"2026-03-18T17:02:48.341882Z","shell.execute_reply":"2026-03-18T17:02:48.409221Z"}},"outputs":[],"execution_count":null},{"id":"ccac243a-eeb6-4189-96a4-b35189e57a98","cell_type":"markdown","source":"Видно, что лучше всего показывает себя AutoETS, что подтверждается качеством 0.43.\n\nТеперь сделаем прогон на всех рядах:","metadata":{}},{"id":"f52a7fe2-d9b7-4d64-86a3-327c5f7e4cfe","cell_type":"code","source":"sf_train = df_full[df_full.ds <= cutoff]\nsf_val = df_full[df_full.ds > cutoff]\n\nsf_train.shape, sf_val.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:05:08.888761Z","iopub.execute_input":"2026-03-18T17:05:08.889389Z","iopub.status.idle":"2026-03-18T17:05:09.085581Z","shell.execute_reply.started":"2026-03-18T17:05:08.889354Z","shell.execute_reply":"2026-03-18T17:05:09.084777Z"}},"outputs":[],"execution_count":null},{"id":"1c9b22ae","cell_type":"code","source":"sf = StatsForecast(\n    models=[\n        Naive(),\n        SeasonalNaive(season_length=7),\n        AutoTheta(season_length=7),\n        AutoETS(season_length=7)\n    ],\n    freq=\"D\",\n    n_jobs=-1,\n).fit(sf_train)\n\nforecasts = sf.predict(h=val_size)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:05:19.215457Z","iopub.execute_input":"2026-03-18T17:05:19.216291Z","iopub.status.idle":"2026-03-18T17:31:25.551791Z","shell.execute_reply.started":"2026-03-18T17:05:19.216248Z","shell.execute_reply":"2026-03-18T17:31:25.550810Z"}},"outputs":[],"execution_count":null},{"id":"673bce62","cell_type":"code","source":"eval_df = sf_val.merge(forecasts.reset_index(), on=[\"unique_id\", \"ds\"])\n\nmodels = [\"Naive\", \"SeasonalNaive\", \"AutoTheta\", \"AutoETS\"]\nfor m in models:\n    score = rmsle(eval_df[\"y\"].values, eval_df[m].values)\n    print(f\"{m} - {score}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:31:25.553748Z","iopub.execute_input":"2026-03-18T17:31:25.554309Z","iopub.status.idle":"2026-03-18T17:31:25.581938Z","shell.execute_reply.started":"2026-03-18T17:31:25.554268Z","shell.execute_reply":"2026-03-18T17:31:25.581380Z"}},"outputs":[],"execution_count":null},{"id":"16acd797-0852-40fa-b387-75ad0a32542b","cell_type":"markdown","source":"Качество в тотале лучше у AutoETS = 0.485 - Будем дальше сравнивать с этим показателем","metadata":{}},{"id":"0857a590-e5f9-4013-8dbc-a6a94ca61904","cell_type":"markdown","source":"Для обучения на классическом ML нужны признаки.\n\nДля начала поглядим на вещественные, он такой только один из всех наших данных - dcoilwtico (цена на нефть)","metadata":{}},{"id":"97e7d7dd-f2c0-4b97-9226-2a1a35ea7672","cell_type":"code","source":"merged = daily_sales.merge(oil, on=\"date\", how=\"left\")\n\nfig = go.Figure()\n\nfig.add_trace(\n    go.Scatter(x=merged.date, y=merged.unit_sales, name=\"unit_sales\", yaxis='y')\n)\nfig.add_trace(\n    go.Scatter(x=merged.date, y=merged.dcoilwtico, name=\"dcoilwtico\", yaxis='y2')\n)\nfig.update_layout(\n    title=\"Продажи vs Цена на нефть\",\n    xaxis=dict(title='Дата'),\n    yaxis=dict(\n        side='left',\n        titlefont=dict(color='blue'),\n        tickfont=dict(color='blue'),\n    ),\n    yaxis2=dict(\n        side='right',\n        anchor='x',\n        overlaying='y',\n        titlefont=dict(color='red'),\n        tickfont=dict(color='red'),\n    ),\n)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:33:25.547140Z","iopub.execute_input":"2026-03-18T17:33:25.547598Z","iopub.status.idle":"2026-03-18T17:33:26.228314Z","shell.execute_reply.started":"2026-03-18T17:33:25.547565Z","shell.execute_reply":"2026-03-18T17:33:26.227685Z"}},"outputs":[],"execution_count":null},{"id":"f82beb50-4d63-4d1c-b825-9d09b1df2303","cell_type":"markdown","source":"Возможно есть обратная корреляция, в любом случае добавим этот признак для обучения\n\nСамые важные признаки - лаг (значение X дней назад) и скользящее среднее, брать можем не раньше 16 дня, так как на тесте ее посчитать нельзя","metadata":{}},{"id":"e965b43c-690c-4ced-9db0-3f76f75bb421","cell_type":"code","source":"def add_calendar_features(df: pd.DataFrame) -> pd.DataFrame:\n    df = df.copy()\n    df[\"month\"] = df[\"date\"].dt.month\n    df[\"day\"] = df[\"date\"].dt.day\n    df[\"is_weekend\"] = (df[\"date\"].dt.dayofweek >= 5).astype(np.int8)\n    return df\n\n\ndef add_store_features(df: pd.DataFrame, stores: pd.DataFrame) -> pd.DataFrame:\n    return df.merge(stores, on=\"store_nbr\", how=\"left\")\n\n\ndef add_oil(df: pd.DataFrame, oil: pd.DataFrame) -> pd.DataFrame:\n    return df.merge(oil, on=\"date\", how=\"left\")\n\n\ndef add_lag_features(df: pd.DataFrame, id_cols: list[str]) -> pd.DataFrame:\n    df = df.sort_values(id_cols + [\"date\"]).copy()\n    lags = [16, 17, 21, 22, 28, 29]\n    for lag in lags:\n        df[f\"lag_{lag}\"] = df.groupby(id_cols)[\"unit_sales\"].shift(lag)\n    return df\n\n\ndef add_rolling_features(df: pd.DataFrame, id_cols: list[str]) -> pd.DataFrame:\n    df = df.sort_values(id_cols + [\"date\"]).copy()\n    # shift(16) чтобы rolling не заглядывал в горизонт прогноза\n    windows = [7, 14, 28]\n    for w in windows:\n        df[f\"roll_mean_{w}\"] = df.groupby(id_cols)[\"unit_sales\"].transform(\n            lambda s: s.shift(16).rolling(w, min_periods=1).mean()\n        )\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:46:16.173379Z","iopub.execute_input":"2026-03-18T17:46:16.174142Z","iopub.status.idle":"2026-03-18T17:46:16.182015Z","shell.execute_reply.started":"2026-03-18T17:46:16.174111Z","shell.execute_reply":"2026-03-18T17:46:16.181319Z"}},"outputs":[],"execution_count":null},{"id":"ad64588c-b74f-43fd-8d14-8fb6b6351ae7","cell_type":"code","source":"merged = train.merge(items[[\"item_nbr\", \"family\", \"perishable\"]], on=\"item_nbr\", how=\"left\")\nmerged.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:38:37.651343Z","iopub.execute_input":"2026-03-18T17:38:37.652143Z","iopub.status.idle":"2026-03-18T17:38:52.508229Z","shell.execute_reply.started":"2026-03-18T17:38:37.652108Z","shell.execute_reply":"2026-03-18T17:38:52.507423Z"}},"outputs":[],"execution_count":null},{"id":"6eb38784-3b29-4623-9d0c-6120e4044bd8","cell_type":"code","source":"agg = (\n    merged.groupby([\"date\", \"store_nbr\", \"family\"], observed=True)\n    .agg(\n        unit_sales=(\"unit_sales\", \"sum\"),\n        onpromotion=(\"onpromotion\", \"sum\"),\n        perishable=(\"perishable\", \"min\"),\n    )\n    .reset_index()\n    .sort_values([\"store_nbr\", \"family\", \"date\"])\n    .reset_index(drop=True)\n)\nagg.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:42:38.141277Z","iopub.execute_input":"2026-03-18T17:42:38.142078Z","iopub.status.idle":"2026-03-18T17:42:56.129942Z","shell.execute_reply.started":"2026-03-18T17:42:38.142047Z","shell.execute_reply":"2026-03-18T17:42:56.129002Z"}},"outputs":[],"execution_count":null},{"id":"971b2c0f","cell_type":"code","source":"cb_df = agg.copy()\n\ncb_df = add_calendar_features(cb_df)\ncb_df = add_oil(cb_df, oil)\ncb_df = add_store_features(cb_df, stores)\ncb_df = add_lag_features(cb_df, id_cols=[\"store_nbr\", \"family\"])\ncb_df = add_rolling_features(cb_df, id_cols=[\"store_nbr\", \"family\"])\n\ncb_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:49:42.931925Z","iopub.execute_input":"2026-03-18T17:49:42.932682Z","iopub.status.idle":"2026-03-18T17:49:48.292124Z","shell.execute_reply.started":"2026-03-18T17:49:42.932646Z","shell.execute_reply":"2026-03-18T17:49:48.291187Z"}},"outputs":[],"execution_count":null},{"id":"174fb727-cfbf-44e0-9d1c-e26907ee8e51","cell_type":"code","source":"cb_df.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:50:59.561136Z","iopub.execute_input":"2026-03-18T17:50:59.561732Z","iopub.status.idle":"2026-03-18T17:51:00.654629Z","shell.execute_reply.started":"2026-03-18T17:50:59.561697Z","shell.execute_reply":"2026-03-18T17:51:00.653966Z"}},"outputs":[],"execution_count":null},{"id":"cbdadf60","cell_type":"code","source":"feature_cols = [c for c in cb_df.columns if c not in [\"date\", \"unit_sales\"]]\ncat_features = [\"store_nbr\", \"family\", \"month\", \"is_weekend\", \"city\", \"state\", \"type\", \"cluster\", \"perishable\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:53:41.474034Z","iopub.execute_input":"2026-03-18T17:53:41.474387Z","iopub.status.idle":"2026-03-18T17:53:41.478995Z","shell.execute_reply.started":"2026-03-18T17:53:41.474358Z","shell.execute_reply":"2026-03-18T17:53:41.478077Z"}},"outputs":[],"execution_count":null},{"id":"e4f65ff0-13cd-4c6f-8154-38fa08ca02a3","cell_type":"code","source":"cb_df_sample = cb_df[cb_df.store_nbr == sample_store]\n\ncb_train = cb_df_sample[cb_df_sample.date <= cutoff]\ncb_val = cb_df_sample[cb_df_sample.date > cutoff]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:54:11.826641Z","iopub.execute_input":"2026-03-18T17:54:11.827374Z","iopub.status.idle":"2026-03-18T17:54:11.865884Z","shell.execute_reply.started":"2026-03-18T17:54:11.827340Z","shell.execute_reply":"2026-03-18T17:54:11.865232Z"}},"outputs":[],"execution_count":null},{"id":"ee546c16-09eb-4b07-8822-e7e26d90e62c","cell_type":"code","source":"len(cb_train), len(cb_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T17:54:13.298280Z","iopub.execute_input":"2026-03-18T17:54:13.298992Z","iopub.status.idle":"2026-03-18T17:54:13.303863Z","shell.execute_reply.started":"2026-03-18T17:54:13.298951Z","shell.execute_reply":"2026-03-18T17:54:13.303265Z"}},"outputs":[],"execution_count":null},{"id":"d9f7049e-a3c2-4db5-9dcd-23215b43d5f2","cell_type":"code","source":"from itertools import product\n\nparam_grid = {\n    \"depth\": [4, 6, 8],\n    \"learning_rate\": [0.03, 0.05, 0.1],\n    \"l2_leaf_reg\": [1, 3, 5],\n}\n\nresults = []\nfor depth, lr, l2 in product(\n    param_grid[\"depth\"],\n    param_grid[\"learning_rate\"],\n    param_grid[\"l2_leaf_reg\"],\n):\n    model = CatBoostRegressor(\n        iterations=1500,\n        depth=depth,\n        learning_rate=lr,\n        l2_leaf_reg=l2,\n        loss_function=\"RMSE\",\n        random_seed=42,\n        verbose=0,\n        early_stopping_rounds=50,\n    )\n\n    model.fit(\n        cb_train[feature_cols], cb_train[\"unit_sales\"],\n        eval_set=(cb_val[feature_cols], cb_val[\"unit_sales\"]),\n        cat_features=cat_features,\n    )\n\n    preds = model.predict(cb_val[feature_cols]).clip(0)\n    score = rmsle(cb_val[\"unit_sales\"].values, preds)\n\n    print({\n        \"depth\": depth,\n        \"lr\": lr,\n        \"l2\": l2,\n        \"rmsle\": score,\n        \"best_iter\": model.best_iteration_\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:07:39.041138Z","iopub.execute_input":"2026-03-18T18:07:39.041570Z","iopub.status.idle":"2026-03-18T18:10:30.889789Z","shell.execute_reply.started":"2026-03-18T18:07:39.041539Z","shell.execute_reply":"2026-03-18T18:10:30.888688Z"}},"outputs":[],"execution_count":null},{"id":"90434cb2","cell_type":"code","source":"model = CatBoostRegressor(\n    iterations=1500,\n    learning_rate=0.1,\n    depth=8,\n    l2_leaf_reg=5,\n    loss_function=\"RMSE\",\n    random_seed=42,\n    verbose=0,\n)\n\nmodel.fit(\n    cb_train[feature_cols], cb_train[\"unit_sales\"],\n    eval_set=(cb_val[feature_cols], cb_val[\"unit_sales\"]),\n    cat_features=cat_features,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:12:13.068405Z","iopub.execute_input":"2026-03-18T18:12:13.068915Z","iopub.status.idle":"2026-03-18T18:12:47.443746Z","shell.execute_reply.started":"2026-03-18T18:12:13.068881Z","shell.execute_reply":"2026-03-18T18:12:47.443001Z"}},"outputs":[],"execution_count":null},{"id":"be8ec6b0","cell_type":"code","source":"cb_preds = model.predict(cb_val[feature_cols]).clip(0)\nrmsle(cb_val['unit_sales'].values, cb_preds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:13:08.113512Z","iopub.execute_input":"2026-03-18T18:13:08.113802Z","iopub.status.idle":"2026-03-18T18:13:08.126549Z","shell.execute_reply.started":"2026-03-18T18:13:08.113776Z","shell.execute_reply":"2026-03-18T18:13:08.125775Z"}},"outputs":[],"execution_count":null},{"id":"2948e619-3bae-4e59-aa14-9d4ed35bdc29","cell_type":"code","source":"val_mask = (cb_val.store_nbr == sample_store) & (cb_val.family == sample_family)\nval_part = cb_val[val_mask].copy()\nval_part[\"pred\"] = cb_preds[val_mask.values]\n\ntrain_mask = (cb_train.store_nbr == sample_store) & (cb_train.family == sample_family)\ntrain_part = cb_train[train_mask]\n\nfig = go.Figure()\n\nfig.add_trace(\n    go.Scatter(x=train_part.date, y=train_part.unit_sales, name=\"train\", line=dict(color=\"blue\"))\n)\nfig.add_trace(\n    go.Scatter(x=val_part.date, y=val_part.unit_sales, name=\"val\", line=dict(color=\"green\"))\n)\nfig.add_trace(\n    go.Scatter(x=val_part.date, y=val_part.pred, name=\"CatBoost\", line=dict(dash=\"dash\"))\n)\n\nfig.update_layout(\n    title=f\"Предсказания CatBoost store 1, family GROCERY I\",\n    xaxis_range=[cutoff - pd.Timedelta(days=60), cb_val.date.max()]\n)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:13:16.215866Z","iopub.execute_input":"2026-03-18T18:13:16.216425Z","iopub.status.idle":"2026-03-18T18:13:16.268369Z","shell.execute_reply.started":"2026-03-18T18:13:16.216391Z","shell.execute_reply":"2026-03-18T18:13:16.267720Z"}},"outputs":[],"execution_count":null},{"id":"9d71e0d4-cb27-402d-b414-f2e59a2369b9","cell_type":"markdown","source":"График довольно сильно напоминает AutoETS но по качеству модель справилась чуть хуже","metadata":{}},{"id":"30b3123e","cell_type":"code","source":"pd.Series(model.feature_importances_, index=feature_cols).sort_values(ascending=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:13:42.160390Z","iopub.execute_input":"2026-03-18T18:13:42.160968Z","iopub.status.idle":"2026-03-18T18:13:42.169259Z","shell.execute_reply.started":"2026-03-18T18:13:42.160912Z","shell.execute_reply":"2026-03-18T18:13:42.168184Z"}},"outputs":[],"execution_count":null},{"id":"7b3136e4-9f39-4428-aba4-fc7dd557807a","cell_type":"markdown","source":"Как оказалось, CatBoost не смотрел на фичи связаные с конкретным магазином, поэтому можем попробовать обучить без них уже на всем датасете","metadata":{}},{"id":"8581c044","cell_type":"code","source":"cb_train = cb_df[cb_df.date <= cutoff]\ncb_val = cb_df[cb_df.date > cutoff]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:13:50.889621Z","iopub.execute_input":"2026-03-18T18:13:50.889918Z","iopub.status.idle":"2026-03-18T18:13:51.117264Z","shell.execute_reply.started":"2026-03-18T18:13:50.889892Z","shell.execute_reply":"2026-03-18T18:13:51.116164Z"}},"outputs":[],"execution_count":null},{"id":"12a4b7c7-5412-4c75-a877-67dc8647805f","cell_type":"code","source":"len(cb_train), len(cb_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:13:53.450186Z","iopub.execute_input":"2026-03-18T18:13:53.451026Z","iopub.status.idle":"2026-03-18T18:13:53.456148Z","shell.execute_reply.started":"2026-03-18T18:13:53.450990Z","shell.execute_reply":"2026-03-18T18:13:53.455252Z"}},"outputs":[],"execution_count":null},{"id":"79ab17dc","cell_type":"code","source":"model = CatBoostRegressor(\n    iterations=1500,\n    learning_rate=0.1,\n    depth=8,\n    l2_leaf_reg=5,\n    loss_function=\"RMSE\",\n    random_seed=42,\n    verbose=0,\n)\n\nmodel.fit(\n    cb_train[feature_cols], cb_train[\"unit_sales\"],\n    eval_set=(cb_val[feature_cols], cb_val[\"unit_sales\"]),\n    cat_features=cat_features,\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"9dd42a86","cell_type":"code","source":"cb_preds = model.predict(cb_val[feature_cols]).clip(0)\nrmsle(cb_val['unit_sales'].values, cb_preds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:26:42.301050Z","iopub.status.idle":"2026-03-18T18:26:42.301403Z","shell.execute_reply.started":"2026-03-18T18:26:42.301254Z","shell.execute_reply":"2026-03-18T18:26:42.301276Z"}},"outputs":[],"execution_count":null},{"id":"32f64eb8-db6a-4727-a48b-1a67d8b94ad1","cell_type":"markdown","source":"Видим что CatBoost справляется чуть хуже чем AutoETS - 0.485 против 0.61 у CatBoost","metadata":{}},{"id":"635a6b84-ac70-4e7d-a2ef-feaa70d0e1df","cell_type":"markdown","source":"Финальным этапом пробуем DL-подход. Обучаем TFT (один из лучших подходов для временных рядов)\nДля него не нужны lag_ и rolling_ фичи, плюс уберем city/state/type/cluster - они занулились в CatBoost, поэтому не очень важны)","metadata":{}},{"id":"b07f36ca-7cb1-416d-92db-7e6666c4e478","cell_type":"code","source":"tft_df = cb_df[[\"store_nbr\", \"family\", \"date\", \"unit_sales\", \"onpromotion\", \"month\", \"day\", \"is_weekend\", \"dcoilwtico\", \"perishable\"]].copy()\ntft_df[\"dcoilwtico\"] = tft_df[\"dcoilwtico\"].ffill().bfill()\ntft_df[\"time_idx\"] = (tft_df[\"date\"] - tft_df[\"date\"].min()).dt.days\n\n# Здесь как для статических методов делаем unique_id \ntft_df[\"series_id\"] = tft_df[\"store_nbr\"].astype(str) + \"_\" + tft_df[\"family\"]\n\nfor col in [\"store_nbr\", \"month\", \"day\", \"is_weekend\", \"perishable\"]:\n    tft_df[col] = tft_df[col].astype(str)\n\ntft_df = tft_df.sort_values([\"series_id\", \"date\"]).reset_index(drop=True)\n\ntft_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:27:24.792672Z","iopub.execute_input":"2026-03-18T18:27:24.793699Z","iopub.status.idle":"2026-03-18T18:27:29.518375Z","shell.execute_reply.started":"2026-03-18T18:27:24.793657Z","shell.execute_reply":"2026-03-18T18:27:29.517482Z"}},"outputs":[],"execution_count":null},{"id":"26a870ee-feb9-439c-815b-78a5f7d13e39","cell_type":"code","source":"tft_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:28:30.343731Z","iopub.execute_input":"2026-03-18T18:28:30.344600Z","iopub.status.idle":"2026-03-18T18:28:30.353751Z","shell.execute_reply.started":"2026-03-18T18:28:30.344565Z","shell.execute_reply":"2026-03-18T18:28:30.352793Z"}},"outputs":[],"execution_count":null},{"id":"39e4d0c8-110e-450a-b643-3d0aa1d53d55","cell_type":"markdown","source":"Сначала обучим и проверим на sample одного стора, чтобы убедиться в корректности написанного","metadata":{}},{"id":"0d561177-20a2-47e6-b1a5-b3ff42f65e52","cell_type":"code","source":"max_encoder_length = 28\nmax_prediction_length = 16\ntraining_cutoff = tft_df.time_idx.max() - max_prediction_length","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"dac1abf1-0c76-4de4-9d0c-6ff6402a63c0","cell_type":"code","source":"tft_sample = tft_df[tft_df.store_nbr == \"1\"].copy()               \n                                                                                          \ntraining_sample = pf.TimeSeriesDataSet(\n    tft_sample[tft_sample.time_idx <= training_cutoff],                                 \n    time_idx=\"time_idx\",                                  \n    target=\"unit_sales\",\n    group_ids=[\"series_id\"],\n    max_encoder_length=max_encoder_length,\n    max_prediction_length=max_prediction_length,\n    static_categoricals=[\"store_nbr\", \"family\", \"perishable\"],\n    time_varying_known_categoricals=[\"month\", \"day\", \"is_weekend\"],\n    time_varying_known_reals=[\"time_idx\", \"dcoilwtico\", \"onpromotion\"],\n    time_varying_unknown_reals=[\"unit_sales\"],\n    target_normalizer=pf.GroupNormalizer(groups=[\"series_id\"]),\n    allow_missing_timesteps=True,\n)\n\nvalidation_sample = pf.TimeSeriesDataSet.from_dataset(\n    training_sample, tft_sample, min_prediction_idx=training_cutoff + 1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:44:57.524775Z","iopub.execute_input":"2026-03-18T18:44:57.525615Z","iopub.status.idle":"2026-03-18T18:44:58.009438Z","shell.execute_reply.started":"2026-03-18T18:44:57.525578Z","shell.execute_reply":"2026-03-18T18:44:58.008413Z"}},"outputs":[],"execution_count":null},{"id":"ab727220-c112-44a9-8db4-dd856a71cb1d","cell_type":"markdown","source":"TimeSeriesDataSet сам исключил для нас короткие ряды. Теперь обучим модель","metadata":{}},{"id":"04fa6d81-bdf3-42d6-9536-3537e742cac2","cell_type":"code","source":"train_dl = training_sample.to_dataloader(train=True, batch_size=32, num_workers=0)\nval_dl = validation_sample.to_dataloader(train=False, batch_size=32, num_workers=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:55:59.177272Z","iopub.execute_input":"2026-03-18T18:55:59.177946Z","iopub.status.idle":"2026-03-18T18:55:59.183016Z","shell.execute_reply.started":"2026-03-18T18:55:59.177908Z","shell.execute_reply":"2026-03-18T18:55:59.182235Z"}},"outputs":[],"execution_count":null},{"id":"cc1b2f6f-2203-442e-82a1-9b5485b648d1","cell_type":"code","source":"len(train_dl), len(val_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:56:00.553674Z","iopub.execute_input":"2026-03-18T18:56:00.554442Z","iopub.status.idle":"2026-03-18T18:56:00.559574Z","shell.execute_reply.started":"2026-03-18T18:56:00.554403Z","shell.execute_reply":"2026-03-18T18:56:00.558824Z"}},"outputs":[],"execution_count":null},{"id":"b34443c3-f942-4e72-b3e6-14daa5bc8f19","cell_type":"code","source":"tft = pf.TemporalFusionTransformer.from_dataset(\n    training_sample,\n    hidden_size=16,\n    attention_head_size=1,\n    dropout=0.1,\n    hidden_continuous_size=8,\n    learning_rate=0.02,\n    reduce_on_plateau_patience=4,\n)\n\ntrainer = pl.Trainer(max_epochs=5, accelerator=\"auto\", gradient_clip_val=0.1)\ntrainer.fit(tft, train_dataloaders=train_dl, val_dataloaders=val_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T18:56:02.052408Z","iopub.execute_input":"2026-03-18T18:56:02.052836Z","iopub.status.idle":"2026-03-18T19:03:23.088054Z","shell.execute_reply.started":"2026-03-18T18:56:02.052802Z","shell.execute_reply":"2026-03-18T19:03:23.087470Z"}},"outputs":[],"execution_count":null},{"id":"e3453529-7d96-43f3-a36a-dda3bbdf19fc","cell_type":"code","source":"preds = tft.predict(val_dl, return_x=True)\npredictions = preds.output\n\nactuals = torch.cat([y[0] for x, y in iter(val_dl)])\npred_flat = preds.flatten().numpy().clip(0)\nactual_flat = actuals.flatten().numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:03:41.461250Z","iopub.execute_input":"2026-03-18T19:03:41.462047Z","iopub.status.idle":"2026-03-18T19:03:41.689723Z","shell.execute_reply.started":"2026-03-18T19:03:41.462014Z","shell.execute_reply":"2026-03-18T19:03:41.688779Z"}},"outputs":[],"execution_count":null},{"id":"46e42ec5-3ee5-4b67-8269-31182d00e1b0","cell_type":"code","source":"rmsle(actual_flat, pred_flat)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"eec699b7-17dc-47d2-975e-55438f9f5941","cell_type":"markdown","source":"Пока это лучший результат для одного стора - 0.43 против 0.48 у AutoETS и 0.61 у CatBoost","metadata":{}},{"id":"b0429b18-0750-46e2-93d9-78bb3678a1ff","cell_type":"code","source":"sid = f\"1_{sample_family}\"\n\nseries_ids = validation_sample.decoded_index[\"series_id\"].unique()\nidx = list(series_ids).index(sid)\n\npred_series = predictions[idx].cpu().numpy().clip(0)\nactual_series = actuals[idx].cpu().numpy()\n\nval_dates = sorted(tft_sample[tft_sample.time_idx > training_cutoff].date.unique())[:16]\n\nhist = tft_sample[(tft_sample.series_id == sid) & (tft_sample.time_idx <= training_cutoff)]\n\nfig = go.Figure()\n\nfig.add_trace(\n    go.Scatter(x=hist.date, y=hist.unit_sales, name=\"train\", line=dict(color=\"blue\"))\n)\nfig.add_trace(\n    go.Scatter(x=val_dates, y=actual_series, name=\"actual\", line=dict(color=\"green\"))\n)\nfig.add_trace(\n    go.Scatter(x=val_dates, y=pred_series, name=\"TFT\", line=dict(dash=\"dash\", color=\"red\"))\n)\n\nfig.update_layout(\n    title=f\"Предсказания TFT store 1, family GROCERY I\",\n    xaxis_range=[cutoff - pd.Timedelta(days=60), val_dates[-1]],\n)\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:10:29.157862Z","iopub.execute_input":"2026-03-18T19:10:29.158707Z","iopub.status.idle":"2026-03-18T19:10:29.212057Z","shell.execute_reply.started":"2026-03-18T19:10:29.158672Z","shell.execute_reply":"2026-03-18T19:10:29.211414Z"}},"outputs":[],"execution_count":null},{"id":"ccd502d5","cell_type":"code","source":"training = pf.TimeSeriesDataSet(\n    tft_df[tft_df.time_idx <= training_cutoff],\n    time_idx=\"time_idx\",\n    target=\"unit_sales\",\n    group_ids=[\"series_id\"],\n    max_encoder_length=max_encoder_length,\n    max_prediction_length=max_prediction_length,\n    static_categoricals=[\"store_nbr\", \"family\", \"perishable\"],\n    time_varying_known_categoricals=[\"day\", \"month\", \"is_weekend\"],\n    time_varying_known_reals=[\"time_idx\", \"dcoilwtico\", \"onpromotion\"],\n    time_varying_unknown_reals=[\"unit_sales\"],\n    target_normalizer=pf.GroupNormalizer(groups=[\"series_id\"]),\n    allow_missing_timesteps=True,\n)\n\nvalidation = pf.TimeSeriesDataSet.from_dataset(\n    training, tft_df, min_prediction_idx=training_cutoff + 1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:11:41.853898Z","iopub.execute_input":"2026-03-18T19:11:41.854380Z","iopub.status.idle":"2026-03-18T19:11:59.050301Z","shell.execute_reply.started":"2026-03-18T19:11:41.854348Z","shell.execute_reply":"2026-03-18T19:11:59.049331Z"}},"outputs":[],"execution_count":null},{"id":"f9fb9f9f-7f09-415c-b111-0622ad47afb1","cell_type":"code","source":"train_dl = training.to_dataloader(train=True, batch_size=512, num_workers=3)\nval_dl = validation.to_dataloader(train=False, batch_size=512, num_workers=3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:12:33.295459Z","iopub.execute_input":"2026-03-18T19:12:33.296067Z","iopub.status.idle":"2026-03-18T19:12:33.300586Z","shell.execute_reply.started":"2026-03-18T19:12:33.296032Z","shell.execute_reply":"2026-03-18T19:12:33.299823Z"}},"outputs":[],"execution_count":null},{"id":"1677795e-25fb-46f2-aeb7-aaff3d196871","cell_type":"code","source":"len(train_dl), len(val_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:12:35.236183Z","iopub.execute_input":"2026-03-18T19:12:35.236863Z","iopub.status.idle":"2026-03-18T19:12:35.241950Z","shell.execute_reply.started":"2026-03-18T19:12:35.236828Z","shell.execute_reply":"2026-03-18T19:12:35.241141Z"}},"outputs":[],"execution_count":null},{"id":"772de2c1","cell_type":"code","source":"tft = pf.TemporalFusionTransformer.from_dataset(\n    training,\n    hidden_size=16,\n    attention_head_size=1,\n    dropout=0.1,\n    hidden_continuous_size=8,\n    learning_rate=0.02,\n    reduce_on_plateau_patience=4,\n)\n\ntrainer = pl.Trainer(max_epochs=5, accelerator=\"auto\", gradient_clip_val=0.1)\ntrainer.fit(tft, train_dataloaders=train_dl, val_dataloaders=val_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T19:12:37.654433Z","iopub.execute_input":"2026-03-18T19:12:37.655250Z"}},"outputs":[],"execution_count":null},{"id":"5aaed8f4","cell_type":"code","source":"preds = tft.predict(val_dl, return_x=True)\npredictions = preds.output\n\nactuals = torch.cat([y[0] for x, y in iter(val_dl)])\npred_flat = preds.flatten().numpy().clip(0)\nactual_flat = actuals.flatten().numpy()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"19cec48c-2c75-4320-b814-97cb09d07f2c","cell_type":"code","source":"rmsle(actual_flat, pred_flat)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"330d65b1-0c59-4503-9d2e-9c2be03dcf59","cell_type":"markdown","source":"Выводы:\n","metadata":{}}]}