Changes in version 2.5.1 rank_contrasts() now reproduces the published calibration rank_contrasts() results change relative to 2.5.0. The ranking protocol it implements (Berry, "Estimand or estimator? Comparing vowel overlap measures against a known ground truth", JASA, under review, Sec. VII A) was calibrated on phontrast's method = "legacy" kernel estimator. Its sample-size floors (protocol_floors()), its 0.25 margin, its 0.99 ceiling, and its bandwidth check all hold for that estimator: the self-normalized sample-point construction that evaluates both categories' densities at the pooled tokens, with no leave-one-out correction. 2.5.0 computed sqrt_jsd, shared_mass, and the bandwidth check with the Monte-Carlo estimator (method = "mc", partial leave-one-out) and a per-category Scott bandwidth instead, so its numbers were not on the scale the thresholds were set on. On the paper's 45 Peterson--Barney F1 x F2 vowel pairs, 2.5.0 put 20 pairs at the ceiling instead of 23, set no pair aside instead of ɝ--u (rank shift 0.080 against the paper's 0.273), and read sqrt_jsd up to 0.11 higher on overlapping pairs (ɑ--ɔ: 0.730 against the published 0.627). 2.5.1 reproduces the paper's per-pair values to floating-point precision: 45 pairs, 23 at the ceiling, 22 ranked, none flagged, ɝ--u set aside. Pillai is unchanged. - recommended_estimator() returns three new elements: method = "legacy", eval_on = "pooled", and bracket_bw = "scott.pooled", at every dimensionality. loo is now FALSE throughout, because the study applied no leave-one-out correction (legacy has none); the earlier TRUE described a correction that the calibrated estimator never applied. - rank_contrasts() routes the reported sqrt_jsd and shared_mass, and the halved and doubled scott.diag bracket of the bandwidth check, through estimator$method. Under bracket_bw = "scott.pooled" the bracket uses one diagonal Scott bandwidth selected on the pooled tokens of the pair for both categories, as the study did; bracket_bw = "scott.diag" selects it per category, as 2.5.0 did. The estimator checker validates the new elements; a list without them, such as one built by hand for 2.5.0, takes the calibrated values. To recompute a 2.5.0 ranking, pass modifyList(recommended_estimator(d), list(method = "mc", loo = TRUE, bracket_bw = "scott.diag")). - print() on a ranking names the estimator method and says when the bracket is pooled; inspect_contrast() draws and annotates its panels with the ranking's own estimator and bracket bandwidth, so the x0.5 and x2 panels repeat sqrt_jsd_half and sqrt_jsd_double. - On the bundled vowel_cohort, the bandwidth check now sets aside spk09 (the planted disagreement, still flagged) and spk06; the vignette says why. - A regression test reproduces the paper's Peterson--Barney outcome and per-pair values from a vendored fixture (tests/testthat/fixtures/). Documentation - The help for method = "legacy" in jsd_kde_nd() and its siblings (estimate_jsd(), percent_overlap_kde(), estimate_overlap(), phontrast(), compare_overlap_metrics(), jsd_summary(), global_boot_jsd()) no longer calls it a pre-1.2.0 index to use only to reproduce phonJSD 1.0.0. It now says that legacy is the estimator the published protocol was validated on, and that no leave-one-out correction applies under it. Other changes - The metric annotation boxes drawn by plot_contrast() and inspect_contrast() no longer use geom_label(label.size =), which recent ggplot2 releases deprecate; they pass the linewidth aesthetic when the installed ggplot2 has it and label.size otherwise. Plots are unchanged. - The citation test no longer depends on where the text rendering wraps its lines, which a four-component development version number could trip. Changes in version 2.5.0 (2026-10-07) The ranking protocol in one call This release implements the measurement protocol recommended by the simulation study "Estimand or estimator? Comparing vowel overlap measures against a known ground truth" (Berry, under review, Sec. VII.A), so that its three steps and their conditions can be run and reported without hand-assembling them. - rank_contrasts(data, features, category_col, group_col) computes, per speaker, Jensen-Shannon distance (sqrt_jsd) and shared probability mass from one shared kernel density estimate and the Pillai trace on the same tokens (step 1); ranks the speakers on the percentile-rank scale (average rank - 1/2) / k, ties averaged, after setting measurements at the sqrt(JSD) ceiling (default 0.99) apart (step 2); and flags speakers whose Pillai and sqrt(JSD) percentile ranks differ by margin (default 0.25 of the ordering) or more (step 3). - The study's conditions are applied and reported rather than left to the user: sample-size floors per speaker from the smaller category's count (rank_licensed, flag_licensed, rank_basis; ranking licensed from 50 tokens per category at d = 2, 200 at d = 3-4, 500 at d = 5-8; the flag readable from 100 at d = 2 and 200 at d = 3-4, undefined when d >= 2n, withheld from d = 5), a warning below eight speakers, and the bandwidth check (sqrt(JSD) recomputed at half and twice the diagonal Scott bandwidth; bw_shift, sign_change, set_aside). - The result is a phontrast_ranking tibble with a print() method that summarizes the protocol, the estimator, the floors, the flagged and set-aside speakers, and the ceiling. The cleaned tokens travel with it in attr(x, "protocol"). - plot_rank_agreement() (also plot() / ggplot2::autoplot() on the ranking) draws Pillai percentile rank against sqrt(JSD) percentile rank with the identity line and the +/- margin inspection band; flagged speakers are coloured and labelled, set-aside speakers crossed, unreadable flags hollow, and ceiling speakers listed in the caption. - inspect_contrast(ranking, group) redraws one speaker with plot_contrast()'s distribution-aware layers at half, the selected, and twice the diagonal Scott bandwidth, annotating each panel with sqrt(JSD) and shared mass at that bandwidth plus the speaker's Pillai, and restating the reported ranks and the bandwidth-check outcome in the subtitle. - Helpers: percentile_rank() (the protocol's rank scale, with an exclude mask), protocol_floors(d) (the licensing floors by dimensionality), and recommended_estimator(d) (the kernel estimator settings the study used at each dimensionality: plug-in / ks at d <= 4, diagonal Scott / fast_diag with 200 evaluation tokens from d = 5, no leave-one-out from d = 14). - A bundled, simulated twelve-speaker cohort, vowel_cohort, with a planted Pillai / sqrt(JSD) disagreement, a ceiling speaker, and two under-sampled speakers; and a vignette, "Ranking speakers by Jensen-Shannon distance and checking Pillai agreement", that walks through the protocol on it. Bandwidth multiplier - New bw_scale argument on the kernel path (jsd_kde_nd(), percent_overlap_kde(), estimate_jsd(), estimate_overlap(), phontrast(), plot_contrast()): a positive multiplier on the selected bandwidth on the standard-deviation scale (h * bw_scale; bandwidth matrices H * bw_scale^2). 0.5 and 2 give the halved and doubled bandwidths of the smoothing-sensitivity check. plot_contrast() draws its regions, overlap shading, and annotations at the scaled bandwidth and records it in the caption. Ignored under density = "mvnorm". The kernel family on one shared density - phontrast() now reads Jensen-Shannon divergence and distance, proportional overlap, total variation, and the matched-kernel Bhattacharyya affinity / distance and Hellinger distance off a single density estimate per comparison, instead of separate kernel passes for JSD and overlap. Values of the existing columns are unchanged; bootstraps do half the kernel work. - New opt-in metrics for phontrast(metrics = ...): "tv" (total variation, total_variation = 1 - percent_overlap); "bhattacharyya_kde" (bhatt_kde_dist, bhatt_kde_affinity, hellinger), the same quantity as the closed-form Gaussian "bhattacharyya" columns under a matched kernel estimator, with the same partial leave-one-out self-density as JSD, so measure and estimator can be told apart; and "euclidean" (euclidean_dist), the distance between the category means after dividing each feature by the pooled two-category standard deviation (bounded by 2 * sqrt(d) for equal category sizes). The default metric set and the column order of earlier releases are unchanged; long output and bootstraps cover the new columns. Citation - citation("phontrast") now lists the study behind the protocol as a second entry (manuscript under review, with the OSF replication package), after the software citation. Changes in version 2.4.1 (2026-09-01) Cross-BLAS robustness of the proportion-standardized Pillai guard - Fixed the test failure reported by CRAN's tests-MKL additional check. The nonsingularity guard for proportion-standardized Pillai estimates decided rank deficiency by whether chol() of the within-class error SSCP threw an error, which is BLAS-dependent on (near-)singular input: reference LAPACK errors where Intel MKL can return a tiny positive pivot, letting degenerate designs slip past the guard and fail later inside summary.manova() with a less specific message. The guard now decides rank by R's tolerance-based QR of the residual matrix -- the same criterion summary.manova() applies -- so degenerate designs raise the documented "nonsingular" error on every BLAS build. Estimates on well-posed designs are unchanged. - Added a near-collinear regression test that pins this behavior. Changes in version 2.4.0 (2026-08-21) Version metadata - Corrected the package version in DESCRIPTION to 2.4.0. The original release tags contained the 2.4.0 changes but retained the 2.3.1 package metadata. - Replaced the citation that was frozen at v2.0.0 with version-aware package citation metadata and the version-specific Zenodo DOI. Added CITATION.cff so GitHub and future Zenodo deposits use the package's declared authorship. Proportion-standardized Pillai estimates - Added the opt-in proportion_standardized = TRUE mode to pillai_overlap(). It appends the plug-in and unbiased squared Mahalanobis separation estimates and Becker's proportion-standardized Pillai score while leaving the default two-field return and raw pillai value unchanged. - Negative unbiased separation estimates follow an explicitly labelled fallback path: pillai_eq is NA, pillai_eq_fallback is TRUE, and the multiplicatively corrected first term appears only as d2_fallback beside its closed-form upward-bias term. - Added definedness and fragility diagnostics for the two-category, no-covariate estimator chain, along with regression vectors, base-R MANOVA comparisons, closed-form anchors, and null Monte-Carlo acceptance tests. Changes in version 2.3.1 (2026-08-09) CRAN release - Published on CRAN for the first time on 2026-08-09. The stable release is available with install.packages("phontrast"). Proportion-standardized Pillai estimates - Added the opt-in proportion_standardized = TRUE mode to pillai_overlap(). It appends the plug-in and unbiased squared Mahalanobis separation estimates and Becker's proportion-standardized Pillai score while leaving the default two-field return and raw pillai value unchanged. - Negative unbiased separation estimates follow an explicitly labelled fallback path: pillai_eq is NA, pillai_eq_fallback is TRUE, and the multiplicatively corrected first term appears only as d2_fallback beside its closed-form upward-bias term. - Added definedness and fragility diagnostics for the two-category, no-covariate estimator chain, along with regression vectors, base-R MANOVA comparisons, closed-form anchors, and null Monte-Carlo acceptance tests. CRAN resubmission fixes - Replaced the non-running extract_mfcc() example with a fast, executable example that creates and removes a short WAV file in the R session's temporary directory. The optional tuneR dependency is guarded with requireNamespace() as recommended for packages in Suggests. - Unwrapped the short phontrast() and hier_boot_jsd_model() bootstrap examples after confirming that they run well under five seconds. Reduced the illustrative estimate_jsd() bootstrap count so that example is also under five seconds and can be unwrapped. No examples now use \dontrun{} or \donttest{}. - Removed all direct access to .GlobalEnv. Seeded KDE subsampling and multivariate-normal Monte Carlo now use a private deterministic generator, retaining reproducibility without reading, writing, or replacing the user's .Random.seed. Unseeded stochastic calls retain their previous behavior. Changes in version 2.3.0 Distribution-aware, accountable plotting - New flagship visualization plot_contrast(). Draws the same density model the distributional metrics are computed from: under density = "kde" it shows highest-density regions of each category's kernel density estimate (same bandwidth selection and ks evaluation as the metrics); under density = "mvnorm" it shows coverage ellipses of the fitted Gaussians used by the parametric backend. The pointwise minimum of the two densities -- the mass the proportional-overlap metric integrates -- is shaded directly (a ribbon in 1-D, a soft raster in 2-D, normalized across panels so fainter panels genuinely overlap less). - Accountability by default. Panels are annotated with the Jensen-Shannon divergence and proportional overlap computed by phontrast() under the plotted density model (optionally with bootstrap intervals via n_boot); the caption records the estimator configuration (backend, bandwidth, mc_n, seed, n); and the full annotation table is attached to the plot as attr(p, "contrast_metrics"). - Results plot themselves. phontrast() output now carries class "phontrast_contrast", so plot() and ggplot2::autoplot() draw the metric comparison directly via plot_overlap_metrics(). - A shared visual identity. New exported theme_phontrast(), scale_colour_phontrast() / scale_color_phontrast() / scale_fill_phontrast(), and phontrast_palette() (a colorblind-safe Okabe-Ito palette ordered so the leading pair maximizes contrast for two-category plots). All plotting functions -- including the existing plot_overlap_metrics(), plot_category_space(), and plot_category_pca(), whose arguments and behavior are otherwise unchanged -- now share this theme and palette. - Plotting remains optional: ggplot2 stays in Suggests. Changes in version 2.2.0 Pluggable density backend for the distributional metrics - Added a density argument to decouple the density estimator from the metric. The distributional metrics (Jensen-Shannon divergence and proportional overlap) previously always used kernel density estimation. They now accept density = "kde" (the default, unchanged) or density = "mvnorm", which fits one multivariate normal per category and estimates the metric between the two Gaussians. This lets the estimator behind JSD and overlap be matched to the same multivariate-normal assumptions the Pillai, Bhattacharyya, and Mahalanobis metrics already make, and makes the metric x estimator interaction a controlled choice rather than hard-wired to KDE. - Jensen-Shannon divergence between two Gaussians has no closed form (the mixture is a Gaussian mixture), so the "mvnorm" backend estimates it by fresh-sample Monte-Carlo: it draws mc_n points (default 10000) from each fitted Gaussian and averages the log density ratio. The estimand is the JSD / overlap between the fitted Gaussians; eval_seed makes the draw reproducible without disturbing the caller's random-number stream. The Gaussian fit has no self-kernel, so the KDE-specific leave-one-out correction does not apply. - density (and mc_n) are threaded through phontrast(), compare_overlap_metrics(), estimate_jsd(), estimate_overlap(), jsd_summary(), global_boot_jsd(), jsd_kde_nd(), and percent_overlap_kde(). In phontrast() the argument affects only the Jensen-Shannon and overlap columns; the Pillai, Bhattacharyya, and Mahalanobis columns are parametric by construction and are unchanged. - The multivariate-normal log-density and sampler are implemented in base R via a Cholesky solve, adding no new package dependency. Rank-deficient category covariances are regularized with a small ridge before factorization. - Fully backward compatible: the default remains density = "kde" and all existing results are unchanged. Changes in version 2.1.0 Monte-Carlo JSD no longer floors small divergences to exactly 0 - Fixed method = "mc" (the default KDE JSD estimator) flooring small but real divergences to exactly 0. The full leave-one-out correction could collapse a category's self-density at isolated points, driving the raw plug-in mean negative, which the final clamp then floored to 0 -- while method = "legacy" still reported a nonzero contrast. The estimator now uses a partial leave-one-out correction that removes a sample-size-scaled fraction n / (n + 20) of each point's own kernel: half at 20 tokens per category (the min_tokens default), approaching the full correction as the category grows. The corrected density stays strictly positive, so near-merged categories yield small positive estimates instead of exact 0. - Validated against grid-integrated JSD of the same KDEs: on 44 real speaker contrasts (20 tokens per phase, 2-D formant space) the previous estimator returned exactly 0 for 24 speakers; the corrected one returns 0 for none, tracks the grid reference more closely than either the previous default or method = "legacy", and still passes the package's grid-calibration test at n = 200 within the original tolerance. - This changes method = "mc" results relative to 2.0.x, most visibly for small divergences and small samples (estimates that were floored at 0 become small positive values; others typically shift upward slightly). method = "legacy" is unchanged, and loo = FALSE is unchanged. CRAN preparation - Replaced the relative ROADMAP.md link in the README with a plain reference, since ROADMAP.md is excluded from the built package; this resolves the "invalid file URI" flagged by the CRAN incoming checks. Changes in version 2.0.2 CRAN preparation - Kept non-package content out of the built tarball: the (untracked) local OSF/ reproducibility-data directory and the top-level ROADMAP.md are now listed in .Rbuildignore, resolving the non-portable-paths and non-standard-top-level-files NOTEs from R CMD check. Added "mel" to inst/WORDLIST. - Trimmed the estimate_jsd() example to a fast point estimate and moved the bootstrap demonstrations into \donttest{}, keeping every example under CRAN's execution-time limit. Changes in version 2.0.1 CRAN preparation - Prepared the package for CRAN submission: expanded acronyms and added a Jensen-Shannon divergence reference (Lin, 1991) to the Description, normalized non-ASCII characters in the R sources, added inst/WORDLIST and cran-comments.md, and removed the AI assistant from Authors@R (the AI-use acknowledgment remains in the README). No user-facing code changes. Changes in version 2.0.0 Package renamed: phonJSD is now phontrast - The package has been renamed from phonJSD to phontrast and reoriented around comparing multiple category contrast and separation metrics rather than Jensen-Shannon divergence alone. Update your code from library(phonJSD) to library(phontrast). Function names are unchanged except as noted below, and no metric estimates change relative to 1.2.0 -- this release is a rename and API reframe, not a numerical change. - The GitHub repository moved to https://github.com/berrygrant/phontrast (old links redirect), and the Zenodo concept DOI (10.5281/zenodo.20816585) is unchanged, so existing citations continue to resolve. New unified entry point: phontrast() - Added phontrast(), the package's headline function: compute and compare any subset of the contrast metrics -- Jensen-Shannon divergence and distance, Pillai-Bartlett trace, Bhattacharyya distance and affinity, Mahalanobis distance, and proportional overlap -- for a two-category contrast in one call, globally or by group, wide or tidy long, with optional bootstrap intervals. - The new metrics argument selects which metrics to compute (default: all), e.g. phontrast(data, features, "vowel", metrics = c("jsd", "pillai")). - compare_overlap_metrics() is deprecated in favor of phontrast(). It still works (it calls phontrast() with output = "wide") and emits a deprecation message; it will be removed in a future release. Roadmap - The next priority (P1) is a full architectural redesign around a metric registry with a uniform per-metric interface, a generalized bootstrap that works for any metric, and first-class metric orientation. See ROADMAP.md. Changes in version 1.2.0 Corrected KDE estimator (changes results) - KDE-based JSD and percent overlap now use a consistent Monte-Carlo plug-in estimator by default (method = "mc"). Each category's KDE is evaluated at that category's own observations and the true log density ratio against the mixture is averaged (with a leave-one-out bias correction, loo = TRUE). This estimates the continuous Jensen-Shannon divergence in any dimension, replacing the previous self-normalized sample-point index, which was a bounded relative separation measure rather than the JSD integral and depended on eval_on. - This changes the numbers relative to phonJSD 1.0.0. To reproduce 1.0.0 results exactly, pass method = "legacy" to jsd_kde_nd(), percent_overlap_kde(), estimate_jsd(), estimate_overlap(), jsd_summary(), global_boot_jsd(), or compare_overlap_metrics(). The eval_on control applies to method = "legacy" only. - The fast_diag engine now evaluates true (normalized) densities, matching ks::kde() to machine precision for diagonal bandwidths. API - Aligned defaults across the estimation API: min_tokens = 20 and n_boot = 1000 everywhere (previously estimate_jsd() defaulted to min_tokens = 5, and jsd_summary()/boot_jsd()/hier_boot_jsd_model() to min_tokens = 30 / n_boot = 300). - Standardized the estimate_*/global_* wrappers to return tibbles uniformly. - Documented the pillai_p_value (wide) and p_value (long) columns returned by compare_overlap_metrics(). Metrics - Added opt-in high-dimensional KDE speed controls: bw = "scott.diag", evaluation-point subsampling via eval_n/eval_seed, and engine = "fast_diag" for chunked diagonal-Gaussian KDE evaluation. - Added engine = "fast_diagonal" as an alias for engine = "fast_diag". - Extended the same KDE controls across JSD, percent overlap, and compare_overlap_metrics() so separation and overlap estimates use aligned density-estimation settings. - Exposed the same KDE controls in lower-level JSD wrappers including speaker_jsd(), boot_jsd(), jsd_summary(), and global_boot_jsd(). - Allowed grouped metric wrappers to use multiple grouping columns via group_col = c("Sex", "Style"); grouped outputs retain a single labeled group column. - Switched extract_mfcc() to tuneR::melfcc() and removed the stale seewave::mfcc() reference. Documentation - Documented the high-dimensional fast KDE path in the README and generated function manuals. - Updated the GitHub Actions checkout step to a current Node runtime action. Bug fixes - Clamped jsd() output to the mathematical range [0, 1], so floating-point rounding on near-identical categories can no longer yield NaN Jensen-Shannon distances (est_distance = TRUE) or abort hier_boot_jsd_model() through prepare_jsd_beta(). - Fixed a latent sample() edge case in hier_boot_jsd_model() that could misdraw a single numeric group identifier. Robustness - Grouped metric wrappers now keep a group as NA when its metric cannot be computed and emit a single summarizing warning, instead of the previous inconsistent behavior where speaker_pillai()/speaker_bhatt() silently dropped failed groups (so estimate_pillai() and estimate_jsd() could return different rows for the same data) while other metrics returned a silent NA. - Added input validation at the main entry points: data must be a data frame, category_col a single column name, features a non-empty character vector, and features may not overlap with category_col/group_col. Package quality - Removed the unused LazyData field (no data/ directory), the unused lme4 suggestion, and dead dplyr imports (filter, n, ungroup); added URL and BugReports; standardized the author name to "Grant M. Berry"; and scoped the CI workflow to the existing main branch. Changes in version 1.0.0 Visualization - Added ggplot2-backed plot_overlap_metrics() and plot_category_space() helpers for visualizing metric comparisons and one- or two-dimensional phonological category spaces. - Added plot_category_pca() for two-dimensional PCA diagnostics of arbitrary multidimensional feature spaces. Metrics - Added compare_overlap_metrics() to compute Pillai trace, Bhattacharyya distance and affinity, Jensen-Shannon divergence and distance, Mahalanobis distance, and percent overlap in one global or grouped comparison table. - Added one-dimensional KDE support for JSD and percent-overlap estimates. - Aligned KDE bandwidth/evaluation controls across JSD and percent overlap. - Made grouped JSD bootstrap intervals respect conf_level. - Improved empty grouped outputs and small-sample diagnostics across metric wrappers. - Made two-category metric checks ignore unused factor levels after filtering, so filtered factor data such as PB52 I/i contrasts work without manually calling droplevels(). - Clarified that percent-overlap outputs are 0--1 proportions, not 0--100 percentages. Documentation - Added quick-start and multidimensional-workflow vignettes. - Added a README quick start that starts from a vowel token table and moves to metric output. - Added metric-choice guidance and PB52 small-sample notes. - Added preferred-path and metric-direction guidance to the package manual. - Added runnable examples for the main metric comparison, JSD estimation, direct KDE JSD, beta-regression preparation, MFCC extraction, and hierarchical bootstrap modeling workflows. - Standardized JSD bootstrap outputs with conf_level, ci_lower, and ci_upper columns while retaining jsd_low and jsd_high as aliases. - Added optional bootstrapping to compare_overlap_metrics() with progress messages and metric-specific confidence intervals. Maintenance - Aligned DESCRIPTION metadata with the v1.0.0 README/DOI release. - Added explicit Author and Maintainer fields for source-package checks on current R tooling. - Removed old draft analysis scripts from the main package branch; the final LabPhon 2026 poster and reproducibility bundle live on labphon_2026. Changes in version 0.5.0 Package Quality - Added testthat coverage for the discrete JSD core, grouped classical metrics, KDE-based JSD wrappers, and grouped bootstrap summaries. - Added .Rbuildignore entries so local analysis, load-test, and manuscript artifacts are excluded from package builds. - Fixed the MIT license stub expected by R package tooling. Metrics - Centralized column validation, complete-case filtering, and two-category checks across metric functions. - Corrected discrete KL/JSD handling of zero-probability events. - Made grouped bootstrap JSD report the number of successful bootstrap replicates. - Replaced bootstrap replicate() control-flow edge cases with explicit vapply() iteration. - Removed deprecated tidyselect usage in grouped JSD summaries. Documentation - Updated generated Rd documentation for KL/JSD and bootstrap JSD outputs. - Updated README release metadata and feature summary for v0.5.0.