# Copyright 2007 Eric Kemp-Benedict
# This library may be used and modified without notification to the author.
# It would be greatly appreciated if corrections and improvements were
# submitted to the appropriate page on the Tcler's Wiki.
package require Tcl 8.4
package require math::linearalgebra 1.0
package require math::statistics 0.1.1
# mvlinreg = Multivariate Linear Regression
namespace eval mvlinreg {
variable epsilon 1.0e-7
namespace import -force \
::math::linearalgebra::mkMatrix \
::math::linearalgebra::mkVector \
::math::linearalgebra::mkIdentity \
::math::linearalgebra::mkDiagonal \
::math::linearalgebra::getrow \
::math::linearalgebra::setrow \
::math::linearalgebra::getcol \
::math::linearalgebra::setcol \
::math::linearalgebra::getelem \
::math::linearalgebra::setelem \
::math::linearalgebra::dotproduct \
::math::linearalgebra::matmul \
::math::linearalgebra::add \
::math::linearalgebra::sub \
::math::linearalgebra::solveGauss
# NOTE: The authors of math::linearalgebra forgot to
# export ::math::linearalgebra::transpose
########################################
#
# t-stats
#
########################################
# mvlinreg::tstat n ?alpha?
# Returns inverse of the single-tailed t distribution
# given number of degrees of freedom & confidence
# Defaults to alpha = 0.05
# Iterates until result is within epsilon of the target
proc tstat {n {alpha 0.05}} {
variable epsilon
variable tvals
if [info exists tvals($n:$alpha)] {
return $tvals($n:$alpha)
}
set deltat [expr {100 * $epsilon}]
# For one-tailed distribution,
set ptarg [expr {1.000 - $alpha/2.0}]
set maxiter 100
# Initial value for t
set t 2.0
set niter 0
while {abs([::math::statistics::cdf-students-t $n $t] - $ptarg) > $epsilon} {
set pstar [::math::statistics::cdf-students-t $n $t]
set pl [::math::statistics::cdf-students-t $n [expr {$t - $deltat}]]
set ph [::math::statistics::cdf-students-t $n [expr {$t + $deltat}]]
set t [expr {$t + 2.0 * $deltat * ($ptarg - $pstar)/($ph - $pl)}]
incr niter
if {$niter == $maxiter} {
error "mvlinreg::tstat: Did not converge after $niter iterations"
return {}
}
}
# Cache the result to shorten the call in future
set tvals($n:$alpha) $t
return $t
}
########################################
#
# Weighted Least Squares
#
########################################
# mvlinreg::wls w [list y x's] w [list y x's] ...
# Returns:
# R-squared
# Adj R-squared
# coefficients of x's in fit
# standard errors of the coefficients
# 95% confidence bounds for coefficients
proc wls {args} {
# Fill the matrices of x & y values, and weights
# For n points, k coefficients
# The number of points is equal to half the arguments (n weights, n points)
set n [expr {[llength $args]/2}]
set firstloop true
# Sum up all y values to take an average
set ysum 0
# Add up the weights
set wtsum 0
# Count over rows (points) as you go
set point 0
foreach {wt pt} $args {
# Check inputs
if {[string is double $wt] == 0} {
error "mvlinreg::wls: Weight \"$wt\" is not a number"
return {}
}
## -- Check dimensions, initialize
if $firstloop {
# k = num of vals in pt = 1 + number of x's (because of constant)
set k [llength $pt]
if {$n <= [expr {$k + 1}]} {
error "mvlinreg::wls: Too few degrees of freedom: $k variables but only $n points"
return {}
}
set X [mkMatrix $n $k]
set y [mkVector $n]
set I_x [mkIdentity $k]
set I_y [mkIdentity $n]
set firstloop false
} else {
# Have to have same number of x's for all points
if {$k != [llength $pt]} {
error "mvlinreg::wls: Point \"$pt\" has wrong number of values (expected $k)"
# Clean up
return {}
}
}
## -- Extract values from set of points
# Make a list of y values
set yval [expr {double([lindex $pt 0])}]
setelem y $point $yval
set ysum [expr {$ysum + $wt * $yval}]
set wtsum [expr {$wtsum + $wt}]
# Add x-values to the x-matrix
set xrow [lrange $pt 1 end]
# Add the constant (value = 1.0)
lappend xrow 1.0
setrow X $point $xrow
# Create list of weights & square root of weights
lappend w [expr {double($wt)}]
lappend sqrtw [expr {sqrt(double($wt))}]
incr point
}
set ymean [expr {double($ysum)/$wtsum}]
set W [mkDiagonal $w]
set sqrtW [mkDiagonal $sqrtw]
# Calculate sum os square differences for x's
for {set r 0} {$r < $k} {incr r} {
set xsqrsum 0.0
set xmeansum 0.0
# Calculate sum of squared differences as: sum(x^2) - (sum x)^2/n
for {set t 0} {$t < $n} {incr t} {
set xval [getelem $X $t $r]
set xmeansum [expr {$xmeansum + double($xval)}]
set xsqrsum [expr {$xsqrsum + double($xval * $xval)}]
}
lappend xsqr [expr {$xsqrsum - $xmeansum * $xmeansum/$n}]
}
## -- Set up the X'WX matrix
set XtW [matmul [::math::linearalgebra::transpose $X] $W]
set XtWX [matmul $XtW $X]
# Invert
set M [solveGauss $XtWX $I_x]
set beta [matmul $M [matmul $XtW $y]]
### -- Residuals & R-squared
# 1) Generate list of diagonals of the hat matrix
set H [matmul $X [matmul $M $XtW]]
for {set i 0} {$i < $n} {incr i} {
lappend h_ii [getelem $H $i $i]
}
set R [matmul $sqrtW [matmul [sub $I_y $H] $y]]
set yhat [matmul $H $y]
# 2) Generate list of residuals, sum of squared residuals, r-squared
set sstot 0.0
set ssreg 0.0
# Note: Relying on representation of Vector as a list for y, yhat
foreach yval $y wt $w yhatval $yhat {
set sstot [expr {$sstot + $wt * ($yval - $ymean) * ($yval - $ymean)}]
set ssreg [expr {$ssreg + $wt * ($yhatval - $ymean) * ($yhatval - $ymean)}]
}
set r2 [expr {double($ssreg)/$sstot}]
set adjr2 [expr {1.0 - (1.0 - $r2) * ($n - 1)/($n - $k)}]
set sumsqresid [dotproduct $R $R]
set s2 [expr {double($sumsqresid) / double($n - $k)}]
### -- Confidence intervals for coefficients
set tvalue [tstat [expr {$n - $k}]]
for {set i 0} {$i < $k} {incr i} {
set stderr [expr {sqrt($s2 * [getelem $M $i $i])}]
set mid [lindex $beta $i]
lappend stderrs $stderr
lappend confinterval [list [expr {$mid - $tvalue * $stderr}] [expr {$mid + $tvalue * $stderr}]]
}
return [list $r2 $adjr2 $beta $stderrs $confinterval]
}
########################################
#
# Ordinary (unweighted) Least Squares
#
########################################
# mvlinreg::ols [list y x's] [list y x's] ...
# Returns:
# R-squared
# Adj R-squared
# coefficients of x's in fit
# standard errors of the coefficients
# 95% confidence bounds for coefficients
proc ols {args} {
set newargs {}
foreach pt $args {
lappend newargs 1 $pt
}
return [eval wls $newargs]
}
}
if 0 {
Here's an example:
}
##
##
## TEST IT
##
##
set data {{183.34 100. 9.43} \
{222.67 100. 9.14} \
{410.77 20.3 8.81} \
{268.57 17.53 8.74} \
{499.28 10.95 8.71} \
{385.97 51.81 8.78} \
{474.53 17.01 8.74} \
{550.04 16.6 8.72} \
{221.54 100. 9.49} \
{284.93 49.9 9.01} \
{324.19 23.02 9.37} \
{364.6 16.59 8.7} \
{338.62 20.03 9.13} \
{543.19 13.44 8.75} \
{356.96 25.64 8.94} \
{296.54 18.63 8.74} \
{336.98 50.07 8.82} \
{398.5 15.35 8.85} \
{216.37 47.41 9.32} \
{325.02 14.83 8.9} \
{315.55 9.43 8.83} \
{321.57 76.98 8.89} \
{443.21 51.28 8.8} \
{493.22 13.19 8.69} \
{554.79 19.62 8.89} \
{527.46 63.23 8.75} \
{490.59 10.72 8.72} \
{389.31 14.39 8.73} \
{218.9 8.65 8.71} \
{315.65 7.29 8.84}}
set results [eval mvlinreg::ols $data]
puts "R-squared: [lindex $results 0]"
puts "Adj R-squared: [lindex $results 1]"
puts "Coefficients \u00B1 s.e. -- \[95% confidence interval\]:"
foreach val [lindex $results 2] se [lindex $results 3] bounds [lindex $results 4] {
set lb [lindex $bounds 0]
set ub [lindex $bounds 1]
puts " $val \u00B1 $se -- \[$lb to $ub\]"
}
if 0 {
This should give the following output:
R-squared: 0.371918955025
Adj R-squared: 0.325394433175
Coefficients ± s.e. -- [95% confidence interval]:
-0.432215837601 ± 0.744774375289 -- [-1.96036662835 to 1.09593495315]
-250.868151309 ± 92.4723901677 -- [-440.605823342 to -61.1304792767]
2615.78338226 ± 807.559562552 -- [958.808028331 to 4272.75873618]
}AM (19 february 2007) You are welcome to contribute this to Tcllib :) Just say the word, and I will incorporate it in the package.EKB I'd be delighted to have it in Tcllib! What should I do to make that happen?LV One place developers can start is visiting http://tcllib.sf.net/
and posting a new Feature Request, specifying that you are the creator of the code in question, specifying the license you use (tcllib uses BSD based licensing, I believe), and mention that we'd asked you to contribute it. Man pages, test cases, demos, etc. are all wonderful things to have available, if possible. But if they are not available and you don't have time/interest in generating them, then perhaps someone who is a fan of the module could write them.EKB Thanks. Sounds good. I'll work on creating demos and a man page.EKB Done! I've submitted it, with a plain text man page (no *roff) to tcllib at sf.net. I hope I've included what's needed![Philou] - 2015-01-09 09:59:57Nice ! Thanks for this

