## Copyright (C) 2003-2005 Andy Adler <adler@ncf.ca>
## Copyright (C) 2021 Christian Scholz
## Copyright (C) 2022 Andreas Bertsatos <abertsatos@biol.uoa.gr>
## Copyright (C) 2022 Andrew Penn <A.C.Penn@sussex.ac.uk>
## Copyright (C) 2024 Swayam Shah <swayamshah66@gmail.com>
##
## This file is part of the statistics package for GNU Octave.
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 3 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {statistics} {@var{p} =} anovan (@var{Y}, @var{GROUP})
## @deftypefnx {statistics} {@var{p} =} anovan (@var{Y}, @var{GROUP}, @var{name}, @var{value})
## @deftypefnx {statistics} {[@var{p}, @var{atab}] =} anovan (@dots{})
## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}] =} anovan (@dots{})
## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}, @var{terms}] =} anovan (@dots{})
##
## Perform a multi (N)-way analysis of (co)variance (ANOVA or ANCOVA) to
## evaluate the effect of one or more categorical or continuous predictors (i.e.
## independent variables) on a continuous outcome (i.e. dependent variable). The
## algorithms used make @code{anovan} suitable for balanced or unbalanced
## factorial (crossed) designs. By default, @code{anovan} treats all factors
## as fixed. Examples of function usage can be found by entering the command
## @code{demo anovan}. A bootstrap resampling variant of this function,
## @code{bootlm}, is available in the statistics-resampling package and has
## similar usage.
##
## Data is a single vector @var{Y} with groups specified by a corresponding
## matrix or cell array of group labels @var{GROUP}, where each column of
## @var{GROUP} has the same number of rows as @var{Y}. For example, if
## @code{@var{Y} = [23; 27; 31; 29; 30; 32]; @var{GROUP} = [1, 2; 1, 3; 1, 2; 2,
## 3; 2, 3; 3,
## 2];}
## then observation 23 was measured under conditions 1,2; observation 27 was
## measured under conditions 1,3; and so on. If the @var{GROUP} provided is
## empty, then the linear model is fit with just the intercept (no predictors).
##
## @code{anovan} can take a number of optional parameters as name-value pairs.
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "continuous",
## @var{continuous})}
##
## @itemize
## @item
## @var{continuous} is a vector of indices indicating which of the columns (i.e.
## factors) in @var{GROUP} should be treated as continuous predictors rather
## than as categorical predictors. The relationship between continuous
## predictors and the outcome should be linear.
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "random", @var{random})}
##
## @itemize
## @item
## @var{random} is a vector of indices indicating which of the columns (i.e.
## factors) in @var{GROUP} should be treated as random effects rather than
## fixed effects.
##
## In the table @code{anovan} prints, the name of a random factor is followed
## by a @qcode{'} so that it can be told apart at a glance.  That mark is a
## convention of the printed output only: the names returned in @var{atab},
## in @var{stats}.varnames, and inside the expected-mean-square and
## denominator expressions carry no mark, so that they read as MATLAB's do.
## Which terms are random is reported in the @qcode{"Type"} column of
## @var{atab} instead.
##
## Every interaction involving a random factor stays in the model, and each
## F ratio is taken against the denominator its expected mean square calls
## for rather than against the mean squared error.  The expected mean square
## of a term names the variance each component contributes to it, written
## @code{Q(@dots{})} for a fixed term and @code{V(@dots{})} for a random one;
## the denominator is the combination of mean squares whose expectation
## matches the term's with the term itself removed.  Often that is a single
## mean square, in which case the F ratio carries its degrees of freedom.
## When no single mean square will do, one is synthesised from several and
## carries Satterthwaite degrees of freedom, which are generally not whole
## numbers.
##
## The variance component of every random term is estimated from the same
## system, and reported with confidence bounds.  A component estimated as
## negative has no interval, as it lies outside the parameter space.
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "model", @var{modeltype})}
##
## @itemize
## @item
## @var{modeltype} can specified as one of the following:
##
## @itemize
## @item
## "linear" (default) : compute @math{N} main effects with no interactions.
##
## @item
## "interaction" : compute @math{N} effects and @math{N*(N-1)} two-factor
## interactions
##
## @item
## "full" : compute the @math{N} main effects and interactions at all levels
##
## @item
## a scalar integer : representing the maximum interaction order
##
## @item
## a matrix of term definitions : each row is a term and each column is a
## factor. Entries are nonnegative integer exponents. Exponents greater than
## one are valid only for factors selected by @qcode{"continuous"}.
## @end itemize
##
## @example
## -- Example:
## A two-way ANOVA with interaction would be: [1 0; 0 1; 1 1]
## @end example
##
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "nested", @var{nested})}
##
## @itemize
## @item
## @var{nested} is an N-by-N logical matrix, where N is the number of factors.
## A true entry @code{@var{nested}(i,j)} specifies that factor i is nested in
## factor j. A factor may be nested in more than one parent. Nested factors
## must be categorical and use the default contrasts.
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "sstype", @var{sstype})}
##
## @itemize
## @item
## @var{sstype} can specified as one of the following:
##
## @itemize
## @item
## 1 : Type I sequential sums-of-squares.
##
## @item
## 2 : Type II partially sequential sums-of-squares.  Each term is adjusted
## for every other term that does not contain it.
##
## @item
## "h" : hierarchical sums-of-squares.  Each term is adjusted only for the
## terms below it in the hierarchy, so for a model whose terms are all of
## first order it agrees with Type II, while a polynomial term is adjusted
## for its lower powers but not for its higher ones.
##
## @item
## 3 (default) : Type III partial, constrained or marginal sums-of-squares
##
## @end itemize
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "varnames", @var{varnames})}
##
## @itemize
## @item
## @var{varnames} must be a cell array of strings with each element containing a
## factor name for each column of @var{GROUP}.  By default (if not parsed as
## optional argument), @var{varnames} are "X1","X2","X3", etc.
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "alpha", @var{alpha})}
##
## @itemize
## @item
## @var{alpha} must be a scalar value between 0 and 1 requesting
## @math{100*(1-@var{alpha})%} confidence bounds for the regression coefficients
## returned in @var{stats}.coeffs (default 0.05 for 95% confidence).
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "display", @var{dispopt})}
##
## @itemize
## @item
## @var{dispopt} can be either "on" (default) or "off" and controls the display
## of the model formula, table of model parameters, the ANOVA table and the
## diagnostic plots. The F-statistic and p-values are formatted in APA-style.
## To avoid p-hacking, the table of model parameters is only displayed if we set
## planned contrasts (see below).
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "contrasts",
## @var{contrasts})}
##
## @itemize
## @item
## @var{contrasts} can be specified as one of the following:
##
## @itemize
## @item
## A string corresponding to one of the built-in contrasts listed below:
##
## @itemize
## @item
## "simple" or "anova" (default): Simple (ANOVA) contrast coding. (The first
## level appearing in the @var{GROUP} column is the reference level)
##
## @item
## "poly": Polynomial contrast coding for trend analysis.
##
## @item
## "helmert": Helmert contrast coding: the difference between each level with
## the mean of the subsequent levels.
##
## @item
## "effect": Deviation effect coding. (The first level appearing in the
## @var{GROUP} column is omitted).
##
## @item
## "sdif" or "sdiff": Successive differences contrast coding: the difference
## between each level with the previous level.
##
## @item
## "treatment": Treatment contrast (or dummy) coding. (The first level appearing
## in the @var{GROUP} column is the reference level). These contrasts are not
## compatible with @var{sstype} = 3.
##
## @end itemize
##
## @item
## A matrix containing a custom contrast coding scheme (i.e. the generalized
## inverse of contrast weights). Rows in the contrast matrices correspond to
## factor levels in the order that they first appear in the @var{GROUP} column.
## The matrix must contain the same number of columns as there are the number of
## factor levels minus one.
## @end itemize
##
## If the anovan model contains more than one factor and a built-in contrast
## coding scheme was specified, then those contrasts are applied to all factors.
## To specify different contrasts for different factors in the model,
## @var{contrasts} should be a cell array with the same number of cells as there
## are columns in @var{GROUP}. Each cell should define contrasts for the
## respective column in @var{GROUP} by one of the methods described above. If
## cells are left empty, then the default contrasts are applied. Contrasts for
## cells corresponding to continuous factors are ignored.
## @end itemize
##
## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "weights", @var{weights})}
##
## @itemize
## @item
## @var{weights} is an optional vector of weights to be used when fitting the
## linear model. Weighted least squares (WLS) is used with weights (that is,
## minimizing @code{sum (@var{weights} * @var{residuals} .^ 2))}; otherwise
## ordinary least squares (OLS) is used (default is empty for OLS).
## @end itemize
##
## @code{anovan} can return up to four output arguments:
##
## @code{@var{p} = anovan (@dots{})} returns a vector of p-values, one for each
## term.
##
## @code{[@var{p}, @var{atab}] = anovan (@dots{})} returns a cell array
## containing the ANOVA table.  Its first row holds the column names, and the
## columns are, in order, the term name, its sum-of-squares, its degrees of
## freedom, a singularity flag, its mean square, the @math{F} statistic, the
## p-value, and then two effect sizes: eta squared and partial eta squared.
## The first seven follow MATLAB's layout, so a caller reading them by
## position gets the same quantity in either language; the two effect sizes
## are an Octave extension and are appended after them.
##
## A model naming any factor as random reports eight further columns after
## the p-value, as MATLAB does, with the two effect sizes still last: the
## term's type, its expected mean square, the mean square and degrees of
## freedom of the denominator its F ratio was taken against, the definition of
## that denominator, and the variance component of a random term with its
## confidence bounds.  A denominator that no single mean square provides is
## synthesised from several and carries Satterthwaite degrees of freedom,
## which are generally not whole numbers.
##
## The singularity flag is @math{1} when a term is aliased with the rest of
## the model, which happens when the design is not of full rank, most often
## because a combination of factor levels holds no observations.  The degrees
## of freedom reported for such a term are the ones that can be estimated,
## which may be fewer than the term's design block has columns and may be
## zero.  Whether a term is aliased depends on the model it is adjusted for,
## and therefore on @qcode{"sstype"}: a term that is estimable in a sequential
## fit may not be estimable in a marginal one.  A flagged term's
## sum-of-squares is not uniquely attributable to it, so the corresponding
## @math{F} and p-value should not be read as a test of that term.
##
## @code{[@var{p}, @var{atab}, @var{stats}] = anovan (@dots{})} returns a
## structure containing additional statistics, including degrees of freedom and
## effect sizes for each term in the linear model, the design matrix, the
## variance-covariance matrix, (weighted) model residuals, and the mean squared
## error. The columns of @var{stats}.coeffs (from left-to-right) report the
## model coefficients, standard errors, lower and upper @math{100*(1-alpha)%}
## confidence interval bounds, t-statistics, and p-values relating to the
## contrasts. The number appended to each term name in @var{stats}.coeffnames
## corresponds to the column number in the relevant contrast matrix for that
## factor. The @var{stats} structure can be used as input for
## @code{multcompare}.
##
## @code{[@var{p}, @var{atab}, @var{stats}, @var{terms}] = anovan (@dots{})}
## returns the model term definitions.
##
## @seealso{anova1, anova2, multcompare, fitlm}
## @end deftypefn

