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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user