Данные временной метки Django Filter GROUP BY день, неделя, месяц, год

У меня есть приложение django (DRF), в котором я периодически храню данные временных рядов на основе ответа API. Вот моя модель.py

# Model to store the Alexa API Data
class Alexa(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    extra = jsonfield.JSONField(null=True)
    rank =  models.PositiveIntegerField(default=0, null=True)

Я использую django-фильтры для запроса данных на основе диапазона (__lte, __gte). Подобно /api/alexa/?created_at__lte=2020-02-14T09:15:52.329641Z вернуть все данные, созданные до 2020-02-14T09:15:52.329641Z

[
    {
        "id": 1,
        "created_at": "2020-02-03T19:30:57.868588Z",
        "extra": "{'load_time': 00, 'backlink': 0}",
        "rank": 0
    },
    ...
 ]

Есть ли способ создать конечную точку для возврата агрегированных данных, сгруппированных по дням, неделям, месяцам и годам, на основе параметров запроса, которые я передаю. Например, /api/alexa/?created_at__lte=2020-02-14T09:15:52.329641Z&group_by=month вернет

[
    {
        "created_at": "2020-01-01T00:00:00.000000Z",
        "extra": "{'load_time': 00, 'backlink': 0}", <- Aggregated Data 
        "rank": 0                                    <- Aggregated Data
    },
    {
        "created_at": "2020-02-01T00:00:00.000000Z",
        "extra": "{'load_time': 00, 'backlink': 0}", <- Aggregated Data 
        "rank": 0                                    <- Aggregated Data 
    },
 ]

Вот мои текущие views.py

class AlexaViewSet(viewsets.ModelViewSet):
    queryset = Alexa.objects.all()
    filter_fields = {'created_at' : ['iexact', 'lte', 'gte']}
    http_method_names = ['get', 'post', 'head']

Я видел несколько фрагментов, выполняющих агрегацию, но ни один из них полностью не удовлетворял моим требованиям и не давал мне полного представления о теме.

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

РЕДАКТИРОВАТЬ: вот мой serializer.py

class AlexaSerializer(serializers.ModelSerializer):
     class Meta:
         model = Alexa
         fields = '__all__'

person Shouvik Mitra    schedule 15.02.2020    source источник


Ответы (1)


Во-первых, класс AlexaViewSet — это не сериализатор, а ViewSet. Вы не указали класс сериализатора в этом ViewSet, поэтому вам нужно указать это.

С другой стороны, если вы хотите передать пользовательский параметр запроса по URL-адресу, вам следует переопределить метод list этого ViewSet и проанализировать строку запроса, переданную в объекте request, чтобы получить значение group_by, проверить его, а затем выполнить агрегация себя.

Еще одна проблема, которую я вижу, заключается в том, что вам также необходимо определить, что нужно для агрегирования поля JSON, которое не поддерживается в SQL и является очень относительным, поэтому вы можете рассмотреть возможность изменения способа хранения информации этого поля JSON, если хотите для выполнения агрегирования полей внутри него. Я бы предложил извлечь поля, которые вы хотите агрегировать, из JSON (при их сохранении в базе данных) и поместить их в столбец SQL отдельно, чтобы вы могли выполнить агрегацию позже. Клиент также может передать операцию агрегирования в качестве параметра запроса, например, aggregation=sum или aggregation=avg.

В простом случае, когда вам просто нужно среднее значение рангов, это должно быть полезно в качестве примера (вы можете добавить TruncQuarter и т. д.):

class AlexaViewSet(viewsets.ModelViewSet):
    serializer_class = AlexaSerializer
    queryset = Alexa.objects.all()
    filter_fields = {'created_at': ['iexact', 'lte', 'gte']}
    http_method_names = ['get', 'post', 'head']

    GROUP_CASTING_MAP = {  # Used for outputing the reset datetime when grouping
        'day': Cast(TruncDate('created_at'), output_field=DateTimeField()),
        'month': Cast(TruncMonth('created_at'), output_field=DateTimeField()),
        'week': Cast(TruncWeek('created_at'), output_field=DateTimeField()),
        'year': Cast(TruncYear('created_at'), output_field=DateTimeField()),
    }

    GROUP_ANNOTATIONS_MAP = {  # Defines the fields used for grouping
        'day': {
            'day': TruncDay('created_at'),
            'month': TruncMonth('created_at'),
            'year': TruncYear('created_at'),
        },
        'week': {
            'week': TruncWeek('created_at')
        },
        'month': {
            'month': TruncMonth('created_at'),
            'year': TruncYear('created_at'),
        },
        'year': {
            'year': TruncYear('created_at'),
        },
    }

    def list(self, request, *args, **kwargs):
        group_by_field = request.GET.get('group_by', None)
        if group_by_field and group_by_field not in self.GROUP_CASTING_MAP.keys():  # validate possible values
            return Response(status=status.HTTP_400_BAD_REQUEST)

        queryset = self.filter_queryset(self.get_queryset())

        if group_by_field:
            queryset = queryset.annotate(**self.GROUP_ANNOTATIONS_MAP[group_by_field]) \
                .values(*self.GROUP_ANNOTATIONS_MAP[group_by_field]) \
                .annotate(rank=Avg('rank'), created_at=self.GROUP_CASTING_MAP[group_by_field]) \
                .values('rank', 'created_at')

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Для этих значений:

GET /alexa
[
    {
        "id": 1,
        "created_at": "2020-03-16T12:04:59.096098Z",
        "extra": "{}",
        "rank": 2
    },
    {
        "id": 2,
        "created_at": "2020-02-15T12:05:01.907920Z",
        "extra": "{}",
        "rank": 64
    },
    {
        "id": 3,
        "created_at": "2020-02-15T12:05:03.890150Z",
        "extra": "{}",
        "rank": 232
    },
    {
        "id": 4,
        "created_at": "2020-02-15T12:05:06.357748Z",
        "extra": "{}",
        "rank": 12
    }
]
GET /alexa/?group_by=day
[
    {
        "created_at": "2020-02-15T00:00:00Z",
        "extra": null,
        "rank": 102
    },
    {
        "created_at": "2020-03-16T00:00:00Z",
        "extra": null,
        "rank": 2
    }
]
GET /alexa/?group_by=week
[
    {
        "created_at": "2020-02-10T00:00:00Z",
        "extra": null,
        "rank": 102
    },
    {
        "created_at": "2020-03-16T00:00:00Z",
        "extra": null,
        "rank": 2
    }
]

GET /alexa/?group_by=month
[
    {
        "created_at": "2020-02-01T00:00:00Z",
        "extra": null,
        "rank": 102
    },
    {
        "created_at": "2020-03-01T00:00:00Z",
        "extra": null,
        "rank": 2
    }
]
GET /alexa/?group_by=year
[
    {
        "created_at": "2020-01-01T00:00:00Z",
        "extra": null,
        "rank": 77
    }
]
person Hernan    schedule 15.02.2020
comment
Спасибо за такой быстрый ответ, я понял ваш подход! Не могли бы вы помочь мне понять, почему эта ошибка? Я получил это, опробовав приведенный выше код. Я добавил свой сериализатор, если он уместен AttributeError at /api/alexa/ 'dict' object has no attribute 'extra' - person Shouvik Mitra; 17.02.2020
comment
Я рад, что ты смог это понять. Не могли бы вы дать мне более подробную информацию о вашей ошибке? Может быть, вся трассировка стека поможет. - person Hernan; 18.02.2020
comment
Насколько я полагаю, ошибка заключается в том, что мой сериализатор ожидает «дополнительный» атрибут в dict. Когда я исключаю его из своего сериализатора, используя exclude = ['extra'], все работает, как и ожидалось. Вот полная трассировка стека - person Shouvik Mitra; 18.02.2020