rbg-ml 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. rbg/Classification/KNN.py +178 -0
  2. rbg/Classification/LDA.py +187 -0
  3. rbg/Classification/Logistic.py +189 -0
  4. rbg/Classification/QDA.py +198 -0
  5. rbg/Classification/__init__.py +5 -0
  6. rbg/Classification/classifier.py +63 -0
  7. rbg/DecisionTree/_ClassificationTree.py +78 -0
  8. rbg/DecisionTree/_ConstructionTree.py +623 -0
  9. rbg/DecisionTree/_RegressionTree.py +76 -0
  10. rbg/DecisionTree/__init__.py +6 -0
  11. rbg/DecisionTree/decisiontree.py +459 -0
  12. rbg/ModelSelection/GridSearch.py +91 -0
  13. rbg/ModelSelection/GroupKFold.py +76 -0
  14. rbg/ModelSelection/KFold.py +38 -0
  15. rbg/ModelSelection/LOOCV.py +25 -0
  16. rbg/ModelSelection/RandomizedSearch.py +94 -0
  17. rbg/ModelSelection/StratifiedKFold.py +67 -0
  18. rbg/ModelSelection/__init__.py +7 -0
  19. rbg/ModelSelection/holdoutCV.py +39 -0
  20. rbg/NeuralNetwork/MLP.py +220 -0
  21. rbg/NeuralNetwork/NeuralNetwork.py +0 -0
  22. rbg/NeuralNetwork/__init__.py +20 -0
  23. rbg/NeuralNetwork/activations.py +58 -0
  24. rbg/NeuralNetwork/data.py +39 -0
  25. rbg/NeuralNetwork/gradcheck.py +69 -0
  26. rbg/NeuralNetwork/layers.py +94 -0
  27. rbg/NeuralNetwork/losses.py +63 -0
  28. rbg/NeuralNetwork/metrics.py +25 -0
  29. rbg/NeuralNetwork/model.py +180 -0
  30. rbg/NeuralNetwork/optimizers.py +62 -0
  31. rbg/Preprocessing/MinMaxScaler.py +58 -0
  32. rbg/Preprocessing/Standard.py +34 -0
  33. rbg/Preprocessing/ZScoreClipper.py +50 -0
  34. rbg/Preprocessing/__init__.py +4 -0
  35. rbg/Preprocessing/_base.py +75 -0
  36. rbg/Regression/GLM.py +179 -0
  37. rbg/Regression/Lasso.py +144 -0
  38. rbg/Regression/Linear.py +43 -0
  39. rbg/Regression/Polynomial.py +168 -0
  40. rbg/Regression/Ridge.py +104 -0
  41. rbg/Regression/__init__.py +18 -0
  42. rbg/Regression/baseestimator.py +126 -0
  43. rbg/Regression/linearmodel.py +74 -0
  44. rbg/Unsupervised/KMeans.py +283 -0
  45. rbg/Unsupervised/PCA.py +196 -0
  46. rbg/Unsupervised/__init__.py +2 -0
  47. rbg/Utilities/ClassificationMetrics.py +177 -0
  48. rbg/Utilities/CoordinateDescent.py +89 -0
  49. rbg/Utilities/ExponentialFamily.py +115 -0
  50. rbg/Utilities/FisherScoring.py +123 -0
  51. rbg/Utilities/MatrixDesign.py +35 -0
  52. rbg/Utilities/Optimizer.py +79 -0
  53. rbg/Utilities/RandomIntervals.py +15 -0
  54. rbg/Utilities/RegressionMetrics.py +71 -0
  55. rbg/Utilities/Resample.py +39 -0
  56. rbg/Utilities/__init__.py +10 -0
  57. rbg/Utilities/crossvalidation.py +134 -0
  58. rbg/__init__.py +1 -0
  59. rbg_ml-0.1.0.dist-info/METADATA +171 -0
  60. rbg_ml-0.1.0.dist-info/RECORD +61 -0
  61. rbg_ml-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,178 @@
