eagleplot 3.0.0__py3-none-any.whl
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.
- eagleplot/Home.py +362 -0
- eagleplot/__init__.py +19 -0
- eagleplot/annotations.py +215 -0
- eagleplot/assets/logo.ico +0 -0
- eagleplot/assets/logo.png +0 -0
- eagleplot/assets/nccu-horiz-logo.png +0 -0
- eagleplot/assets/nccu-wings.png +0 -0
- eagleplot/batch.py +446 -0
- eagleplot/cli.py +719 -0
- eagleplot/data_tools.py +849 -0
- eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
- eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
- eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
- eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
- eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
- eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
- eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
- eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
- eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
- eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
- eagleplot/gallery/box_plot.png +0 -0
- eagleplot/gallery/contour_plot.png +0 -0
- eagleplot/gallery/distribution_plot.png +0 -0
- eagleplot/gallery/heatmap.png +0 -0
- eagleplot/gallery/histogram.png +0 -0
- eagleplot/gallery/interactive_3d_surface.png +0 -0
- eagleplot/gallery/interactive_heatmap.png +0 -0
- eagleplot/gallery/interactive_histogram.png +0 -0
- eagleplot/gallery/interactive_line.png +0 -0
- eagleplot/gallery/interactive_scatter.png +0 -0
- eagleplot/gallery/line_plot.png +0 -0
- eagleplot/gallery/manifest.json +196 -0
- eagleplot/gallery/regression_plot.png +0 -0
- eagleplot/gallery/scatter_plot.png +0 -0
- eagleplot/io.py +1008 -0
- eagleplot/math.py +1210 -0
- eagleplot/molecular/__init__.py +21 -0
- eagleplot/molecular/atom_data.py +287 -0
- eagleplot/molecular/parsers.py +747 -0
- eagleplot/nist.py +241 -0
- eagleplot/pages/10_Molecular_Viz.py +225 -0
- eagleplot/pages/11_Batch_Analysis.py +325 -0
- eagleplot/pages/12_Help.py +199 -0
- eagleplot/pages/13_Settings.py +388 -0
- eagleplot/pages/1_Data_Upload.py +521 -0
- eagleplot/pages/2_Quick_Plot.py +1693 -0
- eagleplot/pages/3_Analysis_Tools.py +2510 -0
- eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
- eagleplot/pages/5_Advanced_Plotting.py +691 -0
- eagleplot/pages/6_Export_Results.py +442 -0
- eagleplot/pages/7_Gallery.py +201 -0
- eagleplot/pages/8_Data_Tools.py +434 -0
- eagleplot/pages/9_Spectroscopy.py +821 -0
- eagleplot/pages/__init__.py +8 -0
- eagleplot/peaks.py +513 -0
- eagleplot/plotting.py +2630 -0
- eagleplot/plugin_loader.py +208 -0
- eagleplot/report.py +448 -0
- eagleplot/signal.py +494 -0
- eagleplot/spectroscopy.py +830 -0
- eagleplot/utils/__init__.py +76 -0
- eagleplot/utils/access.py +77 -0
- eagleplot/utils/data_preview.py +352 -0
- eagleplot/utils/plot_config.py +2721 -0
- eagleplot/utils/session_state.py +535 -0
- eagleplot/utils/theming.py +312 -0
- eagleplot/utils/user_settings.py +294 -0
- eagleplot-3.0.0.dist-info/METADATA +304 -0
- eagleplot-3.0.0.dist-info/RECORD +73 -0
- eagleplot-3.0.0.dist-info/WHEEL +5 -0
- eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
- eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
- eagleplot-3.0.0.dist-info/top_level.txt +1 -0
eagleplot/math.py
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
"""Mathematical Operations Module.
|
|
2
|
+
|
|
3
|
+
This module provides mathematical and statistical functions for
|
|
4
|
+
scientific computing, including numerical operations, statistics,
|
|
5
|
+
and optimization routines.
|
|
6
|
+
|
|
7
|
+
Features
|
|
8
|
+
--------
|
|
9
|
+
- Numerical array operations (NumPy)
|
|
10
|
+
- Statistical analysis and distributions (SciPy)
|
|
11
|
+
- Optimization and fitting algorithms
|
|
12
|
+
- Linear algebra operations
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
--------
|
|
16
|
+
>>> from eagleplot.math import calculate_mean, fit_polynomial
|
|
17
|
+
>>> mean_value = calculate_mean(data)
|
|
18
|
+
>>> coefficients = fit_polynomial(x, y, degree=2)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from scipy import stats, optimize, linalg
|
|
23
|
+
from typing import Dict, Tuple, Callable, Optional, Union, Any
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Basic Statistics
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_mean(data: np.ndarray, axis: Optional[int] = None) -> Union[float, np.ndarray]:
|
|
31
|
+
"""Calculate the mean of data.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
data : np.ndarray
|
|
36
|
+
Input data
|
|
37
|
+
axis : int, optional
|
|
38
|
+
Axis along which to compute the mean. If None, compute over all data.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
mean : float or np.ndarray
|
|
43
|
+
Mean value(s)
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
48
|
+
>>> calculate_mean(data)
|
|
49
|
+
3.0
|
|
50
|
+
"""
|
|
51
|
+
return np.mean(data, axis=axis)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calculate_median(data: np.ndarray, axis: Optional[int] = None) -> Union[float, np.ndarray]:
|
|
55
|
+
"""Calculate the median of data.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
data : np.ndarray
|
|
60
|
+
Input data
|
|
61
|
+
axis : int, optional
|
|
62
|
+
Axis along which to compute the median. If None, compute over all data.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
median : float or np.ndarray
|
|
67
|
+
Median value(s)
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
72
|
+
>>> calculate_median(data)
|
|
73
|
+
3.0
|
|
74
|
+
"""
|
|
75
|
+
return np.median(data, axis=axis)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def calculate_std(
|
|
79
|
+
data: np.ndarray, axis: Optional[int] = None, ddof: int = 0
|
|
80
|
+
) -> Union[float, np.ndarray]:
|
|
81
|
+
"""Calculate the standard deviation of data.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
data : np.ndarray
|
|
86
|
+
Input data
|
|
87
|
+
axis : int, optional
|
|
88
|
+
Axis along which to compute the std. If None, compute over all data.
|
|
89
|
+
ddof : int, default 0
|
|
90
|
+
Delta degrees of freedom (0 for population, 1 for sample)
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
std : float or np.ndarray
|
|
95
|
+
Standard deviation value(s)
|
|
96
|
+
|
|
97
|
+
Examples
|
|
98
|
+
--------
|
|
99
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
100
|
+
>>> calculate_std(data, ddof=1)
|
|
101
|
+
1.5811388300841898
|
|
102
|
+
"""
|
|
103
|
+
return np.std(data, axis=axis, ddof=ddof)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def calculate_statistics(
|
|
107
|
+
data: np.ndarray, axis: Optional[int] = None
|
|
108
|
+
) -> Dict[str, Union[float, np.ndarray]]:
|
|
109
|
+
"""Calculate comprehensive statistics for data.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
data : np.ndarray
|
|
114
|
+
Input data
|
|
115
|
+
axis : int, optional
|
|
116
|
+
Axis along which to compute statistics. If None, compute over all data.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
stats : dict
|
|
121
|
+
Dictionary containing:
|
|
122
|
+
- 'mean': Mean value
|
|
123
|
+
- 'median': Median value
|
|
124
|
+
- 'std': Standard deviation (sample)
|
|
125
|
+
- 'var': Variance (sample)
|
|
126
|
+
- 'min': Minimum value
|
|
127
|
+
- 'max': Maximum value
|
|
128
|
+
- 'q1': First quartile (25th percentile)
|
|
129
|
+
- 'q3': Third quartile (75th percentile)
|
|
130
|
+
- 'iqr': Interquartile range
|
|
131
|
+
- 'range': Data range (max - min)
|
|
132
|
+
|
|
133
|
+
Examples
|
|
134
|
+
--------
|
|
135
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
136
|
+
>>> stats = calculate_statistics(data)
|
|
137
|
+
>>> print(f"Mean: {stats['mean']}, Std: {stats['std']}")
|
|
138
|
+
Mean: 3.0, Std: 1.5811388300841898
|
|
139
|
+
"""
|
|
140
|
+
return {
|
|
141
|
+
"mean": np.mean(data, axis=axis),
|
|
142
|
+
"median": np.median(data, axis=axis),
|
|
143
|
+
"std": np.std(data, axis=axis, ddof=1),
|
|
144
|
+
"var": np.var(data, axis=axis, ddof=1),
|
|
145
|
+
"min": np.min(data, axis=axis),
|
|
146
|
+
"max": np.max(data, axis=axis),
|
|
147
|
+
"q1": np.percentile(data, 25, axis=axis),
|
|
148
|
+
"q3": np.percentile(data, 75, axis=axis),
|
|
149
|
+
"iqr": np.percentile(data, 75, axis=axis) - np.percentile(data, 25, axis=axis),
|
|
150
|
+
"range": np.ptp(data, axis=axis), # peak-to-peak (max - min)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ============================================================================
|
|
155
|
+
# Distribution Analysis
|
|
156
|
+
# ============================================================================
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def check_normality(data: np.ndarray) -> Dict[str, float]:
|
|
160
|
+
"""Test if data follows a normal distribution using Shapiro-Wilk test.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
data : np.ndarray
|
|
165
|
+
1D array of data to test
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
result : dict
|
|
170
|
+
Dictionary containing:
|
|
171
|
+
- 'statistic': Test statistic
|
|
172
|
+
- 'p_value': P-value
|
|
173
|
+
- 'is_normal': Boolean (True if p > 0.05)
|
|
174
|
+
|
|
175
|
+
Examples
|
|
176
|
+
--------
|
|
177
|
+
>>> data = np.random.normal(0, 1, 100)
|
|
178
|
+
>>> result = check_normality(data)
|
|
179
|
+
>>> print(f"Is normal: {result['is_normal']}")
|
|
180
|
+
"""
|
|
181
|
+
data = np.asarray(data).flatten()
|
|
182
|
+
statistic, p_value = stats.shapiro(data)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"statistic": float(statistic),
|
|
186
|
+
"p_value": float(p_value),
|
|
187
|
+
"is_normal": bool(p_value > 0.05),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def fit_distribution(data: np.ndarray, distribution: str = "norm") -> Dict[str, Any]:
|
|
192
|
+
"""Fit a statistical distribution to data.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
data : np.ndarray
|
|
197
|
+
1D array of data to fit
|
|
198
|
+
distribution : str, default 'norm'
|
|
199
|
+
Distribution name. Options: 'norm', 'expon', 'gamma', 'lognorm', 'beta'
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
result : dict
|
|
204
|
+
Dictionary containing:
|
|
205
|
+
- 'params': Fitted distribution parameters
|
|
206
|
+
- 'distribution': Distribution name
|
|
207
|
+
- 'ks_statistic': Kolmogorov-Smirnov test statistic
|
|
208
|
+
- 'ks_pvalue': KS test p-value
|
|
209
|
+
|
|
210
|
+
Raises
|
|
211
|
+
------
|
|
212
|
+
ValueError
|
|
213
|
+
If distribution name is not supported
|
|
214
|
+
|
|
215
|
+
Examples
|
|
216
|
+
--------
|
|
217
|
+
>>> data = np.random.normal(5, 2, 1000)
|
|
218
|
+
>>> result = fit_distribution(data, 'norm')
|
|
219
|
+
>>> print(f"Mean: {result['params'][0]:.2f}, Std: {result['params'][1]:.2f}")
|
|
220
|
+
"""
|
|
221
|
+
data = np.asarray(data).flatten()
|
|
222
|
+
|
|
223
|
+
# Get distribution from scipy.stats
|
|
224
|
+
dist_map = {
|
|
225
|
+
"norm": stats.norm,
|
|
226
|
+
"expon": stats.expon,
|
|
227
|
+
"gamma": stats.gamma,
|
|
228
|
+
"lognorm": stats.lognorm,
|
|
229
|
+
"beta": stats.beta,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if distribution not in dist_map:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Unsupported distribution: {distribution}. Supported: {list(dist_map.keys())}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
dist = dist_map[distribution]
|
|
238
|
+
|
|
239
|
+
# Fit distribution
|
|
240
|
+
params = dist.fit(data)
|
|
241
|
+
|
|
242
|
+
# Perform Kolmogorov-Smirnov test
|
|
243
|
+
ks_stat, ks_pval = stats.kstest(data, lambda x: dist.cdf(x, *params))
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"params": params,
|
|
247
|
+
"distribution": distribution,
|
|
248
|
+
"ks_statistic": float(ks_stat),
|
|
249
|
+
"ks_pvalue": float(ks_pval),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ============================================================================
|
|
254
|
+
# Curve Fitting
|
|
255
|
+
# ============================================================================
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def fit_polynomial(x: np.ndarray, y: np.ndarray, degree: int) -> Dict[str, Any]:
|
|
259
|
+
"""Fit polynomial to data and return coefficients with quality metrics.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
x : np.ndarray
|
|
264
|
+
Independent variable
|
|
265
|
+
y : np.ndarray
|
|
266
|
+
Dependent variable
|
|
267
|
+
degree : int
|
|
268
|
+
Polynomial degree
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
result : dict
|
|
273
|
+
Dictionary containing:
|
|
274
|
+
- 'coefficients': Polynomial coefficients (highest degree first)
|
|
275
|
+
- 'degree': Polynomial degree
|
|
276
|
+
- 'r_squared': R² coefficient of determination
|
|
277
|
+
- 'residuals': Sum of squared residuals
|
|
278
|
+
- 'predict': Function to predict y values for new x
|
|
279
|
+
|
|
280
|
+
Examples
|
|
281
|
+
--------
|
|
282
|
+
>>> x = np.linspace(0, 10, 50)
|
|
283
|
+
>>> y = 2*x + 3 + np.random.normal(0, 1, 50)
|
|
284
|
+
>>> result = fit_polynomial(x, y, degree=1)
|
|
285
|
+
>>> print(f"R²: {result['r_squared']:.3f}")
|
|
286
|
+
"""
|
|
287
|
+
x = np.asarray(x)
|
|
288
|
+
y = np.asarray(y)
|
|
289
|
+
|
|
290
|
+
# Fit polynomial
|
|
291
|
+
coeffs = np.polyfit(x, y, degree)
|
|
292
|
+
|
|
293
|
+
# Calculate predictions and R²
|
|
294
|
+
y_pred = np.polyval(coeffs, x)
|
|
295
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
296
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
297
|
+
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0.0
|
|
298
|
+
|
|
299
|
+
# Create prediction function
|
|
300
|
+
def predict(x_new):
|
|
301
|
+
return np.polyval(coeffs, x_new)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"coefficients": coeffs,
|
|
305
|
+
"degree": degree,
|
|
306
|
+
"r_squared": float(r_squared),
|
|
307
|
+
"residuals": float(ss_res),
|
|
308
|
+
"predict": predict,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def fit_linear(x: np.ndarray, y: np.ndarray) -> Dict[str, float]:
|
|
313
|
+
"""Fit linear model y = mx + b and return parameters with statistics.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
x : np.ndarray
|
|
318
|
+
Independent variable
|
|
319
|
+
y : np.ndarray
|
|
320
|
+
Dependent variable
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
result : dict
|
|
325
|
+
Dictionary containing:
|
|
326
|
+
- 'slope': Slope (m)
|
|
327
|
+
- 'intercept': Intercept (b)
|
|
328
|
+
- 'r_value': Correlation coefficient
|
|
329
|
+
- 'r_squared': R² coefficient of determination
|
|
330
|
+
- 'p_value': P-value for hypothesis test
|
|
331
|
+
- 'std_err': Standard error of the estimate
|
|
332
|
+
|
|
333
|
+
Examples
|
|
334
|
+
--------
|
|
335
|
+
>>> x = np.array([1, 2, 3, 4, 5])
|
|
336
|
+
>>> y = np.array([2, 4, 5, 4, 5])
|
|
337
|
+
>>> result = fit_linear(x, y)
|
|
338
|
+
>>> print(f"y = {result['slope']:.2f}x + {result['intercept']:.2f}")
|
|
339
|
+
"""
|
|
340
|
+
x = np.asarray(x).flatten()
|
|
341
|
+
y = np.asarray(y).flatten()
|
|
342
|
+
|
|
343
|
+
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"slope": float(slope),
|
|
347
|
+
"intercept": float(intercept),
|
|
348
|
+
"r_value": float(r_value),
|
|
349
|
+
"r_squared": float(r_value**2),
|
|
350
|
+
"p_value": float(p_value),
|
|
351
|
+
"std_err": float(std_err),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def fit_exponential(x: np.ndarray, y: np.ndarray) -> Dict[str, Any]:
|
|
356
|
+
"""Fit exponential model y = a * exp(b * x) + c.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
x : np.ndarray
|
|
361
|
+
Independent variable
|
|
362
|
+
y : np.ndarray
|
|
363
|
+
Dependent variable (must be positive)
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
result : dict
|
|
368
|
+
Dictionary containing:
|
|
369
|
+
- 'a': Amplitude parameter
|
|
370
|
+
- 'b': Exponential rate parameter
|
|
371
|
+
- 'c': Offset parameter
|
|
372
|
+
- 'r_squared': R² coefficient of determination
|
|
373
|
+
- 'predict': Function to predict y values for new x
|
|
374
|
+
|
|
375
|
+
Examples
|
|
376
|
+
--------
|
|
377
|
+
>>> x = np.linspace(0, 2, 50)
|
|
378
|
+
>>> y = 2 * np.exp(0.5 * x) + 1 + np.random.normal(0, 0.1, 50)
|
|
379
|
+
>>> result = fit_exponential(x, y)
|
|
380
|
+
>>> print(f"a={result['a']:.2f}, b={result['b']:.2f}, c={result['c']:.2f}")
|
|
381
|
+
"""
|
|
382
|
+
x = np.asarray(x)
|
|
383
|
+
y = np.asarray(y)
|
|
384
|
+
|
|
385
|
+
# Define exponential function
|
|
386
|
+
def exp_func(x, a, b, c):
|
|
387
|
+
return a * np.exp(b * x) + c
|
|
388
|
+
|
|
389
|
+
# Initial guess
|
|
390
|
+
p0 = [1.0, 0.1, 0.0]
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
# Fit using curve_fit
|
|
394
|
+
popt, _ = optimize.curve_fit(exp_func, x, y, p0=p0, maxfev=10000)
|
|
395
|
+
|
|
396
|
+
# Calculate R²
|
|
397
|
+
y_pred = exp_func(x, *popt)
|
|
398
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
399
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
400
|
+
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0.0
|
|
401
|
+
|
|
402
|
+
# Create prediction function
|
|
403
|
+
def predict(x_new):
|
|
404
|
+
return exp_func(x_new, *popt)
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"a": float(popt[0]),
|
|
408
|
+
"b": float(popt[1]),
|
|
409
|
+
"c": float(popt[2]),
|
|
410
|
+
"r_squared": float(r_squared),
|
|
411
|
+
"predict": predict,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
except RuntimeError as e:
|
|
415
|
+
raise RuntimeError(
|
|
416
|
+
"Exponential fit failed to converge. Try adjusting initial data or ensure y > 0."
|
|
417
|
+
) from e
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def fit_custom(
|
|
421
|
+
x: np.ndarray, y: np.ndarray, func: Callable, p0: Optional[list] = None
|
|
422
|
+
) -> Dict[str, Any]:
|
|
423
|
+
"""Fit custom function to data using non-linear least squares.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
x : np.ndarray
|
|
428
|
+
Independent variable
|
|
429
|
+
y : np.ndarray
|
|
430
|
+
Dependent variable
|
|
431
|
+
func : callable
|
|
432
|
+
Function to fit. Must have signature ``func(x, *params)``
|
|
433
|
+
p0 : list, optional
|
|
434
|
+
Initial guess for parameters
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
result : dict
|
|
439
|
+
Dictionary containing:
|
|
440
|
+
- 'parameters': Optimized parameters
|
|
441
|
+
- 'covariance': Covariance matrix
|
|
442
|
+
- 'std_errors': Standard errors of parameters
|
|
443
|
+
- 'r_squared': R² coefficient of determination
|
|
444
|
+
- 'predict': Function to predict y values for new x
|
|
445
|
+
|
|
446
|
+
Examples
|
|
447
|
+
--------
|
|
448
|
+
>>> def gaussian(x, amp, mu, sigma):
|
|
449
|
+
... return amp * np.exp(-(x - mu)**2 / (2 * sigma**2))
|
|
450
|
+
>>> x = np.linspace(-5, 5, 100)
|
|
451
|
+
>>> y = gaussian(x, 10, 0, 1) + np.random.normal(0, 0.5, 100)
|
|
452
|
+
>>> result = fit_custom(x, y, gaussian, p0=[8, 0, 1])
|
|
453
|
+
"""
|
|
454
|
+
x = np.asarray(x)
|
|
455
|
+
y = np.asarray(y)
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
# Fit using curve_fit
|
|
459
|
+
popt, pcov = optimize.curve_fit(func, x, y, p0=p0)
|
|
460
|
+
|
|
461
|
+
# Calculate standard errors
|
|
462
|
+
perr = np.sqrt(np.diag(pcov))
|
|
463
|
+
|
|
464
|
+
# Calculate R²
|
|
465
|
+
y_pred = func(x, *popt)
|
|
466
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
467
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
468
|
+
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0.0
|
|
469
|
+
|
|
470
|
+
# Create prediction function
|
|
471
|
+
def predict(x_new):
|
|
472
|
+
return func(x_new, *popt)
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
"parameters": popt,
|
|
476
|
+
"covariance": pcov,
|
|
477
|
+
"std_errors": perr,
|
|
478
|
+
"r_squared": float(r_squared),
|
|
479
|
+
"predict": predict,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
except RuntimeError as e:
|
|
483
|
+
raise RuntimeError(
|
|
484
|
+
"Custom fit failed to converge. "
|
|
485
|
+
"Try adjusting initial guess (p0) or check your function."
|
|
486
|
+
) from e
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ============================================================================
|
|
490
|
+
# Optimization
|
|
491
|
+
# ============================================================================
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def minimize_function(
|
|
495
|
+
func: Callable,
|
|
496
|
+
x0: np.ndarray,
|
|
497
|
+
method: str = "Nelder-Mead",
|
|
498
|
+
bounds: Optional[list] = None,
|
|
499
|
+
) -> Dict[str, Any]:
|
|
500
|
+
"""Minimize a scalar function using scipy.optimize.minimize.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
func : callable
|
|
505
|
+
Objective function to minimize
|
|
506
|
+
x0 : np.ndarray
|
|
507
|
+
Initial guess
|
|
508
|
+
method : str, default 'Nelder-Mead'
|
|
509
|
+
Optimization method. Options: 'Nelder-Mead', 'BFGS', 'L-BFGS-B', etc.
|
|
510
|
+
bounds : list of tuples, optional
|
|
511
|
+
Bounds for variables (for methods that support bounds)
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
result : dict
|
|
516
|
+
Dictionary containing:
|
|
517
|
+
- 'x': Optimal parameters
|
|
518
|
+
- 'fun': Function value at optimum
|
|
519
|
+
- 'success': Whether optimization succeeded
|
|
520
|
+
- 'message': Termination message
|
|
521
|
+
- 'nfev': Number of function evaluations
|
|
522
|
+
|
|
523
|
+
Examples
|
|
524
|
+
--------
|
|
525
|
+
>>> def rosenbrock(x):
|
|
526
|
+
... return (1 - x[0])**2 + 100*(x[1] - x[0]**2)**2
|
|
527
|
+
>>> result = minimize_function(rosenbrock, x0=[0, 0], method='BFGS')
|
|
528
|
+
>>> print(f"Minimum at: {result['x']}")
|
|
529
|
+
"""
|
|
530
|
+
x0 = np.asarray(x0)
|
|
531
|
+
|
|
532
|
+
# Perform optimization
|
|
533
|
+
res = optimize.minimize(func, x0, method=method, bounds=bounds)
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
"x": res.x,
|
|
537
|
+
"fun": float(res.fun),
|
|
538
|
+
"success": bool(res.success),
|
|
539
|
+
"message": str(res.message),
|
|
540
|
+
"nfev": int(res.nfev) if hasattr(res, "nfev") else None,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def find_roots(func: Callable, bracket: Tuple[float, float]) -> Dict[str, Any]:
|
|
545
|
+
"""Find root of a function in given bracket using Brent's method.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
func : callable
|
|
550
|
+
Function for which to find root
|
|
551
|
+
bracket : tuple of (a, b)
|
|
552
|
+
Bracketing interval [a, b] where func(a) and func(b) have opposite signs
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
result : dict
|
|
557
|
+
Dictionary containing:
|
|
558
|
+
- 'root': Root of the function
|
|
559
|
+
- 'function_value': Function value at root
|
|
560
|
+
- 'iterations': Number of iterations
|
|
561
|
+
- 'converged': Whether root finding converged
|
|
562
|
+
|
|
563
|
+
Examples
|
|
564
|
+
--------
|
|
565
|
+
>>> func = lambda x: x**2 - 4
|
|
566
|
+
>>> result = find_roots(func, bracket=(0, 3))
|
|
567
|
+
>>> print(f"Root: {result['root']:.4f}")
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
root, res = optimize.brentq(func, bracket[0], bracket[1], full_output=True)
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
"root": float(root),
|
|
574
|
+
"function_value": float(res.function_calls),
|
|
575
|
+
"iterations": int(res.iterations),
|
|
576
|
+
"converged": bool(res.converged),
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
except ValueError as e:
|
|
580
|
+
raise ValueError(
|
|
581
|
+
"Root finding failed. Ensure func(a) and func(b) have opposite signs."
|
|
582
|
+
) from e
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ============================================================================
|
|
586
|
+
# Linear Algebra
|
|
587
|
+
# ============================================================================
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def compute_eigenvalues(matrix: np.ndarray, eigenvectors: bool = True) -> Dict[str, np.ndarray]:
|
|
591
|
+
"""Compute eigenvalues and optionally eigenvectors of a matrix.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
matrix : np.ndarray
|
|
596
|
+
Square matrix
|
|
597
|
+
eigenvectors : bool, default True
|
|
598
|
+
Whether to compute eigenvectors
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
result : dict
|
|
603
|
+
Dictionary containing:
|
|
604
|
+
- 'eigenvalues': Array of eigenvalues
|
|
605
|
+
- 'eigenvectors': Matrix of eigenvectors (if requested)
|
|
606
|
+
|
|
607
|
+
Examples
|
|
608
|
+
--------
|
|
609
|
+
>>> A = np.array([[1, 2], [2, 1]])
|
|
610
|
+
>>> result = compute_eigenvalues(A)
|
|
611
|
+
>>> print(f"Eigenvalues: {result['eigenvalues']}")
|
|
612
|
+
"""
|
|
613
|
+
matrix = np.asarray(matrix)
|
|
614
|
+
|
|
615
|
+
if matrix.shape[0] != matrix.shape[1]:
|
|
616
|
+
raise ValueError("Matrix must be square")
|
|
617
|
+
|
|
618
|
+
if eigenvectors:
|
|
619
|
+
eigenvals, eigenvecs = linalg.eig(matrix)
|
|
620
|
+
return {"eigenvalues": eigenvals, "eigenvectors": eigenvecs}
|
|
621
|
+
else:
|
|
622
|
+
eigenvals = linalg.eigvals(matrix)
|
|
623
|
+
return {"eigenvalues": eigenvals}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def solve_linear_system(A: np.ndarray, b: np.ndarray) -> Dict[str, Any]:
|
|
627
|
+
"""Solve linear system Ax = b.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
A : np.ndarray
|
|
632
|
+
Coefficient matrix
|
|
633
|
+
b : np.ndarray
|
|
634
|
+
Right-hand side vector
|
|
635
|
+
|
|
636
|
+
Returns
|
|
637
|
+
-------
|
|
638
|
+
result : dict
|
|
639
|
+
Dictionary containing:
|
|
640
|
+
- 'x': Solution vector
|
|
641
|
+
- 'residual': Residual norm
|
|
642
|
+
- 'condition_number': Condition number of A
|
|
643
|
+
|
|
644
|
+
Examples
|
|
645
|
+
--------
|
|
646
|
+
>>> A = np.array([[3, 1], [1, 2]])
|
|
647
|
+
>>> b = np.array([9, 8])
|
|
648
|
+
>>> result = solve_linear_system(A, b)
|
|
649
|
+
>>> print(f"Solution: {result['x']}")
|
|
650
|
+
"""
|
|
651
|
+
A = np.asarray(A)
|
|
652
|
+
b = np.asarray(b)
|
|
653
|
+
|
|
654
|
+
# Solve system
|
|
655
|
+
x = linalg.solve(A, b)
|
|
656
|
+
|
|
657
|
+
# Calculate residual
|
|
658
|
+
residual = linalg.norm(A @ x - b)
|
|
659
|
+
|
|
660
|
+
# Calculate condition number
|
|
661
|
+
cond = np.linalg.cond(A)
|
|
662
|
+
|
|
663
|
+
return {"x": x, "residual": float(residual), "condition_number": float(cond)}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def matrix_decomposition(matrix: np.ndarray, method: str = "svd") -> Dict[str, np.ndarray]:
|
|
667
|
+
"""Perform matrix decomposition.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
matrix : np.ndarray
|
|
672
|
+
Input matrix
|
|
673
|
+
method : str, default 'svd'
|
|
674
|
+
Decomposition method. Options: 'svd', 'qr', 'lu', 'cholesky'
|
|
675
|
+
|
|
676
|
+
Returns
|
|
677
|
+
-------
|
|
678
|
+
result : dict
|
|
679
|
+
Dictionary with decomposition components (depends on method)
|
|
680
|
+
- 'svd': {'U', 'S', 'Vh'}
|
|
681
|
+
- 'qr': {'Q', 'R'}
|
|
682
|
+
- 'lu': {'P', 'L', 'U'}
|
|
683
|
+
- 'cholesky': {'L'}
|
|
684
|
+
|
|
685
|
+
Examples
|
|
686
|
+
--------
|
|
687
|
+
>>> A = np.random.rand(5, 3)
|
|
688
|
+
>>> result = matrix_decomposition(A, method='svd')
|
|
689
|
+
>>> U, S, Vh = result['U'], result['S'], result['Vh']
|
|
690
|
+
"""
|
|
691
|
+
matrix = np.asarray(matrix)
|
|
692
|
+
|
|
693
|
+
if method == "svd":
|
|
694
|
+
U, S, Vh = linalg.svd(matrix)
|
|
695
|
+
return {"U": U, "S": S, "Vh": Vh}
|
|
696
|
+
|
|
697
|
+
elif method == "qr":
|
|
698
|
+
Q, R = linalg.qr(matrix)
|
|
699
|
+
return {"Q": Q, "R": R}
|
|
700
|
+
|
|
701
|
+
elif method == "lu":
|
|
702
|
+
P, L, U = linalg.lu(matrix)
|
|
703
|
+
return {"P": P, "L": L, "U": U}
|
|
704
|
+
|
|
705
|
+
elif method == "cholesky":
|
|
706
|
+
if matrix.shape[0] != matrix.shape[1]:
|
|
707
|
+
raise ValueError("Cholesky decomposition requires square matrix")
|
|
708
|
+
L = linalg.cholesky(matrix, lower=True)
|
|
709
|
+
return {"L": L}
|
|
710
|
+
|
|
711
|
+
else:
|
|
712
|
+
raise ValueError(f"Unsupported method: {method}. Supported: 'svd', 'qr', 'lu', 'cholesky'")
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# ============================================================================
|
|
716
|
+
# Statistical Tests (M11)
|
|
717
|
+
# ============================================================================
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def ttest_one_sample(data: np.ndarray, popmean: float, alpha: float = 0.05) -> Dict[str, Any]:
|
|
721
|
+
"""One-sample t-test against a known population mean.
|
|
722
|
+
|
|
723
|
+
Parameters
|
|
724
|
+
----------
|
|
725
|
+
data : np.ndarray
|
|
726
|
+
Sample data.
|
|
727
|
+
popmean : float
|
|
728
|
+
Hypothesized population mean (H₀: μ = popmean).
|
|
729
|
+
alpha : float, default 0.05
|
|
730
|
+
Significance level.
|
|
731
|
+
|
|
732
|
+
Returns
|
|
733
|
+
-------
|
|
734
|
+
result : dict
|
|
735
|
+
t_statistic, p_value, df, reject_null, ci_low, ci_high, mean, std, n.
|
|
736
|
+
"""
|
|
737
|
+
data = np.asarray(data).flatten()
|
|
738
|
+
t_stat, p_value = stats.ttest_1samp(data, popmean)
|
|
739
|
+
n = len(data)
|
|
740
|
+
df = n - 1
|
|
741
|
+
se = stats.sem(data)
|
|
742
|
+
ci = stats.t.interval(1 - alpha, df, loc=float(np.mean(data)), scale=se)
|
|
743
|
+
return {
|
|
744
|
+
"t_statistic": float(t_stat),
|
|
745
|
+
"p_value": float(p_value),
|
|
746
|
+
"df": int(df),
|
|
747
|
+
"reject_null": bool(p_value < alpha),
|
|
748
|
+
"alpha": float(alpha),
|
|
749
|
+
"ci_low": float(ci[0]),
|
|
750
|
+
"ci_high": float(ci[1]),
|
|
751
|
+
"mean": float(np.mean(data)),
|
|
752
|
+
"std": float(np.std(data, ddof=1)),
|
|
753
|
+
"n": int(n),
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def ttest_two_sample(
|
|
758
|
+
a: np.ndarray,
|
|
759
|
+
b: np.ndarray,
|
|
760
|
+
equal_var: bool = True,
|
|
761
|
+
alpha: float = 0.05,
|
|
762
|
+
) -> Dict[str, Any]:
|
|
763
|
+
"""Independent two-sample t-test.
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
a, b : np.ndarray
|
|
768
|
+
Two independent samples.
|
|
769
|
+
equal_var : bool, default True
|
|
770
|
+
If True, Student's t-test (equal variances). If False, Welch's t-test.
|
|
771
|
+
alpha : float, default 0.05
|
|
772
|
+
Significance level.
|
|
773
|
+
|
|
774
|
+
Returns
|
|
775
|
+
-------
|
|
776
|
+
result : dict
|
|
777
|
+
t_statistic, p_value, df, reject_null, effect_size_cohens_d,
|
|
778
|
+
mean_a, mean_b, std_a, std_b, n_a, n_b, test_type.
|
|
779
|
+
"""
|
|
780
|
+
a = np.asarray(a).flatten()
|
|
781
|
+
b = np.asarray(b).flatten()
|
|
782
|
+
t_stat, p_value = stats.ttest_ind(a, b, equal_var=equal_var)
|
|
783
|
+
na, nb = len(a), len(b)
|
|
784
|
+
pooled_std = np.sqrt(
|
|
785
|
+
((na - 1) * np.var(a, ddof=1) + (nb - 1) * np.var(b, ddof=1)) / (na + nb - 2)
|
|
786
|
+
)
|
|
787
|
+
cohens_d = (
|
|
788
|
+
(float(np.mean(a)) - float(np.mean(b))) / float(pooled_std) if pooled_std > 0 else 0.0
|
|
789
|
+
)
|
|
790
|
+
df: Optional[int] = int(na + nb - 2) if equal_var else None
|
|
791
|
+
return {
|
|
792
|
+
"t_statistic": float(t_stat),
|
|
793
|
+
"p_value": float(p_value),
|
|
794
|
+
"df": df,
|
|
795
|
+
"reject_null": bool(p_value < alpha),
|
|
796
|
+
"alpha": float(alpha),
|
|
797
|
+
"effect_size_cohens_d": float(cohens_d),
|
|
798
|
+
"mean_a": float(np.mean(a)),
|
|
799
|
+
"mean_b": float(np.mean(b)),
|
|
800
|
+
"std_a": float(np.std(a, ddof=1)),
|
|
801
|
+
"std_b": float(np.std(b, ddof=1)),
|
|
802
|
+
"n_a": int(na),
|
|
803
|
+
"n_b": int(nb),
|
|
804
|
+
"test_type": "Student" if equal_var else "Welch",
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def ttest_paired(a: np.ndarray, b: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
809
|
+
"""Paired t-test.
|
|
810
|
+
|
|
811
|
+
Parameters
|
|
812
|
+
----------
|
|
813
|
+
a, b : np.ndarray
|
|
814
|
+
Two matched samples (must be the same length).
|
|
815
|
+
alpha : float, default 0.05
|
|
816
|
+
Significance level.
|
|
817
|
+
|
|
818
|
+
Returns
|
|
819
|
+
-------
|
|
820
|
+
result : dict
|
|
821
|
+
t_statistic, p_value, df, reject_null, mean_diff, std_diff, n.
|
|
822
|
+
"""
|
|
823
|
+
a = np.asarray(a).flatten()
|
|
824
|
+
b = np.asarray(b).flatten()
|
|
825
|
+
t_stat, p_value = stats.ttest_rel(a, b)
|
|
826
|
+
diff = a - b
|
|
827
|
+
n = len(diff)
|
|
828
|
+
return {
|
|
829
|
+
"t_statistic": float(t_stat),
|
|
830
|
+
"p_value": float(p_value),
|
|
831
|
+
"df": int(n - 1),
|
|
832
|
+
"reject_null": bool(p_value < alpha),
|
|
833
|
+
"alpha": float(alpha),
|
|
834
|
+
"mean_diff": float(np.mean(diff)),
|
|
835
|
+
"std_diff": float(np.std(diff, ddof=1)),
|
|
836
|
+
"n": int(n),
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def mannwhitney_u(a: np.ndarray, b: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
841
|
+
"""Mann-Whitney U test (nonparametric two-sample test).
|
|
842
|
+
|
|
843
|
+
Parameters
|
|
844
|
+
----------
|
|
845
|
+
a, b : np.ndarray
|
|
846
|
+
Two independent samples.
|
|
847
|
+
alpha : float, default 0.05
|
|
848
|
+
Significance level.
|
|
849
|
+
|
|
850
|
+
Returns
|
|
851
|
+
-------
|
|
852
|
+
result : dict
|
|
853
|
+
u_statistic, p_value, reject_null, n_a, n_b.
|
|
854
|
+
"""
|
|
855
|
+
a = np.asarray(a).flatten()
|
|
856
|
+
b = np.asarray(b).flatten()
|
|
857
|
+
u_stat, p_value = stats.mannwhitneyu(a, b, alternative="two-sided")
|
|
858
|
+
return {
|
|
859
|
+
"u_statistic": float(u_stat),
|
|
860
|
+
"p_value": float(p_value),
|
|
861
|
+
"reject_null": bool(p_value < alpha),
|
|
862
|
+
"alpha": float(alpha),
|
|
863
|
+
"n_a": int(len(a)),
|
|
864
|
+
"n_b": int(len(b)),
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def wilcoxon_signed_rank(a: np.ndarray, b: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
869
|
+
"""Wilcoxon signed-rank test (nonparametric paired test).
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
a, b : np.ndarray
|
|
874
|
+
Two matched samples (must be the same length).
|
|
875
|
+
alpha : float, default 0.05
|
|
876
|
+
Significance level.
|
|
877
|
+
|
|
878
|
+
Returns
|
|
879
|
+
-------
|
|
880
|
+
result : dict
|
|
881
|
+
statistic, p_value, reject_null, n.
|
|
882
|
+
"""
|
|
883
|
+
a = np.asarray(a).flatten()
|
|
884
|
+
b = np.asarray(b).flatten()
|
|
885
|
+
stat, p_value = stats.wilcoxon(a, b)
|
|
886
|
+
return {
|
|
887
|
+
"statistic": float(stat),
|
|
888
|
+
"p_value": float(p_value),
|
|
889
|
+
"reject_null": bool(p_value < alpha),
|
|
890
|
+
"alpha": float(alpha),
|
|
891
|
+
"n": int(len(a)),
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def kruskal_wallis(*groups: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
896
|
+
"""Kruskal-Wallis H test (nonparametric one-way ANOVA equivalent).
|
|
897
|
+
|
|
898
|
+
Parameters
|
|
899
|
+
----------
|
|
900
|
+
groups : np.ndarray
|
|
901
|
+
Two or more independent samples (variadic positional arguments).
|
|
902
|
+
alpha : float, default 0.05
|
|
903
|
+
Significance level.
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
result : dict
|
|
908
|
+
h_statistic, p_value, df, reject_null, n_groups, group_sizes.
|
|
909
|
+
"""
|
|
910
|
+
arrays = [np.asarray(g).flatten() for g in groups]
|
|
911
|
+
h_stat, p_value = stats.kruskal(*arrays)
|
|
912
|
+
return {
|
|
913
|
+
"h_statistic": float(h_stat),
|
|
914
|
+
"p_value": float(p_value),
|
|
915
|
+
"df": int(len(arrays) - 1),
|
|
916
|
+
"reject_null": bool(p_value < alpha),
|
|
917
|
+
"alpha": float(alpha),
|
|
918
|
+
"n_groups": int(len(arrays)),
|
|
919
|
+
"group_sizes": [int(len(g)) for g in arrays],
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def anova_oneway(*groups: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
924
|
+
"""One-way analysis of variance (ANOVA).
|
|
925
|
+
|
|
926
|
+
Parameters
|
|
927
|
+
----------
|
|
928
|
+
groups : np.ndarray
|
|
929
|
+
Two or more independent samples (variadic positional arguments).
|
|
930
|
+
alpha : float, default 0.05
|
|
931
|
+
Significance level.
|
|
932
|
+
|
|
933
|
+
Returns
|
|
934
|
+
-------
|
|
935
|
+
result : dict
|
|
936
|
+
f_statistic, p_value, df_between, df_within, reject_null,
|
|
937
|
+
eta_squared, n_groups, group_sizes.
|
|
938
|
+
"""
|
|
939
|
+
arrays = [np.asarray(g).flatten() for g in groups]
|
|
940
|
+
f_stat, p_value = stats.f_oneway(*arrays)
|
|
941
|
+
k = len(arrays)
|
|
942
|
+
N = sum(len(g) for g in arrays)
|
|
943
|
+
grand_mean = float(np.mean(np.concatenate(arrays)))
|
|
944
|
+
ss_between = float(sum(len(g) * (float(np.mean(g)) - grand_mean) ** 2 for g in arrays))
|
|
945
|
+
ss_total = float(sum(float(np.sum((g - grand_mean) ** 2)) for g in arrays))
|
|
946
|
+
eta_squared = ss_between / ss_total if ss_total > 0 else 0.0
|
|
947
|
+
return {
|
|
948
|
+
"f_statistic": float(f_stat),
|
|
949
|
+
"p_value": float(p_value),
|
|
950
|
+
"df_between": int(k - 1),
|
|
951
|
+
"df_within": int(N - k),
|
|
952
|
+
"reject_null": bool(p_value < alpha),
|
|
953
|
+
"alpha": float(alpha),
|
|
954
|
+
"eta_squared": float(eta_squared),
|
|
955
|
+
"n_groups": int(k),
|
|
956
|
+
"group_sizes": [int(len(g)) for g in arrays],
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def tukey_hsd(*groups: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
961
|
+
"""Tukey HSD post-hoc pairwise comparisons (requires scipy >= 1.8).
|
|
962
|
+
|
|
963
|
+
Parameters
|
|
964
|
+
----------
|
|
965
|
+
groups : np.ndarray
|
|
966
|
+
Two or more independent samples (variadic positional arguments).
|
|
967
|
+
alpha : float, default 0.05
|
|
968
|
+
Significance level.
|
|
969
|
+
|
|
970
|
+
Returns
|
|
971
|
+
-------
|
|
972
|
+
result : dict
|
|
973
|
+
comparisons (list of dicts with group_i, group_j, mean_diff,
|
|
974
|
+
p_value, significant), alpha, n_groups.
|
|
975
|
+
"""
|
|
976
|
+
from scipy.stats import tukey_hsd as _tukey_hsd
|
|
977
|
+
|
|
978
|
+
arrays = [np.asarray(g).flatten() for g in groups]
|
|
979
|
+
res = _tukey_hsd(*arrays)
|
|
980
|
+
k = len(arrays)
|
|
981
|
+
comparisons = []
|
|
982
|
+
for i in range(k):
|
|
983
|
+
for j in range(i + 1, k):
|
|
984
|
+
p_val = float(res.pvalue[i, j])
|
|
985
|
+
comparisons.append(
|
|
986
|
+
{
|
|
987
|
+
"group_i": int(i),
|
|
988
|
+
"group_j": int(j),
|
|
989
|
+
"mean_diff": float(np.mean(arrays[i])) - float(np.mean(arrays[j])),
|
|
990
|
+
"p_value": p_val,
|
|
991
|
+
"significant": bool(p_val < alpha),
|
|
992
|
+
}
|
|
993
|
+
)
|
|
994
|
+
return {
|
|
995
|
+
"comparisons": comparisons,
|
|
996
|
+
"alpha": float(alpha),
|
|
997
|
+
"n_groups": int(k),
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def anova_twoway(
|
|
1002
|
+
data: "Any",
|
|
1003
|
+
response: str,
|
|
1004
|
+
factor_a: str,
|
|
1005
|
+
factor_b: str,
|
|
1006
|
+
include_interaction: bool = True,
|
|
1007
|
+
) -> Dict[str, Any]:
|
|
1008
|
+
"""Two-way ANOVA with optional interaction term.
|
|
1009
|
+
|
|
1010
|
+
Uses ``statsmodels`` OLS (Type II sums of squares via
|
|
1011
|
+
``statsmodels.stats.anova.anova_lm``).
|
|
1012
|
+
|
|
1013
|
+
Parameters
|
|
1014
|
+
----------
|
|
1015
|
+
data : pd.DataFrame
|
|
1016
|
+
DataFrame containing the response and both factor columns.
|
|
1017
|
+
response : str
|
|
1018
|
+
Name of the continuous response variable column.
|
|
1019
|
+
factor_a : str
|
|
1020
|
+
Name of the first categorical factor column.
|
|
1021
|
+
factor_b : str
|
|
1022
|
+
Name of the second categorical factor column.
|
|
1023
|
+
include_interaction : bool, default True
|
|
1024
|
+
Whether to include the A×B interaction term.
|
|
1025
|
+
|
|
1026
|
+
Returns
|
|
1027
|
+
-------
|
|
1028
|
+
dict
|
|
1029
|
+
Keys: ``table`` (pd.DataFrame — ANOVA table with F, p-value, df, SS),
|
|
1030
|
+
``factor_a`` (str), ``factor_b`` (str), ``interaction_included`` (bool),
|
|
1031
|
+
``significant_a`` (bool), ``significant_b`` (bool),
|
|
1032
|
+
``significant_interaction`` (bool or None).
|
|
1033
|
+
|
|
1034
|
+
Raises
|
|
1035
|
+
------
|
|
1036
|
+
ImportError
|
|
1037
|
+
If ``statsmodels`` is not installed.
|
|
1038
|
+
|
|
1039
|
+
Examples
|
|
1040
|
+
--------
|
|
1041
|
+
>>> import pandas as pd
|
|
1042
|
+
>>> df = pd.DataFrame({'y': [5,6,7,8,4,5], 'A': ['a','a','b','b','a','b'],
|
|
1043
|
+
... 'B': ['x','y','x','y','x','y']})
|
|
1044
|
+
>>> result = anova_twoway(df, 'y', 'A', 'B')
|
|
1045
|
+
>>> result['table'].index.tolist() # doctest: +SKIP
|
|
1046
|
+
['A', 'B', 'A:B', 'Residual']
|
|
1047
|
+
"""
|
|
1048
|
+
try:
|
|
1049
|
+
import statsmodels.formula.api as smf
|
|
1050
|
+
from statsmodels.stats.anova import anova_lm
|
|
1051
|
+
except ImportError as exc:
|
|
1052
|
+
raise ImportError(
|
|
1053
|
+
"statsmodels is required for two-way ANOVA. Install it with: pip install statsmodels"
|
|
1054
|
+
) from exc
|
|
1055
|
+
|
|
1056
|
+
if include_interaction:
|
|
1057
|
+
formula = f"Q('{response}') ~ C(Q('{factor_a}')) * C(Q('{factor_b}'))"
|
|
1058
|
+
else:
|
|
1059
|
+
formula = f"Q('{response}') ~ C(Q('{factor_a}')) + C(Q('{factor_b}'))"
|
|
1060
|
+
|
|
1061
|
+
model = smf.ols(formula, data=data).fit()
|
|
1062
|
+
table = anova_lm(model, typ=2)
|
|
1063
|
+
|
|
1064
|
+
# Rename index for readability
|
|
1065
|
+
rename_map = {}
|
|
1066
|
+
for idx in table.index:
|
|
1067
|
+
if factor_a in idx and factor_b in idx:
|
|
1068
|
+
rename_map[idx] = f"{factor_a}:{factor_b}"
|
|
1069
|
+
elif factor_a in idx:
|
|
1070
|
+
rename_map[idx] = factor_a
|
|
1071
|
+
elif factor_b in idx:
|
|
1072
|
+
rename_map[idx] = factor_b
|
|
1073
|
+
table = table.rename(index=rename_map)
|
|
1074
|
+
|
|
1075
|
+
alpha = 0.05
|
|
1076
|
+
p_a = float(table.loc[factor_a, "PR(>F)"]) if factor_a in table.index else 1.0
|
|
1077
|
+
p_b = float(table.loc[factor_b, "PR(>F)"]) if factor_b in table.index else 1.0
|
|
1078
|
+
interaction_key = f"{factor_a}:{factor_b}"
|
|
1079
|
+
p_int = float(table.loc[interaction_key, "PR(>F)"]) if interaction_key in table.index else None
|
|
1080
|
+
|
|
1081
|
+
return {
|
|
1082
|
+
"table": table,
|
|
1083
|
+
"factor_a": factor_a,
|
|
1084
|
+
"factor_b": factor_b,
|
|
1085
|
+
"interaction_included": include_interaction,
|
|
1086
|
+
"significant_a": p_a < alpha,
|
|
1087
|
+
"significant_b": p_b < alpha,
|
|
1088
|
+
"significant_interaction": (p_int < alpha) if p_int is not None else None,
|
|
1089
|
+
"p_value_a": p_a,
|
|
1090
|
+
"p_value_b": p_b,
|
|
1091
|
+
"p_value_interaction": p_int,
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def bonferroni_correction(p_values: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
1096
|
+
"""Bonferroni multiple-comparison correction.
|
|
1097
|
+
|
|
1098
|
+
Parameters
|
|
1099
|
+
----------
|
|
1100
|
+
p_values : np.ndarray
|
|
1101
|
+
Array of raw p-values from multiple tests.
|
|
1102
|
+
alpha : float, default 0.05
|
|
1103
|
+
Family-wise significance level.
|
|
1104
|
+
|
|
1105
|
+
Returns
|
|
1106
|
+
-------
|
|
1107
|
+
result : dict
|
|
1108
|
+
adjusted_alpha, corrected_p_values, reject, n_tests, original_alpha.
|
|
1109
|
+
"""
|
|
1110
|
+
p_values = np.asarray(p_values).flatten()
|
|
1111
|
+
m = len(p_values)
|
|
1112
|
+
adjusted_alpha = alpha / m
|
|
1113
|
+
reject = p_values < adjusted_alpha
|
|
1114
|
+
return {
|
|
1115
|
+
"adjusted_alpha": float(adjusted_alpha),
|
|
1116
|
+
"corrected_p_values": [float(p) for p in p_values],
|
|
1117
|
+
"reject": [bool(r) for r in reject],
|
|
1118
|
+
"n_tests": int(m),
|
|
1119
|
+
"original_alpha": float(alpha),
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def pearson_correlation(x: np.ndarray, y: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
1124
|
+
"""Pearson correlation coefficient and significance test.
|
|
1125
|
+
|
|
1126
|
+
Parameters
|
|
1127
|
+
----------
|
|
1128
|
+
x, y : np.ndarray
|
|
1129
|
+
Two paired numeric arrays of equal length.
|
|
1130
|
+
alpha : float, default 0.05
|
|
1131
|
+
Significance level.
|
|
1132
|
+
|
|
1133
|
+
Returns
|
|
1134
|
+
-------
|
|
1135
|
+
result : dict
|
|
1136
|
+
r, p_value, reject_null, alpha, ci_low, ci_high, n.
|
|
1137
|
+
"""
|
|
1138
|
+
x = np.asarray(x).flatten()
|
|
1139
|
+
y = np.asarray(y).flatten()
|
|
1140
|
+
r, p_value = stats.pearsonr(x, y)
|
|
1141
|
+
n = len(x)
|
|
1142
|
+
# Fisher z-transform 95% CI
|
|
1143
|
+
z = np.arctanh(float(r))
|
|
1144
|
+
se_z = 1.0 / np.sqrt(n - 3)
|
|
1145
|
+
z_crit = float(stats.norm.ppf(1 - alpha / 2))
|
|
1146
|
+
ci_low = float(np.tanh(z - z_crit * se_z))
|
|
1147
|
+
ci_high = float(np.tanh(z + z_crit * se_z))
|
|
1148
|
+
return {
|
|
1149
|
+
"r": float(r),
|
|
1150
|
+
"p_value": float(p_value),
|
|
1151
|
+
"reject_null": bool(p_value < alpha),
|
|
1152
|
+
"alpha": float(alpha),
|
|
1153
|
+
"ci_low": ci_low,
|
|
1154
|
+
"ci_high": ci_high,
|
|
1155
|
+
"n": int(n),
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def spearman_correlation(x: np.ndarray, y: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
1160
|
+
"""Spearman rank correlation and significance test.
|
|
1161
|
+
|
|
1162
|
+
Parameters
|
|
1163
|
+
----------
|
|
1164
|
+
x, y : np.ndarray
|
|
1165
|
+
Two paired numeric arrays of equal length.
|
|
1166
|
+
alpha : float, default 0.05
|
|
1167
|
+
Significance level.
|
|
1168
|
+
|
|
1169
|
+
Returns
|
|
1170
|
+
-------
|
|
1171
|
+
result : dict
|
|
1172
|
+
rho, p_value, reject_null, alpha, n.
|
|
1173
|
+
"""
|
|
1174
|
+
x = np.asarray(x).flatten()
|
|
1175
|
+
y = np.asarray(y).flatten()
|
|
1176
|
+
rho, p_value = stats.spearmanr(x, y)
|
|
1177
|
+
return {
|
|
1178
|
+
"rho": float(rho),
|
|
1179
|
+
"p_value": float(p_value),
|
|
1180
|
+
"reject_null": bool(p_value < alpha),
|
|
1181
|
+
"alpha": float(alpha),
|
|
1182
|
+
"n": int(len(x)),
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def chi_square_independence(contingency_table: np.ndarray, alpha: float = 0.05) -> Dict[str, Any]:
|
|
1187
|
+
"""Chi-square test of independence.
|
|
1188
|
+
|
|
1189
|
+
Parameters
|
|
1190
|
+
----------
|
|
1191
|
+
contingency_table : np.ndarray
|
|
1192
|
+
2-D observed frequency table.
|
|
1193
|
+
alpha : float, default 0.05
|
|
1194
|
+
Significance level.
|
|
1195
|
+
|
|
1196
|
+
Returns
|
|
1197
|
+
-------
|
|
1198
|
+
result : dict
|
|
1199
|
+
chi2, p_value, df, reject_null, alpha, expected.
|
|
1200
|
+
"""
|
|
1201
|
+
contingency_table = np.asarray(contingency_table)
|
|
1202
|
+
chi2, p_value, df, expected = stats.chi2_contingency(contingency_table)
|
|
1203
|
+
return {
|
|
1204
|
+
"chi2": float(chi2),
|
|
1205
|
+
"p_value": float(p_value),
|
|
1206
|
+
"df": int(df),
|
|
1207
|
+
"reject_null": bool(p_value < alpha),
|
|
1208
|
+
"alpha": float(alpha),
|
|
1209
|
+
"expected": expected.tolist(),
|
|
1210
|
+
}
|