|
12 | 12 | POOLING_TYPES = Literal["none", "complete", "partial"] |
13 | 13 | valid_pooling = get_args(POOLING_TYPES) |
14 | 14 |
|
15 | | -CURVE_TYPES = Literal["log", "abc", "ns", "nss", "box-cox"] |
16 | | -valid_curves = get_args(CURVE_TYPES) |
17 | | - |
18 | | - |
19 | | -FEATURE_DICT = { |
20 | | - "log": ["slope"], |
21 | | - "box-cox": ["lambda", "slope", "intercept"], |
22 | | - "nss": ["tau", "beta0", "beta1", "beta2"], |
23 | | - "abc": ["a", "b", "c"], |
24 | | -} |
25 | | - |
26 | 15 |
|
27 | 16 | def _validate_pooling_params(pooling_columns: ColumnType, pooling: POOLING_TYPES): |
28 | 17 | """ |
@@ -191,237 +180,6 @@ def build(self, model=None): |
191 | 180 | return intercept |
192 | 181 |
|
193 | 182 |
|
194 | | -def build_curve( |
195 | | - time_pt: pt.TensorVariable, |
196 | | - beta: pt.TensorVariable, |
197 | | - curve_type: Literal["log", "abc", "ns", "nss", "box-cox"], |
198 | | -): |
199 | | - """ |
200 | | - Build a curve based on the time data and parameters beta. |
201 | | -
|
202 | | - In this context, a "curve" is a deterministic function that maps time to a value. The curve should (in general) be |
203 | | - strictly increasing with time (df(t)/dt > 0), and should (in general) exhibit diminishing marginal growth with time |
204 | | - (d^2f(t)/dt^2 < 0). These properties are not strictly necessary; some curve functions (such as nss) allow for |
205 | | - local reversals. |
206 | | -
|
207 | | - Parameters |
208 | | - ---------- |
209 | | - time_pt: TensorVariable |
210 | | - A pytensor variable representing the time data to build the curve from. |
211 | | - beta: TensorVariable |
212 | | - A pytensor variable representing the parameters of the curve. The number of parameters and their meaning depend |
213 | | - on the curve_type. |
214 | | -
|
215 | | - .. warning:: |
216 | | - Currently no checks are in place to ensure that the number of parameters in beta matches the expected number |
217 | | - for the curve_type. |
218 | | -
|
219 | | - curve_type: str, one of ["log", "abc", "ns", "nss", "box-cox"] |
220 | | - Type of curve to build. Options are: |
221 | | -
|
222 | | - - "log": |
223 | | - A simple log-linear curve. The curve is defined as: |
224 | | -
|
225 | | - .. math:: |
226 | | -
|
227 | | - \beta \\log(t) |
228 | | -
|
229 | | - - "abc": |
230 | | - A curve parameterized by "a", "b", and "c", such that the minimum value of the curve is "a", the |
231 | | - maximum value is "a + b", and the inflection point is "a + b / c". "C" thus controls the speed of change |
232 | | - from the minimum to the maximum value. The curve is defined as: |
233 | | -
|
234 | | - .. math:: |
235 | | -
|
236 | | - \frac{a + bc t}{1 + ct} |
237 | | -
|
238 | | - - "ns": |
239 | | - The Nelson-Siegel yield curve model. The curve is parameterized by three parameters: :math:`\tau`, |
240 | | - :math:`\beta_1`, and :math:`\beta_2`. :math:`\tau` is the decay rate of the exponential term, and |
241 | | - :math:`\beta_1` and :math:`\beta_2` control the slope and curvature of the curve. The curve is defined as: |
242 | | -
|
243 | | - .. math:: |
244 | | -
|
245 | | - \begin{align} |
246 | | - x_t &= \beta_1 \\phi(t) + \beta_2 \\left (\\phi(t) - \\exp(-t/\tau) \right ) \\ |
247 | | - \\phi(t) &= \frac{1 - \\exp(-t/\tau)}{t/\tau} |
248 | | - \\end{align} |
249 | | -
|
250 | | - - "nss": |
251 | | - The Nelson-Siegel-Svensson yield curve model. The curve is parameterized by four parameters: |
252 | | - :math:`\tau_1`, :math:`\tau_2`, :math:`\beta_1`, and :math:`\beta_2`. :math:`\beta_3` |
253 | | -
|
254 | | - Where :math:`\tau_1` and :math:`\tau_2` are the decay rates of the two exponential terms, :math:`\beta_1` |
255 | | - controls the slope of the curve, and :math:`\beta_2` and :math:`\beta_3` control the curvature of the curve. |
256 | | - To ensure that short-term rates are strictly postitive, one typically restrices :math:`\beta_1 + \beta_2 > 0`. |
257 | | -
|
258 | | - The curve is defined as: |
259 | | -
|
260 | | - .. math:: |
261 | | - \begin{align} |
262 | | - x_t & = \beta_1 \\phi_1(t) + \beta_2 \\left (\\phi_1(t) - \\exp(-t/\tau_1) \right) + \beta_3 \\left (\\phi_2(t) - \\exp(-t/\tau_2) \right) \\ |
263 | | - \\phi_1(t) &= \frac{1 - \\exp(-t/\tau_1)}{t/\tau_1} \\ |
264 | | - \\phi_2(t) &= \frac{1 - \\exp(-t/\tau_2)}{t/\tau_2} |
265 | | - \\end{align} |
266 | | -
|
267 | | - Note that this definition omits the constant term that is typically included in the Nelson-Siegel-Svensson; |
268 | | - you are assumed to have already accounted for this with another component in the model. |
269 | | -
|
270 | | - - "box-cox": |
271 | | - A curve that applies a box-cox transformation to the time data. The curve is parameterized by two |
272 | | - parameters: :math:`\\lambda` and :math:`\beta`, where :math:`\\lambda` is the box-cox parameter that |
273 | | - interpolates between the log and linear transformations, and :math:`\beta` is the slope of the curve. |
274 | | -
|
275 | | - The curve is defined as: |
276 | | -
|
277 | | - .. math:: |
278 | | -
|
279 | | - \beta \\left ( \frac{t^{\\lambda} - 1}{\\lambda} \right ) |
280 | | -
|
281 | | - Returns |
282 | | - ------- |
283 | | - TensorVariable |
284 | | - A pytensor variable representing the curve. |
285 | | - """ |
286 | | - if curve_type == "box-cox": |
287 | | - lam = beta[0] + 1e-12 |
288 | | - time_scaled = (time_pt**lam - 1) / lam |
289 | | - curve = beta[1] * time_scaled |
290 | | - |
291 | | - elif curve_type == "log": |
292 | | - time_scaled = pt.log(time_pt) |
293 | | - curve = beta[0] * time_scaled |
294 | | - |
295 | | - elif curve_type == "ns": |
296 | | - tau = pt.exp(beta[0]) |
297 | | - t_over_tau = time_pt / tau |
298 | | - time_scaled = (1 - pt.exp(-t_over_tau)) / t_over_tau |
299 | | - curve = beta[1] * time_scaled + beta[2] * (time_scaled - pt.exp(-t_over_tau)) |
300 | | - |
301 | | - elif curve_type == "nss": |
302 | | - tau = pt.exp(beta[:2]) |
303 | | - beta = beta[2:] |
304 | | - t_over_tau_1 = time_pt / tau[0] |
305 | | - t_over_tau_2 = time_pt / tau[1] |
306 | | - time_scaled_1 = (1 - pt.exp(t_over_tau_1)) / t_over_tau_1 |
307 | | - time_scaled_2 = (1 - pt.exp(t_over_tau_2)) / t_over_tau_2 |
308 | | - curve = ( |
309 | | - beta[0] * time_scaled_1 |
310 | | - + beta[1] * (time_scaled_1 - pt.exp(-t_over_tau_1)) |
311 | | - + beta[2] * (time_scaled_2 - pt.exp(-t_over_tau_2)) |
312 | | - ) |
313 | | - |
314 | | - elif curve_type == "abc": |
315 | | - curve = (beta[0] + beta[1] * beta[2] * time_pt) / (1 + beta[2] * time_pt) |
316 | | - |
317 | | - else: |
318 | | - raise ValueError(f"Unknown curve type: {curve_type}") |
319 | | - |
320 | | - return curve |
321 | | - |
322 | | - |
323 | | -class Curve(GLMModel): |
324 | | - def __init__( |
325 | | - self, |
326 | | - name: str, |
327 | | - t: pd.Series | pd.DataFrame, |
328 | | - prior: str = "Normal", |
329 | | - index_data: pd.Series | pd.DataFrame | None = None, |
330 | | - pooling: POOLING_TYPES = "complete", |
331 | | - curve_type: CURVE_TYPES = "log", |
332 | | - prior_params: dict | None = None, |
333 | | - hierarchical_params: dict | None = None, |
334 | | - ): |
335 | | - """ |
336 | | - Class to represent a curve in a GLM model. |
337 | | -
|
338 | | - A curve is a deterministic function that transforms time data via a non-linear function. Currently, the following |
339 | | - curve types are supported: |
340 | | - - "log": A simple log-linear curve. |
341 | | - - "abc": A curve defined by a minimum value (a), maximum value (b), and inflection point ((a + b) / c). |
342 | | - - "ns": The Nelson-Siegel yield curve model. |
343 | | - - "nss": The Nelson-Siegel-Svensson yield curve model. |
344 | | - - "box-cox": A curve that applies a box-cox transformation to the time data. |
345 | | -
|
346 | | - Parameters |
347 | | - ---------- |
348 | | - name: str, optional |
349 | | - Name of the intercept term. If None, a default name is generated based on the index_data. |
350 | | - t: Series |
351 | | - Time data used to build the curve. If Series, must have a name attribute. If dataframe, must have exactly |
352 | | - one column. |
353 | | - index_data: Series or DataFrame, optional |
354 | | - Index data used to build hierarchical priors. If there are multiple columns, the columns are treated as |
355 | | - levels of a "telescoping" hierarchy, with the leftmost column representing the top level of the hierarchy, |
356 | | - and depth increasing to the right. |
357 | | -
|
358 | | - The index of the index_data must match the index of the observed data. |
359 | | - prior: str, optional |
360 | | - Name of the PyMC distribution to use for the intercept term. Default is "Normal". |
361 | | - pooling: str, one of ["none", "complete", "partial"], default "complete" |
362 | | - Type of pooling to use for the intercept term. If "none", no pooling is applied, and each group in the |
363 | | - index_data is treated as independent. If "complete", complete pooling is applied, and all data are treated |
364 | | - as coming from the same group. If "partial", a hierarchical prior is constructed that shares information |
365 | | - across groups in the index_data. |
366 | | - curve_type: str, one of ["log", "abc", "ns", "nss", "box-cox"] |
367 | | - Type of curve to build. For details, see the build_curve function. |
368 | | - prior_params: dict, optional |
369 | | - Additional keyword arguments to pass to the PyMC distribution specified by the prior argument. |
370 | | - hierarchical_params: dict, optional |
371 | | - Additional keyword arguments to configure priors in the hierarchical_prior_to_requested_depth function. |
372 | | - Options include: |
373 | | - sigma_dist: str |
374 | | - Name of the distribution to use for the standard deviation of the hierarchy. Default is "Gamma" |
375 | | - sigma_kwargs: dict |
376 | | - Additional keyword arguments to pass to the sigma distribution specified by the sigma_dist argument. |
377 | | - Default is {"alpha": 2, "beta": 1} |
378 | | - offset_dist: str, one of ["zerosum", "normal", "laplace"] |
379 | | - Name of the distribution to use for the offset distribution. Default is "zerosum" |
380 | | - """ |
381 | | - |
382 | | - _validate_pooling_params(index_data, pooling) |
383 | | - |
384 | | - self.name = name |
385 | | - self.t = t if isinstance(t, pd.Series) else t.iloc[:, 0] |
386 | | - self.curve_type = curve_type |
387 | | - |
388 | | - self.index_data = index_data |
389 | | - self.pooling = pooling |
390 | | - |
391 | | - self.prior = prior |
392 | | - self.prior_params = prior_params if prior_params is not None else {} |
393 | | - self.hierarchical_params = hierarchical_params if hierarchical_params is not None else {} |
394 | | - |
395 | | - super().__init__() |
396 | | - |
397 | | - def build(self, model=None): |
398 | | - model = pm.modelcontext(model) |
399 | | - obs_dim = self.t.index.name |
400 | | - feature_dim = f"{self.name}_features" |
401 | | - if feature_dim not in model.coords: |
402 | | - model.add_coord(feature_dim, FEATURE_DICT[self.curve_type]) |
403 | | - |
404 | | - with model: |
405 | | - t_pt = pm.Data("t", self.t.values, dims=[obs_dim]) |
406 | | - if self.pooling == "complete": |
407 | | - beta = getattr(pm, self.prior)( |
408 | | - f"{self.name}_beta", **self.prior_params, dims=[feature_dim] |
409 | | - ) |
410 | | - curve = build_curve(t_pt, beta, self.curve_type) |
411 | | - return pm.Deterministic(f"{self.name}", curve, dims=[obs_dim]) |
412 | | - |
413 | | - beta = hierarchical_prior_to_requested_depth( |
414 | | - self.name, |
415 | | - self.index_data, |
416 | | - model=model, |
417 | | - dims=[feature_dim], |
418 | | - no_pooling=self.pooling == "none", |
419 | | - ) |
420 | | - |
421 | | - curve = build_curve(t_pt, beta, self.curve_type) |
422 | | - return pm.Deterministic(f"{self.name}", curve, dims=[obs_dim]) |
423 | | - |
424 | | - |
425 | 183 | class Regression(GLMModel): |
426 | 184 | def __init__( |
427 | 185 | self, |
|
0 commit comments