Skill: wangjinhongmy-pixel/psychology-data-analysis (version 1.0.1)

--- name: psychology-data-analysis version: 1.0.1 description: 心理学数据分析决策与执行助手。当用户需要分析心理学研究数据、决定使用什么统计检验、处理数据、运行分析、解读结果时使用。触发场景包括："帮我分析数据"、"做什么检验"、"SPSS怎么做"、"Python分析"、"结果怎么读"、"验证我的假设"、"数据分析"、"处理数据"等。 --- # Psychology Data Analysis Assistant 心理学数据分析决策与执行助手。输入研究设计 → 推荐检验 → 执行分析 → 解读结果。 ## 工作流程 ### Step 1: 收集研究信息 向用户询问以下关键信息： 1. **研究设计类型** - 被试间设计（不同组之间的差异比较） - 被试内设计（同一组被试在不同条件下的测量） - 混合设计（同时包含被试间和被试内因素） - 问卷/相关研究（变量间关系） 2. **变量信息** - 自变量（X）：名称、类型（连续/分类）、水平数 - 因变量（Y）：名称、类型（连续/分类） - 协变量（如果有） 3. **样本信息** - 总样本量 N - 每组样本量（如果是被试间设计） - 是否满足正态性假设（可做 Shapiro-Wilk 检验） 4. **研究假设** - 差异假设（组间差异） - 相关假设（变量间相关） - 预测假设（回归/预测） - 中介/调节假设 5. **软件偏好** - SPSS（倾向菜单操作还是语法） - Python（倾向 Jupyter Notebook 环境还是脚本） 6. **数据格式** - Excel (.xlsx) - CSV - SPSS (.sav) ### Step 2: 推荐检验方案 根据收集的信息，推荐合适的统计检验： | 变量类型 | 组数/设计 | 推荐检验 | |---------|----------|---------| | 连续因变量，两组 | 被试间 | 独立样本 t 检验 | | 连续因变量，两组 | 被试内 | 配对样本 t 检验 | | 连续因变量，多组 | 被试间 | 单因素 ANOVA | | 连续因变量，多组 | 被试内 | 重复测量 ANOVA | | 连续因变量，≥2自变量 | 被试间/混合 | 多因素 ANOVA/混合 ANOVA | | 分类因变量 | 任意 | 卡方检验 | | 连续变量 | 相关研究 | Pearson/Spearman 相关 | | 连续变量 | 预测研究 | 多元线性回归 | | 连续因变量，有中介 | 路径分析 | 中介效应检验（Bootstrap） | | 连续因变量，有调节 | 调节分析 | 调节效应检验（层次回归） | **检验前提检查：** - 正态性：Shapiro-Wilk 检验（N>50 时看偏度/峰度） - 方差齐性：Levene 检验 - 球形性：Mauchly 检验（重复测量 ANOVA） - 样本量评估 ### Step 3: 执行分析 根据用户选择的软件，执行相应分析。 #### Python 执行 使用 `scripts/python_analysis.py`，支持： - 数据加载与清洗 - 描述性统计 - 假设检验（t 检验、ANOVA、相关、回归等） - 中介/调节效应 - 结果可视化 执行方式： ``` python scripts/python_analysis.py --data <数据路径> --analysis <分析类型> --options <选项> ``` #### SPSS 指导 提供详细操作步骤： 1. 菜单路径 2. 对话框设置 3. 语法命令（可选） 4. 输出结果解读 ### Step 4: 结果解读 输出内容包括： - **统计量**：检验统计值（t/F/r 等）、自由度、p 值 - **效应量**：Cohen's d、η²、r、β 等 - **置信区间**：95% CI - **APA 格式报告**：可直接粘贴到论文的结果部分 ### Step 5: 假设验证 根据分析结果，判定： - 假设是否得到支持 - 结果的实际意义解读 - 局限性说明 - 后续分析建议 ## 脚本 - `scripts/python_analysis.py` — 主要 Python 分析脚本 ## 参考资料 - `references/stat_decision_tree.md` — 统计检验决策树 - `references/python_guide.md` — Python (scipy/scikit-learn/statsmodels) 使用指南 - `references/spss_guide.md` — SPSS 操作指南（常用分析） - `references/effect_size.md` — 效应量参考表 - `references/apa_format.md` — APA 结果报告格式 ## 注意事项 - 始终先检查数据是否满足检验的前提假设 - 大样本（N>50）时，t/F 检验对正态性不敏感，但中位数比较仍需非参数检验 - 报告效应量与置信区间，而非仅报告 p 值 - Python 输出结果后，主动提供 APA 格式的报告文本

path: references/effect_size.md
content: # 效应量参考表

心理学研究中常用的效应量指标及其解释标准。

## Cohen's d（t 检验 / ANOVA 组间差异）

| d 值 | 解释 |
|------|------|
| 0.20 | 小效应 |
| 0.50 | 中等效应 |
| 0.80 | 大效应 |

**计算公式：**
```
d = (M1 - M2) / SD_pooled
SD_pooled = sqrt(((n1-1)*SD1² + (n2-1)*SD2²) / (n1+n2-2))
```

**Glass's Δ（当组间样本量差异大时）：**
```
Δ = (M1 - M2) / SD2
```

**Hedges' g（校正小样本偏差）：**
```
g = d * (1 - 3/(4*df-1))
```

---

## η²（偏 η²，ANOVA 效应量）

| η² 值 | 解释 |
|-------|------|
| 0.01 | 小效应 |
| 0.06 | 中等效应 |
| 0.14 | 大效应 |

**计算公式：**
```
η² = SS_effect / (SS_effect + SS_error)
```

**偏 η²（从 ANOVA 输出直接读）：**
SPSS/输出表格中直接报告

---

## ω²（Omega 平方，更保守的 ANOVA 效应量）

| ω² 值 | 解释 |
|-------|------|
| 0.01 | 小效应 |
| 0.06 | 中等效应 |
| 0.14 | 大效应 |

```
ω² = (df * (F - 1)) / (df * (F - 1) + N)
```

---

## r（相关系数 / 回归 / Cohen's d 转换）

