Static Estimation
This example demonstrates static estimation algorithms for parameter estimation, sensor calibration, and model fitting.
Overview
Static estimation methods are fundamental for:
Sensor calibration: Bias and scale factor estimation
Model fitting: Parameter estimation from data
Regression analysis: Relationship between variables
Data fusion: Combining multiple measurements
Least Squares Methods
- Ordinary Least Squares (OLS)
Minimizes sum of squared residuals
Assumes equal measurement uncertainty
Optimal for Gaussian noise
- Weighted Least Squares (WLS)
Accounts for varying measurement precision
Weights = 1/variance for optimal results
Essential for heteroscedastic data
- Total Least Squares (TLS)
Errors in both dependent and independent variables
Also known as errors-in-variables regression
Avoids bias from noisy predictors
- Generalized Least Squares (GLS)
Accounts for correlated measurement errors
Uses full covariance matrix
- Recursive Least Squares (RLS)
Online/sequential estimation
Updates estimate with each new measurement
Useful for real-time applications
- Ridge Regression
L2 regularization for ill-conditioned problems
Shrinks coefficients toward zero
Handles multicollinearity
Robust Estimation
- Huber M-estimator
Combines L2 (small residuals) and L1 (large residuals)
Less sensitive to outliers than OLS
Iteratively reweighted least squares (IRLS)
- Tukey Bisquare M-estimator
Completely rejects large outliers (zero weight)
More aggressive outlier handling
Identifies outliers through weights
- RANSAC
Random Sample Consensus
Robust to high outlier percentages
Separates inliers from outliers
Maximum Likelihood
- MLE for Gaussian
Estimates mean and variance
Optimal for Gaussian data
- Fisher Information
Measures information in data about parameters
Determines estimation precision limits
- Cramer-Rao Bound
Lower bound on estimator variance
Efficiency = CRB / actual variance
Model Selection
- AIC (Akaike Information Criterion)
Balances fit quality and model complexity
Penalizes additional parameters
Lower is better
- BIC (Bayesian Information Criterion)
Stronger penalty for complexity
Consistent model selection
Prefers simpler models than AIC
Code Highlights
The example demonstrates:
OLS with
ordinary_least_squares()WLS with
weighted_least_squares()TLS with
total_least_squares()RLS with
recursive_least_squares()Ridge with
ridge_regression()Huber with
huber_regression()Tukey with
tukey_regression()RANSAC with
ransac()MLE with
mle_gaussian()Model selection with
aic(),bic()
Source Code
1"""
2Static Estimation Example
3=========================
4
5This example demonstrates static estimation algorithms in PyTCL:
6
7Least Squares Methods:
8- Ordinary Least Squares (OLS)
9- Weighted Least Squares (WLS)
10- Total Least Squares (TLS)
11- Generalized Least Squares (GLS)
12- Recursive Least Squares (RLS)
13- Ridge Regression
14
15Robust Estimation:
16- Huber M-estimator
17- Tukey bisquare M-estimator
18- RANSAC for outlier-robust fitting
19
20Maximum Likelihood Estimation:
21- MLE for Gaussian parameters
22- Fisher Information and Cramer-Rao Bounds
23- Model selection (AIC, BIC)
24
25These methods are fundamental for parameter estimation, sensor calibration,
26and model fitting in the presence of noise and outliers.
27"""
28
29from pathlib import Path
30
31import numpy as np
32import plotly.graph_objects as go
33from plotly.subplots import make_subplots
34
35# Output directory for generated plots
36OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
37OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
38
39# Global flag to control plotting
40SHOW_PLOTS = True
41
42
43from pytcl.static_estimation import ( # Least squares; Robust estimation; MLE and Fisher Information; Model selection
44 aic,
45 aicc,
46 bic,
47 cramer_rao_bound,
48 efficiency,
49 fisher_information_gaussian,
50 fisher_information_numerical,
51 generalized_least_squares,
52 huber_regression,
53 irls,
54 mad,
55 mle_gaussian,
56 ordinary_least_squares,
57 ransac,
58 recursive_least_squares,
59 ridge_regression,
60 total_least_squares,
61 tukey_regression,
62 weighted_least_squares,
63)
64
65
66def demo_ordinary_least_squares():
67 """Demonstrate ordinary least squares."""
68 print("=" * 70)
69 print("Ordinary Least Squares Demo")
70 print("=" * 70)
71
72 np.random.seed(42)
73
74 # Generate linear regression data
75 n_samples = 50
76 x = np.linspace(0, 10, n_samples)
77 true_slope = 2.5
78 true_intercept = 1.0
79 noise_std = 0.5
80
81 y = true_intercept + true_slope * x + np.random.randn(n_samples) * noise_std
82
83 # Design matrix [1, x]
84 A = np.column_stack([np.ones(n_samples), x])
85
86 # OLS solution
87 result = ordinary_least_squares(A, y)
88
89 print(f"\nTrue parameters: intercept={true_intercept}, slope={true_slope}")
90 print(f"OLS estimate: intercept={result.x[0]:.4f}, slope={result.x[1]:.4f}")
91 print(f"\nResidual sum of squares: {np.sum(result.residuals**2):.4f}")
92 print(f"Matrix rank: {result.rank}")
93
94 # Coefficient of determination
95 ss_res = np.sum(result.residuals**2)
96 ss_tot = np.sum((y - np.mean(y)) ** 2)
97 r_squared = 1 - ss_res / ss_tot
98 print(f"R² = {r_squared:.4f}")
99
100 # Plot OLS fit
101 if SHOW_PLOTS:
102 fig = make_subplots(
103 rows=1,
104 cols=2,
105 subplot_titles=[
106 f"Ordinary Least Squares (R² = {r_squared:.4f})",
107 "Residual Plot",
108 ],
109 )
110
111 # Fit plot
112 fig.add_trace(
113 go.Scatter(
114 x=x,
115 y=y,
116 mode="markers",
117 marker=dict(color="blue", size=8, opacity=0.6),
118 name="Data",
119 ),
120 row=1,
121 col=1,
122 )
123
124 x_line = np.linspace(x.min(), x.max(), 100)
125 y_true_line = true_intercept + true_slope * x_line
126 y_fit_line = result.x[0] + result.x[1] * x_line
127
128 fig.add_trace(
129 go.Scatter(
130 x=x_line,
131 y=y_true_line,
132 mode="lines",
133 line=dict(color="green", width=2, dash="dash"),
134 name="True line",
135 ),
136 row=1,
137 col=1,
138 )
139
140 fig.add_trace(
141 go.Scatter(
142 x=x_line,
143 y=y_fit_line,
144 mode="lines",
145 line=dict(color="red", width=2),
146 name="OLS fit",
147 ),
148 row=1,
149 col=1,
150 )
151
152 # Residuals plot
153 residuals = y - (result.x[0] + result.x[1] * x)
154 fig.add_trace(
155 go.Scatter(
156 x=x,
157 y=residuals,
158 mode="markers",
159 marker=dict(color="blue", size=8, opacity=0.6),
160 name="Residuals",
161 showlegend=False,
162 ),
163 row=1,
164 col=2,
165 )
166
167 fig.add_hline(y=0, line_dash="dash", line_color="red", row=1, col=2)
168
169 fig.update_xaxes(title_text="x", row=1, col=1)
170 fig.update_yaxes(title_text="y", row=1, col=1)
171 fig.update_xaxes(title_text="x", row=1, col=2)
172 fig.update_yaxes(title_text="Residual", row=1, col=2)
173
174 fig.update_layout(height=500, width=1000, showlegend=True)
175 fig.write_html(str(OUTPUT_DIR / "static_ols.html"))
176 print("\n [Plot saved to static_ols.html]")
177
178
179def demo_weighted_least_squares():
180 """Demonstrate weighted least squares."""
181 print("\n" + "=" * 70)
182 print("Weighted Least Squares Demo")
183 print("=" * 70)
184
185 np.random.seed(42)
186
187 # Data with heteroscedastic noise (varying variance)
188 n_samples = 50
189 x = np.linspace(0, 10, n_samples)
190 true_slope = 2.0
191 true_intercept = 3.0
192
193 # Noise increases with x
194 noise_std = 0.2 + 0.1 * x
195 y = true_intercept + true_slope * x + np.random.randn(n_samples) * noise_std
196
197 A = np.column_stack([np.ones(n_samples), x])
198
199 # OLS (ignores varying noise)
200 result_ols = ordinary_least_squares(A, y)
201
202 # WLS with weights = 1/variance
203 weights = 1 / noise_std**2
204 result_wls = weighted_least_squares(A, y, weights=weights)
205
206 print(f"\nTrue parameters: intercept={true_intercept}, slope={true_slope}")
207 print(
208 f"\nOLS estimate: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}"
209 )
210 print(f"WLS estimate: intercept={result_wls.x[0]:.4f}, slope={result_wls.x[1]:.4f}")
211
212 print("\nNote: WLS gives more weight to precise measurements (low variance)")
213 print("and typically produces better estimates when noise is heteroscedastic.")
214
215
216def demo_total_least_squares():
217 """Demonstrate total least squares (errors-in-variables)."""
218 print("\n" + "=" * 70)
219 print("Total Least Squares Demo")
220 print("=" * 70)
221
222 np.random.seed(42)
223
224 # Both x and y have measurement errors
225 n_samples = 30
226 x_true = np.linspace(0, 10, n_samples)
227 true_slope = 1.5
228 true_intercept = 2.0
229
230 # Add noise to both x and y
231 x_noise_std = 0.3
232 y_noise_std = 0.5
233
234 x = x_true + np.random.randn(n_samples) * x_noise_std
235 y = true_intercept + true_slope * x_true + np.random.randn(n_samples) * y_noise_std
236
237 A = np.column_stack([np.ones(n_samples), x])
238
239 # OLS (assumes x is error-free)
240 result_ols = ordinary_least_squares(A, y)
241
242 # TLS (accounts for errors in x)
243 result_tls = total_least_squares(A, y)
244
245 print(f"\nTrue parameters: intercept={true_intercept}, slope={true_slope}")
246 print(
247 f"\nOLS estimate: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}"
248 )
249 print(f"TLS estimate: intercept={result_tls.x[0]:.4f}, slope={result_tls.x[1]:.4f}")
250
251 print("\nNote: TLS is preferred when independent variables have measurement error.")
252 print("OLS typically underestimates the true slope in this case.")
253
254
255def demo_recursive_least_squares():
256 """Demonstrate recursive least squares for online estimation."""
257 print("\n" + "=" * 70)
258 print("Recursive Least Squares Demo")
259 print("=" * 70)
260
261 np.random.seed(42)
262
263 # Online parameter estimation
264 n_samples = 100
265 x = np.linspace(0, 10, n_samples)
266 true_slope = 2.0
267 true_intercept = 1.0
268 noise_std = 0.3
269
270 y = true_intercept + true_slope * x + np.random.randn(n_samples) * noise_std
271
272 # Initialize RLS (2 parameters: intercept and slope)
273 n_params = 2
274 x_est = np.zeros(n_params) # Initial parameter estimate
275 P = np.eye(n_params) * 100.0 # Initial covariance (large uncertainty)
276
277 print("\nOnline parameter estimation with RLS:")
278 print("-" * 50)
279
280 checkpoints = [10, 25, 50, 100]
281 checkpoint_idx = 0
282
283 for i in range(n_samples):
284 # Measurement vector [1, x_i] for y = intercept + slope * x
285 a = np.array([1.0, x[i]])
286 y_i = y[i]
287
288 # RLS update
289 x_est, P = recursive_least_squares(x_est, P, a, y_i)
290
291 # Print at checkpoints
292 if checkpoint_idx < len(checkpoints) and i + 1 == checkpoints[checkpoint_idx]:
293 print(
294 f" After {i+1:>3} samples: intercept={x_est[0]:.4f}, "
295 f"slope={x_est[1]:.4f}"
296 )
297 checkpoint_idx += 1
298
299 print(f"\nTrue values: intercept={true_intercept}, slope={true_slope}")
300 print("\nNote: RLS converges to true values as more data arrives.")
301
302
303def demo_ridge_regression():
304 """Demonstrate ridge regression for ill-conditioned problems."""
305 print("\n" + "=" * 70)
306 print("Ridge Regression Demo")
307 print("=" * 70)
308
309 np.random.seed(42)
310
311 # Create collinear predictors (ill-conditioned)
312 n_samples = 30
313 x1 = np.random.randn(n_samples)
314 x2 = x1 + np.random.randn(n_samples) * 0.1 # x2 ≈ x1
315
316 true_coef = np.array([1.0, 2.0, 3.0]) # [intercept, b1, b2]
317 y = true_coef[0] + true_coef[1] * x1 + true_coef[2] * x2
318 y += np.random.randn(n_samples) * 0.5
319
320 A = np.column_stack([np.ones(n_samples), x1, x2])
321
322 # OLS solution (may be unstable)
323 result_ols = ordinary_least_squares(A, y)
324
325 # Ridge regression with regularization
326 lambdas = [0.0, 0.01, 0.1, 1.0]
327
328 print(f"\nTrue coefficients: {true_coef}")
329 print("\nEstimates with different regularization:")
330 print("-" * 60)
331 print(f"{'Lambda':>10} {'Intercept':>12} {'b1':>12} {'b2':>12}")
332 print("-" * 60)
333
334 for lam in lambdas:
335 if lam == 0:
336 x_hat = result_ols.x
337 else:
338 x_hat = ridge_regression(A, y, alpha=lam) # Returns array directly
339 print(f"{lam:>10.2f} {x_hat[0]:>12.4f} {x_hat[1]:>12.4f} " f"{x_hat[2]:>12.4f}")
340
341 print("\nNote: Ridge regression shrinks coefficients toward zero,")
342 print("which helps stabilize estimates for collinear predictors.")
343
344
345def demo_robust_estimation():
346 """Demonstrate robust estimation methods."""
347 print("\n" + "=" * 70)
348 print("Robust Estimation Demo")
349 print("=" * 70)
350
351 np.random.seed(42)
352
353 # Generate data with outliers
354 n_samples = 50
355 n_outliers = 5
356
357 x = np.linspace(0, 10, n_samples)
358 true_slope = 2.0
359 true_intercept = 1.0
360
361 y = true_intercept + true_slope * x + np.random.randn(n_samples) * 0.5
362
363 # Add outliers
364 outlier_idx = np.random.choice(n_samples, n_outliers, replace=False)
365 y[outlier_idx] += np.random.randn(n_outliers) * 10
366
367 A = np.column_stack([np.ones(n_samples), x])
368
369 print(f"\nData: {n_samples} samples with {n_outliers} outliers")
370 print(f"True parameters: intercept={true_intercept}, slope={true_slope}")
371
372 # OLS (sensitive to outliers)
373 result_ols = ordinary_least_squares(A, y)
374 print(f"\nOLS: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}")
375
376 # Huber M-estimator
377 result_huber = huber_regression(A, y)
378 print(f"Huber: intercept={result_huber.x[0]:.4f}, slope={result_huber.x[1]:.4f}")
379 print(f" Iterations: {result_huber.n_iter}, Converged: {result_huber.converged}")
380
381 # Tukey bisquare M-estimator
382 result_tukey = tukey_regression(A, y)
383 print(f"Tukey: intercept={result_tukey.x[0]:.4f}, slope={result_tukey.x[1]:.4f}")
384 print(f" Iterations: {result_tukey.n_iter}, Converged: {result_tukey.converged}")
385
386 # Analyze weights from Tukey estimator
387 weights = result_tukey.weights
388 detected_outliers = np.where(weights < 0.1)[0]
389 print(f"\nDetected outliers (low weight): {detected_outliers}")
390 print(f"True outlier indices: {sorted(outlier_idx)}")
391
392 # Plot robust estimation comparison
393 if SHOW_PLOTS:
394 fig = make_subplots(
395 rows=1,
396 cols=2,
397 subplot_titles=[
398 "Robust Estimation: OLS vs M-estimators",
399 "Tukey M-estimator Weights (red = true outliers)",
400 ],
401 )
402
403 # Fit comparison
404 # Regular data points
405 regular_mask = np.ones(n_samples, dtype=bool)
406 regular_mask[outlier_idx] = False
407
408 fig.add_trace(
409 go.Scatter(
410 x=x[regular_mask],
411 y=y[regular_mask],
412 mode="markers",
413 marker=dict(color="blue", size=8, opacity=0.6),
414 name="Data",
415 ),
416 row=1,
417 col=1,
418 )
419
420 fig.add_trace(
421 go.Scatter(
422 x=x[outlier_idx],
423 y=y[outlier_idx],
424 mode="markers",
425 marker=dict(color="red", size=10, opacity=0.8),
426 name="Outliers",
427 ),
428 row=1,
429 col=1,
430 )
431
432 x_line = np.linspace(x.min(), x.max(), 100)
433
434 fig.add_trace(
435 go.Scatter(
436 x=x_line,
437 y=true_intercept + true_slope * x_line,
438 mode="lines",
439 line=dict(color="green", width=2, dash="dash"),
440 name="True",
441 ),
442 row=1,
443 col=1,
444 )
445
446 fig.add_trace(
447 go.Scatter(
448 x=x_line,
449 y=result_ols.x[0] + result_ols.x[1] * x_line,
450 mode="lines",
451 line=dict(color="black", width=2),
452 name="OLS",
453 ),
454 row=1,
455 col=1,
456 )
457
458 fig.add_trace(
459 go.Scatter(
460 x=x_line,
461 y=result_huber.x[0] + result_huber.x[1] * x_line,
462 mode="lines",
463 line=dict(color="magenta", width=2, dash="dash"),
464 name="Huber",
465 ),
466 row=1,
467 col=1,
468 )
469
470 fig.add_trace(
471 go.Scatter(
472 x=x_line,
473 y=result_tukey.x[0] + result_tukey.x[1] * x_line,
474 mode="lines",
475 line=dict(color="cyan", width=2, dash="dot"),
476 name="Tukey",
477 ),
478 row=1,
479 col=1,
480 )
481
482 # Weights from Tukey estimator
483 colors = ["red" if i in outlier_idx else "blue" for i in range(n_samples)]
484 fig.add_trace(
485 go.Scatter(
486 x=x,
487 y=weights,
488 mode="markers",
489 marker=dict(color=colors, size=8, opacity=0.7),
490 name="Weights",
491 showlegend=False,
492 ),
493 row=1,
494 col=2,
495 )
496
497 fig.add_hline(y=0.1, line_dash="dash", line_color="red", row=1, col=2)
498
499 fig.update_xaxes(title_text="x", row=1, col=1)
500 fig.update_yaxes(title_text="y", row=1, col=1)
501 fig.update_xaxes(title_text="x", row=1, col=2)
502 fig.update_yaxes(title_text="Tukey weight", row=1, col=2)
503
504 fig.update_layout(height=500, width=1200, showlegend=True)
505 fig.write_html(str(OUTPUT_DIR / "static_robust_estimation.html"))
506 print("\n [Plot saved to static_robust_estimation.html]")
507
508
509def demo_ransac():
510 """Demonstrate RANSAC for robust fitting."""
511 print("\n" + "=" * 70)
512 print("RANSAC Demo")
513 print("=" * 70)
514
515 np.random.seed(42)
516
517 # Line fitting with many outliers
518 n_inliers = 40
519 n_outliers = 20
520
521 # Inliers: points on line y = 2x + 1
522 x_inliers = np.random.uniform(0, 10, n_inliers)
523 y_inliers = 1 + 2 * x_inliers + np.random.randn(n_inliers) * 0.3
524
525 # Outliers: random points
526 x_outliers = np.random.uniform(0, 10, n_outliers)
527 y_outliers = np.random.uniform(-5, 25, n_outliers)
528
529 x = np.concatenate([x_inliers, x_outliers])
530 y = np.concatenate([y_inliers, y_outliers])
531
532 # Shuffle
533 perm = np.random.permutation(len(x))
534 x, y = x[perm], y[perm]
535
536 A = np.column_stack([np.ones(len(x)), x])
537
538 print(f"\nData: {n_inliers} inliers + {n_outliers} outliers = {len(x)} total")
539 print("True line: y = 2x + 1")
540
541 # OLS
542 result_ols = ordinary_least_squares(A, y)
543 print(f"\nOLS: y = {result_ols.x[1]:.4f}x + {result_ols.x[0]:.4f}")
544
545 # RANSAC
546 threshold = 1.0 # Inlier threshold (residual threshold)
547
548 result_ransac = ransac(
549 A, y, min_samples=2, residual_threshold=threshold, max_trials=100
550 )
551
552 print(f"RANSAC: y = {result_ransac.x[1]:.4f}x + {result_ransac.x[0]:.4f}")
553 print(f" Inliers found: {result_ransac.n_inliers}/{len(x)}")
554
555 print("\nNote: RANSAC successfully recovers the true line despite")
556 print("33% of the data being outliers.")
557
558 # Plot RANSAC
559 if SHOW_PLOTS:
560 fig = go.Figure()
561
562 # Determine inliers based on RANSAC residuals
563 y_pred = result_ransac.x[0] + result_ransac.x[1] * x
564 residuals = np.abs(y - y_pred)
565 is_inlier = residuals < threshold
566
567 fig.add_trace(
568 go.Scatter(
569 x=x[is_inlier],
570 y=y[is_inlier],
571 mode="markers",
572 marker=dict(color="blue", size=8, opacity=0.6),
573 name="Inliers",
574 )
575 )
576
577 fig.add_trace(
578 go.Scatter(
579 x=x[~is_inlier],
580 y=y[~is_inlier],
581 mode="markers",
582 marker=dict(color="red", size=8, opacity=0.6),
583 name="Outliers",
584 )
585 )
586
587 x_line = np.linspace(x.min(), x.max(), 100)
588
589 fig.add_trace(
590 go.Scatter(
591 x=x_line,
592 y=1 + 2 * x_line,
593 mode="lines",
594 line=dict(color="green", width=2, dash="dash"),
595 name="True line",
596 )
597 )
598
599 fig.add_trace(
600 go.Scatter(
601 x=x_line,
602 y=result_ols.x[0] + result_ols.x[1] * x_line,
603 mode="lines",
604 line=dict(color="black", width=2),
605 name="OLS",
606 )
607 )
608
609 fig.add_trace(
610 go.Scatter(
611 x=x_line,
612 y=result_ransac.x[0] + result_ransac.x[1] * x_line,
613 mode="lines",
614 line=dict(color="red", width=2),
615 name="RANSAC",
616 )
617 )
618
619 fig.update_layout(
620 title=f"RANSAC Line Fitting ({result_ransac.n_inliers} inliers / {len(x)} total)",
621 xaxis_title="x",
622 yaxis_title="y",
623 height=500,
624 width=800,
625 showlegend=True,
626 )
627 fig.write_html(str(OUTPUT_DIR / "static_ransac.html"))
628 print("\n [Plot saved to static_ransac.html]")
629
630
631def demo_mle_and_fisher():
632 """Demonstrate MLE and Fisher information."""
633 print("\n" + "=" * 70)
634 print("Maximum Likelihood Estimation Demo")
635 print("=" * 70)
636
637 np.random.seed(42)
638
639 # Generate Gaussian data
640 n_samples = 100
641 true_mean = 5.0
642 true_std = 2.0
643
644 data = true_mean + np.random.randn(n_samples) * true_std
645
646 print(f"\nTrue parameters: mean={true_mean}, std={true_std}")
647 print(f"Sample size: {n_samples}")
648
649 # MLE for Gaussian parameters
650 # theta[0] = mean, theta[1] = variance
651 result = mle_gaussian(data)
652 mle_mean = result.theta[0]
653 mle_var = result.theta[1]
654 mle_std = np.sqrt(mle_var)
655 print(f"\nMLE estimates: mean={mle_mean:.4f}, std={mle_std:.4f}")
656 print(f"Log-likelihood: {result.log_likelihood:.4f}")
657
658 # Fisher information
659 # For Gaussian: I(μ) = n/σ², I(σ²) = n/(2σ⁴)
660 fisher_mean = n_samples / true_std**2
661 fisher_var = n_samples / (2 * true_std**4)
662
663 print(f"\nFisher information:")
664 print(f" I(mean) = {fisher_mean:.4f}")
665 print(f" I(variance) = {fisher_var:.4f}")
666
667 # Cramer-Rao bounds
668 crb_mean = 1 / fisher_mean
669 crb_var = 1 / fisher_var
670
671 print(f"\nCramer-Rao lower bounds (variance of unbiased estimator):")
672 print(f" Var(mean_hat) ≥ {crb_mean:.6f}")
673 print(f" Var(var_hat) ≥ {crb_var:.6f}")
674
675 # Check actual MLE variance (through simulation)
676 n_trials = 1000
677 mean_estimates = []
678 for _ in range(n_trials):
679 sample = true_mean + np.random.randn(n_samples) * true_std
680 result = mle_gaussian(sample)
681 mean_estimates.append(result.theta[0])
682
683 actual_variance = np.var(mean_estimates)
684 print(f"\nSimulated variance of mean estimator: {actual_variance:.6f}")
685 print(f"Efficiency: {crb_mean / actual_variance:.4f}")
686 print("(Efficiency = 1.0 means estimator achieves the CRB)")
687
688
689def demo_model_selection():
690 """Demonstrate model selection using information criteria."""
691 print("\n" + "=" * 70)
692 print("Model Selection Demo")
693 print("=" * 70)
694
695 np.random.seed(42)
696
697 # True model: quadratic y = 1 + 2x + 0.5x²
698 n_samples = 50
699 x = np.linspace(0, 5, n_samples)
700 y_true = 1 + 2 * x + 0.5 * x**2
701 y = y_true + np.random.randn(n_samples) * 0.5
702
703 print("\nTrue model: y = 1 + 2x + 0.5x²")
704 print("Comparing polynomial models of degree 1 through 5:")
705 print("-" * 60)
706 print(f"{'Degree':>6} {'RSS':>10} {'k':>4} {'AIC':>10} {'BIC':>10}")
707 print("-" * 60)
708
709 results = []
710 for degree in range(1, 6):
711 # Build design matrix for polynomial
712 A = np.column_stack([x**i for i in range(degree + 1)])
713 result = ordinary_least_squares(A, y)
714
715 rss = np.sum(result.residuals**2)
716 k = degree + 1 # Number of parameters
717 n = n_samples
718
719 # Compute log-likelihood (assuming Gaussian errors)
720 sigma2 = rss / n
721 log_lik = -n / 2 * np.log(2 * np.pi * sigma2) - rss / (2 * sigma2)
722
723 aic_val = aic(log_lik, k)
724 bic_val = bic(log_lik, k, n)
725
726 results.append((degree, rss, k, aic_val, bic_val))
727 print(f"{degree:>6} {rss:>10.4f} {k:>4} {aic_val:>10.2f} {bic_val:>10.2f}")
728
729 # Find best models
730 best_aic = min(results, key=lambda x: x[3])
731 best_bic = min(results, key=lambda x: x[4])
732
733 print(f"\nBest by AIC: degree {best_aic[0]}")
734 print(f"Best by BIC: degree {best_bic[0]}")
735 print("\nNote: True model is degree 2. AIC/BIC help avoid overfitting.")
736
737 # Plot model selection
738 if SHOW_PLOTS:
739 fig = make_subplots(
740 rows=1,
741 cols=2,
742 subplot_titles=["Polynomial Model Fits", "Model Selection: AIC vs BIC"],
743 )
744
745 # Model fits
746 fig.add_trace(
747 go.Scatter(
748 x=x,
749 y=y,
750 mode="markers",
751 marker=dict(color="blue", size=8, opacity=0.6),
752 name="Data",
753 ),
754 row=1,
755 col=1,
756 )
757
758 x_line = np.linspace(x.min(), x.max(), 100)
759 fig.add_trace(
760 go.Scatter(
761 x=x_line,
762 y=1 + 2 * x_line + 0.5 * x_line**2,
763 mode="lines",
764 line=dict(color="green", width=2, dash="dash"),
765 name="True (deg 2)",
766 ),
767 row=1,
768 col=1,
769 )
770
771 # Fit degrees 1, 2, 3
772 colors = ["red", "green", "purple"]
773 for i, deg in enumerate([1, 2, 3]):
774 A_fit = np.column_stack([x_line**j for j in range(deg + 1)])
775 A_data = np.column_stack([x**j for j in range(deg + 1)])
776 coef = ordinary_least_squares(A_data, y).x
777 y_fit = A_fit @ coef
778 fig.add_trace(
779 go.Scatter(
780 x=x_line,
781 y=y_fit,
782 mode="lines",
783 line=dict(width=1.5, dash="solid" if deg == 2 else "dot"),
784 name=f"Degree {deg}",
785 ),
786 row=1,
787 col=1,
788 )
789
790 # AIC/BIC comparison
791 degrees = [r[0] for r in results]
792 aic_vals = [r[3] for r in results]
793 bic_vals = [r[4] for r in results]
794
795 fig.add_trace(
796 go.Bar(
797 x=[d - 0.2 for d in degrees],
798 y=aic_vals,
799 width=0.35,
800 name="AIC",
801 marker_color="blue",
802 opacity=0.7,
803 ),
804 row=1,
805 col=2,
806 )
807
808 fig.add_trace(
809 go.Bar(
810 x=[d + 0.2 for d in degrees],
811 y=bic_vals,
812 width=0.35,
813 name="BIC",
814 marker_color="orange",
815 opacity=0.7,
816 ),
817 row=1,
818 col=2,
819 )
820
821 fig.update_xaxes(title_text="x", row=1, col=1)
822 fig.update_yaxes(title_text="y", row=1, col=1)
823 fig.update_xaxes(title_text="Polynomial Degree", row=1, col=2)
824 fig.update_yaxes(title_text="Information Criterion", row=1, col=2)
825
826 fig.update_layout(height=500, width=1200, showlegend=True)
827 fig.write_html(str(OUTPUT_DIR / "static_model_selection.html"))
828 print("\n [Plot saved to static_model_selection.html]")
829
830
831def main():
832 """Run all demonstrations."""
833 print("\n" + "#" * 70)
834 print("# PyTCL Static Estimation Example")
835 print("#" * 70)
836
837 # Least squares methods
838 demo_ordinary_least_squares()
839 demo_weighted_least_squares()
840 demo_total_least_squares()
841 demo_recursive_least_squares()
842 demo_ridge_regression()
843
844 # Robust estimation
845 demo_robust_estimation()
846 demo_ransac()
847
848 # MLE and model selection
849 demo_mle_and_fisher()
850 demo_model_selection()
851
852 print("\n" + "=" * 70)
853 print("Example complete!")
854 if SHOW_PLOTS:
855 print("Plots saved: static_ols.html, static_robust_estimation.html,")
856 print(" static_ransac.html, static_model_selection.html")
857 print("=" * 70)
858
859
860if __name__ == "__main__":
861 main()
Running the Example
python examples/static_estimation.py
See Also
Kalman Filter Comparison - Dynamic estimation
performance_evaluation - Tracking metrics
gaussian_mixtures - Clustering and mixture models