Provide a detailed explanation of Twitter's algorithm and its delivery mechanism.
I will provide a detailed explanation of my personal understanding of the Twitter algorithm code and include sources. However, different engineers may draw different conclusions from their analysis, and the Twitter algorithm may change at any time. Therefore, please approach the content of this article with a rational perspective and use it for reference only.
If you intend to republish this article, please provide proper attribution and include a link to the original source.
Twitter Tweet Weight Algorithm
Twitter's algorithm model includes a machine learning model called "Heavy Ranker," which retrieves and ranks the weight scores of tweets for the "For You" section on the homepage.
Below are the summarized weight values and upper limits for each type. Different functions have different upper limit values, and even if the quantity of a certain function is large, the gain will not be infinitely stacked.
category | score | limit |
---|---|---|
Like | +0.5 points | 100 points |
The user has liked the tweet. | ||
Retweeted | +1.0 points | 100 points |
The user has retweeted the tweet. | ||
Reply | +13.5 points | 100 points |
User's reply to a tweet. | ||
Access personal profile page. | +12 points | 1000000 points |
Follow a tweet to the author's page and show your support by liking or replying to their content. | ||
The video is being played. | +0.005 points | 100 points |
For tweets that contain videos, viewers are required to watch at least 50% of the content. | ||
Author's response | +75 points | 200 points |
The author of the tweet responded to a user's reply to the tweet. | ||
Reply to a reply | +11 points | 1000000 points |
Users can like or reply to replies to a tweet. | ||
View 2 minutes | +10 points | 1000000 points |
Users spend no less than 2 minutes view tweets. | ||
Negative reaction | -74 points | -1000 points |
Users have a negative reaction to the tweet (not interested in this tweet, block the author, hide the author). | ||
Reported | -369 points | -20000 points |
The user reported a tweet. Can you please provide me with more context on the situation? |
Code related to scores for various types. As the code for the upper limit is too lengthy, it is not included here but can be found in the source below.
scored_tweets_model_weight_fav: 0.5
scored_tweets_model_weight_retweet: 1.0
scored_tweets_model_weight_reply: 13.5
scored_tweets_model_weight_good_profile_click: 12.0
scored_tweets_model_weight_video_playback50: 0.005
scored_tweets_model_weight_reply_engaged_by_author: 75.0
scored_tweets_model_weight_good_click: 11.0
scored_tweets_model_weight_good_click_v2: 10.0
scored_tweets_model_weight_negative_feedback_v2: -74.0
scored_tweets_model_weight_report: -369.0
Algorithm for Calculating Total Score
score = sum_i { (weight of engagement i) * (probability of engagement i) }
Total Score = Weight * Number of Participations.
For example, let's say 10 users have liked a tweet, giving it a weight of +0.5 points. With 10 participants, the engagement score would be 0.5 * 10 = 5. Thus, the total weight would be 5 points.
Source of tweet algorithm and overall score algorithm: README.md
Source of Upper Limit: ScoredTweetsParam.scala
However, there is a widely circulated notion online that the weight of a "like" is 30, while that of a retweet is 20. I looked up the source code of this claim.
private def getLinearRankingParams: ThriftRankingParams = {
ThriftRankingParams(
`type` = Some(ThriftScoringFunctionType.Linear),
minScore = -1.0e100,
retweetCountParams = Some(ThriftLinearFeatureRankingParams(weight = 20.0)),
replyCountParams = Some(ThriftLinearFeatureRankingParams(weight = 1.0)),
reputationParams = Some(ThriftLinearFeatureRankingParams(weight = 0.2)),
luceneScoreParams = Some(ThriftLinearFeatureRankingParams(weight = 2.0)),
textScoreParams = Some(ThriftLinearFeatureRankingParams(weight = 0.18)),
urlParams = Some(ThriftLinearFeatureRankingParams(weight = 2.0)),
isReplyParams = Some(ThriftLinearFeatureRankingParams(weight = 1.0)),
favCountParams = Some(ThriftLinearFeatureRankingParams(weight = 30.0)),
langEnglishUIBoost = 0.5,
langEnglishTweetBoost = 0.2,
langDefaultBoost = 0.02,
unknownLanguageBoost = 0.05,
offensiveBoost = 0.1,
inTrustedCircleBoost = 3.0,
multipleHashtagsOrTrendsBoost = 0.6,
inDirectFollowBoost = 4.0,
tweetHasTrendBoost = 1.1,
selfTweetBoost = 2.0,
tweetHasImageUrlBoost = 2.0,
tweetHasVideoUrlBoost = 2.0,
useUserLanguageInfo = true,
ageDecayParams = Some(ThriftAgeDecayRankingParams(slope = 0.005, base = 1.0))
)
}
The code did set default values for the permission to like at 30 and to retweet at 20, but it has since been removed as it was deemed unnecessary and was not being used for ranking purposes.
Source: EarlybirdTensorflowBasedSimilarityEngine.scala
Advertising Weight Algorithm
Advertising: +10,000 weight points
object AdsCandidateGenerationScoreBoostFactor
extends FSBoundedParam[Double](
name = "ads_candidate_generation_score_boost_factor",
default = 10000.0,
min = 1.0,
max = 100000.0
)
Source:AdsParams.scala
Twitter Blue Weighting Algorithm
Subscribing to Twitter Blue provides additional weighting bonuses.
category | Score |
---|---|
Twitter Blue (followed) | 4x |
Accounts already followed. | |
Twitter Blue (unfollowed) | 2x |
Account that is not being followed. |
Code related to weight for Twitter Blue.
object BlueVerifiedAuthorInNetworkMultiplierParam
extends FSBoundedParam[Double](
name = "home_mixer_blue_verified_author_in_network_multiplier",
default = 4.0,
min = 0.0,
max = 100.0
)
object BlueVerifiedAuthorOutOfNetworkMultiplierParam
extends FSBoundedParam[Double](
name = "home_mixer_blue_verified_author_out_of_network_multiplier",
default = 2.0,
min = 0.0,
max = 100.0
)
Sources of Weighting for Twitter Blue:HomeGlobalParams.scala
Account Weight Algorithm
Calculate the weight value of an account based on its status.
Inactive accounts: 0 points
Verified accounts: 100 points
Unverified accounts: Score is calculated based on a combination of device, account information, and age, with a maximum of 55 points.
In addition, the ratio of followers to following will also be factored in to determine the final weight value.
Related Code
object UserMass {
private val currentTimestamp = Time.now.inMilliseconds
private val constantDivisionFactorGt_threshFriendsToFollowersRatioUMass = 5.0
private val threshAbsNumFriendsUMass = 500
private val threshFriendsToFollowersRatioUMass = 0.6
private val deviceWeightAdditive = 0.5
private val ageWeightAdditive = 0.2
private val restrictedWeightMultiplicative = 0.1
def getUserMass(combinedUser: CombinedUser): Option[UserMassInfo] = {
val user = Option(combinedUser.user)
val userId = user.map(_.id).getOrElse(0L)
val userExtended = Option(combinedUser.user_extended)
val age = user.map(_.created_at_msec).map(DateUtil.diffDays(_, currentTimestamp)).getOrElse(0)
val isRestricted = user.map(_.safety).exists(_.restricted)
val isSuspended = user.map(_.safety).exists(_.suspended)
val isVerified = user.map(_.safety).exists(_.verified)
val hasValidDevice = user.flatMap(u => Option(u.devices)).exists(_.isSetMessaging_devices)
val numFollowers = userExtended.flatMap(u => Option(u.followers)).map(_.toInt).getOrElse(0)
val numFollowings = userExtended.flatMap(u => Option(u.followings)).map(_.toInt).getOrElse(0)
if (userId == 0L || user.map(_.safety).exists(_.deactivated)) {
None
} else {
val mass =
if (isSuspended)
0
else if (isVerified)
100
else {
var score = deviceWeightAdditive * 0.1 +
(if (hasValidDevice) deviceWeightAdditive else 0)
val normalizedAge = if (age > 30) 1.0 else (1.0 min scala.math.log(1.0 + age / 15.0))
score *= normalizedAge
if (score < 0.01) score = 0.01
if (isRestricted) score *= restrictedWeightMultiplicative
score = (score min 1.0) max 0
score *= 100
score
}
val friendsToFollowersRatio = (1.0 + numFollowings) / (1.0 + numFollowers)
val adjustedMass =
if (numFollowings > threshAbsNumFriendsUMass &&
friendsToFollowersRatio > threshFriendsToFollowersRatioUMass) {
mass / scala.math.exp(
constantDivisionFactorGt_threshFriendsToFollowersRatioUMass *
(friendsToFollowersRatio - threshFriendsToFollowersRatioUMass)
)
} else {
mass
}
Some(UserMassInfo(userId, adjustedMass))
}
}
}
Here are some variable values defined:
- currentTimestamp: current timestamp (used to calculate the difference between account and current time).
- constantDivisionFactorGt_threshFriendsToFollowersRatioUMass: constant factor for the ratio of friends to followers (used to measure the proportion of a user's friends to followers).
- threshAbsNumFriendsUMass: minimum number of friends a user must have (used to calculate user weight).
- threshFriendsToFollowersRatioUMass: threshold for the ratio of friends to followers (minimum allowed value for the ratio of a user's friends to followers; exceeding this value can have negative consequences).
- deviceWeightAdditive: device weight.
- ageWeightAdditive: age weight (seems to be unused).
- restrictedWeightMultiplicative: weight for restricted users.
When calculating the score, the account status is first checked. If the account has been deactivated, the score is 0. If the account is verified, the score is 100. If the account is not verified, the score is calculated based on certain conditions.
if (isSuspended)
0
else if (isVerified)
100
else {
var score = deviceWeightAdditive * 0.1 +
(if (hasValidDevice) deviceWeightAdditive else 0)
val normalizedAge = if (age > 30) 1.0 else (1.0 min scala.math.log(1.0 + age / 15.0))
score *= normalizedAge
if (score < 0.01) score = 0.01
if (isRestricted) score *= restrictedWeightMultiplicative
score = (score min 1.0) max 0
score *= 100
score
}
In this code, it first checks whether a valid device is held and then adds the initial weight. However, I couldn't find any other code that checks the validity of the device in more detail. If the device is valid, it returns 0.5 points, and if it's invalid, it returns 0 points. The default value of deviceWeightAdditive
is 0.5, so according to the following algorithm, assuming the device is valid, the score would be 0.55 points (0.5 * 0.1 + 0.5).
deviceWeightAdditive * 0.1 +
(if (hasValidDevice) deviceWeightAdditive else 0)
After calculating based on age, if the age is greater than 30 years old, return 1 point. If the age is less than or equal to 30 years old, proceed to the next calculation by dividing the age by 15, adding 1, and then taking the natural logarithm.
The natural logarithm is the logarithm with base e, denoted as ln(x).
Assuming the age is 22 years old, ln(1 + 22/15) ≈ 0.9028677115420144.
If the natural logarithm is less than 1, return the natural logarithm. If the natural logarithm is greater than 1, return 1. This means the maximum value of normalizedAge will not exceed 1.
Finally, multiply the calculated result by the previously calculated score, which is 0.55 * 0.9028677115420144 = 0.49657724134810793.
val normalizedAge = if (age > 30) 1.0 else (1.0 min scala.math.log(1.0 + age / 15.0))
score *= normalizedAge
Some additional checks were made below. If the score is less than 0.01, it will return 0.01, but according to the above conditions, the score will not be lower than 0.01.
If the account is restricted, the total score will be multiplied by the restricted weight, which is 0.1.
If the score is greater than 1, it will be set to 1. If the score is less than 1, the current score will be returned. Finally, the score is multiplied by 100.
Assuming the account is not restricted, based on the above conditions, the score is 49.657724134810793.
if (score < 0.01) score = 0.01
if (isRestricted) score *= restrictedWeightMultiplicative
score = (score min 1.0) max 0
score *= 100
Next, the score will be calculated based on the account's number of following and followers. It is calculated by adding 1 to the sum of following and dividing by 1 plus the sum of followers. For example, assuming the account has 600 following and 450 followers, the ratio of following to followers would be (1 + 600) / (1 + 450) ≈ 1.3325942350332594.
val friendsToFollowersRatio = (1.0 + numFollowings) / (1.0 + numFollowers)
First, two conditions are evaluated: whether the number of followers is greater than the user's minimum follower count (500) and whether the ratio of followers to following is greater than the threshold (0.6). If these two conditions are not met, the previously calculated score is returned without further calculation.
If both conditions are met, the difference between the ratio of followers to following and the threshold is multiplied by the constant factor of followers and fans, resulting in 5 * (1.3325942350332594 - 0.6) = 3.6629711751662968. This result is then used to calculate the exponent using the exponential function.
The exponential function formula is: f(x) = a^x.
Using the calculated result, we can calculate the exponential function by computing e^3.6629711751662968 ≈ 2.565617039296528. This result is the final account weight score.
val adjustedMass =
if (numFollowings > threshAbsNumFriendsUMass &&
friendsToFollowersRatio > threshFriendsToFollowersRatioUMass) {
mass / scala.math.exp(
constantDivisionFactorGt_threshFriendsToFollowersRatioUMass *
(friendsToFollowersRatio - threshFriendsToFollowersRatioUMass)
)
} else {
mass
}
Source:UserMass.scala
Ranking weight algorithm
Category | Score |
---|---|
UI English, tweet not English | 0.3x |
While the interface language of Twitter is in English, the tweets posted on the platform can be written in any language. | |
Twitter English, UI not English | 0.7x |
The tweet that was posted is written in English, however, the language used on Twitter's interface is not English. | |
UI and language distinct. | 0.1x |
Although the interface language of Twitter is English, the content of tweets posted on the platform can be written in languages other than English. | |
Cannot recognize the language. | 0.01x |
The language used in this tweet is neither user-friendly nor matches the interface language. | |
UI English, tweet not English | 0.3x |
The interface language of Twitter is English, but the tweets posted on it may not be in English. | |
Images, videos, and news links | 1.0x |
Accumulate a weights based on the number of media. | |
External link | Hide |
If a link is a non-media link and lacks sufficient interaction weight, it will be hidden. |
Downgrade Attribute | |
---|---|
No text content | 1.0x |
There is no text in the content of the tweet. | |
Only link | 1.0x |
The tweet simply includes a link without any accompanying text. | |
Only name | 1.0x |
The tweet only has the name and no other content. |
As the code is too long, I have only extracted a part of it. The following code is not complete.
Here are defined the initial values for each type.
maxHitsPerUser has a default value of 3, which means the algorithm can push up to 3 tweets. If the account weight is greater than the value set by maxTweepcredForAntiGaming, the algorithm will push all tweets.
struct ThriftRankingParams {
30: optional double langEnglishUIBoost = 0.3
31: optional double langEnglishTweetBoost = 0.7
32: optional double langDefaultBoost = 0.1
43: optional double unknownLanguageBoost = 0.01
60: optional bool enableHitDemotion = 0
61: optional double noTextHitDemotion = 1.0
62: optional double urlOnlyHitDemotion = 1.0
63: optional double nameOnlyHitDemotion = 1.0
64: optional double separateTextAndNameHitDemotion = 1.0
65: optional double separateTextAndUrlHitDemotion = 1.0
102: optional double multipleHashtagsOrTrendsBoost = 1
108: optional double tweetHasImageUrlBoost = 1
109: optional double tweetHasVideoUrlBoost = 1
110: optional double tweetHasNewsUrlBoost = 1
}(persisted='true')
struct ThriftFacetRankingOptions {
35: optional i32 maxHitsPerUser = 3
36: optional i32 maxTweepcredForAntiGaming = 65
}(persisted='true')
If a tweet includes a picture, video, or news link, the count will increase by 1 from its original quantity.
Revised version: If a tweet includes a picture, video, or news link, the character count will increase by one from its original quantity.
if (data.hasImageUrl || data.hasVideoUrl) {
relevanceStats.setNumWithMedia(relevanceStats.getNumWithMedia() + 1);
}
if (data.hasNewsUrl) {
relevanceStats.setNumWithNews(relevanceStats.getNumWithNews() + 1);
}
If a tweet contains images, videos, or news links, it will increase the weight of the tweet. The calculation method is: total score * (1 * (number of images + 1)).
// Media/News url boosts.
if (data.hasImageUrl || data.hasVideoUrl) {
data.hasMedialUrlBoostApplied = true;
boostedScore *= params.tweetHasMediaUrlBoost;
}
if (data.hasNewsUrl) {
data.hasNewsUrlBoostApplied = true;
boostedScore *= params.tweetHasNewsUrlBoost;
}
The following code snippet demonstrates that images, videos, or news links receive an additional weightage, whereas the absence of text, presence of only links or names result in reduced weightage.
if (scoringData.tweetHasTrendsBoostApplied) {
boostDetails.add(Explanation.match(
(float) params.tweetHasTrendBoost, "[x] Tweet has trend boost"));
}
if (scoringData.hasMedialUrlBoostApplied) {
boostDetails.add(Explanation.match(
(float) params.tweetHasMediaUrlBoost, "[x] Media url boost"));
}
if (scoringData.hasNewsUrlBoostApplied) {
boostDetails.add(Explanation.match(
(float) params.tweetHasNewsUrlBoost, "[x] News url boost"));
}
boostDetails.add(Explanation.match(0.0f, "[FIELDS HIT] " + scoringData.hitFields));
if (scoringData.hasNoTextHitDemotionApplied) {
boostDetails.add(Explanation.match(
(float) params.noTextHitDemotion, "[x] No text hit demotion"));
}
if (scoringData.hasUrlOnlyHitDemotionApplied) {
boostDetails.add(Explanation.match(
(float) params.urlOnlyHitDemotion, "[x] URL only hit demotion"));
}
if (scoringData.hasNameOnlyHitDemotionApplied) {
boostDetails.add(Explanation.match(
(float) params.nameOnlyHitDemotion, "[x] Name only hit demotion"));
}
if (scoringData.hasSeparateTextAndNameHitDemotionApplied) {
boostDetails.add(Explanation.match((float) params.separateTextAndNameHitDemotion,
"[x] Separate text/name demotion"));
}
if (scoringData.hasSeparateTextAndUrlHitDemotionApplied) {
boostDetails.add(Explanation.match((float) params.separateTextAndUrlHitDemotion,
"[x] Separate text/url demotion"));
}
Source: ranking.thrift
Source: FeatureBasedScoringFunction.java
Tweet publishing time algorithm
The score of a tweet will decrease over time.
Related code:
struct ThriftAgeDecayRankingParams {
// the rate in which the score of older tweets decreases
1: optional double slope = 0.003
// the age, in minutes, where the age score of a tweet is half of the latest tweet
2: optional double halflife = 360.0
// the minimal age decay score a tweet will have
3: optional double base = 0.6
}(persisted='true')
Here, a time decay function is used with a decay rate of 0.003 and a decay half-life of 360 minutes. This decay function indicates that the score of a tweet will gradually decrease over time, with the rate of decrease depending on the time difference and decay rate. In this case, when a tweet's posting time is 500 minutes apart from the current time, the decay score of the tweet can be calculated using the following formula:
Decay Score = Base Score * exp(-Decay Rate * (Posting Time - Current Time) / Decay Half-life)
Source: ranking.thrift
Violations of Policy
If a tweet contains content that violates policy, its ranking will be lowered. This includes, but is not limited to, encouraging self-harm, hate speech, gratuitous violence, promoting violence, encouraging mob harassment, tweets related to a deceased or dying user, releasing private information, violating privacy rights, threatening exposure, violent sexual behavior, sexual harassment, violent threats, interfering with elections, false election information, hacking content, fraud, platform manipulation, false Ukrainian information, misleading information, and false medical information.
Related code
val reasonToPolicyInViolation: Map[Reason, PolicyInViolation] = Map(
AbuseEpisodic -> PolicyInViolation.AbusePolicyEpisodic,
AbuseEpisodicEncourageSelfHarm -> PolicyInViolation.AbusePolicyEpisodicEncourageSelfharm,
AbuseEpisodicHatefulConduct -> PolicyInViolation.AbusePolicyEpisodicHatefulConduct,
AbuseGratuitousGore -> PolicyInViolation.AbusePolicyGratuitousGore,
AbuseGlorificationOfViolence -> PolicyInViolation.AbusePolicyGlorificationofViolence,
AbuseMobHarassment -> PolicyInViolation.AbusePolicyEncourageMobHarassment,
AbuseMomentOfDeathOrDeceasedUser -> PolicyInViolation.AbusePolicyMomentofDeathDeceasedUser,
AbusePrivateInformation -> PolicyInViolation.AbusePolicyPrivateInformation,
AbuseRightToPrivacy -> PolicyInViolation.AbusePolicyRighttoPrivacy,
AbuseThreatToExpose -> PolicyInViolation.AbusePolicyThreattoExpose,
AbuseViolentSexualConduct -> PolicyInViolation.AbusePolicyViolentSexualConduct,
AbuseViolentThreatHatefulConduct -> PolicyInViolation.AbusePolicyViolentThreatsHatefulConduct,
AbuseViolentThreatOrBounty -> PolicyInViolation.AbusePolicyViolentThreatorBounty,
OneOff -> PolicyInViolation.OneOff,
VotingMisinformation -> PolicyInViolation.MisinformationVoting,
HackedMaterials -> PolicyInViolation.HackedMaterials,
Scams -> PolicyInViolation.Scam,
PlatformManipulation -> PolicyInViolation.PlatformManipulation,
MisinfoCivic -> PolicyInViolation.MisinformationCivic,
MisinfoCrisis -> PolicyInViolation.AbusePolicyUkraineCrisisMisinformation,
MisinfoGeneric -> PolicyInViolation.MisinformationGeneric,
MisinfoMedical -> PolicyInViolation.MisinformationMedical,
)
Source: PublicInterestRules.scala
Tweet Push Mechanism
Let's take "me" as an example. When I post a tweet, or interact with a certain post (like, reply, or share), the tweet will be pushed to my fans.
If my followers engage with a tweet, it will be pushed to their own followers. If there is no engagement, it won't be pushed any further.
Pink represents the relationship between blue followers, which are the followers of my followers.
If the followers of my followers, represented by the pink fans in the diagram, engage with this post, it will be shared with their own set of followers. The green lines indicate the connection between the pink fans.
If they persist in their interaction, they will continue to push each other outward in an ongoing cycle.