Coverage for /usr/lib/python3/dist-packages/scipy/stats/_multivariate.py: 27%

1787 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1# 

2# Author: Joris Vankerschaver 2013 

3# 

4import math 

5import numpy as np 

6from numpy import asarray_chkfinite, asarray 

7from numpy.lib import NumpyVersion 

8import scipy.linalg 

9from scipy._lib import doccer 

10from scipy.special import (gammaln, psi, multigammaln, xlogy, entr, betaln, 

11 ive, loggamma) 

12from scipy._lib._util import check_random_state 

13from scipy.linalg.blas import drot 

14from scipy.linalg._misc import LinAlgError 

15from scipy.linalg.lapack import get_lapack_funcs 

16from ._discrete_distns import binom 

17from . import _mvn, _covariance, _rcont 

18from ._qmvnt import _qmvt 

19from ._morestats import directional_stats 

20from scipy.optimize import root_scalar 

21 

22__all__ = ['multivariate_normal', 

23 'matrix_normal', 

24 'dirichlet', 

25 'dirichlet_multinomial', 

26 'wishart', 

27 'invwishart', 

28 'multinomial', 

29 'special_ortho_group', 

30 'ortho_group', 

31 'random_correlation', 

32 'unitary_group', 

33 'multivariate_t', 

34 'multivariate_hypergeom', 

35 'random_table', 

36 'uniform_direction', 

37 'vonmises_fisher'] 

38 

39_LOG_2PI = np.log(2 * np.pi) 

40_LOG_2 = np.log(2) 

41_LOG_PI = np.log(np.pi) 

42 

43 

44_doc_random_state = """\ 

45seed : {None, int, np.random.RandomState, np.random.Generator}, optional 

46 Used for drawing random variates. 

47 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

48 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

49 with seed. 

50 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

51 then that object is used. 

52 Default is `None`. 

53""" 

54 

55 

56def _squeeze_output(out): 

57 """ 

58 Remove single-dimensional entries from array and convert to scalar, 

59 if necessary. 

60 """ 

61 out = out.squeeze() 

62 if out.ndim == 0: 

63 out = out[()] 

64 return out 

65 

66 

67def _eigvalsh_to_eps(spectrum, cond=None, rcond=None): 

68 """Determine which eigenvalues are "small" given the spectrum. 

69 

70 This is for compatibility across various linear algebra functions 

71 that should agree about whether or not a Hermitian matrix is numerically 

72 singular and what is its numerical matrix rank. 

73 This is designed to be compatible with scipy.linalg.pinvh. 

74 

75 Parameters 

76 ---------- 

77 spectrum : 1d ndarray 

78 Array of eigenvalues of a Hermitian matrix. 

79 cond, rcond : float, optional 

80 Cutoff for small eigenvalues. 

81 Singular values smaller than rcond * largest_eigenvalue are 

82 considered zero. 

83 If None or -1, suitable machine precision is used. 

84 

85 Returns 

86 ------- 

87 eps : float 

88 Magnitude cutoff for numerical negligibility. 

89 

90 """ 

91 if rcond is not None: 

92 cond = rcond 

93 if cond in [None, -1]: 

94 t = spectrum.dtype.char.lower() 

95 factor = {'f': 1E3, 'd': 1E6} 

96 cond = factor[t] * np.finfo(t).eps 

97 eps = cond * np.max(abs(spectrum)) 

98 return eps 

99 

100 

101def _pinv_1d(v, eps=1e-5): 

102 """A helper function for computing the pseudoinverse. 

103 

104 Parameters 

105 ---------- 

106 v : iterable of numbers 

107 This may be thought of as a vector of eigenvalues or singular values. 

108 eps : float 

109 Values with magnitude no greater than eps are considered negligible. 

110 

111 Returns 

112 ------- 

113 v_pinv : 1d float ndarray 

114 A vector of pseudo-inverted numbers. 

115 

116 """ 

117 return np.array([0 if abs(x) <= eps else 1/x for x in v], dtype=float) 

118 

119 

120class _PSD: 

121 """ 

122 Compute coordinated functions of a symmetric positive semidefinite matrix. 

123 

124 This class addresses two issues. Firstly it allows the pseudoinverse, 

125 the logarithm of the pseudo-determinant, and the rank of the matrix 

126 to be computed using one call to eigh instead of three. 

127 Secondly it allows these functions to be computed in a way 

128 that gives mutually compatible results. 

129 All of the functions are computed with a common understanding as to 

130 which of the eigenvalues are to be considered negligibly small. 

131 The functions are designed to coordinate with scipy.linalg.pinvh() 

132 but not necessarily with np.linalg.det() or with np.linalg.matrix_rank(). 

133 

134 Parameters 

135 ---------- 

136 M : array_like 

137 Symmetric positive semidefinite matrix (2-D). 

138 cond, rcond : float, optional 

139 Cutoff for small eigenvalues. 

140 Singular values smaller than rcond * largest_eigenvalue are 

141 considered zero. 

142 If None or -1, suitable machine precision is used. 

143 lower : bool, optional 

144 Whether the pertinent array data is taken from the lower 

145 or upper triangle of M. (Default: lower) 

146 check_finite : bool, optional 

147 Whether to check that the input matrices contain only finite 

148 numbers. Disabling may give a performance gain, but may result 

149 in problems (crashes, non-termination) if the inputs do contain 

150 infinities or NaNs. 

151 allow_singular : bool, optional 

152 Whether to allow a singular matrix. (Default: True) 

153 

154 Notes 

155 ----- 

156 The arguments are similar to those of scipy.linalg.pinvh(). 

157 

158 """ 

159 

160 def __init__(self, M, cond=None, rcond=None, lower=True, 

161 check_finite=True, allow_singular=True): 

162 self._M = np.asarray(M) 

163 

164 # Compute the symmetric eigendecomposition. 

165 # Note that eigh takes care of array conversion, chkfinite, 

166 # and assertion that the matrix is square. 

167 s, u = scipy.linalg.eigh(M, lower=lower, check_finite=check_finite) 

168 

169 eps = _eigvalsh_to_eps(s, cond, rcond) 

170 if np.min(s) < -eps: 

171 msg = "The input matrix must be symmetric positive semidefinite." 

172 raise ValueError(msg) 

173 d = s[s > eps] 

174 if len(d) < len(s) and not allow_singular: 

175 msg = ("When `allow_singular is False`, the input matrix must be " 

176 "symmetric positive definite.") 

177 raise np.linalg.LinAlgError(msg) 

178 s_pinv = _pinv_1d(s, eps) 

179 U = np.multiply(u, np.sqrt(s_pinv)) 

180 

181 # Save the eigenvector basis, and tolerance for testing support 

182 self.eps = 1e3*eps 

183 self.V = u[:, s <= eps] 

184 

185 # Initialize the eagerly precomputed attributes. 

186 self.rank = len(d) 

187 self.U = U 

188 self.log_pdet = np.sum(np.log(d)) 

189 

190 # Initialize attributes to be lazily computed. 

191 self._pinv = None 

192 

193 def _support_mask(self, x): 

194 """ 

195 Check whether x lies in the support of the distribution. 

196 """ 

197 residual = np.linalg.norm(x @ self.V, axis=-1) 

198 in_support = residual < self.eps 

199 return in_support 

200 

201 @property 

202 def pinv(self): 

203 if self._pinv is None: 

204 self._pinv = np.dot(self.U, self.U.T) 

205 return self._pinv 

206 

207 

208class multi_rv_generic: 

209 """ 

210 Class which encapsulates common functionality between all multivariate 

211 distributions. 

212 """ 

213 def __init__(self, seed=None): 

214 super().__init__() 

215 self._random_state = check_random_state(seed) 

216 

217 @property 

218 def random_state(self): 

219 """ Get or set the Generator object for generating random variates. 

220 

221 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

222 singleton is used. 

223 If `seed` is an int, a new ``RandomState`` instance is used, 

224 seeded with `seed`. 

225 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

226 that instance is used. 

227 

228 """ 

229 return self._random_state 

230 

231 @random_state.setter 

232 def random_state(self, seed): 

233 self._random_state = check_random_state(seed) 

234 

235 def _get_random_state(self, random_state): 

236 if random_state is not None: 

237 return check_random_state(random_state) 

238 else: 

239 return self._random_state 

240 

241 

242class multi_rv_frozen: 

243 """ 

244 Class which encapsulates common functionality between all frozen 

245 multivariate distributions. 

246 """ 

247 @property 

248 def random_state(self): 

249 return self._dist._random_state 

250 

251 @random_state.setter 

252 def random_state(self, seed): 

253 self._dist._random_state = check_random_state(seed) 

254 

255 

256_mvn_doc_default_callparams = """\ 

257mean : array_like, default: ``[0]`` 

258 Mean of the distribution. 

259cov : array_like or `Covariance`, default: ``[1]`` 

260 Symmetric positive (semi)definite covariance matrix of the distribution. 

261allow_singular : bool, default: ``False`` 

262 Whether to allow a singular covariance matrix. This is ignored if `cov` is 

263 a `Covariance` object. 

264""" 

265 

266_mvn_doc_callparams_note = """\ 

267Setting the parameter `mean` to `None` is equivalent to having `mean` 

268be the zero-vector. The parameter `cov` can be a scalar, in which case 

269the covariance matrix is the identity times that value, a vector of 

270diagonal entries for the covariance matrix, a two-dimensional array_like, 

271or a `Covariance` object. 

272""" 

273 

274_mvn_doc_frozen_callparams = "" 

275 

276_mvn_doc_frozen_callparams_note = """\ 

277See class definition for a detailed description of parameters.""" 

278 

279mvn_docdict_params = { 

280 '_mvn_doc_default_callparams': _mvn_doc_default_callparams, 

281 '_mvn_doc_callparams_note': _mvn_doc_callparams_note, 

282 '_doc_random_state': _doc_random_state 

283} 

284 

285mvn_docdict_noparams = { 

286 '_mvn_doc_default_callparams': _mvn_doc_frozen_callparams, 

287 '_mvn_doc_callparams_note': _mvn_doc_frozen_callparams_note, 

288 '_doc_random_state': _doc_random_state 

289} 

290 

291 

292class multivariate_normal_gen(multi_rv_generic): 

293 r"""A multivariate normal random variable. 

294 

295 The `mean` keyword specifies the mean. The `cov` keyword specifies the 

296 covariance matrix. 

297 

298 Methods 

299 ------- 

300 pdf(x, mean=None, cov=1, allow_singular=False) 

301 Probability density function. 

302 logpdf(x, mean=None, cov=1, allow_singular=False) 

303 Log of the probability density function. 

304 cdf(x, mean=None, cov=1, allow_singular=False, maxpts=1000000*dim, abseps=1e-5, releps=1e-5, lower_limit=None) # noqa 

305 Cumulative distribution function. 

306 logcdf(x, mean=None, cov=1, allow_singular=False, maxpts=1000000*dim, abseps=1e-5, releps=1e-5) 

307 Log of the cumulative distribution function. 

308 rvs(mean=None, cov=1, size=1, random_state=None) 

309 Draw random samples from a multivariate normal distribution. 

310 entropy() 

311 Compute the differential entropy of the multivariate normal. 

312 

313 Parameters 

314 ---------- 

315 %(_mvn_doc_default_callparams)s 

316 %(_doc_random_state)s 

317 

318 Notes 

319 ----- 

320 %(_mvn_doc_callparams_note)s 

321 

322 The covariance matrix `cov` may be an instance of a subclass of 

323 `Covariance`, e.g. `scipy.stats.CovViaPrecision`. If so, `allow_singular` 

324 is ignored. 

325 

326 Otherwise, `cov` must be a symmetric positive semidefinite 

327 matrix when `allow_singular` is True; it must be (strictly) positive 

328 definite when `allow_singular` is False. 

329 Symmetry is not checked; only the lower triangular portion is used. 

330 The determinant and inverse of `cov` are computed 

331 as the pseudo-determinant and pseudo-inverse, respectively, so 

332 that `cov` does not need to have full rank. 

333 

334 The probability density function for `multivariate_normal` is 

335 

336 .. math:: 

337 

338 f(x) = \frac{1}{\sqrt{(2 \pi)^k \det \Sigma}} 

339 \exp\left( -\frac{1}{2} (x - \mu)^T \Sigma^{-1} (x - \mu) \right), 

340 

341 where :math:`\mu` is the mean, :math:`\Sigma` the covariance matrix, 

342 :math:`k` the rank of :math:`\Sigma`. In case of singular :math:`\Sigma`, 

343 SciPy extends this definition according to [1]_. 

344 

345 .. versionadded:: 0.14.0 

346 

347 References 

348 ---------- 

349 .. [1] Multivariate Normal Distribution - Degenerate Case, Wikipedia, 

350 https://en.wikipedia.org/wiki/Multivariate_normal_distribution#Degenerate_case 

351 

352 Examples 

353 -------- 

354 >>> import numpy as np 

355 >>> import matplotlib.pyplot as plt 

356 >>> from scipy.stats import multivariate_normal 

357 

358 >>> x = np.linspace(0, 5, 10, endpoint=False) 

359 >>> y = multivariate_normal.pdf(x, mean=2.5, cov=0.5); y 

360 array([ 0.00108914, 0.01033349, 0.05946514, 0.20755375, 0.43939129, 

361 0.56418958, 0.43939129, 0.20755375, 0.05946514, 0.01033349]) 

362 >>> fig1 = plt.figure() 

363 >>> ax = fig1.add_subplot(111) 

364 >>> ax.plot(x, y) 

365 >>> plt.show() 

366 

367 Alternatively, the object may be called (as a function) to fix the mean 

368 and covariance parameters, returning a "frozen" multivariate normal 

369 random variable: 

370 

371 >>> rv = multivariate_normal(mean=None, cov=1, allow_singular=False) 

372 >>> # Frozen object with the same methods but holding the given 

373 >>> # mean and covariance fixed. 

374 

375 The input quantiles can be any shape of array, as long as the last 

376 axis labels the components. This allows us for instance to 

377 display the frozen pdf for a non-isotropic random variable in 2D as 

378 follows: 

379 

380 >>> x, y = np.mgrid[-1:1:.01, -1:1:.01] 

381 >>> pos = np.dstack((x, y)) 

382 >>> rv = multivariate_normal([0.5, -0.2], [[2.0, 0.3], [0.3, 0.5]]) 

383 >>> fig2 = plt.figure() 

384 >>> ax2 = fig2.add_subplot(111) 

385 >>> ax2.contourf(x, y, rv.pdf(pos)) 

386 

387 """ 

388 

389 def __init__(self, seed=None): 

390 super().__init__(seed) 

391 self.__doc__ = doccer.docformat(self.__doc__, mvn_docdict_params) 

392 

393 def __call__(self, mean=None, cov=1, allow_singular=False, seed=None): 

394 """Create a frozen multivariate normal distribution. 

395 

396 See `multivariate_normal_frozen` for more information. 

397 """ 

398 return multivariate_normal_frozen(mean, cov, 

399 allow_singular=allow_singular, 

400 seed=seed) 

401 

402 def _process_parameters(self, mean, cov, allow_singular=True): 

403 """ 

404 Infer dimensionality from mean or covariance matrix, ensure that 

405 mean and covariance are full vector resp. matrix. 

406 """ 

407 if isinstance(cov, _covariance.Covariance): 

408 return self._process_parameters_Covariance(mean, cov) 

409 else: 

410 # Before `Covariance` classes were introduced, 

411 # `multivariate_normal` accepted plain arrays as `cov` and used the 

412 # following input validation. To avoid disturbing the behavior of 

413 # `multivariate_normal` when plain arrays are used, we use the 

414 # original input validation here. 

415 dim, mean, cov = self._process_parameters_psd(None, mean, cov) 

416 # After input validation, some methods then processed the arrays 

417 # with a `_PSD` object and used that to perform computation. 

418 # To avoid branching statements in each method depending on whether 

419 # `cov` is an array or `Covariance` object, we always process the 

420 # array with `_PSD`, and then use wrapper that satisfies the 

421 # `Covariance` interface, `CovViaPSD`. 

422 psd = _PSD(cov, allow_singular=allow_singular) 

423 cov_object = _covariance.CovViaPSD(psd) 

424 return dim, mean, cov_object 

425 

426 def _process_parameters_Covariance(self, mean, cov): 

427 dim = cov.shape[-1] 

428 mean = np.array([0.]) if mean is None else mean 

429 message = (f"`cov` represents a covariance matrix in {dim} dimensions," 

430 f"and so `mean` must be broadcastable to shape {(dim,)}") 

431 try: 

432 mean = np.broadcast_to(mean, dim) 

433 except ValueError as e: 

434 raise ValueError(message) from e 

435 return dim, mean, cov 

436 

437 def _process_parameters_psd(self, dim, mean, cov): 

438 # Try to infer dimensionality 

439 if dim is None: 

440 if mean is None: 

441 if cov is None: 

442 dim = 1 

443 else: 

444 cov = np.asarray(cov, dtype=float) 

445 if cov.ndim < 2: 

446 dim = 1 

447 else: 

448 dim = cov.shape[0] 

449 else: 

450 mean = np.asarray(mean, dtype=float) 

451 dim = mean.size 

452 else: 

453 if not np.isscalar(dim): 

454 raise ValueError("Dimension of random variable must be " 

455 "a scalar.") 

456 

457 # Check input sizes and return full arrays for mean and cov if 

458 # necessary 

459 if mean is None: 

460 mean = np.zeros(dim) 

461 mean = np.asarray(mean, dtype=float) 

462 

463 if cov is None: 

464 cov = 1.0 

465 cov = np.asarray(cov, dtype=float) 

466 

467 if dim == 1: 

468 mean = mean.reshape(1) 

469 cov = cov.reshape(1, 1) 

470 

471 if mean.ndim != 1 or mean.shape[0] != dim: 

472 raise ValueError("Array 'mean' must be a vector of length %d." % 

473 dim) 

474 if cov.ndim == 0: 

475 cov = cov * np.eye(dim) 

476 elif cov.ndim == 1: 

477 cov = np.diag(cov) 

478 elif cov.ndim == 2 and cov.shape != (dim, dim): 

479 rows, cols = cov.shape 

480 if rows != cols: 

481 msg = ("Array 'cov' must be square if it is two dimensional," 

482 " but cov.shape = %s." % str(cov.shape)) 

483 else: 

484 msg = ("Dimension mismatch: array 'cov' is of shape %s," 

485 " but 'mean' is a vector of length %d.") 

486 msg = msg % (str(cov.shape), len(mean)) 

487 raise ValueError(msg) 

488 elif cov.ndim > 2: 

489 raise ValueError("Array 'cov' must be at most two-dimensional," 

490 " but cov.ndim = %d" % cov.ndim) 

491 

492 return dim, mean, cov 

493 

494 def _process_quantiles(self, x, dim): 

495 """ 

496 Adjust quantiles array so that last axis labels the components of 

497 each data point. 

498 """ 

499 x = np.asarray(x, dtype=float) 

500 

501 if x.ndim == 0: 

502 x = x[np.newaxis] 

503 elif x.ndim == 1: 

504 if dim == 1: 

505 x = x[:, np.newaxis] 

506 else: 

507 x = x[np.newaxis, :] 

508 

509 return x 

510 

511 def _logpdf(self, x, mean, cov_object): 

512 """Log of the multivariate normal probability density function. 

513 

514 Parameters 

515 ---------- 

516 x : ndarray 

517 Points at which to evaluate the log of the probability 

518 density function 

519 mean : ndarray 

520 Mean of the distribution 

521 cov_object : Covariance 

522 An object representing the Covariance matrix 

523 

524 Notes 

525 ----- 

526 As this function does no argument checking, it should not be 

527 called directly; use 'logpdf' instead. 

528 

529 """ 

530 log_det_cov, rank = cov_object.log_pdet, cov_object.rank 

531 dev = x - mean 

532 if dev.ndim > 1: 

533 log_det_cov = log_det_cov[..., np.newaxis] 

534 rank = rank[..., np.newaxis] 

535 maha = np.sum(np.square(cov_object.whiten(dev)), axis=-1) 

536 return -0.5 * (rank * _LOG_2PI + log_det_cov + maha) 

537 

538 def logpdf(self, x, mean=None, cov=1, allow_singular=False): 

539 """Log of the multivariate normal probability density function. 

540 

541 Parameters 

542 ---------- 

543 x : array_like 

544 Quantiles, with the last axis of `x` denoting the components. 

545 %(_mvn_doc_default_callparams)s 

546 

547 Returns 

548 ------- 

549 pdf : ndarray or scalar 

550 Log of the probability density function evaluated at `x` 

551 

552 Notes 

553 ----- 

554 %(_mvn_doc_callparams_note)s 

555 

556 """ 

557 params = self._process_parameters(mean, cov, allow_singular) 

558 dim, mean, cov_object = params 

559 x = self._process_quantiles(x, dim) 

560 out = self._logpdf(x, mean, cov_object) 

561 if np.any(cov_object.rank < dim): 

562 out_of_bounds = ~cov_object._support_mask(x-mean) 

563 out[out_of_bounds] = -np.inf 

564 return _squeeze_output(out) 

565 

566 def pdf(self, x, mean=None, cov=1, allow_singular=False): 

567 """Multivariate normal probability density function. 

568 

569 Parameters 

570 ---------- 

571 x : array_like 

572 Quantiles, with the last axis of `x` denoting the components. 

573 %(_mvn_doc_default_callparams)s 

574 

575 Returns 

576 ------- 

577 pdf : ndarray or scalar 

578 Probability density function evaluated at `x` 

579 

580 Notes 

581 ----- 

582 %(_mvn_doc_callparams_note)s 

583 

584 """ 

585 params = self._process_parameters(mean, cov, allow_singular) 

586 dim, mean, cov_object = params 

587 x = self._process_quantiles(x, dim) 

588 out = np.exp(self._logpdf(x, mean, cov_object)) 

589 if np.any(cov_object.rank < dim): 

590 out_of_bounds = ~cov_object._support_mask(x-mean) 

591 out[out_of_bounds] = 0.0 

592 return _squeeze_output(out) 

593 

594 def _cdf(self, x, mean, cov, maxpts, abseps, releps, lower_limit): 

595 """Multivariate normal cumulative distribution function. 

596 

597 Parameters 

598 ---------- 

599 x : ndarray 

600 Points at which to evaluate the cumulative distribution function. 

601 mean : ndarray 

602 Mean of the distribution 

603 cov : array_like 

604 Covariance matrix of the distribution 

605 maxpts : integer 

606 The maximum number of points to use for integration 

607 abseps : float 

608 Absolute error tolerance 

609 releps : float 

610 Relative error tolerance 

611 lower_limit : array_like, optional 

612 Lower limit of integration of the cumulative distribution function. 

613 Default is negative infinity. Must be broadcastable with `x`. 

614 

615 Notes 

616 ----- 

617 As this function does no argument checking, it should not be 

618 called directly; use 'cdf' instead. 

619 

620 

621 .. versionadded:: 1.0.0 

622 

623 """ 

624 lower = (np.full(mean.shape, -np.inf) 

625 if lower_limit is None else lower_limit) 

626 # In 2d, _mvn.mvnun accepts input in which `lower` bound elements 

627 # are greater than `x`. Not so in other dimensions. Fix this by 

628 # ensuring that lower bounds are indeed lower when passed, then 

629 # set signs of resulting CDF manually. 

630 b, a = np.broadcast_arrays(x, lower) 

631 i_swap = b < a 

632 signs = (-1)**(i_swap.sum(axis=-1)) # odd # of swaps -> negative 

633 a, b = a.copy(), b.copy() 

634 a[i_swap], b[i_swap] = b[i_swap], a[i_swap] 

635 n = x.shape[-1] 

636 limits = np.concatenate((a, b), axis=-1) 

637 

638 # mvnun expects 1-d arguments, so process points sequentially 

639 def func1d(limits): 

640 return _mvn.mvnun(limits[:n], limits[n:], mean, cov, 

641 maxpts, abseps, releps)[0] 

642 

643 out = np.apply_along_axis(func1d, -1, limits) * signs 

644 return _squeeze_output(out) 

645 

646 def logcdf(self, x, mean=None, cov=1, allow_singular=False, maxpts=None, 

647 abseps=1e-5, releps=1e-5, *, lower_limit=None): 

648 """Log of the multivariate normal cumulative distribution function. 

649 

650 Parameters 

651 ---------- 

652 x : array_like 

653 Quantiles, with the last axis of `x` denoting the components. 

654 %(_mvn_doc_default_callparams)s 

655 maxpts : integer, optional 

656 The maximum number of points to use for integration 

657 (default `1000000*dim`) 

658 abseps : float, optional 

659 Absolute error tolerance (default 1e-5) 

660 releps : float, optional 

661 Relative error tolerance (default 1e-5) 

662 lower_limit : array_like, optional 

663 Lower limit of integration of the cumulative distribution function. 

664 Default is negative infinity. Must be broadcastable with `x`. 

665 

666 Returns 

667 ------- 

668 cdf : ndarray or scalar 

669 Log of the cumulative distribution function evaluated at `x` 

670 

671 Notes 

672 ----- 

673 %(_mvn_doc_callparams_note)s 

674 

675 .. versionadded:: 1.0.0 

676 

677 """ 

678 params = self._process_parameters(mean, cov, allow_singular) 

679 dim, mean, cov_object = params 

680 cov = cov_object.covariance 

681 x = self._process_quantiles(x, dim) 

682 if not maxpts: 

683 maxpts = 1000000 * dim 

684 cdf = self._cdf(x, mean, cov, maxpts, abseps, releps, lower_limit) 

685 # the log of a negative real is complex, and cdf can be negative 

686 # if lower limit is greater than upper limit 

687 cdf = cdf + 0j if np.any(cdf < 0) else cdf 

688 out = np.log(cdf) 

689 return out 

690 

691 def cdf(self, x, mean=None, cov=1, allow_singular=False, maxpts=None, 

692 abseps=1e-5, releps=1e-5, *, lower_limit=None): 

693 """Multivariate normal cumulative distribution function. 

694 

695 Parameters 

696 ---------- 

697 x : array_like 

698 Quantiles, with the last axis of `x` denoting the components. 

699 %(_mvn_doc_default_callparams)s 

700 maxpts : integer, optional 

701 The maximum number of points to use for integration 

702 (default `1000000*dim`) 

703 abseps : float, optional 

704 Absolute error tolerance (default 1e-5) 

705 releps : float, optional 

706 Relative error tolerance (default 1e-5) 

707 lower_limit : array_like, optional 

708 Lower limit of integration of the cumulative distribution function. 

709 Default is negative infinity. Must be broadcastable with `x`. 

710 

711 Returns 

712 ------- 

713 cdf : ndarray or scalar 

714 Cumulative distribution function evaluated at `x` 

715 

716 Notes 

717 ----- 

718 %(_mvn_doc_callparams_note)s 

719 

720 .. versionadded:: 1.0.0 

721 

722 """ 

723 params = self._process_parameters(mean, cov, allow_singular) 

724 dim, mean, cov_object = params 

725 cov = cov_object.covariance 

726 x = self._process_quantiles(x, dim) 

727 if not maxpts: 

728 maxpts = 1000000 * dim 

729 out = self._cdf(x, mean, cov, maxpts, abseps, releps, lower_limit) 

730 return out 

731 

732 def rvs(self, mean=None, cov=1, size=1, random_state=None): 

733 """Draw random samples from a multivariate normal distribution. 

734 

735 Parameters 

736 ---------- 

737 %(_mvn_doc_default_callparams)s 

738 size : integer, optional 

739 Number of samples to draw (default 1). 

740 %(_doc_random_state)s 

741 

742 Returns 

743 ------- 

744 rvs : ndarray or scalar 

745 Random variates of size (`size`, `N`), where `N` is the 

746 dimension of the random variable. 

747 

748 Notes 

749 ----- 

750 %(_mvn_doc_callparams_note)s 

751 

752 """ 

753 dim, mean, cov_object = self._process_parameters(mean, cov) 

754 random_state = self._get_random_state(random_state) 

755 

756 if isinstance(cov_object, _covariance.CovViaPSD): 

757 cov = cov_object.covariance 

758 out = random_state.multivariate_normal(mean, cov, size) 

759 out = _squeeze_output(out) 

760 else: 

761 size = size or tuple() 

762 if not np.iterable(size): 

763 size = (size,) 

764 shape = tuple(size) + (cov_object.shape[-1],) 

765 x = random_state.normal(size=shape) 

766 out = mean + cov_object.colorize(x) 

767 return out 

768 

769 def entropy(self, mean=None, cov=1): 

770 """Compute the differential entropy of the multivariate normal. 

771 

772 Parameters 

773 ---------- 

774 %(_mvn_doc_default_callparams)s 

775 

776 Returns 

777 ------- 

778 h : scalar 

779 Entropy of the multivariate normal distribution 

780 

781 Notes 

782 ----- 

783 %(_mvn_doc_callparams_note)s 

784 

785 """ 

786 dim, mean, cov_object = self._process_parameters(mean, cov) 

787 return 0.5 * (cov_object.rank * (_LOG_2PI + 1) + cov_object.log_pdet) 

788 

789 

790multivariate_normal = multivariate_normal_gen() 

791 

792 

793class multivariate_normal_frozen(multi_rv_frozen): 

794 def __init__(self, mean=None, cov=1, allow_singular=False, seed=None, 

795 maxpts=None, abseps=1e-5, releps=1e-5): 

796 """Create a frozen multivariate normal distribution. 

797 

798 Parameters 

799 ---------- 

800 mean : array_like, default: ``[0]`` 

801 Mean of the distribution. 

802 cov : array_like, default: ``[1]`` 

803 Symmetric positive (semi)definite covariance matrix of the 

804 distribution. 

805 allow_singular : bool, default: ``False`` 

806 Whether to allow a singular covariance matrix. 

807 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

808 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

809 singleton is used. 

810 If `seed` is an int, a new ``RandomState`` instance is used, 

811 seeded with `seed`. 

812 If `seed` is already a ``Generator`` or ``RandomState`` instance 

813 then that instance is used. 

814 maxpts : integer, optional 

815 The maximum number of points to use for integration of the 

816 cumulative distribution function (default `1000000*dim`) 

817 abseps : float, optional 

818 Absolute error tolerance for the cumulative distribution function 

819 (default 1e-5) 

820 releps : float, optional 

821 Relative error tolerance for the cumulative distribution function 

822 (default 1e-5) 

823 

824 Examples 

825 -------- 

826 When called with the default parameters, this will create a 1D random 

827 variable with mean 0 and covariance 1: 

828 

829 >>> from scipy.stats import multivariate_normal 

830 >>> r = multivariate_normal() 

831 >>> r.mean 

832 array([ 0.]) 

833 >>> r.cov 

834 array([[1.]]) 

835 

836 """ 

837 self._dist = multivariate_normal_gen(seed) 

838 self.dim, self.mean, self.cov_object = ( 

839 self._dist._process_parameters(mean, cov, allow_singular)) 

840 self.allow_singular = allow_singular or self.cov_object._allow_singular 

841 if not maxpts: 

842 maxpts = 1000000 * self.dim 

843 self.maxpts = maxpts 

844 self.abseps = abseps 

845 self.releps = releps 

846 

847 @property 

848 def cov(self): 

849 return self.cov_object.covariance 

850 

851 def logpdf(self, x): 

852 x = self._dist._process_quantiles(x, self.dim) 

853 out = self._dist._logpdf(x, self.mean, self.cov_object) 

854 if np.any(self.cov_object.rank < self.dim): 

855 out_of_bounds = ~self.cov_object._support_mask(x-self.mean) 

856 out[out_of_bounds] = -np.inf 

857 return _squeeze_output(out) 

858 

859 def pdf(self, x): 

860 return np.exp(self.logpdf(x)) 

861 

862 def logcdf(self, x, *, lower_limit=None): 

863 cdf = self.cdf(x, lower_limit=lower_limit) 

864 # the log of a negative real is complex, and cdf can be negative 

865 # if lower limit is greater than upper limit 

866 cdf = cdf + 0j if np.any(cdf < 0) else cdf 

867 out = np.log(cdf) 

868 return out 

869 

870 def cdf(self, x, *, lower_limit=None): 

871 x = self._dist._process_quantiles(x, self.dim) 

872 out = self._dist._cdf(x, self.mean, self.cov_object.covariance, 

873 self.maxpts, self.abseps, self.releps, 

874 lower_limit) 

875 return _squeeze_output(out) 

876 

877 def rvs(self, size=1, random_state=None): 

878 return self._dist.rvs(self.mean, self.cov_object, size, random_state) 

879 

880 def entropy(self): 

881 """Computes the differential entropy of the multivariate normal. 

882 

883 Returns 

884 ------- 

885 h : scalar 

886 Entropy of the multivariate normal distribution 

887 

888 """ 

889 log_pdet = self.cov_object.log_pdet 

890 rank = self.cov_object.rank 

891 return 0.5 * (rank * (_LOG_2PI + 1) + log_pdet) 

892 

893 

894# Set frozen generator docstrings from corresponding docstrings in 

895# multivariate_normal_gen and fill in default strings in class docstrings 

896for name in ['logpdf', 'pdf', 'logcdf', 'cdf', 'rvs']: 

897 method = multivariate_normal_gen.__dict__[name] 

898 method_frozen = multivariate_normal_frozen.__dict__[name] 

899 method_frozen.__doc__ = doccer.docformat(method.__doc__, 

900 mvn_docdict_noparams) 

901 method.__doc__ = doccer.docformat(method.__doc__, mvn_docdict_params) 

902 

903_matnorm_doc_default_callparams = """\ 

904mean : array_like, optional 

905 Mean of the distribution (default: `None`) 

906rowcov : array_like, optional 

907 Among-row covariance matrix of the distribution (default: `1`) 

908colcov : array_like, optional 

909 Among-column covariance matrix of the distribution (default: `1`) 

910""" 

911 

