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