├── .github ├── dependabot.yml └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── img └── example.png ├── setup.py ├── spatialentropy ├── __init__.py ├── _altieri_entropy.py ├── _leibovici_entropy.py └── _utils.py ├── tests ├── pytest.ini └── test_all.py └── utils.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | python-version: [3.8] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | # install spatialentropy 27 | pip install -e . 28 | - name: Lint with flake8 29 | run: | 30 | pip install flake8 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 spatialentropy --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 spatialentropy --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 35 | - name: Test with pytest 36 | run: | 37 | pip install pytest 38 | pytest tests/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit tests / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | .DS_Store 133 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [Mr-Milk] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpatialEntropy 2 | 3 | ![Test](https://github.com/Mr-Milk/SpatialEntropy/workflows/Test/badge.svg) [![PyPI version](https://badge.fury.io/py/spatialentropy.svg)](https://badge.fury.io/py/spatialentropy) 4 | 5 | This is a python implementation of spatial entropy, inspired by the R package *spatentropy*. For now, two spatial entropy methods have been implemented: 6 | 7 | - Leibovici’s entropy 8 | - Altieri's entropy 9 | 10 | 11 | ## Compare with shannon entropy 12 | 13 | ![Compare](https://github.com/Mr-Milk/SpatialEntropy/blob/master/img/example.png?raw=true) 14 | 15 | 16 | ## Installation 17 | 18 | It's available on PyPI 19 | 20 | ```shell 21 | pip install spatialentropy 22 | ``` 23 | 24 | 25 | ## Usage 26 | 27 | [Check out an example](https://nbviewer.jupyter.org/gist/Mr-Milk/af67ac0957201227723ed76f526487ea) 28 | 29 | Let's generate some fake data first: 30 | 31 | ```python 32 | import numpy as np 33 | 34 | points = 100 * np.random.randn(10000, 2) + 1000 35 | types = np.random.choice(range(30), 10000) 36 | ``` 37 | 38 | Here we have 10,000 points and then we assigned each point with a category from 30 categories. 39 | 40 | ### Quick start 41 | 42 | ```python 43 | from spatialentropy import leibovici_entropy 44 | 45 | e = leibovici_entropy(points, types) 46 | e.entropy 47 | ``` 48 | 49 | ### Leibovici entropy 50 | 51 | To calculate the leibovici entropy, we need to set up a distance or an interval to define the co-occurrences. 52 | 53 | ```python 54 | from spatialentropy import leibovici_entropy 55 | 56 | # set the distance cut-off to 5 57 | e = leibovici_entropy(points, types, d=5) 58 | # if you want to change the base of log 59 | e = leibovici_entropy(points, types, base=2) 60 | 61 | e.entropy # to get the entropy value 62 | e.adj_matrix # to get the adjacency matrix 63 | e.pairs_counts # to get the counts for each pair of co-occurrences 64 | ``` 65 | 66 | ### Altieri entropy 67 | 68 | To calculate the altieri entropy, we need to set up intervals to define the co-occurrences. 69 | 70 | ```python 71 | from spatialentropy import altieri_entropy 72 | 73 | # set cut=2, it means we will create 3 intervals evenly from [0,max] 74 | e = altieri_entropy(points, types, cut=2) 75 | 76 | # or you want to define your own intervals 77 | e = altieri_entropy(points, types, cut=[0,4,10]) 78 | 79 | e.entropy # to get the entropy value, e.entropy = e.mutual_info + e.residue 80 | e.mutual_info # the spatial mutual information 81 | e.residue # the spatial residue entropy 82 | e.adj_matrix # to get the adjacency matrix 83 | ``` 84 | -------------------------------------------------------------------------------- /img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mr-Milk/SpatialEntropy/943143b653d14470287e2fdbbef31a1df1336996/img/example.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup(name="spatialentropy", 7 | packages=find_packages(), 8 | description="A python implementation of spatial entropy", 9 | long_description=long_description, 10 | long_description_content_type="text/markdown", 11 | version="0.1.0", 12 | author="Mr-Milk", 13 | author_email="zym.zym1220@gmail.com", 14 | url="https://github.com/Mr-Milk/SpatialEntropy", 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | python_requires='>=3.5', 21 | install_requires=['numpy', 'scikit-learn', 'neighborhood_analysis']) 22 | -------------------------------------------------------------------------------- /spatialentropy/__init__.py: -------------------------------------------------------------------------------- 1 | from ._altieri_entropy import altieri_entropy, altieri 2 | from ._leibovici_entropy import leibovici_entropy, leibovici 3 | -------------------------------------------------------------------------------- /spatialentropy/_altieri_entropy.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence, Union, List 2 | 3 | import numpy as np 4 | from sklearn.metrics import pairwise_distances 5 | 6 | from ._utils import interval_pairs, type_adj_matrix, pairs_counter 7 | 8 | 9 | def altieri(points: Union[List[float], np.ndarray], 10 | types: Union[List[str], np.ndarray], 11 | cut: Union[int, list, None] = None, 12 | order: bool = True, 13 | base: Union[int, float, None] = None 14 | ) -> (float, float, float): 15 | """Altieri entropy 16 | 17 | Args: 18 | points: array, 2d array 19 | types: array, the length should correspond to points 20 | cut: int or array, number means how many cut to make from [0, max], array allow you to make your own cut 21 | order: bool, if True, (x1, x2) and (x2, x1) is not the same 22 | base: int or float, the log base, default is e 23 | 24 | Returns: 25 | float 26 | 27 | """ 28 | if len(points) != len(types): 29 | raise ValueError("Array of points and types should have same length") 30 | 31 | if base is None: 32 | base = np.e 33 | 34 | points = np.asarray(points) 35 | dist_max = np.sqrt(sum([i * i for i in points.T.max(axis=1) - points.T.min(axis=1)])) 36 | adj_matrix = pairwise_distances(points) 37 | 38 | if isinstance(cut, int): 39 | break_interval = interval_pairs(np.linspace(0, dist_max, cut + 2)) 40 | 41 | elif isinstance(cut, Sequence): 42 | break_interval = interval_pairs(cut) 43 | 44 | elif cut is None: 45 | break_interval = interval_pairs(np.linspace(0, dist_max, 3)) 46 | 47 | else: 48 | raise ValueError("'cut' must be an int or an array-like object") 49 | 50 | w, zw = [], [] 51 | for (p1, p2) in break_interval: 52 | bool_matx = ((adj_matrix > p1) & (adj_matrix <= p2)).astype(int) 53 | type_matx, utypes = type_adj_matrix(bool_matx, types) 54 | pairs_counts = pairs_counter(type_matx, utypes, order) 55 | zw.append(pairs_counts) 56 | w.append(p2 - p1) 57 | 58 | bool_matx = (adj_matrix > 0).astype(int) 59 | type_matx, utypes = type_adj_matrix(bool_matx, types) 60 | z = pairs_counter(type_matx, utypes, order) 61 | 62 | w = np.asarray(w) 63 | w = w / w.sum() 64 | 65 | zw = np.asarray(zw) 66 | 67 | pz = np.array(list(z.values())) 68 | pz = pz / pz.sum() 69 | 70 | H_Zwk = [] # H(Z|w_k) 71 | PI_Zwk = [] # PI(Z|w_k) 72 | 73 | for i in zw: 74 | vi = i.values() 75 | 76 | v, pz_ = [], [] 77 | for ix, x in enumerate(vi): 78 | if x != 0: 79 | v.append(x) 80 | pz_.append(pz[ix]) 81 | 82 | v = np.asarray(v) 83 | pz_ = np.asarray(pz_) 84 | 85 | v = v / v.sum() 86 | H = v * np.log(1 / v) / np.log(base) 87 | PI = v * np.log(v / pz_) / np.log(base) 88 | H_Zwk.append(H.sum()) 89 | PI_Zwk.append(PI.sum()) 90 | 91 | residue = (w * np.asarray(H_Zwk)).sum() 92 | mutual_info = (w * np.asarray(PI_Zwk)).sum() 93 | entropy = residue + mutual_info 94 | 95 | return entropy, mutual_info, residue 96 | 97 | 98 | class altieri_entropy(object): 99 | """Altieri entropy 100 | 101 | Attributes: 102 | mutual_info: float, the value of the spatial mutual information part of the entropy 103 | residue: float, the value of the spatial residue entropy 104 | entropy: float, the value of leibovici entropy, equal to (mutual_info + residue) 105 | 106 | """ 107 | 108 | def __init__(self, 109 | points: Union[list, np.ndarray], 110 | types: Union[list, np.ndarray], 111 | cut: Union[int, list, None] = None, 112 | order: bool = True, 113 | base: Union[int, float, None] = None 114 | ): 115 | """ 116 | 117 | Args: 118 | points: array, 2d array 119 | types: array, the length should correspond to points 120 | cut: int or array, number means how many cut to make from [0, max], array allow you to make your own cut 121 | order: bool, if True, (x1, x2) and (x2, x1) is not the same 122 | base: int or float, the log base, default is e 123 | 124 | """ 125 | self.entropy, self.mutual_info, self.residue = altieri(points, types, cut, order, base) 126 | -------------------------------------------------------------------------------- /spatialentropy/_leibovici_entropy.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | 3 | import numpy as np 4 | from neighborhood_analysis import get_point_neighbors 5 | 6 | from ._utils import get_pair_count, types2int, get_pair 7 | 8 | 9 | def leibovici(points: Union[List[float], np.ndarray], 10 | types: Union[List[str], np.ndarray], 11 | d: Union[int, float, None] = None, 12 | order: bool = True, 13 | base: Union[int, float, None] = None 14 | ) -> float: 15 | """Leibovici entropy 16 | 17 | Args: 18 | points: array, 2d array 19 | types: array, the length should correspond to points 20 | d: int or float, cut-off distance, default is 10 21 | order: bool, if True, (x1, x2) and (x2, x1) is not the same 22 | base: int or float, the log base, default is e 23 | 24 | Returns: 25 | float 26 | 27 | """ 28 | if len(points) != len(types): 29 | raise ValueError("Array of points and types should have same length") 30 | 31 | if base is None: 32 | base = np.e 33 | 34 | if d is None: 35 | d = 10 36 | elif isinstance(d, (int, float)): 37 | pass 38 | else: 39 | raise TypeError("d should be a number.") 40 | 41 | points = [tuple(i) for i in points] 42 | if isinstance(types[0], str): 43 | types = types2int(types) 44 | 45 | neighbors = get_point_neighbors(points, r=d, labels=types) 46 | pair = get_pair(types, neighbors) 47 | pair_count = get_pair_count(pair, order) 48 | v = pair_count.values() 49 | # clean all elements that equal to zero to prevent divide by zero error 50 | v = np.array([i for i in v if i != 0]) 51 | 52 | v = v / v.sum() 53 | v = v * np.log(1 / v) / np.log(base) 54 | 55 | return v.sum() 56 | 57 | 58 | class leibovici_entropy(object): 59 | """Leibovici entropy 60 | 61 | Attributes: 62 | entropy: float, the value of leibovici entropy 63 | 64 | """ 65 | 66 | def __init__(self, points, types, d=None, order=False, base=None): 67 | """ 68 | 69 | Args: 70 | points: array, 2d array 71 | types: array, the length should correspond to points 72 | d: int or float, cut-off distance, default is 10 73 | order: bool, if True, (x1, x2) and (x2, x1) is not the same 74 | base: int or float, the log base, default is e 75 | 76 | """ 77 | self.entropy = leibovici(points, types, d, order, base) 78 | -------------------------------------------------------------------------------- /spatialentropy/_utils.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict, Counter 2 | from itertools import combinations_with_replacement, product 3 | 4 | import numpy as np 5 | 6 | 7 | def reduce_matrix_row(matrix, types, storage): 8 | """reduce matrix size by merging the row that has same type 9 | 10 | Args: 11 | matrix: array, An N * M matrix 12 | types: array, length should equal to N 13 | storage: dict, storage object 14 | 15 | Returns: 16 | array, row-merged matrix based on types 17 | 18 | """ 19 | for i, arr in enumerate(matrix): 20 | storage[types[i]].append(arr) 21 | 22 | for k, v in storage.items(): 23 | storage[k] = np.asarray(v).sum(axis=0) 24 | 25 | new_types = [] 26 | new_matx = [] 27 | for k, v in storage.items(): 28 | new_types.append(k) 29 | new_matx.append(v) 30 | 31 | return new_matx 32 | 33 | 34 | def type_adj_matrix(matrix, types): 35 | """return an N * N matrix, N is the number of unique types 36 | 37 | Args: 38 | matrix: array 39 | types: array 40 | 41 | Returns: 42 | tuple, matrix and the unique types 43 | 44 | """ 45 | unitypes = np.unique(types) 46 | 47 | storage = OrderedDict(zip(unitypes, [[] for _ in range(len(unitypes))])) 48 | new_matrix = reduce_matrix_row(matrix, types, storage) 49 | 50 | storage = OrderedDict(zip(unitypes, [[] for _ in range(len(unitypes))])) 51 | type_matrix = reduce_matrix_row(np.asarray(new_matrix).T, types, storage) 52 | 53 | return np.array(type_matrix), unitypes 54 | 55 | 56 | def pairs_counter(matrix, types, order=False): 57 | """count how many pairs of types in the matrix 58 | 59 | Args: 60 | matrix: array 61 | types: array 62 | order: bool, if True, (x1, x2) and (x2, x1) is not the same 63 | 64 | Returns: 65 | dict, the count of each pairs 66 | 67 | """ 68 | it = np.nditer(matrix, flags=["multi_index"]) 69 | 70 | if order: 71 | combs = [i for i in product(types, repeat=2)] 72 | storage = OrderedDict(zip(combs, [0 for _ in range(len(combs))])) 73 | 74 | for x in it: 75 | (i1, i2) = it.multi_index 76 | storage[(types[i1], types[i2])] += x 77 | else: 78 | combs = [i for i in combinations_with_replacement(types, 2)] 79 | storage = OrderedDict(zip(combs, [0 for _ in range(len(combs))])) 80 | 81 | for x in it: 82 | (i1, i2) = it.multi_index 83 | if i1 <= i2: 84 | storage[(types[i1], types[i2])] += x 85 | else: 86 | storage[(types[i2], types[i1])] += x 87 | 88 | return storage 89 | 90 | 91 | def interval_pairs(arr): 92 | new_arr = [] 93 | for i, x in enumerate(arr): 94 | if i < len(arr) - 1: 95 | new_arr.append((x, arr[i + 1])) 96 | 97 | return new_arr 98 | 99 | 100 | def types2int(types): 101 | uni_types = np.unique(types) 102 | types_mapper = dict(zip(uni_types, range(len(uni_types)))) 103 | types = [types_mapper[t] for t in types] 104 | return types 105 | 106 | 107 | def get_pair(types, neighbors): 108 | pairs = [] 109 | for i, n in zip(types, neighbors): 110 | for j in n: 111 | pairs.append((i, j,)) 112 | return pairs 113 | 114 | 115 | def get_pair_count(pair, order): 116 | pairs_counts = Counter(pair) 117 | 118 | if not order: 119 | unorder_pairs_counts = {} 120 | for k, v in pairs_counts.items(): 121 | k_reverse = k[::-1] 122 | if unorder_pairs_counts.get(k) is None: 123 | if unorder_pairs_counts.get(k_reverse) is None: 124 | unorder_pairs_counts[k] = v 125 | else: 126 | unorder_pairs_counts[k_reverse] += v 127 | return unorder_pairs_counts 128 | 129 | return pairs_counts 130 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning -------------------------------------------------------------------------------- /tests/test_all.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from spatialentropy import leibovici_entropy, altieri_entropy 4 | 5 | # some fake data 6 | points = 100 * np.random.randn(1000, 2) + 1000 7 | types = np.random.choice(range(30), 1000) 8 | 9 | 10 | def test_leibovici(): 11 | e1 = leibovici_entropy(points, types, 5, order=False) 12 | e2 = leibovici_entropy(points, types, 5, order=True) 13 | 14 | 15 | def test_altieri(): 16 | e1 = altieri_entropy(points, types, cut=1, order=False) 17 | e2 = altieri_entropy(points, types, cut=[0, 4, 10], order=False) 18 | e3 = altieri_entropy(points, types, cut=1, order=True) 19 | 20 | assert e1.mutual_info + e1.residue == e1.entropy 21 | assert e2.mutual_info + e2.residue == e2.entropy 22 | assert e3.mutual_info + e3.residue == e3.entropy 23 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import numpy as np 4 | import pandas as pd 5 | 6 | import seaborn as sns 7 | import matplotlib.pyplot as plt 8 | 9 | from pointpats import PoissonPointProcess, PoissonClusterPointProcess, Window 10 | 11 | np.random.seed(0) 12 | 13 | 14 | # the points generated function, credit for pointpats 15 | 16 | 17 | def runif_in_circle(n, radius=1.0, center=(0., 0.), burn=2): 18 | good = np.zeros((n, 2), float) 19 | c = 0 20 | r = radius 21 | r2 = r * r 22 | it = 0 23 | while c < n: 24 | x = np.random.uniform(-r, r, (burn * n, 1)) 25 | y = np.random.uniform(-r, r, (burn * n, 1)) 26 | ids = np.where(x * x + y * y <= r2) 27 | candidates = np.hstack((x, y))[ids[0]] 28 | nc = candidates.shape[0] 29 | need = n - c 30 | if nc > need: # more than we need 31 | good[c:] = candidates[:need] 32 | else: # use them all and keep going 33 | good[c:c + nc] = candidates 34 | c += nc 35 | it += 1 36 | return good + np.asarray(center) 37 | 38 | 39 | def plot_points(points, types, ax=None, title=None): 40 | data = pd.DataFrame(points, columns=['x', 'y']) 41 | data['types'] = types 42 | 43 | p = sns.scatterplot(data=data, x='x', y='y', hue='types', ax=ax) 44 | p.legend(loc='upper right', ncol=1) 45 | if ax is None: 46 | plt.title(title) 47 | else: 48 | ax.title.set_text(title) 49 | return p 50 | 51 | 52 | def random_data(window, n): 53 | window = Window(window) 54 | rpp = PoissonPointProcess(window, 200, 1, conditioning=False, asPP=False) 55 | return rpp.realizations[0] 56 | 57 | 58 | def cluster_data(window, n, parents, d, types): 59 | window = Window(window) 60 | l, b, r, t = window.bbox 61 | children = int(n / parents) 62 | # get parent points 63 | pxs = np.random.uniform(l, r, (int(n / children), 1)) 64 | pys = np.random.uniform(b, t, (int(n / children), 1)) 65 | cents = np.hstack((pxs, pys)) 66 | 67 | pnts = {tuple(center): runif_in_circle(children, d, center) for center in cents} 68 | 69 | types_counts = Counter(types) 70 | utypes = types_counts.keys() 71 | type_mapper = {} 72 | for k, v in pnts.items(): 73 | points_count = len(v) 74 | new_arr = [] 75 | for i in utypes: 76 | if len(new_arr) != points_count: 77 | c = types_counts[i] 78 | if c != 0: 79 | current_len = points_count - len(new_arr) 80 | if c >= current_len: 81 | new_arr += [i for _ in range(current_len)] 82 | types_counts[i] -= current_len 83 | else: 84 | new_arr += [i for _ in range(c)] 85 | types_counts[i] = 0 86 | else: 87 | break 88 | type_mapper[k] = new_arr 89 | 90 | points = [] 91 | ordered_types = [] 92 | 93 | for k, v in pnts.items(): 94 | points += list(v) 95 | ordered_types += type_mapper[k] 96 | 97 | return points, ordered_types 98 | --------------------------------------------------------------------------------