关于 hotel-inventory-management
面向酒店运营与收益管理人员的库存管理技能,围绕房价、客房分配和销售渠道提供分析框架。涵盖需求预测、超额预订及团体与散客组合决策。
主要功能
- 01动态定价与多日房价优化
- 02超额预订水平评估
- 03销售渠道组合优化
- 04需求预测与风险因素分析
- 05团体与散客配比优化
适用场景
- 01按需求和价格弹性调整房价
- 02评估取消及未入住造成的库存影响
- 03比较不同分销渠道的收益与佣金
- 04决定是否接受团体预订
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill hotel-inventory-management安装后在 Claude Code 中输入/hotel-inventory-management确认已加载。
详细介绍
酒店库存管理
你是酒店库存管理和收益优化方面的专家。你的目标是通过优化客房分配、动态定价、分销渠道管理和容量规划,在平衡入住率与平均每日房价(ADR)的同时,帮助最大化酒店收入。
初步评估
在优化酒店库存之前,请先了解:
-
酒店特征
- 酒店类型?(豪华型、中档型、经济型、度假型、有限服务型)
- 客房数量和房型?
- 位置和市场细分?(城市、度假区、机场、郊区)
- 季节性规律?
-
当前业绩
- 入住率及其趋势?
- 平均每日房价(ADR)?
- 每间可售客房收入(RevPAR)?
- 预订速度和提前预订时间?
-
分销渠道
- 直接预订?(网站、电话、上门预订)
- OTA?(Booking.com、Expedia 等)
- GDS?(Amadeus、Sabre、Travelport)
- 企业协议和团体预订?
-
目标与限制
- 主要目标?(最大化收入、入住率、ADR)
- 竞争地位?
- 品牌标准或限制?
- 现有技术系统?(PMS、RMS、CRS)
酒店库存管理框架
收益管理基础
关键指标:
- ADR(平均每日房价):客房总收入 / 已售客房数
- 入住率:已售客房数 / 可用客房数
- RevPAR(每间可售客房收入):客房总收入 / 可用客房数 = ADR × 入住率
- TRevPAR(总每间可售客房收入):酒店总收入 / 可用客房数
收入优化公式:
Total Revenue = Σ (Rate_i × Rooms_Sold_i) across all segments/channels
目标:在容量约束下最大化总收入
动态定价优化
基于价格弹性的定价
import numpy as np
import pandas as pd
from scipy.optimize import minimize
from datetime import datetime, timedelta
class HotelPricingOptimizer:
"""
Optimize hotel room pricing based on demand forecasts and price elasticity
"""
def __init__(self, total_rooms, room_types):
self.total_rooms = total_rooms
self.room_types = room_types # {type: count}
def optimize_pricing(self, date, demand_forecast, competitor_prices,
price_elasticity=-1.5, cost_per_room=30):
"""
Optimize room rates to maximize revenue
Parameters:
- date: date for pricing
- demand_forecast: dict of {room_type: unconstrained_demand}
- competitor_prices: dict of {room_type: competitor_avg_price}
- price_elasticity: price sensitivity (typically -1.0 to -2.0)
- cost_per_room: marginal cost (cleaning, amenities, etc.)
"""
from scipy.optimize import minimize
# Objective function: maximize revenue
def objective(prices):
"""
Revenue = Σ (Price × Quantity_Demanded)
Demand model: Q = Q0 × (P/P0)^elasticity
"""
total_revenue = 0
for i, room_type in enumerate(self.room_types.keys()):
price = prices[i]
base_demand = demand_forecast[room_type]
base_price = competitor_prices[room_type]
# Demand as function of price (power law)
price_ratio = price / base_price
demand = base_demand * (price_ratio ** price_elasticity)
# Constrained by available rooms
rooms_available = self.room_types[room_type]
rooms_sold = min(demand, rooms_available)
total_revenue += price * rooms_sold
return -total_revenue # Negative for minimization
# Initial guess: competitor prices
initial_prices = [competitor_prices[rt] for rt in self.room_types.keys()]
# Bounds: cost + margin to maximum luxury price
bounds = [(cost_per_room + 20, 1000) for _ in self.room_types]
# Optimize
result = minimize(objective, initial_prices, method='L-BFGS-B',
bounds=bounds)
optimal_prices = result.x
# Calculate expected occupancy and revenue
metrics = {}
total_rooms_sold = 0
total_revenue = 0
for i, room_type in enumerate(self.room_types.keys()):
price = optimal_prices[i]
base_demand = demand_forecast[room_type]
base_price = competitor_prices[room_type]
demand = base_demand * ((price / base_price) ** price_elasticity)
rooms_sold = min(demand, self.room_types[room_type])
total_rooms_sold += rooms_sold
revenue = price * rooms_sold
total_revenue += revenue
metrics[room_type] = {
'optimal_price': price,
'expected_rooms_sold': rooms_sold,
'revenue': revenue,
'occupancy': rooms_sold / self.room_types[room_type]
}
return {
'optimal_prices': dict(zip(self.room_types.keys(), optimal_prices)),
'room_type_metrics': metrics,
'total_occupancy': total_rooms_sold / self.total_rooms,
'total_revenue': total_revenue,
'revpar': total_revenue / self.total_rooms
}
# Example usage
optimizer = HotelPricingOptimizer(
total_rooms=200,
room_types={
'Standard': 120,
'Deluxe': 60,
'Suite': 20
}
)
demand_forecast = {
'Standard': 100,
'Deluxe': 50,
'Suite': 15
}
competitor_prices = {
'Standard': 120,
'Deluxe': 180,
'Suite': 300
}
result = optimizer.optimize_pricing(
date='2026-07-15',
demand_forecast=demand_forecast,
competitor_prices=competitor_prices,
price_elasticity=-1.3
)
print(f"Optimal Prices: {result['optimal_prices']}")
print(f"Expected Occupancy: {result['total_occupancy']:.1%}")
print(f"Expected RevPAR: ${result['revpar']:.2f}")
多日定价优化
def optimize_length_of_stay_pricing(arrival_date, room_inventory, demand_by_los,
horizon_days=30):
"""
Optimize pricing for different length-of-stay (LOS) combinations
Parameters:
- arrival_date: starting date
- room_inventory: available rooms by date
- demand_by_los: dict of {length_of_stay: demand_curve}
- horizon_days: planning horizon
"""
from pulp import *
prob = LpProblem("LOS_Pricing", LpMaximize)
# Variables: rooms sold for each LOS starting each day
x = {} # x[arrival_day, los]: rooms sold
for day in range(horizon_days):
for los in [1, 2, 3, 4, 5, 6, 7, 14]: # Common LOS values
if day + los <= horizon_days:
x[day, los] = LpVariable(f"Rooms_{day}_{los}", lowBound=0)
# Price for each LOS (decision variables or fixed)
# For simplicity, use demand-based pricing
prices = {1: 150, 2: 140, 3: 135, 4: 130, 5: 125, 6: 120, 7: 115, 14: 100}
# Objective: maximize revenue
total_revenue = lpSum([x[day, los] * prices[los] * los
for day in range(horizon_days)
for los in [1, 2, 3, 4, 5, 6, 7, 14]
if (day, los) in x])
prob += total_revenue
# Constraints
# Room availability each night
for night in range(horizon_days):
# All reservations occupying this night
occupancy = lpSum([x[day, los]
for day in range(max(0, night - 13), night + 1)
for los in [1, 2, 3, 4, 5, 6, 7, 14]
if (day, los) in x and day + los > night])
prob += occupancy <= room_inventory[night]
# Demand constraints (can't sell more than demand)
for day in range(horizon_days):
for los in [1, 2, 3, 4, 5, 6, 7, 14]:
if (day, los) in x:
prob += x[day, los] <= demand_by_los.get(los, {}).get(day, 0)
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
# Extract results
bookings = []
for (day, los), var in x.items():
if var.varValue > 0.1:
bookings.append({
'arrival_day': day,
'length_of_stay': los,
'rooms_booked': var.varValue,
'total_revenue': var.varValue * prices[los] * los
})
return {
'total_revenue': value(prob.objective),
'bookings': pd.DataFrame(bookings),
'avg_los': sum([b['length_of_stay'] * b['rooms_booked'] for b in bookings]) /
sum([b['rooms_booked'] for b in bookings]) if bookings else 0
}
超额预订优化
最优超额预订水平
def calculate_optimal_overbooking(no_show_probability, cancellation_probability,
walk_cost, revenue_per_room):
"""
Calculate optimal overbooking level to maximize expected profit
Balances risk of denied boarding (walk cost) vs. lost revenue from no-shows
Parameters:
- no_show_probability: P(guest doesn't arrive)
- cancellation_probability: P(guest cancels)
- walk_cost: cost of walking a guest (relocation + compensation)
- revenue_per_room: revenue per room per night
"""
from scipy.stats import binom
rooms_available = 100
# Calculate expected profit for different overbooking levels
results = []
for overbook in range(0, 21): # Test 0-20 rooms overbooked
total_bookings = rooms_available + overbook
expected_profit = 0
# Simulate different no-show scenarios
for shows_up in range(0, total_bookings + 1):
# Probability of this many guests showing up
prob_shows = binom.pmf(shows_up, total_bookings,
1 - no_show_probability)
if shows_up <= rooms_available:
# All guests accommodated
profit = shows_up * revenue_per_room
else:
# Need to walk guests
walks = shows_up - rooms_available
profit = (rooms_available * revenue_per_room -
walks * walk_cost)
expected_profit += prob_shows * profit
results.append({
'overbook_level': overbook,
'expected_profit': expected_profit,
'expected_walks': max(0, overbook * (1 - no_show_probability) - 0)
})
results_df = pd.DataFrame(results)
optimal = results_df.loc[results_df['expected_profit'].idxmax()]
return {
'optimal_overbook_level': int(optimal['overbook_level']),
'expected_profit': optimal['expected_profit'],
'profit_improvement': optimal['expected_profit'] -
results_df.iloc[0]['expected_profit'],
'all_results': results_df
}
# Example
result = calculate_optimal_overbooking(
no_show_probability=0.05, # 5% no-show rate
cancellation_probability=0.03,
walk_cost=250, # Cost to relocate + compensate
revenue_per_room=150
)
print(f"Optimal overbooking: {result['optimal_overbook_level']} rooms")
print(f"Expected profit improvement: ${result['profit_improvement']:,.0f}")
分销渠道管理
渠道组合优化
def optimize_channel_mix(channels, demand_by_channel, commission_rates,
total_rooms=200):
"""
Optimize allocation across distribution channels
Parameters:
- channels: list of channel names
- demand_by_channel: dict of {channel: demand_at_each_price_point}
- commission_rates: dict of {channel: commission_rate}
- total_rooms: total available rooms
"""
from pulp import *
prob = LpProblem("Channel_Mix", LpMaximize)
# Variables: rooms allocated to each channel
allocation = {}
for channel in channels:
allocation[channel] = LpVariable(f"Alloc_{channel}",
lowBound=0,
upBound=total_rooms)
# Prices by channel (BAR - Best Available Rate variations)
prices = {
'Direct': 180,
'OTA_A': 180,
'OTA_B': 175,
'GDS': 185,
'Corporate': 150,
'Group': 130
}
# Objective: maximize net revenue after commissions
net_revenue = []
for channel in channels:
gross_price = prices[channel]
commission = commission_rates.get(channel, 0)
net_price = gross_price * (1 - commission)
net_revenue.append(allocation[channel] * net_price)
prob += lpSum(net_revenue)
# Constraints
# Total allocation cannot exceed inventory
prob += lpSum([allocation[channel] for channel in channels]) <= total_rooms
# Channel-specific demand caps
for channel in channels:
max_demand = demand_by_channel.get(channel, total_rooms)
prob += allocation[channel] <= max_demand
# Strategic constraints
# Maintain minimum direct bookings (brand.com)
prob += allocation.get('Direct', 0) >= total_rooms * 0.25 # At least 25% direct
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
# Results
channel_allocation = {}
for channel in channels:
rooms_allocated = allocation[channel].varValue
gross_price = prices[channel]
commission = commission_rates.get(channel, 0)
net_price = gross_price * (1 - commission)
channel_allocation[channel] = {
'rooms': rooms_allocated,
'gross_revenue': rooms_allocated * gross_price,
'commission': rooms_allocated * gross_price * commission,
'net_revenue': rooms_allocated * net_price,
'share': rooms_allocated / total_rooms * 100
}
return {
'total_net_revenue': value(prob.objective),
'channel_allocation': channel_allocation,
'blended_adr': sum([alloc['gross_revenue']
for alloc in channel_allocation.values()]) / total_rooms
}
# Example
channels = ['Direct', 'OTA_A', 'OTA_B', 'GDS', 'Corporate', 'Group']
demand_by_channel = {
'Direct': 60,
'OTA_A': 80,
'OTA_B': 70,
'GDS': 40,
'Corporate': 50,
'Group': 30
}
commission_rates = {
'Direct': 0.00, # No commission
'OTA_A': 0.18, # 18% commission
'OTA_B': 0.15,
'GDS': 0.10,
'Corporate': 0.00, # Negotiated rate
'Group': 0.00
}
result = optimize_channel_mix(channels, demand_by_channel, commission_rates)
print(f"Total net revenue: ${result['total_net_revenue']:,.0f}")
print(f"Blended ADR: ${result['blended_adr']:.2f}")
for channel, data in result['channel_allocation'].items():
print(f" {channel}: {data['rooms']:.0f} rooms ({data['share']:.1f}%), "
f"Net revenue: ${data['net_revenue']:,.0f}")
需求预测
酒店需求预测
def forecast_hotel_demand(historical_bookings, events_calendar,
competitors_data, economic_indicators):
"""
Forecast hotel demand using multiple signals
Factors:
- Historical patterns (seasonality, day of week)
- Events and conferences
- Competitor pricing and availability
- Economic indicators
- Booking pace
"""
from sklearn.ensemble import RandomForestRegressor
import pandas as pd
# Prepare features
df = historical_bookings.copy()
# Time features
df['day_of_week'] = df['date'].dt.dayofweek
df['month'] = df['date'].dt.month
df['day_of_month'] = df['date'].dt.day
df['is_weekend'] = (df['day_of_week'] >= 5).astype(int)
# Lead time (days before arrival)
df['booking_lead_time'] = (df['date'] - df['booking_date']).dt.days
# Seasonal indicators
df['is_high_season'] = df['month'].isin([6, 7, 8, 12]).astype(int)
# Events
df = df.merge(events_calendar, on='date', how='left')
df['has_major_event'] = df['event_type'].notna().astype(int)
# Competitor data
df = df.merge(competitors_data[['date', 'avg_competitor_price',
'competitor_occupancy']], on='date', how='left')
# Price index (own price vs market)
df['price_index'] = df['adr'] / df['avg_competitor_price']
# Lag features
df['demand_lag_7'] = df['demand'].shift(7)
df['demand_lag_28'] = df['demand'].shift(28)
df['demand_rolling_7'] = df['demand'].rolling(7).mean()
# Drop NaN
df = df.dropna()
# Features
feature_cols = ['day_of_week', 'month', 'is_weekend', 'booking_lead_time',
'is_high_season', 'has_major_event', 'price_index',
'competitor_occupancy', 'demand_lag_7', 'demand_rolling_7']
X = df[feature_cols]
y = df['demand']
# Train model
model = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
model.fit(X, y)
# Feature importance
importance = pd.DataFrame({
'feature': feature_cols,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
return {
'model': model,
'feature_importance': importance,
'train_r2': model.score(X, y)
}
团体预订优化
团体与散客组合
def optimize_group_transient_mix(group_requests, transient_forecast,
rooms_available, dates):
"""
Optimize whether to accept group bookings vs. hold for transient
Parameters:
- group_requests: list of {group_id, rooms, nights, rate_offered}
- transient_forecast: expected transient demand and rates
- rooms_available: available rooms by date
- dates: list of dates to optimize
"""
from pulp import *
prob = LpProblem("Group_Transient", LpMaximize)
# Variables: accept group (binary)
accept_group = {}
for g, group in enumerate(group_requests):
accept_group[g] = LpVariable(f"Accept_Group_{g}", cat='Binary')
# Expected transient revenue (placeholder for optimization)
transient_revenue = {}
for date in dates:
transient_revenue[date] = LpVariable(f"Transient_Rev_{date}", lowBound=0)
# Objective: maximize total revenue
group_revenue = lpSum([accept_group[g] * group['rooms'] *
len(group['dates']) * group['rate_offered']
for g in range(len(group_requests))])
total_transient = lpSum([transient_revenue[date] for date in dates])
prob += group_revenue + total_transient
# Constraints
# Room availability by date
for date in dates:
# Group rooms consumed
group_rooms = lpSum([accept_group[g] * group['rooms']
for g, group in enumerate(group_requests)
if date in group['dates']])
# Transient rooms
transient_rooms = transient_revenue[date] / transient_forecast[date]['expected_rate']
prob += group_rooms + transient_rooms <= rooms_available[date]
# Transient revenue based on remaining capacity
for date in dates:
# Simple model: transient revenue = rooms × rate
# Limited by forecasted demand
prob += transient_revenue[date] <= \
transient_forecast[date]['expected_demand'] * \
transient_forecast[date]['expected_rate']
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
# Extract decisions
accepted_groups = []
for g, group in enumerate(group_requests):
if accept_group[g].varValue > 0.5:
accepted_groups.append({
'group_id': group['group_id'],
'rooms': group['rooms'],
'nights': len(group['dates']),
'rate': group['rate_offered'],
'revenue': group['rooms'] * len(group['dates']) * group['rate_offered']
})
return {
'total_revenue': value(prob.objective),
'accepted_groups': accepted_groups,
'group_revenue': sum([g['revenue'] for g in accepted_groups]),
'transient_revenue': value(prob.objective) -
sum([g['revenue'] for g in accepted_groups])
}
工具与库
Python 库
优化:
PuLP:线性规划scipy.optimize:通用优化cvxpy:凸优化
预测与机器学习:
scikit-learn:机器学习prophet:时间序列预测statsmodels:统计模型xgboost:梯度提升
数据分析:
pandas、numpy:数据处理matplotlib、seaborn:可视化
商业软件
收益管理系统(RMS):
- IDeaS:市场领先的 RMS
- Duetto:基于云的收益策略
- Rainmaker:收益管理软件
- Atomize:AI 驱动的定价
酒店物业管理系统(PMS):
- Oracle Opera:企业级 PMS
- Cloudbeds:面向独立酒店的云 PMS
- Mews:现代化云 PMS
- Protel:欧洲市场领先者
渠道管理:
- SiteMinder:分销平台
- TravelClick:渠道连接
- Synxis (Sabre):CRS 和分销
商业智能:
- STR (CoStar):竞争基准分析
- Kalibri Labs:收益优化分析
- OTA Insight:市场情报
常见挑战与解决方案
挑战:房价一致性问题
问题:
- OTA 显示的房价低于 brand.com
- 违反房价一致性要求
- 损害品牌声誉
解决方案:
- 使用房价监测工具并进行监控
- 动态管理 BAR(最优可用房价)
- 提供直接预订优惠(会员价、礼遇)
- 执行与 OTA 签订的合同
- 战略性地定位房价
挑战:需求波动
问题:
- 预订模式难以预测
- 临近入住时取消
- 季节性波动极端
解决方案:
- 使用先进的需求预测(ML)
- 按价格层级设置灵活的取消政策
- 动态定价(每小时更新)
- 限制最短入住天数
- 设置禁止抵达限制
挑战:控制分销成本
问题:
- OTA 佣金高(15-25%)
- 获客成本不断上升
- 盈利能力承压
解决方案:
- 开展直接预订活动(降低 CAC)
- 提供忠诚度计划奖励
- 优化元搜索竞价
- 建立战略性 OTA 合作关系
- 协商佣金
挑战:团体客房预留管理
问题:
- 团体客房流失(实际预订量低于合同约定)
- 预留客房带来的机会成本
- 团体合同复杂
解决方案:
- 实行动态团体定价
- 制定流失条款和罚则
- 分阶段释放未售出的团体预留客房
- 分析团体收入置换效应
- 自动化管理团体客房预留
输出格式
酒店收益管理报告
执行摘要:
- 物业业绩概览
- 主要优化机会
- 收入提升潜力
- 战略建议
业绩指标(本月至今):
| 指标 | 当前值 | 去年同期 | 变化 | 目标 |
|---|---|---|---|---|
| 入住率 | 78.5% | 75.2% | +3.3 个百分点 | 80% |
| ADR | $185.50 | $178.20 | +4.1% | $190 |
| RevPAR | $145.62 | $134.01 | +8.7% | $152 |
| TRevPAR | $198.25 | $185.50 | +6.9% | $205 |
预订进度(未来 30 天):
| 日期 | 已售客房 | 入住率 | ADR | 状态 | 建议 |
|---|---|---|---|---|---|
| 2026-03-15 | 165 | 82.5% | $195 | 良好 | 维持房价 |
| 2026-03-16 | 192 | 96.0% | $210 | 强劲 | 上调房价 +$10 |
| 2026-03-17 | 145 | 72.5% | $175 | 疲软 | 加大促销力度 |
渠道业绩:
| 渠道 | 已售客房 | 占比 % | 总收入 | 佣金 | 净收入 | 净 ADR |
|---|---|---|---|---|---|---|
| 直接预订(brand.com) | 850 | 28% | $165,750 | $0 | $165,750 | $195 |
| OTA A | 980 | 33% | $176,400 | $31,752 | $144,648 | $148 |
| OTA B | 620 | 21% | $108,500 | $16,275 | $92,225 | $149 |
| GDS | 420 | 14% | $84,000 | $8,400 | $75,600 | $180 |
| 其他 | 130 | 4% | $19,500 | $0 | $19,500 | $150 |
| 总计 | 3,000 | 100% | $554,150 | $56,427 | $497,723 | $166 |
定价建议:
| 客房类型 | 当前房价 | 建议房价 | 预期影响 |
|---|---|---|---|
| 标准客房 | $175 | $185 | 每日收入增加 $1,200 |
| 豪华客房 | $225 | $235 | 每日收入增加 $600 |
| 套房 | $350 | $365 | 每日收入增加 $300 |
战略行动:
- 将直接预订占比从 28% 提高至 35%(每月节省 $45K 佣金)
- 将动态定价更新频率提高至每 4 小时一次(而非每天一次)
- 推出春季促销:预订 3 晚,第 4 晚享 20% 折扣
- 重新协商 OTA 合同,争取更优惠的佣金率
- 优化团体业务策略:仅接受房价达到 $165 及以上的企业团体
需要询问的问题
如果需要更多背景信息:
- 酒店属于哪种类型?(豪华型、中端型、有限服务型、度假村)
- 有多少间客房,各有哪些客房类型?
- 当前入住率、ADR 和 RevPAR 是多少?
- 竞争组有哪些,市场定位如何?
- 使用了哪些分销渠道?
- 已部署哪些系统?(PMS、RMS、渠道管理器)
- 主要挑战是什么?(入住率、房价、分销成本)
相关技能
- tour-operations:用于旅行社运营和套餐管理
- airline-cargo-optimization:用于航空公司运营
- hospitality-procurement:用于酒店采购
- demand-forecasting:用于高级预测技术
- dynamic-pricing:用于定价优化
- optimization-modeling:用于高级优化
- seasonal-planning:用于季节性需求管理
评论
支持 Markdown