the-algorithm/home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/scorer/FeedbackFatigueScorer.scala

145 lines
6.0 KiB
Scala

package com.twitter.home_mixer.functional_component.scorer
import com.twitter.conversions.DurationOps._
import com.twitter.home_mixer.model.HomeFeatures.AuthorIdFeature
import com.twitter.home_mixer.model.HomeFeatures.FeedbackHistoryFeature
import com.twitter.home_mixer.model.HomeFeatures.IsRetweetFeature
import com.twitter.home_mixer.model.HomeFeatures.SGSValidFollowedByUserIdsFeature
import com.twitter.home_mixer.model.HomeFeatures.SGSValidLikedByUserIdsFeature
import com.twitter.home_mixer.model.HomeFeatures.ScoreFeature
import com.twitter.home_mixer.util.CandidatesUtil
import com.twitter.product_mixer.component_library.model.candidate.TweetCandidate
import com.twitter.product_mixer.core.feature.Feature
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.feature.featuremap.FeatureMapBuilder
import com.twitter.product_mixer.core.functional_component.scorer.Scorer
import com.twitter.product_mixer.core.model.common.CandidateWithFeatures
import com.twitter.product_mixer.core.model.common.Conditionally
import com.twitter.product_mixer.core.model.common.identifier.ScorerIdentifier
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.stitch.Stitch
import com.twitter.timelines.common.{thriftscala => tl}
import com.twitter.timelineservice.model.FeedbackEntry
import com.twitter.timelineservice.{thriftscala => tls}
import com.twitter.util.Time
import scala.collection.mutable
object FeedbackFatigueScorer
extends Scorer[PipelineQuery, TweetCandidate]
with Conditionally[PipelineQuery] {
override val identifier: ScorerIdentifier = ScorerIdentifier("FeedbackFatigue")
override def features: Set[Feature[_, _]] = Set(ScoreFeature)
override def onlyIf(query: PipelineQuery): Boolean =
query.features.exists(_.getOrElse(FeedbackHistoryFeature, Seq.empty).nonEmpty)
val DurationForFiltering = 14.days
val DurationForDiscounting = 140.days
private val ScoreMultiplierLowerBound = 0.2
private val ScoreMultiplierUpperBound = 1.0
private val ScoreMultiplierIncrementsCount = 4
private val ScoreMultiplierIncrement =
(ScoreMultiplierUpperBound - ScoreMultiplierLowerBound) / ScoreMultiplierIncrementsCount
private val ScoreMultiplierIncrementDurationInDays =
DurationForDiscounting.inDays / ScoreMultiplierIncrementsCount.toDouble
override def apply(
query: PipelineQuery,
candidates: Seq[CandidateWithFeatures[TweetCandidate]]
): Stitch[Seq[FeatureMap]] = {
val feedbackEntriesByEngagementType =
query.features
.getOrElse(FeatureMap.empty).getOrElse(FeedbackHistoryFeature, Seq.empty)
.filter { entry =>
val timeSinceFeedback = query.queryTime.minus(entry.timestamp)
timeSinceFeedback < DurationForFiltering + DurationForDiscounting &&
entry.feedbackType == tls.FeedbackType.SeeFewer
}.groupBy(_.engagementType)
val authorsToDiscount =
getUserDiscounts(
query.queryTime,
feedbackEntriesByEngagementType.getOrElse(tls.FeedbackEngagementType.Tweet, Seq.empty))
val likersToDiscount =
getUserDiscounts(
query.queryTime,
feedbackEntriesByEngagementType.getOrElse(tls.FeedbackEngagementType.Like, Seq.empty))
val followersToDiscount =
getUserDiscounts(
query.queryTime,
feedbackEntriesByEngagementType.getOrElse(tls.FeedbackEngagementType.Follow, Seq.empty))
val retweetersToDiscount =
getUserDiscounts(
query.queryTime,
feedbackEntriesByEngagementType.getOrElse(tls.FeedbackEngagementType.Retweet, Seq.empty))
val featureMaps = candidates.map { candidate =>
val multiplier = getScoreMultiplier(
candidate,
authorsToDiscount,
likersToDiscount,
followersToDiscount,
retweetersToDiscount
)
val score = candidate.features.getOrElse(ScoreFeature, None)
FeatureMapBuilder().add(ScoreFeature, score.map(_ * multiplier)).build()
}
Stitch.value(featureMaps)
}
def getScoreMultiplier(
candidate: CandidateWithFeatures[TweetCandidate],
authorsToDiscount: Map[Long, Double],
likersToDiscount: Map[Long, Double],
followersToDiscount: Map[Long, Double],
retweetersToDiscount: Map[Long, Double],
): Double = {
val originalAuthorId =
CandidatesUtil.getOriginalAuthorId(candidate.features).getOrElse(0L)
val originalAuthorMultiplier = authorsToDiscount.getOrElse(originalAuthorId, 1.0)
val likers = candidate.features.getOrElse(SGSValidLikedByUserIdsFeature, Seq.empty)
val likerMultipliers = likers.flatMap(likersToDiscount.get)
val likerMultiplier =
if (likerMultipliers.nonEmpty && likers.size == likerMultipliers.size)
likerMultipliers.max
else 1.0
val followers = candidate.features.getOrElse(SGSValidFollowedByUserIdsFeature, Seq.empty)
val followerMultipliers = followers.flatMap(followersToDiscount.get)
val followerMultiplier =
if (followerMultipliers.nonEmpty && followers.size == followerMultipliers.size &&
likers.isEmpty)
followerMultipliers.max
else 1.0
val authorId = candidate.features.getOrElse(AuthorIdFeature, None).getOrElse(0L)
val retweeterMultiplier =
if (candidate.features.getOrElse(IsRetweetFeature, false))
retweetersToDiscount.getOrElse(authorId, 1.0)
else 1.0
originalAuthorMultiplier * likerMultiplier * followerMultiplier * retweeterMultiplier
}
def getUserDiscounts(
queryTime: Time,
feedbackEntries: Seq[FeedbackEntry],
): Map[Long, Double] = {
val userDiscounts = mutable.Map[Long, Double]()
feedbackEntries
.collect {
case FeedbackEntry(_, _, tl.FeedbackEntity.UserId(userId), timestamp, _) =>
val timeSinceFeedback = queryTime.minus(timestamp)
val timeSinceDiscounting = timeSinceFeedback - DurationForFiltering
val multiplier = ((timeSinceDiscounting.inDays / ScoreMultiplierIncrementDurationInDays)
* ScoreMultiplierIncrement + ScoreMultiplierLowerBound)
userDiscounts.update(userId, multiplier)
}
userDiscounts.toMap
}
}