Source code for statsmodels.distributions.bernstein

"""
Created on Wed Feb 17 15:35:23 2021

Author: Josef Perktold
License: BSD-3

"""

from statsmodels.compat.pandas import deprecate_kwarg

import numpy as np
from scipy import stats

from statsmodels.distributions.tools import (
    _eval_bernstein_1d,
    _eval_bernstein_2d,
    _eval_bernstein_dd,
    _Grid,
    cdf2prob_grid,
    prob2cdf_grid,
)
from statsmodels.tools._decorators import cache_readonly
from statsmodels.tools.rng_qrng import check_random_state


[docs] class BernsteinDistribution: """Distribution based on Bernstein Polynomials on unit hypercube. Parameters ---------- cdf_grid : array_like cdf values on a equal spaced grid of the unit hypercube [0, 1]^d. The dimension of the arrays define how many random variables are included in the multivariate distribution. Attributes ---------- cdf_grid : ndarray Grid of cdf values. prob_grid : ndarray Grid of cell or bin probabilities. k_dim : int Number of components, dimension of the random variable. k_grid : tuple of int Shape of `cdf_grid`. k_grid_product : int Total number of bins in the grid. _grid : _Grid Instance with grid helper methods and attributes. """ def __init__(self, cdf_grid): self.cdf_grid = cdf_grid = np.asarray(cdf_grid) self.k_dim = cdf_grid.ndim self.k_grid = cdf_grid.shape self.k_grid_product = np.prod([i - 1 for i in self.k_grid]) self._grid = _Grid(self.k_grid)
[docs] @classmethod def from_data(cls, data, k_bins): """Create distribution instance from data using histogram binning. Classmethod to construct a distribution instance. Parameters ---------- data : array_like Data with observation in rows and random variables in columns. Data can be 1-dimensional in the univariate case. k_bins : int or list of int Number of bins to be used in numpy histogramdd for each component. If k_bins is a scalar int, then the number of bins of each component will be equal to it. Returns ------- BernsteinDistribution Instance of a Bernstein distribution. """ data = np.asarray(data) if np.any(data < 0) or np.any(data > 1): raise ValueError("data needs to be in [0, 1]") if data.ndim == 1: data = data[:, None] k_dim = data.shape[1] if np.size(k_bins) == 1: k_bins = [k_bins] * k_dim bins = [np.linspace(-1 / ni, 1, ni + 2) for ni in k_bins] c, e = np.histogramdd(data, bins=bins, density=False) # TODO: check when we have zero observations, which bin? # check bins start at 0 exept leading bin assert all(ei[1] == 0 for ei in e) c /= len(data) cdf_grid = prob2cdf_grid(c) return cls(cdf_grid)
[docs] @cache_readonly def prob_grid(self): return cdf2prob_grid(self.cdf_grid, prepend=None)
[docs] def cdf(self, x): """cdf values evaluated at x. Parameters ---------- x : array_like Points of multivariate random variable at which cdf is evaluated. This can be a single point with length equal to the dimension of the random variable, or two dimensional with points (observations) in rows and random variables in columns. In the univariate case, a 1-dimensional x will be interpreted as different points for evaluation. Returns ------- ndarray Cdf values evaluated at `x`. Notes ----- Warning: 2-dim x with many points can be memory intensive because currently the bernstein polynomials will be evaluated in a fully vectorized computation. """ x = np.asarray(x) if x.ndim == 1 and self.k_dim == 1: x = x[:, None] cdf_ = _eval_bernstein_dd(x, self.cdf_grid) return cdf_
[docs] def pdf(self, x): """pdf values evaluated at x. Parameters ---------- x : array_like Points of multivariate random variable at which pdf is evaluated. This can be a single point with length equal to the dimension of the random variable, or two dimensional with points (observations) in rows and random variables in columns. In the univariate case, a 1-dimensional x will be interpreted as different points for evaluation. Returns ------- ndarray Pdf values evaluated at `x`. Notes ----- Warning: 2-dim x with many points can be memory intensive because currently the bernstein polynomials will be evaluated in a fully vectorized computation. """ x = np.asarray(x) if x.ndim == 1 and self.k_dim == 1: x = x[:, None] # TODO: check usage of k_grid_product. Should this go into eval? pdf_ = self.k_grid_product * _eval_bernstein_dd(x, self.prob_grid) return pdf_
[docs] def get_marginal(self, idx): """Get marginal BernsteinDistribution. Parameters ---------- idx : int or list of int Index or indices of the component for which the marginal distribution is returned. Returns ------- BernsteinDistribution Instance for the marginal distribution. """ # univariate if self.k_dim == 1: return self sl = [-1] * self.k_dim if np.shape(idx) == (): idx = [idx] for ii in idx: sl[ii] = slice(None, None, None) cdf_m = self.cdf_grid[tuple(sl)] bpd_marginal = BernsteinDistribution(cdf_m) return bpd_marginal
[docs] @deprecate_kwarg("random_state", "rng") def rvs(self, nobs, rng=None): """Generate random numbers from distribution. Parameters ---------- nobs : int Number of random observations to generate. rng : int, array_like of int, numpy.random.Generator, or numpy.random.RandomState, optional If `rng` is None, a new ``Generator`` is created using fresh entropy from the operating system. If `rng` is an int or array of ints, a new ``Generator`` is created, seeded with `rng`. If `rng` is already a ``Generator`` or ``RandomState`` instance, that instance is used. rng : int, array_like of int, numpy.random.Generator, or numpy.random.RandomState, optional .. deprecated:: 0.15 random_state has been deprecated. In-line with SPEC-007, use rng for passing a random number generator or seed. Returns ------- ndarray Random samples from the Bernstein polynomial distribution, with `nobs` rows and `k_dim` columns. """ rng = check_random_state(rng) rvs_mnl = rng.multinomial(nobs, self.prob_grid.flatten()) k_comp = self.k_dim rvs_m = [] for i in range(len(rvs_mnl)): if rvs_mnl[i] != 0: idx = np.unravel_index(i, self.prob_grid.shape) rvsi = [] for j in range(k_comp): n = self.k_grid[j] xgi = self._grid.x_marginal[j][idx[j]] # Note: x_marginal starts at 0 # x_marginal ends with 1 but that is not used by idx rvsi.append( stats.beta.rvs( n * xgi + 1, n * (1 - xgi) + 0, size=rvs_mnl[i], random_state=rng, ) ) rvs_m.append(np.column_stack(rvsi)) rvsm = np.concatenate(rvs_m) return rvsm
[docs] class BernsteinDistributionBV(BernsteinDistribution): """Bivariate distribution based on Bernstein polynomials. Parameters ---------- cdf_grid : array_like cdf values on a equal spaced grid of the unit square [0, 1]^2. """
[docs] def cdf(self, x): """cdf values evaluated at x. Parameters ---------- x : array_like Points at which the bivariate cdf is evaluated. Can be a single point with two coordinates, or two dimensional with points (observations) in rows and the two variables in columns. Returns ------- ndarray Cdf values evaluated at `x`. """ cdf_ = _eval_bernstein_2d(x, self.cdf_grid) return cdf_
[docs] def pdf(self, x): """pdf values evaluated at x. Parameters ---------- x : array_like Points at which the bivariate pdf is evaluated. Can be a single point with two coordinates, or two dimensional with points (observations) in rows and the two variables in columns. Returns ------- ndarray Pdf values evaluated at `x`. """ # TODO: check usage of k_grid_product. Should this go into eval? pdf_ = self.k_grid_product * _eval_bernstein_2d(x, self.prob_grid) return pdf_
[docs] class BernsteinDistributionUV(BernsteinDistribution): """Univariate distribution based on Bernstein polynomials. Parameters ---------- cdf_grid : array_like cdf values on a equal spaced grid of the unit interval [0, 1]. """
[docs] def cdf(self, x, method="binom"): """cdf values evaluated at x. Parameters ---------- x : array_like Points at which the univariate cdf is evaluated. method : {"binom", "beta", "bpoly"}, optional Method used to construct the Bernstein polynomial basis. Returns ------- ndarray Cdf values evaluated at `x`. """ cdf_ = _eval_bernstein_1d(x, self.cdf_grid, method=method) return cdf_
[docs] def pdf(self, x, method="binom"): """pdf values evaluated at x. Parameters ---------- x : array_like Points at which the univariate pdf is evaluated. method : {"binom", "beta", "bpoly"}, optional Method used to construct the Bernstein polynomial basis. Returns ------- ndarray Pdf values evaluated at `x`. """ # TODO: check usage of k_grid_product. Should this go into eval? pdf_ = self.k_grid_product * _eval_bernstein_1d( x, self.prob_grid, method=method ) return pdf_