function [P, T, STATS, TERMS] = anovan (Y, GROUP, varargin)

    if (nargin < 2)
      print_usage;
    endif

    ## Check supplied parameters
    if ((numel (varargin) / 2) != fix (numel (varargin) / 2))
      error ("anovan: wrong number of arguments.")
    endif
    MODELTYPE = 'linear';
    DISPLAY = 'on';
    SSTYPE = 3;
    VARNAMES = [];
    CONTINUOUS = [];
    RANDOM = [];
    NESTED = [];
    CONTRASTS = {};
    ALPHA = 0.05;
    WEIGHTS = [];
    for idx = 3:2:nargin
      name = varargin{idx-2};
      value = varargin{idx-1};
      switch (lower (name))
        case 'model'
          MODELTYPE = value;
        case 'continuous'
          CONTINUOUS = value;
        case 'random'
          RANDOM = value;
        case 'nested'
          NESTED = value;
        case 'sstype'
          SSTYPE = value;
        case 'varnames'
          VARNAMES = value;
        case {'display','displayopt'}
          DISPLAY = value;
        case 'contrasts'
          CONTRASTS = value;
        case 'alpha'
          ALPHA = value;
        case 'weights'
          WEIGHTS = value;
        otherwise
          error ("anovan: parameter '%s' is not supported.", name);
      endswitch
    endfor

    ## Evaluate continuous input argument
    if (isnumeric (CONTINUOUS))
      if (any (CONTINUOUS != abs (fix (CONTINUOUS))))
        error (strcat ("anovan: the value provided for the CONTINUOUS", ...
                       " parameter must be a positive integer."));
      endif
    else
      error (strcat ("anovan: the value provided for the CONTINUOUS", ...
                     " parameter must be numeric."));
    endif

    ## Accommodate for different formats for GROUP
    ## GROUP can be a matrix of numeric identifiers of a cell arrays
    ## of strings or numeric identifiers
    N = size (GROUP, 2); # number of anova "ways"
    n = numel (Y);       # total number of observations
    categorical_levels = cell (1, N);
    categorical_missing = false (n, 1);
    if (prod (size (Y)) != n)
      error ("anovan: for ""anovan (Y, GROUP)"", Y must be a vector.");
    endif
    if (numel (unique (CONTINUOUS)) > N)
      error (strcat ("anovan: the number of factors assigned as continuous", ...
                     " cannot exceed the number of factors in GROUP."));
    endif
    if (any ((CONTINUOUS > N) || any (CONTINUOUS <= 0)))
      error (strcat ("anovan: one or more indices provided in the value", ...
                     " for the continuous parameter are out of range."));
    endif
    cont_vec = false (1, N);
    cont_vec(CONTINUOUS) = true;
    if (isempty (NESTED))
      NESTED = false (N);
    elseif (! (isnumeric (NESTED) || islogical (NESTED)) ...
            || ! isequal (size (NESTED), [N, N]) ...
            || any (! ismember (NESTED(:), [0, 1])) ...
            || any (diag (NESTED)))
      error (strcat ("anovan: NESTED must be an N-by-N logical matrix", ...
                     " with a zero diagonal."));
    else
      NESTED = logical (NESTED);
    endif
    closure = NESTED;
    for factor = 1:N
      closure |= closure(:, factor) * closure(factor, :);
    endfor
    if (any (diag (closure)))
      error ("anovan: NESTED factor relationships must be acyclic.");
    endif
    nested_factors = any (NESTED, 2)' | any (NESTED, 1);
    if (any (cont_vec & nested_factors))
      error ("anovan: nested factors must be categorical.");
    endif
    if (isa (GROUP, "categorical"))
      for j = 1:N
        if (ismember (j, CONTINUOUS))
          error ("anovan: continuous factors must be a numeric datatype.");
        endif
        categorical_levels{j} = categories (GROUP(:,j));
        categorical_missing |= isundefined (GROUP(:,j));
      endfor
      GROUP = double (GROUP);
    elseif (iscell (GROUP))
      if (size (GROUP, 1) == 1)
        tmp = cell (n, N);
        for j = 1:N
          if (isa (GROUP{j}, "categorical"))
            if (ismember (j, CONTINUOUS))
              error ("anovan: continuous factors must be a numeric datatype.");
            endif
            categorical_levels{j} = categories (GROUP{j});
            categorical_missing |= isundefined (GROUP{j}(:));
            tmp(:,j) = num2cell (double (GROUP{j}(:)));
          elseif (isnumeric (GROUP{j}))
            if (ismember (j, CONTINUOUS))
              tmp(:,j) = num2cell (GROUP{j});
            else
              tmp(:,j) = cellstr (num2str (GROUP{j}));
            endif
          else
            if (ismember (j, CONTINUOUS))
              error ("anovan: continuous factors must be a numeric datatype.");
            endif
            tmp(:,j) = GROUP{j};
          endif
        endfor
        GROUP = tmp;
      endif
    endif
    if (! isempty (GROUP))
      if (size (GROUP,1) != n)
        error (strcat ("anovan: GROUP must be a matrix", ...
                       " with the same number of rows as Y."));
      endif
    endif
    if (! isempty (VARNAMES))
      if (iscell (VARNAMES))
        if (all (cellfun (@ischar, VARNAMES)))
          nvarnames = numel (VARNAMES);
        else
          error (strcat ("anovan: all variable names must", ...
                         " be character or character arrays."));
        endif
      elseif (ischar (VARNAMES))
        nvarnames = 1;
        VARNAMES = {VARNAMES};
      elseif (isstring (VARNAMES))
        nvarnames = 1;
        VARNAMES = {char(VARNAMES)};
      else
        error (strcat ("anovan: varnames is not of a valid type.", ...
                       " Must be a cell array of character arrays,", ...
                       " character array or string."));
      endif
    else
      nvarnames = N;
      VARNAMES = arrayfun (@(x) ['X',num2str(x)], 1:N, 'UniformOutput', 0);
    endif
    if (nvarnames != N)
      error (strcat ("anovan: number of variable names is not equal", ...
                     " to the number of grouping variables."));
    endif

    ## Evaluate random argument (if applicable)
    if (! isempty (RANDOM))
      if (isnumeric (RANDOM))
        if (any (RANDOM != abs (fix (RANDOM))))
          error (strcat ("anovan: the value provided for the RANDOM", ...
                         " parameter must be a positive integer."));
        endif
      else
        error (strcat ("anovan: the value provided for the RANDOM", ...
                       "  parameter must be numeric."));
      endif
      if (numel (RANDOM) > N)
        error (strcat ("anovan: the number of elements in RANDOM cannot", ...
                       " exceed the number of columns in GROUP."));
      endif
      if (max (RANDOM) > N)
        error (strcat ("anovan: the indices listed in RANDOM cannot", ...
                       " exceed the number of columns in GROUP."));
      endif
    endif

    ## Evaluate contrasts (if applicable)
    if isempty (CONTRASTS)
      CONTRASTS = cell (1, N);
      planned = false;
    else
      if (ischar (CONTRASTS))
        contr_str = CONTRASTS;
        CONTRASTS = cell (1, N);
        CONTRASTS(:) = {contr_str};
      endif
      if (! iscell (CONTRASTS))
        CONTRASTS = {CONTRASTS};
      endif
      for i = 1:N
        if (! isempty (CONTRASTS{i}))
          msg = strcat ("columns in CONTRASTS must sum to", ...
                       " 0 for SSTYPE 3. Switching to SSTYPE 2 instead.");
          if (isnumeric (CONTRASTS{i}))
            ## Check whether all the columns sum to 0
            if (any (abs (sum (CONTRASTS{i})) > eps ('single')))
              warning (strcat ("Note that the CONTRASTS for predictor", ...
                               " %u do not sum to zero."), i);
            endif
            ## Check whether contrasts are orthogonal
            if (any (abs (reshape (corr (CONTRASTS{i}) - ...
                                          eye (size (CONTRASTS{i}, 2)), [], 1))...
                                        > eps ('single')))
              warning (strcat ("Note that the CONTRASTS for predictor", ...
                               " %u are not orthogonal."), i);
            endif
          else
            if (! ismember (lower (CONTRASTS{i}), ...
                            {'simple','anova','poly','helmert','effect',...
                              'sdif','sdiff','treatment'}))
              error (strcat ("anovan: valid built-in contrasts are:", ...
                             " 'simple', 'poly', 'helmert',",...
                             "'effect', 'sdif' or 'treatment'."));
            endif
            if (strcmpi (CONTRASTS{i}, 'treatment') && (SSTYPE==3))
              warning (msg);
              SSTYPE = 2;
            endif
          endif
        endif
      endfor
      planned = true;
    endif

    ## Evaluate alpha input argument
    if (! isa (ALPHA,'numeric') || numel (ALPHA) != 1)
      error ("anovan: alpha must be a numeric scalar value.");
    endif
    if ((ALPHA <= 0) || (ALPHA >= 1))
      error ("anovan: alpha must be a value between 0 and 1.");
    endif

    ## Remove NaN or non-finite observations
    if (isempty (GROUP))
      excl = any ([isnan(Y), isinf(Y)], 2);
    else
      XC = GROUP(:,CONTINUOUS);
      if iscell (XC)
        XC = cell2mat (XC);
      endif
      excl = any ([isnan(Y), isinf(Y), any(isnan(XC),2), any(isinf(XC),2)], 2) ...
             | categorical_missing;
      GROUP(excl,:) = [];
    endif
    Y(excl) = [];
    if (size (Y, 1) == 1)
      Y = Y.';         # if Y is a row vector, make it a column vector
    endif
    n = numel (Y);     # recalculate total number of observations

    ## Evaluate weights input argument
    if (! isempty (WEIGHTS))
      if (! isnumeric (WEIGHTS))
        error ("anovan: WEIGHTS must be a numeric datatype.");
      endif
      if (any (size (WEIGHTS) != [n,1]))
        error (strcat ("anovan: WEIGHTS must be a vector", ...
                       " with the same dimensions as Y."));
      endif
      if (any (! (WEIGHTS > 0)) || any (isinf (WEIGHTS)))
        error ("anovan: WEIGHTS must be a vector of positive finite values.");
      endif
      # Create diagonal matrix of normalized weights
      W = diag (WEIGHTS / mean (WEIGHTS));
    else
      # Create identity matrix
      W = eye (n);;
    endif

    ## Evaluate model type input argument and create terms matrix if not provided
    msg = strcat ("anovan: the number of columns in the term definitions", ...
                  " cannot exceed the number of columns of GROUP.");
    if (ischar (MODELTYPE))
      switch (lower (MODELTYPE))
        case 'linear'
          MODELTYPE = 1;
        case {'interaction','interactions'}
          MODELTYPE = 2;
        case 'full'
          MODELTYPE = N;
        otherwise
          error ("anovan: model type not recognised");
      endswitch
    endif
    if (isscalar (MODELTYPE))
      TERMS = cell (MODELTYPE,1);
      v = false (1, N);
      switch (lower (MODELTYPE))
        case 1
          ## Create term definitions for an additive linear model
          TERMS = eye (N);
        case 2
          ## Create term definitions for a model with two factor interactions
          if (N > 1)
            Nx = nchoosek (N, 2);
          else
            Nx = 0;
          endif
          TERMS = zeros (N + Nx, N);
          TERMS(1:N,:) = eye (N);
          cnt = N + 1;
          for j = 1:N
            for i = j:N-1
              TERMS(cnt,j) = 1;
              TERMS(cnt,i+1) = 1;
              cnt++;
            endfor
          endfor
        otherwise
          if (MODELTYPE > N)
            error (msg);
          endif
          ## Create term definitions for a full model
          Nx = zeros (1, N-1);
          Nx = 0;
          for k = 1:N
            Nx = Nx + nchoosek (N,k);
          endfor
          for j = 1:MODELTYPE
            v(1:j) = 1;
            TERMS(j) = flipud (unique (perms (v), 'rows'));
          endfor
          TERMS = cell2mat (TERMS);
      endswitch
      TERMS = double (TERMS);
    else
      ## Assume that the user provided a suitable matrix of term definitions
      if (size (MODELTYPE, 2) != N)
        error (msg);
      endif
      if (! isnumeric (MODELTYPE) || any (! isfinite (MODELTYPE(:))) ...
          || any (MODELTYPE(:) < 0) ...
          || any (MODELTYPE(:) != fix (MODELTYPE(:))))
        error (strcat ("anovan: elements of the model terms", ...
                       " matrix must be nonnegative integers."));
      endif
      TERMS = double (MODELTYPE);
    endif
    ## Evaluate terms matrix
    TERMS(! any (TERMS > 0, 2), :) = [];
    Ng = sum (TERMS > 0, 2);
    ## Evaluate terms
    Nt = rows (TERMS);

    ## Calculate total sum-of-squares
    ct  = sum (Y)^2 / n;   % correction term
    sst = sum (Y.^2) - ct;
    dft = n - 1;

    ## Create design matrix
    mDesignMatrix ();

    ## Fit linear models, and calculate sums-of-squares for ANOVA
    sstype_id = lower (SSTYPE);
    switch (sstype_id)
      case 1
        ## Type I sequential sums-of-squares (SSTYPE = 1)
        R = sst;
        ss = zeros (Nt,1);
        df_est = zeros (Nt,1);
        [jnk, jnk, jnk, jnk, jnk, rk_prev] = lmfit (cell2mat (X(1)), Y, W);
        for j = 1:Nt
          XS = cell2mat (X(1:j+1));
          [b, sse, jnk, jnk, jnk, rk_now] = lmfit (XS, Y, W);
          ss(j) = R - sse;
          df_est(j) = rk_now - rk_prev;
          R = sse;
          rk_prev = rk_now;
        endfor
        [b, sse, resid, ucov, hat] = lmfit (XS, Y, W);
        sstype_char = 'I';
      case {2,'h'}
        ## Type II (partially sequential, or hierarchical) sums-of-squares
        ss = zeros (Nt,1);
        df_est = zeros (Nt,1);
        for j = 1:Nt
          i = find (TERMS(j,:) > 0);
          if (isequal (sstype_id, 'h'))
            excludes_term = any (TERMS(:,i) < TERMS(j,i), 2);
          else
            excludes_term = any (TERMS(:,i) != TERMS(j,i), 2);
          endif
          k = cat (1, 1, 1 + find (excludes_term));
          XS = cell2mat (X(k));
          [jnk, R1, jnk, jnk, jnk, rk1] = lmfit (XS, Y, W);
          k = cat (1, j+1, k);
          XS = cell2mat (X(k));
          [jnk, R2, jnk, jnk, jnk, rk2] = lmfit (XS, Y, W);
          ss(j) = R1 - R2;
          df_est(j) = rk2 - rk1;
        endfor
        [b, sse, resid, ucov, hat] = lmfit (cell2mat (X), Y, W);
        sstype_char = 'II';
      case 3
        ## Type III (partial, constrained or marginal) sums-of-squares
        ss = zeros (Nt, 1);
        df_est = zeros (Nt, 1);
        [b, sse, resid, ucov, hat, rk_full] = lmfit (cell2mat (X), Y, W);
        for j = 1:Nt
          XS = cell2mat (X(1:Nt+1 != j+1));
          [jnk, R, jnk, jnk, jnk, rk_red] = lmfit (XS, Y, W);
          ss(j) = R - sse;
          df_est(j) = rk_full - rk_red;
        endfor
        sstype_char = 'III';
      otherwise
        error ("anovan: sstype value not supported.");
    endswitch
    ss = max (0, ss); # Truncate negative SS at 0
    ## A term that is aliased with the rest of the model contributes fewer
    ## degrees of freedom than its design block has columns.  Report what is
    ## estimable and flag the shortfall, so a sum-of-squares of zero can be
    ## told apart from a factor that genuinely has no effect.
    singular = (df_est < df);
    df_coef = df;   ## nominal width of each term's block, for the coefficients
    df = df_est;    ## estimable degrees of freedom, as reported in the table
    dfe = n - rank (cell2mat (X));
    ms = ss ./ df;
    if (dfe > 0)
      mse = sse / dfe;
    else
      ## A saturated model leaves no residual degrees of freedom.  The
      ## residual sum-of-squares is zero up to rounding, so report both as
      ## zero rather than letting 0/0 become Inf and poison every mean square
      ## that is tested against the error term.
      sse = 0;
      mse = 0;
    endif
    eta_sq = ss ./ sst;
    partial_eta_sq = ss ./ (ss + sse);

    ## With every factor fixed, each mean square is tested against the error.
    ## A random factor changes that: the expected mean squares decide which
    ## mean square, or which combination of them, estimates the same variance
    ## as the term under test, and that becomes the denominator.
    is_random = false (Nt, 1);
    ems_coef = [];
    txtems = {};
    denom = [];
    msdenom = [];
    dfdenom = [];
    txtdenom = {};
    varest = [];
    varci = [];
    if (isempty (RANDOM))
      msdenom = repmat (mse, Nt, 1);
      dfdenom = repmat (dfe, Nt, 1);
    else
      [is_random, ems_coef, txtems, denom, msdenom, dfdenom, ...
       txtdenom, varest, varci] = mRandomEffects (TERMS, RANDOM, ...
                                    NESTED, X, gid, CONTINUOUS, ...
                                    sstype_id, df, ms, mse, dfe, ...
                                    ALPHA, VARNAMES, Nt);
    endif
    F = ms ./ msdenom;
    P = 1 - fcdf (F, df, dfdenom);
    ## The denominator columns describe a random-effects fit, so they stay
    ## empty when every factor is fixed and the error term is the denominator.
    if (isempty (RANDOM))
      msdenom_out = [];
      dfdenom_out = [];
      rtnames = {};
    else
      msdenom_out = msdenom;
      dfdenom_out = dfdenom;
      rtnames = [cellfun(@(t) mTermName (t, VARNAMES, NESTED), ...
                         num2cell (TERMS(is_random,:), 2), ...
                         'UniformOutput', false); {'Error'}];
    endif

    ## Prepare model formula and cell array containing the ANOVA table
    ## Columns 1 to 7 follow MATLAB's layout.  The two effect sizes are an
    ## Octave extension and are appended, so a caller indexing the shared
    ## columns by position reads the same quantity in either language.
    if (isempty (RANDOM))
      T = cell (Nt + 3, 9);
      T(1,:) = {'Source', 'Sum Sq.', 'd.f.', 'Singular?', 'Mean Sq.', 'F', ...
                'Prob>F', 'Eta Sq.', 'Part. Eta Sq.'};
      T(2:Nt+1,2:9) = num2cell ([ss, df, singular, ms, F, P, eta_sq, ...
                                 partial_eta_sq]);
    else
      ## A random factor adds the eight columns MATLAB adds, describing what
      ## each mean square estimates and what it was tested against.  The two
      ## effect sizes stay last, as they are in the fixed-effects layout.
      T = cell (Nt + 3, 17);
      T(1,:) = {'Source', 'Sum Sq.', 'd.f.', 'Singular?', 'Mean Sq.', 'F', ...
                'Prob>F', 'Type', 'Expected MS', 'MS denom', 'd.f. denom', ...
                'Denom. defn.', 'Var. est.', 'Var. lower bnd', ...
                'Var. upper bnd', 'Eta Sq.', 'Part. Eta Sq.'};
      T(2:Nt+1,2:7) = num2cell ([ss, df, singular, ms, F, P]);
      types = repmat ({'fixed'}, Nt, 1);
      types(is_random) = {'random'};
      T(2:Nt+1,8) = types;
      T(2:Nt+1,9) = txtems(1:Nt);
      T(2:Nt+1,10) = num2cell (msdenom);
      T(2:Nt+1,11) = num2cell (dfdenom);
      T(2:Nt+1,12) = txtdenom;
      rterms = find (is_random);
      for k = 1:numel (rterms)
        T(1+rterms(k),13:15) = {varest(k), varci(k,1), varci(k,2)};
      endfor
      T(2:Nt+1,16:17) = num2cell ([eta_sq, partial_eta_sq]);
      T(end-1,8:9) = {'random', txtems{end}};
      T(end-1,13:15) = {varest(end), varci(end,1), varci(end,2)};
    endif
    T(end-1,1:5) = {'Error', sse, dfe, 0, mse};
    T(end,1:4) = {'Total', sst, dft, 0};
    formula = sprintf ("Y ~ 1");  # Initialize model formula
    for i = 1:Nt
      str = mTermName (TERMS(i,:), VARNAMES, NESTED);
      T(i+1,1) = str;
      ## Append model term to formula
      if (any (ismember (find (TERMS(i,:) > 0), RANDOM)))
        ## Random intercept term
        formula = sprintf ("%s + (1|%s)", formula, str);
        ## Remove statistics for random factors from the ANOVA table
        #T(RANDOM+1,4:7) = cell(1,4);
        #P(RANDOM) = NaN;
      else
        ## Fixed effect term
        formula = sprintf ("%s + %s", formula, str);
      endif
    endfor

    ## Calculate a standard error, t-statistic and p-value for each
    ## of the regression coefficients (fixed effects only)
    t_crit = tinv (1 - ALPHA / 2, dfe);
    se = sqrt (diag (ucov) * mse);
    t =  b ./ se;
    p = 2 * (1 - (tcdf (abs (t), dfe)));
    coeff_stats = zeros (1 + sum (df_coef), 6);
    coeff_stats(:,1) = b;                                # coefficients
    coeff_stats(:,2) = se;                               # standard errors
    coeff_stats(:,3) = b - se * t_crit;                  # Lower CI bound
    coeff_stats(:,4) = b + se * t_crit;                  # Upper CI bound
    coeff_stats(:,5) = t;                                # t-statistics
    coeff_stats(:,6) = p;                                # p-values
    ## Assign NaN to p-value to avoid printing statistics relating to
    ## coefficients for 'random' effects
    hi = 1 + cumsum (df_coef);
    random_terms = find (any (TERMS(:,RANDOM) > 0, 2));
    for ignore = random_terms'
      coeff_stats(hi(ignore)-df_coef(ignore)+1:hi(ignore), 6) = NaN;
    endfor

    ## Compute leverage values and Cook's distance
    h = diag (hat);          % Leverage values
    D = resid.^2 / ((1 + sum (df_coef)) * mse) ...
        .* h ./ (1 - h).^2;  % Cook's distance

    ## Create STATS structure for MULTCOMPARE
    STATS = struct ('source','anovan', ...
                    'resid', resid, ...      # These are weighted (not raw) residuals
                    'coeffs', coeff_stats, ...
                    'Rtr', [], ...           # Not used by Octave
                    'rowbasis', [], ...      # Not used by Octave
                    'dfe', dfe, ...
                    'mse', mse, ...
                    'nullproject', [], ...   # Not used by Octave
                    'terms', TERMS, ...
                    'nlevels', nlevels, ...
                    'continuous', cont_vec, ...
                    'vmeans', vmeans, ...
                    'termcols', termcols, ...
                    'coeffnames', {cellstr(char(coeffnames{:}))}, ...
                    'vars', [], ...          # Not used by Octave
                    'varnames', {VARNAMES}, ...
                    'grpnames', {levels}, ...
                    'vnested', NESTED, ...
                    ## Empty unless the model holds a random factor, in
                    ## which case they carry its expected mean squares, the
                    ## denominator each F ratio was taken against, and the
                    ## variance component of every random term.
                    'ems', ems_coef, ...
                    'denom', denom, ...
                    'dfdenom', dfdenom_out, ...
                    'msdenom', msdenom_out, ...
                    'varest', varest, ...
                    'varci', varci, ...
                    'txtdenom', {txtdenom}, ...
                    'txtems', {txtems}, ...
                    'rtnames', {rtnames}, ...
                    ## Additional STATS fields used exclusively by Octave
                    'center_continuous', center_continuous, ...
                    'random', RANDOM, ...
                    'formula', formula, ...
                    'alpha', ALPHA, ...
                    'df', df, ...
                    'contrasts', {CONTRASTS}, ...
                    'X', sparse (cell2mat (X)), ...
                    'Y', Y, ...
                    'W', sparse (W), ...
                    'lmfit', @lmfit, ...
                    'vcov', sparse (ucov * mse), ...
                    'CooksD', D, ...
                    'grps', gid, ...
                    'eta_squared', eta_sq, ...
                    'partial_eta_squared', partial_eta_sq);

    ## Print ANOVA table
    switch (lower (DISPLAY))

      case {'on', true}

        ## Print model formula
        fprintf ("\nMODEL FORMULA (based on Wilkinson's notation):\n\n%s\n", formula);

        ## If applicable, print parameter estimates (a.k.a contrasts) for fixed effects
        if (planned && ! isempty (GROUP))
          ## Parameter estimates correspond to the contrasts we set. To avoid
          ## p-hacking, don't print contrasts if we don't specify them to start with
          fprintf ("\nMODEL PARAMETERS (contrasts for the fixed effects)\n\n");
          fprintf ("Parameter               Estimate        SE  Lower.CI  Upper.CI        t Prob>|t|\n");
          fprintf ("--------------------------------------------------------------------------------\n");
          for j = 1:size (coeff_stats, 1)
            if (p(j) < 0.001)
              fprintf ("%-20s  %10.3g %9.3g %9.3g %9.3g %8.2f    <.001 \n", ...
                       STATS.coeffnames{j}, STATS.coeffs(j,1:end-1));
            elseif (p(j) < 0.9995)
              fprintf ("%-20s  %10.3g %9.3g %9.3g %9.3g %8.2f     .%03u \n", ...
                       STATS.coeffnames{j}, STATS.coeffs(j,1:end-1), round (p(j) * 1e+03));
            elseif (isnan (p(j)))
              ## Don't display coefficients for 'random' effects since they were
              ## treated as fixed effects
            else
              fprintf ("%-20s  %10.3g %9.3g %9.3g %9.3g %8.2f    1.000 \n", ...
                       STATS.coeffnames{j}, STATS.coeffs(j,1:end-1));
            endif
          endfor
        endif

        ## Print ANOVA table
        [nrows, ncols] = size (T);
        ## The ' marking a random factor is a printing convention of this
        ## implementation, not part of the names it returns, which carry no
        ## marker so that they match what MATLAB reports.
        VARNAMES_DISP = VARNAMES;
        for v = RANDOM
          VARNAMES_DISP{v} = strcat (VARNAMES{v}, "'");
        endfor
        fprintf ("\nANOVA TABLE (Type %s sums-of-squares):\n\n", sstype_char);
        fprintf ("Source                   Sum Sq.    d.f.    Mean Sq.  Part.Eta         F  Prob>F\n");
        fprintf ("--------------------------------------------------------------------------------\n");
        for i = 1:Nt
          str = mTermName (TERMS(i,:), VARNAMES_DISP, NESTED);
          l = numel (str);  # Needed to truncate source term name at 18 characters
          ## Format and print the statistics for each model term
          ## Format F statistics and p-values in APA style
          ## Mean Sq., F and Prob>F sit at 5, 6 and 7 in both layouts, and
          ## partial eta squared is the last column of either.
          row = {T{i+1,2}, T{i+1,3}, T{i+1,5}, T{i+1,end}, T{i+1,6}};
          if (P(i) < 0.001)
            fprintf ("%-20s  %10.5g  %6d  %10.5g  %4.3f  %11.2f   <.001 \n", ...
                      str(1:min (18,l)), row{:});
          elseif (P(i) < 0.9995)
            fprintf ("%-20s  %10.5g  %6d  %10.5g  %4.3f  %11.2f    .%03u \n", ...
                      str(1:min (18,l)), row{:}, round (P(i) * 1e+03));
          elseif (isnan (P(i)))
            fprintf ("%-20s  %10.5g  %6d \n", str(1:min (18,l)), T{i+1,2:3});
          else
            fprintf ("%-20s  %10.5g  %6d  %10.5g  %4.3f  %11.2f    1.000 \n", ...
                      str(1:min (18,l)), row{:});
          endif
        endfor
        fprintf ("Error                 %10.5g  %6d  %10.5g\n", ...
                 T{end-1,2}, T{end-1,3}, T{end-1,5});
        fprintf ("Total                 %10.5g  %6d \n", T{end,2:3});
        if (any (singular))
          fprintf (strcat ("Singular terms are aliased with the rest of the", ...
                           " model and are not\nuniquely estimable: %s\n"), ...
                   strjoin (T(1 + find (singular), 1)', ", "));
        endif
        if (! isempty (RANDOM))
          fprintf ("\nEach F ratio is taken against the denominator its\n");
          fprintf ("expected mean square calls for:\n");
          for i = 1:Nt
            fprintf ("%-20s  %s  on %.4g d.f.\n", ...
                     T{i+1,1}(1:min (18, numel (T{i+1,1}))), ...
                     txtdenom{i}, dfdenom(i));
          endfor
        endif
        fprintf ("\n");

        ## Make figure of diagnostic plots
        figure ('Name', 'Diagnostic Plots: Model Residuals');
        t = STATS.resid ./ (sqrt (mse * (1 - h))); % Studentized residuals
        fit = STATS.X * STATS.coeffs(:,1);         % Fitted values
        [jnk, DI] = sort (D, 'descend');           % Indices of sorted D
        nk = 4;                               % Top nk residuals with largest D

        ## Normal quantile-quantile plot
        subplot (2, 2, 1);
        x = ((1 : n)' - .5) / n;
        [ts, I] = sort (t);
        q = norminv (x);
        plot (q, ts, 'ok', 'markersize', 3);
        box off;
        grid on;
        xlabel ('Theoretical quantiles');
        ylabel ('Studentized Residuals');
        title ('Normal Q-Q Plot');
        arrayfun (@(i) text (q(I == DI(i)), t(DI(i)), ...
                             sprintf ("  %u", DI(i))), [1:min(nk,n)])
        iqr = [0.25; 0.75];
        yl = quantile (t, iqr, 1, 6);
        xl = norminv (iqr);
        slope = diff (yl) / diff (xl);
        int = yl(1) - slope * xl(1);
        ax1_xlim = get (gca, 'XLim');
        hold on; plot (ax1_xlim, slope * ax1_xlim + int, 'k-'); hold off;
        set (gca, 'Xlim', ax1_xlim);

        ## Spread-Location Plot
        subplot (2, 2, 2);
        plot (fit, sqrt (abs (t)), 'ko', 'markersize', 3);
        box off;
        xlabel ('Fitted values');
        ylabel ('sqrt ( | Studentized Residuals | )');
        title ('Spread-Location Plot')
        ax2_xlim = get (gca, 'XLim');
        hold on;
        plot (ax2_xlim, ones (1, 2) * sqrt (2), 'k:');
        plot (ax2_xlim, ones (1, 2) * sqrt (3), 'k-.');
        plot (ax2_xlim, ones (1, 2) * sqrt (4), 'k--');
        hold off;
        arrayfun (@(i) text (fit(DI(i)), sqrt (abs (t(DI(i)))), ...
                             sprintf ("  %u", DI(i))), [1:min(nk,n)]);
        xlim (ax2_xlim);

        ## Residual-Leverage plot
        subplot (2, 2, 3);
        plot (h, t, 'ko', 'markersize', 3);
        box off;
        xlabel ('Leverage')
        ylabel ('Studentized Residuals');
        title ('Residual-Leverage Plot')
        ax3_xlim = get (gca, 'XLim');
        ax3_ylim = get (gca, 'YLim');
        hold on; plot (ax3_xlim, zeros (1, 2), 'k-'); hold off;
        arrayfun (@(i) text (h(DI(i)), t(DI(i)), ...
                             sprintf ("  %u", DI(i))), [1:min(nk,n)]);
        set (gca, 'ygrid', 'on');
        xlim (ax3_xlim); ylim (ax3_ylim);

        ## Cook's distance stem plot
        subplot (2, 2, 4);
        stem (D, 'ko', 'markersize', 3);
        box off;
        xlabel ('Obs. number')
        ylabel ('Cook''s distance')
        title ('Cook''s Distance Stem Plot')
        xlim ([0, n]);
        ax4_xlim = get (gca, 'XLim');
        ax4_ylim = get (gca, 'YLim');
        hold on;
        plot (ax4_xlim, ones (1, 2) * 4 / dfe, 'k:');
        plot (ax4_xlim, ones (1, 2) * 0.5, 'k-.');
        plot (ax4_xlim, ones (1, 2), 'k--');
        hold off;
        arrayfun (@(i) text (DI(i), D(DI(i)), ...
                             sprintf ("  %u", DI(i))), [1:min(nk,n)]);
        xlim (ax4_xlim); ylim (ax4_ylim);

        set (findall ( gcf, '-property', 'FontSize'), 'FontSize', 7)

      case {'off', false}

        ## do nothing

      otherwise

        error ("anovan: wrong value for 'display' parameter.");

    endswitch

    function mDesignMatrix ()

      ## Nested function that returns a cell array of the design matrix for
      ## each term in the model
      ## Input variables it uses:
      ##  GROUP, TERMS, CONTINUOUS, CONTRASTS, VARNAMES, n
      ## Variables it creates or modifies:
      ##  X, grpnames, nlevels, df, termcols, coeffnames, vmeans, gid, CONTRASTS

      ## EVALUATE FACTOR LEVELS
      levels = cell (N, 1);
      gid = zeros (n, N);
      nlevels = zeros (N, 1);
      for j = 1:N
        if (cont_vec(j))
          nlevels(j) = 1;
          if (iscell (GROUP(:,j)))
            gid(:,j) = cell2mat ([GROUP(:,j)]);
          else
            gid(:,j) = GROUP(:,j);
          endif
        elseif (isempty (categorical_levels{j}))
          levels{j} = unique (GROUP(:,j), 'stable');
          if (isnumeric (levels{j}))
            levels{j} = num2cell (levels{j});
          endif
          nlevels(j) = numel (levels{j});
          for k = 1:nlevels(j)
            gid(ismember (GROUP(:,j), levels{j}{k}),j) = k;
          endfor
        else
          if (iscell (GROUP))
            codes = cell2mat (GROUP(:,j));
          else
            codes = GROUP(:,j);
          endif
          level_values = find (accumarray (codes, 1) > 0);
          levels{j} = categorical_levels{j}(level_values);
          nlevels(j) = numel (level_values);
          for k = 1:nlevels(j)
            gid(codes == level_values(k),j) = k;
          endfor
        endif
      endfor

      ## ENCODE EACH FACTOR ONCE
      base = cell (N, 1);
      vmeans = zeros (N, 1);
      center_continuous = cont_vec;
      for j = 1:N
        if (cont_vec(j))
          ## Keep the predictor uncentred here.  A term raises it to its
          ## exponent first and centres the resulting column, so that x^2
          ## means the square of x rather than the square of x - mean (x).
          base{j} = gid(:,j);
          if (ischar (CONTRASTS{j}) ...
              && strcmpi (CONTRASTS{j}, 'treatment'))
            center_continuous(j) = false;
            CONTRASTS{j} = [];
          else
            vmeans(j) = mean (base{j});
          endif
        elseif (any (NESTED(j,:)))
          if (! isempty (CONTRASTS{j}))
            error (strcat ("anovan: custom contrasts are not supported", ...
                           " for nested factors."));
          endif
          parents = find (NESTED(j,:));
          [~, ~, parent_id] = unique (gid(:,parents), 'rows');
          blocks = cell (max (parent_id), 1);
          for parent = 1:max (parent_id)
            rows_ = find (parent_id == parent);
            child_levels = unique (gid(rows_,j), 'stable');
            [~, local_id] = ismember (gid(rows_,j), child_levels);
            C = contr_simple (numel (child_levels));
            block = zeros (n, columns (C));
            block(rows_,:) = C(local_id,:);
            blocks{parent} = block;
          endfor
          base{j} = cell2mat (blocks');
          CONTRASTS{j} = [];
        else
          CONTRASTS{j} = mContrasts (CONTRASTS{j}, nlevels(j));
          base{j} = CONTRASTS{j}(gid(:,j), :);
        endif
      endfor

      ## BUILD ONE DESIGN BLOCK FOR EACH REQUESTED TERM
      X = cell (1, Nt + 1);
      X{1} = ones (n, 1);
      coeffnames = cell (1, Nt + 1);
      coeffnames{1} = '(Intercept)';
      df = zeros (Nt, 1);
      termcols = ones (Nt + 1, 1);
      for i = 1:Nt
        factors = find (TERMS(i, :) > 0);
        tmp = ones (n, 1);
        for j = factors
          exponent = TERMS(i, j);
          if (! cont_vec(j) && exponent != 1)
            error (strcat ("anovan: categorical factors cannot have", ...
                           " exponent values greater than 1."));
          endif
          block = base{j};
          if (cont_vec(j))
            if (exponent != 1)
              block = block .^ exponent;
            endif
            if (center_continuous(j))
              block -= mean (block);
            endif
          endif
          tmp = reshape (bsxfun (@times, ...
                         reshape (tmp, n, 1, columns (tmp)), ...
                         reshape (block, n, columns (block), 1)), n, []);
        endfor
        X{i + 1} = tmp;
        df(i) = columns (tmp);
        ## termcols counts the columns a dummy coding of the term would take,
        ## which is one per level rather than the contrast width in df.
        termcols(i + 1) = prod (nlevels(factors));
        term_name = mTermName (TERMS(i, :), VARNAMES, NESTED);
        if (df(i) == 1)
          coeffnames{i + 1} = term_name;
        else
          coeffnames{i + 1} = arrayfun (...
            @(v) sprintf ("%s_%u", term_name, v), (1:df(i))', ...
            'UniformOutput', false);
        endif
      endfor

    endfunction


endfunction

function C = mContrasts (specification, nlevels)

  if (nlevels == 1)
    C = zeros (1, 0);
  elseif (isempty (specification))
    C = contr_simple (nlevels);
  elseif (ischar (specification))
    switch (lower (specification))
      case {'simple', 'anova'}
        C = contr_simple (nlevels);
      case 'poly'
        C = contr_poly (nlevels);
      case 'helmert'
        C = contr_helmert (nlevels);
      case 'effect'
        C = contr_sum (nlevels);
      case {'sdif', 'sdiff'}
        C = contr_sdif (nlevels);
      case 'treatment'
        C = contr_treatment (nlevels);
      otherwise
        error ("anovan: unknown contrast specification.");
    endswitch
  else
    C = specification;
    if (! isequal (size (C), [nlevels, nlevels - 1]))
      error (strcat ("anovan: each contrast matrix must have one row per", ...
                     " factor level and one fewer column."));
    elseif (! all (any (C)))
      error ("anovan: a contrast must be coded in every column.");
    endif
  endif

endfunction

function [is_random, ems_coef, txtems, denom, msdenom, dfdenom, txtdenom, ...
      varest, varci] = mRandomEffects (TERMS, RANDOM, NESTED, X, gid, ...
                        CONTINUOUS, sstype_id, df, ms, mse, dfe, ...
                        ALPHA, VARNAMES, Nt)

  ## Derive the expected mean squares of a model holding one or more random
  ## factors, and from them the denominator each F ratio is taken against.

  ## A term is random when any factor in it is random, counting the
  ## parents of a nested factor as part of the term.
  membership = TERMS > 0;
  for factor = 1:columns (membership)
    parents = find (NESTED(factor,:));
    if (! isempty (parents))
      membership(:,parents) |= membership(:,factor);
    endif
  endfor
  is_random = any (membership(:,RANDOM), 2);

  term_names = cell (Nt, 1);
  for i = 1:Nt
    term_names{i} = mTermName (TERMS(i,:), VARNAMES, NESTED);
  endfor

  ## A term's basis is the set of cells its factors define, so that a
  ## coefficient counts observations per cell rather than design columns.
  bases = cell (Nt, 1);
  for i = 1:Nt
    factors = find (TERMS(i,:) > 0);
    nested_here = factors(any (NESTED(factors,:), 2));
    if (! isempty (nested_here))
      factors = union (factors, find (any (NESTED(nested_here,:), 1)));
    endif
    if (all (! ismember (factors, CONTINUOUS)))
      [jnk, jnk, ids] = unique (gid(:,factors), 'rows');
      bases{i} = sparse (1:numel (ids), ids, 1);
    else
      bases{i} = X{i+1};
    endif
  endfor

  ## The coefficient of a component in an expected mean square is the
  ## energy its basis gains when the term enters the model that the term's
  ## sum-of-squares is measured against, per degree of freedom.
  ems_coef = zeros (Nt, Nt);
  for i = 1:Nt
    [included, excluded] = mEmsModels (TERMS, X, sstype_id, Nt, i);
    Qi = orth (full (included));
    Qe = orth (full (excluded));
    for j = 1:Nt
      gain = sum (sumsq (Qi' * bases{j})) - sum (sumsq (Qe' * bases{j}));
      ems_coef(i,j) = max (gain, 0) / max (df(i), 1);
    endfor
  endfor

  ## Name each component: Q for a fixed term, V for a random one.
  txtems = cell (Nt + 1, 1);
  for i = 1:Nt
    pieces = {};
    if (! is_random(i))
      piece = mEmsComponent (ems_coef(i,i), 'Q', term_names{i});
      if (! isempty (piece))
        pieces{end+1} = piece;
      endif
      for j = find (! is_random(:))'
        if (j != i && abs (ems_coef(i,j)) > 1e-10)
          pieces{end+1} = mEmsComponent (ems_coef(i,j), 'Q', ...
                                         term_names{j});
        endif
      endfor
    endif
    for j = find (is_random(:))'
      if (abs (ems_coef(i,j)) > 1e-10)
        pieces{end+1} = mEmsComponent (ems_coef(i,j), 'V', term_names{j});
      endif
    endfor
    pieces{end+1} = 'V(Error)';
    txtems{i} = strjoin (pieces, '+');
  endfor
  txtems{Nt+1} = 'V(Error)';

  ## The denominator of a term's F ratio is the combination of mean
  ## squares whose expectation matches the term's, minus the term itself.
  rt = find (is_random(:));
  random_ms = [ms(rt); mse];
  random_df = [df(rt); dfe];
  coefficients = [ems_coef(rt,rt), ones(numel (rt), 1); ...
                  zeros(1, numel (rt)), 1];
  names = [term_names(rt); {'Error'}];
  denom = zeros (Nt, numel (rt) + 1);
  msdenom = zeros (Nt, 1);
  dfdenom = zeros (Nt, 1);
  txtdenom = cell (Nt, 1);
  for i = 1:Nt
    target = [ems_coef(i,rt), 1];
    own = find (rt == i, 1);
    if (! isempty (own))
      target(own) = 0;
    endif
    weights = pinv (coefficients') * target';
    ## pinv leaves rounding dust on the mean squares a denominator does
    ## not use.  Drop it, so the formula and the number agree.
    weights(abs (weights) <= 1e-10) = 0;
    denom(i,:) = weights';
    msdenom(i) = weights' * random_ms;
    ## Satterthwaite's approximation over the mean squares in use.  One
    ## carrying no degrees of freedom has no sampling distribution to
    ## combine, so the denominator has none either.
    used = (weights != 0);
    if (any (used & (random_df(:) <= 0)))
      dfdenom(i) = 0;
    else
      parts = (weights(used) .* random_ms(used)) .^ 2 ./ random_df(used);
      dfdenom(i) = msdenom(i) ^ 2 / sum (parts);
    endif
    txtdenom{i} = mMeanSquareFormula (weights, names);
  endfor

  ## Variance components, and a confidence interval for each from the
  ## chi-squared bounds of the mean squares it is built from.
  varest = coefficients \ random_ms;
  lower_ms = random_df .* random_ms ./ chi2inv (1 - ALPHA / 2, random_df);
  upper_ms = random_df .* random_ms ./ chi2inv (ALPHA / 2, random_df);
  inverse = pinv (coefficients);
  lo = sum (max (inverse, 0) .* lower_ms' ...
            + min (inverse, 0) .* upper_ms', 2);
  hi = sum (max (inverse, 0) .* upper_ms' ...
            + min (inverse, 0) .* lower_ms', 2);
  lo = max (lo, 0);
  undefined = (varest < 0) | (! isfinite (lo)) | (! isfinite (hi));
  lo(undefined) = NaN;
  hi(undefined) = NaN;
  varci = [lo, hi];

  ## Report the coefficients with a row and column for the error term, as
  ## MATLAB does, so the matrix describes every component of the model.
  ems_coef = [ems_coef, ones(Nt, 1); zeros(1, Nt), 1];

endfunction

function [included, excluded] = mEmsModels (TERMS, X, sstype_id, Nt, term)

  ## Return the models a term's sum-of-squares is measured between, which is
  ## what its expected mean square describes.
  switch (sstype_id)
    case 1
      inc = 1:term;
      exc = 1:term-1;
    case {2, 'h'}
      fac = find (TERMS(term,:) > 0);
      if (isequal (sstype_id, 'h'))
        exc = find (any (TERMS(:,fac) < TERMS(term,fac), 2))';
      else
        exc = find (any (TERMS(:,fac) != TERMS(term,fac), 2))';
      endif
      inc = [term, exc];
    otherwise
      inc = 1:Nt;
      exc = setdiff (inc, term, 'stable');
  endswitch
  included = cell2mat (X([1, inc + 1]));
  excluded = cell2mat (X([1, exc + 1]));

endfunction

function value = mEmsComponent (coefficient, symbol, name)

  if (! isfinite (coefficient) || abs (coefficient) <= 1e-10)
    value = "";
  elseif (abs (coefficient - 1) <= 1e-10)
    value = sprintf ("%s(%s)", symbol, name);
  else
    value = sprintf ("%.6g*%s(%s)", coefficient, symbol, name);
  endif

endfunction

function formula = mMeanSquareFormula (weights, names)

  pieces = {};
  for i = 1:numel (weights)
    coefficient = weights(i);
    if (abs (coefficient) <= 1e-10)
      continue;
    elseif (abs (coefficient - 1) <= 1e-10)
      term = sprintf ("MS(%s)", names{i});
    elseif (abs (coefficient + 1) <= 1e-10)
      term = sprintf ("-MS(%s)", names{i});
    else
      term = sprintf ("%.6g*MS(%s)", coefficient, names{i});
    endif
    if (! isempty (pieces) && coefficient > 0)
      term = ["+", term];
    endif
    pieces{end+1} = term;
  endfor
  formula = strjoin (pieces, "");

endfunction

function name = mTermName (term, varnames, nested)

  pieces = cell (1, nnz (term));
  count = 0;
  for j = find (term > 0)
    count += 1;
    factor = varnames{j};
    if (nargin > 2 && any (nested(j,:)))
      parents = varnames(nested(j,:));
      factor = sprintf ("%s(%s)", factor, strjoin (parents, ","));
    endif
    if (term(j) == 1)
      pieces{count} = factor;
    else
      pieces{count} = sprintf ("%s^%d", factor, term(j));
    endif
  endfor
  ## Wilkinson notation: ':' is the interaction term alone, which is what a
  ## row of the table holds, where '*' would name the crossed set of the
  ## factors and every interaction among them.
  name = strjoin (pieces, ":");

endfunction

## BUILT IN CONTRAST CODING FUNCTIONS

function C = contr_simple (N)

  ## Create contrast matrix (of doubles) using simple (ANOVA) contrast coding
  ## These contrasts are centered (i.e. sum to 0)
  ## Ideal for unordered factors, with comparison to a reference level
  ## The first factor level is the reference level
  C =  cat (1, zeros (1,N-1), eye (N-1)) - 1/N;

endfunction

function C = contr_poly (N)

  ## Create contrast matrix (of doubles) using polynomial contrast coding
  ## for trend analysis of ordered categorical factor levels
  ## These contrasts are orthogonal and centered (i.e. sum to 0)
  ## Ideal for ordered factors
  [C, jnk] = qr (bsxfun (@power, [1:N]' - mean ([1:N]'), [0:N-1]));
  C(:,1) = [];
  s = ones (1, N-1);
  s(1:2:N-1) *= -1;
  f = (sign (C(1,:)) != s);
  C(:,f) *= -1;

endfunction

function C = contr_helmert (N)

  ## Create contrast matrix (of doubles) using Helmert coding contrasts
  ## These contrasts are orthogonal and centered (i.e. sum to 0)
  C = cat (1, tril (-ones (N-1), -1) + diag (N-1:-1:1), ...
              -ones (1, N-1)) ./ (N:-1:2);

endfunction

function C = contr_sum (N)

  ## Create contrast matrix (of doubles) using deviation effect coding
  ## These contrasts are centered (i.e. sum to 0)
  C =  cat (1, - (ones (1,N-1)), eye (N-1));

endfunction

function C = contr_sdif (N)

  ## Create contrast matrix (of doubles) using successive differences coding
  ## These contrasts are centered (i.e. sum to 0)
  C =  tril (ones (N, N - 1), -1) - ones (N, 1) / N * [N - 1 : -1 : 1];

endfunction

function C = contr_treatment (N)

  ## Create contrast matrix (of doubles) using treatment contrast coding
  ## Not compatible with SSTYPE 3 since contrasts are not centered
  ## Ideal for unordered factors, with comparison to a reference level
  ## The first factor level is the reference level
  C =  cat (1, zeros (1,N-1), eye (N-1));

endfunction


## FUNCTION TO FIT THE LINEAR MODEL

function [b, sse, resid, ucov, hat, rk] = lmfit (X, Y, W)

  ## Get model coefficients by solving the linear equation by QR decomposition
  ## The number of free parameters (i.e. intercept + coefficients) is equal
  ## to n - dfe. If optional argument W is provided, it should be a diagonal
  ## matrix of weights or a positive definite covariance matrix
  if (nargin < 3)
    ## If no weights are provided, create an identity matrix
    n = numel (Y);
    W = eye (n);
  endif
  C = chol (W);
  XW = C*X;
  YW = C*Y;
  [Q, R] = qr (XW, 0);

  ## A design that is not of full column rank has no unique solution, and the
  ## triangular solve gives an arbitrarily large one: the residuals follow it,
  ## so the error sum-of-squares can exceed the total.  Detect the deficiency
  ## and take the minimum-norm least-squares solution instead, which leaves
  ## the fitted values, the residuals and SSE correct whatever the rank.
  npar = columns (XW);
  if (npar == 0)
    deficient = false;
  elseif (rows (R) != columns (R))
    deficient = true;
  else
    deficient = (rcond (R) < eps);
  endif
  if (deficient)
    b = pinv (XW) * YW;
    rk = rank (XW);
  else
    b = R \ (Q' * YW);
    rk = npar;
  endif

  ## Get fitted values
  fit = XW * b;

  ## Get residuals from the fit
  resid = YW - fit;

  ## Calculate the residual sums-of-squares
  sse = sum (resid.^2);

  ## Calculate the unscaled covariance matrix (i.e. inv (X'*X ))
  if (nargout > 3)
    if (deficient)
      ucov = pinv (XW' * XW);
    else
      ucov = R \ Q' / XW';
    endif
  endif

  ## Calculate the Hat matrix
  if (nargout > 4)
    w = diag (W);
    rw = sqrt (w);
    if (deficient)
      P = XW * pinv (XW);
      hat = diag (1 ./ rw) * P * diag (rw);
    else
      Q1 = diag (1 ./ rw) * Q;
      Q2 = diag (rw) * Q;
      hat = Q1 * Q2';
    endif
  endif

endfunction

%!demo
%!
%! # Two-sample unpaired test on independent samples (equivalent to Student's
%! # t-test). Note that the absolute value of t-statistic can be obtained by
%! # taking the square root of the reported F statistic. In this example,
%! # t = sqrt (1.44) = 1.20.
%!
%! score = [54 23 45 54 45 43 34 65 77 46 65]';
%! gender = {'male' 'male' 'male' 'male' 'male' 'female' 'female' 'female' ...
%!           'female' 'female' 'female'}';
%!
%! [P, ATAB, STATS] = anovan (score, gender, 'display', 'on', 'varnames', 'gender');

%!demo
%!
%! # Two-sample paired test on dependent or matched samples equivalent to a
%! # paired t-test. As for the first example, the t-statistic can be obtained by
%! # taking the square root of the reported F statistic. Naming subject as a
%! # random factor (') keeps the treatment x subject interaction in the model
%! # and makes its mean square the denominator of the test on treatment.
%!
%! score = [4.5 5.6; 3.7 6.4; 5.3 6.4; 5.4 6.0; 3.9 5.7]';
%! treatment = {'before' 'after'; 'before' 'after'; 'before' 'after';
%!              'before' 'after'; 'before' 'after'}';
%! subject = {'GS' 'GS'; 'JM' 'JM'; 'HM' 'HM'; 'JW' 'JW'; 'PS' 'PS'}';
%!
%! [P, ATAB, STATS] = anovan (score(:), {treatment(:), subject(:)}, ...
%!                            'model', 'full', 'random', 2, 'sstype', 2, ...
%!                            'varnames', {'treatment', 'subject'}, ...
%!                            'display', 'on');

%!demo
%!
%! # One-way ANOVA on the data from a study on the strength of structural beams,
%! # in Hogg and Ledolter (1987) Engineering Statistics. New York: MacMillan
%!
%! strength = [82 86 79 83 84 85 86 87 74 82 ...
%!            78 75 76 77 79 79 77 78 82 79]';
%! alloy = {'st','st','st','st','st','st','st','st', ...
%!          'al1','al1','al1','al1','al1','al1', ...
%!          'al2','al2','al2','al2','al2','al2'}';
%!
%! [P, ATAB, STATS] = anovan (strength, alloy, 'display', 'on', ...
%!                            'varnames', 'alloy');

%!demo
%!
%! # One-way repeated measures ANOVA on the data from a study on the number of
%! # words recalled by 10 subjects for three time conditions, in Loftus & Masson
%! # (1994) Psychon Bull Rev. 1(4):476-490, Table 2. Naming subject as a random
%! # factor (') keeps the seconds x subject interaction in the model and makes
%! # its mean square the denominator of the test on seconds.
%!
%! words = [10 13 13; 6 8 8; 11 14 14; 22 23 25; 16 18 20; ...
%!          15 17 17; 1 1 4; 12 15 17;  9 12 12;  8 9 12];
%! seconds = [1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5; ...
%!            1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5;];
%! subject = [ 1  1  1;  2  2  2;  3  3  3;  4  4  4;  5  5  5; ...
%!             6  6  6;  7  7  7;  8  8  8;  9  9  9; 10 10 10];
%!
%! [P, ATAB, STATS] = anovan (words(:), {seconds(:), subject(:)}, ...
%!                            'model', 'full', 'random', 2, 'sstype', 2, ...
%!                            'display', 'on', 'varnames', {'seconds', 'subject'});

%!demo
%!
%! # Balanced two-way ANOVA with interaction on the data from a study of popcorn
%! # brands and popper types, in Hogg and Ledolter (1987) Engineering Statistics.
%! # New York: MacMillan
%!
%! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ...
%!            6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5];
%! brands = {'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'};
%! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ...
%!           'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'};
%!
%! [P, ATAB, STATS] = anovan (popcorn(:), {brands(:), popper(:)}, ...
%!                            'display', 'on', 'model', 'full', ...
%!                            'varnames', {'brands', 'popper'});

%!demo
%!
%! # Unbalanced two-way ANOVA (2x2) on the data from a study on the effects of
%! # gender and having a college degree on salaries of company employees,
%! # in Maxwell, Delaney and Kelly (2018): Chapter 7, Table 15
%!
%! salary = [24 26 25 24 27 24 27 23 15 17 20 16, ...
%!           25 29 27 19 18 21 20 21 22 19]';
%! gender = {'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f'...
%!           'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm'}';
%! degree = [1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 0 0 0 0 0 0]';
%!
%! [P, ATAB, STATS] = anovan (salary, {gender, degree}, 'model', 'full', ...
%!                            'sstype', 3, 'display', 'on', 'varnames', ...
%!                            {'gender', 'degree'});

%!demo
%!
%! # Unbalanced two-way ANOVA (3x2) on the data from a study of the effect of
%! # adding sugar and/or milk on the tendency of coffee to make people babble,
%! # in from Navarro (2019): 16.10
%!
%! sugar = {'real' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'none' ...
%!          'fake' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'fake'}';
%! milk = {'yes' 'no' 'no' 'yes' 'yes' 'no' 'yes' 'yes' 'yes' ...
%!         'no' 'no' 'yes' 'no' 'no' 'no' 'no' 'no' 'yes'}';
%! babble = [4.6 4.4 3.9 5.6 5.1 5.5 3.9 3.5 3.7...
%!           5.6 4.7 5.9 6.0 5.4 6.6 5.8 5.3 5.7]';
%!
%! [P, ATAB, STATS] = anovan (babble, {sugar, milk}, 'model', 'full',  ...
%!                            'sstype', 3, 'display', 'on', ...
%!                            'varnames', {'sugar', 'milk'});

%!demo
%!
%! # Unbalanced three-way ANOVA (3x2x2) on the data from a study of the effects
%! # of three different drugs, biofeedback and diet on patient blood pressure,
%! # adapted* from Maxwell, Delaney and Kelly (2018): Chapter 8, Table 12
%! # * Missing values introduced to make the sample sizes unequal to test the
%! #   calculation of different types of sums-of-squares
%!
%! drug = {'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' ...
%!         'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X';
%!         'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' ...
%!         'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y';
%!         'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' ...
%!         'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z'};
%! feedback = [1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0;
%!             1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0;
%!             1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0];
%! diet = [0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1;
%!         0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1;
%!         0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1];
%! BP = [170 175 165 180 160 158 161 173 157 152 181 190 ...
%!       173 194 197 190 176 198 164 190 169 164 176 175;
%!       186 194 201 215 219 209 164 166 159 182 187 174 ...
%!       189 194 217 206 199 195 171 173 196 199 180 NaN;
%!       180 187 199 170 204 194 162 184 183 156 180 173 ...
%!       202 228 190 206 224 204 205 199 170 160 NaN NaN];
%!
%! [P, ATAB, STATS] = anovan (BP(:), {drug(:), feedback(:), diet(:)}, ...
%!                                    'model', 'full', 'sstype', 3, ...
%!                                    'display', 'on', ...
%!                                    'varnames', {'drug', 'feedback', 'diet'});

%!demo
%!
%! # Balanced three-way ANOVA (2x2x2) with one of the factors being a blocking
%! # factor. The data is from a randomized block design study on the effects
%! # of antioxidant treatment on glutathione-S-transferase (GST) levels in
%! # different mouse strains, from Festing (2014), ILAR Journal, 55(3):427-476.
%! # Naming block as a random factor (') keeps every interaction with block in
%! # the model; each F ratio then takes the denominator its expected mean
%! # square calls for, which for block itself is a combination of three mean
%! # squares carried on fractional degrees of freedom.
%!
%! measurement = [444 614 423 625 408  856 447 719 ...
%!                764 831 586 782 609 1002 606 766]';
%! strain= {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ...
%!          'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}';
%! treatment={'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T'}';
%! block = [1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2]';
%!
%! [P, ATAB, STATS] = anovan (measurement/10, {strain, treatment, block}, ...
%!                            'sstype', 2, 'model', 'full', 'random', 3, ...
%!                            'display', 'on', ...
%!                            'varnames', {'strain', 'treatment', 'block'});

%!demo
%!
%! # One-way ANCOVA on data from a study of the additive effects of species
%! # and temperature on chirpy pulses of crickets, from Stitch, The Worst Stats
%! # Text eveR
%!
%! pulse = [67.9 65.1 77.3 78.7 79.4 80.4 85.8 86.6 87.5 89.1 ...
%!          98.6 100.8 99.3 101.7 44.3 47.2 47.6 49.6 50.3 51.8 ...
%!          60 58.5 58.9 60.7 69.8 70.9 76.2 76.1 77 77.7 84.7]';
%! temp = [20.8 20.8 24 24 24 24 26.2 26.2 26.2 26.2 28.4 ...
%!         29 30.4 30.4 17.2 18.3 18.3 18.3 18.9 18.9 20.4 ...
%!         21 21 22.1 23.5 24.2 25.9 26.5 26.5 26.5 28.6]';
%! species = {'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' ...
%!            'ex' 'ex' 'ex' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' ...
%!            'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv'};
%!
%! [P, ATAB, STATS] = anovan (pulse, {species, temp}, 'model', 'linear', ...
%!                           'continuous', 2, 'sstype', 'h', 'display', 'on', ...
%!                           'varnames', {'species', 'temp'});

%!demo
%!
%! # Factorial ANCOVA on data from a study of the effects of treatment and
%! # exercise on stress reduction score after adjusting for age. Data from R
%! # datarium package).
%!
%! score = [95.6 82.2 97.2 96.4 81.4 83.6 89.4 83.8 83.3 85.7 ...
%!          97.2 78.2 78.9 91.8 86.9 84.1 88.6 89.8 87.3 85.4 ...
%!          81.8 65.8 68.1 70.0 69.9 75.1 72.3 70.9 71.5 72.5 ...
%!          84.9 96.1 94.6 82.5 90.7 87.0 86.8 93.3 87.6 92.4 ...
%!          100. 80.5 92.9 84.0 88.4 91.1 85.7 91.3 92.3 87.9 ...
%!          91.7 88.6 75.8 75.7 75.3 82.4 80.1 86.0 81.8 82.5]';
%! treatment = {'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'}';
%! exercise = {'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  ...
%!             'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ...
%!             'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  ...
%!             'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  ...
%!             'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ...
%!             'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'}';
%! age = [59 65 70 66 61 65 57 61 58 55 62 61 60 59 55 57 60 63 62 57 ...
%!        58 56 57 59 59 60 55 53 55 58 68 62 61 54 59 63 60 67 60 67 ...
%!        75 54 57 62 65 60 58 61 65 57 56 58 58 58 52 53 60 62 61 61]';
%!
%! [P, ATAB, STATS] = anovan (score, {treatment, exercise, age}, ...
%!                            'model', [1 0 0; 0 1 0; 0 0 1; 1 1 0], ...
%!                            'continuous', 3, 'sstype', 'h', 'display', 'on', ...
%!                            'varnames', {'treatment', 'exercise', 'age'});

%!demo
%!
%! # Unbalanced one-way ANOVA with custom, orthogonal contrasts. The statistics
%! # relating to the contrasts are shown in the table of model parameters, and
%! # can be retrieved from the STATS.coeffs output.
%!
%! dv =  [ 8.706 10.362 11.552  6.941 10.983 10.092  6.421 14.943 15.931 ...
%!        22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ...
%!        22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ...
%!        22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ...
%!        25.694 ]';
%! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 ...
%!      4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]';
%! C = [ 0.4001601  0.3333333  0.5  0.0
%!       0.4001601  0.3333333 -0.5  0.0
%!       0.4001601 -0.6666667  0.0  0.0
%!      -0.6002401  0.0000000  0.0  0.5
%!      -0.6002401  0.0000000  0.0 -0.5];
%!
%! [P,ATAB, STATS] = anovan (dv, g, 'contrasts', C, 'varnames', 'score', ...
%!                          'alpha', 0.05, 'display', 'on');

%!demo
%!
%! # One-way ANOVA with the linear model fit by weighted least squares to
%! # account for heteroskedasticity. In this example, the variance appears
%! # proportional to the outcome, so weights have been estimated by initially
%! # fitting the model without weights and regressing the absolute residuals on
%! # the fitted values. Although this data could have been analysed by Welch's
%! # ANOVA test, the approach here can generalize to ANOVA models with more than
%! # one factor.
%!
%! g = [1, 1, 1, 1, 1, 1, 1, 1, ...
%!      2, 2, 2, 2, 2, 2, 2, 2, ...
%!      3, 3, 3, 3, 3, 3, 3, 3]';
%! y = [13, 16, 16,  7, 11,  5,  1,  9, ...
%!      10, 25, 66, 43, 47, 56,  6, 39, ...
%!      11, 39, 26, 35, 25, 14, 24, 17]';
%!
%! [P,ATAB,STATS] = anovan (y, g, 'display', 'off');
%! fitted = STATS.X * STATS.coeffs(:,1); # fitted values
%! b = polyfit (fitted, abs (STATS.resid), 1);
%! v = polyval (b, fitted);  # Variance as a function of the fitted values
%! figure ('Name', 'Regression of the absolute residuals on the fitted values');
%! plot (fitted, abs (STATS.resid),'ob');hold on; plot (fitted,v,'-r'); hold off;
%! xlabel ('Fitted values'); ylabel ('Absolute residuals');
%!
%! [P,ATAB,STATS] = anovan (y, g, 'weights', v.^-1);


## Test 1 for anovan example 1
## Test compares anovan to results from MATLAB's anovan and ttest2 functions
%!test
%! score = [54 23 45 54 45 43 34 65 77 46 65]';
%! gender = {'male' 'male' 'male' 'male' 'male' 'female' 'female' 'female' ...
%!           'female' 'female' 'female'}';
%!
%! [P, T, STATS] = anovan (score,gender,'display','off');
%! assert_equal (P(1), 0.2612876773271042, 1e-09);              # compared to p calculated by MATLAB anovan
%! assert_equal (sqrt (T{2,6}), abs (1.198608733288208), 1e-09);  # compared to abs(t) calculated from sqrt(F) by MATLAB anovan
%! assert_equal (P(1), 0.2612876773271047, 1e-09);              # compared to p calculated by MATLAB ttest2
%! assert_equal (sqrt (T{2,6}), abs (-1.198608733288208), 1e-09); # compared to abs(t) calculated by MATLAB ttest2

## Test 2 for anovan example 2
## Test compares anovan to results from MATLAB's anovan and ttest functions
%!test
%! score = [4.5 5.6; 3.7 6.4; 5.3 6.4; 5.4 6.0; 3.9 5.7]';
%! treatment = {'before' 'after'; 'before' 'after'; 'before' 'after';
%!              'before' 'after'; 'before' 'after'}';
%! subject = {'GS' 'GS'; 'JM' 'JM'; 'HM' 'HM'; 'JW' 'JW'; 'PS' 'PS'}';
%!
%! [P, ATAB, STATS] = anovan (score(:),{treatment(:),subject(:)},'display','off','sstype',2);
%! assert_equal (P(1), 0.016004356735364, 1e-09);              # compared to p calculated by MATLAB anovan
%! assert_equal (sqrt (ATAB{2,6}), abs (4.00941576558195), 1e-09);  # compared to abs(t) calculated from sqrt(F) by MATLAB anovan
%! assert_equal (P(1), 0.016004356735364, 1e-09);              # compared to p calculated by MATLAB ttest2
%! assert_equal (sqrt (ATAB{2,6}), abs (-4.00941576558195), 1e-09); # compared to abs(t) calculated by MATLAB ttest2

## Test 3 for anovan example 3
## Test compares anovan to results from MATLAB's anovan and anova1 functions
%!test
%! strength = [82 86 79 83 84 85 86 87 74 82 ...
%!            78 75 76 77 79 79 77 78 82 79]';
%! alloy = {'st','st','st','st','st','st','st','st', ...
%!          'al1','al1','al1','al1','al1','al1', ...
%!          'al2','al2','al2','al2','al2','al2'}';
%!
%! [P, ATAB, STATS] = anovan (strength,{alloy},'display','off');
%! assert_equal (P(1), 0.000152643638830491, 1e-09);
%! assert_equal (ATAB{2,6}, 15.4, 1e-09);

## Test 4 for anovan example 4
## Test compares anovan to results from MATLAB's anovan function
%!test
%! words = [10 13 13; 6 8 8; 11 14 14; 22 23 25; 16 18 20; ...
%!          15 17 17; 1 1 4; 12 15 17;  9 12 12;  8 9 12];
%! subject = [ 1  1  1;  2  2  2;  3  3  3;  4  4  4;  5  5  5; ...
%!             6  6  6;  7  7  7;  8  8  8;  9  9  9; 10 10 10];
%! seconds = [1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5; ...
%!            1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5;];
%!
%! [P, ATAB, STATS] = anovan (words(:),{seconds(:),subject(:)},'model','full','random',2,'sstype',2,'display','off');
%! assert_equal (P(1), 1.51865926758752e-07, 1e-09);
%! assert_equal (ATAB{2,2}, 52.2666666666667, 1e-09);
%! assert_equal (ATAB{3,2}, 942.533333333333, 1e-09);
%! assert_equal (ATAB{4,2}, 11.0666666666667, 1e-09);

## Test 5 for anovan example 5
## Test compares anovan to results from MATLAB's anovan function
%!test
%! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ...
%!            6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5];
%! brands = {'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'; ...
%!           'Gourmet', 'National', 'Generic'};
%! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ...
%!           'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'};
%!
%! [P, ATAB, STATS] = anovan (popcorn(:),{brands(:),popper(:)},'display','off','model','full');
%! assert_equal (P(1), 7.67895738278171e-07, 1e-09);
%! assert_equal (P(2), 0.000100373896304998, 1e-09);
%! assert_equal (P(3), 0.746215396636649, 1e-09);
%! assert_equal (ATAB{2,6}, 56.7, 1e-09);
%! assert_equal (ATAB{3,6}, 32.4, 1e-09);
%! assert_equal (ATAB{4,6}, 0.29999999999997, 1e-09);

## Test 6 for anovan example 6
## Test compares anovan to results from MATLAB's anovan function
%!test
%! salary = [24 26 25 24 27 24 27 23 15 17 20 16, ...
%!           25 29 27 19 18 21 20 21 22 19]';
%! gender = {'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f'...
%!           'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm'}';
%! degree = [1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 0 0 0 0 0 0]';
%!
%! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',1,'display','off');
%! assert_equal (P(1), 0.747462549227232, 1e-09);
%! assert_equal (P(2), 1.03809316857694e-08, 1e-09);
%! assert_equal (P(3), 0.523689833702691, 1e-09);
%! assert_equal (ATAB{2,2}, 0.296969696969699, 1e-09);
%! assert_equal (ATAB{3,2}, 272.391841491841, 1e-09);
%! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09);
%! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09);
%! [P, ATAB, STATS] = anovan (salary,{degree,gender},'model','full','sstype',1,'display','off');
%! assert_equal (P(1), 2.53445097305047e-08, 1e-09);
%! assert_equal (P(2), 0.00388133678528749, 1e-09);
%! assert_equal (P(3), 0.523689833702671, 1e-09);
%! assert_equal (ATAB{2,2}, 242.227272727273, 1e-09);
%! assert_equal (ATAB{3,2}, 30.4615384615384, 1e-09);
%! assert_equal (ATAB{4,2}, 1.17482517482523, 1e-09);
%! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09);
%! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',2,'display','off');
%! assert_equal (P(1), 0.00388133678528743, 1e-09);
%! assert_equal (P(2), 1.03809316857694e-08, 1e-09);
%! assert_equal (P(3), 0.523689833702691, 1e-09);
%! assert_equal (ATAB{2,2}, 30.4615384615385, 1e-09);
%! assert_equal (ATAB{3,2}, 272.391841491841, 1e-09);
%! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09);
%! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09);
%! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',3,'display','off');
%! assert_equal (P(1), 0.00442898146583742, 1e-09);
%! assert_equal (P(2), 1.30634252053587e-08, 1e-09);
%! assert_equal (P(3), 0.523689833702691, 1e-09);
%! assert_equal (ATAB{2,2}, 29.3706293706294, 1e-09);
%! assert_equal (ATAB{3,2}, 264.335664335664, 1e-09);
%! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09);
%! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09);

## Test 7 for anovan example 7
## Test compares anovan to results from MATLAB's anovan function
%!test
%! sugar = {'real' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'none' ...
%!          'fake' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'fake'}';
%! milk = {'yes' 'no' 'no' 'yes' 'yes' 'no' 'yes' 'yes' 'yes' ...
%!         'no' 'no' 'yes' 'no' 'no' 'no' 'no' 'no' 'yes'}';
%! babble = [4.6 4.4 3.9 5.6 5.1 5.5 3.9 3.5 3.7...
%!           5.6 4.7 5.9 6.0 5.4 6.6 5.8 5.3 5.7]';
%!
%! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',1,'display','off');
%! assert_equal (P(1), 0.0108632139833963, 1e-09);
%! assert_equal (P(2), 0.0810606976703546, 1e-09);
%! assert_equal (P(3), 0.00175433329935627, 1e-09);
%! assert_equal (ATAB{2,2}, 3.55752380952381, 1e-09);
%! assert_equal (ATAB{3,2}, 0.956108477471702, 1e-09);
%! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09);
%! assert_equal (ATAB{5,2}, 3.1625, 1e-09);
%! [P, ATAB, STATS] = anovan (babble,{milk,sugar},'model','full','sstype',1,'display','off');
%! assert_equal (P(1), 0.0373333189297505, 1e-09);
%! assert_equal (P(2), 0.017075098787169, 1e-09);
%! assert_equal (P(3), 0.00175433329935627, 1e-09);
%! assert_equal (ATAB{2,2}, 1.444, 1e-09);
%! assert_equal (ATAB{3,2}, 3.06963228699552, 1e-09);
%! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09);
%! assert_equal (ATAB{5,2}, 3.1625, 1e-09);
%! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',2,'display','off');
%! assert_equal (P(1), 0.017075098787169, 1e-09);
%! assert_equal (P(2), 0.0810606976703546, 1e-09);
%! assert_equal (P(3), 0.00175433329935627, 1e-09);
%! assert_equal (ATAB{2,2}, 3.06963228699552, 1e-09);
%! assert_equal (ATAB{3,2}, 0.956108477471702, 1e-09);
%! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09);
%! assert_equal (ATAB{5,2}, 3.1625,  1e-09);
%! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',3,'display','off');
%! assert_equal (P(1), 0.0454263063473954, 1e-09);
%! assert_equal (P(2), 0.0746719907091438, 1e-09);
%! assert_equal (P(3), 0.00175433329935627, 1e-09);
%! assert_equal (ATAB{2,2}, 2.13184977578476, 1e-09);
%! assert_equal (ATAB{3,2}, 1.00413461538462, 1e-09);
%! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09);
%! assert_equal (ATAB{5,2}, 3.1625, 1e-09);

