后台
假设我们有一组 * 问题 ,以及一组 * 学生 * 回答了这些问题。答案已经过审查, 分数 * 已经在某个未知的范围内分配。
现在,我们需要将 scores 相对于每个 question 中的极值进行归一化。例如,如果 question 1 的最小 score 为4,最大 score 为12,则这些分数将分别归一化为0和1。之间的分数将线性插值(如Normalization to bring in the range of [0,1]中所述)。
然后,对于每个 * 学生 *,我们想知道所有 * 问题 * 组合的 * 标准化分数 * 的平均值。
最小示例
下面是一个 * 非常幼稚 * 的最小实现,只是为了说明我们想要实现的:
class Question(models.Model):
pass
class Student(models.Model):
def mean_normalized_score(self):
normalized_scores = []
for score in self.score_set.all():
normalized_scores.append(score.normalized_value())
return mean(normalized_scores) if normalized_scores else None
class Score(models.Model):
student = models.ForeignKey(to=Student, on_delete=models.CASCADE)
question = models.ForeignKey(to=Question, on_delete=models.CASCADE)
value = models.FloatField()
def normalized_value(self):
limits = Score.objects.filter(question=self.question).aggregate(
min=models.Min('value'), max=models.Max('value'))
return (self.value - limits['min']) / (limits['max'] - limits['min'])
这工作得很好,但是在数据库查询等方面效率很低。
目标
与上面的实现不同,我更愿意将数字处理工作转移到数据库上。
我所尝试的
例如,考虑以下两个用例:
1.列出所有Score
对象的normalized_value
1.列出所有Student
对象的mean_normalized_score
第一个用例可以在查询中使用window functions来覆盖,如下所示:
w_min = Window(expression=Min('value'), partition_by=[F('question')])
w_max = Window(expression=Max('value'), partition_by=[F('question')])
annotated_scores = Score.objects.annotate(
normalized_value=(F('value') - w_min) / (w_max - w_min))
这很好地工作了,因此不再需要示例中的Score.normalized_value()
方法。
现在,我想为第二个用例做一些类似的事情,用单个数据库查询替换Student.mean_normalized_score()
方法。
原始SQL可能看起来像这样(对于sqlite):
SELECT id, student_id, AVG(normalized_value) AS mean_normalized_score
FROM (
SELECT
myapp_score.*,
((myapp_score.value - MIN(myapp_score.value) OVER (PARTITION BY myapp_score.question_id)) / (MAX(myapp_score.value) OVER (PARTITION BY myapp_score.question_id) - MIN(myapp_score.value) OVER (PARTITION BY myapp_score.question_id)))
AS normalized_value
FROM myapp_score
)
GROUP BY student_id
我可以把它作为一个原始的Django查询,但是我还不能用Django的ORM来重现这个查询。
我尝试过在上面描述的annotated_scores
查询集上构建,使用Django的Subquery,annotate()
,aggregate()
,Prefetch
以及它们的组合,但我一定在某个地方犯了错误。
可能我得到的最接近的是这个:
subquery = Subquery(annotated_scores.values('normalized_value'))
Score.objects.values('student_id').annotate(mean=Avg(subquery))
但这是不正确的。
有人能给我指出正确的方向,而不诉诸原始查询吗?
2条答案
按热度按时间46qrfjad1#
我可能已经找到了一种使用子查询的方法。主要的是,至少从django来说,我们不能在聚合上使用窗口函数,所以这就是阻止计算归一化值的平均值的原因。我在行上添加了注解来解释我试图做的事情:
生成的SQL为:
uqcuzwp82#
正如@bdbd所提到的,从this Django issue判断,似乎还不可能注解窗口化的查询集(使用Django 3.2)。
作为一个临时的解决方案,我重构了@bdbd的优秀Subquery解决方案,如下所示。
注意:如果需要,我们可以在
aggregate_student_mean()
中的values()
上添加更多的Student
查找,例如student__name
。只要我们注意不要弄乱分组。现在,如果有可能注解窗口查询集,我们可以简单地用更简单的
Window
实现替换Subquery
行:编辑:Django 4.2现在支持过滤窗口查询集,但仍然不允许注解。