Source code for socialchoicekit.elicitation_utils

import numpy as np
from preflibtools.instances import OrdinalInstance

from typing import Union, Callable

from socialchoicekit.profile_utils import ValuationProfile, IntegerValuationProfile

[docs]class Elicitor: """ The Elicitor class responds to queries by the elicitation algorithms. This class is the base class and hence is not meant to be instantiated. Parameters ---------- memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. One-indexed by default. """ def __init__( self, memoize: bool = True, zero_indexed: bool = False, ) -> None: self.elicitation_count = 0 self.memoize = memoize if memoize: self.memoized_values = {} self.index_fixer = 0 if zero_indexed else 1
[docs] def elicit( self, agent: int, alternative: int, ) -> float: """ Returns the agent's preference for the alternative. Parameters ---------- agent: int The agent's index. alternative: int The alternative's index. Returns ------- float The agent's preference for the alternative. """ agent += self.index_fixer alternative += self.index_fixer if self.memoize: memoized_value = self.memoized_values.get((agent, alternative)) if memoized_value is not None: return memoized_value self.elicitation_count += 1 elicited_value = self._elicit_impl(agent, alternative) if self.memoize: self.memoized_values[(agent, alternative)] = elicited_value return elicited_value
[docs] def elicit_multiple( self, agents: np.ndarray, alternatives: np.ndarray, ) -> np.ndarray: """ Given an agents array and an alternative array both of size N, returns an array of size N containing the elicited values. (The ith agent is elicited about the ith alternative.) Parameters ---------- agents: np.ndarray The agents array. Must contain only integers that correspond to a valid agent. alternatives: np.ndarray The alternatives array. Must contain only integers that correspond to a valid alternative. Returns ------- np.ndarray The elicited values. Size is the same as the size of the two input arrays. """ if agents.shape != alternatives.shape: raise ValueError("The two input arrays must have the same shape.") if not (np.issubdtype(agents.dtype, np.integer) and np.issubdtype(alternatives.dtype, np.integer)): raise ValueError("The input arrays must contain only integers.") ans = [] for agent, alternative in zip(agents, alternatives): ans.append(self.elicit(agent, alternative)) return np.array(ans)
def _elicit_impl( self, agent: int, alternative: int, ) -> float: # Override this method in the subclass raise NotImplementedError
[docs]class IntegerElicitor(Elicitor): """ This class is the base class for elicitors that elicit integer values Therefore, this is not meant to be instantiated. Parameters ---------- memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. One-indexed by default. """ def __init__( self, memoize: bool = True, zero_indexed: bool = False, ) -> None: super().__init__(memoize=memoize, zero_indexed=zero_indexed)
[docs] def elicit( self, agent: int, alternative: int, ) -> int: """ Returns the agent's preference for the alternative. Parameters ---------- agent: int The agent's index. alternative: int The alternative's index. Returns ------- int The agent's preference for the alternative. """ elicited_value: Union[int, float] = super().elicit(agent, alternative) if isinstance(elicited_value, float) and not elicited_value.is_integer(): raise ValueError("The elicited value must be an integer.") return int(elicited_value)
[docs] def elicit_multiple( self, agents: np.ndarray, alternatives: np.ndarray, ) -> np.ndarray: """ Given an agents array and an alternative array both of size N, returns an array of size N containing the elicited values. (The ith agent is elicited about the ith alternative.) Parameters ---------- agents: np.ndarray The agents array. Must contain only integers that correspond to a valid agent. alternatives: np.ndarray The alternatives array. Must contain only integers that correspond to a valid alternative. Returns ------- np.ndarray The elicited values. Size is the same as the size of the two input arrays. The array is of integer type. """ if agents.shape != alternatives.shape: raise ValueError("The two input arrays must have the same shape.") if not (np.issubdtype(agents.dtype, np.integer) and np.issubdtype(alternatives.dtype, np.integer)): raise ValueError("The input arrays must contain only integers.") ans = [] for agent, alternative in zip(agents, alternatives): ans.append(self.elicit(agent, alternative)) return np.array(ans, dtype=int)
def _elicit_impl( self, agent: int, alternative: int, ) -> float: # This method should return all ints as floats, which will be converted in the elicit method. # Override this method in the subclass raise NotImplementedError
[docs]class ValuationProfileElicitor(Elicitor): """ Responds to queries from a valuation profile that is fully pre-populated. Parameters ---------- valuation_profile: ValuationProfile This is the cardinal profile. A (N, M) array, where N is the number of agents and M is the number of alternatives. The element at (i, j) indicates the agent's preference for alternative j. If the agent finds an alternative unacceptable, the element would be np.nan. memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. """ def __init__( self, valuation_profile: ValuationProfile, memoize: bool = True, ) -> None: self.valuation_profile = valuation_profile super().__init__(memoize=memoize, zero_indexed=True) def _elicit_impl( self, agent: int, alternative: int, ) -> float: return self.valuation_profile[agent, alternative]
[docs]class IntegerValuationProfileElicitor(IntegerElicitor): """ Responds to queries from an integer valuation profile that is fully pre-populated. Parameters ---------- valuation_profile: IntegerValuationProfile This is the cardinal profile. A (N, M) array, where N is the number of agents and M is the number of alternatives. The element at (i, j) indicates the agent's preference for alternative j. memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. """ def __init__( self, valuation_profile: IntegerValuationProfile, memoize: bool = True, ) -> None: self.valuation_profile = valuation_profile super().__init__(memoize=memoize, zero_indexed=True) def _elicit_impl( self, agent: int, alternative: int, ) -> float: return float(self.valuation_profile[agent, alternative])
[docs]class SynchronousStdInElicitor(Elicitor): """ Responds to queries by reading each answer from the standard input synchronously. Outputs questions in English to the standard output. Parameters ---------- zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. One-indexed by default. memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. """ def __init__( self, preflib_instance: Union[OrdinalInstance, None] = None, memoize: bool = True, zero_indexed: bool = False, ) -> None: self.preflib_instance = preflib_instance super().__init__(memoize=memoize, zero_indexed=zero_indexed) def _elicit_impl( self, agent: int, alternative: int, ) -> float: agent_name = agent alternative_name = alternative if self.preflib_instance is not None: alternative_name = self.preflib_instance.alternatives_name[alternative] print(f"Agent {agent_name}, what is your preference for alternative {alternative_name}?") return float(input())
[docs]class IntegerSynchronousStdInElicitor(IntegerElicitor): """ Responds to queries by reading each answer from the standard input synchronously. Outputs questions in English to the standard output. Parameters ---------- zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. One-indexed by default. memoize: bool IF True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. """ def __init__( self, preflib_instance: Union[OrdinalInstance, None] = None, memoize: bool = True, zero_indexed: bool = False, ) -> None: self.preflib_instance = preflib_instance super().__init__(memoize=memoize, zero_indexed=zero_indexed) def _elicit_impl( self, agent: int, alternative: int, ) -> float: agent_name = agent alternative_name = alternative if self.preflib_instance is not None: alternative_name = self.preflib_instance.alternatives_name[alternative] print(f"Agent {agent_name}, what is your preference for alternative {alternative_name}?") return float(input())
[docs]class LambdaElicitor(Elicitor): """ Responds to queries by calling a user-provided function. Parameters ---------- elicitation_function: Callable[[int, int], float] A function that takes in the agent's index and the alternative's index and returns the agent's preference for the alternative. memoize: bool If True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. Zero-indexed by default. """ def __init__( self, elicitation_function: Callable[[int, int], float], memoize: bool = True, zero_indexed: bool = True, ) -> None: self.elicitation_function = elicitation_function super().__init__(memoize=memoize, zero_indexed=zero_indexed) def _elicit_impl( self, agent: int, alternative: int, ) -> float: return self.elicitation_function(agent, alternative)
[docs]class IntegerLambdaElicitor(IntegerElicitor): """ Responds to queries by calling a user-provided function. Parameters ---------- elicitation_function: Callable[[int, int], float] A function that takes in the agent's index and the alternative's index and returns the agent's preference for the alternative. This function should return an integer but in float form. memoize: bool If True, the elicitor will memoize the elicited values. If False, the elicitor may ask repeated questions. When the memoized value is referenced, the elicitation count will not change. True by default. zero_indexed : bool If True, the input of the elicit function will be zero-indexed. If False, the input will be one-indexed. Zero-indexed by default. """ def __init__( self, elicitation_function: Callable[[int, int], float], memoize: bool = True, zero_indexed: bool = True, ) -> None: self.elicitation_function = elicitation_function super().__init__(memoize=memoize, zero_indexed=zero_indexed) def _elicit_impl( self, agent: int, alternative: int, ) -> float: return self.elicitation_function(agent, alternative)