1
+ import numpy as np
2
+ from rbg.Classification.classifier import Classifier
3
+
4
+ class KNN(Classifier):
5
+ """K-nearest neighbors classifier for categorical labels."""
6
+
7
+ def __init__(self, labels=None, k=3, reference_class = None):
8
+ """Initialize the classifier with class labels and the number of neighbors.
9
+
10
+ Args:
11
+ labels: Unique class labels supported by the classifier.
12
+ k: Number of nearest neighbors used to classify each observation.
13
+ Defaults to 3.
14
+ reference_class: Reference label used to initialize the base
15
+ classifier.
16
+
17
+ Raises:
18
+ ValueError: If the class labels or reference label are invalid,
19
+ as defined by the base classifier.
20
+ """
21
+ Classifier.__init__(self, labels, reference_class)
22
+ self.set_params(k)
23
+
24
+ def set_params(self, k):
25
+ """Set the number of nearest neighbors.
26
+
27
+ Args:
28
+ k: Positive integer specifying the number of neighbors to consider.
29
+
30
+ Returns:
31
+ The classifier instance, allowing method chaining.
32
+
33
+ Raises:
34
+ ValueError: If k is not a positive integer or is a boolean.
35
+ """
36
+ if isinstance(k, bool) == False:
37
+ if isinstance(k, int) and k>0:
38
+ self.k = k
39
+ else:
40
+ raise ValueError("k must be a positive integer.")
41
+ else:
42
+ raise ValueError("k must be a positive integer.")
43
+ return self
44
+
45
+ def get_params(self):
46
+ """Return the current classifier parameters.
47
+
48
+ Returns:
49
+ Dictionary containing k, the number of nearest neighbors.
50
+ """
51
+ return {"k": self.k}
52
+
53
+ def fit(self, X,y):
54
+ """Store the training data after validating their format and contents.
55
+
56
+ Args:
57
+ X: Numerical training feature matrix of shape
58
+ (n_samples, n_features). All values must be finite.
59
+ y: One-dimensional array of class labels of shape (n_samples,).
60
+ All labels must belong to the set declared at initialization.
61
+
62
+ Raises:
63
+ ValueError: If X or y has an invalid dimension, X contains
64
+ non-numerical or non-finite values, y contains unknown labels,
65
+ the numbers of observations do not match, the data are empty,
66
+ or X has no features.
67
+ """
68
+ self._fit_done = False
69
+
70
+ X = np.asarray(X)
71
+ y = np.asarray(y)
72
+
73
+ if X.ndim != 2:
74
+ raise ValueError("X must be two-dimensional.")
75
+ if np.issubdtype(X.dtype, np.number) == False:
76
+ raise ValueError("X must be a numerical array.")
77
+ else:
78
+ if np.isfinite(X).all() == False:
79
+ raise ValueError("X must have only finite values.")
80
+ if y.ndim != 1:
81
+ raise ValueError("y must be one-dimensional.")
82
+ if X.shape[0] != y.shape[0]:
83
+ raise ValueError("The data do not match.")
84
+ if X.shape[0] == 0 or y.shape[0] == 0 or y.size == 0:
85
+ raise ValueError("X and y must not be empty.")
86
+ if X.shape[1] == 0:
87
+ raise ValueError("X must have at least one feature.")
88
+ if self._labels is None:
89
+ self._set_labels(np.unique(y))
90
+ elif not np.isin(y, self._labels).all():
91
+ raise ValueError("y contains unknown class labels.")
92
+
93
+ self._X_train = X
94
+ self._y_train = y
95
+ self._fit_done = True
96
+
97
+ def predict(self, X):
98
+ """Predict class labels using the majority vote of the nearest neighbors.
99
+
100
+ For each observation, calculate the Euclidean distances to the training
101
+ observations, select the k nearest neighbors, and assign the most
102
+ frequent class among their labels.
103
+
104
+ Args:
105
+ X: Numerical feature matrix of shape
106
+ (n_new_samples, n_features). The number of features must match
107
+ the training data, and all values must be finite.
108
+
109
+ Returns:
110
+ Array of predicted class labels of shape (n_new_samples,).
111
+
112
+ Raises:
113
+ ValueError: If the model has not been fitted, X has an invalid
114
+ dimension, contains non-numerical or non-finite values, is
115
+ empty, or has a different number of features from the
116
+ training data.
117
+ """
118
+ if self._fit_done == False:
119
+ raise ValueError("The model must be trained before prediction.")
120
+
121
+ X = np.asarray(X)
122
+
123
+ if X.ndim != 2:
124
+ raise ValueError("X must be two-dimensional.")
125
+ if np.issubdtype(X.dtype, np.number) == False:
126
+ raise ValueError("X must be a numerical array.")
127
+ else:
128
+ if np.isfinite(X).all() == False:
129
+ raise ValueError("X must have only finite values.")
130
+ if X.shape[0] == 0:
131
+ raise ValueError("X must not be empty.")
132
+ if X.shape[1] != self._X_train.shape[1]:
133
+ raise ValueError("X must be the same number of features as the training X.")
134
+
135
+ def min_distance(x, k):
136
+ """Return the indices of the k training observations closest to x.
137
+
138
+ Args:
139
+ x: Feature vector of a single observation.
140
+ k: Number of nearest neighbors to select.
141
+
142
+ Returns:
143
+ Indices of the k training observations with the smallest
144
+ Euclidean distances to x.
145
+ """
146
+ dist = [sum((x[i]-amostra[i])**2 for i in range(len(x)))**(1/2) for amostra in self._X_train]
147
+ indice = np.argsort(dist)[:k]
148
+ return indice
149
+
150
+ def class_neighbors(indice):
151
+ """Retrieve the class labels of the selected neighbors.
152
+
153
+ Args:
154
+ indice: Indices of the selected training observations.
155
+
156
+ Returns:
157
+ Array of class labels corresponding to the selected neighbors.
158
+ """
159
+ neighbors = self._y_train[indice]
160
+ return neighbors
161
+
162
+ prediction = [class_neighbors(min_distance(x, self.k)) for x in X]
163
+
164
+ def final_class(prediction):
165
+ """Determine the most frequent class among the neighbors.
166
+
167
+ Args:
168
+ prediction: Array of class labels assigned to the neighbors.
169
+
170
+ Returns:
171
+ Class label with the highest frequency. Ties are resolved by
172
+ selecting the first class returned by np.unique.
173
+ """
174
+ labels, counts = np.unique(prediction, return_counts=True)
175
+ return labels[np.argmax(counts)]
176
+
177
+ predicted_labels = np.array([final_class(neighbors) for neighbors in prediction])
178
+ return predicted_labels
@@ -0,0 +1,187 @@
1
+ import numpy as np
2
+ from rbg.Classification import Classifier
3
+
4
+ class LDA(Classifier):
5
+ """Linear discriminant analysis classifier for categorical labels.
6
+
7
+ Assumes that all classes share a common covariance matrix and uses
8
+ class-specific means and prior probabilities to discriminate between
9
+ classes.
10
+ """
11
+
12
+ def __init__(self, labels, reference_class=None):
13
+ """Initialize the classifier with the supported class labels.
14
+
15
+ Args:
16
+ labels: Unique class labels supported by the classifier.
17
+ reference_class: Reference label used to initialize the base
18
+ classifier.
19
+ """
20
+ Classifier.__init__(self, labels, reference_class)
21
+
22
+ def fit(self, X, y):
23
+ """Estimate class means and the shared covariance matrix from training data.
24
+
25
+ Args:
26
+ X: Numerical training feature matrix of shape
27
+ (n_samples, n_features). All values must be finite.
28
+ y: One-dimensional array of class labels of shape (n_samples,).
29
+ All labels must belong to the set declared at initialization.
30
+
31
+ Raises:
32
+ ValueError: If X or y has an invalid dimension, X contains
33
+ non-numerical or non-finite values, y contains unknown labels,
34
+ the numbers of observations do not match, X is empty or has no
35
+ features, fewer than two classes are present, there are not
36
+ enough samples to estimate the covariance matrix, or the
37
+ estimated covariance matrix is singular.
38
+ """
39
+ self._fit_done = False
40
+
41
+ X = np.asarray(X)
42
+ y = np.asarray(y)
43
+
44
+ if X.ndim != 2:
45
+ raise ValueError("X must be two-dimensional.")
46
+ if np.issubdtype(X.dtype, np.number) == False:
47
+ raise ValueError("X must be a numerical array.")
48
+ else:
49
+ if np.isfinite(X).all() == False:
50
+ raise ValueError("X must have only finite values.")
51
+ if y.ndim != 1:
52
+ raise ValueError("y must be one-dimensional.")
53
+ if not np.isin(y, self._labels).all():
54
+ raise ValueError("y contains unknown class labels.")
55
+ if X.shape[0] != y.shape[0]:
56
+ raise ValueError("The data do not match.")
57
+ if X.shape[0] == 0:
58
+ raise ValueError("X must not be empty.")
59
+ if X.shape[1] == 0:
60
+ raise ValueError("X must have at least one feature.")
61
+
62
+ self.labels = np.unique(y)
63
+ if self.labels.size < 2:
64
+ raise ValueError("y must be at least two classes.")
65
+ if X.shape[0] <= self.labels.size:
66
+ raise ValueError("There must be more samples to estimate covariance.")
67
+
68
+ self._X_train = X
69
+ self._y_train = y
70
+
71
+ self._covariance = self.cov_matrix()
72
+ if np.linalg.matrix_rank(self._covariance) < self._covariance.shape[0]:
73
+ raise ValueError("The covariance matrix is singular.")
74
+
75
+ self._fit_done = True
76
+
77
+ def mean_array(self, k):
78
+ """Calculate the mean feature vector for a given class.
79
+
80
+ Args:
81
+ k: Class label for which the mean is calculated.
82
+
83
+ Returns:
84
+ Mean feature vector of shape (n_features,) for class k.
85
+
86
+ Raises:
87
+ ValueError: If the specified class has no training samples.
88
+ """
89
+ x_k = self._X_train[self._y_train == k]
90
+ if x_k.shape[0] == 0:
91
+ raise ValueError("The class must have training samples.")
92
+ mean_k = np.mean(x_k, axis = 0)
93
+ return mean_k
94
+
95
+ def cov_matrix(self):
96
+ """Estimate the pooled covariance matrix across all training classes.
97
+
98
+ Computes the sum of the within-class scatter matrices divided by the
99
+ total number of observations minus the number of observed classes.
100
+
101
+ Returns:
102
+ Pooled covariance matrix of shape (n_features, n_features).
103
+ """
104
+ matriz = np.zeros((self._X_train.shape[1], self._X_train.shape[1]))
105
+ detours = [self._X_train[self._y_train == k] - self.mean_array(k) for k in self.labels]
106
+ products = [detour.T @ detour for detour in detours]
107
+ matriz = np.sum(np.stack(products), axis=0)/(self._y_train.size - self.labels.size)
108
+ return matriz
109
+
110
+ def prior(self, k):
111
+ """Calculate the empirical prior probability of a given class.
112
+
113
+ Args:
114
+ k: Class label for which the prior probability is calculated.
115
+
116
+ Returns:
117
+ Proportion of training observations belonging to class k.
118
+ """
119
+ sum_k = (self._y_train == k).sum()
120
+ prior_k = sum_k/self._y_train.size
121
+ return prior_k
122
+
123
+ def k_functions(self, k):
124
+ """Construct the linear discriminant function for a given class.
125
+
126
+ The discriminant combines the class mean, the inverse of the shared
127
+ covariance matrix, and the logarithm of the empirical class prior.
128
+
129
+ Args:
130
+ k: Class label for which the discriminant function is constructed.
131
+
132
+ Returns:
133
+ Callable that evaluates the discriminant function for a feature
134
+ vector x.
135
+ """
136
+ def function_discriminant(x):
137
+ """Evaluate the linear discriminant score for a feature vector.
138
+
139
+ Args:
140
+ x: Feature vector of shape (n_features,).
141
+
142
+ Returns:
143
+ Discriminant score for class k.
144
+ """
145
+ function = (x.T @ np.linalg.inv(self._covariance) @ self.mean_array(k)
146
+ - 1/2 * self.mean_array(k).T @ np.linalg.inv(self._covariance)
147
+ @ self.mean_array(k) + np.log(self.prior(k)))
148
+ return function
149
+ return function_discriminant
150
+
151
+ def predict(self, X):
152
+ """Predict class labels by maximizing the linear discriminant scores.
153
+
154
+ For each observation, evaluates the discriminant function of every
155
+ observed training class and selects the class with the highest score.
156
+
157
+ Args:
158
+ X: Numerical feature matrix of shape
159
+ (n_new_samples, n_features). The number of features must match
160
+ the training data, and all values must be finite.
161
+
162
+ Returns:
163
+ Array of predicted class labels of shape (n_new_samples,).
164
+
165
+ Raises:
166
+ ValueError: If the model has not been fitted, X has an invalid
167
+ dimension, contains non-numerical or non-finite values, is
168
+ empty, or has a different number of features from the
169
+ training data.
170
+ """
171
+ X = np.asarray(X)
172
+ if self._fit_done == False:
173
+ raise ValueError("The model must be trained before prediction.")
174
+ if X.ndim != 2:
175
+ raise ValueError("X must be two-dimensional.")
176
+ if np.issubdtype(X.dtype, np.number) == False:
177
+ raise ValueError("X must be a numerical array.")
178
+ else:
179
+ if np.isfinite(X).all() == False:
180
+ raise ValueError("X must have only finite values.")
181
+ if X.shape[0] == 0:
182
+ raise ValueError("X must not be empty.")
183
+ if X.shape[1] != self._X_train.shape[1]:
184
+ raise ValueError("X must be the same number of features as the training X.")
185
+
186
+ functions_class = np.array([self.labels[np.argmax([self.k_functions(k)(x) for k in self.labels])]for x in X])
187
+ return functions_class
@@ -0,0 +1,189 @@
1
+ import numpy as np
2
+ from rbg.Regression import GLM
3
+ from rbg.Utilities import ExponentialFamily
4
+ from .classifier import Classifier
5
+
6
+ class Logistic(Classifier, GLM):
7
+ """Multinomial logistic classifier using a reference-class GLM.
8
+
9
+ Estimate K-1 coefficient vectors, fixing the reference-class coefficients
10
+ to zero. Probability columns follow self._labels, with the reference last.
11
+ Requires a GLM and Fisher-scoring solver supporting multivariate responses.
12
+ """
13
+
14
+ def __init__(self, classes=None, reference_class=None, fit_intercept=True, inference=False, *, labels=None):
15
+ """Configure labels and initialize the categorical GLM.
16
+
17
+ Args:
18
+ labels: Compatibility alias for classes.
19
+ classes: Optional unique class labels. If None, they are identified
20
+ automatically during fitting.
21
+ reference_class: Reference label, defaulting to the first supplied.
22
+ fit_intercept: Whether to estimate an intercept for each modeled class.
23
+ inference: Whether the GLM computes coefficient covariance during fit.
24
+
25
+ Raises:
26
+ ValueError: If class labels or the reference label are invalid.
27
+ """
28
+ if labels is not None:
29
+ if classes is not None:
30
+ raise ValueError("Provide only one of classes or labels.")
31
+ classes = labels
32
+ Classifier.__init__(self, classes, reference_class)
33
+ self._fit_intercept = fit_intercept
34
+ self._inference = inference
35
+ self._glm_initialized = False
36
+
37
+ if classes is not None:
38
+ GLM.__init__(self, family=self._make_family(),fit_intercept=fit_intercept, inference=inference)
39
+ self._glm_initialized = True
40
+
41
+ def fit(self, X,y):
42
+ """Encode class labels and delegate coefficient estimation to the GLM.
43
+
44
+ Args:
45
+ X: Feature matrix of shape (n_samples, p_features), without an
46
+ automatically generated intercept column.
47
+ y: Original class labels of shape (n_samples,).
48
+
49
+ Returns:
50
+ Coefficient matrix of shape (p, K-1), where p includes the intercept when enabled.
51
+
52
+ Raises:
53
+ ValueError: If y is not one-dimensional, contains unknown labels,
54
+ or training inputs fail GLM validation.
55
+ """
56
+ if hasattr(X, "to_numpy"):
57
+ X = X.to_numpy()
58
+ if hasattr(y, "to_numpy"):
59
+ y = y.to_numpy()
60
+
61
+ X = np.asarray(X)
62
+ y = np.asarray(y)
63
+
64
+ if y.ndim != 1:
65
+ raise ValueError("y must be one-dimensional.")
66
+ if X.shape[0] != y.shape[0]:
67
+ raise ValueError("X and y must have the same number of observations.")
68
+ observed_labels = np.unique(y)
69
+ if self._labels is None or not np.isin(observed_labels, self.labels).all():
70
+ labels = observed_labels if self.labels is None else np.unique(np.concatenate((self.labels, observed_labels)))
71
+ self._set_labels(labels, self._reference_class)
72
+ self._glm_initialized = False
73
+ if len(self._labels) < 2:
74
+ raise ValueError("At least two classes are required.")
75
+ if not self._glm_initialized:
76
+ GLM.__init__(self, family=self._make_family(),fit_intercept=self._fit_intercept,inference=self._inference)
77
+ self._glm_initialized = True
78
+ if not np.isin(y, self.labels).all():
79
+ raise ValueError("y contains unknown class labels.")
80
+
81
+ Y = (y[:,None] == self._labels[:-1][None,:]).astype(float)
82
+ return GLM.fit(self, X, Y)
83
+
84
+ def predict_proba(self, X):
85
+ """Return probabilities for all classes, including the reference.
86
+
87
+ Args:
88
+ X: Matrix of shape (n_new_samples, p_features), in training order.
89
+
90
+ Returns:
91
+ Probability matrix of shape (n_new_samples, K). Columns correspond
92
+ to self._labels; the final column is the reference probability.
93
+
94
+ Raises:
95
+ RuntimeError: If the model has not been fitted.
96
+ ValueError: If X has an incompatible shape.
97
+ """
98
+ if hasattr(X, "to_numpy"):
99
+ X = X.to_numpy()
100
+
101
+ pi = GLM.predict(self, X)
102
+ pi_reference = 1 - pi.sum(axis=1, keepdims=True)
103
+ return np.column_stack((pi, pi_reference))
104
+
105
+ def predict(self, X):
106
+ """Return the label with the highest probability for each observation.
107
+
108
+ Args:
109
+ X: Feature matrix of shape (n_new_samples, p_features).
110
+
111
+ Returns:
112
+ Original class labels of shape (n_new_samples,). Ties select the
113
+ first maximum in the stored class order.
114
+
115
+ Raises:
116
+ RuntimeError: If the model has not been fitted.
117
+ ValueError: If X has an incompatible shape.
118
+ """
119
+ pi = self.predict_proba(X)
120
+ return self._labels[np.argmax(pi, axis=1)]
121
+
122
+ def k_function(self, k):
123
+ """Return the fitted probability function for class k."""
124
+ if not self._fit_done:
125
+ raise RuntimeError("The model has not been fitted yet.")
126
+ matches = np.flatnonzero(self._labels == k)
127
+ if not matches.size:
128
+ raise ValueError("Unknown class label.")
129
+ column = int(matches[0])
130
+
131
+ def probability(X):
132
+ X = np.asarray(X)
133
+ single = X.ndim == 1
134
+ result = self.predict_proba(X[None, :] if single else X)[:, column]
135
+ return float(result[0]) if single else result
136
+
137
+ return probability
138
+
139
+ def k_coef(self, k):
140
+ """Return class coefficients, or zeros for the reference class."""
141
+ if not self._fit_done:
142
+ raise RuntimeError("The model has not been fitted yet.")
143
+ matches = np.flatnonzero(self._labels == k)
144
+ if not matches.size:
145
+ raise ValueError("Unknown class label.")
146
+ column = int(matches[0])
147
+ if column == len(self._labels) - 1:
148
+ return np.zeros(self._beta.shape[0])
149
+ return self._beta[:, column].copy()
150
+
151
+ inference = GLM.inference
152
+
153
+ def _make_family(self):
154
+ """Build the categorical exponential family with dispersion fixed at one.
155
+
156
+ Returns:
157
+ ExponentialFamily configured with the cumulant, gradient, Hessian,
158
+ and inverse mean mapping for K-1 non-reference components.
159
+ """
160
+ self._K = len(self._labels)
161
+ def b(theta):
162
+ """Return log(1 + sum(exp(theta))) per row of an (n, K-1) matrix."""
163
+ return np.logaddexp.reduce(
164
+ np.column_stack((np.zeros(len(theta)), theta)), axis=1
165
+ )
166
+ def b_prime(theta):
167
+ """Return the (n, K-1) probabilities of the non-reference classes."""
168
+ pi = np.exp(theta - b(theta)[:, None])
169
+ return pi
170
+ def b_second(theta):
171
+ """Return Hessians diag(pi) - pi*pi.T, with shape (n, K-1, K-1)."""
172
+ pi = b_prime(theta)
173
+ n, n_components = pi.shape
174
+ cov_matrix = np.zeros((n, n_components, n_components))
175
+ for i in range(n):
176
+ cov_matrix[i] = np.diag(pi[i]) - np.outer(pi[i], pi[i])
177
+ return cov_matrix
178
+ def h_inverse(mu):
179
+ """Return log-odds relative to the reference for each row of mu.
180
+
181
+ All supplied probabilities and the remaining reference probability
182
+ must be strictly positive. The caller must satisfy this domain.
183
+ """
184
+ pi = mu
185
+ pi_reference = 1 - np.sum(pi, axis=1, keepdims=True)
186
+ theta = np.log(pi) - np.log(pi_reference)
187
+ return theta
188
+
189
+ return ExponentialFamily(b,b_prime,b_second,h_inverse)