Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --minimum-confidence parameter for vCA reward command #156

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
14 changes: 14 additions & 0 deletions catalyst-toolbox/src/bin/cli/rewards/veterans.rs
Expand Up @@ -62,6 +62,14 @@ pub struct VeteransRewards {
/// if the first cutoff is selected then the first modifier is used.
#[structopt(long, required = true)]
reputation_agreement_rate_modifiers: Vec<Decimal>,

/// Value in range [0.5, 1]
/// The minimum confidence for a vCA ranking to be excluded from eligible rankings when in disagreement from simple majority.
/// Confidence is either `#FO / #Rankings` or `(#Excellent + #Good) / #Rankings` depending on the final ranking.
/// Simple majority is 50%. Qualified majority is 70%. Using 70% avoids punishing vCAs where this confidence is not clear.
/// 70% is because when #vCA == 3 confidence is only 66% and thus in this case, where there is just 1 vote in disagreement, all 3 vCAs get rewarded.
#[structopt(long)]
minimum_confidence: Decimal,
}

impl VeteransRewards {
Expand All @@ -77,6 +85,7 @@ impl VeteransRewards {
rewards_agreement_rate_modifiers,
reputation_agreement_rate_cutoffs,
reputation_agreement_rate_modifiers,
minimum_confidence,
} = self;
let reviews: Vec<VeteranRankingRow> = csv::load_data_from_csv::<_, b','>(&from)?;

Expand All @@ -100,6 +109,10 @@ impl VeteransRewards {
bail!("Expected rewards_agreement_rate_cutoffs to be descending");
}

if minimum_confidence < Decimal::new(5, 1) || minimum_confidence > Decimal::ONE {
bail!("Expected minimum_confidence to range between .5 and 1");
}

let results = veterans::calculate_veteran_advisors_incentives(
&reviews,
total_rewards,
Expand All @@ -113,6 +126,7 @@ impl VeteransRewards {
.into_iter()
.zip(reputation_agreement_rate_modifiers.into_iter())
.collect(),
minimum_confidence,
);

csv::dump_data_to_csv(rewards_to_csv_data(results).iter(), &to).unwrap();
Expand Down
172 changes: 134 additions & 38 deletions catalyst-toolbox/src/rewards/veterans.rs
Expand Up @@ -16,6 +16,15 @@ pub struct VeteranAdvisorIncentive {
pub reputation: u64,
}

struct FinalRankingWithConfidence {
review_ranking: ReviewRanking,

/// This is to be used in conjunction with `ReviewRanking::is_positive()` to assess the
/// confidence of the boolean reply. It is either `#FO / #Rankings` or `(#Excellent + #Good) / #Rankings` depending on the final ranking.
/// For now we do not discriminate between Good and Excellent but this might change in the future.
confidence: Decimal,
}

pub type VcaRewards = HashMap<VeteranAdvisorId, VeteranAdvisorIncentive>;
pub type EligibilityThresholds = std::ops::RangeInclusive<usize>;

Expand All @@ -25,18 +34,38 @@ pub type EligibilityThresholds = std::ops::RangeInclusive<usize>;
// e.g. something like an expanded version of a AdvisorReviewRow
// [proposal_id, advisor, ratings, ..(other fields from AdvisorReviewRow).., ranking (good/excellent/filtered out), vca]

fn calc_final_ranking_per_review(rankings: &[impl Borrow<VeteranRankingRow>]) -> ReviewRanking {
fn calc_final_ranking_with_confidence_per_review(
rankings: &[impl Borrow<VeteranRankingRow>],
) -> FinalRankingWithConfidence {
let rankings_majority = Decimal::from(rankings.len()) / Decimal::from(2);
let ranks = rankings.iter().counts_by(|r| r.borrow().score());

match (ranks.get(&FilteredOut), ranks.get(&Excellent)) {
(Some(filtered_out), _) if Decimal::from(*filtered_out) >= rankings_majority => {
ReviewRanking::FilteredOut
match (
ranks.get(&Excellent),
ranks.get(&Good),
ranks.get(&FilteredOut),
) {
(_, _, Some(filtered_out)) if Decimal::from(*filtered_out) >= rankings_majority => {
FinalRankingWithConfidence {
review_ranking: FilteredOut,
confidence: Decimal::from(*filtered_out) / Decimal::from(rankings.len()),
}
}
(_, Some(excellent)) if Decimal::from(*excellent) > rankings_majority => {
ReviewRanking::Excellent
(Some(excellent), maybe_good, _) if Decimal::from(*excellent) > rankings_majority => {
FinalRankingWithConfidence {
review_ranking: Excellent,
confidence: (Decimal::from(maybe_good.copied().unwrap_or_default())
+ Decimal::from(*excellent))
/ Decimal::from(rankings.len()),
}
}
_ => ReviewRanking::Good,
(maybe_excellent, Some(good), _) => FinalRankingWithConfidence {
review_ranking: Good,
confidence: (Decimal::from(maybe_excellent.copied().unwrap_or_default())
+ Decimal::from(*good))
/ Decimal::from(rankings.len()),
},
_ => unreachable!(),
}
}

Expand Down Expand Up @@ -84,12 +113,18 @@ pub fn calculate_veteran_advisors_incentives(
reputation_thresholds: EligibilityThresholds,
rewards_mod_args: Vec<(Decimal, Decimal)>,
reputation_mod_args: Vec<(Decimal, Decimal)>,
minimum_confidence: Decimal,
) -> HashMap<VeteranAdvisorId, VeteranAdvisorIncentive> {
let final_rankings_per_review = veteran_rankings
let final_rankings_with_confidence_per_review = veteran_rankings
.iter()
.into_group_map_by(|ranking| ranking.review_id())
.into_iter()
.map(|(review, rankings)| (review, calc_final_ranking_per_review(&rankings)))
.map(|(review, rankings)| {
(
review,
calc_final_ranking_with_confidence_per_review(&rankings),
)
})
.collect::<BTreeMap<_, _>>();

let rankings_per_vca = veteran_rankings
Expand All @@ -99,11 +134,13 @@ pub fn calculate_veteran_advisors_incentives(
let eligible_rankings_per_vca = veteran_rankings
.iter()
.filter(|ranking| {
final_rankings_per_review
let final_ranking_with_confidence = final_rankings_with_confidence_per_review
.get(&ranking.review_id())
.unwrap()
.is_positive()
.unwrap();

final_ranking_with_confidence.review_ranking.is_positive()
== ranking.score().is_positive()
|| final_ranking_with_confidence.confidence < minimum_confidence
})
.counts_by(|ranking| ranking.vca.clone());

Expand Down Expand Up @@ -156,6 +193,8 @@ mod tests {
const VCA_1: &str = "vca1";
const VCA_2: &str = "vca2";
const VCA_3: &str = "vca3";
const SIMPLE_MAJORITY_CONFIDENCE: Decimal = dec!(.5);
const QUALIFIED_MAJORITY_CONFIDENCE: Decimal = dec!(.7);

struct RandomIterator;
impl Iterator for RandomIterator {
Expand Down Expand Up @@ -188,23 +227,36 @@ mod tests {
#[test]
fn final_ranking_is_correct() {
assert!(matches!(
calc_final_ranking_per_review(&gen_dummy_rankings("".into(), 5, 5, 5, RandomIterator),),
ReviewRanking::Good
calc_final_ranking_with_confidence_per_review(&gen_dummy_rankings("".into(), 5, 5, 5, RandomIterator)),
FinalRankingWithConfidence {
review_ranking: Good,
confidence
} if confidence == (dec!(10) / dec!(15))
));

assert!(matches!(
calc_final_ranking_per_review(&gen_dummy_rankings("".into(), 4, 2, 5, RandomIterator)),
ReviewRanking::Good
calc_final_ranking_with_confidence_per_review(&gen_dummy_rankings("".into(), 4, 2, 5, RandomIterator)),
FinalRankingWithConfidence {
review_ranking: Good,
confidence
} if confidence == (dec!(6) / dec!(11))

));

assert!(matches!(
calc_final_ranking_per_review(&gen_dummy_rankings("".into(), 4, 1, 5, RandomIterator)),
ReviewRanking::FilteredOut
calc_final_ranking_with_confidence_per_review(&gen_dummy_rankings("".into(), 4, 1, 5, RandomIterator)),
FinalRankingWithConfidence {
review_ranking: FilteredOut,
confidence,
} if confidence == (dec!(5) / dec!(10))
));

assert!(matches!(
calc_final_ranking_per_review(&gen_dummy_rankings("".into(), 3, 1, 1, RandomIterator)),
ReviewRanking::Excellent
calc_final_ranking_with_confidence_per_review(&gen_dummy_rankings("".into(), 3, 1, 1, RandomIterator)),
FinalRankingWithConfidence {
review_ranking: Excellent,
confidence,
} if confidence == (dec!(4) / dec!(5))
));
}

Expand All @@ -231,6 +283,7 @@ mod tests {
.into_iter()
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
.collect(),
SIMPLE_MAJORITY_CONFIDENCE,
);
assert!(results.get(VCA_1).is_none());
let res = results.get(VCA_2).unwrap();
Expand Down Expand Up @@ -260,6 +313,7 @@ mod tests {
.into_iter()
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
.collect(),
SIMPLE_MAJORITY_CONFIDENCE,
);
let res1 = results.get(VCA_1).unwrap();
assert_eq!(res1.reputation, 1);
Expand All @@ -283,21 +337,55 @@ mod tests {
(Rewards::new(8, 1), Rewards::ONE, Rewards::ONE),
(Rewards::new(9, 1), Rewards::new(125, 2), Rewards::ONE),
];
for (agreement, reward_modifier, reputation_modifier) in inputs {
for (vca3_agreement, reward_modifier, reputation_modifier) in inputs {
let rankings = (0..100)
.flat_map(|i| {
let vcas =
vec![VCA_1.to_owned(), VCA_2.to_owned(), VCA_3.to_owned()].into_iter();
let (good, filtered_out) = if Rewards::from(i) < agreement * Rewards::from(100)
{
(3, 0)
} else {
(2, 1)
};
let (good, filtered_out) =
if Rewards::from(i) < vca3_agreement * Rewards::from(100) {
(3, 0)
} else {
(2, 1)
};
gen_dummy_rankings(i.to_string(), 0, good, filtered_out, vcas).into_iter()
})
.collect::<Vec<_>>();
let results = calculate_veteran_advisors_incentives(
let results_simple_confidence = calculate_veteran_advisors_incentives(
&rankings,
total_rewards,
1..=200,
1..=200,
THRESHOLDS
.into_iter()
.zip(REWARDS_DISAGREEMENT_MODIFIERS.into_iter())
.collect(),
THRESHOLDS
.into_iter()
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
.collect(),
SIMPLE_MAJORITY_CONFIDENCE,
);
let vca3_expected_reward_portion_simple_confidence =
vca3_agreement * Rewards::from(100) * reward_modifier;
dbg!(vca3_expected_reward_portion_simple_confidence);
dbg!(vca3_agreement, reward_modifier, reputation_modifier);
let vca3_expected_rewards_simple_confidence = total_rewards
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_simple_confidence)
* vca3_expected_reward_portion_simple_confidence;
let res_vca3_simple_confidence = results_simple_confidence.get(VCA_3).unwrap();
assert_eq!(
res_vca3_simple_confidence.reputation,
(Rewards::from(100) * vca3_agreement * reputation_modifier)
.to_u64()
.unwrap()
);
assert!(are_close(
res_vca3_simple_confidence.rewards,
vca3_expected_rewards_simple_confidence
));

let results_qualified_confidence = calculate_veteran_advisors_incentives(
&rankings,
total_rewards,
1..=200,
Expand All @@ -310,21 +398,29 @@ mod tests {
.into_iter()
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
.collect(),
QUALIFIED_MAJORITY_CONFIDENCE,
);
let expected_reward_portion = agreement * Rewards::from(100) * reward_modifier;
dbg!(expected_reward_portion);
dbg!(agreement, reward_modifier, reputation_modifier);
let expected_rewards = total_rewards
/ (Rewards::from(125 * 2) + expected_reward_portion)
* expected_reward_portion;
let res = results.get(VCA_3).unwrap();

let vca3_expected_reward_portion_qualified_confidence = Rewards::from(100) * dec!(1.25); // low confidence so max reward modifier, agreement ratio doesn't count as all and rankings are all eligible
dbg!(vca3_expected_reward_portion_qualified_confidence);
dbg!(vca3_agreement, reward_modifier, reputation_modifier);

let vca3_expected_rewards_qualified_confidence = total_rewards
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_qualified_confidence)
* vca3_expected_reward_portion_qualified_confidence; // 1/3 of the reward

let res_vca3_qualified_confidence = results_qualified_confidence.get(VCA_3).unwrap();

assert_eq!(
res.reputation,
(Rewards::from(100) * agreement * reputation_modifier)
res_vca3_qualified_confidence.reputation,
(Rewards::from(100)) // all assessment are valid since confidence is low (2/3 < 0.7)
.to_u64()
.unwrap()
);
assert!(are_close(res.rewards, expected_rewards));
assert!(are_close(
res_vca3_qualified_confidence.rewards,
vca3_expected_rewards_qualified_confidence
));
}
}
}