Hospitality occupancy and revenue metrics with cancelled bookings
In this article (6 sections)
Occupancy and room-revenue measures need a room-night population, not just booking records. A canceled reservation may produce a fee without producing a sold room-night, and a complimentary stay may occupy a room without generating room revenue.
Define supply, sold nights and revenue consistently before calculating occupancy, average daily rate or revenue per available room.
Inspect a two-day room inventory
The original synthetic room-night fixture contains four physical rooms across two dates: eight room-date rows. Under this internal operational contract, one out-of-service room-night is excluded from available supply, leaving seven.
Three room-nights are sold for 10,000, 15,000 and 12,000 paise. One is complimentary, two are vacant, and one canceled booking leaves an available unsold room-night with a separate 2,000-paise cancellation fee.
The exclusion of the out-of-service night is an explicit teaching policy. External benchmark reporting may use different supply rules, so these results should not be compared with a benchmark until those definitions match.
Calculate the underlying counts and amounts
SELECT COUNT(*) AS physical_room_nights,
SUM(status<>'out_of_service') AS available_room_nights,
SUM(status='sold') AS sold_room_nights,
SUM(status='complimentary') AS complimentary_room_nights,
SUM(room_revenue_paise) AS room_revenue_paise,
SUM(cancellation_fee_paise) AS cancellation_fee_paise
FROM room_nights;Commercial occupancy under this contract is 3/7 ≈ 42.86%. Physical use among available nights, including the complimentary stay, is 4/7 ≈ 57.14%. Those are separate measures.
Room revenue is 37,000 paise. ADR is 37,000/3 ≈ 12,333.33 paise, or ₹123.33, per sold room-night. RevPAR is 37,000/7 ≈ 5,285.71 paise, or ₹52.86, per available room-night. The amounts are deliberately small synthetic examples, not market room rates.
Verify the metric identity
from math import isclose
from build_and_verify import database
db = database()
rows = db.execute('SELECT * FROM room_nights').fetchall()
db.close()
available = [r for r in rows if r[2]!='out_of_service']
sold = [r for r in available if r[2]=='sold']
occupied = [r for r in available if r[2] in ('sold','complimentary')]
revenue = sum(r[3] for r in rows)
fees = sum(r[4] for r in rows)
assert (len(rows),len(available),len(sold),len(occupied),revenue,fees)==(8,7,3,4,37000,2000)
occupancy = len(sold)/len(available)
adr = revenue/len(sold)
revpar = revenue/len(available)
assert isclose(occupancy*adr,revpar)
print({'commercial_occupancy':occupancy,'physical_use':len(occupied)/len(available),
'ADR_INR':adr/100,'RevPAR_INR':revpar/100,'separate_cancellation_fees_INR':fees/100})The identity occupancy × ADR = RevPAR holds because all three measures use compatible numerators and denominators. Mixing paid occupancy with an ADR denominator that includes complimentary stays would break that relationship.
CoStar's STR glossary defines ADR through room revenue and rooms sold, and distinguishes room demand from complimentary stays. Use the provider's detailed reporting rules when producing an actual benchmark submission.
Keep booking events separate from stay outcomes
A booking can span several nights, change dates or be canceled and resold. Counting bookings as room-nights loses that structure. Build a room-date inventory and reconcile reservation allocations to it without double-booking the same physical supply.
The fixture shows the final state for each date. It cannot reconstruct booking pace, cancellation timing or whether a canceled room was available long enough to resell. Those questions require reservation history and event timestamps.
Cancellation fees are reported separately from room revenue here. Adding them to the numerator without changing the metric name would make comparisons depend on a different revenue scope. Other non-room revenue, taxes and package allocations also need explicit treatment.
Interpret revenue measures without claiming profitability
Higher RevPAR does not automatically mean higher profit. Distribution commissions, service costs, occupancy-related expenses and customer mix can change. This fixture contains no cost data and therefore cannot answer a profitability question.
Exercise: resell the canceled room-night for 9,000 paise while retaining its cancellation fee. Update sold nights and room revenue once, then verify the metric identity and separate fee reconciliation.
NeuraPath's Data Analytics with Generative AI course connects dimensional grain with domain metrics. A trustworthy hospitality report keeps inventory, stay outcomes and revenue scope aligned.
Continue learning
This article is part of the Domain analytics and business cases sequence. Use the neighbouring tasks when you need the prerequisite or the next application.
- Review the prerequisite or neighbouring task in Workforce capacity planning without ranking individual workers unfairly.
- Continue with Analyze marketplace seller performance with comparable cohorts.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI