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.
- openpls_engine-0.7.0a2.dist-info/METADATA +161 -0
- openpls_engine-0.7.0a2.dist-info/RECORD +24 -0
- openpls_engine-0.7.0a2.dist-info/WHEEL +5 -0
- openpls_engine-0.7.0a2.dist-info/licenses/LICENSE +674 -0
- openpls_engine-0.7.0a2.dist-info/top_level.txt +1 -0
- plspm/__init__.py +26 -0
- plspm/bootstrap.py +147 -0
- plspm/config.py +325 -0
- plspm/estimator.py +76 -0
- plspm/fit.py +112 -0
- plspm/htmt.py +119 -0
- plspm/inner_model.py +108 -0
- plspm/inner_summary.py +79 -0
- plspm/long_bootstrap.py +334 -0
- plspm/mga.py +222 -0
- plspm/mode.py +73 -0
- plspm/outer_model.py +43 -0
- plspm/plspm.py +246 -0
- plspm/q_squared.py +137 -0
- plspm/scale.py +108 -0
- plspm/scheme.py +68 -0
- plspm/unidimensionality.py +80 -0
- plspm/util.py +164 -0
- plspm/weights.py +193 -0
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()
|