## Test 8 for anovan example 8
## Test compares anovan to results from MATLAB's anovan function
%!test
%! drug = {'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' ...
%!         'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X';
%!         'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' ...
%!         'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y';
%!         'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' ...
%!         'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z'};
%! feedback = [1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0;
%!             1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0;
%!             1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0];
%! diet = [0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1;
%!         0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1;
%!         0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1];
%! BP = [170 175 165 180 160 158 161 173 157 152 181 190 ...
%!       173 194 197 190 176 198 164 190 169 164 176 175;
%!       186 194 201 215 219 209 164 166 159 182 187 174 ...
%!       189 194 217 206 199 195 171 173 196 199 180 NaN;
%!       180 187 199 170 204 194 162 184 183 156 180 173 ...
%!       202 228 190 206 224 204 205 199 170 160 NaN NaN];
%!
%! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype', 1,'display','off');
%! assert_equal (P(1), 7.02561843825325e-05, 1e-09);
%! assert_equal (P(2), 0.000425806013389362, 1e-09);
%! assert_equal (P(3), 6.16780773446401e-07, 1e-09);
%! assert_equal (P(4), 0.261347622678438, 1e-09);
%! assert_equal (P(5), 0.0542278432357043, 1e-09);
%! assert_equal (P(6), 0.590353225626655, 1e-09);
%! assert_equal (P(7), 0.0861628249564267, 1e-09);
%! assert_equal (ATAB{2,2}, 3614.70355731226, 1e-09);
%! assert_equal (ATAB{3,2}, 2227.46639771024, 1e-09);
%! assert_equal (ATAB{4,2}, 5008.25614451819, 1e-09);
%! assert_equal (ATAB{5,2}, 437.066007908781, 1e-09);
%! assert_equal (ATAB{6,2}, 976.180770397332, 1e-09);
%! assert_equal (ATAB{7,2}, 46.616653365254, 1e-09);
%! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09);
%! assert_equal (ATAB{9,2}, 9065.8,  1e-09);
%! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype',2,'display','off');
%! assert_equal (P(1), 9.4879638470754e-05, 1e-09);
%! assert_equal (P(2), 0.00124177666315809, 1e-09);
%! assert_equal (P(3), 6.86162012732911e-07, 1e-09);
%! assert_equal (P(4), 0.260856132341256, 1e-09);
%! assert_equal (P(5), 0.0523758623892078, 1e-09);
%! assert_equal (P(6), 0.590353225626655, 1e-09);
%! assert_equal (P(7), 0.0861628249564267, 1e-09);
%! assert_equal (ATAB{2,2}, 3481.72176560122, 1e-09);
%! assert_equal (ATAB{3,2}, 1837.08812970469, 1e-09);
%! assert_equal (ATAB{4,2}, 4957.20277938622, 1e-09);
%! assert_equal (ATAB{5,2}, 437.693674777847, 1e-09);
%! assert_equal (ATAB{6,2}, 988.431929811402, 1e-09);
%! assert_equal (ATAB{7,2}, 46.616653365254, 1e-09);
%! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09);
%! assert_equal (ATAB{9,2}, 9065.8,  1e-09);
%! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype', 3,'display','off');
%! assert_equal (P(1), 0.000106518678028207, 1e-09);
%! assert_equal (P(2), 0.00125371366571508, 1e-09);
%! assert_equal (P(3), 5.30813260778464e-07, 1e-09);
%! assert_equal (P(4), 0.308353667232981, 1e-09);
%! assert_equal (P(5), 0.0562901327343161, 1e-09);
%! assert_equal (P(6), 0.599091042141092, 1e-09);
%! assert_equal (P(7), 0.0861628249564267, 1e-09);
%! assert_equal (ATAB{2,2}, 3430.88156424581, 1e-09);
%! assert_equal (ATAB{3,2}, 1833.68031496063, 1e-09);
%! assert_equal (ATAB{4,2}, 5080.48346456693, 1e-09);
%! assert_equal (ATAB{5,2}, 382.07709497207, 1e-09);
%! assert_equal (ATAB{6,2}, 963.037988826813, 1e-09);
%! assert_equal (ATAB{7,2}, 44.4519685039322, 1e-09);
%! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09);
%! assert_equal (ATAB{9,2}, 9065.8, 1e-09);

