📊

Django + Pandas + Plotlyでホテル収益管理ダッシュボード構築

に公開

はじめに

ホテル業界では、GOP(総営業利益)、RevPAR(販売可能客室1室あたり収益)、ADR(平均客室単価)といった指標が経営判断の要となります。これらをリアルタイムで可視化するダッシュボードをDjango + Pandas + Plotlyで実装します。

主要指標の計算ロジック

まず、ホテル業界特有の指標を理解しましょう。

# models.py
from django.db import models
from decimal import Decimal

class DailyRevenue(models.Model):
    date = models.DateField()
    room_revenue = models.DecimalField(max_digits=10, decimal_places=2)
    fb_revenue = models.DecimalField(max_digits=10, decimal_places=2)  # 料飲収益
    other_revenue = models.DecimalField(max_digits=10, decimal_places=2)
    rooms_sold = models.IntegerField()
    rooms_available = models.IntegerField()
    operating_expenses = models.DecimalField(max_digits=10, decimal_places=2)
    
    @property
    def occupancy_rate(self):
        return (self.rooms_sold / self.rooms_available * 100) if self.rooms_available else 0
    
    @property
    def adr(self):
        """平均客室単価"""
        return self.room_revenue / self.rooms_sold if self.rooms_sold else Decimal('0')
    
    @property
    def revpar(self):
        """販売可能客室1室あたり収益"""
        return self.room_revenue / self.rooms_available if self.rooms_available else Decimal('0')
    
    @property
    def gop(self):
        """総営業利益"""
        total_revenue = self.room_revenue + self.fb_revenue + self.other_revenue
        return total_revenue - self.operating_expenses

データパイプライン実装

PMSやOTAからのデータを集約し、Pandasで前処理を行います。

# data_pipeline.py
import pandas as pd
from datetime import datetime, timedelta
from .models import DailyRevenue

class RevenueDataPipeline:
    def __init__(self):
        self.df = None
    
    def fetch_data(self, start_date, end_date):
        """データベースから期間指定でデータ取得"""
        queryset = DailyRevenue.objects.filter(
            date__range=[start_date, end_date]
        ).values()
        self.df = pd.DataFrame(list(queryset))
        return self
    
    def calculate_metrics(self):
        """KPI計算と移動平均追加"""
        if self.df.empty:
            return self
        
        # 基本指標計算
        self.df['occupancy'] = (self.df['rooms_sold'] / self.df['rooms_available']) * 100
        self.df['adr'] = self.df['room_revenue'] / self.df['rooms_sold']
        self.df['revpar'] = self.df['room_revenue'] / self.df['rooms_available']
        self.df['gop'] = (self.df['room_revenue'] + self.df['fb_revenue'] + 
                          self.df['other_revenue']) - self.df['operating_expenses']
        
        # 7日移動平均
        self.df['revpar_ma7'] = self.df['revpar'].rolling(window=7).mean()
        self.df['gop_ma7'] = self.df['gop'].rolling(window=7).mean()
        
        return self
    
    def get_dataframe(self):
        return self.df

Plotlyでインタラクティブな可視化

# views.py
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import json
from django.shortcuts import render
from datetime import datetime, timedelta
from .data_pipeline import RevenueDataPipeline

def dashboard_view(request):
    # 過去30日間のデータ取得
    end_date = datetime.now().date()
    start_date = end_date - timedelta(days=30)
    
    pipeline = RevenueDataPipeline()
    df = pipeline.fetch_data(start_date, end_date).calculate_metrics().get_dataframe()
    
    # 複合グラフ作成
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=('RevPAR推移', 'ADR vs 稼働率', 'GOP推移', '収益構成'),
        specs=[[{'secondary_y': False}, {'secondary_y': True}],
               [{'secondary_y': False}, {'type': 'pie'}]]
    )
    
    # RevPAR推移(実績と移動平均)
    fig.add_trace(
        go.Scatter(x=df['date'], y=df['revpar'], name='RevPAR',
                  mode='lines+markers', line=dict(color='#1f77b4')),
        row=1, col=1
    )
    fig.add_trace(
        go.Scatter(x=df['date'], y=df['revpar_ma7'], name='7日移動平均',
                  line=dict(dash='dash', color='#ff7f0e')),
        row=1, col=1
    )
    
    # ADRと稼働率の相関
    fig.add_trace(
        go.Bar(x=df['date'], y=df['adr'], name='ADR', marker_color='#2ca02c'),
        row=1, col=2, secondary_y=False
    )
    fig.add_trace(
        go.Scatter(x=df['date'], y=df['occupancy'], name='稼働率(%)',
                  line=dict(color='#d62728')),
        row=1, col=2, secondary_y=True
    )
    
    # GOP推移
    fig.add_trace(
        go.Scatter(x=df['date'], y=df['gop'], name='GOP',
                  fill='tozeroy', fillcolor='rgba(0,100,80,0.2)'),
        row=2, col=1
    )
    
    # 収益構成(最新日)
    latest = df.iloc[-1]
    fig.add_trace(
        go.Pie(labels=['客室', 'F&B', 'その他'],
               values=[latest['room_revenue'], latest['fb_revenue'], latest['other_revenue']]),
        row=2, col=2
    )
    
    fig.update_layout(height=800, showlegend=True, title_text="ホテル収益管理ダッシュボード")
    graph_json = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
    
    # KPIサマリー計算
    kpi_summary = {
        'current_revpar': f"¥{df['revpar'].iloc[-1]:,.0f}",
        'revpar_change': f"{((df['revpar'].iloc[-1] / df['revpar'].iloc[-7] - 1) * 100):.1f}%",
        'current_gop': f"¥{df['gop'].iloc[-1]:,.0f}",
        'avg_occupancy': f"{df['occupancy'].mean():.1f}%"
    }
    
    return render(request, 'dashboard.html', {
        'graph_json': graph_json,
        'kpi_summary': kpi_summary
    })

まとめ

この実装により、ホテルの経営指標をリアルタイムで監視できるダッシュボードが構築できます。Pandasによる効率的なデータ処理とPlotlyのインタラクティブな可視化により、経営判断に必要な情報を即座に把握できます。

次回は、このダッシュボードにWebSocketを組み込んでリアルタイム更新を実現する方法を解説予定です。

Discussion