"""Functions for inference under relative magnitudes with monotonicity restrictions."""
from typing import NamedTuple
import numpy as np
import scipy.optimize as opt
from ...arp_nuisance import compute_arp_nuisance_ci, compute_least_favorable_cv
from ...bounds import create_monotonicity_constraint_matrix
from ...delta.rm.rm import _create_relative_magnitudes_constraint_matrix
from ...numba import find_rows_with_post_period_values
from ...utils import basis_vector
class DeltaRMMResult(NamedTuple):
"""Container for relative magnitudes with monotonicity restriction identified set results.
Attributes
----------
id_lb : float
Lower bound of the identified set.
id_ub : float
Upper bound of the identified set.
"""
#: Lower bound of the identified set.
id_lb: float
#: Upper bound of the identified set.
id_ub: float
[docs]
def compute_conditional_cs_rmm(
betahat,
sigma,
num_pre_periods,
num_post_periods,
l_vec=None,
m_bar=0,
alpha=0.05,
hybrid_flag="LF",
hybrid_kappa=None,
return_length=False,
monotonicity_direction="increasing",
post_period_moments_only=True,
grid_points=1000,
grid_lb=None,
grid_ub=None,
seed=None,
):
r"""Compute conditional confidence set for :math:`\Delta^{RMI}(\bar{M})`.
Computes a confidence set for :math:`l'\tau_{post}` under the restriction that :math:`\delta`
lies in :math:`\Delta^{RMI}(\bar{M})`, which combines the relative magnitudes
restriction with a monotonicity constraint.
The combined restriction is defined in Section 2.4.4 of [2]_ as
.. math::
\Delta^{RMI}(\bar{M}) = \Delta^{RM}(\bar{M}) \cap \Delta^{I},
where :math:`\Delta^{I} = \{\delta: \delta_t \ge \delta_{t-1} \, \forall t\}` for
increasing monotonicity. This is useful when there is prior knowledge that
the trend is monotonic.
Since :math:`\Delta^{RMI}(\bar{M})` is a finite union of polyhedra, a valid
confidence set is constructed by taking the union of confidence sets for each
component polyhedron, following Lemma 2.2 of [2]_.
Parameters
----------
betahat : ndarray
Estimated event study coefficients.
sigma : ndarray
Covariance matrix of betahat.
num_pre_periods : int
Number of pre-treatment periods.
num_post_periods : int
Number of post-treatment periods.
l_vec : ndarray, optional
Vector defining parameter of interest. If None, defaults to first post-period.
m_bar : float, default=0
Relative magnitude parameter :math:`\bar{M}`.
alpha : float, default=0.05
Significance level.
hybrid_flag : {'LF', 'ARP'}, default='LF'
Type of hybrid test.
hybrid_kappa : float, optional
First-stage size for hybrid test. If None, defaults to alpha/10.
return_length : bool, default=False
If True, return only the length of the confidence interval.
monotonicity_direction : {'increasing', 'decreasing'}, default='increasing'
Direction of monotonicity restriction.
post_period_moments_only : bool, default=True
If True, use only post-period moments for ARP test.
grid_points : int, default=1000
Number of grid points for confidence interval search.
grid_lb : float, optional
Lower bound for grid search.
grid_ub : float, optional
Upper bound for grid search.
seed : int, optional
Random seed for reproducibility.
Returns
-------
dict or float
If return_length is False, returns dict with 'grid' and 'accept' arrays.
If return_length is True, returns the length of the confidence interval.
Notes
-----
The confidence set is constructed using the moment inequality approach from [1]_,
as described in Section 3 of [2]_. The intersection of :math:`\Delta^{RM}(\bar{M})`
and :math:`\Delta^{I}` forms a finite union of polyhedra (Section 2.4.5 of [2]_),
allowing the application of Lemma 2.2 for constructing a valid confidence set.
The monotonicity restriction can sharpen the identified set.
References
----------
.. [1] Andrews, I., Roth, J., & Pakes, A. (2021). Inference for linear
conditional moment inequalities. Review of Economic Studies.
.. [2] Rambachan, A., & Roth, J. (2023). A more credible approach to
parallel trends. Review of Economic Studies, 90(5), 2555-2591.
"""
if num_pre_periods < 2:
raise ValueError("Need at least 2 pre-periods for relative magnitudes restriction")
if l_vec is None:
l_vec = basis_vector(1, num_post_periods)
if hybrid_kappa is None:
hybrid_kappa = alpha / 10
if grid_lb is None or grid_ub is None:
sel = slice(num_pre_periods, num_pre_periods + num_post_periods)
sigma_post = sigma[sel, sel]
sd_theta = np.sqrt(l_vec.T @ sigma_post @ l_vec)
if num_pre_periods > 1:
pre_betas_with_zero = np.concatenate([betahat[:num_pre_periods], [0]])
maxpre = np.max(np.abs(np.diff(pre_betas_with_zero)))
else:
maxpre = np.abs(betahat[0])
gridoff = betahat[sel] @ l_vec
post_period_indices = np.arange(1, num_post_periods + 1)
gridhalf = (m_bar * post_period_indices @ np.abs(l_vec) * maxpre) + (20 * sd_theta)
if grid_ub is None:
grid_ub = gridoff + gridhalf
if grid_lb is None:
grid_lb = gridoff - gridhalf
min_s = -(num_pre_periods - 1)
s_values = list(range(min_s, 1))
all_accepts = np.zeros((grid_points, len(s_values) * 2))
col_idx = 0
for s in s_values:
# (+) restriction
cs_plus = _compute_conditional_cs_rmm_fixed_s(
s=s,
max_positive=True,
m_bar=m_bar,
betahat=betahat,
sigma=sigma,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
l_vec=l_vec,
alpha=alpha,
hybrid_flag=hybrid_flag,
hybrid_kappa=hybrid_kappa,
monotonicity_direction=monotonicity_direction,
post_period_moments_only=post_period_moments_only,
grid_points=grid_points,
grid_lb=grid_lb,
grid_ub=grid_ub,
seed=seed,
)
all_accepts[:, col_idx] = cs_plus["accept"]
col_idx += 1
# (-) restriction
cs_minus = _compute_conditional_cs_rmm_fixed_s(
s=s,
max_positive=False,
m_bar=m_bar,
betahat=betahat,
sigma=sigma,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
l_vec=l_vec,
alpha=alpha,
hybrid_flag=hybrid_flag,
hybrid_kappa=hybrid_kappa,
monotonicity_direction=monotonicity_direction,
post_period_moments_only=post_period_moments_only,
grid_points=grid_points,
grid_lb=grid_lb,
grid_ub=grid_ub,
seed=seed,
)
all_accepts[:, col_idx] = cs_minus["accept"]
col_idx += 1
# Take union: accept if any (s, sign) combination accepts
accept = np.any(all_accepts, axis=1).astype(float)
grid = np.linspace(grid_lb, grid_ub, grid_points)
if return_length:
grid_length = np.concatenate([[0], np.diff(grid) / 2, [0]])
grid_length = grid_length[:-1] + grid_length[1:]
return np.sum(accept * grid_length)
return {"grid": grid, "accept": accept}
[docs]
def compute_identified_set_rmm(
m_bar, true_beta, l_vec, num_pre_periods, num_post_periods, monotonicity_direction="increasing"
):
r"""Compute identified set for :math:`\Delta^{RMI}(\bar{M})`.
Computes the identified set for :math:`l'\tau_{post}` under the restriction
:math:`\delta \in \Delta^{RM}(\bar{M}) \cap \Delta^I`.
The identified set is the union of identified sets for each component polyhedron,
as stated in (7) of [2]_
.. math::
\mathcal{S}(\beta, \Delta^{RMI}(\bar{M})) = \bigcup_{s<0, \text{sign} \in \{+,-\}}
\mathcal{S}(\beta, \Delta^{RM}_{s, \text{sign}}(\bar{M}) \cap \Delta^I).
For each component, the bounds are determined by solving the linear programs
from Lemma 2.1 of [2]_.
Parameters
----------
m_bar : float
Relative magnitude parameter :math:`\bar{M}`.
true_beta : ndarray
True coefficient values (pre and post periods).
l_vec : ndarray
Vector defining parameter of interest.
num_pre_periods : int
Number of pre-treatment periods.
num_post_periods : int
Number of post-treatment periods.
monotonicity_direction : {'increasing', 'decreasing'}, default='increasing'
Direction of monotonicity restriction.
Returns
-------
DeltaRMMResult
Lower and upper bounds of the identified set.
References
----------
.. [1] Andrews, I., Roth, J., & Pakes, A. (2021). Inference for linear
conditional moment inequalities. Review of Economic Studies.
.. [2] Rambachan, A., & Roth, J. (2023). A more credible approach to
parallel trends. Review of Economic Studies, 90(5), 2555-2591.
"""
if num_pre_periods < 2:
raise ValueError("Need at least 2 pre-periods for relative magnitudes restriction")
min_s = -(num_pre_periods - 1)
s_values = list(range(min_s, 1))
all_bounds = []
for s in s_values:
# Compute for (+) restriction
bounds_plus = _compute_identified_set_rmm_fixed_s(
s=s,
m_bar=m_bar,
max_positive=True,
true_beta=true_beta,
l_vec=l_vec,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
monotonicity_direction=monotonicity_direction,
)
all_bounds.append(bounds_plus)
# Compute for (-) restriction
bounds_minus = _compute_identified_set_rmm_fixed_s(
s=s,
m_bar=m_bar,
max_positive=False,
true_beta=true_beta,
l_vec=l_vec,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
monotonicity_direction=monotonicity_direction,
)
all_bounds.append(bounds_minus)
id_lb = min(b.id_lb for b in all_bounds)
id_ub = max(b.id_ub for b in all_bounds)
return DeltaRMMResult(id_lb=id_lb, id_ub=id_ub)
def _create_relative_magnitudes_monotonicity_constraint_matrix(
num_pre_periods,
num_post_periods,
m_bar=1,
s=0,
max_positive=True,
drop_zero_period=True,
monotonicity_direction="increasing",
):
r"""Create constraint matrix for :math:`\Delta^{RM}_{s,sign}(\bar{M}) \cap \Delta^I`.
Creates matrix :math:`A` for the polyhedral restriction :math:`A\delta \le d`
representing the intersection of a relative magnitudes component polyhedron
and a monotonicity restriction.
The combined constraint set is
.. math::
\Delta^{RMI}_{s,sign}(\bar{M}) = \Delta^{RM}_{s,sign}(\bar{M}) \cap \Delta^{I}
where :math:`\Delta^{RM}_{s,sign}(\bar{M})` is a component of the relative
magnitudes restriction and :math:`\Delta^I` is the monotonicity restriction from
Section 2.4.4 of [2]_. Both are polyhedra, so their intersection is also a
polyhedron.
Parameters
----------
num_pre_periods : int
Number of pre-treatment periods.
num_post_periods : int
Number of post-treatment periods.
m_bar : float, default=1
Relative magnitude parameter :math:`\bar{M}`.
s : int, default=0
Reference period for relative magnitudes restriction.
max_positive : bool, default=True
If True, uses (+) restriction; if False, uses (-) restriction.
drop_zero_period : bool, default=True
If True, drops period t=0 from the constraint matrix.
monotonicity_direction : {'increasing', 'decreasing'}, default='increasing'
Direction of monotonicity restriction.
Returns
-------
ndarray
Constraint matrix A for the combined restriction.
References
----------
.. [1] Andrews, I., Roth, J., & Pakes, A. (2021). Inference for linear
conditional moment inequalities. Review of Economic Studies.
.. [2] Rambachan, A., & Roth, J. (2023). A more credible approach to
parallel trends. Review of Economic Studies, 90(5), 2555-2591.
"""
A_rm = _create_relative_magnitudes_constraint_matrix(
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
m_bar=m_bar,
s=s,
max_positive=max_positive,
drop_zero_period=drop_zero_period,
)
A_m = create_monotonicity_constraint_matrix(
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
monotonicity_direction=monotonicity_direction,
post_period_moments_only=False,
)
return np.vstack([A_rm, A_m])
def _compute_identified_set_rmm_fixed_s(
s,
m_bar,
max_positive,
true_beta,
l_vec,
num_pre_periods,
num_post_periods,
monotonicity_direction="increasing",
):
r"""Compute identified set for a component of :math:`\Delta^{RMI}(\bar{M})` at fixed s.
Helper function that solves the linear programs from Lemma 2.1 of [2]_ for a
specific polyhedron :math:`\Delta^{RM}_{s,\text{sign}}(\bar{M}) \cap \Delta^I`.
Parameters
----------
s : int
Reference period for relative magnitudes restriction.
m_bar : float
Relative magnitude parameter :math:`\bar{M}`.
max_positive : bool
If True, uses (+) restriction; if False, uses (-) restriction.
true_beta : ndarray
True coefficient values (pre and post periods).
l_vec : ndarray
Vector defining parameter of interest.
num_pre_periods : int
Number of pre-treatment periods.
num_post_periods : int
Number of post-treatment periods.
monotonicity_direction : {'increasing', 'decreasing'}, default='increasing'
Direction of monotonicity restriction.
Returns
-------
DeltaRMMResult
Lower and upper bounds of the identified set for the component polyhedron.
References
----------
.. [1] Andrews, I., Roth, J., & Pakes, A. (2021). Inference for linear
conditional moment inequalities. Review of Economic Studies.
.. [2] Rambachan, A., & Roth, J. (2023). A more credible approach to
parallel trends. Review of Economic Studies, 90(5), 2555-2591.
"""
# Objective: min/max l'*delta_post
f_delta = np.concatenate([np.zeros(num_pre_periods), l_vec])
A_rmm = _create_relative_magnitudes_monotonicity_constraint_matrix(
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
m_bar=m_bar,
s=s,
max_positive=max_positive,
monotonicity_direction=monotonicity_direction,
)
d_rmm = _create_relative_magnitudes_monotonicity_constraint_vector(A_rmm)
pre_period_equality = np.hstack([np.eye(num_pre_periods), np.zeros((num_pre_periods, num_post_periods))])
A_ineq = A_rmm
b_ineq = d_rmm
A_eq = pre_period_equality
b_eq = true_beta[:num_pre_periods]
# Bounds: all variables unconstrained
bounds = [(None, None) for _ in range(num_pre_periods + num_post_periods)]
if b_ineq.ndim != 1:
b_ineq = b_ineq.flatten()
if b_eq.ndim != 1:
b_eq = b_eq.flatten()
result_max = opt.linprog(
c=-f_delta,
A_ub=A_ineq,
b_ub=b_ineq,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds,
method="highs",
)
result_min = opt.linprog(
c=f_delta,
A_ub=A_ineq,
b_ub=b_ineq,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds,
method="highs",
)
if not result_max.success or not result_min.success:
observed_val = l_vec.flatten() @ true_beta[num_pre_periods:]
return DeltaRMMResult(id_lb=observed_val, id_ub=observed_val)
id_ub = l_vec.flatten() @ true_beta[num_pre_periods:] - result_min.fun
id_lb = l_vec.flatten() @ true_beta[num_pre_periods:] + result_max.fun
return DeltaRMMResult(id_lb=id_lb, id_ub=id_ub)
def _compute_conditional_cs_rmm_fixed_s(
s,
max_positive,
m_bar,
betahat,
sigma,
num_pre_periods,
num_post_periods,
l_vec,
alpha=0.05,
hybrid_flag="LF",
hybrid_kappa=None,
monotonicity_direction="increasing",
post_period_moments_only=True,
grid_points=1000,
grid_lb=None,
grid_ub=None,
seed=None,
):
r"""Compute conditional CS for a component of :math:`\Delta^{RMI}(\bar{M})` at fixed :math:`s`.
Helper function that implements the moment inequality testing approach for a
single polyhedron :math:`\Delta^{RM}_{s,\text{sign}}(\bar{M}) \cap \Delta^I`.
Parameters
----------
s : int
Reference period for relative magnitudes restriction.
max_positive : bool
If True, uses (+) restriction; if False, uses (-) restriction.
m_bar : float
Relative magnitude parameter :math:`\bar{M}`.
betahat : ndarray
Estimated event study coefficients.
sigma : ndarray
Covariance matrix of betahat.
num_pre_periods : int
Number of pre-treatment periods.
num_post_periods : int
Number of post-treatment periods.
l_vec : ndarray
Vector defining parameter of interest.
alpha : float, default=0.05
Significance level.
hybrid_flag : {'LF', 'ARP'}, default='LF'
Type of hybrid test.
hybrid_kappa : float, optional
First-stage size for hybrid test. If None, defaults to alpha/10.
monotonicity_direction : {'increasing', 'decreasing'}, default='increasing'
Direction of monotonicity restriction.
post_period_moments_only : bool, default=True
If True, use only post-period moments for ARP test.
grid_points : int, default=1000
Number of grid points for confidence interval search.
grid_lb : float, optional
Lower bound for grid search.
grid_ub : float, optional
Upper bound for grid search.
seed : int, optional
Random seed for reproducibility.
Returns
-------
dict
Dictionary with 'grid' and 'accept' arrays.
References
----------
.. [1] Andrews, I., Roth, J., & Pakes, A. (2021). Inference for linear
conditional moment inequalities. Review of Economic Studies.
.. [2] Rambachan, A., & Roth, J. (2023). A more credible approach to
parallel trends. Review of Economic Studies, 90(5), 2555-2591.
"""
if hybrid_kappa is None:
hybrid_kappa = alpha / 10
if hybrid_flag not in ["LF", "ARP"]:
raise ValueError("hybrid_flag must be 'LF' or 'ARP'")
hybrid_list = {"hybrid_kappa": hybrid_kappa}
A_rmm = _create_relative_magnitudes_monotonicity_constraint_matrix(
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
m_bar=m_bar,
s=s,
max_positive=max_positive,
monotonicity_direction=monotonicity_direction,
)
d_rmm = _create_relative_magnitudes_monotonicity_constraint_vector(A_rmm)
if post_period_moments_only and num_post_periods > 1:
post_period_indices = list(range(num_pre_periods, num_pre_periods + num_post_periods))
rows_for_arp = find_rows_with_post_period_values(A_rmm, post_period_indices)
else:
rows_for_arp = None
if num_post_periods == 1:
post_period_rows = np.where(A_rmm[:, -1] != 0)[0]
if len(post_period_rows) > 0:
A_rmm = A_rmm[post_period_rows, :]
d_rmm = d_rmm[post_period_rows]
if grid_lb is None or grid_ub is None:
sel = slice(num_pre_periods, num_pre_periods + num_post_periods)
sigma_post = sigma[sel, sel]
sd_theta = np.sqrt(l_vec.T @ sigma_post @ l_vec)
if num_pre_periods > 1:
pre_betas_with_zero = np.concatenate([betahat[:num_pre_periods], [0]])
maxpre = np.max(np.abs(np.diff(pre_betas_with_zero)))
else:
maxpre = np.abs(betahat[0])
gridoff = betahat[sel] @ l_vec
post_period_indices = np.arange(1, num_post_periods + 1)
gridhalf = (m_bar * post_period_indices @ np.abs(l_vec) * maxpre) + (20 * sd_theta)
if grid_ub is None:
grid_ub = gridoff + gridhalf
if grid_lb is None:
grid_lb = gridoff - gridhalf
if num_post_periods == 1:
if hybrid_flag == "LF":
lf_cv = compute_least_favorable_cv(
x_t=None,
sigma=A_rmm @ sigma @ A_rmm.T,
hybrid_kappa=hybrid_kappa,
seed=seed,
)
hybrid_list["lf_cv"] = lf_cv
# Use no-nuisance CI function
result = compute_arp_nuisance_ci(
betahat=betahat,
sigma=sigma,
l_vec=l_vec,
a_matrix=A_rmm,
d_vec=d_rmm,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
alpha=alpha,
hybrid_flag=hybrid_flag,
hybrid_list=hybrid_list,
grid_lb=grid_lb,
grid_ub=grid_ub,
grid_points=grid_points,
rows_for_arp=None,
)
return {"grid": result.accept_grid[:, 0], "accept": result.accept_grid[:, 1]}
# Multiple post-periods case
result = compute_arp_nuisance_ci(
betahat=betahat,
sigma=sigma,
l_vec=l_vec,
a_matrix=A_rmm,
d_vec=d_rmm,
num_pre_periods=num_pre_periods,
num_post_periods=num_post_periods,
alpha=alpha,
hybrid_flag=hybrid_flag,
hybrid_list=hybrid_list,
grid_lb=grid_lb,
grid_ub=grid_ub,
grid_points=grid_points,
rows_for_arp=rows_for_arp,
)
return {"grid": result.accept_grid[:, 0], "accept": result.accept_grid[:, 1]}
def _create_relative_magnitudes_monotonicity_constraint_vector(A_rmm):
r"""Create constraint vector for :math:`\Delta^{RMI}_{s,sign}(\bar{M})`.
Creates vector d for the constraint :math:`A\delta \le d`.
For the combined relative magnitudes and monotonicity restriction, the constraint
vector :math:`d` is a vector of zeros because both restrictions are defined
by homogeneous inequalities.
Parameters
----------
A_rmm : ndarray
The constraint matrix A_rmm.
Returns
-------
ndarray
Constraint vector d (all zeros).
"""
return np.zeros(A_rmm.shape[0])