## Test 9 for anovan example 9
## Test compares anovan to results from MATLAB's anovan function
%!test
%! measurement = [444 614 423 625 408  856 447 719 ...
%!                764 831 586 782 609 1002 606 766]';
%! strain= {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ...
%!          'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}';
%! treatment={'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T'}';
%! block = [1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2]';
%!
%! [P, ATAB, STATS] = anovan (measurement/10,{strain,treatment,block},'model','full','random',3,'display','off');
%! assert_equal (P(1:6), [0.288811428913179; 0.091455278902114; ...
%!                        0.042134889025806; 0.010944863181481; ...
%!                        0.061814763198376; 0.066584161056625], 1e-12);
%! assert_equal (P(7), NaN);
%! assert_equal (cell2mat (ATAB(2:8,2)), ...
%!               [286.132499999999; 2275.28999999999; 1242.5625; ...
%!                495.905000000000; 141.472499999998; 47.6099999999987; ...
%!                17.9249999999992], 1e-9);
%! assert_equal (ATAB{9,2}, 0);
%! assert_equal (ATAB{10,2}, 4506.8975, 1e-9);
%! assert_equal (STATS.msdenom, ...
%!               [47.1575; 47.61; 88.7925; 5.975; 5.975; 5.975; 0], 1e-9);
%! assert_equal (STATS.dfdenom, ...
%!               [3; 1; 2.610727841363640; 3; 3; 3; 0], 1e-12);
%! assert_equal (STATS.varest, ...
%!               [144.22125; 20.59125; 10.40875; 5.975; 0], 1e-9);

## Test 10 for anovan example 10
## Test compares anovan to results from MATLAB's anovan function
%!test
%! pulse = [67.9 65.1 77.3 78.7 79.4 80.4 85.8 86.6 87.5 89.1 ...
%!          98.6 100.8 99.3 101.7 44.3 47.2 47.6 49.6 50.3 51.8 ...
%!          60 58.5 58.9 60.7 69.8 70.9 76.2 76.1 77 77.7 84.7]';
%! temp = [20.8 20.8 24 24 24 24 26.2 26.2 26.2 26.2 28.4 ...
%!         29 30.4 30.4 17.2 18.3 18.3 18.3 18.9 18.9 20.4 ...
%!         21 21 22.1 23.5 24.2 25.9 26.5 26.5 26.5 28.6]';
%! species = {'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' ...
%!            'ex' 'ex' 'ex' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' ...
%!            'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv'};
%!
%! [P, ATAB, STATS] = anovan (pulse,{species,temp},'model','linear','continuous',2,'sstype','h','display','off');
%! assert_equal (P(1), 6.27153318786007e-14, 1e-09);
%! assert_equal (P(2), 2.48773241196644e-25, 1e-09);
%! assert_equal (ATAB{2,2}, 598.003953318404, 1e-09);
%! assert_equal (ATAB{3,2}, 4376.08256843712, 1e-09);
%! assert_equal (ATAB{4,2}, 89.3498685376726, 1e-09);
%! assert_equal (ATAB{2,6}, 187.399388123951, 1e-09);
%! assert_equal (ATAB{3,6}, 1371.35413763454, 1e-09);

## Test 11 for anovan example 11
## Test compares anovan to results from MATLAB's anovan function
%!test
%! score = [95.6 82.2 97.2 96.4 81.4 83.6 89.4 83.8 83.3 85.7 ...
%!          97.2 78.2 78.9 91.8 86.9 84.1 88.6 89.8 87.3 85.4 ...
%!          81.8 65.8 68.1 70.0 69.9 75.1 72.3 70.9 71.5 72.5 ...
%!          84.9 96.1 94.6 82.5 90.7 87.0 86.8 93.3 87.6 92.4 ...
%!          100. 80.5 92.9 84.0 88.4 91.1 85.7 91.3 92.3 87.9 ...
%!          91.7 88.6 75.8 75.7 75.3 82.4 80.1 86.0 81.8 82.5]';
%! treatment = {'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  ...
%!              'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'  'no'}';
%! exercise = {'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  ...
%!             'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ...
%!             'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  ...
%!             'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  'lo'  ...
%!             'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ...
%!             'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'  'hi'}';
%! age = [59 65 70 66 61 65 57 61 58 55 62 61 60 59 55 57 60 63 62 57 ...
%!        58 56 57 59 59 60 55 53 55 58 68 62 61 54 59 63 60 67 60 67 ...
%!        75 54 57 62 65 60 58 61 65 57 56 58 58 58 52 53 60 62 61 61]';
%!
%! [P, ATAB, STATS] = anovan (score,{treatment,exercise,age},'model','full','continuous',3,'sstype','h','display','off');
%! assert_equal (P(5), 0.9245630968248468, 1e-09);
%! assert_equal (P(6), 0.791115159521822, 1e-09);
%! assert_equal (P(7), 0.9296668751457956, 1e-09);
%! [P, ATAB, STATS] = anovan (score,{treatment,exercise,age},'model',[1 0 0; 0 1 0; 0 0 1; 1 1 0],'continuous',3,'sstype','h','display','off');
%! assert_equal (P(1), 0.00158132928938933, 1e-09);
%! assert_equal (P(2), 2.12537505039986e-07, 1e-09);
%! assert_equal (P(3), 0.00390292555160047, 1e-09);
%! assert_equal (P(4), 0.0164086580775543, 1e-09);
%! assert_equal (ATAB{2,6}, 11.0956027650549, 1e-09);
%! assert_equal (ATAB{3,6}, 20.8195665467178, 1e-09);
%! assert_equal (ATAB{4,6}, 9.10966630720186, 1e-09);
%! assert_equal (ATAB{5,6}, 4.4457923698584, 1e-09);

## Test 12 for anovan example 12
## Test compares anovan regression coefficients to R:
## https://www.uvm.edu/~statdhtx/StatPages/Unequal-ns/Unequal_n%27s_contrasts.html
%!test
%! dv =  [ 8.706 10.362 11.552  6.941 10.983 10.092  6.421 14.943 15.931 ...
%!        22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ...
%!        22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ...
%!        22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ...
%!        25.694 ]';
%! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]';
%! C = [ 0.4001601  0.3333333  0.5  0.0
%!       0.4001601  0.3333333 -0.5  0.0
%!       0.4001601 -0.6666667  0.0  0.0
%!      -0.6002401  0.0000000  0.0  0.5
%!      -0.6002401  0.0000000  0.0 -0.5];
%!
%! [P,ATAB,STATS] = anovan (dv,g,'contrasts',{C},'display','off');
%! assert_equal (STATS.coeffs(1,1), 19.4001, 1e-04);
%! assert_equal (STATS.coeffs(2,1), -9.3297, 1e-04);
%! assert_equal (STATS.coeffs(3,1), -5.0000, 1e-04);
%! assert_equal (STATS.coeffs(4,1), -8.0000, 1e-04);
%! assert_equal (STATS.coeffs(5,1), -8.0000, 1e-04);
%! assert_equal (STATS.coeffs(1,2), 0.4831, 1e-04);
%! assert_equal (STATS.coeffs(2,2), 0.9694, 1e-04);
%! assert_equal (STATS.coeffs(3,2), 1.3073, 1e-04);
%! assert_equal (STATS.coeffs(4,2), 1.6411, 1e-04);
%! assert_equal (STATS.coeffs(5,2), 1.4507, 1e-04);
%! assert_equal (STATS.coeffs(1,5), 40.161, 1e-03);
%! assert_equal (STATS.coeffs(2,5), -9.624, 1e-03);
%! assert_equal (STATS.coeffs(3,5), -3.825, 1e-03);
%! assert_equal (STATS.coeffs(4,5), -4.875, 1e-03);
%! assert_equal (STATS.coeffs(5,5), -5.515, 1e-03);
%! assert_equal (STATS.coeffs(2,6), 5.74e-11, 1e-12);
%! assert_equal (STATS.coeffs(3,6), 0.000572, 1e-06);
%! assert_equal (STATS.coeffs(4,6), 2.86e-05, 1e-07);
%! assert_equal (STATS.coeffs(5,6), 4.44e-06, 1e-08);

