Repository: oppia/oppia

Issue:
Create a Top Rated Category in Library
Create a new category group in library which will show the top 4-5 rated explorations.
- [x] Have the gallery display the Top Rated category group - #1910 
- [x] Implement the [Wilson Score](http://www.goproblems.com/test/wilson/wilson.php?v1=0&v2=0&v3=3&v4=0&v5=0) - #1935 



Discussion:

Resolving patch:
diff --git a/core/domain/exp_services.py b/core/domain/exp_services.py
--- a/core/domain/exp_services.py
+++ b/core/domain/exp_services.py
@@ -25,6 +25,7 @@
 import copy
 import datetime
 import logging
+import math
 import os
 import pprint
 import StringIO
@@ -1382,6 +1383,26 @@ def get_average_rating_from_exp_summary(exp_summary):
     return 0
 
 
+def get_scaled_average_rating_from_exp_summary(exp_summary):
+    """Returns the lower bound wilson score of the document as a float. If
+    there are no ratings, it will return 0. The confidence of this result is
+    95%.
+    """
+    # The following is the number of ratings.
+    n = sum(exp_summary.ratings.values())
+    if n == 0:
+        return 0
+    average_rating = get_average_rating_from_exp_summary(exp_summary)
+    z = 1.9599639715843482
+    x = (average_rating - 1) / 4
+    # The following calculates the lower bound Wilson Score as documented
+    # http://www.goproblems.com/test/wilson/wilson.php?v1=0&v2=0&v3=0&v4=&v5=1
+    a = x + (z**2)/(2*n)
+    b = z * math.sqrt((x*(1-x))/n + (z**2)/(4*n**2))
+    wilson_score_lower_bound = (a - b)/(1 + z**2/n)
+    return 1 + 4 * wilson_score_lower_bound
+
+
 def get_search_rank_from_exp_summary(exp_summary):
     """Returns an integer determining the document's rank in search.
 
diff --git a/core/domain/summary_services.py b/core/domain/summary_services.py
--- a/core/domain/summary_services.py
+++ b/core/domain/summary_services.py
@@ -236,23 +236,16 @@ def get_top_rated_exploration_summary_dicts(language_codes):
         if exp_summary.language_code in language_codes and
         sum(exp_summary.ratings.values()) > 0]
 
-    average_ratings = {
-        exp_summary.id: exp_services.get_average_rating_from_exp_summary(
-            exp_summary)
+    lower_bound_wilson_scores = {
+        exp_summary.id:
+            exp_services.get_scaled_average_rating_from_exp_summary(
+                exp_summary)
         for exp_summary in filtered_exp_summaries
     }
 
-    # The following two calls to 'sorted' ensure that the return list is sorted
-    # by average rating, breaking ties by the number of ratings.
-
-    sorted_by_ratings_count_exp_summaries = sorted(
-        filtered_exp_summaries,
-        key=lambda exp_summary: sum(exp_summary.ratings.values()),
-        reverse=True)
-
     sorted_exp_summaries = sorted(
-        sorted_by_ratings_count_exp_summaries,
-        key=lambda exp_summary: average_ratings[exp_summary.id],
+        filtered_exp_summaries,
+        key=lambda exp_summary: lower_bound_wilson_scores[exp_summary.id],
         reverse=True)[:NUMBER_OF_TOP_RATED_EXPLORATIONS]
 
     return _get_displayable_exp_summary_dicts(sorted_exp_summaries)


Test patch:
diff --git a/core/domain/exp_services_test.py b/core/domain/exp_services_test.py
--- a/core/domain/exp_services_test.py
+++ b/core/domain/exp_services_test.py
@@ -1994,6 +1994,27 @@ def test_get_average_rating_from_exp_summary(self):
         self.assertEqual(
             exp_services.get_average_rating_from_exp_summary(exp), 3.5)
 
+    def test_get_lower_bound_wilson_rating_from_exp_summary(self):
+        self.save_new_valid_exploration(self.EXP_ID, self.owner_id)
+        exp = exp_services.get_exploration_summary_by_id(self.EXP_ID)
+
+        self.assertEqual(
+            exp_services.get_scaled_average_rating_from_exp_summary(exp), 0)
+
+        rating_services.assign_rating_to_exploration(
+            self.owner_id, self.EXP_ID, 5)
+        self.assertAlmostEqual(
+            exp_services.get_scaled_average_rating_from_exp_summary(exp),
+            1.8261731658956, places=4)
+
+        rating_services.assign_rating_to_exploration(
+            self.USER_ID_1, self.EXP_ID, 4)
+
+        exp = exp_services.get_exploration_summary_by_id(self.EXP_ID)
+        self.assertAlmostEqual(
+            exp_services.get_scaled_average_rating_from_exp_summary(exp),
+            2.056191454757, places=4)
+
 
     def test_get_search_rank(self):
         self.save_new_valid_exploration(self.EXP_ID, self.owner_id)
diff --git a/core/domain/summary_services_test.py b/core/domain/summary_services_test.py
--- a/core/domain/summary_services_test.py
+++ b/core/domain/summary_services_test.py
@@ -487,9 +487,9 @@ def test_at_most_eight_top_rated_explorations(self):
             'tags': [],
             'thumbnail_icon_url': '/images/subjects/Lightbulb.svg',
             'language_code': feconf.DEFAULT_LANGUAGE_CODE,
-            'id': self.EXP_ID_2,
+            'id': self.EXP_ID_3,
             'category': u'A category',
-            'ratings': {u'1': 0, u'3': 0, u'2': 0, u'5': 1, u'4': 0},
+            'ratings': {u'1': 0, u'3': 0, u'2': 0, u'5': 1, u'4': 1},
             'title': u'A title',
             'num_views': 0,
             'objective': u'An objective'
@@ -499,7 +499,7 @@ def test_at_most_eight_top_rated_explorations(self):
             expected_summary, top_rated_exploration_summaries[0])
 
         expected_ordering = [
-            self.EXP_ID_2, self.EXP_ID_3, self.EXP_ID_4, self.EXP_ID_5,
+            self.EXP_ID_3, self.EXP_ID_2, self.EXP_ID_5, self.EXP_ID_4,
             self.EXP_ID_6, self.EXP_ID_8, self.EXP_ID_7, self.EXP_ID_9]
 
         actual_ordering = [exploration['id'] for exploration in