openpls-engine 0.7.0a2__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.
plspm/util.py ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/python3
2
+ #
3
+ # Copyright (C) 2019 Google Inc.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import collections as c
19
+ import math
20
+
21
+ import numpy as np
22
+ import pandas as pd
23
+
24
+
25
+ def treat(data: pd.DataFrame, center: bool = True, scale: bool = True, scale_values=None) -> pd.DataFrame:
26
+ """Internal function that treats data in Pandas Dataframe format.
27
+
28
+ Args:
29
+ data: The data to treat
30
+ center: Whether to center the data
31
+ scale: Whether to scale the data
32
+ scale_values: The scaling to use
33
+
34
+ Returns:
35
+ The treated data
36
+ """
37
+ if center:
38
+ data = data.subtract(data.mean())
39
+ if scale:
40
+ if scale_values:
41
+ data = data.divide(scale_values)
42
+ else:
43
+ data = data.divide(data.std())
44
+ return data
45
+
46
+
47
+ def treat_numpy(data: np.ndarray) -> np.ndarray:
48
+ """Internal function that centers and scales data in Numpy format.
49
+
50
+ Args:
51
+ data: The data to treat
52
+
53
+ Returns:
54
+ The treated data
55
+ """
56
+ data = data - np.nanmean(data)
57
+ return data / np.nanstd(data, axis=0, ddof=1)
58
+
59
+
60
+ def sort_cols(data: pd.DataFrame) -> pd.DataFrame:
61
+ """Internal convenience function to sort data by column."""
62
+ return data.reindex(sorted(data.columns), axis=1)
63
+
64
+
65
+ def impute(data: pd.DataFrame) -> pd.DataFrame:
66
+ """Internal function that imputes missing data using the mean value (only suitable for metric data)."""
67
+ imputed = pd.DataFrame(0, data.index, data.columns)
68
+ for column in list(data):
69
+ average = data[column].mean(skipna=True)
70
+ imputed[column] = data[column].fillna(average)
71
+ assert math.isclose(imputed[column].mean(), average, rel_tol=1e-09, abs_tol=0.0)
72
+ return imputed
73
+
74
+
75
+ def list_to_dummy(data: dict) -> pd.DataFrame:
76
+ """Internal function used to create the outer design matrix."""
77
+ matrix = pd.DataFrame()
78
+ for col in data:
79
+ dummy = pd.DataFrame(1, index=data[col], columns=[col])
80
+ matrix = pd.concat([matrix, dummy], axis=1, sort=False)
81
+ return matrix.fillna(0)
82
+
83
+
84
+ def rank(data: pd.Series) -> pd.Series:
85
+ """Internal function used to rank ordinal and nominal data."""
86
+ unique = pd.Series(data.unique())
87
+ ranked = unique.rank()
88
+ lookup = pd.concat([unique, ranked], axis=1)
89
+ lookup_series = pd.Series(lookup.iloc[:, 1].values, index=lookup.iloc[:, 0])
90
+ return data.replace(lookup_series.to_dict()).astype(float)
91
+
92
+
93
+ def dummy(data: pd.Series) -> pd.DataFrame:
94
+ """Internal function used to create a dummy matrix used to perform calculations with ordinal and nominal data"""
95
+ unique = data.unique().size
96
+ dummy = pd.DataFrame(0, data.index, range(1, unique + 1))
97
+ for i in range(unique):
98
+ dummy.loc[data[data == i + 1].index, i + 1] = 1
99
+ return dummy
100
+
101
+
102
+ def groupby_mean(data: np.ndarray) -> np.ndarray:
103
+ """Internal function which performs the Numpy equivalent of Pandas ``.groupby(...).mean()``"""
104
+ values = {}
105
+ reduced = 0
106
+ for i in range(data.shape[1]):
107
+ index = data[0, i]
108
+ if index not in values:
109
+ values[index] = []
110
+ reduced += 1
111
+ values[index].append(data[1, i])
112
+ means = np.zeros((2, reduced), dtype=np.float64)
113
+ for i, index in enumerate(sorted(values.keys())):
114
+ means[0, i] = index
115
+ means[1, i] = np.mean(values[index])
116
+ return means
117
+
118
+
119
+ class Value:
120
+ """Internal class which models a value type"""
121
+ def __init__(self, val):
122
+ self.__value = val
123
+
124
+ def __eq__(self, other):
125
+ return self.__value == other.__value
126
+
127
+ def __ne__(self, other):
128
+ return self.__value != other.__value
129
+
130
+
131
+ class TopoSort:
132
+ """Internal function which performs a topological sort using Kahn's algorithm"""
133
+
134
+ def __init__(self):
135
+ self.__indegree = c.Counter()
136
+ self.__children = {}
137
+ self.__edges = []
138
+
139
+ def append(self, src: str, dest: str):
140
+ self.__edges.append((src, dest))
141
+ self.__indegree[dest] += 1
142
+ self.__indegree[src] += 0
143
+ for vertex in [src, dest]:
144
+ if vertex not in self.__children:
145
+ self.__children[vertex] = []
146
+ self.__children[src].append(dest)
147
+
148
+ def order(self):
149
+ ordered = []
150
+ orphaned = c.deque([v for v in self.__indegree if self.__indegree[v] == 0])
151
+ while orphaned:
152
+ vertex = orphaned.pop()
153
+ ordered.append(vertex)
154
+ for child in self.__children[vertex]:
155
+ self.__indegree[child] -= 1
156
+ if self.__indegree[child] == 0:
157
+ orphaned.append(child)
158
+ for v in self.__indegree:
159
+ if self.__indegree[v] != 0:
160
+ raise ValueError("Structural graph contains cycles.")
161
+ return ordered
162
+
163
+ def elements(self):
164
+ return self.__edges
plspm/weights.py ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/python3
2
+ #
3
+ # Copyright (C) 2019 Google Inc.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import math
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+ import statsmodels.api as sm
23
+
24
+ import plspm.config as c
25
+ import plspm.util as util
26
+ from plspm.mode import Mode
27
+ from plspm.scheme import Scheme
28
+
29
+ pd.options.mode.chained_assignment = None # default='warn'
30
+
31
+
32
+ class _MetricWeights:
33
+ """Internal class that calculates weights and scores when using metric data."""
34
+ def __init__(self, data: pd.DataFrame, config: c.Config, correction: float, path: pd.DataFrame):
35
+ odm = config.odm(path)
36
+ weight_factors = correction / data.dot(odm).std(axis=0)
37
+ self.__mvs = list(odm.index)
38
+ wf_diag = pd.DataFrame(np.diag(weight_factors), index=weight_factors.index, columns=weight_factors.index)
39
+ weights = odm.dot(wf_diag)
40
+ self.__weights_old = weights.sum(axis=1).to_frame(name="weight")
41
+ self.__data = data
42
+ self.__config = config
43
+ self.__weights = weights
44
+ self.__correction = correction
45
+ self.__path = path
46
+
47
+ def iterate(self, inner_weight_calculator: Scheme) -> float:
48
+ lvs = list(self.__path)
49
+ scores = self.__data.dot(self.__weights).reindex(lvs, axis=1)
50
+ scores = util.treat(scores) / self.__correction
51
+ inner_weights = pd.DataFrame(inner_weight_calculator.value.calculate(self.__path, scores.values), index=lvs, columns=lvs)
52
+ Z = scores.dot(inner_weights)
53
+ for lv in list(lvs):
54
+ mvs = self.__config.mvs(lv)
55
+ weights = self.__config.mode(lv).value.outer_weights_metric(self.__data, Z, lv, mvs)
56
+ self.__weights.loc[mvs, [lv]] = weights
57
+ weights_new = self.__weights.sum(axis=1).to_frame(name="weight")
58
+ convergence = np.power(self.__weights_old.abs() - weights_new.abs(), 2).sum(axis=1).sum(axis=0)
59
+ self.__weights_old = weights_new
60
+ return convergence
61
+
62
+ def calculate(self) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
63
+ weight_factors = 1 / (self.__data.dot(self.__weights).std(axis=0) / self.__correction)
64
+ wf_diag = pd.DataFrame(np.diag(weight_factors), index=weight_factors.index, columns=weight_factors.index)
65
+ weights = self.__weights.dot(wf_diag)
66
+ scores = self.__data.dot(weights)
67
+ cor = pd.concat([self.__data, scores], axis=1).corr().loc[list(self.__data), list(scores)]
68
+ odm = weights.apply(lambda x: x!= 0).astype(int)
69
+ sign = lambda x : math.copysign(1.0, x)
70
+ w_sign = (cor * odm).map(sign).sum(axis=0).apply(sign)
71
+ if -1 in w_sign.values:
72
+ w_sign = w_sign.apply(lambda x : -1 if x == 0 else x)
73
+ w_sign_matrix = pd.DataFrame(np.diag(w_sign), index=w_sign.index, columns=w_sign.index)
74
+ scores = scores.dot(w_sign_matrix)
75
+ weights = pd.DataFrame(weights.sum(axis=1), index=self.__mvs, columns=["weight"])
76
+ return self.__data, scores, weights
77
+
78
+
79
+ class _NonmetricWeights:
80
+ """Internal class that calculates weights and scores when using nonmetric data."""
81
+ def __init__(self, data: pd.DataFrame, config: c.Config, correction: float, path: pd.DataFrame):
82
+ self.__mv_grouped_by_lv_initial = {}
83
+ self.__mvs = []
84
+ mv_grouped_by_lv = {}
85
+ self.__mv_grouped_by_lv_missing = {}
86
+ lvs = list(path)
87
+ scores = np.zeros((len(data.index), len(lvs)), dtype=np.float64)
88
+ for i, lv in enumerate(lvs):
89
+ mvs = config.mvs(lv)
90
+ self.__mvs.extend(mvs)
91
+ mv_grouped_by_lv[lv] = data.filter(config.mvs(lv)).values.astype(np.float64)
92
+ self.__mv_grouped_by_lv_initial[lv] = mv_grouped_by_lv[lv].copy()
93
+ sizes = mv_grouped_by_lv[lv].shape[1]
94
+ weight = [1 / np.sqrt(sizes)] * sizes
95
+ if np.isnan(np.sum(mv_grouped_by_lv[lv])):
96
+ self.__mv_grouped_by_lv_missing[lv] = 1 - np.isnan(mv_grouped_by_lv[lv])
97
+ for j in range(len(data.index)):
98
+ numerator = np.nansum(mv_grouped_by_lv[lv][j, :] * weight)
99
+ denominator = np.power(weight * self.__mv_grouped_by_lv_missing[lv][j, :], 2).sum()
100
+ if denominator == 0:
101
+ raise ValueError("All mvs for lv " + lv + " in row " + str(j) + " are NaN.")
102
+ scores[j, i] = numerator / denominator
103
+ else:
104
+ scores[:, i] = np.dot(mv_grouped_by_lv[lv], weight)
105
+ self.__weights = {}
106
+ self.__scores = scores
107
+ self.__mv_grouped_by_lv = mv_grouped_by_lv
108
+ self.__config = config
109
+ self.__correction = correction
110
+ self.__index = data.index
111
+ self.__path = path
112
+
113
+ def iterate(self, inner_weight_calculator: Scheme) -> float:
114
+ self.__betas = {}
115
+ scores_old = self.__scores.copy()
116
+ inner_weights = inner_weight_calculator.value.calculate(self.__path, self.__scores)
117
+ Z = np.dot(self.__scores, inner_weights)
118
+ for i, lv in enumerate(list(self.__path)):
119
+ for j, mv in enumerate(list(self.__config.mvs(lv))):
120
+ self.__mv_grouped_by_lv[lv][:, j] = \
121
+ self.__config.scale(mv).value.scale(lv, mv, Z[:, i], self)
122
+ self.__weights[lv], self.__scores[:, i] = \
123
+ self.__config.mode(lv).value.outer_weights_nonmetric(self.__mv_grouped_by_lv,
124
+ self.__mv_grouped_by_lv_missing, Z[:, i], lv,
125
+ self.__correction)
126
+ return np.power(np.abs(scores_old) - np.abs(self.__scores), 2).sum()
127
+
128
+ def calculate(self) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
129
+ lvs = list(self.__path)
130
+ weights = pd.DataFrame(0.0, index=self.__mvs, columns=lvs)
131
+ data_new = pd.DataFrame(0.0, index=self.__index, columns=self.__mvs)
132
+ for lv in lvs:
133
+ mvs = self.__config.mvs(lv)
134
+ weights.loc[mvs, [lv]] = self.__weights[lv].reshape(self.__weights[lv].shape[0],1)
135
+ data_new.loc[:, mvs] = self.__mv_grouped_by_lv[lv]
136
+ weight_factors = 1 / (data_new.dot(weights).std(axis=0, skipna=True) / self.__correction)
137
+ wf_diag = pd.DataFrame(np.diag(weight_factors), index=lvs, columns=lvs)
138
+ weights = weights.dot(wf_diag).sum(axis=1).to_frame(name="weight")
139
+ return data_new, pd.DataFrame(self.__scores, index=self.__index, columns=lvs), weights
140
+
141
+ def get_Z_for_mode_b(self, lv, mv, z_by_lv):
142
+ mv_index = self.__config.mv_index(lv, mv)
143
+ if self.__config.mode(lv) != Mode.B or len(self.__config.mvs(lv)) == 1:
144
+ return z_by_lv
145
+ if lv not in self.__betas:
146
+ exogenous = sm.add_constant(self.__mv_grouped_by_lv[lv])
147
+ regression = sm.OLS(z_by_lv, exogenous).fit()
148
+ self.__betas[lv] = regression.params[1:]
149
+ correction = 1 / self.__betas[lv][mv_index]
150
+ return correction * (z_by_lv - np.delete(self.__mv_grouped_by_lv[lv], mv_index, axis=1).dot(
151
+ np.delete(self.__betas[lv], mv_index)))
152
+
153
+ def correction(self) -> float:
154
+ return self.__correction
155
+
156
+ def mv_grouped_by_lv(self, lv: str, mv: str):
157
+ return self.__mv_grouped_by_lv_initial[lv][:, self.__config.mv_index(lv, mv)]
158
+
159
+ def dummies(self, mv: str) -> pd.DataFrame:
160
+ return self.__config.dummies(mv)
161
+
162
+
163
+ class WeightsCalculatorFactory:
164
+ """Internal class that is used to calculate weights and scores from the data using the model."""
165
+ def __init__(self, config: c.Config, iterations: int, tolerance: float, correction: float, scheme: Scheme):
166
+ self.__iterations = iterations
167
+ self.__tolerance = tolerance
168
+ self.__config = config
169
+ self.__correction = correction
170
+ self.__scheme = scheme
171
+
172
+ def clone(self):
173
+ return WeightsCalculatorFactory(self.__config.clone(), self.__iterations, self.__tolerance, self.__correction, self.__scheme)
174
+
175
+ def config(self):
176
+ return self.__config
177
+
178
+ def calculate(self, data: pd.DataFrame, path: pd.DataFrame):
179
+ """Internal method that performs the calculation to estimate weights and scores."""
180
+ if self.__config.metric():
181
+ calculator = _MetricWeights(data, self.__config, self.__correction, path)
182
+ else:
183
+ calculator = _NonmetricWeights(data, self.__config, self.__correction, path)
184
+
185
+ iteration = 0
186
+ while True:
187
+ iteration += 1
188
+ convergence = calculator.iterate(self.__scheme)
189
+ if (convergence < self.__tolerance) or (iteration > self.__iterations):
190
+ break
191
+ if iteration > self.__iterations:
192
+ raise Exception("Could not converge after " + str(iteration) + " iterations")
193
+ return calculator.calculate()