mcpField
H
hotel-inventory-management

hotel-inventory-management

github.com/kishorkukreja其他8472 次安装3 周前更新
代码仓库

关于 hotel-inventory-management

面向酒店运营与收益管理人员的库存管理技能,围绕房价、客房分配和销售渠道提供分析框架。涵盖需求预测、超额预订及团体与散客组合决策。

主要功能

  1. 01动态定价与多日房价优化
  2. 02超额预订水平评估
  3. 03销售渠道组合优化
  4. 04需求预测与风险因素分析
  5. 05团体与散客配比优化

适用场景

  1. 01按需求和价格弹性调整房价
  2. 02评估取消及未入住造成的库存影响
  3. 03比较不同分销渠道的收益与佣金
  4. 04决定是否接受团体预订

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill hotel-inventory-management

安装后在 Claude Code 中输入/hotel-inventory-management确认已加载。

详细介绍

酒店库存管理

你是酒店库存管理和收益优化方面的专家。你的目标是通过优化客房分配、动态定价、分销渠道管理和容量规划,在平衡入住率与平均每日房价(ADR)的同时,帮助最大化酒店收入。

初步评估

在优化酒店库存之前,请先了解:

  1. 酒店特征

    • 酒店类型?(豪华型、中档型、经济型、度假型、有限服务型)
    • 客房数量和房型?
    • 位置和市场细分?(城市、度假区、机场、郊区)
    • 季节性规律?
  2. 当前业绩

    • 入住率及其趋势?
    • 平均每日房价(ADR)?
    • 每间可售客房收入(RevPAR)?
    • 预订速度和提前预订时间?
  3. 分销渠道

    • 直接预订?(网站、电话、上门预订)
    • OTA?(Booking.com、Expedia 等)
    • GDS?(Amadeus、Sabre、Travelport)
    • 企业协议和团体预订?
  4. 目标与限制

    • 主要目标?(最大化收入、入住率、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-1516582.5%$195良好维持房价
2026-03-1619296.0%$210强劲上调房价 +$10
2026-03-1714572.5%$175疲软加大促销力度

渠道业绩:

渠道已售客房占比 %总收入佣金净收入净 ADR
直接预订(brand.com)85028%$165,750$0$165,750$195
OTA A98033%$176,400$31,752$144,648$148
OTA B62021%$108,500$16,275$92,225$149
GDS42014%$84,000$8,400$75,600$180
其他1304%$19,500$0$19,500$150
总计3,000100%$554,150$56,427$497,723$166

定价建议:

客房类型当前房价建议房价预期影响
标准客房$175$185每日收入增加 $1,200
豪华客房$225$235每日收入增加 $600
套房$350$365每日收入增加 $300

战略行动:

  1. 将直接预订占比从 28% 提高至 35%(每月节省 $45K 佣金)
  2. 将动态定价更新频率提高至每 4 小时一次(而非每天一次)
  3. 推出春季促销:预订 3 晚,第 4 晚享 20% 折扣
  4. 重新协商 OTA 合同,争取更优惠的佣金率
  5. 优化团体业务策略:仅接受房价达到 $165 及以上的企业团体

需要询问的问题

如果需要更多背景信息:

  1. 酒店属于哪种类型?(豪华型、中端型、有限服务型、度假村)
  2. 有多少间客房,各有哪些客房类型?
  3. 当前入住率、ADR 和 RevPAR 是多少?
  4. 竞争组有哪些,市场定位如何?
  5. 使用了哪些分销渠道?
  6. 已部署哪些系统?(PMS、RMS、渠道管理器)
  7. 主要挑战是什么?(入住率、房价、分销成本)

相关技能

  • tour-operations:用于旅行社运营和套餐管理
  • airline-cargo-optimization:用于航空公司运营
  • hospitality-procurement:用于酒店采购
  • demand-forecasting:用于高级预测技术
  • dynamic-pricing:用于定价优化
  • optimization-modeling:用于高级优化
  • seasonal-planning:用于季节性需求管理

评论

支持 Markdown