diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 91ce4a6..7b591a2 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -48,4 +48,4 @@ jobs: uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ - password: ${{ secrets.PYPI_PASSWORD }} \ No newline at end of file + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index d3a319d..9dc019a 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: test the library +name: Test the library on: push: @@ -13,13 +13,18 @@ jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} + cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,4 +37,4 @@ jobs: # # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with tox run: | - tox \ No newline at end of file + tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..df3a2f4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,52 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +- repo: https://github.com/PyCQA/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + additional_dependencies: [tomli] + args: [--in-place, --wrap-descriptions=120, --wrap-summaries=120] + # --config, ./pyproject.toml + +- repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + language_version: python3 + +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.9 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + +## If like to embrace black styles even in the docs: +# - repo: https://github.com/asottile/blacken-docs +# rev: v1.13.0 +# hooks: +# - id: blacken-docs +# additional_dependencies: [black] + +## Check for misspells in documentation files: +# - repo: https://github.com/codespell-project/codespell +# rev: v2.2.5 +# hooks: +# - id: codespell diff --git a/CHANGELOG.md b/CHANGELOG.md index d203ca4..941c1af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## Version 0.2.0 +## Version 0.2.0 - Support multi apply ## Version 0.1 (development) - first release -- performs some basic operations over numpy or scipy matrices. +- performs some basic operations over numpy or scipy matrices. - provides apply method so user can extend the underlying logic to any function diff --git a/docs/conf.py b/docs/conf.py index 862e58a..7cf736a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -169,6 +169,15 @@ def setup(app): # If this is True, todo emits a warning for each TODO entries. The default is False. todo_emit_warnings = True +autodoc_default_options = { + 'special-members': True, + 'undoc-members': False, + 'exclude-members': '__weakref__, __dict__, __str__, __module__, __init__' + } + +autosummary_generate = True +autosummary_imported_members = True + # -- Options for HTML output ------------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 2662cf2..0aec8ef 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ +furo # Requirements file for ReadTheDocs, check .readthedocs.yml. # To build the module reference correctly, make sure every external package # under `install_requires` in `setup.cfg` is also listed here! # sphinx_rtd_theme recommonmark sphinx>=3.2.1 -furo \ No newline at end of file diff --git a/docs/tutorial.md b/docs/tutorial.md index 1bca5f9..39f797f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -50,7 +50,7 @@ apply(nz_func, mat, axis=1) ## Multiple functions -`mopsy` also supports applying multiple functions at the same time. +`mopsy` also supports applying multiple functions at the same time. ```python from mopsy import multi_apply @@ -73,4 +73,4 @@ tmat_wrow = append_row(mat, np.array([0, 0, 0, 0, 0])) tmat_wcol = append_col(mat, np.array([[0], [0], [0], [0], [0]])) ``` -That's all for today! \ No newline at end of file +That's all for today! diff --git a/pyproject.toml b/pyproject.toml index 2c63dbb..a7cea75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,18 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] # See configuration details in https://github.com/pypa/setuptools_scm version_scheme = "no-guess-dev" + +[tool.ruff] +line-length = 120 +src = ["src"] +exclude = ["tests"] +extend-ignore = ["F821"] + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.ruff.per-file-ignores] +"__init__.py" = ["E402", "F401"] + +[tool.black] +force-exclude = "__init__.py" diff --git a/setup.py b/setup.py index 2cbcbdb..eb4e889 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,8 @@ -""" - Setup file for mopsy. - Use setup.cfg to configure your project. +"""Setup file for mopsy. Use setup.cfg to configure your project. - This file was generated with PyScaffold 4.1.1. - PyScaffold helps you to put up the scaffold of your new Python project. - Learn more under: https://pyscaffold.org/ +This file was generated with PyScaffold 4.1.1. +PyScaffold helps you to put up the scaffold of your new Python project. +Learn more under: https://pyscaffold.org/ """ from setuptools import setup diff --git a/src/mopsy/__init__.py b/src/mopsy/__init__.py index 938f2dc..dabba4e 100644 --- a/src/mopsy/__init__.py +++ b/src/mopsy/__init__.py @@ -21,5 +21,5 @@ finally: del version, PackageNotFoundError -from .helpers import * -from .adders import * +from .helpers import * # noqa: F403 +from .adders import * # noqa: F403 diff --git a/src/mopsy/adders.py b/src/mopsy/adders.py index 68ec930..fbdb260 100644 --- a/src/mopsy/adders.py +++ b/src/mopsy/adders.py @@ -1,10 +1,10 @@ -from .sops import Sops -from .checkutils import check_axis - from typing import Union -import scipy as sp import numpy as np +import scipy as sp + +from .checkutils import check_axis +from .sops import Sops __author__ = "jkanche" __copyright__ = "jkanche" @@ -14,56 +14,69 @@ def append_row( mat: sp.sparse.spmatrix, row: Union[sp.sparse.spmatrix, np.ndarray] ) -> sp.sparse.spmatrix: - """A generic append function for sparse matrices + """A generic append function for sparse matrices. Args: - mat (scipy.sparse.spmatrix): sparse matrix - row (Union[sp.sparse.spmatrix, np.ndarray]): rows to append + mat: + Sparse matrix. + row: + Rows to append. Raises: - TypeError: if axis is neither 0 nor 1 + TypeError: + If axis is neither 0 nor 1. Returns: - scipy.sparse.spmatrix: new matrix + A new sparse matrix, usually the same type as the input matrix. """ - return sparse_append(mat=mat, rowOrCols=row, axis=0) + return sparse_append(mat=mat, row_or_column=row, axis=0) def append_col( mat: sp.sparse.spmatrix, col: Union[sp.sparse.spmatrix, np.ndarray] ) -> sp.sparse.spmatrix: - """A generic append function for sparse matrices + """A generic append function for sparse matrices. Args: - mat (scipy.sparse.spmatrix): sparse matrix - col (Union[sp.sparse.spmatrix, np.ndarray]): columns to append + mat: + Sparse matrix. + + col: + Columns to append. Raises: - TypeError: if axis is neither 0 nor 1 + TypeError: + If axis is neither 0 nor 1. Returns: - scipy.sparse.spmatrix: new matrix + A new sparse matrix, usually the same type as the input matrix. """ - return sparse_append(mat=mat, rowOrCols=col, axis=1) + return sparse_append(mat=mat, row_or_column=col, axis=1) def sparse_append( mat: sp.sparse.spmatrix, - rowOrCols: Union[sp.sparse.spmatrix, np.ndarray], + row_or_column: Union[sp.sparse.spmatrix, np.ndarray], axis: Union[int, bool], ) -> sp.sparse.spmatrix: - """A generic append function for sparse matrices + """A generic append function for sparse matrices. Args: - mat (scipy.sparse.spmatrix): sparse matrix - rowOrCols (Union[sp.sparse.spmatrix, np.ndarray]): rows to append - axis (Union[int, bool]): 0 for rows, 1 for columns. + mat: + Sparse matrix. + + row_or_column: + Rows or columns to append. + + axis: + 0 for rows, 1 for columns. Raises: - TypeError: if axis is neither 0 nor 1 + TypeError: + If axis is neither 0 nor 1. Returns: - scipy.sparse.spmatrix: new matrix + A new sparse matrix, usually the same type as the input matrix. """ if not isinstance(mat, sp.sparse.spmatrix): @@ -75,19 +88,19 @@ def sparse_append( new_mat = None if axis == 0: - if mat.shape[0] != rowOrCols.shape[0]: + if mat.shape[0] != row_or_column.shape[0]: raise TypeError( - f"matrix and new row do not have the same length. matrix: {mat.shape[0]}, row: {row.shape[0]}" + "Matrix and new row do not have the same shape along the first dimension." ) - new_mat = sp.sparse.vstack([mat, rowOrCols]) + new_mat = sp.sparse.vstack([mat, row_or_column]) else: - if mat.shape[1] != rowOrCols.shape[0]: + if mat.shape[1] != row_or_column.shape[0]: raise TypeError( - f"matrix and new row do not have the same length. matrix: {mat.shape[1]}, row: {row.shape[1]}" + "Matrix and new row do not have the same shape along the second dimension." ) - new_mat = sp.sparse.hstack([mat, rowOrCols]) + new_mat = sp.sparse.hstack([mat, row_or_column]) if new_mat is None: raise Exception("This should never happen") diff --git a/src/mopsy/checkutils.py b/src/mopsy/checkutils.py index f9763c8..0135438 100644 --- a/src/mopsy/checkutils.py +++ b/src/mopsy/checkutils.py @@ -8,13 +8,15 @@ def check_axis(axis: Union[int, bool]): - """Check if axis has a correct value + """Check if axis has a correct value. Args: - axis (Union[int, bool]): axis, 0 for rows, 1 for columns + axis: + Axis, 0 for rows, 1 for columns Raises: - TypeError: if axis is neither 0 nor 1 + ValueError: + If axis is neither 0 nor 1 """ if not (axis == 0 or axis == 1): - raise TypeError(f"axis is neither 0 or 1, provided {axis}") + raise ValueError(f"'axis' is neither 0 or 1, provided {axis}.") diff --git a/src/mopsy/helpers.py b/src/mopsy/helpers.py index 0d49661..57e2293 100644 --- a/src/mopsy/helpers.py +++ b/src/mopsy/helpers.py @@ -1,11 +1,12 @@ from statistics import mean, median -from .utils import get_matrix_type -from .checkutils import check_axis +from typing import Callable, Sequence, Union -from typing import Sequence, Union, Callable, Any import numpy import scipy +from .checkutils import check_axis +from .utils import get_matrix_type + __author__ = "jkanche" __copyright__ = "jkanche" __license__ = "MIT" @@ -16,15 +17,23 @@ def colsum( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply colsum + """Apply column sum. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of columns. + Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the column sums. """ return apply(sum, mat, group=group, axis=1, non_zero=non_zero) @@ -34,15 +43,22 @@ def rowsum( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply rowsum + """Apply row sum. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of rows. Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the row sums. """ return apply(sum, mat, group=group, axis=0, non_zero=non_zero) @@ -52,15 +68,22 @@ def colmean( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply colmean + """Apply column mean. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of columns. Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the column means. """ return apply(mean, mat, group=group, axis=1, non_zero=non_zero) @@ -70,15 +93,22 @@ def rowmean( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply rowmean + """Apply row mean. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of rows. Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the row means. """ return apply(mean, mat, group=group, axis=0, non_zero=non_zero) @@ -88,15 +118,22 @@ def colmedian( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply colmedian + """Apply column median. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of columns. Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the column medians. """ return apply(median, mat, group=group, axis=1, non_zero=non_zero) @@ -106,37 +143,55 @@ def rowmedian( group: Sequence = None, non_zero: bool = False, ) -> numpy.ndarray: - """Apply rowmedian + """Apply row median. Args: - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - non_zero (bool): filter zero values ? + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of rows. Defaults to None. + + non_zero: + Whether to filter zero values. + Defaults to False. Returns: - numpy.ndarray: matrix + A matrix with the row medians. """ return apply(median, mat, group=group, axis=0, non_zero=non_zero) def apply( - func: Callable[[list], Any], + func: Callable, mat: Union[numpy.ndarray, scipy.sparse.spmatrix], axis: Union[int, bool], group: Sequence = None, non_zero: bool = False, ): - """A generic apply function + """A generic `apply` function. Args: - func (Callable): function to be called. - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - axis (Union[int, bool]): 0 for rows, 1 for columns. - non_zero (bool): filter zero values ? + func: + Function to apply over the groups. + + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. + + axis: + 0 for rows, 1 for columns. + + non_zero: + Whether to filter zero values. Defaults to False. Returns: - numpy.ndarray: matrix + A matrix containing the result of the function. """ check_axis(axis) @@ -145,25 +200,35 @@ def apply( def multi_apply( - funcs: Sequence[Callable[[list], Any]], + funcs: Sequence[Callable], mat: Union[numpy.ndarray, scipy.sparse.spmatrix], axis: Union[int, bool], group: Sequence = None, non_zero: bool = False, ): - """Apply multiple functions, the first axis - of the ndarray specifies the results of the inputs functions in - the same order + """A generic `multi_apply` to apply multiple function over the subset matrices. Args: - funcs (Sequence[Callable[[list], Any]]): functions to be called. - mat (Union[numpy.ndarray, scipy.sparse.spmatrix]): matrix - group (Sequence, optional): group variable. Defaults to None. - axis (Union[int, bool]): 0 for rows, 1 for columns. - non_zero (bool): filter zero values ? + func: + List of function to apply over the groups. + + mat: + Input matrix. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. + + axis: + 0 for rows, 1 for columns. + + non_zero: + Whether to filter zero values. Defaults to False. Returns: - numpy.ndarray: matrix + A list of matrices, in the same order as the functions + containing the result of each the function. """ check_axis(axis) diff --git a/src/mopsy/mops.py b/src/mopsy/mops.py index c88931c..474284e 100644 --- a/src/mopsy/mops.py +++ b/src/mopsy/mops.py @@ -1,6 +1,7 @@ from itertools import groupby +from typing import Any, Callable, Optional, Sequence, Tuple, Union + import numpy as np -from typing import Any, Callable, Tuple, Optional, Sequence, Union from .checkutils import check_axis @@ -10,26 +11,33 @@ class Mops: - """Base class for all matrix operations""" + """Base class for all matrix operations.""" def __init__(self, mat, non_zero: bool = False) -> None: - """Intialize the matrix + """Intialize the matrix. Args: - mat (numpy.ndarray or scipy.sparse.spmatrix): a matrix - non_zero (bool): filter zero values ? + mat: + Input matrix. + + non_zero: + Whether to filter zero values. + Defaults to False. """ self.matrix = mat self.non_zero = non_zero def groupby_indices(self, group: Sequence) -> dict: - """From a group vector, get the list of indices that map to each group + """From a group vector, get the list of indices that map to each group. Args: - group (list): group variable, any list or array like object + group: + Group vector, expected to ne the same as the + number of rows or column. Returns: - dict: each group and the list of indices that map to it + A dictionary with each group name as the key and the values containing + the list of indices that map to it. """ return { k: [x[0] for x in v] @@ -39,15 +47,6 @@ def groupby_indices(self, group: Sequence) -> dict: } def _apply(self, func: Callable[[list], Any], axis: Union[int, bool]): - """Internal function that wraps numpy's apply_along_axis - - Args: - func (Callable): a function to apply - axis (Union[int, bool]): 0 for rows, 1 for columns - - Returns: - numpy.ndarray: a dense vector after appling group by - """ if self.non_zero: def funcwrapper(mat): @@ -64,19 +63,22 @@ def apply( group: Sequence = None, axis: Union[int, bool] = 0, ) -> Tuple[np.ndarray, Optional[Sequence]]: - """Apply a function to groups along an axis + """Apply a function to groups along an axis. Args: - func (Callable): a function to apply - group (list, optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + func: + List of function to apply over the groups. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. - Raises: - Exception: ApplyFuncError, when a function cannot be applied - TypeError: if axis is neither 0 nor 1 + axis: + 0 for rows, 1 for columns. Returns: - Tuple[np.ndarray, Optional[Sequence]]: a tuple of matrix and its labels + A tuple of matrix and its labels. """ check_axis(axis) @@ -106,35 +108,33 @@ def multi_apply( group: list = None, axis: int = 0, ) -> Tuple[np.ndarray, Optional[Sequence]]: - """Apply multiple functions, the first axis - of the ndarray specifies the results of the inputs functions in - the same order + """Apply multiple functions, the first axis of the ndarray specifies the results of the inputs functions in the + same order. Args: - funcs (List[Callable[[list], Any]]): functions to be called. - group (list, optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + func: + List of function to apply over the groups. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. - Raises: - Exception: ApplyFuncError, when a function cannot be applied - TypeError: if axis is neither 0 nor 1 + axis: + 0 for rows, 1 for columns. Returns: - Tuple[np.ndarray, Optional[Sequence]]: a tuple of matrix and its labels + A tuple of matrix and its labels. """ - + check_axis(axis) result = None rgroups = None try: if group is None: - tmats = [self._apply(f, axis=axis) for f in funcs] - nmats = [ - x[np.newaxis] if axis == 0 else x[np.newaxis].T - for x in tmats - ] + nmats = [x[np.newaxis] if axis == 0 else x[np.newaxis].T for x in tmats] result = nmats else: rgroups = [] diff --git a/src/mopsy/nops.py b/src/mopsy/nops.py index b72418a..b7a3d63 100644 --- a/src/mopsy/nops.py +++ b/src/mopsy/nops.py @@ -1,36 +1,34 @@ -from typing import Iterator, Tuple, Union, Sequence -from .mops import Mops +from typing import Iterator, Sequence, Tuple, Union import numpy as np +from .mops import Mops + __author__ = "jkanche" __copyright__ = "jkanche" __license__ = "MIT" class Nops(Mops): - """Internal representation for numpy arrays""" + """Internal representation for numpy arrays.""" def __init__(self, mat: np.ndarray, non_zero: bool = False) -> None: - """Intialize with a numpy matrix - - Args: - mat (numpy.ndarray): numpy matrix - non_zero (bool): filter zero values ? - """ super().__init__(mat, non_zero=non_zero) def iter( self, group: Sequence[str] = None, axis: Union[int, bool] = 0 ) -> Iterator[Tuple]: - """Iterator over groups and an axis + """Iterator over groups and an axis. Args: - group (Sequence[str], optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + group: + Group variable. Defaults to None. + + axis: + 0 for rows, 1 for columns. Defaults to 0. Yields: - Tuple (str, Nops): of group and the submatrix + A tuple (str, Nops) of group and the submatrix. """ mat = self.matrix @@ -42,7 +40,10 @@ def iter( if axis == 0: yield ( k, - Nops(mat[v,], self.non_zero,), + Nops( + mat[v,], + self.non_zero, + ), ) else: yield (k, Nops(mat[:, v], self.non_zero)) diff --git a/src/mopsy/sops.py b/src/mopsy/sops.py index 429a1b1..e5f84fc 100644 --- a/src/mopsy/sops.py +++ b/src/mopsy/sops.py @@ -1,11 +1,11 @@ -from .mops import Mops -from .nops import Nops +from statistics import mean +from typing import Any, Callable, Iterator, Optional, Sequence, Tuple, Union -from scipy import sparse as sp import numpy as np -from statistics import mean +from scipy import sparse as sp -from typing import Callable, Any, Iterator, Tuple, Sequence, Optional, Union +from .mops import Mops +from .nops import Nops __author__ = "jkanche" __copyright__ = "jkanche" @@ -13,26 +13,33 @@ class Sops(Mops): - """Sops, Sparse Matrix Operation Class""" + """Sops, Sparse Matrix Operation Class.""" def __init__(self, mat: sp.spmatrix, non_zero: bool = False) -> None: """Initialize the class from a scipy sparse matrix. Args: - mat (scipy.sparse.spmatrix): a scipy sparse matrix - non_zero (bool): filter zero values ? + mat: + Input scipy sparse matrix. + + non_zero: + Whether to filter zero values. + Defaults to False. """ super().__init__(mat, non_zero=non_zero) def iter(self, group: list = None, axis: Union[int, bool] = 0) -> Iterator[Tuple]: - """Iterator over groups and an axis + """Iterator over groups and an axis. Args: - group (list, optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + group: + Group variable. Defaults to None. + + axis: + 0 for rows, 1 for columns. Defaults to 0. Yields: - tuple (str, matrix): of group and the submatrix + a tuple (str, matrix) of group and the submatrix. """ mat = self.matrix.tocsr() if axis == 0 else self.matrix.tocsc() if group is None: @@ -43,21 +50,17 @@ def iter(self, group: list = None, axis: Union[int, bool] = 0) -> Iterator[Tuple if axis == 0: yield ( k, - Sops(mat[v,], self.non_zero,), + Sops( + mat[v,], + self.non_zero, + ), ) else: yield (k, Sops(mat[:, v], self.non_zero)) - def _apply(self, func: Callable[[list], Any], axis: Union[int, bool] = 0) -> np.ndarray: - """Apply a function over the matrix - - Args: - func (Callable): function to apply over row or col wise vectors - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. - - Returns: - numpy.ndarray: a dense vector - """ + def _apply( + self, func: Callable[[list], Any], axis: Union[int, bool] = 0 + ) -> np.ndarray: mat = self.matrix.tocsc() if axis == 0 else self.matrix.tocsr() if self.non_zero: # reduction along an axis @@ -96,18 +99,26 @@ def apply( group: Sequence = None, axis: Union[int, bool] = 0, ) -> Tuple[np.ndarray, Optional[Sequence]]: - """Apply a function to groups along an axis + """Apply a function to groups along an axis. Args: - func (Callable): a function to apply - group (list, optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + func: + List of function to apply over the groups. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. + + axis: + 0 for rows, 1 for columns. Raises: - Exception: ApplyFuncError, when a function cannot be applied + Exception: + ApplyFuncError, when a function cannot be applied. Returns: - Tuple[np.ndarray, Optional[Sequence]]: a tuple of matrix and its labels + A tuple of matrix and its labels. """ original_sparse_type = Sops.identify_sparse_type(self.matrix) mat, groups = super().apply(func, group, axis) @@ -122,46 +133,51 @@ def multi_apply( group: list = None, axis: Union[int, bool] = 0, ) -> Tuple[np.ndarray, Optional[Sequence]]: - """Apply multiple functions, the first axis - of the ndarray specifies the results of the inputs functions in - the same order + """Apply multiple functions, the first axis of the ndarray specifies the results of the inputs functions in the + same order. Args: - funcs (List[Callable[[list], Any]]): functions to be called. - group (list, optional): group variable. Defaults to None. - axis (Union[int, bool], optional): 0 for rows, 1 for columns. Defaults to 0. + func: + List of function to apply over the groups. + + group: + Group vector, must be the same length as the number + of rows or columns depending on the axis. + Defaults to None. + + axis: + 0 for rows, 1 for columns. Raises: - Exception: ApplyFuncError, when a function cannot be applied + Exception: + ApplyFuncError, when a function cannot be applied. Returns: - Tuple[np.ndarray, Optional[Sequence]]: a tuple of matrix and its labels + A tuple of matrix and its labels. """ original_sparse_type = Sops.identify_sparse_type(self.matrix) mats, groups = super().multi_apply(funcs, group, axis) - cmats = [ - Sops.convert_sparse_type(m, original_sparse_type) for m in mats - ] + cmats = [Sops.convert_sparse_type(m, original_sparse_type) for m in mats] return cmats, groups @staticmethod def identify_sparse_type(mat: sp.spmatrix): - """Identify the sparse matrix format + """Identify the sparse matrix format. Args: - mat (scipy.sparse.spmatrix): a scipy matrix + mat: + Input scipy matrix. Raises: - TypeError: matrix is not sparse + TypeError: + If matrix is not sparse. Returns: - an internal matrix representation object + An internal matrix representation object """ if not isinstance(mat, sp.spmatrix): - raise TypeError( - f"mat is not a sparse representation, it is {type(mat)}" - ) + raise TypeError(f"mat is not a sparse representation, it is {type(mat)}") if sp.isspmatrix_csc(mat): return "csc" @@ -180,17 +196,21 @@ def identify_sparse_type(mat: sp.spmatrix): @staticmethod def convert_sparse_type(mat: sp.spmatrix, format: str): - """Convert to a sparse matrix format + """Convert to a sparse matrix format. Args: - mat (scipy.sparse.spmatrix): a numpy or scipy matrix - format (str): sparse matrix format, one of `identify_sparse_type()` + mat: + A numpy or scipy matrix. + + format: + Sparse matrix format, one of `identify_sparse_type()`. Raises: - TypeError: matrix is not sparse + TypeError: + If matrix is not sparse. Returns: - an internal matrix representation object + An internal matrix representation object. """ if isinstance(mat, np.ndarray): if format == "csc": @@ -223,4 +243,4 @@ def convert_sparse_type(mat: sp.spmatrix, format: str): elif format == "lil": return mat.tolil() else: - raise Exception(f"unknown matrix format") + raise Exception("unknown matrix format.") diff --git a/src/mopsy/utils.py b/src/mopsy/utils.py index df5ecc4..e800ab0 100644 --- a/src/mopsy/utils.py +++ b/src/mopsy/utils.py @@ -1,27 +1,32 @@ +from typing import Union + import numpy as np import scipy.sparse as sp + from .nops import Nops from .sops import Sops -from typing import Union - __author__ = "jkanche" __copyright__ = "jkanche" __license__ = "MIT" def get_matrix_type(mat: Union[np.ndarray, sp.spmatrix], non_zero: bool = False): - """Get an internal matrix state + """Get an internal matrix state. Args: - mat (Numpy.ndarray or scipy.sparse.spmatrix): a numpy or scipy matrix - non_zero (bool): filter zero values ? + mat: + An input numpy or scipy matrix. + + non_zero: + Whether to filter zero value. Defaults to False. Raises: - Exception: TypeNotSupported, when the matrix type is not supported + Exception: + TypeNotSupported, when the matrix type is not supported. Returns: - an internal matrix representation object + An internal matrix representation object. """ if isinstance(mat, np.ndarray): return Nops(mat, non_zero=non_zero) diff --git a/tests/conftest.py b/tests/conftest.py index 7c452b3..6b38a9e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,9 @@ -""" - Dummy conftest.py for mopsy. +"""Dummy conftest.py for mopsy. - If you don't know what this is for, just leave it empty. - Read more about conftest.py under: - - https://docs.pytest.org/en/stable/fixture.html - - https://docs.pytest.org/en/stable/writing_plugins.html +If you don't know what this is for, just leave it empty. +Read more about conftest.py under: +- https://docs.pytest.org/en/stable/fixture.html +- https://docs.pytest.org/en/stable/writing_plugins.html """ # import pytest diff --git a/tests/test_interface.py b/tests/test_interface.py index a9cb45a..1108bc4 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -19,4 +19,3 @@ def test_apply_cols(): def test_multiapply_cols(): rmat = multi_apply([mean], mat, 1, None, False) assert rmat is not None - diff --git a/tests/test_sparse_add.py b/tests/test_sparse_add.py index 0765299..56c9fb1 100644 --- a/tests/test_sparse_add.py +++ b/tests/test_sparse_add.py @@ -10,7 +10,7 @@ def test_sparse_add_row(): mat = eye(5).tocsr() tmat = sparse_append(mat, np.array([0, 0, 0, 0, 0]), axis=0) - + assert tmat is not None assert tmat.shape[0] == mat.shape[0] + 1 assert type(tmat) == type(mat) @@ -23,4 +23,3 @@ def test_sparse_add_col(): assert tmat is not None assert tmat.shape[1] == mat.shape[1] + 1 assert type(tmat) == type(mat) -