Группировка по неделям для каждого года в django

Я нашел этот комментарий от @HenryHeikkinen очень полезным Как группировать по неделям в postgresql

Так что я исправил свой код. Но все равно не все в порядке. Я обнаружил несколько ситуаций, когда он работает неправильно. Когда год меняется в течение первой недели.

В моей модели данных хранится некоторое значение для каждого дня в году. Я хочу сгруппировать данные по неделям и сравнить несколько лет.

2018 год - это интересно 1.1.2018 - понедельник, поэтому первая неделя 2018 года начинается в этот день. последняя неделя 2018 года - 52 и начинается 24.12.2018

31.12.2018 - понедельник следующей недели, но это первая неделя 2019 года, и это нормально. Но при группировке по неделям результат для этой недели (неделя 1 2019 года) имеет дату недели = 31.12.2018, поэтому извлечение информации о годе из этого результата дает 2018 год вместо 2019. и таким образом получается дубликат для первой недели 2018 года.

Год 2019 имеет эту проблему в начале и в конце.

  • Неделя 1 2019 года начинается 31.12.2018
  • Неделя 1 2020 года начинается 30.12.2019

вот мой код django, который возвращает данные за неделю

self.dataday_set.annotate(ww=TruncWeek('date')) \
                            .values('ww') \
                            .annotate(consumption=Sum('consumption'), amount=Max('amount'), w=ExtractWeek('ww'), r=ExtractYear('ww')) \
                            .order_by('-ww')

вот раздел вывода между 2017 и 2018 годами, который в порядке

{'ww': datetime.date(2018, 1, 8), 'consumption': Decimal('42.39700'), 'amount': Decimal('12878.230'), 'w': 2, 'r': 2018}
{'ww': datetime.date(2018, 1, 1), 'consumption': Decimal('56.60000'), 'amount': Decimal('12835.833'), 'w': 1, 'r': 2018}
{'ww': datetime.date(2017, 12, 25), 'consumption': Decimal('45.95800'), 'amount': Decimal('12779.233'), 'w': 52, 'r': 2017}
{'ww': datetime.date(2017, 12, 18), 'consumption': Decimal('43.71600'), 'amount': Decimal('12733.275'), 'w': 51, 'r': 2017}

                        

раздел между 2018 и 2019 годами, и, как вы можете видеть, неделя 1 2019 года указана как неделя 2018 года

{'ww': datetime.date(2019, 1, 14), 'consumption': Decimal('40.03400'), 'amount': Decimal('14911.079'), 'w': 3, 'r': 2019}
{'ww': datetime.date(2019, 1, 7), 'consumption': Decimal('47.14400'), 'amount': Decimal('14871.045'), 'w': 2, 'r': 2019}
{'ww': datetime.date(2018, 12, 31), 'consumption': Decimal('52.35000'), 'amount': Decimal('14823.901'), 'w': 1, 'r': 2018}
{'ww': datetime.date(2018, 12, 24), 'consumption': Decimal('48.69800'), 'amount': Decimal('14771.551'), 'w': 52, 'r': 2018}
{'ww': datetime.date(2018, 12, 17), 'consumption': Decimal('45.07700'), 'amount': Decimal('14722.853'), 'w': 51, 'r': 2018}
{'ww': datetime.date(2018, 12, 10), 'consumption': Decimal('44.17100'), 'amount': Decimal('14677.776'), 'w': 50, 'r': 2018}

и раздел между 2019 и 2020, та же проблема

{'ww': datetime.date(2020, 1, 13), 'consumption': Decimal('39.31200'), 'amount': Decimal('16976.174'), 'w': 3, 'r': 2020}
{'ww': datetime.date(2020, 1, 6), 'consumption': Decimal('50.84500'), 'amount': Decimal('16936.862'), 'w': 2, 'r': 2020}
{'ww': datetime.date(2019, 12, 30), 'consumption': Decimal('51.67200'), 'amount': Decimal('16886.017'), 'w': 1, 'r': 2019}
{'ww': datetime.date(2019, 12, 23), 'consumption': Decimal('49.71200'), 'amount': Decimal('16834.345'), 'w': 52, 'r': 2019}
{'ww': datetime.date(2019, 12, 16), 'consumption': Decimal('43.23600'), 'amount': Decimal('16784.633'), 'w': 51, 'r': 2019}
{'ww': datetime.date(2019, 12, 9), 'consumption': Decimal('41.13100'), 'amount': Decimal('16741.397'), 'w': 50, 'r': 2019}

Я передаю этот набор запросов в pandas dataframe и создаю таблицу pivot из полей r w и consumption.

Спасибо за любой совет, как я могу решить это в django вместо того, чтобы создавать какой-то собственный код. Или может быть есть хорошее решение с помощью pandas, используя только ww и поле потребления для создания pivot таблицы, где у меня есть годы в строке и недели в столбцах

Этот вопрос или рассуждения описаны здесь: https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT

Номер недели года по стандарту ISO 8601. По определению, недели ISO начинаются по понедельникам, и первая неделя года содержит 4 января этого года. Другими словами, первый четверг года находится на первой неделе этого года.

В системе нумерации недель ISO возможно, что даты начала января входят в 52-ю неделю года. могут быть частью 52-й или 53-й недели предыдущего года, и для дат конца декабря - часть первой недели следующего года. Например, 2005-01-01 является частью 53-й недели 2004 года, а 2006-01-01 является частью 52-й недели 2005 года, а 2012-12-31 является часть первой недели 2013 года. Рекомендуется использовать поле isoyear вместе с полем week для получения последовательных результатов.

Я предлагаю вам рассчитать неделю следующим образом:

self.dataday_set\
    .values('date')\
    .annotate(
        consumption=Sum('consumption'),
        amount=Max('amount'),
        w=Greatest(Round(Extract('date', 'doy') / 7), 1),
        r=Extract('date', 'year')
    )\
    .order_by('-date')

Round() используется для округления до ближайшего целого числа (PostgreSQL округляет 0.5 до 1), Greatest() используется для корректировки на первую неделю года.

В целом, это все еще будет проблемой в определенных ситуациях. Например, 2022-01-03 будет неделей 1, а вы ожидаете неделю 2. Неделя 1 будет правильной, если учесть, что неделя начинается в понедельник. Но 2022-01-17 будет рассчитан неправильно.

Вы должны выбрать между неделей ISO 8601 или этим.

Я бы использовал ISO 8601, как предлагает PostgreSQL, и изменил бы код на:

self.dataday_set\
    .values('date')\
    .annotate(
        consumption=Sum('consumption'),
        amount=Max('amount'),
        w=Extract('date', 'week'),
        r=Extract('date', 'isoyear')
    )\
    .order_by('-date')

Я нашел решение и окончательный код django выглядит так

self.dataday_set.annotate(ww=TruncWeek('date')) \
                            .values('ww') \
                            .annotate(consumption=Sum('consumption'), amount=Max('amount'), w=ExtractWeek('ww'), r=ExtractIsoYear('ww')) \
                            .order_by('-ww')
Вернуться на верх