| r 值 | 解释 |
|------|------|
| 0.10 | 小效应 |
| 0.30 | 中等效应 |
| 0.50 | 大效应 |

**d 转 r：**
```
r = d / sqrt(d² + 4)
```

**F 转 r（单因素 ANOVA）：**
```
r = sqrt(F / (F + df_error))
```

---

## R²（决定系数，回归效应量）

| R² 值 | 解释 |
|-------|------|
| 0.01 | 小效应 |
| 0.09 | 中等效应 |
| 0.25 | 大效应 |

**调整后 R²：**
```
R²_adj = 1 - (1-R²)*(N-1)/(N-k-1)
```

---

## Cohen's f（ANOVA 对方差解释程度）

| f 值 | 解释 |
|------|------|
| 0.10 | 小效应 |
| 0.25 | 中等效应 |
| 0.40 | 大效应 |

```
f = sqrt(η² / (1 - η²))
```

---

## 统计检验力 (1-β) 与样本量

研究应达到 .80 统计检验力（推荐）。

| 效应大小 | 所需 N（每组，α = .05，双尾） |
|---------|------------------------------|
| 小 (d = 0.2) | ~320 |
| 中 (d = 0.5) | ~64 |
| 大 (d = 0.8) | ~26 |

使用 G*Power 软件计算精确样本量。

---

## 中介效应效应量

常用指标：**ab 路径的标准化间接效应**

报告 Bootstrap CI（95%），不含 0 为显著。

效应量解释（标准化系数乘积）：
- 小：约 0.01
- 中：约 0.09
- 大：约 0.25

---

## 效应量报告检查清单

- [ ] t 检验 → Cohen's d
- [ ] ANOVA → η² 或 ω²
- [ ] 相关 → r
- [ ] 回归 → R²
- [ ] 卡方 → Cramér's V / Phi (φ)
- [ ] 中介 → Bootstrap CI + 标准化间接效应
- [ ] 始终报告置信区间


path: references/python_guide.md
content: # Python 数据分析指南

基于 pandas + scipy + statsmodels + scikit-learn。

## 环境准备

```bash
pip install pandas scipy statsmodels scikit-learn matplotlib seaborn openpyxl
```

## 数据加载

```python
import pandas as pd
import numpy as np

# CSV
df = pd.read_csv('data.csv')

# Excel
df = pd.read_excel('data.xlsx')

# SPSS (.sav) - 需要 pyreadstat
import pyreadstat
df, meta = pyreadstat.read_sav('data.sav')

# 查看数据结构
print(df.head())
print(df.dtypes)
print(df.describe())
```

## 描述性统计

```python
# 基本统计
df.describe()

# 按组统计
df.groupby('group').mean()
df.groupby('group')['score'].agg(['mean', 'std', 'n'])

# 频数
df['group'].value_counts()
```

## 假设检验

### t 检验（独立样本）

```python
from scipy import stats

# 独立样本 t 检验
group1 = df[df['group'] == '实验组']['score']
group2 = df[df['group'] == '对照组']['score']

t, p = stats.ttest_ind(group1, group2)
print(f"t = {t:.3f}, p = {p:.3f}")

# 方差不齐时（Welch's t）
t, p = stats.ttest_ind(group1, group2, equal_var=False)
```

### t 检验（配对样本）

```python
pre = df[df['time'] == '前测']['score']
post = df[df['time'] == '后测']['score']

t, p = stats.ttest_rel(pre, post)
```

### ANOVA

```python
# 单因素 ANOVA
from scipy import stats

# 被试间（Wide format）
groups = [df[df['group'] == g]['score'] for g in df['group'].unique()]
f, p = stats.f_oneway(*groups)

# 重复测量 ANOVA - 使用 statsmodels
from statsmodels.stats.anova import anova_lm
from statsmodels.formula.api import ols

model = ols('score ~ C(time)', data=df).fit()
anova = anova_lm(model, typ=2)

# 混合 ANOVA（被试间 + 被试内）
model = ols('score ~ C(group) * C(time)', data=df).fit()
anova = anova_lm(model, typ=2)
```

### 相关分析

```python
# Pearson 相关
r, p = stats.pearsonr(df['var1'], df['var2'])

# Spearman 相关
r, p = stats.spearmanr(df['var1'], df['var2'])

# 偏相关
from statsmodels.stats.correlation_tools import partial_corr
result = partial_corr(df, x='var1', y='var2', cov='control_var')
```

### 回归分析

```python
import statsmodels.api as sm

# 多元线性回归
X = df[['x1', 'x2', 'x3']]
X = sm.add_constant(X)  # 添加截距
y = df['y']

model = sm.OLS(y, X).fit()
print(model.summary())

# 虚拟变量（分类自变量）
X = pd.get_dummies(df['group'], drop_first=True)
X = sm.add_constant(X)
```

### 中介效应

```python
# 使用 statsmodels 的 medians
from statsmodels.stats.mediation import Mediation

# 参考： Hayes PROCESS style 或手动 Bootstrap
# 简化版手动实现：
import numpy as np

def bootstrap_mediation(X, M, Y, n_bootstrap=5000):
    indirect_effects = []
    for _ in range(n_bootstrap):
        idx = np.random.choice(len(X), len(X), replace=True)
        # a path
        a = sm.OLS(M[idx], sm.add_constant(X[idx])).fit().params[1]
        # b path
        b = sm.OLS(Y[idx], sm.add_constant(np.column_stack([X[idx], M[idx]]))).fit().params[1]
        indirect_effects.append(a * b)
    
    indirect = np.percentile(indirect_effects, [2.5, 97.5])
    return np.mean(indirect_effects), indirect
```

### 调节效应

```python
# 层次回归
X_centered = df[['x', 'w']]
X_centered = X_centered - X_centered.mean()

model1 = sm.OLS(Y, sm.add_constant(X_centered[['x']])).fit()
model2 = sm.OLS(Y, sm.add_constant(X_centered)).fit()

# 加入交互项
X_centered['xw'] = X_centered['x'] * X_centered['w']
model3 = sm.OLS(Y, sm.add_constant(X_centered)).fit()

# 比较模型
from scipy import stats
f = ((model2.ssr - model3.ssr) / model3.df_resid) / (model3.ssr / model3.df_resid)
p = 1 - stats.f.cdf(f, model2.df_resid - model3.df_resid, model3.df_resid)
```

