Mann-Kendall Test w/Sen's Slope

by cormip

HTML

<h2>Mann Kendall Trend Test with Sen's Slope Calculation</h2>
<label>Values: <input style="width:100%;" id="values" value="6.8, 5.9, 5.7, 5.5, 5.5, 4.5, 4, 5.1, 4.5, 4.5, 3.3, 4.8"></label><br />
<small>comma-delimited numeric values</small><br />
<br />
<label>alpha <input id="alpha" value="0.05"></label><br />
<br />
<button id="btnMKtest">Run Mann-Kendall Test</button>

<div id="mk" style="display: none;">
  <h3>Mann Kendall Test Results</h3>
  <table>
    <tr><td align=right>n:</td><td><span id="n"></span></td></tr>
    <tr><td align=right>MK-stat:</td><td><span id="mkstat"></span></td></tr>
    <tr><td align=right>stdErr:</td><td><span id="stderr"></span></td></tr>
    <tr><td align=right>z-stat:</td><td><span id="zstat"></span></td></tr>
    <tr><td align=right>p-value:</td><td><span id="pvalue"></span></td></tr>
    <tr><td align=right>trend:</td><td><strong id="trend"></strong></td></tr>
  </table>
</div>

<div id="sen" style="display: none;">
  <h3>Sen's Slope</h3>
  <table>
    <tr><td align=right>pairCount:</td><td><span id="paircount"></span></td></tr>
    <tr><td align=right>k:</td><td><span id="k"></span></td></tr>
    <tr><td align=right>lower:</td><td><span id="lower"></span></td></tr>
    <tr><td align=right>upper:</td><td><span id="upper"></span></td></tr>
    <tr><td colspan="2">&nbsp;</td></tr>
    <tr><td align=right>slope:</td><td><span id="slope"></span></td></tr>
    <tr><td align=right>confLower:</td><td><span id="conflower"></span></td></tr>
    <tr><td align=right>confUpper:</td><td><span id="confupper"></span></td></tr>
  </table>
</div>

JavaScript 1.7

//Special thanks to: https://www.real-statistics.com/time-series-analysis/time-series-miscellaneous/mann-kendall-test/

const erf = x => {
  //A&S formula 7.1.26
  const 
  	a1 = 0.254829592,
    a2 = -0.284496736,
    a3 = 1.421413741,
    a4 = -1.453152027,
    a5 = 1.061405429,
    p = 0.3275911

  x = Math.abs(x);
  const t = 1 / (1 + p * x);
  //Direct calculation using formula 7.1.26 is absolutely correct
  //But calculation of nth order polynomial takes O(n^2) operations
  //return 1 - (a1 * t + a2 * t * t + a3 * t * t * t + a4 * t * t * t * t + a5 * t * t * t * t * t) * Math.Exp(-1 * x * x);

  //Horner's method, takes O(n) operations for nth order polynomial
  return 1 - ((((((a5 * t + a4) * t) + a3) * t + a2) * t) + a1) * t * Math.exp(-1 * x * x);
}

const NORMSDIST = z => {
  let sign = 1;
  if (z < 0) sign = -1;
  return 0.5 * (1.0 + sign * erf(Math.abs(z)/Math.sqrt(2)))
}

const NORMSINV = p => {
    const 
    	a1 = -39.6968302866538, 
      a2 = 220.946098424521, 
      a3 = -275.928510446969,
      a4 = 138.357751867269, 
      a5 = -30.6647980661472, 
      a6 = 2.50662827745924,
      b1 = -54.4760987982241, 
      b2 = 161.585836858041, 
      b3 = -155.698979859887,
      b4 = 66.8013118877197, 
      b5 = -13.2806815528857, 
      c1 = -7.78489400243029E-03,
      c2 = -0.322396458041136, 
      c3 = -2.40075827716184, 
      c4 = -2.54973253934373,
      c5 = 4.37466414146497, 
      c6 = 2.93816398269878, 
      d1 = 7.78469570904146E-03,
    	d2 = 0.32246712907004, 
      d3 = 2.445134137143, 
      d4 = 3.75440866190742,
      p_low = 0.02425, 
      p_high = 1 - p_low
    let q, r, retVal

    if ((p < 0) || (p > 1)) {
        console.log("NORMSINV: Argument out of range.");
        retVal = 0;
    } else if (p < p_low) {
        q = Math.sqrt(-2 * Math.log(p))
        retVal = (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1)
    } else if (p <= p_high) {
        q = p - 0.5
      ...