912_matnorm_doc_callparams_note = """\ 

913If `mean` is set to `None` then a matrix of zeros is used for the mean. 

914The dimensions of this matrix are inferred from the shape of `rowcov` and 

915`colcov`, if these are provided, or set to `1` if ambiguous. 

916 

917`rowcov` and `colcov` can be two-dimensional array_likes specifying the 

918covariance matrices directly. Alternatively, a one-dimensional array will 

919be be interpreted as the entries of a diagonal matrix, and a scalar or 

920zero-dimensional array will be interpreted as this value times the 

921identity matrix. 

922""" 

923 

924_matnorm_doc_frozen_callparams = "" 

925 

926_matnorm_doc_frozen_callparams_note = """\ 

927See class definition for a detailed description of parameters.""" 

928 

929matnorm_docdict_params = { 

930 '_matnorm_doc_default_callparams': _matnorm_doc_default_callparams, 

931 '_matnorm_doc_callparams_note': _matnorm_doc_callparams_note, 

932 '_doc_random_state': _doc_random_state 

933} 

934 

935matnorm_docdict_noparams = { 

936 '_matnorm_doc_default_callparams': _matnorm_doc_frozen_callparams, 

937 '_matnorm_doc_callparams_note': _matnorm_doc_frozen_callparams_note, 

938 '_doc_random_state': _doc_random_state 

939} 

940 

941 

942class matrix_normal_gen(multi_rv_generic): 

943 r"""A matrix normal random variable. 

944 

945 The `mean` keyword specifies the mean. The `rowcov` keyword specifies the 

946 among-row covariance matrix. The 'colcov' keyword specifies the 

947 among-column covariance matrix. 

948 

949 Methods 

950 ------- 

951 pdf(X, mean=None, rowcov=1, colcov=1) 

952 Probability density function. 

953 logpdf(X, mean=None, rowcov=1, colcov=1) 

954 Log of the probability density function. 

955 rvs(mean=None, rowcov=1, colcov=1, size=1, random_state=None) 

956 Draw random samples. 

957 entropy(rowcol=1, colcov=1) 

958 Differential entropy. 

959 

960 Parameters 

961 ---------- 

962 %(_matnorm_doc_default_callparams)s 

963 %(_doc_random_state)s 

964 

965 Notes 

966 ----- 

967 %(_matnorm_doc_callparams_note)s 

968 

969 The covariance matrices specified by `rowcov` and `colcov` must be 

970 (symmetric) positive definite. If the samples in `X` are 

971 :math:`m \times n`, then `rowcov` must be :math:`m \times m` and 

972 `colcov` must be :math:`n \times n`. `mean` must be the same shape as `X`. 

973 

974 The probability density function for `matrix_normal` is 

975 

976 .. math:: 

977 

978 f(X) = (2 \pi)^{-\frac{mn}{2}}|U|^{-\frac{n}{2}} |V|^{-\frac{m}{2}} 

979 \exp\left( -\frac{1}{2} \mathrm{Tr}\left[ U^{-1} (X-M) V^{-1} 

980 (X-M)^T \right] \right), 

981 

982 where :math:`M` is the mean, :math:`U` the among-row covariance matrix, 

983 :math:`V` the among-column covariance matrix. 

984 

985 The `allow_singular` behaviour of the `multivariate_normal` 

986 distribution is not currently supported. Covariance matrices must be 

987 full rank. 

988 

989 The `matrix_normal` distribution is closely related to the 

990 `multivariate_normal` distribution. Specifically, :math:`\mathrm{Vec}(X)` 

991 (the vector formed by concatenating the columns of :math:`X`) has a 

992 multivariate normal distribution with mean :math:`\mathrm{Vec}(M)` 

993 and covariance :math:`V \otimes U` (where :math:`\otimes` is the Kronecker 

994 product). Sampling and pdf evaluation are 

995 :math:`\mathcal{O}(m^3 + n^3 + m^2 n + m n^2)` for the matrix normal, but 

996 :math:`\mathcal{O}(m^3 n^3)` for the equivalent multivariate normal, 

997 making this equivalent form algorithmically inefficient. 

998 

999 .. versionadded:: 0.17.0 

1000 

1001 Examples 

1002 -------- 

1003 

1004 >>> import numpy as np 

1005 >>> from scipy.stats import matrix_normal 

1006 

1007 >>> M = np.arange(6).reshape(3,2); M 

1008 array([[0, 1], 

1009 [2, 3], 

1010 [4, 5]]) 

1011 >>> U = np.diag([1,2,3]); U 

1012 array([[1, 0, 0], 

1013 [0, 2, 0], 

1014 [0, 0, 3]]) 

1015 >>> V = 0.3*np.identity(2); V 

1016 array([[ 0.3, 0. ], 

1017 [ 0. , 0.3]]) 

1018 >>> X = M + 0.1; X 

1019 array([[ 0.1, 1.1], 

1020 [ 2.1, 3.1], 

1021 [ 4.1, 5.1]]) 

1022 >>> matrix_normal.pdf(X, mean=M, rowcov=U, colcov=V) 

1023 0.023410202050005054 

1024 

1025 >>> # Equivalent multivariate normal 

1026 >>> from scipy.stats import multivariate_normal 

1027 >>> vectorised_X = X.T.flatten() 

1028 >>> equiv_mean = M.T.flatten() 

1029 >>> equiv_cov = np.kron(V,U) 

1030 >>> multivariate_normal.pdf(vectorised_X, mean=equiv_mean, cov=equiv_cov) 

1031 0.023410202050005054 

1032 

1033 Alternatively, the object may be called (as a function) to fix the mean 

1034 and covariance parameters, returning a "frozen" matrix normal 

1035 random variable: 

1036 

1037 >>> rv = matrix_normal(mean=None, rowcov=1, colcov=1) 

1038 >>> # Frozen object with the same methods but holding the given 

1039 >>> # mean and covariance fixed. 

1040 

1041 """ 

1042 

1043 def __init__(self, seed=None): 

1044 super().__init__(seed) 

1045 self.__doc__ = doccer.docformat(self.__doc__, matnorm_docdict_params) 

1046 

1047 def __call__(self, mean=None, rowcov=1, colcov=1, seed=None): 

1048 """Create a frozen matrix normal distribution. 

1049 

1050 See `matrix_normal_frozen` for more information. 

1051 

1052 """ 

1053 return matrix_normal_frozen(mean, rowcov, colcov, seed=seed) 

1054 

1055 def _process_parameters(self, mean, rowcov, colcov): 

1056 """ 

1057 Infer dimensionality from mean or covariance matrices. Handle 

1058 defaults. Ensure compatible dimensions. 

1059 """ 

1060 

1061 # Process mean 

1062 if mean is not None: 

1063 mean = np.asarray(mean, dtype=float) 

1064 meanshape = mean.shape 

1065 if len(meanshape) != 2: 

1066 raise ValueError("Array `mean` must be two dimensional.") 

1067 if np.any(meanshape == 0): 

1068 raise ValueError("Array `mean` has invalid shape.") 

1069 

1070 # Process among-row covariance 

1071 rowcov = np.asarray(rowcov, dtype=float) 

1072 if rowcov.ndim == 0: 

1073 if mean is not None: 

1074 rowcov = rowcov * np.identity(meanshape[0]) 

1075 else: 

1076 rowcov = rowcov * np.identity(1) 

1077 elif rowcov.ndim == 1: 

1078 rowcov = np.diag(rowcov) 

1079 rowshape = rowcov.shape 

1080 if len(rowshape) != 2: 

1081 raise ValueError("`rowcov` must be a scalar or a 2D array.") 

1082 if rowshape[0] != rowshape[1]: 

1083 raise ValueError("Array `rowcov` must be square.") 

1084 if rowshape[0] == 0: 

1085 raise ValueError("Array `rowcov` has invalid shape.") 

1086 numrows = rowshape[0] 

1087 

1088 # Process among-column covariance 

1089 colcov = np.asarray(colcov, dtype=float) 

1090 if colcov.ndim == 0: 

1091 if mean is not None: 

1092 colcov = colcov * np.identity(meanshape[1]) 

1093 else: 

1094 colcov = colcov * np.identity(1) 

1095 elif colcov.ndim == 1: 

1096 colcov = np.diag(colcov) 

1097 colshape = colcov.shape 

1098 if len(colshape) != 2: 

1099 raise ValueError("`colcov` must be a scalar or a 2D array.") 

1100 if colshape[0] != colshape[1]: 

1101 raise ValueError("Array `colcov` must be square.") 

1102 if colshape[0] == 0: 

1103 raise ValueError("Array `colcov` has invalid shape.") 

1104 numcols = colshape[0] 

1105 

1106 # Ensure mean and covariances compatible 

1107 if mean is not None: 

1108 if meanshape[0] != numrows: 

1109 raise ValueError("Arrays `mean` and `rowcov` must have the " 

1110 "same number of rows.") 

1111 if meanshape[1] != numcols: 

1112 raise ValueError("Arrays `mean` and `colcov` must have the " 

1113 "same number of columns.") 

1114 else: 

1115 mean = np.zeros((numrows, numcols)) 

1116 

1117 dims = (numrows, numcols) 

1118 

1119 return dims, mean, rowcov, colcov 

1120 

1121 def _process_quantiles(self, X, dims): 

1122 """ 

1123 Adjust quantiles array so that last two axes labels the components of 

1124 each data point. 

1125 """ 

1126 X = np.asarray(X, dtype=float) 

1127 if X.ndim == 2: 

1128 X = X[np.newaxis, :] 

1129 if X.shape[-2:] != dims: 

1130 raise ValueError("The shape of array `X` is not compatible " 

1131 "with the distribution parameters.") 

1132 return X 

1133 

1134 def _logpdf(self, dims, X, mean, row_prec_rt, log_det_rowcov, 

1135 col_prec_rt, log_det_colcov): 

1136 """Log of the matrix normal probability density function. 

1137 

1138 Parameters 

1139 ---------- 

1140 dims : tuple 

1141 Dimensions of the matrix variates 

1142 X : ndarray 

1143 Points at which to evaluate the log of the probability 

1144 density function 

1145 mean : ndarray 

1146 Mean of the distribution 

1147 row_prec_rt : ndarray 

1148 A decomposition such that np.dot(row_prec_rt, row_prec_rt.T) 

1149 is the inverse of the among-row covariance matrix 

1150 log_det_rowcov : float 

1151 Logarithm of the determinant of the among-row covariance matrix 

1152 col_prec_rt : ndarray 

1153 A decomposition such that np.dot(col_prec_rt, col_prec_rt.T) 

1154 is the inverse of the among-column covariance matrix 

1155 log_det_colcov : float 

1156 Logarithm of the determinant of the among-column covariance matrix 

1157 

1158 Notes 

1159 ----- 

1160 As this function does no argument checking, it should not be 

1161 called directly; use 'logpdf' instead. 

1162 

1163 """ 

1164 numrows, numcols = dims 

1165 roll_dev = np.moveaxis(X-mean, -1, 0) 

1166 scale_dev = np.tensordot(col_prec_rt.T, 

1167 np.dot(roll_dev, row_prec_rt), 1) 

1168 maha = np.sum(np.sum(np.square(scale_dev), axis=-1), axis=0) 

1169 return -0.5 * (numrows*numcols*_LOG_2PI + numcols*log_det_rowcov 

1170 + numrows*log_det_colcov + maha) 

1171 

1172 def logpdf(self, X, mean=None, rowcov=1, colcov=1): 

1173 """Log of the matrix normal probability density function. 

1174 

1175 Parameters 

1176 ---------- 

1177 X : array_like 

1178 Quantiles, with the last two axes of `X` denoting the components. 

1179 %(_matnorm_doc_default_callparams)s 

1180 

1181 Returns 

1182 ------- 

1183 logpdf : ndarray 

1184 Log of the probability density function evaluated at `X` 

1185 

1186 Notes 

1187 ----- 

1188 %(_matnorm_doc_callparams_note)s 

1189 

1190 """ 

1191 dims, mean, rowcov, colcov = self._process_parameters(mean, rowcov, 

1192 colcov) 

1193 X = self._process_quantiles(X, dims) 

1194 rowpsd = _PSD(rowcov, allow_singular=False) 

1195 colpsd = _PSD(colcov, allow_singular=False) 

1196 out = self._logpdf(dims, X, mean, rowpsd.U, rowpsd.log_pdet, colpsd.U, 

1197 colpsd.log_pdet) 

1198 return _squeeze_output(out) 

1199 

1200 def pdf(self, X, mean=None, rowcov=1, colcov=1): 

1201 """Matrix normal probability density function. 

1202 

1203 Parameters 

1204 ---------- 

1205 X : array_like 

1206 Quantiles, with the last two axes of `X` denoting the components. 

1207 %(_matnorm_doc_default_callparams)s 

1208 

1209 Returns 

1210 ------- 

1211 pdf : ndarray 

1212 Probability density function evaluated at `X` 

1213 

1214 Notes 

1215 ----- 

1216 %(_matnorm_doc_callparams_note)s 

1217 

1218 """ 

1219 return np.exp(self.logpdf(X, mean, rowcov, colcov)) 

1220 

1221 def rvs(self, mean=None, rowcov=1, colcov=1, size=1, random_state=None): 

1222 """Draw random samples from a matrix normal distribution. 

1223 

1224 Parameters 

1225 ---------- 

1226 %(_matnorm_doc_default_callparams)s 

1227 size : integer, optional 

1228 Number of samples to draw (default 1). 

1229 %(_doc_random_state)s 

1230 

1231 Returns 

1232 ------- 

1233 rvs : ndarray or scalar 

1234 Random variates of size (`size`, `dims`), where `dims` is the 

1235 dimension of the random matrices. 

1236 

1237 Notes 

1238 ----- 

1239 %(_matnorm_doc_callparams_note)s 

1240 

1241 """ 

1242 size = int(size) 

1243 dims, mean, rowcov, colcov = self._process_parameters(mean, rowcov, 

1244 colcov) 

1245 rowchol = scipy.linalg.cholesky(rowcov, lower=True) 

1246 colchol = scipy.linalg.cholesky(colcov, lower=True) 

1247 random_state = self._get_random_state(random_state) 

1248 # We aren't generating standard normal variates with size=(size, 

1249 # dims[0], dims[1]) directly to ensure random variates remain backwards 

1250 # compatible. See https://github.com/scipy/scipy/pull/12312 for more 

1251 # details. 

1252 std_norm = random_state.standard_normal( 

1253 size=(dims[1], size, dims[0]) 

1254 ).transpose(1, 2, 0) 

1255 out = mean + np.einsum('jp,ipq,kq->ijk', 

1256 rowchol, std_norm, colchol, 

1257 optimize=True) 

1258 if size == 1: 

1259 out = out.reshape(mean.shape) 

1260 return out 

1261 

1262 def entropy(self, rowcov=1, colcov=1): 

1263 """Log of the matrix normal probability density function. 

1264 

1265 Parameters 

1266 ---------- 

1267 rowcov : array_like, optional 

1268 Among-row covariance matrix of the distribution (default: `1`) 

1269 colcov : array_like, optional 

1270 Among-column covariance matrix of the distribution (default: `1`) 

1271 

1272 Returns 

1273 ------- 

1274 entropy : float 

1275 Entropy of the distribution 

1276 

1277 Notes 

1278 ----- 

1279 %(_matnorm_doc_callparams_note)s 

1280 

1281 """ 

1282 dummy_mean = np.zeros((rowcov.shape[0], colcov.shape[0])) 

1283 dims, _, rowcov, colcov = self._process_parameters(dummy_mean, 

1284 rowcov, 

1285 colcov) 

1286 rowpsd = _PSD(rowcov, allow_singular=False) 

1287 colpsd = _PSD(colcov, allow_singular=False) 

1288 

1289 return self._entropy(dims, rowpsd.log_pdet, colpsd.log_pdet) 

1290 

1291 def _entropy(self, dims, row_cov_logdet, col_cov_logdet): 

1292 n, p = dims 

1293 return (0.5 * n * p * (1 + _LOG_2PI) + 0.5 * p * row_cov_logdet + 

1294 0.5 * n * col_cov_logdet) 

1295 

1296 

1297matrix_normal = matrix_normal_gen() 

1298 

1299 

1300class matrix_normal_frozen(multi_rv_frozen): 

1301 """ 

1302 Create a frozen matrix normal distribution. 

1303 

1304 Parameters 

1305 ---------- 

1306 %(_matnorm_doc_default_callparams)s 

1307 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

1308 If `seed` is `None` the `~np.random.RandomState` singleton is used. 

1309 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

1310 with seed. 

1311 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

1312 then that object is used. 

1313 Default is `None`. 

1314 

1315 Examples 

1316 -------- 

1317 >>> import numpy as np 

1318 >>> from scipy.stats import matrix_normal 

1319 

1320 >>> distn = matrix_normal(mean=np.zeros((3,3))) 

1321 >>> X = distn.rvs(); X 

1322 array([[-0.02976962, 0.93339138, -0.09663178], 

1323 [ 0.67405524, 0.28250467, -0.93308929], 

1324 [-0.31144782, 0.74535536, 1.30412916]]) 

1325 >>> distn.pdf(X) 

1326 2.5160642368346784e-05 

1327 >>> distn.logpdf(X) 

1328 -10.590229595124615 

1329 """ 

1330 

1331 def __init__(self, mean=None, rowcov=1, colcov=1, seed=None): 

1332 self._dist = matrix_normal_gen(seed) 

1333 self.dims, self.mean, self.rowcov, self.colcov = \ 

1334 self._dist._process_parameters(mean, rowcov, colcov) 

1335 self.rowpsd = _PSD(self.rowcov, allow_singular=False) 

1336 self.colpsd = _PSD(self.colcov, allow_singular=False) 

1337 

1338 def logpdf(self, X): 

1339 X = self._dist._process_quantiles(X, self.dims) 

1340 out = self._dist._logpdf(self.dims, X, self.mean, self.rowpsd.U, 

1341 self.rowpsd.log_pdet, self.colpsd.U, 

1342 self.colpsd.log_pdet) 

1343 return _squeeze_output(out) 

1344 

1345 def pdf(self, X): 

1346 return np.exp(self.logpdf(X)) 

1347 

1348 def rvs(self, size=1, random_state=None): 

1349 return self._dist.rvs(self.mean, self.rowcov, self.colcov, size, 

1350 random_state) 

1351 

1352 def entropy(self): 

1353 return self._dist._entropy(self.dims, self.rowpsd.log_pdet, 

1354 self.colpsd.log_pdet) 

1355 

1356 

1357# Set frozen generator docstrings from corresponding docstrings in 

1358# matrix_normal_gen and fill in default strings in class docstrings 

1359for name in ['logpdf', 'pdf', 'rvs', 'entropy']: 

1360 method = matrix_normal_gen.__dict__[name] 

1361 method_frozen = matrix_normal_frozen.__dict__[name] 

1362 method_frozen.__doc__ = doccer.docformat(method.__doc__, 

1363 matnorm_docdict_noparams) 

1364 method.__doc__ = doccer.docformat(method.__doc__, matnorm_docdict_params) 

1365 

1366_dirichlet_doc_default_callparams = """\ 

1367alpha : array_like 

1368 The concentration parameters. The number of entries determines the 

1369 dimensionality of the distribution. 

1370""" 

1371_dirichlet_doc_frozen_callparams = "" 

1372 

1373_dirichlet_doc_frozen_callparams_note = """\ 

1374See class definition for a detailed description of parameters.""" 

1375 

1376dirichlet_docdict_params = { 

1377 '_dirichlet_doc_default_callparams': _dirichlet_doc_default_callparams, 

1378 '_doc_random_state': _doc_random_state 

1379} 

1380 

1381dirichlet_docdict_noparams = { 

1382 '_dirichlet_doc_default_callparams': _dirichlet_doc_frozen_callparams, 

1383 '_doc_random_state': _doc_random_state 

1384} 

1385 

1386 

1387def _dirichlet_check_parameters(alpha): 

1388 alpha = np.asarray(alpha) 

1389 if np.min(alpha) <= 0: 

1390 raise ValueError("All parameters must be greater than 0") 

1391 elif alpha.ndim != 1: 

1392 raise ValueError("Parameter vector 'a' must be one dimensional, " 

1393 "but a.shape = {}.".format(alpha.shape)) 

1394 return alpha 

1395 

1396 

1397def _dirichlet_check_input(alpha, x): 

1398 x = np.asarray(x) 

1399 

1400 if x.shape[0] + 1 != alpha.shape[0] and x.shape[0] != alpha.shape[0]: 

1401 raise ValueError("Vector 'x' must have either the same number " 

1402 "of entries as, or one entry fewer than, " 

1403 "parameter vector 'a', but alpha.shape = {} " 

1404 "and x.shape = {}.".format(alpha.shape, x.shape)) 

1405 

1406 if x.shape[0] != alpha.shape[0]: 

1407 xk = np.array([1 - np.sum(x, 0)]) 

1408 if xk.ndim == 1: 

1409 x = np.append(x, xk) 

1410 elif xk.ndim == 2: 

1411 x = np.vstack((x, xk)) 

1412 else: 

1413 raise ValueError("The input must be one dimensional or a two " 

1414 "dimensional matrix containing the entries.") 

1415 

1416 if np.min(x) < 0: 

1417 raise ValueError("Each entry in 'x' must be greater than or equal " 

1418 "to zero.") 

1419 

1420 if np.max(x) > 1: 

1421 raise ValueError("Each entry in 'x' must be smaller or equal one.") 

1422 

1423 # Check x_i > 0 or alpha_i > 1 

1424 xeq0 = (x == 0) 

1425 alphalt1 = (alpha < 1) 

1426 if x.shape != alpha.shape: 

1427 alphalt1 = np.repeat(alphalt1, x.shape[-1], axis=-1).reshape(x.shape) 

1428 chk = np.logical_and(xeq0, alphalt1) 

1429 

1430 if np.sum(chk): 

1431 raise ValueError("Each entry in 'x' must be greater than zero if its " 

1432 "alpha is less than one.") 

1433 

1434 if (np.abs(np.sum(x, 0) - 1.0) > 10e-10).any(): 

1435 raise ValueError("The input vector 'x' must lie within the normal " 

1436 "simplex. but np.sum(x, 0) = %s." % np.sum(x, 0)) 

1437 

1438 return x 

1439 

1440 

1441def _lnB(alpha): 

1442 r"""Internal helper function to compute the log of the useful quotient. 

1443 

1444 .. math:: 

1445 

1446 B(\alpha) = \frac{\prod_{i=1}{K}\Gamma(\alpha_i)} 

1447 {\Gamma\left(\sum_{i=1}^{K} \alpha_i \right)} 

1448 

1449 Parameters 

1450 ---------- 

1451 %(_dirichlet_doc_default_callparams)s 

1452 

1453 Returns 

1454 ------- 

1455 B : scalar 

1456 Helper quotient, internal use only 

1457 

1458 """ 

1459 return np.sum(gammaln(alpha)) - gammaln(np.sum(alpha)) 

1460 

1461 

1462class dirichlet_gen(multi_rv_generic): 

1463 r"""A Dirichlet random variable. 

1464 

1465 The ``alpha`` keyword specifies the concentration parameters of the 

1466 distribution. 

1467 

1468 .. versionadded:: 0.15.0 

1469 

1470 Methods 

1471 ------- 

1472 pdf(x, alpha) 

1473 Probability density function. 

1474 logpdf(x, alpha) 

1475 Log of the probability density function. 

1476 rvs(alpha, size=1, random_state=None) 

1477 Draw random samples from a Dirichlet distribution. 

1478 mean(alpha) 

1479 The mean of the Dirichlet distribution 

1480 var(alpha) 

1481 The variance of the Dirichlet distribution 

1482 entropy(alpha) 

1483 Compute the differential entropy of the Dirichlet distribution. 

1484 

1485 Parameters 

1486 ---------- 

1487 %(_dirichlet_doc_default_callparams)s 

1488 %(_doc_random_state)s 

1489 

1490 Notes 

1491 ----- 

1492 Each :math:`\alpha` entry must be positive. The distribution has only 

1493 support on the simplex defined by 

1494 

1495 .. math:: 

1496 \sum_{i=1}^{K} x_i = 1 

1497 

1498 where :math:`0 < x_i < 1`. 

1499 

1500 If the quantiles don't lie within the simplex, a ValueError is raised. 

1501 

1502 The probability density function for `dirichlet` is 

1503 

1504 .. math:: 

1505 

1506 f(x) = \frac{1}{\mathrm{B}(\boldsymbol\alpha)} \prod_{i=1}^K x_i^{\alpha_i - 1} 

1507 

1508 where 

1509 

1510 .. math:: 

1511 

1512 \mathrm{B}(\boldsymbol\alpha) = \frac{\prod_{i=1}^K \Gamma(\alpha_i)} 

1513 {\Gamma\bigl(\sum_{i=1}^K \alpha_i\bigr)} 

1514 

1515 and :math:`\boldsymbol\alpha=(\alpha_1,\ldots,\alpha_K)`, the 

1516 concentration parameters and :math:`K` is the dimension of the space 

1517 where :math:`x` takes values. 

1518 

1519 Note that the `dirichlet` interface is somewhat inconsistent. 

1520 The array returned by the rvs function is transposed 

1521 with respect to the format expected by the pdf and logpdf. 

1522 

1523 Examples 

1524 -------- 

1525 >>> import numpy as np 

1526 >>> from scipy.stats import dirichlet 

1527 

1528 Generate a dirichlet random variable 

1529 

1530 >>> quantiles = np.array([0.2, 0.2, 0.6]) # specify quantiles 

1531 >>> alpha = np.array([0.4, 5, 15]) # specify concentration parameters 

1532 >>> dirichlet.pdf(quantiles, alpha) 

1533 0.2843831684937255 

1534 

1535 The same PDF but following a log scale 

1536 

1537 >>> dirichlet.logpdf(quantiles, alpha) 

1538 -1.2574327653159187 

1539 

1540 Once we specify the dirichlet distribution 

1541 we can then calculate quantities of interest 

1542 

1543 >>> dirichlet.mean(alpha) # get the mean of the distribution 

1544 array([0.01960784, 0.24509804, 0.73529412]) 

1545 >>> dirichlet.var(alpha) # get variance 

1546 array([0.00089829, 0.00864603, 0.00909517]) 

1547 >>> dirichlet.entropy(alpha) # calculate the differential entropy 

1548 -4.3280162474082715 

1549 

1550 We can also return random samples from the distribution 

1551 

1552 >>> dirichlet.rvs(alpha, size=1, random_state=1) 

1553 array([[0.00766178, 0.24670518, 0.74563305]]) 

1554 >>> dirichlet.rvs(alpha, size=2, random_state=2) 

1555 array([[0.01639427, 0.1292273 , 0.85437844], 

1556 [0.00156917, 0.19033695, 0.80809388]]) 

1557 

1558 Alternatively, the object may be called (as a function) to fix 

1559 concentration parameters, returning a "frozen" Dirichlet 

1560 random variable: 

1561 

1562 >>> rv = dirichlet(alpha) 

1563 >>> # Frozen object with the same methods but holding the given 

1564 >>> # concentration parameters fixed. 

1565 

1566 """ 

1567 

1568 def __init__(self, seed=None): 

1569 super().__init__(seed) 

1570 self.__doc__ = doccer.docformat(self.__doc__, dirichlet_docdict_params) 

1571 

1572 def __call__(self, alpha, seed=None): 

1573 return dirichlet_frozen(alpha, seed=seed) 

1574 

1575 def _logpdf(self, x, alpha): 

1576 """Log of the Dirichlet probability density function. 

1577 

1578 Parameters 

1579 ---------- 

1580 x : ndarray 

1581 Points at which to evaluate the log of the probability 

1582 density function 

1583 %(_dirichlet_doc_default_callparams)s 

1584 

1585 Notes 

1586 ----- 

1587 As this function does no argument checking, it should not be 

1588 called directly; use 'logpdf' instead. 

1589 

1590 """ 

1591 lnB = _lnB(alpha) 

1592 return - lnB + np.sum((xlogy(alpha - 1, x.T)).T, 0) 

1593 

1594 def logpdf(self, x, alpha): 

1595 """Log of the Dirichlet probability density function. 

1596 

1597 Parameters 

1598 ---------- 

1599 x : array_like 

1600 Quantiles, with the last axis of `x` denoting the components. 

1601 %(_dirichlet_doc_default_callparams)s 

1602 

1603 Returns 

1604 ------- 

1605 pdf : ndarray or scalar 

1606 Log of the probability density function evaluated at `x`. 

1607 

1608 """ 

1609 alpha = _dirichlet_check_parameters(alpha) 

1610 x = _dirichlet_check_input(alpha, x) 

1611 

1612 out = self._logpdf(x, alpha) 

1613 return _squeeze_output(out) 

1614 

1615 def pdf(self, x, alpha): 

1616 """The Dirichlet probability density function. 

1617 

1618 Parameters 

1619 ---------- 

1620 x : array_like 

1621 Quantiles, with the last axis of `x` denoting the components. 

1622 %(_dirichlet_doc_default_callparams)s 

1623 

1624 Returns 

1625 ------- 

1626 pdf : ndarray or scalar 

1627 The probability density function evaluated at `x`. 

1628 

1629 """ 

1630 alpha = _dirichlet_check_parameters(alpha) 

1631 x = _dirichlet_check_input(alpha, x) 

1632 

1633 out = np.exp(self._logpdf(x, alpha)) 

1634 return _squeeze_output(out) 

1635 

1636 def mean(self, alpha): 

1637 """Mean of the Dirichlet distribution. 

1638 

1639 Parameters 

1640 ---------- 

1641 %(_dirichlet_doc_default_callparams)s 

1642 

1643 Returns 

1644 ------- 

1645 mu : ndarray or scalar 

1646 Mean of the Dirichlet distribution. 

1647 

1648 """ 

1649 alpha = _dirichlet_check_parameters(alpha) 

1650 

1651 out = alpha / (np.sum(alpha)) 

1652 return _squeeze_output(out) 

1653 

1654 def var(self, alpha): 

1655 """Variance of the Dirichlet distribution. 

1656 

1657 Parameters 

1658 ---------- 

1659 %(_dirichlet_doc_default_callparams)s 

1660 

1661 Returns 

1662 ------- 

1663 v : ndarray or scalar 

1664 Variance of the Dirichlet distribution. 

1665 

1666 """ 

1667 

1668 alpha = _dirichlet_check_parameters(alpha) 

1669 

1670 alpha0 = np.sum(alpha) 

1671 out = (alpha * (alpha0 - alpha)) / ((alpha0 * alpha0) * (alpha0 + 1)) 

1672 return _squeeze_output(out) 

1673 

1674 def entropy(self, alpha): 

1675 """ 

1676 Differential entropy of the Dirichlet distribution. 

1677 

1678 Parameters 

1679 ---------- 

1680 %(_dirichlet_doc_default_callparams)s 

1681 

1682 Returns 

1683 ------- 

1684 h : scalar 

1685 Entropy of the Dirichlet distribution 

1686 

1687 """ 

1688 

1689 alpha = _dirichlet_check_parameters(alpha) 

1690 

1691 alpha0 = np.sum(alpha) 

1692 lnB = _lnB(alpha) 

1693 K = alpha.shape[0] 

1694 

1695 out = lnB + (alpha0 - K) * scipy.special.psi(alpha0) - np.sum( 

1696 (alpha - 1) * scipy.special.psi(alpha)) 

1697 return _squeeze_output(out) 

1698 

1699 def rvs(self, alpha, size=1, random_state=None): 

1700 """ 

1701 Draw random samples from a Dirichlet distribution. 

1702 

1703 Parameters 

1704 ---------- 

1705 %(_dirichlet_doc_default_callparams)s 

1706 size : int, optional 

1707 Number of samples to draw (default 1). 

1708 %(_doc_random_state)s 

1709 

1710 Returns 

1711 ------- 

1712 rvs : ndarray or scalar 

1713 Random variates of size (`size`, `N`), where `N` is the 

1714 dimension of the random variable. 

1715 

1716 """ 

1717 alpha = _dirichlet_check_parameters(alpha) 

1718 random_state = self._get_random_state(random_state) 

1719 return random_state.dirichlet(alpha, size=size) 

1720 

1721 

1722dirichlet = dirichlet_gen() 

1723 

1724 

1725class dirichlet_frozen(multi_rv_frozen): 

1726 def __init__(self, alpha, seed=None): 

1727 self.alpha = _dirichlet_check_parameters(alpha) 

1728 self._dist = dirichlet_gen(seed) 

1729 

1730 def logpdf(self, x): 

1731 return self._dist.logpdf(x, self.alpha) 

1732 

1733 def pdf(self, x): 

1734 return self._dist.pdf(x, self.alpha) 

1735 

1736 def mean(self): 

1737 return self._dist.mean(self.alpha) 

1738 

1739 def var(self): 

1740 return self._dist.var(self.alpha) 

1741 

1742 def entropy(self): 

1743 return self._dist.entropy(self.alpha) 

1744 

1745 def rvs(self, size=1, random_state=None): 

1746 return self._dist.rvs(self.alpha, size, random_state) 

1747 

1748 

1749# Set frozen generator docstrings from corresponding docstrings in 

1750# multivariate_normal_gen and fill in default strings in class docstrings 

1751for name in ['logpdf', 'pdf', 'rvs', 'mean', 'var', 'entropy']: 

1752 method = dirichlet_gen.__dict__[name] 

1753 method_frozen = dirichlet_frozen.__dict__[name] 

1754 method_frozen.__doc__ = doccer.docformat( 

1755 method.__doc__, dirichlet_docdict_noparams) 

1756 method.__doc__ = doccer.docformat(method.__doc__, dirichlet_docdict_params) 

1757 

1758 

1759_wishart_doc_default_callparams = """\ 

1760df : int 

1761 Degrees of freedom, must be greater than or equal to dimension of the 

1762 scale matrix 

1763scale : array_like 

1764 Symmetric positive definite scale matrix of the distribution 

1765""" 

