ORM

Django ORMで高度なデータ分析|集計・グループ化・サブクエリの応用

DjangoのORMは、複雑なデータ分析やレポート作成にも対応できる高度なクエリ機能を備えています。ここでは、グループ化、複合集計、サブクエリなどを組み合わせた実践的なデータ分析パターンを解説します。

基本的な使い方

views.py
from django.db.models import Window, F
from django.db.models.functions import Rank

# 商品を価格順にランク付け
products = Product.objects.annotate(
    price_rank=Window(
        expression=Rank(),
        order_by=F('price').desc()
    )
)

説明

Step 1Window関数の基本

Djangoの Window 関数は、Django 3.2から導入された高度なデータ分析機能です。Window関数を使用すると、行間の関係性を分析したり、集計値をパーティション(グループ)ごとに計算したりすることができます。

from django.db.models import Window, F
from django.db.models.functions import Rank, RowNumber, DenseRank, Sum, Avg

# 基本的な使い方
モデル名.objects.annotate(
    計算フィールド=Window(
        expression=集計関数(),  # Sum, Avg, Rank など
        partition_by=[F('分割するフィールド')],  # オプション
        order_by=F('並べ替えるフィールド').asc()  # オプション
    )
)

Window関数は、SQL の OVER 句に相当し、データをフレーム(ウィンドウ)として扱います。

Step 2基本的な使用例:ランキング

Window関数の最も一般的な使用例の一つは、ランキングの計算です:

from django.db.models import Window, F
from django.db.models.functions import Rank

# 商品を価格順にランク付け
products = Product.objects.annotate(
    price_rank=Window(
        expression=Rank(),
        order_by=F('price').desc()
    )
).order_by('price_rank')

# 結果例: 
# Product(name='高級品', price=10000, price_rank=1)
# Product(name='中級品', price=5000, price_rank=2)
# Product(name='普通品', price=1000, price_rank=3)

この例では、全商品を価格の降順でランク付けしています。

Step 3パーティション(分割)の活用

partition_byパラメータを使用すると、特定のフィールドごとにグループ化してWindow関数を適用できます:

from django.db.models import Window, F
from django.db.models.functions import Rank

# カテゴリー別の価格ランキング
products = Product.objects.annotate(
    category_rank=Window(
        expression=Rank(),
        partition_by=[F('category')],  # カテゴリーごとにパーティション
        order_by=F('price').desc()
    )
).order_by('category', 'category_rank')

# 結果例:
# Product(name='電子高級品', category='電子機器', price=10000, category_rank=1)
# Product(name='電子一般品', category='電子機器', price=5000, category_rank=2)
# Product(name='家具高級品', category='家具', price=8000, category_rank=1)
# Product(name='家具一般品', category='家具', price=3000, category_rank=2)

この例では、各カテゴリー内での価格ランキングを計算しています。例えば、「電子機器」カテゴリと「家具」カテゴリそれぞれで独立したランキングが付きます。

Step 4様々なWindow関数

Djangoは複数のWindow関数をサポートしています:

from django.db.models import Window, F
from django.db.models.functions import Rank, DenseRank, RowNumber, Lead, Lag, Sum

# Rank() - 通常のランキング(同値は同順位、次の順位はスキップ)
products = Product.objects.annotate(
    rank=Window(expression=Rank(), order_by=F('price').desc())
)  # 結果例: 1, 2, 2, 4, 5...

# DenseRank() - 詰めたランキング(同値は同順位、次の順位は連続)
products = Product.objects.annotate(
    dense_rank=Window(expression=DenseRank(), order_by=F('price').desc())
)  # 結果例: 1, 2, 2, 3, 4...

# RowNumber() - 行番号(同値でも連番)
products = Product.objects.annotate(
    row_number=Window(expression=RowNumber(), order_by=F('price').desc())
)  # 結果例: 1, 2, 3, 4, 5...

# Lead() - 次の行の値を取得
products = Product.objects.annotate(
    next_price=Window(
        expression=Lead('price', offset=1, default=0),
        order_by=F('price').desc()
    )
)

# Lag() - 前の行の値を取得
products = Product.objects.annotate(
    prev_price=Window(
        expression=Lag('price', offset=1, default=0),
        order_by=F('price').desc()
    )
)

Step 5累積集計と移動平均

Window関数は累積集計や移動平均の計算にも利用できます:

from django.db.models import Window, F
from django.db.models.functions import Sum, Avg

