Arjen Markus (23 february 2025) A small experiment with a statistical technique called LOESS, locally estimated scatterplot smoothing. I intend to include it in the math::statistics module of Tcllib. The idea is quite simple, though like in most statistical topics the devil is in the details: use data points around a chosen point to cancel out noise and get a smoother curve. The Wikipedia page on the subject explains in more detail.
Basically:
The result is a much smoother line that captures the underlying function.
Of course you need to specify some size of the subset, a fraction of the total data set, so that is one free parameter.
The code below implements the LOESS algorithm, so no weighing of the data points. It is a start.
# lowess.tcl --
# Perform "locally estimated scatterplot smoothing" (LOESS) or
# "locally weighted scatterplot smoothing"
#
# Note:
# LOWESS still needs to be implemented.
#
package require math::statistics
# loess --
# Perform LOESS: result is a list of estimates of the dependent variable
#
# Arguments:
# xvalues The x-values (sorted in increasing order)
# yvalues The corresponding y-values
# alpha (Optional) fraction of the data points to be used
# per local estimate. (Default: 0.1, should be between 0 and 1)
#
# Result:
# Local estimates for the y-values
#
proc ::math::statistics::loess {xvalues yvalues {alpha 0.1}} {
variable TOOFEWDATA
if { [llength $xvalues] != [llength $yvalues] || [llength $xvalues] == 0 } {
return -code error -errorcode DATA -errorinfo $TOOFEWDATA $TOOFEWDATA
}
set number [llength $xvalues]
if { $alpha <= 0.0 || $alpha >= 1.0 } {
return -code error -errorcode ARG -errorinfo "Alpha should be between 0 and 1" \
"Alpha should be between 0 and 1"
}
set samples [expr {int($alpha * $number)}]
if { $samples < 2 } {
return -code error -errorcode ARG -errorinfo "Alpha should be at least [expr {2.0/$number}]" \
"Alpha should be at least [expr {2.0/$number}]"
}
#
# Per data point:
# - Select the set of samples to be used
# - Perform the linear regression
# - Estimate the value according to that regression for the data point
#
# Since we expect the data to be sorted, we can simply use a sublist - no need to
# collect the data in a more sophisticated way.
#
set estimates {}
for {set i 0} {$i < $number} {incr i} {
set first [expr {$i - $samples/2}]
set last [expr {$i + ($samples - $samples/2) - 1}] ;# Make sure we N samples
if { $first < 0 } {
set correction [expr {0 - $first}]
set first 0
set last [expr {$last + $correction}]
}
if { $last >= $number } {
set correction [expr {$number - $last + 1}]
set last [expr {$number - 1}]
set first [expr {$first + $correction}]
}
set x [lindex $xvalues $i]
set xsubset [lrange $xvalues $first $last]
set ysubset [lrange $yvalues $first $last]
lassign [::math::statistics::linear-model $xsubset $ysubset] A B dummy
lappend estimates [expr {$A + $B * $x}]
}
return $estimates
}To see if it works, I used this test program:
# test_lowess.tcl --
# Generate a data set with noisy data and perform
# the LOESS or LOWESS regression. The test is that
# the resulting curve looks like the nominal sine.
#
package require Plotchart
source lowess.tcl
pack [canvas .c -width 600 -height 600]
set p [::Plotchart::createXYPlot .c {0.0 10.0 2.0} {-3.0 3.0 1.0}]
$p dataconfig data -type symbol
$p dataconfig curve -colour red
$p dataconfig estimate -colour blue
for {set i 0} {$i < 100} {incr i} {
set x [expr {$i/10.0}]
set y [expr {sin($x) + 2.0 * (rand() - 0.5)}]
set z [expr {sin($x)}]
$p plot data $x $y
$p plot curve $x $z
lappend xvalues $x
lappend yvalues $y
}
set estimates [::math::statistics::loess $xvalues $yvalues]
foreach x $xvalues y $estimates {
$p plot estimate $x $y
}The result, for a particular run, was:
In this picture, the red line is the underlying curve without noise and the blue line is curve after smoothing via the given code. I used the default parameter value - use 10% of the total set.
Notes: This technique is mostly useful for enhancing scatter plots, though you could use the new y values for interpolation.
Also, the data need to be sorted in ascending order with respect to the x values. Otherwise selecting the various subsets would become more complex.