scitex 2.10.3__py3-none-any.whl → 2.13.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.
Files changed (504) hide show
  1. scitex/__init__.py +1 -4
  2. scitex/__main__.py +24 -5
  3. scitex/__version__.py +1 -1
  4. scitex/_install_guide.py +14 -2
  5. scitex/_optional_deps.py +33 -0
  6. scitex/ai/classification/reporters/_ClassificationReporter.py +1 -1
  7. scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +2 -2
  8. scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +2 -2
  9. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +2 -2
  10. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +2 -2
  11. scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +2 -2
  12. scitex/ai/classification/timeseries/_normalize_timestamp.py +1 -1
  13. scitex/ai/metrics/_calc_seizure_prediction_metrics.py +1 -1
  14. scitex/ai/plt/_plot_feature_importance.py +1 -1
  15. scitex/ai/plt/_plot_learning_curve.py +1 -1
  16. scitex/ai/plt/_plot_optuna_study.py +1 -1
  17. scitex/ai/plt/_plot_pre_rec_curve.py +1 -1
  18. scitex/ai/plt/_plot_roc_curve.py +1 -1
  19. scitex/ai/plt/_stx_conf_mat.py +1 -1
  20. scitex/ai/training/_LearningCurveLogger.py +1 -1
  21. scitex/audio/mcp_server.py +38 -8
  22. scitex/bridge/_figrecipe.py +1 -1
  23. scitex/bridge/_helpers.py +1 -1
  24. scitex/bridge/_plt_vis.py +1 -1
  25. scitex/bridge/_stats_plt.py +1 -1
  26. scitex/bridge/_stats_vis.py +2 -2
  27. scitex/browser/automation/CookieHandler.py +1 -1
  28. scitex/browser/core/BrowserMixin.py +1 -1
  29. scitex/browser/core/ChromeProfileManager.py +1 -1
  30. scitex/browser/debugging/_browser_logger.py +1 -1
  31. scitex/browser/debugging/_highlight_element.py +1 -1
  32. scitex/browser/debugging/_show_grid.py +1 -1
  33. scitex/browser/interaction/click_center.py +1 -1
  34. scitex/browser/interaction/click_with_fallbacks.py +1 -1
  35. scitex/browser/interaction/close_popups.py +1 -1
  36. scitex/browser/interaction/fill_with_fallbacks.py +1 -1
  37. scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +1 -1
  38. scitex/browser/pdf/detect_chrome_pdf_viewer.py +1 -1
  39. scitex/browser/stealth/HumanBehavior.py +1 -1
  40. scitex/browser/stealth/StealthManager.py +1 -1
  41. scitex/{fig → canvas}/__init__.py +84 -96
  42. scitex/canvas/_mcp_handlers.py +372 -0
  43. scitex/canvas/_mcp_tool_schemas.py +219 -0
  44. scitex/{fig → canvas}/backend/_parser.py +1 -1
  45. scitex/{fig → canvas}/canvas.py +13 -14
  46. scitex/{fts/_fig/_editor → canvas/editor}/_defaults.py +2 -2
  47. scitex/{fig → canvas}/editor/edit/__init__.py +11 -14
  48. scitex/{fig → canvas}/editor/edit/bundle_resolver.py +56 -48
  49. scitex/{fig → canvas}/editor/edit/editor_launcher.py +79 -26
  50. scitex/{fts/_fig/_editor/_cui/_panel_loader.py → canvas/editor/edit/panel_loader.py} +8 -8
  51. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_bbox.py +2 -1
  52. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_core.py +84 -84
  53. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_renderer.py +7 -6
  54. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/static/css/features/canvas.css +2 -2
  55. scitex/{fig → canvas}/editor/flask_editor/static/css/features/panel-grid.css +1 -1
  56. scitex/{fig → canvas}/editor/flask_editor/static/js/core/api.js +3 -4
  57. scitex/{fig → canvas}/editor/flask_editor/static/js/editor/preview.js +5 -5
  58. scitex/{fig → canvas}/editor/flask_editor/templates/_html.py +3 -3
  59. scitex/{fig → canvas}/editor/flask_editor/templates/_scripts.py +10 -10
  60. scitex/{fig → canvas}/editor/flask_editor/templates/_styles.py +3 -3
  61. scitex/{fig → canvas}/io/__init__.py +32 -38
  62. scitex/{fig → canvas}/io/_bundle.py +217 -154
  63. scitex/{fig → canvas}/io/_canvas.py +1 -1
  64. scitex/{fig → canvas}/io/_data.py +1 -1
  65. scitex/{fig → canvas}/io/_export.py +1 -1
  66. scitex/{fig → canvas}/io/_load.py +1 -1
  67. scitex/{fig → canvas}/io/_panel.py +1 -1
  68. scitex/{fig → canvas}/io/_save.py +1 -1
  69. scitex/canvas/mcp_server.py +151 -0
  70. scitex/{fig → canvas}/model/__init__.py +1 -1
  71. scitex/{fig → canvas}/model/_annotations.py +1 -1
  72. scitex/{fig → canvas}/model/_axes.py +1 -1
  73. scitex/{fig → canvas}/model/_figure.py +1 -1
  74. scitex/{fig → canvas}/model/_guides.py +1 -1
  75. scitex/{fig → canvas}/model/_plot.py +1 -1
  76. scitex/{fig → canvas}/model/_styles.py +1 -1
  77. scitex/{fig → canvas}/utils/__init__.py +1 -1
  78. scitex/capture/mcp_server.py +41 -12
  79. scitex/cli/audio.py +233 -0
  80. scitex/cli/capture.py +307 -0
  81. scitex/cli/convert.py +10 -6
  82. scitex/cli/main.py +27 -4
  83. scitex/cli/repro.py +233 -0
  84. scitex/cli/resource.py +240 -0
  85. scitex/cli/stats.py +325 -0
  86. scitex/cli/template.py +236 -0
  87. scitex/cli/tex.py +286 -0
  88. scitex/cli/web.py +11 -12
  89. scitex/dev/__init__.py +3 -0
  90. scitex/dev/_pyproject.py +405 -0
  91. scitex/dev/plt/__init__.py +2 -2
  92. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  93. scitex/dev/plt/mpl/get_signatures.py +1 -1
  94. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  95. scitex/diagram/README.md +7 -7
  96. scitex/diagram/_mcp_handlers.py +400 -0
  97. scitex/diagram/_mcp_tool_schemas.py +157 -0
  98. scitex/diagram/mcp_server.py +151 -0
  99. scitex/dsp/_demo_sig.py +51 -5
  100. scitex/dsp/_mne.py +13 -2
  101. scitex/dsp/_modulation_index.py +15 -3
  102. scitex/dsp/_pac.py +23 -5
  103. scitex/dsp/_psd.py +16 -4
  104. scitex/dsp/_resample.py +24 -4
  105. scitex/dsp/_transform.py +16 -3
  106. scitex/dsp/add_noise.py +15 -1
  107. scitex/dsp/norm.py +17 -2
  108. scitex/dsp/reference.py +17 -1
  109. scitex/dsp/utils/_differential_bandpass_filters.py +20 -2
  110. scitex/dsp/utils/_zero_pad.py +18 -4
  111. scitex/dt/_normalize_timestamp.py +1 -1
  112. scitex/git/_session.py +1 -1
  113. scitex/io/__init__.py +7 -19
  114. scitex/io/_load.py +15 -19
  115. scitex/io/_load_modules/_canvas.py +2 -2
  116. scitex/io/_load_modules/_con.py +17 -6
  117. scitex/io/_load_modules/_eeg.py +28 -13
  118. scitex/io/_load_modules/_optuna.py +21 -63
  119. scitex/io/_load_modules/_torch.py +11 -3
  120. scitex/io/_save.py +11 -16
  121. scitex/io/_save_modules/__init__.py +6 -10
  122. scitex/io/_save_modules/_canvas.py +3 -3
  123. scitex/io/_save_modules/_optuna_study_as_csv_and_pngs.py +13 -2
  124. scitex/io/_save_modules/_plot_bundle.py +112 -0
  125. scitex/io/_save_modules/{_pltz_stx.py → _plot_scitex.py} +7 -7
  126. scitex/io/_save_modules/_stx_bundle.py +16 -16
  127. scitex/io/_save_modules/_torch.py +11 -3
  128. scitex/io/bundle/README.md +89 -80
  129. scitex/{fts/_bundle/_FTS.py → io/bundle/_Bundle.py} +197 -95
  130. scitex/io/bundle/__init__.py +67 -35
  131. scitex/{fts/_bundle → io/bundle}/_children.py +32 -40
  132. scitex/io/bundle/_core.py +184 -97
  133. scitex/{fts/_bundle/_dataclasses/_Node.py → io/bundle/_dataclasses/_Spec.py} +29 -23
  134. scitex/{fts/_bundle/_dataclasses/_NodeRefs.py → io/bundle/_dataclasses/_SpecRefs.py} +6 -6
  135. scitex/{fts/_bundle → io/bundle}/_dataclasses/__init__.py +4 -4
  136. scitex/{fts/_bundle → io/bundle}/_loader.py +19 -19
  137. scitex/io/bundle/_manifest.py +99 -0
  138. scitex/{fts/_bundle → io/bundle}/_mpl_helpers.py +119 -28
  139. scitex/io/bundle/_nested.py +113 -100
  140. scitex/{fts/_bundle → io/bundle}/_saver.py +13 -14
  141. scitex/{fts/_bundle → io/bundle}/_storage.py +3 -3
  142. scitex/io/bundle/_types.py +41 -16
  143. scitex/{fts/_bundle → io/bundle}/_validation.py +20 -18
  144. scitex/io/bundle/_zip.py +21 -31
  145. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_backend/_parser.py +1 -1
  146. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Annotations.py +1 -1
  147. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Axes.py +1 -1
  148. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Figure.py +1 -1
  149. scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Guides.py +1 -1
  150. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Plot.py +1 -1
  151. scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Styles.py +1 -1
  152. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_utils/_plot_layout.py +1 -1
  153. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/__init__.py +1 -1
  154. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/_app.py +1 -1
  155. scitex/{fts/_tables → io/bundle/kinds/_table}/_latex/_export.py +1 -1
  156. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_figure_exporter.py +1 -1
  157. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_table_exporter.py +1 -1
  158. scitex/io/bundle/schemas/__init__.py +30 -0
  159. scitex/mcp_server.py +159 -0
  160. scitex/parallel/_run.py +5 -4
  161. scitex/path/_find.py +60 -83
  162. scitex/path/_get_module_path.py +23 -21
  163. scitex/path/_get_spath.py +6 -27
  164. scitex/path/_getsize.py +23 -9
  165. scitex/path/_increment_version.py +31 -38
  166. scitex/path/_mk_spath.py +26 -29
  167. scitex/path/_path.py +5 -12
  168. scitex/path/_split.py +27 -15
  169. scitex/path/_this_path.py +23 -9
  170. scitex/plt/_mcp_handlers.py +361 -0
  171. scitex/plt/_mcp_tool_schemas.py +169 -0
  172. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +2 -1
  173. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +2 -2
  174. scitex/plt/gallery/_generate.py +76 -50
  175. scitex/plt/io/__init__.py +17 -19
  176. scitex/plt/io/_bundle.py +99 -52
  177. scitex/plt/io/_layered_bundle.py +303 -168
  178. scitex/plt/mcp_server.py +205 -0
  179. scitex/plt/utils/_csv_column_naming.py +250 -118
  180. scitex/repro/README_RandomStateManager.md +3 -3
  181. scitex/repro/_RandomStateManager.py +14 -14
  182. scitex/repro/_gen_ID.py +1 -1
  183. scitex/repro/_gen_timestamp.py +1 -1
  184. scitex/repro/_hash_array.py +4 -4
  185. scitex/schema/__init__.py +69 -73
  186. scitex/schema/_canvas.py +1 -1
  187. scitex/schema/_stats.py +2 -2
  188. scitex/scholar/__main__.py +24 -2
  189. scitex/scholar/_mcp_handlers.py +685 -0
  190. scitex/scholar/_mcp_tool_schemas.py +339 -0
  191. scitex/scholar/docs/template.py +1 -1
  192. scitex/scholar/examples/07_storage_integration.py +1 -1
  193. scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +1 -1
  194. scitex/scholar/impact_factor/jcr/build_database.py +1 -1
  195. scitex/scholar/mcp_server.py +315 -0
  196. scitex/scholar/pdf_download/ScholarPDFDownloader.py +1 -1
  197. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +1 -1
  198. scitex/scholar/pipelines/ScholarPipelineParallel.py +1 -1
  199. scitex/scholar/pipelines/ScholarPipelineSingle.py +1 -1
  200. scitex/scholar/storage/PaperIO.py +1 -1
  201. scitex/session/README.md +4 -4
  202. scitex/session/__init__.py +1 -1
  203. scitex/session/_decorator.py +9 -9
  204. scitex/session/_lifecycle.py +5 -5
  205. scitex/session/template.py +1 -1
  206. scitex/stats/__init__.py +30 -33
  207. scitex/stats/__main__.py +281 -0
  208. scitex/stats/_mcp_handlers.py +1191 -0
  209. scitex/stats/_mcp_tool_schemas.py +384 -0
  210. scitex/stats/_schema.py +1 -1
  211. scitex/stats/correct/_correct_bonferroni.py +1 -1
  212. scitex/stats/correct/_correct_fdr.py +1 -1
  213. scitex/stats/correct/_correct_fdr_.py +1 -1
  214. scitex/stats/correct/_correct_holm.py +1 -1
  215. scitex/stats/correct/_correct_sidak.py +1 -1
  216. scitex/stats/effect_sizes/_cliffs_delta.py +1 -1
  217. scitex/stats/effect_sizes/_cohens_d.py +1 -1
  218. scitex/stats/effect_sizes/_epsilon_squared.py +1 -1
  219. scitex/stats/effect_sizes/_eta_squared.py +1 -1
  220. scitex/stats/effect_sizes/_prob_superiority.py +1 -1
  221. scitex/stats/io/__init__.py +10 -11
  222. scitex/stats/io/_bundle.py +16 -16
  223. scitex/stats/mcp_server.py +405 -0
  224. scitex/stats/posthoc/_dunnett.py +1 -1
  225. scitex/stats/posthoc/_games_howell.py +1 -1
  226. scitex/stats/posthoc/_tukey_hsd.py +1 -1
  227. scitex/stats/power/_power.py +1 -1
  228. scitex/stats/utils/_effect_size.py +1 -1
  229. scitex/stats/utils/_formatters.py +1 -1
  230. scitex/stats/utils/_power.py +1 -1
  231. scitex/template/_mcp_handlers.py +259 -0
  232. scitex/template/_mcp_tool_schemas.py +112 -0
  233. scitex/template/mcp_server.py +186 -0
  234. scitex/utils/_verify_scitex_format.py +2 -2
  235. scitex/utils/template.py +1 -1
  236. scitex/web/__init__.py +12 -11
  237. scitex/web/_scraping.py +26 -265
  238. scitex/web/download_images.py +316 -0
  239. scitex/writer/Writer.py +1 -1
  240. scitex/writer/_clone_writer_project.py +1 -1
  241. scitex/writer/_validate_tree_structures.py +1 -1
  242. scitex/writer/dataclasses/config/_WriterConfig.py +1 -1
  243. scitex/writer/dataclasses/contents/_ManuscriptContents.py +1 -1
  244. scitex/writer/dataclasses/core/_Document.py +1 -1
  245. scitex/writer/dataclasses/core/_DocumentSection.py +1 -1
  246. scitex/writer/dataclasses/results/_CompilationResult.py +1 -1
  247. scitex/writer/dataclasses/results/_LaTeXIssue.py +1 -1
  248. scitex/writer/utils/.legacy_git_retry.py +7 -5
  249. scitex/writer/utils/_parse_latex_logs.py +1 -1
  250. scitex-2.13.0.dist-info/METADATA +1231 -0
  251. {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/RECORD +376 -470
  252. scitex-2.13.0.dist-info/entry_points.txt +11 -0
  253. scitex/fig/editor/_defaults.py +0 -300
  254. scitex/fig/editor/edit/panel_loader.py +0 -232
  255. scitex/fig/editor/flask_editor/_bbox.py +0 -1299
  256. scitex/fig/editor/flask_editor/_core.py +0 -1429
  257. scitex/fig/editor/flask_editor/_renderer.py +0 -813
  258. scitex/fig/editor/flask_editor/static/css/features/canvas.css +0 -176
  259. scitex/fts/README.md +0 -262
  260. scitex/fts/TODO.md +0 -66
  261. scitex/fts/__init__.py +0 -90
  262. scitex/fts/_bundle/README_IN_BUNDLE.md +0 -102
  263. scitex/fts/_bundle/__init__.py +0 -38
  264. scitex/fts/_bundle/_utils/__init__.py +0 -55
  265. scitex/fts/_bundle/_utils/_const.py +0 -26
  266. scitex/fts/_bundle/_utils/_errors.py +0 -73
  267. scitex/fts/_bundle/_utils/_generate.py +0 -21
  268. scitex/fts/_bundle/_utils/_types.py +0 -76
  269. scitex/fts/_bundle/_zipbundle.py +0 -165
  270. scitex/fts/_fig/__init__.py +0 -22
  271. scitex/fts/_fig/_backend/_parser.py +0 -188
  272. scitex/fts/_fig/_editor/__init__.py +0 -14
  273. scitex/fts/_fig/_editor/_cui/__init__.py +0 -33
  274. scitex/fts/_fig/_editor/_cui/_backend_detector.py +0 -39
  275. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +0 -366
  276. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +0 -175
  277. scitex/fts/_fig/_editor/_cui/_manual_handler.py +0 -52
  278. scitex/fts/_fig/_editor/_cui/_path_resolver.py +0 -66
  279. scitex/fts/_fig/_editor/_gui/__init__.py +0 -11
  280. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +0 -20
  281. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +0 -664
  282. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +0 -79
  283. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +0 -41
  284. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +0 -16
  285. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +0 -85
  286. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +0 -217
  287. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +0 -93
  288. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +0 -57
  289. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +0 -112
  290. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +0 -59
  291. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +0 -212
  292. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +0 -190
  293. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +0 -59
  294. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +0 -45
  295. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +0 -95
  296. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +0 -101
  297. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +0 -138
  298. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +0 -31
  299. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +0 -7
  300. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +0 -56
  301. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +0 -78
  302. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +0 -314
  303. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +0 -107
  304. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +0 -54
  305. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +0 -172
  306. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +0 -258
  307. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +0 -48
  308. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +0 -71
  309. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +0 -288
  310. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +0 -143
  311. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +0 -245
  312. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +0 -992
  313. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +0 -339
  314. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +0 -286
  315. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +0 -371
  316. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +0 -293
  317. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +0 -426
  318. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +0 -152
  319. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +0 -265
  320. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +0 -184
  321. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +0 -57
  322. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +0 -100
  323. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +0 -34
  324. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +0 -124
  325. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +0 -851
  326. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +0 -4932
  327. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +0 -1657
  328. scitex/fts/_fig/_editor/_gui/_flask_editor.py +0 -36
  329. scitex/fts/_fig/_models/_Annotations.py +0 -115
  330. scitex/fts/_fig/_models/_Axes.py +0 -152
  331. scitex/fts/_fig/_models/_Figure.py +0 -138
  332. scitex/fts/_fig/_models/_Plot.py +0 -123
  333. scitex/fts/_fig/_utils/_plot_layout.py +0 -397
  334. scitex/fts/_kinds/_figure/_composite.py +0 -345
  335. scitex/fts/_kinds/_plot/_backend/__init__.py +0 -53
  336. scitex/fts/_kinds/_plot/_backend/_export.py +0 -165
  337. scitex/fts/_kinds/_plot/_backend/_render.py +0 -538
  338. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +0 -46
  339. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +0 -82
  340. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +0 -441
  341. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +0 -52
  342. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +0 -47
  343. scitex/fts/_kinds/_plot/_models/_Guides.py +0 -104
  344. scitex/fts/_kinds/_plot/_models/_Styles.py +0 -245
  345. scitex/fts/_kinds/_plot/_models/__init__.py +0 -80
  346. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +0 -156
  347. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +0 -43
  348. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +0 -38
  349. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +0 -36
  350. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +0 -60
  351. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +0 -30
  352. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +0 -61
  353. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +0 -57
  354. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +0 -30
  355. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +0 -121
  356. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +0 -36
  357. scitex/fts/_kinds/_plot/_utils/__init__.py +0 -129
  358. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +0 -127
  359. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +0 -111
  360. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +0 -48
  361. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +0 -77
  362. scitex/fts/_kinds/_plot/_utils/_get_template.py +0 -178
  363. scitex/fts/_kinds/_plot/_utils/_normalize.py +0 -73
  364. scitex/fts/_kinds/_plot/_utils/_validate.py +0 -197
  365. scitex/fts/_kinds/_table/_latex/_export.py +0 -279
  366. scitex/fts/_stats/__init__.py +0 -48
  367. scitex/fts/_stats/_dataclasses/_Stats.py +0 -423
  368. scitex/fts/_stats/_dataclasses/__init__.py +0 -48
  369. scitex/fts/_tables/__init__.py +0 -65
  370. scitex/fts/_tables/_latex/__init__.py +0 -93
  371. scitex/fts/_tables/_latex/_editor/__init__.py +0 -11
  372. scitex/fts/_tables/_latex/_editor/_app.py +0 -725
  373. scitex/fts/_tables/_latex/_figure_exporter.py +0 -153
  374. scitex/fts/_tables/_latex/_stats_formatter.py +0 -274
  375. scitex/fts/_tables/_latex/_table_exporter.py +0 -362
  376. scitex/fts/_tables/_latex/_utils.py +0 -369
  377. scitex/fts/_tables/_latex/_validator.py +0 -445
  378. scitex/io/_save_modules/_pltz_bundle.py +0 -356
  379. scitex-2.10.3.dist-info/METADATA +0 -952
  380. scitex-2.10.3.dist-info/entry_points.txt +0 -2
  381. /scitex/{fig → canvas}/README.md +0 -0
  382. /scitex/{fig → canvas}/backend/__init__.py +0 -0
  383. /scitex/{fig → canvas}/backend/_export.py +0 -0
  384. /scitex/{fig → canvas}/backend/_render.py +0 -0
  385. /scitex/{fig → canvas}/docs/CANVAS_ARCHITECTURE.md +0 -0
  386. /scitex/{fig → canvas}/editor/__init__.py +0 -0
  387. /scitex/{fig → canvas}/editor/_dearpygui_editor.py +0 -0
  388. /scitex/{fig → canvas}/editor/_flask_editor.py +0 -0
  389. /scitex/{fig → canvas}/editor/_mpl_editor.py +0 -0
  390. /scitex/{fig → canvas}/editor/_qt_editor.py +0 -0
  391. /scitex/{fig → canvas}/editor/_tkinter_editor.py +0 -0
  392. /scitex/{fig → canvas}/editor/edit/backend_detector.py +0 -0
  393. /scitex/{fig → canvas}/editor/edit/manual_handler.py +0 -0
  394. /scitex/{fig → canvas}/editor/edit/path_resolver.py +0 -0
  395. /scitex/{fig → canvas}/editor/flask_editor/__init__.py +0 -0
  396. /scitex/{fig → canvas}/editor/flask_editor/_plotter.py +0 -0
  397. /scitex/{fig → canvas}/editor/flask_editor/_utils.py +0 -0
  398. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/reset.css +0 -0
  399. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/typography.css +0 -0
  400. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/variables.css +0 -0
  401. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/buttons.css +0 -0
  402. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/context-menu.css +0 -0
  403. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/dropdown.css +0 -0
  404. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/forms.css +0 -0
  405. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/modal.css +0 -0
  406. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/sections.css +0 -0
  407. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/element-inspector.css +0 -0
  408. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/loading.css +0 -0
  409. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/overlay.css +0 -0
  410. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/selection.css +0 -0
  411. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/statistics.css +0 -0
  412. /scitex/{fig → canvas}/editor/flask_editor/static/css/index.css +0 -0
  413. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/container.css +0 -0
  414. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/controls.css +0 -0
  415. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/preview.css +0 -0
  416. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/axis.js +0 -0
  417. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/basic.js +0 -0
  418. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/distribute.js +0 -0
  419. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/canvas.js +0 -0
  420. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/dragging.js +0 -0
  421. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/resize.js +0 -0
  422. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/selection.js +0 -0
  423. /scitex/{fig → canvas}/editor/flask_editor/static/js/core/state.js +0 -0
  424. /scitex/{fig → canvas}/editor/flask_editor/static/js/core/utils.js +0 -0
  425. /scitex/{fig → canvas}/editor/flask_editor/static/js/dev/element-inspector.js +0 -0
  426. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/bbox.js +0 -0
  427. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/element-drag.js +0 -0
  428. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/overlay.js +0 -0
  429. /scitex/{fig → canvas}/editor/flask_editor/static/js/main.js +0 -0
  430. /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/context-menu.js +0 -0
  431. /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/keyboard.js +0 -0
  432. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/controls.js +0 -0
  433. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/download.js +0 -0
  434. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/help.js +0 -0
  435. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/theme.js +0 -0
  436. /scitex/{fig → canvas}/editor/flask_editor/templates/__init__.py +0 -0
  437. /scitex/{fig → canvas}/io/_directory.py +0 -0
  438. /scitex/{fig → canvas}/model/_plot_types.py +0 -0
  439. /scitex/{fig → canvas}/utils/_defaults.py +0 -0
  440. /scitex/{fig → canvas}/utils/_validate.py +0 -0
  441. /scitex/{fts/_bundle → io/bundle}/_conversion/__init__.py +0 -0
  442. /scitex/{fts/_bundle → io/bundle}/_conversion/_bundle2dict.py +0 -0
  443. /scitex/{fts/_bundle → io/bundle}/_conversion/_dict2bundle.py +0 -0
  444. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_Axes.py +0 -0
  445. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_BBox.py +0 -0
  446. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_ColumnDef.py +0 -0
  447. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataFormat.py +0 -0
  448. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataInfo.py +0 -0
  449. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataSource.py +0 -0
  450. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_SizeMM.py +0 -0
  451. /scitex/{fts/_bundle → io/bundle}/_extractors/__init__.py +0 -0
  452. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_bar.py +0 -0
  453. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_line.py +0 -0
  454. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_scatter.py +0 -0
  455. /scitex/{fts/_kinds → io/bundle/kinds}/__init__.py +0 -0
  456. /scitex/{fts/_kinds → io/bundle/kinds}/_figure/__init__.py +0 -0
  457. /scitex/{fts/_fig → io/bundle/kinds/_figure}/_composite.py +0 -0
  458. /scitex/{fts/_kinds → io/bundle/kinds}/_plot/__init__.py +0 -0
  459. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/__init__.py +0 -0
  460. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_export.py +0 -0
  461. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_render.py +0 -0
  462. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_ChannelEncoding.py +0 -0
  463. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Encoding.py +0 -0
  464. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Theme.py +0 -0
  465. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_TraceEncoding.py +0 -0
  466. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/__init__.py +0 -0
  467. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/__init__.py +0 -0
  468. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/__init__.py +0 -0
  469. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_bar.py +0 -0
  470. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_box.py +0 -0
  471. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_distribution.py +0 -0
  472. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_errorbar.py +0 -0
  473. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_histogram.py +0 -0
  474. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_image.py +0 -0
  475. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_line.py +0 -0
  476. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_scatter.py +0 -0
  477. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_seaborn.py +0 -0
  478. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_violin.py +0 -0
  479. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/__init__.py +0 -0
  480. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_auto_layout.py +0 -0
  481. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_calc_bounds.py +0 -0
  482. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_const_sizes.py +0 -0
  483. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_convert_coords.py +0 -0
  484. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_get_template.py +0 -0
  485. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_normalize.py +0 -0
  486. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_validate.py +0 -0
  487. /scitex/{fts/_kinds → io/bundle/kinds}/_shape/__init__.py +0 -0
  488. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/__init__.py +0 -0
  489. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/_Stats.py +0 -0
  490. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/__init__.py +0 -0
  491. /scitex/{fts/_kinds → io/bundle/kinds}/_table/__init__.py +0 -0
  492. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/__init__.py +0 -0
  493. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_stats_formatter.py +0 -0
  494. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_utils.py +0 -0
  495. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_validator.py +0 -0
  496. /scitex/{fts/_kinds → io/bundle/kinds}/_text/__init__.py +0 -0
  497. /scitex/{fts/_schemas → io/bundle/schemas}/data_info.schema.json +0 -0
  498. /scitex/{fts/_schemas → io/bundle/schemas}/encoding.schema.json +0 -0
  499. /scitex/{fts/_schemas → io/bundle/schemas}/node.schema.json +0 -0
  500. /scitex/{fts/_schemas → io/bundle/schemas}/render_manifest.schema.json +0 -0
  501. /scitex/{fts/_schemas → io/bundle/schemas}/stats.schema.json +0 -0
  502. /scitex/{fts/_schemas → io/bundle/schemas}/theme.schema.json +0 -0
  503. {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/WHEEL +0 -0
  504. {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1191 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-08
3
+ # File: src/scitex/stats/_mcp_handlers.py
4
+ # ----------------------------------------
5
+
6
+ """Handler implementations for the scitex-stats MCP server."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from datetime import datetime
12
+
13
+ import numpy as np
14
+
15
+ __all__ = [
16
+ "recommend_tests_handler",
17
+ "run_test_handler",
18
+ "format_results_handler",
19
+ "power_analysis_handler",
20
+ "correct_pvalues_handler",
21
+ "describe_handler",
22
+ "effect_size_handler",
23
+ "normality_test_handler",
24
+ "posthoc_test_handler",
25
+ "p_to_stars_handler",
26
+ ]
27
+
28
+
29
+ async def recommend_tests_handler(
30
+ n_groups: int = 2,
31
+ sample_sizes: list[int] | None = None,
32
+ outcome_type: str = "continuous",
33
+ design: str = "between",
34
+ paired: bool = False,
35
+ has_control_group: bool = False,
36
+ top_k: int = 3,
37
+ ) -> dict:
38
+ """Recommend appropriate statistical tests based on data characteristics."""
39
+ try:
40
+ from scitex.stats.auto import StatContext, recommend_tests
41
+
42
+ loop = asyncio.get_event_loop()
43
+
44
+ def do_recommend():
45
+ ctx = StatContext(
46
+ n_groups=n_groups,
47
+ sample_sizes=sample_sizes or [30] * n_groups,
48
+ outcome_type=outcome_type,
49
+ design=design,
50
+ paired=paired,
51
+ has_control_group=has_control_group,
52
+ n_factors=1,
53
+ )
54
+ tests = recommend_tests(ctx, top_k=top_k)
55
+
56
+ # Get details about each recommended test
57
+ from scitex.stats.auto._rules import TEST_RULES
58
+
59
+ recommendations = []
60
+ for test_name in tests:
61
+ rule = TEST_RULES.get(test_name)
62
+ if rule:
63
+ recommendations.append(
64
+ {
65
+ "name": test_name,
66
+ "family": rule.family,
67
+ "priority": rule.priority,
68
+ "needs_normality": rule.needs_normality,
69
+ "needs_equal_variance": rule.needs_equal_variance,
70
+ "rationale": _get_test_rationale(test_name),
71
+ }
72
+ )
73
+
74
+ return recommendations
75
+
76
+ recommendations = await loop.run_in_executor(None, do_recommend)
77
+
78
+ return {
79
+ "success": True,
80
+ "context": {
81
+ "n_groups": n_groups,
82
+ "sample_sizes": sample_sizes,
83
+ "outcome_type": outcome_type,
84
+ "design": design,
85
+ "paired": paired,
86
+ "has_control_group": has_control_group,
87
+ },
88
+ "recommendations": recommendations,
89
+ "timestamp": datetime.now().isoformat(),
90
+ }
91
+
92
+ except Exception as e:
93
+ return {"success": False, "error": str(e)}
94
+
95
+
96
+ def _get_test_rationale(test_name: str) -> str:
97
+ """Get rationale for recommending a specific test."""
98
+ rationales = {
99
+ "brunner_munzel": "Robust nonparametric test - no normality/equal variance assumptions",
100
+ "ttest_ind": "Classic parametric test for comparing two independent groups",
101
+ "ttest_paired": "Parametric test for paired/matched samples",
102
+ "ttest_1samp": "One-sample t-test for comparing to a population mean",
103
+ "mannwhitneyu": "Nonparametric alternative to independent t-test",
104
+ "wilcoxon": "Nonparametric alternative to paired t-test",
105
+ "anova": "Parametric test for comparing 3+ groups",
106
+ "kruskal": "Nonparametric alternative to one-way ANOVA",
107
+ "chi2": "Test for independence in contingency tables",
108
+ "fisher_exact": "Exact test for small sample contingency tables",
109
+ "pearson": "Parametric correlation coefficient",
110
+ "spearman": "Nonparametric rank correlation",
111
+ "kendall": "Robust nonparametric correlation for ordinal data",
112
+ }
113
+ return rationales.get(test_name, "Applicable to the given context")
114
+
115
+
116
+ async def run_test_handler(
117
+ test_name: str,
118
+ data: list[list[float]],
119
+ alternative: str = "two-sided",
120
+ ) -> dict:
121
+ """Execute a statistical test on provided data."""
122
+ try:
123
+ from scipy import stats as scipy_stats
124
+
125
+ loop = asyncio.get_event_loop()
126
+
127
+ def do_test():
128
+ # Convert data to numpy arrays
129
+ groups = [np.array(g, dtype=float) for g in data]
130
+
131
+ result = {}
132
+
133
+ # Run the appropriate test
134
+ if test_name == "ttest_ind":
135
+ if len(groups) != 2:
136
+ raise ValueError("t-test requires exactly 2 groups")
137
+ stat, p_value = scipy_stats.ttest_ind(
138
+ groups[0], groups[1], alternative=alternative
139
+ )
140
+ df = len(groups[0]) + len(groups[1]) - 2
141
+ result = {
142
+ "test": "Independent t-test",
143
+ "statistic": float(stat),
144
+ "statistic_name": "t",
145
+ "p_value": float(p_value),
146
+ "df": df,
147
+ }
148
+
149
+ elif test_name == "ttest_paired":
150
+ if len(groups) != 2:
151
+ raise ValueError("Paired t-test requires exactly 2 groups")
152
+ stat, p_value = scipy_stats.ttest_rel(
153
+ groups[0], groups[1], alternative=alternative
154
+ )
155
+ df = len(groups[0]) - 1
156
+ result = {
157
+ "test": "Paired t-test",
158
+ "statistic": float(stat),
159
+ "statistic_name": "t",
160
+ "p_value": float(p_value),
161
+ "df": df,
162
+ }
163
+
164
+ elif test_name == "ttest_1samp":
165
+ if len(groups) != 1:
166
+ raise ValueError("One-sample t-test requires exactly 1 group")
167
+ stat, p_value = scipy_stats.ttest_1samp(
168
+ groups[0], 0, alternative=alternative
169
+ )
170
+ df = len(groups[0]) - 1
171
+ result = {
172
+ "test": "One-sample t-test",
173
+ "statistic": float(stat),
174
+ "statistic_name": "t",
175
+ "p_value": float(p_value),
176
+ "df": df,
177
+ }
178
+
179
+ elif test_name == "brunner_munzel":
180
+ if len(groups) != 2:
181
+ raise ValueError("Brunner-Munzel requires exactly 2 groups")
182
+ res = scipy_stats.brunnermunzel(
183
+ groups[0], groups[1], alternative=alternative
184
+ )
185
+ result = {
186
+ "test": "Brunner-Munzel test",
187
+ "statistic": float(res.statistic),
188
+ "statistic_name": "BM",
189
+ "p_value": float(res.pvalue),
190
+ }
191
+
192
+ elif test_name == "mannwhitneyu":
193
+ if len(groups) != 2:
194
+ raise ValueError("Mann-Whitney U requires exactly 2 groups")
195
+ stat, p_value = scipy_stats.mannwhitneyu(
196
+ groups[0], groups[1], alternative=alternative
197
+ )
198
+ result = {
199
+ "test": "Mann-Whitney U test",
200
+ "statistic": float(stat),
201
+ "statistic_name": "U",
202
+ "p_value": float(p_value),
203
+ }
204
+
205
+ elif test_name == "wilcoxon":
206
+ if len(groups) != 2:
207
+ raise ValueError("Wilcoxon requires exactly 2 paired groups")
208
+ stat, p_value = scipy_stats.wilcoxon(
209
+ groups[0], groups[1], alternative=alternative
210
+ )
211
+ result = {
212
+ "test": "Wilcoxon signed-rank test",
213
+ "statistic": float(stat),
214
+ "statistic_name": "W",
215
+ "p_value": float(p_value),
216
+ }
217
+
218
+ elif test_name == "anova":
219
+ if len(groups) < 2:
220
+ raise ValueError("ANOVA requires at least 2 groups")
221
+ stat, p_value = scipy_stats.f_oneway(*groups)
222
+ df_between = len(groups) - 1
223
+ df_within = sum(len(g) for g in groups) - len(groups)
224
+ result = {
225
+ "test": "One-way ANOVA",
226
+ "statistic": float(stat),
227
+ "statistic_name": "F",
228
+ "p_value": float(p_value),
229
+ "df_between": df_between,
230
+ "df_within": df_within,
231
+ }
232
+
233
+ elif test_name == "kruskal":
234
+ if len(groups) < 2:
235
+ raise ValueError("Kruskal-Wallis requires at least 2 groups")
236
+ stat, p_value = scipy_stats.kruskal(*groups)
237
+ result = {
238
+ "test": "Kruskal-Wallis H test",
239
+ "statistic": float(stat),
240
+ "statistic_name": "H",
241
+ "p_value": float(p_value),
242
+ "df": len(groups) - 1,
243
+ }
244
+
245
+ elif test_name == "chi2":
246
+ # Expects contingency table as data
247
+ table = np.array(data)
248
+ chi2, p_value, dof, expected = scipy_stats.chi2_contingency(table)
249
+ result = {
250
+ "test": "Chi-square test of independence",
251
+ "statistic": float(chi2),
252
+ "statistic_name": "chi2",
253
+ "p_value": float(p_value),
254
+ "df": int(dof),
255
+ "expected_frequencies": expected.tolist(),
256
+ }
257
+
258
+ elif test_name == "fisher_exact":
259
+ # Expects 2x2 contingency table
260
+ table = np.array(data)
261
+ if table.shape != (2, 2):
262
+ raise ValueError("Fisher's exact test requires a 2x2 table")
263
+ odds_ratio, p_value = scipy_stats.fisher_exact(
264
+ table, alternative=alternative
265
+ )
266
+ result = {
267
+ "test": "Fisher's exact test",
268
+ "statistic": float(odds_ratio),
269
+ "statistic_name": "odds_ratio",
270
+ "p_value": float(p_value),
271
+ }
272
+
273
+ elif test_name == "pearson":
274
+ if len(groups) != 2:
275
+ raise ValueError("Pearson correlation requires exactly 2 variables")
276
+ r, p_value = scipy_stats.pearsonr(groups[0], groups[1])
277
+ result = {
278
+ "test": "Pearson correlation",
279
+ "statistic": float(r),
280
+ "statistic_name": "r",
281
+ "p_value": float(p_value),
282
+ }
283
+
284
+ elif test_name == "spearman":
285
+ if len(groups) != 2:
286
+ raise ValueError(
287
+ "Spearman correlation requires exactly 2 variables"
288
+ )
289
+ r, p_value = scipy_stats.spearmanr(groups[0], groups[1])
290
+ result = {
291
+ "test": "Spearman correlation",
292
+ "statistic": float(r),
293
+ "statistic_name": "rho",
294
+ "p_value": float(p_value),
295
+ }
296
+
297
+ elif test_name == "kendall":
298
+ if len(groups) != 2:
299
+ raise ValueError("Kendall correlation requires exactly 2 variables")
300
+ tau, p_value = scipy_stats.kendalltau(groups[0], groups[1])
301
+ result = {
302
+ "test": "Kendall tau correlation",
303
+ "statistic": float(tau),
304
+ "statistic_name": "tau",
305
+ "p_value": float(p_value),
306
+ }
307
+
308
+ else:
309
+ raise ValueError(f"Unknown test: {test_name}")
310
+
311
+ # Calculate effect size if applicable
312
+ if test_name in [
313
+ "ttest_ind",
314
+ "ttest_paired",
315
+ "brunner_munzel",
316
+ "mannwhitneyu",
317
+ ]:
318
+ from scitex.stats.effect_sizes import cliffs_delta, cohens_d
319
+
320
+ if len(groups) == 2:
321
+ d = cohens_d(groups[0], groups[1])
322
+ delta = cliffs_delta(groups[0], groups[1])
323
+ result["effect_size"] = {
324
+ "cohens_d": float(d),
325
+ "cliffs_delta": float(delta),
326
+ }
327
+
328
+ # Add significance determination
329
+ alpha = 0.05
330
+ result["significant"] = result["p_value"] < alpha
331
+ result["alpha"] = alpha
332
+
333
+ return result
334
+
335
+ result = await loop.run_in_executor(None, do_test)
336
+
337
+ return {
338
+ "success": True,
339
+ "test_name": test_name,
340
+ "alternative": alternative,
341
+ **result,
342
+ "timestamp": datetime.now().isoformat(),
343
+ }
344
+
345
+ except Exception as e:
346
+ return {"success": False, "error": str(e)}
347
+
348
+
349
+ async def format_results_handler(
350
+ test_name: str,
351
+ statistic: float,
352
+ p_value: float,
353
+ df: float | None = None,
354
+ effect_size: float | None = None,
355
+ effect_size_name: str | None = None,
356
+ style: str = "apa",
357
+ ci_lower: float | None = None,
358
+ ci_upper: float | None = None,
359
+ ) -> dict:
360
+ """Format statistical results in journal style."""
361
+ try:
362
+ loop = asyncio.get_event_loop()
363
+
364
+ def do_format():
365
+ from scitex.stats.auto import format_test_line, p_to_stars
366
+ from scitex.stats.auto._formatting import EffectResultDict, TestResultDict
367
+
368
+ # Build test result dict
369
+ test_result: TestResultDict = {
370
+ "test_name": test_name,
371
+ "stat": statistic,
372
+ "p_raw": p_value,
373
+ }
374
+ if df is not None:
375
+ test_result["df"] = df
376
+
377
+ # Build effect result if provided
378
+ effects = None
379
+ if effect_size is not None:
380
+ effects = [
381
+ EffectResultDict(
382
+ name=effect_size_name or "d",
383
+ label=effect_size_name or "Cohen's d",
384
+ value=effect_size,
385
+ ci_lower=ci_lower,
386
+ ci_upper=ci_upper,
387
+ )
388
+ ]
389
+
390
+ # Map style names
391
+ style_map = {
392
+ "apa": "apa_latex",
393
+ "nature": "nature",
394
+ "science": "science",
395
+ "brief": "brief",
396
+ }
397
+ style_id = style_map.get(style, "apa_latex")
398
+
399
+ # Format the line
400
+ formatted = format_test_line(
401
+ test_result,
402
+ effects=effects,
403
+ style=style_id,
404
+ include_n=False,
405
+ )
406
+
407
+ # Get stars representation
408
+ stars = p_to_stars(p_value)
409
+
410
+ return {
411
+ "formatted": formatted,
412
+ "stars": stars,
413
+ }
414
+
415
+ result = await loop.run_in_executor(None, do_format)
416
+
417
+ return {
418
+ "success": True,
419
+ "style": style,
420
+ **result,
421
+ "timestamp": datetime.now().isoformat(),
422
+ }
423
+
424
+ except Exception as e:
425
+ return {"success": False, "error": str(e)}
426
+
427
+
428
+ async def power_analysis_handler(
429
+ test_type: str = "ttest",
430
+ effect_size: float | None = None,
431
+ alpha: float = 0.05,
432
+ power: float = 0.8,
433
+ n: int | None = None,
434
+ n_groups: int = 2,
435
+ ratio: float = 1.0,
436
+ ) -> dict:
437
+ """Calculate statistical power or required sample size."""
438
+ try:
439
+ loop = asyncio.get_event_loop()
440
+
441
+ def do_power():
442
+ from scitex.stats.power._power import power_ttest, sample_size_ttest
443
+
444
+ result = {}
445
+
446
+ if test_type == "ttest":
447
+ if n is not None and effect_size is not None:
448
+ # Calculate power given n and effect size
449
+ calculated_power = power_ttest(
450
+ effect_size=effect_size,
451
+ n1=n,
452
+ n2=int(n * ratio),
453
+ alpha=alpha,
454
+ test_type="two-sample",
455
+ )
456
+ result = {
457
+ "mode": "power_calculation",
458
+ "power": calculated_power,
459
+ "n1": n,
460
+ "n2": int(n * ratio),
461
+ "effect_size": effect_size,
462
+ "alpha": alpha,
463
+ }
464
+ elif effect_size is not None:
465
+ # Calculate required sample size
466
+ n1, n2 = sample_size_ttest(
467
+ effect_size=effect_size,
468
+ power=power,
469
+ alpha=alpha,
470
+ ratio=ratio,
471
+ )
472
+ result = {
473
+ "mode": "sample_size_calculation",
474
+ "required_n1": n1,
475
+ "required_n2": n2,
476
+ "total_n": n1 + n2,
477
+ "effect_size": effect_size,
478
+ "target_power": power,
479
+ "alpha": alpha,
480
+ }
481
+ else:
482
+ raise ValueError("Either n or effect_size must be provided")
483
+
484
+ elif test_type == "anova":
485
+ # Simplified ANOVA power (using f = d * sqrt(k-1) / sqrt(2k))
486
+ if effect_size is None:
487
+ raise ValueError("effect_size required for ANOVA power")
488
+
489
+ # Convert Cohen's f to d for approximation
490
+ # This is a simplified calculation
491
+ from scipy import stats as scipy_stats
492
+
493
+ if n is not None:
494
+ df1 = n_groups - 1
495
+ df2 = n_groups * n - n_groups
496
+ nc = effect_size**2 * n * n_groups
497
+ f_crit = scipy_stats.f.ppf(1 - alpha, df1, df2)
498
+ power_val = 1 - scipy_stats.ncf.cdf(f_crit, df1, df2, nc)
499
+ result = {
500
+ "mode": "power_calculation",
501
+ "power": power_val,
502
+ "n_per_group": n,
503
+ "n_groups": n_groups,
504
+ "effect_size_f": effect_size,
505
+ "alpha": alpha,
506
+ }
507
+ else:
508
+ # Binary search for n
509
+ n_min, n_max = 2, 1000
510
+ while n_max - n_min > 1:
511
+ n_mid = (n_min + n_max) // 2
512
+ df1 = n_groups - 1
513
+ df2 = n_groups * n_mid - n_groups
514
+ nc = effect_size**2 * n_mid * n_groups
515
+ f_crit = scipy_stats.f.ppf(1 - alpha, df1, df2)
516
+ power_val = 1 - scipy_stats.ncf.cdf(f_crit, df1, df2, nc)
517
+ if power_val < power:
518
+ n_min = n_mid
519
+ else:
520
+ n_max = n_mid
521
+
522
+ result = {
523
+ "mode": "sample_size_calculation",
524
+ "required_n_per_group": n_max,
525
+ "total_n": n_max * n_groups,
526
+ "n_groups": n_groups,
527
+ "effect_size_f": effect_size,
528
+ "target_power": power,
529
+ "alpha": alpha,
530
+ }
531
+
532
+ elif test_type == "correlation":
533
+ # Power for correlation coefficient
534
+ from scipy import stats as scipy_stats
535
+
536
+ if effect_size is None:
537
+ raise ValueError("effect_size (r) required for correlation power")
538
+
539
+ if n is not None:
540
+ # Calculate power
541
+ z = 0.5 * np.log((1 + effect_size) / (1 - effect_size))
542
+ se = 1 / np.sqrt(n - 3)
543
+ z_crit = scipy_stats.norm.ppf(1 - alpha / 2)
544
+ power_val = (
545
+ 1
546
+ - scipy_stats.norm.cdf(z_crit - z / se)
547
+ + scipy_stats.norm.cdf(-z_crit - z / se)
548
+ )
549
+ result = {
550
+ "mode": "power_calculation",
551
+ "power": power_val,
552
+ "n": n,
553
+ "effect_size_r": effect_size,
554
+ "alpha": alpha,
555
+ }
556
+ else:
557
+ # Calculate required n (binary search)
558
+ z = 0.5 * np.log((1 + effect_size) / (1 - effect_size))
559
+ z_crit = scipy_stats.norm.ppf(1 - alpha / 2)
560
+ z_power = scipy_stats.norm.ppf(power)
561
+ required_n = int(np.ceil(((z_crit + z_power) / z) ** 2 + 3))
562
+ result = {
563
+ "mode": "sample_size_calculation",
564
+ "required_n": required_n,
565
+ "effect_size_r": effect_size,
566
+ "target_power": power,
567
+ "alpha": alpha,
568
+ }
569
+
570
+ elif test_type == "chi2":
571
+ # Chi-square power (simplified)
572
+ from scipy import stats as scipy_stats
573
+
574
+ if effect_size is None:
575
+ raise ValueError("effect_size (w) required for chi2 power")
576
+
577
+ df = n_groups - 1 # Simplified: using n_groups as number of cells
578
+
579
+ if n is not None:
580
+ nc = effect_size**2 * n
581
+ chi2_crit = scipy_stats.chi2.ppf(1 - alpha, df)
582
+ power_val = 1 - scipy_stats.ncx2.cdf(chi2_crit, df, nc)
583
+ result = {
584
+ "mode": "power_calculation",
585
+ "power": power_val,
586
+ "n": n,
587
+ "df": df,
588
+ "effect_size_w": effect_size,
589
+ "alpha": alpha,
590
+ }
591
+ else:
592
+ # Binary search for n
593
+ n_min, n_max = 10, 10000
594
+ while n_max - n_min > 1:
595
+ n_mid = (n_min + n_max) // 2
596
+ nc = effect_size**2 * n_mid
597
+ chi2_crit = scipy_stats.chi2.ppf(1 - alpha, df)
598
+ power_val = 1 - scipy_stats.ncx2.cdf(chi2_crit, df, nc)
599
+ if power_val < power:
600
+ n_min = n_mid
601
+ else:
602
+ n_max = n_mid
603
+
604
+ result = {
605
+ "mode": "sample_size_calculation",
606
+ "required_n": n_max,
607
+ "df": df,
608
+ "effect_size_w": effect_size,
609
+ "target_power": power,
610
+ "alpha": alpha,
611
+ }
612
+
613
+ else:
614
+ raise ValueError(f"Unknown test_type: {test_type}")
615
+
616
+ return result
617
+
618
+ result = await loop.run_in_executor(None, do_power)
619
+
620
+ return {
621
+ "success": True,
622
+ "test_type": test_type,
623
+ **result,
624
+ "timestamp": datetime.now().isoformat(),
625
+ }
626
+
627
+ except Exception as e:
628
+ return {"success": False, "error": str(e)}
629
+
630
+
631
+ async def correct_pvalues_handler(
632
+ pvalues: list[float],
633
+ method: str = "fdr_bh",
634
+ alpha: float = 0.05,
635
+ ) -> dict:
636
+ """Apply multiple comparison correction to p-values."""
637
+ try:
638
+ loop = asyncio.get_event_loop()
639
+
640
+ def do_correct():
641
+ from statsmodels.stats.multitest import multipletests
642
+
643
+ # Map method names
644
+ method_map = {
645
+ "bonferroni": "bonferroni",
646
+ "fdr_bh": "fdr_bh",
647
+ "fdr_by": "fdr_by",
648
+ "holm": "holm",
649
+ "sidak": "sidak",
650
+ }
651
+ sm_method = method_map.get(method, "fdr_bh")
652
+
653
+ pvals = np.array(pvalues)
654
+ reject, pvals_corrected, _, _ = multipletests(
655
+ pvals, alpha=alpha, method=sm_method
656
+ )
657
+
658
+ return {
659
+ "original_pvalues": pvalues,
660
+ "corrected_pvalues": pvals_corrected.tolist(),
661
+ "reject_null": reject.tolist(),
662
+ "n_significant": int(reject.sum()),
663
+ "n_tests": len(pvalues),
664
+ }
665
+
666
+ result = await loop.run_in_executor(None, do_correct)
667
+
668
+ return {
669
+ "success": True,
670
+ "method": method,
671
+ "alpha": alpha,
672
+ **result,
673
+ "timestamp": datetime.now().isoformat(),
674
+ }
675
+
676
+ except ImportError:
677
+ # Fallback implementation without statsmodels
678
+ try:
679
+ n = len(pvalues)
680
+ pvals = np.array(pvalues)
681
+
682
+ if method == "bonferroni":
683
+ corrected = np.minimum(pvals * n, 1.0)
684
+ elif method == "holm":
685
+ sorted_idx = np.argsort(pvals)
686
+ corrected = np.empty(n)
687
+ cummax = 0.0
688
+ for rank, idx in enumerate(sorted_idx, start=1):
689
+ adj = min((n - rank + 1) * pvals[idx], 1.0)
690
+ adj = max(adj, cummax)
691
+ corrected[idx] = adj
692
+ cummax = adj
693
+ elif method == "fdr_bh":
694
+ sorted_idx = np.argsort(pvals)
695
+ corrected = np.empty(n)
696
+ prev = 1.0
697
+ for rank in range(n, 0, -1):
698
+ idx = sorted_idx[rank - 1]
699
+ bh = pvals[idx] * n / rank
700
+ val = min(bh, prev, 1.0)
701
+ corrected[idx] = val
702
+ prev = val
703
+ elif method == "sidak":
704
+ corrected = 1 - (1 - pvals) ** n
705
+ else:
706
+ corrected = pvals
707
+
708
+ return {
709
+ "success": True,
710
+ "method": method,
711
+ "alpha": alpha,
712
+ "original_pvalues": pvalues,
713
+ "corrected_pvalues": corrected.tolist(),
714
+ "reject_null": (corrected < alpha).tolist(),
715
+ "n_significant": int((corrected < alpha).sum()),
716
+ "n_tests": n,
717
+ "timestamp": datetime.now().isoformat(),
718
+ }
719
+
720
+ except Exception as e:
721
+ return {"success": False, "error": str(e)}
722
+
723
+ except Exception as e:
724
+ return {"success": False, "error": str(e)}
725
+
726
+
727
+ async def describe_handler(
728
+ data: list[float],
729
+ percentiles: list[float] | None = None,
730
+ ) -> dict:
731
+ """Calculate descriptive statistics for data."""
732
+ try:
733
+ loop = asyncio.get_event_loop()
734
+
735
+ def do_describe():
736
+ arr = np.array(data, dtype=float)
737
+ arr = arr[~np.isnan(arr)] # Remove NaN
738
+
739
+ if len(arr) == 0:
740
+ return {"error": "No valid data points"}
741
+
742
+ percs = percentiles or [25, 50, 75]
743
+ percentile_values = np.percentile(arr, percs)
744
+
745
+ result = {
746
+ "n": int(len(arr)),
747
+ "mean": float(np.mean(arr)),
748
+ "std": float(np.std(arr, ddof=1)) if len(arr) > 1 else 0.0,
749
+ "var": float(np.var(arr, ddof=1)) if len(arr) > 1 else 0.0,
750
+ "sem": (
751
+ float(np.std(arr, ddof=1) / np.sqrt(len(arr)))
752
+ if len(arr) > 1
753
+ else 0.0
754
+ ),
755
+ "min": float(np.min(arr)),
756
+ "max": float(np.max(arr)),
757
+ "range": float(np.max(arr) - np.min(arr)),
758
+ "median": float(np.median(arr)),
759
+ "percentiles": {
760
+ str(int(p)): float(v) for p, v in zip(percs, percentile_values)
761
+ },
762
+ "iqr": float(np.percentile(arr, 75) - np.percentile(arr, 25)),
763
+ }
764
+
765
+ # Add skewness and kurtosis if scipy available
766
+ try:
767
+ from scipy import stats as scipy_stats
768
+
769
+ result["skewness"] = float(scipy_stats.skew(arr))
770
+ result["kurtosis"] = float(scipy_stats.kurtosis(arr))
771
+ except ImportError:
772
+ pass
773
+
774
+ return result
775
+
776
+ result = await loop.run_in_executor(None, do_describe)
777
+
778
+ return {
779
+ "success": True,
780
+ **result,
781
+ "timestamp": datetime.now().isoformat(),
782
+ }
783
+
784
+ except Exception as e:
785
+ return {"success": False, "error": str(e)}
786
+
787
+
788
+ async def effect_size_handler(
789
+ group1: list[float],
790
+ group2: list[float],
791
+ measure: str = "cohens_d",
792
+ pooled: bool = True,
793
+ ) -> dict:
794
+ """Calculate effect size between groups."""
795
+ try:
796
+ from scitex.stats.effect_sizes import (
797
+ cliffs_delta,
798
+ cohens_d,
799
+ interpret_cliffs_delta,
800
+ interpret_cohens_d,
801
+ )
802
+
803
+ loop = asyncio.get_event_loop()
804
+
805
+ def do_effect_size():
806
+ g1 = np.array(group1, dtype=float)
807
+ g2 = np.array(group2, dtype=float)
808
+
809
+ result = {}
810
+
811
+ if measure == "cohens_d":
812
+ d = cohens_d(g1, g2)
813
+ result = {
814
+ "measure": "Cohen's d",
815
+ "value": float(d),
816
+ "interpretation": interpret_cohens_d(d),
817
+ }
818
+
819
+ elif measure == "hedges_g":
820
+ # Hedges' g is Cohen's d with bias correction
821
+ d = cohens_d(g1, g2)
822
+ n1, n2 = len(g1), len(g2)
823
+ correction = 1 - (3 / (4 * (n1 + n2) - 9))
824
+ g = d * correction
825
+ result = {
826
+ "measure": "Hedges' g",
827
+ "value": float(g),
828
+ "interpretation": interpret_cohens_d(g), # Same thresholds
829
+ }
830
+
831
+ elif measure == "glass_delta":
832
+ # Glass's delta uses only control group std
833
+ mean_diff = np.mean(g1) - np.mean(g2)
834
+ delta = mean_diff / np.std(g2, ddof=1)
835
+ result = {
836
+ "measure": "Glass's delta",
837
+ "value": float(delta),
838
+ "interpretation": interpret_cohens_d(delta),
839
+ }
840
+
841
+ elif measure == "cliffs_delta":
842
+ delta = cliffs_delta(g1, g2)
843
+ result = {
844
+ "measure": "Cliff's delta",
845
+ "value": float(delta),
846
+ "interpretation": interpret_cliffs_delta(delta),
847
+ }
848
+
849
+ else:
850
+ raise ValueError(f"Unknown measure: {measure}")
851
+
852
+ # Add confidence interval approximation for Cohen's d
853
+ if measure in ["cohens_d", "hedges_g", "glass_delta"]:
854
+ n1, n2 = len(g1), len(g2)
855
+ se = np.sqrt(
856
+ (n1 + n2) / (n1 * n2) + result["value"] ** 2 / (2 * (n1 + n2))
857
+ )
858
+ result["ci_lower"] = float(result["value"] - 1.96 * se)
859
+ result["ci_upper"] = float(result["value"] + 1.96 * se)
860
+
861
+ return result
862
+
863
+ result = await loop.run_in_executor(None, do_effect_size)
864
+
865
+ return {
866
+ "success": True,
867
+ "group1_n": len(group1),
868
+ "group2_n": len(group2),
869
+ **result,
870
+ "timestamp": datetime.now().isoformat(),
871
+ }
872
+
873
+ except Exception as e:
874
+ return {"success": False, "error": str(e)}
875
+
876
+
877
+ async def normality_test_handler(
878
+ data: list[float],
879
+ method: str = "shapiro",
880
+ ) -> dict:
881
+ """Test whether data follows a normal distribution."""
882
+ try:
883
+ from scipy import stats as scipy_stats
884
+
885
+ loop = asyncio.get_event_loop()
886
+
887
+ def do_normality():
888
+ arr = np.array(data, dtype=float)
889
+ arr = arr[~np.isnan(arr)]
890
+
891
+ if len(arr) < 3:
892
+ return {"error": "Need at least 3 data points"}
893
+
894
+ result = {}
895
+
896
+ if method == "shapiro":
897
+ stat, p_value = scipy_stats.shapiro(arr)
898
+ result = {
899
+ "test": "Shapiro-Wilk",
900
+ "statistic": float(stat),
901
+ "statistic_name": "W",
902
+ "p_value": float(p_value),
903
+ }
904
+
905
+ elif method == "dagostino":
906
+ if len(arr) < 8:
907
+ return {"error": "D'Agostino test requires at least 8 samples"}
908
+ stat, p_value = scipy_stats.normaltest(arr)
909
+ result = {
910
+ "test": "D'Agostino-Pearson",
911
+ "statistic": float(stat),
912
+ "statistic_name": "K2",
913
+ "p_value": float(p_value),
914
+ }
915
+
916
+ elif method == "anderson":
917
+ res = scipy_stats.anderson(arr, dist="norm")
918
+ # Use 5% significance level
919
+ idx = 2 # Index for 5% level
920
+ result = {
921
+ "test": "Anderson-Darling",
922
+ "statistic": float(res.statistic),
923
+ "statistic_name": "A2",
924
+ "critical_value_5pct": float(res.critical_values[idx]),
925
+ "normal": bool(res.statistic < res.critical_values[idx]),
926
+ }
927
+
928
+ elif method == "lilliefors":
929
+ try:
930
+ from statsmodels.stats.diagnostic import lilliefors
931
+
932
+ stat, p_value = lilliefors(arr, dist="norm")
933
+ result = {
934
+ "test": "Lilliefors",
935
+ "statistic": float(stat),
936
+ "statistic_name": "D",
937
+ "p_value": float(p_value),
938
+ }
939
+ except ImportError:
940
+ return {"error": "statsmodels required for Lilliefors test"}
941
+
942
+ else:
943
+ raise ValueError(f"Unknown method: {method}")
944
+
945
+ # Add interpretation
946
+ if "p_value" in result:
947
+ result["is_normal"] = result["p_value"] >= 0.05
948
+ result["interpretation"] = (
949
+ "Data appears normally distributed (p >= 0.05)"
950
+ if result["is_normal"]
951
+ else "Data deviates from normal distribution (p < 0.05)"
952
+ )
953
+
954
+ return result
955
+
956
+ result = await loop.run_in_executor(None, do_normality)
957
+
958
+ return {
959
+ "success": True,
960
+ "method": method,
961
+ "n": len(data),
962
+ **result,
963
+ "timestamp": datetime.now().isoformat(),
964
+ }
965
+
966
+ except Exception as e:
967
+ return {"success": False, "error": str(e)}
968
+
969
+
970
+ async def posthoc_test_handler(
971
+ groups: list[list[float]],
972
+ group_names: list[str] | None = None,
973
+ method: str = "tukey",
974
+ control_group: int = 0,
975
+ ) -> dict:
976
+ """Run post-hoc pairwise comparisons."""
977
+ try:
978
+ loop = asyncio.get_event_loop()
979
+
980
+ def do_posthoc():
981
+ group_arrays = [np.array(g, dtype=float) for g in groups]
982
+ names = group_names or [f"Group_{i + 1}" for i in range(len(groups))]
983
+
984
+ comparisons = []
985
+
986
+ if method == "tukey":
987
+ from scipy import stats as scipy_stats
988
+
989
+ # All pairwise comparisons with Tukey HSD approximation
990
+ all_data = np.concatenate(group_arrays)
991
+ group_labels = np.concatenate(
992
+ [[names[i]] * len(g) for i, g in enumerate(group_arrays)]
993
+ )
994
+
995
+ # Use statsmodels if available, otherwise manual calculation
996
+ try:
997
+ from statsmodels.stats.multicomp import pairwise_tukeyhsd
998
+
999
+ tukey = pairwise_tukeyhsd(all_data, group_labels)
1000
+
1001
+ for i in range(len(tukey.summary().data) - 1):
1002
+ row = tukey.summary().data[i + 1]
1003
+ comparisons.append(
1004
+ {
1005
+ "group1": str(row[0]),
1006
+ "group2": str(row[1]),
1007
+ "mean_diff": float(row[2]),
1008
+ "p_adj": float(row[3]),
1009
+ "ci_lower": float(row[4]),
1010
+ "ci_upper": float(row[5]),
1011
+ "reject": bool(row[6]),
1012
+ }
1013
+ )
1014
+ except ImportError:
1015
+ # Fallback: Bonferroni-corrected t-tests
1016
+ n_comparisons = len(groups) * (len(groups) - 1) // 2
1017
+ for i in range(len(groups)):
1018
+ for j in range(i + 1, len(groups)):
1019
+ stat, p = scipy_stats.ttest_ind(
1020
+ group_arrays[i], group_arrays[j]
1021
+ )
1022
+ p_adj = min(p * n_comparisons, 1.0)
1023
+ comparisons.append(
1024
+ {
1025
+ "group1": names[i],
1026
+ "group2": names[j],
1027
+ "mean_diff": float(
1028
+ np.mean(group_arrays[i])
1029
+ - np.mean(group_arrays[j])
1030
+ ),
1031
+ "t_statistic": float(stat),
1032
+ "p_value": float(p),
1033
+ "p_adj": float(p_adj),
1034
+ "reject": p_adj < 0.05,
1035
+ }
1036
+ )
1037
+
1038
+ elif method == "dunnett":
1039
+ from scipy import stats as scipy_stats
1040
+
1041
+ # Compare all groups to control
1042
+ control = group_arrays[control_group]
1043
+ n_comparisons = len(groups) - 1
1044
+
1045
+ for i, (name, group) in enumerate(zip(names, group_arrays)):
1046
+ if i == control_group:
1047
+ continue
1048
+ stat, p = scipy_stats.ttest_ind(group, control)
1049
+ p_adj = min(p * n_comparisons, 1.0)
1050
+ comparisons.append(
1051
+ {
1052
+ "group": name,
1053
+ "vs_control": names[control_group],
1054
+ "mean_diff": float(np.mean(group) - np.mean(control)),
1055
+ "t_statistic": float(stat),
1056
+ "p_value": float(p),
1057
+ "p_adj": float(p_adj),
1058
+ "reject": p_adj < 0.05,
1059
+ }
1060
+ )
1061
+
1062
+ elif method == "games_howell":
1063
+ from scipy import stats as scipy_stats
1064
+
1065
+ # Games-Howell doesn't assume equal variances
1066
+ for i in range(len(groups)):
1067
+ for j in range(i + 1, len(groups)):
1068
+ g1, g2 = group_arrays[i], group_arrays[j]
1069
+ n1, n2 = len(g1), len(g2)
1070
+ m1, m2 = np.mean(g1), np.mean(g2)
1071
+ v1, v2 = np.var(g1, ddof=1), np.var(g2, ddof=1)
1072
+
1073
+ se = np.sqrt(v1 / n1 + v2 / n2)
1074
+ t_stat = (m1 - m2) / se
1075
+
1076
+ # Welch-Satterthwaite df
1077
+ df = (v1 / n1 + v2 / n2) ** 2 / (
1078
+ (v1 / n1) ** 2 / (n1 - 1) + (v2 / n2) ** 2 / (n2 - 1)
1079
+ )
1080
+
1081
+ p = 2 * (1 - scipy_stats.t.cdf(abs(t_stat), df))
1082
+ n_comparisons = len(groups) * (len(groups) - 1) // 2
1083
+ p_adj = min(p * n_comparisons, 1.0)
1084
+
1085
+ comparisons.append(
1086
+ {
1087
+ "group1": names[i],
1088
+ "group2": names[j],
1089
+ "mean_diff": float(m1 - m2),
1090
+ "t_statistic": float(t_stat),
1091
+ "df": float(df),
1092
+ "p_value": float(p),
1093
+ "p_adj": float(p_adj),
1094
+ "reject": p_adj < 0.05,
1095
+ }
1096
+ )
1097
+
1098
+ elif method == "dunn":
1099
+ from scipy import stats as scipy_stats
1100
+
1101
+ # Dunn's test for Kruskal-Wallis post-hoc
1102
+ all_data = np.concatenate(group_arrays)
1103
+ ranks = scipy_stats.rankdata(all_data)
1104
+
1105
+ # Assign ranks to groups
1106
+ idx = 0
1107
+ group_ranks = []
1108
+ for g in group_arrays:
1109
+ group_ranks.append(ranks[idx : idx + len(g)])
1110
+ idx += len(g)
1111
+
1112
+ n_total = len(all_data)
1113
+ n_comparisons = len(groups) * (len(groups) - 1) // 2
1114
+
1115
+ for i in range(len(groups)):
1116
+ for j in range(i + 1, len(groups)):
1117
+ n_i, n_j = len(group_arrays[i]), len(group_arrays[j])
1118
+ r_i, r_j = np.mean(group_ranks[i]), np.mean(group_ranks[j])
1119
+
1120
+ se = np.sqrt(n_total * (n_total + 1) / 12 * (1 / n_i + 1 / n_j))
1121
+ z = (r_i - r_j) / se
1122
+ p = 2 * (1 - scipy_stats.norm.cdf(abs(z)))
1123
+ p_adj = min(p * n_comparisons, 1.0)
1124
+
1125
+ comparisons.append(
1126
+ {
1127
+ "group1": names[i],
1128
+ "group2": names[j],
1129
+ "mean_rank_diff": float(r_i - r_j),
1130
+ "z_statistic": float(z),
1131
+ "p_value": float(p),
1132
+ "p_adj": float(p_adj),
1133
+ "reject": p_adj < 0.05,
1134
+ }
1135
+ )
1136
+
1137
+ else:
1138
+ raise ValueError(f"Unknown method: {method}")
1139
+
1140
+ return comparisons
1141
+
1142
+ comparisons = await loop.run_in_executor(None, do_posthoc)
1143
+
1144
+ return {
1145
+ "success": True,
1146
+ "method": method,
1147
+ "n_groups": len(groups),
1148
+ "n_comparisons": len(comparisons),
1149
+ "comparisons": comparisons,
1150
+ "timestamp": datetime.now().isoformat(),
1151
+ }
1152
+
1153
+ except Exception as e:
1154
+ return {"success": False, "error": str(e)}
1155
+
1156
+
1157
+ async def p_to_stars_handler(
1158
+ p_value: float,
1159
+ thresholds: list[float] | None = None,
1160
+ ) -> dict:
1161
+ """Convert p-value to significance stars."""
1162
+ try:
1163
+ thresh = thresholds or [0.001, 0.01, 0.05]
1164
+
1165
+ if p_value < thresh[0]:
1166
+ stars = "***"
1167
+ significance = f"p < {thresh[0]}"
1168
+ elif p_value < thresh[1]:
1169
+ stars = "**"
1170
+ significance = f"p < {thresh[1]}"
1171
+ elif p_value < thresh[2]:
1172
+ stars = "*"
1173
+ significance = f"p < {thresh[2]}"
1174
+ else:
1175
+ stars = "ns"
1176
+ significance = f"p >= {thresh[2]} (not significant)"
1177
+
1178
+ return {
1179
+ "success": True,
1180
+ "p_value": p_value,
1181
+ "stars": stars,
1182
+ "significance": significance,
1183
+ "thresholds": thresh,
1184
+ "timestamp": datetime.now().isoformat(),
1185
+ }
1186
+
1187
+ except Exception as e:
1188
+ return {"success": False, "error": str(e)}
1189
+
1190
+
1191
+ # EOF