Skip to content

Commit 4b72fcc

Browse files
committed
refactor(plotfig): add validation for single-element data groups and comprehensive tests
1 parent ec300f3 commit 4b72fcc

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

src/plotfig/utils/bar.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def compute_summary(data: ArrayLike) -> tuple[float, float, float]:
3131
Mean: 3.00, SD: 1.58, SE: 0.71
3232
"""
3333
data = np.asarray(data)
34+
if len(data) <= 1:
35+
raise ValueError(
36+
f"数据组只有 {len(data)} 个元素,无法计算标准差和标准误。每组数据至少需要 2 个元素。"
37+
)
3438
mean = np.mean(data)
3539
sd = np.std(data, ddof=1)
3640
se = sd / np.sqrt(len(data))

tests/plotfig/test_single_bar.py

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
import pytest
4+
from matplotlib.axes import Axes
5+
6+
from plotfig import (
7+
plot_one_group_bar_figure,
8+
)
9+
10+
11+
class TestPlotSingleBarFigureSuccesses:
12+
def setup_method(self):
13+
"""测试前初始化:创建图形和测试数据"""
14+
self.fig, self.ax = plt.subplots()
15+
self.test_data = [np.random.rand(2), np.random.rand(3), np.random.rand(4)]
16+
17+
def teardown_method(self):
18+
"""测试后清理:关闭图形"""
19+
plt.close(self.fig)
20+
21+
def test_basic_plotting(self):
22+
"""最基本的烟雾测试:确保函数能正常运行并返回Axes对象"""
23+
result = plot_one_group_bar_figure(self.test_data, ax=self.ax)
24+
assert isinstance(result, Axes)
25+
26+
def test_with_custom_parameters(self):
27+
"""测试常用参数组合是否能正常工作"""
28+
custom_data = [
29+
[1.1, 2.2, 3.3, 4.4],
30+
[5.5, 6.6, 7.7, 8.8, 9.9],
31+
[10.1, 11.1, 12.1, 13.1, 14.1, 15.1],
32+
]
33+
dots_color = [
34+
["#ff0000", "#00ff00", "#0000ff", "#ffff00"],
35+
["#00ffff", "#ff00ff", "#ff8800", "#0088ff", "#88ff00"],
36+
["#8800ff", "#00ff88", "#ff0088", "#888888", "#444444", "#000000"],
37+
]
38+
result = plot_one_group_bar_figure(
39+
custom_data,
40+
ax=self.ax,
41+
labels_name=["A", "B", "C"],
42+
edgecolor="#ffff00",
43+
gradient_color=True,
44+
colors_start=["#ff0000", "#00ff00", "#0000ff"],
45+
colors_end=["#00ffff", "#ff00ff", "#ffff00"],
46+
show_dots=True,
47+
dots_color=dots_color,
48+
width=0.7,
49+
color_alpha=0.8,
50+
dots_size=25,
51+
errorbar_type="se",
52+
title_name="Test Fig",
53+
x_label_name="x label",
54+
y_label_name="y label",
55+
y_lim=(0, 16),
56+
statistic=True,
57+
test_method=["mannwhitneyu", "ttest_1samp"],
58+
popmean=0,
59+
)
60+
assert isinstance(result, Axes)
61+
assert result.get_title() == "Test Fig"
62+
63+
64+
class TestPlotSingleBarFigureErrors:
65+
"""测试错误处理"""
66+
67+
def setup_method(self):
68+
"""测试前初始化:创建图形和基础测试数据"""
69+
self.fig, self.ax = plt.subplots()
70+
self.basic_data = [[1, 2], [3, 4]]
71+
72+
def teardown_method(self):
73+
"""测试后清理:关闭图形"""
74+
plt.close(self.fig)
75+
76+
def test_invalid_data_format(self):
77+
"""测试无效数据格式应抛出 ValueError"""
78+
with pytest.raises(ValueError, match="无效的 data"):
79+
plot_one_group_bar_figure("invalid_data")
80+
81+
def test_invalid_errorbar_type(self):
82+
"""测试无效的 errorbar_type 应抛出 ValueError"""
83+
with pytest.raises(ValueError, match="errorbar_type 只能是"):
84+
plot_one_group_bar_figure(self.basic_data, ax=self.ax, errorbar_type="invalid")
85+
86+
def test_invalid_test_method(self):
87+
"""测试无效的 test_method 应抛出 ValueError"""
88+
with pytest.raises(ValueError, match="未知统计方法"):
89+
plot_one_group_bar_figure(
90+
self.basic_data, ax=self.ax, statistic=True, test_method=["invalid"]
91+
)
92+
93+
def test_test_method_too_many_elements(self):
94+
"""测试 test_method 超过2个元素且不包含 ttest_1samp 应抛出 ValueError"""
95+
with pytest.raises(ValueError, match="test_method 最多只能有2个元素"):
96+
plot_one_group_bar_figure(
97+
self.basic_data,
98+
ax=self.ax,
99+
statistic=True,
100+
test_method=["ttest_ind", "mannwhitneyu", "ttest_rel"],
101+
)
102+
103+
def test_statistic_external_missing_p_list(self):
104+
"""测试 external 方法缺少 p_list 应抛出 ValueError"""
105+
with pytest.raises(ValueError, match="p_list参数不能为空"):
106+
plot_one_group_bar_figure(
107+
self.basic_data, ax=self.ax, statistic=True, test_method=["external"]
108+
)
109+
110+
def test_single_element_per_group(self):
111+
"""测试每组只有一个元素时应抛出 ValueError"""
112+
with pytest.raises(
113+
ValueError,
114+
match="数据组只有 1 个元素,无法计算标准差和标准误。每组数据至少需要 2 个元素。",
115+
):
116+
plot_one_group_bar_figure([[1], [2], [3]], ax=self.ax)
117+
118+
119+
class TestPlotSingleBarFigureDataTypes:
120+
"""测试数据类型"""
121+
122+
def setup_method(self):
123+
"""测试前初始化:创建图形和不同类型的测试数据"""
124+
self.fig, self.ax = plt.subplots()
125+
self.numpy_data = [np.array([1.0, 2.0, 3.0]), np.array([4.0, 5.0, 6.0])]
126+
self.list_data = [[1, 2, 3], [4, 5, 6]]
127+
self.mixed_data = [[1.0, 2.0, 3.0], np.array([4.0, 5.0, 6.0])]
128+
129+
def teardown_method(self):
130+
"""测试后清理:关闭图形"""
131+
plt.close(self.fig)
132+
133+
def test_with_numpy_arrays(self):
134+
"""测试 numpy array 数据"""
135+
result = plot_one_group_bar_figure(self.numpy_data, ax=self.ax)
136+
assert isinstance(result, Axes)
137+
138+
def test_with_pure_lists(self):
139+
"""测试纯 list 数据"""
140+
result = plot_one_group_bar_figure(self.list_data, ax=self.ax)
141+
assert isinstance(result, Axes)
142+
143+
def test_with_mixed_data_types(self):
144+
"""测试混合数据类型"""
145+
result = plot_one_group_bar_figure(self.mixed_data, ax=self.ax)
146+
assert isinstance(result, Axes)
147+
148+
149+
class TestPlotSingleBarFigureFeatures:
150+
"""测试功能分支"""
151+
152+
def setup_method(self):
153+
"""测试前初始化:创建图形和基础测试数据"""
154+
self.fig, self.ax = plt.subplots()
155+
self.basic_data = [[1, 2, 3], [4, 5, 6]]
156+
self.custom_colors_start = ["#ff0000", "#00ff00"]
157+
self.custom_colors_end = ["#0000ff", "#ffff00"]
158+
159+
def teardown_method(self):
160+
"""测试后清理:关闭图形"""
161+
plt.close(self.fig)
162+
163+
def test_errorbar_sd(self):
164+
"""测试标准差误差条"""
165+
result = plot_one_group_bar_figure(self.basic_data, ax=self.ax, errorbar_type="sd")
166+
assert isinstance(result, Axes)
167+
168+
def test_errorbar_se(self):
169+
"""测试标准误误差条"""
170+
result = plot_one_group_bar_figure(self.basic_data, ax=self.ax, errorbar_type="se")
171+
assert isinstance(result, Axes)
172+
173+
def test_without_dots(self):
174+
"""测试不显示散点"""
175+
result = plot_one_group_bar_figure(self.basic_data, ax=self.ax, show_dots=False)
176+
assert isinstance(result, Axes)
177+
178+
def test_gradient_color_defaults(self):
179+
"""测试渐变色默认行为"""
180+
result = plot_one_group_bar_figure(self.basic_data, ax=self.ax, gradient_color=True)
181+
assert isinstance(result, Axes)
182+
183+
def test_gradient_color_custom(self):
184+
"""测试自定义渐变色"""
185+
result = plot_one_group_bar_figure(
186+
self.basic_data,
187+
ax=self.ax,
188+
gradient_color=True,
189+
colors_start=self.custom_colors_start,
190+
colors_end=self.custom_colors_end,
191+
)
192+
assert isinstance(result, Axes)
193+
194+
195+
class TestPlotSingleBarFigureStatistics:
196+
"""测试统计检验"""
197+
198+
def setup_method(self):
199+
"""测试前初始化:创建图形和统计测试数据"""
200+
self.fig, self.ax = plt.subplots()
201+
self.statistic_data = [[1, 2, 3], [10, 11, 12]]
202+
self.popmean = 5
203+
self.p_list = [0.01]
204+
205+
def teardown_method(self):
206+
"""测试后清理:关闭图形"""
207+
plt.close(self.fig)
208+
209+
def test_statistic_ttest_ind(self):
210+
"""测试独立样本t检验"""
211+
result = plot_one_group_bar_figure(
212+
self.statistic_data, ax=self.ax, statistic=True, test_method=["ttest_ind"]
213+
)
214+
assert isinstance(result, Axes)
215+
216+
def test_statistic_mannwhitneyu(self):
217+
"""测试 Mann-Whitney U 检验"""
218+
result = plot_one_group_bar_figure(
219+
self.statistic_data, ax=self.ax, statistic=True, test_method=["mannwhitneyu"]
220+
)
221+
assert isinstance(result, Axes)
222+
223+
def test_statistic_ttest_1samp(self):
224+
"""测试单样本t检验"""
225+
result = plot_one_group_bar_figure(
226+
self.statistic_data,
227+
ax=self.ax,
228+
statistic=True,
229+
test_method=["ttest_1samp"],
230+
popmean=self.popmean,
231+
)
232+
assert isinstance(result, Axes)
233+
234+
def test_statistic_multiple_methods(self):
235+
"""测试多种统计方法组合"""
236+
result = plot_one_group_bar_figure(
237+
self.statistic_data,
238+
ax=self.ax,
239+
statistic=True,
240+
test_method=["ttest_ind", "ttest_1samp"],
241+
popmean=self.popmean,
242+
)
243+
assert isinstance(result, Axes)
244+
245+
def test_statistic_external_with_p_list(self):
246+
"""测试 external 方法与 p_list"""
247+
result = plot_one_group_bar_figure(
248+
self.statistic_data,
249+
ax=self.ax,
250+
statistic=True,
251+
test_method=["external"],
252+
p_list=self.p_list,
253+
)
254+
assert isinstance(result, Axes)
255+
256+
257+
class TestPlotSingleBarFigureYAxis:
258+
"""测试 Y轴设置"""
259+
260+
def setup_method(self):
261+
"""测试前初始化:创建图形和 Y轴测试数据"""
262+
self.fig, self.ax = plt.subplots()
263+
self.basic_data = [[1, 2, 3], [4, 5, 6]]
264+
self.percentage_data = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
265+
266+
def teardown_method(self):
267+
"""测试后清理:关闭图形"""
268+
plt.close(self.fig)
269+
270+
def test_y_lim(self):
271+
"""测试自定义 Y轴范围"""
272+
result = plot_one_group_bar_figure(self.basic_data, ax=self.ax, y_lim=(0, 10))
273+
assert result.get_ylim() == (0, 10)
274+
275+
def test_ax_bottom_is_0(self):
276+
"""测试 Y轴从0开始"""
277+
result = plot_one_group_bar_figure(
278+
self.basic_data, ax=self.ax, ax_bottom_is_0=True
279+
)
280+
assert result.get_ylim()[0] == 0
281+
282+
def test_y_max_tick_is_1(self):
283+
"""测试 Y轴最大刻度限制为1"""
284+
result = plot_one_group_bar_figure(
285+
self.percentage_data, ax=self.ax, y_max_tick_is_1=True
286+
)
287+
assert result.get_ylim()[1] <= 1
288+
289+
def test_percentage_format(self):
290+
"""测试百分比格式"""
291+
result = plot_one_group_bar_figure(
292+
self.percentage_data, ax=self.ax, percentage=True, math_text=False
293+
)
294+
assert isinstance(result, Axes)
295+
296+
297+
class TestPlotSingleBarFigureEdgeCases:
298+
"""测试边界条件"""
299+
300+
def setup_method(self):
301+
"""测试前初始化:创建图形和边界测试数据"""
302+
self.fig, self.ax = plt.subplots()
303+
self.single_group_data = [[1, 2, 3]]
304+
self.basic_data = [[1, 2, 3], [4, 5, 6]]
305+
self.similar_data = [[1, 2, 3], [1.1, 2.1, 3.1]]
306+
307+
def teardown_method(self):
308+
"""测试后清理:关闭图形"""
309+
plt.close(self.fig)
310+
311+
def test_single_group(self):
312+
"""测试单组数据"""
313+
result = plot_one_group_bar_figure(self.single_group_data, ax=self.ax)
314+
assert isinstance(result, Axes)
315+
316+
def test_ax_none(self):
317+
"""测试 ax=None 时使用当前坐标轴"""
318+
# 这个测试明确需要 ax=None,不使用 self.ax
319+
fig, ax = plt.subplots()
320+
try:
321+
result = plot_one_group_bar_figure(self.basic_data, ax=None)
322+
assert isinstance(result, Axes)
323+
finally:
324+
plt.close(fig)
325+
326+
def test_no_significant_differences(self):
327+
"""测试无显著差异时不应显示显著性标记"""
328+
result = plot_one_group_bar_figure(
329+
self.similar_data, ax=self.ax, statistic=True, test_method=["ttest_ind"]
330+
)
331+
assert isinstance(result, Axes)

0 commit comments

Comments
 (0)