compehndly 0.0.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compehndly-0.0.1a1/.gitignore +140 -0
- compehndly-0.0.1a1/PKG-INFO +14 -0
- compehndly-0.0.1a1/README.md +0 -0
- compehndly-0.0.1a1/pyproject.toml +168 -0
- compehndly-0.0.1a1/src/compehndly/__init__.py +70 -0
- compehndly-0.0.1a1/src/compehndly/adapters/__init__.py +25 -0
- compehndly-0.0.1a1/src/compehndly/adapters/base.py +26 -0
- compehndly-0.0.1a1/src/compehndly/adapters/numpy_adapter.py +20 -0
- compehndly-0.0.1a1/src/compehndly/adapters/pandas_adapter.py +20 -0
- compehndly-0.0.1a1/src/compehndly/adapters/polars_adapter.py +22 -0
- compehndly-0.0.1a1/src/compehndly/core/__init__.py +3 -0
- compehndly-0.0.1a1/src/compehndly/core/conversion.py +14 -0
- compehndly-0.0.1a1/src/compehndly/core/models.py +9 -0
- compehndly-0.0.1a1/src/compehndly/core/registry.py +68 -0
- compehndly-0.0.1a1/src/compehndly/core/registrymanager.py +1 -0
- compehndly-0.0.1a1/src/compehndly/derived_variables/__init__.py +0 -0
- compehndly-0.0.1a1/src/compehndly/derived_variables/correction.py +96 -0
- compehndly-0.0.1a1/src/compehndly/derived_variables/imputation.py +195 -0
- compehndly-0.0.1a1/src/compehndly/derived_variables/statsutils.py +50 -0
- compehndly-0.0.1a1/src/compehndly/derived_variables/summation.py +63 -0
- compehndly-0.0.1a1/src/compehndly/py.typed +0 -0
- compehndly-0.0.1a1/src/compehndly/secondary_variables/__init__.py +0 -0
- compehndly-0.0.1a1/src/compehndly/utils/__init__.py +0 -0
- compehndly-0.0.1a1/src/compehndly/utils/bins.py +26 -0
- compehndly-0.0.1a1/src/compehndly/utils/conditionals.py +16 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
*venv/
|
|
29
|
+
MANIFEST
|
|
30
|
+
uv.lock
|
|
31
|
+
|
|
32
|
+
# PyInstaller
|
|
33
|
+
# Usually these files are written by a python script from a template
|
|
34
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
35
|
+
*.manifest
|
|
36
|
+
*.spec
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
pip-log.txt
|
|
40
|
+
pip-delete-this-directory.txt
|
|
41
|
+
|
|
42
|
+
# Unit test / coverage reports
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
.nox/
|
|
46
|
+
.coverage
|
|
47
|
+
.coverage.*
|
|
48
|
+
.cache
|
|
49
|
+
nosetests.xml
|
|
50
|
+
coverage.xml
|
|
51
|
+
*.cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
_notebooks/
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
.python-version
|
|
88
|
+
|
|
89
|
+
# pipenv
|
|
90
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
91
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
92
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
93
|
+
# install all needed dependencies.
|
|
94
|
+
#Pipfile.lock
|
|
95
|
+
|
|
96
|
+
# celery beat schedule file
|
|
97
|
+
celerybeat-schedule
|
|
98
|
+
|
|
99
|
+
# SageMath parsed files
|
|
100
|
+
*.sage.py
|
|
101
|
+
|
|
102
|
+
# Environments
|
|
103
|
+
.env
|
|
104
|
+
.venv
|
|
105
|
+
env/
|
|
106
|
+
venv/
|
|
107
|
+
ENV/
|
|
108
|
+
env.bak/
|
|
109
|
+
venv.bak/
|
|
110
|
+
|
|
111
|
+
# Spyder project settings
|
|
112
|
+
.spyderproject
|
|
113
|
+
.spyproject
|
|
114
|
+
|
|
115
|
+
# Rope project settings
|
|
116
|
+
.ropeproject
|
|
117
|
+
|
|
118
|
+
# mkdocs documentation
|
|
119
|
+
/site
|
|
120
|
+
|
|
121
|
+
# mypy
|
|
122
|
+
.mypy_cache/
|
|
123
|
+
.dmypy.json
|
|
124
|
+
dmypy.json
|
|
125
|
+
|
|
126
|
+
# Pyre type checker
|
|
127
|
+
.pyre/
|
|
128
|
+
|
|
129
|
+
# Output folders
|
|
130
|
+
*_output*/
|
|
131
|
+
|
|
132
|
+
# Data
|
|
133
|
+
_data/
|
|
134
|
+
data/
|
|
135
|
+
|
|
136
|
+
# temp
|
|
137
|
+
_temp/
|
|
138
|
+
|
|
139
|
+
# VScode stuff
|
|
140
|
+
.vscode
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: compehndly
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Personal Exposure Health Registry
|
|
5
|
+
Author-email: Gertjan Bisschop <gertjan.bisschop@vito.be>, Dirk Devriendt <dirk.devriendt@vito.be>, Caio Mescouto <caio.mescouto@vito.be>
|
|
6
|
+
Requires-Python: <4.0.0,>=3.10
|
|
7
|
+
Requires-Dist: pyarrow<23.0,>=22.0
|
|
8
|
+
Requires-Dist: scipy
|
|
9
|
+
Provides-Extra: numpy
|
|
10
|
+
Requires-Dist: numpy>=2.0; extra == 'numpy'
|
|
11
|
+
Provides-Extra: pandas
|
|
12
|
+
Requires-Dist: pandas<3.0,>=2.0; extra == 'pandas'
|
|
13
|
+
Provides-Extra: polars
|
|
14
|
+
Requires-Dist: polars<2.0,>=1.0; extra == 'polars'
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "compehndly"
|
|
3
|
+
version = "0.0.1a1"
|
|
4
|
+
description = "Personal Exposure Health Registry"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Gertjan Bisschop", email = "gertjan.bisschop@vito.be" },
|
|
8
|
+
{ name = "Dirk Devriendt", email = "dirk.devriendt@vito.be" },
|
|
9
|
+
{ name = "Caio Mescouto", email = "caio.mescouto@vito.be" },
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.10, <4.0.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pyarrow >= 22.0, <23.0",
|
|
14
|
+
"scipy",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
test = [
|
|
19
|
+
"pytest>=8.2.0,<9",
|
|
20
|
+
"polars>=1.0, < 2.0",
|
|
21
|
+
"pandas>=2.0, < 3.0"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dev = [
|
|
25
|
+
"ruff>=0.4.4,<0.5",
|
|
26
|
+
"black",
|
|
27
|
+
"mypy",
|
|
28
|
+
"isort",
|
|
29
|
+
"pytest-cov>=7.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.uv]
|
|
33
|
+
default-groups = [
|
|
34
|
+
"test",
|
|
35
|
+
"dev",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
polars = [
|
|
40
|
+
"polars>=1.0,<2.0"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
pandas = [
|
|
44
|
+
"pandas>=2.0,<3.0"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
numpy = [
|
|
48
|
+
"numpy>=2.0"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.entry-points."compehndly.adapters"]
|
|
52
|
+
polars = "compehndly.adapters.polars_adapter:PolarsAdapter"
|
|
53
|
+
pandas = "compehndly.adapters.pandas_adapter:PandasAdapter"
|
|
54
|
+
numpy = "compehndly.adapters.numpy_adapter:NumpyAdapter"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
include = [
|
|
59
|
+
"src/compehndly/",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
include = [
|
|
64
|
+
"src/compehndly/",
|
|
65
|
+
"src/compehndly/py.typed",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
70
|
+
"src/compehndly" = "compehndly"
|
|
71
|
+
|
|
72
|
+
[build-system]
|
|
73
|
+
requires = ["hatchling"]
|
|
74
|
+
build-backend = "hatchling.build"
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
pythonpath = [
|
|
78
|
+
"src"
|
|
79
|
+
]
|
|
80
|
+
addopts = [
|
|
81
|
+
"--import-mode=importlib",
|
|
82
|
+
]
|
|
83
|
+
markers = [
|
|
84
|
+
"base: base functionality",
|
|
85
|
+
"polars: polars dependency",
|
|
86
|
+
"numpy: numpy dependency",
|
|
87
|
+
"pandas: pandas dependency",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
# Same as Black.
|
|
92
|
+
line-length = 120
|
|
93
|
+
indent-width = 4
|
|
94
|
+
|
|
95
|
+
# Assume Python 3.10
|
|
96
|
+
target-version = "py310"
|
|
97
|
+
|
|
98
|
+
# Exclude a variety of commonly ignored directories.
|
|
99
|
+
exclude = [
|
|
100
|
+
".bzr",
|
|
101
|
+
".direnv",
|
|
102
|
+
".eggs",
|
|
103
|
+
".git",
|
|
104
|
+
".git-rewrite",
|
|
105
|
+
".hg",
|
|
106
|
+
".ipynb_checkpoints",
|
|
107
|
+
".mypy_cache",
|
|
108
|
+
".nox",
|
|
109
|
+
".pants.d",
|
|
110
|
+
".pyenv",
|
|
111
|
+
".pytest_cache",
|
|
112
|
+
".pytype",
|
|
113
|
+
".ruff_cache",
|
|
114
|
+
".svn",
|
|
115
|
+
".tox",
|
|
116
|
+
".venv",
|
|
117
|
+
".vscode",
|
|
118
|
+
"__pypackages__",
|
|
119
|
+
"_build",
|
|
120
|
+
"buck-out",
|
|
121
|
+
"build",
|
|
122
|
+
"dist",
|
|
123
|
+
"node_modules",
|
|
124
|
+
"site-packages",
|
|
125
|
+
"venv",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint]
|
|
130
|
+
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
|
131
|
+
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
|
132
|
+
# McCabe complexity (`C901`) by default.
|
|
133
|
+
select = ["E4", "E7", "E9", "F"]
|
|
134
|
+
ignore = []
|
|
135
|
+
|
|
136
|
+
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
137
|
+
fixable = ["ALL"]
|
|
138
|
+
unfixable = []
|
|
139
|
+
|
|
140
|
+
# Allow unused variables when underscore-prefixed.
|
|
141
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
142
|
+
|
|
143
|
+
[tool.ruff.format]
|
|
144
|
+
# Like Black, use double quotes for strings.
|
|
145
|
+
quote-style = "double"
|
|
146
|
+
|
|
147
|
+
# Like Black, indent with spaces, rather than tabs.
|
|
148
|
+
indent-style = "space"
|
|
149
|
+
|
|
150
|
+
# Like Black, respect magic trailing commas.
|
|
151
|
+
skip-magic-trailing-comma = false
|
|
152
|
+
|
|
153
|
+
# Like Black, automatically detect the appropriate line ending.
|
|
154
|
+
line-ending = "auto"
|
|
155
|
+
|
|
156
|
+
# Enable auto-formatting of code examples in docstrings. Markdown,
|
|
157
|
+
# reStructuredText code/literal blocks and doctests are all supported.
|
|
158
|
+
#
|
|
159
|
+
# This is currently disabled by default, but it is planned for this
|
|
160
|
+
# to be opt-out in the future.
|
|
161
|
+
docstring-code-format = false
|
|
162
|
+
|
|
163
|
+
# Set the line length limit used when formatting code snippets in
|
|
164
|
+
# docstrings.
|
|
165
|
+
#
|
|
166
|
+
# This only has an effect when the `docstring-code-format` setting is
|
|
167
|
+
# enabled.
|
|
168
|
+
docstring-code-line-length = "dynamic"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from compehndly.core.registry import FunctionRegistry
|
|
4
|
+
from compehndly.adapters import register_all_adapters
|
|
5
|
+
|
|
6
|
+
register_all_adapters()
|
|
7
|
+
|
|
8
|
+
# Internal override hook
|
|
9
|
+
_REGISTRY_BUILDER = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _set_registry_builder(builder):
|
|
13
|
+
"""
|
|
14
|
+
Test helper: override how the registry is built.
|
|
15
|
+
`builder` must be a callable returning a FunctionRegistry instance.
|
|
16
|
+
"""
|
|
17
|
+
global _REGISTRY_BUILDER
|
|
18
|
+
_REGISTRY_BUILDER = builder
|
|
19
|
+
_REGISTRY.cache_clear() # ensures next call uses new builder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@functools.lru_cache
|
|
23
|
+
def _REGISTRY():
|
|
24
|
+
"""
|
|
25
|
+
Lazily build the registry, using the override when present.
|
|
26
|
+
"""
|
|
27
|
+
if _REGISTRY_BUILDER is not None:
|
|
28
|
+
return _REGISTRY_BUILDER()
|
|
29
|
+
|
|
30
|
+
return FunctionRegistry.build_registry()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _FunctionAccessor:
|
|
34
|
+
"""
|
|
35
|
+
Returned from compehndly.<func_name>
|
|
36
|
+
and provides:
|
|
37
|
+
compehndly.add_one(...)
|
|
38
|
+
compehndly.add_one["0.0.1"](...)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str):
|
|
42
|
+
self._name = name
|
|
43
|
+
|
|
44
|
+
def __call__(self, *args, **kwargs):
|
|
45
|
+
"""Invoke latest version."""
|
|
46
|
+
registry = _REGISTRY()
|
|
47
|
+
fn = registry.get(self._name, version=None)
|
|
48
|
+
return fn(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, version: str | None):
|
|
51
|
+
"""Invoke specific version (or None for latest)."""
|
|
52
|
+
registry = _REGISTRY()
|
|
53
|
+
fn = registry.get(self._name, version)
|
|
54
|
+
return fn
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def __getattr__(name: str):
|
|
58
|
+
"""
|
|
59
|
+
Dynamically expose registered functions as attributes:
|
|
60
|
+
compehndly.add_one
|
|
61
|
+
"""
|
|
62
|
+
registry = _REGISTRY()
|
|
63
|
+
|
|
64
|
+
if name in registry._functions:
|
|
65
|
+
return _FunctionAccessor(name)
|
|
66
|
+
|
|
67
|
+
raise AttributeError(f"'compehndly' has no function '{name}'")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["FunctionRegistry"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
from compehndly.adapters.base import ArrayAdapter
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
_ADAPTERS = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_adapter(adapter: ArrayAdapter):
|
|
12
|
+
_ADAPTERS[adapter.name] = adapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_all_adapters():
|
|
16
|
+
logger.debug("Registering adapters")
|
|
17
|
+
for ep in entry_points(group="compehndly.adapters"):
|
|
18
|
+
try:
|
|
19
|
+
logger.debug(f"loading {ep}")
|
|
20
|
+
AdapterClass = ep.load()
|
|
21
|
+
register_adapter(AdapterClass())
|
|
22
|
+
except ImportError as e:
|
|
23
|
+
logger.warning(f"Failed to load {ep.name}: {e}")
|
|
24
|
+
|
|
25
|
+
register_adapter(ArrayAdapter())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import numbers
|
|
3
|
+
import pyarrow as pa
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArrayAdapter:
|
|
9
|
+
name: str = "base"
|
|
10
|
+
|
|
11
|
+
def _to_arrow(self, obj):
|
|
12
|
+
return pa.array(obj)
|
|
13
|
+
|
|
14
|
+
def to_arrow(self, obj) -> pa.Array:
|
|
15
|
+
if isinstance(obj, (pa.Array, pa.Scalar)):
|
|
16
|
+
return obj
|
|
17
|
+
|
|
18
|
+
# Python scalar → pass through unchanged
|
|
19
|
+
elif isinstance(obj, (numbers.Number, bool, str)):
|
|
20
|
+
return obj
|
|
21
|
+
|
|
22
|
+
# Python list/iterable → fallback to Arrow
|
|
23
|
+
return self._to_arrow(obj)
|
|
24
|
+
|
|
25
|
+
def from_arrow(self, obj):
|
|
26
|
+
return obj
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyarrow as pa
|
|
4
|
+
|
|
5
|
+
from compehndly.adapters.base import ArrayAdapter
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NumpyAdapter(ArrayAdapter):
|
|
11
|
+
name = "numpy"
|
|
12
|
+
|
|
13
|
+
def matches(self, obj):
|
|
14
|
+
return isinstance(obj, np.ndarray) and obj.ndim == 1
|
|
15
|
+
|
|
16
|
+
def _to_arrow(self, obj):
|
|
17
|
+
return pa.array(obj) # zero copy for many numeric dtypes
|
|
18
|
+
|
|
19
|
+
def from_arrow(self, arrow_obj):
|
|
20
|
+
return arrow_obj.to_numpy() # zero-copy where possible
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pyarrow as pa
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from compehndly.adapters.base import ArrayAdapter
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PandasAdapter(ArrayAdapter):
|
|
11
|
+
name = "pandas"
|
|
12
|
+
|
|
13
|
+
def matches(self, obj):
|
|
14
|
+
return isinstance(obj, pd.Series)
|
|
15
|
+
|
|
16
|
+
def _to_arrow(self, obj):
|
|
17
|
+
return pa.array(obj) # may be zero-copy if ArrowExtensionArray
|
|
18
|
+
|
|
19
|
+
def from_arrow(self, arrow_obj):
|
|
20
|
+
return pd.Series(arrow_obj)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import polars as pl
|
|
3
|
+
import pyarrow as pa
|
|
4
|
+
|
|
5
|
+
from compehndly.adapters.base import ArrayAdapter
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PolarsAdapter(ArrayAdapter):
|
|
11
|
+
name = "polars"
|
|
12
|
+
|
|
13
|
+
def matches(self, obj):
|
|
14
|
+
return isinstance(obj, pl.Series)
|
|
15
|
+
|
|
16
|
+
def _to_arrow(self, obj):
|
|
17
|
+
if isinstance(obj, pl.Series):
|
|
18
|
+
return obj.to_arrow()
|
|
19
|
+
return pa.array(obj)
|
|
20
|
+
|
|
21
|
+
def from_arrow(self, arrow_obj):
|
|
22
|
+
return pl.from_arrow(arrow_obj)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def arrowize_arguments(func, adapter):
|
|
2
|
+
def wrapper(*args, **kwargs):
|
|
3
|
+
# Convert args → arrow
|
|
4
|
+
arr_args = [adapter.to_arrow(a) for a in args]
|
|
5
|
+
|
|
6
|
+
# Convert kwargs → arrow (only values!)
|
|
7
|
+
arr_kwargs = {k: adapter.to_arrow(v) for k, v in kwargs.items()}
|
|
8
|
+
|
|
9
|
+
result = func(*arr_args, **arr_kwargs)
|
|
10
|
+
|
|
11
|
+
# Convert result → library-specific type
|
|
12
|
+
return adapter.from_arrow(result)
|
|
13
|
+
|
|
14
|
+
return wrapper
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
|
|
7
|
+
from compehndly.core.conversion import arrowize_arguments
|
|
8
|
+
from compehndly.adapters import _ADAPTERS
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
TO_REGISTER = ["compehndly.utils.example_function"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FunctionRegistry:
|
|
16
|
+
def __init__(self, adapter: str | None = None):
|
|
17
|
+
self._functions = defaultdict(dict)
|
|
18
|
+
logging.debug("Running function registry")
|
|
19
|
+
if adapter is None:
|
|
20
|
+
self.adapter = _ADAPTERS["base"]
|
|
21
|
+
else:
|
|
22
|
+
if adapter not in _ADAPTERS:
|
|
23
|
+
raise ValueError(f"Unknown adapter '{adapter}'. " f"Available: {', '.join(_ADAPTERS)}")
|
|
24
|
+
self.adapter = _ADAPTERS[adapter]
|
|
25
|
+
|
|
26
|
+
def register(self, name, version, func):
|
|
27
|
+
version = Version(version)
|
|
28
|
+
if version in self._functions[name]:
|
|
29
|
+
raise ValueError(f"Function {name} version {version} already registered.")
|
|
30
|
+
|
|
31
|
+
wrapped_func = arrowize_arguments(func, adapter=self.adapter)
|
|
32
|
+
self._functions[name][version] = wrapped_func
|
|
33
|
+
|
|
34
|
+
def get(self, name, version=None):
|
|
35
|
+
"""Return the function. If version is None, return latest."""
|
|
36
|
+
if name not in self._functions:
|
|
37
|
+
raise KeyError(f"No function registered with name '{name}'")
|
|
38
|
+
versions = sorted(self._functions[name].keys())
|
|
39
|
+
if version is None:
|
|
40
|
+
version = versions[-1]
|
|
41
|
+
else:
|
|
42
|
+
version = Version(version)
|
|
43
|
+
return self._functions[name][version]
|
|
44
|
+
|
|
45
|
+
def list_versions(self, name):
|
|
46
|
+
if name not in self._functions:
|
|
47
|
+
return []
|
|
48
|
+
return sorted(str(v) for v in self._functions[name].keys())
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def build_registry(cls, _to_register=TO_REGISTER, adapter: str | None = None):
|
|
52
|
+
registry = cls(adapter=adapter)
|
|
53
|
+
|
|
54
|
+
for module_path in _to_register:
|
|
55
|
+
try:
|
|
56
|
+
module = importlib.import_module(module_path)
|
|
57
|
+
except ImportError as e:
|
|
58
|
+
raise ImportError(f"Failed to import module '{module_path}': {e}")
|
|
59
|
+
|
|
60
|
+
if not hasattr(module, "__registrations__") or not isinstance(module.__registrations__, list):
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
for registry_name, func_name, version, func in module.__registrations__:
|
|
64
|
+
if registry_name != "default":
|
|
65
|
+
raise ValueError(f"Unsupported registry name '{registry_name}' in module '{module_path}'")
|
|
66
|
+
registry.register(func_name, version, func)
|
|
67
|
+
|
|
68
|
+
return registry
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# TODO: allow support for multiple registries
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import pyarrow as pa
|
|
2
|
+
import pyarrow.compute as pc
|
|
3
|
+
|
|
4
|
+
__registrations__ = []
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# TODO: move decorator for joint use
|
|
8
|
+
def register(registry_name, name, version):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
__registrations__.append((registry_name, name, version, func))
|
|
11
|
+
return func
|
|
12
|
+
|
|
13
|
+
return decorator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------- STANDARDIZE ----------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _standardize_v0_0_1_reference(
|
|
20
|
+
measured: float,
|
|
21
|
+
standard: float,
|
|
22
|
+
) -> float:
|
|
23
|
+
return 100 * measured / standard
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register(registry_name="default", name="standardize", version="0.0.1")
|
|
27
|
+
def _standardize_v0_0_1_arrow(measured: pa.Array, standard: pa.Array) -> pa.Array:
|
|
28
|
+
return pc.divide(pc.multiply(measured, 100), standard)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------- URINARY BIOMARKERS ----------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _standardize_creatinine_v0_0_1_reference(
|
|
35
|
+
measured: float,
|
|
36
|
+
crt: float,
|
|
37
|
+
) -> float:
|
|
38
|
+
"measured in micrograms/L, crt in mg/dL"
|
|
39
|
+
return _standardize_v0_0_1_reference(measured, crt)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register(registry_name="default", name="standardize_creatinine", version="0.0.1")
|
|
43
|
+
def _standardize_creatinine_v0_0_1_arrow(measured: pa.Array, crt: pa.Array) -> pa.Array:
|
|
44
|
+
return _standardize_v0_0_1_arrow(measured, crt)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_specific_gravity_v0_0_1_reference(
|
|
48
|
+
measured: float,
|
|
49
|
+
sg_measured: float,
|
|
50
|
+
sg_ref: float,
|
|
51
|
+
) -> float:
|
|
52
|
+
ret = measured * (sg_ref - 1) / sg_measured
|
|
53
|
+
|
|
54
|
+
return ret
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@register(registry_name="default", name="normalize_specific_gravity", version="0.0.1")
|
|
58
|
+
def _normalize_specific_gravity_v0_0_1_arrow(measured: pa.Array, sg_measured: pa.Array, sg_ref: float) -> pa.Array:
|
|
59
|
+
# Compute (sg_ref - 1) as a scalar
|
|
60
|
+
sg_factor = pa.scalar(sg_ref - 1, type=pa.float64())
|
|
61
|
+
|
|
62
|
+
# measured * (sg_ref - 1)
|
|
63
|
+
numerator = pc.multiply(measured, sg_factor)
|
|
64
|
+
|
|
65
|
+
# (measured * (sg_ref - 1)) / sg_measured
|
|
66
|
+
ret = pc.divide(numerator, sg_measured)
|
|
67
|
+
|
|
68
|
+
return ret
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------- LIPID SOLUBLE BIOMARKERS ----------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _total_lipid_concentration_v0_0_1_reference(chol: float, trigl: float) -> float:
|
|
75
|
+
"""The total blood lipid content was calculated using the formula proposed by
|
|
76
|
+
(Bernert et al., 2007; Phillips et al., 1989) as advised by the analytical experts of HBM4EU.
|
|
77
|
+
Total lipids (mg/dL) = 2.27 * cholesterol (mg/dL) + triglycerides (mg/dL) + 62.3
|
|
78
|
+
"""
|
|
79
|
+
return 2.27 * chol + trigl + 62.3
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@register(registry_name="default", name="total_lipid_concentration", version="0.0.1")
|
|
83
|
+
def _total_lipid_concentration_v0_0_1_arrow(chol: pa.Array, trigl: pa.Array) -> pa.Array:
|
|
84
|
+
return pc.add(pc.multiply(chol, 2.27), pc.add(trigl, 62.3))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _standardize_lipid_v0_0_1_reference(
|
|
88
|
+
measured: float,
|
|
89
|
+
lipid_value: float,
|
|
90
|
+
) -> float:
|
|
91
|
+
return _standardize_v0_0_1_reference(measured, lipid_value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@register(registry_name="default", name="normalize_specific_gravity", version="0.0.1")
|
|
95
|
+
def _standardize_lipid_v0_0_1_arrow(measured: pa.Array, lipid_value: pa.Array) -> pa.Array:
|
|
96
|
+
return _standardize_v0_0_1_arrow(measured, lipid_value)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pyarrow as pa
|
|
3
|
+
import pyarrow.compute as pc
|
|
4
|
+
|
|
5
|
+
from compehndly.derived_variables.statsutils import fit_censored_lognorm
|
|
6
|
+
|
|
7
|
+
__registrations__ = []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# TODO: move decorator for joint use
|
|
11
|
+
def register(registry_name, name, version):
|
|
12
|
+
def decorator(func):
|
|
13
|
+
__registrations__.append((registry_name, name, version, func))
|
|
14
|
+
return func
|
|
15
|
+
|
|
16
|
+
return decorator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _medium_bound_imputation_v0_0_1_reference(
|
|
20
|
+
measurement: float,
|
|
21
|
+
loq: float,
|
|
22
|
+
lod: float | None = None,
|
|
23
|
+
) -> float:
|
|
24
|
+
if lod is not None:
|
|
25
|
+
assert lod > 0.0
|
|
26
|
+
assert loq > 0.0
|
|
27
|
+
ret = measurement
|
|
28
|
+
|
|
29
|
+
if lod is None:
|
|
30
|
+
if measurement < loq:
|
|
31
|
+
ret = loq / 2
|
|
32
|
+
else:
|
|
33
|
+
assert lod < loq
|
|
34
|
+
if measurement < lod:
|
|
35
|
+
ret = lod / 2
|
|
36
|
+
elif measurement < loq:
|
|
37
|
+
ret = (loq + lod) / 2
|
|
38
|
+
|
|
39
|
+
return ret
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register(registry_name="default", name="medium_bound_imputation", version="0.0.1")
|
|
43
|
+
def _medium_bound_imputation_v0_0_1_arrow(
|
|
44
|
+
measurement: pa.Array,
|
|
45
|
+
loq: float,
|
|
46
|
+
lod: float | None = None,
|
|
47
|
+
) -> pa.Array:
|
|
48
|
+
"""
|
|
49
|
+
Vectorized medium-bound imputation.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
if loq <= 0:
|
|
54
|
+
raise ValueError("loq must be > 0")
|
|
55
|
+
|
|
56
|
+
if lod is not None:
|
|
57
|
+
if lod <= 0:
|
|
58
|
+
raise ValueError("lod must be > 0")
|
|
59
|
+
if lod >= loq:
|
|
60
|
+
raise ValueError("lod must be < loq")
|
|
61
|
+
|
|
62
|
+
# Start with identity (original measurement)
|
|
63
|
+
result = measurement
|
|
64
|
+
|
|
65
|
+
if lod is None:
|
|
66
|
+
# measurement < loq → loq / 2
|
|
67
|
+
mask = pc.less(measurement, loq)
|
|
68
|
+
result = pc.if_else(mask, loq / 2, result)
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
# measurement < lod → lod / 2
|
|
72
|
+
mask_below_lod = pc.less(measurement, lod)
|
|
73
|
+
result = pc.if_else(mask_below_lod, lod / 2, result)
|
|
74
|
+
|
|
75
|
+
# lod <= measurement < loq → (lod + loq) / 2
|
|
76
|
+
mask_between = pc.and_(
|
|
77
|
+
pc.greater_equal(measurement, lod),
|
|
78
|
+
pc.less(measurement, loq),
|
|
79
|
+
)
|
|
80
|
+
result = pc.if_else(mask_between, (lod + loq) / 2, result)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@register(registry_name="default", name="medium_bound_imputation_array", version="0.0.1")
|
|
86
|
+
def _medium_bound_imputation_v0_0_1_arrow_array(
|
|
87
|
+
measurement: pa.Array,
|
|
88
|
+
loq: pa.Array,
|
|
89
|
+
lod: pa.Array | None = None,
|
|
90
|
+
) -> pa.Array:
|
|
91
|
+
"""
|
|
92
|
+
Vectorized medium-bound imputation with row-wise LOQ / LOD arrays.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
length = len(measurement)
|
|
96
|
+
|
|
97
|
+
if len(loq) != length:
|
|
98
|
+
raise ValueError("measurement and loq must have the same length")
|
|
99
|
+
|
|
100
|
+
if lod is not None and len(lod) != length:
|
|
101
|
+
raise ValueError("measurement and lod must have the same length")
|
|
102
|
+
|
|
103
|
+
result = measurement
|
|
104
|
+
|
|
105
|
+
if lod is None:
|
|
106
|
+
# measurement < loq → loq / 2
|
|
107
|
+
mask = pc.less(measurement, loq)
|
|
108
|
+
result = pc.if_else(mask, pc.divide(loq, 2.0), result)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
# measurement < lod → lod / 2
|
|
112
|
+
mask_below_lod = pc.less(measurement, lod)
|
|
113
|
+
result = pc.if_else(mask_below_lod, pc.divide(lod, 2.0), result)
|
|
114
|
+
|
|
115
|
+
# lod <= measurement < loq → (lod + loq) / 2
|
|
116
|
+
mask_between = pc.and_(
|
|
117
|
+
pc.greater_equal(measurement, lod),
|
|
118
|
+
pc.less(measurement, loq),
|
|
119
|
+
)
|
|
120
|
+
midpoint = pc.divide(pc.add(lod, loq), 2.0)
|
|
121
|
+
result = pc.if_else(mask_between, midpoint, result)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _random_single_imputation_reference_v0_0_1(
|
|
127
|
+
measurement: float,
|
|
128
|
+
loq: float,
|
|
129
|
+
lod: float | None = None,
|
|
130
|
+
) -> float:
|
|
131
|
+
# TODO:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@register(registry_name="default", name="random_single_imputation", version="0.0.1")
|
|
136
|
+
def _random_single_imputation_arrow_v0_0_1(
|
|
137
|
+
biomarker_pa: pa.Array,
|
|
138
|
+
lod: float,
|
|
139
|
+
loq: float,
|
|
140
|
+
seed: int | None = None,
|
|
141
|
+
) -> pa.Array:
|
|
142
|
+
"""
|
|
143
|
+
Perform random single imputation for left-censored lognormal data
|
|
144
|
+
using PyArrow arrays for maximum compatibility.
|
|
145
|
+
|
|
146
|
+
biomarker_pa : arrow array of floats or censored indicators (-1, -2, -3)
|
|
147
|
+
lod : limit of detection
|
|
148
|
+
loq : limit of quantification
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Convert Arrow → NumPy
|
|
152
|
+
# NOTE: suboptimal solution
|
|
153
|
+
biomarker = biomarker_pa.to_numpy(zero_copy_only=False)
|
|
154
|
+
|
|
155
|
+
# Fill NA as -1 (your original convention)
|
|
156
|
+
is_na = np.isnan(biomarker)
|
|
157
|
+
biomarker_filled = np.where(is_na, -1, biomarker)
|
|
158
|
+
|
|
159
|
+
# Censored if negative category code
|
|
160
|
+
censored = biomarker_filled < 0
|
|
161
|
+
values_np = np.where(censored, lod, biomarker_filled)
|
|
162
|
+
dist = fit_censored_lognorm(values_np, censored)
|
|
163
|
+
rng = np.random.default_rng(seed=seed)
|
|
164
|
+
|
|
165
|
+
# Compute sampling bounds (vectorized)
|
|
166
|
+
lower = np.zeros_like(biomarker_filled, dtype=float)
|
|
167
|
+
upper = np.zeros_like(biomarker_filled, dtype=float)
|
|
168
|
+
|
|
169
|
+
cat_below_lod = biomarker_filled == -1
|
|
170
|
+
cat_between = biomarker_filled == -2
|
|
171
|
+
cat_below_loq = biomarker_filled == -3
|
|
172
|
+
|
|
173
|
+
# <LOD -> [0, LOD]
|
|
174
|
+
lower[cat_below_lod] = 0
|
|
175
|
+
upper[cat_below_lod] = lod
|
|
176
|
+
# Between LOD & LOQ -> [LOD, LOQ]
|
|
177
|
+
lower[cat_between] = lod
|
|
178
|
+
upper[cat_between] = loq
|
|
179
|
+
|
|
180
|
+
# <LOQ -> [0, LOQ]
|
|
181
|
+
lower[cat_below_loq] = 0
|
|
182
|
+
upper[cat_below_loq] = loq
|
|
183
|
+
|
|
184
|
+
# Convert bounds to CDF space
|
|
185
|
+
cdf_lo = dist.cdf(lower)
|
|
186
|
+
cdf_hi = dist.cdf(upper)
|
|
187
|
+
|
|
188
|
+
# generate U ~ Uniform(cdf_lo, cdf_hi)
|
|
189
|
+
u = rng.uniform(cdf_lo, cdf_hi)
|
|
190
|
+
imputed = dist.ppf(u)
|
|
191
|
+
# replace censored with imputed
|
|
192
|
+
result = biomarker.copy()
|
|
193
|
+
result[censored] = imputed[censored]
|
|
194
|
+
|
|
195
|
+
return pa.array(result)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from scipy.stats import lognorm
|
|
4
|
+
from scipy.optimize import minimize
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fit_censored_lognorm(values_np, censored_np):
|
|
8
|
+
vals = values_np
|
|
9
|
+
cens = censored_np
|
|
10
|
+
|
|
11
|
+
# Require at least some uncensored observations
|
|
12
|
+
if (~cens).sum() == 0:
|
|
13
|
+
raise RuntimeError("Cannot fit lognormal: all observations are censored.")
|
|
14
|
+
|
|
15
|
+
def nll(params):
|
|
16
|
+
sigma, mu = params
|
|
17
|
+
if sigma <= 0:
|
|
18
|
+
return np.inf
|
|
19
|
+
|
|
20
|
+
dist = lognorm(s=sigma, scale=np.exp(mu))
|
|
21
|
+
ll_unc = dist.logpdf(vals[~cens]).sum()
|
|
22
|
+
ll_cens = np.log(dist.cdf(vals[cens])).sum()
|
|
23
|
+
ll = ll_unc + ll_cens
|
|
24
|
+
|
|
25
|
+
# Regularization to avoid pathological sigma
|
|
26
|
+
penalty = 0
|
|
27
|
+
if sigma < 0.05:
|
|
28
|
+
penalty += 1e3 * (0.05 - sigma) ** 2
|
|
29
|
+
if sigma > 5.0:
|
|
30
|
+
penalty += 1e3 * (sigma - 5.0) ** 2
|
|
31
|
+
|
|
32
|
+
return -(ll - penalty)
|
|
33
|
+
|
|
34
|
+
unc = vals[~cens]
|
|
35
|
+
mu0 = np.log(np.median(unc))
|
|
36
|
+
sigma0 = np.std(np.log(unc))
|
|
37
|
+
sigma0 = sigma0 if sigma0 > 0.1 else 0.5 # stability
|
|
38
|
+
|
|
39
|
+
res = minimize(
|
|
40
|
+
nll,
|
|
41
|
+
x0=[sigma0, mu0],
|
|
42
|
+
method="L-BFGS-B",
|
|
43
|
+
bounds=[(1e-6, None), (None, None)],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if not res.success:
|
|
47
|
+
raise RuntimeError(f"Censored MLE did not converge: {res.message}")
|
|
48
|
+
|
|
49
|
+
sigma_hat, mu_hat = res.x
|
|
50
|
+
return lognorm(s=sigma_hat, scale=np.exp(mu_hat))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pyarrow as pa
|
|
2
|
+
import pyarrow.compute as pc
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__registrations__ = []
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# TODO: move decorator for joint use
|
|
9
|
+
def register(registry_name, name, version):
|
|
10
|
+
def decorator(func):
|
|
11
|
+
__registrations__.append((registry_name, name, version, func))
|
|
12
|
+
return func
|
|
13
|
+
|
|
14
|
+
return decorator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _summation_v0_0_1_reference(
|
|
18
|
+
*arrays: float,
|
|
19
|
+
all_required=True,
|
|
20
|
+
) -> float | None:
|
|
21
|
+
if not arrays:
|
|
22
|
+
raise ValueError("At least one input array is required")
|
|
23
|
+
|
|
24
|
+
if all_required:
|
|
25
|
+
if not any(x is not None for x in arrays):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
ret = 0
|
|
29
|
+
for val in arrays:
|
|
30
|
+
if val is not None:
|
|
31
|
+
ret += val
|
|
32
|
+
|
|
33
|
+
return ret
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register(registry_name="default", name="summation", version="0.0.1")
|
|
37
|
+
def _summation_v0_0_1_arrow(*arrays: pa.Array, all_required=True) -> pa.Array:
|
|
38
|
+
"""
|
|
39
|
+
Vectorized summation over multiple Arrow arrays.
|
|
40
|
+
|
|
41
|
+
Semantics:
|
|
42
|
+
- If ANY input array is entirely null, return an all-null array.
|
|
43
|
+
- Otherwise, nulls are treated as zero during summation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if not arrays:
|
|
47
|
+
raise ValueError("At least one input array is required")
|
|
48
|
+
|
|
49
|
+
length = len(arrays[0])
|
|
50
|
+
|
|
51
|
+
for arr in arrays:
|
|
52
|
+
if len(arr) != length:
|
|
53
|
+
raise ValueError("All input arrays must have the same length")
|
|
54
|
+
|
|
55
|
+
if arr.null_count == length and all_required:
|
|
56
|
+
return pa.nulls(length, type=pa.float64())
|
|
57
|
+
|
|
58
|
+
result = pc.fill_null(arrays[0], 0)
|
|
59
|
+
|
|
60
|
+
for arr in arrays[1:]:
|
|
61
|
+
result = pc.add(result, pc.fill_null(arr, 0))
|
|
62
|
+
|
|
63
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pyarrow as pa
|
|
2
|
+
import pyarrow.compute as pc
|
|
3
|
+
|
|
4
|
+
__registrations__ = []
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# TODO: move decorator for joint use
|
|
8
|
+
def register(registry_name, name, version):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
__registrations__.append((registry_name, name, version, func))
|
|
11
|
+
return func
|
|
12
|
+
|
|
13
|
+
return decorator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _simple_bin_v0_0_1_reference(
|
|
17
|
+
*arrays: float,
|
|
18
|
+
) -> float:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register(registry_name="default", name="summation", version="0.0.1")
|
|
23
|
+
def _simple_bin_v0_0_1_arrow(
|
|
24
|
+
array: pa.Array,
|
|
25
|
+
) -> pa.Array:
|
|
26
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pyarrow as pa
|
|
2
|
+
import pyarrow.compute as pc
|
|
3
|
+
|
|
4
|
+
__registrations__ = []
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# TODO: move decorator for joint use
|
|
8
|
+
def register(registry_name, name, version):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
__registrations__.append((registry_name, name, version, func))
|
|
11
|
+
return func
|
|
12
|
+
|
|
13
|
+
return decorator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# TODO: add conditional helper functions
|