Skip to content

Commit

Permalink
feat: 单样本T检验和配对样本T检验
Browse files Browse the repository at this point in the history
  • Loading branch information
LeafYeeXYZ committed Oct 8, 2024
1 parent 9ffa453 commit 75e048e
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 49 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
- [ ] 箱线图
- [ ] ...
- [ ] 图像导出 (基于 `html2canvas`)
- 统计功能 (近期基于 `math.js`, 远期基于 `WebAssembly`)
- 统计功能 (近期基于 `simple-statistics`, 远期基于 `WebAssembly`)
- [ ] t 检验
- [ ] 单样本 t 检验
- [ ] 独立样本 t 检验
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Loading...</title>
<title>加载中...</title>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@ant-design/plots": "^2.3.2",
"@stdlib/stdlib": "^0.3.0",
"antd": "^5.18.3",
"mathjs": "^13.2.0",
"react": "rc",
"react-dom": "rc",
"simple-statistics": "^7.8.5",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zustand": "^5.0.0-rc.2"
},
Expand Down
8 changes: 0 additions & 8 deletions src/Statistics/TTest.tsx

This file was deleted.

46 changes: 45 additions & 1 deletion src/components/DataView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,54 @@ import { read } from 'xlsx'
import { useZustand } from '../lib/useZustand'
import { Upload, Button, Tag, Table, Popconfirm } from 'antd'
import { SlidersOutlined, DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useEffect } from 'react'
import * as ss from 'simple-statistics'

