Standardised Statistical Comparison Workflows
Overview
Runs the applicable tests for a comparison in one call and returns standardised result tables. One-sample, two-group, multi-group, factorial and categorical workflows report parametric, rank-based and robust results side by side with effect sizes, confidence intervals and multiplicity-adjusted p-values. Supervised fits, embeddings, clustering and simulators follow the same result contracts. Methods include those of Welch (1947) doi:10.1093/biomet/34.1-2.28, Wilcoxon (1945) doi:10.2307/3001968, Mann and Whitney (1947) doi:10.1214/aoms/1177730491, Kruskal and Wallis (1952) doi:10.1080/01621459.1952.10483441, Friedman (1937) doi:10.1080/01621459.1937.10503522, Tukey (1949) doi:10.2307/3001913, Dunn (1964) doi:10.1080/00401706.1964.10490181, Yuen (1974) doi:10.1093/biomet/61.1.165, Brunner and Munzel (2000) doi:10.1002/(SICI)1521-4036(200001)42:1%3C17::AID-BIMJ17%3E3.0.CO;2-U, Algina, Keselman and Penfield (2005) doi:10.1037/1082-989X.10.3.317, DeLong, DeLong and Clarke-Pearson (1988) doi:10.2307/2531595, Sun and Xu (2014) doi:10.1109/LSP.2014.2337313, Pencina, D'Agostino, D'Agostino and Vasan (2008) doi:10.1002/sim.2929, and Pencina, D'Agostino and Steyerberg (2011) doi:10.1002/sim.4085.
Scoring the models. The five fitting functions returned a model and the README
scored it by hand, with a pROC snippet for the curve and a scatter written out
each time. predict.sa_model() already abstracts the five engines, so a layer
above it can score any of them side by side, and holding several models against
one baseline is where the metrics that need a pair — DeLong's test, the IDI and
the NRI — become available.
And the other half of the unsupervised family. The three reductions answered
where each point sits once the features have been squeezed into two dimensions,
which left the neighbouring question — which points belong together — with no
function to ask it. The four cluster_*() functions ask it, and they reuse the
reductions' row axis and input reading rather than starting a fourth of their
own, so a clustering and an embedding of the same frame are about the same rows.
Show all changes in 1.0.0Hide the rest of the changes in 1.0.0
draw_dim_reduction_plot() is what that shared axis was for: it paints the
labels one of them found onto the coordinates the other placed, beside the
grouping that was known before either ran.
And the pair of features, which nothing had asked about. Every function so far
reduced one feature at a time or every feature at once; the question of how two
of them move together had no entry point, and a correlation matrix computed by
hand had no p-value beside it and no way of being drawn.
And the summary that had a table but no picture of its own.
summarize_descriptive_stats() has reduced every feature and group to a row of
locations and spreads since the first release, and seeing one of those columns
across a set of groups meant reshaping the table and calling barplot() by hand,
with the interval either side of each mean worked out again on the way.
New features
- New
summarize_association_stats()reduces every pair of features to the association between them. Pearson, Spearman and Kendall come back side by side on the same pairs, the way a comparison reports a parametric, a rank-based and a robust test side by side, so that a linear coefficient and a monotonic one disagreeing is a fact about the data rather than about which call was made. Each method gets a slot of four features-by-features matrices:corrfromstats::cor(),pvaluefromstats::cor.test(),adj_pvalueadjusted across the pairs, andn, the observations the pair shared. Beside themdesignrecords the call. The adjustment runs over the pairs that produced a p-value rather than over every cell of the triangle. A pairstats::cor.test()refused, having fewer than three shared observations or no variance on one side, is not a test that was performed, and counting it would shrink the other pairs for a comparison that never happened. Such a pair comes backNArather than aborting the screen. The diagonal is a property of the matrix and not an estimate:corris 1 there and both p-values areNA. It is not ansa_comparison. This is a screen, the pairwise counterpart ofsummarize_descriptive_stats(), and it returns a plain list for the same reason that one returns a plain data.frame. - New
draw_corrplot()draws that matrix, and adds three decisions todraw_heatmap()rather than a second drawing engine. Nothing is standardised, a coefficient already being on a common scale. The colours are fixed at -1 to 1, so the same colour means the same strength from one plot to the next. And the two axes hold the same features, so they are clustered once and permuted together; a symmetric matrix clustered twice can come back with its rows and columns in different orders, and the diagonal then wanders off the diagonal. Given the p-values, the pairs that did not clearsig_levelare drawn as blank cells. The blanking happens after the clustering: a cell removed first would change the tree, and the picture would no longer be the matrix the reader is being shown. The diagonal is never blanked, a feature not being tested against itself, which is why the diagonal carries no p-value in the first place. The distance is1 - cor(), the same onecluster_hclust()anddraw_heatmap(dist_method = "correlation")mean, so a corrplot and a heatmap of the same features group them the same way. - New
draw_grouped_barplot()draws a column ofsummarize_descriptive_stats()as clusters of bars, one cluster per feature and one bar per group level. It is the summary counterpart ofdraw_grouped_boxplot(): a box shows the distribution a group's observations have, a bar shows one number standing for them, which is what a figure wants when the point is about a location rather than a spread. The heights are that function's own column rather than a second pass over the data, so a bar and a row of the table are the same number.errorbaris read undermainbarrather than independently of it, because only two of the summary columns are locations an interval either side says anything about. A mean takes"se","sd"and Student's"ci"atconf_level. A median takes"ci"alone, the notch intervalmedian +/- 1.58 * IQR / sqrt(n), which is the intervaldraw_grouped_boxplot()already returns asmedian_confidence_stats, so a bar and the notch of the box beside it are the same width on the same data. Every other height — a count, a spread, a shape — has no second quantity to be uncertain by and takes"none". Asking for one of the others is an error rather than a silently dropped interval, the alternative being a figure that answers a question other than the one it was asked. A bar is read against the zero it stands on, so a derivedylimalways holds zero and puts its headroom on the side the bars run to; heights that go both ways, as"skewness"does, get the baseline drawn as well. A height the summary could not compute leaves its bar blank rather than shifting the ones beside it, which is what makes a group too small for a shape estimate visible instead of absent. - New
evaluate_regression_models()scores one or more fitted regressions on the same held-out rows.$metricsreportscor,r_squared,rmse,mae,biasand the calibration line, and$comparisonsreports each model less the baseline.corandr_squaredare both there because they answer different questions:r_squaredis1 - SSE/SSTon the scored rows, so it is the variance the predictions actually removed and can be negative, whilecor^2is what that would be after rescaling by a line fitted to those same rows. The gap between them is whatcalib_slopeandcalib_interceptreport. There is no p-value beside the differences. A difference of held-out errors has no null this package is in a position to state, and the two numbers it is made of are in$metrics. - New
evaluate_classification_models()does the same for a two-class outcome.$metricsreportsaucwith a DeLong interval,brier, andaccuracy,sensitivityandspecificityat a statedthreshold;$curvesholds the ROC operating points as a rectangular table rather than a curve object.$comparisonsasks three questions of each model against the baseline rather than picking one.delta_aucasks whether the ranking improved and is tested by DeLong's paired test.idiasks how much further apart the two classes' predicted probabilities moved, which an AUC is blind to, since reordering nothing leaves it untouched.nriasks how often a probability moved the right way, counting direction only, so a model that helps many rows slightly and hurts a few badly scores well here and badly on the IDI. The class order is the fits'.outcome_lvandcontrol_labelare checked against the order the models were fitted with rather than used to change it, since a fitted classification predicts the probability of its own second level and cannot be re-pointed after the fact. - Both return the new
sa_performancecontract, a plain list ofanalysis,models,design,parameters,predictions,metrics,comparisons,curvesandmetadata. No engine object is stored: the calibration line is two numbers rather than thelmthat produced them, so the whole object writes out as JSON. Every model is scored on the rows **all** of them could predict.predict.sa_model()answersNAfor a row incomplete across that model's predictors, so models fitted on different predictor sets otherwise come back with different rows filled in, and DeLong's test, the IDI and the NRI are paired statistics with no meaning across different rows. One message reports how many rows went and why. - New
draw_prediction_plot()anddraw_roc_curve(), withplot.sa_performance()dispatching to whichever the result calls for. The scatter draws the identity line and the calibration line from the two numbers$metricsholds rather than fitting again, so the picture and the table cannot drift apart. Itstypeis"panel"past one model, because two clouds of points on shared axes make a third that belongs to neither;type = "overlay"withpoints = FALSEcompares the calibration lines alone. - The AUC, ROC, DeLong, IDI, NRI and Brier kernels are written out in the package rather than taken from
pROC.pROCcovers the first three and none of the last three, and DeLong's test has no counterpart inscikit-learnorscipyfor the Python port to call, so depending on it would buy the easy half and leave the hard half to be written twice against two sets of defaults.pROCis inSuggestsas the oracle the tests check these against. Runtime dependencies are unchanged. - New
cluster_hclust(),cluster_kmeans(),cluster_dbscan()andcluster_snn(), the second half of the unsupervised family. The reductions ask where each point sits; these ask which points belong together. There are four because they disagree about what a cluster is, and the disagreement is the information: the first two are told how many groups to find and will always find that many, while the other two are told how dense a group has to be and derive the count from that, so they can return two clusters, or nine, or none, and they can refuse to place a point.cluster_hclust()returns the tree as well as the cut, socutree($fit, k = 5)asks for another cut without rebuilding the distance matrix. It is also the only one of the four with a choice of distance, because it is the only one where the choice is real: k-means minimises squared Euclidean distance by construction and the two density methods measure a radius in it.cluster_kmeans()runsnstart = 25rather thanstats::kmeans()'s own 1, which makes the answer a search rather than a draw; the override is reported inengine.seedmakes a run repeatable and restores the random stream afterwards. - All four return the new
sa_clustercontract, a plain list ofanalysis,points,design,parameters,assignments,clusters,engine,fitandmetadata. The row axis ispoints, the same onesa_reductionuses, and the four read their input through the same helper the three reductions do — so the rows are the same rows and an assignment can be painted straight onto a set of scores.cluster_scalesits whereembedding_scaledoes and chooses the same way between clustering the samples and clustering the features. Noise is cluster0, which isdbscan's own convention.clustersnever has a row for it, sonrow(clusters)is the number of groups found anddesign$n_noiseis what did not join one; a partitioning method reportsn_noise = 0and both shapes read down the same columns. Every point being noise is a possible answer rather than an error, and it says so.assignments$silhouetteis the one number that compares across all four, since coordinates from two methods share no scale but a ratio of distances does. It is Rousseeuw's definition written out in the package rather than taken fromcluster: a singleton scores 0, noise scoresNAand takes no part in anyone else's arithmetic, and a single cluster scoresNAthroughout. cluster_dbscan()derives both of its arguments when they are not given, and says what it derived.min_ptsfollows the textbookd + 1, floored at 4 and capped at half the points, since this package's usual input is wider than it is tall and a threshold above half can only ever return one cluster.epsis a radius in the units of the data and so can have no constant default; it is taken as the 95th percentile of the k-distance curve, which reads as "assume about one point in twenty is noise".parameters$eps_sourcerecords whether the value was supplied or derived.cluster_snn()is shared nearest neighbour clustering: each point keeps itsknearest neighbours and two points are linked when their neighbour lists overlap. Itsepscounts shared neighbours wherecluster_dbscan()'s is a distance. Both engines call the argumenteps, so renaming one here would put this documentation at odds with theirs; the two say so instead.dbscanis a new dependency, the engine behind those two.draw_heatmap()now measures its dendrogram distances through the same functioncluster_hclust()does, so the tree it draws and the tree that gets cut are one tree.- New
draw_dim_reduction_plot(), whichplot()on ansa_reductionnow calls. Two coordinates of a reduction as a scatter, with a clustering of the same points taking the colours and a grouping that was known already taking the shapes. Two channels rather than one, because the question a clustering usually raises is whether it recovered a grouping nobody told it about, and one channel would answer that by hiding it: either the clustering or the grouping would have to be left out, or the two crossed into a single set of labels whose count is the product of theirs. On two channels it is read directly — one colour per shape is a clustering that found the groups, and a shape split across colours is a group the data does not see as one thing. Give one and it takes the colours on its own; give neither and the points are drawn in the foreground colour. The two objects have to be about the same points, which is checked rather than assumed: both contracts carrypointsand both read their input through the same function, so a mismatch means two different frames were reduced and clustered and is refused instead of lined up by position. Points that joined no cluster are grey rather than given a colour beside the others, since a point left out is the absence of a cluster and not one more of them, and a PCA's axes carry the share of the variance they explain while an embedding's do not.group_lvhere orders the levels and selects no rows, which is the opposite of what it does indraw_grouped_boxplot(). Dropping a row at this point would erase a point the reduction had already placed, so a levelgroupuses andgroup_lvleaves out is an error. draw_dim_reduction_plot()gainscluster_lv, one label per cluster for the legend.NULLkeeps#1,#2, and so on; noise is still listed asnoise (n)when present.draw_dim_reduction_plot()'scolnow names group levels when onlygroupis given, and its newpchargument names group shapes or, with nogroup, the points themselves. Defaults are unchanged: shapes from the built-in sequence and16on a plain scatter.- Clustering on a reduction's
$scoresnow reads sample names from itspointscolumn when the table has no row names, so a clustering ofPC1andPC2lines up with the reduction it is drawn on. make_block_cor()gainsagainst, which splits a block in two: the indices infeaturesand the indices inagainsteach correlate atcoramong themselves and at-corwith the other side. A block that shares one value has a floor on how negative that value can be —-1/(k - 1)forkpredictors, so three of them could not disagree past -0.5 and four past -0.333 — which left a strong negative correlation unavailable at the sizes worth simulating. A split block is o
The NEWS section is longer than shown here.
From NEWS.md in the release
Install
Health
- OK13 OKNow
- OK8 OK2026-09-08
- ERROR4 OK, 1 ERROR2026-09-07
- OK2 OK2026-09-06
Documentation
- Help pages
- 65 (1 internal)includes a package overview page
- Examples that run
- 100%64 run in a plain check
- Exported functions with examples
- 100%
- Documented parameters
- 80%
- Return-value docs
- 100%
- Help pages citing references
- 18%
Downloads
Repository
Stars over time
2 development-tooling and community-health practices detected across 2 families in the upstream repository
Checks run against github.com/hiows/statassist on 2026-10-11.
This repository shows signs of AI coding tools.
- Cursorfirst seen 2026-07-31Commits: at least 24 crediting it in the message
Dependencies
Installing STATassist adds 81 packages (50 with compiled code, 62 MB of source tarballs). Heaviest: stringi 11 MB, ggplot2 6.0 MB, data.table 5.7 MB.
Code & Tests
People & History
- 1.0.0Latest2026-09-05 · current release
- RR 4.6.0 released · 2026-04-24
Package metadata
- First published
- 2026-09-05
- Total releases
- 1 / 1 yrs
- License
- MIT + file LICENSE OSI
- Minimum R
- ≥ 4.1.0
- Download size
- 1.0 MB
Cite
Cite this package
Run in R for the authors' preferred citation:
citation("STATassist")This package has no citation file, so citation() returns this entry built from DESCRIPTION.
Cite the R Observatory
For a number measured here: a download total, a coverage figure, an archival date.
From data release v2026-10-17, which the citation names so these numbers can be found later. More on citing and the projects behind them.