Two new helpers build the input to a growth model on SSM coordinates, so
the reader of the "Growth Models on SSM Parameters" vignette writes neither
the reshape nor the model formula by hand. ssm_growth_data() scores each
row of a person-by-time table as its own profile and stacks the three
coordinates e, x and y into a long table with columns <id>,
<time>, dv and value, one row per input row per coordinate. It
refuses an id or time name absent from the data, a non-numeric time
column, missing values in either, and an id or time named dv or
value or equal to each other. ssm_growth_formula() returns the
package's one joint growth model as formula objects for glmmTMB, nlme or
brms, with the time and id names substituted. Printing it shows the
engine's complete fit call on the long table. Below the call it shows the
line that extracts the fixed effects and their covariance, or the draws
for brms. The package still fits nothing and calls no engine. nlme is a
new suggested package, and its dialect fits the same model: its fixed
effects match glmmTMB's to 1e-6 on the bundled growth data.
ssm_trajectory() is the output side of the same recipe. It takes the
fitted joint model's fixed effects and their covariance, or a matrix of
posterior coefficient draws such as as.matrix(fit) from brms, and
returns one row per time point with the estimate and interval of e,
x, y, a and d and the displacement certification. From
coefficients and covariance it draws n_draws coefficient vectors; a
draws matrix is summarized as given. The default contrast reads the six
coefficient names the model from ssm_growth_formula() produces, drops a
b_ prefix and ignores other columns, and a contrast function replaces
it for a model with other terms. A covariance matrix whose implied
covariance between x(t) and y(t) is exactly zero at every time is
refused, unless it is zero everywhere, since coordinates fit in separate
models give wrong displacement intervals. A draws matrix is not checked.
The object records its input shape in an input attribute and prints
rounded, with each uncertified row marked and a closing caution for that
shape: under coef and vcov, that intervals from a fitted model's
fixed-effect covariance condition on its estimated variance components
and are too narrow at small samples; under draws, that the intervals
summarize the draws as given and the joint fit was not checked. An
object that no longer carries the attribute prints a caution that says
the shape is not recorded. rbind() of trajectory tables keeps a shared
shape and drops a mixed one. ssm_plot_trajectory() plots the object
with no time argument. Its amplitude and displacement columns match
the hand-built per-wave loop the growth vignette used before, to 1e-12
under the same seed.
geom_ssm_ellipse() is a new ggplot2 layer for the circumplex canvas. For
each row it draws the confidence ellipse of a bivariate normal region on
the Cartesian (x, y) SSM coordinates, from a centre (x0, y0) and the
three elements of a 2 by 2 covariance matrix, at a chosen level. An
ellipse that straddles 0/360 degrees is drawn across the seam, and one whose
inscribed polygon contains the origin winds once round the centre.
ssm_ellipse_data() is a
new generic that computes those five columns from an ssm_draws() object:
the centre is the object's x_est and y_est, and the covariance is the
sample covariance of the draws of x and y. The "Advanced Circumplex
Visualization" vignette draws the ellipse beside the wedge and the point
for the same posterior draws and states how the two regions differ.
The package has three new simulated datasets. simulated_occasions holds
octant scores for 200 persons at three waves. simulated_growth and
simulated_growth_origin hold octant scores for 150 persons at five waves.
Their help pages state how the data were simulated. The "Advanced Circumplex
Visualization" and "Growth Models on SSM Parameters" vignettes load them with
data().
coord_circumplex() and ggcircumplex() take a grid argument. The
default "polar" is the canvas as before. "cartesian" draws one ring at
the outer amplitude, a crosshair along displacements 0/180 and 90/270 with
a tick mark and a signed label at each amplitude break between the center
and the rim on every half-axis, and no other ring, spoke or amplitude axis.
The labels on the 180 and 270 halves are Cartesian coordinates, not
negative amplitudes. On ggcircumplex() the cartesian grid also puts tick
marks outward from the rim at the scale angles. This is the canvas of Nagy,
Etzel and Lüdtke (2019). The data are drawn the same way on both grids.
Plotmath amplitude labels (an expression, or a labelling function returning
one) draw as plotmath on the crosshair. On the 180 and 270 halves they take
a leading minus. A label that is a call is put in parentheses, as in
-(a + b), unless it is a subscript, a superscript or already in
parentheses.
ggcircumplex() also has a new angle_labels argument, independent of
grid: TRUE labels each text label as <label> (<angle>°), the 0/360
pole written as 360, read along its radius. The default degree labels are
left as they are. Each side of the plot margin is widened only for the
labels that point toward it, estimated at half the font size per
character, so the circle can sit off-center when the labels are uneven.
On the displacement panel of ssm_plot_trajectory(), the line segments on
either side of a time point whose displacement is not interpretable are now
drawn dashed, and the "Displacement interpretable" legend shows the line
type beside the point shape. A dashed segment touches a time point with no
interpretable direction, so the direction of change along it is not to be
read; the segment is kept so the unwrapped branch stays visible. A
trajectory table row whose certified value is NA is now drawn as a
hollow point on dashed segments when another row carries a verdict, where
before its point was silently left out of the panel. The "Growth Models on
SSM Parameters" and "Advanced Circumplex Visualization" vignettes describe
the dashed segments.
axes_reliability() no longer refuses a fit as "unidentified" on the
conditioning of its information matrix. That refusal came from the default
tolerance of solve(), and at a fitted matrix near machine precision it
fired on some platforms and not on others. The per-fit accuracy check now
judges every such fit: below the conditioning floor, a fit that a platform
used to refuse "unidentified" is refused "uncertified" with its
estimated relative error in the warning, or computes when the check places
its numbers inside the accuracy target. A fit the floor admits is also
checked when its information matrix is nearly singular. "unidentified"
now names only a design defect: two identical derivative matrices, a
derivative matrix equal to the identity, or an exactly singular inversion.
Where the previous tolerance inverted, every reported number is unchanged,
because the tolerance is the only change to that arithmetic, and every
newly checked fit of a kind the function can produce that the package's
sweep measured passed the check. A fit the floor admits but that the function itself refuses at its
door (fewer than four scales) can now be refused "uncertified" at the
internal helpers where it used to compute. The internal uncorrected arm,
reported in details$naive_reason, keeps the default tolerance and can
still differ across platforms at such a matrix.
scales(x, items = TRUE) no longer stops with "subscript out of bounds"
on iip32 and iip64. These licensed instruments ship a notice in place
of their item text, and scales() now prints that notice once, after the
scale lines.
items() and scales(items = TRUE) wrap long item text, and the item
prefix and suffix, to the console width. A wrapped item continues under
the first character of its text.
print() and summary() of a cpm_fit() result no longer show the
Communality column in the results table. It equals Zeta squared, which
the table still prints, and results$Communality still holds it. Without
it, the table prints as one block at width 77 for scale names of up to 16
characters.
Printed notes wrap more cleanly at narrow console widths. In cpm_fit()
output, the Heywood note keeps "(ζ > 0.995," on one line, and the
bootstrap marker note breaks its opening words instead of running past the
width. In summary() of an axes_reliability() result, the sentence about
lavaan's *.scaled definitions starts a new line. In print() of an
ssm_sem() result with an invariance ladder, the Verdict: line wraps,
and all ladder notes wrap by the same rule as the package's other notes.
These changes move line breaks only. The text of every note is the same.
The "Growth Models on SSM Parameters" vignette prints the variance
components of the joint model after the first fit, so the reader sees the
correlated person block that separate fits cannot estimate. It also
explains the check ssm_trajectory() makes on the fixed-effect covariance
matrix: a matrix assembled from separate fits implies a covariance of
exactly zero between x(t) and y(t) at every time, and the helper
refuses that structure only. A nonzero covariance does not show that the
model is right.
The "Advanced Circumplex Visualization" vignette has a new section, "The
latent circumplex from a CPM fit". Its circle figures follow Nagy, Etzel
and Lüdtke (2019, Figures 3, 4 and 6) and are drawn on the cartesian
canvas with the layers the package already exports. The first is the
canvas alone, its rim ticks and labels at the angles cpm_fit()
estimated. The second adds five measures from ssm_analyze() as labelled
points joined to the origin. The third annotates one measure with an arc
for its displacement and its two values. A fourth, linear figure draws the
fitted correlation function against angular separation, with the observed
and reproduced correlations as points. The ellipse figure adds two
tangent lines from the origin out to the rim, at the angles of the
confidence ellipse's tangents, and the lines from the origin to the
ellipse's nearest and farthest boundary points, computed in an echoed chunk
that the test suite runs against independent oracles. The prose states
which model each set of angles comes from.
Every prose caution and note that print() and summary() show now wraps
to the console width you set with options(width = ). Before this, the
text ignored the width you set. Some cautions broke their lines at a column
written into the package and some printed as one long line, so they ran
past a narrow console or stopped short of a wide one. The
words and their order do not change. Only the line breaks move. A
fired marker label, such as Heywood communality, still stays on one line.
Tables, column headers, fit lines and section headings do not wrap,
because wrapping them would destroy their columns.
In summary() for an ssm_ci_accuracy() result, the heading above the
verdicts now reads Verdicts (c = 1, as estimated), Bradley (1978) liberal band, 95% Wilson CIs:. It was two characters longer. The heading is not
wrapped, so in a vignette, where each output line starts with #> , the old
heading took 81 columns and the new one takes 79.
In print() and summary() for a cpm_fit() result, the table of angles
and communality indices uses shorter column headers: Theory for
Angle_theory, and lci and uci after each estimate for its confidence
limits. At a width of 77 the Communality column no longer drops into a
second block below the table. The values do not change, and the results
data frame keeps its column names.
The heading of print() for an ssm_ci_accuracy() result now wraps to the
console width. The first entry in this section keeps headings unwrapped to
protect their columns. This heading is a sentence about the simulation
settings with no columns under it, so it wraps. Before this, it printed as
one line of any length.
The "Evaluating Circumplex Structure" vignette now shows the short
print() report of ssm_ci_accuracy() first, and the full summary()
report after it. It also says before each affected chunk that a warning
about an ill-conditioned CPM Hessian will print.
The vignettes set options(width = 77), so output that follows the console
width fits, with its #> prefix, in 80 columns. The package website's
code box shows 81 monospace columns without scrolling. The generated lavaan
syntax in the SEM vignette is printed with cat() and stays wider.
summary() for an ssm_ci_accuracy() result is about half as long. The
settings print as three short sentences, usually on three lines. The
coverage and guardrail tables are replaced
by one table with a row for each profile and amplitude condition. That table
shows the coverage of each parameter, the displacement coverage when
certified, the certification rate and the Structural flag. The other
columns are no longer printed, but they are still in the coverage and
guardrail elements of the result. The verdicts and cautions say the same
things. Only their punctuation and a few joining words change (see the
entry on dashes below).
print() for a multi-group ssm_sem() result shows the invariance verdict
as a Verdict: line and short labeled lines under it. The labels are
Test: and Result:, and Also: for a rung rejected above the required
one. Contrast: and Profiles: print when the groups cannot be compared.
If they cannot be compared and a contrast was requested, Instead: also
prints. The labeled values wrap to the console width. In a UTF-8 locale,
every line of the block fits at 77 and 80 columns. The stored verdict text
does not change, and the warning and the error that quote it do not change.
print() for a multi-group ssm_sem() result shows the dcfi and cr
columns and a short ΔCFI note only for a two-group fit estimated by ML with
a plain CFI. The .01 rule was simulated only for that case. The columns and
the note also need a rung with a dcfi value. The note is two lines at 80
columns. For other fits, including the default estimator = "MLR", the
dcfi values stay in the invariance element of the result.
invariance$dcfi_scope records the number of groups and the estimator as
lavaan reports it ("ML" for "MLR" and "MLM"). It also records
whether the CFI is plain.
The printed output of ssm_ci_accuracy(), axes_reliability() and
ssm_sem() results, and the refusal warning of axes_reliability() for
nearly collinear items, no longer use -- as a dash. For example, a verdict
now reads Verdict: CAUTION. Amplitude CIs ... and a coverage line reads
coverage 96.7%: borderline.
plot() for a cpm_fit() result now draws a confidence interval of zero
width as a visible line. Before, a zero-width angle interval was not drawn at
all. A zero-width angle interval is now drawn as a line along the radius. A
zero-width communality interval is drawn as a line along the arc. When both
widths are zero, a short cap is drawn across the interval. The cap has the
same length at every radius except near the centre, where it is shorter. The
reference scale, whose angle is fixed, now shows its communality interval
this way.
plot() for a cpm_fit() result now warns about a scale whose
communality interval is 0 at both ends, and draws that scale as a point. The
warning for a scale drawn without its interval now names the reason for each
scale.
ssm_plot_circle() and plot() for a cpm_fit() result now draw the
amplitude (or communality) axis in the widest gap between spokes that holds
no plotted point, so that the axis labels do not cover a point. When every
gap holds a point, the axis goes in the widest gap, as before, and a label
can still cover a point.
Four vignettes now open with a "Not yet peer reviewed" notice under their Level line. The pages are "SEM-Based SSM Analysis", "Latent Group Contrasts", "Bayesian SSM Analysis" and "Growth Models on SSM Parameters". Each teaches a method that is the package's own proposal. The notice says that the package's authors have not yet published the method in a peer-reviewed venue. The package website shows the notice as a warning box.
The "Growth Models on SSM Parameters" vignette is rewritten on the growth
helpers, and every chunk is shown except the unlabeled options chunk at
the top and the note for a missing glmmTMB. The first worked
example is five calls. ssm_growth_data() builds the long table, and
ssm_growth_formula() prints the glmmTMB fit call. That call is pasted
with its two formulas read from the object. ssm_trajectory() turns the
fixed effects and their covariance into the trajectory table, and
ssm_plot_trajectory() draws it. The second example reuses the formula
object, so it is four calls. The reshape, the draw function and the
per-wave loop the page used to hide are gone. A new Section 10 shows the
same model's nlme call and its brms call. The brms fit is shown and not
run. Its six fixed-effect draw columns ship as
vignettes/growth_brms_draws.rds, written by the seeded
data-raw/growth-brms-draws.R. So the page summarizes posterior draws
through ssm_trajectory(draws = ) without a Stan toolchain.
?simulated_growth now names the vignette under See also.
The same vignette's Section 7 now states what the package measured about
the REML intervals rather than naming remedies for engines the package
does not write. It reports the coverage oracle's result at 200 persons
per cell, says that the package ships no small-sample correction, and
explains why a percentile parametric bootstrap of the fixed effects is
not one on complete balanced data. It also says that a matrix of
coefficient draws from any source enters ssm_trajectory() as draws =.
The lme4 remedies it named before are gone.
?axes_reliability now says that at a severely ill-conditioned fit, the
error estimate printed with an "uncertified" refusal depends on the
machine as well as the data. It cites one test matrix whose estimate
differed between macOS with R's reference BLAS and LAPACK and Linux with
OpenBLAS, while both machines refused it.
The website's vignette menu is now built by pkgdown from the articles index rather than written out a second time by hand, so the menu is titled "Articles" and lists each level's pages in the index's own order.
The "SEM-Based SSM Analysis" and "Axes Reliability" vignettes are each now two pages, with every code chunk kept and the prose moved rather than cut. "SEM-Based SSM Analysis" keeps its name and holds the latent profile of a measure, its measurement model, its confidence intervals and what its parameters mean. "Latent Group Contrasts" holds the two questions about group differences, the invariance-gated latent contrast, the limitations of the method and its relation to the literature. "Axes Reliability" keeps its name and holds the worked example, the variance components and the correlation-matrix input. "Axes Reliability Caveats" holds the correlation-metric correction and its calibration table, missing data, boundary fits, blockwise instruments and the standard error of measurement. The four pages are read in that order.
The "Evaluating Circumplex Structure" vignette is now four shorter pages,
with every code chunk kept and the prose moved rather than cut.
"Evaluating Circumplex Structure"
keeps its name and holds the two questions, the circular process model fit,
its fit indices and its model variants. "CPM Fits at a Boundary", an
Advanced page, holds the boundary section behind a refit of the same
model. "Confidence Interval Accuracy" holds the published accuracy
findings, ssm_ci_accuracy() and the checklist. "Structure Tests and
Ipsatization" holds fit_structure() and ipsatize(). Cross-references
that named a section now name a page.
Every vignette now opens with a Level line (Introductory, Intermediate or Advanced) and an Overview that names its sections. Each closes with a Wrap-up that names the next page, or a related page where a path ends. The website groups the vignettes by level in reading order, and the "Axes Reliability" vignette now appears in its menu.
The code shown in six vignettes now keeps to the package calls and model
fits they teach. Code that simulates data, builds or formats a display table,
or checks internals is hidden, and the prose says what it does. The "Advanced
Circumplex Visualization" and "Growth Models on SSM Parameters" vignettes now
load the new simulated datasets with data(). The growth vignette shows
ssm_draws() called for a single wave and hides the loop over waves. The
"Evaluating Circumplex Structure" vignette writes out its three cpm_fit()
calls.
The ssm_plot_trajectory() help page now describes correctly where each
confidence bound is drawn on the displacement panel. The lower bound sits
below the estimate by the counterclockwise angle from the lower bound to the
estimate, and the upper bound sits above the lower bound by the interval's
counterclockwise width. The plot itself did not change.
In the "Advanced Circumplex Visualization" vignette, the three panels of the occasions trajectory figure are no longer tall and narrow, and the axis labels of the two circle figures of the occasions no longer cover a point. The first of those circle figures now labels its spokes with the octant abbreviations (PA to NO) instead of degrees.
Six vignettes now use plain English: "Bayesian SSM Analysis", "Growth Models on SSM Parameters", "Advanced Circumplex Visualization", "Axes Reliability", "SEM-Based SSM Analysis" and "Evaluating Circumplex Structure". Their sentences are shorter and carry no em dashes or semicolons. Each now links the introduction vignette near its start. That pass changed neither their code nor its output.
Four statements in two vignettes now match what the package does. In
"SEM-Based SSM Analysis", the plane factors are fixed isotropic and
orthogonal only under the default scaled tier. The strict tier frees the
factor variances and covariances. In "Evaluating Circumplex Structure",
print() shows an uncertified displacement and its interval with a note
that the displacement is not interpretable. It does not withhold them. The
fit_structure() label is "not clearly supported" in print() and
"unsupported" in summary(), not "weak". And the default cpm_fit() model
estimates every scale angle except the reference scale's, which anchors the
rotation. It does not fix them all at their theoretical values.
The latent profile table in "SEM-Based SSM Analysis" now leaves out the X and Y values. On the package website, each estimate and its interval now stay on one line.
The prose of "Evaluating Circumplex Structure" now names what the shorter
printed reports show. For example, it reads the certification rate from the
cert column of the accuracy summary's coverage table. "Axes Reliability"
now says that its table prints a missing Nunnally-Bernstein comparison as
--.
The package's tests now pass on CRAN's macOS x86_64 flavors. The check of
ssm_sem_syntax()'s single-group output against a stored copy compared
cosine and sine loadings printed at 17 significant digits, and that platform's
math library rounds one of them a single unit in the last place differently.
The comparison now ignores differences beyond 12 significant digits and
nothing else. ssm_sem_syntax()'s output is unchanged.
The package's tests now pass on Linux arm64. Two checks of ssm_draws()
compared amplitudes computed by the package's C++ code against the same
amplitudes computed in R, and required the two to agree to the last bit.
They do on every other platform, but on that one they can differ by one
unit in the last place. The checks now require agreement to 12 significant
digits. No result reported by ssm_draws() has changed.
The package's tests now pass on Linux arm64 in a third place. One check
priced a deliberately ill-conditioned matrix and required
axes_reliability()'s internal accuracy check to report a number for it.
Whether a number can be computed at that matrix depends on the platform's
linear algebra library: on Linux arm64 the inversion gives up, which the
check treated as a defect. It now accepts either outcome and requires what
is true on both — that the fit is refused, and that the accuracy check says
so. axes_reliability() refuses that matrix on every platform, as it did
before, and no result it reports has changed.
The figures in the pre-rendered vignettes (Introduction to SSM Analysis, Intermediate SSM Analysis, Advanced Circumplex Visualization, Evaluating Circumplex Structure, SEM-Based SSM Analysis, Growth Models on SSM Parameters, and Axes Reliability) are rendered at twice the previous resolution. They had been rendered at 72 dpi and stretched to the article width on the package website, so they looked soft on high-resolution screens.
This is a major release. Its flagship addition is cpm_fit(), a native
reimplementation of Browne's (1992) circular stochastic process model for the
correlational structure of circumplex scales — filling the gap left by the
archived CircE package, the previous R implementation. Alongside it come four
other new analysis families: latent-variable SSM analysis with ssm_sem(),
repeated-measures (longitudinal) SSM analysis, fit_structure() for
exploratory circumplex-structure tests, axes_reliability() for the
reliability of the circumplex axes (Strack et al., 2013), and
ssm_ci_accuracy(), a diagnostic for whether an ssm_analyze() result's
confidence intervals can be trusted at your sample size and profile
(Zimmermann & Wright, 2017). The
plotting layer has been rebuilt on a real ggplot2 coordinate system.
The component standard errors reported by axes_reliability() are now
calibrated. Previously they were computed as if the item correlation matrix
were a covariance matrix — the source paper's own practice, documented as
approximate — which for strong-axes instruments overstated the standard
error of the axes variance by 25–45%, and for weak-axes, strong-general
instruments could understate it slightly. Because the error changed sign
across the range of instruments the function accepts, no fixed caveat could
state it honestly. Point estimates, reliabilities, SEm, degrees of freedom,
and SRMR are all unchanged; the remaining fit statistics are corrected
separately, below. Corrected standard errors are
typically smaller than those printed in Strack et al. (2013), whose LISREL
values carry the same uncorrected approximation. The uncorrected values
remain available in details$se_uncorrected.
The global fit statistics reported by axes_reliability() are now
calibrated to the correlation metric. chisq, pvalue, rmsea and cfi
are Satorra-Bentler-type scaled values, computed by dividing the
chi-square by a factor evaluated at the fitted matrix (with cfi also
scaling its baseline model); df and srmr are unchanged. Previously these
carried the same correlation-as-covariance mismatch as the standard errors
did, running the other way: sample correlations vary less than covariances,
so the test statistic came out too small and fit was flattered — by roughly
4% at one reference population, which the documentation had reported as
though it were a constant. It is not a constant, and the scaling factor is
now recomputed for every fit. All three input paths scale, including
missing = "fiml". Expect slightly larger chi-squares, smaller p-values,
slightly higher RMSEA, and slightly lower CFI than previous versions
reported on the same data. The unscaled values remain available in
details$fit_uncorrected, with the factors in details$scaling_factor. If
the factor cannot be computed, the four are NA with the reason in
details$fit_scaling_failed rather than falling back to the unscaled
values. The correction is calibrated in mean and its test is asymptotically
exact, but at small samples relative to the item count it over-rejects: see
?axes_reliability for the measured rates and the sample sizes they were
measured at. Note the direction — the scaled test over-flags misfit, where
the uncorrected one flattered it. These follow the definitions lavaan calls
chisq.scaled, pvalue.scaled, rmsea.scaled and cfi.scaled, not its
*.robust forms. Since the fit itself is estimated with plain ML,
fitMeasures() on an equivalent fit reports the unscaled values under the
bare names and no *.scaled or *.robust measure at all, so a cross-check
against lavaan's bare cfi will differ and a request for cfi.robust will
come back empty.
axes_reliability() now refuses a degenerate fitted covariance matrix under
a single stated criterion, evaluated in the metric every reported number is
computed in. Where it refuses, both surfaces refuse: the component standard
errors and the four scaled statistics (chisq, pvalue, rmsea, cfi)
are NA together, each surface's warning names that shared reason, and
df and srmr still report. Which fits it refuses is settled in two
steps. A fitted matrix whose smallest eigenvalue, relative to its largest,
falls at or below sqrt(p * .Machine$double.eps / 1e-5) is not refused for
that alone — it is checked. The check replays that fit's own arithmetic in
roughly 31-digit precision and estimates the relative error the numbers it
produced actually carry — the corrected standard errors, the factor the
scaled statistics are divided by, and the ratio that multiplies the
reported standard error on the missing = "fiml" path; a fit whose worst
estimate is within the accuracy target 1e-4 computes normally. Most fits
below the floor do: over the geometries measured, their estimated errors
run around 1e-11. A fit whose worst estimate exceeds the target, or that
the check cannot price at all, is refused as "uncertified", and its
warning names that same worst estimate — so a fit can be refused on that
ratio while its standard errors alone would have passed. The other two
reasons never reach the check: "indefinite" where the smallest eigenvalue is
negative by more than the fit's own convergence noise — a statement about
the model rather than about arithmetic, which no arithmetic check can
license — and "singular" where the matrix carries non-finite entries,
which the check cannot price. The 1e-5 is the accuracy
target 1e-4 — the largest relative error a reported standard error may
carry, set from the resolution those standard errors are printed at and
from the coverage of a nominal 95% Wald interval, and corroborated by the
standard error's own sampling variability, under which a numerical error at
the target is about a tenth of the statistical noise already in the number
at sample sizes up to about 500,000 for 1/sqrt(2), the typical relative
sampling coefficient, and only up to about 2,000 for 0.045, the least
favorable geometry measured (below the sample sizes typical of published
circumplex correlation matrices) — divided by the
factor of 10 by which the criterion's error bound may undershoot the error
it stands for. Where this criterion refuses a fit as "uncertified", the
warning also names the estimated relative error, and then the
conditioning — the condition number where the
smallest eigenvalue is positive, and otherwise that the matrix is
numerically rank-deficient,
which is what a duplicate item pair makes it — and names item pairs
correlated tightly enough to force the refusal on their own: one pair with
advice to drop one of them, several with the count and up to three of them
named. That diagnosis rides the warning; the stored result's reason
fields, and the note print() shows for them, still carry the bare
code.
(The scaled-fit surface has a second, separate refusal, "ill_conditioned",
for a numerical cancellation rather than for conditioning; that one carries
no such diagnosis, because it is reached only by a matrix this criterion
accepted, whose conditioning is therefore not the reason.)
The standard-error surface additionally applies the same criterion to the
raw fitted matrix, which one internal arm of its computation — the
uncorrected normal-theory pricing kept only as a diagnostic tie to lavaan's
own standard errors — inverts. A matrix degenerate only in the raw metric
(wildly unequal fitted variances over a well-conditioned correlation
structure) refuses that internal arm alone: the reported standard errors
and scaled fit statistics all compute, with no warning, and the internal
refusal is recorded silently in details$naive_reason (a new details
field, NULL whenever that arm computed) under the same reason
vocabulary. Under the shared criterion the two surfaces' user-facing
refusals therefore agree exactly, and on a unit-diagonal fitted matrix the
two metrics coincide; each surface retains its own refusals outside the
criterion (such as the saturated-model door, which touches only the fit
statistics).
Previously the two surfaces disagreed at the numerical margin — whichever
internal solve() failed first refused with an incidental label — so a
sufficiently degenerate fitted matrix could yield NA corrected standard
errors beside silently scaled fit statistics derived from the same matrix.
The failure-reason vocabulary is now shared across both surfaces, and the
reported literals change as follows. On details$se_correction_failed alone: a
nonpositive fitted variance reports "singular" where it previously
reported "nonpositive_diagonal", and a positive-infinite one reports
"infinite_diagonal" rather than "unidentified". On
details$se_correction_failed and details$fit_scaling_failed alike: an
exactly singular fitted matrix reports "uncertified" where both
previously reported "singular", and an indefinite one reports
"indefinite" deliberately or "uncertified" at the numerical
margin, per the refusal-vocabulary split in the next entry. Code
that branches on any of these reason strings needs updating.
axes_reliability()'s refusal reasons now say which degeneracy happened.
Within the degeneracy criterion's refusal region, a fitted correlation
structure whose smallest eigenvalue is decisively negative — beyond the
fit's own numerical noise band — reports "indefinite", a statement about
the model; roundoff-level negativity, exact singularity, and
ill-conditioning severe enough to fail the per-fit accuracy check report
"uncertified", a numerical caution. A
saturated model (zero degrees of freedom) is refused as "saturated"
before any scaling arithmetic runs, where it previously surfaced as
"indefinite" through an internal division by zero; and the final
nonpositive-scaling-factor backstop likewise reports "ill_conditioned"
rather than "indefinite", an indefiniteness it cannot diagnose. When the
standard-error surface's two internal arms would label one matrix
differently, the reported literal is the correlation-metric arm's — the
same arm the fit-scaling surface prices — so the two surfaces never name
the same matrix differently. Two of these changes are visible today only
at the internal helpers' documented contract boundary, not through any
axes_reliability() call: "saturated" needs a three-item map, which
axes_reliability() refuses, and the backstop's relabel has not been
observed to fire (a 30,000-draw search found no reaching input — recorded
as not-reached, not as unreachable); they are documented for code that
branches on the details reason fields.
axes_reliability() objects now report details$n_moments, the number of
distinct analyzed moments p* = p(p+1)/2, and details$baseline, the
independence model's unscaled chi-square and degrees of freedom.
details$n is documented now as the sample size the fit was priced at, as
distinct from n_total and n_complete. Together n_moments and n let
you locate a fit on the calibration table in vignette("axes-reliability");
baseline, with fit$chisq, fit$df and the baseline element of
details$scaling_factor, lets you reproduce the reported cfi.
The displacement-interpretability guardrail in print() and summary()
now uses a scale-free rule: a profile's displacement is certified as
interpretable only when the amplitude confidence interval's lower bound sits
at least 0.35 interval-widths above zero. This replaces the rule introduced
in 1.2.0, which certified whenever the lower bound rounded above zero at the
display precision — a threshold that moved with the print digits and meant
different things on different score metrics, and that (as the new
ssm_ci_accuracy() diagnostic makes visible) certified a genuinely zero
amplitude almost every time. The new rule holds false-certification near the
interval's one-sided error rate regardless of scale or display settings. As a
result, some near-zero-amplitude profiles that were previously certified are
now flagged uninterpretable. The threshold is calibrated for the default 95%
confidence interval.
Displacement and angle confidence-interval endpoints that land exactly on
the 0/360 pole are now reported as 360, never 0, matching how the package
labels that pole everywhere else (LM = 360): ssm_analyze() bootstrap
displacement CIs and cpm_fit() bootstrap angle CIs both use the shared
circular-quantile machinery that now applies this labeling. cpm_fit()'s
reported Angle column likewise labels the pole 360 — a reference scale
with a theory angle of 360 previously printed Angle = 0 with a degenerate
CI of [0, 0], and now prints 360 throughout. An exact-pole endpoint is a
measure-zero floating-point corner for real data, so numeric results are
otherwise unchanged.
The CAIS's second normative sample has been withdrawn, and cais now
ships one sample, the child sample. The CAIS is rated on a 5-point scale, but
three of the octant means its source publishes for the second sample's
respondents fall above 5, so that sample is not on the metric of the scores
it would standardize and the z-scores it produced were wrong in an undefined
unit. It was therefore refused rather than used (see the next item), which
left it shipping as data no call could accept. The transcription was faithful
— the discrepancy originates in the source's own table — so the values
survive, with the evidence and what a reply from the authors would reopen, in
the package's source repository under cairn/references/sodano2006.md.
Code that passed sample = 2 to norm_standardize() for the CAIS was
already erroring and now errors saying the sample does not exist. The CAIS
child sample and every other instrument are unaffected. See ?cais.
norm_standardize() now refuses a normative sample whose mean scores fall
outside the instrument's own response range, instead of returning z-scores
computed from it. Such a sample cannot be on the same metric as the scores
being standardized, so the values it produced were wrong in an undefined
unit, with nothing in the output to indicate it. No shipped sample is now
affected — the one that was is the withdrawn CAIS sample above — and the
check stands so that no future sample can enter the roster off-metric.
norm_standardize() now reports which normative sample it used. Every
successful call prints the sample number, its size, its description and its
reference kind — for example, "Standardized against IIP-SC normative sample
1: N = 872, American college students. Reference kind: identified published
source." — and, where the instrument carries more than one sample,
says how many others are available. Which sample you standardize against is
a result-determining choice rather than a technicality: across the shipped
instruments, different samples of the same instrument move a respondent's
z-scores by roughly half a standard deviation on average, and by nearly
twice that at the extreme. Pass the new
quiet = TRUE to suppress the message in loops and knitted documents.
Every normative sample now records what kind of reference distribution it
is, in a new Kind column readable at instrument$Norms[[2]]. Six of the 24
shipped samples — the IIP-32's and IIP-64's — were drawn to represent a
defined population, so their means and standard deviations estimate that
population's; 16 are described in an identified published source and
describe that group of people and no wider frame; and two appear in no
source that has been identified at all. norms() prints the kind for each
sample it lists, and norm_standardize() names it in both its message and
its attribute, so the distinction is available where you choose a sample and
where you use one. See ?norms for what each kind means.
Every data frame returned by norm_standardize() now carries a
"norm_sample" attribute recording the instrument, the sample number, its
size, its description and its reference kind, so a script that never sees the
console can still report what its z-scores are relative to. Retrieve it with
attr(x, "norm_sample"). It is attached whether or not quiet is set, and
on both the append = TRUE and append = FALSE return paths.
The package now requires ggplot2 (>= 4.0.0), and ggforce is no longer a
dependency. Two base-R packages join Imports: grid (the rebuilt coordinate
system builds its axis-label backdrops as grobs) and parallel (the worker
pool behind ssm_ci_accuracy()'s parallel/ncpus arguments). The declared
R requirement moves to R (>= 4.1) to match the floor ggplot2 already imposes;
no installation that worked before is affected.
ssm_score()'s extra arguments passed through ... must now be named
(e.g. prefix = "IIP_") and must be single strings; an unnamed or
non-scalar argument is now an error rather than being silently ignored
(previously it could yield unlabeled or garbled output columns). Rows whose
profile has undefined displacement now produce a single warning reporting how
many such rows there are, rather than one warning per row.
Count-valued arguments (e.g. boots, reps, ncpus, digits, and the
sample size n) across ssm_analyze(), ssm_ci_accuracy(), cpm_fit(),
cpm_simulate(), and ssm_sem() are now uniformly validated as a single
non-negative whole number. A few of these previously accepted a
length-greater-than-one vector without complaint; such input now raises a
clear error.
ssm_plot_circle() now warns and names any profile it cannot place on the
circle because its displacement is undefined (a flat or zero-amplitude
profile), instead of dropping it from the figure without notice.
New cpm_fit() function estimates Browne's (1992) circular stochastic
process model for the correlational structure of circumplex scales or items,
a native replacement for the archived CircE package. It accepts either raw
data or a correlation matrix, estimates item angles and communality indices
(with four model variants), and reports the usual covariance-structure fit
indices (chi-square, RMSEA with a 90% confidence interval, SRMR, CFI, TLI,
AIC, BIC). The returned circumplex_cpm object has print() and summary()
methods. On the raw-data path, confidence intervals are estimated by a
nonparametric bootstrap by default (resampling rows and refitting the model,
with percentile intervals; angle intervals use the package's circular
quantile machinery, so an interval straddling the 0/360 degree boundary is
reported wrapped, as with displacement intervals). Resamples that are
degenerate or fail the convergence criterion are excluded with a warning and
counted in the output. Only the bootstrap consumes R's random number stream:
call set.seed() immediately before cpm_fit() for reproducible intervals
(point estimates are deterministic). On the correlation-matrix path,
intervals are analytic (Wald) — there is no raw data to resample — and
summary() cautions when the sample size is small enough that these may
mis-cover. A scaling argument selects the covariance-scaling family:
"unit" (the default) fits the correlation structure, while "free" fits
Browne's covariance structure with p free variance scales — the
parameterization CIRCUM and CircE use — so cpm_fit() can reproduce their
published output exactly. Free scaling adds p parameters without changing
the degrees of freedom, and reports the fitted variance ratios in a
VarRatio column (without confidence intervals). With correlation input the
two families' model-test statistics are calibration-indistinguishable
(paired simulation at sample sizes 250–50,000), so the default remains the
recommended family for routine inference; use scaling = "free" when the
goal is reproducing published CIRCUM/CircE output. cpm_fit(scaling = "free") also starts its optimizer from the unit-scaling solution, so the
free family's fit statistic can never exceed the default family's on the same
input beyond numerical tolerance (the free family mathematically nests the
default).
The cpm_fit() estimator has been validated against the published
CIRCUM/CircE literature (Grassi, Luccio, & Di Blas, 2010, reanalyzing
Browne's 1992 vocational-interest example) and against independent
OpenMx and lavaan implementations of the same model (both now in Suggests
as test oracles only). CIRCUM and CircE fit Browne's covariance
parameterization with free variance scalings; cpm_fit(scaling = "free")
fits that same family and reproduces their published estimates, chi-square,
and fit indices to printed precision, while the default correlation-structure
fit differs from them slightly in finite samples (same degrees of freedom,
asymptotically equivalent); see the package's design notes for details. A
large seeded simulation study measured the coverage of both interval methods:
based on its results, summary() now also cautions about analytic intervals
at any sample size below 50,000 when the fitted solution is near a parameter
boundary or weakly identified (Heywood case, removed harmonic, very small
correlation-function weight, ill-conditioning, or competing near-tied
optima — the caution names which), the regime where they measurably
mis-covered. Percentile bootstrap intervals were confirmed as the better
default but are themselves conservative-liberal in spots (notably for
near-boundary correlation-function weights); improving them is planned
follow-up work. On the bootstrap path, summary() also lists any fired
markers in a descriptive note at every sample size, stating that what has
been measured about the markers covers analytic intervals only — not every
marker was measured, and none is validated as a predictor of the bootstrap
intervals shown.
New fit_structure() function evaluates whether a set of scales forms a
circumplex using the exploratory criteria of Acton & Revelle (2004). Four
criteria are computed from the first two unrotated principal-axis factors of
the scales' correlations — the Fisher Test of equal axes, the Gap Test of
equal spacing, and the Variance (VT2) and Rotation tests of interstitiality —
and a fifth, the RANDALL correspondence index (Hubert & Arabie, 1987; Tracey,
1997), tests the hypothesized circular order of the scales with a
randomization test that yields an exact p-value. The factor-analytic
statistics are classified against interpretive cutoffs that were re-derived by
simulation under Acton & Revelle's own generating model for eight (octant)
scales — their published cutoffs were calibrated on far more variables and do
not transfer — and that are keyed to the scoring, since these criteria work
best with a general factor removed. fit_structure() deviation-scores
(ipsatizes) by default for that reason, with a raw opt-out. Missing values
are handled by listwise deletion by default (a listwise argument, matching
ssm_analyze()), so all five tests share one complete-case correlation
matrix — the metric the cutoffs were calibrated on. The returned
circumplex_structure object has print(), summary(), and plot()
methods; interpretations are presented as the heuristic likelihood
classifications they are, never as significance tests.
New axes_reliability() function estimates the reliability (and standard
error of measurement) of the two circumplex axes with the item-level
restricted tau-equivalent CFA of Strack, Jacobs, and Grosse Holtforth (2013).
The model decomposes each item's variance into a general factor, the two
circumplex axes, scale specificity, and item specificity, and reads the axes'
reliability off the isolated axes-variance component with the Spearman-Brown
formula — a confirmatory, item-level complement to fit_structure()'s
exploratory scale-level criteria. The Nunnally-Bernstein axis reliability is
reported alongside for comparison (it overestimates when scale specificity is
large). Items are supplied through a circumplex_instrument or an explicit
angle-and-item map. Any equally spaced set of scale angles is accepted, at
any rotation and any count from four scales up — the canonical octants, an
interstitial set rotated off the axes, or a six- or twelve-scale circumplex.
Unequally spaced (quasi-circumplex) angles are refused rather than
approximated, and three scales are refused because the variance components
are not separately identified at that count. Scales may carry a single item
each, as Strack's single-item circumplex types do: with one item at every
position no two items share a scale, so the scale-specificity component is
not identified and is dropped from the model rather than estimated, leaving
a three-row components table. A mixed instrument carrying at least one
multi-item scale still estimates it. Because coefficient alpha is undefined
for a one-item scale, the Nunnally-Bernstein comparison is reported as NA
with a stated reason whenever any scale has fewer than two items — as Strack
et al. themselves do, leaving it blank for such instruments. Blockwise
instruments — those administering items in blocks cutting across the scales —
are supported through a blocks argument taking a list of item columns, one
element per block, which adds Strack's block-specificity component to the
model and a zeta2 row to the components table. Blocks that carry no
information the model lacks (blocks that coincide with the scales, one block
for everything, or one block per item) leave the component unidentified, and
it is dropped with details$zeta2_fitted recording that, as scale
specificity is on a single-item instrument. Whether ignoring real blocks
matters depends on their geometry: the general factor is inflated under most
layouts and never deflated, while the axes variance — and so the reliability
— moves only when block membership carries information about the angular
distance between items. When each block draws exactly one item from every
scale it carries none, and the reliability is unaffected; other layouts bias
it in either direction, and being evenly spread around the circle is not
sufficient for safety. Estimation works either from raw item data or, through
cormat and n, from a published item correlation matrix alone, for
reanalyzing a matrix whose raw data is not available; on that path the
Nunnally-Bernstein comparison is reported as NA and sd = "raw" is refused,
since both need the respondents' own item scores. Missing data are handled by
listwise deletion by default, and a missing = "fiml" setting estimates from
every respondent who answered at least one item by full-information maximum
likelihood instead. FIML assumes the data are missing at random and
multivariate normal, both stronger assumptions than listwise deletion needs:
under MCAR listwise deletion is already consistent and merely inefficient, so
the gain there is precision rather than correctness, while under MAR listwise
deletion is biased and FIML is not. The items are standardized by the
saturated model's own FIML moments rather than by whichever cells happen to be
observed, and those columns feed a single FIML fit; the reported standard
errors are observed-information standard errors on that standardized metric,
conditional on the standardization constants. Under missing = "fiml" the
Nunnally-Bernstein comparison is NA and sd = "raw" is refused, both
needing items observed by every respondent. Pairwise-deletion correlations are
never used on either setting. Strack et al. report no missing-data analyses,
so the FIML variant is certified against the package's own synthetic oracle
rather than against their results. A boundary
fit returns NA reliability rather than a clipped value; and the returned
circumplex_axes_reliability object has print() and summary() methods. A
bundled simulated dataset, simulated_items, is included for the examples.
New cpm_simulate() function draws standardized observations from a fitted
cpm_fit() model's implied correlation matrix, using the model's exact
positive-semidefinite factor representation. It returns a numeric matrix with
one column per scale (in fitted order, named), whose population correlation
matrix is the fitted Phat. Call set.seed() immediately before it for
reproducible draws.
New SEM-based (latent-variable) SSM analysis: ssm_sem() estimates the
Structural Summary Method profile of one or more external measures against
the latent circumplex content of the scales — the disattenuated analog of
the correlation-based ssm_analyze() — from a fixed-theoretical-angle
measurement model fitted with lavaan (now a runtime Suggests dependency for
this feature family; everything else works without it). Confidence
intervals for all parameters are built in-package by propagating draws of
the model's free parameters (multivariate-normal by default, or a full
lavaan bootstrap via ci_method = "boot") through the profile and SSM
transforms and the same circular-quantile machinery as ssm_analyze() —
never lavaan's delta-method or percentile intervals, which ignore the
angular branch cut. The default draws propagate lavaan's robust
(sandwich) covariance, which the package's coverage validation found
necessary to keep the intervals calibrated when the fixed-angle model
only approximates the data; the default estimator is MLR, so the global
fit indices print() reports are likewise the robust/scaled versions
(circumplex scale scores are typically skewed). Includes ssm_sem_syntax()
(an inspectable lavaan model-syntax generator that works without lavaan
installed) and ssm_sem_parameters() (estimate from a lavaan fit you have
modified or fitted yourself). Results are circumplex_ssm objects, so
ssm_table() and the ssm_plot_* functions work on them unchanged. With a
grouping variable, ssm_sem() fits the measurement model across groups and
gates a latent group contrast on measurement invariance: it tests a
configural-metric-scalar ladder using lavaan's own nested-model test (the
scaled difference test under robust estimators) at the invariance rung
(defaulting to each path's required level) and the invariance_alpha level,
and computes the disattenuated contrast only when the required rung is
retained. When invariance is rejected it reports an honest non-comparison —
the verdict plus each group's separate configural profile — rather than a
contrast that would confound structural difference with measurement
non-invariance. Supplying grouping without measures analyzes the latent
mean path (each group's model-implied latent mean profile). The
observed-score group contrast in ssm_analyze() remains the right tool when
invariance cannot be assumed; it answers its own, different question.
The invariance ladder that print() reports now also carries dcfi, the
change in CFI from the previous fitted rung, as a labeled secondary
criterion: Cheung and Rensvold's (2002) general rule rejects an invariance
step when CFI falls by more than .01. (That direction is taken from the
article's own Table 5, whose critical values are the 1% lower tails of the
simulated null distributions; the sentence stating the rule on its p. 251
reads backwards relative to that table, and the package follows the
simulation.) It is reported and never gates — the
nested test alone decides comparability, the verdict, and which fit the
estimates come from — and the two criteria can legitimately disagree, since a
change in CFI is insensitive to sample size where the nested test is not. The
retain/reject label prints only inside the envelope their simulation covers,
which is narrow: they simulated two groups, ML estimation, and
multivariate normal data, and examined Type I error only — not power — and
robust CFI variants were not part of their study. So the label appears only
for a two-group fit estimated by ML whose CFI is the plain, non-robust one.
estimator = "ML" is necessary but not sufficient: missing = "fiml" also
makes lavaan report a robust CFI, as do the "MLR" default and "MLM".
Under a robust CFI from any of those routes, a non-ML estimator such as
"GLS", or more than two groups, the dcfi value still prints, marked as
not validated for that configuration, with a note naming which condition
applies and carrying no verdict — extending the cutoff there would require
simulation that has not been done.
New repeated-measures (longitudinal) SSM analyses: ssm_analyze() gains an
occasions argument taking a named list of column blocks, one per
occasion, each selecting the same circumplex scales measured at that
occasion (wide data, one row per person). Every occasion yields its own
profile row, occasions cross with grouping, and contrast = TRUE with
exactly two occasions (single group) estimates the paired within-person
contrast — second listed occasion minus first — through both engines: the
bootstrap resamples persons (preserving within-person dependence
nonparametrically) and the Monte Carlo engine draws the stacked occasion
mean vectors jointly. Cross-occasion column alignment is validated by stem
matching (a reordered occasion block errors instead of silently rotating
displacement). Occasions analyses are listwise-only across waves, with the
dropped-person count messaged and a selection caution documented. Results
from occasions analyses carry a new Occasion column that is present only
for such analyses — downstream code should test for the column by name.
Coverage of the paired contrasts was validated by simulation at nominal
rate across boundary cells (displacement changes near 0 and 180 degrees,
CIs straddling the 0/360 pole, small samples, three occasions); note that
paired contrasts are not unconditionally more efficient than
independent-groups designs (see the new Occasions section in
?ssm_analyze).
New ssm_analyze_long() provides a long-format (one row per person per
occasion) interface to the repeated-measures occasions analysis. It reshapes
the data to the wide layout ssm_analyze() expects and delegates to it, so
the estimation, paired within-person contrasts, and listwise missing-wave
handling are unchanged. Occasion order is taken from the factor levels (or
first appearance) of the occasion column and is never sorted
alphabetically, so a T10/T2 pair keeps its temporal order.
New per-person (intraindividual) SSM scoring: ssm_parameters_id() scores
each person's own circumplex profile through the closed-form SSM transform
and returns a per-person parameter table — one row per person, with an
id argument that first averages a person's rows (e.g., occasions of
intensive longitudinal data) within person before scoring. Degenerate
profiles keep their row with NA parameters (never a silent drop), and an
na_rate column exposes each person's share of missing scale cells. A
summary() method aggregates the table at the group level using circular
statistics for displacement (circular mean and mean resultant length,
never arithmetic means of angles), reporting how many undefined
displacements were excluded. Two documented caveats: the circular mean of
per-person displacements (equal weight per person) is a different quantity
from the displacement of the group mean profile (amplitude-weighted), and
by the triangle inequality the group profile's amplitude is at most the
mean per-person amplitude, strictly smaller when directions disperse.
New Bayesian draws adapter: ssm_draws() converts posterior draws from a
user-fitted Bayesian model (e.g., a brms cosine regression) into SSM
parameter draws and summarizes them with the package's circular-statistics
machinery — circular quantiles for displacement (credible intervals that
straddle 0/360 wrap instead of inverting), posterior medians for the
linear parameters (the amplitude posterior is right-skewed), and the
circular mean for displacement, with the marginal-coherence caveat
documented. Two draw shapes are accepted and never guessed: (e, x, y)
parameter draws (type = "parameters", required because a 3-column
matrix is ambiguous) and profile draws (one column per scale, with
angles). Draws with undefined displacement are excluded from the
displacement summaries only, with an honest warning that says "posterior
draws" and "credible interval". ssm_draws() objects also apply the
package's displacement-certification rule to the amplitude credible
interval: when the interval's lower bound sits under 0.35 interval-widths
above zero, printing notes that the displacement is not interpretable, and
the verdict is stored in $details$certified.
New growth-model support for repeated-measures SSM analysis, documented in a
new vignette ("Growth Models on SSM Parameters"): fit a joint mixed model
to the per-person Cartesian coordinates from ssm_parameters_id() (the
reference recipe uses glmmTMB, now in Suggests; fitting the coordinates with
separate univariate models silently zeroes their cross-covariance and
produces wrong displacement intervals), then convert fixed-effect draws to
amplitude/displacement trajectories with ssm_draws(). The recipe was
validated by simulation: pointwise displacement coverage is nominal in a
pole-crossing design, and the univariate shortcut demonstrably fails coverage
under correlated person effects.
New angle_unwrap() helper unwraps a temporally ordered sequence of
angles onto a continuous branch (350, 10, 30 becomes 350, 370, 390),
supporting the vignette's alternative unwrap-then-model recipe. Inputs
are wrapped to [0, 360) first; an exact 180-degree step ascends (the
package's half-turn convention); NA makes later waves branch-ambiguous
and so propagates onward.
New ssm_ci_accuracy() function assesses, by simulation, whether the
confidence intervals of an ssm_analyze() result would cover the true SSM
parameters at their nominal rate if the population looked like the fitted
estimates, at the observed sample size(s) — the CI-trustworthiness
diagnostic of Zimmermann & Wright (2017), generalized to the user's own
configuration (grouping, contrasts, measures, engine, resample count, and
interval level). The population's scale structure is characterized by a
cpm_fit() model (or, optionally, the observed correlations); each
simulated dataset replays the object's own interval procedure. Coverage is
reported per profile row, parameter, and amplitude condition — a ladder of
populations with the amplitude scaled toward zero, where percentile
amplitude intervals are theoretically weakest — along with one-sided miss
rates, interval widths, the certification rate of the printed
displacement-interpretability guardrail, and displacement coverage
conditional on certification. For a contrast row — a signed difference that
print.circumplex_ssm() never certification-gates — the displacement
verdict and printed coverage are reported unconditionally, matching that
profiles-only stance (its conditional coverage is retained in the object as
a descriptive). Coverage at the as-estimated condition is
classified against Bradley's (1978) liberal robustness band using 95%
Wilson score intervals, and print()/summary() translate the
classifications into a plain-language verdict, including a line reporting
how often the guardrail would certify displacement if the true amplitude
were zero (the scale-free rule holds this near the interval's one-sided
error rate, and a caution is raised only if it materially exceeds that).
summary() also annotates the realism of the simulated population
(structural-model convergence and fit, against conventional RMSEA/SRMR
benchmarks with citations) and, when an amplitude estimate is itself below
half its CI width, notes that the analysis already sits in the near-zero
regime and adds a ladder rung at the certification margin. A plot() method
draws coverage across the amplitude ladder with the Bradley band shaded.
Simulation replicates can be parallelized (parallel/ncpus) with
seed-identical results, and the caller's random-number state is restored
on exit. To support the diagnostic, ssm_analyze() now stores per-group
sufficient statistics (sizes, scale SDs, and correlation matrices) in its
output; objects created by earlier versions can be assessed by re-supplying
the original data via ssm_ci_accuracy(..., data = ), which is checked
for consistency against the stored profiles.
ssm_ci_accuracy() also assesses repeated-measures occasions analyses. Its
plug-in population is a multivariate normal with the observed stacked
cross-occasion covariance, so the within-person dependence across occasions
is carried into the simulation (rather than ignored); it reports CI
trustworthiness per occasion and for the paired within-person contrast. A
flat occasion is refused by name, a rank-deficient stacked covariance is
flagged (the fit-statistic pass rate becomes descriptive), and because the
occasions population is the observed covariance the structure/cpm
arguments are not accepted on that path.
ssm_analyze() gains a method argument offering a Monte Carlo alternative
to the bootstrap (method = "montecarlo"): SSM parameter replicates are
drawn from the asymptotic sampling distribution of the group mean vector or
measure-scale correlation vector (a multivariate normal with empirically
estimated covariance; correlations are drawn jointly across measures on the
Fisher z scale) and propagated through the SSM transformation. It produces
intervals closely matching the bootstrap on large samples while running in a
fraction of the time, but relies on asymptotic normality and requires
listwise-complete data, so the bootstrap remains the default and the
recommended choice for small samples. summary() reports which method
produced the intervals.
ssm_analyze() gains parallel and ncpus arguments (passed to
boot::boot()) to distribute the bootstrap computation across multiple CPU
cores. Because the resample indices are drawn in the main R process before
any work is distributed, results for a given set.seed() are identical
regardless of these settings, so parallelizing never changes your estimates
or confidence intervals.
The Monte Carlo interval engine (ssm_analyze(method = "montecarlo")) is
faster on correlation-based analyses: the influence-function covariance is
built in one vectorized pass and all profile rows are propagated through
the SSM transformation in a single compiled call. Results are unchanged
(byte-identical for a fixed seed).
ssm_score() is now vectorized internally (one compiled call instead of a
row-wise loop), making it much faster on large data sets. Results are
unchanged.
Circumplex figures are now built on a real ggplot2 coordinate system. The new
coord_circumplex() owns the amplitude-to-radius scaling and the
displacement-to-angle transform in one place, so a canvas and its data layers
can no longer disagree about the outer-ring amplitude. It adds a configurable
amplitude center (the rings relabel and the amplitudes remap together) and a
theme-responsive canvas: the rings, spokes, and labels drawn by
ggcircumplex() now restyle through + theme_*(). It always draws an
amplitude ring at amax, so every circumplex canvas closes at its rim and no
point is drawn past the last visible ring; that rim ring is unlabeled unless
amax is itself one of the axis breaks. A non-finite amax or center is
rejected with a message naming the argument. ggcircumplex(),
geom_ssm_point(), geom_ssm_arc(), and ssm_plot_circle() keep their
signatures and correct output. The per-layer amax argument (and
geom_ssm_arc()'s n) are no longer needed and are ignored with a one-time
note.
New ggcircumplex() function builds an empty circumplex plotting canvas
(amplitude rings, displacement spokes, and scale labels) as a ggplot2
object that you can add layers to with +. It accepts a set of scale
angles and labels, or a circumplex_instrument object to derive both
automatically. The package's own ssm_plot_circle() draws on the same canvas.
ggcircumplex() and scale_x_circumplex() label and place circumplex scales
at their exact angles, including non-integer angles (for example, the
22.5-degree spacing of a 16-scale instrument), instead of rounding them to
whole degrees.
New geom_ssm_point() and geom_ssm_arc() layers draw SSM profile points
and their confidence-region arcs directly in circumplex space on a
ggcircumplex() canvas, taking amplitude and displacement as aesthetics and
handling the polar transform (including wrap-around at the 0/360 degree
boundary) internally. These make it possible to build custom circumplex
figures by composing ggplot2 layers.
New scale_x_circumplex() provides an angle-labeled x-axis scale for linear
circumplex plots (such as the score-by-angle curve). It labels axis breaks
with their angle in degrees by default, or with custom labels or a
circumplex_instrument's scale abbreviations, using the same conventions as
ggcircumplex().
The circumplex ggplot2 layers are extensible and ergonomic. The
GeomSsmPoint, GeomSsmArc, and CoordCircumplex ggproto generators are
exported so downstream packages can subclass them. The amplitude (radial) axis
and its labels are drawn in the widest gap between the displacement spokes,
so they no longer overlap a spoke label; coord_circumplex() gains an
r_axis_angle argument to place it manually. The canvas theme is exported as
theme_circumplex(). geom_ssm_point() and geom_ssm_arc() follow the
ggplot2 na.rm convention: with na.rm = FALSE they warn (with the count)
before dropping profiles that cannot be placed, while the default
na.rm = TRUE drops them silently. ssm_plot_circle(repel = TRUE) now gives
a clear error when the suggested ggrepel package is not installed.
The new geom_ssm_path() layer draws a profile's movement across occasions as
a path on the circumplex canvas, so change in amplitude and displacement reads
as motion in circumplex space rather than only as separate parameter panels.
Each segment is curved along the circle by coord_circumplex(). Consecutive
occasions are joined the short way around the 0/360 boundary, so a step from
350 to 10 degrees is drawn as a 20 degree arc across the pole rather than a
340 degree sweep the long way round. Occasions are connected in data order,
with group separating one series from another and an optional order
aesthetic to sort within a series; an optional arrow marks the direction of
time. An occasion with no defined displacement (a flat or zero-amplitude
profile) breaks the path rather than being interpolated through, and the
segment after the gap is still drawn on the correct branch.
ssm_plot_circle() gains a path argument that adds this movement path to
its usual points and confidence wedges, for results from
ssm_analyze(occasions = ) and ssm_analyze_long(). Occasions are connected
in the order they were supplied, never alphabetically.
The new ssm_plot_trajectory() plots how each SSM parameter changes across
occasions, one panel per parameter with its confidence interval as a band, for
results from ssm_analyze(occasions = ) and ssm_analyze_long(). The
displacement panel is drawn on an unwrapped branch, so a profile whose
displacement crosses the 0/360 boundary is shown as one continuous path
instead of jumping a full turn, and each confidence bound is placed on its own
estimate's branch. Occasions appear in the order they were supplied, never
alphabetically. An occasion whose amplitude is too close to zero for its
displacement to be interpretable is marked with a hollow point, and a profile
with no defined displacement leaves a gap rather than a spurious segment.
ssm_plot_trajectory() also accepts a trajectory table: a data frame with
one row per time point, a numeric time column named by the new time
argument, and a_est/a_lci/a_uci and d_est/d_lci/d_uci columns
(optionally the e_*, x_*, and y_* triples and a logical certified
column). This is the shape a model-based workflow assembles by evaluating a
fitted growth model at each time point and passing the draws through
ssm_draws(), and it is plotted on a continuous time axis, so unequally
spaced time points are drawn at their actual spacing. Only the panels the
table can fill are drawn. The displacement unwrap, the interval placement, and
the hollow marking of uninterpretable time points are shared with the
occasions path; when no certified column is supplied, the figure makes no
interpretability claim rather than asserting one.
New plot() method for circumplex_cpm objects draws the estimated item
configuration on the ggcircumplex() canvas: each scale appears at its
estimated angle and at a radius given by its communality, with a wedge
spanning its angle and communality confidence intervals where these are
estimable (scales with an inestimable interval are drawn as a point only and
named).
The amplitude axis labels are now drawn over a translucent backdrop, so they stay readable where a data layer falls behind them. The amplitude axis is drawn on top of the plotted data, which kept the labels visible but not legible: a label crossing a dark marker, an arrowhead, or a dense scatter had too little contrast against it to read. The backdrop is deliberately translucent rather than opaque, so it restores contrast without hiding the data it covers.
csie, csig, csip, csiv and iitc has
been re-verified against its published sources, value by value. Every mean,
standard deviation and sample size was confirmed correct, as was every
item-to-scale assignment its source publishes; no norm value changed.csiv now
reports its norms as unpublished data from the instrument's author rather
than attributing them to Locke (2000), whose article reports a different
sample and publishes no octant statistics. The URL recorded for csie and
csiv now points at the author's current norms tables; the previous
addresses had been redirected to a site homepage. Use norms() to see the
provenance recorded for any instrument.?norms now states that the population shown for a normative sample is a
short standardized label chosen by this package, deliberately broader than
the description the original source gives.norm_standardize() now works with iei. The sample column of the IEI's
normative data had been built so that its two samples were interleaved rather
than stacked, which left each sample holding four octants twice and four not
at all; standardizing against either IEI sample failed with an error about
duplicate angles. No normative value was wrong, and no other instrument was
affected.iei norms misspelled the second author's
name and now reads Horner, Locke, & Hulsey (2024).iis32, iis64, ipipipc and isc has now
been re-verified the same way. For iis64 and isc, every mean, standard
deviation and sample size was confirmed correct against the published source,
as was every item-to-scale assignment the source publishes.iis32 and ipipipc, it could not be. Neither instrument's cited article
publishes the octant means and standard deviations the package ships:
Hatcher and Rogers (2012) reports no descriptive statistics at all, and
Markey and Markey (2009) reports them only for a sample other than the one
the package names. No other source for them has been identified. The values
ship unchanged, since nothing establishes they are wrong either, but the
Reference recorded for each now says the norms source is unconfirmed
instead of crediting an article that does not carry them, and ?iis32 and
?ipipipc say the same. Standardized scores from these two instruments should
be treated as resting on unverified norms.iis64, item 5 had been truncated to "I realize " and now reads "I realize
that I don't have to be friends with everyone", and item 7 read "not
agreeable with others" where the source prints "not agreeable to others". In
iis32, item 28 read "I'm ok with not being included in all activities"
where its own source prints "okay" (the wording differs between the two IIS
articles). In ipipipc, item 16 read "Don't fall for sob-stories" where the
source prints "sob stories".iitc and iei were recorded as "in press" and now
give the published citations.cais scores change. The CAIS item-to-scale key was wrong and is
corrected. The instrument's 37 items are not distributed evenly across the
eight octants — its source assigns five items each to PA, BC, DE, HI, LM and
NO, four to FG and three to JK — but the key shipped four items per octant,
which put one Warm-Agreeable item into Unassuming-Ingenuous, one
Gregarious-Extraverted item into Warm-Agreeable and one Assured-Dominant item
into Gregarious-Extraverted, and left the last five items scored into no scale
at all. score() and anything downstream of it therefore returned different
values than the instrument's authors defined, and norm_standardize()
compared those values against norms computed the correct way. Seven of the
eight octants change under the correction: PA gains the item that had been
scored as Gregarious-Extraverted, BC, DE and HI each gain one of the
previously unscored items, JK loses an item, and LM and NO each lose one item
and gain two. Only FG (items 4, 12, 20 and 28) is unchanged. Re-run any CAIS analysis. The item text and its ordering were
correct throughout.cais, iei, igicr and iipsc has now
been re-verified against its published sources the same way as the other nine.
Every mean and standard deviation of the nine normative samples those four
instruments carried at the time was confirmed correct, as was every scale
angle the sources publish. (One of the nine, the CAIS's second sample, has
since been withdrawn — see above.) Four shipped values did
not match their source and are corrected: the cais item-to-scale key above,
and the three provenance records below.cais sample
size for the child sample now reports 204, the sample size printed on the
table its means and standard deviations come from, rather than the 213 given
for the child sample elsewhere in the same article. The iipsc
college-student norms were credited to a 2011 publication and now name
Hopwood, Pincus, DeMoor, & Koonce (2008), the article that publishes them,
which is also the DOI the instrument already recorded; ?iipsc now cites both
of its normative sources rather than only one. The iei URL pointed at the
study's data repository, which publishes neither of its normative tables, and
now gives one address per sample: the author's IEI norms page for the
undergraduate sample and the article for the community sample.cais failure above — cannot ship again.iip32 and iip64 has now been re-verified
against the IIP professional manual, which completes the sweep: every one of
the fifteen bundled instruments has now been checked against a published
source, though for iis32 and ipipipc that check is what established that
no source publishes their values. All 96 means and standard deviations of the
six IIP normative samples were confirmed correct, as were all 96 item-to-scale
assignments and the three IIP-64 sample sizes. No value changed. The manual
prints no sample sizes for the IIP-32 specifically, so its 800/400/400 are
carried over from the same standardization sample the manual describes for the
longer form, which is what the shorter form was scored from.?iip32 and ?iip64 now cite the third edition of the manual (Horowitz,
Alden, Wiggins, & Pincus, 2003, Mind Garden), the edition the shipped values
were verified against, rather than the earlier edition from a different
publisher; both help pages also carry the credit line the publisher's
reproduction permission requires for the normative statistics.Population recorded for both IIP instruments' normative samples now
describes them as a national standardization sample rather than as community
adults, which is what the manual reports: 800 adults sampled to be
representative of the U.S. adult population, with separate norms for women
and men.The "Evaluating Circumplex Structure" vignette gains a section, When a fit
sits at a boundary, on reading a cpm_fit() solution that sits at or near
a parameter boundary — the regime the vignette's own worked example turns
out to occupy. It glosses each of the five boundary and
weak-identification markers cpm_fit() records, reads the displayed fit's
Heywood case and its zero-width communality interval, shows a fit whose
summary() prints the fired-marker list, separates what the package's
validation simulations measured from what they did not, and gives four
concrete next steps. ?summary.circumplex_cpm points at it. The section on
reading the estimated angles is also corrected: it now says that one scale
is held fixed to identify the configuration, and describes the spacing the
printed table actually shows rather than calling the departures minor.
The "Using Circumplex Instruments" vignette and ?norms now say precisely
what the bundled normative statistics are, instead of implying that a
normative sample stands in for a population. The standardizing section
characterizes the shipped samples from the instrument objects themselves —
the counts are computed in the vignette rather than written down — noting
that many are single-study samples of college students, that the IIP-32 and
IIP-64 national standardization samples are the exception at one end, and
that two of the tables are published in no identified source at the other.
It drops a claim
that some instruments offer samples matched on nationality (none do; the
matched sets are by gender and by age), and it resolves the choice between
samples on which group your participants resemble rather than on which
sample is larger. ?norms now adds that the Population label names the
group a sample was drawn from rather than a population it was drawn to
represent, and points at the vignette.
New vignette, "Evaluating Circumplex Structure": how to test whether an
instrument fits a circumplex in your sample with cpm_fit() (reading and
benchmarking the fit indices, comparing the constrained model variants,
and the boundary-solution/chi-square cautions from the package's
validation simulations), and how to check whether SSM confidence
intervals can be trusted at your sample size and profile with
ssm_ci_accuracy(). Summarizes Zimmermann & Wright's (2017) simulation
findings as cited context (transcribed from the published article),
reproduces their Study 5 analyses on the bundled jz2017 data, and adds
guidance on when to trust each SSM parameter and on what ipsatizing
octant scores costs an SSM analysis. The diagnostic itself was validated
against the article: configured to transcribed Zimmermann & Wright
simulation conditions, it reproduces their published accuracy
classifications (validation scripts and results are recorded in the
package's development repository).
New vignette, "SEM-Based SSM Analysis," teaching the latent SSM: the disattenuated estimand and how it differs from the observed profile, why amplitude and displacement intervals are built in-package rather than by lavaan, the two group-difference estimands (observed vs. invariance-gated latent) side by side, and the model-conditional assumptions that make the latent parameters interpretable.
New precomputed vignette, "Bayesian SSM Analysis," derives the
cosine-regression mapping (pinning the atan2 argument order with an
executable known-direction check), walks a brms random-intercept example
whose posterior draws ship with the package, and exhibits the Rayleigh-shaped
prior that independent (x, y) priors induce on amplitude (brms is a new
optional Suggests dependency used only by that vignette's frozen
model-fitting chunk).
New vignette, "Advanced Circumplex Visualization," teaches the plotting API:
coord_circumplex() as the owner of the amplitude-to-radius mapping, the
configurable circle center and amplitude-axis placement, restyling the canvas
through theme_circumplex() and ordinary theme() calls, subclassing the
exported GeomSsmPoint/GeomSsmArc objects to build reusable layers, and
plotting a trajectory across occasions.
The reference index now groups the plotting API into "Complete Plots" and
"Building Blocks". The ssm_plot_* functions cross-link to each other, so
ssm_plot_trajectory() is reachable from its siblings' help pages, and the
composable layers (ggcircumplex(), coord_circumplex(), the geom_ssm_*()
layers, scale_x_circumplex(), and theme_circumplex()) likewise cross-link
to each other.
Clarified in the documentation of ssm_parameters(), ssm_score(), and
ssm_analyze() that the reported model fit is a bounded R-squared in
[0, 1] for equally spaced angles (more generally, for any angle set
satisfying first- and second-harmonic balance); for angle sets violating
that balance the closed-form estimator is not a least-squares fit and the
reported fit can fall below 0.
axes_reliability(missing = "fiml") no longer refuses, on Windows only,
data it estimates on other platforms. The saturated-stage EM that estimates
the standardizing moments now always runs unaccelerated: lavaan 0.7 defaults
that stage to SQUAREM acceleration, whose convergence on items with very few
observed responses proved platform-sensitive — an item observed 20 times out
of 300 estimated cleanly on macOS and Linux but stalled at any iteration cap
on Windows, so the same data raised "The saturated (EM) stage did not
converge" on one platform and not the others. The package's iteration cap
was calibrated under the unaccelerated EM, so this restores the measured
regime rather than adding a new one. Estimates on data with such
thinly-observed items may shift within the EM's own convergence tolerance
(differences on the order of 1e-3 in the estimated moments); healthy data is
unaffected in both value and speed.
norm_standardize()'s refusal of an off-metric normative sample now names
the offending scales for every instrument. On the seven instruments whose
normative data labels its scale column Abbrev rather than Scale, the
message previously named no scale at all.
Asking norm_standardize() for a normative sample an instrument does not
carry now produces an error naming that argument and listing the sample
numbers the instrument does carry. Previously the call fell through to an
unrelated check and failed with a message about scales not matching the
normative data, which named neither the argument at fault nor a valid value.
Fixed a bug where a bootstrap resample under pairwise deletion
(listwise = FALSE) could crash ssm_analyze() with mean(): object has no elements when the resample happened to draw only missing values for one
scale. Such a scale now yields an NA mean (matching the correlation path),
and the affected resample is excluded from the confidence intervals as a
degenerate profile, consistent with the existing degeneracy handling.
Fixed a bug where the displacement of a group contrast between two exactly
opposed profiles (a half-turn apart) was reported as -180 degrees instead
of +180, inconsistent with the documented (-180, 180] convention for
contrasts. Such a contrast is now reported as +180.
instruments() now derives its listing from the bundled instrument data
rather than a hardcoded table, so it always reflects the instruments
actually shipped. As part of this, the listed name for the IIP-SC now
reads "Inventory of Interpersonal Problems Short Circumplex" (matching its
stored metadata).ssm_plot_circle(), ssm_plot_curve(),
ssm_plot_contrast()) now warn when given an unrecognized argument (e.g., a
misspelled parameter name) instead of silently ignoring it.ssm_analyze(),
ssm_score(), ipsatize(), score(), norm_standardize(), and
self_standardize() previously errored when given a matrix despite
advertising matrix support; they now coerce it to a data frame internally.ssm_score() now accepts numeric column indexes for scales (e.g.,
scales = 1:8), consistent with its documentation and with ssm_analyze();
it previously required character names.print() or summary()) now adds a note under
any profile whose model fit is inadequate (R-squared < .70; interpret only
elevation) or whose amplitude confidence interval includes zero (the
displacement is not interpretable). The notes apply to profiles only, not to
contrast rows.norm_standardize() now matches each scale to its normative data by angular
position rather than exact numeric equality, so 0 and 360 degrees are treated
as the same angle (previously passing 0 for a scale stored at 360 failed with
a cryptic error). An angle with no matching normative row, or with more than
one, now produces an informative error naming the available angles.NA displacement
and fit with a warning (previously an arbitrary angle and -Inf); a profile
with real variance but zero amplitude returns NA displacement and a fit
of 0. Bootstrap resamples that produce degenerate profiles (e.g., a
resampled measure with zero variance) no longer crash ssm_analyze(); they
are excluded from the confidence intervals with a warning reporting the
count. Genuinely small amplitudes are unaffected — the degeneracy test
operates at machine-noise scale only.NA) value in the grouping variable of
ssm_analyze() crashed with a cryptic error under pairwise deletion
(listwise = FALSE). Such observations are now dropped before analysis with
a message reporting how many were removed, in both deletion modes; if no
observations remain, a clear error is given.is_null_or_char() dropped its n argument). ssm_analyze() now
errors if measures_labels does not match the number of measures (or is
given without measures), ssm_plot_circle()/ssm_plot_curve() now error
if angle_labels does not match the number of angles (previously mismatched
labels could be silently recycled onto the wrong scales), and
ssm_table()/html_render() now require caption to be a single string.ssm_score() silently ignored its angles argument and
always used octants(): custom angle sets of the same length produced
incorrect results without warning, and angle sets of a different length
(e.g., poles() with four scales) errored. Results from ssm_score() with
the default angles = octants() are unaffected. (found in 2026-07 audit)Improve handling of radian distributions crossing the 0/2pi boundary
Add unit tests regarding the above cases
Optimize pairwise correlation C++ code
Fix bug with angular median calculation retaining rejected candidates
Update RcppArmadillo dependency
Fix some deprecated ggplot args
self_standardize() function for standardizing variables using sample means and SDsFix some typos in documentation
Change plot tests to accommodate changes to ggplot2
Nearly all code rewritten/refactored to streamline and reduce dependencies.
Removed support for non-standard evaluation
The contrast argument to ssm_analyze() is now TRUE or FALSE instead of "none", "model", or "test". Model contrasts were removed and TRUE yields test contrasts.
Many arguments renamed (e.g., .data to data, .ssm_object to ssm_object, xy to drop_xy)
Removed ssm_plot() function in favor of ssm_plot_circle(), ssm_plot_curve(), and ssm_plot_contrast().
Renamed standardize() function to norm_standardize()
Added ssm_plot_curve()
Added CAIS and IEI instrument data
Added profile scores, results, and plotting to models with contrasts
Added PANO() function for conveniently creating scale names
All internal and external data are now data frames instead of tibbles
Rewrote all vignettes to use the updated functions, arguments, etc.
Harmonized the results and scores fields in the output of ssm_analyze()
Added many unit tests, increasing the package to 100% code coverage
Added many assertions to check for invalid input arguments
Harmonized the tidying function arguments (e.g., prefix, suffix, append)
Added print methods for degree and radian classes
Replace internal non-standard evaluation with .data references
Minor visual improvements to print and summary methods for ssm_objects
Fix a bug when comparing R versions
Update {vdiffr} tests
Update GitHub Actions
Fixed a bug related to NaN values and dplyr::na_if()
Updated package website using new version of {pkgdown}
Fix testing error on Solaris systems
Update package description paragraph
Add cpp11 plugin for Rcpp
Exclude devel folder from linguist statistics
Add angle_labels argument to ssm_plot() to allow users to customize the angle labels around a circular plot
Add palette argument to ssm_plot() to allow users to customize the color palette (from {RColorBrewer}) of a circular plot
Replaced the font_size argument to ssm_plot() with the legend_font_size and scale_font_size arguments to allow users to customize the font size of different elements of a circular plot
Update ggsave() documentation for future compatibility
Update {Rcpp} code for future compatibility
Added a black border to the points in a circular plot to greater distinguish them visually
Change CI notation from [] to () to play nice with pandoc
Update to {testthat} 3E and add ssm_plot() tests using {vdiffr}
Recompile vignettes with new version of {roxygen2}
Replace TravisCI with GitHub Actions
Update dependency versions and require R >= 3.4.0
Fix issues related to how R 4.0.0 handles S3 methods
Modernize ssm_plot() function to use new tidyr syntax
Update travis CI configuration to be more explicit
Adjust the test of quantile.radian() to account for changes to %% starting in R 3.6.1 Patched
Add the name of the package to the S3 class names (e.g., circumplex_radian instead of radian) to minimize the risk of overlapping classes between packages
Add some supplementary files to the R build ignore list to avoid notes during CRAN check
Add APA-style citations to instrument documentation in addition to DOI links.
Add "Instruments" menu to package website for viewing documentation pages.
Adjust the test of quantile.radian() to account for changes to %% starting in R 4.0.0
iitc provides instrument information for the Inventory of Influence
Tactics Circumplex.Fix CRAN warnings by setting LazyData: true.
Fix CRAN note by replacing relative URLs with absolute URLs.
Nonstandard evaluation is now handled using {{}} notation.
Updated the formatting on this NEWS changelog to match tidyverse style.
Avoid a bug with dplyr 0.8.1 and S3 methods on Linux systems.
Update the web address for Johannes in the README document.
New ssm_parameters() calculates SSM parameters (without
confidence intervals) from a vector of scores.
New ssm_score() calculates SSM parameters by row.
Added support for older versions of R (3.3.x).
Updated the "Introduction to SSM" vignette's figures.
Replaced use of dplyr::funs() as this function is being deprecated.
Fixed a bug in the normative data for ipipipc that prevented standardization.
Fixed a bug caused by changes in how random numbers are generated in R 3.6.x.
Fixed several broken links by running package through new version of usethis.
Fixed warnings related to documentation inherited from other packages.
iis32 now has normative data.
Added open-access (i.e., full item text) to the iis32 and iis64.
iis32 item ordering and scoring now match the author's version.
iis32 response anchors now range from 1 to 6 and match norms.
Changed use of tibble functions to avoid problems when new version releases.
Removed dependency on MASS package (until it is used by exported functions).
Added functions and documentation for numerous circumplex instruments.
Added functions for ipsatizing and scoring item-level data.
Added function for standardizing scale-level data using normative data.
Changed OpenMP flags in Makevars to fix a compile problem on Debian machines.
Fixed a bug related to calculating angular medians in the presence of NAs.
Changed the default to plot profiles with low fit (but with dashed borders).
Import and export functions from rlang tidy evaluation.
Added unit testing of various functions to increase code coverage.
Redesigned package website to be more attractive and clear.
Updated the SSM vignette to use the standardize() function.
ssm_plot() now uses dashed borders to indicate that a profile has low prototypicality/fit.Fixed bug that prevented compilation on Solaris systems.
Fixed bug that prevented CRAN checks on old R versions.
Improved the formatting of vignette source code.