Skip to content

Commit

Permalink
Add framework for agent QRE estimation in Python.
Browse files Browse the repository at this point in the history
  • Loading branch information
tturocy committed Apr 11, 2024
1 parent de892c5 commit 4844289
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 15 deletions.
7 changes: 5 additions & 2 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions doc/pygambit.user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions src/pygambit/nash.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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
110 changes: 100 additions & 10 deletions src/pygambit/qre.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<LogitQREMixedStrategyFitResult(method={self.method},"
f"lam={self.lam},profile={self.profile})>"
)


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
----------
Expand All @@ -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
----------
Expand All @@ -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
Expand All @@ -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
-------
Expand All @@ -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
----------
Expand Down Expand Up @@ -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"<LogitQREMixedBehaviorFitResult(method={self.method},"
f"lam={self.lam},profile={self.profile})>"
)


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
)

0 comments on commit 4844289

Please sign in to comment.