user_classifier.cc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. // Copyright 2016 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/ntp_snippets/user_classifier.h"
  5. #include <algorithm>
  6. #include <cfloat>
  7. #include <string>
  8. #include "base/cxx17_backports.h"
  9. #include "base/logging.h"
  10. #include "base/metrics/histogram_macros.h"
  11. #include "base/strings/string_number_conversions.h"
  12. #include "base/time/clock.h"
  13. #include "components/ntp_snippets/features.h"
  14. #include "components/ntp_snippets/pref_names.h"
  15. #include "components/ntp_snippets/time_serialization.h"
  16. #include "components/prefs/pref_registry_simple.h"
  17. #include "components/prefs/pref_service.h"
  18. #include "components/variations/variations_associated_data.h"
  19. namespace ntp_snippets {
  20. namespace {
  21. // The discount rate for computing the discounted-average metrics. Must be
  22. // strictly larger than 0 and strictly smaller than 1!
  23. const double kDiscountRatePerDay = 0.25;
  24. const char kDiscountRatePerDayParam[] = "user_classifier_discount_rate_per_day";
  25. // Never consider any larger interval than this (so that extreme situations such
  26. // as losing your phone or going for a long offline vacation do not skew the
  27. // average too much).
  28. // When everriding via variation parameters, it is better to use smaller values
  29. // than |kMaxHours| as this it the maximum value reported in the histograms.
  30. const double kMaxHours = 7 * 24;
  31. const char kMaxHoursParam[] = "user_classifier_max_hours";
  32. // Ignore events within |kMinHours| hours since the last event (|kMinHours| is
  33. // the length of the browsing session where subsequent events of the same type
  34. // do not count again).
  35. const double kMinHours = 0.5;
  36. const char kMinHoursParam[] = "user_classifier_min_hours";
  37. // Classification constants.
  38. const double kActiveConsumerClicksAtLeastOncePerHours = 96;
  39. const char kActiveConsumerClicksAtLeastOncePerHoursParam[] =
  40. "user_classifier_active_consumer_clicks_at_least_once_per_hours";
  41. // The previous value in production was 66, i.e. 2.75 days. The new value is a
  42. // shift in the direction we want (having more active users).
  43. const double kRareUserOpensNTPAtMostOncePerHours = 96;
  44. const char kRareUserOpensNTPAtMostOncePerHoursParam[] =
  45. "user_classifier_rare_user_opens_ntp_at_most_once_per_hours";
  46. // Histograms for logging the estimated average hours to next event.
  47. const char kHistogramAverageHoursToOpenNTP[] =
  48. "NewTabPage.UserClassifier.AverageHoursToOpenNTP";
  49. const char kHistogramAverageHoursToShowSuggestions[] =
  50. "NewTabPage.UserClassifier.AverageHoursToShowSuggestions";
  51. const char kHistogramAverageHoursToUseSuggestions[] =
  52. "NewTabPage.UserClassifier.AverageHoursToUseSuggestions";
  53. // The enum used for iteration.
  54. const UserClassifier::Metric kMetrics[] = {
  55. UserClassifier::Metric::NTP_OPENED,
  56. UserClassifier::Metric::SUGGESTIONS_SHOWN,
  57. UserClassifier::Metric::SUGGESTIONS_USED};
  58. // The summary of the prefs.
  59. const char* kMetricKeys[] = {
  60. prefs::kUserClassifierAverageNTPOpenedPerHour,
  61. prefs::kUserClassifierAverageSuggestionsShownPerHour,
  62. prefs::kUserClassifierAverageSuggestionsUsedPerHour};
  63. const char* kLastTimeKeys[] = {prefs::kUserClassifierLastTimeToOpenNTP,
  64. prefs::kUserClassifierLastTimeToShowSuggestions,
  65. prefs::kUserClassifierLastTimeToUseSuggestions};
  66. // Default lengths of the intervals for new users for the metrics.
  67. const double kInitialHoursBetweenEvents[] = {24, 48, 120};
  68. const char* kInitialHoursBetweenEventsParams[] = {
  69. "user_classifier_default_interval_ntp_opened",
  70. "user_classifier_default_interval_suggestions_shown",
  71. "user_classifier_default_interval_suggestions_used"};
  72. static_assert(std::size(kMetrics) ==
  73. static_cast<int>(UserClassifier::Metric::COUNT) &&
  74. std::size(kMetricKeys) ==
  75. static_cast<int>(UserClassifier::Metric::COUNT) &&
  76. std::size(kLastTimeKeys) ==
  77. static_cast<int>(UserClassifier::Metric::COUNT) &&
  78. std::size(kInitialHoursBetweenEvents) ==
  79. static_cast<int>(UserClassifier::Metric::COUNT) &&
  80. std::size(kInitialHoursBetweenEventsParams) ==
  81. static_cast<int>(UserClassifier::Metric::COUNT),
  82. "Fill in info for all metrics.");
  83. // Computes the discount rate.
  84. double GetDiscountRatePerHour() {
  85. double discount_rate_per_day = variations::GetVariationParamByFeatureAsDouble(
  86. kArticleSuggestionsFeature, kDiscountRatePerDayParam,
  87. kDiscountRatePerDay);
  88. // Check for illegal values.
  89. if (discount_rate_per_day <= 0 || discount_rate_per_day >= 1) {
  90. DLOG(WARNING) << "Illegal value " << discount_rate_per_day
  91. << " for the parameter " << kDiscountRatePerDayParam
  92. << " (must be strictly between 0 and 1; the default "
  93. << kDiscountRatePerDay << " is used, instead).";
  94. discount_rate_per_day = kDiscountRatePerDay;
  95. }
  96. // Compute discount_rate_per_hour such that
  97. // discount_rate_per_day = 1 - e^{-discount_rate_per_hour * 24}.
  98. return std::log(1.0 / (1.0 - discount_rate_per_day)) / 24.0;
  99. }
  100. double GetInitialHoursBetweenEvents(UserClassifier::Metric metric) {
  101. return variations::GetVariationParamByFeatureAsDouble(
  102. kArticleSuggestionsFeature,
  103. kInitialHoursBetweenEventsParams[static_cast<int>(metric)],
  104. kInitialHoursBetweenEvents[static_cast<int>(metric)]);
  105. }
  106. double GetMinHours() {
  107. return variations::GetVariationParamByFeatureAsDouble(
  108. kArticleSuggestionsFeature, kMinHoursParam, kMinHours);
  109. }
  110. double GetMaxHours() {
  111. return variations::GetVariationParamByFeatureAsDouble(
  112. kArticleSuggestionsFeature, kMaxHoursParam, kMaxHours);
  113. }
  114. // Returns the new value of the metric using its |old_value|, assuming
  115. // |hours_since_last_time| hours have passed since it was last discounted.
  116. double DiscountMetric(double old_value,
  117. double hours_since_last_time,
  118. double discount_rate_per_hour) {
  119. // Compute the new discounted average according to the formula
  120. // avg_events := e^{-discount_rate_per_hour * hours_since} * avg_events
  121. return std::exp(-discount_rate_per_hour * hours_since_last_time) * old_value;
  122. }
  123. // Compute the number of hours between two events for the given metric value
  124. // assuming the events were equally distributed.
  125. double GetEstimateHoursBetweenEvents(double metric_value,
  126. double discount_rate_per_hour,
  127. double min_hours,
  128. double max_hours) {
  129. // The computation below is well-defined only for |metric_value| > 1 (log of
  130. // negative value or division by zero). When |metric_value| -> 1, the estimate
  131. // below -> infinity, so max_hours is a natural result, here.
  132. if (metric_value <= 1) {
  133. return max_hours;
  134. }
  135. // This is the estimate with the assumption that last event happened right
  136. // now and the system is in the steady-state. Solve estimate_hours in the
  137. // steady-state equation:
  138. // metric_value = 1 + e^{-discount_rate * estimate_hours} * metric_value,
  139. // i.e.
  140. // -discount_rate * estimate_hours = log((metric_value - 1) / metric_value),
  141. // discount_rate * estimate_hours = log(metric_value / (metric_value - 1)),
  142. // estimate_hours = log(metric_value / (metric_value - 1)) / discount_rate.
  143. double estimate_hours =
  144. std::log(metric_value / (metric_value - 1)) / discount_rate_per_hour;
  145. return base::clamp(estimate_hours, min_hours, max_hours);
  146. }
  147. // The inverse of GetEstimateHoursBetweenEvents().
  148. double GetMetricValueForEstimateHoursBetweenEvents(
  149. double estimate_hours,
  150. double discount_rate_per_hour,
  151. double min_hours,
  152. double max_hours) {
  153. estimate_hours = base::clamp(estimate_hours, min_hours, max_hours);
  154. // Return |metric_value| such that GetEstimateHoursBetweenEvents for
  155. // |metric_value| returns |estimate_hours|. Thus, solve |metric_value| in
  156. // metric_value = 1 + e^{-discount_rate * estimate_hours} * metric_value,
  157. // i.e.
  158. // metric_value * (1 - e^{-discount_rate * estimate_hours}) = 1,
  159. // metric_value = 1 / (1 - e^{-discount_rate * estimate_hours}).
  160. return 1.0 / (1.0 - std::exp(-discount_rate_per_hour * estimate_hours));
  161. }
  162. } // namespace
  163. UserClassifier::UserClassifier(PrefService* pref_service, base::Clock* clock)
  164. : pref_service_(pref_service),
  165. clock_(clock),
  166. discount_rate_per_hour_(GetDiscountRatePerHour()),
  167. min_hours_(GetMinHours()),
  168. max_hours_(GetMaxHours()),
  169. active_consumer_clicks_at_least_once_per_hours_(
  170. variations::GetVariationParamByFeatureAsDouble(
  171. kArticleSuggestionsFeature,
  172. kActiveConsumerClicksAtLeastOncePerHoursParam,
  173. kActiveConsumerClicksAtLeastOncePerHours)),
  174. rare_user_opens_ntp_at_most_once_per_hours_(
  175. variations::GetVariationParamByFeatureAsDouble(
  176. kArticleSuggestionsFeature,
  177. kRareUserOpensNTPAtMostOncePerHoursParam,
  178. kRareUserOpensNTPAtMostOncePerHours)) {
  179. // The pref_service_ can be null in tests.
  180. if (!pref_service_) {
  181. return;
  182. }
  183. // TODO(jkrcal): Store the current discount rate per hour into prefs. If it
  184. // differs from the previous value, rescale the metric values so that the
  185. // expectation does not change abruptly!
  186. // Initialize the prefs storing the last time: the counter has just started!
  187. for (const Metric metric : kMetrics) {
  188. if (!HasLastTime(metric)) {
  189. SetLastTimeToNow(metric);
  190. }
  191. }
  192. }
  193. UserClassifier::~UserClassifier() = default;
  194. // static
  195. void UserClassifier::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  196. double discount_rate = GetDiscountRatePerHour();
  197. double min_hours = GetMinHours();
  198. double max_hours = GetMaxHours();
  199. for (Metric metric : kMetrics) {
  200. double default_metric_value = GetMetricValueForEstimateHoursBetweenEvents(
  201. GetInitialHoursBetweenEvents(metric), discount_rate, min_hours,
  202. max_hours);
  203. registry->RegisterDoublePref(kMetricKeys[static_cast<int>(metric)],
  204. default_metric_value);
  205. registry->RegisterInt64Pref(kLastTimeKeys[static_cast<int>(metric)], 0);
  206. }
  207. }
  208. void UserClassifier::OnEvent(Metric metric) {
  209. DCHECK_NE(metric, Metric::COUNT);
  210. double metric_value = UpdateMetricOnEvent(metric);
  211. double avg = GetEstimateHoursBetweenEvents(
  212. metric_value, discount_rate_per_hour_, min_hours_, max_hours_);
  213. // We use kMaxHours as the max value below as the maximum value for the
  214. // histograms must be constant.
  215. switch (metric) {
  216. case Metric::NTP_OPENED:
  217. UMA_HISTOGRAM_CUSTOM_COUNTS(kHistogramAverageHoursToOpenNTP, avg, 1,
  218. kMaxHours, 50);
  219. break;
  220. case Metric::SUGGESTIONS_SHOWN:
  221. UMA_HISTOGRAM_CUSTOM_COUNTS(kHistogramAverageHoursToShowSuggestions, avg,
  222. 1, kMaxHours, 50);
  223. break;
  224. case Metric::SUGGESTIONS_USED:
  225. UMA_HISTOGRAM_CUSTOM_COUNTS(kHistogramAverageHoursToUseSuggestions, avg,
  226. 1, kMaxHours, 50);
  227. break;
  228. case Metric::COUNT:
  229. NOTREACHED();
  230. break;
  231. }
  232. }
  233. double UserClassifier::GetEstimatedAvgTime(Metric metric) const {
  234. DCHECK_NE(metric, Metric::COUNT);
  235. double metric_value = GetUpToDateMetricValue(metric);
  236. return GetEstimateHoursBetweenEvents(metric_value, discount_rate_per_hour_,
  237. min_hours_, max_hours_);
  238. }
  239. UserClassifier::UserClass UserClassifier::GetUserClass() const {
  240. // The pref_service_ can be null in tests.
  241. if (!pref_service_) {
  242. return UserClass::ACTIVE_NTP_USER;
  243. }
  244. if (GetEstimatedAvgTime(Metric::NTP_OPENED) >=
  245. rare_user_opens_ntp_at_most_once_per_hours_) {
  246. return UserClass::RARE_NTP_USER;
  247. }
  248. if (GetEstimatedAvgTime(Metric::SUGGESTIONS_USED) <=
  249. active_consumer_clicks_at_least_once_per_hours_) {
  250. return UserClass::ACTIVE_SUGGESTIONS_CONSUMER;
  251. }
  252. return UserClass::ACTIVE_NTP_USER;
  253. }
  254. std::string UserClassifier::GetUserClassDescriptionForDebugging() const {
  255. switch (GetUserClass()) {
  256. case UserClass::RARE_NTP_USER:
  257. return "Rare user of the NTP";
  258. case UserClass::ACTIVE_NTP_USER:
  259. return "Active user of the NTP";
  260. case UserClass::ACTIVE_SUGGESTIONS_CONSUMER:
  261. return "Active consumer of NTP suggestions";
  262. }
  263. NOTREACHED();
  264. return std::string();
  265. }
  266. void UserClassifier::ClearClassificationForDebugging() {
  267. // The pref_service_ can be null in tests.
  268. if (!pref_service_) {
  269. return;
  270. }
  271. for (const Metric& metric : kMetrics) {
  272. ClearMetricValue(metric);
  273. SetLastTimeToNow(metric);
  274. }
  275. }
  276. double UserClassifier::UpdateMetricOnEvent(Metric metric) {
  277. // The pref_service_ can be null in tests.
  278. if (!pref_service_) {
  279. return 0;
  280. }
  281. double hours_since_last_time =
  282. std::min(max_hours_, GetHoursSinceLastTime(metric));
  283. // Ignore events within the same "browsing session".
  284. if (hours_since_last_time < min_hours_) {
  285. return GetUpToDateMetricValue(metric);
  286. }
  287. SetLastTimeToNow(metric);
  288. double metric_value = GetMetricValue(metric);
  289. // Add 1 to the discounted metric as the event has happened right now.
  290. double new_metric_value =
  291. 1 + DiscountMetric(metric_value, hours_since_last_time,
  292. discount_rate_per_hour_);
  293. SetMetricValue(metric, new_metric_value);
  294. return new_metric_value;
  295. }
  296. double UserClassifier::GetUpToDateMetricValue(Metric metric) const {
  297. // The pref_service_ can be null in tests.
  298. if (!pref_service_) {
  299. return 0;
  300. }
  301. double hours_since_last_time =
  302. std::min(max_hours_, GetHoursSinceLastTime(metric));
  303. double metric_value = GetMetricValue(metric);
  304. return DiscountMetric(metric_value, hours_since_last_time,
  305. discount_rate_per_hour_);
  306. }
  307. double UserClassifier::GetHoursSinceLastTime(Metric metric) const {
  308. if (!HasLastTime(metric)) {
  309. return 0;
  310. }
  311. const base::TimeDelta since_last_time =
  312. clock_->Now() - DeserializeTime(pref_service_->GetInt64(
  313. kLastTimeKeys[static_cast<int>(metric)]));
  314. return since_last_time / base::Hours(1);
  315. }
  316. bool UserClassifier::HasLastTime(Metric metric) const {
  317. return pref_service_->HasPrefPath(kLastTimeKeys[static_cast<int>(metric)]);
  318. }
  319. void UserClassifier::SetLastTimeToNow(Metric metric) {
  320. pref_service_->SetInt64(kLastTimeKeys[static_cast<int>(metric)],
  321. SerializeTime(clock_->Now()));
  322. }
  323. double UserClassifier::GetMetricValue(Metric metric) const {
  324. return pref_service_->GetDouble(kMetricKeys[static_cast<int>(metric)]);
  325. }
  326. void UserClassifier::SetMetricValue(Metric metric, double metric_value) {
  327. pref_service_->SetDouble(kMetricKeys[static_cast<int>(metric)], metric_value);
  328. }
  329. void UserClassifier::ClearMetricValue(Metric metric) {
  330. pref_service_->ClearPref(kMetricKeys[static_cast<int>(metric)]);
  331. }
  332. } // namespace ntp_snippets