Repository: siapy/siapy-lib (language: python)

Issue:
Feature



Discussion / hints:

Fix patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 78badf3..2081993 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,23 +1,11 @@
 ## Description
 
-This repository needs <!-- Add 1-2 sentences explaining the purpose of this PR. -->
+<!--
+    - Add 1-2 sentences explaining the purpose of this PR.
+    - (Optionally] Add commit ID and [GitHub will autolink](https://help.github.com/en/github/writing-on-github/autolinked-references-and-urls).
+    - (Optionally] Reference related commits, issues and pull requests. Type `#` and select from the list.
+-->
 
-The commits in this pull request will <!-- Summarize PR. -->
-
-## Changes
-
-- Imperative commit or change title (commit ID) <!-- Add commit ID and [GitHub will autolink](https://help.github.com/en/github/writing-on-github/autolinked-references-and-urls). -->
-  - Add some bullet points to explain the change
-- Next commit or change (commit ID)
-
-## Related
-
-Relates to organization/repo#number
-
-<!-- Reference related commits, issues and pull requests. Type `#` and select from the list. -->
-
-Closes organization/repo#number
-
-<!-- List issues for GitHub to automatically close after merge to default branch. Type `#` and select from the list. See the [GitHub docs on linking PRs to issues](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) for more. -->
+## Checklist
 
 - [ ] I have reviewed the [Guidelines for Contributing](../CONTRIBUTING.md) and the [Code of Conduct](../CODE_OF_CONDUCT.md).
diff --git a/.gitignore b/.gitignore
index 16cb68a..4abf71e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 venv
 __pycache__
 dist
+tests/data
 .venv
 .idea
 .ruff_cache
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7b8fdd1..28ce31d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -121,15 +121,19 @@ Spell check is performed with [CSpell](https://cspell.org/). The CSpell command
 - **To commit:**
   - Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) specification for commit messages. This standardizes the commit history and facilitates automatic generation of the changelog.
   - _Type_ must be one of the following:
-    - `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
-    - `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
-    - `docs`: Documentation only changes
     - `feat`: A new feature
     - `fix`: A bug fix
     - `perf`: A code change that improves performance
-    - `refactor`: A code change that neither fixes a bug nor adds a feature
-    - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
-    - `test`: Adding missing tests or correcting existing tests
+    - `deps`: Dependency updates
+    - `revert`: Reverts a previous commit
+    - `docs`: Documentation only changes
+    - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc), hidden
+    - `chore`: Miscellaneous chores, hidden
+    - `refactor`: A code change that neither fixes a bug nor adds a feature, hidden
+    - `test`: Adding missing tests or correcting existing tests, hidden
+    - `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm), hidden
+    - `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs), hidden
+
   - Ensure your commit messages clearly describe the changes made and follow the format `type(scope?): subject`, where `scope` is optional.
 
 - **To create a release:**
diff --git a/pyproject.toml b/pyproject.toml
index 712567e..64322ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -74,7 +74,7 @@ module = "spectral"
 ignore_missing_imports = true
 
 [tool.flake8]
-ignore = "E302, E305, E203, E501, W503"
+ignore = "E302, E305, E203, E501, W503, E501"
 select = "C,E,F,W,B,B950"
 max-line-length = 100
 exclude = [".git", "__pycache__", "__init__.py", ".mypy_cache", ".pytest_cache"]
diff --git a/siapy/core/types.py b/siapy/core/types.py
index f5b3a46..cfdd829 100644
--- a/siapy/core/types.py
+++ b/siapy/core/types.py
@@ -2,7 +2,7 @@
 import spectral as sp
 from PIL.Image import Image
 
-from siapy.entities import SpectralImage
+from siapy.entities import SpectralImage, SpectralImageSet
 
 SpectralType = sp.io.envi.BilFile | sp.io.envi.BipFile | sp.io.envi.BsqFile
 ImageType = SpectralImage | np.ndarray | Image
