| """ |
| Functiona ANOVA Decomposition Basis. |
| |
| This module implements an incremental hierarchical-orthogonal basis construction for |
| discrete multivariate data. It computes the ANOVA-like decomposition |
| matrix and solves the regularized linear system for model interpretation. |
| |
| Dependencies: |
| - numpy |
| - scipy |
| - tqdm |
| """ |
|
|
| |
| |
| |
|
|
| from typing import Tuple, List, Optional, Dict, Any |
| import numpy as np |
| from tqdm import tqdm |
| from itertools import combinations |
| from math import prod |
| from scipy.linalg import cho_factor, cho_solve |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| def _compute_patterns(X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: |
| """ |
| Computes distinct patterns and empirical probabilities from the dataset. |
| |
| Args: |
| X (np.ndarray): Input dataset of shape (n_samples, n_features). |
| |
| Returns: |
| combs (np.ndarray): Distinct rows (patterns) present in X, shape (r, d). |
| P (np.ndarray): Empirical probabilities for each pattern, shape (r,). |
| N (np.ndarray): Cardinality (number of modalities) for each variable, shape (d,). |
| """ |
| X = np.asarray(X, dtype=int) |
| combs, counts = np.unique(X, axis=0, return_counts=True) |
| P = counts.astype(float) / counts.sum() |
| N = combs.max(axis=0) + 1 |
| return combs, P, N |
|
|
|
|
| |
| |
| |
|
|
| def _next_set(d: int, current_A: List[int]) -> Optional[List[int]]: |
| """ |
| Generates the next subset A (1-based indices) in lexicographical order |
| by increasing size. |
| |
| Args: |
| d (int): Total number of dimensions/features. |
| current_A (List[int]): The current subset of indices (1-based). |
| |
| Returns: |
| List[int] or None: The next subset, or None if all subsets are exhausted. |
| """ |
| size = len(current_A) |
|
|
| |
| if size == 0: |
| return [1] if d >= 1 else None |
|
|
| |
| pivot = -1 |
| for i in range(size - 1, -1, -1): |
| if current_A[i] < d - (size - 1 - i): |
| pivot = i |
| break |
|
|
| if pivot != -1: |
| next_A = list(current_A) |
| next_A[pivot] += 1 |
| for j in range(pivot + 1, size): |
| next_A[j] = next_A[j - 1] + 1 |
| return next_A |
|
|
| |
| if size < d: |
| return list(range(1, size + 2)) |
| else: |
| return None |
|
|
|
|
| |
| |
| |
|
|
| def _get_y_from_N(N: np.ndarray, S: List[int]) -> np.ndarray: |
| """ |
| Generates the grid of coordinate vectors 'y' for a given subset S. |
| |
| For a subset S, generates matrix Y (m x d) where each row y satisfies: |
| - for j in S: y_j in {0, ..., N_j - 2} |
| - for j not in S: y_j = 0 |
| |
| Where m = product_{i in S}(N_i - 1). |
| |
| Args: |
| N (np.ndarray): Number of modalities per variable. |
| S (List[int]): List of 1-based variable indices. |
| |
| Returns: |
| np.ndarray: The grid of y vectors. |
| """ |
| N = np.asarray(N, dtype=int) |
| d = N.size |
|
|
| S = sorted(S) |
| S0 = [s - 1 for s in S] |
| k = len(S0) |
|
|
| if k == 0: |
| return np.zeros((1, d), dtype=int) |
|
|
| sizes = N[S0] - 1 |
| if np.any(sizes <= 0): |
| return np.zeros((0, d), dtype=int) |
|
|
| |
| grids = np.indices(sizes, dtype=int) |
| vals_S = grids.reshape(k, -1).T |
|
|
| Y = np.zeros((vals_S.shape[0], d), dtype=int) |
| Y[:, S0] = vals_S |
| return Y |
|
|
|
|
| |
| |
| |
|
|
| def _compute_P_A_for_patterns(combs: np.ndarray, P: np.ndarray, A0: List[int]) -> np.ndarray: |
| """ |
| Computes marginal probabilities P(X_A = x_A) for all patterns in a vectorized manner. |
| |
| Args: |
| combs (np.ndarray): Distinct patterns (r, d). |
| P (np.ndarray): Empirical probabilities (r,). |
| A0 (List[int]): List of 0-based indices for the subset A. |
| |
| Returns: |
| np.ndarray: A vector v of size r, where v[i] = P(X_A = combs[i, A]). |
| """ |
| sub = combs[:, A0] |
| _, inv = np.unique(sub, axis=0, return_inverse=True) |
| sums = np.bincount(inv, weights=P) |
| return sums[inv] |
|
|
|
|
| |
| |
| |
|
|
| def _psi_from_precomputed(xA: np.ndarray, |
| yA: np.ndarray, |
| Ni_minus1: np.ndarray, |
| sign: np.ndarray, |
| P_A_vec: np.ndarray) -> np.ndarray: |
| """ |
| Computes the basis vector e_{S,y}(x) using pre-calculated components. |
| |
| Args: |
| xA (np.ndarray): Values of X restricted to S, shape (r, |S|). |
| yA (np.ndarray): Current y vector restricted to S, shape (|S|,). |
| Ni_minus1 (np.ndarray): N[A0] - 1 values, shape (|S|,). |
| sign (np.ndarray): Precomputed sign term (-1)^{1{x_i = N_i-1}}, shape (r,). |
| P_A_vec (np.ndarray): Marginal probabilities P(X_S = x_S), shape (r,). |
| |
| Returns: |
| np.ndarray: The evaluated basis vector of size r. |
| """ |
| |
| mask_in = (xA == yA) | (xA == Ni_minus1) |
| valid = np.all(mask_in, axis=1) |
|
|
| vec = np.zeros(xA.shape[0], dtype=float) |
| if np.any(valid): |
| vec[valid] = sign[valid] / P_A_vec[valid] |
| return vec |
|
|
|
|
| |
| |
| |
|
|
| def _update_basis(Q: Optional[np.ndarray], |
| v: np.ndarray, |
| rtol: float = 1e-3, |
| atol: float = 1e-3) -> Tuple[Optional[np.ndarray], bool]: |
| """ |
| Incrementally updates the orthonormal basis Q with vector v using Gram-Schmidt. |
| |
| Args: |
| Q (np.ndarray or None): Current orthonormal basis matrix. |
| v (np.ndarray): New candidate vector. |
| rtol (float): Relative tolerance for linear independence check. |
| atol (float): Absolute tolerance for linear independence check. |
| |
| Returns: |
| Tuple[np.ndarray, bool]: The updated basis Q (or original if dependent), |
| and a boolean indicating if v was linearly independent. |
| """ |
| v = np.asarray(v, dtype=float) |
| norm_v = np.linalg.norm(v) |
|
|
| if Q is None or Q.size == 0: |
| if norm_v < atol: |
| return Q, False |
| return (v / norm_v)[:, None], True |
|
|
| |
| coeffs = Q.T @ v |
| v_res = v - Q @ coeffs |
| norm_res = np.linalg.norm(v_res) |
|
|
| |
| tol = atol + rtol * norm_v |
| if norm_res < tol: |
| return Q, False |
|
|
| |
| v_res /= norm_res |
| Q_new = np.column_stack((Q, v_res)) |
| return Q_new, True |
|
|
|
|
| |
| |
| |
|
|
| def get_matrix_optimized(X: np.ndarray, |
| tol_percent: float = 100, |
| eps: float = 1e-3) -> Tuple[np.ndarray, List[List[int]]]: |
| """ |
| Constructs a matrix where columns are linearly independent basis functions |
| e_{S,y} evaluated on the distinct patterns of X. |
| |
| Optimized version using incremental QR updates and pre-computations. |
| |
| Args: |
| X (np.ndarray): Input dataset. |
| tol_percent (float): Percentage of the total rank (r) to target. |
| eps (float): Tolerance for linear independence checks. |
| |
| Returns: |
| matrix (np.ndarray): The constructed basis matrix (r, r_partial). |
| complete_powerset (List[List[int]]): List of subsets S corresponding to columns. |
| """ |
| |
| combs, P, N = _compute_patterns(X) |
| d = N.size |
| r = combs.shape[0] |
|
|
| if r == 0: |
| return np.zeros((0, 0)), [] |
|
|
| |
| target_rank = int(r * tol_percent / 100) |
| target_rank = max(1, min(target_rank, r)) |
|
|
| |
| first_col = np.ones(r, dtype=float) |
| matrix = first_col[:, None] |
| Q, _ = _update_basis(None, first_col, atol=eps) |
| actual_rank = Q.shape[1] |
|
|
| S = [] |
| complete_powerset = [[]] |
| cache_P_A = {} |
|
|
| |
| pbar = tqdm(total=target_rank, desc="Constructing Basis Matrix", colour="green") |
| pbar.update(actual_rank) |
|
|
| try: |
| while actual_rank < target_rank: |
| S = _next_set(d, S) |
| if S is None: |
| break |
|
|
| Y = _get_y_from_N(N, S) |
| if Y.shape[0] == 0: |
| continue |
|
|
| |
| A0 = tuple(a - 1 for a in S) |
|
|
| |
| if A0 not in cache_P_A: |
| cache_P_A[A0] = _compute_P_A_for_patterns(combs, P, list(A0)) |
| P_A_vec = cache_P_A[A0] |
|
|
| |
| A0_list = list(A0) |
| xA = combs[:, A0_list] |
| NA = N[A0_list] |
| Ni_minus1 = NA - 1 |
|
|
| |
| sign_i = np.where(xA == Ni_minus1, -1.0, 1.0) |
| sign = np.prod(sign_i, axis=1) |
|
|
| |
| for y in Y: |
| yA = y[A0_list] |
| vec = _psi_from_precomputed(xA, yA, Ni_minus1, sign, P_A_vec) |
|
|
| |
| old_rank = actual_rank |
| Q_new, independent = _update_basis(Q, vec, atol=eps) |
| |
| if not independent: |
| continue |
|
|
| |
| Q = Q_new |
| matrix = np.column_stack((matrix, vec)) |
| complete_powerset.append(list(S)) |
| actual_rank = Q.shape[1] |
|
|
| |
| delta_rank = actual_rank - old_rank |
| if delta_rank > 0: |
| pbar.update(min(delta_rank, target_rank - pbar.n)) |
|
|
| if actual_rank >= target_rank or matrix.shape[1] >= r: |
| break |
|
|
| if actual_rank >= target_rank or matrix.shape[1] >= r: |
| break |
| finally: |
| pbar.close() |
|
|
| return matrix, complete_powerset |
|
|
| |
| |
| |
|
|
| |
|
|
| def aggregate_duplicate_coalitions( |
| S: List[List[int]], |
| M: np.ndarray |
| ) -> Tuple[List[List[int]], np.ndarray]: |
| """ |
| Aggregates columns of matrix M corresponding to identical sets in S. |
| |
| This function identifies duplicate sets in the list S. For every unique set found, |
| it sums the corresponding columns in M. The order of unique sets in the output |
| preserves their first appearance in the input S. |
| |
| Parameters |
| ---------- |
| S : List[List[int]] |
| A list of sets (represented as lists), potentially containing duplicates. |
| Length = k. |
| M : np.ndarray |
| Input matrix of shape (n, k). Each column j corresponds to S[j]. |
| |
| Returns |
| ------- |
| reduced_S : List[List[int]] |
| List of unique sets, length r. |
| reduced_M : np.ndarray |
| Aggregated matrix of shape (n, r). |
| """ |
| |
| |
| set_to_indices_map = {} |
| |
| for col_idx, subset in enumerate(S): |
| |
| |
| key = tuple(sorted(subset)) |
| |
| if key not in set_to_indices_map: |
| set_to_indices_map[key] = [] |
| set_to_indices_map[key].append(col_idx) |
| |
| |
| unique_sets = [] |
| aggregated_columns = [] |
| |
| |
| |
| for key_tuple, indices in set_to_indices_map.items(): |
| |
| unique_sets.append(list(key_tuple)) |
| |
| |
| if len(indices) == 1: |
| |
| col_sum = M[:, indices[0]] |
| else: |
| |
| col_sum = np.sum(M[:, indices], axis=1) |
| |
| aggregated_columns.append(col_sum) |
| |
| |
| reduced_M = np.column_stack(aggregated_columns) |
| |
| return unique_sets, reduced_M |
|
|
| |
|
|
| class ModelAnalysis: |
| """ |
| Main framework for analyzing model responses using orthogonal decomposition. |
| """ |
|
|
| def __init__(self, |
| X_encoded: np.ndarray, |
| f_model: callable, |
| percentage_set: float, |
| a_tol: float, |
| eps_reg_gamma: float = 1e-10): |
| """ |
| Initializes the analysis and performs all computations. |
| |
| Args: |
| X_encoded (np.ndarray): Integer-encoded input dataset. |
| f_model (callable): The model function to analyze (takes X_encoded as input). |
| percentage_set (float): Percentage of rank to preserve in basis construction. |
| a_tol (float): Tolerance for linear independence checks. |
| eps_reg_gamma (float, optional): Regularization term for Gamma matrix inversion. |
| """ |
| |
| |
| self._G_matrix = get_matrix_optimized(X_encoded, percentage_set, a_tol) |
| |
| patterns = _compute_patterns(X_encoded) |
| self._X_uniq = patterns[0] |
| self._P = patterns[1] |
| |
| self._M = self._G_matrix[0] |
| self._S = self._G_matrix[1] |
| |
| |
| self._Y = f_model(self._X_uniq) |
| self._var = self._Y**2 @ self._P - (self._Y @ self._P)**2 |
|
|
| |
| self._Gamma = self._M.T @ np.diag(self._P) @ self._M |
| self._Gamma[np.diag_indices_from(self._Gamma)] += eps_reg_gamma |
|
|
| |
| self._mu = (self._M.T * self._P) @ self._Y |
| |
| |
| c, low = cho_factor(self._Gamma, lower=True) |
| self._lamb = cho_solve((c, low), self._mu) |
|
|
| |
| self._Obliq_matrix = self._M * self._lamb |
| self._functional_decomposition = aggregate_duplicate_coalitions( self._S , self._Obliq_matrix ) |
|
|
| |
|
|
| self._Err_L2 = (np.sum(self._Obliq_matrix, axis=1) - self._Y)**2 @ self._P |
| |
| |
| norm_Y = self._Y**2 @ self._P |
| self._Err_L2_rel = (self._Err_L2 / norm_Y) if norm_Y > 1e-12 else 0.0 |
| |
| self._R_2 = 1 - (self._Err_L2) / (self._var) if self._var > 1e-12 else 0.0 |
| |
| print("Computations complete. Results ready.") |
|
|
| |
| |
| |
|
|
| |
| def functional_anova(self): |
| """Returns Sets and f_A(X_A)""" |
| return self._functional_decomposition |
| |
| |
|
|
| def get_M(self) -> np.ndarray: |
| """Returns the basis matrix M.""" |
| return self._M |
| |
| def get_S(self) -> List[List[int]]: |
| """Returns the list of subsets corresponding to M's columns.""" |
| return self._S |
|
|
| def get_P(self) -> np.ndarray: |
| """Returns the empirical probability vector.""" |
| return self._P |
|
|
| def get_X_uniq(self) -> np.ndarray: |
| """Returns the unique patterns found in X.""" |
| return self._X_uniq |
|
|
| def get_Y(self) -> np.ndarray: |
| """Returns the model response vector f_model(X_uniq).""" |
| return self._Y |
|
|
| def get_Gamma(self) -> np.ndarray: |
| """Returns the regularized Gamma matrix.""" |
| return self._Gamma |
|
|
| def get_mu(self) -> np.ndarray: |
| """Returns the mean vector (mu).""" |
| return self._mu |
|
|
| def get_lambda(self) -> np.ndarray: |
| """Returns the coefficient vector lambda (system solution).""" |
| return self._lamb |
|
|
| def get_var(self) -> float: |
| """Returns the calculated variance of Y.""" |
| return self._var |
|
|
| def get_Obliq_matrix(self) -> np.ndarray: |
| """Returns the final oblique matrix (M * lambda).""" |
| return self._Obliq_matrix |
|
|
| def get_L2_Error(self) -> float: |
| """Returns the weighted Mean Squared Error (L2).""" |
| return self._Err_L2 |
| |
| def get_L2_Error_rel(self) -> float: |
| """Returns the relative L2 error.""" |
| return self._Err_L2_rel |
|
|
| def get_R2(self) -> float: |
| """Returns the coefficient of determination R².""" |
| return self._R_2 |
| |
| |
| |
| |
|
|
| def batch_shapley_values( |
| n_players: int, |
| coalitions: List[List[int]], |
| dividends_matrix: np.ndarray |
| ) -> np.ndarray: |
| """ |
| Vectorized computation of Shapley values from a batch of Harsanyi dividends. |
| |
| This function linearly transforms Harsanyi dividends into Shapley values |
| using a pre-computed weight matrix. This allows for processing multiple |
| game instances (samples) simultaneously. |
| |
| The relationship relies on the axiom that the Shapley value of a player i |
| is the sum of dividends of all coalitions T containing i, divided by the |
| size of T: |
| phi_i = sum_{T : i in T} ( dividend(T) / |T| ) |
| |
| Parameters |
| ---------- |
| n_players : int |
| The total dimension of the game (number of players), denoted as d. |
| coalitions : List[List[int]] |
| A list of length k representing the coalitions (subsets) associated |
| with the columns of the dividends matrix. |
| Note: Indices are expected to be 1-based (e.g., {1, 2, ...}). |
| dividends_matrix : np.ndarray |
| A matrix of shape (n_samples, n_coalitions) containing the Harsanyi |
| dividends. Each row p corresponds to a specific game instance v_p, |
| and columns correspond to the coalitions in S. |
| |
| Returns |
| ------- |
| np.ndarray |
| The matrix of Shapley values Phi of shape (n_samples, n_players). |
| Phi[p, :] corresponds to the Shapley vector for the game instance p. |
| |
| Raises |
| ------ |
| ValueError |
| If the number of coalitions in S does not match the number of columns |
| in dividends_matrix. |
| """ |
| |
| V = np.asarray(dividends_matrix, dtype=float) |
| n_samples, n_coalitions = V.shape |
|
|
| |
| if n_coalitions != len(coalitions): |
| raise ValueError( |
| f"Dimension mismatch: length of coalitions list ({len(coalitions)}) " |
| f"must match number of columns in dividends_matrix ({n_coalitions})." |
| ) |
|
|
| |
| |
| |
| A = np.zeros((n_coalitions, n_players), dtype=float) |
|
|
| for idx, indices_set in enumerate(coalitions): |
| cardinality = len(indices_set) |
| |
| if cardinality == 0: |
| continue |
| |
| share = 1.0 / cardinality |
| |
| |
| |
| idx_zero_based = [j - 1 for j in indices_set] |
| |
| |
| A[idx, idx_zero_based] = share |
|
|
| |
| |
| |
| shapley_values = V @ A |
| |
| return shapley_values |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| class FullSupportAnova: |
| def __init__(self, N, P, f): |
| """ |
| Initializes the FullSupportAnova class. |
| |
| Args: |
| N (array-like): List or array of dimensions (N1, ..., Nd). |
| P (array-like): 1D vector of probabilities (size prod(N)) |
| or nD array of shape N. |
| f (callable): Function to apply on the generated tuples. |
| """ |
| self.N = np.asarray(N, dtype=int) |
| self.f = f |
| self.d = self.N.size |
| |
| |
| P = np.asarray(P, dtype=float) |
| expected_size = int(np.prod(self.N)) |
| if P.size != expected_size: |
| raise ValueError(f"Size of P ({P.size}) != prod(N) ({expected_size})") |
| |
| |
| self.P = P |
| self.P_nd = P.reshape(tuple(self.N), order="C") |
|
|
| |
| |
| |
|
|
| def _generate_subsets(self): |
| """ |
| Generates subsets of {1,...,d} (1-based indices). |
| """ |
| subsets = [[]] |
| for k in range(1, self.d + 1): |
| for comb in combinations(range(1, self.d + 1), k): |
| subsets.append(list(comb)) |
| return subsets |
|
|
| def _repeated_subsets(self): |
| """ |
| Returns a list of subsets S ⊆ {1,...,d} (in 1-based indices), |
| in itertools order, where each S is repeated |
| ∏_{i∈S} (N_i - 1) |
| times, consecutively. |
| """ |
| subsets = self._generate_subsets() |
| N_list = list(self.N) |
| |
| result = [] |
| for S in subsets: |
| |
| if len(S) == 0: |
| reps = 1 |
| else: |
| reps = prod(N_list[i - 1] - 1 for i in S) |
| |
| if reps > 0: |
| result.extend([S] * reps) |
| return result |
|
|
| def _generate_tuples(self): |
| """ |
| Generates all d-tuples (x1,...,xd) in canonical order (C-order). |
| Returns a matrix of shape (prod(N), d). |
| """ |
| grids = np.indices(self.N) |
| return grids.reshape(self.d, -1).T |
|
|
| def _generate_y(self, S): |
| """ |
| Generates y vectors for a given subset S. |
| Returns Y of shape (∏_{i∈S} (Ni-1), d). |
| """ |
| S = sorted(S) |
| S0 = [s - 1 for s in S] |
| k = len(S0) |
|
|
| if k == 0: |
| return np.zeros((1, self.d), dtype=int) |
|
|
| sizes = self.N[S0] - 1 |
| if np.any(sizes <= 0): |
| return np.zeros((0, self.d), dtype=int) |
|
|
| grids = np.indices(sizes) |
| vals_S = grids.reshape(k, -1).T |
|
|
| Y = np.zeros((vals_S.shape[0], self.d), dtype=int) |
| Y[:, S0] = vals_S |
| return Y |
|
|
| |
| |
| |
|
|
| def _compute_denominators(self, combs, S): |
| """ |
| Calculates the denominator vector 'den' of size n = prod(N) such that |
| den[j] = P(X_S = x_S) where x = combs[j]. |
| """ |
| S0 = [s - 1 for s in S] |
|
|
| if len(S) == 0: |
| |
| n = combs.shape[0] |
| return np.ones(n, dtype=float) |
|
|
| |
| axes_sum = tuple(ax for ax in range(self.d) if ax not in S0) |
| |
| |
| P_marg = self.P_nd.sum(axis=axes_sum) |
|
|
| |
| idx = tuple(combs[:, S0].T) |
| den = P_marg[idx] |
| |
| return den.astype(float) |
|
|
| def _compute_numerators(self, combs, S, Y_S): |
| """ |
| Calculates the numerator matrix 'num' of shape (n, m). |
| num[j, k] corresponds to the orthogonal interaction term. |
| """ |
| n = combs.shape[0] |
| m = Y_S.shape[0] |
|
|
| if len(S) == 0: |
| return np.ones((n, m), dtype=int) |
|
|
| S0 = np.array(S, dtype=int) - 1 |
| num = np.ones((n, m), dtype=int) |
|
|
| |
| for ax in S0: |
| Xi = combs[:, ax][:, None] |
| Yi = Y_S[:, ax][None, :] |
| Ni_minus1 = self.N[ax] - 1 |
|
|
| |
| mask_in = (Xi == Yi) | (Xi == Ni_minus1) |
| sign_i = np.where(Xi == Ni_minus1, -1, 1) |
|
|
| factor = sign_i * mask_in |
| num *= factor |
|
|
| return num |
|
|
| |
| |
| |
|
|
| def _build_base_matrix(self): |
| """ |
| Main function constructing the base matrix. |
| |
| Returns: |
| The base matrix (n, columns). |
| """ |
| |
| combs = self._generate_tuples() |
| subsets = self._generate_subsets() |
| n = combs.shape[0] |
|
|
| |
| cols = [np.ones((n, 1), dtype=float)] |
|
|
| |
| for S in subsets[1:]: |
| Y_S = self._generate_y(S) |
| m = Y_S.shape[0] |
| |
| if m == 0: |
| continue |
|
|
| den = self._compute_denominators(combs, S) |
| num = self._compute_numerators(combs, S, Y_S) |
|
|
| |
| |
| with np.errstate(divide='ignore', invalid='ignore'): |
| psi_S = num / den[:, None] |
| psi_S = np.nan_to_num(psi_S) |
|
|
| cols.append(psi_S) |
|
|
| return np.concatenate(cols, axis=1) |
|
|
| def get_obliq_matrix(self): |
| """ |
| Computes the oblique matrix by solving the linear system against f(X). |
| """ |
| X_numpy = self._generate_tuples() |
| base_matrix = self._build_base_matrix() |
| |
| |
| y = self.f(X_numpy) |
| |
| |
| coeff = np.linalg.solve(base_matrix, y) |
| |
| obliq_matrix = base_matrix * coeff |
| return obliq_matrix |
|
|
| def get_anova_full(self): |
| """ |
| Computes the full support ANOVA decomposition. |
| Requires 'aggregate_duplicate_coalitions' to be defined externally. |
| """ |
| matrix = self.get_obliq_matrix() |
| sets = self._repeated_subsets() |
| |
| |
| anova_indep = aggregate_duplicate_coalitions(sets, matrix) |
| return anova_indep |