smartcor is available in R and in Python, and both versions have the same features. They detect the same variable types, run the same diagnostics (outer fence, Mardia, Jarque-Bera, the likelihood-ratio test of latent normality, and the linearity test), and choose the method with the same rules. Both return the estimate, confidence interval, p-value, and the reason for the choice. The numbers usually match, with a few exceptions listed at the end of this page. Use this page to translate any R example on this site into Python.
Install
The package is on PyPI:
Optional plotting support (matplotlib and seaborn):
The package needs NumPy, SciPy, pandas, and statsmodels. It does not
need R or any R package. The latent-variable estimators (polychoric,
polyserial, biserial, tetrachoric) use our own corrected copy of the
maximum-likelihood code from semopy. Mardia’s test uses the
same formulas as MVN::mardia(), and the Jarque-Bera test
uses scipy.stats.jarque_bera.
Function-by-function translation
| R | Python |
|---|---|
smart_cor(x, y) |
smart_cor(x, y) |
smart_cormat(data) |
smart_cormat(data) |
smart_cor_df(data) |
smart_cor_df(data) |
compare_methods(x, y) |
compare_methods(x, y) |
detect_outliers(x) |
detect_outliers(x) |
test_bivariate_normality(x, y) |
test_bivariate_normality(x, y) |
test_linearity(x, y) |
test_linearity(x, y) |
detect_type(x) |
detect_type(x) |
available_methods(tx, ty) |
available_methods(tx, ty) |
tidy(result) |
result.tidy() |
print(result) |
print(result) |
plot(m) /
ggcor_heatmap(m)
|
cor_heatmap(m) |
ggcor_method_heatmap(m) |
method_heatmap(m) |
alpha = 0.01 |
alpha=0.01 |
seed = 42 |
rng = 42 |
result$p.value |
result.p_value |
result$diagnostics$outliers |
result.diagnostics["outliers"] |
lin$p.value,
lin$null_hypothesis (from
test_linearity()) |
lin.p_value,
lin.null_hypothesis
|
The result classes match up as follows:
smartcor_outliers is OutlierResult,
smartcor_normtest is NormalityTestResult, and
smartcor_lintest is LinearityTestResult. The
fields have the same names, except that p.value becomes
p_value (also in the components table of a
normality test). detect_outliers() returns 1-based
positions in index in R and 0-based positions in
Python.
The tidy output has the same 24 columns in the same order in both
languages. In Python they are listed in
pysmartcor.TIDY_COLUMNS; the last four are
outlier_flag, normality_p,
latent_p, and linearity_p.
All 18 method codes are shared, including the four added in 1.2.0:
biserial, eta_squared,
kw_eta_squared, and freemans_theta. So are the
two-letter codes used in the method heatmaps (Bs,
E2, EH, Ft; tetrachoric is
Tc and Tschuprow’s T is Ts).
A worked pair, side by side
R:
library(smartcor)
gss = read.csv(system.file("extdata", "gss_2024_casestudy.csv", package = "smartcor"))
res = smart_cor(gss$coninc, gss$sex)
tidy(res)Python:
from importlib.resources import as_file, files
import pandas as pd
from pysmartcor import smart_cor
with as_file(files("pysmartcor").joinpath("data/gss_2024_casestudy.csv")) as csv:
gss = pd.read_csv(csv)
res = smart_cor(gss["coninc"], gss["sex"])
res.tidy()Both detect the same types and run the same diagnostics. Income residuals within each sex fail the Jarque-Bera test, so both choose the rank-biserial correlation and report the same estimate, interval, and p-value.
Diagnostics in Python
from pysmartcor import detect_outliers, test_bivariate_normality, test_linearity
detect_outliers(gss["coninc"])
test_bivariate_normality(gss["coninc"], gss["degree"])
lin = test_linearity(gss["coninc"], gss["age"])
lin.p_value, lin.decision
res = smart_cor(gss["coninc"], gss["sex"], alpha=0.01, verbose=False)
res.diagnostics["normality"]
res.outlier_flag, res.normality_p, res.latent_p, res.linearity_pres.diagnostics is a dictionary with the keys
outliers, normality, latent, and
linearity; a test that did not run is None.
For two continuous variables the outliers entry is a
dictionary with "x" and "y".
smart_cormat() does not keep diagnostics for
each pair, but it keeps the four summary fields, so
to_long() still includes them.
Changes in 1.2.0
from pysmartcor import smart_cormat, smart_cor_df
result = smart_cor(gss["coninc"], gss["age"], verbose=False)
print(result.linearity_test)
# Adjustment is off unless explicitly enabled.
adjusted = smart_cormat(gss[["coninc", "age", "sex"]], nearPD=True, verbose=False)
original = adjusted.original_correlations
print(adjusted.nearPD)
tables = smart_cor_df(gss[["coninc", "age", "sex"]], nearPD=True)In both languages the rules now follow Table 3 of the paper. The default method changes for eight of the ten type pairs, and the changelog lists them. Each rule starts with the Step 0 diagnostics: the outer-fence rule, Mardia’s test for two continuous variables, the Jarque-Bera test for one continuous variable, and the likelihood-ratio test of latent normality.
The linearity test runs in both directions. It orders the residuals
by the regressor, averages them at tied values, and runs Box-Pierce,
Ljung-Box, BDS, Breusch-Pagan, and RESET. For two continuous or count
variables without outliers, Spearman is chosen only when both directions
reject linearity; otherwise Pearson is kept. Each direction’s p-value is
max(min(p_bp, p_lb, p_bds), p_reset), and the combined
p_value is the larger of the two. The BDS p-value is the
smallest over the distance thresholds. The result does not depend on
argument order. If the tests are inconclusive, Pearson is kept and the
rationale says why. Passing a method skips the rule.
When the rule recommends robust inference for Pearson,
bootstrap="auto" gives a paired-bootstrap interval. The
p-value is still the standard Pearson test. Use
bootstrap=False to turn resampling off.
Both matrix functions still default to nearPD=False.
With nearPD=True, correlations holds the
adjusted matrix, while the methods, details, and to_long()
describe the original estimates. Only smart_cormat() keeps
the original matrix and the adjustment details. Remove missing or
infinite entries before adjusting.
What matches and what differs
Mardia’s test uses the formulas of MVN::mardia(),
including its small-sample correction. The Jarque-Bera test uses the
same moment estimators as moments::jarque.test(). We test
these and the linearity diagnostics against results from R, but only on
our test inputs, so we cannot promise identical numbers for every
function and dataset.
Each language uses its own random number generator. A seed makes the bootstrap and permutation results repeatable within a language, but R and Python will not give the same draws. This affects the intervals of Kruskal-Wallis , Freeman’s theta, and Theil’s U, and the permutation p-values of Freeman’s theta and Theil’s U.
The latent-variable estimates (polychoric, polyserial, biserial,
tetrachoric) can differ in the last decimals when the correlation is
close to
.
R fits rho and the thresholds together by maximum likelihood with
polycor, while Python fits rho with the thresholds fixed at
their marginal estimates.