Repository: scipy/scipy

Issue:
DOC: publicly expose `multivariate_normal` attributes `mean` and `cov`
### Issue with current documentation:

In scipy 1.9.3, the following code was valid and printed the covariance matrix of the distribution:

```
from scipy import stats
distribution = stats.multivariate_normal([0, 0], [[1, 0], [0, 1]])
print(distribution.cov)
```

In scipy 1.10.0, this gives the error
`AttributeError: 'multivariate_normal_frozen' object has no attribute 'cov'`
and in order to access the covariance from the frozen distribution the approach I found to work is 

```
from scipy import stats
distribution = stats.multivariate_normal([0, 0], [[1, 0], [0, 1]])
print(distribution.cov_object.covariance)
```

which does not work in scipy 1.9.3.
None of this is documented in the [docs](https://scipy.github.io/devdocs/reference/generated/scipy.stats.multivariate_normal.html#scipy-stats-multivariate-normal), neither is the change reported in the [changelog](https://github.com/scipy/scipy/releases/tag/v1.10.0).

### Idea or request for content:

It seems useful to document the [cov_object](https://github.com/scipy/scipy/blob/5beeda995449c55332ce141cad1be77621789f31/scipy/stats/_multivariate.py#L833) attribute of the frozen distribution somewhere; alternatively, to add a convenience `rv.cov` property redirecting to `rv.cov_object.covariance` for backwards compatibility.

### Additional context (e.g. screenshots, GIFs)

_No response_


Discussion:
I'm unable to find documentation of a `cov` attribute. `cov` is the name of a parameter accepted by `multivariate_normal` methods, but I don't think it's guaranteed to be accessible as an attribute. I would agree that best practice would have been to prefix the `cov` attribute name with an underscore if it was not intended to be public, but that may not have been common practice when the distribution was written. Note that while it may be common for frozen multivariate distributions to store shape parameters as attributes with the same name, I don't think it's guaranteed, and frozen univariate distributions do not:
```python3
from scipy import stats
dist = stats.weibull_min(c=1)
dist.c  # AttributeError: 'rv_continuous_frozen' object has no attribute 'c'
```
That said, there have been requests (at least for univariate distributions) to make shape, location, and scale parameters available as attributes for all distributions.

Please correct me if it is documentated that `cov` is a public attribute. Otherwise, shall we consider this to be a request for making `cov` a public attribute?
Indeed, I have not been able to find documentation for `cov`  being available as an attribute in the API, but I think it should be.

They (`mean` and `cov`) seem like simple enough attributes to make available as part of the API since they have to be stored anyway - I am willing to start a PR with some tests and documentation for this additional feature.
Shall I rename this issue to reflect the change?
The `cov` attribute is used in the `multivariate_normal_frozen` example in the docstring, so it was documented in that sense.

Note that we did change the implementation of `multivariate_normal` to allow the use of `Covariance` instances that allow for different representations of the covariance matrix (e.g. a precision matrix or a pre-factored Cholesky decomposition). That's the `cov_object`. You could restore the public `cov` attribute as a `@property` that returns `self.cov_object.covariance`, but I would not store the `cov` explicitly upon construction. Some `Covariance` implementations construct that matrix as-needed with properties; defer to that choice.

I think a PR that added explicit `Attributes` documentation for `mean`, `cov`, and `cov_object` with accompanying tests to ensure that they remain would be welcome.

Resolving patch:
diff --git a/scipy/stats/_multivariate.py b/scipy/stats/_multivariate.py
--- a/scipy/stats/_multivariate.py
+++ b/scipy/stats/_multivariate.py
@@ -839,6 +839,10 @@ def __init__(self, mean=None, cov=1, allow_singular=False, seed=None,
         self.abseps = abseps
         self.releps = releps
 
+    @property
+    def cov(self):
+        return self.cov_object.covariance
+
     def logpdf(self, x):
         x = self._dist._process_quantiles(x, self.dim)
         out = self._dist._logpdf(x, self.mean, self.cov_object)


Test patch:
diff --git a/scipy/stats/tests/test_multivariate.py b/scipy/stats/tests/test_multivariate.py
--- a/scipy/stats/tests/test_multivariate.py
+++ b/scipy/stats/tests/test_multivariate.py
@@ -513,6 +513,20 @@ def test_frozen(self):
         assert_allclose(norm_frozen.cdf(x), multivariate_normal.cdf(x, mean, cov))
         assert_allclose(norm_frozen.logcdf(x),
                         multivariate_normal.logcdf(x, mean, cov))
+    
+    @pytest.mark.parametrize(
+        'covariance',
+        [
+            np.eye(2),
+            Covariance.from_diagonal([1, 1]),
+        ]
+    )
+    def test_frozen_multivariate_normal_exposes_attributes(self, covariance):
+        mean = np.ones((2,))
+        cov_should_be = np.eye(2)
+        norm_frozen = multivariate_normal(mean, covariance)
+        assert np.allclose(norm_frozen.mean, mean)
+        assert np.allclose(norm_frozen.cov, cov_should_be)
 
     def test_pseudodet_pinv(self):
         # Make sure that pseudo-inverse and pseudo-det agree on cutoff