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(
176 str(OUTPUT_DIR / "static_ols.html"),
177 include_plotlyjs="cdn",
178 div_id="static_ols",
179 )
180 print("\n [Plot saved to static_ols.html]")
181
182
183def demo_weighted_least_squares():
184 """Demonstrate weighted least squares."""
185 print("\n" + "=" * 70)
186 print("Weighted Least Squares Demo")
187 print("=" * 70)
188
189 np.random.seed(42)
190
191 # Data with heteroscedastic noise (varying variance)
192 n_samples = 50
193 x = np.linspace(0, 10, n_samples)
194 true_slope = 2.0
195 true_intercept = 3.0
196
197 # Noise increases with x
198 noise_std = 0.2 + 0.1 * x
199 y = true_intercept + true_slope * x + np.random.randn(n_samples) * noise_std
200
201 A = np.column_stack([np.ones(n_samples), x])
202
203 # OLS (ignores varying noise)
204 result_ols = ordinary_least_squares(A, y)
205
206 # WLS with weights = 1/variance
207 weights = 1 / noise_std**2
208 result_wls = weighted_least_squares(A, y, weights=weights)
209
210 print(f"\nTrue parameters: intercept={true_intercept}, slope={true_slope}")
211 print(
212 f"\nOLS estimate: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}"
213 )
214 print(f"WLS estimate: intercept={result_wls.x[0]:.4f}, slope={result_wls.x[1]:.4f}")
215
216 print("\nNote: WLS gives more weight to precise measurements (low variance)")
217 print("and typically produces better estimates when noise is heteroscedastic.")
218
219
220def demo_total_least_squares():
221 """Demonstrate total least squares (errors-in-variables)."""
222 print("\n" + "=" * 70)
223 print("Total Least Squares Demo")
224 print("=" * 70)
225
226 np.random.seed(42)
227
228 # Both x and y have measurement errors
229 n_samples = 30
230 x_true = np.linspace(0, 10, n_samples)
231 true_slope = 1.5
232 true_intercept = 2.0
233
234 # Add noise to both x and y
235 x_noise_std = 0.3
236 y_noise_std = 0.5
237
238 x = x_true + np.random.randn(n_samples) * x_noise_std
239 y = true_intercept + true_slope * x_true + np.random.randn(n_samples) * y_noise_std
240
241 A = np.column_stack([np.ones(n_samples), x])
242
243 # OLS (assumes x is error-free)
244 result_ols = ordinary_least_squares(A, y)
245
246 # TLS (accounts for errors in x)
247 result_tls = total_least_squares(A, y)
248
249 print(f"\nTrue parameters: intercept={true_intercept}, slope={true_slope}")
250 print(
251 f"\nOLS estimate: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}"
252 )
253 print(f"TLS estimate: intercept={result_tls.x[0]:.4f}, slope={result_tls.x[1]:.4f}")
254
255 print("\nNote: TLS is preferred when independent variables have measurement error.")
256 print("OLS typically underestimates the true slope in this case.")
257
258
259def demo_recursive_least_squares():
260 """Demonstrate recursive least squares for online estimation."""
261 print("\n" + "=" * 70)
262 print("Recursive Least Squares Demo")
263 print("=" * 70)
264
265 np.random.seed(42)
266
267 # Online parameter estimation
268 n_samples = 100
269 x = np.linspace(0, 10, n_samples)
270 true_slope = 2.0
271 true_intercept = 1.0
272 noise_std = 0.3
273
274 y = true_intercept + true_slope * x + np.random.randn(n_samples) * noise_std
275
276 # Initialize RLS (2 parameters: intercept and slope)
277 n_params = 2
278 x_est = np.zeros(n_params) # Initial parameter estimate
279 P = np.eye(n_params) * 100.0 # Initial covariance (large uncertainty)
280
281 print("\nOnline parameter estimation with RLS:")
282 print("-" * 50)
283
284 checkpoints = [10, 25, 50, 100]
285 checkpoint_idx = 0
286
287 for i in range(n_samples):
288 # Measurement vector [1, x_i] for y = intercept + slope * x
289 a = np.array([1.0, x[i]])
290 y_i = y[i]
291
292 # RLS update
293 x_est, P = recursive_least_squares(x_est, P, a, y_i)
294
295 # Print at checkpoints
296 if checkpoint_idx < len(checkpoints) and i + 1 == checkpoints[checkpoint_idx]:
297 print(
298 f" After {i + 1:>3} samples: intercept={x_est[0]:.4f}, "
299 f"slope={x_est[1]:.4f}"
300 )
301 checkpoint_idx += 1
302
303 print(f"\nTrue values: intercept={true_intercept}, slope={true_slope}")
304 print("\nNote: RLS converges to true values as more data arrives.")
305
306
307def demo_ridge_regression():
308 """Demonstrate ridge regression for ill-conditioned problems."""
309 print("\n" + "=" * 70)
310 print("Ridge Regression Demo")
311 print("=" * 70)
312
313 np.random.seed(42)
314
315 # Create collinear predictors (ill-conditioned)
316 n_samples = 30
317 x1 = np.random.randn(n_samples)
318 x2 = x1 + np.random.randn(n_samples) * 0.1 # x2 ≈ x1
319
320 true_coef = np.array([1.0, 2.0, 3.0]) # [intercept, b1, b2]
321 y = true_coef[0] + true_coef[1] * x1 + true_coef[2] * x2
322 y += np.random.randn(n_samples) * 0.5
323
324 A = np.column_stack([np.ones(n_samples), x1, x2])
325
326 # OLS solution (may be unstable)
327 result_ols = ordinary_least_squares(A, y)
328
329 # Ridge regression with regularization
330 lambdas = [0.0, 0.01, 0.1, 1.0]
331
332 print(f"\nTrue coefficients: {true_coef}")
333 print("\nEstimates with different regularization:")
334 print("-" * 60)
335 print(f"{'Lambda':>10} {'Intercept':>12} {'b1':>12} {'b2':>12}")
336 print("-" * 60)
337
338 for lam in lambdas:
339 if lam == 0:
340 x_hat = result_ols.x
341 else:
342 x_hat = ridge_regression(A, y, alpha=lam) # Returns array directly
343 print(f"{lam:>10.2f} {x_hat[0]:>12.4f} {x_hat[1]:>12.4f} {x_hat[2]:>12.4f}")
344
345 print("\nNote: Ridge regression shrinks coefficients toward zero,")
346 print("which helps stabilize estimates for collinear predictors.")
347
348
349def demo_robust_estimation():
350 """Demonstrate robust estimation methods."""
351 print("\n" + "=" * 70)
352 print("Robust Estimation Demo")
353 print("=" * 70)
354
355 np.random.seed(42)
356
357 # Generate data with outliers
358 n_samples = 50
359 n_outliers = 5
360
361 x = np.linspace(0, 10, n_samples)
362 true_slope = 2.0
363 true_intercept = 1.0
364
365 y = true_intercept + true_slope * x + np.random.randn(n_samples) * 0.5
366
367 # Add outliers
368 outlier_idx = np.random.choice(n_samples, n_outliers, replace=False)
369 y[outlier_idx] += np.random.randn(n_outliers) * 10
370
371 A = np.column_stack([np.ones(n_samples), x])
372
373 print(f"\nData: {n_samples} samples with {n_outliers} outliers")
374 print(f"True parameters: intercept={true_intercept}, slope={true_slope}")
375
376 # OLS (sensitive to outliers)
377 result_ols = ordinary_least_squares(A, y)
378 print(f"\nOLS: intercept={result_ols.x[0]:.4f}, slope={result_ols.x[1]:.4f}")
379
380 # Huber M-estimator
381 result_huber = huber_regression(A, y)
382 print(f"Huber: intercept={result_huber.x[0]:.4f}, slope={result_huber.x[1]:.4f}")
383 print(f" Iterations: {result_huber.n_iter}, Converged: {result_huber.converged}")
384
385 # Tukey bisquare M-estimator
386 result_tukey = tukey_regression(A, y)
387 print(f"Tukey: intercept={result_tukey.x[0]:.4f}, slope={result_tukey.x[1]:.4f}")
388 print(f" Iterations: {result_tukey.n_iter}, Converged: {result_tukey.converged}")
389
390 # Analyze weights from Tukey estimator
391 weights = result_tukey.weights
392 detected_outliers = np.where(weights < 0.1)[0]
393 print(f"\nDetected outliers (low weight): {detected_outliers}")
394 print(f"True outlier indices: {sorted(outlier_idx)}")
395
396 # Plot robust estimation comparison
397 if SHOW_PLOTS:
398 fig = make_subplots(
399 rows=1,
400 cols=2,
401 subplot_titles=[
402 "Robust Estimation: OLS vs M-estimators",
403 "Tukey M-estimator Weights (red = true outliers)",
404 ],
405 )
406
407 # Fit comparison
408 # Regular data points
409 regular_mask = np.ones(n_samples, dtype=bool)
410 regular_mask[outlier_idx] = False
411
412 fig.add_trace(
413 go.Scatter(
414 x=x[regular_mask],
415 y=y[regular_mask],
416 mode="markers",
417 marker=dict(color="blue", size=8, opacity=0.6),
418 name="Data",
419 ),
420 row=1,
421 col=1,
422 )
423
424 fig.add_trace(
425 go.Scatter(
426 x=x[outlier_idx],
427 y=y[outlier_idx],
428 mode="markers",
429 marker=dict(color="red", size=10, opacity=0.8),
430 name="Outliers",
431 ),
432 row=1,
433 col=1,
434 )
435
436 x_line = np.linspace(x.min(), x.max(), 100)
437
438 fig.add_trace(
439 go.Scatter(
440 x=x_line,
441 y=true_intercept + true_slope * x_line,
442 mode="lines",
443 line=dict(color="green", width=2, dash="dash"),
444 name="True",
445 ),
446 row=1,
447 col=1,
448 )
449
450 fig.add_trace(
451 go.Scatter(
452 x=x_line,
453 y=result_ols.x[0] + result_ols.x[1] * x_line,
454 mode="lines",
455 line=dict(color="black", width=2),
456 name="OLS",
457 ),
458 row=1,
459 col=1,
460 )
461
462 fig.add_trace(
463 go.Scatter(
464 x=x_line,
465 y=result_huber.x[0] + result_huber.x[1] * x_line,
466 mode="lines",
467 line=dict(color="magenta", width=2, dash="dash"),
468 name="Huber",
469 ),
470 row=1,
471 col=1,
472 )
473
474 fig.add_trace(
475 go.Scatter(
476 x=x_line,
477 y=result_tukey.x[0] + result_tukey.x[1] * x_line,
478 mode="lines",
479 line=dict(color="cyan", width=2, dash="dot"),
480 name="Tukey",
481 ),
482 row=1,
483 col=1,
484 )
485
486 # Weights from Tukey estimator
487 colors = ["red" if i in outlier_idx else "blue" for i in range(n_samples)]
488 fig.add_trace(
489 go.Scatter(
490 x=x,
491 y=weights,
492 mode="markers",
493 marker=dict(color=colors, size=8, opacity=0.7),
494 name="Weights",
495 showlegend=False,
496 ),
497 row=1,
498 col=2,
499 )
500
501 fig.add_hline(y=0.1, line_dash="dash", line_color="red", row=1, col=2)
502
503 fig.update_xaxes(title_text="x", row=1, col=1)
504 fig.update_yaxes(title_text="y", row=1, col=1)
505 fig.update_xaxes(title_text="x", row=1, col=2)
506 fig.update_yaxes(title_text="Tukey weight", row=1, col=2)
507
508 fig.update_layout(height=500, width=1200, showlegend=True)
509 fig.write_html(
510 str(OUTPUT_DIR / "static_robust_estimation.html"),
511 include_plotlyjs="cdn",
512 div_id="static_robust_estimation",
513 )
514 print("\n [Plot saved to static_robust_estimation.html]")
515
516
517def demo_ransac():
518 """Demonstrate RANSAC for robust fitting."""
519 print("\n" + "=" * 70)
520 print("RANSAC Demo")
521 print("=" * 70)
522
523 np.random.seed(42)
524
525 # Line fitting with many outliers
526 n_inliers = 40
527 n_outliers = 20
528
529 # Inliers: points on line y = 2x + 1
530 x_inliers = np.random.uniform(0, 10, n_inliers)
531 y_inliers = 1 + 2 * x_inliers + np.random.randn(n_inliers) * 0.3
532
533 # Outliers: random points
534 x_outliers = np.random.uniform(0, 10, n_outliers)
535 y_outliers = np.random.uniform(-5, 25, n_outliers)
536
537 x = np.concatenate([x_inliers, x_outliers])
538 y = np.concatenate([y_inliers, y_outliers])
539
540 # Shuffle
541 perm = np.random.permutation(len(x))
542 x, y = x[perm], y[perm]
543
544 A = np.column_stack([np.ones(len(x)), x])
545
546 print(f"\nData: {n_inliers} inliers + {n_outliers} outliers = {len(x)} total")
547 print("True line: y = 2x + 1")
548
549 # OLS
550 result_ols = ordinary_least_squares(A, y)
551 print(f"\nOLS: y = {result_ols.x[1]:.4f}x + {result_ols.x[0]:.4f}")
552
553 # RANSAC
554 threshold = 1.0 # Inlier threshold (residual threshold)
555
556 result_ransac = ransac(
557 A, y, min_samples=2, residual_threshold=threshold, max_trials=100
558 )
559
560 print(f"RANSAC: y = {result_ransac.x[1]:.4f}x + {result_ransac.x[0]:.4f}")
561 print(f" Inliers found: {result_ransac.n_inliers}/{len(x)}")
562
563 print("\nNote: RANSAC successfully recovers the true line despite")
564 print("33% of the data being outliers.")
565
566 # Plot RANSAC
567 if SHOW_PLOTS:
568 fig = go.Figure()
569
570 # Determine inliers based on RANSAC residuals
571 y_pred = result_ransac.x[0] + result_ransac.x[1] * x
572 residuals = np.abs(y - y_pred)
573 is_inlier = residuals < threshold
574
575 fig.add_trace(
576 go.Scatter(
577 x=x[is_inlier],
578 y=y[is_inlier],
579 mode="markers",
580 marker=dict(color="blue", size=8, opacity=0.6),
581 name="Inliers",
582 )
583 )
584
585 fig.add_trace(
586 go.Scatter(
587 x=x[~is_inlier],
588 y=y[~is_inlier],
589 mode="markers",
590 marker=dict(color="red", size=8, opacity=0.6),
591 name="Outliers",
592 )
593 )
594
595 x_line = np.linspace(x.min(), x.max(), 100)
596
597 fig.add_trace(
598 go.Scatter(
599 x=x_line,
600 y=1 + 2 * x_line,
601 mode="lines",
602 line=dict(color="green", width=2, dash="dash"),
603 name="True line",
604 )
605 )
606
607 fig.add_trace(
608 go.Scatter(
609 x=x_line,
610 y=result_ols.x[0] + result_ols.x[1] * x_line,
611 mode="lines",
612 line=dict(color="black", width=2),
613 name="OLS",
614 )
615 )
616
617 fig.add_trace(
618 go.Scatter(
619 x=x_line,
620 y=result_ransac.x[0] + result_ransac.x[1] * x_line,
621 mode="lines",
622 line=dict(color="red", width=2),
623 name="RANSAC",
624 )
625 )
626
627 fig.update_layout(
628 title=f"RANSAC Line Fitting ({result_ransac.n_inliers} inliers / {len(x)} total)",
629 xaxis_title="x",
630 yaxis_title="y",
631 height=500,
632 width=800,
633 showlegend=True,
634 )
635 fig.write_html(
636 str(OUTPUT_DIR / "static_ransac.html"),
637 include_plotlyjs="cdn",
638 div_id="static_ransac",
639 )
640 print("\n [Plot saved to static_ransac.html]")
641
642
643def demo_mle_and_fisher():
644 """Demonstrate MLE and Fisher information."""
645 print("\n" + "=" * 70)
646 print("Maximum Likelihood Estimation Demo")
647 print("=" * 70)
648
649 np.random.seed(42)
650
651 # Generate Gaussian data
652 n_samples = 100
653 true_mean = 5.0
654 true_std = 2.0
655
656 data = true_mean + np.random.randn(n_samples) * true_std
657
658 print(f"\nTrue parameters: mean={true_mean}, std={true_std}")
659 print(f"Sample size: {n_samples}")
660
661 # MLE for Gaussian parameters
662 # theta[0] = mean, theta[1] = variance
663 result = mle_gaussian(data)
664 mle_mean = result.theta[0]
665 mle_var = result.theta[1]
666 mle_std = np.sqrt(mle_var)
667 print(f"\nMLE estimates: mean={mle_mean:.4f}, std={mle_std:.4f}")
668 print(f"Log-likelihood: {result.log_likelihood:.4f}")
669
670 # Fisher information
671 # For Gaussian: I(μ) = n/σ², I(σ²) = n/(2σ⁴)
672 fisher_mean = n_samples / true_std**2
673 fisher_var = n_samples / (2 * true_std**4)
674
675 print(f"\nFisher information:")
676 print(f" I(mean) = {fisher_mean:.4f}")
677 print(f" I(variance) = {fisher_var:.4f}")
678
679 # Cramer-Rao bounds
680 crb_mean = 1 / fisher_mean
681 crb_var = 1 / fisher_var
682
683 print(f"\nCramer-Rao lower bounds (variance of unbiased estimator):")
684 print(f" Var(mean_hat) >= {crb_mean:.6f}")
685 print(f" Var(var_hat) >= {crb_var:.6f}")
686
687 # Check actual MLE variance (through simulation)
688 n_trials = 1000
689 mean_estimates = []
690 for _ in range(n_trials):
691 sample = true_mean + np.random.randn(n_samples) * true_std
692 result = mle_gaussian(sample)
693 mean_estimates.append(result.theta[0])
694
695 actual_variance = np.var(mean_estimates)
696 print(f"\nSimulated variance of mean estimator: {actual_variance:.6f}")
697 print(f"Efficiency: {crb_mean / actual_variance:.4f}")
698 print("(Efficiency = 1.0 means estimator achieves the CRB)")
699
700
701def demo_model_selection():
702 """Demonstrate model selection using information criteria."""
703 print("\n" + "=" * 70)
704 print("Model Selection Demo")
705 print("=" * 70)
706
707 np.random.seed(42)
708
709 # True model: quadratic y = 1 + 2x + 0.5x²
710 n_samples = 50
711 x = np.linspace(0, 5, n_samples)
712 y_true = 1 + 2 * x + 0.5 * x**2
713 y = y_true + np.random.randn(n_samples) * 0.5
714
715 print("\nTrue model: y = 1 + 2x + 0.5x²")
716 print("Comparing polynomial models of degree 1 through 5:")
717 print("-" * 60)
718 print(f"{'Degree':>6} {'RSS':>10} {'k':>4} {'AIC':>10} {'BIC':>10}")
719 print("-" * 60)
720
721 results = []
722 for degree in range(1, 6):
723 # Build design matrix for polynomial
724 A = np.column_stack([x**i for i in range(degree + 1)])
725 result = ordinary_least_squares(A, y)
726
727 rss = np.sum(result.residuals**2)
728 k = degree + 1 # Number of parameters
729 n = n_samples
730
731 # Compute log-likelihood (assuming Gaussian errors)
732 sigma2 = rss / n
733 log_lik = -n / 2 * np.log(2 * np.pi * sigma2) - rss / (2 * sigma2)
734
735 aic_val = aic(log_lik, k)
736 bic_val = bic(log_lik, k, n)
737
738 results.append((degree, rss, k, aic_val, bic_val))
739 print(f"{degree:>6} {rss:>10.4f} {k:>4} {aic_val:>10.2f} {bic_val:>10.2f}")
740
741 # Find best models
742 best_aic = min(results, key=lambda x: x[3])
743 best_bic = min(results, key=lambda x: x[4])
744
745 print(f"\nBest by AIC: degree {best_aic[0]}")
746 print(f"Best by BIC: degree {best_bic[0]}")
747 print("\nNote: True model is degree 2. AIC/BIC help avoid overfitting.")
748
749 # Plot model selection
750 if SHOW_PLOTS:
751 fig = make_subplots(
752 rows=1,
753 cols=2,
754 subplot_titles=["Polynomial Model Fits", "Model Selection: AIC vs BIC"],
755 )
756
757 # Model fits
758 fig.add_trace(
759 go.Scatter(
760 x=x,
761 y=y,
762 mode="markers",
763 marker=dict(color="blue", size=8, opacity=0.6),
764 name="Data",
765 ),
766 row=1,
767 col=1,
768 )
769
770 x_line = np.linspace(x.min(), x.max(), 100)
771 fig.add_trace(
772 go.Scatter(
773 x=x_line,
774 y=1 + 2 * x_line + 0.5 * x_line**2,
775 mode="lines",
776 line=dict(color="green", width=2, dash="dash"),
777 name="True (deg 2)",
778 ),
779 row=1,
780 col=1,
781 )
782
783 # Fit degrees 1, 2, 3
784 colors = ["red", "green", "purple"]
785 for i, deg in enumerate([1, 2, 3]):
786 A_fit = np.column_stack([x_line**j for j in range(deg + 1)])
787 A_data = np.column_stack([x**j for j in range(deg + 1)])
788 coef = ordinary_least_squares(A_data, y).x
789 y_fit = A_fit @ coef
790 fig.add_trace(
791 go.Scatter(
792 x=x_line,
793 y=y_fit,
794 mode="lines",
795 line=dict(width=1.5, dash="solid" if deg == 2 else "dot"),
796 name=f"Degree {deg}",
797 ),
798 row=1,
799 col=1,
800 )
801
802 # AIC/BIC comparison
803 degrees = [r[0] for r in results]
804 aic_vals = [r[3] for r in results]
805 bic_vals = [r[4] for r in results]
806
807 fig.add_trace(
808 go.Bar(
809 x=[d - 0.2 for d in degrees],
810 y=aic_vals,
811 width=0.35,
812 name="AIC",
813 marker_color="blue",
814 opacity=0.7,
815 ),
816 row=1,
817 col=2,
818 )
819
820 fig.add_trace(
821 go.Bar(
822 x=[d + 0.2 for d in degrees],
823 y=bic_vals,
824 width=0.35,
825 name="BIC",
826 marker_color="orange",
827 opacity=0.7,
828 ),
829 row=1,
830 col=2,
831 )
832
833 fig.update_xaxes(title_text="x", row=1, col=1)
834 fig.update_yaxes(title_text="y", row=1, col=1)
835 fig.update_xaxes(title_text="Polynomial Degree", row=1, col=2)
836 fig.update_yaxes(title_text="Information Criterion", row=1, col=2)
837
838 fig.update_layout(height=500, width=1200, showlegend=True)
839 fig.write_html(
840 str(OUTPUT_DIR / "static_model_selection.html"),
841 include_plotlyjs="cdn",
842 div_id="static_model_selection",
843 )
844 print("\n [Plot saved to static_model_selection.html]")
845
846
847def main():
848 """Run all demonstrations."""
849 print("\n" + "#" * 70)
850 print("# PyTCL Static Estimation Example")
851 print("#" * 70)
852
853 # Least squares methods
854 demo_ordinary_least_squares()
855 demo_weighted_least_squares()
856 demo_total_least_squares()
857 demo_recursive_least_squares()
858 demo_ridge_regression()
859
860 # Robust estimation
861 demo_robust_estimation()
862 demo_ransac()
863
864 # MLE and model selection
865 demo_mle_and_fisher()
866 demo_model_selection()
867
868 print("\n" + "=" * 70)
869 print("Example complete!")
870 if SHOW_PLOTS:
871 print("Plots saved: static_ols.html, static_robust_estimation.html,")
872 print(" static_ransac.html, static_model_selection.html")
873 print("=" * 70)
874
875
876if __name__ == "__main__":
877 main()
Running the Example
python examples/static_estimation.py
See Also
Kalman Filter Comparison - Dynamic estimation
Performance Evaluation - Tracking metrics
Gaussian Mixtures and Clustering - Clustering and mixture models