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(総 RevPAR):施設全体の売上合計 / 販売可能客室数
収益最適化の方程式:
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:凸最適化
予測と ML:
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)
- 料金区分を設けた柔軟なキャンセルポリシー
- 動的な価格設定(1 時間ごとの更新)
- 最低宿泊日数の制限
- 到着日指定による販売制限
課題:流通コストの管理
問題:
- OTAの高い手数料(15~25%)
- 顧客獲得コストの上昇
- 収益性への圧力
解決策:
- 直接予約キャンペーン(顧客獲得コストを削減)
- ロイヤルティプログラムの特典
- メタサーチ入札の最適化
- OTAとの戦略的パートナーシップ
- 手数料の交渉
課題:団体予約枠の管理
問題:
- 団体予約のキャンセル(契約数を下回る予約)
- 客室を確保することによる機会損失
- 複雑な団体契約
解決策:
- 団体向けダイナミックプライシング
- 予約未達条項と違約金
- 未販売の団体予約枠を段階的に開放
- 団体予約による収益の置き換え分析
- 団体予約枠の自動管理
出力形式
ホテル収益管理レポート
エグゼクティブサマリー:
- 施設の業績概要
- 主な最適化の機会
- 収益改善の可能性
- 戦略的な提言
業績指標(月初来):
| 指標 | 現在 | 昨年 | 増減 | 目標 |
|---|---|---|---|---|
| 客室稼働率 | 78.5% | 75.2% | +3.3 pts | 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日あたり収益が+ $1,200 |
| デラックス | $225 | $235 | 1日あたり収益が+ $600 |
| スイート | $350 | $365 | 1日あたり収益が+ $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 対応