Skip to content

feat: implement EBM (and other things) - #49

Merged
selvarajuv merged 10 commits into
mainfrom
43-implement-ebm
Oct 15, 2026
Merged

selvarajuv merged 10 commits into
mainfrom
43-implement-ebm

Conversation

@selvarajuv

@selvarajuv selvarajuv commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the EBM pricing model end to end: training runs as a background job on ingested performance
and weather data, and GET /hotels/{id}/recommendations now returns a real rate from the hotel's
active model instead of a placeholder. Also moves Postgres to Neon and standardizes the backend
layers and API docs.

Closes #43

Type of change

  • feat — new feature
  • fix — bug fix
  • docs — documentation only
  • refactor — code change that neither fixes a bug nor adds a feature
  • perf — performance improvement
  • test — adding or updating tests
  • chore / build / ci — tooling, deps, or pipeline changes

Changes

  • Model: EBM versioned per hotel (one active at a time), with an 80% price range, a per-night
    uncertainty value and occupancy-weighted training (decisions 0001–0009).
  • Training (new API): POST /hotels/{id}/train (description, date window, activate, EBM
    settings) runs as a job. Also GET /hotels/{id}/models, GET /models/{id} and POST /models/{id}/activate.
  • Recommendations (response changed): served by the active model, using only data already
    ingested. Weather is recorded, then forecast, then typical for that week. Returns the rate, range,
    uncertainty and the factors behind the price (detail + amount). is_placeholder is now always
    false.
  • Ingest: runs as background jobs through the API, with cities as the market. Endpoints moved
    to routes/ingest.py; URLs unchanged.
  • Backend layers: repositories never commit, services raise domain errors mapped to HTTP in
    one place, and there's one router per domain.
  • Database: hosted on Neon, using the pooled URL for the app and the direct URL for Alembic.
    just <recipe> --neon switches between local and Neon (decision 0027).
  • API docs: every field is described, and /docs example bodies are built from the fields'
    defaults and examples (decision 0030).
  • Tests: the outdated suite is replaced with a health check test.

Database migrations

  • No schema changes
  • Includes an Alembic migration (just migrate-create "...")
  • Migration runs cleanly (just migrate) and rolls back (just migrate-down)

Checklist

  • Commits follow Conventional Commits
  • Lint passes (just lint) and code is formatted (just format)
  • Pre-commit hooks run clean (just hooks installed)
  • Updated docs / README / .env.example where relevant
  • Tested locally (just dev) and verified the API at http://localhost:8000/docs

How to test

  1. Run neon env pull, then just dev --neon. Or run just dev for local Postgres, which also
    applies migrations.
  2. Ingest data:
  • POST /hotels/{id}/ingest/performance with a CSV and a base_rate.
  • POST /hotels/{id}/ingest/weather with {"start": "...", "end": "..."}.
  1. Train: POST /hotels/{id}/train with a description, then poll GET /jobs/{id} until it
    succeeds.
  2. Get a price: GET /hotels/{id}/recommendations?date=YYYY-MM-DD.
  3. Compare versions with GET /hotels/{id}/models and switch with POST /models/{id}/activate.
  4. Run just test.

Notes for reviewers

  • History is squashed to 10 commits by topic. The large ones are the layer refactor and the
    jobs/cities redesign.
  • Events and flights aren't in the model yet: our PredictHQ plan has no events before 2026-07-01,
    and there's no flights ingest. Their model inputs are left empty.
  • Tests are thin: only the health check runs in CI.
  • Pyright isn't run in CI: it shows 24 errors in files outside this work.
  • Follow-ups:
  • store each model's settings and window in their own columns, then bring back retraining from
    an earlier version;
  • drop the unused training_summary column;
  • untrack .cache.sqlite.

- pricing_models table and repository: versioned, one active per hotel
- EBMModel with conformal intervals and per-night uncertainty
- Occupancy sample weighting and a shared feature builder
- Decision records 0001-0009 for the model design
- Airport IATA/ICAO codes on hotels
- Regenerate the corrupted uv.lock
- Linearize the Alembic migration history
- Models share constraint names and an ON DELETE policy for every FK
- One repository module per model; repositories never commit
- Services raise domain errors (404/409/422/503 mapped in one place)
- Event merging lives in a service; signals split per source
- Outside adapters moved under services; async weather, flight and
  PredictHQ clients, airports taken from the hotel
…omain

- Ingests and backfills run as background jobs started through the API;
  standalone scripts removed. One job kind per signal; the dates choose
  refresh or backfill
- Cities are the market: weather, events and flights are kept per city
- Hotel-scoped reads, PATCH for hotels and cities, schemas in route modules
- Services split into domain, adapters, ingest and ml; outside-signal
  models and repositories grouped under signals/
- Production pinned to one uvicorn worker
- System design overview and decision records 0010-0026
App traffic uses Neon's pooled URL and Alembic the direct one; config.py
converts the libpq URLs neon env pull writes for asyncpg. `just <recipe>
--neon` layers docker-compose.neon.yml over the local stack. See
decision 0027.
- routes/ingest.py holds POST /hotels/{id}/ingest/*; routes/job.py keeps
  job reads. URLs unchanged; date bodies document YYYY-MM-DD with examples
- OpenAPI tags capitalized
- Correct the base_rate comment: the model doesn't use it
The old tests predated the redesign and no longer imported. The new test
calls GET /health in-process through httpx's ASGI transport; no database.
POST /hotels/{id}/train queues a TRAIN job that fits an EBM on ingested
performance and recorded weather and saves a new, described version.
Versions can be listed and activated. Recommendations come from the active
model, use only ingested data (recorded, forecast, then typical-week
weather) and explain the rate by its largest factors. See decisions
0028 and 0029.
…elds

Every request/response field, path id and query parameter has a
description with units, inclusive dates and currency. RequestSchema builds
each body's /docs example from the fields' defaults and examples. PATCH
bodies reuse their create body's field settings; boxed section headers.
See decision 0030.
- Recommendations no longer return a signals list or per-factor signal
  labels; both were fixed text, not data. Factors carry detail and amount
- Training no longer writes training_summary; the model response no
  longer returns it
- copy_settings_from removed until a version's settings and window are
  stored in their own columns
- Clear pyright errors in training, recommendations and job windows
@selvarajuv selvarajuv self-assigned this Oct 15, 2026
@selvarajuv selvarajuv added the feature Introduces a new and complete feature label Oct 15, 2026
@selvarajuv selvarajuv linked an issue Oct 15, 2026 that may be closed by this pull request
5 tasks
@selvarajuv
selvarajuv merged commit 26186a7 into main Oct 15, 2026
4 checks passed
@selvarajuv
selvarajuv deleted the 43-implement-ebm branch October 15, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Introduces a new and complete feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Explainable Boosting Machine (EBM) pricing model

1 participant