| Title: | Asymmetric Smoothed-Association Matrices via GAM Fits |
|---|---|
| Description: | Render a pairwise, asymmetric smoothed-association matrix of continuous variables. Each cell shows the fitted spline from an 'mgcv' generalised additive model, with the upper triangle displaying 'gam(x_j ~ s(x_i))' and the lower triangle 'gam(x_i ~ s(x_j))'. Unlike Pearson's correlation matrix, the visualisation is intentionally asymmetric, revealing heteroscedasticity, leverage, and directional non-linearity that a single scalar correlation hides. An asymmetry index and a 24-category shape taxonomy quantify the directional difference and qualitative form of each fitted smooth. |
| Authors: | Max Moldovan [aut, cre, cph] (ORCID: <https://orcid.org/0000-0001-9680-8474>) |
| Maintainer: | Max Moldovan <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0 |
| Built: | 2026-10-19 20:17:05 UTC |
| Source: | https://github.com/max578/janusplot |
Render a pairwise, asymmetric matrix of smoothed associations between
numeric variables. Each cell [i, j] where i != j shows the fitted
spline from mgcv::gam():
Upper triangle (i < j): gam(x_j ~ s(x_i) + <adjust>).
Lower triangle (i > j): gam(x_i ~ s(x_j) + <adjust>).
Diagonal: blank panel when labels live on the border (default),
or a variable-name label when labels = "diagonal".
The two triangles intentionally differ – the asymmetry reveals heteroscedasticity, leverage, and directional non-linearity that a single scalar correlation hides.
janusplot( data, vars = NULL, adjust = NULL, method = NULL, k = -1L, bs = "tp", engine = c("bam", "gam"), discrete = FALSE, nthreads = 1L, order = c("original", "hclust", "alphabetical"), show_data = TRUE, show_ci = TRUE, display = c("fit", "d1", "d2"), derivative_ci = c("none", "pointwise", "simultaneous"), derivative_ci_nsim = 1000L, n_grid = NULL, colour_by = c("pearson", "spearman", "kendall", "edf", "deviance_gap", "none"), fill_by = NULL, palette = NULL, annotations = c("edf", "A"), shape_cutoffs = janusplot_shape_cutoffs(), show_shape_legend = TRUE, glyph_style = c("ascii", "unicode"), labels = c("border", "diagonal", "none"), diagonal = c("auto", "blank", "name", "density"), label_srt = 45, label_cex = 1, signif_glyph = TRUE, show_asymmetry = NULL, na_action = c("pairwise", "complete"), parallel = FALSE, with_data = FALSE, text_scale_diag = 1, text_scale_off_diag = 1, show_glossary = TRUE, glossary_scale = 1, k_check_thresholds = NULL, auto_refit_k = FALSE, k_max_iter = 2L, compact = c("auto", "always", "never"), compact_threshold = 12L, compact_levels = NULL, focus_by = NA_character_, focus_threshold = "q90", focus_dim_alpha = 0.25, axes = c("original", "standardised", "centred", "rank"), save_as = NULL, save_width = NULL, save_height = NULL, save_dpi = 300, ... )janusplot( data, vars = NULL, adjust = NULL, method = NULL, k = -1L, bs = "tp", engine = c("bam", "gam"), discrete = FALSE, nthreads = 1L, order = c("original", "hclust", "alphabetical"), show_data = TRUE, show_ci = TRUE, display = c("fit", "d1", "d2"), derivative_ci = c("none", "pointwise", "simultaneous"), derivative_ci_nsim = 1000L, n_grid = NULL, colour_by = c("pearson", "spearman", "kendall", "edf", "deviance_gap", "none"), fill_by = NULL, palette = NULL, annotations = c("edf", "A"), shape_cutoffs = janusplot_shape_cutoffs(), show_shape_legend = TRUE, glyph_style = c("ascii", "unicode"), labels = c("border", "diagonal", "none"), diagonal = c("auto", "blank", "name", "density"), label_srt = 45, label_cex = 1, signif_glyph = TRUE, show_asymmetry = NULL, na_action = c("pairwise", "complete"), parallel = FALSE, with_data = FALSE, text_scale_diag = 1, text_scale_off_diag = 1, show_glossary = TRUE, glossary_scale = 1, k_check_thresholds = NULL, auto_refit_k = FALSE, k_max_iter = 2L, compact = c("auto", "always", "never"), compact_threshold = 12L, compact_levels = NULL, focus_by = NA_character_, focus_threshold = "q90", focus_dim_alpha = 0.25, axes = c("original", "standardised", "centred", "rank"), save_as = NULL, save_width = NULL, save_height = NULL, save_dpi = 300, ... )
data |
A data frame with numeric columns to include. |
vars |
Character vector of column names to use. |
adjust |
A one-sided formula RHS giving additional covariates
and/or random effects to include in every pairwise GAM. For example,
|
method |
Smoothing-parameter estimation method passed to
the chosen fitting backend. Default |
k |
Integer, or named list mapping variable names to integers.
Basis dimension for |
bs |
Basis type for |
engine |
One of
|
discrete |
Logical. |
nthreads |
Integer. |
order |
One of |
show_data |
Logical. If |
show_ci |
Logical. If |
display |
One of
A single matrix shows a single quantity by design: stacked
multi-panel cells crowd the matrix at any realistic variable
count. To compare fit against derivative, render two or three
Orders |
derivative_ci |
One of
|
derivative_ci_nsim |
Integer. Number of Monte Carlo samples
used when |
n_grid |
Integer or |
colour_by |
One of |
fill_by |
Deprecated alias for |
palette |
Character. Colour palette for the cell fill scale.
Defaults to |
annotations |
Character vector, a subset of
Default |
shape_cutoffs |
Named list of classification thresholds used to
map the continuous shape indices into discrete |
show_shape_legend |
Logical. If |
glyph_style |
One of |
labels |
One of
|
diagonal |
One of
|
label_srt |
Numeric. Rotation (degrees) of top labels when
|
label_cex |
Positive numeric multiplier on the border-label
font size. Default |
signif_glyph |
Logical. If |
show_asymmetry |
Deprecated. Use |
na_action |
One of |
parallel |
Logical. If |
with_data |
Logical. If |
text_scale_diag |
Positive numeric multiplier applied to the
diagonal variable-name labels. Default |
text_scale_off_diag |
Positive numeric multiplier applied to
all off-diagonal annotations ( |
show_glossary |
Logical. If |
glossary_scale |
Positive numeric multiplier on the glossary
caption font size. Default |
k_check_thresholds |
Named list giving the three flag-criterion
thresholds used by |
auto_refit_k |
Logical. If |
k_max_iter |
Integer. Maximum number of doublings allowed
per cell when |
compact |
One of
|
compact_threshold |
Integer. The |
compact_levels |
Optional named list with entries |
focus_by |
One of |
focus_threshold |
Either a quantile-string like |
focus_dim_alpha |
Numeric in |
axes |
One of
At compact tier 3 ( |
save_as |
Optional file path with extension. When set,
the final assembled plot is written to this path via
|
save_width |
Numeric. Override width (inches) for
|
save_height |
Numeric. Override height (inches) for
|
save_dpi |
Integer. Raster DPI for |
... |
Additional arguments passed to |
If with_data = FALSE (default), a ggplot2::ggplot object
(via patchwork::wrap_plots()) carrying a top-of-matrix title
that names the displayed quantity ("Direct fit",
"First derivative f'", or "Second derivative f''"). If
with_data = TRUE, a list with two elements: plot (the
ggplot) and data (a tidy table with columns var_x, var_y,
position, n_used, edf, pvalue, signif, dev_exp,
asymmetry_index, cor_pearson, cor_spearman,
cor_kendall, tie_ratio, monotonicity_index,
convexity_index, n_turning_points, n_inflections,
flat_range_ratio, shape_category, colour_value,
display, one row per off-diagonal cell). The display
column tags which quantity the call rendered, so separate
calls for fit / d1 / d2 yield comparable, stackable tables.
Derivative curves themselves (grid of , fitted
, SE) live on janusplot_data() – see there.
janusplot_data() for the raw per-cell fits + metrics.
Other smooth-associations:
janusplot_data()
# Minimal runnable example -- 3 variables, 6 asymmetric pairwise GAM fits. janusplot(mtcars[, c("mpg", "hp", "wt")]) # Heteroscedastic DGP: Pearson r is ~ 0.9 but the inverse fit is # clearly non-linear, yielding asymmetry index > 0.5. set.seed(2026L) n <- 200L x1 <- stats::runif(n, 0, 10) x2 <- x1 + stats::rnorm(n, sd = 0.2 * x1) janusplot(data.frame(x1 = x1, x2 = x2, x3 = stats::rnorm(n))) # A single matrix renders a single quantity. To compare the fit # against its derivatives, render three calls and place them # side-by-side; each call's title makes the quantity explicit. set.seed(2026L) xs <- stats::runif(300L, -3, 3) df <- data.frame( x = xs, y1 = sin(xs) + stats::rnorm(300L, sd = 0.3), y2 = xs^2 + stats::rnorm(300L, sd = 0.6) ) janusplot(df, display = "fit") janusplot(df, display = "d1") janusplot(df, display = "d2") # Simultaneous CI bands on a derivative panel, per Simpson (2018). janusplot(df, display = "d1", derivative_ci = "simultaneous")# Minimal runnable example -- 3 variables, 6 asymmetric pairwise GAM fits. janusplot(mtcars[, c("mpg", "hp", "wt")]) # Heteroscedastic DGP: Pearson r is ~ 0.9 but the inverse fit is # clearly non-linear, yielding asymmetry index > 0.5. set.seed(2026L) n <- 200L x1 <- stats::runif(n, 0, 10) x2 <- x1 + stats::rnorm(n, sd = 0.2 * x1) janusplot(data.frame(x1 = x1, x2 = x2, x3 = stats::rnorm(n))) # A single matrix renders a single quantity. To compare the fit # against its derivatives, render three calls and place them # side-by-side; each call's title makes the quantity explicit. set.seed(2026L) xs <- stats::runif(300L, -3, 3) df <- data.frame( x = xs, y1 = sin(xs) + stats::rnorm(300L, sd = 0.3), y2 = xs^2 + stats::rnorm(300L, sd = 0.6) ) janusplot(df, display = "fit") janusplot(df, display = "d1") janusplot(df, display = "d2") # Simultaneous CI bands on a derivative panel, per Simpson (2018). janusplot(df, display = "d1", derivative_ci = "simultaneous")
Companion to janusplot() returning the raw list of GAM fits plus
per-cell metrics (EDF, F-test p-value, deviance explained, asymmetry
index, pairwise correlations, shape descriptors) without constructing
the ggplot. Useful for custom rendering or downstream analysis.
janusplot_data( data, vars = NULL, adjust = NULL, method = NULL, k = -1L, bs = "tp", na_action = c("pairwise", "complete"), parallel = FALSE, keep_fits = FALSE, derivatives = integer(), derivative_ci = c("pointwise", "none", "simultaneous"), derivative_ci_nsim = 1000L, n_grid = NULL, shape_cutoffs = janusplot_shape_cutoffs(), k_check_thresholds = NULL, auto_refit_k = FALSE, k_max_iter = 2L, engine = c("bam", "gam"), discrete = FALSE, nthreads = 1L, ... )janusplot_data( data, vars = NULL, adjust = NULL, method = NULL, k = -1L, bs = "tp", na_action = c("pairwise", "complete"), parallel = FALSE, keep_fits = FALSE, derivatives = integer(), derivative_ci = c("pointwise", "none", "simultaneous"), derivative_ci_nsim = 1000L, n_grid = NULL, shape_cutoffs = janusplot_shape_cutoffs(), k_check_thresholds = NULL, auto_refit_k = FALSE, k_max_iter = 2L, engine = c("bam", "gam"), discrete = FALSE, nthreads = 1L, ... )
data |
A data frame with numeric columns to include. |
vars |
Character vector of column names to use. |
adjust |
A one-sided formula RHS giving additional covariates
and/or random effects to include in every pairwise GAM. For example,
|
method |
Smoothing-parameter estimation method passed to
the chosen fitting backend. Default |
k |
Integer, or named list mapping variable names to integers.
Basis dimension for |
bs |
Basis type for |
na_action |
One of |
parallel |
Logical. If |
keep_fits |
Logical. If |
derivatives |
Integer vector of derivative orders to compute
on every pair (subset of |
derivative_ci |
One of
|
derivative_ci_nsim |
Integer. Number of Monte Carlo samples
used when |
n_grid |
Integer or |
shape_cutoffs |
Named list of classification thresholds used to
map the continuous shape indices ( |
k_check_thresholds |
Named list giving the three flag-criterion
thresholds used by |
auto_refit_k |
Logical. If |
k_max_iter |
Integer. Maximum number of doublings allowed
per cell when |
engine |
One of
|
discrete |
Logical. |
nthreads |
Integer. |
... |
Additional arguments passed to |
A list with components:
varsCharacter vector of variables used, in plotted order.
pairsList of per-pair results. Each element has i, j,
var_i, var_j, fit_yx, fit_xy (NULL if keep_fits = FALSE),
pred_yx, pred_xy (data frames with x, fit, se, lo,
hi), edf_yx, edf_xy, pvalue_yx, pvalue_xy, dev_exp_yx,
dev_exp_xy, n_used, asymmetry_index, plus Pearson /
Spearman / Kendall correlations (cor_pearson, cor_spearman,
cor_kendall), the maximum tie ratio across x and y
(tie_ratio), and per-direction shape descriptors
(monotonicity_index_yx, convexity_index_yx,
monotonicity_index_xy, convexity_index_xy,
n_turning_yx, n_inflect_yx, n_turning_xy,
n_inflect_xy, shape_yx, shape_xy).
When derivatives is non-empty, each pair additionally
carries deriv_yx and deriv_xy, each a named list keyed by
order ("1", "2") whose entries are data frames with
columns x, fit, se, lo, hi, ci_type matching the
schema of pred_yx / pred_xy. The ci_type column records
whether the lo / hi columns are "pointwise" (default),
"simultaneous" (Ruppert–Wand–Carroll / Simpson 2018
critical-multiplier bands), or "none". When
derivative_ci = "simultaneous", each derivative frame also
carries a "crit_multiplier" attribute giving the MC-derived
critical multiplier used.
See janusplot_shape_metrics() for the definition of the
monotonicity and convexity indices.
callMatch call.
janusplot() for the ggplot front-end,
janusplot_shape_metrics() for the shape-metric primitives.
Other smooth-associations:
janusplot()
# Per-pair fits + metrics on a small mtcars slice out <- janusplot_data(mtcars[, c("mpg", "hp", "wt")]) out$pairs[[1L]]$asymmetry_index out$pairs[[1L]]$cor_spearman out$pairs[[1L]]$shape_yx# Per-pair fits + metrics on a small mtcars slice out <- janusplot_data(mtcars[, c("mpg", "hp", "wt")]) out$pairs[[1L]]$asymmetry_index out$pairs[[1L]]$cor_spearman out$pairs[[1L]]$shape_yx
janusplot() renders a pair twice – y ~ s(x) in one triangle and
x ~ s(y) in the other – and summarises the difference between the
two fits as an asymmetry index. That index is a diagnostic: it says
the two directions of fit are not interchangeable, not that either one
is the causal direction.
This function runs the test that turns the picture into a claim. Under
the additive-noise model y = f(x) + e with e independent of x,
the model holds in at most one direction for all but a small set of
exceptional joint distributions. So a direction is supported when
the residuals of the fit in that direction are independent of the putative cause, and
the residuals of the fit in the opposite direction are not.
Independence is assessed with the kernel (HSIC) permutation test in
kernR::hsic_test(). Anything other than the clean one-sided pattern
– both directions admitting an additive-noise model, neither doing
so, or the known non-identifiable linear-Gaussian case – returns
"undecided". The smaller of the two p-values is never read as a
direction.
janusplot_direction_test( data, x, y, k = -1L, bs = "tp", method = NULL, alpha = 0.05, n_permutations = 500L, linear_tol = 0.01, gaussian_alpha = 0.01, seed = NULL )janusplot_direction_test( data, x, y, k = -1L, bs = "tp", method = NULL, alpha = 0.05, n_permutations = 500L, linear_tol = 0.01, gaussian_alpha = 0.01, seed = NULL )
data |
A data frame containing both variables. |
x |
Character. Column name of the first variable – the putative cause under the forward hypothesis. |
y |
Character. Column name of the second variable – the putative effect under the forward hypothesis. |
k |
Basis dimension passed to |
bs |
Character. Spline basis passed to |
method |
Smoothing-parameter selection method passed to
|
alpha |
Numeric in |
n_permutations |
Integer. Permutations used to build each HSIC
null distribution. Must be at least |
linear_tol |
Numeric in |
gaussian_alpha |
Numeric in |
seed |
Integer or |
Identifiability fails exactly when the relationship is linear and both
the cause and the noise are Gaussian: a bivariate Gaussian is
reproduced perfectly by a linear model with Gaussian noise in either
direction, so the residual-independence condition holds both ways and
carries no directional information. This case is detected before the
two independence tests are read, and returns "undecided" with
reason_code = "linear_gaussian". It is flagged when both of the
following hold:
Linearity. Each fitted smooth departs from a straight line by
less than linear_tol, measured as 1 - cor(fitted, x)^2 – the
share of the fitted variation that no straight line in the predictor
can reproduce.
Gaussianity. Shapiro-Wilk applied to x, y, and both residual
vectors fails to reject normality at gaussian_alpha in all four
cases.
gaussian_alpha defaults to 0.01, not 0.05: the consequence of
the flag is abstention, so the threshold is set to withhold a
directional claim unless the data argue clearly against Gaussianity.
A "forward" or "reverse" verdict states that the condition
licensing that direction holds on this sample. It is not a proof of
causation: the additive-noise model assumes no unobserved common
cause, no feedback, and additive noise, and none of those assumptions
is testable from the pair alone.
A named list:
verdictOne of "forward" (x drives y),
"reverse" (y drives x), or "undecided".
reason_codeMachine-readable ground for the verdict:
"forward_only", "reverse_only", "both_independent",
"neither_independent", or "linear_gaussian".
reasonOne-paragraph statement of the same ground.
x, y
The column names, as supplied.
forward, reverse
The two hypotheses as printable
strings, e.g. "x -> y".
p_forward, p_reverse
HSIC permutation p-values for residual-versus-cause independence in each direction.
statistic_forward, statistic_reverse
The two observed HSIC statistics.
independent_forward, independent_reverse
Whether each
p-value exceeds alpha.
edf_forward, edf_reverse
Effective degrees of freedom of each fitted smooth.
curvature_forward, curvature_reverse
Departure of each smooth from a straight line; see Details.
normality_pNamed numeric vector of the four
Shapiro-Wilk p-values (x, y, residuals_forward,
residuals_reverse).
linear_gaussianTRUE when the non-identifiable
linear-Gaussian case was detected.
nComplete cases used.
alpha, gaussian_alpha, linear_tol,
n_permutations
The settings the verdict was reached under.
Hoyer, P. O., Janzing, D., Mooij, J. M., Peters, J., & Scholkopf, B. (2009). Nonlinear causal discovery with additive noise models. Advances in Neural Information Processing Systems, 21, 689-696.
Gretton, A., Fukumizu, K., Teo, C. H., Song, L., Scholkopf, B., & Smola, A. J. (2008). A kernel statistical test of independence. Advances in Neural Information Processing Systems, 20, 585-592.
Shimizu, S., Hoyer, P. O., Hyvarinen, A., & Kerminen, A. (2006). A linear non-Gaussian acyclic model for causal discovery. Journal of Machine Learning Research, 7, 2003-2030.
janusplot(), janusplot_data().
if (requireNamespace("kernR", quietly = TRUE)) { # x drives y through a non-linear function with non-Gaussian noise: # the additive-noise model is identifiable here. set.seed(2026L) n <- 200L x <- stats::runif(n, -3, 3) y <- x^3 / 5 + stats::runif(n, -1, 1) d <- data.frame(x = x, y = y) res <- janusplot_direction_test(d, "x", "y", n_permutations = 199L, seed = 1L) res$verdict # "forward" res$reason c(forward = res$p_forward, reverse = res$p_reverse) # The same pair read the other way round returns "reverse". janusplot_direction_test(d, "y", "x", n_permutations = 199L, seed = 1L)$verdict }if (requireNamespace("kernR", quietly = TRUE)) { # x drives y through a non-linear function with non-Gaussian noise: # the additive-noise model is identifiable here. set.seed(2026L) n <- 200L x <- stats::runif(n, -3, 3) y <- x^3 / 5 + stats::runif(n, -1, 1) d <- data.frame(x = x, y = y) res <- janusplot_direction_test(d, "x", "y", n_permutations = 199L, seed = 1L) res$verdict # "forward" res$reason c(forward = res$p_forward, reverse = res$p_reverse) # The same pair read the other way round returns "reverse". janusplot_direction_test(d, "y", "x", n_permutations = 199L, seed = 1L)$verdict }
shape_category classificationReturns the named list of thresholds used to map the continuous
monotonicity (M) and convexity (C) indices (plus inflection
counts) into a discrete shape_category. Expose so callers can
override individual thresholds or pass a fully custom list to
janusplot() / janusplot_shape_metrics().
janusplot_shape_cutoffs(...)janusplot_shape_cutoffs(...)
... |
Optional named overrides to merge into the defaults. |
A named list with numeric thresholds:
mono_strong|M| threshold for a strictly monotone smooth (default 0.9).
mono_mod|M| threshold for a curved-but-monotone smooth (default 0.5).
mono_nonmono|M| below this is considered non-monotone (default 0.3).
mono_s|M| threshold for labelling an S-shape (default 0.5).
curv_low|C| below this is considered near-linear curvature (default 0.2).
curv_mod|C| threshold for a clearly curved monotone (default 0.5).
curv_strong|C| threshold for a U-shape / inverted-U shape
(default 0.5).
flatrange(fit) / sd(y) below this is called flat (default 0.05).
Other shape-metrics:
janusplot_shape_hierarchy(),
janusplot_shape_metrics()
janusplot_shape_cutoffs() janusplot_shape_cutoffs(curv_mod = 0.6, flat = 0.02)janusplot_shape_cutoffs() janusplot_shape_cutoffs(curv_mod = 0.6, flat = 0.02)
Return the full janusplot shape taxonomy as a data frame with four
hierarchy columns plus presentation fields. The taxonomy is the
single source of truth consumed by the classifier, the cell
renderer, the legend plate, and the janusplot_data() output.
Hierarchy columns (finest -> coarsest):
category24-way fine label (linear_up, skewed_peak,
bimodal, ...). Computed per cell by janusplot().
codeUnique two-letter ASCII shorthand (safe on any
font or typesetting pipeline) – e.g. lu for linear_up.
archetypeSeven-family grouping: monotone_linear,
monotone_curved, unimodal, wave, multimodal,
chaotic, degenerate.
monotonicThree-way coarse classification: monotone
/ non_monotone / degenerate.
linearBinary: linear / non_linear /
degenerate.
The broader tiers (linear/non-linear, monotone/non-monotone) are
textbook calculus; the archetype layer maps cleanly to
shape-constrained regression vocabulary (Pya & Wood 2015;
Meyer 2008) and to dose-response shape categories (Calabrese
2008; Calabrese & Baldwin 2001). The (T, I) dispatch
underlying each fine category is a coarsened Morse-theoretic
critical-point classification (Milnor 1963).
janusplot_shape_hierarchy()janusplot_shape_hierarchy()
A data frame with 24 rows and columns category,
code, archetype, monotonic, linear, glyph, ascii,
label, gloss.
Calabrese, E. J. (2008). Hormesis: why it is important to toxicology and toxicologists. Environmental Toxicology and Chemistry, 27(7), 1451-1474.
Meyer, M. C. (2008). Inference using shape-restricted regression splines. Annals of Applied Statistics, 2(3), 1013-1033.
Milnor, J. (1963). Morse Theory. Princeton University Press.
Pya, N., & Wood, S. N. (2015). Shape constrained additive models. Statistics and Computing, 25(3), 543-559.
Other shape-metrics:
janusplot_shape_cutoffs(),
janusplot_shape_metrics()
tax <- janusplot_shape_hierarchy() head(tax[, c("category", "code", "archetype", "monotonic", "linear")]) # Count how many categories live in each archetype table(tax$archetype)tax <- janusplot_shape_hierarchy() head(tax[, c("category", "code", "archetype", "monotonic", "linear")]) # Count how many categories live in each archetype table(tax$archetype)
Compute the continuous monotonicity and convexity indices, inflection
and turning-point counts, and rule-based shape category for a fitted
univariate smooth. Works on either a per-pair fit object returned
from the janusplot internal machinery or a freshly fitted
mgcv::gam() with a single s() term.
Both indices are bounded in [-1, 1] and weighted by the empirical
density of the predictor:
monotonicity_index (paper symbol M). Let f be the fitted
smooth evaluated on a dense grid of n_grid equally-spaced
points across the predictor range, f' its numerical first
derivative, and w the empirical density of the predictor on
the same grid with sum(w) = 1. Then
monotonicity_index = sum(w * f') / sum(w * |f'|) in [-1, 1].
+1 is strictly increasing, -1 strictly decreasing, 0
non-monotone.
convexity_index (paper symbol C). With f'' the numerical
second derivative on the same grid,
convexity_index = sum(w * f'') / sum(w * |f''|) in [-1, 1].
+1 is globally convex (bowl-up), -1 globally concave
(bowl-down), 0 inflection-dominated (S-curve, sine, flat).
Both indices are scale-invariant (replacing y -> a*y + b leaves
them unchanged) and density-weighted so they describe the smooth
where the data actually live, not extrapolated tails.
janusplot_shape_metrics( fit, x_name = NULL, newdata = NULL, n_grid = 200L, cutoffs = janusplot_shape_cutoffs() )janusplot_shape_metrics( fit, x_name = NULL, newdata = NULL, n_grid = 200L, cutoffs = janusplot_shape_cutoffs() )
fit |
Either a list returned by a janusplot pair-fit helper
(must contain |
x_name |
Character. Column name of the predictor when |
newdata |
Optional data frame supplying the raw predictor values
used for density weighting when |
n_grid |
Integer. Prediction grid length when |
cutoffs |
Named list of classification thresholds; see
|
A named list with components:
monotonicity_indexM in [-1, 1]. See Description.
convexity_indexC in [-1, 1]. See Description.
n_turning_pointsInteger count of lobe-mass-weighted
sign changes of f'. Equals the number of interior extrema.
n_inflectionsInteger count of lobe-mass-weighted
sign changes of f''.
flat_range_ratiorange(f) / sd(y) – small values
indicate a degenerate flat smooth.
shape_categoryOne of 24 labels from
janusplot_shape_hierarchy() dispatched on
(n_turning_points, n_inflections) with
(monotonicity_index, convexity_index) disambiguation for
the monotone case.
janusplot_shape_cutoffs(), janusplot(), janusplot_data().
Other shape-metrics:
janusplot_shape_cutoffs(),
janusplot_shape_hierarchy()
# On a fitted gam set.seed(2026L) n <- 200L x <- stats::runif(n, 0, 10) y <- log1p(x) + stats::rnorm(n, sd = 0.3) d <- data.frame(x = x, y = y) fit <- mgcv::gam(y ~ s(x), data = d, method = "REML") janusplot_shape_metrics(fit, x_name = "x", newdata = d)# On a fitted gam set.seed(2026L) n <- 200L x <- stats::runif(n, 0, 10) y <- log1p(x) + stats::rnorm(n, sd = 0.3) d <- data.frame(x = x, y = y) fit <- mgcv::gam(y ~ s(x), data = d, method = "REML") janusplot_shape_metrics(fit, x_name = "x", newdata = d)
Run a full-factorial sensitivity sweep for the janusplot 24-category
shape classifier. For each combination of ground-truth shape, sample
size n, noise level sigma, and replicate, the sweep:
Generates n points from the noiseless canonical curve on
[0, 1] + Gaussian noise with SD = sigma (fraction of the
y-range, so signal-to-noise is comparable across shapes).
Fits mgcv::gam(y ~ s(x), method = "REML").
Runs janusplot_shape_metrics() to classify the fitted smooth.
Records correctness at both the fine (24-category) and archetype (7-family) levels.
The function is the package-native implementation of
simulation/scripts/scenario_4_shape_recognition.R. A small
precomputed dataset is shipped as shape_sensitivity_demo for
downstream examples without requiring users to re-run the sweep.
janusplot_shape_sensitivity( shapes = NULL, n_grid = c(50L, 100L, 200L, 500L), sigma_grid = c(0.02, 0.05, 0.1, 0.2, 0.4), n_rep = 200L, cutoffs = janusplot_shape_cutoffs(), parallel = FALSE, seed = 2026L, verbose = interactive() )janusplot_shape_sensitivity( shapes = NULL, n_grid = c(50L, 100L, 200L, 500L), sigma_grid = c(0.02, 0.05, 0.1, 0.2, 0.4), n_rep = 200L, cutoffs = janusplot_shape_cutoffs(), parallel = FALSE, seed = 2026L, verbose = interactive() )
shapes |
Character vector of ground-truth names from
|
n_grid |
Integer vector of sample sizes. Default
|
sigma_grid |
Numeric vector of noise levels (fraction of the
y-range). Default |
n_rep |
Integer. Replicates per cell. Default |
cutoffs |
Named list of classification thresholds; see
|
parallel |
Logical. If |
seed |
Integer. Base seed – each fit uses |
verbose |
Logical. Print progress messages to the console.
Default is |
A data frame with one row per fit. Columns:
truthGround-truth shape name.
nSample size for this fit.
sigmaNoise level for this fit.
seedRNG seed used.
predictedClassifier output at the fine (24-category) level.
correctLogical – does predicted == truth?
archetype_truthExpected archetype for truth.
archetype_predArchetype of predicted.
archetype_correctLogical – archetype-level correctness.
monotonicity_indexMonotonicity index M (see
janusplot_shape_metrics()).
convexity_indexConvexity index C (see
janusplot_shape_metrics()).
n_turn, n_inflect
Recovered turning-point and inflection counts.
error"gam_fit_failed" when mgcv::gam() errored;
NA otherwise.
janusplot_shape_sensitivity_summary(),
janusplot_shape_sensitivity_plot(),
janusplot_shape_sensitivity_shapes(), shape_sensitivity_demo.
Other shape-sensitivity:
janusplot_shape_sensitivity_plot(),
janusplot_shape_sensitivity_shapes(),
janusplot_shape_sensitivity_summary()
# Tiny-run smoke test (< 2 seconds): 3 shapes x 2 n x 2 sigma x 5 reps. res <- janusplot_shape_sensitivity( shapes = c("linear_up", "u_shape", "wave"), n_grid = c(100L, 200L), sigma_grid = c(0.05, 0.20), n_rep = 5L, verbose = FALSE ) head(res) janusplot_shape_sensitivity_summary(res, level = "archetype")# Tiny-run smoke test (< 2 seconds): 3 shapes x 2 n x 2 sigma x 5 reps. res <- janusplot_shape_sensitivity( shapes = c("linear_up", "u_shape", "wave"), n_grid = c(100L, 200L), sigma_grid = c(0.05, 0.20), n_rep = 5L, verbose = FALSE ) head(res) janusplot_shape_sensitivity_summary(res, level = "archetype")
Produce one of four diagnostic plots from the raw data frame returned
by janusplot_shape_sensitivity():
"confusion_fine"24 x (|shapes|) confusion matrix at the
fine category level – rows = ground truth, columns = predicted,
cells coloured by P(pred | truth).
"confusion_archetype"7 x 7 confusion matrix at the archetype level.
"accuracy_grid"per-shape heatmap of archetype-level
accuracy across the (n, sigma) design.
"recovery_curves"accuracy as a function of sigma, one line per sample size, faceted by shape.
janusplot_shape_sensitivity_plot( results, type = c("confusion_fine", "confusion_archetype", "accuracy_grid", "recovery_curves") )janusplot_shape_sensitivity_plot( results, type = c("confusion_fine", "confusion_archetype", "accuracy_grid", "recovery_curves") )
results |
Data frame from |
type |
One of |
A ggplot2::ggplot object.
Other shape-sensitivity:
janusplot_shape_sensitivity(),
janusplot_shape_sensitivity_shapes(),
janusplot_shape_sensitivity_summary()
data("shape_sensitivity_demo", package = "janusplot") janusplot_shape_sensitivity_plot(shape_sensitivity_demo, "recovery_curves")data("shape_sensitivity_demo", package = "janusplot") janusplot_shape_sensitivity_plot(shape_sensitivity_demo, "recovery_curves")
Return the names of every canonical ground-truth shape that
janusplot_shape_sensitivity() can simulate from. Fourteen shapes
spanning five archetypes (monotone_linear, monotone_curved,
unimodal, wave, multimodal). The chaotic and degenerate
archetypes are out of scope (no realistic deterministic generator).
janusplot_shape_sensitivity_shapes()janusplot_shape_sensitivity_shapes()
Character vector of length 14 – the generator names.
janusplot_shape_sensitivity(), janusplot_shape_hierarchy().
Other shape-sensitivity:
janusplot_shape_sensitivity(),
janusplot_shape_sensitivity_plot(),
janusplot_shape_sensitivity_summary()
janusplot_shape_sensitivity_shapes()janusplot_shape_sensitivity_shapes()
Aggregate the raw output of janusplot_shape_sensitivity() into a
per-cell mean-accuracy table at either the fine (24-category) or
archetype (7-family) level.
janusplot_shape_sensitivity_summary(results, level = c("fine", "archetype"))janusplot_shape_sensitivity_summary(results, level = c("fine", "archetype"))
results |
Data frame returned by |
level |
One of |
A data frame with columns truth, n, sigma, accuracy.
Other shape-sensitivity:
janusplot_shape_sensitivity(),
janusplot_shape_sensitivity_plot(),
janusplot_shape_sensitivity_shapes()
data("shape_sensitivity_demo", package = "janusplot") head(janusplot_shape_sensitivity_summary(shape_sensitivity_demo, level = "archetype"))data("shape_sensitivity_demo", package = "janusplot") head(janusplot_shape_sensitivity_summary(shape_sensitivity_demo, level = "archetype"))
Raw output from a small-footprint invocation of
janusplot_shape_sensitivity(). Shipped so users can explore the
sensitivity API and regenerate every figure in the
shape-recognition-sensitivity vignette without having to re-run
the sweep themselves. Regenerated via
data-raw/shape_sensitivity_demo.R.
Design:
Shapes (6, across 5 of the 7 archetypes – u_shape
and inverted_u both roll up to unimodal):
linear_up, concave_up, u_shape, inverted_u, wave,
bimodal.
Sample sizes (3): c(100, 200, 500).
Noise levels (4): c(0.05, 0.10, 0.20, 0.40) fraction
of y-range.
Replicates: 30.
Total fits: 2160.
Seed: 2026.
shape_sensitivity_demoshape_sensitivity_demo
A data frame with 2160 rows and 14 columns – see the
"Value" section of janusplot_shape_sensitivity() for the
column schema.
janusplot_shape_sensitivity(),
janusplot_shape_sensitivity_plot(),
janusplot_shape_sensitivity_summary().
data("shape_sensitivity_demo", package = "janusplot") head(shape_sensitivity_demo) janusplot_shape_sensitivity_plot(shape_sensitivity_demo, "recovery_curves")data("shape_sensitivity_demo", package = "janusplot") head(shape_sensitivity_demo) janusplot_shape_sensitivity_plot(shape_sensitivity_demo, "recovery_curves")