From 4844289ec3594269d19aae72febeaa6d2e1323d2 Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Thu, 11 Apr 2024 13:55:51 +0100 Subject: [PATCH] Add framework for agent QRE estimation in Python. --- doc/pygambit.api.rst | 7 ++- doc/pygambit.user.rst | 4 +- src/pygambit/gambit.pxd | 2 +- src/pygambit/nash.pxi | 57 +++++++++++++++++++++ src/pygambit/qre.py | 110 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 165 insertions(+), 15 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index e1a8a1710..bc98fc63e 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -296,6 +296,9 @@ Computation of quantal response equilibria .. autosummary:: :toctree: api/ - fit_empirical - fit_fixedpoint + fit_strategy_empirical + fit_strategy_fixedpoint LogitQREMixedStrategyFitResult + + fit_behavior_fixedpoint + LogitQREMixedBehaviorFitResult diff --git a/doc/pygambit.user.rst b/doc/pygambit.user.rst index 23574f6bc..7bf5be199 100644 --- a/doc/pygambit.user.rst +++ b/doc/pygambit.user.rst @@ -704,11 +704,11 @@ analysed in [McKPal95]_ using QRE. ) data = g.mixed_strategy_profile([[128*0.527, 128*(1-0.527)], [128*0.366, 128*(1-0.366)]]) -Estimation of QRE is done using :py:func:`.fit_fixedpoint`. +Estimation of QRE in the strategic form is done using :py:func:`.fit_strategy_fixedpoint`. .. ipython:: python - fit = gbt.qre.fit_fixedpoint(data) + fit = gbt.qre.fit_strategy_fixedpoint(data) The returned :py:class:`.LogitQREMixedStrategyFitResult` object contains the results of the estimation. diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 059fc8509..45dd0a1c9 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -463,7 +463,7 @@ cdef extern from "solvers/logit/efglogit.h": c_MixedBehaviorProfileDouble GetProfile() # except + doesn't compile double GetLambda() except + double GetLogLike() except + - int MixedBehaviorLength() except + + int BehaviorProfileLength() except + double getitem "operator[]"(int) except +IndexError c_List[c_MixedBehaviorProfileDouble] LogitBehaviorSolve(c_Game, diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi index e2fb9ed4c..f88b9757a 100644 --- a/src/pygambit/nash.pxi +++ b/src/pygambit/nash.pxi @@ -271,3 +271,60 @@ def logit_principal_branch(game: Game, first_step: float = .03, max_accel: float p.thisptr = copyitem_list_qrem(solns, i+1) ret.append(p) return ret + + +@cython.cclass +class LogitQREMixedBehaviorProfile: + thisptr = cython.declare(shared_ptr[c_LogitQREMixedBehaviorProfile]) + + def __init__(self, game=None): + if game is not None: + self.thisptr = make_shared[c_LogitQREMixedBehaviorProfile]( + cython.cast(Game, game).game + ) + + def __repr__(self): + return f"LogitQREMixedBehaviorProfile(lam={self.lam},profile={self.profile})" + + def __len__(self): + return deref(self.thisptr).BehaviorProfileLength() + + def __getitem__(self, int i): + return deref(self.thisptr).getitem(i+1) + + @property + def game(self) -> Game: + """The game on which this mixed strategy profile is defined.""" + g = Game() + g.game = deref(self.thisptr).GetGame() + return g + + @property + def lam(self) -> double: + """The value of the precision parameter.""" + return deref(self.thisptr).GetLambda() + + @property + def log_like(self) -> double: + """The log-likelihood of the data.""" + return deref(self.thisptr).GetLogLike() + + @property + def profile(self) -> MixedBehaviorProfileDouble: + """The mixed strategy profile.""" + profile = MixedBehaviorProfileDouble() + profile.profile = ( + make_shared[c_MixedBehaviorProfileDouble](deref(self.thisptr).GetProfile()) + ) + return profile + + +def _logit_behavior_estimate(profile: MixedBehaviorProfileDouble, + first_step: float = .03, + max_accel: float = 1.1) -> LogitQREMixedBehaviorProfile: + """Estimate QRE corresponding to mixed behavior profile using + maximum likelihood along the principal branch. + """ + ret = LogitQREMixedBehaviorProfile(profile.game) + ret.thisptr = LogitBehaviorEstimateHelper(profile.profile, first_step, max_accel) + return ret diff --git a/src/pygambit/qre.py b/src/pygambit/qre.py index e2b3573d4..656816c32 100644 --- a/src/pygambit/qre.py +++ b/src/pygambit/qre.py @@ -301,8 +301,8 @@ class LogitQREMixedStrategyFitResult: See Also -------- - fit_fixedpoint - fit_empirical + fit_strategy_fixedpoint + fit_strategy_empirical """ def __init__(self, data, method, lam, profile, log_like): self._data = data @@ -336,21 +336,23 @@ def log_like(self) -> float: """The log-likelihood of the data at the estimated QRE.""" return self._log_like - def __repr__(self): + def __repr__(self) -> str: return ( f"" ) -def fit_fixedpoint( +def fit_strategy_fixedpoint( data: libgbt.MixedStrategyProfileDouble ) -> LogitQREMixedStrategyFitResult: """Use maximum likelihood estimation to find the logit quantal response equilibrium on the principal branch for a strategic game which best fits empirical frequencies of play. [1]_ - .. versionadded:: 16.1.0 + .. versionchanged:: 16.2.0 + + Renamed from `fit_fixedpoint` to disambiguate from agent version Parameters ---------- @@ -368,8 +370,8 @@ def fit_fixedpoint( See Also -------- - fit_empirical : Estimate QRE by approximation of the correspondence - using independent decision problems. + fit_strategy_empirical : Estimate QRE by approximation of the correspondence + using independent decision problems. References ---------- @@ -383,7 +385,7 @@ def fit_fixedpoint( ) -def fit_empirical( +def fit_strategy_empirical( data: libgbt.MixedStrategyProfileDouble ) -> LogitQREMixedStrategyFitResult: """Use maximum likelihood estimation to estimate a quantal @@ -392,7 +394,9 @@ def fit_empirical( considerations of the QRE and approximates instead by a collection of independent decision problems. [1]_ - .. versionadded:: 16.1.0 + .. versionchanged:: 16.2.0 + + Renamed from `fit_empirical` to disambiguate from agent version Returns ------- @@ -402,7 +406,7 @@ def fit_empirical( See Also -------- - fit_fixedpoint : Estimate QRE precisely by computing the correspondence + fit_strategy_fixedpoint : Estimate QRE precisely by computing the correspondence References ---------- @@ -431,3 +435,89 @@ def log_like(lam: float) -> float: return LogitQREMixedStrategyFitResult( data, "empirical", res.x[0], do_logit(res.x[0]), -res.fun ) + + +class LogitQREMixedBehaviorFitResult: + """The result of fitting a QRE to a given probability distribution + over actions. + + See Also + -------- + fit_behavior_fixedpoint + """ + def __init__(self, data, method, lam, profile, log_like): + self._data = data + self._method = method + self._lam = lam + self._profile = profile + self._log_like = log_like + + @property + def method(self) -> str: + """The method used to estimate the QRE; either "fixedpoint" or "empirical".""" + return self._method + + @property + def data(self) -> libgbt.MixedBehaviorProfileDouble: + """The empirical actions frequencies used to estimate the QRE.""" + return self._data + + @property + def lam(self) -> float: + """The value of lambda corresponding to the QRE.""" + return self._lam + + @property + def profile(self) -> libgbt.MixedBehaviorProfileDouble: + """The mixed behavior profile corresponding to the QRE.""" + return self._profile + + @property + def log_like(self) -> float: + """The log-likelihood of the data at the estimated QRE.""" + return self._log_like + + def __repr__(self) -> str: + return ( + f"" + ) + + +def fit_behavior_fixedpoint( + data: libgbt.MixedBehaviorProfileDouble +) -> LogitQREMixedBehaviorFitResult: + """Use maximum likelihood estimation to find the logit quantal + response equilibrium on the principal branch for an extensive game + which best fits empirical frequencies of play. [1]_ + + .. versionadded:: 16.2.0 + + Parameters + ---------- + data : MixedBehaviorProfileDouble + The empirical distribution of play to which to fit the QRE. + To obtain the correct resulting log-likelihood, these should + be expressed as total counts of observations of each action + rather than probabilities. + + Returns + ------- + LogitQREMixedBehaviorFitResult + The result of the estimation represented as a + ``LogitQREMixedBehaviorFitResult`` object. + + See Also + -------- + fit_strategy_fixedpoint : Estimate QRE using the strategic representation + + References + ---------- + .. [1] Bland, J. R. and Turocy, T. L., 2023. Quantal response equilibrium + as a structural model for estimation: The missing manual. + SSRN working paper 4425515. + """ + res = libgbt.logit_behavior_estimate(data) + return LogitQREMixedBehaviorFitResult( + data, "fixedpoint", res.lam, res.profile, res.log_like + )