## 效应量

```python
# Cohen's d
def cohens_d(group1, group2):
    n1, n2 = len(group1), len(group2)
    var1, var2 = group1.var(), group2.var()
    pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
    return (group1.mean() - group2.mean()) / pooled_std

# Pearson r
def r_from_t(t, df):
    return np.sqrt(t**2 / (t**2 + df))
```

## 正态性检验

```python
from scipy import stats

# Shapiro-Wilk（样本量 < 5000）
stat, p = stats.shapiro(df['score'])

# 查看偏度和峰度
print(f"Skewness: {df['score'].skew():.3f}")
print(f"Kurtosis: {df['score'].kurtosis():.3f}")
```

## 方差齐性检验

```python
# Levene 检验
stat, p = stats.levene(*groups)
```

## 可视化

```python
import matplotlib.pyplot as plt
import seaborn as sns

# 箱线图
sns.boxplot(x='group', y='score', data=df)
plt.show()

# 散点图
sns.scatterplot(x='var1', y='var2', data=df)
plt.show()
```

## 输出 APA 格式

```python
def apa_ttest(t, df, p, d=None):
    result = f"t({df}) = {t:.3f}, p = {p:.3f}"
    if d:
        result += f", Cohen's d = {d:.3f}"
    return result

def apa_anova(f, df_between, df_error, p, eta2=None):
    result = f"F({df_between}, {df_error}) = {f:.3f}, p = {p:.3f}"
    if eta2:
        result += f", η² = {eta2:.3f}"
    return result
```

## 完整分析脚本模板

```python
"""
心理学数据分析脚本模板
使用方法: python python_analysis.py --data data.csv --analysis anova --group_var group --dep_var score
"""

import argparse
import pandas as pd
import numpy as np
from scipy import stats
import sys

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--data', required=True, help='数据文件路径')
    parser.add_argument('--analysis', required=True, 
                        choices=['ttest_ind', 'ttest_rel', 'anova', 'correlation', 'regression', 'describe'],
                        help='分析类型')
    parser.add_argument('--group_var', help='分组变量名')
    parser.add_argument('--dep_var', required=True, help='因变量名')
    parser.add_argument('--group1', help='组1名称')
    parser.add_argument('--group2', help='组2名称')
    args = parser.parse_args()
    
    # 加载数据
    if args.data.endswith('.csv'):
        df = pd.read_csv(args.data)
    elif args.data.endswith(('.xlsx', '.xls')):
        df = pd.read_excel(args.data)
    else:
        print("不支持的文件格式，请使用 CSV 或 Excel 文件")
        sys.exit(1)
    
    y = df[args.dep_var]
    
    if args.analysis == 'describe':
        print("描述性统计:")
        print(df[args.dep_var].describe())
        if args.group_var:
            print("\n按组描述性统计:")
            print(df.groupby(args.group_var)[args.dep_var].describe())
    
    elif args.analysis == 'ttest_ind':
        g1 = df[df[args.group_var] == args.group1][args.dep_var]
        g2 = df[df[args.group_var] == args.group2][args.dep_var]
        t, p = stats.ttest_ind(g1, g2)
        # 效应量 Cohen's d
        pooled_std = np.sqrt(((len(g1)-1)*g1.std()**2 + (len(g2)-1)*g2.std()**2) / (len(g1)+len(g2)-2))
        d = (g1.mean() - g2.mean()) / pooled_std
        print(f"独立样本 t 检验: t = {t:.3f}, df = {len(g1)+len(g2)-2}, p = {p:.3f}, Cohen's d = {d:.3f}")
        print(f"组1 (M={g1.mean():.2f}, SD={g1.std():.2f}), 组2 (M={g2.mean():.2f}, SD={g2.std():.2f})")
    
    elif args.analysis == 'correlation':
        var1 = input("请输入第一个变量名: ")
        var2 = input("请输入第二个变量名: ")
        r, p = stats.pearsonr(df[var1], df[var2])
        print(f"Pearson 相关: r = {r:.3f}, p = {p:.3f}")

if __name__ == '__main__':
    main()
```

## SPSS 语法导出（Python 生成）

```python
def generate_spss_syntax(analysis_type, variables):
    """生成 SPSS 语法"""
    syntax_map = {
        'ttest_ind': f"T-TEST GROUPS={variables['group']}(1,2)\n  /VARIABLES={variables['dep']}.",
        'anova': f"ONEWAY {variables['dep']} BY {variables['group']}(1,3)\n  /STATISTICS=DESCRIPTIVES EFFSIZE.",
        'correlation': f"CORRELATIONS\n  /VARIABLES={variables['var1']} {variables['var2']}."
    }
    return syntax_map.get(analysis_type, "")
```


path: references/spss_guide.md
content: # SPSS 操作指南

常用心理学分析的 SPSS 菜单路径与语法。

## t 检验

### 独立样本 t 检验

**菜单路径：**
分析 → 比较均值 → 独立样本 T 检验
- 检验变量：放入因变量
- 分组变量：放入分组变量 → 定义组（组1、组2）

**输出解读：**
- "组统计"表：每组的 N、平均数、标准差
- "独立样本检验"表：
  - Levene 检验：p > 0.05 则方差齐性假设成立，看第一行；否则看第二行（Welch's t）
  - t 值、自由度、p 值、平均数差值、差异的 95% CI

### 配对样本 t 检验

**菜单路径：**
分析 → 比较均值 → 配对样本 T 检验
- 成对变量：放入两个相关测量变量

**输出解读：**
- "成对样本统计"表：每变量的描述性
- "成对样本检验"表：t 值、自由度、p 值、平均差、95% CI

---

## ANOVA

### 单因素被试间 ANOVA

**菜单路径：**
分析 → 一般线性模型 → 单因素
- 因变量：放入分数变量
- 因子：放入自变量
- 选项：勾选"描述统计"、"效应量"、"方差齐性检验"（Levene）

