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/__init__.py +0 -0
- iad/maths/geom/__init__.py +1 -0
- iad/maths/geom/plane.py +656 -0
- iad/maths/geom/shapes.py +496 -0
- iad/maths/hist.py +1458 -0
- iad/maths/regress.py +336 -0
- ialdev_maths-0.1.0.dist-info/METADATA +19 -0
- ialdev_maths-0.1.0.dist-info/RECORD +9 -0
- ialdev_maths-0.1.0.dist-info/WHEEL +4 -0
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,,
|