MODULE C15Stats ;
(* Chapter 15: statistics on a week of eddy-covariance data -- a
regression of net ecosystem exchange on air temperature, a test of
whether warm and cool half-hours differ, and a thousand draws from
a normal distribution to show what a p-value actually says.
Same file as chapter 14 (the shipped week, or a path given on the
command line); this time only two columns and no time series. *)
IMPORT Io ;
IMPORT Faults ;
IMPORT Fmt ;
IMPORT Csv ;
IMPORT Math ;
IMPORT Stats ;
CONST
Default = 'data/fluxnet_week.csv' ;
Trials = 1000 ;
SeedValue = 20260906 ; (* a fixed seed: the draws are reproducible *)
(* the half-hours where BOTH values are present, as the two F64
slices the statistics take -- a gap in either drops the pair *)
PROCEDURE Pairs (VAR pool: POOL ; RO ta, nee: SLICE OF F32 ;
VAR x, y: SLICE OF F64) RAISES ValueRange =
VAR i, n : I64 ;
BEGIN
n := 0 ;
FOR i := 0 TO LEN (ta) - 1 DO
IF NOT (Math.IsNaNF32 (ta[i]) OR Math.IsNaNF32 (nee[i])) THEN
n := n + 1
END
END ;
x := NEW (pool, F64, n) ;
y := NEW (pool, F64, n) ;
n := 0 ;
FOR i := 0 TO LEN (ta) - 1 DO
IF NOT (Math.IsNaNF32 (ta[i]) OR Math.IsNaNF32 (nee[i])) THEN
x[n] := F64 (ta[i]) ;
y[n] := F64 (nee[i]) ;
n := n + 1
END
END
END Pairs ;
(* the y values whose x lies below the cut, and those at or above it *)
PROCEDURE Split (VAR pool: POOL ; RO x, y: SLICE OF F64 ; cut: F64 ;
VAR lo, hi: SLICE OF F64) =
VAR i, nl, nh : I64 ;
BEGIN
nl := 0 ; nh := 0 ;
FOR i := 0 TO LEN (x) - 1 DO
IF x[i] < cut THEN nl := nl + 1 ELSE nh := nh + 1 END
END ;
lo := NEW (pool, F64, nl) ;
hi := NEW (pool, F64, nh) ;
nl := 0 ; nh := 0 ;
FOR i := 0 TO LEN (x) - 1 DO
IF x[i] < cut THEN lo[nl] := y[i] ; nl := nl + 1
ELSE hi[nh] := y[i] ; nh := nh + 1 END
END
END Split ;
PROCEDURE Abs (v: F64) : F64 =
BEGIN
IF v < 0.0 THEN RETURN -v END ;
RETURN v
END Abs ;
VAR
pool : POOL ;
path : STR ;
opt : Csv.Options ;
t : PTR Csv.Table IN pool ;
cNee, cTa, c, i, k, hits : I64 ;
ta, nee : SLICE OF F32 ;
x, y, cool, warm, ysim : SLICE OF F64 ;
reg, sim : Stats.Reg ;
tt : Stats.Test ;
fit : Stats.Fit ;
st : Stats.Stream ;
cut, observed : F64 ;
what : STR ;
ln : I64 ;
BEGIN
BEGIN
path := Default ;
IF Io.ArgCount () > 1 THEN path := Io.Arg (pool, 1) END ;
(* ---- the two columns, everything else skipped ---- *)
opt := Csv.Defaults () ;
opt.missing := -9999.0 ;
opt.hasMissing := TRUE ;
t := Csv.Open (pool, path, opt) ;
cNee := Csv.Find (t, 'NEE_VUT_REF') ;
cTa := Csv.Find (t, 'TA_F') ;
IF (cNee < 0) OR (cTa < 0) THEN
Io.ErrLine ('the file has no NEE_VUT_REF or no TA_F column') ;
Io.Halt (1)
END ;
Csv.SetReal (t, cNee) ;
Csv.SetReal (t, cTa) ;
Csv.Parse (pool, t) ;
ta := Csv.ColF32 (t, cTa) ;
nee := Csv.ColF32 (t, cNee) ;
Pairs (pool, ta, nee, x, y) ;
Io.WriteLine (Fmt.I64Str (LEN (x)) + ' half-hours with both NEE and TA') ;
(* ---- 1. the regression: NEE = intercept + slope * TA ---- *)
reg := Stats.LinReg (x, y) ;
Io.WriteLine ('') ;
Io.WriteLine ('NEE on TA, least squares:') ;
Io.WriteLine (' slope ' + Fmt.Fixed (reg.slope, 4)
+ ' umol m-2 s-1 per degC (stderr '
+ Fmt.Fixed (reg.stderr, 4) + ')') ;
Io.WriteLine (' intercept ' + Fmt.Fixed (reg.intercept, 3)
+ ' umol m-2 s-1') ;
Io.WriteLine (' r ' + Fmt.Fixed (reg.r, 3)
+ ' r^2 ' + Fmt.Fixed (reg.r * reg.r, 3)) ;
Io.WriteLine (' p ' + Fmt.Sci (reg.p, 2)
+ ' (two-sided, against slope = 0)') ;
(* ---- 2. warm half-hours against cool ones: Welch's t ---- *)
cut := Stats.Median (x) ;
Split (pool, x, y, cut, cool, warm) ;
tt := Stats.TTest2 (warm, cool) ;
Io.WriteLine ('') ;
Io.WriteLine ('NEE when TA is above its median ' + Fmt.Fixed (cut, 2)
+ ' degC against below it:') ;
Io.WriteLine (' warm mean ' + Fmt.Fixed (Stats.Mean (warm), 3)
+ ' (n ' + Fmt.I64Str (LEN (warm)) + ')') ;
Io.WriteLine (' cool mean ' + Fmt.Fixed (Stats.Mean (cool), 3)
+ ' (n ' + Fmt.I64Str (LEN (cool)) + ')') ;
Io.WriteLine (' t ' + Fmt.Fixed (tt.t, 2) + ' dof '
+ Fmt.Fixed (tt.dof, 1) + ' p ' + Fmt.Sci (tt.p, 2)) ;
(* ---- 3. what the p-value means, by drawing it ----
Fit a normal to NEE, then make a thousand series with that mean
and spread but NO relation to TA, and regress each one. The
fraction whose |slope| reaches the real one is a p-value
obtained by counting instead of by a formula. *)
fit := Stats.NormFit (y) ;
st := Stats.Seed (SeedValue) ;
Io.WriteLine ('') ;
Io.WriteLine ('normal fit of NEE: mu ' + Fmt.Fixed (fit.mu, 3)
+ ', sigma ' + Fmt.Fixed (fit.sigma, 3)) ;
Io.Write ('three draws from it:') ;
FOR i := 1 TO 3 DO
Io.Write (' ' + Fmt.Fixed (fit.mu + fit.sigma * Stats.Normal (st), 3))
END ;
Io.WriteLine ('') ;
observed := Abs (reg.slope) ;
ysim := NEW (pool, F64, LEN (y)) ;
hits := 0 ;
FOR k := 1 TO Trials DO
FOR i := 0 TO LEN (ysim) - 1 DO
ysim[i] := fit.mu + fit.sigma * Stats.Normal (st)
END ;
sim := Stats.LinReg (x, ysim) ;
IF Abs (sim.slope) >= observed THEN hits := hits + 1 END
END ;
Io.WriteLine ('in ' + Fmt.I64Str (Trials)
+ ' series with that mean and spread and no relation to TA,'
+ ' |slope| reached ' + Fmt.Fixed (observed, 4) + ': '
+ Fmt.I64Str (hits) + ' times')
EXCEPT
| Csv.ParseError (whatP, lineP, colP) :
what := whatP ; ln := lineP ;
Io.ErrLine ('CSV: ' + what + ' at line ' + Fmt.I64Str (ln)) ;
Io.Halt (1)
| Csv.RangeError : Io.ErrLine ('CSV: a value out of range') ; Io.Halt (1)
| Io.IOError : Io.ErrLine ('cannot read ' + path) ; Io.Halt (1)
| Stats.TooFew : Io.ErrLine ('too few values for that statistic') ; Io.Halt (1)
| Faults.BadArg : Io.ErrLine ('a statistic refused its argument') ; Io.Halt (1)
| ValueRange : Io.ErrLine ('a value out of range') ; Io.Halt (1)
END
END C15Stats.