1766 

1767_wishart_doc_callparams_note = "" 

1768 

1769_wishart_doc_frozen_callparams = "" 

1770 

1771_wishart_doc_frozen_callparams_note = """\ 

1772See class definition for a detailed description of parameters.""" 

1773 

1774wishart_docdict_params = { 

1775 '_doc_default_callparams': _wishart_doc_default_callparams, 

1776 '_doc_callparams_note': _wishart_doc_callparams_note, 

1777 '_doc_random_state': _doc_random_state 

1778} 

1779 

1780wishart_docdict_noparams = { 

1781 '_doc_default_callparams': _wishart_doc_frozen_callparams, 

1782 '_doc_callparams_note': _wishart_doc_frozen_callparams_note, 

1783 '_doc_random_state': _doc_random_state 

1784} 

1785 

1786 

1787class wishart_gen(multi_rv_generic): 

1788 r"""A Wishart random variable. 

1789 

1790 The `df` keyword specifies the degrees of freedom. The `scale` keyword 

1791 specifies the scale matrix, which must be symmetric and positive definite. 

1792 In this context, the scale matrix is often interpreted in terms of a 

1793 multivariate normal precision matrix (the inverse of the covariance 

1794 matrix). These arguments must satisfy the relationship 

1795 ``df > scale.ndim - 1``, but see notes on using the `rvs` method with 

1796 ``df < scale.ndim``. 

1797 

1798 Methods 

1799 ------- 

1800 pdf(x, df, scale) 

1801 Probability density function. 

1802 logpdf(x, df, scale) 

1803 Log of the probability density function. 

1804 rvs(df, scale, size=1, random_state=None) 

1805 Draw random samples from a Wishart distribution. 

1806 entropy() 

1807 Compute the differential entropy of the Wishart distribution. 

1808 

1809 Parameters 

1810 ---------- 

1811 %(_doc_default_callparams)s 

1812 %(_doc_random_state)s 

1813 

1814 Raises 

1815 ------ 

1816 scipy.linalg.LinAlgError 

1817 If the scale matrix `scale` is not positive definite. 

1818 

1819 See Also 

1820 -------- 

1821 invwishart, chi2 

1822 

1823 Notes 

1824 ----- 

1825 %(_doc_callparams_note)s 

1826 

1827 The scale matrix `scale` must be a symmetric positive definite 

1828 matrix. Singular matrices, including the symmetric positive semi-definite 

1829 case, are not supported. Symmetry is not checked; only the lower triangular 

1830 portion is used. 

1831 

1832 The Wishart distribution is often denoted 

1833 

1834 .. math:: 

1835 

1836 W_p(\nu, \Sigma) 

1837 

1838 where :math:`\nu` is the degrees of freedom and :math:`\Sigma` is the 

1839 :math:`p \times p` scale matrix. 

1840 

1841 The probability density function for `wishart` has support over positive 

1842 definite matrices :math:`S`; if :math:`S \sim W_p(\nu, \Sigma)`, then 

1843 its PDF is given by: 

1844 

1845 .. math:: 

1846 

1847 f(S) = \frac{|S|^{\frac{\nu - p - 1}{2}}}{2^{ \frac{\nu p}{2} } 

1848 |\Sigma|^\frac{\nu}{2} \Gamma_p \left ( \frac{\nu}{2} \right )} 

1849 \exp\left( -tr(\Sigma^{-1} S) / 2 \right) 

1850 

1851 If :math:`S \sim W_p(\nu, \Sigma)` (Wishart) then 

1852 :math:`S^{-1} \sim W_p^{-1}(\nu, \Sigma^{-1})` (inverse Wishart). 

1853 

1854 If the scale matrix is 1-dimensional and equal to one, then the Wishart 

1855 distribution :math:`W_1(\nu, 1)` collapses to the :math:`\chi^2(\nu)` 

1856 distribution. 

1857 

1858 The algorithm [2]_ implemented by the `rvs` method may 

1859 produce numerically singular matrices with :math:`p - 1 < \nu < p`; the 

1860 user may wish to check for this condition and generate replacement samples 

1861 as necessary. 

1862 

1863 

1864 .. versionadded:: 0.16.0 

1865 

1866 References 

1867 ---------- 

1868 .. [1] M.L. Eaton, "Multivariate Statistics: A Vector Space Approach", 

1869 Wiley, 1983. 

1870 .. [2] W.B. Smith and R.R. Hocking, "Algorithm AS 53: Wishart Variate 

1871 Generator", Applied Statistics, vol. 21, pp. 341-345, 1972. 

1872 

1873 Examples 

1874 -------- 

1875 >>> import numpy as np 

1876 >>> import matplotlib.pyplot as plt 

1877 >>> from scipy.stats import wishart, chi2 

1878 >>> x = np.linspace(1e-5, 8, 100) 

1879 >>> w = wishart.pdf(x, df=3, scale=1); w[:5] 

1880 array([ 0.00126156, 0.10892176, 0.14793434, 0.17400548, 0.1929669 ]) 

1881 >>> c = chi2.pdf(x, 3); c[:5] 

1882 array([ 0.00126156, 0.10892176, 0.14793434, 0.17400548, 0.1929669 ]) 

1883 >>> plt.plot(x, w) 

1884 >>> plt.show() 

1885 

1886 The input quantiles can be any shape of array, as long as the last 

1887 axis labels the components. 

1888 

1889 Alternatively, the object may be called (as a function) to fix the degrees 

1890 of freedom and scale parameters, returning a "frozen" Wishart random 

1891 variable: 

1892 

1893 >>> rv = wishart(df=1, scale=1) 

1894 >>> # Frozen object with the same methods but holding the given 

1895 >>> # degrees of freedom and scale fixed. 

1896 

1897 """ 

1898 

1899 def __init__(self, seed=None): 

1900 super().__init__(seed) 

1901 self.__doc__ = doccer.docformat(self.__doc__, wishart_docdict_params) 

1902 

1903 def __call__(self, df=None, scale=None, seed=None): 

1904 """Create a frozen Wishart distribution. 

1905 

1906 See `wishart_frozen` for more information. 

1907 """ 

1908 return wishart_frozen(df, scale, seed) 

1909 

1910 def _process_parameters(self, df, scale): 

1911 if scale is None: 

1912 scale = 1.0 

1913 scale = np.asarray(scale, dtype=float) 

1914 

1915 if scale.ndim == 0: 

1916 scale = scale[np.newaxis, np.newaxis] 

1917 elif scale.ndim == 1: 

1918 scale = np.diag(scale) 

1919 elif scale.ndim == 2 and not scale.shape[0] == scale.shape[1]: 

1920 raise ValueError("Array 'scale' must be square if it is two" 

1921 " dimensional, but scale.scale = %s." 

1922 % str(scale.shape)) 

1923 elif scale.ndim > 2: 

1924 raise ValueError("Array 'scale' must be at most two-dimensional," 

1925 " but scale.ndim = %d" % scale.ndim) 

1926 

1927 dim = scale.shape[0] 

1928 

1929 if df is None: 

1930 df = dim 

1931 elif not np.isscalar(df): 

1932 raise ValueError("Degrees of freedom must be a scalar.") 

1933 elif df <= dim - 1: 

1934 raise ValueError("Degrees of freedom must be greater than the " 

1935 "dimension of scale matrix minus 1.") 

1936 

1937 return dim, df, scale 

1938 

1939 def _process_quantiles(self, x, dim): 

1940 """ 

1941 Adjust quantiles array so that last axis labels the components of 

1942 each data point. 

1943 """ 

1944 x = np.asarray(x, dtype=float) 

1945 

1946 if x.ndim == 0: 

1947 x = x * np.eye(dim)[:, :, np.newaxis] 

1948 if x.ndim == 1: 

1949 if dim == 1: 

1950 x = x[np.newaxis, np.newaxis, :] 

1951 else: 

1952 x = np.diag(x)[:, :, np.newaxis] 

1953 elif x.ndim == 2: 

1954 if not x.shape[0] == x.shape[1]: 

1955 raise ValueError("Quantiles must be square if they are two" 

1956 " dimensional, but x.shape = %s." 

1957 % str(x.shape)) 

1958 x = x[:, :, np.newaxis] 

1959 elif x.ndim == 3: 

1960 if not x.shape[0] == x.shape[1]: 

1961 raise ValueError("Quantiles must be square in the first two" 

1962 " dimensions if they are three dimensional" 

1963 ", but x.shape = %s." % str(x.shape)) 

1964 elif x.ndim > 3: 

1965 raise ValueError("Quantiles must be at most two-dimensional with" 

1966 " an additional dimension for multiple" 

1967 "components, but x.ndim = %d" % x.ndim) 

1968 

1969 # Now we have 3-dim array; should have shape [dim, dim, *] 

1970 if not x.shape[0:2] == (dim, dim): 

1971 raise ValueError('Quantiles have incompatible dimensions: should' 

1972 ' be {}, got {}.'.format((dim, dim), x.shape[0:2])) 

1973 

1974 return x 

1975 

1976 def _process_size(self, size): 

1977 size = np.asarray(size) 

1978 

1979 if size.ndim == 0: 

1980 size = size[np.newaxis] 

1981 elif size.ndim > 1: 

1982 raise ValueError('Size must be an integer or tuple of integers;' 

1983 ' thus must have dimension <= 1.' 

1984 ' Got size.ndim = %s' % str(tuple(size))) 

1985 n = size.prod() 

1986 shape = tuple(size) 

1987 

1988 return n, shape 

1989 

1990 def _logpdf(self, x, dim, df, scale, log_det_scale, C): 

1991 """Log of the Wishart probability density function. 

1992 

1993 Parameters 

1994 ---------- 

1995 x : ndarray 

1996 Points at which to evaluate the log of the probability 

1997 density function 

1998 dim : int 

1999 Dimension of the scale matrix 

2000 df : int 

2001 Degrees of freedom 

2002 scale : ndarray 

2003 Scale matrix 

2004 log_det_scale : float 

2005 Logarithm of the determinant of the scale matrix 

2006 C : ndarray 

2007 Cholesky factorization of the scale matrix, lower triagular. 

2008 

2009 Notes 

2010 ----- 

2011 As this function does no argument checking, it should not be 

2012 called directly; use 'logpdf' instead. 

2013 

2014 """ 

2015 # log determinant of x 

2016 # Note: x has components along the last axis, so that x.T has 

2017 # components alone the 0-th axis. Then since det(A) = det(A'), this 

2018 # gives us a 1-dim vector of determinants 

2019 

2020 # Retrieve tr(scale^{-1} x) 

2021 log_det_x = np.empty(x.shape[-1]) 

2022 scale_inv_x = np.empty(x.shape) 

2023 tr_scale_inv_x = np.empty(x.shape[-1]) 

2024 for i in range(x.shape[-1]): 

2025 _, log_det_x[i] = self._cholesky_logdet(x[:, :, i]) 

2026 scale_inv_x[:, :, i] = scipy.linalg.cho_solve((C, True), x[:, :, i]) 

2027 tr_scale_inv_x[i] = scale_inv_x[:, :, i].trace() 

2028 

2029 # Log PDF 

2030 out = ((0.5 * (df - dim - 1) * log_det_x - 0.5 * tr_scale_inv_x) - 

2031 (0.5 * df * dim * _LOG_2 + 0.5 * df * log_det_scale + 

2032 multigammaln(0.5*df, dim))) 

2033 

2034 return out 

2035 

2036 def logpdf(self, x, df, scale): 

2037 """Log of the Wishart probability density function. 

2038 

2039 Parameters 

2040 ---------- 

2041 x : array_like 

2042 Quantiles, with the last axis of `x` denoting the components. 

2043 Each quantile must be a symmetric positive definite matrix. 

2044 %(_doc_default_callparams)s 

2045 

2046 Returns 

2047 ------- 

2048 pdf : ndarray 

2049 Log of the probability density function evaluated at `x` 

2050 

2051 Notes 

2052 ----- 

2053 %(_doc_callparams_note)s 

2054 

2055 """ 

2056 dim, df, scale = self._process_parameters(df, scale) 

2057 x = self._process_quantiles(x, dim) 

2058 

2059 # Cholesky decomposition of scale, get log(det(scale)) 

2060 C, log_det_scale = self._cholesky_logdet(scale) 

2061 

2062 out = self._logpdf(x, dim, df, scale, log_det_scale, C) 

2063 return _squeeze_output(out) 

2064 

2065 def pdf(self, x, df, scale): 

2066 """Wishart probability density function. 

2067 

2068 Parameters 

2069 ---------- 

2070 x : array_like 

2071 Quantiles, with the last axis of `x` denoting the components. 

2072 Each quantile must be a symmetric positive definite matrix. 

2073 %(_doc_default_callparams)s 

2074 

2075 Returns 

2076 ------- 

2077 pdf : ndarray 

2078 Probability density function evaluated at `x` 

2079 

2080 Notes 

2081 ----- 

2082 %(_doc_callparams_note)s 

2083 

2084 """ 

2085 return np.exp(self.logpdf(x, df, scale)) 

2086 

2087 def _mean(self, dim, df, scale): 

2088 """Mean of the Wishart distribution. 

2089 

2090 Parameters 

2091 ---------- 

2092 dim : int 

2093 Dimension of the scale matrix 

2094 %(_doc_default_callparams)s 

2095 

2096 Notes 

2097 ----- 

2098 As this function does no argument checking, it should not be 

2099 called directly; use 'mean' instead. 

2100 

2101 """ 

2102 return df * scale 

2103 

2104 def mean(self, df, scale): 

2105 """Mean of the Wishart distribution. 

2106 

2107 Parameters 

2108 ---------- 

2109 %(_doc_default_callparams)s 

2110 

2111 Returns 

2112 ------- 

2113 mean : float 

2114 The mean of the distribution 

2115 """ 

2116 dim, df, scale = self._process_parameters(df, scale) 

2117 out = self._mean(dim, df, scale) 

2118 return _squeeze_output(out) 

2119 

2120 def _mode(self, dim, df, scale): 

2121 """Mode of the Wishart distribution. 

2122 

2123 Parameters 

2124 ---------- 

2125 dim : int 

2126 Dimension of the scale matrix 

2127 %(_doc_default_callparams)s 

2128 

2129 Notes 

2130 ----- 

2131 As this function does no argument checking, it should not be 

2132 called directly; use 'mode' instead. 

2133 

2134 """ 

2135 if df >= dim + 1: 

2136 out = (df-dim-1) * scale 

2137 else: 

2138 out = None 

2139 return out 

2140 

2141 def mode(self, df, scale): 

2142 """Mode of the Wishart distribution 

2143 

2144 Only valid if the degrees of freedom are greater than the dimension of 

2145 the scale matrix. 

2146 

2147 Parameters 

2148 ---------- 

2149 %(_doc_default_callparams)s 

2150 

2151 Returns 

2152 ------- 

2153 mode : float or None 

2154 The Mode of the distribution 

2155 """ 

2156 dim, df, scale = self._process_parameters(df, scale) 

2157 out = self._mode(dim, df, scale) 

2158 return _squeeze_output(out) if out is not None else out 

2159 

2160 def _var(self, dim, df, scale): 

2161 """Variance of the Wishart distribution. 

2162 

2163 Parameters 

2164 ---------- 

2165 dim : int 

2166 Dimension of the scale matrix 

2167 %(_doc_default_callparams)s 

2168 

2169 Notes 

2170 ----- 

2171 As this function does no argument checking, it should not be 

2172 called directly; use 'var' instead. 

2173 

2174 """ 

2175 var = scale**2 

2176 diag = scale.diagonal() # 1 x dim array 

2177 var += np.outer(diag, diag) 

2178 var *= df 

2179 return var 

2180 

2181 def var(self, df, scale): 

2182 """Variance of the Wishart distribution. 

2183 

2184 Parameters 

2185 ---------- 

2186 %(_doc_default_callparams)s 

2187 

2188 Returns 

2189 ------- 

2190 var : float 

2191 The variance of the distribution 

2192 """ 

2193 dim, df, scale = self._process_parameters(df, scale) 

2194 out = self._var(dim, df, scale) 

2195 return _squeeze_output(out) 

2196 

2197 def _standard_rvs(self, n, shape, dim, df, random_state): 

2198 """ 

2199 Parameters 

2200 ---------- 

2201 n : integer 

2202 Number of variates to generate 

2203 shape : iterable 

2204 Shape of the variates to generate 

2205 dim : int 

2206 Dimension of the scale matrix 

2207 df : int 

2208 Degrees of freedom 

2209 random_state : {None, int, `numpy.random.Generator`, 

2210 `numpy.random.RandomState`}, optional 

2211 

2212 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

2213 singleton is used. 

2214 If `seed` is an int, a new ``RandomState`` instance is used, 

2215 seeded with `seed`. 

2216 If `seed` is already a ``Generator`` or ``RandomState`` instance 

2217 then that instance is used. 

2218 

2219 Notes 

2220 ----- 

2221 As this function does no argument checking, it should not be 

2222 called directly; use 'rvs' instead. 

2223 

2224 """ 

2225 # Random normal variates for off-diagonal elements 

2226 n_tril = dim * (dim-1) // 2 

2227 covariances = random_state.normal( 

2228 size=n*n_tril).reshape(shape+(n_tril,)) 

2229 

2230 # Random chi-square variates for diagonal elements 

2231 variances = (np.r_[[random_state.chisquare(df-(i+1)+1, size=n)**0.5 

2232 for i in range(dim)]].reshape((dim,) + 

2233 shape[::-1]).T) 

2234 

2235 # Create the A matri(ces) - lower triangular 

2236 A = np.zeros(shape + (dim, dim)) 

2237 

2238 # Input the covariances 

2239 size_idx = tuple([slice(None, None, None)]*len(shape)) 

2240 tril_idx = np.tril_indices(dim, k=-1) 

2241 A[size_idx + tril_idx] = covariances 

2242 

2243 # Input the variances 

2244 diag_idx = np.diag_indices(dim) 

2245 A[size_idx + diag_idx] = variances 

2246 

2247 return A 

2248 

2249 def _rvs(self, n, shape, dim, df, C, random_state): 

2250 """Draw random samples from a Wishart distribution. 

2251 

2252 Parameters 

2253 ---------- 

2254 n : integer 

2255 Number of variates to generate 

2256 shape : iterable 

2257 Shape of the variates to generate 

2258 dim : int 

2259 Dimension of the scale matrix 

2260 df : int 

2261 Degrees of freedom 

2262 C : ndarray 

2263 Cholesky factorization of the scale matrix, lower triangular. 

2264 %(_doc_random_state)s 

2265 

2266 Notes 

2267 ----- 

2268 As this function does no argument checking, it should not be 

2269 called directly; use 'rvs' instead. 

2270 

2271 """ 

2272 random_state = self._get_random_state(random_state) 

2273 # Calculate the matrices A, which are actually lower triangular 

2274 # Cholesky factorizations of a matrix B such that B ~ W(df, I) 

2275 A = self._standard_rvs(n, shape, dim, df, random_state) 

2276 

2277 # Calculate SA = C A A' C', where SA ~ W(df, scale) 

2278 # Note: this is the product of a (lower) (lower) (lower)' (lower)' 

2279 # or, denoting B = AA', it is C B C' where C is the lower 

2280 # triangular Cholesky factorization of the scale matrix. 

2281 # this appears to conflict with the instructions in [1]_, which 

2282 # suggest that it should be D' B D where D is the lower 

2283 # triangular factorization of the scale matrix. However, it is 

2284 # meant to refer to the Bartlett (1933) representation of a 

2285 # Wishart random variate as L A A' L' where L is lower triangular 

2286 # so it appears that understanding D' to be upper triangular 

2287 # is either a typo in or misreading of [1]_. 

2288 for index in np.ndindex(shape): 

2289 CA = np.dot(C, A[index]) 

2290 A[index] = np.dot(CA, CA.T) 

2291 

2292 return A 

2293 

2294 def rvs(self, df, scale, size=1, random_state=None): 

2295 """Draw random samples from a Wishart distribution. 

2296 

2297 Parameters 

2298 ---------- 

2299 %(_doc_default_callparams)s 

2300 size : integer or iterable of integers, optional 

2301 Number of samples to draw (default 1). 

2302 %(_doc_random_state)s 

2303 

2304 Returns 

2305 ------- 

2306 rvs : ndarray 

2307 Random variates of shape (`size`) + (`dim`, `dim), where `dim` is 

2308 the dimension of the scale matrix. 

2309 

2310 Notes 

2311 ----- 

2312 %(_doc_callparams_note)s 

2313 

2314 """ 

2315 n, shape = self._process_size(size) 

2316 dim, df, scale = self._process_parameters(df, scale) 

2317 

2318 # Cholesky decomposition of scale 

2319 C = scipy.linalg.cholesky(scale, lower=True) 

2320 

2321 out = self._rvs(n, shape, dim, df, C, random_state) 

2322 

2323 return _squeeze_output(out) 

2324 

2325 def _entropy(self, dim, df, log_det_scale): 

2326 """Compute the differential entropy of the Wishart. 

2327 

2328 Parameters 

2329 ---------- 

2330 dim : int 

2331 Dimension of the scale matrix 

2332 df : int 

2333 Degrees of freedom 

2334 log_det_scale : float 

2335 Logarithm of the determinant of the scale matrix 

2336 

2337 Notes 

2338 ----- 

2339 As this function does no argument checking, it should not be 

2340 called directly; use 'entropy' instead. 

2341 

2342 """ 

2343 return ( 

2344 0.5 * (dim+1) * log_det_scale + 

2345 0.5 * dim * (dim+1) * _LOG_2 + 

2346 multigammaln(0.5*df, dim) - 

2347 0.5 * (df - dim - 1) * np.sum( 

2348 [psi(0.5*(df + 1 - (i+1))) for i in range(dim)] 

2349 ) + 

2350 0.5 * df * dim 

2351 ) 

2352 

2353 def entropy(self, df, scale): 

2354 """Compute the differential entropy of the Wishart. 

2355 

2356 Parameters 

2357 ---------- 

2358 %(_doc_default_callparams)s 

2359 

2360 Returns 

2361 ------- 

2362 h : scalar 

2363 Entropy of the Wishart distribution 

2364 

2365 Notes 

2366 ----- 

2367 %(_doc_callparams_note)s 

2368 

2369 """ 

2370 dim, df, scale = self._process_parameters(df, scale) 

2371 _, log_det_scale = self._cholesky_logdet(scale) 

2372 return self._entropy(dim, df, log_det_scale) 

2373 

2374 def _cholesky_logdet(self, scale): 

2375 """Compute Cholesky decomposition and determine (log(det(scale)). 

2376 

2377 Parameters 

2378 ---------- 

2379 scale : ndarray 

2380 Scale matrix. 

2381 

2382 Returns 

2383 ------- 

2384 c_decomp : ndarray 

2385 The Cholesky decomposition of `scale`. 

2386 logdet : scalar 

2387 The log of the determinant of `scale`. 

2388 

2389 Notes 

2390 ----- 

2391 This computation of ``logdet`` is equivalent to 

2392 ``np.linalg.slogdet(scale)``. It is ~2x faster though. 

2393 

2394 """ 

2395 c_decomp = scipy.linalg.cholesky(scale, lower=True) 

2396 logdet = 2 * np.sum(np.log(c_decomp.diagonal())) 

2397 return c_decomp, logdet 

2398 

2399 

2400wishart = wishart_gen() 

2401 

2402 

2403class wishart_frozen(multi_rv_frozen): 

2404 """Create a frozen Wishart distribution. 

2405 

2406 Parameters 

2407 ---------- 

2408 df : array_like 

2409 Degrees of freedom of the distribution 

2410 scale : array_like 

2411 Scale matrix of the distribution 

2412 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

2413 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

2414 singleton is used. 

2415 If `seed` is an int, a new ``RandomState`` instance is used, 

2416 seeded with `seed`. 

2417 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

2418 that instance is used. 

2419 

2420 """ 

2421 def __init__(self, df, scale, seed=None): 

2422 self._dist = wishart_gen(seed) 

2423 self.dim, self.df, self.scale = self._dist._process_parameters( 

2424 df, scale) 

2425 self.C, self.log_det_scale = self._dist._cholesky_logdet(self.scale) 

2426 

2427 def logpdf(self, x): 

2428 x = self._dist._process_quantiles(x, self.dim) 

2429 

2430 out = self._dist._logpdf(x, self.dim, self.df, self.scale, 

2431 self.log_det_scale, self.C) 

2432 return _squeeze_output(out) 

2433 

2434 def pdf(self, x): 

2435 return np.exp(self.logpdf(x)) 

2436 

2437 def mean(self): 

2438 out = self._dist._mean(self.dim, self.df, self.scale) 

2439 return _squeeze_output(out) 

2440 

2441 def mode(self): 

2442 out = self._dist._mode(self.dim, self.df, self.scale) 

2443 return _squeeze_output(out) if out is not None else out 

2444 

2445 def var(self): 

2446 out = self._dist._var(self.dim, self.df, self.scale) 

2447 return _squeeze_output(out) 

2448 

2449 def rvs(self, size=1, random_state=None): 

2450 n, shape = self._dist._process_size(size) 

2451 out = self._dist._rvs(n, shape, self.dim, self.df, 

2452 self.C, random_state) 

2453 return _squeeze_output(out) 

2454 

2455 def entropy(self): 

2456 return self._dist._entropy(self.dim, self.df, self.log_det_scale) 

2457 

2458 

2459# Set frozen generator docstrings from corresponding docstrings in 

2460# Wishart and fill in default strings in class docstrings 

2461for name in ['logpdf', 'pdf', 'mean', 'mode', 'var', 'rvs', 'entropy']: 

2462 method = wishart_gen.__dict__[name] 

2463 method_frozen = wishart_frozen.__dict__[name] 

2464 method_frozen.__doc__ = doccer.docformat( 

2465 method.__doc__, wishart_docdict_noparams) 

2466 method.__doc__ = doccer.docformat(method.__doc__, wishart_docdict_params) 

2467 

2468 

2469def _cho_inv_batch(a, check_finite=True): 

2470 """ 

2471 Invert the matrices a_i, using a Cholesky factorization of A, where 

2472 a_i resides in the last two dimensions of a and the other indices describe 

2473 the index i. 

2474 

2475 Overwrites the data in a. 

2476 

2477 Parameters 

2478 ---------- 

2479 a : array 

2480 Array of matrices to invert, where the matrices themselves are stored 

2481 in the last two dimensions. 

2482 check_finite : bool, optional 

2483 Whether to check that the input matrices contain only finite numbers. 

2484 Disabling may give a performance gain, but may result in problems 

2485 (crashes, non-termination) if the inputs do contain infinities or NaNs. 

2486 

2487 Returns 

2488 ------- 

2489 x : array 

2490 Array of inverses of the matrices ``a_i``. 

2491 

2492 See Also 

2493 -------- 

2494 scipy.linalg.cholesky : Cholesky factorization of a matrix 

2495 

2496 """ 

2497 if check_finite: 

2498 a1 = asarray_chkfinite(a) 

2499 else: 

2500 a1 = asarray(a) 

2501 if len(a1.shape) < 2 or a1.shape[-2] != a1.shape[-1]: 

2502 raise ValueError('expected square matrix in last two dimensions') 

2503 

2504 potrf, potri = get_lapack_funcs(('potrf', 'potri'), (a1,)) 

2505 

2506 triu_rows, triu_cols = np.triu_indices(a.shape[-2], k=1) 

2507 for index in np.ndindex(a1.shape[:-2]): 

2508 

2509 # Cholesky decomposition 

2510 a1[index], info = potrf(a1[index], lower=True, overwrite_a=False, 

2511 clean=False) 

2512 if info > 0: 

2513 raise LinAlgError("%d-th leading minor not positive definite" 

2514 % info) 

2515 if info < 0: 

2516 raise ValueError('illegal value in %d-th argument of internal' 

2517 ' potrf' % -info) 

2518 # Inversion 

2519 a1[index], info = potri(a1[index], lower=True, overwrite_c=False) 

2520 if info > 0: 

2521 raise LinAlgError("the inverse could not be computed") 

2522 if info < 0: 

2523 raise ValueError('illegal value in %d-th argument of internal' 

2524 ' potrf' % -info) 

2525 

2526 # Make symmetric (dpotri only fills in the lower triangle) 

2527 a1[index][triu_rows, triu_cols] = a1[index][triu_cols, triu_rows] 

2528 

2529 return a1 

2530 

2531 

2532class invwishart_gen(wishart_gen): 

2533 r"""An inverse Wishart random variable. 

2534 

2535 The `df` keyword specifies the degrees of freedom. The `scale` keyword 

2536 specifies the scale matrix, which must be symmetric and positive definite. 

2537 In this context, the scale matrix is often interpreted in terms of a 

2538 multivariate normal covariance matrix. 

2539 

2540 Methods 

2541 ------- 

2542 pdf(x, df, scale) 

2543 Probability density function. 

2544 logpdf(x, df, scale) 

2545 Log of the probability density function. 

2546 rvs(df, scale, size=1, random_state=None) 

2547 Draw random samples from an inverse Wishart distribution. 

2548 entropy(df, scale) 

2549 Differential entropy of the distribution. 

2550 

2551 Parameters 

2552 ---------- 

2553 %(_doc_default_callparams)s 

2554 %(_doc_random_state)s 

2555 

2556 Raises 

2557 ------ 

2558 scipy.linalg.LinAlgError 

2559 If the scale matrix `scale` is not positive definite. 

2560 

2561 See Also 

2562 -------- 

2563 wishart 

2564 

2565 Notes 

2566 ----- 

2567 %(_doc_callparams_note)s 

2568 

2569 The scale matrix `scale` must be a symmetric positive definite 

2570 matrix. Singular matrices, including the symmetric positive semi-definite 

2571 case, are not supported. Symmetry is not checked; only the lower triangular 

2572 portion is used. 

2573 

2574 The inverse Wishart distribution is often denoted 

2575 

2576 .. math:: 

2577 

2578 W_p^{-1}(\nu, \Psi) 

2579 

2580 where :math:`\nu` is the degrees of freedom and :math:`\Psi` is the 

2581 :math:`p \times p` scale matrix. 

2582 

2583 The probability density function for `invwishart` has support over positive 

2584 definite matrices :math:`S`; if :math:`S \sim W^{-1}_p(\nu, \Sigma)`, 

2585 then its PDF is given by: 

2586 

2587 .. math:: 

2588 

2589 f(S) = \frac{|\Sigma|^\frac{\nu}{2}}{2^{ \frac{\nu p}{2} } 

2590 |S|^{\frac{\nu + p + 1}{2}} \Gamma_p \left(\frac{\nu}{2} \right)} 

2591 \exp\left( -tr(\Sigma S^{-1}) / 2 \right) 

2592 

2593 If :math:`S \sim W_p^{-1}(\nu, \Psi)` (inverse Wishart) then 

2594 :math:`S^{-1} \sim W_p(\nu, \Psi^{-1})` (Wishart). 

2595 

2596 If the scale matrix is 1-dimensional and equal to one, then the inverse 

2597 Wishart distribution :math:`W_1(\nu, 1)` collapses to the 

2598 inverse Gamma distribution with parameters shape = :math:`\frac{\nu}{2}` 

2599 and scale = :math:`\frac{1}{2}`. 

2600 

2601 .. versionadded:: 0.16.0 

2602 

2603 References 

2604 ---------- 

2605 .. [1] M.L. Eaton, "Multivariate Statistics: A Vector Space Approach", 

2606 Wiley, 1983. 

2607 .. [2] M.C. Jones, "Generating Inverse Wishart Matrices", Communications 

2608 in Statistics - Simulation and Computation, vol. 14.2, pp.511-514, 

2609 1985. 

2610 .. [3] Gupta, M. and Srivastava, S. "Parametric Bayesian Estimation of 

2611 Differential Entropy and Relative Entropy". Entropy 12, 818 - 843. 

2612 2010. 

2613 

2614 Examples 

2615 -------- 

2616 >>> import numpy as np 

2617 >>> import matplotlib.pyplot as plt 

2618 >>> from scipy.stats import invwishart, invgamma 

2619 >>> x = np.linspace(0.01, 1, 100) 

2620 >>> iw = invwishart.pdf(x, df=6, scale=1) 

2621 >>> iw[:3] 

2622 array([ 1.20546865e-15, 5.42497807e-06, 4.45813929e-03]) 

2623 >>> ig = invgamma.pdf(x, 6/2., scale=1./2) 

2624 >>> ig[:3] 

2625 array([ 1.20546865e-15, 5.42497807e-06, 4.45813929e-03]) 

2626 >>> plt.plot(x, iw) 

2627 >>> plt.show() 

2628 

2629 The input quantiles can be any shape of array, as long as the last 

2630 axis labels the components. 

2631 

2632 Alternatively, the object may be called (as a function) to fix the degrees 

2633 of freedom and scale parameters, returning a "frozen" inverse Wishart 

2634 random variable: 

2635 

2636 >>> rv = invwishart(df=1, scale=1) 

2637 >>> # Frozen object with the same methods but holding the given 

2638 >>> # degrees of freedom and scale fixed. 

2639 

2640 """ 

2641 

2642 def __init__(self, seed=None): 

2643 super().__init__(seed) 

2644 self.__doc__ = doccer.docformat(self.__doc__, wishart_docdict_params) 

2645 

2646 def __call__(self, df=None, scale=None, seed=None): 

2647 """Create a frozen inverse Wishart distribution. 

2648 

2649 See `invwishart_frozen` for more information. 

2650 

2651 """ 

2652 return invwishart_frozen(df, scale, seed) 

2653 

2654 def _logpdf(self, x, dim, df, scale, log_det_scale): 

