turtles-glms 1.0.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.
turtles/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ Configure global settings for package.
3
+ """
4
+
5
+ from importlib.metadata import version, PackageNotFoundError
6
+
7
+ try:
8
+ __version__ = version("turtles-glms")
9
+ except PackageNotFoundError:
10
+ __version__ = "0.0.0"
11
+
12
+ # modules
13
+ __all__ = [
14
+ "preprocess",
15
+ "stats",
16
+ "plotting"
17
+ ]
turtles/_utils.py ADDED
@@ -0,0 +1,110 @@
1
+ """
2
+ General utility functions.
3
+ """
4
+
5
+ from typing import (
6
+ Dict,
7
+ Tuple,
8
+ Any,
9
+ Union
10
+ )
11
+
12
+ import numpy as np
13
+
14
+
15
+ def _shape_check(x: np.ndarray, var_name: str, dim: int = 1):
16
+ """
17
+ Check if numpy array has at least `dim` dimensions.
18
+
19
+ x : np.ndarray
20
+ Numpy matrix.
21
+ var_name : str
22
+ The variable name to return in the error message.
23
+ dim : int, default 2
24
+ The min number of dimensions in the array.
25
+
26
+ Raises
27
+ ------
28
+ ValueError
29
+ If the array has less than `dim` dimensions.
30
+
31
+ Returns
32
+ -------
33
+ None
34
+ """
35
+
36
+ if np.ndim(x) == 1 or x.shape[1] < dim:
37
+ raise ValueError(
38
+ f"'{var_name}' must contain more than {dim} dimensions."
39
+ )
40
+
41
+
42
+ def _validate_args(
43
+ map: Dict[str, Tuple[Any, Union[type, Tuple[type, ...]]]]
44
+ ):
45
+ """
46
+ Simple type validation of a set of input arguments.
47
+
48
+ Parameters
49
+ ----------
50
+ map : Dict[str, Tuple[Any, Union[type, Tuple[type, ...]]]]
51
+ A dictionary where keys are the argument names and the values are
52
+ tuples containing:
53
+ - the object to check
54
+ - the expected type or a tuple of types
55
+
56
+ Raises
57
+ ------
58
+ TypeError
59
+ If the type of an argument does not match the expected type.
60
+
61
+ Example
62
+ -------
63
+ _validate_args(
64
+ {
65
+ 'X': (X, np.array),
66
+ 'Y': (Y, (int, float))
67
+ }
68
+ )
69
+ """
70
+ for key, (obj, expected_type) in map.items():
71
+ if isinstance(expected_type, tuple):
72
+ if not any(isinstance(obj, t) for t in expected_type):
73
+ raise TypeError(
74
+ f"Parameter '{key}' must be one of the types {expected_type}; "
75
+ f"received {type(obj)}"
76
+ )
77
+ else:
78
+ if not isinstance(obj, expected_type):
79
+ raise TypeError(
80
+ f"Parameter '{key}' must be of type {expected_type}; "
81
+ f"received {type(obj)}"
82
+ )
83
+
84
+
85
+ def _add_intercept(X: np.ndarray) -> np.ndarray:
86
+ """
87
+ Add intercept to the design matrix. The intercept is simply
88
+ a column of ones.
89
+
90
+ Parameters
91
+ ----------
92
+ X : np.ndarray, shape (M, N)
93
+ Design matrix with M rows (samples) and N columns (features).
94
+
95
+ Returns
96
+ -------
97
+ np.ndarray, shape (M, N+1)
98
+ Design matrix with the intercept as the first column.
99
+ """
100
+
101
+ _validate_args(
102
+ {
103
+ "X": (X, np.ndarray)
104
+ }
105
+ )
106
+
107
+ m = X.shape[0]
108
+ intercept = np.ones(m).reshape(m, 1)
109
+ X = np.concatenate((intercept, X), axis=1)
110
+ return X
@@ -0,0 +1,14 @@
1
+ """
2
+ Plotting module.
3
+
4
+ Contains various plotting functions.
5
+ """
6
+
7
+ from ._plotting import (
8
+ plot_y_vs_x
9
+ )
10
+
11
+
12
+ __all__ = [
13
+ "plot_y_vs_x"
14
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ Plotting functions.
3
+ """
4
+
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+
8
+ from .._utils import _validate_args
9
+
10
+
11
+ def plot_y_vs_x(
12
+ x: np.ndarray,
13
+ y: np.ndarray,
14
+ title: str = "Dependent vs. Independent",
15
+ xlabel: str = "Independent",
16
+ ylabel: str = "Dependent"
17
+ ):
18
+ """
19
+ Scatter plot of a variable (y) against another variable (x).
20
+
21
+ Input arrays must be the same shape.
22
+
23
+ Parameters
24
+ ----------
25
+ x : np.ndarray
26
+ Input array for x-axis.
27
+ y : np.ndarray
28
+ Input aray for y-axis.
29
+ title : str
30
+ Plot title. Defaults to 'Dependent vs. Independent'.
31
+ xlabel : str
32
+ Plot label for x-axis. Defaults to 'Independent'.
33
+ ylabel : str
34
+ Plot label for y axis. Defaults to 'Dependent'.
35
+
36
+ Returns
37
+ -------
38
+ None
39
+ Displays scatter plot.
40
+ """
41
+
42
+ _validate_args(
43
+ {
44
+ "x": (x, np.ndarray),
45
+ "y": (y, np.ndarray),
46
+ "title": (title, str),
47
+ "xlabel": (xlabel, str),
48
+ "ylabel": (ylabel, str)
49
+ }
50
+ )
51
+
52
+ plt.scatter(
53
+ x=x,
54
+ y=y,
55
+ color="blue",
56
+ edgecolor="black"
57
+ )
58
+ plt.title(title)
59
+ plt.xlabel(xlabel)
60
+ plt.ylabel(ylabel)
61
+ plt.show()
@@ -0,0 +1,17 @@
1
+ """
2
+ Preprocess module.
3
+
4
+ Functions support common data transformation tasks to prep datasets
5
+ for GLMs.
6
+ """
7
+
8
+ from ._transform import (
9
+ one_hot_encode
10
+ )
11
+ from .._utils import _add_intercept as add_intercept
12
+
13
+
14
+ __all__ = [
15
+ "one_hot_encode",
16
+ "add_intercept"
17
+ ]
@@ -0,0 +1,81 @@
1
+ """
2
+ Functions to support preprocessing/transformations.
3
+ """
4
+
5
+ from typing import List, Tuple, Union
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from .._utils import (
11
+ _validate_args,
12
+ )
13
+
14
+
15
+ def one_hot_encode(
16
+ df: pd.DataFrame,
17
+ columns: List[str],
18
+ drop_first: bool = True,
19
+ return_df: bool = True
20
+ ) -> Union[pd.DataFrame, Tuple[np.ndarray, List[str]]]:
21
+ """
22
+ One Hot Encode categorical variables in a Pandas DataFrame, also known
23
+ as dummy variables or indicator variables.
24
+
25
+ This is essentially just a wrapper around the Pandas `get_dummies()`
26
+ function, but it optioanlly returns a Numpy matrix, making the resulting
27
+ object immediately compatible with the `turtles` GLM classes.
28
+
29
+ https://pandas.pydata.org/docs/reference/api/pandas.get_dummies.html
30
+
31
+ Parameters
32
+ ----------
33
+ df : pd.DataFrame, shape (M, N)
34
+ Pandas DataFrame containing data to be encoded.
35
+ columns : List[str]
36
+ List of column names in `df` to be encoded. Column values must be
37
+ strings or categorical.
38
+ drop_first : bool, default True
39
+ Whether to drop the first category from the resulting table. Dropping
40
+ the first category is useful when building a GLM, because the dropped
41
+ category will then be treated as the 'base case' or 'reference category'
42
+ for model coefficient interpretation.
43
+ return_df : bool, default True
44
+ Optionally return a pandas DataFrame. If False, a Tuple of a numpy
45
+ matrix and resulting column names is returned.
46
+
47
+ Returns
48
+ -------
49
+ Union[pd.DataFrame, Tuple[np.ndarray, List[str]]]
50
+
51
+ If `return_df` = True, a pandas DataFrame is returned, containing the
52
+ encoded columns.
53
+
54
+ If `return_df` = False, a Tuple is returned, where the new matrix is
55
+ in position [0] and the list of new column names in position [1].
56
+ """
57
+
58
+ _validate_args(
59
+ {
60
+ "df": (df, pd.DataFrame),
61
+ "columns": (columns, list),
62
+ "drop_first": (drop_first, bool),
63
+ "return_df": (return_df, bool)
64
+ }
65
+ )
66
+
67
+ temp = df.copy()
68
+
69
+ one_hot_encoded_df = pd.get_dummies(
70
+ temp,
71
+ columns=columns,
72
+ drop_first=drop_first,
73
+ dtype=int
74
+ )
75
+
76
+ if return_df:
77
+ return one_hot_encoded_df
78
+
79
+ column_names = list(one_hot_encoded_df.columns)
80
+
81
+ return one_hot_encoded_df.to_numpy(), column_names
@@ -0,0 +1,23 @@
1
+ """
2
+ Stats module.
3
+
4
+ These functions support various statistical tasks.
5
+ """
6
+
7
+ from ._stats import (
8
+ calculate_errors,
9
+ variance_inflation_factor,
10
+ pearson_corr,
11
+ covariance_matrix,
12
+ pca
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ "glms", # submodule
18
+ "calculate_errors",
19
+ "variance_inflation_factor",
20
+ "pearson_corr",
21
+ "covariance_matrix",
22
+ "pca"
23
+ ]
@@ -0,0 +1,276 @@
1
+ """
2
+ Various statistical functions.
3
+ """
4
+
5
+ from typing import Optional, Tuple, Dict, List
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from scipy.stats import pearsonr
10
+ from scipy.sparse.linalg import eigs
11
+
12
+ from .._utils import (
13
+ _validate_args,
14
+ _add_intercept,
15
+ _shape_check
16
+ )
17
+
18
+
19
+ def calculate_errors(
20
+ y_true: np.ndarray,
21
+ y_pred: np.ndarray,
22
+ m: Optional[int] = None
23
+ ) -> Dict[str, float]:
24
+ """
25
+ Calculate errors for arrays containing continuous values. Not suitable
26
+ for classification cases.
27
+
28
+ 1. Sum of Squared Errors (or Residual Sum of Squares)
29
+ 2. Mean Squared Error
30
+ 3. Root Mean Squared Error
31
+
32
+ Parameters
33
+ ----------
34
+ y_true : np.ndarray
35
+ True target array. Must be the same shape as y_pred and have 1 dimension
36
+ or be flattened.
37
+ y_pred : np.ndarray
38
+ Predicted values target array. Must be the same shape as y_true and have
39
+ 1 dimension or be flattened.
40
+ m : int
41
+ Number of observations. If using this function to calculate model fit
42
+ statistics, 'm' should be the degrees of freedom.
43
+
44
+ Returns
45
+ -------
46
+ Dict[str, float]
47
+ Dictionary where {key:value} is {"error name": value}.
48
+ """
49
+
50
+ m = m or max(y_true.shape)
51
+
52
+ _validate_args(
53
+ {
54
+ "y_true": (y_true, np.ndarray),
55
+ "y_pred": (y_pred, np.ndarray),
56
+ "m": (m, int)
57
+ }
58
+ )
59
+
60
+ errors = y_true - y_pred
61
+ sse = np.sum(errors ** 2).item()
62
+ mse = sse / m
63
+ rmse = np.sqrt(mse).item()
64
+
65
+ response = {
66
+ "Sum of Squared Errors": sse,
67
+ "Mean Squared Error": mse,
68
+ "Root Mean Squared Error": rmse
69
+ }
70
+
71
+ return response
72
+
73
+
74
+ def variance_inflation_factor(
75
+ X: np.ndarray,
76
+ var_names: Optional[List[str]] = None
77
+ ) -> pd.DataFrame:
78
+ """
79
+ Calculate Variance Inflation Factor (VIF) for all predictors in the
80
+ input matrix.
81
+
82
+ Fits one linear model for each dimension, where that single dimension
83
+ is the dependent variable, and all other dimensions (including an intercept)
84
+ are the independent variables.
85
+
86
+ Parameters
87
+ ----------
88
+ X : np.ndarray, shape (m, n)
89
+ Design matrix. Should not include an intercept.
90
+ var_names : Optional[List[str]]
91
+ Variable names. If not passed, defaults to `x_i` where `i` is the order
92
+ of the dimension in the input matrix. Names must appear in the order
93
+ that they do in the input matrix.
94
+
95
+ Returns
96
+ -------
97
+ pd.DataFrame
98
+ Pandas DataFrame containing the Coefficient (str), R-squared (float),
99
+ VIF (float).
100
+ """
101
+
102
+ _validate_args(
103
+ {
104
+ "X": (X, np.ndarray)
105
+ }
106
+ )
107
+ _shape_check(X, "X", 1)
108
+
109
+ vifs = {
110
+ "Coefficient": [],
111
+ "R-squared": [],
112
+ "VIF": []
113
+ }
114
+
115
+ for idx in range(X.shape[1]):
116
+ # create design matrix -- all except one predictor
117
+ exog = np.delete(X, idx, axis=1)
118
+ # target -- the one predictor
119
+ endog = X[:, idx]
120
+
121
+ # add intercept to design matrix
122
+ exog = _add_intercept(exog)
123
+
124
+ # fit model -- one predictor vs all others
125
+ coefs = np.linalg.inv(exog.T @ exog) @ exog.T @ endog
126
+
127
+ # calculate residuals
128
+ fitted_vals = exog @ coefs.T
129
+ residuals = endog - fitted_vals
130
+
131
+ # Redisual Sum of Squares and Sum of Squares Total
132
+ rss = np.sum(residuals ** 2)
133
+ sst = np.sum((endog - np.mean(endog))**2)
134
+
135
+ # R-squared
136
+ r2 = 1 - rss / sst
137
+ vifs["R-squared"].append(r2.item())
138
+
139
+ # VIF
140
+ vif = 1 / (1 - r2)
141
+ vifs["VIF"].append(vif.item())
142
+
143
+ vifs["Coefficient"].append(f"x{idx}")
144
+
145
+ if var_names is not None:
146
+ vifs["Coefficient"] = var_names
147
+
148
+ return pd.DataFrame(vifs)
149
+
150
+
151
+ def pearson_corr(
152
+ x: np.ndarray,
153
+ y: np.ndarray,
154
+ ) -> Tuple[float, float]:
155
+ """
156
+ Calculate the Pearson Correlation Coefficient.
157
+
158
+ This is basically a wrapper around the SciPy function.
159
+
160
+ https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.pearsonr.html
161
+
162
+ Parameters
163
+ ----------
164
+ x : np.ndarray
165
+ First input array. Usually a single independent variable.
166
+ y : np.ndarray
167
+ Second input array. A single independent or dependent variable.
168
+
169
+ Returns
170
+ -------
171
+ Tuple[float, float]
172
+ Tuple containing the correlation coefficient in position [0],
173
+ and the p-value in position [1].
174
+ """
175
+
176
+ _validate_args(
177
+ {
178
+ "x": (x, np.ndarray),
179
+ "y": (y, np.ndarray)
180
+ }
181
+ )
182
+
183
+ result = pearsonr(x=x.flatten(), y=y.flatten())
184
+ return (result.statistic.item(), result.pvalue.item())
185
+
186
+
187
+ def covariance_matrix(X: np.ndarray, return_xc=False) -> np.ndarray:
188
+ """
189
+ Calculate the covariance between variables in a matrix.
190
+
191
+ Parameters
192
+ ----------
193
+ X : np.ndarray
194
+ Design matrix of shape (M, N).
195
+ return_xc : bool, defaults to False
196
+ Optionally return the centered matrix.
197
+
198
+ Returns
199
+ -------
200
+ C : np.ndarray
201
+ Covariance matrix of size (N, N).
202
+ X_c : np.ndarray
203
+ Centered input matrix.
204
+ """
205
+
206
+ _validate_args(
207
+ {
208
+ "X": (X, np.ndarray),
209
+ "return_xc": (return_xc, bool)
210
+ }
211
+ )
212
+ _shape_check(X, "X", 2)
213
+
214
+ # estimate mean for each variable / column
215
+ m = X.shape[0]
216
+ X_t = X.T
217
+ avg = np.mean(X_t, axis=1)
218
+
219
+ # 'center' the matrix
220
+ X_c = X_t - avg[:, None]
221
+
222
+ # covariance matrix
223
+ C = (X_c @ X_c.T)/m
224
+
225
+ if return_xc:
226
+ return C, X_c
227
+
228
+ return C
229
+
230
+
231
+ def pca(X: np.ndarray, p: int) -> np.ndarray:
232
+ """
233
+ Principal Component Analysis.
234
+
235
+ Parameters
236
+ ----------
237
+ X : np.ndarray, shape (M, N)
238
+ Input data (design matrix) for calculating pricipal components.
239
+ p : int
240
+ Number of principle components to return. Must be less than or
241
+ equal to the number of dimensions in X.
242
+
243
+ Raises
244
+ ------
245
+ ValueError
246
+ If the requested number of principal components exceeds the number
247
+ of dimensions in the input matrix.
248
+
249
+ Returns
250
+ -------
251
+ np.ndarray, shape (M, p)
252
+ Matrix with p dimensions.
253
+ """
254
+
255
+ _validate_args(
256
+ {
257
+ "X": (X, np.ndarray),
258
+ "p": (p, int)
259
+ }
260
+ )
261
+ _shape_check(X, "X", 2)
262
+
263
+ if p > X.shape[1]:
264
+ raise ValueError("'p' must be <= 'X' dimensions.")
265
+
266
+ # get covariance matrix
267
+ C, X_c = covariance_matrix(X, return_xc=True)
268
+
269
+ # calculate eigenvalues, eigenvectors
270
+ s, w = eigs(C, p)
271
+ s = s.real
272
+ s = np.reshape(s, (p, 1))
273
+ w = w.real
274
+ pca = w.T @ X_c
275
+
276
+ return pca.T
@@ -0,0 +1,14 @@
1
+ """
2
+ GLMs submodule.
3
+ """
4
+
5
+ from ._mlr import MLR
6
+ from ._logreg import LogReg
7
+ from ._poisson import PoissonReg
8
+
9
+
10
+ __all__ = [
11
+ "MLR",
12
+ "LogReg",
13
+ "PoissonReg"
14
+ ]