This is based on the solution from @Mark Chackerian but addesses the following issues:
- If the base
queryset is empty, the result is None
- The data might change between the queries for
count() and values(), so this locks the queryset inside a transaction.
The result type will generally be the same as the database field type (Decimal, int, or float) with the only exception being an int column of even values, which will result in a float.
For example, the median of [1,2,3] is 2 (int) while [1,2] yields 1.5 (float). If you need the result to be an int in this case, apply round or floor
This requires Python 3.10+, although after removing the type annotations it probably works with earlier versions, too.
from decimal import Decimal
from django.db import transaction
from django.db.models import QuerySet
def median_value(queryset: QuerySet, field: str) -> Decimal | float | int | None:
with transaction.atomic():
count = queryset.select_for_update().count()
if count == 0:
return None
values = queryset.values_list(field, flat=True).order_by(field)
if count % 2 == 1:
return values[count // 2]
else:
return sum(values[count // 2 - 1 : count // 2 + 1]) / 2