2655 """Log of the inverse Wishart probability density function. 

2656 

2657 Parameters 

2658 ---------- 

2659 x : ndarray 

2660 Points at which to evaluate the log of the probability 

2661 density function. 

2662 dim : int 

2663 Dimension of the scale matrix 

2664 df : int 

2665 Degrees of freedom 

2666 scale : ndarray 

2667 Scale matrix 

2668 log_det_scale : float 

2669 Logarithm of the determinant of the scale matrix 

2670 

2671 Notes 

2672 ----- 

2673 As this function does no argument checking, it should not be 

2674 called directly; use 'logpdf' instead. 

2675 

2676 """ 

2677 log_det_x = np.empty(x.shape[-1]) 

2678 x_inv = np.copy(x).T 

2679 if dim > 1: 

2680 _cho_inv_batch(x_inv) # works in-place 

2681 else: 

2682 x_inv = 1./x_inv 

2683 tr_scale_x_inv = np.empty(x.shape[-1]) 

2684 

2685 for i in range(x.shape[-1]): 

2686 C, lower = scipy.linalg.cho_factor(x[:, :, i], lower=True) 

2687 

2688 log_det_x[i] = 2 * np.sum(np.log(C.diagonal())) 

2689 

2690 tr_scale_x_inv[i] = np.dot(scale, x_inv[i]).trace() 

2691 

2692 # Log PDF 

2693 out = ((0.5 * df * log_det_scale - 0.5 * tr_scale_x_inv) - 

2694 (0.5 * df * dim * _LOG_2 + 0.5 * (df + dim + 1) * log_det_x) - 

2695 multigammaln(0.5*df, dim)) 

2696 

2697 return out 

2698 

2699 def logpdf(self, x, df, scale): 

2700 """Log of the inverse Wishart probability density function. 

2701 

2702 Parameters 

2703 ---------- 

2704 x : array_like 

2705 Quantiles, with the last axis of `x` denoting the components. 

2706 Each quantile must be a symmetric positive definite matrix. 

2707 %(_doc_default_callparams)s 

2708 

2709 Returns 

2710 ------- 

2711 pdf : ndarray 

2712 Log of the probability density function evaluated at `x` 

2713 

2714 Notes 

2715 ----- 

2716 %(_doc_callparams_note)s 

2717 

2718 """ 

2719 dim, df, scale = self._process_parameters(df, scale) 

2720 x = self._process_quantiles(x, dim) 

2721 _, log_det_scale = self._cholesky_logdet(scale) 

2722 out = self._logpdf(x, dim, df, scale, log_det_scale) 

2723 return _squeeze_output(out) 

2724 

2725 def pdf(self, x, df, scale): 

2726 """Inverse Wishart probability density function. 

2727 

2728 Parameters 

2729 ---------- 

2730 x : array_like 

2731 Quantiles, with the last axis of `x` denoting the components. 

2732 Each quantile must be a symmetric positive definite matrix. 

2733 %(_doc_default_callparams)s 

2734 

2735 Returns 

2736 ------- 

2737 pdf : ndarray 

2738 Probability density function evaluated at `x` 

2739 

2740 Notes 

2741 ----- 

2742 %(_doc_callparams_note)s 

2743 

2744 """ 

2745 return np.exp(self.logpdf(x, df, scale)) 

2746 

2747 def _mean(self, dim, df, scale): 

2748 """Mean of the inverse Wishart distribution. 

2749 

2750 Parameters 

2751 ---------- 

2752 dim : int 

2753 Dimension of the scale matrix 

2754 %(_doc_default_callparams)s 

2755 

2756 Notes 

2757 ----- 

2758 As this function does no argument checking, it should not be 

2759 called directly; use 'mean' instead. 

2760 

2761 """ 

2762 if df > dim + 1: 

2763 out = scale / (df - dim - 1) 

2764 else: 

2765 out = None 

2766 return out 

2767 

2768 def mean(self, df, scale): 

2769 """Mean of the inverse Wishart distribution. 

2770 

2771 Only valid if the degrees of freedom are greater than the dimension of 

2772 the scale matrix plus one. 

2773 

2774 Parameters 

2775 ---------- 

2776 %(_doc_default_callparams)s 

2777 

2778 Returns 

2779 ------- 

2780 mean : float or None 

2781 The mean of the distribution 

2782 

2783 """ 

2784 dim, df, scale = self._process_parameters(df, scale) 

2785 out = self._mean(dim, df, scale) 

2786 return _squeeze_output(out) if out is not None else out 

2787 

2788 def _mode(self, dim, df, scale): 

2789 """Mode of the inverse Wishart distribution. 

2790 

2791 Parameters 

2792 ---------- 

2793 dim : int 

2794 Dimension of the scale matrix 

2795 %(_doc_default_callparams)s 

2796 

2797 Notes 

2798 ----- 

2799 As this function does no argument checking, it should not be 

2800 called directly; use 'mode' instead. 

2801 

2802 """ 

2803 return scale / (df + dim + 1) 

2804 

2805 def mode(self, df, scale): 

2806 """Mode of the inverse Wishart distribution. 

2807 

2808 Parameters 

2809 ---------- 

2810 %(_doc_default_callparams)s 

2811 

2812 Returns 

2813 ------- 

2814 mode : float 

2815 The Mode of the distribution 

2816 

2817 """ 

2818 dim, df, scale = self._process_parameters(df, scale) 

2819 out = self._mode(dim, df, scale) 

2820 return _squeeze_output(out) 

2821 

2822 def _var(self, dim, df, scale): 

2823 """Variance of the inverse Wishart distribution. 

2824 

2825 Parameters 

2826 ---------- 

2827 dim : int 

2828 Dimension of the scale matrix 

2829 %(_doc_default_callparams)s 

2830 

2831 Notes 

2832 ----- 

2833 As this function does no argument checking, it should not be 

2834 called directly; use 'var' instead. 

2835 

2836 """ 

2837 if df > dim + 3: 

2838 var = (df - dim + 1) * scale**2 

2839 diag = scale.diagonal() # 1 x dim array 

2840 var += (df - dim - 1) * np.outer(diag, diag) 

2841 var /= (df - dim) * (df - dim - 1)**2 * (df - dim - 3) 

2842 else: 

2843 var = None 

2844 return var 

2845 

2846 def var(self, df, scale): 

2847 """Variance of the inverse Wishart distribution. 

2848 

2849 Only valid if the degrees of freedom are greater than the dimension of 

2850 the scale matrix plus three. 

2851 

2852 Parameters 

2853 ---------- 

2854 %(_doc_default_callparams)s 

2855 

2856 Returns 

2857 ------- 

2858 var : float 

2859 The variance of the distribution 

2860 """ 

2861 dim, df, scale = self._process_parameters(df, scale) 

2862 out = self._var(dim, df, scale) 

2863 return _squeeze_output(out) if out is not None else out 

2864 

2865 def _rvs(self, n, shape, dim, df, C, random_state): 

2866 """Draw random samples from an inverse Wishart distribution. 

2867 

2868 Parameters 

2869 ---------- 

2870 n : integer 

2871 Number of variates to generate 

2872 shape : iterable 

2873 Shape of the variates to generate 

2874 dim : int 

2875 Dimension of the scale matrix 

2876 df : int 

2877 Degrees of freedom 

2878 C : ndarray 

2879 Cholesky factorization of the scale matrix, lower triagular. 

2880 %(_doc_random_state)s 

2881 

2882 Notes 

2883 ----- 

2884 As this function does no argument checking, it should not be 

2885 called directly; use 'rvs' instead. 

2886 

2887 """ 

2888 random_state = self._get_random_state(random_state) 

2889 # Get random draws A such that A ~ W(df, I) 

2890 A = super()._standard_rvs(n, shape, dim, df, random_state) 

2891 

2892 # Calculate SA = (CA)'^{-1} (CA)^{-1} ~ iW(df, scale) 

2893 eye = np.eye(dim) 

2894 trtrs = get_lapack_funcs(('trtrs'), (A,)) 

2895 

2896 for index in np.ndindex(A.shape[:-2]): 

2897 # Calculate CA 

2898 CA = np.dot(C, A[index]) 

2899 # Get (C A)^{-1} via triangular solver 

2900 if dim > 1: 

2901 CA, info = trtrs(CA, eye, lower=True) 

2902 if info > 0: 

2903 raise LinAlgError("Singular matrix.") 

2904 if info < 0: 

2905 raise ValueError('Illegal value in %d-th argument of' 

2906 ' internal trtrs' % -info) 

2907 else: 

2908 CA = 1. / CA 

2909 # Get SA 

2910 A[index] = np.dot(CA.T, CA) 

2911 

2912 return A 

2913 

2914 def rvs(self, df, scale, size=1, random_state=None): 

2915 """Draw random samples from an inverse Wishart distribution. 

2916 

2917 Parameters 

2918 ---------- 

2919 %(_doc_default_callparams)s 

2920 size : integer or iterable of integers, optional 

2921 Number of samples to draw (default 1). 

2922 %(_doc_random_state)s 

2923 

2924 Returns 

2925 ------- 

2926 rvs : ndarray 

2927 Random variates of shape (`size`) + (`dim`, `dim), where `dim` is 

2928 the dimension of the scale matrix. 

2929 

2930 Notes 

2931 ----- 

2932 %(_doc_callparams_note)s 

2933 

2934 """ 

2935 n, shape = self._process_size(size) 

2936 dim, df, scale = self._process_parameters(df, scale) 

2937 

2938 # Invert the scale 

2939 eye = np.eye(dim) 

2940 L, lower = scipy.linalg.cho_factor(scale, lower=True) 

2941 inv_scale = scipy.linalg.cho_solve((L, lower), eye) 

2942 # Cholesky decomposition of inverted scale 

2943 C = scipy.linalg.cholesky(inv_scale, lower=True) 

2944 

2945 out = self._rvs(n, shape, dim, df, C, random_state) 

2946 

2947 return _squeeze_output(out) 

2948 

2949 def _entropy(self, dim, df, log_det_scale): 

2950 # reference: eq. (17) from ref. 3 

2951 psi_eval_points = [0.5 * (df - dim + i) for i in range(1, dim + 1)] 

2952 psi_eval_points = np.asarray(psi_eval_points) 

2953 return multigammaln(0.5 * df, dim) + 0.5 * dim * df + \ 

2954 0.5 * (dim + 1) * (log_det_scale - _LOG_2) - \ 

2955 0.5 * (df + dim + 1) * \ 

2956 psi(psi_eval_points, out=psi_eval_points).sum() 

2957 

2958 def entropy(self, df, scale): 

2959 dim, df, scale = self._process_parameters(df, scale) 

2960 _, log_det_scale = self._cholesky_logdet(scale) 

2961 return self._entropy(dim, df, log_det_scale) 

2962 

2963 

2964invwishart = invwishart_gen() 

2965 

2966 

2967class invwishart_frozen(multi_rv_frozen): 

2968 def __init__(self, df, scale, seed=None): 

2969 """Create a frozen inverse Wishart distribution. 

2970 

2971 Parameters 

2972 ---------- 

2973 df : array_like 

2974 Degrees of freedom of the distribution 

2975 scale : array_like 

2976 Scale matrix of the distribution 

2977 seed : {None, int, `numpy.random.Generator`}, optional 

2978 If `seed` is None the `numpy.random.Generator` singleton is used. 

2979 If `seed` is an int, a new ``Generator`` instance is used, 

2980 seeded with `seed`. 

2981 If `seed` is already a ``Generator`` instance then that instance is 

2982 used. 

2983 

2984 """ 

2985 self._dist = invwishart_gen(seed) 

2986 self.dim, self.df, self.scale = self._dist._process_parameters( 

2987 df, scale 

2988 ) 

2989 

2990 # Get the determinant via Cholesky factorization 

2991 C, lower = scipy.linalg.cho_factor(self.scale, lower=True) 

2992 self.log_det_scale = 2 * np.sum(np.log(C.diagonal())) 

2993 

2994 # Get the inverse using the Cholesky factorization 

2995 eye = np.eye(self.dim) 

2996 self.inv_scale = scipy.linalg.cho_solve((C, lower), eye) 

2997 

2998 # Get the Cholesky factorization of the inverse scale 

2999 self.C = scipy.linalg.cholesky(self.inv_scale, lower=True) 

3000 

3001 def logpdf(self, x): 

3002 x = self._dist._process_quantiles(x, self.dim) 

3003 out = self._dist._logpdf(x, self.dim, self.df, self.scale, 

3004 self.log_det_scale) 

3005 return _squeeze_output(out) 

3006 

3007 def pdf(self, x): 

3008 return np.exp(self.logpdf(x)) 

3009 

3010 def mean(self): 

3011 out = self._dist._mean(self.dim, self.df, self.scale) 

3012 return _squeeze_output(out) if out is not None else out 

3013 

3014 def mode(self): 

3015 out = self._dist._mode(self.dim, self.df, self.scale) 

3016 return _squeeze_output(out) 

3017 

3018 def var(self): 

3019 out = self._dist._var(self.dim, self.df, self.scale) 

3020 return _squeeze_output(out) if out is not None else out 

3021 

3022 def rvs(self, size=1, random_state=None): 

3023 n, shape = self._dist._process_size(size) 

3024 

3025 out = self._dist._rvs(n, shape, self.dim, self.df, 

3026 self.C, random_state) 

3027 

3028 return _squeeze_output(out) 

3029 

3030 def entropy(self): 

3031 return self._dist._entropy(self.dim, self.df, self.log_det_scale) 

3032 

3033 

3034# Set frozen generator docstrings from corresponding docstrings in 

3035# inverse Wishart and fill in default strings in class docstrings 

3036for name in ['logpdf', 'pdf', 'mean', 'mode', 'var', 'rvs']: 

3037 method = invwishart_gen.__dict__[name] 

3038 method_frozen = wishart_frozen.__dict__[name] 

3039 method_frozen.__doc__ = doccer.docformat( 

3040 method.__doc__, wishart_docdict_noparams) 

3041 method.__doc__ = doccer.docformat(method.__doc__, wishart_docdict_params) 

3042 

3043_multinomial_doc_default_callparams = """\ 

3044n : int 

3045 Number of trials 

3046p : array_like 

3047 Probability of a trial falling into each category; should sum to 1 

3048""" 

3049 

3050_multinomial_doc_callparams_note = """\ 

3051`n` should be a nonnegative integer. Each element of `p` should be in the 

3052interval :math:`[0,1]` and the elements should sum to 1. If they do not sum to 

30531, the last element of the `p` array is not used and is replaced with the 

3054remaining probability left over from the earlier elements. 

3055""" 

3056 

3057_multinomial_doc_frozen_callparams = "" 

3058 

3059_multinomial_doc_frozen_callparams_note = """\ 

3060See class definition for a detailed description of parameters.""" 

3061 

3062multinomial_docdict_params = { 

3063 '_doc_default_callparams': _multinomial_doc_default_callparams, 

3064 '_doc_callparams_note': _multinomial_doc_callparams_note, 

3065 '_doc_random_state': _doc_random_state 

3066} 

3067 

3068multinomial_docdict_noparams = { 

3069 '_doc_default_callparams': _multinomial_doc_frozen_callparams, 

3070 '_doc_callparams_note': _multinomial_doc_frozen_callparams_note, 

3071 '_doc_random_state': _doc_random_state 

3072} 

3073 

3074 

3075class multinomial_gen(multi_rv_generic): 

3076 r"""A multinomial random variable. 

3077 

3078 Methods 

3079 ------- 

3080 pmf(x, n, p) 

3081 Probability mass function. 

3082 logpmf(x, n, p) 

3083 Log of the probability mass function. 

3084 rvs(n, p, size=1, random_state=None) 

3085 Draw random samples from a multinomial distribution. 

3086 entropy(n, p) 

3087 Compute the entropy of the multinomial distribution. 

3088 cov(n, p) 

3089 Compute the covariance matrix of the multinomial distribution. 

3090 

3091 Parameters 

3092 ---------- 

3093 %(_doc_default_callparams)s 

3094 %(_doc_random_state)s 

3095 

3096 Notes 

3097 ----- 

3098 %(_doc_callparams_note)s 

3099 

3100 The probability mass function for `multinomial` is 

3101 

3102 .. math:: 

3103 

3104 f(x) = \frac{n!}{x_1! \cdots x_k!} p_1^{x_1} \cdots p_k^{x_k}, 

3105 

3106 supported on :math:`x=(x_1, \ldots, x_k)` where each :math:`x_i` is a 

3107 nonnegative integer and their sum is :math:`n`. 

3108 

3109 .. versionadded:: 0.19.0 

3110 

3111 Examples 

3112 -------- 

3113 

3114 >>> from scipy.stats import multinomial 

3115 >>> rv = multinomial(8, [0.3, 0.2, 0.5]) 

3116 >>> rv.pmf([1, 3, 4]) 

3117 0.042000000000000072 

3118 

3119 The multinomial distribution for :math:`k=2` is identical to the 

3120 corresponding binomial distribution (tiny numerical differences 

3121 notwithstanding): 

3122 

3123 >>> from scipy.stats import binom 

3124 >>> multinomial.pmf([3, 4], n=7, p=[0.4, 0.6]) 

3125 0.29030399999999973 

3126 >>> binom.pmf(3, 7, 0.4) 

3127 0.29030400000000012 

3128 

3129 The functions ``pmf``, ``logpmf``, ``entropy``, and ``cov`` support 

3130 broadcasting, under the convention that the vector parameters (``x`` and 

3131 ``p``) are interpreted as if each row along the last axis is a single 

3132 object. For instance: 

3133 

3134 >>> multinomial.pmf([[3, 4], [3, 5]], n=[7, 8], p=[.3, .7]) 

3135 array([0.2268945, 0.25412184]) 

3136 

3137 Here, ``x.shape == (2, 2)``, ``n.shape == (2,)``, and ``p.shape == (2,)``, 

3138 but following the rules mentioned above they behave as if the rows 

3139 ``[3, 4]`` and ``[3, 5]`` in ``x`` and ``[.3, .7]`` in ``p`` were a single 

3140 object, and as if we had ``x.shape = (2,)``, ``n.shape = (2,)``, and 

3141 ``p.shape = ()``. To obtain the individual elements without broadcasting, 

3142 we would do this: 

3143 

3144 >>> multinomial.pmf([3, 4], n=7, p=[.3, .7]) 

3145 0.2268945 

3146 >>> multinomial.pmf([3, 5], 8, p=[.3, .7]) 

3147 0.25412184 

3148 

3149 This broadcasting also works for ``cov``, where the output objects are 

3150 square matrices of size ``p.shape[-1]``. For example: 

3151 

3152 >>> multinomial.cov([4, 5], [[.3, .7], [.4, .6]]) 

3153 array([[[ 0.84, -0.84], 

3154 [-0.84, 0.84]], 

3155 [[ 1.2 , -1.2 ], 

3156 [-1.2 , 1.2 ]]]) 

3157 

3158 In this example, ``n.shape == (2,)`` and ``p.shape == (2, 2)``, and 

3159 following the rules above, these broadcast as if ``p.shape == (2,)``. 

3160 Thus the result should also be of shape ``(2,)``, but since each output is 

3161 a :math:`2 \times 2` matrix, the result in fact has shape ``(2, 2, 2)``, 

3162 where ``result[0]`` is equal to ``multinomial.cov(n=4, p=[.3, .7])`` and 

3163 ``result[1]`` is equal to ``multinomial.cov(n=5, p=[.4, .6])``. 

3164 

3165 Alternatively, the object may be called (as a function) to fix the `n` and 

3166 `p` parameters, returning a "frozen" multinomial random variable: 

3167 

3168 >>> rv = multinomial(n=7, p=[.3, .7]) 

3169 >>> # Frozen object with the same methods but holding the given 

3170 >>> # degrees of freedom and scale fixed. 

3171 

3172 See also 

3173 -------- 

3174 scipy.stats.binom : The binomial distribution. 

3175 numpy.random.Generator.multinomial : Sampling from the multinomial distribution. 

3176 scipy.stats.multivariate_hypergeom : 

3177 The multivariate hypergeometric distribution. 

3178 """ # noqa: E501 

3179 

3180 def __init__(self, seed=None): 

3181 super().__init__(seed) 

3182 self.__doc__ = \ 

3183 doccer.docformat(self.__doc__, multinomial_docdict_params) 

3184 

3185 def __call__(self, n, p, seed=None): 

3186 """Create a frozen multinomial distribution. 

3187 

3188 See `multinomial_frozen` for more information. 

3189 """ 

3190 return multinomial_frozen(n, p, seed) 

3191 

3192 def _process_parameters(self, n, p, eps=1e-15): 

3193 """Returns: n_, p_, npcond. 

3194 

3195 n_ and p_ are arrays of the correct shape; npcond is a boolean array 

3196 flagging values out of the domain. 

3197 """ 

3198 p = np.array(p, dtype=np.float64, copy=True) 

3199 p_adjusted = 1. - p[..., :-1].sum(axis=-1) 

3200 i_adjusted = np.abs(p_adjusted) > eps 

3201 p[i_adjusted, -1] = p_adjusted[i_adjusted] 

3202 

3203 # true for bad p 

3204 pcond = np.any(p < 0, axis=-1) 

3205 pcond |= np.any(p > 1, axis=-1) 

3206 

3207 n = np.array(n, dtype=np.int_, copy=True) 

3208 

3209 # true for bad n 

3210 ncond = n < 0 

3211 

3212 return n, p, ncond | pcond 

3213 

3214 def _process_quantiles(self, x, n, p): 

3215 """Returns: x_, xcond. 

3216 

3217 x_ is an int array; xcond is a boolean array flagging values out of the 

3218 domain. 

3219 """ 

3220 xx = np.asarray(x, dtype=np.int_) 

3221 

3222 if xx.ndim == 0: 

3223 raise ValueError("x must be an array.") 

3224 

3225 if xx.size != 0 and not xx.shape[-1] == p.shape[-1]: 

3226 raise ValueError("Size of each quantile should be size of p: " 

3227 "received %d, but expected %d." % 

3228 (xx.shape[-1], p.shape[-1])) 

3229 

3230 # true for x out of the domain 

3231 cond = np.any(xx != x, axis=-1) 

3232 cond |= np.any(xx < 0, axis=-1) 

3233 cond = cond | (np.sum(xx, axis=-1) != n) 

3234 

3235 return xx, cond 

3236 

3237 def _checkresult(self, result, cond, bad_value): 

3238 result = np.asarray(result) 

3239 

3240 if cond.ndim != 0: 

3241 result[cond] = bad_value 

3242 elif cond: 

3243 if result.ndim == 0: 

3244 return bad_value 

3245 result[...] = bad_value 

3246 return result 

3247 

3248 def _logpmf(self, x, n, p): 

3249 return gammaln(n+1) + np.sum(xlogy(x, p) - gammaln(x+1), axis=-1) 

3250 

3251 def logpmf(self, x, n, p): 

3252 """Log of the Multinomial probability mass function. 

3253 

3254 Parameters 

3255 ---------- 

3256 x : array_like 

3257 Quantiles, with the last axis of `x` denoting the components. 

3258 %(_doc_default_callparams)s 

3259 

3260 Returns 

3261 ------- 

3262 logpmf : ndarray or scalar 

3263 Log of the probability mass function evaluated at `x` 

3264 

3265 Notes 

3266 ----- 

3267 %(_doc_callparams_note)s 

3268 """ 

3269 n, p, npcond = self._process_parameters(n, p) 

3270 x, xcond = self._process_quantiles(x, n, p) 

3271 

3272 result = self._logpmf(x, n, p) 

3273 

3274 # replace values for which x was out of the domain; broadcast 

3275 # xcond to the right shape 

3276 xcond_ = xcond | np.zeros(npcond.shape, dtype=np.bool_) 

3277 result = self._checkresult(result, xcond_, -np.inf) 

3278 

3279 # replace values bad for n or p; broadcast npcond to the right shape 

3280 npcond_ = npcond | np.zeros(xcond.shape, dtype=np.bool_) 

3281 return self._checkresult(result, npcond_, np.nan) 

3282 

3283 def pmf(self, x, n, p): 

3284 """Multinomial probability mass function. 

3285 

3286 Parameters 

3287 ---------- 

3288 x : array_like 

3289 Quantiles, with the last axis of `x` denoting the components. 

3290 %(_doc_default_callparams)s 

3291 

3292 Returns 

3293 ------- 

3294 pmf : ndarray or scalar 

3295 Probability density function evaluated at `x` 

3296 

3297 Notes 

3298 ----- 

3299 %(_doc_callparams_note)s 

3300 """ 

3301 return np.exp(self.logpmf(x, n, p)) 

3302 

3303 def mean(self, n, p): 

3304 """Mean of the Multinomial distribution. 

3305 

3306 Parameters 

3307 ---------- 

3308 %(_doc_default_callparams)s 

3309 

3310 Returns 

3311 ------- 

3312 mean : float 

3313 The mean of the distribution 

3314 """ 

3315 n, p, npcond = self._process_parameters(n, p) 

3316 result = n[..., np.newaxis]*p 

3317 return self._checkresult(result, npcond, np.nan) 

3318 

3319 def cov(self, n, p): 

3320 """Covariance matrix of the multinomial distribution. 

3321 

3322 Parameters 

3323 ---------- 

3324 %(_doc_default_callparams)s 

3325 

3326 Returns 

3327 ------- 

3328 cov : ndarray 

3329 The covariance matrix of the distribution 

3330 """ 

3331 n, p, npcond = self._process_parameters(n, p) 

3332 

3333 nn = n[..., np.newaxis, np.newaxis] 

3334 result = nn * np.einsum('...j,...k->...jk', -p, p) 

3335 

3336 # change the diagonal 

3337 for i in range(p.shape[-1]): 

3338 result[..., i, i] += n*p[..., i] 

3339 

3340 return self._checkresult(result, npcond, np.nan) 

3341 

3342 def entropy(self, n, p): 

3343 r"""Compute the entropy of the multinomial distribution. 

3344 

3345 The entropy is computed using this expression: 

3346 

3347 .. math:: 

3348 

3349 f(x) = - \log n! - n\sum_{i=1}^k p_i \log p_i + 

3350 \sum_{i=1}^k \sum_{x=0}^n \binom n x p_i^x(1-p_i)^{n-x} \log x! 

3351 

3352 Parameters 

3353 ---------- 

3354 %(_doc_default_callparams)s 

3355 

3356 Returns 

3357 ------- 

3358 h : scalar 

3359 Entropy of the Multinomial distribution 

3360 

3361 Notes 

3362 ----- 

3363 %(_doc_callparams_note)s 

3364 """ 

3365 n, p, npcond = self._process_parameters(n, p) 

3366 

3367 x = np.r_[1:np.max(n)+1] 

3368 

3369 term1 = n*np.sum(entr(p), axis=-1) 

3370 term1 -= gammaln(n+1) 

3371 

3372 n = n[..., np.newaxis] 

3373 new_axes_needed = max(p.ndim, n.ndim) - x.ndim + 1 

3374 x.shape += (1,)*new_axes_needed 

3375 

3376 term2 = np.sum(binom.pmf(x, n, p)*gammaln(x+1), 

3377 axis=(-1, -1-new_axes_needed)) 

3378 

3379 return self._checkresult(term1 + term2, npcond, np.nan) 

3380 

3381 def rvs(self, n, p, size=None, random_state=None): 

3382 """Draw random samples from a Multinomial distribution. 

3383 

3384 Parameters 

3385 ---------- 

3386 %(_doc_default_callparams)s 

3387 size : integer or iterable of integers, optional 

3388 Number of samples to draw (default 1). 

3389 %(_doc_random_state)s 

3390 

3391 Returns 

3392 ------- 

3393 rvs : ndarray or scalar 

3394 Random variates of shape (`size`, `len(p)`) 

3395 

3396 Notes 

3397 ----- 

3398 %(_doc_callparams_note)s 

3399 """ 

3400 n, p, npcond = self._process_parameters(n, p) 

3401 random_state = self._get_random_state(random_state) 

3402 return random_state.multinomial(n, p, size) 

3403 

3404 

3405multinomial = multinomial_gen() 

3406 

3407 

3408class multinomial_frozen(multi_rv_frozen): 

3409 r"""Create a frozen Multinomial distribution. 

3410 

3411 Parameters 

3412 ---------- 

3413 n : int 

3414 number of trials 

3415 p: array_like 

3416 probability of a trial falling into each category; should sum to 1 

3417 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

3418 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

3419 singleton is used. 

3420 If `seed` is an int, a new ``RandomState`` instance is used, 

3421 seeded with `seed`. 

3422 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

3423 that instance is used. 

3424 """ 

3425 def __init__(self, n, p, seed=None): 

3426 self._dist = multinomial_gen(seed) 

3427 self.n, self.p, self.npcond = self._dist._process_parameters(n, p) 

3428 

3429 # monkey patch self._dist 

3430 def _process_parameters(n, p): 

3431 return self.n, self.p, self.npcond 

3432 

3433 self._dist._process_parameters = _process_parameters 

3434 

3435 def logpmf(self, x): 

3436 return self._dist.logpmf(x, self.n, self.p) 

3437 

3438 def pmf(self, x): 

3439 return self._dist.pmf(x, self.n, self.p) 

3440 

3441 def mean(self): 

3442 return self._dist.mean(self.n, self.p) 

3443 

3444 def cov(self): 

3445 return self._dist.cov(self.n, self.p) 

3446 

3447 def entropy(self): 

3448 return self._dist.entropy(self.n, self.p) 

3449 

3450 def rvs(self, size=1, random_state=None): 

3451 return self._dist.rvs(self.n, self.p, size, random_state) 

3452 

3453 

3454# Set frozen generator docstrings from corresponding docstrings in 

3455# multinomial and fill in default strings in class docstrings 

3456for name in ['logpmf', 'pmf', 'mean', 'cov', 'rvs']: 

3457 method = multinomial_gen.__dict__[name] 

3458 method_frozen = multinomial_frozen.__dict__[name] 

3459 method_frozen.__doc__ = doccer.docformat( 

3460 method.__doc__, multinomial_docdict_noparams) 

3461 method.__doc__ = doccer.docformat(method.__doc__, 

3462 multinomial_docdict_params) 

3463 

3464 

3465class special_ortho_group_gen(multi_rv_generic): 

3466 r"""A Special Orthogonal matrix (SO(N)) random variable. 

3467 

3468 Return a random rotation matrix, drawn from the Haar distribution 

3469 (the only uniform distribution on SO(N)) with a determinant of +1. 

3470 

3471 The `dim` keyword specifies the dimension N. 

3472 

3473 Methods 

3474 ------- 

3475 rvs(dim=None, size=1, random_state=None) 

3476 Draw random samples from SO(N). 

3477 

3478 Parameters 

3479 ---------- 

3480 dim : scalar 

3481 Dimension of matrices 

3482 seed : {None, int, np.random.RandomState, np.random.Generator}, optional 

3483 Used for drawing random variates. 

3484 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

3485 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

3486 with seed. 

3487 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

3488 then that object is used. 

3489 Default is `None`. 

3490 

3491 Notes 

3492 ----- 

3493 This class is wrapping the random_rot code from the MDP Toolkit, 

3494 https://github.com/mdp-toolkit/mdp-toolkit 

3495 

3496 Return a random rotation matrix, drawn from the Haar distribution 

3497 (the only uniform distribution on SO(N)). 

3498 The algorithm is described in the paper 

3499 Stewart, G.W., "The efficient generation of random orthogonal 

3500 matrices with an application to condition estimators", SIAM Journal 

3501 on Numerical Analysis, 17(3), pp. 403-409, 1980. 

3502 For more information see 

3503 https://en.wikipedia.org/wiki/Orthogonal_matrix#Randomization 

3504 

3505 See also the similar `ortho_group`. For a random rotation in three 

3506 dimensions, see `scipy.spatial.transform.Rotation.random`. 

3507 

3508 Examples 

3509 -------- 

3510 >>> import numpy as np 

3511 >>> from scipy.stats import special_ortho_group 

3512 >>> x = special_ortho_group.rvs(3) 

3513 

3514 >>> np.dot(x, x.T) 

3515 array([[ 1.00000000e+00, 1.13231364e-17, -2.86852790e-16], 

3516 [ 1.13231364e-17, 1.00000000e+00, -1.46845020e-16], 

3517 [ -2.86852790e-16, -1.46845020e-16, 1.00000000e+00]]) 

3518 

3519 >>> import scipy.linalg 

3520 >>> scipy.linalg.det(x) 

3521 1.0 

3522 

3523 This generates one random matrix from SO(3). It is orthogonal and 

3524 has a determinant of 1. 

3525 

3526 Alternatively, the object may be called (as a function) to fix the `dim` 

3527 parameter, returning a "frozen" special_ortho_group random variable: 

3528 

3529 >>> rv = special_ortho_group(5) 

3530 >>> # Frozen object with the same methods but holding the 

3531 >>> # dimension parameter fixed. 

3532 

3533 See Also 

3534 -------- 

3535 ortho_group, scipy.spatial.transform.Rotation.random 

3536 

3537 """ 

3538 

3539 def __init__(self, seed=None): 

3540 super().__init__(seed) 

3541 self.__doc__ = doccer.docformat(self.__doc__) 

3542 

3543 def __call__(self, dim=None, seed=None): 

3544 """Create a frozen SO(N) distribution. 

3545 

3546 See `special_ortho_group_frozen` for more information. 

3547 """ 

3548 return special_ortho_group_frozen(dim, seed=seed) 

3549 

3550 def _process_parameters(self, dim): 

3551 """Dimension N must be specified; it cannot be inferred.""" 

3552 if dim is None or not np.isscalar(dim) or dim <= 1 or dim != int(dim): 

3553 raise ValueError("""Dimension of rotation must be specified, 

3554 and must be a scalar greater than 1.""") 

3555 

3556 return dim 

3557 

3558 def rvs(self, dim, size=1, random_state=None): 

