limiteddepkit 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- limiteddepkit/__init__.py +162 -0
- limiteddepkit/_continuous.py +354 -0
- limiteddepkit/_count_common.py +280 -0
- limiteddepkit/_duration.py +400 -0
- limiteddepkit/_irls.py +132 -0
- limiteddepkit/_small_sample.py +1108 -0
- limiteddepkit/binary.py +482 -0
- limiteddepkit/binary_probit.py +250 -0
- limiteddepkit/censored_quantile.py +449 -0
- limiteddepkit/censoring/__init__.py +14 -0
- limiteddepkit/conditional_logit.py +332 -0
- limiteddepkit/count/__init__.py +23 -0
- limiteddepkit/discrete_time_duration.py +335 -0
- limiteddepkit/duration/__init__.py +24 -0
- limiteddepkit/dynamic_fixed_effects_ordinal.py +626 -0
- limiteddepkit/dynamic_ordinal.py +437 -0
- limiteddepkit/experimental/__init__.py +102 -0
- limiteddepkit/experimental/small_sample.py +23 -0
- limiteddepkit/exponential_duration.py +282 -0
- limiteddepkit/fixed_effects_ordinal.py +951 -0
- limiteddepkit/gamma_duration.py +359 -0
- limiteddepkit/generalized_ordinal.py +810 -0
- limiteddepkit/hurdle_poisson.py +368 -0
- limiteddepkit/integrations/__init__.py +5 -0
- limiteddepkit/integrations/outputhub.py +197 -0
- limiteddepkit/interval_regression.py +248 -0
- limiteddepkit/ml/__init__.py +190 -0
- limiteddepkit/ml/adapter.py +430 -0
- limiteddepkit/ml/bridges.py +394 -0
- limiteddepkit/ml/calibration.py +402 -0
- limiteddepkit/ml/compare.py +280 -0
- limiteddepkit/ml/metrics.py +932 -0
- limiteddepkit/ml/neural.py +718 -0
- limiteddepkit/ml/split.py +718 -0
- limiteddepkit/ml/survival.py +470 -0
- limiteddepkit/ml/tuning.py +363 -0
- limiteddepkit/ml/uncertainty.py +707 -0
- limiteddepkit/ml/validation.py +886 -0
- limiteddepkit/model_comparison.py +104 -0
- limiteddepkit/multinomial.py +344 -0
- limiteddepkit/negative_binomial.py +404 -0
- limiteddepkit/ordinal.py +799 -0
- limiteddepkit/panel/__init__.py +30 -0
- limiteddepkit/panel_ordinal.py +622 -0
- limiteddepkit/plotting.py +96 -0
- limiteddepkit/poisson.py +375 -0
- limiteddepkit/postestimation.py +170 -0
- limiteddepkit/sample_selection.py +294 -0
- limiteddepkit/sequential_logit.py +332 -0
- limiteddepkit/simulation.py +430 -0
- limiteddepkit/small_sample.py +11 -0
- limiteddepkit/tobit.py +229 -0
- limiteddepkit/truncated_regression.py +218 -0
- limiteddepkit/weibull_duration.py +374 -0
- limiteddepkit/zero_inflated_poisson.py +905 -0
- limiteddepkit-0.1.0a1.dist-info/METADATA +1366 -0
- limiteddepkit-0.1.0a1.dist-info/RECORD +60 -0
- limiteddepkit-0.1.0a1.dist-info/WHEEL +5 -0
- limiteddepkit-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- limiteddepkit-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Validated limited-dependent-variable and duration estimators."""
|
|
2
|
+
|
|
3
|
+
from .binary import BinaryLogit, BinaryLogitResult
|
|
4
|
+
from .binary_probit import BinaryProbit, BinaryProbitResult
|
|
5
|
+
from .censoring import (
|
|
6
|
+
IntervalRegression,
|
|
7
|
+
IntervalRegressionResult,
|
|
8
|
+
Tobit,
|
|
9
|
+
TobitResult,
|
|
10
|
+
TruncatedRegression,
|
|
11
|
+
TruncatedRegressionResult,
|
|
12
|
+
)
|
|
13
|
+
from .count import (
|
|
14
|
+
NegativeBinomial,
|
|
15
|
+
NegativeBinomialNB2,
|
|
16
|
+
NegativeBinomialResult,
|
|
17
|
+
PoissonRegressor,
|
|
18
|
+
PoissonResult,
|
|
19
|
+
)
|
|
20
|
+
from .duration import (
|
|
21
|
+
DiscreteTimeDuration,
|
|
22
|
+
DiscreteTimeDurationResult,
|
|
23
|
+
ExponentialDuration,
|
|
24
|
+
ExponentialDurationResult,
|
|
25
|
+
GammaDuration,
|
|
26
|
+
GammaDurationResult,
|
|
27
|
+
GeometricDuration,
|
|
28
|
+
GeometricDurationResult,
|
|
29
|
+
WeibullDuration,
|
|
30
|
+
WeibullDurationResult,
|
|
31
|
+
)
|
|
32
|
+
from .dynamic_ordinal import (
|
|
33
|
+
DynamicRandomEffectsOrderedLogit,
|
|
34
|
+
DynamicRandomEffectsOrderedLogitResult,
|
|
35
|
+
)
|
|
36
|
+
from .fixed_effects_ordinal import (
|
|
37
|
+
FixedEffectsOrderedLogit,
|
|
38
|
+
FixedEffectsOrderedLogitResult,
|
|
39
|
+
)
|
|
40
|
+
from .generalized_ordinal import (
|
|
41
|
+
GeneralizedOrderedLogit,
|
|
42
|
+
GeneralizedOrderedLogitResult,
|
|
43
|
+
PartialProportionalOdds,
|
|
44
|
+
PartialProportionalOddsResult,
|
|
45
|
+
)
|
|
46
|
+
from .integrations import add_to_outputhub, to_outputhub_model
|
|
47
|
+
from .model_comparison import LikelihoodRatioTestResult, likelihood_ratio_test
|
|
48
|
+
from .ordinal import (
|
|
49
|
+
OrderedLogit,
|
|
50
|
+
OrderedLogitResult,
|
|
51
|
+
OrderedProbit,
|
|
52
|
+
OrderedProbitResult,
|
|
53
|
+
OrderedResult,
|
|
54
|
+
ProportionalOddsTestResult,
|
|
55
|
+
)
|
|
56
|
+
from .panel_ordinal import (
|
|
57
|
+
RandomEffectsOrderedLogit,
|
|
58
|
+
RandomEffectsOrderedLogitResult,
|
|
59
|
+
RandomEffectsOrderedProbit,
|
|
60
|
+
RandomEffectsOrderedProbitResult,
|
|
61
|
+
RandomEffectsOrderedResult,
|
|
62
|
+
)
|
|
63
|
+
from .plotting import plot_marginal_effects, plot_probabilities
|
|
64
|
+
from .postestimation import (
|
|
65
|
+
confint,
|
|
66
|
+
lincom,
|
|
67
|
+
marginal_effects,
|
|
68
|
+
margins,
|
|
69
|
+
posterior_predict_proba,
|
|
70
|
+
posterior_random_effects,
|
|
71
|
+
predict,
|
|
72
|
+
predict_proba,
|
|
73
|
+
summary_frame,
|
|
74
|
+
vcov,
|
|
75
|
+
wald_test,
|
|
76
|
+
)
|
|
77
|
+
from .simulation import (
|
|
78
|
+
DynamicOrderedLogitSimulation,
|
|
79
|
+
GeneralizedOrdinalSimulation,
|
|
80
|
+
RandomEffectsOrderedLogitSimulation,
|
|
81
|
+
RandomEffectsOrderedProbitSimulation,
|
|
82
|
+
simulate_dynamic_random_effects_ordered_logit,
|
|
83
|
+
simulate_generalized_ordered_logit,
|
|
84
|
+
simulate_random_effects_ordered_logit,
|
|
85
|
+
simulate_random_effects_ordered_probit,
|
|
86
|
+
)
|
|
87
|
+
from .small_sample import FirthBinaryLogit, FirthBinaryLogitResult
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
"BinaryLogit",
|
|
91
|
+
"BinaryLogitResult",
|
|
92
|
+
"BinaryProbit",
|
|
93
|
+
"BinaryProbitResult",
|
|
94
|
+
"Tobit",
|
|
95
|
+
"TobitResult",
|
|
96
|
+
"TruncatedRegression",
|
|
97
|
+
"TruncatedRegressionResult",
|
|
98
|
+
"IntervalRegression",
|
|
99
|
+
"IntervalRegressionResult",
|
|
100
|
+
"PoissonRegressor",
|
|
101
|
+
"PoissonResult",
|
|
102
|
+
"NegativeBinomial",
|
|
103
|
+
"NegativeBinomialNB2",
|
|
104
|
+
"NegativeBinomialResult",
|
|
105
|
+
"DiscreteTimeDuration",
|
|
106
|
+
"DiscreteTimeDurationResult",
|
|
107
|
+
"ExponentialDuration",
|
|
108
|
+
"ExponentialDurationResult",
|
|
109
|
+
"GammaDuration",
|
|
110
|
+
"GammaDurationResult",
|
|
111
|
+
"GeometricDuration",
|
|
112
|
+
"GeometricDurationResult",
|
|
113
|
+
"WeibullDuration",
|
|
114
|
+
"WeibullDurationResult",
|
|
115
|
+
"OrderedLogit",
|
|
116
|
+
"OrderedLogitResult",
|
|
117
|
+
"OrderedProbit",
|
|
118
|
+
"OrderedProbitResult",
|
|
119
|
+
"OrderedResult",
|
|
120
|
+
"ProportionalOddsTestResult",
|
|
121
|
+
"plot_marginal_effects",
|
|
122
|
+
"plot_probabilities",
|
|
123
|
+
"add_to_outputhub",
|
|
124
|
+
"to_outputhub_model",
|
|
125
|
+
"GeneralizedOrderedLogit",
|
|
126
|
+
"GeneralizedOrderedLogitResult",
|
|
127
|
+
"PartialProportionalOdds",
|
|
128
|
+
"PartialProportionalOddsResult",
|
|
129
|
+
"LikelihoodRatioTestResult",
|
|
130
|
+
"likelihood_ratio_test",
|
|
131
|
+
"confint",
|
|
132
|
+
"lincom",
|
|
133
|
+
"marginal_effects",
|
|
134
|
+
"margins",
|
|
135
|
+
"predict",
|
|
136
|
+
"predict_proba",
|
|
137
|
+
"posterior_predict_proba",
|
|
138
|
+
"posterior_random_effects",
|
|
139
|
+
"summary_frame",
|
|
140
|
+
"vcov",
|
|
141
|
+
"wald_test",
|
|
142
|
+
"GeneralizedOrdinalSimulation",
|
|
143
|
+
"simulate_generalized_ordered_logit",
|
|
144
|
+
"RandomEffectsOrderedLogit",
|
|
145
|
+
"RandomEffectsOrderedLogitResult",
|
|
146
|
+
"RandomEffectsOrderedLogitSimulation",
|
|
147
|
+
"simulate_random_effects_ordered_logit",
|
|
148
|
+
"RandomEffectsOrderedProbit",
|
|
149
|
+
"RandomEffectsOrderedProbitResult",
|
|
150
|
+
"RandomEffectsOrderedProbitSimulation",
|
|
151
|
+
"RandomEffectsOrderedResult",
|
|
152
|
+
"simulate_random_effects_ordered_probit",
|
|
153
|
+
"FixedEffectsOrderedLogit",
|
|
154
|
+
"FixedEffectsOrderedLogitResult",
|
|
155
|
+
"DynamicRandomEffectsOrderedLogit",
|
|
156
|
+
"DynamicRandomEffectsOrderedLogitResult",
|
|
157
|
+
"DynamicOrderedLogitSimulation",
|
|
158
|
+
"simulate_dynamic_random_effects_ordered_logit",
|
|
159
|
+
"FirthBinaryLogit",
|
|
160
|
+
"FirthBinaryLogitResult",
|
|
161
|
+
]
|
|
162
|
+
__version__ = "0.1.0a1"
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Shared contracts for continuous limited-outcome models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from scipy.special import ndtr
|
|
10
|
+
from scipy.stats import norm
|
|
11
|
+
|
|
12
|
+
from .ordinal import _as_2d_array, _numerical_hessian, _numerical_jacobian
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_fit_design(X: Any) -> tuple[np.ndarray, list[str]]:
|
|
16
|
+
design, feature_names = _as_2d_array(X)
|
|
17
|
+
if len(set(feature_names)) != len(feature_names):
|
|
18
|
+
raise ValueError("X feature names must be unique after conversion to strings.")
|
|
19
|
+
if design.shape[0] <= design.shape[1] + 1:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"Continuous limited-outcome inference requires more observations "
|
|
22
|
+
"than regression and dispersion parameters."
|
|
23
|
+
)
|
|
24
|
+
if np.linalg.matrix_rank(design) < design.shape[1]:
|
|
25
|
+
raise ValueError("X is rank deficient; regression parameters are not identified.")
|
|
26
|
+
return design, feature_names
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_outcome(
|
|
30
|
+
values: Any,
|
|
31
|
+
*,
|
|
32
|
+
name: str,
|
|
33
|
+
nobs: int,
|
|
34
|
+
allow_infinite: bool = False,
|
|
35
|
+
) -> np.ndarray:
|
|
36
|
+
array = np.asarray(values)
|
|
37
|
+
if array.ndim != 1:
|
|
38
|
+
raise ValueError(f"{name} must be one-dimensional.")
|
|
39
|
+
try:
|
|
40
|
+
array = array.astype(float)
|
|
41
|
+
except (TypeError, ValueError) as error:
|
|
42
|
+
raise ValueError(f"{name} must contain numeric values.") from error
|
|
43
|
+
if array.size != nobs:
|
|
44
|
+
raise ValueError(f"X and {name} must contain the same number of observations.")
|
|
45
|
+
if np.isnan(array).any():
|
|
46
|
+
raise ValueError(f"{name} contains missing values.")
|
|
47
|
+
if not allow_infinite and not np.isfinite(array).all():
|
|
48
|
+
raise ValueError(f"{name} contains non-finite values.")
|
|
49
|
+
return array
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_optimizer_options(maxiter: int, tolerance: float) -> tuple[int, float]:
|
|
53
|
+
if isinstance(maxiter, bool) or not isinstance(maxiter, (int, np.integer)) or maxiter < 1:
|
|
54
|
+
raise ValueError("maxiter must be a positive integer.")
|
|
55
|
+
if not np.isfinite(tolerance) or tolerance <= 0.0:
|
|
56
|
+
raise ValueError("tolerance must be finite and positive.")
|
|
57
|
+
return int(maxiter), float(tolerance)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_covariance_options(
|
|
61
|
+
covariance_type: str,
|
|
62
|
+
clusters: Any,
|
|
63
|
+
*,
|
|
64
|
+
nobs: int,
|
|
65
|
+
) -> tuple[str, np.ndarray | None, int | None]:
|
|
66
|
+
"""Validate likelihood covariance options and optional cluster labels."""
|
|
67
|
+
allowed = {"observed-information", "robust", "cluster"}
|
|
68
|
+
if not isinstance(covariance_type, str) or covariance_type not in allowed:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"covariance_type must be 'observed-information', 'robust', or 'cluster'."
|
|
71
|
+
)
|
|
72
|
+
if covariance_type != "cluster":
|
|
73
|
+
if clusters is not None:
|
|
74
|
+
raise ValueError("clusters may be supplied only with covariance_type='cluster'.")
|
|
75
|
+
return covariance_type, None, None
|
|
76
|
+
if clusters is None:
|
|
77
|
+
raise ValueError("clusters is required when covariance_type='cluster'.")
|
|
78
|
+
labels = np.asarray(clusters)
|
|
79
|
+
if labels.ndim != 1:
|
|
80
|
+
raise ValueError("clusters must be one-dimensional.")
|
|
81
|
+
if labels.size != nobs:
|
|
82
|
+
raise ValueError("clusters must contain one label per observation.")
|
|
83
|
+
if pd.isna(labels).any():
|
|
84
|
+
raise ValueError("clusters contains missing values.")
|
|
85
|
+
codes, unique = pd.factorize(labels, sort=False)
|
|
86
|
+
if unique.size < 2:
|
|
87
|
+
raise ValueError("Cluster covariance requires at least two distinct clusters.")
|
|
88
|
+
return covariance_type, codes.astype(int), int(unique.size)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _validate_prediction_design(
|
|
92
|
+
X: Any, feature_names: tuple[str, ...]
|
|
93
|
+
) -> tuple[np.ndarray, pd.Index]:
|
|
94
|
+
design, names = _as_2d_array(X)
|
|
95
|
+
if design.shape[1] != len(feature_names):
|
|
96
|
+
raise ValueError(f"X has {design.shape[1]} columns; expected {len(feature_names)}.")
|
|
97
|
+
if isinstance(X, pd.DataFrame) and tuple(names) != feature_names:
|
|
98
|
+
raise ValueError("DataFrame columns must match the fitted feature names and order.")
|
|
99
|
+
index = X.index.copy() if isinstance(X, pd.DataFrame) else pd.RangeIndex(design.shape[0])
|
|
100
|
+
return design, index
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _score_norm(optimizer_result: Any) -> float:
|
|
104
|
+
gradient = np.asarray(getattr(optimizer_result, "jac", np.nan), dtype=float)
|
|
105
|
+
if gradient.size == 0 or not np.isfinite(gradient).all():
|
|
106
|
+
return np.inf
|
|
107
|
+
return float(np.max(np.abs(gradient)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_optimizer_result(
|
|
111
|
+
optimizer_result: Any,
|
|
112
|
+
*,
|
|
113
|
+
model_name: str,
|
|
114
|
+
tolerance: float,
|
|
115
|
+
nobs: int,
|
|
116
|
+
) -> float:
|
|
117
|
+
score_norm = _score_norm(optimizer_result)
|
|
118
|
+
scaled_score_norm = score_norm / max(1, int(nobs))
|
|
119
|
+
stationarity_limit = max(min(100.0 * float(tolerance), 1e-4), 1e-5)
|
|
120
|
+
converged = bool(
|
|
121
|
+
np.isfinite(optimizer_result.fun)
|
|
122
|
+
and np.isfinite(scaled_score_norm)
|
|
123
|
+
and scaled_score_norm <= stationarity_limit
|
|
124
|
+
)
|
|
125
|
+
parameters = np.asarray(optimizer_result.x, dtype=float)
|
|
126
|
+
if (
|
|
127
|
+
not converged
|
|
128
|
+
or not np.isfinite(optimizer_result.fun)
|
|
129
|
+
or not np.isfinite(parameters).all()
|
|
130
|
+
):
|
|
131
|
+
raise RuntimeError(f"{model_name} optimization failed: {optimizer_result.message}")
|
|
132
|
+
return score_norm
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _observed_information_covariance(
|
|
136
|
+
negative_loglike: Any,
|
|
137
|
+
raw_parameters: np.ndarray,
|
|
138
|
+
sigma: float,
|
|
139
|
+
) -> np.ndarray:
|
|
140
|
+
information = _numerical_hessian(negative_loglike, raw_parameters)
|
|
141
|
+
information = (information + information.T) / 2.0
|
|
142
|
+
if not np.isfinite(information).all():
|
|
143
|
+
raise RuntimeError("The observed-information matrix contains non-finite values.")
|
|
144
|
+
eigenvalues = np.linalg.eigvalsh(information)
|
|
145
|
+
largest = float(eigenvalues[-1])
|
|
146
|
+
if largest <= 0.0 or eigenvalues[0] <= max(1e-10 * largest, 1e-10):
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
"The observed-information matrix is singular or not positive definite; "
|
|
149
|
+
"parameter inference is not reliable."
|
|
150
|
+
)
|
|
151
|
+
raw_covariance = np.linalg.inv(information)
|
|
152
|
+
jacobian = np.eye(raw_parameters.size, dtype=float)
|
|
153
|
+
jacobian[-1, -1] = sigma
|
|
154
|
+
covariance = jacobian @ raw_covariance @ jacobian.T
|
|
155
|
+
return (covariance + covariance.T) / 2.0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _mle_covariance(
|
|
159
|
+
negative_loglike: Any,
|
|
160
|
+
loglike_contributions: Any,
|
|
161
|
+
raw_parameters: np.ndarray,
|
|
162
|
+
sigma: float,
|
|
163
|
+
*,
|
|
164
|
+
covariance_type: str,
|
|
165
|
+
cluster_codes: np.ndarray | None,
|
|
166
|
+
) -> np.ndarray:
|
|
167
|
+
"""Return observed-information or likelihood-score sandwich covariance."""
|
|
168
|
+
if covariance_type == "observed-information":
|
|
169
|
+
return _observed_information_covariance(
|
|
170
|
+
negative_loglike,
|
|
171
|
+
raw_parameters,
|
|
172
|
+
sigma,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
information = _numerical_hessian(negative_loglike, raw_parameters)
|
|
176
|
+
information = (information + information.T) / 2.0
|
|
177
|
+
if not np.isfinite(information).all():
|
|
178
|
+
raise RuntimeError("The observed-information matrix contains non-finite values.")
|
|
179
|
+
eigenvalues = np.linalg.eigvalsh(information)
|
|
180
|
+
largest = float(eigenvalues[-1])
|
|
181
|
+
if largest <= 0.0 or eigenvalues[0] <= max(1e-10 * largest, 1e-10):
|
|
182
|
+
raise RuntimeError(
|
|
183
|
+
"The observed-information matrix is singular or not positive definite; "
|
|
184
|
+
"parameter inference is not reliable."
|
|
185
|
+
)
|
|
186
|
+
bread = np.linalg.inv(information)
|
|
187
|
+
scores = _numerical_jacobian(loglike_contributions, raw_parameters)
|
|
188
|
+
if not np.isfinite(scores).all():
|
|
189
|
+
raise RuntimeError("The per-observation score matrix contains non-finite values.")
|
|
190
|
+
|
|
191
|
+
if covariance_type == "robust":
|
|
192
|
+
meat = scores.T @ scores
|
|
193
|
+
else:
|
|
194
|
+
if cluster_codes is None:
|
|
195
|
+
raise RuntimeError("Internal error: cluster labels were not retained.")
|
|
196
|
+
n_clusters = int(np.max(cluster_codes)) + 1
|
|
197
|
+
cluster_scores = np.zeros((n_clusters, scores.shape[1]), dtype=float)
|
|
198
|
+
np.add.at(cluster_scores, cluster_codes, scores)
|
|
199
|
+
meat = cluster_scores.T @ cluster_scores
|
|
200
|
+
nobs, n_parameters = scores.shape
|
|
201
|
+
if nobs <= n_parameters:
|
|
202
|
+
raise RuntimeError(
|
|
203
|
+
"Cluster covariance requires more observations than estimated parameters."
|
|
204
|
+
)
|
|
205
|
+
meat *= (n_clusters / (n_clusters - 1.0)) * (
|
|
206
|
+
(nobs - 1.0) / (nobs - n_parameters)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
raw_covariance = bread @ meat @ bread
|
|
210
|
+
jacobian = np.eye(raw_parameters.size, dtype=float)
|
|
211
|
+
jacobian[-1, -1] = sigma
|
|
212
|
+
covariance = jacobian @ raw_covariance @ jacobian.T
|
|
213
|
+
covariance = (covariance + covariance.T) / 2.0
|
|
214
|
+
if not np.isfinite(covariance).all() or np.any(np.diag(covariance) < -1e-12):
|
|
215
|
+
raise RuntimeError("The sandwich covariance matrix is not numerically valid.")
|
|
216
|
+
return covariance
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _inference_series(
|
|
220
|
+
beta: np.ndarray,
|
|
221
|
+
sigma: float,
|
|
222
|
+
feature_names: list[str],
|
|
223
|
+
covariance: np.ndarray,
|
|
224
|
+
) -> tuple[pd.DataFrame, pd.Series, pd.Series, pd.Series]:
|
|
225
|
+
labels = [*feature_names, "sigma"]
|
|
226
|
+
estimates = np.concatenate([np.asarray(beta, dtype=float), [float(sigma)]])
|
|
227
|
+
standard_errors = np.sqrt(np.clip(np.diag(covariance), 0.0, None))
|
|
228
|
+
zstats = estimates / standard_errors
|
|
229
|
+
pvalues = 2.0 * norm.sf(np.abs(zstats))
|
|
230
|
+
return (
|
|
231
|
+
pd.DataFrame(covariance, index=labels, columns=labels),
|
|
232
|
+
pd.Series(standard_errors, index=labels, name="std_err"),
|
|
233
|
+
pd.Series(zstats, index=labels, name="z"),
|
|
234
|
+
pd.Series(pvalues, index=labels, name="p_value"),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class _ContinuousResultMixin:
|
|
239
|
+
"""Shared result API for Gaussian continuous limited-outcome models."""
|
|
240
|
+
|
|
241
|
+
params: pd.Series
|
|
242
|
+
sigma: float
|
|
243
|
+
covariance: pd.DataFrame
|
|
244
|
+
standard_errors: pd.Series
|
|
245
|
+
zstats: pd.Series
|
|
246
|
+
pvalues: pd.Series
|
|
247
|
+
loglike: float
|
|
248
|
+
nobs: int
|
|
249
|
+
feature_names: tuple[str, ...]
|
|
250
|
+
_covariance_type: str
|
|
251
|
+
score_norm: float
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def scaled_score_norm(self) -> float:
|
|
255
|
+
return self.score_norm / max(1, self.nobs)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def all_params(self) -> pd.Series:
|
|
259
|
+
dispersion = pd.Series({"sigma": self.sigma}, name=self.params.name)
|
|
260
|
+
return pd.concat([self.params, dispersion])
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def n_params(self) -> int:
|
|
264
|
+
return len(self.all_params)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def df_resid(self) -> int:
|
|
268
|
+
return self.nobs - self.n_params
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def aic(self) -> float:
|
|
272
|
+
return -2.0 * self.loglike + 2.0 * self.n_params
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def bic(self) -> float:
|
|
276
|
+
return -2.0 * self.loglike + np.log(self.nobs) * self.n_params
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def inference_valid(self) -> bool:
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def covariance_type(self) -> str:
|
|
284
|
+
return self._covariance_type
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def backend(self) -> str:
|
|
288
|
+
return "native-mle"
|
|
289
|
+
|
|
290
|
+
def conf_int(self, level: float = 0.95) -> pd.DataFrame:
|
|
291
|
+
if not 0.0 < level < 1.0:
|
|
292
|
+
raise ValueError("level must be strictly between zero and one.")
|
|
293
|
+
critical = float(norm.ppf(0.5 + level / 2.0))
|
|
294
|
+
estimates = self.all_params
|
|
295
|
+
intervals = pd.DataFrame(
|
|
296
|
+
{
|
|
297
|
+
"lower": estimates - critical * self.standard_errors,
|
|
298
|
+
"upper": estimates + critical * self.standard_errors,
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
log_sigma_standard_error = float(self.standard_errors["sigma"] / self.sigma)
|
|
302
|
+
intervals.loc["sigma", "lower"] = self.sigma * np.exp(
|
|
303
|
+
-critical * log_sigma_standard_error
|
|
304
|
+
)
|
|
305
|
+
intervals.loc["sigma", "upper"] = self.sigma * np.exp(
|
|
306
|
+
critical * log_sigma_standard_error
|
|
307
|
+
)
|
|
308
|
+
return intervals
|
|
309
|
+
|
|
310
|
+
def summary_frame(self) -> pd.DataFrame:
|
|
311
|
+
from .postestimation import summary_frame
|
|
312
|
+
|
|
313
|
+
return summary_frame(self)
|
|
314
|
+
|
|
315
|
+
def vcov(self) -> pd.DataFrame:
|
|
316
|
+
return self.covariance.copy()
|
|
317
|
+
|
|
318
|
+
def predict_latent(self, X: Any) -> pd.Series:
|
|
319
|
+
"""Predict the latent Gaussian conditional mean ``X beta``."""
|
|
320
|
+
design, index = _validate_prediction_design(X, self.feature_names)
|
|
321
|
+
values = design @ self.params.to_numpy(dtype=float)
|
|
322
|
+
return pd.Series(values, index=index, name="predicted_latent")
|
|
323
|
+
|
|
324
|
+
def predict_latent_cdf(self, X: Any, values: Any) -> pd.Series:
|
|
325
|
+
"""Evaluate the fitted latent Gaussian CDF at scalar or rowwise values."""
|
|
326
|
+
mean = self.predict_latent(X)
|
|
327
|
+
evaluation = np.asarray(values, dtype=float)
|
|
328
|
+
if evaluation.ndim == 0:
|
|
329
|
+
evaluation = np.full(len(mean), float(evaluation))
|
|
330
|
+
elif evaluation.shape != (len(mean),):
|
|
331
|
+
raise ValueError("values must be scalar or contain one value per prediction row.")
|
|
332
|
+
if not np.isfinite(evaluation).all():
|
|
333
|
+
raise ValueError("values must be finite.")
|
|
334
|
+
probabilities = ndtr((evaluation - mean.to_numpy(dtype=float)) / self.sigma)
|
|
335
|
+
return pd.Series(probabilities, index=mean.index, name="latent_cdf")
|
|
336
|
+
|
|
337
|
+
def predict_latent_interval(
|
|
338
|
+
self,
|
|
339
|
+
X: Any,
|
|
340
|
+
*,
|
|
341
|
+
level: float = 0.95,
|
|
342
|
+
) -> pd.DataFrame:
|
|
343
|
+
"""Return a latent-outcome predictive interval, not a coefficient interval."""
|
|
344
|
+
if not 0.0 < level < 1.0:
|
|
345
|
+
raise ValueError("level must be strictly between zero and one.")
|
|
346
|
+
mean = self.predict_latent(X)
|
|
347
|
+
critical = float(norm.ppf(0.5 + level / 2.0))
|
|
348
|
+
return pd.DataFrame(
|
|
349
|
+
{
|
|
350
|
+
"lower": mean - critical * self.sigma,
|
|
351
|
+
"upper": mean + critical * self.sigma,
|
|
352
|
+
},
|
|
353
|
+
index=mean.index,
|
|
354
|
+
)
|