ialdev-maths 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.
iad/maths/regress.py ADDED
@@ -0,0 +1,336 @@
1
+ import numpy as np
2
+ from collections import namedtuple
3
+
4
+
5
+ def linear_regression(X, y, *, show=False) -> tuple[np.ndarray, np.ndarray]:
6
+ """
7
+ Estimate values and std errors of n+1 parameters (p0, ..., pn)
8
+ of the model with n variables (x1..xn):
9
+
10
+ y = p0 + p1*x1 +... p*xn
11
+
12
+ *Notice*! intercept (the constant parameter) is returned on the `0`-th
13
+
14
+ :param X: array [L×n] (L samples of n variables)
15
+ :param y: array [L] of the corresponding values
16
+ :param show: if `True` print the results
17
+ :return: tuple of 2 arrays: (params, std-errs)
18
+ """
19
+ X = np.c_[np.ones((X.shape[0], 1)), X]
20
+ XtX = np.linalg.inv(X.T @ X)
21
+
22
+ pₑ = XtX @ X.T @ y # estimated parameters
23
+
24
+ res = X @ pₑ - y # residuals
25
+ σₑ = (res.T @ res) / (len(X) - len(pₑ)) # estimated noise (sigma)
26
+ σₚ = σₑ * XtX.diagonal() ** .5 # estimated param variances
27
+
28
+ if show:
29
+ with np.printoptions(precision=5, ):
30
+ print(f'Estimating {X.shape[1]} parameters from {len(y)} data points')
31
+ print(f" z-noise: {σₑ = :.4f}")
32
+ print(f" parameters:\n\t{pₑ = }\n\t{σₚ = }")
33
+ return pₑ, σₚ
34
+
35
+
36
+ RobustRes = namedtuple('RobustRes', ['params', 'stderr', 'outliers_num', 'inliers_mask'])
37
+
38
+
39
+ def robust_linear_regression(X, y, *, robust=True, fit_intercept=True,
40
+ show=False, **ransac_args) -> RobustRes:
41
+ """
42
+ Robustly estimate parameters (end their estimation std errors)
43
+ for a linear model `y = p0 + p1*x1 +... p*xn`
44
+
45
+ and values and std of `n+1` parameters `(p0, ..., pn)`
46
+ of the model with `n` variables `(x1..xn)`:
47
+
48
+ :param X: array `[L × n]` (L samples of n variables)
49
+ :param y: array `[L]` of the corresponding values
50
+ :param robust: if False - skip the RANSAC robust part
51
+ :param show: if `True` print the results
52
+ :return: (array `[n+1 × 2]` of estimated parameters and their std errors,
53
+ number of outliers, mask of inliers)
54
+
55
+ Robustness achieved using RANSAC - an iterative algorithm selecting
56
+ a subset of inliers from the complete data set.
57
+
58
+ RANSAC Parameters
59
+ ----------
60
+
61
+ residual_threshold : float, default=None
62
+ Maximum residual for a data sample to be classified as an inlier.
63
+ By default, the threshold is chosen as the MAD (median absolute
64
+ deviation) of the target values `y`. Points whose residuals are
65
+ strictly equal to the threshold are considered as inliers.
66
+
67
+ is_data_valid : callable, default=None
68
+ This function is called with the randomly selected data before the
69
+ model is fitted to it: `is_data_valid(X, y)`. If its return value is
70
+ False the current randomly chosen sub-sample is skipped.
71
+
72
+ is_model_valid : callable, default=None
73
+ This function is called with the estimated model and the randomly
74
+ selected data: `is_model_valid(model, X, y)`. If its return value is
75
+ False the current randomly chosen sub-sample is skipped.
76
+ Rejecting samples with this function is computationally costlier than
77
+ with `is_data_valid`. `is_model_valid` should therefore only be used if
78
+ the estimated model is needed for making the rejection decision.
79
+
80
+ max_trials : int, default=100
81
+ Maximum number of iterations for random sample selection.
82
+
83
+ max_skips : int, default=np.inf
84
+ Maximum number of iterations that can be skipped due to finding zero
85
+ inliers or invalid data defined by ``is_data_valid`` or invalid models
86
+ defined by ``is_model_valid``.
87
+
88
+ stop_n_inliers : int, default=np.inf
89
+ Stop iteration if at least this number of inliers are found.
90
+
91
+ stop_score : float, default=np.inf
92
+ Stop iteration if score is greater equal than this threshold.
93
+
94
+ stop_probability : float in range [0, 1], default=0.99
95
+ RANSAC iteration stops if at least one outlier-free set of the training
96
+ data is sampled in RANSAC. This requires to generate at least N
97
+ samples (iterations)::
98
+
99
+ N >= log(1 - probability) / log(1 - e**m)
100
+
101
+ where the probability (confidence) is typically set to high value such
102
+ as 0.99 (the default) and e is the current fraction of inliers w.r.t.
103
+ the total number of samples.
104
+
105
+ loss : str, callable, default='absolute_error'
106
+ String inputs, 'absolute_error' and 'squared_error' are supported which
107
+ find the absolute error and squared error per sample respectively.
108
+
109
+ If ``loss`` is a callable, then it should be a function that takes
110
+ two arrays as inputs, the true and predicted value and returns a 1-D
111
+ array with the i-th value of the array corresponding to the loss
112
+ on ``X[i]``.
113
+
114
+ If the loss on a sample is greater than the ``residual_threshold``,
115
+ then this sample is classified as an outlier.
116
+
117
+ .. versionadded:: 0.18
118
+
119
+ random_state : int, RandomState instance, default=None
120
+ The generator used to initialize the centers.
121
+ Pass an int for reproducible output across multiple function calls.
122
+ """
123
+
124
+ if robust:
125
+ from sklearn.linear_model import RANSACRegressor, LinearRegression
126
+ ransac = RANSACRegressor(
127
+ estimator=LinearRegression(fit_intercept=fit_intercept),
128
+ **ransac_args)
129
+ ransac.fit(X, y)
130
+
131
+ in_mask: np.ndarray = ransac.inlier_mask_
132
+ if num_ouliers := in_mask.size - in_mask.sum():
133
+ X, y = X[in_mask, :], y[in_mask]
134
+
135
+ return RobustRes(*linear_regression(X, y, show=show), num_ouliers, in_mask)
136
+
137
+
138
+ if __name__ == '__main__':
139
+ import numpy as np
140
+ from collections import namedtuple
141
+
142
+ PlaneParams = namedtuple('PlaneParams', ['d', 'kx', 'ky'])
143
+
144
+ p = PlaneParams(d=4, kx=.2, ky=-0.5)
145
+ σ = 1
146
+ outliers = 0.2 # potion of data
147
+ n = 30
148
+ np.random.seed(42)
149
+
150
+
151
+ def random_points_in_rect(num, xlim, ylim):
152
+ # x-y locations
153
+ r = np.array([xlim, ylim]).reshape(2, 2)
154
+ xy = np.random.rand(num, 2)
155
+ return xy * (r[:, 1] - r[:, 0]) + r[:, 0]
156
+
157
+
158
+ xy = random_points_in_rect(n, (0, 10), (20, 40))
159
+ zₘ = np.c_[np.ones(n), xy] @ p
160
+ z = zₘ + σ * np.random.randn(n)
161
+
162
+ # zₒ
163
+ outliers_mask = np.random.rand(n) < outliers
164
+ zₒ = z.copy()
165
+ zₒ[:2] += 6
166
+
167
+ from .geom.plane import Plane
168
+ from matplotlib import pyplot as plt
169
+ from matplotlib import use
170
+
171
+ use('QTAgg')
172
+
173
+ pn = Plane.from_points(np.c_[xy, z])
174
+ ax = pn.plot()
175
+ ax.scatter(*pn.points.T)
176
+
177
+ pn = Plane.from_points(np.c_[xy, zₒ])
178
+ ax = pn.plot()
179
+ ax.scatter(*pn.points.T, c='r')
180
+
181
+ print(f'Gausian noise {σ=}\n{"-" * 30}')
182
+ linear_regression(xy, z, show=True)
183
+ robust_linear_regression(xy, z, show=True);
184
+
185
+ print(f'Gausian noise {σ=} + {outliers_mask.sum()} outliers (+10 σ)\n{"-" * 30}')
186
+ linear_regression(xy, zₒ, show=True)
187
+ robust_linear_regression(xy, zₒ, show=True);
188
+ plt.show()
189
+
190
+ import numpy as np
191
+ from sklearn.linear_model import RANSACRegressor
192
+ from sklearn.base import BaseEstimator, RegressorMixin
193
+
194
+
195
+ class SVDPlaneEstimator(BaseEstimator, RegressorMixin):
196
+ """
197
+ Custom estimator to fit a plane (ax + by + cz + d = 0) using Singular Value Decomposition (SVD),
198
+ adapted for use with sklearn's `RANSACRegressor`.
199
+
200
+ Fitted parameters can be accessed as attributes `a_`, `b_`, `c_`, `d_`
201
+ or as array `coef_` in the order of axis enumeration (1,2,3) with intercept at 0:
202
+ p0 + ∑ p_i x_i = 0
203
+ """
204
+
205
+ def fit(self, X, y, norm=True):
206
+ """
207
+ Fit a plane to the given 3D points using SVD.
208
+ Normalize coefficients |a,b,c| = 1
209
+
210
+ Args:
211
+ X (numpy.ndarray): Nx2 array of (x, y) coordinates.
212
+ y (numpy.ndarray): N array of z values.
213
+ norm: if True - normalize to |a,b,c| = 1
214
+
215
+ Returns:
216
+ self: Fitted estimator.
217
+ """
218
+ # Construct the augmented matrix [1 X Y Z ]
219
+ A = np.c_[np.ones(len(y)), X, y]
220
+ _, _, Vt = np.linalg.svd(A)
221
+
222
+ # Extract plane parameters (smallest singular value)
223
+ c = Vt[-1, :]
224
+ self.coef_ = c / (c[1:] @ c[1:])**.5 if norm else c
225
+ self.d_, self.a_, self.b_, self.c_ = self.coef_
226
+
227
+ return self
228
+
229
+ def __repr__(self):
230
+ if not hasattr(self, 'coef_'):
231
+ return super().__repr__()
232
+
233
+ a, b, c, d = self.coef_[[-1, 0, 1, 2]]
234
+ return f"{type(self).__name__}({a=:.3g}, {b=:.3g}, {c=:.3g}, {d=:.3g})"
235
+
236
+ def predict(self, X):
237
+ """
238
+ Predict the z-values given x and y using the estimated plane equation.
239
+
240
+ Args:
241
+ X (numpy.ndarray): Nx2 array of (x, y) values.
242
+
243
+ Returns:
244
+ z_pred (numpy.ndarray): Predicted z values.
245
+ """
246
+ # Solve for z using the plane equation: ax + by + cz + d = 0
247
+ # Rearrange: z = (-ax - by - d) / c
248
+ if self.c_ == 0: # Prevent division by zero (degenerated case)
249
+ raise ValueError(
250
+ "Fitted plane has c = 0, which is not valid for regression.")
251
+
252
+ z_pred = (-self.a_ * X[:, 0] - self.b_ * X[:, 1] - self.d_) / self.c_
253
+ return z_pred
254
+
255
+
256
+ # class SVDPlaneEstimator(BaseEstimator, RegressorMixin):
257
+ # """
258
+ # Custom estimator to fit a plane (ax + by + cz + d = 0) using Singular Value Decomposition (SVD),
259
+ # adapted for use with sklearn's RANSACRegressor.
260
+ # """
261
+ #
262
+ # def fit(self, X, y):
263
+ # """
264
+ # Fit a plane to the given 3D points using SVD.
265
+ #
266
+ # Args:
267
+ # X (numpy.ndarray): Nx2 array of (x, y) coordinates.
268
+ # y (numpy.ndarray): N array of z values.
269
+ #
270
+ # Returns:
271
+ # self: Fitted estimator.
272
+ # """
273
+ # # Convert to full 3D points
274
+ # points = np.c_[X, y] # Combine (x, y) with target z
275
+ #
276
+ # # Construct the augmented matrix [X Y Z 1]
277
+ # A = np.c_[points, np.ones(points.shape[0])] # Add column of ones for d
278
+ # # Compute SVD
279
+ # _, _, Vt = np.linalg.svd(A)
280
+ #
281
+ # # Extract plane parameters (smallest singular value)
282
+ # self.a_, self.b_, self.c_, self.d_ = Vt[-1, :]
283
+ #
284
+ # return self
285
+ #
286
+ # def predict(self, X):
287
+ # """
288
+ # Predict the z-values given x and y using the estimated plane equation.
289
+ #
290
+ # Args:
291
+ # X (numpy.ndarray): Nx2 array of (x, y) values.
292
+ #
293
+ # Returns:
294
+ # z_pred (numpy.ndarray): Predicted z values.
295
+ # """
296
+ # # Solve for z using the plane equation: ax + by + cz + d = 0
297
+ # # Rearrange: z = (-ax - by - d) / c
298
+ # if self.c_ == 0: # Prevent division by zero (degenerated case)
299
+ # raise ValueError("Fitted plane has c = 0, which is not valid for regression.")
300
+ #
301
+ # z_pred = (-self.a_ * X[:, 0] - self.b_ * X[:, 1] - self.d_) / self.c_
302
+ # return z_pred
303
+
304
+
305
+ # Generate synthetic dataset
306
+ np.random.seed(42)
307
+ num_points = 200
308
+
309
+ # True plane equation: x - 2y + z - 5 = 0 → z = 5 - x + 2y
310
+ X = np.random.rand(num_points, 2) * 10 # (x, y) values
311
+ z_true = 5 - X[:, 0] + 2 * X[:, 1] # Compute true z values
312
+ noise = np.random.randn(num_points) * 0.2 # Add small noise
313
+ y = z_true + noise
314
+
315
+ # Add outliers
316
+ num_outliers = 40
317
+ outliers = np.random.rand(num_outliers, 2) * 10
318
+ outlier_z = np.random.rand(num_outliers) * 20 # Outliers far from the plane
319
+
320
+ # Combine inliers and outliers
321
+ X_all = np.vstack((X, outliers))
322
+ y_all = np.hstack((y, outlier_z))
323
+
324
+ # Fit RANSAC using the custom SVD estimator
325
+ ransac = RANSACRegressor(SVDPlaneEstimator(),
326
+ residual_threshold=0.5,
327
+ min_samples=5,
328
+ max_trials=100)
329
+ ransac.fit(X_all, y_all)
330
+
331
+ # Get the best estimated plane parameters
332
+ a, b, c, d = ransac.estimator_.a_, ransac.estimator_.b_, ransac.estimator_.c_, ransac.estimator_.d_
333
+
334
+ print(f"Estimated Plane Equation: {a:.3f}x + {b:.3f}y + {c:.3f}z + {d:.3f} = 0")
335
+ print(f"Number of inliers found: {ransac.inlier_mask_.sum()} / {len(X_all)}")
336
+
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: ialdev-maths
3
+ Version: 0.1.0
4
+ Summary: iad.maths — histograms, geometry, regression
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: ialdev-core
8
+ Requires-Dist: numpy>=1.20.0
9
+ Requires-Dist: pandas>=1.3.0
10
+ Requires-Dist: numba>=0.55.0
11
+ Requires-Dist: scikit-learn>=1.0.0
12
+ Requires-Dist: pytest>=7.0.0 ; extra == "dev"
13
+ Requires-Dist: ialdev-vis ; extra == "dev"
14
+ Provides-Extra: dev
15
+
16
+ # maths
17
+
18
+ Histogram utilities, geometry primitives, and regression helpers extracted from algutils.
19
+
@@ -0,0 +1,9 @@
1
+ iad/maths/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ iad/maths/hist.py,sha256=n4PhinoHlvzSDJeIyiRGgwHuRdtFbQ6qtza3b1PtNug,59661
3
+ iad/maths/regress.py,sha256=LNv35UMBLnw5tvLLAszY8LzyVcWqo4VOgmuYIdUtrVY,11934
4
+ iad/maths/geom/__init__.py,sha256=ImrG2cBoJpIgVTiMvifTsbIQbMILD_WlZBRwLQcgvLw,22
5
+ iad/maths/geom/plane.py,sha256=R8GeBxCUSYdCX3g-eAJzs_aeJqjvz6RQflgR-fcRm9k,23112
6
+ iad/maths/geom/shapes.py,sha256=xFo3KZjfDYmV0sFoiLa8rx5QN_5q6gkQrz4KbWUjE6o,15620
7
+ ialdev_maths-0.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
8
+ ialdev_maths-0.1.0.dist-info/METADATA,sha256=WPjYPX2Kw8D3W6MgNoWim8IocQxNQtIV8KArKOnT7ig,535
9
+ ialdev_maths-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any