From 30d5c24a9339c6a6a945e1c24732219991bcc001 Mon Sep 17 00:00:00 2001 From: ogrechko Date: Sat, 27 Dec 2025 21:49:39 +0000 Subject: [PATCH] Backup: Traffic Forecast Dynamic --- backups/2025-12-27/Traffic Forecast Dynamic.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 backups/2025-12-27/Traffic Forecast Dynamic.json diff --git a/backups/2025-12-27/Traffic Forecast Dynamic.json b/backups/2025-12-27/Traffic Forecast Dynamic.json new file mode 100644 index 0000000..4cbb399 --- /dev/null +++ b/backups/2025-12-27/Traffic Forecast Dynamic.json @@ -0,0 +1 @@ +{"updatedAt":"2025-12-12T09:44:10.802Z","createdAt":"2025-12-12T09:43:57.181Z","id":"HeNuVT2DB61vKm6y","name":"Traffic Forecast Dynamic","active":false,"isArchived":false,"nodes":[{"parameters":{"updates":["message"],"additionalFields":{}},"name":"Telegram Trigger","type":"n8n-nodes-base.telegramTrigger","typeVersion":1,"position":[-592,-112],"id":"9bd9cd1c-102e-4aa8-8d6e-b69e5003931c","webhookId":"traffic-forecast-bot","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}},{"parameters":{"resource":"file","fileId":"={{ $json.message.document.file_id }}","additionalFields":{}},"name":"Get File","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[-368,-112],"id":"f4e44340-4096-4c3d-b043-855f418977e0","webhookId":"b572f623-3010-47da-b480-eae5ae111b38","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}},{"parameters":{"options":{}},"name":"Parse File","type":"n8n-nodes-base.spreadsheetFile","typeVersion":2,"position":[-144,-112],"id":"80ae83b6-aa9a-48fb-b2ac-18826df7f7ad"},{"parameters":{"jsCode":"// --- НАСТРОЙКИ МОДЕЛИ ---\n// Alpha (0.1 - 0.9). Чем больше, тем быстрее прогноз реагирует на последние изменения.\n// 0.1 - очень плавный прогноз (учитывает долгую историю)\n// 0.8 - нервный прогноз (повторяет вчерашний день)\nconst ALPHA = 0.3; \n\n// --- ПОЛУЧЕНИЕ ДАННЫХ ---\nconst items = $input.all();\nif (items.length === 0) return [];\nconst inputData = items.map(item => item.json);\n\n// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---\nfunction parseCleanNumber(value) {\n if (typeof value === 'number') return value;\n if (!value) return 0;\n const cleanStr = String(value).replace(/\\s/g, '').replace(/,/g, '.');\n const parsed = parseFloat(cleanStr);\n return isNaN(parsed) ? 0 : parsed;\n}\n\nfunction getJsDateFromExcel(serial) {\n const utc_days = Math.floor(serial - 25569);\n const utc_value = utc_days * 86400; \n const date_info = new Date(utc_value * 1000);\n return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate());\n}\n\nfunction formatDate(date) {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}-${month}-${day}`;\n}\n\n// --- ШАГ 0: ПАРСИНГ И ПОДГОТОВКА ---\nconst keys = Object.keys(inputData[0]);\nconst dateKey = keys.find(k => /date|дата|день/i.test(k)) || keys[0];\nconst trafficKey = keys.find(k => /traffic|трафик|кол-во|value|факт/i.test(k)) || keys[1];\n\n// Преобразуем в удобный массив объектов\nlet parsedRows = inputData.map(row => {\n const rawTraffic = row[trafficKey];\n const rawDate = row[dateKey];\n const traffic = parseCleanNumber(rawTraffic);\n \n let date;\n if (typeof rawDate === 'number') date = getJsDateFromExcel(rawDate);\n else date = new Date(rawDate);\n\n return {\n dateObj: date,\n originalDate: rawDate,\n traffic: traffic,\n hasTraffic: traffic > 0 && !isNaN(traffic),\n isValidDate: !isNaN(date.getTime())\n };\n}).filter(r => r.isValidDate);\n\n// ВАЖНО: Сортируем по дате от прошлого к будущему\nparsedRows.sort((a, b) => a.dateObj - b.dateObj);\n\n// --- ШАГ 1: РАСЧЕТ СЕЗОННОСТИ (Глобальные паттерны) ---\n// Считаем коэффициенты дней недели и месяцев по всей доступной истории\nconst statsDay = {};\nconst statsMonth = {};\nlet globalSum = 0;\nlet globalCount = 0;\n\nparsedRows.forEach(row => {\n if (!row.hasTraffic) return;\n \n const d = row.dateObj.getDay();\n const m = row.dateObj.getMonth();\n \n globalSum += row.traffic;\n globalCount++;\n\n statsDay[d] = (statsDay[d] || {s:0, c:0});\n statsDay[d].s += row.traffic;\n statsDay[d].c++;\n\n statsMonth[m] = (statsMonth[m] || {s:0, c:0});\n statsMonth[m].s += row.traffic;\n statsMonth[m].c++;\n});\n\nconst globalAvg = globalCount > 0 ? globalSum / globalCount : 0;\n\n// Вычисляем коэффициенты (K)\nconst K_Day = {};\nconst K_Month = {};\n\nfor(let i=0; i<7; i++) {\n K_Day[i] = statsDay[i] ? (statsDay[i].s / statsDay[i].c) / globalAvg : 1.0;\n}\nfor(let i=0; i<12; i++) {\n K_Month[i] = statsMonth[i] ? (statsMonth[i].s / statsMonth[i].c) / globalAvg : 1.0;\n}\n\n// --- ШАГ 2: АДАПТИВНЫЙ ПРОГНОЗ (ЭКСПОНЕНЦИАЛЬНОЕ СГЛАЖИВАНИЕ) ---\n// Идем по времени и обновляем \"Уровень\" трафика (Level)\n\n// Начальный уровень берем как глобальное среднее (или среднее первых N дней)\nlet currentLevel = globalAvg;\n\nconst finalOutput = parsedRows.map(row => {\n const d = row.dateObj.getDay();\n const m = row.dateObj.getMonth();\n \n // 1. Рассчитываем сезонный коэффициент для текущей точки\n const seasonality = (K_Day[d] || 1) * (K_Month[m] || 1);\n \n // 2. Делаем прогноз НА СЕГОДНЯ, используя уровень ВЧЕРАШНЕГО дня\n // Forecast = Level * Seasonality\n let forecastVal = Math.round(currentLevel * seasonality);\n \n // 3. Если у нас есть ФАКТ (История), мы корректируем уровень (Обучаемся)\n if (row.hasTraffic) {\n // Очищаем факт от сезонности, чтобы получить \"чистый уровень\"\n const deseasonalizedActual = row.traffic / seasonality;\n \n // Обновляем уровень: \n // Новый уровень = (Alpha * Текущий факт) + ((1-Alpha) * Старый уровень)\n currentLevel = (ALPHA * deseasonalizedActual) + ((1 - ALPHA) * currentLevel);\n }\n \n // Формируем красивую дату\n const dateStr = formatDate(row.dateObj);\n\n return {\n json: {\n \"Дата\": dateStr,\n \"День недели\": row.dateObj.toLocaleDateString('ru-RU', { weekday: 'short' }),\n \"Исторический трафик (Факт)\": row.traffic || 0,\n \"Прогнозный трафик\": forecastVal\n }\n };\n});\n\nreturn finalOutput;"},"name":"Calculate Forecast","type":"n8n-nodes-base.code","typeVersion":2,"position":[80,-112],"id":"17bebbe3-a7bd-4bfb-894b-1d4080b69ba7"},{"parameters":{"operation":"toFile","fileFormat":"xlsx","options":{}},"name":"Create CSV","type":"n8n-nodes-base.spreadsheetFile","typeVersion":2,"position":[304,-112],"id":"a19692de-39db-4622-a15e-cc52f3f5f5e7"},{"parameters":{"operation":"sendDocument","chatId":"={{ $('Telegram Trigger').first().json.message.chat.id }}","binaryData":true,"additionalFields":{}},"name":"Send Result","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[528,-112],"id":"0804d9ba-d8bd-4986-b036-8fba1a95d151","webhookId":"b7e31308-df8c-44ee-9e89-87c4b1b585f7","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}}],"connections":{"Telegram Trigger":{"main":[[{"node":"Get File","type":"main","index":0}]]},"Get File":{"main":[[{"node":"Parse File","type":"main","index":0}]]},"Parse File":{"main":[[{"node":"Calculate Forecast","type":"main","index":0}]]},"Calculate Forecast":{"main":[[{"node":"Create CSV","type":"main","index":0}]]},"Create CSV":{"main":[[{"node":"Send Result","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"be154c7b-ea55-4241-b0c8-6a3e6fc47d4f","activeVersionId":null,"triggerCount":0,"shared":[{"updatedAt":"2025-12-12T09:43:57.181Z","createdAt":"2025-12-12T09:43:57.181Z","role":"workflow:owner","workflowId":"HeNuVT2DB61vKm6y","projectId":"sYuiFAb87pAir6jV"}],"activeVersion":null,"tags":[]} \ No newline at end of file