scitex 2.14.0__py3-none-any.whl → 2.15.2__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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +481 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +23 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +132 -8
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.2.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# External Package Branding Guide
|
|
2
|
+
|
|
3
|
+
When scitex wraps external packages (like `figrecipe` for `scitex.plt` or `crossref-local` for `scitex.scholar`), those packages should support configurable branding so documentation and error messages show the scitex namespace.
|
|
4
|
+
|
|
5
|
+
## When to Use Branding
|
|
6
|
+
|
|
7
|
+
- **Use branding**: External packages that scitex wraps (figrecipe, crossref-local, etc.)
|
|
8
|
+
- **Don't use branding**: Internal scitex modules (scitex.audio, scitex.stats, etc.) - just hardcode `SCITEX_*` prefix
|
|
9
|
+
|
|
10
|
+
## Pattern Overview
|
|
11
|
+
|
|
12
|
+
The external package provides a `_branding.py` module that:
|
|
13
|
+
1. Reads brand name from environment variable
|
|
14
|
+
2. Derives environment variable prefix from brand name
|
|
15
|
+
3. Provides helper functions for rebranding text/docstrings
|
|
16
|
+
|
|
17
|
+
## Implementation Template
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# external_package/_branding.py
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
# Environment variables for branding
|
|
27
|
+
# Parent package sets these before importing
|
|
28
|
+
BRAND_NAME = os.environ.get("{PACKAGE}_BRAND", "{package}")
|
|
29
|
+
BRAND_ALIAS = os.environ.get("{PACKAGE}_ALIAS", "{alias}")
|
|
30
|
+
|
|
31
|
+
# Original values for replacement
|
|
32
|
+
_ORIGINAL_NAME = "{package}"
|
|
33
|
+
_ORIGINAL_ALIAS = "{alias}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _brand_to_env_prefix(brand: str) -> str:
|
|
37
|
+
"""Convert brand name to environment variable prefix.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
"figrecipe" -> "FIGRECIPE"
|
|
41
|
+
"scitex.plt" -> "SCITEX_PLT"
|
|
42
|
+
"crossref-local" -> "CROSSREF_LOCAL"
|
|
43
|
+
"""
|
|
44
|
+
return brand.upper().replace(".", "_").replace("-", "_")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Environment variable prefix based on brand
|
|
48
|
+
ENV_PREFIX = _brand_to_env_prefix(BRAND_NAME)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
|
|
52
|
+
"""Get environment variable with brand-aware prefix.
|
|
53
|
+
|
|
54
|
+
Checks {ENV_PREFIX}_{key} first, then falls back to original prefix.
|
|
55
|
+
"""
|
|
56
|
+
value = os.environ.get(f"{ENV_PREFIX}_{key}")
|
|
57
|
+
if value is not None:
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
# Fall back to original prefix if different
|
|
61
|
+
original_prefix = _brand_to_env_prefix(_ORIGINAL_NAME)
|
|
62
|
+
if ENV_PREFIX != original_prefix:
|
|
63
|
+
value = os.environ.get(f"{original_prefix}_{key}")
|
|
64
|
+
if value is not None:
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
return default
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def rebrand_text(text: Optional[str]) -> Optional[str]:
|
|
71
|
+
"""Apply branding to a text string (docstrings, error messages)."""
|
|
72
|
+
if text is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if BRAND_NAME == _ORIGINAL_NAME and BRAND_ALIAS == _ORIGINAL_ALIAS:
|
|
76
|
+
return text
|
|
77
|
+
|
|
78
|
+
result = text
|
|
79
|
+
|
|
80
|
+
# Replace import statements
|
|
81
|
+
result = re.sub(
|
|
82
|
+
rf"import\s+{_ORIGINAL_NAME}\s+as\s+{_ORIGINAL_ALIAS}",
|
|
83
|
+
f"import {BRAND_NAME} as {BRAND_ALIAS}",
|
|
84
|
+
result,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Replace "from package" statements
|
|
88
|
+
result = re.sub(
|
|
89
|
+
rf"from\s+{_ORIGINAL_NAME}(\s+import|\s*\.)",
|
|
90
|
+
lambda m: f"from {BRAND_NAME}{m.group(1)}",
|
|
91
|
+
result,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_mcp_server_name() -> str:
|
|
98
|
+
"""Get the MCP server name based on branding."""
|
|
99
|
+
return BRAND_NAME.replace(".", "-")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Usage in Parent Package (scitex)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# scitex/plt/__init__.py
|
|
106
|
+
import os
|
|
107
|
+
|
|
108
|
+
# Set branding BEFORE importing the external package
|
|
109
|
+
os.environ["FIGRECIPE_BRAND"] = "scitex.plt"
|
|
110
|
+
os.environ["FIGRECIPE_ALIAS"] = "plt"
|
|
111
|
+
|
|
112
|
+
# Now import - docstrings will show scitex.plt instead of figrecipe
|
|
113
|
+
from figrecipe import *
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Port Scheme
|
|
117
|
+
|
|
118
|
+
SciTeX uses port scheme 3129X (sa-i-te-ku-su → 3-1-2-9 in Japanese):
|
|
119
|
+
|
|
120
|
+
| Port | Service |
|
|
121
|
+
|-------|------------------|
|
|
122
|
+
| 31290 | scitex-cloud |
|
|
123
|
+
| 31291 | crossref-local |
|
|
124
|
+
| 31292 | openalex |
|
|
125
|
+
| 31293 | scitex-audio |
|
|
126
|
+
|
|
127
|
+
## Environment Variable Pattern
|
|
128
|
+
|
|
129
|
+
External packages should use `{ENV_PREFIX}_{SETTING}`:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
SCITEX_PLT_MODE=local
|
|
133
|
+
CROSSREF_LOCAL_API_URL=http://localhost:31291
|
|
134
|
+
SCITEX_AUDIO_RELAY_URL=http://localhost:31293
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Example: crossref-local
|
|
138
|
+
|
|
139
|
+
See GitHub Issue: https://github.com/ywatanabe1989/crossref-local/issues/11
|
|
140
|
+
|
|
141
|
+
The crossref-local package should implement:
|
|
142
|
+
- `CROSSREF_LOCAL_BRAND` / `CROSSREF_LOCAL_ALIAS` env vars
|
|
143
|
+
- Dynamic `ENV_PREFIX` derived from brand name
|
|
144
|
+
- When used via scitex.scholar, shows `scitex.scholar` in docs
|
|
145
|
+
|
|
146
|
+
## References
|
|
147
|
+
|
|
148
|
+
- figrecipe/_branding.py - Reference implementation
|
|
149
|
+
- scitex/audio/_branding.py - Simple internal module (no rebranding needed)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/__init__.py
|
|
4
|
+
|
|
5
|
+
"""Stats MCP handler implementations split into modules."""
|
|
6
|
+
|
|
7
|
+
from ._corrections import correct_pvalues_handler
|
|
8
|
+
from ._descriptive import describe_handler
|
|
9
|
+
from ._effect_size import effect_size_handler
|
|
10
|
+
from ._format import format_results_handler
|
|
11
|
+
from ._normality import normality_test_handler
|
|
12
|
+
from ._posthoc import posthoc_test_handler
|
|
13
|
+
from ._power import power_analysis_handler
|
|
14
|
+
from ._recommend import recommend_tests_handler
|
|
15
|
+
from ._run_test import run_test_handler
|
|
16
|
+
from ._stars import p_to_stars_handler
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"recommend_tests_handler",
|
|
20
|
+
"run_test_handler",
|
|
21
|
+
"format_results_handler",
|
|
22
|
+
"power_analysis_handler",
|
|
23
|
+
"correct_pvalues_handler",
|
|
24
|
+
"describe_handler",
|
|
25
|
+
"effect_size_handler",
|
|
26
|
+
"normality_test_handler",
|
|
27
|
+
"posthoc_test_handler",
|
|
28
|
+
"p_to_stars_handler",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# EOF
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/_corrections.py
|
|
4
|
+
|
|
5
|
+
"""P-value correction handler for multiple comparisons."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
__all__ = ["correct_pvalues_handler"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def correct_pvalues_handler(
|
|
18
|
+
pvalues: list[float],
|
|
19
|
+
method: str = "fdr_bh",
|
|
20
|
+
alpha: float = 0.05,
|
|
21
|
+
) -> dict:
|
|
22
|
+
"""Apply multiple comparison correction to p-values."""
|
|
23
|
+
try:
|
|
24
|
+
loop = asyncio.get_event_loop()
|
|
25
|
+
|
|
26
|
+
def do_correct():
|
|
27
|
+
from statsmodels.stats.multitest import multipletests
|
|
28
|
+
|
|
29
|
+
# Map method names
|
|
30
|
+
method_map = {
|
|
31
|
+
"bonferroni": "bonferroni",
|
|
32
|
+
"fdr_bh": "fdr_bh",
|
|
33
|
+
"fdr_by": "fdr_by",
|
|
34
|
+
"holm": "holm",
|
|
35
|
+
"sidak": "sidak",
|
|
36
|
+
}
|
|
37
|
+
sm_method = method_map.get(method, "fdr_bh")
|
|
38
|
+
|
|
39
|
+
pvals = np.array(pvalues)
|
|
40
|
+
reject, pvals_corrected, _, _ = multipletests(
|
|
41
|
+
pvals, alpha=alpha, method=sm_method
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"original_pvalues": pvalues,
|
|
46
|
+
"corrected_pvalues": pvals_corrected.tolist(),
|
|
47
|
+
"reject_null": reject.tolist(),
|
|
48
|
+
"n_significant": int(reject.sum()),
|
|
49
|
+
"n_tests": len(pvalues),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result = await loop.run_in_executor(None, do_correct)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"success": True,
|
|
56
|
+
"method": method,
|
|
57
|
+
"alpha": alpha,
|
|
58
|
+
**result,
|
|
59
|
+
"timestamp": datetime.now().isoformat(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
except ImportError:
|
|
63
|
+
# Fallback implementation without statsmodels
|
|
64
|
+
try:
|
|
65
|
+
n = len(pvalues)
|
|
66
|
+
pvals = np.array(pvalues)
|
|
67
|
+
|
|
68
|
+
if method == "bonferroni":
|
|
69
|
+
corrected = np.minimum(pvals * n, 1.0)
|
|
70
|
+
elif method == "holm":
|
|
71
|
+
sorted_idx = np.argsort(pvals)
|
|
72
|
+
corrected = np.empty(n)
|
|
73
|
+
cummax = 0.0
|
|
74
|
+
for rank, idx in enumerate(sorted_idx, start=1):
|
|
75
|
+
adj = min((n - rank + 1) * pvals[idx], 1.0)
|
|
76
|
+
adj = max(adj, cummax)
|
|
77
|
+
corrected[idx] = adj
|
|
78
|
+
cummax = adj
|
|
79
|
+
elif method == "fdr_bh":
|
|
80
|
+
sorted_idx = np.argsort(pvals)
|
|
81
|
+
corrected = np.empty(n)
|
|
82
|
+
prev = 1.0
|
|
83
|
+
for rank in range(n, 0, -1):
|
|
84
|
+
idx = sorted_idx[rank - 1]
|
|
85
|
+
bh = pvals[idx] * n / rank
|
|
86
|
+
val = min(bh, prev, 1.0)
|
|
87
|
+
corrected[idx] = val
|
|
88
|
+
prev = val
|
|
89
|
+
elif method == "sidak":
|
|
90
|
+
corrected = 1 - (1 - pvals) ** n
|
|
91
|
+
else:
|
|
92
|
+
corrected = pvals
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"method": method,
|
|
97
|
+
"alpha": alpha,
|
|
98
|
+
"original_pvalues": pvalues,
|
|
99
|
+
"corrected_pvalues": corrected.tolist(),
|
|
100
|
+
"reject_null": (corrected < alpha).tolist(),
|
|
101
|
+
"n_significant": int((corrected < alpha).sum()),
|
|
102
|
+
"n_tests": n,
|
|
103
|
+
"timestamp": datetime.now().isoformat(),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return {"success": False, "error": str(e)}
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return {"success": False, "error": str(e)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# EOF
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/_descriptive.py
|
|
4
|
+
|
|
5
|
+
"""Descriptive statistics 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__ = ["describe_handler"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def describe_handler(
|
|
18
|
+
data: list[float],
|
|
19
|
+
percentiles: list[float] | None = None,
|
|
20
|
+
) -> dict:
|
|
21
|
+
"""Calculate descriptive statistics for data."""
|
|
22
|
+
try:
|
|
23
|
+
loop = asyncio.get_event_loop()
|
|
24
|
+
|
|
25
|
+
def do_describe():
|
|
26
|
+
arr = np.array(data, dtype=float)
|
|
27
|
+
arr = arr[~np.isnan(arr)] # Remove NaN
|
|
28
|
+
|
|
29
|
+
if len(arr) == 0:
|
|
30
|
+
return {"error": "No valid data points"}
|
|
31
|
+
|
|
32
|
+
percs = percentiles or [25, 50, 75]
|
|
33
|
+
percentile_values = np.percentile(arr, percs)
|
|
34
|
+
|
|
35
|
+
result = {
|
|
36
|
+
"n": int(len(arr)),
|
|
37
|
+
"mean": float(np.mean(arr)),
|
|
38
|
+
"std": float(np.std(arr, ddof=1)) if len(arr) > 1 else 0.0,
|
|
39
|
+
"var": float(np.var(arr, ddof=1)) if len(arr) > 1 else 0.0,
|
|
40
|
+
"sem": (
|
|
41
|
+
float(np.std(arr, ddof=1) / np.sqrt(len(arr)))
|
|
42
|
+
if len(arr) > 1
|
|
43
|
+
else 0.0
|
|
44
|
+
),
|
|
45
|
+
"min": float(np.min(arr)),
|
|
46
|
+
"max": float(np.max(arr)),
|
|
47
|
+
"range": float(np.max(arr) - np.min(arr)),
|
|
48
|
+
"median": float(np.median(arr)),
|
|
49
|
+
"percentiles": {
|
|
50
|
+
str(int(p)): float(v) for p, v in zip(percs, percentile_values)
|
|
51
|
+
},
|
|
52
|
+
"iqr": float(np.percentile(arr, 75) - np.percentile(arr, 25)),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Add skewness and kurtosis if scipy available
|
|
56
|
+
try:
|
|
57
|
+
from scipy import stats as scipy_stats
|
|
58
|
+
|
|
59
|
+
result["skewness"] = float(scipy_stats.skew(arr))
|
|
60
|
+
result["kurtosis"] = float(scipy_stats.kurtosis(arr))
|
|
61
|
+
except ImportError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
result = await loop.run_in_executor(None, do_describe)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"success": True,
|
|
70
|
+
**result,
|
|
71
|
+
"timestamp": datetime.now().isoformat(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return {"success": False, "error": str(e)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# EOF
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/_effect_size.py
|
|
4
|
+
|
|
5
|
+
"""Effect size calculation 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__ = ["effect_size_handler"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def effect_size_handler(
|
|
18
|
+
group1: list[float],
|
|
19
|
+
group2: list[float],
|
|
20
|
+
measure: str = "cohens_d",
|
|
21
|
+
pooled: bool = True,
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""Calculate effect size between groups."""
|
|
24
|
+
try:
|
|
25
|
+
from scitex.stats.effect_sizes import (
|
|
26
|
+
cliffs_delta,
|
|
27
|
+
cohens_d,
|
|
28
|
+
interpret_cliffs_delta,
|
|
29
|
+
interpret_cohens_d,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
loop = asyncio.get_event_loop()
|
|
33
|
+
|
|
34
|
+
def do_effect_size():
|
|
35
|
+
g1 = np.array(group1, dtype=float)
|
|
36
|
+
g2 = np.array(group2, dtype=float)
|
|
37
|
+
|
|
38
|
+
result = {}
|
|
39
|
+
|
|
40
|
+
if measure == "cohens_d":
|
|
41
|
+
d = cohens_d(g1, g2)
|
|
42
|
+
result = {
|
|
43
|
+
"measure": "Cohen's d",
|
|
44
|
+
"value": float(d),
|
|
45
|
+
"interpretation": interpret_cohens_d(d),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
elif measure == "hedges_g":
|
|
49
|
+
# Hedges' g is Cohen's d with bias correction
|
|
50
|
+
d = cohens_d(g1, g2)
|
|
51
|
+
n1, n2 = len(g1), len(g2)
|
|
52
|
+
correction = 1 - (3 / (4 * (n1 + n2) - 9))
|
|
53
|
+
g = d * correction
|
|
54
|
+
result = {
|
|
55
|
+
"measure": "Hedges' g",
|
|
56
|
+
"value": float(g),
|
|
57
|
+
"interpretation": interpret_cohens_d(g), # Same thresholds
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
elif measure == "glass_delta":
|
|
61
|
+
# Glass's delta uses only control group std
|
|
62
|
+
mean_diff = np.mean(g1) - np.mean(g2)
|
|
63
|
+
delta = mean_diff / np.std(g2, ddof=1)
|
|
64
|
+
result = {
|
|
65
|
+
"measure": "Glass's delta",
|
|
66
|
+
"value": float(delta),
|
|
67
|
+
"interpretation": interpret_cohens_d(delta),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
elif measure == "cliffs_delta":
|
|
71
|
+
delta = cliffs_delta(g1, g2)
|
|
72
|
+
result = {
|
|
73
|
+
"measure": "Cliff's delta",
|
|
74
|
+
"value": float(delta),
|
|
75
|
+
"interpretation": interpret_cliffs_delta(delta),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(f"Unknown measure: {measure}")
|
|
80
|
+
|
|
81
|
+
# Add confidence interval approximation for Cohen's d
|
|
82
|
+
if measure in ["cohens_d", "hedges_g", "glass_delta"]:
|
|
83
|
+
n1, n2 = len(g1), len(g2)
|
|
84
|
+
se = np.sqrt(
|
|
85
|
+
(n1 + n2) / (n1 * n2) + result["value"] ** 2 / (2 * (n1 + n2))
|
|
86
|
+
)
|
|
87
|
+
result["ci_lower"] = float(result["value"] - 1.96 * se)
|
|
88
|
+
result["ci_upper"] = float(result["value"] + 1.96 * se)
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
result = await loop.run_in_executor(None, do_effect_size)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"group1_n": len(group1),
|
|
97
|
+
"group2_n": len(group2),
|
|
98
|
+
**result,
|
|
99
|
+
"timestamp": datetime.now().isoformat(),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return {"success": False, "error": str(e)}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# EOF
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/_format.py
|
|
4
|
+
|
|
5
|
+
"""Results formatting handler."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
__all__ = ["format_results_handler"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def format_results_handler(
|
|
16
|
+
test_name: str,
|
|
17
|
+
statistic: float,
|
|
18
|
+
p_value: float,
|
|
19
|
+
df: float | None = None,
|
|
20
|
+
effect_size: float | None = None,
|
|
21
|
+
effect_size_name: str | None = None,
|
|
22
|
+
style: str = "apa",
|
|
23
|
+
ci_lower: float | None = None,
|
|
24
|
+
ci_upper: float | None = None,
|
|
25
|
+
) -> dict:
|
|
26
|
+
"""Format statistical results in journal style."""
|
|
27
|
+
try:
|
|
28
|
+
loop = asyncio.get_event_loop()
|
|
29
|
+
|
|
30
|
+
def do_format():
|
|
31
|
+
from scitex.stats.auto import format_test_line, p_to_stars
|
|
32
|
+
from scitex.stats.auto._formatting import EffectResultDict, TestResultDict
|
|
33
|
+
|
|
34
|
+
# Build test result dict
|
|
35
|
+
test_result: TestResultDict = {
|
|
36
|
+
"test_name": test_name,
|
|
37
|
+
"stat": statistic,
|
|
38
|
+
"p_raw": p_value,
|
|
39
|
+
}
|
|
40
|
+
if df is not None:
|
|
41
|
+
test_result["df"] = df
|
|
42
|
+
|
|
43
|
+
# Build effect result if provided
|
|
44
|
+
effects = None
|
|
45
|
+
if effect_size is not None:
|
|
46
|
+
effects = [
|
|
47
|
+
EffectResultDict(
|
|
48
|
+
name=effect_size_name or "d",
|
|
49
|
+
label=effect_size_name or "Cohen's d",
|
|
50
|
+
value=effect_size,
|
|
51
|
+
ci_lower=ci_lower,
|
|
52
|
+
ci_upper=ci_upper,
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Map style names
|
|
57
|
+
style_map = {
|
|
58
|
+
"apa": "apa_latex",
|
|
59
|
+
"nature": "nature",
|
|
60
|
+
"science": "science",
|
|
61
|
+
"brief": "brief",
|
|
62
|
+
}
|
|
63
|
+
style_id = style_map.get(style, "apa_latex")
|
|
64
|
+
|
|
65
|
+
# Format the line
|
|
66
|
+
formatted = format_test_line(
|
|
67
|
+
test_result,
|
|
68
|
+
effects=effects,
|
|
69
|
+
style=style_id,
|
|
70
|
+
include_n=False,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Get stars representation
|
|
74
|
+
stars = p_to_stars(p_value)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"formatted": formatted,
|
|
78
|
+
"stars": stars,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = await loop.run_in_executor(None, do_format)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"success": True,
|
|
85
|
+
"style": style,
|
|
86
|
+
**result,
|
|
87
|
+
"timestamp": datetime.now().isoformat(),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return {"success": False, "error": str(e)}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# EOF
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-25
|
|
3
|
+
# File: src/scitex/stats/_mcp/_handlers/_normality.py
|
|
4
|
+
|
|
5
|
+
"""Normality 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__ = ["normality_test_handler"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def normality_test_handler(
|
|
18
|
+
data: list[float],
|
|
19
|
+
method: str = "shapiro",
|
|
20
|
+
) -> dict:
|
|
21
|
+
"""Test whether data follows a normal distribution."""
|
|
22
|
+
try:
|
|
23
|
+
from scipy import stats as scipy_stats
|
|
24
|
+
|
|
25
|
+
loop = asyncio.get_event_loop()
|
|
26
|
+
|
|
27
|
+
def do_normality():
|
|
28
|
+
arr = np.array(data, dtype=float)
|
|
29
|
+
arr = arr[~np.isnan(arr)]
|
|
30
|
+
|
|
31
|
+
if len(arr) < 3:
|
|
32
|
+
return {"error": "Need at least 3 data points"}
|
|
33
|
+
|
|
34
|
+
result = {}
|
|
35
|
+
|
|
36
|
+
if method == "shapiro":
|
|
37
|
+
stat, p_value = scipy_stats.shapiro(arr)
|
|
38
|
+
result = {
|
|
39
|
+
"test": "Shapiro-Wilk",
|
|
40
|
+
"statistic": float(stat),
|
|
41
|
+
"statistic_name": "W",
|
|
42
|
+
"p_value": float(p_value),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
elif method == "dagostino":
|
|
46
|
+
if len(arr) < 8:
|
|
47
|
+
return {"error": "D'Agostino test requires at least 8 samples"}
|
|
48
|
+
stat, p_value = scipy_stats.normaltest(arr)
|
|
49
|
+
result = {
|
|
50
|
+
"test": "D'Agostino-Pearson",
|
|
51
|
+
"statistic": float(stat),
|
|
52
|
+
"statistic_name": "K2",
|
|
53
|
+
"p_value": float(p_value),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
elif method == "anderson":
|
|
57
|
+
res = scipy_stats.anderson(arr, dist="norm")
|
|
58
|
+
# Use 5% significance level
|
|
59
|
+
idx = 2 # Index for 5% level
|
|
60
|
+
result = {
|
|
61
|
+
"test": "Anderson-Darling",
|
|
62
|
+
"statistic": float(res.statistic),
|
|
63
|
+
"statistic_name": "A2",
|
|
64
|
+
"critical_value_5pct": float(res.critical_values[idx]),
|
|
65
|
+
"normal": bool(res.statistic < res.critical_values[idx]),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
elif method == "lilliefors":
|
|
69
|
+
try:
|
|
70
|
+
from statsmodels.stats.diagnostic import lilliefors
|
|
71
|
+
|
|
72
|
+
stat, p_value = lilliefors(arr, dist="norm")
|
|
73
|
+
result = {
|
|
74
|
+
"test": "Lilliefors",
|
|
75
|
+
"statistic": float(stat),
|
|
76
|
+
"statistic_name": "D",
|
|
77
|
+
"p_value": float(p_value),
|
|
78
|
+
}
|
|
79
|
+
except ImportError:
|
|
80
|
+
return {"error": "statsmodels required for Lilliefors test"}
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(f"Unknown method: {method}")
|
|
84
|
+
|
|
85
|
+
# Add interpretation
|
|
86
|
+
if "p_value" in result:
|
|
87
|
+
result["is_normal"] = result["p_value"] >= 0.05
|
|
88
|
+
result["interpretation"] = (
|
|
89
|
+
"Data appears normally distributed (p >= 0.05)"
|
|
90
|
+
if result["is_normal"]
|
|
91
|
+
else "Data deviates from normal distribution (p < 0.05)"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
result = await loop.run_in_executor(None, do_normality)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"success": True,
|
|
100
|
+
"method": method,
|
|
101
|
+
"n": len(data),
|
|
102
|
+
**result,
|
|
103
|
+
"timestamp": datetime.now().isoformat(),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return {"success": False, "error": str(e)}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# EOF
|