A quick bit of "Claude"-ing comes up with this:
// =====================================================================
// Mann-Kendall Trend Test + Sen's Slope (JSL)
// ---------------------------------------------------------------------
// Code created by Claude (Anthropic).
// Prompt summary: User asked for a JSL script that performs the
// Mann-Kendall test for monotonic trend in a time series (S statistic,
// tie-corrected variance, Z, p-value, Kendall's tau) plus Sen's slope
// with a confidence interval.
////
// Usage: open a data table, run this script, pick the response column
// (required), an optional time/order column, and alpha.
// =====================================================================
Names Default To Here( 1 );
// ---------------------------------------------------------------------
// Helper: Sign( v ) returns +1 if v > 0, -1 if v < 0, and 0 if v == 0
// ---------------------------------------------------------------------
Sign = Function( {v},
If( v > 0, 1, v < 0, -1, 0 )
);
// ---------------------------------------------------------------------
// STEP 1: Select data table and columns
// ---------------------------------------------------------------------
dt = Current Data Table();
If( Is Empty( dt ),
Throw( "Open a data table before running this script." )
);
dlg = Column Dialog(
yCol = ColList( "Y, Response", Max Col( 1 ), Min Col( 1 ), Data Type( Numeric ) ),
xCol = ColList( "Time / Order (optional; row order used if blank)", Max Col( 1 ) ),
HList( "Alpha", alphaBox = EditNumber( 0.05 ) )
);
If( dlg["Button"] == -1, Throw( "Cancelled." ) );
yCol = dlg["yCol"];
xCol = dlg["xCol"];
alpha = dlg["alphaBox"];
yName = Column( dt, yCol[1] ) << Get Name;
hasX = N Items( xCol ) > 0;
If( hasX, xName = Column( dt, xCol[1] ) << Get Name, xName = "Observation Order" );
// ---------------------------------------------------------------------
// STEP 2: Pull values, drop missing, and sort by time/order
// ---------------------------------------------------------------------
yAll = Column( dt, yName ) << Get Values;
nAll = N Rows( yAll );
If( hasX,
xAll = Column( dt, xName ) << Get Values,
xAll = 1 :: nAll // row order as time index
);
keep = {};
For( i = 1, i <= nAll, i++,
If( !Is Missing( yAll[i] ) & !Is Missing( xAll[i] ),
Insert Into( keep, i )
)
);
n = N Items( keep );
If( n < 4, Throw( "Need at least 4 non-missing observations." ) );
y = J( n, 1, . );
x = J( n, 1, . );
For( i = 1, i <= n, i++,
y[i] = yAll[keep[i]];
x[i] = xAll[keep[i]];
);
ord = Rank( x ); // indices that sort x ascending
y = y[ord];
x = x[ord];
// ---------------------------------------------------------------------
// STEP 3: Mann-Kendall S statistic and Sen's pairwise slopes
// ---------------------------------------------------------------------
nPairs = n * (n - 1) / 2;
slopes = J( nPairs, 1, . );
S = 0;
k = 0;
For( i = 1, i < n, i++,
For( j = i + 1, j <= n, j++,
S += Sign( y[j] - y[i] ); // +1 up, -1 down, 0 tie
If( x[j] != x[i], // skip pairs with identical time stamps
k++;
slopes[k] = (y[j] - y[i]) / (x[j] - x[i]);
);
)
);
slopes = slopes[1 :: k]; // trim unused preallocated slots
nSlopes = k;
// ---------------------------------------------------------------------
// STEP 4: Tie-corrected variance of S
// ---------------------------------------------------------------------
ySorted = Sort Ascending( y );
tieTerm = 0;
runLen = 1;
For( i = 2, i <= n + 1, i++,
If( i <= n & ySorted[i] == ySorted[i - 1],
runLen += 1, // still inside a tied group
If( runLen > 1, tieTerm += runLen * (runLen - 1) * (2 * runLen + 5) );
runLen = 1;
)
);
varS = (n * (n - 1) * (2 * n + 5) - tieTerm) / 18;
// ---------------------------------------------------------------------
// STEP 5: Z statistic (continuity corrected), p-value, Kendall's tau
// ---------------------------------------------------------------------
If( S > 0,
Z = (S - 1) / Sqrt( varS ),
S < 0,
Z = (S + 1) / Sqrt( varS ),
Z = 0
);
pValue = 2 * (1 - Normal Distribution( Abs( Z ) ));
tau = S / nPairs; // simple tau (not tau-b); equals tau-b when no ties
zCrit = Normal Quantile( 1 - alpha / 2 );
If( pValue < alpha,
If( Z > 0, trendText = "Significant INCREASING trend", trendText = "Significant DECREASING trend" ),
trendText = "No significant monotonic trend"
);
// ---------------------------------------------------------------------
// STEP 6: Sen's slope, intercept, and confidence interval
// CI follows the Gilbert (1987) rank-based approach (normal approx).
// ---------------------------------------------------------------------
sortedSlopes = Sort Ascending( slopes );
senSlope = Median( slopes );
senIntercept = Median( y - senSlope * x );
cAlpha = zCrit * Sqrt( varS );
m1 = (nSlopes - cAlpha) / 2;
m2 = (nSlopes + cAlpha) / 2;
loIdx = Max( 1, Min( nSlopes, Round( m1 ) ) );
hiIdx = Max( 1, Min( nSlopes, Round( m2 + 1 ) ) );
senLower = sortedSlopes[loIdx];
senUpper = sortedSlopes[hiIdx];
// ---------------------------------------------------------------------
// STEP 7: Assemble labels/values for the test-statistic table + log
// ---------------------------------------------------------------------
ciPct = Round( 100 * (1 - alpha), 1 );
statLabels = {"Response", "Time / order variable", "n (non-missing)", "S", "Var(S) (tie-corrected)", "Z (continuity corrected)",
"p-value (two-sided)", "Kendall tau (S / n(n-1)/2)", "Alpha", "Sen's slope (per unit time)",
"Sen's slope lower " || Char( ciPct ) || "% CI", "Sen's slope upper " || Char( ciPct ) || "% CI", "Sen's intercept"};
statValues = {yName, xName, Char( n ), Char( S ), Format( varS, "Best", 8 ), Format( Z, "Fixed Dec", 4 ),
Format( pValue, "Best", 6 ), Format( tau, "Fixed Dec", 4 ), Char( alpha ), Format( senSlope, "Best", 6 ),
Format( senLower, "Best", 6 ), Format( senUpper, "Best", 6 ), Format( senIntercept, "Best", 6 )};
Write(
"\!NMann-Kendall: ", yName, " vs ", xName,
"\!N n = ", n, ", S = ", S, ", Z = ", Round( Z, 4 ), ", p = ", Round( pValue, 6 ),
"\!N Sen's slope = ", senSlope, " (", ciPct, "% CI: ", senLower, " to ", senUpper, ")",
"\!N ", trendText, "\!N"
);
// ---------------------------------------------------------------------
// STEP 8: Build (hidden) plot data table: time, observed value, Sen fit
// Graph Builder needs a data table; it is closed with the report.
// ---------------------------------------------------------------------
dtPlot = New Table( "Mann-Kendall Plot Data: " || yName,
Invisible,
New Column( "Time", Numeric, Continuous, Set Values( x ) ),
New Column( "Value", Numeric, Continuous, Set Values( y ) ),
New Column( "Sen Fit", Numeric, Continuous, Set Values( senIntercept + senSlope * x ) )
);
// Color the conclusion by outcome (red = trend detected, green = none)
concColor = If( pValue < alpha, "Red", "Green" );
// ---------------------------------------------------------------------
// STEP 9: Build the custom output report window (Graph Builder + stats)
// ---------------------------------------------------------------------
win = New Window( "Mann-Kendall Trend Test: " || yName,
V List Box(
Text Box( "Mann-Kendall Trend Test with Sen's Slope", << Set Font Size( 14 ), << Set Font Style( "Bold" ) ),
Text Box( "Response: " || yName || " | Time/Order: " || xName || " | n = " || Char( n ) ),
Spacer Box( Size( 0, 8 ) ),
// ----- Graph Builder: data points with Sen's slope line -----
Outline Box( "Trend Plot",
dtPlot << Graph Builder(
Size( 650, 400 ),
Show Control Panel( 0 ),
Variables( X( :Time ), Y( :Value ), Y( :Sen Fit, Position( 1 ) ) ),
Elements( Points( X, Y( 1 ) ), Line( X, Y( 2 ) ) )
)
),
// ----- Test statistics table -----
Outline Box( "Test Statistics",
Table Box(
String Col Box( "Statistic", statLabels ),
String Col Box( "Value", statValues )
)
),
// ----- Conclusion -----
Outline Box( "Conclusion",
Text Box( trendText || " at alpha = " || Char( alpha ) || " (p = " || Format( pValue, "Best", 4 ) || ").",
<< Set Font Style( "Bold" ), << Font Color( concColor ) ),
Text Box( "Sen's slope estimates the median change in " || yName || " per unit of " || xName || "." )
),
// ----- Notes -----
Outline Box( "Notes",
Text Box(
"H0: no monotonic trend. Z uses a continuity correction and the tie-corrected variance of S.\!N"
|| "Assumes independent observations; positive serial correlation inflates false positives.\!N"
|| "Normal approximation is weak for n < 10." ),
<< Close( 1 )
),
// ----- Optional export of statistics to a data table -----
Button Box( "Save Statistics to Data Table",
New Table( "Mann-Kendall Results: " || yName,
New Column( "Statistic", Character, Nominal, Set Values( statLabels ) ),
New Column( "Value", Character, Nominal, Set Values( statValues ) )
)
)
)
);
// Close the hidden plot data table when the report window is closed
win << On Close( Close( dtPlot, No Save ) );