@@ -20,3 +20,4 @@
     | np.int64
     | np.uint64
 )
+ImageContainerType = SpectralImage | SpectralImageSet
diff --git a/siapy/datasets/__init__.py b/siapy/datasets/__init__.py
new file mode 100644
index 0000000..31b64f1
--- /dev/null
+++ b/siapy/datasets/__init__.py
@@ -0,0 +1,6 @@
+from .tabular import TabularDataset
+
+
+__all__ = [
+    "TabularDataset",
+]
diff --git a/siapy/datasets/tabular.py b/siapy/datasets/tabular.py
new file mode 100644
index 0000000..8359a7f
--- /dev/null
+++ b/siapy/datasets/tabular.py
@@ -0,0 +1,23 @@
+from dataclasses import dataclass
+
+from siapy.core.types import ImageContainerType
+from siapy.entities import SpectralImage, SpectralImageSet
+
+
+@dataclass()
+class TabularDataset:
+    def __init__(self, container: ImageContainerType):
+        self._image_set = (
+            SpectralImageSet([container])
+            if isinstance(container, SpectralImage)
+            else container
+        )
+
+    @property
+    def image_set(self):
+        return self._image_set
+
+    def generate(self):
+        for image in self.image_set:
+            for shape in image.geometric_shapes.shapes:
+                image.to_signatures(shape.convex_hull())
diff --git a/siapy/models/__init__.py b/siapy/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/siapy/models/models.py b/siapy/models/models.py
new file mode 100644
index 0000000..44d827a
--- /dev/null
+++ b/siapy/models/models.py
@@ -0,0 +1,16 @@
+from typing import Protocol
+
+
+class ClassificationAlgorithm(Protocol):
+    def fit(self, X, y): ...
+
+    def predict(self, X): ...
+
+
+class ClassificationModel:
+    def __init__(self, model: ClassificationAlgorithm):
+        self._model = model
+
+    @property
+    def model(self) -> ClassificationAlgorithm:
+        return self._model
diff --git a/siapy/models/schemas.py b/siapy/models/schemas.py
new file mode 100644
index 0000000..15d2eae
--- /dev/null
+++ b/siapy/models/schemas.py
@@ -0,0 +1,150 @@
+import pickle
+
+import numpy as np
+import pandas as pd
+from pydantic import BaseModel
+
+
+class BaseModelConfig(BaseModel):
+    class Config:
+        arbitrary_types_allowed = True
+
+
+class ClassificationTarget(BaseModelConfig):
+    label: pd.Series
+    value: pd.Series
+    encoding: pd.Series
+
+    def __getitem__(self, indices):
+        label = self.label.iloc[indices]
+        value = self.value.iloc[indices]
+        return ClassificationTarget(label=label, value=value, encoding=self.encoding)
+
+    def __len__(self):
+        return len(self.label)
+
+    @classmethod
+    def from_dict(cls, data):
+        label = pd.Series(data["label"], name="label")
+        value = pd.Series(data["value"], name="value")
+        encoding = pd.Series(data["encoding"], name="encoding")
+        return cls(label=label, value=value, encoding=encoding)
+
+    def to_dict(self):
+        return {
+            "label": self.label.to_list(),
+            "value": self.value.to_list(),
+            "encoding": self.encoding.to_list(),
+        }
+
+    def reset_index(self):
+        return ClassificationTarget(
+            label=self.label.reset_index(drop=True),
+            value=self.value.reset_index(drop=True),
+            encoding=self.encoding,
+        )
+
+
+class RegressionTarget(BaseModelConfig):
+    value: pd.Series
+    name: str
+
+    def __getitem__(self, indices):
+        value = self.value.iloc[indices]
+        return RegressionTarget(value=value, name=self.name)
+
+    def __len__(self):
+        return len(self.value)
+
+    @classmethod
+    def from_dict(cls, data):
+        value = pd.Series(data["value"], name="value")
+        name = data["name"]
+        return cls(value=value, name=name)
+
+    def to_dict(self):
+        return {
+            "value": self.value.to_list(),
+            "name": self.name,
+        }
+
+    def reset_index(self):
+        return RegressionTarget(value=self.value.reset_index(drop=True), name=self.name)
+
+
+class StructuredData(BaseModelConfig):
+    data: pd.DataFrame
+    meta: pd.DataFrame
+    target: ClassificationTarget | RegressionTarget | None = None
+
+    def __getitem__(self, indices):
+        data = self.data.iloc[indices]
+        meta = self.meta.iloc[indices]
+        target = None if self.target is None else self.target[indices]
+        return StructuredData(data=data, meta=meta, target=target)
+
+    @classmethod
+    def from_dict(cls, data):
+        data_ = pd.DataFrame(data["data"])
+        meta = pd.DataFrame(data["meta"])
+        target = StructuredData.target_from_dict(data["target"])
+        return cls(data=data_, meta=meta, target=target)
+
+    @staticmethod
+    def target_from_dict(data):
+        if data is None:
+            return None
+
+        elif "name" in data:
+            return RegressionTarget.from_dict(data)
+
+        elif "encoding" in data:
+            return ClassificationTarget.from_dict(data)
+
+        else:
+            raise ValueError("Invalid target dict.")
+
+    @classmethod
+    def from_bytes(cls, data):
+        return cls.from_dict(pickle.loads(data))
+
+    def to_dict(self):
+        return {
+            "data": self.data.to_dict(),
+            "meta": self.meta.to_dict(),
+            "target": self.target.to_dict() if self.target is not None else None,
+        }
+
+    def to_bytes(self):
+        return pickle.dumps(self.to_dict())
+
+    def reset_index(self):
+        return StructuredData(
+            data=self.data.reset_index(drop=True),
+            meta=self.meta.reset_index(drop=True),
+            target=self.target.reset_index() if self.target is not None else None,
+        )
+
+
+class Prediction(BaseModelConfig):
+    predictions: np.ndarray
+    name: str = ""
+
+    @classmethod
+    def from_dict(cls, data):
+        predictions = np.array(data["predictions"])
+        name = data["name"]
+        return cls(predictions=predictions, name=name)
+
+    @classmethod
+    def from_bytes(cls, data):
+        return cls.from_dict(pickle.loads(data))
+
+    def to_dict(self):
+        return {
+            "predictions": self.predictions,
+            "name": self.name,
+        }
+
+    def to_bytes(self):
+        return pickle.dumps(self.to_dict())


