Files
Other.NF_Hotel-dataanalysis…/README.md
T
Tirsvad 769c777b48 Add data generation scripts and public holidays service
- Add scripts for generating booking data, building metadata, and
  updating holidays
- Implement public_holidays.py service with holiday lookup functionality
- Update CSV data with expanded hotel booking records
- Add holidays.json dataset for public holiday dates
- Enhance LLM report service with improved formatting
- Update metadata domain model and API dependencies
- Add test coverage for dataset and public holidays
2026-09-21 14:13:39 +08:00

11 KiB

NF Hotel Analytics API

Secure FastAPI service that cleans NF Hotel booking data, computes descriptive statistics, and asks a local LLM to turn those statistics into a marketing / operations report in markdown.

Architecture

Clean-architecture layering, each concern isolated behind classes/interfaces:

src/nf_hotel_api/
  domain/          # Pydantic models (Booking, ReportResponse, HotelMetadata) - no framework/IO deps
  repositories/     # Where data comes from: CsvBookingRepository, JsonBookingRepository, JsonHotelMetadataRepository
  services/         # Business logic
    cleaning.py       # DataCleaningService - wrong format / empty cells / wrong data / duplicates / pricing
    statistics.py      # DescriptiveStatsService - df.describe() (booking_id dropped)
    public_holidays.py  # PublicHolidayCalendar - Cambodian holidays from the `holidays` package
    llm_report.py       # LLMReportService - calls the local LLM, strips <think> blocks
    report.py             # ReportService - orchestrates the above use case
  api/               # FastAPI routers, DI wiring, HTTP-only concerns
  core/              # Settings (env config) and API-key security

Dependencies point inward: api -> services -> domain, and repositories implement an abstract interface consumed by services, so the analysis logic never depends on FastAPI or pandas I/O directly.

Setup

python -m venv .venv
.venv/Scripts/pip install -e ".[dev]"
cp .env.example .env
# edit .env: set API_KEY, and point LLM_BASE_URL at your local
# OpenAI-compatible inference server serving qwen3.8-whittle-moe-27b-a17.8b

Run

.venv/Scripts/python -m uvicorn nf_hotel_api.main:app --app-dir src --reload

Docs: http://localhost:8000/docs

Security

