In "How Not To Sort By Average Rating", Evan Miller explained in simple terms and demonstrated with compelling examples that the usual naïve methods of calculating the average rating are flawed, and that the maths for this problem has been worked out almost a century ago — (lower bound of) the Wilson score confidence interval:
$$ \left(\hat{p} + \frac{z_{α/2}^2}{2n}\pm z_{α/2}\sqrt{[\hat{p}(1-\hat{p}) + z_{α/2}^2/4n]/n}\right)/(1 + z_{α/2}^2/n) $$ (Use minus where it says plus/minus to calculate the lower bound.) Here \$\hat{p}\$ is the observed fraction of positive ratings, \$z_{α/2}\$ is the \$(1-α/2)\$ quantile of the standard normal distribution, and \$n\$ is the total number of ratings.
Take \$α\$ to be 0.05, i.e. assume 95% confidence interval; from the Standard normal table, the z value corresponding to \$1 - 0.05/2 = 0.97500\$ is 1.96.
Hence the hard-coded magic numbers 1.96
, 1.9208
, 0.9604
and 3.8416
, which represent \$z_{α/2}\$, \$z_{α/2}^2/2\$, \$z_{α/2}^2/4\$ and \$z_{α/2}^2\$, respectively, at \$α = 0.05\$.
I decided to put the concepts described therein to use on rating tags on the main site of a Stack Exchange subnetwork. I considered rating questions and answers (including the community wikis) separately, and defined a positive rating as an upvote and a negative rating as a downvote. I filtered tags with 0 votes to avoid division by 0.
This SEDE query is the result:
DECLARE @SQLString nvarchar(MAX);
DECLARE @SQLSubStringQ nvarchar(2000);
DECLARE @SQLSubStringA nvarchar(2000);
DECLARE @positive nvarchar(100) = 'CAST(SUM(CASE WHEN Votes.VoteTypeId = 2 THEN 1 ELSE 0 END) AS numeric(19, 9))';
DECLARE @negative nvarchar(100) = 'CAST(SUM(CASE WHEN Votes.VoteTypeId = 3 THEN 1 ELSE 0 END) AS numeric(19, 9))';
SET @SQLSubStringQ = 'SELECT ((' + @positive + ' + ' + '1.9208) / (' + @positive + ' + ' + @negative + ') -
1.96 * SQRT((' + @positive + ' * ' + @negative + ') / (' + @positive + ' + ' + @negative + ') + 0.9604) /
(' + @positive + ' + ' + @negative + ')) / (1 + 3.8416 / (' + @positive + ' + ' + @negative + '))
FROM Posts
INNER JOIN Votes
ON Posts.Id = Votes.PostId
WHERE Posts.Id = ANY (SELECT a.PostId FROM PostTags a WHERE a.TagId = PostTags.TagId)
HAVING ' + @positive + ' + ' + @negative + ' > 0';
SET @SQLSubStringA = 'SELECT ((' + @positive + ' + ' + '1.9208) / (' + @positive + ' + ' + @negative + ') -
1.96 * SQRT((' + @positive + ' * ' + @negative + ') / (' + @positive + ' + ' + @negative + ') + 0.9604) /
(' + @positive + ' + ' + @negative + ')) / (1 + 3.8416 / (' + @positive + ' + ' + @negative + '))
FROM Posts
INNER JOIN Votes
ON Posts.Id = Votes.PostId
WHERE Posts.ParentId = ANY (SELECT a.PostId FROM PostTags a WHERE a.TagId = PostTags.TagId)
HAVING ' + @positive + ' + ' + @negative + ' > 0';
SET @SQLString = CAST('' as nvarchar(MAX)) + -- Workaround: https://stackoverflow.com/questions/4833549/nvarcharmax-still-being-truncated/17785175#17785175
'SELECT Tags.TagName,
Tags.Count AS QuestionCount,
Sum(Posts.Score) AS NetQuestionScore,
(' + @SQLSubStringQ + ') AS QuestionRatings,
Sum(Posts.AnswerCount) AS AnswerCount,
(
SELECT COALESCE(SUM(a.Score), 0) FROM Posts a WHERE a.ParentId = ANY
(SELECT b.PostId FROM PostTags b WHERE b.TagId = PostTags.TagId)
) AS NetAnswerScore,
(' + @SQLSubStringA + ') AS AnswerRatings
FROM Tags
INNER JOIN PostTags
ON Tags.Id = PostTags.TagId
INNER JOIN Posts
ON PostTags.PostId = Posts.Id
AND Posts.PostTypeId IN (1, 2, 3)
WHERE (' + @SQLSubStringQ + ') IS NOT NULL OR (' + @SQLSubStringA + ') IS NOT NULL
GROUP BY PostTags.TagId, Tags.TagName, Tags.Count
ORDER BY Tags.TagName ASC';
EXECUTE(@SQLString);
But this not only looks abominable, it's SLOW.
- How can the readability be improved?
- Any way to boost the performance so that it can be run on larger SE sites?
- Any suggestion for a version 2?
EXECUTE
SQL code from a string it will be slow, that's the nature of the beast. I recommend against it unless it's absolutely necessary, \$\endgroup\$1.9208
,1.96
and3.8416
... \$\endgroup\$