3559 """Draw random samples from SO(N). 

3560 

3561 Parameters 

3562 ---------- 

3563 dim : integer 

3564 Dimension of rotation space (N). 

3565 size : integer, optional 

3566 Number of samples to draw (default 1). 

3567 

3568 Returns 

3569 ------- 

3570 rvs : ndarray or scalar 

3571 Random size N-dimensional matrices, dimension (size, dim, dim) 

3572 

3573 """ 

3574 random_state = self._get_random_state(random_state) 

3575 

3576 size = int(size) 

3577 size = (size,) if size > 1 else () 

3578 

3579 dim = self._process_parameters(dim) 

3580 

3581 # H represents a (dim, dim) matrix, while D represents the diagonal of 

3582 # a (dim, dim) diagonal matrix. The algorithm that follows is 

3583 # broadcasted on the leading shape in `size` to vectorize along 

3584 # samples. 

3585 H = np.empty(size + (dim, dim)) 

3586 H[..., :, :] = np.eye(dim) 

3587 D = np.empty(size + (dim,)) 

3588 

3589 for n in range(dim-1): 

3590 

3591 # x is a vector with length dim-n, xrow and xcol are views of it as 

3592 # a row vector and column vector respectively. It's important they 

3593 # are views and not copies because we are going to modify x 

3594 # in-place. 

3595 x = random_state.normal(size=size + (dim-n,)) 

3596 xrow = x[..., None, :] 

3597 xcol = x[..., :, None] 

3598 

3599 # This is the squared norm of x, without vectorization it would be 

3600 # dot(x, x), to have proper broadcasting we use matmul and squeeze 

3601 # out (convert to scalar) the resulting 1x1 matrix 

3602 norm2 = np.matmul(xrow, xcol).squeeze((-2, -1)) 

3603 

3604 x0 = x[..., 0].copy() 

3605 D[..., n] = np.where(x0 != 0, np.sign(x0), 1) 

3606 x[..., 0] += D[..., n]*np.sqrt(norm2) 

3607 

3608 # In renormalizing x we have to append an additional axis with 

3609 # [..., None] to broadcast the scalar against the vector x 

3610 x /= np.sqrt((norm2 - x0**2 + x[..., 0]**2) / 2.)[..., None] 

3611 

3612 # Householder transformation, without vectorization the RHS can be 

3613 # written as outer(H @ x, x) (apart from the slicing) 

3614 H[..., :, n:] -= np.matmul(H[..., :, n:], xcol) * xrow 

3615 

3616 D[..., -1] = (-1)**(dim-1)*D[..., :-1].prod(axis=-1) 

3617 

3618 # Without vectorization this could be written as H = diag(D) @ H, 

3619 # left-multiplication by a diagonal matrix amounts to multiplying each 

3620 # row of H by an element of the diagonal, so we add a dummy axis for 

3621 # the column index 

3622 H *= D[..., :, None] 

3623 return H 

3624 

3625 

3626special_ortho_group = special_ortho_group_gen() 

3627 

3628 

3629class special_ortho_group_frozen(multi_rv_frozen): 

3630 def __init__(self, dim=None, seed=None): 

3631 """Create a frozen SO(N) distribution. 

3632 

3633 Parameters 

3634 ---------- 

3635 dim : scalar 

3636 Dimension of matrices 

3637 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

3638 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

3639 singleton is used. 

3640 If `seed` is an int, a new ``RandomState`` instance is used, 

3641 seeded with `seed`. 

3642 If `seed` is already a ``Generator`` or ``RandomState`` instance 

3643 then that instance is used. 

3644 

3645 Examples 

3646 -------- 

3647 >>> from scipy.stats import special_ortho_group 

3648 >>> g = special_ortho_group(5) 

3649 >>> x = g.rvs() 

3650 

3651 """ 

3652 self._dist = special_ortho_group_gen(seed) 

3653 self.dim = self._dist._process_parameters(dim) 

3654 

3655 def rvs(self, size=1, random_state=None): 

3656 return self._dist.rvs(self.dim, size, random_state) 

3657 

3658 

3659class ortho_group_gen(multi_rv_generic): 

3660 r"""An Orthogonal matrix (O(N)) random variable. 

3661 

3662 Return a random orthogonal matrix, drawn from the O(N) Haar 

3663 distribution (the only uniform distribution on O(N)). 

3664 

3665 The `dim` keyword specifies the dimension N. 

3666 

3667 Methods 

3668 ------- 

3669 rvs(dim=None, size=1, random_state=None) 

3670 Draw random samples from O(N). 

3671 

3672 Parameters 

3673 ---------- 

3674 dim : scalar 

3675 Dimension of matrices 

3676 seed : {None, int, np.random.RandomState, np.random.Generator}, optional 

3677 Used for drawing random variates. 

3678 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

3679 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

3680 with seed. 

3681 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

3682 then that object is used. 

3683 Default is `None`. 

3684 

3685 Notes 

3686 ----- 

3687 This class is closely related to `special_ortho_group`. 

3688 

3689 Some care is taken to avoid numerical error, as per the paper by Mezzadri. 

3690 

3691 References 

3692 ---------- 

3693 .. [1] F. Mezzadri, "How to generate random matrices from the classical 

3694 compact groups", :arXiv:`math-ph/0609050v2`. 

3695 

3696 Examples 

3697 -------- 

3698 >>> import numpy as np 

3699 >>> from scipy.stats import ortho_group 

3700 >>> x = ortho_group.rvs(3) 

3701 

3702 >>> np.dot(x, x.T) 

3703 array([[ 1.00000000e+00, 1.13231364e-17, -2.86852790e-16], 

3704 [ 1.13231364e-17, 1.00000000e+00, -1.46845020e-16], 

3705 [ -2.86852790e-16, -1.46845020e-16, 1.00000000e+00]]) 

3706 

3707 >>> import scipy.linalg 

3708 >>> np.fabs(scipy.linalg.det(x)) 

3709 1.0 

3710 

3711 This generates one random matrix from O(3). It is orthogonal and 

3712 has a determinant of +1 or -1. 

3713 

3714 Alternatively, the object may be called (as a function) to fix the `dim` 

3715 parameter, returning a "frozen" ortho_group random variable: 

3716 

3717 >>> rv = ortho_group(5) 

3718 >>> # Frozen object with the same methods but holding the 

3719 >>> # dimension parameter fixed. 

3720 

3721 See Also 

3722 -------- 

3723 special_ortho_group 

3724 """ 

3725 

3726 def __init__(self, seed=None): 

3727 super().__init__(seed) 

3728 self.__doc__ = doccer.docformat(self.__doc__) 

3729 

3730 def __call__(self, dim=None, seed=None): 

3731 """Create a frozen O(N) distribution. 

3732 

3733 See `ortho_group_frozen` for more information. 

3734 """ 

3735 return ortho_group_frozen(dim, seed=seed) 

3736 

3737 def _process_parameters(self, dim): 

3738 """Dimension N must be specified; it cannot be inferred.""" 

3739 if dim is None or not np.isscalar(dim) or dim <= 1 or dim != int(dim): 

3740 raise ValueError("Dimension of rotation must be specified," 

3741 "and must be a scalar greater than 1.") 

3742 

3743 return dim 

3744 

3745 def rvs(self, dim, size=1, random_state=None): 

3746 """Draw random samples from O(N). 

3747 

3748 Parameters 

3749 ---------- 

3750 dim : integer 

3751 Dimension of rotation space (N). 

3752 size : integer, optional 

3753 Number of samples to draw (default 1). 

3754 

3755 Returns 

3756 ------- 

3757 rvs : ndarray or scalar 

3758 Random size N-dimensional matrices, dimension (size, dim, dim) 

3759 

3760 """ 

3761 random_state = self._get_random_state(random_state) 

3762 

3763 size = int(size) 

3764 if size > 1 and NumpyVersion(np.__version__) < '1.22.0': 

3765 return np.array([self.rvs(dim, size=1, random_state=random_state) 

3766 for i in range(size)]) 

3767 

3768 dim = self._process_parameters(dim) 

3769 

3770 size = (size,) if size > 1 else () 

3771 z = random_state.normal(size=size + (dim, dim)) 

3772 q, r = np.linalg.qr(z) 

3773 # The last two dimensions are the rows and columns of R matrices. 

3774 # Extract the diagonals. Note that this eliminates a dimension. 

3775 d = r.diagonal(offset=0, axis1=-2, axis2=-1) 

3776 # Add back a dimension for proper broadcasting: we're dividing 

3777 # each row of each R matrix by the diagonal of the R matrix. 

3778 q *= (d/abs(d))[..., np.newaxis, :] # to broadcast properly 

3779 return q 

3780 

3781 

3782ortho_group = ortho_group_gen() 

3783 

3784 

3785class ortho_group_frozen(multi_rv_frozen): 

3786 def __init__(self, dim=None, seed=None): 

3787 """Create a frozen O(N) distribution. 

3788 

3789 Parameters 

3790 ---------- 

3791 dim : scalar 

3792 Dimension of matrices 

3793 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

3794 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

3795 singleton is used. 

3796 If `seed` is an int, a new ``RandomState`` instance is used, 

3797 seeded with `seed`. 

3798 If `seed` is already a ``Generator`` or ``RandomState`` instance 

3799 then that instance is used. 

3800 

3801 Examples 

3802 -------- 

3803 >>> from scipy.stats import ortho_group 

3804 >>> g = ortho_group(5) 

3805 >>> x = g.rvs() 

3806 

3807 """ 

3808 self._dist = ortho_group_gen(seed) 

3809 self.dim = self._dist._process_parameters(dim) 

3810 

3811 def rvs(self, size=1, random_state=None): 

3812 return self._dist.rvs(self.dim, size, random_state) 

3813 

3814 

3815class random_correlation_gen(multi_rv_generic): 

3816 r"""A random correlation matrix. 

3817 

3818 Return a random correlation matrix, given a vector of eigenvalues. 

3819 

3820 The `eigs` keyword specifies the eigenvalues of the correlation matrix, 

3821 and implies the dimension. 

3822 

3823 Methods 

3824 ------- 

3825 rvs(eigs=None, random_state=None) 

3826 Draw random correlation matrices, all with eigenvalues eigs. 

3827 

3828 Parameters 

3829 ---------- 

3830 eigs : 1d ndarray 

3831 Eigenvalues of correlation matrix 

3832 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

3833 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

3834 singleton is used. 

3835 If `seed` is an int, a new ``RandomState`` instance is used, 

3836 seeded with `seed`. 

3837 If `seed` is already a ``Generator`` or ``RandomState`` instance 

3838 then that instance is used. 

3839 tol : float, optional 

3840 Tolerance for input parameter checks 

3841 diag_tol : float, optional 

3842 Tolerance for deviation of the diagonal of the resulting 

3843 matrix. Default: 1e-7 

3844 

3845 Raises 

3846 ------ 

3847 RuntimeError 

3848 Floating point error prevented generating a valid correlation 

3849 matrix. 

3850 

3851 Returns 

3852 ------- 

3853 rvs : ndarray or scalar 

3854 Random size N-dimensional matrices, dimension (size, dim, dim), 

3855 each having eigenvalues eigs. 

3856 

3857 Notes 

3858 ----- 

3859 

3860 Generates a random correlation matrix following a numerically stable 

3861 algorithm spelled out by Davies & Higham. This algorithm uses a single O(N) 

3862 similarity transformation to construct a symmetric positive semi-definite 

3863 matrix, and applies a series of Givens rotations to scale it to have ones 

3864 on the diagonal. 

3865 

3866 References 

3867 ---------- 

3868 

3869 .. [1] Davies, Philip I; Higham, Nicholas J; "Numerically stable generation 

3870 of correlation matrices and their factors", BIT 2000, Vol. 40, 

3871 No. 4, pp. 640 651 

3872 

3873 Examples 

3874 -------- 

3875 >>> import numpy as np 

3876 >>> from scipy.stats import random_correlation 

3877 >>> rng = np.random.default_rng() 

3878 >>> x = random_correlation.rvs((.5, .8, 1.2, 1.5), random_state=rng) 

3879 >>> x 

3880 array([[ 1. , -0.02423399, 0.03130519, 0.4946965 ], 

3881 [-0.02423399, 1. , 0.20334736, 0.04039817], 

3882 [ 0.03130519, 0.20334736, 1. , 0.02694275], 

3883 [ 0.4946965 , 0.04039817, 0.02694275, 1. ]]) 

3884 >>> import scipy.linalg 

3885 >>> e, v = scipy.linalg.eigh(x) 

3886 >>> e 

3887 array([ 0.5, 0.8, 1.2, 1.5]) 

3888 

3889 """ 

3890 

3891 def __init__(self, seed=None): 

3892 super().__init__(seed) 

3893 self.__doc__ = doccer.docformat(self.__doc__) 

3894 

3895 def __call__(self, eigs, seed=None, tol=1e-13, diag_tol=1e-7): 

3896 """Create a frozen random correlation matrix. 

3897 

3898 See `random_correlation_frozen` for more information. 

3899 """ 

3900 return random_correlation_frozen(eigs, seed=seed, tol=tol, 

3901 diag_tol=diag_tol) 

3902 

3903 def _process_parameters(self, eigs, tol): 

3904 eigs = np.asarray(eigs, dtype=float) 

3905 dim = eigs.size 

3906 

3907 if eigs.ndim != 1 or eigs.shape[0] != dim or dim <= 1: 

3908 raise ValueError("Array 'eigs' must be a vector of length " 

3909 "greater than 1.") 

3910 

3911 if np.fabs(np.sum(eigs) - dim) > tol: 

3912 raise ValueError("Sum of eigenvalues must equal dimensionality.") 

3913 

3914 for x in eigs: 

3915 if x < -tol: 

3916 raise ValueError("All eigenvalues must be non-negative.") 

3917 

3918 return dim, eigs 

3919 

3920 def _givens_to_1(self, aii, ajj, aij): 

3921 """Computes a 2x2 Givens matrix to put 1's on the diagonal. 

3922 

3923 The input matrix is a 2x2 symmetric matrix M = [ aii aij ; aij ajj ]. 

3924 

3925 The output matrix g is a 2x2 anti-symmetric matrix of the form 

3926 [ c s ; -s c ]; the elements c and s are returned. 

3927 

3928 Applying the output matrix to the input matrix (as b=g.T M g) 

3929 results in a matrix with bii=1, provided tr(M) - det(M) >= 1 

3930 and floating point issues do not occur. Otherwise, some other 

3931 valid rotation is returned. When tr(M)==2, also bjj=1. 

3932 

3933 """ 

3934 aiid = aii - 1. 

3935 ajjd = ajj - 1. 

3936 

3937 if ajjd == 0: 

3938 # ajj==1, so swap aii and ajj to avoid division by zero 

3939 return 0., 1. 

3940 

3941 dd = math.sqrt(max(aij**2 - aiid*ajjd, 0)) 

3942 

3943 # The choice of t should be chosen to avoid cancellation [1] 

3944 t = (aij + math.copysign(dd, aij)) / ajjd 

3945 c = 1. / math.sqrt(1. + t*t) 

3946 if c == 0: 

3947 # Underflow 

3948 s = 1.0 

3949 else: 

3950 s = c*t 

3951 return c, s 

3952 

3953 def _to_corr(self, m): 

3954 """ 

3955 Given a psd matrix m, rotate to put one's on the diagonal, turning it 

3956 into a correlation matrix. This also requires the trace equal the 

3957 dimensionality. Note: modifies input matrix 

3958 """ 

3959 # Check requirements for in-place Givens 

3960 if not (m.flags.c_contiguous and m.dtype == np.float64 and 

3961 m.shape[0] == m.shape[1]): 

3962 raise ValueError() 

3963 

3964 d = m.shape[0] 

3965 for i in range(d-1): 

3966 if m[i, i] == 1: 

3967 continue 

3968 elif m[i, i] > 1: 

3969 for j in range(i+1, d): 

3970 if m[j, j] < 1: 

3971 break 

3972 else: 

3973 for j in range(i+1, d): 

3974 if m[j, j] > 1: 

3975 break 

3976 

3977 c, s = self._givens_to_1(m[i, i], m[j, j], m[i, j]) 

3978 

3979 # Use BLAS to apply Givens rotations in-place. Equivalent to: 

3980 # g = np.eye(d) 

3981 # g[i, i] = g[j,j] = c 

3982 # g[j, i] = -s; g[i, j] = s 

3983 # m = np.dot(g.T, np.dot(m, g)) 

3984 mv = m.ravel() 

3985 drot(mv, mv, c, -s, n=d, 

3986 offx=i*d, incx=1, offy=j*d, incy=1, 

3987 overwrite_x=True, overwrite_y=True) 

3988 drot(mv, mv, c, -s, n=d, 

3989 offx=i, incx=d, offy=j, incy=d, 

3990 overwrite_x=True, overwrite_y=True) 

3991 

3992 return m 

3993 

3994 def rvs(self, eigs, random_state=None, tol=1e-13, diag_tol=1e-7): 

3995 """Draw random correlation matrices. 

3996 

3997 Parameters 

3998 ---------- 

3999 eigs : 1d ndarray 

4000 Eigenvalues of correlation matrix 

4001 tol : float, optional 

4002 Tolerance for input parameter checks 

4003 diag_tol : float, optional 

4004 Tolerance for deviation of the diagonal of the resulting 

4005 matrix. Default: 1e-7 

4006 

4007 Raises 

4008 ------ 

4009 RuntimeError 

4010 Floating point error prevented generating a valid correlation 

4011 matrix. 

4012 

4013 Returns 

4014 ------- 

4015 rvs : ndarray or scalar 

4016 Random size N-dimensional matrices, dimension (size, dim, dim), 

4017 each having eigenvalues eigs. 

4018 

4019 """ 

4020 dim, eigs = self._process_parameters(eigs, tol=tol) 

4021 

4022 random_state = self._get_random_state(random_state) 

4023 

4024 m = ortho_group.rvs(dim, random_state=random_state) 

4025 m = np.dot(np.dot(m, np.diag(eigs)), m.T) # Set the trace of m 

4026 m = self._to_corr(m) # Carefully rotate to unit diagonal 

4027 

4028 # Check diagonal 

4029 if abs(m.diagonal() - 1).max() > diag_tol: 

4030 raise RuntimeError("Failed to generate a valid correlation matrix") 

4031 

4032 return m 

4033 

4034 

4035random_correlation = random_correlation_gen() 

4036 

4037 

4038class random_correlation_frozen(multi_rv_frozen): 

4039 def __init__(self, eigs, seed=None, tol=1e-13, diag_tol=1e-7): 

4040 """Create a frozen random correlation matrix distribution. 

4041 

4042 Parameters 

4043 ---------- 

4044 eigs : 1d ndarray 

4045 Eigenvalues of correlation matrix 

4046 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

4047 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

4048 singleton is used. 

4049 If `seed` is an int, a new ``RandomState`` instance is used, 

4050 seeded with `seed`. 

4051 If `seed` is already a ``Generator`` or ``RandomState`` instance 

4052 then that instance is used. 

4053 tol : float, optional 

4054 Tolerance for input parameter checks 

4055 diag_tol : float, optional 

4056 Tolerance for deviation of the diagonal of the resulting 

4057 matrix. Default: 1e-7 

4058 

4059 Raises 

4060 ------ 

4061 RuntimeError 

4062 Floating point error prevented generating a valid correlation 

4063 matrix. 

4064 

4065 Returns 

4066 ------- 

4067 rvs : ndarray or scalar 

4068 Random size N-dimensional matrices, dimension (size, dim, dim), 

4069 each having eigenvalues eigs. 

4070 """ 

4071 

4072 self._dist = random_correlation_gen(seed) 

4073 self.tol = tol 

4074 self.diag_tol = diag_tol 

4075 _, self.eigs = self._dist._process_parameters(eigs, tol=self.tol) 

4076 

4077 def rvs(self, random_state=None): 

4078 return self._dist.rvs(self.eigs, random_state=random_state, 

4079 tol=self.tol, diag_tol=self.diag_tol) 

4080 

4081 

4082class unitary_group_gen(multi_rv_generic): 

4083 r"""A matrix-valued U(N) random variable. 

4084 

4085 Return a random unitary matrix. 

4086 

4087 The `dim` keyword specifies the dimension N. 

4088 

4089 Methods 

4090 ------- 

4091 rvs(dim=None, size=1, random_state=None) 

4092 Draw random samples from U(N). 

4093 

4094 Parameters 

4095 ---------- 

4096 dim : scalar 

4097 Dimension of matrices 

4098 seed : {None, int, np.random.RandomState, np.random.Generator}, optional 

4099 Used for drawing random variates. 

4100 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

4101 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

4102 with seed. 

4103 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

4104 then that object is used. 

4105 Default is `None`. 

4106 

4107 Notes 

4108 ----- 

4109 This class is similar to `ortho_group`. 

4110 

4111 References 

4112 ---------- 

4113 .. [1] F. Mezzadri, "How to generate random matrices from the classical 

4114 compact groups", :arXiv:`math-ph/0609050v2`. 

4115 

4116 Examples 

4117 -------- 

4118 >>> import numpy as np 

4119 >>> from scipy.stats import unitary_group 

4120 >>> x = unitary_group.rvs(3) 

4121 

4122 >>> np.dot(x, x.conj().T) 

4123 array([[ 1.00000000e+00, 1.13231364e-17, -2.86852790e-16], 

4124 [ 1.13231364e-17, 1.00000000e+00, -1.46845020e-16], 

4125 [ -2.86852790e-16, -1.46845020e-16, 1.00000000e+00]]) 

4126 

4127 This generates one random matrix from U(3). The dot product confirms that 

4128 it is unitary up to machine precision. 

4129 

4130 Alternatively, the object may be called (as a function) to fix the `dim` 

4131 parameter, return a "frozen" unitary_group random variable: 

4132 

4133 >>> rv = unitary_group(5) 

4134 

4135 See Also 

4136 -------- 

4137 ortho_group 

4138 

4139 """ 

4140 

4141 def __init__(self, seed=None): 

4142 super().__init__(seed) 

4143 self.__doc__ = doccer.docformat(self.__doc__) 

4144 

4145 def __call__(self, dim=None, seed=None): 

4146 """Create a frozen (U(N)) n-dimensional unitary matrix distribution. 

4147 

4148 See `unitary_group_frozen` for more information. 

4149 """ 

4150 return unitary_group_frozen(dim, seed=seed) 

4151 

4152 def _process_parameters(self, dim): 

4153 """Dimension N must be specified; it cannot be inferred.""" 

4154 if dim is None or not np.isscalar(dim) or dim <= 1 or dim != int(dim): 

4155 raise ValueError("Dimension of rotation must be specified," 

4156 "and must be a scalar greater than 1.") 

4157 

4158 return dim 

4159 

4160 def rvs(self, dim, size=1, random_state=None): 

4161 """Draw random samples from U(N). 

4162 

4163 Parameters 

4164 ---------- 

4165 dim : integer 

4166 Dimension of space (N). 

4167 size : integer, optional 

4168 Number of samples to draw (default 1). 

4169 

4170 Returns 

4171 ------- 

4172 rvs : ndarray or scalar 

4173 Random size N-dimensional matrices, dimension (size, dim, dim) 

4174 

4175 """ 

4176 random_state = self._get_random_state(random_state) 

4177 

4178 size = int(size) 

4179 if size > 1 and NumpyVersion(np.__version__) < '1.22.0': 

4180 return np.array([self.rvs(dim, size=1, random_state=random_state) 

4181 for i in range(size)]) 

4182 

4183 dim = self._process_parameters(dim) 

4184 

4185 size = (size,) if size > 1 else () 

4186 z = 1/math.sqrt(2)*(random_state.normal(size=size + (dim, dim)) + 

4187 1j*random_state.normal(size=size + (dim, dim))) 

4188 q, r = np.linalg.qr(z) 

4189 # The last two dimensions are the rows and columns of R matrices. 

4190 # Extract the diagonals. Note that this eliminates a dimension. 

4191 d = r.diagonal(offset=0, axis1=-2, axis2=-1) 

4192 # Add back a dimension for proper broadcasting: we're dividing 

4193 # each row of each R matrix by the diagonal of the R matrix. 

4194 q *= (d/abs(d))[..., np.newaxis, :] # to broadcast properly 

4195 return q 

4196 

4197 

4198unitary_group = unitary_group_gen() 

4199 

4200 

4201class unitary_group_frozen(multi_rv_frozen): 

4202 def __init__(self, dim=None, seed=None): 

4203 """Create a frozen (U(N)) n-dimensional unitary matrix distribution. 

4204 

4205 Parameters 

4206 ---------- 

4207 dim : scalar 

4208 Dimension of matrices 

4209 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

4210 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

4211 singleton is used. 

4212 If `seed` is an int, a new ``RandomState`` instance is used, 

4213 seeded with `seed`. 

4214 If `seed` is already a ``Generator`` or ``RandomState`` instance 

4215 then that instance is used. 

4216 

4217 Examples 

4218 -------- 

4219 >>> from scipy.stats import unitary_group 

4220 >>> x = unitary_group(3) 

4221 >>> x.rvs() 

4222 

4223 """ 

4224 self._dist = unitary_group_gen(seed) 

4225 self.dim = self._dist._process_parameters(dim) 

4226 

4227 def rvs(self, size=1, random_state=None): 

4228 return self._dist.rvs(self.dim, size, random_state) 

4229 

4230 

4231_mvt_doc_default_callparams = """\ 

4232loc : array_like, optional 

4233 Location of the distribution. (default ``0``) 

4234shape : array_like, optional 

4235 Positive semidefinite matrix of the distribution. (default ``1``) 

4236df : float, optional 

4237 Degrees of freedom of the distribution; must be greater than zero. 

4238 If ``np.inf`` then results are multivariate normal. The default is ``1``. 

4239allow_singular : bool, optional 

4240 Whether to allow a singular matrix. (default ``False``) 

4241""" 

4242 

4243_mvt_doc_callparams_note = """\ 

4244Setting the parameter `loc` to ``None`` is equivalent to having `loc` 

4245be the zero-vector. The parameter `shape` can be a scalar, in which case 

4246the shape matrix is the identity times that value, a vector of 

4247diagonal entries for the shape matrix, or a two-dimensional array_like. 

4248""" 

4249 

4250_mvt_doc_frozen_callparams_note = """\ 

4251See class definition for a detailed description of parameters.""" 

4252 

4253mvt_docdict_params = { 

4254 '_mvt_doc_default_callparams': _mvt_doc_default_callparams, 

4255 '_mvt_doc_callparams_note': _mvt_doc_callparams_note, 

4256 '_doc_random_state': _doc_random_state 

4257} 

4258 

4259mvt_docdict_noparams = { 

4260 '_mvt_doc_default_callparams': "", 

4261 '_mvt_doc_callparams_note': _mvt_doc_frozen_callparams_note, 

4262 '_doc_random_state': _doc_random_state 

4263} 

4264 

4265 

4266class multivariate_t_gen(multi_rv_generic): 

4267 r"""A multivariate t-distributed random variable. 

4268 

4269 The `loc` parameter specifies the location. The `shape` parameter specifies 

4270 the positive semidefinite shape matrix. The `df` parameter specifies the 

4271 degrees of freedom. 

4272 

4273 In addition to calling the methods below, the object itself may be called 

4274 as a function to fix the location, shape matrix, and degrees of freedom 

4275 parameters, returning a "frozen" multivariate t-distribution random. 

4276 

4277 Methods 

4278 ------- 

4279 pdf(x, loc=None, shape=1, df=1, allow_singular=False) 

4280 Probability density function. 

4281 logpdf(x, loc=None, shape=1, df=1, allow_singular=False) 

4282 Log of the probability density function. 

4283 cdf(x, loc=None, shape=1, df=1, allow_singular=False, *, 

4284 maxpts=None, lower_limit=None, random_state=None) 

4285 Cumulative distribution function. 

4286 rvs(loc=None, shape=1, df=1, size=1, random_state=None) 

4287 Draw random samples from a multivariate t-distribution. 

4288 entropy(loc=None, shape=1, df=1) 

4289 Differential entropy of a multivariate t-distribution. 

4290 

4291 Parameters 

4292 ---------- 

4293 %(_mvt_doc_default_callparams)s 

4294 %(_doc_random_state)s 

4295 

4296 Notes 

4297 ----- 

4298 %(_mvt_doc_callparams_note)s 

4299 The matrix `shape` must be a (symmetric) positive semidefinite matrix. The 

4300 determinant and inverse of `shape` are computed as the pseudo-determinant 

4301 and pseudo-inverse, respectively, so that `shape` does not need to have 

4302 full rank. 

4303 

4304 The probability density function for `multivariate_t` is 

4305 

4306 .. math:: 

4307 

4308 f(x) = \frac{\Gamma((\nu + p)/2)}{\Gamma(\nu/2)\nu^{p/2}\pi^{p/2}|\Sigma|^{1/2}} 

4309 \left[1 + \frac{1}{\nu} (\mathbf{x} - \boldsymbol{\mu})^{\top} 

4310 \boldsymbol{\Sigma}^{-1} 

4311 (\mathbf{x} - \boldsymbol{\mu}) \right]^{-(\nu + p)/2}, 

4312 

4313 where :math:`p` is the dimension of :math:`\mathbf{x}`, 

4314 :math:`\boldsymbol{\mu}` is the :math:`p`-dimensional location, 

4315 :math:`\boldsymbol{\Sigma}` the :math:`p \times p`-dimensional shape 

4316 matrix, and :math:`\nu` is the degrees of freedom. 

4317 

4318 .. versionadded:: 1.6.0 

4319 

4320 References 

4321 ---------- 

4322 [1] Arellano-Valle et al. "Shannon Entropy and Mutual Information for 

4323 Multivariate Skew-Elliptical Distributions". Scandinavian Journal 

4324 of Statistics. Vol. 40, issue 1. 

4325 

4326 Examples 

4327 -------- 

4328 The object may be called (as a function) to fix the `loc`, `shape`, 

4329 `df`, and `allow_singular` parameters, returning a "frozen" 

4330 multivariate_t random variable: 

4331 

4332 >>> import numpy as np 

4333 >>> from scipy.stats import multivariate_t 

4334 >>> rv = multivariate_t([1.0, -0.5], [[2.1, 0.3], [0.3, 1.5]], df=2) 

4335 >>> # Frozen object with the same methods but holding the given location, 

4336 >>> # scale, and degrees of freedom fixed. 

4337 

4338 Create a contour plot of the PDF. 

4339 

4340 >>> import matplotlib.pyplot as plt 

4341 >>> x, y = np.mgrid[-1:3:.01, -2:1.5:.01] 

4342 >>> pos = np.dstack((x, y)) 

4343 >>> fig, ax = plt.subplots(1, 1) 

4344 >>> ax.set_aspect('equal') 

4345 >>> plt.contourf(x, y, rv.pdf(pos)) 

4346 

4347 """ 

4348 

4349 def __init__(self, seed=None): 

4350 """Initialize a multivariate t-distributed random variable. 

4351 

4352 Parameters 

4353 ---------- 

4354 seed : Random state. 

4355 

4356 """ 

4357 super().__init__(seed) 

4358 self.__doc__ = doccer.docformat(self.__doc__, mvt_docdict_params) 

4359 self._random_state = check_random_state(seed) 

4360 

4361 def __call__(self, loc=None, shape=1, df=1, allow_singular=False, 

4362 seed=None): 

4363 """Create a frozen multivariate t-distribution. 

4364 

4365 See `multivariate_t_frozen` for parameters. 

4366 """ 

4367 if df == np.inf: 

4368 return multivariate_normal_frozen(mean=loc, cov=shape, 

4369 allow_singular=allow_singular, 

4370 seed=seed) 

4371 return multivariate_t_frozen(loc=loc, shape=shape, df=df, 

4372 allow_singular=allow_singular, seed=seed) 

4373 

4374 def pdf(self, x, loc=None, shape=1, df=1, allow_singular=False): 

4375 """Multivariate t-distribution probability density function. 

4376 

4377 Parameters 

4378 ---------- 

4379 x : array_like 

4380 Points at which to evaluate the probability density function. 

4381 %(_mvt_doc_default_callparams)s 

4382 

4383 Returns 

4384 ------- 

4385 pdf : Probability density function evaluated at `x`. 

4386 

4387 Examples 

4388 -------- 

4389 >>> from scipy.stats import multivariate_t 

4390 >>> x = [0.4, 5] 

4391 >>> loc = [0, 1] 

4392 >>> shape = [[1, 0.1], [0.1, 1]] 

4393 >>> df = 7 

4394 >>> multivariate_t.pdf(x, loc, shape, df) 

4395 0.00075713 

4396 

4397 """ 

4398 dim, loc, shape, df = self._process_parameters(loc, shape, df) 

4399 x = self._process_quantiles(x, dim) 

4400 shape_info = _PSD(shape, allow_singular=allow_singular) 

4401 logpdf = self._logpdf(x, loc, shape_info.U, shape_info.log_pdet, df, 

4402 dim, shape_info.rank) 

4403 return np.exp(logpdf) 

4404 

4405 def logpdf(self, x, loc=None, shape=1, df=1): 

4406 """Log of the multivariate t-distribution probability density function. 

4407 

4408 Parameters 

4409 ---------- 

4410 x : array_like 

4411 Points at which to evaluate the log of the probability density 

4412 function. 

4413 %(_mvt_doc_default_callparams)s 

4414 

4415 Returns 

4416 ------- 

4417 logpdf : Log of the probability density function evaluated at `x`. 

4418 

4419 Examples 

4420 -------- 

4421 >>> from scipy.stats import multivariate_t 

4422 >>> x = [0.4, 5] 

4423 >>> loc = [0, 1] 

4424 >>> shape = [[1, 0.1], [0.1, 1]] 

4425 >>> df = 7 

4426 >>> multivariate_t.logpdf(x, loc, shape, df) 

4427 -7.1859802 

4428 

4429 See Also 

4430 -------- 

4431 pdf : Probability density function. 

4432 

4433 """ 

4434 dim, loc, shape, df = self._process_parameters(loc, shape, df) 

4435 x = self._process_quantiles(x, dim) 

