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.
Files changed (60) hide show
  1. limiteddepkit/__init__.py +162 -0
  2. limiteddepkit/_continuous.py +354 -0
  3. limiteddepkit/_count_common.py +280 -0
  4. limiteddepkit/_duration.py +400 -0
  5. limiteddepkit/_irls.py +132 -0
  6. limiteddepkit/_small_sample.py +1108 -0
  7. limiteddepkit/binary.py +482 -0
  8. limiteddepkit/binary_probit.py +250 -0
  9. limiteddepkit/censored_quantile.py +449 -0
  10. limiteddepkit/censoring/__init__.py +14 -0
  11. limiteddepkit/conditional_logit.py +332 -0
  12. limiteddepkit/count/__init__.py +23 -0
  13. limiteddepkit/discrete_time_duration.py +335 -0
  14. limiteddepkit/duration/__init__.py +24 -0
  15. limiteddepkit/dynamic_fixed_effects_ordinal.py +626 -0
  16. limiteddepkit/dynamic_ordinal.py +437 -0
  17. limiteddepkit/experimental/__init__.py +102 -0
  18. limiteddepkit/experimental/small_sample.py +23 -0
  19. limiteddepkit/exponential_duration.py +282 -0
  20. limiteddepkit/fixed_effects_ordinal.py +951 -0
  21. limiteddepkit/gamma_duration.py +359 -0
  22. limiteddepkit/generalized_ordinal.py +810 -0
  23. limiteddepkit/hurdle_poisson.py +368 -0
  24. limiteddepkit/integrations/__init__.py +5 -0
  25. limiteddepkit/integrations/outputhub.py +197 -0
  26. limiteddepkit/interval_regression.py +248 -0
  27. limiteddepkit/ml/__init__.py +190 -0
  28. limiteddepkit/ml/adapter.py +430 -0
  29. limiteddepkit/ml/bridges.py +394 -0
  30. limiteddepkit/ml/calibration.py +402 -0
  31. limiteddepkit/ml/compare.py +280 -0
  32. limiteddepkit/ml/metrics.py +932 -0
  33. limiteddepkit/ml/neural.py +718 -0
  34. limiteddepkit/ml/split.py +718 -0
  35. limiteddepkit/ml/survival.py +470 -0
  36. limiteddepkit/ml/tuning.py +363 -0
  37. limiteddepkit/ml/uncertainty.py +707 -0
  38. limiteddepkit/ml/validation.py +886 -0
  39. limiteddepkit/model_comparison.py +104 -0
  40. limiteddepkit/multinomial.py +344 -0
  41. limiteddepkit/negative_binomial.py +404 -0
  42. limiteddepkit/ordinal.py +799 -0
  43. limiteddepkit/panel/__init__.py +30 -0
  44. limiteddepkit/panel_ordinal.py +622 -0
  45. limiteddepkit/plotting.py +96 -0
  46. limiteddepkit/poisson.py +375 -0
  47. limiteddepkit/postestimation.py +170 -0
  48. limiteddepkit/sample_selection.py +294 -0
  49. limiteddepkit/sequential_logit.py +332 -0
  50. limiteddepkit/simulation.py +430 -0
  51. limiteddepkit/small_sample.py +11 -0
  52. limiteddepkit/tobit.py +229 -0
  53. limiteddepkit/truncated_regression.py +218 -0
  54. limiteddepkit/weibull_duration.py +374 -0
  55. limiteddepkit/zero_inflated_poisson.py +905 -0
  56. limiteddepkit-0.1.0a1.dist-info/METADATA +1366 -0
  57. limiteddepkit-0.1.0a1.dist-info/RECORD +60 -0
  58. limiteddepkit-0.1.0a1.dist-info/WHEEL +5 -0
  59. limiteddepkit-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  60. 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
+ )