Test patch:
diff --git a/tests/datasets/test_datasets_tabular.py b/tests/datasets/test_datasets_tabular.py
new file mode 100644
index 0000000..cbd7497
--- /dev/null
+++ b/tests/datasets/test_datasets_tabular.py
@@ -0,0 +1,21 @@
+from siapy.datasets import TabularDataset
+from siapy.entities import Pixels, Shape, SpectralImageSet
+
+
+def test_tabular(spectral_images):
+    pixels_input = [(10, 15), (60, 66)]
+    pixels = Pixels.from_iterable(pixels_input)
+    rectangle = Shape.from_shape_type(shape_type="rectangle", pixels=pixels)
+
+    spectral_images.vnir.geometric_shapes.append(rectangle)
+    spectral_images.swir.geometric_shapes.append(rectangle)
+
+    images = [
+        spectral_images.vnir,
+        spectral_images.swir,
+        spectral_images.vnir,
+    ]
+
+    image_set = SpectralImageSet(images)
+    dataset = TabularDataset(image_set)
+    dataset.generate()
diff --git a/tests/models/test_mlmodels_models.py b/tests/models/test_mlmodels_models.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/models/test_mlmodels_schemas.py b/tests/models/test_mlmodels_schemas.py
new file mode 100644
index 0000000..e69de29