4436 shape_info = _PSD(shape) 

4437 return self._logpdf(x, loc, shape_info.U, shape_info.log_pdet, df, dim, 

4438 shape_info.rank) 

4439 

4440 def _logpdf(self, x, loc, prec_U, log_pdet, df, dim, rank): 

4441 """Utility method `pdf`, `logpdf` for parameters. 

4442 

4443 Parameters 

4444 ---------- 

4445 x : ndarray 

4446 Points at which to evaluate the log of the probability density 

4447 function. 

4448 loc : ndarray 

4449 Location of the distribution. 

4450 prec_U : ndarray 

4451 A decomposition such that `np.dot(prec_U, prec_U.T)` is the inverse 

4452 of the shape matrix. 

4453 log_pdet : float 

4454 Logarithm of the determinant of the shape matrix. 

4455 df : float 

4456 Degrees of freedom of the distribution. 

4457 dim : int 

4458 Dimension of the quantiles x. 

4459 rank : int 

4460 Rank of the shape matrix. 

4461 

4462 Notes 

4463 ----- 

4464 As this function does no argument checking, it should not be called 

4465 directly; use 'logpdf' instead. 

4466 

4467 """ 

4468 if df == np.inf: 

4469 return multivariate_normal._logpdf(x, loc, prec_U, log_pdet, rank) 

4470 

4471 dev = x - loc 

4472 maha = np.square(np.dot(dev, prec_U)).sum(axis=-1) 

4473 

4474 t = 0.5 * (df + dim) 

4475 A = gammaln(t) 

4476 B = gammaln(0.5 * df) 

4477 C = dim/2. * np.log(df * np.pi) 

4478 D = 0.5 * log_pdet 

4479 E = -t * np.log(1 + (1./df) * maha) 

4480 

4481 return _squeeze_output(A - B - C - D + E) 

4482 

4483 def _cdf(self, x, loc, shape, df, dim, maxpts=None, lower_limit=None, 

4484 random_state=None): 

4485 

4486 # All of this - random state validation, maxpts, apply_along_axis, 

4487 # etc. needs to go in this private method unless we want 

4488 # frozen distribution's `cdf` method to duplicate it or call `cdf`, 

4489 # which would require re-processing parameters 

4490 if random_state is not None: 

4491 rng = check_random_state(random_state) 

4492 else: 

4493 rng = self._random_state 

4494 

4495 if not maxpts: 

4496 maxpts = 1000 * dim 

4497 

4498 x = self._process_quantiles(x, dim) 

4499 lower_limit = (np.full(loc.shape, -np.inf) 

4500 if lower_limit is None else lower_limit) 

4501 

4502 # remove the mean 

4503 x, lower_limit = x - loc, lower_limit - loc 

4504 

4505 b, a = np.broadcast_arrays(x, lower_limit) 

4506 i_swap = b < a 

4507 signs = (-1)**(i_swap.sum(axis=-1)) # odd # of swaps -> negative 

4508 a, b = a.copy(), b.copy() 

4509 a[i_swap], b[i_swap] = b[i_swap], a[i_swap] 

4510 n = x.shape[-1] 

4511 limits = np.concatenate((a, b), axis=-1) 

4512 

4513 def func1d(limits): 

4514 a, b = limits[:n], limits[n:] 

4515 return _qmvt(maxpts, df, shape, a, b, rng)[0] 

4516 

4517 res = np.apply_along_axis(func1d, -1, limits) * signs 

4518 # Fixing the output shape for existing distributions is a separate 

4519 # issue. For now, let's keep this consistent with pdf. 

4520 return _squeeze_output(res) 

4521 

4522 def cdf(self, x, loc=None, shape=1, df=1, allow_singular=False, *, 

4523 maxpts=None, lower_limit=None, random_state=None): 

4524 """Multivariate t-distribution cumulative distribution function. 

4525 

4526 Parameters 

4527 ---------- 

4528 x : array_like 

4529 Points at which to evaluate the cumulative distribution function. 

4530 %(_mvt_doc_default_callparams)s 

4531 maxpts : int, optional 

4532 Maximum number of points to use for integration. The default is 

4533 1000 times the number of dimensions. 

4534 lower_limit : array_like, optional 

4535 Lower limit of integration of the cumulative distribution function. 

4536 Default is negative infinity. Must be broadcastable with `x`. 

4537 %(_doc_random_state)s 

4538 

4539 Returns 

4540 ------- 

4541 cdf : ndarray or scalar 

4542 Cumulative distribution function evaluated at `x`. 

4543 

4544 Examples 

4545 -------- 

4546 >>> from scipy.stats import multivariate_t 

4547 >>> x = [0.4, 5] 

4548 >>> loc = [0, 1] 

4549 >>> shape = [[1, 0.1], [0.1, 1]] 

4550 >>> df = 7 

4551 >>> multivariate_t.cdf(x, loc, shape, df) 

4552 0.64798491 

4553 

4554 """ 

4555 dim, loc, shape, df = self._process_parameters(loc, shape, df) 

4556 shape = _PSD(shape, allow_singular=allow_singular)._M 

4557 

4558 return self._cdf(x, loc, shape, df, dim, maxpts, 

4559 lower_limit, random_state) 

4560 

4561 def _entropy(self, dim, df=1, shape=1): 

4562 if df == np.inf: 

4563 return multivariate_normal(None, cov=shape).entropy() 

4564 

4565 shape_info = _PSD(shape) 

4566 halfsum = 0.5 * (dim + df) 

4567 half_df = 0.5 * df 

4568 return (-gammaln(halfsum) + gammaln(half_df) 

4569 + 0.5 * dim * np.log(df * np.pi) + halfsum 

4570 * (psi(halfsum) - psi(half_df)) 

4571 + 0.5 * shape_info.log_pdet) 

4572 

4573 def entropy(self, loc=None, shape=1, df=1): 

4574 """Calculate the differential entropy of a multivariate 

4575 t-distribution. 

4576 

4577 Parameters 

4578 ---------- 

4579 %(_mvt_doc_default_callparams)s 

4580 

4581 Returns 

4582 ------- 

4583 h : float 

4584 Differential entropy 

4585 

4586 """ 

4587 dim, loc, shape, df = self._process_parameters(None, shape, df) 

4588 return self._entropy(dim, df, shape) 

4589 

4590 def rvs(self, loc=None, shape=1, df=1, size=1, random_state=None): 

4591 """Draw random samples from a multivariate t-distribution. 

4592 

4593 Parameters 

4594 ---------- 

4595 %(_mvt_doc_default_callparams)s 

4596 size : integer, optional 

4597 Number of samples to draw (default 1). 

4598 %(_doc_random_state)s 

4599 

4600 Returns 

4601 ------- 

4602 rvs : ndarray or scalar 

4603 Random variates of size (`size`, `P`), where `P` is the 

4604 dimension of the random variable. 

4605 

4606 Examples 

4607 -------- 

4608 >>> from scipy.stats import multivariate_t 

4609 >>> x = [0.4, 5] 

4610 >>> loc = [0, 1] 

4611 >>> shape = [[1, 0.1], [0.1, 1]] 

4612 >>> df = 7 

4613 >>> multivariate_t.rvs(loc, shape, df) 

4614 array([[0.93477495, 3.00408716]]) 

4615 

4616 """ 

4617 # For implementation details, see equation (3): 

4618 # 

4619 # Hofert, "On Sampling from the Multivariatet Distribution", 2013 

4620 # http://rjournal.github.io/archive/2013-2/hofert.pdf 

4621 # 

4622 dim, loc, shape, df = self._process_parameters(loc, shape, df) 

4623 if random_state is not None: 

4624 rng = check_random_state(random_state) 

4625 else: 

4626 rng = self._random_state 

4627 

4628 if np.isinf(df): 

4629 x = np.ones(size) 

4630 else: 

4631 x = rng.chisquare(df, size=size) / df 

4632 

4633 z = rng.multivariate_normal(np.zeros(dim), shape, size=size) 

4634 samples = loc + z / np.sqrt(x)[..., None] 

4635 return _squeeze_output(samples) 

4636 

4637 def _process_quantiles(self, x, dim): 

4638 """ 

4639 Adjust quantiles array so that last axis labels the components of 

4640 each data point. 

4641 """ 

4642 x = np.asarray(x, dtype=float) 

4643 if x.ndim == 0: 

4644 x = x[np.newaxis] 

4645 elif x.ndim == 1: 

4646 if dim == 1: 

4647 x = x[:, np.newaxis] 

4648 else: 

4649 x = x[np.newaxis, :] 

4650 return x 

4651 

4652 def _process_parameters(self, loc, shape, df): 

4653 """ 

4654 Infer dimensionality from location array and shape matrix, handle 

4655 defaults, and ensure compatible dimensions. 

4656 """ 

4657 if loc is None and shape is None: 

4658 loc = np.asarray(0, dtype=float) 

4659 shape = np.asarray(1, dtype=float) 

4660 dim = 1 

4661 elif loc is None: 

4662 shape = np.asarray(shape, dtype=float) 

4663 if shape.ndim < 2: 

4664 dim = 1 

4665 else: 

4666 dim = shape.shape[0] 

4667 loc = np.zeros(dim) 

4668 elif shape is None: 

4669 loc = np.asarray(loc, dtype=float) 

4670 dim = loc.size 

4671 shape = np.eye(dim) 

4672 else: 

4673 shape = np.asarray(shape, dtype=float) 

4674 loc = np.asarray(loc, dtype=float) 

4675 dim = loc.size 

4676 

4677 if dim == 1: 

4678 loc = loc.reshape(1) 

4679 shape = shape.reshape(1, 1) 

4680 

4681 if loc.ndim != 1 or loc.shape[0] != dim: 

4682 raise ValueError("Array 'loc' must be a vector of length %d." % 

4683 dim) 

4684 if shape.ndim == 0: 

4685 shape = shape * np.eye(dim) 

4686 elif shape.ndim == 1: 

4687 shape = np.diag(shape) 

4688 elif shape.ndim == 2 and shape.shape != (dim, dim): 

4689 rows, cols = shape.shape 

4690 if rows != cols: 

4691 msg = ("Array 'cov' must be square if it is two dimensional," 

4692 " but cov.shape = %s." % str(shape.shape)) 

4693 else: 

4694 msg = ("Dimension mismatch: array 'cov' is of shape %s," 

4695 " but 'loc' is a vector of length %d.") 

4696 msg = msg % (str(shape.shape), len(loc)) 

4697 raise ValueError(msg) 

4698 elif shape.ndim > 2: 

4699 raise ValueError("Array 'cov' must be at most two-dimensional," 

4700 " but cov.ndim = %d" % shape.ndim) 

4701 

4702 # Process degrees of freedom. 

4703 if df is None: 

4704 df = 1 

4705 elif df <= 0: 

4706 raise ValueError("'df' must be greater than zero.") 

4707 elif np.isnan(df): 

4708 raise ValueError("'df' is 'nan' but must be greater than zero or 'np.inf'.") 

4709 

4710 return dim, loc, shape, df 

4711 

4712 

4713class multivariate_t_frozen(multi_rv_frozen): 

4714 

4715 def __init__(self, loc=None, shape=1, df=1, allow_singular=False, 

4716 seed=None): 

4717 """Create a frozen multivariate t distribution. 

4718 

4719 Parameters 

4720 ---------- 

4721 %(_mvt_doc_default_callparams)s 

4722 

4723 Examples 

4724 -------- 

4725 >>> import numpy as np 

4726 >>> loc = np.zeros(3) 

4727 >>> shape = np.eye(3) 

4728 >>> df = 10 

4729 >>> dist = multivariate_t(loc, shape, df) 

4730 >>> dist.rvs() 

4731 array([[ 0.81412036, -1.53612361, 0.42199647]]) 

4732 >>> dist.pdf([1, 1, 1]) 

4733 array([0.01237803]) 

4734 

4735 """ 

4736 self._dist = multivariate_t_gen(seed) 

4737 dim, loc, shape, df = self._dist._process_parameters(loc, shape, df) 

4738 self.dim, self.loc, self.shape, self.df = dim, loc, shape, df 

4739 self.shape_info = _PSD(shape, allow_singular=allow_singular) 

4740 

4741 def logpdf(self, x): 

4742 x = self._dist._process_quantiles(x, self.dim) 

4743 U = self.shape_info.U 

4744 log_pdet = self.shape_info.log_pdet 

4745 return self._dist._logpdf(x, self.loc, U, log_pdet, self.df, self.dim, 

4746 self.shape_info.rank) 

4747 

4748 def cdf(self, x, *, maxpts=None, lower_limit=None, random_state=None): 

4749 x = self._dist._process_quantiles(x, self.dim) 

4750 return self._dist._cdf(x, self.loc, self.shape, self.df, self.dim, 

4751 maxpts, lower_limit, random_state) 

4752 

4753 def pdf(self, x): 

4754 return np.exp(self.logpdf(x)) 

4755 

4756 def rvs(self, size=1, random_state=None): 

4757 return self._dist.rvs(loc=self.loc, 

4758 shape=self.shape, 

4759 df=self.df, 

4760 size=size, 

4761 random_state=random_state) 

4762 

4763 def entropy(self): 

4764 return self._dist._entropy(self.dim, self.df, self.shape) 

4765 

4766 

4767multivariate_t = multivariate_t_gen() 

4768 

4769 

4770# Set frozen generator docstrings from corresponding docstrings in 

4771# multivariate_t_gen and fill in default strings in class docstrings 

4772for name in ['logpdf', 'pdf', 'rvs', 'cdf', 'entropy']: 

4773 method = multivariate_t_gen.__dict__[name] 

4774 method_frozen = multivariate_t_frozen.__dict__[name] 

4775 method_frozen.__doc__ = doccer.docformat(method.__doc__, 

4776 mvt_docdict_noparams) 

4777 method.__doc__ = doccer.docformat(method.__doc__, mvt_docdict_params) 

4778 

4779 

4780_mhg_doc_default_callparams = """\ 

4781m : array_like 

4782 The number of each type of object in the population. 

4783 That is, :math:`m[i]` is the number of objects of 

4784 type :math:`i`. 

4785n : array_like 

4786 The number of samples taken from the population. 

4787""" 

4788 

4789_mhg_doc_callparams_note = """\ 

4790`m` must be an array of positive integers. If the quantile 

4791:math:`i` contains values out of the range :math:`[0, m_i]` 

4792where :math:`m_i` is the number of objects of type :math:`i` 

4793in the population or if the parameters are inconsistent with one 

4794another (e.g. ``x.sum() != n``), methods return the appropriate 

4795value (e.g. ``0`` for ``pmf``). If `m` or `n` contain negative 

4796values, the result will contain ``nan`` there. 

4797""" 

4798 

4799_mhg_doc_frozen_callparams = "" 

4800 

4801_mhg_doc_frozen_callparams_note = """\ 

4802See class definition for a detailed description of parameters.""" 

4803 

4804mhg_docdict_params = { 

4805 '_doc_default_callparams': _mhg_doc_default_callparams, 

4806 '_doc_callparams_note': _mhg_doc_callparams_note, 

4807 '_doc_random_state': _doc_random_state 

4808} 

4809 

4810mhg_docdict_noparams = { 

4811 '_doc_default_callparams': _mhg_doc_frozen_callparams, 

4812 '_doc_callparams_note': _mhg_doc_frozen_callparams_note, 

4813 '_doc_random_state': _doc_random_state 

4814} 

4815 

4816 

4817class multivariate_hypergeom_gen(multi_rv_generic): 

4818 r"""A multivariate hypergeometric random variable. 

4819 

4820 Methods 

4821 ------- 

4822 pmf(x, m, n) 

4823 Probability mass function. 

4824 logpmf(x, m, n) 

4825 Log of the probability mass function. 

4826 rvs(m, n, size=1, random_state=None) 

4827 Draw random samples from a multivariate hypergeometric 

4828 distribution. 

4829 mean(m, n) 

4830 Mean of the multivariate hypergeometric distribution. 

4831 var(m, n) 

4832 Variance of the multivariate hypergeometric distribution. 

4833 cov(m, n) 

4834 Compute the covariance matrix of the multivariate 

4835 hypergeometric distribution. 

4836 

4837 Parameters 

4838 ---------- 

4839 %(_doc_default_callparams)s 

4840 %(_doc_random_state)s 

4841 

4842 Notes 

4843 ----- 

4844 %(_doc_callparams_note)s 

4845 

4846 The probability mass function for `multivariate_hypergeom` is 

4847 

4848 .. math:: 

4849 

4850 P(X_1 = x_1, X_2 = x_2, \ldots, X_k = x_k) = \frac{\binom{m_1}{x_1} 

4851 \binom{m_2}{x_2} \cdots \binom{m_k}{x_k}}{\binom{M}{n}}, \\ \quad 

4852 (x_1, x_2, \ldots, x_k) \in \mathbb{N}^k \text{ with } 

4853 \sum_{i=1}^k x_i = n 

4854 

4855 where :math:`m_i` are the number of objects of type :math:`i`, :math:`M` 

4856 is the total number of objects in the population (sum of all the 

4857 :math:`m_i`), and :math:`n` is the size of the sample to be taken 

4858 from the population. 

4859 

4860 .. versionadded:: 1.6.0 

4861 

4862 Examples 

4863 -------- 

4864 To evaluate the probability mass function of the multivariate 

4865 hypergeometric distribution, with a dichotomous population of size 

4866 :math:`10` and :math:`20`, at a sample of size :math:`12` with 

4867 :math:`8` objects of the first type and :math:`4` objects of the 

4868 second type, use: 

4869 

4870 >>> from scipy.stats import multivariate_hypergeom 

4871 >>> multivariate_hypergeom.pmf(x=[8, 4], m=[10, 20], n=12) 

4872 0.0025207176631464523 

4873 

4874 The `multivariate_hypergeom` distribution is identical to the 

4875 corresponding `hypergeom` distribution (tiny numerical differences 

4876 notwithstanding) when only two types (good and bad) of objects 

4877 are present in the population as in the example above. Consider 

4878 another example for a comparison with the hypergeometric distribution: 

4879 

4880 >>> from scipy.stats import hypergeom 

4881 >>> multivariate_hypergeom.pmf(x=[3, 1], m=[10, 5], n=4) 

4882 0.4395604395604395 

4883 >>> hypergeom.pmf(k=3, M=15, n=4, N=10) 

4884 0.43956043956044005 

4885 

4886 The functions ``pmf``, ``logpmf``, ``mean``, ``var``, ``cov``, and ``rvs`` 

4887 support broadcasting, under the convention that the vector parameters 

4888 (``x``, ``m``, and ``n``) are interpreted as if each row along the last 

4889 axis is a single object. For instance, we can combine the previous two 

4890 calls to `multivariate_hypergeom` as 

4891 

4892 >>> multivariate_hypergeom.pmf(x=[[8, 4], [3, 1]], m=[[10, 20], [10, 5]], 

4893 ... n=[12, 4]) 

4894 array([0.00252072, 0.43956044]) 

4895 

4896 This broadcasting also works for ``cov``, where the output objects are 

4897 square matrices of size ``m.shape[-1]``. For example: 

4898 

4899 >>> multivariate_hypergeom.cov(m=[[7, 9], [10, 15]], n=[8, 12]) 

4900 array([[[ 1.05, -1.05], 

4901 [-1.05, 1.05]], 

4902 [[ 1.56, -1.56], 

4903 [-1.56, 1.56]]]) 

4904 

4905 That is, ``result[0]`` is equal to 

4906 ``multivariate_hypergeom.cov(m=[7, 9], n=8)`` and ``result[1]`` is equal 

4907 to ``multivariate_hypergeom.cov(m=[10, 15], n=12)``. 

4908 

4909 Alternatively, the object may be called (as a function) to fix the `m` 

4910 and `n` parameters, returning a "frozen" multivariate hypergeometric 

4911 random variable. 

4912 

4913 >>> rv = multivariate_hypergeom(m=[10, 20], n=12) 

4914 >>> rv.pmf(x=[8, 4]) 

4915 0.0025207176631464523 

4916 

4917 See Also 

4918 -------- 

4919 scipy.stats.hypergeom : The hypergeometric distribution. 

4920 scipy.stats.multinomial : The multinomial distribution. 

4921 

4922 References 

4923 ---------- 

4924 .. [1] The Multivariate Hypergeometric Distribution, 

4925 http://www.randomservices.org/random/urn/MultiHypergeometric.html 

4926 .. [2] Thomas J. Sargent and John Stachurski, 2020, 

4927 Multivariate Hypergeometric Distribution 

4928 https://python.quantecon.org/_downloads/pdf/multi_hyper.pdf 

4929 """ 

4930 def __init__(self, seed=None): 

4931 super().__init__(seed) 

4932 self.__doc__ = doccer.docformat(self.__doc__, mhg_docdict_params) 

4933 

4934 def __call__(self, m, n, seed=None): 

4935 """Create a frozen multivariate_hypergeom distribution. 

4936 

4937 See `multivariate_hypergeom_frozen` for more information. 

4938 """ 

4939 return multivariate_hypergeom_frozen(m, n, seed=seed) 

4940 

4941 def _process_parameters(self, m, n): 

4942 m = np.asarray(m) 

4943 n = np.asarray(n) 

4944 if m.size == 0: 

4945 m = m.astype(int) 

4946 if n.size == 0: 

4947 n = n.astype(int) 

4948 if not np.issubdtype(m.dtype, np.integer): 

4949 raise TypeError("'m' must an array of integers.") 

4950 if not np.issubdtype(n.dtype, np.integer): 

4951 raise TypeError("'n' must an array of integers.") 

4952 if m.ndim == 0: 

4953 raise ValueError("'m' must be an array with" 

4954 " at least one dimension.") 

4955 

4956 # check for empty arrays 

4957 if m.size != 0: 

4958 n = n[..., np.newaxis] 

4959 

4960 m, n = np.broadcast_arrays(m, n) 

4961 

4962 # check for empty arrays 

4963 if m.size != 0: 

4964 n = n[..., 0] 

4965 

4966 mcond = m < 0 

4967 

4968 M = m.sum(axis=-1) 

4969 

4970 ncond = (n < 0) | (n > M) 

4971 return M, m, n, mcond, ncond, np.any(mcond, axis=-1) | ncond 

4972 

4973 def _process_quantiles(self, x, M, m, n): 

4974 x = np.asarray(x) 

4975 if not np.issubdtype(x.dtype, np.integer): 

4976 raise TypeError("'x' must an array of integers.") 

4977 if x.ndim == 0: 

4978 raise ValueError("'x' must be an array with" 

4979 " at least one dimension.") 

4980 if not x.shape[-1] == m.shape[-1]: 

4981 raise ValueError(f"Size of each quantile must be size of 'm': " 

4982 f"received {x.shape[-1]}, " 

4983 f"but expected {m.shape[-1]}.") 

4984 

4985 # check for empty arrays 

4986 if m.size != 0: 

4987 n = n[..., np.newaxis] 

4988 M = M[..., np.newaxis] 

4989 

4990 x, m, n, M = np.broadcast_arrays(x, m, n, M) 

4991 

4992 # check for empty arrays 

4993 if m.size != 0: 

4994 n, M = n[..., 0], M[..., 0] 

4995 

4996 xcond = (x < 0) | (x > m) 

4997 return (x, M, m, n, xcond, 

4998 np.any(xcond, axis=-1) | (x.sum(axis=-1) != n)) 

4999 

5000 def _checkresult(self, result, cond, bad_value): 

5001 result = np.asarray(result) 

5002 if cond.ndim != 0: 

5003 result[cond] = bad_value 

5004 elif cond: 

5005 return bad_value 

5006 if result.ndim == 0: 

5007 return result[()] 

5008 return result 

5009 

5010 def _logpmf(self, x, M, m, n, mxcond, ncond): 

5011 # This equation of the pmf comes from the relation, 

5012 # n combine r = beta(n+1, 1) / beta(r+1, n-r+1) 

5013 num = np.zeros_like(m, dtype=np.float_) 

5014 den = np.zeros_like(n, dtype=np.float_) 

5015 m, x = m[~mxcond], x[~mxcond] 

5016 M, n = M[~ncond], n[~ncond] 

5017 num[~mxcond] = (betaln(m+1, 1) - betaln(x+1, m-x+1)) 

5018 den[~ncond] = (betaln(M+1, 1) - betaln(n+1, M-n+1)) 

5019 num[mxcond] = np.nan 

5020 den[ncond] = np.nan 

5021 num = num.sum(axis=-1) 

5022 return num - den 

5023 

5024 def logpmf(self, x, m, n): 

5025 """Log of the multivariate hypergeometric probability mass function. 

5026 

5027 Parameters 

5028 ---------- 

5029 x : array_like 

5030 Quantiles, with the last axis of `x` denoting the components. 

5031 %(_doc_default_callparams)s 

5032 

5033 Returns 

5034 ------- 

5035 logpmf : ndarray or scalar 

5036 Log of the probability mass function evaluated at `x` 

5037 

5038 Notes 

5039 ----- 

5040 %(_doc_callparams_note)s 

5041 """ 

5042 M, m, n, mcond, ncond, mncond = self._process_parameters(m, n) 

5043 (x, M, m, n, xcond, 

5044 xcond_reduced) = self._process_quantiles(x, M, m, n) 

5045 mxcond = mcond | xcond 

5046 ncond = ncond | np.zeros(n.shape, dtype=np.bool_) 

5047 

5048 result = self._logpmf(x, M, m, n, mxcond, ncond) 

5049 

5050 # replace values for which x was out of the domain; broadcast 

5051 # xcond to the right shape 

5052 xcond_ = xcond_reduced | np.zeros(mncond.shape, dtype=np.bool_) 

5053 result = self._checkresult(result, xcond_, -np.inf) 

5054 

5055 # replace values bad for n or m; broadcast 

5056 # mncond to the right shape 

5057 mncond_ = mncond | np.zeros(xcond_reduced.shape, dtype=np.bool_) 

5058 return self._checkresult(result, mncond_, np.nan) 

5059 

5060 def pmf(self, x, m, n): 

5061 """Multivariate hypergeometric probability mass function. 

5062 

5063 Parameters 

5064 ---------- 

5065 x : array_like 

5066 Quantiles, with the last axis of `x` denoting the components. 

5067 %(_doc_default_callparams)s 

5068 

5069 Returns 

5070 ------- 

5071 pmf : ndarray or scalar 

5072 Probability density function evaluated at `x` 

5073 

5074 Notes 

5075 ----- 

5076 %(_doc_callparams_note)s 

5077 """ 

5078 out = np.exp(self.logpmf(x, m, n)) 

5079 return out 

5080 

5081 def mean(self, m, n): 

5082 """Mean of the multivariate hypergeometric distribution. 

5083 

5084 Parameters 

5085 ---------- 

5086 %(_doc_default_callparams)s 

5087 

5088 Returns 

5089 ------- 

5090 mean : array_like or scalar 

5091 The mean of the distribution 

5092 """ 

5093 M, m, n, _, _, mncond = self._process_parameters(m, n) 

5094 # check for empty arrays 

5095 if m.size != 0: 

5096 M, n = M[..., np.newaxis], n[..., np.newaxis] 

5097 cond = (M == 0) 

5098 M = np.ma.masked_array(M, mask=cond) 

5099 mu = n*(m/M) 

5100 if m.size != 0: 

5101 mncond = (mncond[..., np.newaxis] | 

5102 np.zeros(mu.shape, dtype=np.bool_)) 

5103 return self._checkresult(mu, mncond, np.nan) 

5104 

5105 def var(self, m, n): 

5106 """Variance of the multivariate hypergeometric distribution. 

5107 

5108 Parameters 

5109 ---------- 

5110 %(_doc_default_callparams)s 

5111 

5112 Returns 

5113 ------- 

5114 array_like 

5115 The variances of the components of the distribution. This is 

5116 the diagonal of the covariance matrix of the distribution 

5117 """ 

5118 M, m, n, _, _, mncond = self._process_parameters(m, n) 

5119 # check for empty arrays 

5120 if m.size != 0: 

5121 M, n = M[..., np.newaxis], n[..., np.newaxis] 

5122 cond = (M == 0) & (M-1 == 0) 

5123 M = np.ma.masked_array(M, mask=cond) 

5124 output = n * m/M * (M-m)/M * (M-n)/(M-1) 

5125 if m.size != 0: 

5126 mncond = (mncond[..., np.newaxis] | 

5127 np.zeros(output.shape, dtype=np.bool_)) 

5128 return self._checkresult(output, mncond, np.nan) 

5129 

5130 def cov(self, m, n): 

5131 """Covariance matrix of the multivariate hypergeometric distribution. 

5132 

5133 Parameters 

5134 ---------- 

5135 %(_doc_default_callparams)s 

5136 

5137 Returns 

5138 ------- 

5139 cov : array_like 

5140 The covariance matrix of the distribution 

5141 """ 

5142 # see [1]_ for the formula and [2]_ for implementation 

5143 # cov( x_i,x_j ) = -n * (M-n)/(M-1) * (K_i*K_j) / (M**2) 

5144 M, m, n, _, _, mncond = self._process_parameters(m, n) 

5145 # check for empty arrays 

5146 if m.size != 0: 

5147 M = M[..., np.newaxis, np.newaxis] 

5148 n = n[..., np.newaxis, np.newaxis] 

5149 cond = (M == 0) & (M-1 == 0) 

5150 M = np.ma.masked_array(M, mask=cond) 

5151 output = (-n * (M-n)/(M-1) * 

5152 np.einsum("...i,...j->...ij", m, m) / (M**2)) 

5153 # check for empty arrays 

5154 if m.size != 0: 

5155 M, n = M[..., 0, 0], n[..., 0, 0] 

5156 cond = cond[..., 0, 0] 

5157 dim = m.shape[-1] 

5158 # diagonal entries need to be computed differently 

5159 for i in range(dim): 

5160 output[..., i, i] = (n * (M-n) * m[..., i]*(M-m[..., i])) 

5161 output[..., i, i] = output[..., i, i] / (M-1) 

5162 output[..., i, i] = output[..., i, i] / (M**2) 

5163 if m.size != 0: 

5164 mncond = (mncond[..., np.newaxis, np.newaxis] | 

5165 np.zeros(output.shape, dtype=np.bool_)) 

5166 return self._checkresult(output, mncond, np.nan) 

5167 

5168 def rvs(self, m, n, size=None, random_state=None): 

5169 """Draw random samples from a multivariate hypergeometric distribution. 

5170 

5171 Parameters 

5172 ---------- 

5173 %(_doc_default_callparams)s 

5174 size : integer or iterable of integers, optional 

5175 Number of samples to draw. Default is ``None``, in which case a 

5176 single variate is returned as an array with shape ``m.shape``. 

5177 %(_doc_random_state)s 

5178 

5179 Returns 

5180 ------- 

5181 rvs : array_like 

5182 Random variates of shape ``size`` or ``m.shape`` 

5183 (if ``size=None``). 

5184 

5185 Notes 

5186 ----- 

5187 %(_doc_callparams_note)s 

5188 

5189 Also note that NumPy's `multivariate_hypergeometric` sampler is not 

5190 used as it doesn't support broadcasting. 

5191 """ 

5192 M, m, n, _, _, _ = self._process_parameters(m, n) 

5193 

5194 random_state = self._get_random_state(random_state) 

5195 

5196 if size is not None and isinstance(size, int): 

5197 size = (size, ) 

5198 

5199 if size is None: 

5200 rvs = np.empty(m.shape, dtype=m.dtype) 

5201 else: 

5202 rvs = np.empty(size + (m.shape[-1], ), dtype=m.dtype) 

5203 rem = M 

5204 

5205 # This sampler has been taken from numpy gh-13794 

5206 # https://github.com/numpy/numpy/pull/13794 

5207 for c in range(m.shape[-1] - 1): 

5208 rem = rem - m[..., c] 

5209 n0mask = n == 0 

5210 rvs[..., c] = (~n0mask * 

5211 random_state.hypergeometric(m[..., c], 

5212 rem + n0mask, 

5213 n + n0mask, 

5214 size=size)) 

5215 n = n - rvs[..., c] 

5216 rvs[..., m.shape[-1] - 1] = n 

5217 

5218 return rvs 

5219 

5220 

5221multivariate_hypergeom = multivariate_hypergeom_gen() 

5222 

5223 

5224class multivariate_hypergeom_frozen(multi_rv_frozen): 

5225 def __init__(self, m, n, seed=None): 

5226 self._dist = multivariate_hypergeom_gen(seed) 

5227 (self.M, self.m, self.n, 

5228 self.mcond, self.ncond, 

5229 self.mncond) = self._dist._process_parameters(m, n) 

5230 

5231 # monkey patch self._dist 

5232 def _process_parameters(m, n): 

5233 return (self.M, self.m, self.n, 

5234 self.mcond, self.ncond, 

5235 self.mncond) 

5236 self._dist._process_parameters = _process_parameters 

5237 

5238 def logpmf(self, x): 

5239 return self._dist.logpmf(x, self.m, self.n) 

5240 

5241 def pmf(self, x): 

5242 return self._dist.pmf(x, self.m, self.n) 

5243 

5244 def mean(self): 

5245 return self._dist.mean(self.m, self.n) 

5246 

5247 def var(self): 

5248 return self._dist.var(self.m, self.n) 

5249 

5250 def cov(self): 

5251 return self._dist.cov(self.m, self.n) 

5252 

5253 def rvs(self, size=1, random_state=None): 

5254 return self._dist.rvs(self.m, self.n, 

5255 size=size, 

5256 random_state=random_state) 

5257 

5258 

5259# Set frozen generator docstrings from corresponding docstrings in 

5260# multivariate_hypergeom and fill in default strings in class docstrings 

5261for name in ['logpmf', 'pmf', 'mean', 'var', 'cov', 'rvs']: 

5262 method = multivariate_hypergeom_gen.__dict__[name] 

5263 method_frozen = multivariate_hypergeom_frozen.__dict__[name] 

