Source code for rizemind.strategies.contribution.calculators.shapley_value
from collections.abc import Callable
from math import factorial
from eth_typing import ChecksumAddress
from rizemind.strategies.contribution.calculators.calculator import (
ContributionCalculator,
PlayerScore,
default_coalition_to_score,
)
from rizemind.strategies.contribution.shapley.trainer_mapping import ParticipantMapping
from rizemind.strategies.contribution.shapley.trainer_set import (
TrainerSetAggregate,
TrainerSetAggregateStore,
)
[docs]
class ShapleyValueCalculator(ContributionCalculator):
"""Calculates participant contributions using Shapley values.
Implements the Shapley value method from cooperative game theory to fairly
assess each participant's contribution. For each participant, it computes
their marginal contributions across all coalitions.
"""
[docs]
def get_scores(
self,
*,
participant_mapping: ParticipantMapping,
store: TrainerSetAggregateStore,
coalition_to_score_fn: Callable[[TrainerSetAggregate], float]
| None = default_coalition_to_score,
) -> dict[ChecksumAddress, PlayerScore]:
"""Calculates Shapley value-based contribution scores.
Computes the Shapley value for each participant by evaluating their
marginal contribution.
Args:
participant_mapping: Maps participants to their coalition memberships.
store: Contains metrics and losses for all evaluated coalitions.
coalition_to_score_fn: Function to convert a coalition's aggregate
to a numerical score. If None, uses default_coalition_to_score.
Returns:
A dictionary mapping each participant's address to their PlayerScore
containing their Shapley value. If a participant has no valid
marginal contributions (weight_total is 0), their score is 0.
"""
num_players = participant_mapping.get_total_participants()
player_scores: dict[ChecksumAddress, PlayerScore] = dict()
for player in participant_mapping.get_participants():
weighted_sum = 0.0
weight_total = 0.0
for set in store.get_sets():
if not participant_mapping.in_set(
player, set.id
): # Player is not in the coalition
in_set_id = participant_mapping.include_participants(
player, set.id
) # Add player to the coalition
if not store.is_available(set.id) or not store.is_available(
in_set_id
):
continue # skips if either of the coalitions are unavailable
not_in_set_aggregate = store.get_set(set.id)
to_score = (
default_coalition_to_score
if coalition_to_score_fn is None
else coalition_to_score_fn
)
not_in_set_score = to_score(not_in_set_aggregate)
in_set_aggregate = store.get_set(in_set_id)
in_set_score = to_score(in_set_aggregate)
marginal_contribution = in_set_score - not_in_set_score
s = set.size()
w = factorial(s) * factorial(
num_players - s - 1
) # Shapley weight numerator
weighted_sum += w * marginal_contribution
weight_total += w
score = (weighted_sum / weight_total) if weight_total > 0 else 0
# Renormalize by the total weight we actually used
player_scores[player] = PlayerScore(trainer_address=player, score=score)
return player_scores