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