Прогноза на търсенето на билети с Prophet и локален AI
Как да прогнозираш продажбите на билети за културен и спортен комплекс по тип събитие, как да оцениш прогнозата честно и как локален модел да подсказва идеи за цени. Работим само с агрегирани дневни суми и синтетични данни — без данни за купувачи.
llama3.1:8b през Ollama. Махнахме таблицата с „точност 92 %, 88 %, 85 %, 94 %“ и статуси „Prod/Test“ — никъде не се виждаше откъде са. Вместо нея има начин да измериш точността сам: последните 60 дни извън обучението, наивна прогноза за сравнение и кръстосана проверка. Добавихме: синтетичен генератор, за да върви урокът без реални данни; отрязване на отрицателните прогнози; интервал на прогнозата; правилото, че регресорът трябва да е известен за бъдещето (по документацията на Prophet). Поправихме SQL: по документацията на TimescaleDB първо се създава агрегатът WITH NO DATA, после се опреснява историята, после се добавя политика. Ценовите идеи вече минават през твърди граници и човек; махнахме твърдението, че Prophet „е проектиран“ точно за театрален сезон.
BG е в списъка на пакета holidays и как Prophet се държи при твоя Python. Резултатите върху синтетични данни не казват нищо за реалните ти продажби. Не е финансов съвет.01Какво ще научиш
- Как да подредиш продажбите на билети като дневен времеви ред и защо за урока ползваме синтетични данни.
- Как Prophet разлага продажбите на тренд, седмична и годишна сезонност и празници.
- Как да оцениш прогнозата честно: на последните 60 дни, които моделът не е виждал, и срещу наивна прогноза.
- Как да вземаш дневните суми от TimescaleDB (по желание).
- Как да използваш локален LLM за идеи за цени, а правилата и решението да останат при човека.
02Преди да започнеш
- Машина от класа NVIDIA GB10 (например ASUS Ascent GX10 или DGX Spark) с Ollama и Python 3.10 или по-нов. Prophet и обучението на модела работят на процесора и не изискват видеокарта.
- Пакети:
prophet1.4.0,pandas,numpy,ollama0.6.3 (версии в PyPI към 03.10.2026). За по-голямата част от урока не е нужна база данни. - Модел:
ollama pull llama3.1:8b(4,9 GB по ollama.com). Този модел е от семейството Llama 3.1 с размери 8B, 70B и 405B — модел „14B“ няма в това семейство. - Данни: само агрегирани продажби (брой билети и приход на ден и тип събитие). Данни за купувачи не са нужни и не се събират.
03Стъпки
-
Данните
Защо синтетични: така урокът върви на всяка машина и не излага реални продажби. Prophet иска таблица с две колони:
ds(дата) иy(число).bash · на машината · не е пусканоpython3 -m venv .venv && . .venv/bin/activate pip install prophet pandas numpy ollamapython · gen_data.py · не е пускано# gen_data.py · синтетични дневни продажби на билети за един тип събитие import numpy as np import pandas as pd rng = np.random.default_rng(42) # фиксирано зърно: същият резултат всеки път days = pd.date_range("2024-01-01", "2026-09-30", freq="D") year = 1 + 0.35 * np.sin(2 * np.pi * (days.dayofyear.to_numpy() - 60) / 365.25) # годишна вълна week = np.where(days.dayofweek.to_numpy() >= 4, 1.5, 0.8) # петък–неделя по-силни summer = np.where(np.isin(days.month.to_numpy(), [7, 8]), 0.55, 1.0) # лятна пауза y = rng.poisson(120 * year * week * summer) pd.DataFrame({"ds": days, "y": y}).to_csv("sales_synthetic.csv", index=False) print(len(days), "rows") -
Прогноза с Prophet
Как работи: Prophet разлага редa на тренд, седмична и годишна сезонност и (по желание) празници. Официалните празници на държава се добавят с
add_country_holidays; те идват от пакетаholidays— провери, че твоята държава е в списъка му. Прогнозата има интервал (yhat_lower,yhat_upper): показвай го, не само средната линия. Prophet може да даде отрицателно число — затова го отрязваме до 0.python · forecast.py · не е пускано# forecast.py · Prophet + проверка срещу наивна прогноза import numpy as np import pandas as pd from prophet import Prophet HORIZON = 60 df = pd.read_csv("sales_synthetic.csv", parse_dates=["ds"]) train, test = df.iloc[:-HORIZON], df.iloc[-HORIZON:] # последните 60 дни не се виждат от модела m = Prophet(yearly_seasonality=True, weekly_seasonality=True, changepoint_prior_scale=0.05) m.add_country_holidays(country_name="BG") # официални празници от пакета holidays m.fit(train) future = m.make_future_dataframe(periods=HORIZON) fc = m.predict(future).tail(HORIZON) pred = fc["yhat"].clip(lower=0).to_numpy() # билети под нула няма actual = test["y"].to_numpy() mae_model = float(np.abs(actual - pred).mean()) # наивна прогноза: повтаряме последните 7 дни от обучението (същите дни от седмицата) last_week = train["y"].iloc[-7:].to_numpy() naive = np.tile(last_week, HORIZON // 7 + 1)[:HORIZON] mae_naive = float(np.abs(actual - naive).mean()) print(f"MAE Prophet: {mae_model:.1f} MAE naive: {mae_naive:.1f}") print(fc[["ds", "yhat", "yhat_lower", "yhat_upper"]].head()) -
Честна оценка
Защо наивна прогноза: „Грешим средно с 12 билета“ не значи нищо, докато не знаеш колко би сгрешила най-простата възможна прогноза. Ако Prophet не бие повторението на последната седмица, не ти трябва. Мерката е MAE — средната абсолютна грешка в билети. MAPE (в проценти) подвежда, когато продажбите са близо до нула.
Една прогноза на един период е слаба оценка. Prophet има вградена кръстосана проверка, която прави няколко „минали“ прогнози:
python · към forecast.py · не е пускано# по-стабилна оценка: няколко „минали“ прогнози вместо една from prophet.diagnostics import cross_validation, performance_metrics cv = cross_validation(m, initial="730 days", period="30 days", horizon="60 days") print(performance_metrics(cv)[["horizon", "mae", "mape", "coverage"]].tail())Попълни своя таблица с резултати — тя заменя числата „92 %“ и „85 %“ от старата версия, които не бяха подкрепени с данни:
Тип събитие MAE Prophet MAE наивна Хоризонт Извод … … … 60 дни твоят резултат -
Външни сигнали (по желание)
Prophet приема допълнителни колони чрез
add_regressor(например „има конкурентно събитие“ или „почивен ден“). Правилото по документацията: стойността трябва да е известна и за бъдещето. Календарът на събитията и почивните дни са известни; времето е известно само за няколко дни напред, затова не е добър регресор за 60-дневна прогноза. Регресор, който е константа в историята, води до грешка. -
Данни от TimescaleDB (по желание)
Защо: когато продажбите растат на милиони редове, непрекъснатият агрегат (continuous aggregate) държи готови дневни суми. Синтаксисът по-долу следва документацията на TimescaleDB от 03.10.2026;
create_hypertableи параметрите на политиката провери за твоята версия — документацията се променя.sql · TimescaleDB · не е пускано-- ticket_sales: само агрегирани продажби, без данни за купувачи CREATE TABLE ticket_sales ( time TIMESTAMPTZ NOT NULL, event_type TEXT NOT NULL, tickets INTEGER NOT NULL, revenue_eur NUMERIC(12,2) NOT NULL ); SELECT create_hypertable('ticket_sales', 'time'); CREATE MATERIALIZED VIEW ticket_sales_daily WITH (timescaledb.continuous) AS SELECT time_bucket(INTERVAL '1 day', time) AS day, event_type, SUM(tickets) AS tickets, SUM(revenue_eur) AS revenue_eur FROM ticket_sales GROUP BY day, event_type WITH NO DATA; -- първо историята, после политиката за опресняване CALL refresh_continuous_aggregate('ticket_sales_daily', NULL, localtimestamp - INTERVAL '1 week'); SELECT add_continuous_aggregate_policy('ticket_sales_daily', start_offset => INTERVAL '1 week', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour');python · load.py · не е пускано# load.py · четене от агрегата вместо от CSV import os import pandas as pd import psycopg2 def load_daily(event_type: str) -> pd.DataFrame: with psycopg2.connect(os.environ["PG_DSN"]) as conn, conn.cursor() as cur: cur.execute( "SELECT day, tickets FROM ticket_sales_daily WHERE event_type = %s ORDER BY day", (event_type,), ) rows = cur.fetchall() df = pd.DataFrame(rows, columns=["ds", "y"]) df["ds"] = pd.to_datetime(df["ds"]).dt.tz_localize(None) # Prophet не приема часови зони return dfДатабазният адрес идва от променливата
PG_DSN, не от кода. -
Идеи за цени от локален модел
Кой какво решава: моделът предлага, кодът проверява твърди граници (тук ±30 % от базовата цена), човек решава. Моделът може да върне невалиден JSON или число извън границите — такива предложения се изхвърлят. Размерът на модела (4,9 GB) е незначителен за 128 GB обща памет.
python · pricing.py · не е пускано# pricing.py · идеи за цени от LLM, правилата са в кода import json from ollama import chat def pricing_ideas(avg: float, peak: float, low: float, base_eur: float) -> list[dict]: prompt = ( f"Прогноза за 60 дни (билети на ден): средно {avg:.0f}, пик {peak:.0f}, минимум {low:.0f}. " f"Базова цена: {base_eur} EUR. Върни САМО JSON масив с три предложения: " '[{"strategy": "...", "price_eur": 0, "condition": "..."}]. ' "Не измисляй числа извън дадените." ) resp = chat(model="llama3.1:8b", messages=[{"role": "user", "content": prompt}], options={"temperature": 0.2}) text = resp.message.content.strip() text = text.removeprefix("```json").removeprefix("```").removesuffix("```").strip() try: ideas = json.loads(text) except json.JSONDecodeError: return [] if not isinstance(ideas, list): return [] ok = [] for idea in ideas: try: price = float(idea["price_eur"]) except (KeyError, TypeError, ValueError): continue if 0.7 * base_eur <= price <= 1.3 * base_eur: # твърди граници: ±30 % от базовата цена ok.append(idea) return ok # само предложения за човек, нищо не се публикува автоматично⚖️Не е финансов съветПрогнозата е оценка с неопределеност, а ценовите идеи са чернови. Промяна на цени на билети се съгласува с ръководството на организацията и с юрист (правилата за защита на потребителите и за обявяване на цените); този урок не е правна или финансова консултация. Тук цената не се персонализира по човек — само по събитие и период.
04Проверка
sales_synthetic.csvима 1004 реда (от 2024-01-01 до 2026-09-30).forecast.pyпечата две MAE стойности — на Prophet и на наивната прогноза.- Знаеш как се чете интервалът
yhat_lower–yhat_upperи го показваш до прогнозата. - Предложение за цена извън ±30 % не стига до човека.
- Таблицата с резултати е попълнена с твои данни, не със синтетичните.
Тест
1. Защо сравняваме Prophet с наивна прогноза?
2. Кое условие важи за допълнителен регресор?
3. Какво правим с предложение за цена извън границите?
4. Какво доказва точност върху синтетични данни?
05Какво следва
06Източници
- Prophet: сезонност, празници и регресори —
add_country_holidays,add_regressor, бъдещи стойности на регресора,cross_validation(прочетено 03.10.2026). - prophet 1.4.0 · ollama 0.6.3 — версии в PyPI към 03.10.2026.
- TimescaleDB (Tiger Data): създаване на continuous aggregate —
WITH (timescaledb.continuous),WITH NO DATA,add_continuous_aggregate_policy(прочетено 03.10.2026). - Ollama: llama3.1 🔒 локално — размери 8B, 70B, 405B; файл 4,9 GB за 8B (прочетено 03.10.2026).