Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(610)

Unified Diff: components/ntp_snippets/category_rankers/click_based_category_ranker.cc

Issue 2585263002: [NTP::SectionOrder] First version of click based category ranker. (Closed)
Patch Set: tschumann@ & bauerb@ comments. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: components/ntp_snippets/category_rankers/click_based_category_ranker.cc
diff --git a/components/ntp_snippets/category_rankers/click_based_category_ranker.cc b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc
new file mode 100644
index 0000000000000000000000000000000000000000..45da64bd5bdf2a0c55db471f604eec4f1cef2e4f
--- /dev/null
+++ b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc
@@ -0,0 +1,236 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/ntp_snippets/category_rankers/click_based_category_ranker.h"
+
+#include <algorithm>
+
+#include "base/memory/ptr_util.h"
+#include "base/values.h"
+#include "components/ntp_snippets/category_rankers/constant_category_ranker.h"
+#include "components/ntp_snippets/pref_names.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+
+namespace ntp_snippets {
+
+namespace {
+
+// In order to increase stability and predictability of the order, an extra
+// level of "confidence" is required before moving a category upwards. In other
+// words, the category is moved not when it reaches the previous one, but rather
+// when it leads by some amount. We refer to this required extra "confidence" as
+// a passing margin. Each position has its own passing margin. The category is
+// moved upwards (i.e. passes another category) when it has at least passing
+// margin of the previous category position more clicks.
+const int kPassingMargin = 5;
+
+// The first categories get more attention and, therefore, here more stability
+// is needed. The passing margin of such categories is increased and they are
+// referred to as top categories (with extra margin). Only category position
+// defines whether a category is top, but not its content.
+const int kNumTopCategoriesWithExtraMargin = 3;
+
+// The increase of passing margin for each top category compared to the next
+// category (e.g. the first top category has passing margin larger by this value
+// than the second top category, the last top category has it larger by this
+// value than the first non-top category).
+const int kExtraPassingMargin = 2;
+
+const char kCategoryIdKey[] = "category";
+const char kClicksKey[] = "clicks";
+
+} // namespace
+
+ClickBasedCategoryRanker::ClickBasedCategoryRanker(PrefService* pref_service)
+ : pref_service_(pref_service) {
+ if (!ReadOrderFromPrefs(&ordered_categories_)) {
+ // TODO(crbug.com/676273): Handle adding new hardcoded KnownCategories to
+ // existing order from prefs. Currently such new category is completely
+ // ignored and may be never shown.
+ RestoreDefaultOrder();
+ }
+}
+
+ClickBasedCategoryRanker::~ClickBasedCategoryRanker() = default;
+
+bool ClickBasedCategoryRanker::Compare(Category left, Category right) const {
+ if (!ContainsCategory(left)) {
+ LOG(DFATAL) << "The category with ID " << left.id()
+ << " has not been added using AppendCategoryIfNecessary.";
+ }
+ if (!ContainsCategory(right)) {
+ LOG(DFATAL) << "The category with ID " << right.id()
+ << " has not been added using AppendCategoryIfNecessary.";
+ }
+ if (left == right) {
+ return false;
+ }
+ for (const RankedCategory& ranked_category : ordered_categories_) {
+ if (ranked_category.category == left) {
+ return true;
+ }
+ if (ranked_category.category == right) {
+ return false;
+ }
+ }
+ // This fallback is provided only to satisfy "Compare" contract if by mistake
+ // categories are not added using AppendCategoryIfNecessary. One should not
+ // rely on this, instead the order must be defined explicitly using
+ // AppendCategoryIfNecessary.
+ return left.id() < right.id();
+}
+
+void ClickBasedCategoryRanker::ClearHistory(base::Time begin, base::Time end) {
+ // TODO(crbug.com/675953): Preserve remote categories with 0 counts instead of
+ // removing them.
+ RestoreDefaultOrder();
+}
+
+void ClickBasedCategoryRanker::AppendCategoryIfNecessary(Category category) {
+ if (!ContainsCategory(category)) {
+ ordered_categories_.push_back(RankedCategory(category, /*clicks=*/0));
+ }
+}
+
+// TODO(vitaliii): Listen to dismissed sections and penalise them.
+void ClickBasedCategoryRanker::OnSuggestionOpened(Category category) {
+ if (!ContainsCategory(category)) {
+ LOG(DFATAL) << "The category with ID " << category.id()
+ << " has not been added using AppendCategoryIfNecessary.";
+ return;
+ }
+
+ std::vector<RankedCategory>::iterator current = FindCategory(category);
+ current->clicks++;
+
+ // TODO(crbug.com/676279): Prevent overflow.
+ if (current != ordered_categories_.begin()) {
+ std::vector<RankedCategory>::iterator previous = current - 1;
+ const int passing_margin = GetPositionPassingMargin(previous);
+ if (current->clicks >= previous->clicks + passing_margin) {
+ // It is intended to move only by one position at a time in order to avoid
+ // dramatic changes, which could confuse the user.
+ std::swap(*current, *previous);
+ }
+ }
+
+ StoreOrderToPrefs(ordered_categories_);
+}
+
+// static
+void ClickBasedCategoryRanker::RegisterProfilePrefs(
+ PrefRegistrySimple* registry) {
+ registry->RegisterListPref(prefs::kClickBasedCategoryRankerOrderWithClicks);
+}
+
+// static
+int ClickBasedCategoryRanker::GetPassingMargin() {
+ return kPassingMargin;
+}
+
+// static
+int ClickBasedCategoryRanker::GetNumTopCategoriesWithExtraMargin() {
+ return kNumTopCategoriesWithExtraMargin;
+}
+
+ClickBasedCategoryRanker::RankedCategory::RankedCategory(Category category,
+ int clicks)
+ : category(category), clicks(clicks) {}
+
+// Returns passing margin for a given position taking into account whether it is
+// a top category.
+int ClickBasedCategoryRanker::GetPositionPassingMargin(
+ std::vector<RankedCategory>::const_iterator category_position) const {
+ int index = category_position - ordered_categories_.cbegin();
+ int passing_margin_increase = 0;
+ if (index < kNumTopCategoriesWithExtraMargin) {
+ passing_margin_increase =
+ kExtraPassingMargin * (kNumTopCategoriesWithExtraMargin - index);
+ }
+ return kPassingMargin + passing_margin_increase;
+}
+
+void ClickBasedCategoryRanker::RestoreDefaultOrder() {
+ ordered_categories_.clear();
+
+ std::vector<KnownCategories> ordered_known_categories =
+ ConstantCategoryRanker::GetKnownCategoriesDefaultOrder();
+
+ for (KnownCategories known_category : ordered_known_categories) {
+ AppendKnownCategory(known_category);
+ }
+
+ StoreOrderToPrefs(ordered_categories_);
+}
+
+void ClickBasedCategoryRanker::AppendKnownCategory(
+ KnownCategories known_category) {
+ Category category = Category::FromKnownCategory(known_category);
+ DCHECK(!ContainsCategory(category));
+ ordered_categories_.push_back(RankedCategory(category, /*clicks=*/0));
+}
+
+bool ClickBasedCategoryRanker::ReadOrderFromPrefs(
+ std::vector<RankedCategory>* result_categories) {
+ result_categories->clear();
+ const base::ListValue* list =
+ pref_service_->GetList(prefs::kClickBasedCategoryRankerOrderWithClicks);
+ if (!list || list->GetSize() == 0) {
+ return false;
+ }
+
+ for (const std::unique_ptr<base::Value>& value : *list) {
+ base::DictionaryValue* dictionary;
+ if (!value->GetAsDictionary(&dictionary)) {
+ LOG(DFATAL) << "Failed to parse category data from prefs param "
+ << prefs::kClickBasedCategoryRankerOrderWithClicks
+ << " into dictionary.";
+ return false;
+ }
+ int category_id, clicks;
+ if (!dictionary->GetInteger(kCategoryIdKey, &category_id)) {
+ LOG(DFATAL) << "Dictionary does not have '" << kCategoryIdKey << "' key.";
+ return false;
+ }
+ if (!dictionary->GetInteger(kClicksKey, &clicks)) {
+ LOG(DFATAL) << "Dictionary does not have '" << kClicksKey << "' key.";
+ return false;
+ }
+ Category category = Category::FromIDValue(category_id);
+ result_categories->push_back(RankedCategory(category, clicks));
+ }
+ return true;
+}
+
+void ClickBasedCategoryRanker::StoreOrderToPrefs(
+ const std::vector<RankedCategory>& ordered_categories) {
+ base::ListValue list;
+ for (const RankedCategory& category : ordered_categories) {
+ auto dictionary = base::MakeUnique<base::DictionaryValue>();
+ dictionary->SetInteger(kCategoryIdKey, category.category.id());
+ dictionary->SetInteger(kClicksKey, category.clicks);
+ list.Append(std::move(dictionary));
+ }
+ pref_service_->Set(prefs::kClickBasedCategoryRankerOrderWithClicks, list);
+}
+
+std::vector<ClickBasedCategoryRanker::RankedCategory>::iterator
+ClickBasedCategoryRanker::FindCategory(Category category) {
+ return std::find_if(ordered_categories_.begin(), ordered_categories_.end(),
+ [category](const RankedCategory& ranked_category) {
+ return category == ranked_category.category;
+ });
+}
+
+bool ClickBasedCategoryRanker::ContainsCategory(Category category) const {
+ for (const auto& ranked_category : ordered_categories_) {
+ if (category == ranked_category.category) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace ntp_snippets

Powered by Google App Engine
This is Rietveld 408576698