## Interaction columns preserve factor-column ordering.
%!test
%! y = (1:12)';
%! g1 = repmat ([1; 2; 3], 4, 1);
%! g2 = kron ([1; 2], ones (6, 1));
%! [~, ~, stats] = anovan (y, {g1, g2}, 'model', 'full', ...
%!                         'display', 'off');
%! assert_equal (stats.X(:,5), stats.X(:,2) .* stats.X(:,4));
%! assert_equal (stats.X(:,6), stats.X(:,3) .* stats.X(:,4));

## Full six-factor interactions preserve every model term.
%!test
%! n = 128;
%! group = cell (1, 6);
%! for k = 1:6
%!   group{k} = mod (floor ((0:n-1)' / 2^(k-1)), 2) + 1;
%! endfor
%! [p, ~, stats, terms] = anovan ((1:n)', group, 'model', 'full', ...
%!                                'display', 'off');
%! assert_equal (rows (terms), 63);
%! assert_equal (columns (stats.X), 64);
%! assert_equal (numel (p), 63);

## A one-level factor has no estimable main effect.
%!test
%! group = ones (3, 1);
%! [p, tbl, stats] = anovan ((1:3)', group, ...
%!                          'sstype', 2, 'display', 'off');
%! assert_equal (p, NaN);
%! assert_equal (tbl{2, 2}, 0);
%! assert_equal (tbl{2, 3}, 0);
%! assert_equal (tbl{2, 6}, NaN);
%! assert_equal (size (stats.X), [3, 1]);

## Categorical factors retain their declared order and omit missing observations.
%!test
%! group = categorical ([3; 3; NaN; 1; 1], [3, 2, 1]);
%! [p, tbl, stats] = anovan ((1:5)', group, 'sstype', 2, 'display', 'off');
%! [p_ref, tbl_ref] = anovan ([1; 2; 4; 5], [1; 1; 2; 2], ...
%!                           'sstype', 2, 'display', 'off');
%! assert_equal (p, p_ref, 1e-12);
%! assert_equal (tbl, tbl_ref);
%! assert_equal (stats.grpnames{1}, {'3'; '1'});
%! assert_equal (stats.Y, [1; 2; 4; 5]);

## Terms matrices select factors by column rather than row position.
%!test
%! A = [1; 1; 2; 2];
%! B = [1; 2; 1; 2];
%! y = 100 * A + B;
%! [~, tbl, stats, terms] = anovan (y, {A, B}, 'model', [0, 1], ...
%!                                  'display', 'off');
%! assert_equal (terms, [0, 1]);
%! assert_equal (tbl{2, 1}, 'X2');
%! assert_equal (tbl{2, 2}, 1, 1e-12);
%! assert_equal (full (stats.X(:, 2)), [-0.5; 0.5; -0.5; 0.5]);

## Reordering terms changes Type I order without relabeling their columns.
%!test
%! A = [1; 1; 2; 2];
%! B = [1; 2; 1; 2];
%! y = 100 * A + B;
%! [~, tbl] = anovan (y, {A, B}, 'model', [0, 1; 1, 0], ...
%!                    'sstype', 1, 'display', 'off');
%! assert_equal (tbl(2:3, 1), {'X2'; 'X1'});
%! assert_equal (cell2mat (tbl(2:3, 2)), [1; 10000], 1e-10);

## A continuous factor raised to a power is the power of the raw predictor.
%!test
%! x = (1:5)';
%! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6];
%! [p, tbl, ~, terms] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ...
%!                              'sstype', 2, 'display', 'off');
%! assert_equal (terms, [1; 2]);
%! assert_equal (tbl(2:3, 1), {'X1'; 'X1^2'});
%! assert_equal (cell2mat (tbl(2:3, 2)), ...
%!               [4.09767456073500; 216.071428571430], 1e-9);
%! assert_equal (cell2mat (tbl(2:3, 3)), [1; 1]);
%! assert_equal (cell2mat (tbl(4, 2:3)), [0.444571428570271, 2], 1e-9);
%! assert_equal (p, [0.0501972848960940; 0.00102717552592300], 1e-12);

## Hierarchical sums-of-squares adjust a power only for its lower powers.
%!test
%! x = (1:5)';
%! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6];
%! [p, tbl] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ...
%!                    'sstype', 'h', 'display', 'off');
%! assert_equal (cell2mat (tbl(2:3, 2)), ...
%!               [7225.34400000000; 216.071428571430], 1e-8);
%! assert_equal (p, [3.07633044150000e-05; 0.00102717552592300], 1e-12);

