Skip to content

Commit

Permalink
ENH: support logit random coefficients
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffgortmaker committed Aug 23, 2022
1 parent 84ac65b commit 3b9d82f
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 5 deletions.
4 changes: 2 additions & 2 deletions pyblp/economies/economy.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ def __init__(
raise TypeError("rc_types must be None or a sequence.")
if len(rc_types) != self.K2:
raise ValueError(f"rc_types must be None or a sequence of length {self.K2}.")
if any(d not in {'linear', 'log'} for d in rc_types):
raise TypeError("rc_types must be None or a sequence of 'linear' or 'log' strings.")
if any(d not in {'linear', 'log', 'logit'} for d in rc_types):
raise TypeError("rc_types must be None or a sequence of 'linear', 'log', or 'logit' strings.")
self.rc_types = list(rc_types)

# validate the scale of epsilon
Expand Down
6 changes: 5 additions & 1 deletion pyblp/economies/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,11 @@ class Problem(ProblemEconomy):
- ``'linear'`` (default) - The random coefficient is as defined in :eq:`mu`.
- ``'log'`` - The random coefficient's column in :eq:`mu` is exponentiated before being pre-multiplied by
:math:`X_2`.
:math:`X_2`. It will take on values bounded from below by zero.
- ``'logit'`` - The random coefficient's column in :eq:`mu` is passed through the inverse logit function
before being pre-multiplied by :math:`X_2`. It will take on values bounded from below by zero and above by
one.
The list should have as many strings as there are columns in :math:`X_2`. Each string determines the type of the
random coefficient on the corresponding product characteristic in :math:`X_2`.
Expand Down
6 changes: 5 additions & 1 deletion pyblp/economies/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ class Simulation(Economy):
- ``'linear'`` (default) - The random coefficient is as defined in :eq:`mu`.
- ``'log'`` - The random coefficient's column in :eq:`mu` is exponentiated before being pre-multiplied by
:math:`X_2`.
:math:`X_2`. It will take on values bounded from below by zero.
- ``'logit'`` - The random coefficient's column in :eq:`mu` is passed through the inverse logit function
before being pre-multiplied by :math:`X_2`. It will take on values bounded from below by zero and above by
one.
The list should have as many strings as there are columns in :math:`X_2`. Each string determines the type of the
random coefficient on the corresponding product characteristic in :math:`X_2`.
Expand Down
30 changes: 30 additions & 0 deletions pyblp/markets/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Dict, Iterator, List, Optional, Tuple

import numpy as np
import scipy.special

from .. import exceptions, options
from ..configurations.iteration import ContractionResults, Iteration
Expand Down Expand Up @@ -167,6 +168,14 @@ def compute_random_coefficients(self, sigma: Optional[Array] = None, pi: Optiona
else:
assert len(coefficients.shape) == 3
coefficients[:, k] = np.exp(coefficients[:, k])
elif rc_type == 'logit':
if len(coefficients.shape) == 2:
coefficients[k] = scipy.special.expit(coefficients[k])
else:
assert len(coefficients.shape) == 3
coefficients[:, k] = scipy.special.expit(coefficients[:, k])
else:
assert rc_type == 'linear'

return coefficients

Expand All @@ -180,6 +189,10 @@ def compute_single_random_coefficient(self, k: int) -> Array:

if self.parameters.rc_types[k] == 'log':
coefficient = np.exp(coefficient)
elif self.parameters.rc_types[k] == 'logit':
coefficient = scipy.special.expit(coefficient)
else:
assert self.parameters.rc_types[k] == 'linear'

return coefficient

Expand Down Expand Up @@ -856,6 +869,12 @@ def compute_utility_derivatives_by_parameter_tangent(
v = parameter.get_agent_characteristic(self)
if parameter.get_rc_type(self) == 'log':
v = v * self.compute_single_random_coefficient(parameter.location[0]).T
elif parameter.get_rc_type(self) == 'logit':
rc = self.compute_single_random_coefficient(parameter.location[0]).T
v = v * rc * (1 - rc)
else:
assert parameter.get_rc_type(self) == 'linear'

tangent += X2_derivatives[:, [parameter.location[0]]] * v.T
else:
assert isinstance(parameter, (GammaParameter, RhoParameter))
Expand Down Expand Up @@ -939,6 +958,12 @@ def compute_probabilities_by_parameter_tangent(
v = parameter.get_agent_characteristic(self)
if parameter.get_rc_type(self) == 'log':
v = v * self.compute_single_random_coefficient(parameter.location[0]).T
elif parameter.get_rc_type(self) == 'logit':
rc = self.compute_single_random_coefficient(parameter.location[0]).T
v = v * rc * (1 - rc)
else:
assert parameter.get_rc_type(self) == 'linear'

probabilities_tangent = probabilities * v.T * (x - x.T @ probabilities)
else:
assert isinstance(parameter, GammaParameter)
Expand Down Expand Up @@ -970,6 +995,11 @@ def compute_probabilities_by_parameter_tangent(
v = parameter.get_agent_characteristic(self)
if parameter.get_rc_type(self) == 'log':
v = v * self.compute_single_random_coefficient(parameter.location[0]).T
elif parameter.get_rc_type(self) == 'logit':
rc = self.compute_single_random_coefficient(parameter.location[0]).T
v = v * rc * (1 - rc)
else:
assert parameter.get_rc_type(self) == 'linear'

# compute the tangent of conditional probabilities with respect to the parameter
vx = v.T * x
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def large_nested_blp_simulation() -> SimulationFixture:
xi_variance=0.00001,
omega_variance=0.00001,
correlation=0.9,
rc_types=['log', 'linear'],
rc_types=['log', 'logit'],
costs_type='log',
seed=0,
)
Expand Down

0 comments on commit 3b9d82f

Please sign in to comment.