Мотивация
У меня есть набор данных с результатами спортивных матчей, которые я хочу прокомментировать с результатами прошлых выступлений за период до каждого матча.
Я думаю об этом:
- Аннотируйте каждый
Match
набором совпадений, которые имели место в течение соответствующего периода времени (MatchManager.matchset_within_period
ниже).
- Аннотируйте каждый
Match
с агрегацией статистики по этому набору связанных совпадений (MatchManager.annotate_with_stats
ниже).
Я могу сделать это с несколько запутанным запросом (обрисованным в общих чертах ниже), включающим дополнительную модель Dataset
, по которой я перемещаюсь вверх и обратно, чтобы получить ссылку на полный набор совпадений, которые я могу затем отфильтруйте и агрегируйте.
Вопрос
Этот подход кажется действительно сложным и, вероятно, плохим для производительности. Определенно трудно следовать (или, по крайней мере, не интуитивно понятно) читателю.
Есть ли способ получить набор совпадений, который мне нужен для шага (1) напрямую, без дополнительной модели (например, используя директиву подзапроса на Match
)?
Пример использования
In [1]
test_matches = Match.objects.filter(...)
Match.objects \
.annotate_with_stats(for_days=300) \
.filter(id__in=test_matches) \
.values('pk', 'home_team_avg_score')
Out[1]
<MatchQuerySet [{'id': 287, 'home_team_avg_score': 91.04166666666667}, {'id': 288, 'home_team_avg_score': 91.21739130434783}, {'id': 289, 'home_team_avg_score': 92.45833333333333}]>
Упрощенный код
models.py (simplified)
class Team(models.Model):
name = models.CharField(max_length=255, unique=True)
# This model has no semantic meaning - it's purely for the query
class Dataset(models.Model):
name = models.CharField(max_length=255, unique=True)
class Season(models.Model):
dataset = models.ForeignKey(
Dataset, on_delete=models.CASCADE, related_name='seasons',
)
# ...
class Round(models.Model):
season = models.ForeignKey(
Season, on_delete=models.CASCADE, related_name='rounds',
)
# ...
class Match(models.Model):
round = models.ForeignKey(
Round, on_delete=models.CASCADE, related_name='matches',
)
home_team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='home_matches',
)
date = models.DateTimeField()
# ...
objects = MatchManager()
class TeamMatchStats(models.Model):
match = models.ForeignKey(
Match, on_delete=models.CASCADE, related_name='team_stats',
)
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_stats',
)
score = models.IntegerField()
# ...
managers.py (simplified)
def fm(x):
'''
Helper function for obtaining a self-referential matches set.
'''
if x.startswith('round'):
raise ValueError('Cannot re-traverse upwards')
return f'round__season__dataset__seasons__rounds__matches__{x}'
class MatchQuerySet(models.QuerySet):
def matchset_within_period(self, td):
# filter(date__lt): before this match
# annotate/filter(time_before__lte): within x period
return self \
.filter(**{fm('date__lt'): F('date')}) \
.annotate(
time_before=ExpressionWrapper(
F('date') - F(fm('date')),
output_field=DurationField(),
)
) \
.filter(time_before__lte=td) \
.values('pk')
def annotate_with_stats(self, for_days):
q_home_team = Q(**{fm('team_stats__team'): F('home_team')})
team_avg_params = {
'home_team_avg_score': Avg(
fm('team_stats__score'), filter=q_home_team,
)
} # In reality this is a dict comp getting a number of stats
return self \
.matchset_within_period(timedelta(days=for_days)) \
.annotate(**team_avg_params)
MatchManager = MatchQuerySet.as_manager