## The hierarchical sums-of-squares selector is not case sensitive.
%!test
%! x = (1:5)';
%! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6];
%! [p_lower, tbl_lower] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ...
%!                                'sstype', 'h', 'display', 'off');
%! [p_upper, tbl_upper] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ...
%!                                'sstype', 'H', 'display', 'off');
%! assert_equal (p_upper, p_lower);
%! assert_equal (tbl_upper, tbl_lower);

%!error <categorical factors cannot have exponent> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'model', [2, 0], 'display', 'off')

%!error <NESTED must be an N-by-N logical matrix> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'nested', [0, 1], 'display', 'off')

%!error <NESTED must be an N-by-N logical matrix> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'nested', [0, 2; 0, 0], 'display', 'off')

%!error <NESTED must be an N-by-N logical matrix> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'nested', eye (2), 'display', 'off')

%!error <NESTED factor relationships must be acyclic> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'nested', [0, 1; 1, 0], 'display', 'off')

%!error <nested factors must be categorical> ...
%! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ...
%!         'nested', [0, 0; 1, 0], 'continuous', 1, 'display', 'off')

%!error <custom contrasts are not supported for nested factors> ...
%! anovan ((1:8)', [kron([1; 2], ones(4, 1)), ...
%!         repmat([1; 1; 2; 2], 2, 1)], 'nested', [0, 0; 1, 0], ...
%!         'contrasts', {[]; [-0.5; 0.5]}, 'display', 'off')