5264 method_frozen.__doc__ = doccer.docformat( 

5265 method.__doc__, mhg_docdict_noparams) 

5266 method.__doc__ = doccer.docformat(method.__doc__, 

5267 mhg_docdict_params) 

5268 

5269 

5270class random_table_gen(multi_rv_generic): 

5271 r"""Contingency tables from independent samples with fixed marginal sums. 

5272 

5273 This is the distribution of random tables with given row and column vector 

5274 sums. This distribution represents the set of random tables under the null 

5275 hypothesis that rows and columns are independent. It is used in hypothesis 

5276 tests of independence. 

5277 

5278 Because of assumed independence, the expected frequency of each table 

5279 element can be computed from the row and column sums, so that the 

5280 distribution is completely determined by these two vectors. 

5281 

5282 Methods 

5283 ------- 

5284 logpmf(x) 

5285 Log-probability of table `x` to occur in the distribution. 

5286 pmf(x) 

5287 Probability of table `x` to occur in the distribution. 

5288 mean(row, col) 

5289 Mean table. 

5290 rvs(row, col, size=None, method=None, random_state=None) 

5291 Draw random tables with given row and column vector sums. 

5292 

5293 Parameters 

5294 ---------- 

5295 %(_doc_row_col)s 

5296 %(_doc_random_state)s 

5297 

5298 Notes 

5299 ----- 

5300 %(_doc_row_col_note)s 

5301 

5302 Random elements from the distribution are generated either with Boyett's 

5303 [1]_ or Patefield's algorithm [2]_. Boyett's algorithm has 

5304 O(N) time and space complexity, where N is the total sum of entries in the 

5305 table. Patefield's algorithm has O(K x log(N)) time complexity, where K is 

5306 the number of cells in the table and requires only a small constant work 

5307 space. By default, the `rvs` method selects the fastest algorithm based on 

5308 the input, but you can specify the algorithm with the keyword `method`. 

5309 Allowed values are "boyett" and "patefield". 

5310 

5311 .. versionadded:: 1.10.0 

5312 

5313 Examples 

5314 -------- 

5315 >>> from scipy.stats import random_table 

5316 

5317 >>> row = [1, 5] 

5318 >>> col = [2, 3, 1] 

5319 >>> random_table.mean(row, col) 

5320 array([[0.33333333, 0.5 , 0.16666667], 

5321 [1.66666667, 2.5 , 0.83333333]]) 

5322 

5323 Alternatively, the object may be called (as a function) to fix the row 

5324 and column vector sums, returning a "frozen" distribution. 

5325 

5326 >>> dist = random_table(row, col) 

5327 >>> dist.rvs(random_state=123) 

5328 array([[1., 0., 0.], 

5329 [1., 3., 1.]]) 

5330 

5331 References 

5332 ---------- 

5333 .. [1] J. Boyett, AS 144 Appl. Statist. 28 (1979) 329-332 

5334 .. [2] W.M. Patefield, AS 159 Appl. Statist. 30 (1981) 91-97 

5335 """ 

5336 

5337 def __init__(self, seed=None): 

5338 super().__init__(seed) 

5339 

5340 def __call__(self, row, col, *, seed=None): 

5341 """Create a frozen distribution of tables with given marginals. 

5342 

5343 See `random_table_frozen` for more information. 

5344 """ 

5345 return random_table_frozen(row, col, seed=seed) 

5346 

5347 def logpmf(self, x, row, col): 

5348 """Log-probability of table to occur in the distribution. 

5349 

5350 Parameters 

5351 ---------- 

5352 %(_doc_x)s 

5353 %(_doc_row_col)s 

5354 

5355 Returns 

5356 ------- 

5357 logpmf : ndarray or scalar 

5358 Log of the probability mass function evaluated at `x`. 

5359 

5360 Notes 

5361 ----- 

5362 %(_doc_row_col_note)s 

5363 

5364 If row and column marginals of `x` do not match `row` and `col`, 

5365 negative infinity is returned. 

5366 

5367 Examples 

5368 -------- 

5369 >>> from scipy.stats import random_table 

5370 >>> import numpy as np 

5371 

5372 >>> x = [[1, 5, 1], [2, 3, 1]] 

5373 >>> row = np.sum(x, axis=1) 

5374 >>> col = np.sum(x, axis=0) 

5375 >>> random_table.logpmf(x, row, col) 

5376 -1.6306401200847027 

5377 

5378 Alternatively, the object may be called (as a function) to fix the row 

5379 and column vector sums, returning a "frozen" distribution. 

5380 

5381 >>> d = random_table(row, col) 

5382 >>> d.logpmf(x) 

5383 -1.6306401200847027 

5384 """ 

5385 r, c, n = self._process_parameters(row, col) 

5386 x = np.asarray(x) 

5387 

5388 if x.ndim < 2: 

5389 raise ValueError("`x` must be at least two-dimensional") 

5390 

5391 dtype_is_int = np.issubdtype(x.dtype, np.integer) 

5392 with np.errstate(invalid='ignore'): 

5393 if not dtype_is_int and not np.all(x.astype(int) == x): 

5394 raise ValueError("`x` must contain only integral values") 

5395 

5396 # x does not contain NaN if we arrive here 

5397 if np.any(x < 0): 

5398 raise ValueError("`x` must contain only non-negative values") 

5399 

5400 r2 = np.sum(x, axis=-1) 

5401 c2 = np.sum(x, axis=-2) 

5402 

5403 if r2.shape[-1] != len(r): 

5404 raise ValueError("shape of `x` must agree with `row`") 

5405 

5406 if c2.shape[-1] != len(c): 

5407 raise ValueError("shape of `x` must agree with `col`") 

5408 

5409 res = np.empty(x.shape[:-2]) 

5410 

5411 mask = np.all(r2 == r, axis=-1) & np.all(c2 == c, axis=-1) 

5412 

5413 def lnfac(x): 

5414 return gammaln(x + 1) 

5415 

5416 res[mask] = (np.sum(lnfac(r), axis=-1) + np.sum(lnfac(c), axis=-1) 

5417 - lnfac(n) - np.sum(lnfac(x[mask]), axis=(-1, -2))) 

5418 res[~mask] = -np.inf 

5419 

5420 return res[()] 

5421 

5422 def pmf(self, x, row, col): 

5423 """Probability of table to occur in the distribution. 

5424 

5425 Parameters 

5426 ---------- 

5427 %(_doc_x)s 

5428 %(_doc_row_col)s 

5429 

5430 Returns 

5431 ------- 

5432 pmf : ndarray or scalar 

5433 Probability mass function evaluated at `x`. 

5434 

5435 Notes 

5436 ----- 

5437 %(_doc_row_col_note)s 

5438 

5439 If row and column marginals of `x` do not match `row` and `col`, 

5440 zero is returned. 

5441 

5442 Examples 

5443 -------- 

5444 >>> from scipy.stats import random_table 

5445 >>> import numpy as np 

5446 

5447 >>> x = [[1, 5, 1], [2, 3, 1]] 

5448 >>> row = np.sum(x, axis=1) 

5449 >>> col = np.sum(x, axis=0) 

5450 >>> random_table.pmf(x, row, col) 

5451 0.19580419580419592 

5452 

5453 Alternatively, the object may be called (as a function) to fix the row 

5454 and column vector sums, returning a "frozen" distribution. 

5455 

5456 >>> d = random_table(row, col) 

5457 >>> d.pmf(x) 

5458 0.19580419580419592 

5459 """ 

5460 return np.exp(self.logpmf(x, row, col)) 

5461 

5462 def mean(self, row, col): 

5463 """Mean of distribution of conditional tables. 

5464 %(_doc_mean_params)s 

5465 

5466 Returns 

5467 ------- 

5468 mean: ndarray 

5469 Mean of the distribution. 

5470 

5471 Notes 

5472 ----- 

5473 %(_doc_row_col_note)s 

5474 

5475 Examples 

5476 -------- 

5477 >>> from scipy.stats import random_table 

5478 

5479 >>> row = [1, 5] 

5480 >>> col = [2, 3, 1] 

5481 >>> random_table.mean(row, col) 

5482 array([[0.33333333, 0.5 , 0.16666667], 

5483 [1.66666667, 2.5 , 0.83333333]]) 

5484 

5485 Alternatively, the object may be called (as a function) to fix the row 

5486 and column vector sums, returning a "frozen" distribution. 

5487 

5488 >>> d = random_table(row, col) 

5489 >>> d.mean() 

5490 array([[0.33333333, 0.5 , 0.16666667], 

5491 [1.66666667, 2.5 , 0.83333333]]) 

5492 """ 

5493 r, c, n = self._process_parameters(row, col) 

5494 return np.outer(r, c) / n 

5495 

5496 def rvs(self, row, col, *, size=None, method=None, random_state=None): 

5497 """Draw random tables with fixed column and row marginals. 

5498 

5499 Parameters 

5500 ---------- 

5501 %(_doc_row_col)s 

5502 size : integer, optional 

5503 Number of samples to draw (default 1). 

5504 method : str, optional 

5505 Which method to use, "boyett" or "patefield". If None (default), 

5506 selects the fastest method for this input. 

5507 %(_doc_random_state)s 

5508 

5509 Returns 

5510 ------- 

5511 rvs : ndarray 

5512 Random 2D tables of shape (`size`, `len(row)`, `len(col)`). 

5513 

5514 Notes 

5515 ----- 

5516 %(_doc_row_col_note)s 

5517 

5518 Examples 

5519 -------- 

5520 >>> from scipy.stats import random_table 

5521 

5522 >>> row = [1, 5] 

5523 >>> col = [2, 3, 1] 

5524 >>> random_table.rvs(row, col, random_state=123) 

5525 array([[1., 0., 0.], 

5526 [1., 3., 1.]]) 

5527 

5528 Alternatively, the object may be called (as a function) to fix the row 

5529 and column vector sums, returning a "frozen" distribution. 

5530 

5531 >>> d = random_table(row, col) 

5532 >>> d.rvs(random_state=123) 

5533 array([[1., 0., 0.], 

5534 [1., 3., 1.]]) 

5535 """ 

5536 r, c, n = self._process_parameters(row, col) 

5537 size, shape = self._process_size_shape(size, r, c) 

5538 

5539 random_state = self._get_random_state(random_state) 

5540 meth = self._process_rvs_method(method, r, c, n) 

5541 

5542 return meth(r, c, n, size, random_state).reshape(shape) 

5543 

5544 @staticmethod 

5545 def _process_parameters(row, col): 

5546 """ 

5547 Check that row and column vectors are one-dimensional, that they do 

5548 not contain negative or non-integer entries, and that the sums over 

5549 both vectors are equal. 

5550 """ 

5551 r = np.array(row, dtype=np.int64, copy=True) 

5552 c = np.array(col, dtype=np.int64, copy=True) 

5553 

5554 if np.ndim(r) != 1: 

5555 raise ValueError("`row` must be one-dimensional") 

5556 if np.ndim(c) != 1: 

5557 raise ValueError("`col` must be one-dimensional") 

5558 

5559 if np.any(r < 0): 

5560 raise ValueError("each element of `row` must be non-negative") 

5561 if np.any(c < 0): 

5562 raise ValueError("each element of `col` must be non-negative") 

5563 

5564 n = np.sum(r) 

5565 if n != np.sum(c): 

5566 raise ValueError("sums over `row` and `col` must be equal") 

5567 

5568 if not np.all(r == np.asarray(row)): 

5569 raise ValueError("each element of `row` must be an integer") 

5570 if not np.all(c == np.asarray(col)): 

5571 raise ValueError("each element of `col` must be an integer") 

5572 

5573 return r, c, n 

5574 

5575 @staticmethod 

5576 def _process_size_shape(size, r, c): 

5577 """ 

5578 Compute the number of samples to be drawn and the shape of the output 

5579 """ 

5580 shape = (len(r), len(c)) 

5581 

5582 if size is None: 

5583 return 1, shape 

5584 

5585 size = np.atleast_1d(size) 

5586 if not np.issubdtype(size.dtype, np.integer) or np.any(size < 0): 

5587 raise ValueError("`size` must be a non-negative integer or `None`") 

5588 

5589 return np.prod(size), tuple(size) + shape 

5590 

5591 @classmethod 

5592 def _process_rvs_method(cls, method, r, c, n): 

5593 known_methods = { 

5594 None: cls._rvs_select(r, c, n), 

5595 "boyett": cls._rvs_boyett, 

5596 "patefield": cls._rvs_patefield, 

5597 } 

5598 try: 

5599 return known_methods[method] 

5600 except KeyError: 

5601 raise ValueError(f"'{method}' not recognized, " 

5602 f"must be one of {set(known_methods)}") 

5603 

5604 @classmethod 

5605 def _rvs_select(cls, r, c, n): 

5606 fac = 1.0 # benchmarks show that this value is about 1 

5607 k = len(r) * len(c) # number of cells 

5608 # n + 1 guards against failure if n == 0 

5609 if n > fac * np.log(n + 1) * k: 

5610 return cls._rvs_patefield 

5611 return cls._rvs_boyett 

5612 

5613 @staticmethod 

5614 def _rvs_boyett(row, col, ntot, size, random_state): 

5615 return _rcont.rvs_rcont1(row, col, ntot, size, random_state) 

5616 

5617 @staticmethod 

5618 def _rvs_patefield(row, col, ntot, size, random_state): 

5619 return _rcont.rvs_rcont2(row, col, ntot, size, random_state) 

5620 

5621 

5622random_table = random_table_gen() 

5623 

5624 

5625class random_table_frozen(multi_rv_frozen): 

5626 def __init__(self, row, col, *, seed=None): 

5627 self._dist = random_table_gen(seed) 

5628 self._params = self._dist._process_parameters(row, col) 

5629 

5630 # monkey patch self._dist 

5631 def _process_parameters(r, c): 

5632 return self._params 

5633 self._dist._process_parameters = _process_parameters 

5634 

5635 def logpmf(self, x): 

5636 return self._dist.logpmf(x, None, None) 

5637 

5638 def pmf(self, x): 

5639 return self._dist.pmf(x, None, None) 

5640 

5641 def mean(self): 

5642 return self._dist.mean(None, None) 

5643 

5644 def rvs(self, size=None, method=None, random_state=None): 

5645 # optimisations are possible here 

5646 return self._dist.rvs(None, None, size=size, method=method, 

5647 random_state=random_state) 

5648 

5649 

5650_ctab_doc_row_col = """\ 

5651row : array_like 

5652 Sum of table entries in each row. 

5653col : array_like 

5654 Sum of table entries in each column.""" 

5655 

5656_ctab_doc_x = """\ 

5657x : array-like 

5658 Two-dimensional table of non-negative integers, or a 

5659 multi-dimensional array with the last two dimensions 

5660 corresponding with the tables.""" 

5661 

5662_ctab_doc_row_col_note = """\ 

5663The row and column vectors must be one-dimensional, not empty, 

5664and each sum up to the same value. They cannot contain negative 

5665or noninteger entries.""" 

5666 

5667_ctab_doc_mean_params = f""" 

5668Parameters 

5669---------- 

5670{_ctab_doc_row_col}""" 

5671 

5672_ctab_doc_row_col_note_frozen = """\ 

5673See class definition for a detailed description of parameters.""" 

5674 

5675_ctab_docdict = { 

5676 "_doc_random_state": _doc_random_state, 

5677 "_doc_row_col": _ctab_doc_row_col, 

5678 "_doc_x": _ctab_doc_x, 

5679 "_doc_mean_params": _ctab_doc_mean_params, 

5680 "_doc_row_col_note": _ctab_doc_row_col_note, 

5681} 

5682 

5683_ctab_docdict_frozen = _ctab_docdict.copy() 

5684_ctab_docdict_frozen.update({ 

5685 "_doc_row_col": "", 

5686 "_doc_mean_params": "", 

5687 "_doc_row_col_note": _ctab_doc_row_col_note_frozen, 

5688}) 

5689 

5690 

5691def _docfill(obj, docdict, template=None): 

5692 obj.__doc__ = doccer.docformat(template or obj.__doc__, docdict) 

5693 

5694 

5695# Set frozen generator docstrings from corresponding docstrings in 

5696# random_table and fill in default strings in class docstrings 

5697_docfill(random_table_gen, _ctab_docdict) 

5698for name in ['logpmf', 'pmf', 'mean', 'rvs']: 

5699 method = random_table_gen.__dict__[name] 

5700 method_frozen = random_table_frozen.__dict__[name] 

5701 _docfill(method_frozen, _ctab_docdict_frozen, method.__doc__) 

5702 _docfill(method, _ctab_docdict) 

5703 

5704 

5705class uniform_direction_gen(multi_rv_generic): 

5706 r"""A vector-valued uniform direction. 

5707 

5708 Return a random direction (unit vector). The `dim` keyword specifies 

5709 the dimensionality of the space. 

5710 

5711 Methods 

5712 ------- 

5713 rvs(dim=None, size=1, random_state=None) 

5714 Draw random directions. 

5715 

5716 Parameters 

5717 ---------- 

5718 dim : scalar 

5719 Dimension of directions. 

5720 seed : {None, int, `numpy.random.Generator`, 

5721 `numpy.random.RandomState`}, optional 

5722 

5723 Used for drawing random variates. 

5724 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

5725 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

5726 with seed. 

5727 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

5728 then that object is used. 

5729 Default is `None`. 

5730 

5731 Notes 

5732 ----- 

5733 This distribution generates unit vectors uniformly distributed on 

5734 the surface of a hypersphere. These can be interpreted as random 

5735 directions. 

5736 For example, if `dim` is 3, 3D vectors from the surface of :math:`S^2` 

5737 will be sampled. 

5738 

5739 References 

5740 ---------- 

5741 .. [1] Marsaglia, G. (1972). "Choosing a Point from the Surface of a 

5742 Sphere". Annals of Mathematical Statistics. 43 (2): 645-646. 

5743 

5744 Examples 

5745 -------- 

5746 >>> import numpy as np 

5747 >>> from scipy.stats import uniform_direction 

5748 >>> x = uniform_direction.rvs(3) 

5749 >>> np.linalg.norm(x) 

5750 1. 

5751 

5752 This generates one random direction, a vector on the surface of 

5753 :math:`S^2`. 

5754 

5755 Alternatively, the object may be called (as a function) to return a frozen 

5756 distribution with fixed `dim` parameter. Here, 

5757 we create a `uniform_direction` with ``dim=3`` and draw 5 observations. 

5758 The samples are then arranged in an array of shape 5x3. 

5759 

5760 >>> rng = np.random.default_rng() 

5761 >>> uniform_sphere_dist = uniform_direction(3) 

5762 >>> unit_vectors = uniform_sphere_dist.rvs(5, random_state=rng) 

5763 >>> unit_vectors 

5764 array([[ 0.56688642, -0.1332634 , -0.81294566], 

5765 [-0.427126 , -0.74779278, 0.50830044], 

5766 [ 0.3793989 , 0.92346629, 0.05715323], 

5767 [ 0.36428383, -0.92449076, -0.11231259], 

5768 [-0.27733285, 0.94410968, -0.17816678]]) 

5769 """ 

5770 

5771 def __init__(self, seed=None): 

5772 super().__init__(seed) 

5773 self.__doc__ = doccer.docformat(self.__doc__) 

5774 

5775 def __call__(self, dim=None, seed=None): 

5776 """Create a frozen n-dimensional uniform direction distribution. 

5777 

5778 See `uniform_direction` for more information. 

5779 """ 

5780 return uniform_direction_frozen(dim, seed=seed) 

5781 

5782 def _process_parameters(self, dim): 

5783 """Dimension N must be specified; it cannot be inferred.""" 

5784 if dim is None or not np.isscalar(dim) or dim < 1 or dim != int(dim): 

5785 raise ValueError("Dimension of vector must be specified, " 

5786 "and must be an integer greater than 0.") 

5787 

5788 return int(dim) 

5789 

5790 def rvs(self, dim, size=None, random_state=None): 

5791 """Draw random samples from S(N-1). 

5792 

5793 Parameters 

5794 ---------- 

5795 dim : integer 

5796 Dimension of space (N). 

5797 size : int or tuple of ints, optional 

5798 Given a shape of, for example, (m,n,k), m*n*k samples are 

5799 generated, and packed in an m-by-n-by-k arrangement. 

5800 Because each sample is N-dimensional, the output shape 

5801 is (m,n,k,N). If no shape is specified, a single (N-D) 

5802 sample is returned. 

5803 random_state : {None, int, `numpy.random.Generator`, 

5804 `numpy.random.RandomState`}, optional 

5805 

5806 Pseudorandom number generator state used to generate resamples. 

5807 

5808 If `random_state` is ``None`` (or `np.random`), the 

5809 `numpy.random.RandomState` singleton is used. 

5810 If `random_state` is an int, a new ``RandomState`` instance is 

5811 used, seeded with `random_state`. 

5812 If `random_state` is already a ``Generator`` or ``RandomState`` 

5813 instance then that instance is used. 

5814 

5815 Returns 

5816 ------- 

5817 rvs : ndarray 

5818 Random direction vectors 

5819 

5820 """ 

5821 random_state = self._get_random_state(random_state) 

5822 if size is None: 

5823 size = np.array([], dtype=int) 

5824 size = np.atleast_1d(size) 

5825 

5826 dim = self._process_parameters(dim) 

5827 

5828 samples = _sample_uniform_direction(dim, size, random_state) 

5829 return samples 

5830 

5831 

5832uniform_direction = uniform_direction_gen() 

5833 

5834 

5835class uniform_direction_frozen(multi_rv_frozen): 

5836 def __init__(self, dim=None, seed=None): 

5837 """Create a frozen n-dimensional uniform direction distribution. 

5838 

5839 Parameters 

5840 ---------- 

5841 dim : int 

5842 Dimension of matrices 

5843 seed : {None, int, `numpy.random.Generator`, 

5844 `numpy.random.RandomState`}, optional 

5845 

5846 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

5847 singleton is used. 

5848 If `seed` is an int, a new ``RandomState`` instance is used, 

5849 seeded with `seed`. 

5850 If `seed` is already a ``Generator`` or ``RandomState`` instance 

5851 then that instance is used. 

5852 

5853 Examples 

5854 -------- 

5855 >>> from scipy.stats import uniform_direction 

5856 >>> x = uniform_direction(3) 

5857 >>> x.rvs() 

5858 

5859 """ 

5860 self._dist = uniform_direction_gen(seed) 

5861 self.dim = self._dist._process_parameters(dim) 

5862 

5863 def rvs(self, size=None, random_state=None): 

5864 return self._dist.rvs(self.dim, size, random_state) 

5865 

5866 

5867def _sample_uniform_direction(dim, size, random_state): 

5868 """ 

5869 Private method to generate uniform directions 

5870 Reference: Marsaglia, G. (1972). "Choosing a Point from the Surface of a 

5871 Sphere". Annals of Mathematical Statistics. 43 (2): 645-646. 

5872 """ 

5873 samples_shape = np.append(size, dim) 

5874 samples = random_state.standard_normal(samples_shape) 

5875 samples /= np.linalg.norm(samples, axis=-1, keepdims=True) 

5876 return samples 

5877 

5878 

5879_dirichlet_mn_doc_default_callparams = """\ 

5880alpha : array_like 

5881 The concentration parameters. The number of entries along the last axis 

5882 determines the dimensionality of the distribution. Each entry must be 

5883 strictly positive. 

5884n : int or array_like 

5885 The number of trials. Each element must be a strictly positive integer. 

5886""" 

5887 

5888_dirichlet_mn_doc_frozen_callparams = "" 

5889 

5890_dirichlet_mn_doc_frozen_callparams_note = """\ 

5891See class definition for a detailed description of parameters.""" 

5892 

5893dirichlet_mn_docdict_params = { 

5894 '_dirichlet_mn_doc_default_callparams': _dirichlet_mn_doc_default_callparams, # noqa 

5895 '_doc_random_state': _doc_random_state 

5896} 

5897 

5898dirichlet_mn_docdict_noparams = { 

5899 '_dirichlet_mn_doc_default_callparams': _dirichlet_mn_doc_frozen_callparams, # noqa 

5900 '_doc_random_state': _doc_random_state 

5901} 

5902 

5903 

5904def _dirichlet_multinomial_check_parameters(alpha, n, x=None): 

5905 

5906 alpha = np.asarray(alpha) 

5907 n = np.asarray(n) 

5908 

5909 if x is not None: 

5910 # Ensure that `x` and `alpha` are arrays. If the shapes are 

5911 # incompatible, NumPy will raise an appropriate error. 

5912 try: 

5913 x, alpha = np.broadcast_arrays(x, alpha) 

5914 except ValueError as e: 

5915 msg = "`x` and `alpha` must be broadcastable." 

5916 raise ValueError(msg) from e 

5917 

5918 x_int = np.floor(x) 

5919 if np.any(x < 0) or np.any(x != x_int): 

5920 raise ValueError("`x` must contain only non-negative integers.") 

5921 x = x_int 

5922 

5923 if np.any(alpha <= 0): 

5924 raise ValueError("`alpha` must contain only positive values.") 

5925 

5926 n_int = np.floor(n) 

5927 if np.any(n <= 0) or np.any(n != n_int): 

5928 raise ValueError("`n` must be a positive integer.") 

5929 n = n_int 

5930 

5931 sum_alpha = np.sum(alpha, axis=-1) 

5932 sum_alpha, n = np.broadcast_arrays(sum_alpha, n) 

5933 

5934 return (alpha, sum_alpha, n) if x is None else (alpha, sum_alpha, n, x) 

5935 

5936 

5937class dirichlet_multinomial_gen(multi_rv_generic): 

5938 r"""A Dirichlet multinomial random variable. 

5939 

5940 The Dirichlet multinomial distribution is a compound probability 

5941 distribution: it is the multinomial distribution with number of trials 

5942 `n` and class probabilities ``p`` randomly sampled from a Dirichlet 

5943 distribution with concentration parameters ``alpha``. 

5944 

5945 Methods 

5946 ------- 

5947 logpmf(x, alpha, n): 

5948 Log of the probability mass function. 

5949 pmf(x, alpha, n): 

5950 Probability mass function. 

5951 mean(alpha, n): 

5952 Mean of the Dirichlet multinomial distribution. 

5953 var(alpha, n): 

5954 Variance of the Dirichlet multinomial distribution. 

5955 cov(alpha, n): 

5956 The covariance of the Dirichlet multinomial distribution. 

5957 

5958 Parameters 

5959 ---------- 

5960 %(_dirichlet_mn_doc_default_callparams)s 

5961 %(_doc_random_state)s 

5962 

5963 See Also 

5964 -------- 

5965 scipy.stats.dirichlet : The dirichlet distribution. 

5966 scipy.stats.multinomial : The multinomial distribution. 

5967 

5968 References 

5969 ---------- 

5970 .. [1] Dirichlet-multinomial distribution, Wikipedia, 

5971 https://www.wikipedia.org/wiki/Dirichlet-multinomial_distribution 

5972 

5973 Examples 

5974 -------- 

5975 >>> from scipy.stats import dirichlet_multinomial 

5976 

5977 Get the PMF 

5978 

5979 >>> n = 6 # number of trials 

5980 >>> alpha = [3, 4, 5] # concentration parameters 

5981 >>> x = [1, 2, 3] # counts 

5982 >>> dirichlet_multinomial.pmf(x, alpha, n) 

5983 0.08484162895927604 

5984 

5985 If the sum of category counts does not equal the number of trials, 

5986 the probability mass is zero. 

5987 

5988 >>> dirichlet_multinomial.pmf(x, alpha, n=7) 

5989 0.0 

5990 

5991 Get the log of the PMF 

5992 

5993 >>> dirichlet_multinomial.logpmf(x, alpha, n) 

5994 -2.4669689491013327 

5995 

5996 Get the mean 

5997 

5998 >>> dirichlet_multinomial.mean(alpha, n) 

5999 array([1.5, 2. , 2.5]) 

6000 

6001 Get the variance 

6002 

6003 >>> dirichlet_multinomial.var(alpha, n) 

6004 array([1.55769231, 1.84615385, 2.01923077]) 

6005 

6006 Get the covariance 

6007 

6008 >>> dirichlet_multinomial.cov(alpha, n) 

6009 array([[ 1.55769231, -0.69230769, -0.86538462], 

6010 [-0.69230769, 1.84615385, -1.15384615], 

6011 [-0.86538462, -1.15384615, 2.01923077]]) 

6012 

6013 Alternatively, the object may be called (as a function) to fix the 

6014 `alpha` and `n` parameters, returning a "frozen" Dirichlet multinomial 

6015 random variable. 

6016 

6017 >>> dm = dirichlet_multinomial(alpha, n) 

6018 >>> dm.pmf(x) 

6019 0.08484162895927579 

6020 

6021 All methods are fully vectorized. Each element of `x` and `alpha` is 

6022 a vector (along the last axis), each element of `n` is an 

6023 integer (scalar), and the result is computed element-wise. 

6024 

6025 >>> x = [[1, 2, 3], [4, 5, 6]] 

6026 >>> alpha = [[1, 2, 3], [4, 5, 6]] 

6027 >>> n = [6, 15] 

6028 >>> dirichlet_multinomial.pmf(x, alpha, n) 

6029 array([0.06493506, 0.02626937]) 

6030 

6031 >>> dirichlet_multinomial.cov(alpha, n).shape # both covariance matrices 

6032 (2, 3, 3) 

6033 

6034 Broadcasting according to standard NumPy conventions is supported. Here, 

6035 we have four sets of concentration parameters (each a two element vector) 

6036 for each of three numbers of trials (each a scalar). 

6037 

6038 >>> alpha = [[3, 4], [4, 5], [5, 6], [6, 7]] 

6039 >>> n = [[6], [7], [8]] 

6040 >>> dirichlet_multinomial.mean(alpha, n).shape 

6041 (3, 4, 2) 

6042 

6043 """ 

6044 def __init__(self, seed=None): 

6045 super().__init__(seed) 

6046 self.__doc__ = doccer.docformat(self.__doc__, 

6047 dirichlet_mn_docdict_params) 

6048 

6049 def __call__(self, alpha, n, seed=None): 

6050 return dirichlet_multinomial_frozen(alpha, n, seed=seed) 

6051 

6052 def logpmf(self, x, alpha, n): 

6053 """The log of the probability mass function. 

6054 

6055 Parameters 

6056 ---------- 

6057 x: ndarray 

6058 Category counts (non-negative integers). Must be broadcastable 

6059 with shape parameter ``alpha``. If multidimensional, the last axis 

6060 must correspond with the categories. 

6061 %(_dirichlet_mn_doc_default_callparams)s 

6062 

6063 Returns 

6064 ------- 

6065 out: ndarray or scalar 

6066 Log of the probability mass function. 

6067 

6068 """ 

6069 

6070 a, Sa, n, x = _dirichlet_multinomial_check_parameters(alpha, n, x) 

6071 

6072 out = np.asarray(loggamma(Sa) + loggamma(n + 1) - loggamma(n + Sa)) 

6073 out += (loggamma(x + a) - (loggamma(a) + loggamma(x + 1))).sum(axis=-1) 

6074 np.place(out, n != x.sum(axis=-1), -np.inf) 

6075 return out[()] 

6076 

6077 def pmf(self, x, alpha, n): 

6078 """Probability mass function for a Dirichlet multinomial distribution. 

6079 

6080 Parameters 

6081 ---------- 

6082 x: ndarray 

6083 Category counts (non-negative integers). Must be broadcastable 

6084 with shape parameter ``alpha``. If multidimensional, the last axis 

6085 must correspond with the categories. 

6086 %(_dirichlet_mn_doc_default_callparams)s 

6087 

6088 Returns 

6089 ------- 

6090 out: ndarray or scalar 

6091 Probability mass function. 

6092 

6093 """ 

6094 return np.exp(self.logpmf(x, alpha, n)) 

6095 

6096 def mean(self, alpha, n): 

6097 """Mean of a Dirichlet multinomial distribution. 

6098 

6099 Parameters 

6100 ---------- 

6101 %(_dirichlet_mn_doc_default_callparams)s 

6102 

6103 Returns 

6104 ------- 

6105 out: ndarray 

6106 Mean of a Dirichlet multinomial distribution. 

6107 

6108 """ 

6109 a, Sa, n = _dirichlet_multinomial_check_parameters(alpha, n) 

6110 n, Sa = n[..., np.newaxis], Sa[..., np.newaxis] 

6111 return n * a / Sa 

6112 

6113 def var(self, alpha, n): 

6114 """The variance of the Dirichlet multinomial distribution. 

6115 

6116 Parameters 

6117 ---------- 

6118 %(_dirichlet_mn_doc_default_callparams)s 

6119 

6120 Returns 

6121 ------- 

6122 out: array_like 

6123 The variances of the components of the distribution. This is 

6124 the diagonal of the covariance matrix of the distribution. 

6125 

6126 """ 

6127 a, Sa, n = _dirichlet_multinomial_check_parameters(alpha, n) 

6128 n, Sa = n[..., np.newaxis], Sa[..., np.newaxis] 

6129 return n * a / Sa * (1 - a/Sa) * (n + Sa) / (1 + Sa) 

6130 

6131 def cov(self, alpha, n): 

6132 """Covariance matrix of a Dirichlet multinomial distribution. 

6133 

6134 Parameters 

6135 ---------- 

6136 %(_dirichlet_mn_doc_default_callparams)s 

6137 

6138 Returns 

6139 ------- 

6140 out : array_like 

6141 The covariance matrix of the distribution. 

6142 

6143 """ 

6144 a, Sa, n = _dirichlet_multinomial_check_parameters(alpha, n) 

6145 var = dirichlet_multinomial.var(a, n) 

6146 

6147 n, Sa = n[..., np.newaxis, np.newaxis], Sa[..., np.newaxis, np.newaxis] 

6148 aiaj = a[..., :, np.newaxis] * a[..., np.newaxis, :] 

