scitex 2.14.0__py3-none-any.whl → 2.15.3__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.
Files changed (264) hide show
  1. scitex/__init__.py +71 -17
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-25
3
+ # File: src/scitex/stats/_mcp/_handlers/_posthoc.py
4
+
5
+ """Post-hoc test handler."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from datetime import datetime
11
+
12
+ import numpy as np
13
+
14
+ __all__ = ["posthoc_test_handler"]
15
+
16
+
17
+ async def posthoc_test_handler(
18
+ groups: list[list[float]],
19
+ group_names: list[str] | None = None,
20
+ method: str = "tukey",
21
+ control_group: int = 0,
22
+ ) -> dict:
23
+ """Run post-hoc pairwise comparisons."""
24
+ try:
25
+ loop = asyncio.get_event_loop()
26
+
27
+ def do_posthoc():
28
+ group_arrays = [np.array(g, dtype=float) for g in groups]
29
+ names = group_names or [f"Group_{i + 1}" for i in range(len(groups))]
30
+
31
+ if method == "tukey":
32
+ comparisons = _tukey_hsd(group_arrays, names)
33
+ elif method == "dunnett":
34
+ comparisons = _dunnett(group_arrays, names, control_group)
35
+ elif method == "games_howell":
36
+ comparisons = _games_howell(group_arrays, names)
37
+ elif method == "dunn":
38
+ comparisons = _dunn(group_arrays, names)
39
+ else:
40
+ raise ValueError(f"Unknown method: {method}")
41
+
42
+ return comparisons
43
+
44
+ comparisons = await loop.run_in_executor(None, do_posthoc)
45
+
46
+ return {
47
+ "success": True,
48
+ "method": method,
49
+ "n_groups": len(groups),
50
+ "n_comparisons": len(comparisons),
51
+ "comparisons": comparisons,
52
+ "timestamp": datetime.now().isoformat(),
53
+ }
54
+
55
+ except Exception as e:
56
+ return {"success": False, "error": str(e)}
57
+
58
+
59
+ def _tukey_hsd(group_arrays, names):
60
+ """Tukey HSD test."""
61
+ from scipy import stats as scipy_stats
62
+
63
+ all_data = np.concatenate(group_arrays)
64
+ group_labels = np.concatenate(
65
+ [[names[i]] * len(g) for i, g in enumerate(group_arrays)]
66
+ )
67
+
68
+ comparisons = []
69
+
70
+ try:
71
+ from statsmodels.stats.multicomp import pairwise_tukeyhsd
72
+
73
+ tukey = pairwise_tukeyhsd(all_data, group_labels)
74
+
75
+ for i in range(len(tukey.summary().data) - 1):
76
+ row = tukey.summary().data[i + 1]
77
+ comparisons.append(
78
+ {
79
+ "group1": str(row[0]),
80
+ "group2": str(row[1]),
81
+ "mean_diff": float(row[2]),
82
+ "p_adj": float(row[3]),
83
+ "ci_lower": float(row[4]),
84
+ "ci_upper": float(row[5]),
85
+ "reject": bool(row[6]),
86
+ }
87
+ )
88
+ except ImportError:
89
+ # Fallback: Bonferroni-corrected t-tests
90
+ n_comparisons = len(group_arrays) * (len(group_arrays) - 1) // 2
91
+ for i in range(len(group_arrays)):
92
+ for j in range(i + 1, len(group_arrays)):
93
+ stat, p = scipy_stats.ttest_ind(group_arrays[i], group_arrays[j])
94
+ p_adj = min(p * n_comparisons, 1.0)
95
+ comparisons.append(
96
+ {
97
+ "group1": names[i],
98
+ "group2": names[j],
99
+ "mean_diff": float(
100
+ np.mean(group_arrays[i]) - np.mean(group_arrays[j])
101
+ ),
102
+ "t_statistic": float(stat),
103
+ "p_value": float(p),
104
+ "p_adj": float(p_adj),
105
+ "reject": p_adj < 0.05,
106
+ }
107
+ )
108
+
109
+ return comparisons
110
+
111
+
112
+ def _dunnett(group_arrays, names, control_group):
113
+ """Dunnett's test (compare all to control)."""
114
+ from scipy import stats as scipy_stats
115
+
116
+ control = group_arrays[control_group]
117
+ n_comparisons = len(group_arrays) - 1
118
+
119
+ comparisons = []
120
+ for i, (name, group) in enumerate(zip(names, group_arrays)):
121
+ if i == control_group:
122
+ continue
123
+ stat, p = scipy_stats.ttest_ind(group, control)
124
+ p_adj = min(p * n_comparisons, 1.0)
125
+ comparisons.append(
126
+ {
127
+ "group": name,
128
+ "vs_control": names[control_group],
129
+ "mean_diff": float(np.mean(group) - np.mean(control)),
130
+ "t_statistic": float(stat),
131
+ "p_value": float(p),
132
+ "p_adj": float(p_adj),
133
+ "reject": p_adj < 0.05,
134
+ }
135
+ )
136
+
137
+ return comparisons
138
+
139
+
140
+ def _games_howell(group_arrays, names):
141
+ """Games-Howell test (doesn't assume equal variances)."""
142
+ from scipy import stats as scipy_stats
143
+
144
+ comparisons = []
145
+ n_comparisons = len(group_arrays) * (len(group_arrays) - 1) // 2
146
+
147
+ for i in range(len(group_arrays)):
148
+ for j in range(i + 1, len(group_arrays)):
149
+ g1, g2 = group_arrays[i], group_arrays[j]
150
+ n1, n2 = len(g1), len(g2)
151
+ m1, m2 = np.mean(g1), np.mean(g2)
152
+ v1, v2 = np.var(g1, ddof=1), np.var(g2, ddof=1)
153
+
154
+ se = np.sqrt(v1 / n1 + v2 / n2)
155
+ t_stat = (m1 - m2) / se
156
+
157
+ # Welch-Satterthwaite df
158
+ df = (v1 / n1 + v2 / n2) ** 2 / (
159
+ (v1 / n1) ** 2 / (n1 - 1) + (v2 / n2) ** 2 / (n2 - 1)
160
+ )
161
+
162
+ p = 2 * (1 - scipy_stats.t.cdf(abs(t_stat), df))
163
+ p_adj = min(p * n_comparisons, 1.0)
164
+
165
+ comparisons.append(
166
+ {
167
+ "group1": names[i],
168
+ "group2": names[j],
169
+ "mean_diff": float(m1 - m2),
170
+ "t_statistic": float(t_stat),
171
+ "df": float(df),
172
+ "p_value": float(p),
173
+ "p_adj": float(p_adj),
174
+ "reject": p_adj < 0.05,
175
+ }
176
+ )
177
+
178
+ return comparisons
179
+
180
+
181
+ def _dunn(group_arrays, names):
182
+ """Dunn's test for Kruskal-Wallis post-hoc."""
183
+ from scipy import stats as scipy_stats
184
+
185
+ all_data = np.concatenate(group_arrays)
186
+ ranks = scipy_stats.rankdata(all_data)
187
+
188
+ # Assign ranks to groups
189
+ idx = 0
190
+ group_ranks = []
191
+ for g in group_arrays:
192
+ group_ranks.append(ranks[idx : idx + len(g)])
193
+ idx += len(g)
194
+
195
+ n_total = len(all_data)
196
+ n_comparisons = len(group_arrays) * (len(group_arrays) - 1) // 2
197
+
198
+ comparisons = []
199
+ for i in range(len(group_arrays)):
200
+ for j in range(i + 1, len(group_arrays)):
201
+ n_i, n_j = len(group_arrays[i]), len(group_arrays[j])
202
+ r_i, r_j = np.mean(group_ranks[i]), np.mean(group_ranks[j])
203
+
204
+ se = np.sqrt(n_total * (n_total + 1) / 12 * (1 / n_i + 1 / n_j))
205
+ z = (r_i - r_j) / se
206
+ p = 2 * (1 - scipy_stats.norm.cdf(abs(z)))
207
+ p_adj = min(p * n_comparisons, 1.0)
208
+
209
+ comparisons.append(
210
+ {
211
+ "group1": names[i],
212
+ "group2": names[j],
213
+ "mean_rank_diff": float(r_i - r_j),
214
+ "z_statistic": float(z),
215
+ "p_value": float(p),
216
+ "p_adj": float(p_adj),
217
+ "reject": p_adj < 0.05,
218
+ }
219
+ )
220
+
221
+ return comparisons
222
+
223
+
224
+ # EOF
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-25
3
+ # File: src/scitex/stats/_mcp/_handlers/_power.py
4
+
5
+ """Power analysis handler."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from datetime import datetime
11
+
12
+ import numpy as np
13
+
14
+ __all__ = ["power_analysis_handler"]
15
+
16
+
17
+ async def power_analysis_handler(
18
+ test_type: str = "ttest",
19
+ effect_size: float | None = None,
20
+ alpha: float = 0.05,
21
+ power: float = 0.8,
22
+ n: int | None = None,
23
+ n_groups: int = 2,
24
+ ratio: float = 1.0,
25
+ ) -> dict:
26
+ """Calculate statistical power or required sample size."""
27
+ try:
28
+ loop = asyncio.get_event_loop()
29
+
30
+ def do_power():
31
+ from scitex.stats.power._power import power_ttest, sample_size_ttest
32
+
33
+ result = {}
34
+
35
+ if test_type == "ttest":
36
+ if n is not None and effect_size is not None:
37
+ # Calculate power given n and effect size
38
+ calculated_power = power_ttest(
39
+ effect_size=effect_size,
40
+ n1=n,
41
+ n2=int(n * ratio),
42
+ alpha=alpha,
43
+ test_type="two-sample",
44
+ )
45
+ result = {
46
+ "mode": "power_calculation",
47
+ "power": calculated_power,
48
+ "n1": n,
49
+ "n2": int(n * ratio),
50
+ "effect_size": effect_size,
51
+ "alpha": alpha,
52
+ }
53
+ elif effect_size is not None:
54
+ # Calculate required sample size
55
+ n1, n2 = sample_size_ttest(
56
+ effect_size=effect_size,
57
+ power=power,
58
+ alpha=alpha,
59
+ ratio=ratio,
60
+ )
61
+ result = {
62
+ "mode": "sample_size_calculation",
63
+ "required_n1": n1,
64
+ "required_n2": n2,
65
+ "total_n": n1 + n2,
66
+ "effect_size": effect_size,
67
+ "target_power": power,
68
+ "alpha": alpha,
69
+ }
70
+ else:
71
+ raise ValueError("Either n or effect_size must be provided")
72
+
73
+ elif test_type == "anova":
74
+ result = _power_anova(effect_size, alpha, power, n, n_groups)
75
+
76
+ elif test_type == "correlation":
77
+ result = _power_correlation(effect_size, alpha, power, n)
78
+
79
+ elif test_type == "chi2":
80
+ result = _power_chi2(effect_size, alpha, power, n, n_groups)
81
+
82
+ else:
83
+ raise ValueError(f"Unknown test_type: {test_type}")
84
+
85
+ return result
86
+
87
+ result = await loop.run_in_executor(None, do_power)
88
+
89
+ return {
90
+ "success": True,
91
+ "test_type": test_type,
92
+ **result,
93
+ "timestamp": datetime.now().isoformat(),
94
+ }
95
+
96
+ except Exception as e:
97
+ return {"success": False, "error": str(e)}
98
+
99
+
100
+ def _power_anova(
101
+ effect_size: float | None,
102
+ alpha: float,
103
+ power: float,
104
+ n: int | None,
105
+ n_groups: int,
106
+ ) -> dict:
107
+ """ANOVA power calculation."""
108
+ from scipy import stats as scipy_stats
109
+
110
+ if effect_size is None:
111
+ raise ValueError("effect_size required for ANOVA power")
112
+
113
+ if n is not None:
114
+ df1 = n_groups - 1
115
+ df2 = n_groups * n - n_groups
116
+ nc = effect_size**2 * n * n_groups
117
+ f_crit = scipy_stats.f.ppf(1 - alpha, df1, df2)
118
+ power_val = 1 - scipy_stats.ncf.cdf(f_crit, df1, df2, nc)
119
+ return {
120
+ "mode": "power_calculation",
121
+ "power": power_val,
122
+ "n_per_group": n,
123
+ "n_groups": n_groups,
124
+ "effect_size_f": effect_size,
125
+ "alpha": alpha,
126
+ }
127
+ else:
128
+ # Binary search for n
129
+ n_min, n_max = 2, 1000
130
+ while n_max - n_min > 1:
131
+ n_mid = (n_min + n_max) // 2
132
+ df1 = n_groups - 1
133
+ df2 = n_groups * n_mid - n_groups
134
+ nc = effect_size**2 * n_mid * n_groups
135
+ f_crit = scipy_stats.f.ppf(1 - alpha, df1, df2)
136
+ power_val = 1 - scipy_stats.ncf.cdf(f_crit, df1, df2, nc)
137
+ if power_val < power:
138
+ n_min = n_mid
139
+ else:
140
+ n_max = n_mid
141
+
142
+ return {
143
+ "mode": "sample_size_calculation",
144
+ "required_n_per_group": n_max,
145
+ "total_n": n_max * n_groups,
146
+ "n_groups": n_groups,
147
+ "effect_size_f": effect_size,
148
+ "target_power": power,
149
+ "alpha": alpha,
150
+ }
151
+
152
+
153
+ def _power_correlation(
154
+ effect_size: float | None,
155
+ alpha: float,
156
+ power: float,
157
+ n: int | None,
158
+ ) -> dict:
159
+ """Correlation power calculation."""
160
+ from scipy import stats as scipy_stats
161
+
162
+ if effect_size is None:
163
+ raise ValueError("effect_size (r) required for correlation power")
164
+
165
+ if n is not None:
166
+ # Calculate power
167
+ z = 0.5 * np.log((1 + effect_size) / (1 - effect_size))
168
+ se = 1 / np.sqrt(n - 3)
169
+ z_crit = scipy_stats.norm.ppf(1 - alpha / 2)
170
+ power_val = (
171
+ 1
172
+ - scipy_stats.norm.cdf(z_crit - z / se)
173
+ + scipy_stats.norm.cdf(-z_crit - z / se)
174
+ )
175
+ return {
176
+ "mode": "power_calculation",
177
+ "power": power_val,
178
+ "n": n,
179
+ "effect_size_r": effect_size,
180
+ "alpha": alpha,
181
+ }
182
+ else:
183
+ # Calculate required n
184
+ z = 0.5 * np.log((1 + effect_size) / (1 - effect_size))
185
+ z_crit = scipy_stats.norm.ppf(1 - alpha / 2)
186
+ z_power = scipy_stats.norm.ppf(power)
187
+ required_n = int(np.ceil(((z_crit + z_power) / z) ** 2 + 3))
188
+ return {
189
+ "mode": "sample_size_calculation",
190
+ "required_n": required_n,
191
+ "effect_size_r": effect_size,
192
+ "target_power": power,
193
+ "alpha": alpha,
194
+ }
195
+
196
+
197
+ def _power_chi2(
198
+ effect_size: float | None,
199
+ alpha: float,
200
+ power: float,
201
+ n: int | None,
202
+ n_groups: int,
203
+ ) -> dict:
204
+ """Chi-square power calculation."""
205
+ from scipy import stats as scipy_stats
206
+
207
+ if effect_size is None:
208
+ raise ValueError("effect_size (w) required for chi2 power")
209
+
210
+ df = n_groups - 1 # Simplified: using n_groups as number of cells
211
+
212
+ if n is not None:
213
+ nc = effect_size**2 * n
214
+ chi2_crit = scipy_stats.chi2.ppf(1 - alpha, df)
215
+ power_val = 1 - scipy_stats.ncx2.cdf(chi2_crit, df, nc)
216
+ return {
217
+ "mode": "power_calculation",
218
+ "power": power_val,
219
+ "n": n,
220
+ "df": df,
221
+ "effect_size_w": effect_size,
222
+ "alpha": alpha,
223
+ }
224
+ else:
225
+ # Binary search for n
226
+ n_min, n_max = 10, 10000
227
+ while n_max - n_min > 1:
228
+ n_mid = (n_min + n_max) // 2
229
+ nc = effect_size**2 * n_mid
230
+ chi2_crit = scipy_stats.chi2.ppf(1 - alpha, df)
231
+ power_val = 1 - scipy_stats.ncx2.cdf(chi2_crit, df, nc)
232
+ if power_val < power:
233
+ n_min = n_mid
234
+ else:
235
+ n_max = n_mid
236
+
237
+ return {
238
+ "mode": "sample_size_calculation",
239
+ "required_n": n_max,
240
+ "df": df,
241
+ "effect_size_w": effect_size,
242
+ "target_power": power,
243
+ "alpha": alpha,
244
+ }
245
+
246
+
247
+ # EOF
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-25
3
+ # File: src/scitex/stats/_mcp/_handlers/_recommend.py
4
+
5
+ """Test recommendation handler."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from datetime import datetime
11
+
12
+ __all__ = ["recommend_tests_handler"]
13
+
14
+
15
+ def _get_test_rationale(test_name: str) -> str:
16
+ """Get rationale for recommending a specific test."""
17
+ rationales = {
18
+ "brunner_munzel": "Robust nonparametric test - no normality/equal variance assumptions",
19
+ "ttest_ind": "Classic parametric test for comparing two independent groups",
20
+ "ttest_paired": "Parametric test for paired/matched samples",
21
+ "ttest_1samp": "One-sample t-test for comparing to a population mean",
22
+ "mannwhitneyu": "Nonparametric alternative to independent t-test",
23
+ "wilcoxon": "Nonparametric alternative to paired t-test",
24
+ "anova": "Parametric test for comparing 3+ groups",
25
+ "kruskal": "Nonparametric alternative to one-way ANOVA",
26
+ "chi2": "Test for independence in contingency tables",
27
+ "fisher_exact": "Exact test for small sample contingency tables",
28
+ "pearson": "Parametric correlation coefficient",
29
+ "spearman": "Nonparametric rank correlation",
30
+ "kendall": "Robust nonparametric correlation for ordinal data",
31
+ }
32
+ return rationales.get(test_name, "Applicable to the given context")
33
+
34
+
35
+ async def recommend_tests_handler(
36
+ n_groups: int = 2,
37
+ sample_sizes: list[int] | None = None,
38
+ outcome_type: str = "continuous",
39
+ design: str = "between",
40
+ paired: bool = False,
41
+ has_control_group: bool = False,
42
+ top_k: int = 3,
43
+ ) -> dict:
44
+ """Recommend appropriate statistical tests based on data characteristics."""
45
+ try:
46
+ from scitex.stats.auto import StatContext, recommend_tests
47
+
48
+ loop = asyncio.get_event_loop()
49
+
50
+ def do_recommend():
51
+ ctx = StatContext(
52
+ n_groups=n_groups,
53
+ sample_sizes=sample_sizes or [30] * n_groups,
54
+ outcome_type=outcome_type,
55
+ design=design,
56
+ paired=paired,
57
+ has_control_group=has_control_group,
58
+ n_factors=1,
59
+ )
60
+ tests = recommend_tests(ctx, top_k=top_k)
61
+
62
+ # Get details about each recommended test
63
+ from scitex.stats.auto._rules import TEST_RULES
64
+
65
+ recommendations = []
66
+ for test_name in tests:
67
+ rule = TEST_RULES.get(test_name)
68
+ if rule:
69
+ recommendations.append(
70
+ {
71
+ "name": test_name,
72
+ "family": rule.family,
73
+ "priority": rule.priority,
74
+ "needs_normality": rule.needs_normality,
75
+ "needs_equal_variance": rule.needs_equal_variance,
76
+ "rationale": _get_test_rationale(test_name),
77
+ }
78
+ )
79
+
80
+ return recommendations
81
+
82
+ recommendations = await loop.run_in_executor(None, do_recommend)
83
+
84
+ return {
85
+ "success": True,
86
+ "context": {
87
+ "n_groups": n_groups,
88
+ "sample_sizes": sample_sizes,
89
+ "outcome_type": outcome_type,
90
+ "design": design,
91
+ "paired": paired,
92
+ "has_control_group": has_control_group,
93
+ },
94
+ "recommendations": recommendations,
95
+ "timestamp": datetime.now().isoformat(),
96
+ }
97
+
98
+ except Exception as e:
99
+ return {"success": False, "error": str(e)}
100
+
101
+
102
+ # EOF