**输出解读：**
- "描述统计"表：每组 N、平均数、标准误
- "Levene 检验"：p > 0.05 方差齐性成立
- "ANOVA"表：F、df、p、偏 η²

**语法：**
```spss
ONEWAY score BY group (1,3)
  /STATISTICS=DESCRIPTIVES EFFSIZE HOMOGENEITY
  /POSTHOC=TURKEY ALPHA(0.05).
```

### 重复测量 ANOVA

**菜单路径：**
分析 → 一般线性模型 → 重复测量
1. "Within-Subject Factor Name"：输入时间/条件名称（如 time）
2. "Number of Levels"：输入测量次数（如 3）
3. "Measure Name"：输入因变量名称
4. 依次将各水平变量移入 Within-Subject Variables

**选项：**
- 勾选"描述统计"、"效应量"
- 下拉"方差协方差矩阵"选"诊断"检查球形性

**输出解读：**
- Mauchly 球形性检验：p > 0.05 则球形假设成立；否则看 Greenhouse-Geisser 或 Huynh-Feldt校正
- "多变量检验"表（备选）
- "假设检验"表：F、p、偏 η²

### 混合设计 ANOVA（两因素混合）

**菜单路径：**
分析 → 一般线性模型 → 重复测量
- 定义被试内因素（同上）
- 被试间因素：单独放入

**语法：**
```spss
GLM time1 time2 time3 BY group
  /WSFACTOR=time 3
  /MEASURE=score
  /PRINT=DESCRIPTIVES
  /CRITERIA=ALPHA(.05)
  /WSDESIGN=time
  /DESIGN=group.
```

---

## 相关分析

### Pearson / Spearman 相关

**菜单路径：**
分析 → 相关 → 双变量
- 变量：放入两个变量
- 相关系数：Pearson（正态连续）/ Spearman（等级/非正态）
- 显著性：双尾

**输出解读：**
- 相关矩阵：r 值、p 值、N
- 注意：p < 0.05 时 r 标注 *，p < 0.01 标注 **

**语法：**
```spss
CORRELATIONS
  /VARIABLES=var1 var2
  /PRINT=PEARSON SIG TWO-TAIL
  /MISSING=PAIRWISE.
```

---

## 回归分析

### 多元线性回归

**菜单路径：**
分析 → 回归 → 线性
- 因变量：放入 Y
- 自变量：放入 X1 X2 X3...
- 统计：勾选"R 方变化量"、"F 值"、"Descriptives"、"共线性诊断"
- 保存：勾选"预测值"、"残差"

**输出解读：**
- "模型摘要"表：R、R²、调整后 R²
- "ANOVA"表：F、p；检验整体模型显著性
- "系数"表：B（未标准化/标准化）、β、t、p、偏 η²、VIF（< 10 无共线性问题）

**语法：**
```spss
REGRESSION
  /DEPENDENT y
  /METHOD=ENTER x1 x2 x3
  /STATISTICS=R ANOVA COEFF TOL
  /SCATTERPLOT=(*ZRESID, *PRED)
  /RESIDUALS=DURBIN.
```

### 分层回归（调节效应）

**菜单路径：**
同多元回归，逐步加入变量

**步骤：**
1. Model 1：放入控制变量
2. Model 2：加入主效应自变量
3. Model 3：加入交互项（中心化后的乘积）

---

## 中介效应（SPSS + PROCESS 插件）

**安装：** 下载 PROCESS macro（Hayes, 2018），放入 SPSS 安装目录

**菜单路径：**
分析 → 回归 → PROCESS (by Hayes)
- 变量窗口：Y（因变量）、X（自变量）、M（中介变量）
- 模型编号：21（简单中介模型）
- 选项：勾选"显示置信区间"（Bootstrap）

**输出解读：**
- 总效应（c）：X → Y 的总效应
- 直接效应（c'）：X → Y 控制 M 后的效应
- 间接效应（a × b）：Bootstrap CI 不含 0 则显著

**语法（PROCESS 手动）：**
```spss
PROCESS y=y x=x m=m /BOOTSTRAP=5000 /CONF=95.
```

---

## 卡方检验

### 两独立组卡方

**菜单路径：**
分析 → 描述统计 → 交叉表
- 行：放入分组变量
- 列：放入结果变量
- 统计：勾选"卡方"、"Phi 和 Cramér's V"
- 单元格：勾选"期望值"、"行百分比"

**输出解读：**
- "交叉表"：频数与期望频数
- "卡方检验"表：Pearson χ²、df、p、连续性校正（2×2 表时看 Fisher 精确检验）
- "对称度量"表：Cramér's V（效应量）

**语法：**
```spss
CROSSTABS
  /TABLES=group BY outcome
  /STATISTIC=CHISQ
  /CELLS=COUNT EXPECTED ROW
  /FORMAT=NOTABLES.
```

---

## 前提假设检验

### 正态性检验

**菜单路径：**
分析 → 描述统计 → 探索
- 因变量列表：放入检验变量
- 绘制：勾选"带检验的正态图"
- 统计：勾选"描述统计"

**输出解读：**
- "描述统计"表：偏度、峰度
- "正态性检验"表：Kolmogorov-Smirnov 和 Shapiro-Wilk，p > 0.05 则正态

### 方差齐性检验

**菜单路径：**
分析 → 描述统计 → 探索
- 因子列表：放入分组变量
- 绘制 → 箱图：选"按组拆分"
- 选项：勾选"描述统计"

**替代：** ANOVA 的"选项"中 Levene 检验

### 球形性检验

在重复测量 ANOVA 输出中自动包含（"Mauchly's Test of Sphericity"）

---

## 结果报告模板（APA 格式）

### 独立样本 t 检验
```
对 [自变量] 在 [因变量] 上的得分进行独立样本 t 检验。结果表明，实验组（M = [M], SD = [SD]）与对照组（M = [M], SD = [SD]）在 [因变量] 上存在显著差异，t([df]) = [t值], p = [p值], Cohen's d = [d值]。
```