# 日付順の累積売上
sales = Sale.objects.annotate(
    cumulative_sum=Window(
        expression=Sum('amount'),
        order_by=F('date').asc()
    )
).order_by('date')

# 直近3件の移動平均
from django.db.models import Window, F, FrameSpec
from django.db.models.functions import Avg

sales = Sale.objects.annotate(
    moving_avg=Window(
        expression=Avg('amount'),
        order_by=F('date').asc(),
        frame=FrameSpec(
            start=-2,  # 2行前から
            end=0      # 現在行まで
        )
    )
).order_by('date')

FrameSpecを使用すると、計算対象の行の範囲(フレーム)を詳細に指定できます。

Step 6実践的な使用例

views.pyでのWindow関数の使用例:

from django.shortcuts import render
from django.db.models import Window, F, Sum, Count
from django.db.models.functions import Rank, DenseRank, RowNumber
from .models import Sale, Product, Category

def sales_analysis(request):
    # カテゴリ別の売上トップ3商品を取得
    top_products_by_category = Product.objects.annotate(
        total_sales=Sum('sales__amount'),
        category_rank=Window(
            expression=Rank(),
            partition_by=[F('category_id')],
            order_by=F('total_sales').desc()
        )
    ).filter(category_rank__lte=3).order_by('category_id', 'category_rank')
    
    # 月ごとの累積売上
    from django.db.models.functions import TruncMonth
    
    monthly_sales = Sale.objects.annotate(
        month=TruncMonth('date')
    ).values('month').annotate(
        monthly_total=Sum('amount')
    ).annotate(
        cumulative_total=Window(
            expression=Sum('monthly_total'),
            order_by=F('month').asc()
        )
    ).order_by('month')
    
    # 売上上位10%の商品を「トップセラー」としてマーク
    from django.db.models.functions import Ntile
    
    products_with_percentile = Product.objects.annotate(
        total_sales=Sum('sales__amount'),
        percentile=Window(
            expression=Ntile(10),  # 10分位で分割
            order_by=F('total_sales').desc()
        )
    ).annotate(
        is_top_seller=F('percentile').exact(1)  # 1分位(上位10%)か判定
    )
    
    return render(request, 'sales/analysis.html', {
        'top_products_by_category': top_products_by_category,
        'monthly_sales': monthly_sales,
        'products_with_percentile': products_with_percentile
    })

テンプレートでの使用例(analysis.html):

<h1>売上分析</h1>

<h2>カテゴリ別トップ3商品</h2>
{% regroup top_products_by_category by category as category_list %}
{% for category in category_list %}
    <h3>{{ category.grouper }}</h3>
    <table>
        <tr>
            <th>ランク</th>
            <th>商品名</th>
            <th>売上合計</th>
        </tr>
        {% for product in category.list %}
            <tr>
                <td>{{ product.category_rank }}</td>
                <td>{{ product.name }}</td>
                <td>{{ product.total_sales|floatformat:0 }}円</td>
            </tr>
        {% endfor %}
    </table>
{% endfor %}

<h2>月別累積売上</h2>
<table>
    <tr>
        <th>月</th>
        <th>月間売上</th>
        <th>累積売上</th>
    </tr>
    {% for sale in monthly_sales %}
        <tr>
            <td>{{ sale.month|date:"Y年m月" }}</td>
            <td>{{ sale.monthly_total|floatformat:0 }}円</td>
            <td>{{ sale.cumulative_total|floatformat:0 }}円</td>
        </tr>
    {% endfor %}
</table>
重要ポイント:
  • Window関数はDjango 3.2以降で使用可能です。
  • 使用するデータベースがWindow関数をサポートしている必要があります(PostgreSQL、MySQL 8.0+、SQLite 3.25+、Oracle、SQL Server)。
  • Window関数は大量データの分析に有用ですが、複雑なクエリになるためパフォーマンスに注意が必要です。
  • partition_byは複数フィールドを指定可能で、複数の条件でグループ化できます。
  • FrameSpecを使用することで、より細かい行の範囲指定が可能になります。
  • Window関数の結果は通常、annotateメソッドを通じて一時フィールドとして追加されます。

まとめ

  • values()annotate()の組み合わせでGROUP BYを実現できる
  • 複数の集計関数を同時に使って多角的な分析が可能
  • サブクエリと組み合わせることで複雑なデータ抽出ができる
  • 日付のTrunc関数で時系列分析(日別・月別・年別)が行える
  • パフォーマンスに注意し、必要に応じてインデックスやキャッシュを活用する
  • 複雑すぎるクエリはRaw SQLの使用も検討する