feat(stats): correlation_matrix 按方案 A 拆成三个函数,收口返回契约(L0 9/12)
用户 2026-10-03 拍板方案 A(见决策 41),由 Codex 落地。
拆成三个函数:
- correlation_matrix -> 数值系数矩阵(对角线 1.0)
- correlation_pvalues -> 数值 p 值矩阵(对角线 NaN)
- correlation_table -> 星标字符串表(对角线 1.000 不带星)
三者共用 _check_corr_args 守卫与成对删缺失口径;correlation_table 在最外层调另外两个。
理由: 字符串表做不了后续计算(挑高相关对、喂聚类/PCA、画热力图),反过来随时能格式化;
一旦返回字符串,数值信息不可逆地丢失。
顺带修掉两处:
- 对角线 p 值由 0.0 改成 NaN —— 写 0 会被下游读成"极显著",而"自己和自己"没有 p 值
- p 值与 r 现在用同一批观测: 先成对 dropna() 再算,并同样尊重 min_periods。
此前 corr 是成对删缺失、pearsonr 不删,两者样本量不同,r 与 p 对不上
- p 值只算上三角再镜像(成对变量的 p 天然对称),省一半重复计算
tests: test_correlation_matrix_diagonal_is_one 摘掉 xfail 转正;新增 8 条
(数值性+对称性、非数值列跳过、p 值对照解析式 t = r*sqrt((n-2)/(1-r^2))、
对角线 NaN、成对删缺失口径、min_periods、对角线无星标、表格与数值输出一致)。
测试 105 passed / 38 skipped / 1 xfail -> 114 passed / 38 skipped / 0 xfail。
验证: pytest 114 passed / 38 skipped;ruff All checks passed;pyright 0 errors。
docs: STATUS.md(L0 9/12 + 决策 41 + 下一步);CHANGELOG;
personal-pitfalls 追加第 14 条(把"能看的表"和"能算的表"塞进同一个返回值);
roadmap 记录 correlation_matrix 的两个伴随函数
This commit is contained in:
1 parent
3432bf3e50
commit
95f5266563
7 files changed
+308
-71
No files matched your search
@@ -18,6 +18,8 @@ __all__ = [
|
||||
"chi_square_test",
|
||||
"confidence_interval",
|
||||
"correlation_matrix",
|
||||
"correlation_pvalues",
|
||||
"correlation_table",
|
||||
"fit_distribution",
|
||||
"goodness_of_fit",
|
||||
"t_test",
|
||||
@@ -34,12 +36,28 @@ def _pvalue(result: Any) -> float:
|
||||
return float(result.pvalue)
|
||||
|
||||
|
||||
_VALID_CORR_METHODS = ("pearson", "kendall", "spearman")
|
||||
|
||||
|
||||
def _check_corr_args(data: Any, method: str) -> pd.DataFrame:
|
||||
"""三个 `correlation_*` 共用的入参守卫,返回收窄后的**数值子表**。"""
|
||||
if not isinstance(data, pd.DataFrame):
|
||||
raise TypeError(f"data必须是 pd.DataFrame,而你传的是{type(data)}")
|
||||
if method not in _VALID_CORR_METHODS:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持{list(_VALID_CORR_METHODS)}")
|
||||
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("没有找到数值列")
|
||||
# data[Index] 在 pandas 存根里是 DataFrame | Series 重载,按运行时真值收窄(坑 8)
|
||||
return cast(pd.DataFrame, data[numeric_cols])
|
||||
|
||||
|
||||
def correlation_matrix(
|
||||
data: Any,
|
||||
method: Literal["pearson", "kendall", "spearman"] = "pearson",
|
||||
min_periods: int = 1,
|
||||
) -> Any:
|
||||
"""相关矩阵 + 显著性星标。
|
||||
) -> pd.DataFrame:
|
||||
"""相关系数矩阵(**数值**,不带星标)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -49,12 +67,12 @@ def correlation_matrix(
|
||||
`"pearson"` / `"kendall"` / `"spearman"`。
|
||||
min_periods:
|
||||
每对变量至少要有这么多个**共同非缺失**的观测才给相关系数,透传给 `DataFrame.corr`。
|
||||
(pandas 对每对变量都是**成对删缺失**,不是整行删。)
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame,值 = 相关系数;`min_periods` 控制缺失值容差。
|
||||
⚠️ 当前实现返回的是**带星标的字符串表**(`1.000***` 这种),与上面这句契约不一致 ——
|
||||
口径待定,见 `STATUS.md`。
|
||||
pandas.DataFrame
|
||||
对称矩阵,对角线恒为 `1.0`;观测不足的格子是 NaN。
|
||||
|
||||
Raises
|
||||
------
|
||||
@@ -65,58 +83,121 @@ def correlation_matrix(
|
||||
|
||||
Notes
|
||||
-----
|
||||
星标读法:`***` p < 0.001,`**` p < 0.01,`*` p < 0.05,其余不带星。
|
||||
要"系数 + 显著性星标"的论文表格用 `correlation_table`;要 p 值矩阵用 `correlation_pvalues`。
|
||||
之所以拆开:这张表是**数值**,能挑高相关对、喂聚类 / PCA、画热力图;星标字符串表这些都做不了。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> correlation_matrix(df).round(3) # doctest: +SKIP
|
||||
"""
|
||||
if not isinstance(data, pd.DataFrame):
|
||||
raise TypeError(f"data必须是 pd.DataFrame,而你传的是{type(data)}")
|
||||
df_num = _check_corr_args(data, method)
|
||||
return df_num.corr(method=method, min_periods=min_periods)
|
||||
|
||||
valid_method = ["pearson", "kendall", "spearman"]
|
||||
if method not in valid_method:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持{valid_method}")
|
||||
|
||||
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("没有找到数值列")
|
||||
# data[Index] 在 pandas 存根里是 DataFrame | Series 重载,按运行时真值收窄(坑 8)
|
||||
df_num = cast(pd.DataFrame, data[numeric_cols])
|
||||
def correlation_pvalues(
|
||||
data: Any,
|
||||
method: Literal["pearson", "kendall", "spearman"] = "pearson",
|
||||
min_periods: int = 1,
|
||||
) -> pd.DataFrame:
|
||||
"""`correlation_matrix` 对应的 **p 值矩阵**(数值,不含星标)。
|
||||
|
||||
corr_matrix = df_num.corr(method=method, min_periods=min_periods)
|
||||
Parameters
|
||||
----------
|
||||
data / method / min_periods:
|
||||
含义与 `correlation_matrix` 完全一致,两者一定同形。
|
||||
|
||||
p_values = pd.DataFrame(0.0, index=corr_matrix.index, columns=corr_matrix.columns, dtype=float)
|
||||
for i in range(len(corr_matrix.columns)):
|
||||
for j in range(len(corr_matrix.columns)):
|
||||
if i == j:
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
对称矩阵。**对角线是 NaN** —— "自己和自己相关"没有 p 值可言,
|
||||
写成 `0.0` 会被下游读成"极显著"。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError / ValueError:
|
||||
同 `correlation_matrix`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
每对变量都先**成对删缺失**再算,与 `DataFrame.corr` 的口径一致 ——
|
||||
否则 r 和 p 会基于不同的样本量,对不上;成对有效观测少于 `min_periods` 时给 NaN。
|
||||
只算上三角再镜像,成对变量的 p 天然对称,省掉一半重复计算。
|
||||
"""
|
||||
df_num = _check_corr_args(data, method)
|
||||
corr = df_num.corr(method=method, min_periods=min_periods)
|
||||
|
||||
p_values = pd.DataFrame(np.nan, index=corr.index, columns=corr.columns, dtype=float)
|
||||
for i in range(len(corr.columns)):
|
||||
for j in range(i + 1, len(corr.columns)):
|
||||
pair = df_num.iloc[:, [i, j]].dropna()
|
||||
if len(pair) < min_periods:
|
||||
continue
|
||||
x = pair.iloc[:, 0]
|
||||
y = pair.iloc[:, 1]
|
||||
if method == "pearson":
|
||||
p = _pvalue(stats.pearsonr(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||
p = _pvalue(stats.pearsonr(x, y))
|
||||
elif method == "kendall":
|
||||
p = _pvalue(stats.kendalltau(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||
p = _pvalue(stats.kendalltau(x, y))
|
||||
else:
|
||||
p = _pvalue(stats.spearmanr(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||
p = _pvalue(stats.spearmanr(x, y))
|
||||
p_values.iloc[i, j] = p
|
||||
p_values.iloc[j, i] = p
|
||||
return p_values
|
||||
|
||||
def add_stars(val: float, p: float) -> str:
|
||||
# 保留三位小数
|
||||
val_str = f"{val:.3f}"
|
||||
if p < 0.001:
|
||||
return val_str + "***"
|
||||
if p < 0.01:
|
||||
return val_str + "**"
|
||||
if p < 0.05:
|
||||
return val_str + "*"
|
||||
return val_str
|
||||
|
||||
formatted_corr = corr_matrix.copy().astype(str)
|
||||
for i in range(len(corr_matrix)):
|
||||
for j in range(len(corr_matrix)):
|
||||
formatted_corr.iloc[i, j] = add_stars(corr_matrix.iloc[i, j], p_values.iloc[i, j])
|
||||
def _star(value: float, p: float) -> str:
|
||||
"""把系数与 p 值拼成 `"0.812**"`。p 为 NaN(对角线 / 观测不足)时不加星标。"""
|
||||
text = f"{value:.3f}"
|
||||
if p < 0.001:
|
||||
return text + "***"
|
||||
if p < 0.01:
|
||||
return text + "**"
|
||||
if p < 0.05:
|
||||
return text + "*"
|
||||
return text
|
||||
|
||||
return formatted_corr
|
||||
|
||||
def correlation_table(
|
||||
data: Any,
|
||||
method: Literal["pearson", "kendall", "spearman"] = "pearson",
|
||||
min_periods: int = 1,
|
||||
) -> pd.DataFrame:
|
||||
"""可直接贴论文的相关性表:**系数 + 显著性星标**。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data / method / min_periods:
|
||||
含义与 `correlation_matrix` 完全一致。
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
每个格子是字符串,形如 `"0.812**"`。
|
||||
星标:`***` p < 0.001、`**` p < 0.01、`*` p < 0.05,其余不带星。
|
||||
**对角线只显示 `1.000` 不带星标** —— 自己和自己没有显著性可言。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError / ValueError:
|
||||
同 `correlation_matrix`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
这张表**只能看**:格子里是字符串,做不了后续计算。
|
||||
要数值请用 `correlation_matrix`(系数)与 `correlation_pvalues`(p 值)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> print(correlation_table(df)) # doctest: +SKIP
|
||||
"""
|
||||
corr = correlation_matrix(data, method=method, min_periods=min_periods)
|
||||
p_values = correlation_pvalues(data, method=method, min_periods=min_periods)
|
||||
|
||||
table = corr.astype(str)
|
||||
for i in range(len(corr)):
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user