export function DataView() {

const { data, setData, dataCols, dataRows, ACCEPT_FILE_TYPES } = useZustand()
const { data, setData, dataCols, dataRows, ACCEPT_FILE_TYPES, setDataCols } = useZustand()
const handleCalculate = () => { // 和 VariableView.tsx 中的 handleCalculate 函数相同
try {
const cols = dataCols.map((col) => {
// 原始数据
const data = dataRows.map((row) => row[col.name])
const numData: number[] = data
.filter((v) => typeof +v === 'number' && !isNaN(+v))
.map((v) => +v)
// 基础统计量
const count = data.length
const missing = data.filter((v) => v === undefined).length
const valid = count - missing
const unique = new Set(data).size
// 判断数据类型, 并计算描述统计量
let type: '称名或等级数据' | '等距或等比数据' = '称名或等级数据'
if (
numData.length > 0
// 不是等差数列
&& !numData.every((v, i, arr) => i === 0 || v - arr[i - 1] === arr[1] - arr[0])
) {
type = '等距或等比数据'
const min = +Math.min(...numData).toFixed(4)
const max = +Math.max(...numData).toFixed(4)
const mean = +ss.mean(numData).toFixed(4)
const mode = +ss.mode(numData).toFixed(4)
const q1 = +ss.quantile(numData, 0.25).toFixed(4)
const q2 = +ss.quantile(numData, 0.5).toFixed(4)
const q3 = +ss.quantile(numData, 0.75).toFixed(4)
const std = +ss.standardDeviation(numData).toFixed(4)
return { ...col, count, missing, valid, unique, min, max, mean, mode, q1, q2, q3, std, type }
} else {
return { ...col, count, missing, valid, unique, type }
}
})
setDataCols(cols)
} catch (error) {
console.error(error)
}
}
useEffect(() => {
data && handleCalculate()
}, [data])

return (
<div className='w-full h-full overflow-hidden'>
Expand Down
67 changes: 47 additions & 20 deletions src/components/StatisticsView.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
import { TTest } from '../Statistics/TTest'
import { Select } from 'antd'
import { OneSampleTTest } from '../statistics/OneSampleTTest'
import { PeerSampleTTest } from '../statistics/PeerSampleTTest'
import { Cascader } from 'antd'
import { useState } from 'react'

type Option = {
value: string
label: string
children?: Option[]
}
const CASCADER_OPTIONS: Option[] = [
{
value: 'TTest',
label: 'T检验',
children: [
{
value: 'OneSampleTTest',
label: '单样本T检验',
},
{
value: 'PeerSampleTTest',
label: '配对样本T检验',
},
],
},
]
const CASCADER_ONCHANGE = (value: string[], set: (page: React.ReactElement) => void) => {
switch (value[1]) {
case 'OneSampleTTest':
set(<OneSampleTTest />)
break
case 'PeerSampleTTest':
set(<PeerSampleTTest />)
break
default:
set(DEFAULT_PAGE)
}
}
const CASCADER_DEFAULT_VALUE = ['TTest', 'OneSampleTTest']
const DEFAULT_PAGE = <OneSampleTTest />

export function StatisticsView() {

// 加入新统计: 1. 导入 2. 修改 onChange 3. 加入 select.option
const [page, setPage] = useState<React.ReactElement>(<TTest />)
// 加入新统计: 导入并修改常数定义
const [page, setPage] = useState<React.ReactElement>(DEFAULT_PAGE)

return (
<div className='w-full h-full overflow-hidden'>
<div className='flex flex-col justify-start items-center w-full h-full p-4'>
{/* 上方工具栏 */}
<div className='w-full flex justify-start items-center gap-3 mb-4'>
<Select
placeholder='请选择绘图类型'
defaultValue='TTest'
className='w-48'
onChange={(value) => {
switch (value) {
case 'TTest':
setPage(<TTest />)
break
default:
break
}
}}
>
<Select.Option value='TTest'>T检验</Select.Option>
</Select>
<Cascader
placeholder='请选择统计方法'
defaultValue={CASCADER_DEFAULT_VALUE}
options={CASCADER_OPTIONS}
onChange={(value) => CASCADER_ONCHANGE(value, setPage)}
/>
</div>
{/* 画图界面 */}
<div className='w-full h-full overflow-auto border rounded-md'>
Expand Down
26 changes: 10 additions & 16 deletions src/components/VariableView.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useZustand } from '../lib/useZustand'
import { Button, Table } from 'antd'
import { CalculatorOutlined } from '@ant-design/icons'
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { flushSync } from 'react-dom'
import * as math from 'mathjs'
import * as ss from 'simple-statistics'

export function VariableView() {

const { dataCols, setDataCols, dataRows } = useZustand()
const [calculating, setCalculating] = useState<boolean>(false)
const handleCalculate = () => {
const handleCalculate = () => { // 和 DataView.tsx 中的 handleCalculate 函数相同
try {
const cols = dataCols.map((col) => {
// 原始数据
Expand All @@ -32,14 +32,12 @@ export function VariableView() {
type = '等距或等比数据'
const min = +Math.min(...numData).toFixed(4)
const max = +Math.max(...numData).toFixed(4)
const mean = +math.mean(numData).toFixed(4)
let mode: string = ''
const rawMode = math.mode(numData).map((v) => v.toFixed(4))
rawMode.length <= 3 && (mode = rawMode.join('/'))
const q1 = +math.quantileSeq(numData, 0.25).toFixed(4)
const q2 = +math.quantileSeq(numData, 0.5).toFixed(4)
const q3 = +math.quantileSeq(numData, 0.75).toFixed(4)
const std = +(+math.std(numData, 0)).toFixed(4)
const mean = +ss.mean(numData).toFixed(4)
const mode = +ss.mode(numData).toFixed(4)
const q1 = +ss.quantile(numData, 0.25).toFixed(4)
const q2 = +ss.quantile(numData, 0.5).toFixed(4)
const q3 = +ss.quantile(numData, 0.75).toFixed(4)
const std = +ss.standardDeviation(numData).toFixed(4)
return { ...col, count, missing, valid, unique, min, max, mean, mode, q1, q2, q3, std, type }
} else {
return { ...col, count, missing, valid, unique, type }
Expand All @@ -48,13 +46,8 @@ export function VariableView() {
setDataCols(cols)
} catch (error) {
console.error(error)
} finally {
setCalculating(false)
}
}
useEffect(() => {
handleCalculate()
}, [])

return (
<div className='w-full h-full overflow-hidden'>
Expand All @@ -68,6 +61,7 @@ export function VariableView() {
onClick={() => {
flushSync(() => setCalculating(true))
handleCalculate()
flushSync(() => setCalculating(false))
}}
>
重新计算描述统计量
Expand Down
4 changes: 2 additions & 2 deletions src/lib/useZustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ type Variable = {
max?: number
/** 均值 */
mean?: number
/** 众数, 用“/”分隔 */
mode?: string
/** 众数 */
mode?: number
/** 25%分位数 */
q1?: number
/** 50%分位数 */
Expand Down
147 changes: 147 additions & 0 deletions src/statistics/OneSampleTTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useZustand } from '../lib/useZustand'
import { Select, Input, Button, Form } from 'antd'
import { useState } from 'react'
import { ttest } from '@stdlib/stats'

type Option = {
/** 变量名 */
variable: string
/** 检验值, 默认 0 */
expect: number
/** 显著性水平, 默认 0.05 */
alpha: number
/** 单双尾检验, 默认 two-sided */
alternative: 'two-sided' | 'less' | 'greater'
}
type Result = {
[key: string]: any
} & Option

export function OneSampleTTest() {

const { dataCols, dataRows } = useZustand()
const [result, setResult] = useState<Result | null>(null)
const handleCalculate = (values: Option) => {
try {
const data = dataRows.map((row) => +row[values.variable] as number)
const result = ttest(data, { mu: values.expect, alpha: values.alpha, alternative: values.alternative })
setResult({ variable: values.variable, expect: values.expect, ...result } as Result)
} catch (error) {
console.error(error)
}
}

return (
<div className='w-full h-full overflow-hidden flex justify-start items-center gap-4 p-4'>

<div className='w-1/2 h-full max-w-sm min-w-80 flex flex-col justify-center items-center rounded-md border bg-gray-50 px-4'>

<Form<Option>
className='w-full'
layout='vertical'
onFinish={handleCalculate}
autoComplete='off'
initialValues={{
expect: 0,
alpha: 0.05,
alternative: 'two-sided',
}}
>
<Form.Item
label='选择变量'
name='variable'
rules={[{ required: true, message: '请选择变量' }]}
>
<Select
className='w-full'
placeholder='请选择变量'
>
{dataCols.map((col) => col.type === '等距或等比数据' && (
<Select.Option key={col.name} value={col.name}>
{col.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label='检验值'
name='expect'
rules={[{ required: true, message: '请输入检验值' }]}
>
<Input
className='w-full'
placeholder='请输入检验值'
type='number'
/>
</Form.Item>
<Form.Item
label='显著性水平'
name='alpha'
rules={[{ required: true, message: '请输入显著性水平' }]}
>
<Input
className='w-full'
placeholder='请输入显著性水平'
type='number'
/>
</Form.Item>
<Form.Item
label='单双尾检验'
name='alternative'
rules={[{ required: true, message: '请选择单双尾检验' }]}
>
<Select
className='w-full'
placeholder='请选择单双尾检验'
>
<Select.Option value='two-sided'>双尾检验</Select.Option>
<Select.Option value='less'>单尾检验(左)</Select.Option>
<Select.Option value='greater'>单尾检验(右)</Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Button
className='w-full mt-4'
type='default'
htmlType='submit'
>
计算
</Button>
</Form.Item>
</Form>

</div>

<div className='w-full h-full flex flex-col justify-start items-center gap-4 rounded-md border bg-white overflow-auto p-4'>

{result ? (
<div className='w-full h-full flex flex-col justify-start items-center gap-4 p-4'>
<div key='header' className='w-72 flex justify-between items-center border-t-[2px] border-b-[1px] border-black p-2'>
<span>统计量</span>
<span></span>
</div>
{Object.keys(result).map((key) => !(result[key] instanceof Function) && (
<div key={key} className='w-72 flex justify-between items-center px-2'>
<span>{key}</span>
<span>{
typeof result[key] === 'number' ? result[key].toFixed(4) :
typeof result[key] === 'string' ? result[key] :
typeof result[key] === 'boolean' ? result[key] ? 'true' : 'false' :
result[key] instanceof Array ? result[key].map((v) => +v.toFixed(4)).join(', ') :
''
}</span>
</div>
))}
<div key='footer' className='w-72 flex justify-between items-center border-t-[2px] border-black py-2' />
</div>
) : (
<div className='w-full h-full flex justify-center items-center'>
<span>请填写参数并点击计算</span>
</div>
)}

</div>

</div>
)
}
Loading

0 comments on commit 75e048e

Please sign in to comment.