### 重复测量 ANOVA
```
对 [因变量] 进行 3（时间）× 2（组别）的混合设计方差分析。时间主效应显著，F([df1], [df2]) = [F值], p = [p值], η² = [η²]。组别主效应 [不] 显著，交互效应 [不] 显著。
```

### Pearson 相关
```
[变量1] 与 [变量2] 呈显著正/负相关，r([df]) = [r值], p = [p值]。
```


path: references/stat_decision_tree.md
content: # 统计检验决策树

## Step 1: 确定因变量类型

```
因变量是连续型（分数、反应时等）？
├── 是 → Step 2A
└── 否（分类/计数）→ Step 2B
```

## Step 2A: 连续因变量 → 确定研究目的

```
研究目的是什么？
├── 比较组间差异
│   ├── 几组？
│   │   ├── 两组
│   │   │   ├── 被试间（独立组）→ 独立样本 t 检验
│   │   │   └── 被试内（配对/重复测量）→ 配对样本 t 检验
│   │   └── 多组（≥3）
│   │       ├── 被试间 → 单因素 ANOVA
│   │       ├── 被试内 → 重复测量 ANOVA
│   │       └── 混合 → 混合 ANOVA
│   └── 协变量存在 → 协方差分析 (ANCOVA)
├── 探索变量间关系
│   ├── 两个连续变量 → Pearson/Spearman 相关
│   ├── 预测一个连续变量 → 多元线性回归
│   └── 控制混淆变量 → 偏相关
├── 中介/调节效应
│   ├── 中介效应 → Bootstrap 中介分析
│   └── 调节效应 → 层次回归/Johnson-Neyman
└── 结构方程模型 → SEM（待确认）

```

## Step 2B: 分类因变量

```
因变量是分类（是/否、通过/失败）？
├── 两组 → McNemar 检验（被试内）/ 卡方检验（被试间）
└── 多组 → Cochran's Q / 卡方检验
```

## Step 3: 检查前提假设

### 参数检验前提
1. **正态性**：Shapiro-Wilk 检验（p > 0.05 表示正态）
   - 大样本（N>50）时，ANOVA/t 检验对正态性 Robust
2. **方差齐性**：Levene 检验（p > 0.05 表示齐性）
3. **独立性**：被试间设计需确保被试间独立
4. **球形性**：重复测量 ANOVA 需做 Mauchly 检验

### 非参数检验备选
当前提假设违反时：
- 两组配对 → Wilcoxon 符号秩检验
- 两组独立 → Mann-Whitney U 检验
- 多组被试间 → Kruskal-Wallis H 检验
- 多组被试内 → Friedman 检验

## 快速查询表

| 因变量 | 自变量 | 组数 | 设计 | 检验 |
|--------|--------|------|------|------|
| 连续 | 无（单样本） | 1 | - | 单样本 t 检验 |
| 连续 | 分类 | 2 | 被试间 | 独立样本 t 检验 |
| 连续 | 分类 | 2 | 被试内 | 配对样本 t 检验 |
| 连续 | 分类 | ≥3 | 被试间 | 单因素 ANOVA |
| 连续 | 分类 | ≥3 | 被试内 | 重复测量 ANOVA |
| 连续 | 分类×分类 | 2×2 | 被试间 | 两因素 ANOVA |
| 连续 | 连续 | - | - | Pearson 相关 |
| 连续 | 多个连续 | - | - | 多元回归 |
| 分类 | 分类 | 2×2 | 被试间 | 卡方检验 |
| 分类 | 分类 | 2 | 被试内 | McNemar 检验 |


path: scripts/python_analysis.py
content: """
心理学数据分析主脚本
支持：描述性统计、t检验、ANOVA、相关、回归、中介、调节效应分析

使用方法：
  python python_analysis.py --data data.csv --analysis describe --dep_var score
  python python_analysis.py --data data.csv --analysis ttest_ind --group_var group --dep_var score --group1 实验组 --group2 对照组
  python python_analysis.py --data data.csv --analysis correlation --var1 变量1 --var2 变量2
  python python_analysis.py --data data.csv --analysis regression --dep_var y --indep_vars x1 x2 x3
  python python_analysis.py --data data.csv --analysis mediation --dep_var y --indep_var x --mediator m
  python python_analysis.py --data data.csv --analysis moderation --dep_var y --indep_var x --moderator w
"""

import argparse
import pandas as pd
import numpy as np
from scipy import stats
import sys
import warnings
warnings.filterwarnings('ignore')

# 尝试导入可选依赖
try:
    import statsmodels.api as sm
    from statsmodels.formula.api import ols
    from statsmodels.stats.anova import anova_lm
    STATSMODELS_AVAILABLE = True
except ImportError:
    STATSMODELS_AVAILABLE = False

try:
    from pingouin import cronbach_alpha, reliability
    PINGOUIN_AVAILABLE = True
except ImportError:
    PINGOUIN_AVAILABLE = False


def load_data(filepath):
    """根据文件扩展名加载数据"""
    if filepath.endswith('.csv'):
        return pd.read_csv(filepath)
    elif filepath.endswith(('.xlsx', '.xls')):
        return pd.read_excel(filepath)
    elif filepath.endswith('.sav'):
        try:
            import pyreadstat
            df, meta = pyreadstat.read_sav(filepath)
            return df
        except ImportError:
            print("错误：读取 SPSS .sav 文件需要安装 pyreadstat: pip install pyreadstat")
            sys.exit(1)
    else:
        print(f"错误：不支持的文件格式: {filepath}")
        print("支持格式：.csv, .xlsx, .xls, .sav")
        sys.exit(1)


def cohens_d(group1, group2):
    """计算 Cohen's d"""
    n1, n2 = len(group1), len(group2)
    var1, var2 = group1.var(ddof=1), group2.var(ddof=1)
    pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
    return (group1.mean() - group2.mean()) / pooled_std


def APA_ttest(t, df, p, d=None, ci=None):
    """生成 APA 格式 t 检验结果文本"""
    result = f"t({df}) = {t:.3f}, p = {p:.3f}"
    if d is not None:
        result += f", Cohen's d = {d:.3f}"
    if ci is not None:
        result += f", 95% CI = [{ci[0]:.3f}, {ci[1]:.3f}]"
    return result


