Знакът на КАГАМИ КАГАМИ
kagami.bg/academy · lesson · machine-readable viewUPDATED 2026-10-03
IDENTITY
module
GX10-04-158 · Ticket demand forecast with Prophet and local price ideas for a cultural and sports complex
series
GX10 (local AI server class: NVIDIA GB10, e.g. ASUS Ascent GX10 / DGX Spark)
level
Advanced
duration
2-3 h
prerequisites
Python 3.10+, pandas basics; Ollama with llama3.1:8b for the pricing step; TimescaleDB only for the optional step
trust_label
UPDATED 2026-10-03 (Prophet docs, PyPI versions, TimescaleDB docs, Ollama model page read on 2026-10-03) · NOT TESTED on a GB10 machine · no VERIFIED label · all data is synthetic · not financial advice
versions
prophet 1.4.0 · ollama (Python) 0.6.3 (PyPI, 2026-10-03) · llama3.1:8b = 4.9 GB (ollama.com, 2026-10-03)
language
human view: bg · english edition: /en/academy/gx10/ (same file name)
previous / next
04-156_Venue_RAG_Assistant.html · 04-159_Audience_Segmentation.html
PURPOSE

Forecast daily ticket sales per event type from aggregated data with Prophet, evaluate honestly on a 60-day hold-out against a naive repeat-last-week forecast, optionally keep daily totals in a TimescaleDB continuous aggregate, and use a local LLM only for price ideas filtered by hard bounds and approved by a person. No buyer data is used.

KEY CONCEPTS
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

04-159 · Audience segmentation without profiling people (04-159_Audience_Segmentation.html) · related: 04-156 Venue assistant · series index: kagami.bg/academy/gx10/ · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
gx10nvidia-gb10forecastingprophettimescaledbollamaticketing
ОБНОВЕНО · 03.10.2026

Прогноза на търсенето на билети с Prophet и локален AI

Как да прогнозираш продажбите на билети за културен и спортен комплекс по тип събитие, как да оцениш прогнозата честно и как локален модел да подсказва идеи за цени. Работим само с агрегирани дневни суми и синтетични данни — без данни за купувачи.

⏱ 2–3 ч Напреднали GX10 Prophet · TimescaleDB · Ollama
Prophet 1.4.0 (прогнозата)🔒 локално Ollama · llama3.1:8b (идеи за цени)🔒 локално TimescaleDB (по желание)🔒 локално pandas · numpy🔒 локално
🔄
ОБНОВЕНО · 03.10.2026 — какво
Обобщихме примера: урокът вече не е за конкретна сграда; всички данни са синтетични и цените са в EUR (старата версия беше в лева). Поправихме грешка във фактите: старият урок ползваше модел „Llama 3.1 14B“, а семейството Llama 3.1 има размери 8B, 70B и 405B — сега ползваме llama3.1:8b през Ollama. Махнахме таблицата с „точност 92 %, 88 %, 85 %, 94 %“ и статуси „Prod/Test“ — никъде не се виждаше откъде са. Вместо нея има начин да измериш точността сам: последните 60 дни извън обучението, наивна прогноза за сравнение и кръстосана проверка. Добавихме: синтетичен генератор, за да върви урокът без реални данни; отрязване на отрицателните прогнози; интервал на прогнозата; правилото, че регресорът трябва да е известен за бъдещето (по документацията на Prophet). Поправихме SQL: по документацията на TimescaleDB първо се създава агрегатът WITH NO DATA, после се опреснява историята, после се добавя политика. Ценовите идеи вече минават през твърди граници и човек; махнахме твърдението, че Prophet „е проектиран“ точно за театрален сезон.
⚠️
Какво не сме пускали сами
Кодът не е пускан на машина от класа GB10, нито с реален Prophet, TimescaleDB и Ollama, затова няма етикет „ТЕСТВАНО“ и няма „ПРОВЕРЕНО“. Нито една команда или скрипт не са пускани („не е пускано“). Версиите и документацията са прочетени на 03.10.2026. Не сме проверявали дали BG е в списъка на пакета holidays и как Prophet се държи при твоя Python. Резултатите върху синтетични данни не казват нищо за реалните ти продажби. Не е финансов съвет.

01Какво ще научиш

02Преди да започнеш

💡
Синтетичните данни не доказват нищо
Генераторът по-долу слага седмичен и годишен модел, така че Prophet ще го „намери“. Това показва как се работи, а не колко точен е методът върху твоите продажби. Истинската оценка е само на твои исторически данни.

03Стъпки

  1. Данните

    Защо синтетични: така урокът върви на всяка машина и не излага реални продажби. Prophet иска таблица с две колони: ds (дата) и y (число).

    bash · на машината · не е пускано
    python3 -m venv .venv && . .venv/bin/activate
    pip install prophet pandas numpy ollama
    python · 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")
  2. Прогноза с 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())
  3. Честна оценка

    Защо наивна прогноза: „Грешим средно с 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 ProphetMAE наивнаХоризонтИзвод
    ………60 днитвоят резултат
  4. Външни сигнали (по желание)

    Prophet приема допълнителни колони чрез add_regressor (например „има конкурентно събитие“ или „почивен ден“). Правилото по документацията: стойността трябва да е известна и за бъдещето. Календарът на събитията и почивните дни са известни; времето е известно само за няколко дни напред, затова не е добър регресор за 60-дневна прогноза. Регресор, който е константа в историята, води до грешка.

  5. Данни от 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, не от кода.

  6. Идеи за цени от локален модел

    Кой какво решава: моделът предлага, кодът проверява твърди граници (тук ±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Проверка

Тест

1. Защо сравняваме Prophet с наивна прогноза?

2. Кое условие важи за допълнителен регресор?

3. Какво правим с предложение за цена извън границите?

4. Какво доказва точност върху синтетични данни?

05Какво следва

06Източници

  1. Prophet: сезонност, празници и регресори — add_country_holidays, add_regressor, бъдещи стойности на регресора, cross_validation (прочетено 03.10.2026).
  2. prophet 1.4.0 · ollama 0.6.3 — версии в PyPI към 03.10.2026.
  3. TimescaleDB (Tiger Data): създаване на continuous aggregate — WITH (timescaledb.continuous), WITH NO DATA, add_continuous_aggregate_policy (прочетено 03.10.2026).
  4. Ollama: llama3.1 🔒 локално — размери 8B, 70B, 405B; файл 4,9 GB за 8B (прочетено 03.10.2026).