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

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

Issue 2599293002: [NTP::SectionOrder] Implement click counts decay. (Closed)
Patch Set: ios factory. Created 3 years, 11 months 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
index 5b7ca83b02ef93fc436c1589512772024abebc82..fa6a28c7ad440245c9b7ac531faee2aec4ad7488 100644
--- a/components/ntp_snippets/category_rankers/click_based_category_ranker.cc
+++ b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc
@@ -38,6 +38,23 @@ const int kNumTopCategoriesWithExtraMargin = 3;
// value than the first non-top category).
const int kExtraPassingMargin = 2;
+// The ranker must "forget" history with time, so that changes in the user
+// behavior are reflected by the order in reasonable time. This is done using
+// click count decay with time. However, if there is not enough data, there is
+// no need in "forgetting" it. This value defines how many total clicks (across
+// categories) are considered enough to decay.
+const int kMinNumClicksToDecay = 30;
+
+// Time between two consecutive decays (assuming enough clicks).
+const base::TimeDelta kTimeBetweenDecays = base::TimeDelta::FromDays(1);
+
+// Decay factor as a fraction. The current value approximates the seventh root
+// of 0.5. This yields a 50% decay per seven decays. Seven weak decays are used
+// instead of one 50% decay in order to decrease difference of click weight in
+// time.
+const int kDecayFactorNumerator = 91;
+const int kDecayFactorDenominator = 100; // pow(0.91, 7) = 0.517
+
// Number of positions by which a dismissed category is downgraded.
const int kDismissedCategoryPenalty = 1;
@@ -46,14 +63,20 @@ const char kClicksKey[] = "clicks";
} // namespace
-ClickBasedCategoryRanker::ClickBasedCategoryRanker(PrefService* pref_service)
- : pref_service_(pref_service) {
+ClickBasedCategoryRanker::ClickBasedCategoryRanker(
+ PrefService* pref_service,
+ std::unique_ptr<base::Clock> clock)
+ : pref_service_(pref_service), clock_(std::move(clock)) {
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();
}
+
+ if (ReadLastDecayTimeFromPrefs() == base::Time::FromInternalValue(0)) {
+ StoreLastDecayTimeToPrefs(clock_->Now());
+ }
}
ClickBasedCategoryRanker::~ClickBasedCategoryRanker() = default;
@@ -86,6 +109,8 @@ bool ClickBasedCategoryRanker::Compare(Category left, Category right) const {
}
void ClickBasedCategoryRanker::ClearHistory(base::Time begin, base::Time end) {
+ StoreLastDecayTimeToPrefs(base::Time::FromInternalValue(0));
+
// The categories added through |AppendCategoryIfNecessary| cannot be
// completely removed, since no one is required to reregister them. Instead
// they are preserved in the default order (sorted by id).
@@ -128,15 +153,20 @@ void ClickBasedCategoryRanker::OnSuggestionOpened(Category category) {
return;
}
+ DecayClicksIfNeeded();
+
std::vector<RankedCategory>::iterator current = FindCategory(category);
+ DCHECK_GE(current->clicks, 0);
+ // The overflow is ignored. It is unlikely to happen, because of click count
+ // decay.
current->clicks++;
- // TODO(crbug.com/676279): Prevent overflow.
+ // Move the category up if appropriate.
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
+ // It is intended to move only by one position per click in order to avoid
// dramatic changes, which could confuse the user.
std::swap(*current, *previous);
}
@@ -172,10 +202,16 @@ void ClickBasedCategoryRanker::OnCategoryDismissed(Category category) {
StoreOrderToPrefs(ordered_categories_);
}
+base::Time ClickBasedCategoryRanker::GetLastDecayTime() const {
+ return ReadLastDecayTimeFromPrefs();
+}
+
// static
void ClickBasedCategoryRanker::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kClickBasedCategoryRankerOrderWithClicks);
+ registry->RegisterInt64Pref(prefs::kClickBasedCategoryRankerLastDecayTime,
+ /*default_value=*/0);
}
// static
@@ -231,7 +267,7 @@ void ClickBasedCategoryRanker::AppendKnownCategory(
}
bool ClickBasedCategoryRanker::ReadOrderFromPrefs(
- std::vector<RankedCategory>* result_categories) {
+ std::vector<RankedCategory>* result_categories) const {
result_categories->clear();
const base::ListValue* list =
pref_service_->GetList(prefs::kClickBasedCategoryRankerOrderWithClicks);
@@ -291,4 +327,59 @@ bool ClickBasedCategoryRanker::ContainsCategory(Category category) const {
return false;
}
+base::Time ClickBasedCategoryRanker::ReadLastDecayTimeFromPrefs() const {
+ return base::Time::FromInternalValue(
+ pref_service_->GetInt64(prefs::kClickBasedCategoryRankerLastDecayTime));
+}
+
+void ClickBasedCategoryRanker::StoreLastDecayTimeToPrefs(
+ base::Time last_decay_time) {
+ pref_service_->SetInt64(prefs::kClickBasedCategoryRankerLastDecayTime,
+ last_decay_time.ToInternalValue());
+}
+
+bool ClickBasedCategoryRanker::IsEnoughClicksToDecay() const {
+ int64_t num_clicks = 0;
+ for (const RankedCategory& ranked_category : ordered_categories_) {
+ num_clicks += ranked_category.clicks;
+ }
+ return num_clicks >= kMinNumClicksToDecay;
+}
+
+bool ClickBasedCategoryRanker::DecayClicksIfNeeded() {
+ base::Time now = clock_->Now();
+ base::Time last_decay = ReadLastDecayTimeFromPrefs();
+ if (last_decay == base::Time::FromInternalValue(0)) {
+ // No last decay time, start from now.
+ StoreLastDecayTimeToPrefs(clock_->Now());
+ return false;
+ }
+ DCHECK_LE(last_decay, now);
+
+ int num_pending_decays = (now - last_decay) / kTimeBetweenDecays;
+ int executed_decays = 0;
+ while (executed_decays < num_pending_decays && IsEnoughClicksToDecay()) {
+ for (RankedCategory& ranked_category : ordered_categories_) {
+ DCHECK_GE(ranked_category.clicks, 0);
+ const int64_t old_clicks = static_cast<int64_t>(ranked_category.clicks);
+ ranked_category.clicks =
+ old_clicks * kDecayFactorNumerator / kDecayFactorDenominator;
+ }
+
+ ++executed_decays;
+ }
+
+ // No matter how many decays were actually executed, all of them are marked
+ // done. Even if some were ignored due to absense of clicks, they would have
+ // no effect anyway for the same reason.
+ StoreLastDecayTimeToPrefs(last_decay +
+ num_pending_decays * kTimeBetweenDecays);
+
+ if (executed_decays > 0) {
+ StoreOrderToPrefs(ordered_categories_);
+ return true;
+ }
+ return false;
+}
+
} // namespace ntp_snippets

Powered by Google App Engine
This is Rietveld 408576698