Every /api/v1/* route requires an X-API-Key header matching API_KEY from the environment, checked with a constant-time comparison (secrets.compare_digest). CORS is locked to ALLOWED_ORIGINS. Never commit the real .env.

Endpoints

  • GET /health - unauthenticated liveness check.
  • POST /api/v1/report/from-file - runs the report over the bundled data/nf_hotel_bookings.csv.
  • POST /api/v1/report/from-json - runs the report over booking records supplied in the request body, same shape as the CSV columns.

JSON format for POST /api/v1/report/from-json

Request body is {"records": [<Booking>, ...]}, at least one record. Each Booking has these fields (mirrors domain/schemas.py):

Field Type Example
booking_id int 1
hotel string "NF Hotel"
is_canceled int (0/1) 0
lead_time int 342
arrival_date_week_number int 27
booking_date string (YYYY-MM-DD or DD-MM-YYYY) "2017-07-24"
arrival_date string (YYYY-MM-DD or DD-MM-YYYY) "2018-07-01"
arrival_date_day_of_month int 1
stays_in_weekend_nights int 0
stays_in_week_nights int 0
adults int 2
children int 0
babies int 0
meal string "BB"
country string "Portugal"
market_segment string "Direct"
is_repeated_guest int (0/1) 0
previous_cancellations int 0
assigned_room_type string ("A" small, "B" large) "A"
booking_changes int 3
deposit_type string "No Deposit"
agent int 0
customer_type string "No Contract (Single)"
required_car_parking_spaces int 0
total_of_special_requests int 0
prize_per_nigth number, optional (price paid per night; standard price if omitted) 20

Fields are intentionally accepted as raw/untrusted input - values with the wrong format, blanks, or implausible numbers are fine; DataCleaningService fixes them before statistics are computed.

A ready-to-use example payload lives in examples/sample_booking_request.json:

curl -X POST http://localhost:8000/api/v1/report/from-json \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  --data @examples/sample_booking_request.json

Both report endpoints return:

{
  "descriptive_stats": { "<column>": { "mean": ..., "top": ..., "...": ... } },
  "llm_report": "## Markdown report from the LLM, <think> blocks stripped"
}

Data cleaning pipeline (DataCleaningService)

Applied in order, before any statistics are computed:

  1. Wrong format - dates parsed to datetime, numeric columns coerced to numeric, categorical columns trimmed of whitespace.
  2. Empty cells - blank/placeholder values normalized to "Unknown" for categoricals and 0 for numerics; rows with unparseable dates are dropped.
  3. Wrong data - implausible guest counts (e.g. 55 adults) are clipped to a sane maximum; bookings with zero total occupants are corrected to one adult instead of being discarded.
  4. Duplicates - exact duplicate bookings (ignoring booking_id, which is just a row identifier) are removed, keeping the first occurrence.
  5. Pricing - room size and revenue are derived, and missing prices are filled from the hotel metadata (see below).

Dates are accepted as YYYY-MM-DD (JSON) and DD-MM-YYYY (the CSV export). Each value is tried against both layouts, so a mixed column does not lose rows.

Hotel metadata and pricing

Reference data that is not part of the bookings lives in data/hotel_metadata.json (path set by METADATA_PATH, loaded by JsonHotelMetadataRepository):

Field Content
hotel, address, currency NF Hotel, Street 172, Phnom Penh, Cambodia; prices in USD
room_types per assigned_room_type: size, standard price per night, number of rooms
nearby_events events in Phnom Penh 2022-2025 (name, dates, venue, source_url)
assigned_room_type room_size Standard price Rooms in hotel
A Small $20 10
B Large $25 10
  • prize_per_nigth in the CSV is the price the customer actually paid and is never overwritten. Only a missing or negative value is replaced with the room type's standard price.
  • Room types that are not in the metadata get room_size = "Unknown" and no invented price.
  • revenue = (stays_in_weekend_nights + stays_in_week_nights) x prize_per_nigth for non-cancelled bookings, 0 for cancelled ones.
  • room_size, prize_per_nigth and revenue are part of the descriptive statistics sent to the LLM. The prompt also includes the metadata (address, room types with prices and room counts, nearby events) and the public holidays for the years in the data.

To change a price, a room count, a holiday or an event, edit data/hotel_metadata.json and restart the service (it is read once at startup). scripts/build_metadata.py is the helper that produced the file.

Public holidays

Public holidays are not stored in the hotel metadata. They are looked up at run time in the Python holidays package (country KH, Cambodia) by PublicHolidayCalendar (services/public_holidays.py). The LLM prompt lists the holidays for the years the bookings' arrival dates span, and the dataset generator uses them to shape demand.

data/holidays.json is a generated export of that calendar (holiday name, start and end date, with the package version it came from) for anyone who wants to read the holidays without running Python. The application does not read it; the package stays the source of truth. Refresh it after upgrading the package or when a new year starts:

.venv/Scripts/python scripts/update_holidays.py

By default it covers 2022 up to next year; use --first-year / --last-year to change that. tests/test_public_holidays.py fails when the file no longer matches the package, which is the signal to re-run the script.

Nearby events

Looked up on the web; each entry carries its source_url. All venues are in Phnom Penh, but the distance to Street 172 was not measured, so the 20 km radius is an assumption based on the venues being in the city.

Event Date Venue
40th and 41st ASEAN Summits 10-13 Nov 2022 Phnom Penh
2023 SEA Games 5-17 May 2023 Morodok Techo Sports Complex, Olympic Sports Complex, Chroy Changvar Convention Centre
12th ASEAN Para Games 3-9 Jun 2023 Morodok Techo National Stadium
Phnom Penh International Half Marathon 11 Jun 2023, 16 Jun 2024, 15 Jun 2025 Phnom Penh
Miss Grand Cambodia 2024 final 12 Jul 2024 Koh Pich Theater
CAMFOOD & CAMHOTEL 2024 6-8 Nov 2024 Diamond Island Convention & Exhibition Center
Cambodia ASEAN Business Summit 2025 6 Mar 2025 Sofitel Phnom Penh Phokeetra
2025 AFC Challenge League final 10 May 2025 Phnom Penh
Mekong Forum 2025 30-31 Jul 2025 Shangri-La Hotel Phnom Penh
CamboP&ELight 2025 6-9 Aug 2025 Diamond Island Convention & Exhibition Center
Phnom Penh Design Festival 2025 31 Oct - 2 Nov 2025 Factory Phnom Penh

The list is not exhaustive (2022 and 2024 in particular have few entries).

The dataset (data/nf_hotel_bookings.csv)

The bookings are synthetic, not real reservations. They are generated by scripts/generate_bookings.py (fixed seed, so the file is reproducible) to fit this hotel:

  • Arrivals 1 Jan 2022 - 31 Dec 2025, about 8,500 rows.
  • The hotel has only 10 small and 10 large rooms. The simulation never sells more rooms of a type than exist on any night; requests for a sold-out night are turned away and do not appear in the file. tests/test_dataset.py checks this.
  • Demand follows the calendar in the metadata: it is higher on Fridays and Saturdays and in the cool season, much higher during big events (SEA Games, ASEAN Summit) and the Water Festival, and lower during Khmer New Year and Pchum Ben, when people leave the capital. Average occupancy comes out around 55-60 %, close to full during the biggest events.
  • prize_per_nigth is the price paid: the standard price with a discount for Corporate/Groups/Offline TA bookings and a surcharge on peak days.
  • About 2 % deliberately dirty rows (duplicates, impossible guest counts, blank meals) are added so DataCleaningService has real work to do.

All multipliers (holiday and event effects, price rules, guest mix) are modelling assumptions, not measurements. They are constants at the top of the script; change them and regenerate:

.venv/Scripts/python scripts/generate_bookings.py

(Close the CSV in Excel first, otherwise Windows blocks the write.) Replace the file with real bookings when they are available; nothing else needs to change.

Tests

.venv/Scripts/python -m pytest