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
This commit is contained in:
2026-09-21 14:13:39 +08:00
parent 225d28feb7
commit 769c777b48
19 changed files with 50164 additions and 40297 deletions
+91 -18
View File
@@ -15,6 +15,7 @@ src/nf_hotel_api/
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
@@ -140,21 +141,16 @@ Reference data that is not part of the bookings lives in
[data/hotel_metadata.json](data/hotel_metadata.json) (path set by
`METADATA_PATH`, loaded by `JsonHotelMetadataRepository`):
```json
{
"hotel": "NF Hotel",
"currency": "USD",
"room_types": {
"A": { "size": "Small", "standard_price_per_night": 20, "room_count": null },
"B": { "size": "Large", "standard_price_per_night": 25, "room_count": null }
}
}
```
| 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 | *to be filled in* |
| `B` | Large | $25 | *to be filled in* |
| `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
@@ -164,13 +160,90 @@ Reference data that is not part of the bookings lives in
- `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, and the prompt includes the room catalogue
(standard prices and room counts) from the metadata file.
- `room_count` is `null` until the real number of rooms is known. Occupancy
rate needs it, so it is not calculated yet. Events are also still missing.
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 standard price or set a room count, edit `data/hotel_metadata.json`
and restart the service (it is read once at startup).
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`](https://pypi.org/project/holidays/) package
(country `KH`, Cambodia) by `PublicHolidayCalendar`
([services/public_holidays.py](src/nf_hotel_api/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](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:
```bash
.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](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:
```bash
.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