csv-stats 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: csv-stats
3
+ Version: 0.1.0
4
+ Summary: A Python package for the life sciences to conduct hypothesis testing from CSV files.
5
+ Author-email: Mitchell Tillman <mtillman14@gmail.com>
6
+ Maintainer-email: Mitchell Tillman <mtillman14@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/mtillman14/csv-stats
9
+ Project-URL: Documentation, https://github.com/mtillman14/csv-stats
10
+ Project-URL: Repository, https://github.com/mtillman14/csv-stats
11
+ Project-URL: Issues, https://github.com/mtillman14/csv-stats/issues
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Development Status :: 3 - Alpha
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pandas>=2.3.3
20
+ Requires-Dist: pingouin>=0.5.5
21
+ Requires-Dist: scipy>=1.16.2
22
+ Requires-Dist: statsmodels>=0.14.5
23
+
24
+ # csv-stats
25
+ Python package for rapid hypothesis testing on CSV files with data in long table format, which are common in the life sciences.
@@ -0,0 +1,2 @@
1
+ # csv-stats
2
+ Python package for rapid hypothesis testing on CSV files with data in long table format, which are common in the life sciences.
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "csv-stats"
3
+ version = "0.1.0"
4
+ description = "A Python package for the life sciences to conduct hypothesis testing from CSV files."
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Mitchell Tillman", email = "mtillman14@gmail.com"},
10
+ ]
11
+ maintainers = [
12
+ {name = "Mitchell Tillman", email = "mtillman14@gmail.com"},
13
+ ]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Science/Research",
19
+ "Development Status :: 3 - Alpha",
20
+ ]
21
+ dependencies = [
22
+ "pandas>=2.3.3",
23
+ "pingouin>=0.5.5",
24
+ "scipy>=1.16.2",
25
+ "statsmodels>=0.14.5",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/mtillman14/csv-stats"
30
+ Documentation = "https://github.com/mtillman14/csv-stats"
31
+ Repository = "https://github.com/mtillman14/csv-stats"
32
+ Issues = "https://github.com/mtillman14/csv-stats/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,301 @@
1
+ from typing import Union
2
+ from pathlib import Path
3
+
4
+ import statsmodels.api as sm
5
+ from statsmodels.formula.api import ols
6
+ from statsmodels.stats.anova import AnovaRM
7
+ import pandas as pd
8
+
9
+ from .utils.summary_stats import calculate_summary_statistics
10
+ from .utils.load_data import load_data_from_path
11
+ from .utils.test_assumptions import test_normality_assumption, test_variance_homogeneity_assumption, test_sphericity_assumption
12
+ from .utils.save_stats import dict_to_pdf
13
+
14
+ def anova1way(data: Union[Path, str, pd.DataFrame], group_column: str, data_column: str, repeated_measures_column: str = "", filename: Union[str, None] = 'anova1way_results.pdf') -> dict:
15
+ """
16
+ Perform one-way ANOVA.
17
+
18
+ Parameters:
19
+ data : file path, str, or pd.DataFrame
20
+ The input data containing the groups and values.
21
+ group_column : str
22
+ The name of the column containing group labels.
23
+ data_column : str
24
+ The name of the column containing data values.
25
+ repeated_measures_column : str, optional
26
+ The name of the column containing the repeated measures identifiers. Defaults to "".
27
+ filename : str, optional
28
+ The filename to save the results to. Defaults to 'anova1way_results.pdf'. If None, results are not saved to a file.
29
+
30
+ Returns:
31
+ result: dict
32
+ A dictionary containing:
33
+ "F" : float
34
+ The computed F-statistic.
35
+ "p" : float
36
+ The associated p-value, rounded to four decimal places.
37
+ """
38
+
39
+ if repeated_measures_column is None:
40
+ repeated_measures_column = ""
41
+
42
+ # Define a boolean flag for repeated measures ANOVA
43
+ is_repeated_measures = repeated_measures_column == ""
44
+
45
+ result = {}
46
+ result["group_column"] = group_column
47
+ result["data_column"] = data_column
48
+ result["repeated_measures_column"] = repeated_measures_column
49
+
50
+ # Load the data as a pandas DataFrame
51
+ data = load_data_from_path(data)
52
+
53
+ # Perform ANOVA
54
+ formula = f"{data_column} ~ C({group_column})"
55
+ anova_result = _perform_anova(data, formula, group_column, data_column, repeated_measures_column, is_repeated_measures)
56
+
57
+ # Extract F-statistic and p-value
58
+ F = anova_result["anova_table"].loc[f"C({group_column})", "F"]
59
+ p = anova_result["anova_table"].loc[f"C({group_column})", "PR(>F)"]
60
+
61
+ # Calculate degrees of freedom
62
+ df_between = len(data[group_column].unique()) - 1
63
+ df_within = len(data) - len(data[group_column].unique())
64
+
65
+ summary_stats = calculate_summary_statistics(data, group_column, data_column)
66
+
67
+ # Store results in the dictionary
68
+ result["F"] = F
69
+ result["p"] = round(p, 4)
70
+ result["df_between"] = df_between
71
+ result["df_within"] = df_within
72
+ result["summary_stats"] = summary_stats
73
+ result["normality_test"] = anova_result["normality_test"]
74
+ result["homogeneity_of_variance_test"] = anova_result["homogeneity_of_variance_test"]
75
+ result["sphericity_test"] = anova_result["sphericity_test"]
76
+
77
+ if filename is not None:
78
+ dict_to_pdf(result, filename=filename)
79
+
80
+ return result
81
+
82
+
83
+ def anova2way(data: Union[Path, str, pd.DataFrame], group_column1: str, group_column2: str, data_column: str, repeated_measures_column: str = "", filename: Union[str, None] = 'anova2way_results.pdf') -> dict:
84
+ """
85
+ Perform two-way ANOVA.
86
+
87
+ Parameters:
88
+ data : file path, str, or pd.DataFrame
89
+ The input data containing the groups and values.
90
+ group_column1 : str
91
+ The name of the first column containing group labels.
92
+ group_column2 : str
93
+ The name of the second column containing group labels.
94
+ data_column : str
95
+ The name of the column containing data values.
96
+ repeated_measures_column : str, optional
97
+ The name of the column containing the repeated measures identifiers. Defaults to "".
98
+ filename : str, optional
99
+ The filename to save the results to. Defaults to 'anova2way_results.pdf'. If None, results are not saved to a file.
100
+
101
+ Returns:
102
+ result: dict
103
+ A dictionary containing:
104
+ "F1" : float
105
+ The computed F-statistic for the first factor.
106
+ "p1" : float
107
+ The associated p-value for the first factor, rounded to four decimal places.
108
+ "F2" : float
109
+ The computed F-statistic for the second factor.
110
+ "p2" : float
111
+ The associated p-value for the second factor, rounded to four decimal places.
112
+ "F_interaction" : float
113
+ The computed F-statistic for the interaction between factors.
114
+ "p_interaction" : float
115
+ The associated p-value for the interaction, rounded to four decimal places.
116
+ """
117
+
118
+ if repeated_measures_column is None:
119
+ repeated_measures_column = ""
120
+
121
+ # Define a boolean flag for repeated measures ANOVA
122
+ is_repeated_measures = repeated_measures_column == ""
123
+
124
+ result = {}
125
+ result["group_column1"] = group_column1
126
+ result["group_column2"] = group_column2
127
+ result["data_column"] = data_column
128
+ result["repeated_measures_column"] = repeated_measures_column
129
+
130
+ # Load the data as a pandas DataFrame
131
+ data = load_data_from_path(data)
132
+
133
+ # Fit the model with interaction
134
+ formula = f"{data_column} ~ C({group_column1}) + C({group_column2}) + C({group_column1}):C({group_column2})"
135
+ anova_result = _perform_anova(data, formula, [group_column1, group_column2], data_column, repeated_measures_column, is_repeated_measures)
136
+
137
+ summary_stats_group1 = calculate_summary_statistics(data, group_column1, data_column)
138
+ summary_stats_group2 = calculate_summary_statistics(data, group_column2, data_column)
139
+ # Prepare the dataframe to calculate the summary statistics for the interaction effect
140
+ interaction_column_name = f"{group_column1}_{group_column2}"
141
+ data[interaction_column_name] = data[group_column1].astype(str) + '_' + data[group_column2].astype(str)
142
+ summary_stats_interaction = calculate_summary_statistics(data, interaction_column_name, data_column)
143
+
144
+ # Store results in the dictionary
145
+ result["main_effects"] = {}
146
+ result["main_effects"][group_column1] = {}
147
+ result["main_effects"][group_column2] = {}
148
+ result["interaction"] = {}
149
+
150
+ anova_table = anova_result["anova_table"]
151
+ result["main_effects"][group_column1]["F"] = anova_table.loc[f"C({group_column1})", "F"]
152
+ result["main_effects"][group_column1]["p"] = round(anova_table.loc[f"C({group_column1})", "PR(>F)"], 4)
153
+ result["main_effects"][group_column2]["F"] = anova_table.loc[f"C({group_column2})", "F"]
154
+ result["main_effects"][group_column2]["p"] = round(anova_table.loc[f"C({group_column2})", "PR(>F)"], 4)
155
+
156
+ interaction_key = f"C({group_column1}):C({group_column2})"
157
+ result["interaction"]["F"] = anova_table.loc[interaction_key, "F"]
158
+ result["interaction"]["p"] = round(anova_table.loc[interaction_key, "PR(>F)"], 4)
159
+ result[f"summary_stats_{group_column1}"] = summary_stats_group1
160
+ result[f"summary_stats_{group_column2}"] = summary_stats_group2
161
+ result["summary_stats_interaction"] = summary_stats_interaction
162
+
163
+ result["normality_test"] = anova_result["normality_test"]
164
+ result["homogeneity_of_variance_test"] = anova_result["homogeneity_of_variance_test"]
165
+ result["sphericity_test"] = anova_result["sphericity_test"]
166
+
167
+ if filename is not None:
168
+ dict_to_pdf(result, filename='anova2way_results.pdf')
169
+
170
+ return result
171
+
172
+
173
+ def anova3way(data: Union[Path, str, pd.DataFrame], group_column1: str, group_column2: str, group_column3: str, data_column: str, repeated_measures_column: str, filename: Union[str, None] = 'anova3way_results.pdf') -> dict:
174
+ """
175
+ Perform three-way ANOVA.
176
+
177
+ Parameters:
178
+ data : file path, str, or pd.DataFrame
179
+ The input data containing the groups and values.
180
+ group_column1 : str
181
+ The name of the first column containing group labels.
182
+ group_column2 : str
183
+ The name of the second column containing group labels.
184
+ group_column3 : str
185
+ The name of the third column containing group labels.
186
+ data_column : str
187
+ The name of the column containing data values.
188
+
189
+ Returns:
190
+ result: dict
191
+ A dictionary containing:
192
+ "F1" : float
193
+ The computed F-statistic for the first factor.
194
+ "p1" : float
195
+ The associated p-value for the first factor, rounded to four decimal places.
196
+ "F2" : float
197
+ The computed F-statistic for the second factor.
198
+ "p2" : float
199
+ The associated p-value for the second factor, rounded to four decimal places.
200
+ "F3" : float
201
+ The computed F-statistic for the third factor.
202
+ "p3" : float
203
+ The associated p-value for the third factor, rounded to four decimal places.
204
+ "F_interaction" : float
205
+ The computed F-statistic for the interaction between factors.
206
+ "p_interaction" : float
207
+ The associated p-value for the interaction, rounded to four decimal places.
208
+ """
209
+
210
+ result = {}
211
+ result["group_column1"] = group_column1
212
+ result["group_column2"] = group_column2
213
+ result["group_column3"] = group_column3
214
+ result["data_column"] = data_column
215
+
216
+ # Load the data as a pandas DataFrame
217
+ data = load_data_from_path(data)
218
+
219
+ # Perform ANOVA
220
+ formula = f"{data_column} ~ C({group_column1}) * C({group_column2}) * C({group_column3})"
221
+ anova_result = _perform_anova(data, formula, [group_column1, group_column2, group_column3], data_column, repeated_measures_column, False)
222
+
223
+ summary_stats_group1 = calculate_summary_statistics(data, group_column1, data_column)
224
+ summary_stats_group2 = calculate_summary_statistics(data, group_column2, data_column)
225
+ summary_stats_group3 = calculate_summary_statistics(data, group_column3, data_column)
226
+ # Prepare the dataframe to calculate the summary statistics for the interaction effect
227
+ interaction_column_name = f"{group_column1}_{group_column2}+{group_column3}"
228
+ data[interaction_column_name] = data[group_column1].astype(str) + '_' + data[group_column2].astype(str) + '_' + data[group_column3].astype(str)
229
+ summary_stats_interaction = calculate_summary_statistics(data, interaction_column_name, data_column)
230
+
231
+ # Store results in the dictionary
232
+ result["main_effects"] = {}
233
+ result["main_effects"][group_column1] = {}
234
+ result["main_effects"][group_column2] = {}
235
+ result["main_effects"][group_column3] = {}
236
+ result["interaction"] = {}
237
+
238
+ anova_table = anova_result["anova_table"]
239
+ result["main_effects"][group_column1]["F"] = anova_table.loc[f"C({group_column1})", "F"]
240
+ result["main_effects"][group_column1]["p"] = round(anova_table.loc[f"C({group_column1})", "PR(>F)"], 4)
241
+ result["main_effects"][group_column2]["F"] = anova_table.loc[f"C({group_column2})", "F"]
242
+ result["main_effects"][group_column2]["p"] = round(anova_table.loc[f"C({group_column2})", "PR(>F)"], 4)
243
+ result["main_effects"][group_column3]["F"] = anova_table.loc[f"C({group_column3})", "F"]
244
+ result["main_effects"][group_column3]["p"] = round(anova_table.loc[f"C({group_column3})", "PR(>F)"], 4)
245
+
246
+ interaction_key = f"C({group_column1}):C({group_column2}):C({group_column3})"
247
+ result["interaction"]["F"] = anova_table.loc[interaction_key, "F"]
248
+ result["interaction"]["p"] = round(anova_table.loc[interaction_key, "PR(>F)"], 4)
249
+ result[f"summary_stats_{group_column1}"] = summary_stats_group1
250
+ result[f"summary_stats_{group_column2}"] = summary_stats_group2
251
+ result[f"summary_stats_{group_column3}"] = summary_stats_group3
252
+ result["summary_stats_interaction"] = summary_stats_interaction
253
+
254
+ result["normality_test"] = anova_result["normality_test"]
255
+ result["homogeneity_of_variance_test"] = anova_result["homogeneity_of_variance_test"]
256
+ result["sphericity_test"] = anova_result["sphericity_test"]
257
+
258
+ if filename is not None:
259
+ dict_to_pdf(result, filename=filename)
260
+
261
+ return result
262
+
263
+
264
+ def _perform_anova(data: pd.DataFrame, formula: str, group_column: Union[list, str], data_column: str, repeated_measures_column: str, is_repeated_measures: bool) -> dict:
265
+ """Perform the actual ANOVA computation. Works for all types of ANOVA.
266
+
267
+ Args:
268
+ data (pd.DataFrame): The input data
269
+ group_column (str): The name of the column containing group labels.
270
+ data_column (str): The name of the column containing data values.
271
+ repeated_measures_column (str): The name of the column containing the repeated measures identifiers.
272
+ is_repeated_measures (bool): The boolean flag indicating if this is a repeated measures ANOVA.
273
+
274
+ Returns:
275
+ dict: The ANOVA results including F-statistic and p-value.
276
+ """
277
+ result = {}
278
+ if not is_repeated_measures:
279
+ # Fit the one-way ANOVA model using statsmodels
280
+ model = ols(formula, data=data).fit()
281
+ anova_table = sm.stats.anova_lm(model, typ=2)
282
+ homogeneity_variances_result = test_variance_homogeneity_assumption(data, group_column, data_column)
283
+ mauchly_test_result = "Not applicable"
284
+ else:
285
+ model = AnovaRM(data,
286
+ depvar=data_column,
287
+ subject=repeated_measures_column,
288
+ within=[group_column]).fit()
289
+ anova_table = model.anova_table
290
+ mauchly_test_result = test_sphericity_assumption(data, group_column, repeated_measures_column, data_column)
291
+ homogeneity_variances_result = "Not applicable"
292
+
293
+ result["model"] = model
294
+ result["anova_table"] = anova_table
295
+ result["homogeneity_of_variance_test"] = homogeneity_variances_result
296
+ result["sphericity_test"] = mauchly_test_result
297
+
298
+ normality_result = test_normality_assumption(model)
299
+ result["normality_test"] = normality_result
300
+
301
+ return result
@@ -0,0 +1,107 @@
1
+ from typing import Union
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+ from scipy import stats
6
+ from statsmodels.formula.api import ols
7
+
8
+ from .anova import anova1way
9
+ from .utils.load_data import load_data_from_path
10
+ from .utils.save_stats import dict_to_pdf
11
+ from .utils.summary_stats import calculate_summary_statistics
12
+ from .utils.test_assumptions import test_normality_assumption, test_variance_homogeneity_assumption
13
+
14
+ def ttest_ind(data: Union[Path, str, pd.DataFrame], group_column: str, data_column: str, filename: Union[str, None] = 'ttest_ind_results.pdf') -> dict:
15
+ """Conduct a two-sample t-test (independent or paired) and save results to a PDF.
16
+
17
+ Args:
18
+ data (Union[Path, str, pd.DataFrame]): The input data, which can be a file path, string, or DataFrame.
19
+ group_column (str): The name of the column containing group labels. If this is a one sample t-test, this should be an empty string.
20
+ data_column (str): The name of the column containing the data values.
21
+ repeated_measures_column (str, optional): The name of the. Defaults to "".
22
+ filename (str, optional): The filename to save the results to. Defaults to 'ttest_ind_results.pdf'. If None, results are not saved to a file.
23
+ """
24
+
25
+ result = {} # Initialize result dictionary
26
+
27
+ # Load the data
28
+ data = load_data_from_path(data)
29
+
30
+ # Make sure there is a 'group_column' even for one-sample t-tests
31
+ if group_column is None:
32
+ group_column = ""
33
+ if group_column == "":
34
+ group_column = "one_sample"
35
+ data[group_column] = "all"
36
+
37
+ # Store metadata
38
+ result["group_column"] = group_column
39
+ result["data_column"] = data_column
40
+
41
+ groups = data[group_column].unique()
42
+ num_groups = len(groups)
43
+ if num_groups == 2:
44
+ result = anova1way(data, group_column, data_column, group_column, filename)
45
+ dict_to_pdf(result, filename=filename)
46
+ return result
47
+
48
+ if num_groups != 1:
49
+ raise ValueError("Data must contain exactly one or two groups for a t-test.")
50
+
51
+ t_stat, p_value = stats.ttest_1samp(data[data_column], popmean=0)
52
+ result["t_statistic"] = round(t_stat, 4)
53
+ result["p_value"] = round(p_value, 4)
54
+
55
+ summary_stats = calculate_summary_statistics(data, group_column, data_column)
56
+ result["summary_statistics"] = summary_stats
57
+
58
+ # Get the model for normality assumption testing
59
+ formula = f"{data_column} ~ C({group_column})"
60
+ model = ols(formula, data=data).fit()
61
+
62
+ # Test model assumptions
63
+ normality_test_result = test_normality_assumption(model)
64
+ homogeneity_variances_result = test_variance_homogeneity_assumption(data, group_column, data_column)
65
+ result["normality_test"] = normality_test_result
66
+ result["homogeneity_of_variance_test"] = homogeneity_variances_result
67
+
68
+ if filename is not None:
69
+ dict_to_pdf(result, filename=filename)
70
+ return result
71
+
72
+
73
+ def ttest_dep(data: Union[Path, str, pd.DataFrame], group_column: str, data_column: str, repeated_measures_column: str, filename: Union[str, None] = 'ttest_dep_results.pdf') -> dict:
74
+ """Conduct a paired t-test and save results to a PDF.
75
+
76
+ Args:
77
+ data (Union[Path, str, pd.DataFrame]): The input data, which can be a file path, string, or DataFrame.
78
+ group_column (str): The name of the column containing group labels. If this is a one sample t-test, this should be an empty string.
79
+ data_column (str): The name of the column containing the data values.
80
+ repeated_measures_column (str, optional): The name of the column containing the repeated measures identifiers. Defaults to "".
81
+ filename (str, optional): The filename to save the results to. Defaults to 'ttest_dep_results.pdf'. If None, results are not saved to a file.
82
+ """
83
+
84
+ # Load the data
85
+ data = load_data_from_path(data)
86
+
87
+ groups = data[group_column].unique()
88
+ num_groups = len(groups)
89
+ if num_groups != 2:
90
+ raise ValueError("Data must contain exactly two groups for a paired t-test.")
91
+
92
+ # Pivot the data to have one column per group
93
+ pivot_data = data.pivot(index=repeated_measures_column, columns=group_column, values=data_column)
94
+ delta_column_name = f"{groups[0]}_minus_{groups[1]}"
95
+ pivot_data[delta_column_name] = pivot_data[groups[0]] - pivot_data[groups[1]]
96
+
97
+ # Perform paired t-test on the difference column
98
+ ttest_ind_result = ttest_ind(pivot_data, "", delta_column_name, filename=None)
99
+
100
+ # Calculate and add summary statistics for the repeated measures
101
+ # i.e. if there was a pre and post test per subject, this will provide a mean per subject
102
+ # repeated_measures_summary = calculate_summary_statistics(data, repeated_measures_column, delta_column_name)
103
+ # ttest_ind_result["repeated_measures_summary_statistics"] = repeated_measures_summary
104
+
105
+ if filename is not None:
106
+ dict_to_pdf(ttest_ind_result, filename=filename)
107
+ return ttest_ind_result
@@ -0,0 +1,45 @@
1
+ from typing import Union
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+
6
+ def load_data_from_path(data: Union[Path, str, pd.DataFrame]) -> pd.DataFrame:
7
+ """
8
+ Load data from a file path or return the DataFrame if already provided.
9
+
10
+ Parameters:
11
+ data : Union[Path, str, pd.DataFrame]
12
+ The file path to load data from or a DataFrame.
13
+
14
+ Returns:
15
+ pd.DataFrame
16
+ The loaded DataFrame.
17
+ """
18
+
19
+ if isinstance(data, pd.DataFrame):
20
+ return data
21
+
22
+ # Convert string paths to Path objects
23
+ if isinstance(data, str):
24
+ data = Path(data)
25
+
26
+ if isinstance(data, Path):
27
+ check_file_exists(data)
28
+ return pd.read_csv(data)
29
+ else:
30
+ raise ValueError("Input must be a file path or a pandas DataFrame.")
31
+
32
+
33
+ def check_file_exists(path: Union[Path, str]):
34
+ """
35
+ Check that the provided file path exists.
36
+
37
+ Parameters:
38
+ path : Union[Path, str]
39
+ The file path to check.
40
+ """
41
+ if isinstance(path, str):
42
+ path = Path(path)
43
+
44
+ if not path.exists():
45
+ raise ValueError(f"Unable to find a file at {path}")
@@ -0,0 +1,65 @@
1
+ import json
2
+ from typing import Union, Any
3
+
4
+ import pandas as pd
5
+ from reportlab.lib.pagesizes import letter
6
+ from reportlab.pdfgen import canvas
7
+ from reportlab.lib.units import inch
8
+ from reportlab.pdfbase import pdfmetrics
9
+ from reportlab.pdfbase.ttfonts import TTFont
10
+
11
+ def dict_to_pdf(data: dict, filename: Union[str, None] = 'output.pdf'):
12
+ """
13
+ Convert a dictionary to a formatted JSON string and save as PDF.
14
+
15
+ Args:
16
+ data: The dictionary to convert
17
+ filename: Output PDF filename (default: 'output.pdf')
18
+ """
19
+ # Convert NumPy types to native Python types for JSON serialization
20
+ def convert_types(obj: Any) -> Any:
21
+ if isinstance(obj, dict):
22
+ return {k: convert_types(v) for k, v in obj.items()}
23
+ elif isinstance(obj, (list, tuple)):
24
+ return [convert_types(item) for item in obj]
25
+ elif hasattr(obj, 'item'): # NumPy scalar types
26
+ return obj.item()
27
+ return obj
28
+
29
+ if filename is None:
30
+ return
31
+
32
+ filename = str(filename)
33
+
34
+ data = convert_types(data)
35
+
36
+ # Convert dictionary to formatted JSON string
37
+ json_str = json.dumps(data, indent=2)
38
+
39
+ # Create PDF
40
+ c = canvas.Canvas(filename, pagesize=letter)
41
+ width, height = letter
42
+
43
+ # Margins and setup
44
+ margin = 0.5 * inch
45
+ y_position = height - margin
46
+ line_height = 0.15 * inch
47
+
48
+ # Use monospace font for better JSON formatting
49
+ c.setFont("Courier", 9)
50
+
51
+ # Draw each line of the JSON
52
+ for line in json_str.split('\n'):
53
+ # Check if we need a new page
54
+ if y_position < margin:
55
+ c.showPage()
56
+ c.setFont("Courier", 9)
57
+ y_position = height - margin
58
+
59
+ # Draw the line
60
+ c.drawString(margin, y_position, line)
61
+ y_position -= line_height
62
+
63
+ # Save the PDF
64
+ c.save()
65
+ print(f"PDF saved as {filename}")
@@ -0,0 +1,62 @@
1
+
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+ def calculate_summary_statistics(data: pd.DataFrame, group_column: str, data_column: str) -> dict:
6
+ """
7
+ Calculate summary statistics for each group in the data. If there is only one group, grouped statistics will still be calculated.
8
+
9
+ Parameters:
10
+ data : pd.DataFrame
11
+ The input data containing the groups and values.
12
+ group_column : str
13
+ The name of the column containing group labels.
14
+ data_column : str
15
+ The name of the column containing data values.
16
+
17
+ Returns:
18
+ summary_stats: dict
19
+ A dictionary where keys are group names and values are dictionaries of summary statistics:
20
+ "count", "mean", "std", "min", "25%", "50%", "75%", "max"
21
+ """
22
+ summary_stats = {}
23
+ grouped = data.groupby(group_column)[data_column]
24
+
25
+ # Calculate summary statistics for each group
26
+ means = {name: group.mean() for name, group in grouped}
27
+ std_devs = {name: group.std() for name, group in grouped}
28
+ counts = {name: len(group) for name, group in grouped}
29
+ variances = {name: group.var() for name, group in grouped}
30
+ n_missing = {name: group.isnull().sum() for name, group in grouped}
31
+ mins = {name: group.min() for name, group in grouped}
32
+ maxs = {name: group.max() for name, group in grouped}
33
+
34
+ # Calculate quartiles
35
+ quartiles = {name: group.quantile([0.25, 0.5, 0.75]).to_dict() for name, group in grouped}
36
+
37
+ # Store group-wise statistics
38
+ summary_stats_grouped = {}
39
+ summary_stats_grouped["mean"] = means
40
+ summary_stats_grouped["std_dev"] = std_devs
41
+ summary_stats_grouped["count"] = counts
42
+ summary_stats_grouped["variance"] = variances
43
+ summary_stats_grouped["n_missing"] = n_missing
44
+ summary_stats_grouped["quartiles"] = quartiles
45
+ summary_stats_grouped["min"] = mins
46
+ summary_stats_grouped["max"] = maxs
47
+
48
+ # Store overall statistics
49
+ summary_stats_overall = {}
50
+ summary_stats_overall["mean"] = data[data_column].mean()
51
+ summary_stats_overall["std_dev"] = data[data_column].std()
52
+ summary_stats_overall["count"] = len(data)
53
+ summary_stats_overall["n_missing"] = data[data_column].isnull().sum()
54
+ summary_stats_overall["variance"] = data[data_column].var()
55
+ summary_stats_overall["quartiles"] = data[data_column].quantile([0.25,0.5, 0.75]).to_dict()
56
+ summary_stats_overall["min"] = data[data_column].min()
57
+ summary_stats_overall["max"] = data[data_column].max()
58
+
59
+ summary_stats["grouped"] = summary_stats_grouped
60
+ summary_stats["overall"] = summary_stats_overall
61
+
62
+ return summary_stats
@@ -0,0 +1,87 @@
1
+ from typing import Union
2
+
3
+ from scipy import stats
4
+ import pandas as pd
5
+ import pingouin as pg
6
+
7
+ def test_normality_assumption(model) -> dict:
8
+ """Test the normality assumption of residuals using the Shapiro-Wilk test.
9
+
10
+ Args:
11
+ model (_type_): The fitted statistical model from statsmodels.
12
+
13
+ Returns:
14
+ dict: The result of the Shapiro-Wilk test containing the test statistic and p-value.
15
+ """
16
+
17
+ # Test for normality
18
+ residuals = model.resid
19
+ normality_test_statistic, p_value_normality = stats.shapiro(residuals)
20
+
21
+ normality_result = {}
22
+ normality_result["shapiro_wilk"] = {"test_statistic": normality_test_statistic, "p_value": p_value_normality}
23
+
24
+ if p_value_normality < 0.05:
25
+ print(f"Warning: The Shapiro-Wilk test for normality returned a p-value of {p_value_normality:.4f}. A p-value less than 0.05 suggests that the residuals may not be normally distributed. Consider using a non-parametric test if normality is violated.")
26
+
27
+ return normality_result
28
+
29
+
30
+ def test_variance_homogeneity_assumption(data: pd.DataFrame, group_columns: Union[str, list], data_column: str) -> dict:
31
+ """Test the homogeneity of variances using Levene's test. For 2 or more factors, uses the interaction of all factors.
32
+
33
+ Args:
34
+ data (pd.DataFrame): The input data containing the groups and values.
35
+ group_columns (Union[str, list]): The name(s) of the column(s) containing group labels.
36
+ data_column (str): The name of the column containing data values.
37
+
38
+ Returns:
39
+ dict: The result of Levene's test containing the test statistic and p-value.
40
+ """
41
+
42
+ if isinstance(group_columns, str):
43
+ group_columns = [group_columns]
44
+
45
+ # Test for homogeneity of variances
46
+ data['combined_group'] = data[group_columns].astype(str).agg('_'.join, axis=1)
47
+ grouped_data = [group[data_column].values for name, group in data.groupby('combined_group')]
48
+ levene_statistic, p_value_levene = stats.levene(*grouped_data)
49
+
50
+ homogeneity_variances_result = {}
51
+ homogeneity_variances_result["levene"] = {"test_statistic": levene_statistic, "p_value": p_value_levene}
52
+
53
+ if p_value_levene < 0.05:
54
+ print(f"Warning: The Levene's test for homogeneity of variances returned a p-value of {p_value_levene:.4f}. A p-value less than 0.05 suggests that the variances across groups may not be equal. Consider using a non-parametric test if homogeneity of variances is violated.")
55
+
56
+ return homogeneity_variances_result
57
+
58
+
59
+ def test_sphericity_assumption(data: pd.DataFrame, group_columns: Union[str, list], subject_column: str, data_column: str) -> dict:
60
+ """Test the sphericity assumption using Mauchly's test.
61
+
62
+ Args:
63
+ data (pd.DataFrame): The input data containing the groups, subjects, and values.
64
+ group_column (str): The name of the column containing group labels.
65
+ subject_column (str): The name of the column containing subject identifiers.
66
+ data_column (str): The name of the column containing data values.
67
+
68
+ Returns:
69
+ dict: The result of Mauchly's test containing the test statistic and p-value.
70
+ """
71
+
72
+ if isinstance(group_columns, str):
73
+ group_columns = [group_columns]
74
+
75
+ # Perform Mauchly's test for sphericity using pingouin
76
+ mauchly_result = pg.sphericity(data, dv=data_column, subject=subject_column, within=group_columns)
77
+
78
+ sphericity_result = {}
79
+ sphericity_result["mauchly"] = {
80
+ "W": mauchly_result['W'].values[0],
81
+ "p_value": mauchly_result['pval'].values[0]
82
+ }
83
+
84
+ if mauchly_result['pval'].values[0] < 0.05:
85
+ print(f"Warning: Mauchly's test for sphericity returned a p-value of {mauchly_result['pval'].values[0]:.4f}. A p-value less than 0.05 suggests that the sphericity assumption may be violated. Consider applying a correction (e.g., Greenhouse-Geisser or Huynh-Feldt) to the ANOVA results.")
86
+
87
+ return sphericity_result
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: csv-stats
3
+ Version: 0.1.0
4
+ Summary: A Python package for the life sciences to conduct hypothesis testing from CSV files.
5
+ Author-email: Mitchell Tillman <mtillman14@gmail.com>
6
+ Maintainer-email: Mitchell Tillman <mtillman14@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/mtillman14/csv-stats
9
+ Project-URL: Documentation, https://github.com/mtillman14/csv-stats
10
+ Project-URL: Repository, https://github.com/mtillman14/csv-stats
11
+ Project-URL: Issues, https://github.com/mtillman14/csv-stats/issues
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Development Status :: 3 - Alpha
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pandas>=2.3.3
20
+ Requires-Dist: pingouin>=0.5.5
21
+ Requires-Dist: scipy>=1.16.2
22
+ Requires-Dist: statsmodels>=0.14.5
23
+
24
+ # csv-stats
25
+ Python package for rapid hypothesis testing on CSV files with data in long table format, which are common in the life sciences.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/csv-stats/__init__.py
4
+ src/csv-stats/anova.py
5
+ src/csv-stats/ttest.py
6
+ src/csv-stats/utils/load_data.py
7
+ src/csv-stats/utils/save_stats.py
8
+ src/csv-stats/utils/summary_stats.py
9
+ src/csv-stats/utils/test_assumptions.py
10
+ src/csv_stats.egg-info/PKG-INFO
11
+ src/csv_stats.egg-info/SOURCES.txt
12
+ src/csv_stats.egg-info/dependency_links.txt
13
+ src/csv_stats.egg-info/requires.txt
14
+ src/csv_stats.egg-info/top_level.txt
15
+ tests/test_anova1way.py
@@ -0,0 +1,4 @@
1
+ pandas>=2.3.3
2
+ pingouin>=0.5.5
3
+ scipy>=1.16.2
4
+ statsmodels>=0.14.5
@@ -0,0 +1 @@
1
+ csv-stats
@@ -0,0 +1,26 @@
1
+
2
+
3
+ from csv_stats.anova import anova1way
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ def test_anova1way():
8
+
9
+ # Create sample data
10
+ np.random.seed(0)
11
+ group_A = np.random.normal(loc=5.0, scale=1.0,
12
+ size=30)
13
+ group_B = np.random.normal(loc=6.0, scale=1.0, size=30)
14
+ data = np.concatenate([group_A, group_B])
15
+ groups = np.array(['A'] * 30 + ['B'] * 30)
16
+ data = pd.DataFrame({'group': groups, 'value': data})
17
+ data_column = 'value'
18
+ group_column = 'group'
19
+
20
+ # Perform one-way ANOVA using the function
21
+ result = anova1way(data, group_column=group_column, data_column=data_column)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ test_anova1way()
26
+ print("All tests passed.")