from statsmodels.compat.pandas import Appender, is_numeric_dtype
from statsmodels.compat.python import lmap, lrange
from typing import Sequence, Union
import warnings
import numpy as np
import pandas as pd
from pandas.core.dtypes.common import is_categorical_dtype
from scipy import stats
from statsmodels.iolib.table import SimpleTable
from statsmodels.stats.stattools import jarque_bera
from statsmodels.tools.decorators import cache_readonly
from statsmodels.tools.docstring import Docstring, Parameter
from statsmodels.tools.validation import (
array_like,
bool_like,
float_like,
int_like,
)
DEPRECATION_MSG = """/
``Describe`` has been deprecated in favor of ``Description`` and it's
simplified functional version, ``describe``. ``Describe`` will be removed
after 0.13.
"""
PERCENTILES = (1, 5, 10, 25, 50, 75, 90, 95, 99)
QUANTILES = np.array(PERCENTILES) / 100.0
def pd_ptp(df):
return df.max() - df.min()
def pd_percentiles(df):
return df.quantiles(QUANTILES)
PANDAS = {
"obs": lambda df: df.count(),
"mean": lambda df: df.mean(),
"std": lambda df: df.std(),
"max": lambda df: df.max(),
"min": lambda df: df.min(),
"mode": lambda df: df.mode(),
"ptp": pd_ptp,
"var": lambda df: df.var(),
"skew": lambda df: df.skewness(),
"uss": lambda df: (df ** 2).sum(),
"kurtosis": lambda df: df.kurtosis(),
"percentiles": pd_percentiles,
}
def nancount(x, axis=0):
return (1 - np.isnan(x)).sum(axis=axis)
def nanptp(arr, axis=0):
return np.nanmax(arr, axis=axis) - np.nanmin(arr, axis=axis)
def nanuss(arr, axis=0):
return np.nansum(arr ** 2, axis=axis)
def nanpercentile(arr, axis=0):
return np.nanpercentile(arr, PERCENTILES, axis=axis)
def nankurtosis(arr, axis=0):
return stats.kurtosis(arr, axis=axis, nan_policy="omit")
def nanskewness(arr, axis=0):
return stats.skew(arr, axis=axis, nan_policy="omit")
MISSING = {
"obs": nancount,
"mean": np.nanmean,
"std": np.nanstd,
"max": np.nanmax,
"min": np.nanmin,
"ptp": nanptp,
"var": np.nanvar,
"skew": nanskewness,
"uss": nanuss,
"kurtosis": nankurtosis,
"percentiles": nanpercentile,
}
def _kurtosis(a):
"""
wrapper for scipy.stats.kurtosis that returns nan instead of raising Error
missing options
"""
try:
res = stats.kurtosis(a)
except ValueError:
res = np.nan
return res
def _skew(a):
"""
wrapper for scipy.stats.skew that returns nan instead of raising Error
missing options
"""
try:
res = stats.skew(a)
except ValueError:
res = np.nan
return res
[docs]def sign_test(samp, mu0=0):
"""
Signs test
Parameters
----------
samp : array_like
1d array. The sample for which you want to perform the sign test.
mu0 : float
See Notes for the definition of the sign test. mu0 is 0 by
default, but it is common to set it to the median.
Returns
-------
M
p-value
Notes
-----
The signs test returns
M = (N(+) - N(-))/2
where N(+) is the number of values above `mu0`, N(-) is the number of
values below. Values equal to `mu0` are discarded.
The p-value for M is calculated using the binomial distribution
and can be interpreted the same as for a t-test. The test-statistic
is distributed Binom(min(N(+), N(-)), n_trials, .5) where n_trials
equals N(+) + N(-).
See Also
--------
scipy.stats.wilcoxon
"""
samp = np.asarray(samp)
pos = np.sum(samp > mu0)
neg = np.sum(samp < mu0)
M = (pos - neg) / 2.0
p = stats.binom_test(min(pos, neg), pos + neg, 0.5)
return M, p
NUMERIC_STATISTICS = (
"nobs",
"missing",
"mean",
"std_err",
"ci",
"std",
"iqr",
"iqr_normal",
"mad",
"mad_normal",
"coef_var",
"range",
"max",
"min",
"skew",
"kurtosis",
"jarque_bera",
"mode",
"median",
"percentiles",
)
CATEGORICAL_STATISTICS = ("nobs", "missing", "distinct", "top", "freq")
_additional = [
stat for stat in CATEGORICAL_STATISTICS if stat not in NUMERIC_STATISTICS
]
DEFAULT_STATISTICS = NUMERIC_STATISTICS + tuple(_additional)
[docs]class Description:
"""
Extended descriptive statistics for data
Parameters
----------
data : array_like
Data to describe. Must be convertible to a pandas DataFrame.
stats : Sequence[str], optional
Statistics to include. If not provided the full set of statistics is
computed. This list may evolve across versions to reflect best
practices. Supported options are:
"nobs", "missing", "mean", "std_err", "ci", "ci", "std", "iqr",
"iqr_normal", "mad", "mad_normal", "coef_var", "range", "max",
"min", "skew", "kurtosis", "jarque_bera", "mode", "freq",
"median", "percentiles", "distinct", "top", and "freq". See Notes for
details.
numeric : bool, default True
Whether to include numeric columns in the descriptive statistics.
categorical : bool, default True
Whether to include categorical columns in the descriptive statistics.
alpha : float, default 0.05
A number between 0 and 1 representing the size used to compute the
confidence interval, which has coverage 1 - alpha.
use_t : bool, default False
Use the Student's t distribution to construct confidence intervals.
percentiles : sequence[float]
A distinct sequence of floating point values all between 0 and 100.
The default percentiles are 1, 5, 10, 25, 50, 75, 90, 95, 99.
ntop : int, default 5
The number of top categorical labels to report. Default is
Attributes
----------
numeric_statistics
The list of supported statistics for numeric data
categorical_statistics
The list of supported statistics for categorical data
default_statistics
The default list of statistics
See Also
--------
pandas.DataFrame.describe
Basic descriptive statistics
describe
A simplified version that returns a DataFrame
Notes
-----
The selectable statistics include:
* "nobs" - Number of observations
* "missing" - Number of missing observations
* "mean" - Mean
* "std_err" - Standard Error of the mean assuming no correlation
* "ci" - Confidence interval with coverage (1 - alpha) using the normal or
t. This option creates two entries in any tables: lower_ci and upper_ci.
* "std" - Standard Deviation
* "iqr" - Interquartile range
* "iqr_normal" - Interquartile range relative to a Normal
* "mad" - Mean absolute deviation
* "mad_normal" - Mean absolute deviation relative to a Normal
* "coef_var" - Coefficient of variation
* "range" - Range between the maximum and the minimum
* "max" - The maximum
* "min" - The minimum
* "skew" - The skewness defined as the standardized 3rd central moment
* "kurtosis" - The kurtosis defined as the standardized 4th central moment
* "jarque_bera" - The Jarque-Bera test statistic for normality based on
the skewness and kurtosis. This option creates two entries, jarque_bera
and jarque_beta_pval.
* "mode" - The mode of the data. This option creates two entries in all tables,
mode and mode_freq which is the empirical frequency of the modal value.
* "median" - The median of the data.
* "percentiles" - The percentiles. Values included depend on the input value of
``percentiles``.
* "distinct" - The number of distinct categories in a categorical.
* "top" - The mode common categories. Labeled top_n for n in 1, 2, ..., ``ntop``.
* "freq" - The frequency of the common categories. Labeled freq_n for n in 1,
2, ..., ``ntop``.
"""
_int_fmt = ["nobs", "missing", "distinct"]
numeric_statistics = NUMERIC_STATISTICS
categorical_statistics = CATEGORICAL_STATISTICS
default_statistics = DEFAULT_STATISTICS
def __init__(
self,
data: Union[np.ndarray, pd.Series, pd.DataFrame],
stats: Sequence[str] = None,
*,
numeric: bool = True,
categorical: bool = True,
alpha: float = 0.05,
use_t: bool = False,
percentiles: Sequence[Union[int, float]] = PERCENTILES,
ntop: bool = 5,
):
data_arr = data
if not isinstance(data, (pd.Series, pd.DataFrame)):
data_arr = array_like(data, "data", maxdim=2)
if data_arr.ndim == 1:
data = pd.Series(data)
numeric = bool_like(numeric, "numeric")
categorical = bool_like(categorical, "categorical")
include = []
col_types = ""
if numeric:
include.append(np.number)
col_types = "numeric"
if categorical:
include.append("category")
col_types += "and " if col_types != "" else ""
col_types += "categorical"
if not numeric and not categorical:
raise ValueError(
"At least one of numeric and categorical must be True"
)
self._data = pd.DataFrame(data).select_dtypes(include)
if self._data.shape[1] == 0:
raise ValueError(
"Selecting {col_types} results in an empty DataFrame"
)
self._is_numeric = [is_numeric_dtype(dt) for dt in self._data.dtypes]
self._is_cat_like = [
is_categorical_dtype(dt) for dt in self._data.dtypes
]
if stats is not None:
undef = [stat for stat in stats if stat not in DEFAULT_STATISTICS]
if undef:
raise ValueError(
f"{', '.join(undef)} are not known statistics"
)
self._stats = (
list(DEFAULT_STATISTICS) if stats is None else list(stats)
)
self._ntop = int_like(ntop, "ntop")
self._compute_top = "top" in self._stats
self._compute_freq = "freq" in self._stats
if self._compute_top and self._ntop <= 0 < sum(self._is_cat_like):
raise ValueError("top must be a non-negative integer")
# Expand special stats
replacements = {
"mode": ["mode", "mode_freq"],
"ci": ["upper_ci", "lower_ci"],
"jarque_bera": ["jarque_bera", "jarque_bera_pval"],
"top": [f"top_{i}" for i in range(1, self._ntop + 1)],
"freq": [f"freq_{i}" for i in range(1, self._ntop + 1)],
}
for key in replacements:
if key in self._stats:
idx = self._stats.index(key)
self._stats = (
self._stats[:idx]
+ replacements[key]
+ self._stats[idx + 1 :]
)
self._percentiles = array_like(
percentiles, "percentiles", maxdim=1, dtype="d"
)
self._percentiles = np.sort(self._percentiles)
if np.unique(self._percentiles).shape[0] != self._percentiles.shape[0]:
raise ValueError("percentiles must be distinct")
if np.any(self._percentiles >= 100) or np.any(self._percentiles <= 0):
raise ValueError("percentiles must be strictly between 0 and 100")
self._alpha = float_like(alpha, "alpha")
if not 0 < alpha < 1:
raise ValueError("alpha must be strictly between 0 and 1")
self._use_t = bool_like(use_t, "use_t")
def _reorder(self, df: pd.DataFrame) -> pd.DataFrame:
return df.loc[[s for s in self._stats if s in df.index]]
@cache_readonly
def frame(self) -> pd.DataFrame:
"""
Descriptive statistics for both numeric and categorical data
Returns
-------
DataFrame
The statistics
"""
numeric = self.numeric
categorical = self.categorical
if categorical.shape[1] == 0:
return numeric
elif numeric.shape[1] == 0:
return categorical
df = pd.concat([numeric, categorical], axis=1)
return self._reorder(df[self._data.columns])
@cache_readonly
def numeric(self) -> pd.DataFrame:
"""
Descriptive statistics for numeric data
Returns
-------
DataFrame
The statistics of the numeric columns
"""
df: pd.DataFrame = self._data.loc[:, self._is_numeric]
cols = df.columns
_, k = df.shape
std = df.std()
count = df.count()
mean = df.mean()
mad = (df - mean).abs().mean()
std_err = std.copy()
std_err.loc[count > 0] /= count.loc[count > 0]
if self._use_t:
q = stats.t(count - 1).ppf(1.0 - self._alpha / 2)
else:
q = stats.norm.ppf(1.0 - self._alpha / 2)
def _mode(ser):
mode_res = stats.mode(ser.dropna())
if mode_res[0].shape[0] > 0:
return [float(val) for val in mode_res]
return np.nan, np.nan
mode_values = df.apply(_mode).T
if mode_values.size > 0:
if isinstance(mode_values, pd.DataFrame):
# pandas 1.0 or later
mode = np.asarray(mode_values[0], dtype=float)
mode_counts = np.asarray(mode_values[1], dtype=np.int64)
else:
# pandas before 1.0 returns a Series of 2-elem list
mode = []
mode_counts = []
for idx in mode_values.index:
val = mode_values.loc[idx]
mode.append(val[0])
mode_counts.append(val[1])
mode = np.atleast_1d(mode)
mode_counts = np.atleast_1d(mode_counts)
else:
mode = mode_counts = np.empty(0)
loc = count > 0
mode_freq = np.full(mode.shape[0], np.nan)
mode_freq[loc] = mode_counts[loc] / count.loc[loc]
if df.shape[1] > 0:
iqr = df.quantile(0.75) - df.quantile(0.25)
else:
iqr = mean
jb = df.apply(
lambda x: list(jarque_bera(x.dropna())), result_type="expand"
).T
nan_mean = mean.copy()
nan_mean.loc[nan_mean == 0] = np.nan
coef_var = std / nan_mean
results = {
"nobs": pd.Series(
np.ones(k, dtype=np.int64) * df.shape[0], index=cols
),
"missing": df.shape[0] - count,
"mean": mean,
"std_err": std_err,
"upper_ci": mean + q * std_err,
"lower_ci": mean - q * std_err,
"std": std,
"iqr": iqr,
"mad": mad,
"coef_var": coef_var,
"range": pd_ptp(df),
"max": df.max(),
"min": df.min(),
"skew": jb[2],
"kurtosis": jb[3],
"iqr_normal": iqr / np.diff(stats.norm.ppf([0.25, 0.75])),
"mad_normal": mad / np.sqrt(2 / np.pi),
"jarque_bera": jb[0],
"jarque_bera_pval": jb[1],
"mode": pd.Series(mode, index=cols),
"mode_freq": pd.Series(mode_freq, index=cols),
"median": df.median(),
}
final = {k: v for k, v in results.items() if k in self._stats}
results_df = pd.DataFrame(
list(final.values()), columns=cols, index=list(final.keys())
)
if "percentiles" not in self._stats:
return results_df
# Pandas before 1.0 cannot handle empty DF
if df.shape[1] > 0:
perc = df.quantile(self._percentiles / 100).astype(float)
else:
perc = pd.DataFrame(index=self._percentiles / 100, dtype=float)
if np.all(np.floor(100 * perc.index) == (100 * perc.index)):
perc.index = [f"{int(100 * idx)}%" for idx in perc.index]
else:
dupe = True
scale = 100
index = perc.index
while dupe:
scale *= 10
idx = np.floor(scale * perc.index)
if np.all(np.diff(idx) > 0):
dupe = False
index = np.floor(scale * index) / (scale / 100)
fmt = f"0.{len(str(scale//100))-1}f"
output = f"{{0:{fmt}}}%"
perc.index = [output.format(val) for val in index]
return self._reorder(pd.concat([results_df, perc], 0))
@cache_readonly
def categorical(self) -> pd.DataFrame:
"""
Descriptive statistics for categorical data
Returns
-------
DataFrame
The statistics of the categorical columns
"""
df = self._data.loc[:, [col for col in self._is_cat_like]]
k = df.shape[1]
cols = df.columns
vc = {col: df[col].value_counts(normalize=True) for col in df}
distinct = pd.Series(
{col: vc[col].shape[0] for col in vc}, dtype=np.int64
)
top = {}
freq = {}
for col in vc:
single = vc[col]
if single.shape[0] >= self._ntop:
top[col] = single.index[: self._ntop]
freq[col] = np.asarray(single.iloc[:5])
else:
val = list(single.index)
val += [None] * (self._ntop - len(val))
top[col] = val
freq_val = list(single)
freq_val += [np.nan] * (self._ntop - len(freq_val))
freq[col] = np.asarray(freq_val)
index = [f"top_{i}" for i in range(1, self._ntop + 1)]
top_df = pd.DataFrame(top, dtype="object", index=index, columns=cols)
index = [f"freq_{i}" for i in range(1, self._ntop + 1)]
freq_df = pd.DataFrame(freq, dtype="object", index=index, columns=cols)
results = {
"nobs": pd.Series(
np.ones(k, dtype=np.int64) * df.shape[0], index=cols
),
"missing": df.shape[0] - df.count(),
"distinct": distinct,
}
final = {k: v for k, v in results.items() if k in self._stats}
results_df = pd.DataFrame(
list(final.values()),
columns=cols,
index=list(final.keys()),
dtype="object",
)
if self._compute_top:
results_df = pd.concat([results_df, top_df], axis=0)
if self._compute_freq:
results_df = pd.concat([results_df, freq_df], axis=0)
return self._reorder(results_df)
[docs] def summary(self) -> SimpleTable:
"""
Summary table of the descriptive statistics
Returns
-------
SimpleTable
A table instance supporting export to text, csv and LaTeX
"""
df = self.frame.astype(object)
df = df.fillna("")
cols = [str(col) for col in df.columns]
stubs = [str(idx) for idx in df.index]
data = []
for _, row in df.iterrows():
data.append([v for v in row])
def _formatter(v):
if isinstance(v, str):
return v
elif v // 1 == v:
return str(int(v))
return f"{v:0.4g}"
return SimpleTable(
data,
header=cols,
stubs=stubs,
title="Descriptive Statistics",
txt_fmt={"data_fmts": {0: "%s", 1: _formatter}},
datatypes=[1] * len(data),
)
def __str__(self) -> str:
return str(self.summary().as_text())
ds = Docstring(Description.__doc__)
ds.replace_block(
"Returns", Parameter(None, "DataFrame", ["Descriptive statistics"])
)
ds.replace_block("Attributes", [])
ds.replace_block(
"See Also",
[
(
[("pandas.DataFrame.describe", None)],
["Basic descriptive statistics"],
),
(
[("Description", None)],
["Descriptive statistics class with additional output options"],
),
],
)
[docs]@Appender(str(ds))
def describe(
data: Union[np.ndarray, pd.Series, pd.DataFrame],
stats: Sequence[str] = None,
*,
numeric: bool = True,
categorical: bool = True,
alpha: float = 0.05,
use_t: bool = False,
percentiles: Sequence[Union[int, float]] = PERCENTILES,
ntop: bool = 5,
) -> pd.DataFrame:
return Description(
data,
stats,
numeric=numeric,
categorical=categorical,
alpha=alpha,
use_t=use_t,
percentiles=percentiles,
ntop=ntop,
).frame
class Describe(object):
"""
Calculates descriptive statistics for data.
.. deprecated:: 0.12
Use ``Description`` or ``describe`` instead
Defaults to a basic set of statistics, "all" can be specified, or a list
can be given.
Parameters
----------
dataset : array_like
2D dataset for descriptive statistics.
"""
def __init__(self, dataset):
warnings.warn(DEPRECATION_MSG, DeprecationWarning)
self.dataset = dataset
# better if this is initially a list to define order, or use an
# ordered dict. First position is the function
# Second position is the tuple/list of column names/numbers
# third is are the results in order of the columns
self.univariate = dict(
obs=[len, None, None],
mean=[np.mean, None, None],
std=[np.std, None, None],
min=[np.min, None, None],
max=[np.max, None, None],
ptp=[np.ptp, None, None],
var=[np.var, None, None],
mode_val=[self._mode_val, None, None],
mode_bin=[self._mode_bin, None, None],
median=[np.median, None, None],
skew=[stats.skew, None, None],
uss=[lambda x: np.sum(np.asarray(x) ** 2, axis=0), None, None],
kurtosis=[stats.kurtosis, None, None],
percentiles=[self._percentiles, None, None],
# BUG: not single value
# sign_test_M = [self.sign_test_m, None, None],
# sign_test_P = [self.sign_test_p, None, None]
)
# TODO: Basic stats for strings
# self.strings = dict(
# unique = [np.unique, None, None],
# number_uniq = [len(
# most = [
# least = [
# TODO: Multivariate
# self.multivariate = dict(
# corrcoef(x[, y, rowvar, bias]),
# cov(m[, y, rowvar, bias]),
# histogram2d(x, y[, bins, range, normed, weights])
# )
self._arraytype = None
self._columns_list = None
def _percentiles(self, x):
p = [
stats.scoreatpercentile(x, per)
for per in (1, 5, 10, 25, 50, 75, 90, 95, 99)
]
return p
def _mode_val(self, x):
return stats.mode(x)[0][0]
def _mode_bin(self, x):
return stats.mode(x)[1][0]
def _array_typer(self):
"""if not a sctructured array"""
if not (self.dataset.dtype.names):
"""homogeneous dtype array"""
self._arraytype = "homog"
elif self.dataset.dtype.names:
"""structured or rec array"""
self._arraytype = "sctruct"
else:
assert self._arraytype == "sctruct" or self._arraytype == "homog"
def _is_dtype_like(self, col):
"""
Check whether self.dataset.[col][0] behaves like a string, numbern
unknown. `numpy.lib._iotools._is_string_like`
"""
def string_like():
# TODO: not sure what the result is if the first item is some
# type of missing value
try:
self.dataset[col][0] + ""
except (TypeError, ValueError):
return False
return True
def number_like():
try:
self.dataset[col][0] + 1.0
except (TypeError, ValueError):
return False
return True
if number_like() and not string_like():
return "number"
elif not number_like() and string_like():
return "string"
else:
assert number_like() or string_like(), (
"\
Not sure of dtype"
+ str(self.dataset[col][0])
)
# @property
def summary(self, stats="basic", columns="all", orientation="auto"):
"""
Return a summary of descriptive statistics.
Parameters
----------
stats: list or str
The desired statistics, Accepts 'basic' or 'all' or a list.
'basic' = ('obs', 'mean', 'std', 'min', 'max')
'all' = ('obs', 'mean', 'std', 'min', 'max', 'ptp', 'var',
'mode', 'meadian', 'skew', 'uss', 'kurtosis',
'percentiles')
columns : list or str
The columns/variables to report the statistics, default is 'all'
If an object with named columns is given, you may specify the
column names. For example
"""
# NOTE
# standard array: Specifiy column numbers (NEED TO TEST)
# percentiles currently broken
# mode requires mode_val and mode_bin separately
if self._arraytype is None:
self._array_typer()
if stats == "basic":
stats = ("obs", "mean", "std", "min", "max")
elif stats == "all":
# stats = self.univariate.keys()
# dict does not keep an order, use full list instead
stats = [
"obs",
"mean",
"std",
"min",
"max",
"ptp",
"var",
"mode_val",
"mode_bin",
"median",
"uss",
"skew",
"kurtosis",
"percentiles",
]
else:
for astat in stats:
pass
# assert astat in self.univariate
# hack around percentiles multiple output
# bad naming
import scipy.stats
# BUG: the following has all per the same per=99
##perdict = dict(('perc_%2d'%per, [lambda x:
# scipy.stats.scoreatpercentile(x, per), None, None])
## for per in (1,5,10,25,50,75,90,95,99))
def _fun(per):
return lambda x: scipy.stats.scoreatpercentile(x, per)
perdict = dict(
("perc_%02d" % per, [_fun(per), None, None])
for per in (1, 5, 10, 25, 50, 75, 90, 95, 99)
)
if "percentiles" in stats:
self.univariate.update(perdict)
idx = stats.index("percentiles")
stats[idx : idx + 1] = sorted(perdict.keys())
# JP: this does not allow a change in sequence, sequence in stats is
# ignored
# this is just an if condition
if any(
[
aitem[1]
for aitem in self.univariate.items()
if aitem[0] in stats
]
):
if columns == "all":
self._columns_list = []
if self._arraytype == "sctruct":
self._columns_list = self.dataset.dtype.names
# self._columns_list = [col for col in
# self.dataset.dtype.names if
# (self._is_dtype_like(col)=='number')]
else:
self._columns_list = lrange(self.dataset.shape[1])
else:
self._columns_list = columns
if self._arraytype == "sctruct":
for col in self._columns_list:
assert col in self.dataset.dtype.names
else:
assert self._is_dtype_like(self.dataset) == "number"
columstypes = self.dataset.dtype
# TODO: do we need to make sure they dtype is float64 ?
for astat in stats:
calc = self.univariate[astat]
if self._arraytype == "sctruct":
calc[1] = self._columns_list
calc[2] = [
calc[0](self.dataset[col])
for col in self._columns_list
if (self._is_dtype_like(col) == "number")
]
# calc[2].append([len(np.unique(self.dataset[col])) for col
# in self._columns_list if
# self._is_dtype_like(col)=='string']
else:
calc[1] = ["Col " + str(col) for col in self._columns_list]
calc[2] = [
calc[0](self.dataset[:, col])
for col in self._columns_list
]
return self.print_summary(stats, orientation=orientation)
else:
return self.print_summary(stats, orientation=orientation)
def print_summary(self, stats, orientation="auto"):
# TODO: need to specify a table formating for the numbers, using defualt
title = "Summary Statistics"
header = stats
stubs = self.univariate["obs"][1]
data = [
[self.univariate[astat][2][col] for astat in stats]
for col in range(len(self.univariate["obs"][2]))
]
if (orientation == "varcols") or (
orientation == "auto" and len(stubs) < len(header)
):
# swap rows and columns
data = lmap(lambda *row: list(row), *data)
header, stubs = stubs, header
part_fmt = dict(data_fmts=["%#8.4g"] * (len(header) - 1))
table = SimpleTable(data, header, stubs, title=title, txt_fmt=part_fmt)
return table
@Appender(sign_test.__doc__) # i.e. module-level sign_test
def sign_test(self, samp, mu0=0):
return sign_test(samp, mu0)
# TODO: There must be a better way but formating the stats of a fuction that
# returns 2 values is a problem.
# def sign_test_m(samp,mu0=0):
# return self.sign_test(samp,mu0)[0]
# def sign_test_p(samp,mu0=0):
# return self.sign_test(samp,mu0)[1]