Среднее значение из 5 лучших значений в модели

У меня есть модель django с большим количеством полей. Я пытаюсь в одном запросе получить среднее значение данного поля и среднее значение 5 лучших значений этого же поля (из моего другого вопроса о чистом SQL: Среднее значение 5 лучших значений в таблице для заданной группы by). Не то чтобы это имело значение, но: моя база данных - redshift.

Я нашел два разных способа добиться этого на SQL, но у меня возникают проблемы с реализацией этих запросов с помощью django ORM

Вот пример того, что я хочу сделать с помощью Cars:

class Cars(models.Model):
    manufacturer = models.CharField()
    model = models.CharField()
    price = models.FloatField()

Данные:

manufacturer | model | price
Citroen        C1      1
Citroen        C2      2
Citroen        C3      3
Citroen        C4      4
Citroen        C5      5
Citroen        C6      6
Ford           F1      7
Ford           F2      8
Ford           F3      9
Ford           F4      10
Ford           F5      11
Ford           F6      12 
Ford           F6      19 
GenMotor       G1      20
GenMotor       G3      25
GenMotor       G4      22

Ожидаемый выход:

manufacturer | average_price | average_top_5_price
Citroen        3.5             4.0
Ford           10.85           12.2
GenMotor       22.33           22.33

Вот два чистых SQL-запроса, достигающих желаемого эффекта:

SELECT
    main.manufacturer,
    AVG(main.price) AS average_price,
    AVG(CASE WHEN rank <= 5 THEN main.price END) AS average_top_5_price
FROM (
    SELECT
        manufacturer,
        price,
        ROW_NUMBER() OVER (PARTITION BY manufacturer ORDER BY price DESC) AS rank
    FROM
        cars
) main
GROUP BY
    main.manufacturer;

А второй способ:

SELECT A.manufacturer, A.avg_price, B.top5_price
FROM (
    SELECT manufacturer, AVG(price) as avg_price
    FROM cars
    GROUP BY manufacturer
) A
JOIN (
    SELECT manufacturer, AVG(psv_99) as top5_price
    FROM (
        SELECT manufacturer, price, RANK()
        OVER (PARTITION BY manufacturer ORDER BY price DESC, id)
        FROM cars
    )
    WHERE rank <= 5
    GROUP BY manufacturer
) B
ON A.manufacturer = B.manufacturer
ORDER BY manufacturer

Пока что мне не удалось реализовать ни один из этих запросов с помощью django ORM, для первого я не могу найти способ заставить django сделать "select from subquery", для второго я не могу найти хороший способ заставить django "join two subquery"

Вы можете использовать комбинацию values и annotate для group by производителя, а затем вычислить среднее по группе с помощью Avg.

Вычислить average_price довольно просто:

from django.db.models import Avg
from django.db.models.functions import Round

averages =
Car.objects.values("manufacturer").annotate(average_price=Round(Avg("price"), precision=2))

Но чтобы вычислить пятерку лучших в каждой группе, все становится немного сложнее (как мне кажется). Для этого вам понадобится Subquery. Итак, полный код будет выглядеть так:


from django.db.models import Subquery, OuterRef, Avg, Q
from django.db.models.functions import Round

group_top_5 = Car.objects.filter(manufacturer=OuterRef("manufacturer")).order_by("-price")[:5].values("price")

query_filter = Q(price__in=group_top_5)
averages = (
Car.objects.values("manufacturer")
.annotate(
average_price=Round(Avg("price"), precision=2), 
average_top_5_price=Round(Avg("price", filter=query_filter), precision=2))

)

Это должно дать вам:

{'manufacturer': 'Citroen', 'average_price': 3.5, 'average_top_5_price': 4.0}
{'manufacturer': 'Ford', 'average_price': 10.86, 'average_top_5_price': 12.2}
{'manufacturer': 'GenMotor', 'average_price': 22.33, 'average_top_5_price': 22.33}

Предполагая, что ваша СУБД поддерживает функцию AVG со спецификацией оконной рамки, вы можете использовать следующее:

from carsdemo.models import Cars
from django.db.models import OuterRef, Subquery
from django.db.models import Window, RowRange, F, Avg

windowFrameSpec = {
  "order_by": F("price").desc(),
  "frame": RowRange(start=0, end=4)
}

averageSubquery = Cars.objects.filter(manufacturer=OuterRef("manufacturer")).annotate(
  the_value = Window(expression=Avg("price"), **windowFrameSpec)
).order_by("-price")

result = Cars.objects.values("manufacturer").annotate(
  average_price=Avg("price"),
  average_top_5_price=Subquery(averageSubquery.values("the_value")[:1])
)

На SQL Server он выдает следующий запрос:

SELECT carsdemo_cars.manufacturer, AVG(carsdemo_cars.price) AS average_price, (
  SELECT TOP 1 AVG(U0.price) OVER (
    ORDER BY U0.price DESC 
    ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING
  ) AS the_value
  FROM carsdemo_cars U0
  WHERE U0.manufacturer = (carsdemo_cars.manufacturer)
  ORDER BY U0.price DESC
) AS average_top_5_price
FROM carsdemo_cars
GROUP BY carsdemo_cars.manufacturer

И результат:

<QuerySet [
{'manufacturer': 'Citroen', 'average_price': 3.5, 'average_top_5_price': 4.0},
{'manufacturer': 'Ford', 'average_price': 10.857142857142858, 'average_top_5_price': 12.2},
{'manufacturer': 'GenMotor', 'average_price': 22.333333333333332, 'average_top_5_price': 22.333333333333332}
]>

Обратите внимание, что, возможно, есть лучшие способы написать приведенный выше запрос, но я нахожу этот способ более простым в реализации.

Вернуться на верх