## A design that is not of full rank keeps a finite error sum-of-squares.
%!test
%! A = [1; 1; 1; 1; 2; 2];
%! B = [1; 1; 2; 2; 1; 1];
%! y = [3; 4; 7; 8; 5; 6];
%! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'sstype', 3, ...
%!                    'display', 'off');
%! assert_equal (tbl{end-1, 2}, 1.5, 1e-12);
%! assert_equal (tbl{end-1, 3}, 3);
%! assert_equal (tbl{end-1, 5}, 0.5, 1e-12);
%! assert_equal (tbl{end, 2}, 17.5, 1e-12);

## Which terms are aliased depends on the model each is adjusted for.
%!test
%! A = [1; 1; 1; 1; 2; 2];
%! B = [1; 1; 2; 2; 1; 1];
%! y = [3; 4; 7; 8; 5; 6];
%! [~, t3] = anovan (y, {A, B}, 'model', 'full', 'sstype', 3, 'display', 'off');
%! assert_equal (cell2mat (t3(2:4, 3)), [0; 0; 0]);
%! assert_equal (cell2mat (t3(2:4, 4)), [1; 1; 1]);
%! [~, t2] = anovan (y, {A, B}, 'model', 'full', 'sstype', 2, 'display', 'off');
%! assert_equal (cell2mat (t2(2:4, 3)), [1; 1; 0]);
%! assert_equal (cell2mat (t2(2:4, 4)), [0; 0; 1]);
%! assert_equal (cell2mat (t2(2:4, 2)), [4; 16; 0], 1e-12);

## A partially aliased term reports the degrees of freedom it can estimate.
%!test
%! A = [1; 1; 2; 2; 3; 3];
%! B = [1; 1; 1; 1; 2; 2];
%! y = [3; 4; 7; 8; 5; 6];
%! [~, tbl] = anovan (y, {A, B}, 'model', 'linear', 'sstype', 3, ...
%!                    'display', 'off');
%! assert_equal (cell2mat (tbl(2:3, 3)), [1; 0]);
%! assert_equal (cell2mat (tbl(2:3, 4)), [1; 1]);
%! assert_equal (tbl{2, 2}, 16, 1e-12);

## A full rank design flags nothing.
%!test
%! A = [1; 1; 2; 2; 1; 1; 2; 2];
%! B = [1; 2; 1; 2; 1; 2; 1; 2];
%! y = [3; 4; 7; 8; 5; 6; 9; 11];
%! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'display', 'off');
%! assert_equal (cell2mat (tbl(2:4, 4)), [0; 0; 0]);
%! assert_equal (cell2mat (tbl(2:4, 2)), [36.125; 3.125; 0.125], 1e-12);

## A factor with a single level has no degrees of freedom but is not aliased.
%!test
%! [~, tbl] = anovan ((1:3)', ones (3, 1), 'sstype', 2, 'display', 'off');
%! assert_equal (tbl{2, 3}, 0);
%! assert_equal (tbl{2, 4}, 0);
%! assert_equal (tbl{end-1, 2}, 2, 1e-12);

## Both effect sizes are reported, each under its own name.
%!test
%! A = [1; 1; 2; 2; 1; 1; 2; 2];
%! B = [1; 2; 1; 2; 1; 2; 1; 2];
%! y = [3; 4; 7; 8; 5; 6; 9; 11];
%! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'display', 'off');
%! assert_equal (tbl(1, :), {'Source', 'Sum Sq.', 'd.f.', 'Singular?', ...
%!                           'Mean Sq.', 'F', 'Prob>F', 'Eta Sq.', ...
%!                           'Part. Eta Sq.'});
%! ss = cell2mat (tbl(2:4, 2));
%! sse = tbl{end-1, 2};
%! sst = tbl{end, 2};
%! assert_equal (cell2mat (tbl(2:4, 8)), ss ./ sst, 1e-12);
%! assert_equal (cell2mat (tbl(2:4, 9)), ss ./ (ss + sse), 1e-12);

## A model with a random factor reports what each mean square estimates.
%!test
%! y = [444 614 423 625 408 856 447 719 ...
%!      764 831 586 782 609 1002 606 766]' / 10;
%! g1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ...
%!       'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}';
%! g2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}';
%! g3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2];
%! [~, tbl] = anovan (y, {g1, g2, g3}, 'model', 'full', 'random', 3, ...
%!                    'display', 'off');
%! assert_equal (size (tbl), [10, 17]);
%! assert_equal (tbl(1, 1:15), {'Source', 'Sum Sq.', 'd.f.', 'Singular?', ...
%!               'Mean Sq.', 'F', 'Prob>F', 'Type', 'Expected MS', ...
%!               'MS denom', 'd.f. denom', 'Denom. defn.', 'Var. est.', ...
%!               'Var. lower bnd', 'Var. upper bnd'});
%! assert_equal (tbl(2:9, 8), {'fixed'; 'fixed'; 'random'; 'fixed'; ...
%!                             'random'; 'random'; 'random'; 'random'});
%! assert_equal (cell2mat (tbl(2:8, 11)), ...
%!               [3; 1; 2.610727841363640; 3; 3; 3; 0], 1e-12);

## With every factor fixed the table keeps the fixed-effects layout.
%!test
%! [~, tbl] = anovan ((1:12)', {repmat([1;2;3],4,1), kron([1;2],ones(6,1))}, ...
%!                    'model', 'full', 'display', 'off');
%! assert_equal (size (tbl), [6, 9]);
%! assert_equal (tbl{1, 9}, 'Part. Eta Sq.');

## A random factor keeps its interactions in the model.
%!test
%! y = [444 614 423 625 408 856 447 719 ...
%!      764 831 586 782 609 1002 606 766]' / 10;
%! g1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ...
%!       'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}';
%! g2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}';
%! g3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2];
%! [p, tbl, s, terms] = anovan (y, {g1, g2, g3}, 'model', 'full', ...
%!                              'random', 3, 'display', 'off');
%! assert_equal (rows (terms), 7);
%! assert_equal (numel (p), 7);
%! assert_equal (size (s.denom), [7, 5]);
%! assert_equal (size (s.ems), [8, 8]);
%! assert_equal (numel (s.txtems), 8);

## An interaction term is named with Wilkinson's interaction operator.
%!test
%! [~, tbl] = anovan ((1:12)', {repmat([1;2;3],4,1), kron([1;2],ones(6,1))}, ...
%!                    'model', 'full', 'display', 'off');
%! assert_equal (tbl(2:end, 1), ...
%!               {'X1'; 'X2'; 'X1:X2'; 'Error'; 'Total'});

## A three-factor interaction names every factor in it.
%!test
%! g = cell (1, 3);
%! for k = 1:3
%!   g{k} = mod (floor ((0:15)' / 2^(k-1)), 2) + 1;
%! endfor
%! [~, tbl] = anovan ((1:16)', g, 'model', 'full', 'display', 'off');
%! assert_equal (tbl{8, 1}, 'X1:X2:X3');

## The random-factor marker is a printing convention, not part of the names.
%!test
%! y = [3; 4; 7; 8; 5; 6; 9; 11];
%! A = [1; 1; 2; 2; 1; 1; 2; 2];
%! B = [1; 2; 1; 2; 1; 2; 1; 2];
%! [~, tbl, s] = anovan (y, {A, B}, 'model', 'full', 'random', 2, ...
%!                       'display', 'off');
%! assert_equal (tbl(2:4, 1), {'X1'; 'X2'; 'X1:X2'});
%! assert_equal (s.varnames, {'X1', 'X2'});
%! assert_equal (any (cellfun (@(t) any (t == "'"), s.txtems)), false);
%! assert_equal (any (cellfun (@(t) any (t == "'"), s.txtdenom)), false);

## The printed table marks a random factor with a trailing quote.
%!test
%! y = [3; 4; 7; 8; 5; 6; 9; 11];
%! A = [1; 1; 2; 2; 1; 1; 2; 2];
%! B = [1; 2; 1; 2; 1; 2; 1; 2];
%! visible = get (0, 'defaultfigurevisible');
%! unwind_protect
%!   set (0, 'defaultfigurevisible', 'off');
%!   txt = evalc (["anovan (y, {A, B}, 'model', 'full', 'random', 2, ", ...
%!                 "'display', 'on');"]);
%! unwind_protect_cleanup
%!   set (0, 'defaultfigurevisible', visible);
%!   close all;
%! end_unwind_protect
%! assert_equal (! isempty (strfind (txt, "X2'")), true);
%! assert_equal (! isempty (strfind (txt, "X1:X2'")), true);
