Nothing
#' Print and format an efa_screen object
#'
#' `print()` turns the factor-analysis screening diagnostics computed by
#' [efa_screen()] into a sectioned report with banded, colour-coded verdicts:
#' sampling adequacy and sphericity (the Kaiser-Meyer-Olkin measure and Bartlett's
#' test of sphericity), multicollinearity (the determinant and condition number of
#' the correlation matrix), the per-variable diagnostics, and, when raw data were
#' supplied, multivariate normality and multivariate outliers. It closes with a
#' consolidated list of actionable recommendations (for example, which items to
#' consider dropping, whether to prefer an ordinal or a robust estimator, and a
#' caveat that keeps an over-powered Bartlett's test from being over-trusted).
#' `format()` assembles the same report and returns it as a character vector;
#' `print()` is `cat(format(x), sep = "\n")`. The lines follow the active console
#' theme, so they are plain when colours are disabled (for example when captured
#' into a file or stripped with [cli::ansi_strip()]). `print()` does not draw a
#' plot.
#'
#' @param x An object of class `efa_screen` (output from [efa_screen()]).
#' @param digits Integer. The number of decimal places the reported values are
#' rounded to. Default is 3.
#' @param ... Not used; for consistency with the generic.
#'
#' @returns `print()` returns its argument `x` invisibly. `format()` returns a
#' character vector with the report lines.
#'
#' @family factor analysis suitability
#'
#' @export
#'
#' @method print efa_screen
#'
#' @examples
#' # From raw data
#' efa_screen(iris[, 1:4])
#'
#' # From a correlation matrix (supply N for Bartlett's test of sphericity)
#' efa_screen(test_models$baseline$cormat, N = 500)
#'
#' # format() returns the same lines as a character vector:
#' writeLines(format(efa_screen(test_models$baseline$cormat, N = 500)))
#'
print.efa_screen <- function(x, digits = 3, ...) {
cat(format(x, digits = digits, ...), sep = "\n")
invisible(x)
}
#' @rdname print.efa_screen
#' @export
#' @method format efa_screen
format.efa_screen <- function(x, digits = 3, ...) {
raw <- !is.null(x$per_item)
# Overall KMO band (reused Kaiser & Rice, 1974 cut-offs) and the significance flags
# that several sections and the recommendations share.
kmo <- x$kmo$KMO
bart_sig <- .screen_is_sig(x$bartlett$p_value)
# The multivariate-normality verdict is a count over the tests that are available. The
# Henze-Zirkler p-value is withheld when its null approximation degenerates at many
# variables, so the set is not always all three. `mvn_nonnormal` gates the
# recommendations as before; the counts let the verdict sentence separate one marginal
# rejection from a unanimous one.
mvn_p <- if (raw && !inherits(x$normality, "efa_screen_no_mvn")) {
c(x$normality$mardia$skewness_p, x$normality$mardia$kurtosis_p,
x$normality$hz$p_value)
}
mvn_n_avail <- sum(!is.na(mvn_p))
mvn_n_rej <- sum(vapply(mvn_p, .screen_is_sig, logical(1)))
mvn_nonnormal <- mvn_n_rej > 0L
cli::cli_format_method({
# -- Sampling adequacy and sphericity --------------------------------------
.print_efa_rule("Sampling adequacy and sphericity")
if (!is.null(kmo) && !is.na(kmo)) {
band <- .kmo_band(kmo)
kval <- round(kmo, digits)
band$alert("The overall KMO value for your data is {band$label} (Overall KMO = {kval}).",
wrap = TRUE)
# The bands are named on screen for the same reason the condition-index rule is:
# a verbal band is a convention, not a finding, and an unattributed cut-off
# standing next to an attributed one reads as the latter.
cli::cli_text(paste("These data are {band$suitability} suitable for factor",
"analysis (verbal bands: Kaiser & Rice, 1974)."))
} else {
cli::cli_alert_warning("The overall KMO value for your data is not available.",
wrap = TRUE)
}
cli::cli_text("")
bart <- x$bartlett
if (is.null(bart)) {
cli::cli_alert_warning(
"Bartlett's test of sphericity was not computed; no sample size (N) was supplied.",
wrap = TRUE)
} else if (!is.null(bart$p_value) && !is.na(bart$p_value)) {
# Bartlett significance wording reused verbatim from format.efa_bartlett.
if (bart$p_value < .05) {
cli::cli_alert_success(
"The Bartlett's test of sphericity was significant at an alpha level of .05.",
wrap = TRUE)
cli::cli_text("These data are probably suitable for factor analysis.")
} else {
cli::cli_alert_danger(
"The Bartlett's test of sphericity was not significant at an alpha level of .05.",
wrap = TRUE)
cli::cli_text("These data are probably not suitable for factor analysis.")
}
cli::cli_verbatim(paste0("\U03C7\U00B2(", bart$df, ") = ", round(bart$chisq, 2),
", ", cli::style_italic("p"), .screen_p_str(bart$p_value)))
} else {
.print_bartlett_no_result()
}
# -- Multicollinearity -----------------------------------------------------
.print_efa_rule("Multicollinearity")
# The determinant is the product of the p eigenvalues of R, so it shrinks
# geometrically as variables are added even when every eigenvalue stays far from
# zero. A fixed cut-off on it (below 0.00001; Field, 2018) is therefore a statement
# about the number of variables as much as about the data: it fires on wide but
# well-conditioned item pools and contradicts the condition index printed directly
# under it. The determinant is reported as a number only, and the condition index
# (above 30; Belsley, 1991) carries this section's verdict and the consolidated
# recommendation, so the one cut-off lives in one place.
det_R <- x$determinant
dstr <- format(det_R, digits = digits, scientific = FALSE)
cli::cli_alert_info(
"Determinant: {dstr}. It falls as variables are added, so the condition index below carries the verdict.",
wrap = TRUE)
cond <- x$condition
cstr <- formatC(cond, format = "f", digits = digits)
# The verdict is on the condition *index*, and its bands live in .ci_band(). The
# index is shown alongside the rule that governs it: quoting the rule without the
# quantity invites the value to be read against the wrong scale. Above 30 the
# strength is named as well, because a flat label would give an index of 35 and an
# index of 3000 the same verdict.
ci <- sqrt(cond)
cistr <- formatC(ci, format = "f", digits = digits)
ci_band <- .ci_band(ci)
if (ci_band$flag) {
ci_band$alert(
"Condition number: {cstr} (condition index {cistr}). The index is above 30, which indicates a near linear dependency; its relative strength is {ci_band$strength} (Belsley, 1991).",
wrap = TRUE)
} else {
ci_band$alert("Condition number: {cstr} (condition index {cistr}). {ci_band$note}",
wrap = TRUE)
}
# -- Per-variable diagnostics ----------------------------------------------
.print_efa_rule("Per-variable diagnostics")
# The standard display names are MSA (the per-variable measure of sampling adequacy,
# stored as kmo_i) and SMC (the squared multiple correlation). The per-variable
# values are laid out as base R's data-frame print, emitted verbatim so cli does not
# reflow the aligned columns (as format.efa_kmo does for the KMO vector). This is the
# one table not routed through .efa_num(), so it is also the one place a leading zero
# is kept: the frame mixes bounded coefficients with variances and missing-data
# percentages on the data's own scale, for which the leading zero is not redundant.
disp <- .screen_per_item_display(x, digits)
cli::cli_verbatim(utils::capture.output(print(disp)))
# -- Multivariate normality (raw data only) --------------------------------
if (raw) {
.print_efa_rule("Multivariate normality")
nm <- x$normality
if (inherits(nm, "efa_screen_no_mvn")) {
cli::cli_alert_warning(
"Multivariate normality tests were skipped (singular complete-case covariance).",
wrap = TRUE)
} else {
md <- nm$mardia
.screen_mvn_line(.screen_is_sig(md$skewness_p),
paste0("Mardia's skewness: \U03C7\U00B2(", md$skewness_df, ") = ",
round(md$skewness, 2), ", p", .screen_p_str(md$skewness_p), "."))
# An undefined kurtosis statistic is neither a pass nor a rejection, so it gets
# an info alert, as a withheld Henze-Zirkler p-value does below.
if (is.na(md$kurtosis)) {
cli::cli_alert_info(
"Mardia's kurtosis: not available at this number of complete cases.")
} else {
.screen_mvn_line(.screen_is_sig(md$kurtosis_p),
paste0("Mardia's kurtosis: z = ", round(md$kurtosis, 2),
", p", .screen_p_str(md$kurtosis_p), "."))
}
# A withheld Henze-Zirkler p-value is neither a pass nor a rejection, so it gets
# an info alert rather than the success/danger pair the other two lines use.
if (inherits(nm$hz, "efa_screen_no_hz")) {
cli::cli_alert_info(
paste0("Henze-Zirkler: HZ = ", round(nm$hz$statistic, 2),
", p not available at this number of variables."))
} else {
.screen_mvn_line(.screen_is_sig(nm$hz$p_value),
paste0("Henze-Zirkler: HZ = ", round(nm$hz$statistic, 2),
", p", .screen_p_str(nm$hz$p_value), "."))
}
cli::cli_text(if (isTRUE(mvn_nonnormal)) {
paste0("These data depart from multivariate normality: ", mvn_n_rej, " of the ",
mvn_n_avail, " tests ", .screen_plural(mvn_n_rej, "rejects", "reject"),
" it.")
} else {
paste0("These data are consistent with multivariate normality: none of the ",
mvn_n_avail, " tests rejects it.")
})
# The tests use the complete cases, which under non-ignorable missingness are not
# a random subsample: with missing data the verdict above is a statement about a
# subset, so say which one. Silent when every row is complete.
if (.screen_incomplete(nm$n_complete, x$settings$n_obs)) {
cli::cli_text("(Computed from {nm$n_complete} complete cases of the
{x$settings$n_obs} rows supplied.)")
}
}
}
# -- Outliers (raw data only) ----------------------------------------------
if (raw) {
.print_efa_rule("Outliers")
o <- x$outliers
if (inherits(o, "efa_screen_no_outliers")) {
cli::cli_alert_warning("Outlier diagnostics were skipped.")
# The recorded reason, as in the classical-fallback branch below: which of the
# three ways the complete-case covariance failed decides what the user should do,
# and "singular" alone points at collinearity even when the cause is missingness.
if (!is.null(o$reason)) cli::cli_text(o$reason)
} else {
if (identical(o$method, "classical")) {
cli::cli_alert_warning(paste(
"A robust (MCD) covariance could not be computed; classical Mahalanobis",
"distances were used."), wrap = TRUE)
# The recorded reason (too few complete cases, near-collinear variables, or an
# exact fit) and the consequence of the fallback: the classical covariance is
# computed from every observation, outliers included, so the diagnostic no longer
# has the high-breakdown property the MCD gives it.
if (!is.null(o$fallback_reason)) cli::cli_text(o$fallback_reason)
cli::cli_text(paste(
"These distances come from a covariance the outliers themselves inflate, so",
"the diagnostic is no longer high-breakdown and tends to under-flag."))
}
nf <- length(o$flagged)
cstr_o <- format(o$cutoff, digits = digits, scientific = FALSE)
# Name the distance for the estimate actually used (robust MCD, or classical
# Mahalanobis when the robust covariance could not be formed).
dist_label <- if (identical(o$method, "classical")) "Mahalanobis distance" else "robust distance"
cli::cli_alert_info(paste0(
nf, " of ", .screen_case_base(o$n_complete, x$settings$n_obs), " ",
.screen_plural(nf, "was", "were"), " flagged as ",
.screen_plural(nf, "a multivariate outlier", "multivariate outliers"),
" (", dist_label, " > ", cstr_o, ")."), wrap = TRUE)
}
}
# -- Recommendations -------------------------------------------------------
.print_efa_rule("Recommendations")
recs <- .screen_recommendations(x, raw, kmo, bart_sig, mvn_nonnormal,
ci_band$flag, digits)
# cli treats each bullet as a glue/cli template, so double any braces coming from
# user variable names to render them literally (as .efa_emit_bullets does).
msg <- gsub("}", "}}", gsub("{", "{{", recs$message, fixed = TRUE), fixed = TRUE)
cli::cli_bullets(stats::setNames(msg, recs$symbol))
})
}
# Kaiser & Rice (1974) verbal bands for the overall KMO value, and the single place they
# are written down: the highest band the value clears gives its label, the suitability it
# implies, and the severity (success >= .7, warning >= .6, danger below) as an alert
# function, the matching colour, and the equivalent cli_bullets() symbol, so that callers
# reporting the same value as an alert, a coloured label, or a bullet cannot drift apart.
.kmo_band <- function(kmo) {
bands <- list(
list(min = .9, label = "marvellous", colour = cli::col_green, alert = cli::cli_alert_success, symbol = "v", suitability = "probably"),
list(min = .8, label = "meritorious", colour = cli::col_green, alert = cli::cli_alert_success, symbol = "v", suitability = "probably"),
list(min = .7, label = "middling", colour = cli::col_green, alert = cli::cli_alert_success, symbol = "v", suitability = "probably"),
list(min = .6, label = "mediocre", colour = cli::col_yellow, alert = cli::cli_alert_warning, symbol = "!", suitability = "probably"),
list(min = .5, label = "miserable", colour = cli::col_red, alert = cli::cli_alert_danger, symbol = "x", suitability = "hardly"),
list(min = -Inf, label = "unacceptable", colour = cli::col_red, alert = cli::cli_alert_danger, symbol = "x", suitability = "not")
)
Find(function(b) kmo >= b$min, bands)
}
# Belsley's (1991) reading of the condition index, and the single place it is written
# down: the highest band the index clears gives the severity as an alert function, the
# verdict text, and `flag`, which gates both this section's verdict and the consolidated
# recommendation so the two cannot drift apart. The bands are steps on the progression
# 1, 3, 10, 30, 100, 300, 1000 that Belsley reads relative strength from (p. 38); they
# are not absolute cut-offs. An index of 10 or less is rarely of interest (pp. 43 and
# 46). Belsley gives 30 as one example value for flagging a near dependency, and a band
# must exceed it, not only reach it (p. 38). He calls 30 to 100 moderate (p. 42), and in
# his worked example he grades indexes of 35, 153, and 455 as moderately strong, strong,
# and very strong (pp. 44-45).
.ci_band <- function(ci) {
bands <- list(
list(above = 300, alert = cli::cli_alert_danger, flag = TRUE, strength = "very strong"),
list(above = 100, alert = cli::cli_alert_danger, flag = TRUE, strength = "strong"),
list(above = 30, alert = cli::cli_alert_danger, flag = TRUE, strength = "moderate"),
list(above = 10, alert = cli::cli_alert_warning, flag = FALSE,
note = "The index is above 10, but not above 30, the value that flags a near linear dependency (Belsley, 1991)."),
list(above = -Inf, alert = cli::cli_alert_success, flag = FALSE,
note = "An index of 10 or less is rarely of interest (Belsley, 1991).")
)
# An index that is not available takes the lowest band, as the report has always done.
if (is.na(ci)) return(bands[[length(bands)]])
Find(function(b) ci > b$above, bands)
}
# TRUE when the complete cases the normality and outlier diagnostics use are fewer than
# the rows supplied, i.e. when the two possible denominators differ.
.screen_incomplete <- function(n_complete, n_obs) {
!is.null(n_obs) && !is.na(n_obs) && !is.null(n_complete) && n_obs > n_complete
}
# The denominator of the complete-case diagnostics, named as such -- together with the
# number of rows supplied -- whenever missing values make the two differ, so a count or a
# rate cannot be read against the wrong base.
.screen_case_base <- function(n_complete, n_obs) {
if (.screen_incomplete(n_complete, n_obs)) {
paste0(n_complete, " complete cases of the ", n_obs, " rows supplied")
} else {
paste0(n_complete, " observations")
}
}
# TRUE when a p-value is present and below .05 (guards NA / NULL p-values).
.screen_is_sig <- function(p) !is.null(p) && !is.na(p) && p < .05
# p-value tail formatting shared by the Bartlett and MVN lines and by format.efa_bartlett.
.screen_p_str <- function(p) {
if (is.null(p) || is.na(p)) " = NA" else if (p < .001) " < .001" else paste0(" = ", round(p, 3))
}
# Singular/plural picker for a scalar count (avoids cli's numeric pluralizer).
.screen_plural <- function(n, singular, plural) if (n == 1L) singular else plural
# One multivariate-normality test line: a danger alert when significant, a success alert
# otherwise. `text` already carries the test statistic and p-value (no cli braces). Left
# unwrapped, unlike the prose alerts around it: it is a fixed-format statistic line, and
# wrapping would let a break fall inside the statistic.
.screen_mvn_line <- function(sig, text) {
if (sig) cli::cli_alert_danger(text) else cli::cli_alert_success(text)
}
# Oxford-comma list of names ("a", "a and b", "a, b, and c").
.screen_and_list <- function(x) {
if (length(x) == 1L) return(x)
if (length(x) == 2L) return(paste(x, collapse = " and "))
paste0(paste(x[-length(x)], collapse = ", "), ", and ", x[length(x)])
}
# Build the per-variable display data frame: the full per-item table for raw data
# (columns renamed to the standard MSA/SMC abbreviations), or MSA and SMC alone for a
# correlation-matrix input. Numeric columns are rounded to `digits`.
.screen_per_item_display <- function(x, digits) {
if (!is.null(x$per_item)) {
disp <- x$per_item
num <- vapply(disp, is.numeric, logical(1))
disp[num] <- lapply(disp[num], round, digits)
names(disp)[names(disp) == "smc"] <- "SMC"
names(disp)[names(disp) == "kmo_i"] <- "MSA"
# `missing` is a percentage; the bare header reads as a count next to `variance`,
# which is on the data's own scale.
names(disp)[names(disp) == "missing"] <- "missing%"
# A `flags` entry is NA for a variable treated as continuous, i.e. "no category
# screening applies". Printed as <NA> that reads as a failed computation, so drop
# the column when it says nothing at all and render the remaining NAs as a dash.
if (all(is.na(disp$flags))) disp$flags <- NULL
else disp$flags[is.na(disp$flags)] <- "-"
disp
} else {
# make.unique() guards against duplicate variable names (a correlation matrix with
# duplicate dimnames is a valid input), which base data.frame() row names reject; it
# matches how the raw per-item table is keyed (efa_screen()).
data.frame(MSA = round(x$kmo$KMO_i, digits),
SMC = round(x$smc, digits),
row.names = make.unique(names(x$kmo$KMO_i)))
}
}
# Assemble the consolidated recommendations as parallel symbol/message vectors. Each
# check fires only when it applies; when no warning fires a single success line is
# returned, and a correlation-matrix input always closes with a raw-data note. Every
# recommendation is anchored in the literature cited in efa_screen()'s @source.
.screen_recommendations <- function(x, raw, kmo, bart_sig, mvn_nonnormal, multicollinear,
digits) {
sym <- character(0)
msg <- character(0)
push <- function(s, m) { sym[[length(sym) + 1L]] <<- s; msg[[length(msg) + 1L]] <<- m }
# Few-category items (fewer than 5 response categories) are detected up front because
# they steer the estimator recommendation away from the continuous-non-normal advice.
few <- character(0)
if (raw && !is.null(x$categories)) {
n_cat <- vapply(x$categories, function(ct) {
if (length(ct) == 1L && is.na(ct[1])) NA_integer_ else length(ct)
}, integer(1))
few <- names(n_cat)[!is.na(n_cat) & n_cat < 5L]
}
# Low overall sampling adequacy (KMO < .6): the set may not be factorable.
if (!is.null(kmo) && !is.na(kmo) && kmo < .6) {
push("!", paste0("Overall sampling adequacy is ", .kmo_band(kmo)$label,
" (KMO = ", round(kmo, digits),
"); the variables may not form a factorable set."))
}
# Variables with a low individual MSA (< .5) share little variance and are drop
# candidates (Kaiser & Rice, 1974). make.unique() keys the names as the per-item table
# and the sparse/empty recommendations do, so a variable is labelled consistently even
# when the input has duplicate variable names.
msa <- x$kmo$KMO_i
low <- make.unique(names(msa))[!is.na(msa) & msa < .5]
if (length(low)) {
push("!", paste0(length(low), " ", .screen_plural(length(low), "variable has", "variables have"),
" a low individual MSA (< .5): ", .screen_and_list(low),
"; consider removing ", .screen_plural(length(low), "it", "them"),
" (little shared variance)."))
}
# Few-category ordinal data: prefer a categorical estimator (Rhemtulla et al., 2012).
if (length(few)) {
push("!", paste0(length(few), " ", .screen_plural(length(few), "item has", "items have"),
" fewer than 5 response categories; treating ",
.screen_plural(length(few), "it", "them"),
" as ordinal (polychoric correlations with a categorical estimator such as DWLS)",
" is less biased than normal-theory ML."))
}
# Continuous / 5+-category non-normal data: normal-theory standard errors are biased.
# Suppressed when few-category items already routed the user to an ordinal estimator.
if (isTRUE(mvn_nonnormal) && !length(few)) {
push("!", paste("These data depart from multivariate normality; normal-theory standard errors",
"and fit statistics may be biased - prefer robust (sandwich) or bootstrapped",
"standard errors."))
}
# An over-powered Bartlett's test: significant but built on the normality it violates.
if (bart_sig && isTRUE(mvn_nonnormal)) {
push("!", paste("Bartlett's test is significant, but it assumes multivariate normality and grows",
"more sensitive as N increases; because these data are non-normal, treat it as",
"uninformative here and rely on the KMO."))
}
# Multicollinearity: a condition index above 30, the value Belsley (1991) gives for
# flagging a near linear dependency, decided once by the caller from the same band
# table as the section verdict. A high index does not have to come from one redundant
# pair - a set of items that is together nearly linearly dependent gives the same
# result - so the remedy names both.
if (isTRUE(multicollinear)) {
push("!", paste("A high condition index indicates multicollinearity; look for a redundant item",
"pair (r > .8) or a larger set of items that is nearly linearly dependent, and",
"consider removing some of them."))
}
if (raw) {
flags <- x$per_item$flags
items <- rownames(x$per_item)
sparse <- items[!is.na(flags) & grepl("sparse", flags)]
if (length(sparse)) {
push("!", paste0(length(sparse), " ", .screen_plural(length(sparse), "variable has", "variables have"),
" a sparse response category (< 5 responses): ", .screen_and_list(sparse),
"; a low-frequency category can destabilise polychoric estimates",
" - consider collapsing it into an adjacent category."))
}
empty <- items[!is.na(flags) & grepl("empty", flags)]
if (length(empty)) {
push("!", paste0(length(empty), " ", .screen_plural(length(empty), "variable has", "variables have"),
" an unused interior response category (a gap in the scale): ",
.screen_and_list(empty), "; check the coding."))
}
if (!inherits(x$outliers, "efa_screen_no_outliers")) {
nf <- length(x$outliers$flagged)
nominal <- 1 - (x$settings$outlier_cutoff %||% 0.975)
frac <- nf / x$outliers$n_complete
# Far more flags than the cutoff nominally admits is not a contamination count to
# work through: a high-breakdown estimate fitted to the most concentrated half of a
# clustered or mixture sample legitimately calls the rest distant, so the excess is
# evidence that the data are not elliptically distributed. The rate alone is not
# enough to draw that conclusion - at a tight cutoff, or in a small sample, a couple
# of genuine outliers clear it - so the flagged set must also be too long to work
# through case by case, which is what makes the ordinary advice below unhelpful.
if (nf >= 10L && frac > 5 * nominal) {
# The rate is over the complete cases, so with missing data both bases are named:
# read against the rows supplied it would look milder than it is.
of_those <- if (.screen_incomplete(x$outliers$n_complete, x$settings$n_obs)) {
"% of those cases"
} else {
"%"
}
push("!", paste0(nf, " of ",
.screen_case_base(x$outliers$n_complete, x$settings$n_obs),
" (", round(100 * frac), of_those,
") exceed the outlier cutoff, far above the ",
# keep a significant digit: a tight cutoff has a nominal rate well
# below 0.1%, which rounds to a self-defeating "0%"
format(signif(100 * nominal, 2), trim = TRUE, scientific = FALSE),
"% expected under multivariate normality; this usually means the",
" data are not elliptically distributed (subgroups or a mixture)",
" rather than that this many cases are contaminated."))
} else if (nf > 0L) {
push("!", paste0(nf, " ", .screen_plural(nf, "observation was", "observations were"),
" flagged as ",
.screen_plural(nf, "a potential multivariate outlier", "potential multivariate outliers"),
"; inspect ", .screen_plural(nf, "it", "them"),
" (see `$outliers$flagged`) before down-weighting or excluding."))
}
}
}
# A clean bill of health when nothing above fired; the raw-data note always closes a
# correlation-matrix report (its raw-only sections were absent above).
if (!length(msg)) {
push("v", "The data appear suitable for factor analysis.")
}
if (!raw) {
push("i", paste("Per-item variance, missing-data, category, normality, and outlier diagnostics",
"require raw data; only a correlation matrix was supplied."))
}
list(symbol = sym, message = msg)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.