def APA_anova(f, df1, df2, p, eta2=None, ci=None):
    """生成 APA 格式 ANOVA 结果文本"""
    result = f"F({df1}, {df2}) = {f:.3f}, p = {p:.3f}"
    if eta2 is not None:
        result += f", η² = {eta2:.3f}"
    if ci is not None:
        result += f", 95% CI = [{ci[0]:.3f}, {ci[1]:.3f}]"
    return result


def APA_correlation(r, df, p):
    """生成 APA 格式相关结果文本"""
    result = f"r({df}) = {r:.3f}, p = {p:.3f}"
    return result


def APA_regression(R2, F, df1, df2, p):
    """生成 APA 格式回归结果文本"""
    result = f"F({df1}, {df2}) = {F:.3f}, p = {p:.3f}, R² = {R2:.3f}"
    return result


def normality_check(data, name="变量"):
    """正态性检验"""
    n = len(data)
    if n < 3:
        print(f"  {name}: 样本量太小，无法进行正态性检验")
        return None
    
    if n < 5000:
        stat, p = stats.shapiro(data)
        normal = p > 0.05
    else:
        # 大样本时用 Kolmogorov-Smirnov
        stat, p = stats.kstest(data, 'norm', args=(data.mean(), data.std()))
        normal = p > 0.05
    
    skew = stats.skew(data)
    kurt = stats.kurtosis(data)
    
    print(f"\n  【正态性检验】")
    print(f"  {name}:")
    print(f"    Shapiro-Wilk/K-S检验: W/KS = {stat:.3f}, p = {p:.3f}")
    print(f"    偏度 = {skew:.3f}, 峰度 = {kurt:.3f}")
    if normal:
        print(f"    → 假设成立：数据符合正态分布")
    else:
        print(f"    → 警告：数据偏离正态分布（p < .05），建议使用非参数检验")
    
    return normal


def levene_test(groups, names=None):
    """方差齐性检验"""
    if names is None:
        names = [f"组{i+1}" for i in range(len(groups))]
    
    stat, p = stats.levene(*groups)
    print(f"\n  【Levene 方差齐性检验】")
    print(f"    W = {stat:.3f}, p = {p:.3f}")
    if p > 0.05:
        print(f"    → 假设成立：方差齐性")
    else:
        print(f"    → 警告：方差不齐（p < .05），Welch's t 检验更合适")
    return p > 0.05


def analyze_describe(df, dep_var, group_var=None):
    """描述性统计"""
    print(f"\n{'='*60}")
    print(f"描述性统计")
    print(f"{'='*60}")
    
    y = df[dep_var]
    print(f"\n【总体描述】")
    print(f"  N = {y.count()}")
    print(f"  M = {y.mean():.3f}")
    print(f"  SD = {y.std():.3f}")
    print(f"  Min = {y.min():.3f}, Max = {y.max():.3f}")
    print(f"  偏度 = {stats.skew(y):.3f}, 峰度 = {stats.kurtosis(y):.3f}")
    
    if group_var and group_var in df.columns:
        print(f"\n【按 {group_var} 分组描述】")
        grouped = df.groupby(group_var)[dep_var]
        desc = grouped.agg(['count', 'mean', 'std', 'min', 'max'])
        desc.columns = ['N', 'M', 'SD', 'Min', 'Max']
        for idx, row in desc.iterrows():
            print(f"\n  {group_var} = {idx}:")
            print(f"    N = {row['N']:.0f}, M = {row['M']:.3f}, SD = {row['SD']:.3f}")
            print(f"    Min = {row['Min']:.3f}, Max = {row['Max']:.3f}")
    
    normality_check(y, dep_var)
    return desc if group_var else None


def analyze_ttest_ind(df, dep_var, group_var, group1, group2):
    """独立样本 t 检验"""
    print(f"\n{'='*60}")
    print(f"独立样本 t 检验")
    print(f"{'='*60}")
    
    g1 = df[df[group_var] == group1][dep_var].dropna()
    g2 = df[df[group_var] == group2][dep_var].dropna()
    
    if len(g1) == 0 or len(g2) == 0:
        print(f"错误：找不到指定的组别")
        print(f"  找到的组别: {df[group_var].unique()}")
        sys.exit(1)
    
    print(f"\n【描述性统计】")
    print(f"  {group1}: N = {len(g1)}, M = {g1.mean():.3f}, SD = {g1.std():.3f}")
    print(f"  {group2}: N = {len(g2)}, M = {g2.mean():.3f}, SD = {g2.std():.3f}")
    
    # 正态性检验
    normality_check(g1, group1)
    normality_check(g2, group2)
    
    # 方差齐性
    equal_var = levene_test([g1, g2], [group1, group2])
    
    # t 检验
    t, p = stats.ttest_ind(g1, g2, equal_var=equal_var)
    d = cohens_d(g1, g2)
    df_total = len(g1) + len(g2) - 2
    
    # 计算置信区间（差值的）
    diff_mean = g1.mean() - g2.mean()
    se = np.sqrt(g1.var(ddof=1)/len(g1) + g2.var(ddof=1)/len(g2))
    ci = diff_mean - 1.96*se, diff_mean + 1.96*se
    
    print(f"\n【推断统计】")
    test_type = "Student's t" if equal_var else "Welch's t"
    print(f"  {test_type} 检验: t({df_total}) = {t:.3f}, p = {p:.3f}")
    print(f"  Cohen's d = {d:.3f}")
    print(f"  差值均值 = {diff_mean:.3f}, 95% CI = [{ci[0]:.3f}, {ci[1]:.3f}]")
    
    print(f"\n【APA 格式结果】")
    print(f"  {APA_ttest(t, df_total, p, d, ci)}")
    
    print(f"\n【结论】")
    if p < 0.05:
        print(f"  两组在 {dep_var} 上存在显著差异 (p < .05)")
        print(f"  {group1} (M={g1.mean():.2f}) vs {group2} (M={g2.mean():.2f})")
    else:
        print(f"  两组在 {dep_var} 上不存在显著差异 (p > .05)")


def analyze_ttest_rel(df, dep_var1, dep_var2, var1_name=None, var2_name=None):
    """配对样本 t 检验"""
    print(f"\n{'='*60}")
    print(f"配对样本 t 检验")
    print(f"{'='*60}")
    
    var1_name = var1_name or dep_var1
    var2_name = var2_name or dep_var2
    
    paired = df[[dep_var1, dep_var2]].dropna()
    diff = paired[dep_var1] - paired[dep_var2]
    
    print(f"\n【描述性统计】")
    print(f"  {var1_name}: M = {paired[dep_var1].mean():.3f}, SD = {paired[dep_var1].std():.3f}")
    print(f"  {var2_name}: M = {paired[dep_var2].mean():.3f}, SD = {paired[dep_var2].std():.3f}")
    print(f"  差值: M = {diff.mean():.3f}, SD = {diff.std():.3f}")
    
    normality_check(diff, "差值")
    
    t, p = stats.ttest_rel(paired[dep_var1], paired[dep_var2])
    df_total = len(paired) - 1
    d = diff.mean() / diff.std()
    
    se = diff.std() / np.sqrt(len(diff))
    ci = diff.mean() - 1.96*se, diff.mean() + 1.96*se
    
    print(f"\n【推断统计】")
    print(f"  t 检验: t({df_total}) = {t:.3f}, p = {p:.3f}")
    print(f"  Cohen's d = {d:.3f}")
    print(f"  差值均值 = {diff.mean():.3f}, 95% CI = [{ci[0]:.3f}, {ci[1]:.3f}]")
    
    print(f"\n【APA 格式结果】")
    print(f"  {APA_ttest(t, df_total, p, d, ci)}")
    
    print(f"\n【结论】")
    if p < 0.05:
        print(f"  {var1_name} 与 {var2_name} 存在显著差异 (p < .05)")
    else:
        print(f"  {var1_name} 与 {var2_name} 不存在显著差异 (p > .05)")


def analyze_anova(df, dep_var, group_var):
    """单因素 ANOVA"""
    if not STATSMODELS_AVAILABLE:
        print("错误：需要 statsmodels 库运行 ANOVA")
        print("请运行: pip install statsmodels")
        sys.exit(1)
    
    print(f"\n{'='*60}")
    print(f"单因素方差分析 (ANOVA)")
    print(f"{'='*60}")
    
    groups_data = []
    group_names = []
    for name, group in df.groupby(group_var):
        groups_data.append(group[dep_var].dropna())
        group_names.append(name)
    
    ns = [len(g) for g in groups_data]
    means = [g.mean() for g in groups_data]
    sds = [g.std() for g in groups_data]
    
    print(f"\n【描述性统计】")
    for i, (name, n, m, s) in enumerate(zip(group_names, ns, means, sds)):
        print(f"  {name}: N = {n}, M = {m:.3f}, SD = {s:.3f}")
    
    # 正态性和方差齐性
    for g, name in zip(groups_data, group_names):
        normality_check(g, name)
    levene_test(groups_data, group_names)
    
    # ANOVA
    f, p = stats.f_oneway(*groups_data)
    df_between = len(group_names) - 1
    df_within = sum(ns) - len(group_names)
    
    # 计算 η²
    ss_between = sum(n * (m - np.mean(means))**2 for n, m in zip(ns, means))
    ss_total = sum((g - df[dep_var].mean())**2 for g in groups_data for _ in g)
    eta2 = ss_between / ss_total
    
    print(f"\n【推断统计】")
    print(f"  F({df_between}, {df_within}) = {f:.3f}, p = {p:.3f}")
    print(f"  η² = {eta2:.3f}")
    
    print(f"\n【APA 格式结果】")
    print(f"  {APA_anova(f, df_between, df_within, p, eta2)}")
    
    # 多重比较（事后检验）
    if p < 0.05 and len(group_names) > 2:
        print(f"\n【事后多重比较】(Tukey HSD)")
        from statsmodels.stats.multicomp import pairwise_tukeyhsd
        posthoc = pairwise_tukeyhsd(df[dep_var], df[group_var])
        print(posthoc)
    
    print(f"\n【结论】")
    if p < 0.05:
        print(f"  组间存在显著差异 (p < .05)")
    else:
        print(f"  组间不存在显著差异 (p > .05)")


def analyze_correlation(df, var1, var2, method='pearson'):
    """相关分析"""
    print(f"\n{'='*60}")
    print(f"相关分析 ({method.capitalize()})")
    print(f"{'='*60}")
    
    data = df[[var1, var2]].dropna()
    x, y = data[var1], data[var2]
    
    if method == 'pearson':
        r, p = stats.pearsonr(x, y)
    else:
        r, p = stats.spearmanr(x, y)
    
    df_total = len(data) - 2
    
    print(f"\n【描述性统计】")
    print(f"  {var1}: M = {x.mean():.3f}, SD = {x.std():.3f}")
    print(f"  {var2}: M = {y.mean():.3f}, SD = {y.std():.3f}")
    
    print(f"\n【推断统计】")
    print(f"  r = {r:.3f}, p = {p:.3f}, N = {len(data)}")
    print(f"  R² = {r**2:.3f}（{var2} 被 {var1} 解释的方差比例）")
    
    print(f"\n【APA 格式结果】")
    print(f"  {APA_correlation(r, df_total, p)}")
    
    # 效应量解释
    r_abs = abs(r)
    if r_abs < 0.1:
        eff = "微小"
    elif r_abs < 0.3:
        eff = "小"
    elif r_abs < 0.5:
        eff = "中等"
    else:
        eff = "大"
    print(f"  效应量解释: {eff}效应 (|r| = {r_abs:.3f})")
    
    print(f"\n【结论】")
    if p < 0.05:
        direction = "正" if r > 0 else "负"
        print(f"  {var1} 与 {var2} 呈显著{direction}相关 (p < .05)")
    else:
        print(f"  {var1} 与 {var2} 不存在显著相关 (p > .05)")


