fix(stats): t_test/confidence_interval 收口 —— 307/308 是存根误报,CI 的夹取才是真 bug(L1 提前 2 个)
## 用户问的「t_test 307 / 308 两行报错」 - 那两行是 `float(res.statistic)` / `float(res.pvalue)`,报错来自 scipy 的 `TtestResult`: 它和 `PearsonRResult` 一样是 `_make_tuple_bunch` **运行时**拼出来的类,存根里没有这两个字段。 - 探针实测 `TtestResult(statistic=3.873, pvalue=0.0305)`,运行时取值完全正常 —— **不是真 bug**。 - 按决策 40 的路子补 `_statistic(result: Any) -> float`(与 `_pvalue` 成对),一行 `# type: ignore` 都没挂。 详情见 `docs/04-environment.md` 坑 10(已扩写为 pvalue / statistic 两条)。 ## 顺手查出来的真 bug:`confidence_interval` - 原码 `return (float(min(0.0, lower)), float(max(upper, 1.0)))`: ① `min` / `max` 的**参数写反**,下界被钉死成 `0.0`; ② **无条件**把三种 method 的区间都夹到 `[0, 1]`。 - 实测 `confidence_interval(rng.normal(170, 8, 50), method="t")` 下界是 `0.0`(正确 `171.0 ± 2.1`); `confidence=1.5` 被夹成 `(0.0, nan)` 而不报错。 - 现在:**只有 `proportion` 夹**(决策 42),`normal` / `t` 返回 `mean ± z|t·se` 不夹; `method` 非法抛 `ValueError`(§4.7)、`confidence` 走 `(0, 1)` 开区间、样本量 < 2 抛 `ValueError`。 - 入参放宽到 `list` / `tuple` / `ndarray` / `Series` / `DataFrame`: docstring 的 Examples 传 list、L0 用例传 ndarray,原来**两条都直接 `TypeError`**。 ## t_test - docstring 补齐 `Parameters` / `Returns` / `Raises` / `Notes`(§4.7); `_clean_data` 改用 `isinstance(data, pd.DataFrame)` 收窄(坑 8 的 `cast`); `alpha` 开区间校验;`df` 按 Welch–Satterthwaite / pooled / paired 分别给。 - **4 个口径未代选**(决策 39),已记进 `STATUS.md`「下一步」第 6 条待用户拍板: 单样本结论文案、`paired` 的 NaN 对齐、`value` 被静默忽略、`DataFrame` 摊平多列。 ## 测试与文档 - `tests/test_stats.py` +10 条(36 → 46),含把原先挂 skip 的 `test_confidence_interval_matches_scipy` 转正;对照一律用解析式(z 公式 / Welch df / t 公式)。 - 全量:**125 passed / 37 skipped / 0 xfail**,ruff 0,pyright 0。 - 个人易错点新增第 15 条,并把**停在 6 号**的索引表补齐到 15(只补索引,正文未动)。 - `docs/04-environment.md` 顺手修掉残留的 `[FENCE]` 代码围栏占位符与一处重复的 `---`。 - `STATUS.md`:L0 记 9/12;校正测试数为 162 条;新增决策 42 / 43。
This commit is contained in:
1 parent
95f5266563
commit
f95fbd2726
6 files changed
+422
-24
No files matched your search
@@ -36,7 +36,17 @@ def _pvalue(result: Any) -> float:
|
||||
return float(result.pvalue)
|
||||
|
||||
|
||||
def _statistic(result: Any) -> float:
|
||||
"""把 scipy 检验结果取成**统计量**(float)。
|
||||
|
||||
与 `_pvalue` 同一个原因:`TtestResult` 也是 `_make_tuple_bunch` 运行时拼的类,
|
||||
存根里看不到 `statistic` 字段(见 `docs/04-environment.md` 坑 10)。
|
||||
"""
|
||||
return float(result.statistic)
|
||||
|
||||
|
||||
_VALID_CORR_METHODS = ("pearson", "kendall", "spearman")
|
||||
_VALID_CONFIDENCE_METHODS = ("normal", "t", "proportion")
|
||||
|
||||
|
||||
def _check_corr_args(data: Any, method: str) -> pd.DataFrame:
|
||||
@@ -198,6 +208,8 @@ def correlation_table(
|
||||
for j in range(len(corr.columns)):
|
||||
table.iloc[i, j] = _star(corr.iloc[i, j], p_values.iloc[i, j])
|
||||
return table
|
||||
|
||||
|
||||
def confidence_interval(
|
||||
x: Any,
|
||||
confidence: float = 0.95,
|
||||
@@ -207,29 +219,171 @@ def confidence_interval(
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x:
|
||||
样本。`list` / `tuple` / `numpy.ndarray` / `pandas.Series` / `pandas.DataFrame`;
|
||||
传 DataFrame 时**所有数值列会被摊平成一个样本**,非数值列跳过。
|
||||
confidence:
|
||||
置信水平,取 `(0, 1)` 开区间,默认 0.95。
|
||||
method:
|
||||
`"normal"`:`mean ± z·s/√n`(n 大时用);
|
||||
`"t"`:小样本用 t 分布(更保守,n < 30 建议用这个);
|
||||
`"proportion"`:比例的标准误 `√(p(1-p)/n)`。
|
||||
`"proportion"`:比例的标准误 `√(p(1-p)/n)`,结果会被夹到 `[0, 1]`。
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[float, float]
|
||||
`(下限, 上限)`,双侧区间。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
`x` 不是受支持的容器类型,或里面的元素转不成数值。
|
||||
ValueError
|
||||
`x` 里没有可用数值;`confidence` 不在 `(0, 1)` 内;
|
||||
`method` 不在支持列表内;样本量不足 2(估不出标准差)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**只有 `proportion` 会把结果夹到 `[0, 1]`** —— 比例的区间不可能越界。
|
||||
`normal` / `t` 是**均值**的区间,量纲上完全可以落在 `[0, 1]` 之外
|
||||
(平均身高 170cm 的区间下界不可能是 0);夹一下只会给出**错的下界**。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> lo, hi = confidence_interval([1, 2, 3, 4, 5], confidence=0.95)
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
if not isinstance(x, (list, tuple, np.ndarray, pd.Series, pd.DataFrame)):
|
||||
raise TypeError(f"x 必须是 list / tuple / ndarray / Series / DataFrame,而你传入了{type(x)}")
|
||||
if method not in _VALID_CONFIDENCE_METHODS:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持{list(_VALID_CONFIDENCE_METHODS)}")
|
||||
if not 0 < confidence < 1:
|
||||
raise ValueError(f"confidence 必须在 (0, 1) 开区间内,而你传入了{confidence}")
|
||||
|
||||
if isinstance(x, pd.DataFrame):
|
||||
numeric_cols = x.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("x 里没有数值列")
|
||||
arr = cast(pd.DataFrame, x[numeric_cols]).to_numpy(dtype=float).ravel()
|
||||
else:
|
||||
try:
|
||||
arr = np.asarray(x, dtype=float).ravel()
|
||||
except (ValueError, TypeError) as e:
|
||||
raise TypeError(f"x 必须是数值型数据,无法转成 float 数组:{e}") from None
|
||||
|
||||
arr = arr[~np.isnan(arr)]
|
||||
n = len(arr)
|
||||
if n < 2:
|
||||
raise ValueError(f"样本量至少要有 2 个才估得出标准差,而你只有{n}个")
|
||||
|
||||
if method == "proportion":
|
||||
p = float(np.mean(arr))
|
||||
se = float(np.sqrt(p * (1 - p) / n))
|
||||
z = float(stats.norm.ppf(1 - (1 - confidence) / 2))
|
||||
return (max(0.0, p - z * se), min(1.0, p + z * se))
|
||||
|
||||
mean = float(np.mean(arr))
|
||||
se = float(np.std(arr, ddof=1)) / np.sqrt(n)
|
||||
if method == "normal":
|
||||
half = float(stats.norm.ppf(1 - (1 - confidence) / 2)) * se
|
||||
else:
|
||||
half = float(stats.t.ppf(1 - (1 - confidence) / 2, df=n - 1)) * se
|
||||
return (mean - half, mean + half)
|
||||
|
||||
def t_test(a: Any, b: Any | None = None, value: float = 0.0, equal_var: bool = False, paired: bool = False, alpha: float = 0.05) -> Any:
|
||||
"""t 检验。`b=None` 时是单样本检验。
|
||||
"""t 检验:两组均值有没有统计学差异。`b=None` 时是单样本检验。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a:
|
||||
第一组样本(`DataFrame` / `Series` / list / tuple / `ndarray`),缺失值自动剔除。
|
||||
b:
|
||||
第二组样本;`None` 表示单样本检验(对照 `value`)。
|
||||
value:
|
||||
单样本检验的原假设均值,`b is None` 时生效。
|
||||
equal_var:
|
||||
`True` 走学生 t(假设方差齐性),`False` 走 Welch(不假设,默认,更稳)。
|
||||
paired:
|
||||
配对样本检验;要求 `a` 与 `b` 等长。
|
||||
alpha:
|
||||
显著性水平,取 `(0, 1)` 开区间。
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:`{"statistic", "pvalue", "df", "significant", "conclusion"}`。
|
||||
`conclusion` 是一句能直接抄进论文的中文结论。
|
||||
dict
|
||||
`{"statistic", "pvalue", "df", "significant", "conclusion"}`。
|
||||
`conclusion` 是一句能直接抄进论文的中文结论。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
样本转不成数值数组。
|
||||
ValueError
|
||||
`alpha` 不在 `(0, 1)` 内;样本里没有数值列;样本量不足 2;
|
||||
配对检验时两组长度不一致。
|
||||
|
||||
Notes
|
||||
-----
|
||||
传 `DataFrame` 时**所有数值列会被摊平成一个样本** —— 多列不同量纲时请先自己取列。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
if not (0 < alpha < 1):
|
||||
raise ValueError(f"显著性水平 alpha 必须在 (0, 1) 之间,你传入了 {alpha}")
|
||||
def _clean_data(data: Any) -> np.ndarray:
|
||||
if isinstance(data, pd.DataFrame):
|
||||
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("没有找到数值列")
|
||||
arr = cast(pd.DataFrame, data[numeric_cols]).to_numpy(dtype=float).ravel()
|
||||
else:
|
||||
try:
|
||||
arr = np.asarray(data, dtype=float).ravel()
|
||||
except (ValueError, TypeError) as e:
|
||||
raise TypeError(f"样本必须是数值型数据,无法转成 float 数组:{e}") from None
|
||||
return arr[~np.isnan(arr)]
|
||||
|
||||
a_arr = _clean_data(a)
|
||||
|
||||
if len(a_arr) < 2:
|
||||
raise ValueError("样本 a 的数据量少于 2 个,无法进行 t 检验")
|
||||
if b is None:
|
||||
res = stats.ttest_1samp(a_arr, popmean=value)
|
||||
df = len(a_arr) - 1
|
||||
test_name = f"单样本 t 检验 (检验均值是否等于 {value})"
|
||||
else:
|
||||
b_arr = _clean_data(b)
|
||||
if paired:
|
||||
if len(a_arr) != len(b_arr):
|
||||
raise ValueError("配对 t 检验要求两组数据长度一致")
|
||||
res = stats.ttest_rel(a_arr, b_arr)
|
||||
df = len(a_arr) - 1
|
||||
test_name = "配对样本 t 检验"
|
||||
else:
|
||||
res = stats.ttest_ind(a_arr, b_arr, equal_var=equal_var)
|
||||
if equal_var:
|
||||
df = len(a_arr) + len(b_arr) - 2
|
||||
test_name = "独立双样本 t 检验 (假设方差齐性)"
|
||||
else:
|
||||
v1, v2 = np.var(a_arr, ddof=1), np.var(b_arr, ddof=1)
|
||||
n1, n2 = len(a_arr), len(b_arr)
|
||||
df = (v1/n1 + v2/n2)**2 / ((v1/n1)**2/(n1-1) + (v2/n2)**2/(n2-1))
|
||||
test_name = "Welch's 独立双样本 t 检验"
|
||||
|
||||
statistic = _statistic(res)
|
||||
pvalue = _pvalue(res)
|
||||
significant = bool(pvalue < alpha)
|
||||
|
||||
if significant:
|
||||
conclusion = f"在 {alpha} 显著性水平下,{test_name} 结果显著 (p={pvalue:.4f} < {alpha}),拒绝原假设,认为两组数据存在统计学上的显著差异。"
|
||||
else:
|
||||
conclusion = f"在 {alpha} 显著性水平下,{test_name} 结果不显著 (p={pvalue:.4f} >= {alpha}),不能拒绝原假设,认为两组数据没有统计学上的显著差异。"
|
||||
|
||||
# 6. 返回字典
|
||||
return {
|
||||
"statistic": statistic,
|
||||
"pvalue": pvalue,
|
||||
"df": float(df),
|
||||
"significant": significant,
|
||||
"conclusion": conclusion
|
||||
}
|
||||
|
||||
|
||||
def anova(*groups: Any, alpha: float = 0.05) -> Any:
|
||||
|
||||
Reference in new issue
Block a user