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の使用も検討する