Djangoで、集計処理

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年11月22日21:15に更新(約21日前)
2018年11月20日23:08に作成(約23日前)

旧ブログ移行記事です。

概要

Webアプリケーションにおいて、データの作成、一覧、更新、削除といったCRUD処理はよく使います。場合によってはそれらだけでなく、データの平均値を求めたり、何個のデータが紐づくか、といった集計処理も必要になります。

今回はその集計処理のいくつかのサンプルを紹介します。

サンプルモデルとして、以下のようなものを使います。本と、それへの評価モデルです。本屋さんのシステムかもしれませんね。

from django.db import models

SCORE_CHOICES = (
    (1, '★1'),
    (2, '★2'),
    (3, '★3'),
    (4, '★4'),
    (5, '★5'),
)


class Book(models.Model):
    """本"""
    title = models.CharField('タイトル', max_length=255)
    price = models.IntegerField('価格')

    def __str__(self):
        return '{} - {}'.format(self.title, self.price)


class Review(models.Model):
    """評価"""
    point = models.IntegerField('評価点', choices=SCORE_CHOICES)
    target = models.ForeignKey(Book, verbose_name='評価対象の本', on_delete=models.CASCADE)

    def __str__(self):
        # 'よくわかるPythonの本 - ★5' のように返す
        return '{} - {}'.format(self.target.title, self.get_point_display())

Bookは以下のようなデータがあります。

Reviewは以下のように。

count()

Queryset.count()は、そのクエリで何件のデータが取得できたかを返します。

単純に、本とレビューの件数を取得してみましょう。

# 本の個数
>>> Book.objects.count()
3

# レビューの件数
>>> Review.objects.count()
7

count()は、クエリセットに対してのlen()と同様の結果を返します。

>>> len(Book.objects.all())
3

>>> len(Review.objects.all())
7

が、個数だけ知りたい場合はcount()のほうがパフォーマンス的に優れます。

「よくわかるPythonの本」のレビュー件数を取得してみます。

# targetのnameが、よくわかるPythonの本
>>> Review.objects.filter(target__title='よくわかるPythonの本').count()
3

2000円以上の書籍をカウントしてみます。

# 価格が2000以上。gteはgreater than equal の略
>>> Book.objects.filter(price__gte=2000).count()
2

aggregate()

aggregate()は、Querysetに対しての集計を行います。難しいと思うので、実際の例を見ていきましょう。

全ての書籍の、価格平均を求めます。

>>> from django.db.models import Avg
>>> Book.objects.aggregate(Avg('price'))
{'price__avg': 2166.6666666666665}

全ての書籍の価格で、いちばん高いものを求めます。

>>> from django.db.models import Max
>>> Book.objects.aggregate(Max('price'))
{'price__max': 3000}

一番安いものも同様に求めれます。

>>> from django.db.models import Min
>>> Book.objects.aggregate(Min('price'))
{'price__min': 1500}

これらを同時に求めることもできます。

>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Max('price'), Min('price'), Avg('price'))
{'price__max': 3000, 'price__min': 1500, 'price__avg': 2166.6666666666665}

結果は辞書で帰りますが、その名前はフィールド名__集計関数というルールで作成されます。その名前を変えたい場合は、以下のようにキーワード引数名として指定します。

>>> from django.db.models import Min
>>> Book.objects.aggregate(yasui=Min('price'))
{'yasui': 1500}

全てのレビューの平均なんかも同様に求めれますね。

>>> from django.db.models import Avg
>>> Review.objects.aggregate(Avg('point'))
{'point__avg': 3.5714285714285716}

filter()等と一緒にも使えますが、aggregate()は最後に呼び出す必要があります。2000円以上の書籍の、評価の平均値を求めてみます。

>>> Review.objects.filter(target__price__gte=2000).aggregate(Avg('point'))
{'point__avg': 3.6}

annotate()

annotate()は、QuerySet の各アイテムに対する集計を生成します。ForeignKeyManyToManyField等、他のモデルと何らかの関係で紐づいていて、それらに関する集計に使うことになります。

イメージとしては、以下のような感じです。

for book in Book.objects.annotate...:
    # ここで、各アイテム...書籍ごとに取得した集計データが取れる
    # その書籍に紐づいたモデル(今回ならレビュー)の件数や平均、最小、最大など

実際に試してみましょう。各書籍に、何件のレビューがついているかです。

>>> from django.db.models import Count
>>> for book in Book.objects.annotate(Count('review')):
...     '書籍名:{} - レビュー数:{}'.format(book.title, book.review__count)
...
'書籍名:よくわかるPythonの本 - レビュー数:3'
'書籍名:普通のPythonの本 - レビュー数:2'
'書籍名:わからないPythonの本 - レビュー数:2'

for book in Book.objects.annotate(Count('review')):ですが、for book in Book.objects.all()と似ていますが、bookにreview_countという属性が追加され、そこに紐づいたレビューの数が格納されています。

aggregate同様フィールド名__集計関数という属性名で、キーワード引数名として指定するとその名前が使えます。

# reviewsという名前にする
>>> for book in Book.objects.annotate(reviews=Count('review')):
...     '書籍名:{} - 評価数:{}'.format(book.title, book.reviews)

紐づいたレビューのうち、一番高い評価も取得します。

>>> for book in Book.objects.annotate(max_review=Max('review__point')):
...     '書籍名:{} - 最高評価:{}'.format(book.title, book.max_review)
...
'書籍名:よくわかるPythonの本 - 最高評価:5'
'書籍名:普通のPythonの本 - 最高評価:4'
'書籍名:わからないPythonの本 - 最高評価:2'

一番低い評価を取得します。

>>> for book in Book.objects.annotate(min_review=Min('review__point')):
...     '書籍名:{} - 最低評価:{}'.format(book.title, book.min_review)
...
'書籍名:よくわかるPythonの本 - 最低評価:4'
'書籍名:普通のPythonの本 - 最低評価:3'
'書籍名:わからないPythonの本 - 最低評価:2'

そして、平均評価です。

>>> for book in Book.objects.annotate(avg_review=Avg('review__point')):
...     '書籍名:{} - 平均評価:{}'.format(book.title, book.avg_review)
...
'書籍名:よくわかるPythonの本 - 平均評価:4.666666666666667'
'書籍名:普通のPythonの本 - 平均評価:3.5'
'書籍名:わからないPythonの本 - 平均評価:2.0'

また、追加された属性名を使ってorder_by()として並び替えるのも有用です。レビュー数の多い本から表示するようにしてみましょう。

>>> for book in Book.objects.annotate(reviews=Count('review')).order_by('-reviews'):
...     '書籍名:{} - 評価数:{}'.format(book.title, book.reviews)
...
'書籍名:よくわかるPythonの本 - 評価数:3'
'書籍名:普通のPythonの本 - 評価数:2'
'書籍名:わからないPythonの本 - 評価数:2'

ブログでよくやるのは、カテゴリ名を表示しつつそのカテゴリに属する記事数も一緒に表示したりします。Python(100記事) みたいな感じですね。これもannotate()でやることができます。

order_byで記事が多いカテゴリから順に表示する、とかもよくやる処理ですね。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする