Skip to content

Commit 0892f9c

Browse files
committed
feat: 新增标准多元线性回归
1 parent 8f0dc06 commit 0892f9c

File tree

6 files changed

+270
-9
lines changed

6 files changed

+270
-9
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
- [5.5.1 Pearson 相关检验](#551-pearson-相关检验)
6363
- [5.5.2 一元线性回归](#552-一元线性回归)
6464
- [5.5.3 二元线性回归](#553-二元线性回归)
65+
- [5.5.4 多元线性回归](#554-多元线性回归)
6566
- [5.6 信度分析](#56-信度分析)
6667
- [5.6.1 重测信度/复本信度](#561-重测信度复本信度)
6768
- [5.6.2 分半信度](#562-分半信度)
@@ -470,6 +471,12 @@ Pearson 相关检验用于检验两组数据之间的线性相关性. 在 Pearso
470471
| :---: | :---: |
471472
| ![](readme/stat-16.png) | ![](readme/stat-17.png) |
472473

474+
#### 5.5.4 多元线性回归
475+
476+
多元线性回归用于检验多个自变量对一个因变量的影响. PsychPen 暂时仅支持标准多元线性回归 (即最小二乘法), 所有自变量同时进入模型, 共同影响将被排除. 在多元线性回归页面中, 你可以选择你要进行检验的多个自变量和因变量, 点击 `计算` 按钮即可进行多元线性回归
477+
478+
![](readme/stat-27.png)
479+
473480
### 5.6 信度分析
474481

475482
信度指测量结果的稳定性, 即多次测量能得到一致的结果. 选择的信度测量方法取决于可能的误差来源, 详见[作者的心理测量学笔记](https://blog.leafyee.xyz/2023/11/02/心理测量学/#x2B50-信度)

bun.lockb

4.45 KB
Binary file not shown.

package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
"@leaf/parse-think": "npm:@jsr/leaf__parse-think",
3232
"@psych/lib": "npm:@jsr/psych__lib",
3333
"@psych/sheet": "npm:@jsr/psych__sheet",
34-
"@tailwindcss/vite": "^4.0.7",
34+
"@tailwindcss/vite": "^4.0.9",
3535
"ag-grid-react": "^32.3.3",
36-
"antd": "^5.24.1",
36+
"antd": "^5.24.2",
3737
"bowser": "^2.11.0",
3838
"echarts": "^5.6.0",
3939
"echarts-gl": "^2.0.9",
@@ -44,26 +44,26 @@
4444
"jieba-wasm": "^2.2.0",
4545
"markdown-it": "^14.1.0",
4646
"ml-kmeans": "^6.0.0",
47-
"openai": "^4.85.3",
47+
"openai": "^4.85.4",
4848
"react": "^19.0.0",
4949
"react-dom": "^19.0.0",
5050
"zustand": "^5.0.3"
5151
},
5252
"devDependencies": {
53-
"@eslint/js": "^9.20.0",
54-
"@types/bun": "^1.2.2",
53+
"@eslint/js": "^9.21.0",
54+
"@types/bun": "^1.2.4",
5555
"@types/react": "^19.0.10",
5656
"@types/react-dom": "^19.0.4",
5757
"@vitejs/plugin-react": "^4.3.4",
5858
"babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
5959
"commit-and-tag-version": "^12.5.0",
60-
"eslint": "^9.20.1",
60+
"eslint": "^9.21.0",
6161
"eslint-plugin-react": "^7.37.4",
6262
"globals": "^15.15.0",
63-
"tailwindcss": "^4.0.7",
63+
"tailwindcss": "^4.0.9",
6464
"typescript": "^5.7.3",
65-
"typescript-eslint": "^8.24.1",
66-
"vite": "^6.1.1"
65+
"typescript-eslint": "^8.25.0",
66+
"vite": "^6.2.0"
6767
},
6868
"commit-and-tag-version": {
6969
"types": [

readme/stat-27.png

255 KB
Loading
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// 如果支持序列多元线性回归后, 修改 README.md 中的说明
2+
import { useZustand } from '../../lib/useZustand'
3+
import { Select, Button, Form, Tag } from 'antd'
4+
import { useState } from 'react'
5+
import { flushSync } from 'react-dom'
6+
import { markP, markS } from '../../lib/utils'
7+
import { LinearRegression, corr } from '@psych/lib'
8+
9+
type Option = {
10+
/** 自变量 */
11+
x: string[]
12+
/** 因变量 */
13+
y: string
14+
/** 回归方式 */
15+
method: 'standard'
16+
}
17+
type Result = {
18+
/** 模型 */
19+
m: LinearRegression
20+
} & Option
21+
22+
export function MultiLinearRegression() {
23+
24+
const { dataCols, dataRows, messageApi } = useZustand()
25+
const [result, setResult] = useState<Result | null>(null)
26+
const [disabled, setDisabled] = useState<boolean>(false)
27+
const handleCalculate = (values: Option) => {
28+
try {
29+
messageApi?.loading('正在处理数据...')
30+
const timestamp = Date.now()
31+
const { x, y } = values
32+
const filteredRows = dataRows.filter((row) => [...x, y].every((variable) => typeof row[variable] !== 'undefined' && !isNaN(Number(row[variable]))))
33+
const xData: number[][] = []
34+
const yData: number[] = []
35+
for (const row of filteredRows) {
36+
const xRow = x.map((variable) => Number(row[variable]))
37+
xData.push(xRow)
38+
yData.push(Number(row[y]))
39+
}
40+
const m = new LinearRegression(xData, yData)
41+
setResult({ ...values, m })
42+
messageApi?.destroy()
43+
messageApi?.success(`数据处理完成, 用时 ${Date.now() - timestamp} 毫秒`)
44+
} catch (error) {
45+
messageApi?.destroy()
46+
messageApi?.error(`数据处理失败: ${error instanceof Error ? error.message : String(error)}`)
47+
}
48+
}
49+
50+
return (
51+
<div className='component-main'>
52+
53+
<div className='component-form'>
54+
55+
<Form<Option>
56+
className='w-full py-4 overflow-auto'
57+
layout='vertical'
58+
onFinish={(values) => {
59+
flushSync(() => setDisabled(true))
60+
handleCalculate(values)
61+
flushSync(() => setDisabled(false))
62+
}}
63+
autoComplete='off'
64+
disabled={disabled}
65+
initialValues={{
66+
method: 'standard'
67+
}}
68+
>
69+
<Form.Item
70+
label={<span>自变量(可多选) <Tag color='blue'>X</Tag></span>}
71+
name='x'
72+
rules={[
73+
{ required: true, message: '请选择自变量' },
74+
({ getFieldValue }) => ({
75+
validator: (_, value) => {
76+
if (value?.some((variable: string) => variable === getFieldValue('y'))) {
77+
return Promise.reject('自变量和因变量不能相同')
78+
} else {
79+
return Promise.resolve()
80+
}
81+
}
82+
})
83+
]}
84+
>
85+
<Select
86+
className='w-full'
87+
placeholder='请选择自变量'
88+
mode='multiple'
89+
options={dataCols.filter((col) => col.type === '等距或等比数据').map((col) => ({ label: col.name, value: col.name }))}
90+
/>
91+
</Form.Item>
92+
<Form.Item
93+
label={<span>因变量 <Tag color='pink'>Y</Tag></span>}
94+
name='y'
95+
rules={[
96+
{ required: true, message: '请选择因变量' },
97+
({ getFieldValue }) => ({
98+
validator: (_, value) => {
99+
if (getFieldValue('x')?.some((variable: string) => variable === value)) {
100+
return Promise.reject('自变量和因变量不能相同')
101+
} else {
102+
return Promise.resolve()
103+
}
104+
}
105+
})
106+
]}
107+
>
108+
<Select
109+
className='w-full'
110+
placeholder='请选择因变量'
111+
options={dataCols.filter((col) => col.type === '等距或等比数据').map((col) => ({ label: col.name, value: col.name }))}
112+
/>
113+
</Form.Item>
114+
<Form.Item
115+
label='回归方式'
116+
name='method'
117+
rules={[{ required: true, message: '请选择回归方式' }]}
118+
>
119+
<Select
120+
className='w-full'
121+
placeholder='请选择回归方式'
122+
options={[
123+
{ label: '标准回归', value: 'standard' },
124+
]}
125+
/>
126+
</Form.Item>
127+
<Form.Item>
128+
<Button
129+
className='w-full mt-4'
130+
type='default'
131+
htmlType='submit'
132+
>
133+
计算
134+
</Button>
135+
</Form.Item>
136+
</Form>
137+
138+
</div>
139+
140+
<div className='component-result'>
141+
142+
{result ? (
143+
<div className='w-full h-full overflow-auto'>
144+
145+
<p className='text-lg mb-2 text-center w-full'>{result.method === 'standard' ? '标准' : ''}多元线性回归</p>
146+
<p className='text-xs mb-2 text-center w-full'>模型: y = {result.m.coefficients[0].toFixed(4)} + {result.m.coefficients.slice(1).map((coefficient, index) => `${coefficient.toFixed(4)} * x${index + 1}`).join(' + ')}</p>
147+
<p className='text-xs mb-3 text-center w-full'>测定系数 (R<sup>2</sup>): {result.m.r2.toFixed(4)} | 调整后测定系数 (R<sup>2</sup><sub>adj</sub>): {result.m.r2adj.toFixed(4)}</p>
148+
<table className='three-line-table'>
149+
<thead>
150+
<tr>
151+
<td>参数</td>
152+
<td></td>
153+
<td>H<sub>0</sub></td>
154+
<td>统计量</td>
155+
<td>显著性</td>
156+
</tr>
157+
</thead>
158+
<tbody>
159+
<tr>
160+
<td>模型</td>
161+
<td>b0: {result.m.coefficients[0].toFixed(4)} | {result.m.coefficients.slice(1).map((coefficient, index) => `b${index + 1}: ${coefficient.toFixed(4)}`).join(' | ')}</td>
162+
<td>{result.m.coefficients.slice(1).map((_, index) => `b${index + 1}`).join(' + ')} = 0</td>
163+
<td>F = {markS(result.m.F, result.m.p)}</td>
164+
<td>{markP(result.m.p)}</td>
165+
</tr>
166+
{result.m.coefficients.slice(1).map((_, index) => (
167+
<tr key={index}>
168+
<td>b{index + 1}</td>
169+
<td>{result.m.coefficients[index + 1].toFixed(4)} (偏回归系数)</td>
170+
<td>b{index + 1} = 0</td>
171+
<td>t = {markS(result.m.tValues[index], result.m.pValues[index])}</td>
172+
<td>{markP(result.m.pValues[index])}</td>
173+
</tr>
174+
))}
175+
</tbody>
176+
</table>
177+
<p className='text-xs mt-3 text-center w-full'>{result.x.map((variable, index) => `x${index + 1}: ${variable}`).join(' | ')} | y: {result.y}</p>
178+
179+
<p className='text-lg mb-2 text-center w-full mt-8'>模型细节</p>
180+
<table className='three-line-table'>
181+
<thead>
182+
<tr>
183+
<td>误差项</td>
184+
<td>自由度 (df)</td>
185+
<td>平方和 (SS)</td>
186+
<td>均方 (MS)</td>
187+
</tr>
188+
</thead>
189+
<tbody>
190+
<tr>
191+
<td>总和 (T)</td>
192+
<td>{result.m.dfT}</td>
193+
<td>{result.m.SSt.toFixed(4)}</td>
194+
<td>{(result.m.SSt / result.m.dfT).toFixed(4)}</td>
195+
</tr>
196+
<tr>
197+
<td>回归 (R)</td>
198+
<td>{result.m.dfR}</td>
199+
<td>{result.m.SSr.toFixed(4)}</td>
200+
<td>{(result.m.SSr / result.m.dfR).toFixed(4)}</td>
201+
</tr>
202+
<tr>
203+
<td>残差 (E)</td>
204+
<td>{result.m.dfE}</td>
205+
<td>{result.m.SSe.toFixed(4)}</td>
206+
<td>{(result.m.SSe / result.m.dfE).toFixed(4)}</td>
207+
</tr>
208+
</tbody>
209+
</table>
210+
211+
<p className='text-lg mb-2 text-center w-full mt-8'>描述统计</p>
212+
<table className='three-line-table'>
213+
<thead>
214+
<tr>
215+
<td>变量</td>
216+
<td>均值</td>
217+
<td>标准差</td>
218+
<td>与Y相关系数</td>
219+
</tr>
220+
</thead>
221+
<tbody>
222+
{result.x.map((variable, index) => (
223+
<tr key={index}>
224+
<td>{variable} (x{index + 1})</td>
225+
<td>{result.m.ivMeans[index].toFixed(4)}</td>
226+
<td>{result.m.ivStds[index].toFixed(4)}</td>
227+
<td>{corr(result.m.iv.map((xRow) => xRow[index]), result.m.dv).toFixed(4)}</td>
228+
</tr>
229+
))}
230+
<tr>
231+
<td>{result.y} (y)</td>
232+
<td>{result.m.dvMean.toFixed(4)}</td>
233+
<td>{result.m.dvStd.toFixed(4)}</td>
234+
<td>1</td>
235+
</tr>
236+
</tbody>
237+
</table>
238+
239+
</div>
240+
) : (
241+
<div className='w-full h-full flex justify-center items-center'>
242+
<span>请填写参数并点击计算</span>
243+
</div>
244+
)}
245+
246+
</div>
247+
248+
</div>
249+
)
250+
}

src/lib/useNav.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import { SimpleMediatorTest } from '../components/statistics/SimpleMediatorTest'
126126
import { WelchTTest } from '../components/statistics/WelchTTest'
127127
import { OneWayANOVA } from '../components/statistics/OneWayANOVA'
128128
import { PeerANOVA } from '../components/statistics/PeerANOVA'
129+
import { MultiLinearRegression } from '../components/statistics/MultiLinearRegression'
129130

130131
export enum STATISTICS_SUB_PAGES_LABELS {
131132
DESCRIPTION = '描述统计',
@@ -141,6 +142,7 @@ export enum STATISTICS_SUB_PAGES_LABELS {
141142
PEARSON_CORRELATION_TEST = 'Pearson 相关检验',
142143
ONE_LINEAR_REGRESSION = '一元线性回归',
143144
TWO_LINEAR_REGRESSION = '二元线性回归',
145+
MULTI_LINEAR_REGRESSION = '多元线性回归',
144146
CORR_RELIABILITY = '重测或复本信度',
145147
HALF_RELIABILITY = '分半信度',
146148
HOMO_RELIABILITY = '同质性信度',
@@ -160,6 +162,7 @@ export const STATISTICS_SUB_PAGES_ELEMENTS: Record<STATISTICS_SUB_PAGES_LABELS,
160162
[STATISTICS_SUB_PAGES_LABELS.PEARSON_CORRELATION_TEST]: <PearsonCorrelationTest />,
161163
[STATISTICS_SUB_PAGES_LABELS.ONE_LINEAR_REGRESSION]: <OneLinearRegression />,
162164
[STATISTICS_SUB_PAGES_LABELS.TWO_LINEAR_REGRESSION]: <TwoLinearRegression />,
165+
[STATISTICS_SUB_PAGES_LABELS.MULTI_LINEAR_REGRESSION]: <MultiLinearRegression />,
163166
[STATISTICS_SUB_PAGES_LABELS.CORR_RELIABILITY]: <CorrReliability />,
164167
[STATISTICS_SUB_PAGES_LABELS.HALF_RELIABILITY]: <HalfReliability />,
165168
[STATISTICS_SUB_PAGES_LABELS.HOMO_RELIABILITY]: <HomoReliability />,
@@ -188,6 +191,7 @@ export const STATISTICS_SUB_PAGES_MAP: Record<string, STATISTICS_SUB_PAGES_LABEL
188191
STATISTICS_SUB_PAGES_LABELS.PEARSON_CORRELATION_TEST,
189192
STATISTICS_SUB_PAGES_LABELS.ONE_LINEAR_REGRESSION,
190193
STATISTICS_SUB_PAGES_LABELS.TWO_LINEAR_REGRESSION,
194+
STATISTICS_SUB_PAGES_LABELS.MULTI_LINEAR_REGRESSION,
191195
],
192196
'信度分析': [
193197
STATISTICS_SUB_PAGES_LABELS.CORR_RELIABILITY,

0 commit comments

Comments
 (0)