Competitor Rate Monitoring & Dynamic Pricing System
Everyone sees a competitor’s rate; nobody sees their occupancy.
- TypeScript
- Express
- node:sqlite
- Chrome Extension
Problem
Rate monitoring is a commodity: a hotel that only matches the street is matching a street that is already pricing the calendar correctly. The distinguishing input is not rate but occupancy — and no channel publishes it.
All a channel will tell you is whether rooms are available on a date. That is not a value, it is a lower bound: "8 rooms available" means eight or more. Reading a censored observation as an exact one produces an estimate that is systematically wrong in one direction.
Between the truth and the answer sit three separate ceilings: the hotel's channel allocation (30–65%), the selector capping out at 8, and a published room count that is stale for a quarter of hotels.
Solution
Occupancy is recovered from the availability query by binary search over a monotone predicate: three requests instead of eight. No reservation is ever created.
Censoring is flagged against a learned ceiling rather than the query cap: the highest availability a hotel has ever returned is the most it will ever show. Allocation censoring is invisible to a test that only looks for 8.
The estimator is a Tobit regression fitted by EM (12 passes) rather than a fit on uncensored rows only: a censored row carries its bound and is imputed with the conditional mean of a truncated normal, while sigma is computed from exact rows alone. There are two regressors — own occupancy and the night's uncensored competitors; without the second the estimate anchors on your own data and cancels the signal you already had.
On the collection side the headless approach was stopped by bot protection, so readings are taken by a Chrome extension running inside a real session.
Outcome
The inference works but does not improve the decision — and that is how it is reported.
The censored regression beats the naive reading in 12 of 12 markets: mean absolute error falls from 35 points to 12. On revenue it does not beat a free one-line control (matching the competitor median). Two of the three hypotheses written before the run were refuted 12/12.
The real return was the measurement itself: the harness caught four defects in the project's own code, none of which raised an error. Censoring was tested only against the query cap; the model was fitted on a selected sample; the estimator anchored on its own data (accuracy improved while revenue got worse); and the placebo still knew a third of the answer because the season is a sine wave.
The boundary between collection and estimation is deliberately narrow: the extension writes only raw observations — date, available rooms, published rooms, rate and a censoring flag. Censoring is stored, not derived, because deriving it later means estimating after the ceiling information is gone.
Validation at the ingest end is strict, checking each numeric field with
typeof. The first version used Number(null), which wrote a reading with
unknown availability into the database as a confident zero. It stayed
invisible until a test was written for it.
Source code
Comments are shown in the page language. Files in the repository carry both English and Turkish.
Project structure
README.md
132 lines · 12 KB
Loading…