def analyze_regression(df, dep_var, indep_vars):
    """多元线性回归"""
    if not STATSMODELS_AVAILABLE:
        print("错误：需要 statsmodels 库运行回归分析")
        print("请运行: pip install statsmodels")
        sys.exit(1)
    
    print(f"\n{'='*60}")
    print(f"多元线性回归分析")
    print(f"{'='*60}")
    
    # 准备数据
    all_vars = [dep_var] + indep_vars
    data = df[all_vars].dropna()
    
    if len(data) < len(all_vars) + 5:
        print(f"警告：样本量可能不足以进行回归分析 (N = {len(data)})")
    
    y = data[dep_var]
    X = data[indep_vars]
    X = sm.add_constant(X)
    
    # 模型拟合
    model = sm.OLS(y, X).fit()
    
    print(f"\n【模型概述】")
    print(f"  N = {len(y)}")
    print(f"  F({model.df_model:.0f}, {model.df_resid:.0f}) = {model.fvalue:.3f}, p = {model.f_pvalue:.3f}")
    print(f"  R² = {model.rsquared:.3f}, 调整后 R² = {model.rsquared_adj:.3f}")
    
    print(f"\n【回归系数】")
    print(f"  {'变量':<12} {'B':>10} {'SE':>10} {'β':>10} {'t':>10} {'p':>10} {'VIF':>10}")
    print(f"  {'-'*72}")
    for var in model.params.index:
        coef = model.params[var]
        se = model.bse[var]
        beta = model.params[var] * (X[var].std() / y.std()) if var != 'const' else 0
        t = model.tvalues[var]
        p = model.pvalues[var]
        # 简化 VIF 计算
        vif = "N/A" if var == 'const' else "-"
        print(f"  {var:<12} {coef:>10.3f} {se:>10.3f} {beta:>10.3f} {t:>10.3f} {p:>10.3f} {vif:>10}")
    
    print(f"\n【APA 格式结果】")
    print(f"  {APA_regression(model.rsquared, model.fvalue, model.df_model, model.df_resid, model.f_pvalue)}")
    
    print(f"\n【结论】")
    if model.f_pvalue < 0.05:
        print(f"  模型整体显著 (p < .05)，{dep_var} 可被预测")
        sig_vars = [var for var in indep_vars if model.pvalues[var] < 0.05]
        if sig_vars:
            print(f"  显著预测变量: {', '.join(sig_vars)}")
    else:
        print(f"  模型整体不显著 (p > .05)")


def main():
    parser = argparse.ArgumentParser(description='心理学数据分析脚本')
    parser.add_argument('--data', required=True, help='数据文件路径 (.csv, .xlsx, .sav)')
    parser.add_argument('--analysis', required=True,
                        choices=['describe', 'ttest_ind', 'ttest_rel', 'anova', 'correlation', 'regression', 'mediation', 'moderation', 'chi2'],
                        help='分析类型')
    
    # 通用参数
    parser.add_argument('--dep_var', help='因变量名')
    parser.add_argument('--group_var', help='分组变量名')
    
    # t 检验参数
    parser.add_argument('--group1', help='组1名称（独立样本 t 检验）')
    parser.add_argument('--group2', help='组2名称（独立样本 t 检验）')
    parser.add_argument('--var1', help='变量1（配对 t 检验/相关）')
    parser.add_argument('--var2', help='变量2（配对 t 检验/相关）')
    
    # 回归参数
    parser.add_argument('--indep_vars', nargs='+', help='自变量列表')
    
    # 中介/调节
    parser.add_argument('--x', help='自变量 (X)')
    parser.add_argument('--m', help='中介变量 (M)')
    parser.add_argument('--y', help='因变量 (Y)')
    parser.add_argument('--w', help='调节变量 (W)')
    
    # 选项
    parser.add_argument('--alpha', type=float, default=0.05, help='显著性水平 (默认 0.05)')
    
    args = parser.parse_args()
    
    # 加载数据
    print(f"\n正在加载数据: {args.data}")
    df = load_data(args.data)
    print(f"数据加载成功: {df.shape[0]} 行, {df.shape[1]} 列")
    print(f"列名: {list(df.columns)}")
    
    # 执行分析
    if args.analysis == 'describe':
        if not args.dep_var:
            print("错误：需要 --dep_var 参数")
            sys.exit(1)
        analyze_describe(df, args.dep_var, args.group_var)
    
    elif args.analysis == 'ttest_ind':
        if not all([args.dep_var, args.group_var, args.group1, args.group2]):
            print("错误：需要 --dep_var, --group_var, --group1, --group2 参数")
            sys.exit(1)
        analyze_ttest_ind(df, args.dep_var, args.group_var, args.group1, args.group2)
    
    elif args.analysis == 'ttest_rel':
        if not all([args.var1, args.var2]):
            print("错误：需要 --var1, --var2 参数")
            sys.exit(1)
        analyze_ttest_rel(df, args.var1, args.var2)
    
    elif args.analysis == 'anova':
        if not all([args.dep_var, args.group_var]):
            print("错误：需要 --dep_var, --group_var 参数")
            sys.exit(1)
        analyze_anova(df, args.dep_var, args.group_var)
    
    elif args.analysis == 'correlation':
        if not all([args.var1, args.var2]):
            print("错误：需要 --var1, --var2 参数")
            sys.exit(1)
        analyze_correlation(df, args.var1, args.var2)
    
    elif args.analysis == 'regression':
        if not all([args.dep_var, args.indep_vars]):
            print("错误：需要 --dep_var, --indep_vars 参数")
            sys.exit(1)
        analyze_regression(df, args.dep_var, args.indep_vars)
    
    elif args.analysis == 'chi2':
        if not all([args.group_var, args.dep_var]):
            print("错误：需要 --group_var, --dep_var 参数")
            sys.exit(1)
        analyze_chi2(df, args.group_var, args.dep_var)
    
    else:
        print(f"分析类型 {args.analysis} 暂不支持或需要更多参数")
        sys.exit(1)
    
    print(f"\n{'='*60}")
    print("分析完成")
    print(f"{'='*60}\n")


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)