6149 cov = -n * aiaj / Sa ** 2 * (n + Sa) / (1 + Sa) 

6150 

6151 ii = np.arange(cov.shape[-1]) 

6152 cov[..., ii, ii] = var 

6153 return cov 

6154 

6155 

6156dirichlet_multinomial = dirichlet_multinomial_gen() 

6157 

6158 

6159class dirichlet_multinomial_frozen(multi_rv_frozen): 

6160 def __init__(self, alpha, n, seed=None): 

6161 alpha, Sa, n = _dirichlet_multinomial_check_parameters(alpha, n) 

6162 self.alpha = alpha 

6163 self.n = n 

6164 self._dist = dirichlet_multinomial_gen(seed) 

6165 

6166 def logpmf(self, x): 

6167 return self._dist.logpmf(x, self.alpha, self.n) 

6168 

6169 def pmf(self, x): 

6170 return self._dist.pmf(x, self.alpha, self.n) 

6171 

6172 def mean(self): 

6173 return self._dist.mean(self.alpha, self.n) 

6174 

6175 def var(self): 

6176 return self._dist.var(self.alpha, self.n) 

6177 

6178 def cov(self): 

6179 return self._dist.cov(self.alpha, self.n) 

6180 

6181 

6182# Set frozen generator docstrings from corresponding docstrings in 

6183# dirichlet_multinomial and fill in default strings in class docstrings. 

6184for name in ['logpmf', 'pmf', 'mean', 'var', 'cov']: 

6185 method = dirichlet_multinomial_gen.__dict__[name] 

6186 method_frozen = dirichlet_multinomial_frozen.__dict__[name] 

6187 method_frozen.__doc__ = doccer.docformat( 

6188 method.__doc__, dirichlet_mn_docdict_noparams) 

6189 method.__doc__ = doccer.docformat(method.__doc__, 

6190 dirichlet_mn_docdict_params) 

6191 

6192 

6193class vonmises_fisher_gen(multi_rv_generic): 

6194 r"""A von Mises-Fisher variable. 

6195 

6196 The `mu` keyword specifies the mean direction vector. The `kappa` keyword 

6197 specifies the concentration parameter. 

6198 

6199 Methods 

6200 ------- 

6201 pdf(x, mu=None, kappa=1) 

6202 Probability density function. 

6203 logpdf(x, mu=None, kappa=1) 

6204 Log of the probability density function. 

6205 rvs(mu=None, kappa=1, size=1, random_state=None) 

6206 Draw random samples from a von Mises-Fisher distribution. 

6207 entropy(mu=None, kappa=1) 

6208 Compute the differential entropy of the von Mises-Fisher distribution. 

6209 fit(data) 

6210 Fit a von Mises-Fisher distribution to data. 

6211 

6212 Parameters 

6213 ---------- 

6214 mu : array_like 

6215 Mean direction of the distribution. Must be a one-dimensional unit 

6216 vector of norm 1. 

6217 kappa : float 

6218 Concentration parameter. Must be positive. 

6219 seed : {None, int, np.random.RandomState, np.random.Generator}, optional 

6220 Used for drawing random variates. 

6221 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

6222 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

6223 with seed. 

6224 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

6225 then that object is used. 

6226 Default is `None`. 

6227 

6228 See Also 

6229 -------- 

6230 scipy.stats.vonmises : Von-Mises Fisher distribution in 2D on a circle 

6231 uniform_direction : uniform distribution on the surface of a hypersphere 

6232 

6233 Notes 

6234 ----- 

6235 The von Mises-Fisher distribution is a directional distribution on the 

6236 surface of the unit hypersphere. The probability density 

6237 function of a unit vector :math:`\mathbf{x}` is 

6238 

6239 .. math:: 

6240 

6241 f(\mathbf{x}) = \frac{\kappa^{d/2-1}}{(2\pi)^{d/2}I_{d/2-1}(\kappa)} 

6242 \exp\left(\kappa \mathbf{\mu}^T\mathbf{x}\right), 

6243 

6244 where :math:`\mathbf{\mu}` is the mean direction, :math:`\kappa` the 

6245 concentration parameter, :math:`d` the dimension and :math:`I` the 

6246 modified Bessel function of the first kind. As :math:`\mu` represents 

6247 a direction, it must be a unit vector or in other words, a point 

6248 on the hypersphere: :math:`\mathbf{\mu}\in S^{d-1}`. :math:`\kappa` is a 

6249 concentration parameter, which means that it must be positive 

6250 (:math:`\kappa>0`) and that the distribution becomes more narrow with 

6251 increasing :math:`\kappa`. In that sense, the reciprocal value 

6252 :math:`1/\kappa` resembles the variance parameter of the normal 

6253 distribution. 

6254 

6255 The von Mises-Fisher distribution often serves as an analogue of the 

6256 normal distribution on the sphere. Intuitively, for unit vectors, a 

6257 useful distance measure is given by the angle :math:`\alpha` between 

6258 them. This is exactly what the scalar product 

6259 :math:`\mathbf{\mu}^T\mathbf{x}=\cos(\alpha)` in the 

6260 von Mises-Fisher probability density function describes: the angle 

6261 between the mean direction :math:`\mathbf{\mu}` and the vector 

6262 :math:`\mathbf{x}`. The larger the angle between them, the smaller the 

6263 probability to observe :math:`\mathbf{x}` for this particular mean 

6264 direction :math:`\mathbf{\mu}`. 

6265 

6266 In dimensions 2 and 3, specialized algorithms are used for fast sampling 

6267 [2]_, [3]_. For dimenions of 4 or higher the rejection sampling algorithm 

6268 described in [4]_ is utilized. This implementation is partially based on 

6269 the geomstats package [5]_, [6]_. 

6270 

6271 .. versionadded:: 1.11 

6272 

6273 References 

6274 ---------- 

6275 .. [1] Von Mises-Fisher distribution, Wikipedia, 

6276 https://en.wikipedia.org/wiki/Von_Mises%E2%80%93Fisher_distribution 

6277 .. [2] Mardia, K., and Jupp, P. Directional statistics. Wiley, 2000. 

6278 .. [3] J. Wenzel. Numerically stable sampling of the von Mises Fisher 

6279 distribution on S2. 

6280 https://www.mitsuba-renderer.org/~wenzel/files/vmf.pdf 

6281 .. [4] Wood, A. Simulation of the von mises fisher distribution. 

6282 Communications in statistics-simulation and computation 23, 

6283 1 (1994), 157-164. https://doi.org/10.1080/03610919408813161 

6284 .. [5] geomstats, Github. MIT License. Accessed: 06.01.2023. 

6285 https://github.com/geomstats/geomstats 

6286 .. [6] Miolane, N. et al. Geomstats: A Python Package for Riemannian 

6287 Geometry in Machine Learning. Journal of Machine Learning Research 

6288 21 (2020). http://jmlr.org/papers/v21/19-027.html 

6289 

6290 Examples 

6291 -------- 

6292 **Visualization of the probability density** 

6293 

6294 Plot the probability density in three dimensions for increasing 

6295 concentration parameter. The density is calculated by the ``pdf`` 

6296 method. 

6297 

6298 >>> import numpy as np 

6299 >>> import matplotlib.pyplot as plt 

6300 >>> from scipy.stats import vonmises_fisher 

6301 >>> from matplotlib.colors import Normalize 

6302 >>> n_grid = 100 

6303 >>> u = np.linspace(0, np.pi, n_grid) 

6304 >>> v = np.linspace(0, 2 * np.pi, n_grid) 

6305 >>> u_grid, v_grid = np.meshgrid(u, v) 

6306 >>> vertices = np.stack([np.cos(v_grid) * np.sin(u_grid), 

6307 ... np.sin(v_grid) * np.sin(u_grid), 

6308 ... np.cos(u_grid)], 

6309 ... axis=2) 

6310 >>> x = np.outer(np.cos(v), np.sin(u)) 

6311 >>> y = np.outer(np.sin(v), np.sin(u)) 

6312 >>> z = np.outer(np.ones_like(u), np.cos(u)) 

6313 >>> def plot_vmf_density(ax, x, y, z, vertices, mu, kappa): 

6314 ... vmf = vonmises_fisher(mu, kappa) 

6315 ... pdf_values = vmf.pdf(vertices) 

6316 ... pdfnorm = Normalize(vmin=pdf_values.min(), vmax=pdf_values.max()) 

6317 ... ax.plot_surface(x, y, z, rstride=1, cstride=1, 

6318 ... facecolors=plt.cm.viridis(pdfnorm(pdf_values)), 

6319 ... linewidth=0) 

6320 ... ax.set_aspect('equal') 

6321 ... ax.view_init(azim=-130, elev=0) 

6322 ... ax.axis('off') 

6323 ... ax.set_title(rf"$\kappa={kappa}$") 

6324 >>> fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(9, 4), 

6325 ... subplot_kw={"projection": "3d"}) 

6326 >>> left, middle, right = axes 

6327 >>> mu = np.array([-np.sqrt(0.5), -np.sqrt(0.5), 0]) 

6328 >>> plot_vmf_density(left, x, y, z, vertices, mu, 5) 

6329 >>> plot_vmf_density(middle, x, y, z, vertices, mu, 20) 

6330 >>> plot_vmf_density(right, x, y, z, vertices, mu, 100) 

6331 >>> plt.subplots_adjust(top=1, bottom=0.0, left=0.0, right=1.0, wspace=0.) 

6332 >>> plt.show() 

6333 

6334 As we increase the concentration parameter, the points are getting more 

6335 clustered together around the mean direction. 

6336 

6337 **Sampling** 

6338 

6339 Draw 5 samples from the distribution using the ``rvs`` method resulting 

6340 in a 5x3 array. 

6341 

6342 >>> rng = np.random.default_rng() 

6343 >>> mu = np.array([0, 0, 1]) 

6344 >>> samples = vonmises_fisher(mu, 20).rvs(5, random_state=rng) 

6345 >>> samples 

6346 array([[ 0.3884594 , -0.32482588, 0.86231516], 

6347 [ 0.00611366, -0.09878289, 0.99509023], 

6348 [-0.04154772, -0.01637135, 0.99900239], 

6349 [-0.14613735, 0.12553507, 0.98126695], 

6350 [-0.04429884, -0.23474054, 0.97104814]]) 

6351 

6352 These samples are unit vectors on the sphere :math:`S^2`. To verify, 

6353 let us calculate their euclidean norms: 

6354 

6355 >>> np.linalg.norm(samples, axis=1) 

6356 array([1., 1., 1., 1., 1.]) 

6357 

6358 Plot 20 observations drawn from the von Mises-Fisher distribution for 

6359 increasing concentration parameter :math:`\kappa`. The red dot highlights 

6360 the mean direction :math:`\mu`. 

6361 

6362 >>> def plot_vmf_samples(ax, x, y, z, mu, kappa): 

6363 ... vmf = vonmises_fisher(mu, kappa) 

6364 ... samples = vmf.rvs(20) 

6365 ... ax.plot_surface(x, y, z, rstride=1, cstride=1, linewidth=0, 

6366 ... alpha=0.2) 

6367 ... ax.scatter(samples[:, 0], samples[:, 1], samples[:, 2], c='k', s=5) 

6368 ... ax.scatter(mu[0], mu[1], mu[2], c='r', s=30) 

6369 ... ax.set_aspect('equal') 

6370 ... ax.view_init(azim=-130, elev=0) 

6371 ... ax.axis('off') 

6372 ... ax.set_title(rf"$\kappa={kappa}$") 

6373 >>> mu = np.array([-np.sqrt(0.5), -np.sqrt(0.5), 0]) 

6374 >>> fig, axes = plt.subplots(nrows=1, ncols=3, 

6375 ... subplot_kw={"projection": "3d"}, 

6376 ... figsize=(9, 4)) 

6377 >>> left, middle, right = axes 

6378 >>> plot_vmf_samples(left, x, y, z, mu, 5) 

6379 >>> plot_vmf_samples(middle, x, y, z, mu, 20) 

6380 >>> plot_vmf_samples(right, x, y, z, mu, 100) 

6381 >>> plt.subplots_adjust(top=1, bottom=0.0, left=0.0, 

6382 ... right=1.0, wspace=0.) 

6383 >>> plt.show() 

6384 

6385 The plots show that with increasing concentration :math:`\kappa` the 

6386 resulting samples are centered more closely around the mean direction. 

6387 

6388 **Fitting the distribution parameters** 

6389 

6390 The distribution can be fitted to data using the ``fit`` method returning 

6391 the estimated parameters. As a toy example let's fit the distribution to 

6392 samples drawn from a known von Mises-Fisher distribution. 

6393 

6394 >>> mu, kappa = np.array([0, 0, 1]), 20 

6395 >>> samples = vonmises_fisher(mu, kappa).rvs(1000, random_state=rng) 

6396 >>> mu_fit, kappa_fit = vonmises_fisher.fit(samples) 

6397 >>> mu_fit, kappa_fit 

6398 (array([0.01126519, 0.01044501, 0.99988199]), 19.306398751730995) 

6399 

6400 We see that the estimated parameters `mu_fit` and `kappa_fit` are 

6401 very close to the ground truth parameters. 

6402 

6403 """ 

6404 def __init__(self, seed=None): 

6405 super().__init__(seed) 

6406 

6407 def __call__(self, mu=None, kappa=1, seed=None): 

6408 """Create a frozen von Mises-Fisher distribution. 

6409 

6410 See `vonmises_fisher_frozen` for more information. 

6411 """ 

6412 return vonmises_fisher_frozen(mu, kappa, seed=seed) 

6413 

6414 def _process_parameters(self, mu, kappa): 

6415 """ 

6416 Infer dimensionality from mu and ensure that mu is a one-dimensional 

6417 unit vector and kappa positive. 

6418 """ 

6419 mu = np.asarray(mu) 

6420 if mu.ndim > 1: 

6421 raise ValueError("'mu' must have one-dimensional shape.") 

6422 if not np.allclose(np.linalg.norm(mu), 1.): 

6423 raise ValueError("'mu' must be a unit vector of norm 1.") 

6424 if not mu.size > 1: 

6425 raise ValueError("'mu' must have at least two entries.") 

6426 kappa_error_msg = "'kappa' must be a positive scalar." 

6427 if not np.isscalar(kappa) or kappa < 0: 

6428 raise ValueError(kappa_error_msg) 

6429 if float(kappa) == 0.: 

6430 raise ValueError("For 'kappa=0' the von Mises-Fisher distribution " 

6431 "becomes the uniform distribution on the sphere " 

6432 "surface. Consider using " 

6433 "'scipy.stats.uniform_direction' instead.") 

6434 dim = mu.size 

6435 

6436 return dim, mu, kappa 

6437 

6438 def _check_data_vs_dist(self, x, dim): 

6439 if x.shape[-1] != dim: 

6440 raise ValueError("The dimensionality of the last axis of 'x' must " 

6441 "match the dimensionality of the " 

6442 "von Mises Fisher distribution.") 

6443 if not np.allclose(np.linalg.norm(x, axis=-1), 1.): 

6444 msg = "'x' must be unit vectors of norm 1 along last dimension." 

6445 raise ValueError(msg) 

6446 

6447 def _log_norm_factor(self, dim, kappa): 

6448 # normalization factor is given by 

6449 # c = kappa**(dim/2-1)/((2*pi)**(dim/2)*I[dim/2-1](kappa)) 

6450 # = kappa**(dim/2-1)*exp(-kappa) / 

6451 # ((2*pi)**(dim/2)*I[dim/2-1](kappa)*exp(-kappa) 

6452 # = kappa**(dim/2-1)*exp(-kappa) / 

6453 # ((2*pi)**(dim/2)*ive[dim/2-1](kappa) 

6454 # Then the log is given by 

6455 # log c = 1/2*(dim -1)*log(kappa) - kappa - -1/2*dim*ln(2*pi) - 

6456 # ive[dim/2-1](kappa) 

6457 halfdim = 0.5 * dim 

6458 return (0.5 * (dim - 2)*np.log(kappa) - halfdim * _LOG_2PI - 

6459 np.log(ive(halfdim - 1, kappa)) - kappa) 

6460 

6461 def _logpdf(self, x, dim, mu, kappa): 

6462 """Log of the von Mises-Fisher probability density function. 

6463 

6464 As this function does no argument checking, it should not be 

6465 called directly; use 'logpdf' instead. 

6466 

6467 """ 

6468 x = np.asarray(x) 

6469 self._check_data_vs_dist(x, dim) 

6470 dotproducts = np.einsum('i,...i->...', mu, x) 

6471 return self._log_norm_factor(dim, kappa) + kappa * dotproducts 

6472 

6473 def logpdf(self, x, mu=None, kappa=1): 

6474 """Log of the von Mises-Fisher probability density function. 

6475 

6476 Parameters 

6477 ---------- 

6478 x : array_like 

6479 Points at which to evaluate the log of the probability 

6480 density function. The last axis of `x` must correspond 

6481 to unit vectors of the same dimensionality as the distribution. 

6482 mu : array_like, default: None 

6483 Mean direction of the distribution. Must be a one-dimensional unit 

6484 vector of norm 1. 

6485 kappa : float, default: 1 

6486 Concentration parameter. Must be positive. 

6487 

6488 Returns 

6489 ------- 

6490 logpdf : ndarray or scalar 

6491 Log of the probability density function evaluated at `x`. 

6492 

6493 """ 

6494 dim, mu, kappa = self._process_parameters(mu, kappa) 

6495 return self._logpdf(x, dim, mu, kappa) 

6496 

6497 def pdf(self, x, mu=None, kappa=1): 

6498 """Von Mises-Fisher probability density function. 

6499 

6500 Parameters 

6501 ---------- 

6502 x : array_like 

6503 Points at which to evaluate the probability 

6504 density function. The last axis of `x` must correspond 

6505 to unit vectors of the same dimensionality as the distribution. 

6506 mu : array_like 

6507 Mean direction of the distribution. Must be a one-dimensional unit 

6508 vector of norm 1. 

6509 kappa : float 

6510 Concentration parameter. Must be positive. 

6511 

6512 Returns 

6513 ------- 

6514 pdf : ndarray or scalar 

6515 Probability density function evaluated at `x`. 

6516 

6517 """ 

6518 dim, mu, kappa = self._process_parameters(mu, kappa) 

6519 return np.exp(self._logpdf(x, dim, mu, kappa)) 

6520 

6521 def _rvs_2d(self, mu, kappa, size, random_state): 

6522 """ 

6523 In 2D, the von Mises-Fisher distribution reduces to the 

6524 von Mises distribution which can be efficiently sampled by numpy. 

6525 This method is much faster than the general rejection 

6526 sampling based algorithm. 

6527 

6528 """ 

6529 mean_angle = np.arctan2(mu[1], mu[0]) 

6530 angle_samples = random_state.vonmises(mean_angle, kappa, size=size) 

6531 samples = np.stack([np.cos(angle_samples), np.sin(angle_samples)], 

6532 axis=-1) 

6533 return samples 

6534 

6535 def _rvs_3d(self, kappa, size, random_state): 

6536 """ 

6537 Generate samples from a von Mises-Fisher distribution 

6538 with mu = [1, 0, 0] and kappa. Samples then have to be 

6539 rotated towards the desired mean direction mu. 

6540 This method is much faster than the general rejection 

6541 sampling based algorithm. 

6542 Reference: https://www.mitsuba-renderer.org/~wenzel/files/vmf.pdf 

6543 

6544 """ 

6545 if size is None: 

6546 sample_size = 1 

6547 else: 

6548 sample_size = size 

6549 

6550 # compute x coordinate acc. to equation from section 3.1 

6551 x = random_state.random(sample_size) 

6552 x = 1. + np.log(x + (1. - x) * np.exp(-2 * kappa))/kappa 

6553 

6554 # (y, z) are random 2D vectors that only have to be 

6555 # normalized accordingly. Then (x, y z) follow a VMF distribution 

6556 temp = np.sqrt(1. - np.square(x)) 

6557 uniformcircle = _sample_uniform_direction(2, sample_size, random_state) 

6558 samples = np.stack([x, temp * uniformcircle[..., 0], 

6559 temp * uniformcircle[..., 1]], 

6560 axis=-1) 

6561 if size is None: 

6562 samples = np.squeeze(samples) 

6563 return samples 

6564 

6565 def _rejection_sampling(self, dim, kappa, size, random_state): 

6566 """ 

6567 Generate samples from a n-dimensional von Mises-Fisher distribution 

6568 with mu = [1, 0, ..., 0] and kappa via rejection sampling. 

6569 Samples then have to be rotated towards the desired mean direction mu. 

6570 Reference: https://doi.org/10.1080/03610919408813161 

6571 """ 

6572 dim_minus_one = dim - 1 

6573 # calculate number of requested samples 

6574 if size is not None: 

6575 if not np.iterable(size): 

6576 size = (size, ) 

6577 n_samples = math.prod(size) 

6578 else: 

6579 n_samples = 1 

6580 # calculate envelope for rejection sampler (eq. 4) 

6581 sqrt = np.sqrt(4 * kappa ** 2. + dim_minus_one ** 2) 

6582 envelop_param = (-2 * kappa + sqrt) / dim_minus_one 

6583 if envelop_param == 0: 

6584 # the regular formula suffers from loss of precision for high 

6585 # kappa. This can only be detected by checking for 0 here. 

6586 # Workaround: expansion for sqrt variable 

6587 # https://www.wolframalpha.com/input?i=sqrt%284*x%5E2%2Bd%5E2%29 

6588 # e = (-2 * k + sqrt(k**2 + d**2)) / d 

6589 # ~ (-2 * k + 2 * k + d**2/(4 * k) - d**4/(64 * k**3)) / d 

6590 # = d/(4 * k) - d**3/(64 * k**3) 

6591 envelop_param = (dim_minus_one/4 * kappa**-1. 

6592 - dim_minus_one**3/64 * kappa**-3.) 

6593 # reference step 0 

6594 node = (1. - envelop_param) / (1. + envelop_param) 

6595 # t = ln(1 - ((1-x)/(1+x))**2) 

6596 # = ln(4 * x / (1+x)**2) 

6597 # = ln(4) + ln(x) - 2*log1p(x) 

6598 correction = (kappa * node + dim_minus_one 

6599 * (np.log(4) + np.log(envelop_param) 

6600 - 2 * np.log1p(envelop_param))) 

6601 n_accepted = 0 

6602 x = np.zeros((n_samples, )) 

6603 halfdim = 0.5 * dim_minus_one 

6604 # main loop 

6605 while n_accepted < n_samples: 

6606 # generate candidates acc. to reference step 1 

6607 sym_beta = random_state.beta(halfdim, halfdim, 

6608 size=n_samples - n_accepted) 

6609 coord_x = (1 - (1 + envelop_param) * sym_beta) / ( 

6610 1 - (1 - envelop_param) * sym_beta) 

6611 # accept or reject: reference step 2 

6612 # reformulation for numerical stability: 

6613 # t = ln(1 - (1-x)/(1+x) * y) 

6614 # = ln((1 + x - y +x*y)/(1 +x)) 

6615 accept_tol = random_state.random(n_samples - n_accepted) 

6616 criterion = ( 

6617 kappa * coord_x 

6618 + dim_minus_one * (np.log((1 + envelop_param - coord_x 

6619 + coord_x * envelop_param) / (1 + envelop_param))) 

6620 - correction) > np.log(accept_tol) 

6621 accepted_iter = np.sum(criterion) 

6622 x[n_accepted:n_accepted + accepted_iter] = coord_x[criterion] 

6623 n_accepted += accepted_iter 

6624 # concatenate x and remaining coordinates: step 3 

6625 coord_rest = _sample_uniform_direction(dim_minus_one, n_accepted, 

6626 random_state) 

6627 coord_rest = np.einsum( 

6628 '...,...i->...i', np.sqrt(1 - x ** 2), coord_rest) 

6629 samples = np.concatenate([x[..., None], coord_rest], axis=1) 

6630 # reshape output to (size, dim) 

6631 if size is not None: 

6632 samples = samples.reshape(size + (dim, )) 

6633 else: 

6634 samples = np.squeeze(samples) 

6635 return samples 

6636 

6637 def _rotate_samples(self, samples, mu, dim): 

6638 """A QR decomposition is used to find the rotation that maps the 

6639 north pole (1, 0,...,0) to the vector mu. This rotation is then 

6640 applied to all samples. 

6641 

6642 Parameters 

6643 ---------- 

6644 samples: array_like, shape = [..., n] 

6645 mu : array-like, shape=[n, ] 

6646 Point to parametrise the rotation. 

6647 

6648 Returns 

6649 ------- 

6650 samples : rotated samples 

6651 

6652 """ 

6653 base_point = np.zeros((dim, )) 

6654 base_point[0] = 1. 

6655 embedded = np.concatenate([mu[None, :], np.zeros((dim - 1, dim))]) 

6656 rotmatrix, _ = np.linalg.qr(np.transpose(embedded)) 

6657 if np.allclose(np.matmul(rotmatrix, base_point[:, None])[:, 0], mu): 

6658 rotsign = 1 

6659 else: 

6660 rotsign = -1 

6661 

6662 # apply rotation 

6663 samples = np.einsum('ij,...j->...i', rotmatrix, samples) * rotsign 

6664 return samples 

6665 

6666 def _rvs(self, dim, mu, kappa, size, random_state): 

6667 if dim == 2: 

6668 samples = self._rvs_2d(mu, kappa, size, random_state) 

6669 elif dim == 3: 

6670 samples = self._rvs_3d(kappa, size, random_state) 

6671 else: 

6672 samples = self._rejection_sampling(dim, kappa, size, 

6673 random_state) 

6674 

6675 if dim != 2: 

6676 samples = self._rotate_samples(samples, mu, dim) 

6677 return samples 

6678 

6679 def rvs(self, mu=None, kappa=1, size=1, random_state=None): 

6680 """Draw random samples from a von Mises-Fisher distribution. 

6681 

6682 Parameters 

6683 ---------- 

6684 mu : array_like 

6685 Mean direction of the distribution. Must be a one-dimensional unit 

6686 vector of norm 1. 

6687 kappa : float 

6688 Concentration parameter. Must be positive. 

6689 size : int or tuple of ints, optional 

6690 Given a shape of, for example, (m,n,k), m*n*k samples are 

6691 generated, and packed in an m-by-n-by-k arrangement. 

6692 Because each sample is N-dimensional, the output shape 

6693 is (m,n,k,N). If no shape is specified, a single (N-D) 

6694 sample is returned. 

6695 random_state : {None, int, np.random.RandomState, np.random.Generator}, 

6696 optional 

6697 Used for drawing random variates. 

6698 If `seed` is `None`, the `~np.random.RandomState` singleton is used. 

6699 If `seed` is an int, a new ``RandomState`` instance is used, seeded 

6700 with seed. 

6701 If `seed` is already a ``RandomState`` or ``Generator`` instance, 

6702 then that object is used. 

6703 Default is `None`. 

6704 

6705 Returns 

6706 ------- 

6707 rvs : ndarray 

6708 Random variates of shape (`size`, `N`), where `N` is the 

6709 dimension of the distribution. 

6710 

6711 """ 

6712 dim, mu, kappa = self._process_parameters(mu, kappa) 

6713 random_state = self._get_random_state(random_state) 

6714 samples = self._rvs(dim, mu, kappa, size, random_state) 

6715 return samples 

6716 

6717 def _entropy(self, dim, kappa): 

6718 halfdim = 0.5 * dim 

6719 return (-self._log_norm_factor(dim, kappa) - kappa * 

6720 ive(halfdim, kappa) / ive(halfdim - 1, kappa)) 

6721 

6722 def entropy(self, mu=None, kappa=1): 

6723 """Compute the differential entropy of the von Mises-Fisher 

6724 distribution. 

6725 

6726 Parameters 

6727 ---------- 

6728 mu : array_like, default: None 

6729 Mean direction of the distribution. Must be a one-dimensional unit 

6730 vector of norm 1. 

6731 kappa : float, default: 1 

6732 Concentration parameter. Must be positive. 

6733 

6734 Returns 

6735 ------- 

6736 h : scalar 

6737 Entropy of the von Mises-Fisher distribution. 

6738 

6739 """ 

6740 dim, _, kappa = self._process_parameters(mu, kappa) 

6741 return self._entropy(dim, kappa) 

6742 

6743 def fit(self, x): 

6744 """Fit the von Mises-Fisher distribution to data. 

6745 

6746 Parameters 

6747 ---------- 

6748 x : array-like 

6749 Data the distribution is fitted to. Must be two dimensional. 

6750 The second axis of `x` must be unit vectors of norm 1 and 

6751 determine the dimensionality of the fitted 

6752 von Mises-Fisher distribution. 

6753 

6754 Returns 

6755 ------- 

6756 mu : ndarray 

6757 Estimated mean direction. 

6758 kappa : float 

6759 Estimated concentration parameter. 

6760 

6761 """ 

6762 # validate input data 

6763 x = np.asarray(x) 

6764 if x.ndim != 2: 

6765 raise ValueError("'x' must be two dimensional.") 

6766 if not np.allclose(np.linalg.norm(x, axis=-1), 1.): 

6767 msg = "'x' must be unit vectors of norm 1 along last dimension." 

6768 raise ValueError(msg) 

6769 dim = x.shape[-1] 

6770 

6771 # mu is simply the directional mean 

6772 dirstats = directional_stats(x) 

6773 mu = dirstats.mean_direction 

6774 r = dirstats.mean_resultant_length 

6775 

6776 # kappa is the solution to the equation: 

6777 # r = I[dim/2](kappa) / I[dim/2 -1](kappa) 

6778 # = I[dim/2](kappa) * exp(-kappa) / I[dim/2 -1](kappa) * exp(-kappa) 

6779 # = ive(dim/2, kappa) / ive(dim/2 -1, kappa) 

6780 

6781 halfdim = 0.5 * dim 

6782 

6783 def solve_for_kappa(kappa): 

6784 bessel_vals = ive([halfdim, halfdim - 1], kappa) 

6785 return bessel_vals[0]/bessel_vals[1] - r 

6786 

6787 root_res = root_scalar(solve_for_kappa, method="brentq", 

6788 bracket=(1e-8, 1e9)) 

6789 kappa = root_res.root 

6790 return mu, kappa 

6791 

6792 

6793vonmises_fisher = vonmises_fisher_gen() 

6794 

6795 

6796class vonmises_fisher_frozen(multi_rv_frozen): 

6797 def __init__(self, mu=None, kappa=1, seed=None): 

6798 """Create a frozen von Mises-Fisher distribution. 

6799 

6800 Parameters 

6801 ---------- 

6802 mu : array_like, default: None 

6803 Mean direction of the distribution. 

6804 kappa : float, default: 1 

6805 Concentration parameter. Must be positive. 

6806 seed : {None, int, `numpy.random.Generator`, 

6807 `numpy.random.RandomState`}, optional 

6808 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

6809 singleton is used. 

6810 If `seed` is an int, a new ``RandomState`` instance is used, 

6811 seeded with `seed`. 

6812 If `seed` is already a ``Generator`` or ``RandomState`` instance 

6813 then that instance is used. 

6814 

6815 """ 

6816 self._dist = vonmises_fisher_gen(seed) 

6817 self.dim, self.mu, self.kappa = ( 

6818 self._dist._process_parameters(mu, kappa) 

6819 ) 

6820 

6821 def logpdf(self, x): 

6822 """ 

6823 Parameters 

6824 ---------- 

6825 x : array_like 

6826 Points at which to evaluate the log of the probability 

6827 density function. The last axis of `x` must correspond 

6828 to unit vectors of the same dimensionality as the distribution. 

6829 

6830 Returns 

6831 ------- 

6832 logpdf : ndarray or scalar 

6833 Log of probability density function evaluated at `x`. 

6834 

6835 """ 

6836 return self._dist._logpdf(x, self.dim, self.mu, self.kappa) 

6837 

6838 def pdf(self, x): 

6839 """ 

6840 Parameters 

6841 ---------- 

6842 x : array_like 

6843 Points at which to evaluate the log of the probability 

6844 density function. The last axis of `x` must correspond 

6845 to unit vectors of the same dimensionality as the distribution. 

6846 

6847 Returns 

6848 ------- 

6849 pdf : ndarray or scalar 

6850 Probability density function evaluated at `x`. 

6851 

6852 """ 

6853 return np.exp(self.logpdf(x)) 

6854 

6855 def rvs(self, size=1, random_state=None): 

6856 """Draw random variates from the Von Mises-Fisher distribution. 

6857 

6858 Parameters 

6859 ---------- 

6860 size : int or tuple of ints, optional 

6861 Given a shape of, for example, (m,n,k), m*n*k samples are 

6862 generated, and packed in an m-by-n-by-k arrangement. 

6863 Because each sample is N-dimensional, the output shape 

6864 is (m,n,k,N). If no shape is specified, a single (N-D) 

6865 sample is returned. 

6866 random_state : {None, int, `numpy.random.Generator`, 

6867 `numpy.random.RandomState`}, optional 

6868 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

6869 singleton is used. 

6870 If `seed` is an int, a new ``RandomState`` instance is used, 

6871 seeded with `seed`. 

6872 If `seed` is already a ``Generator`` or ``RandomState`` instance 

6873 then that instance is used. 

6874 

6875 Returns 

6876 ------- 

6877 rvs : ndarray or scalar 

6878 Random variates of size (`size`, `N`), where `N` is the 

6879 dimension of the distribution. 

6880 

6881 """ 

6882 random_state = self._dist._get_random_state(random_state) 

6883 return self._dist._rvs(self.dim, self.mu, self.kappa, size, 

6884 random_state) 

6885 

6886 def entropy(self): 

6887 """ 

6888 Calculate the differential entropy of the von Mises-Fisher 

6889 distribution. 

6890 

6891 Returns 

6892 ------- 

6893 h: float 

6894 Entropy of the Von Mises-Fisher distribution. 

6895 

6896 """ 

6897 return self._dist._entropy(self.dim, self.kappa)