├── .gitignore ├── LICENSE ├── README.md ├── README.rst ├── fastpip ├── __init__.py ├── fastpip.py └── tests │ ├── __init__.py │ ├── piptest.py │ ├── sample_1 │ ├── sample_2 │ └── sample_3 ├── runtests.py ├── setup.py └── tox.ini /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | *.pyc 92 | *.swo 93 | *.swp 94 | *DS_Store 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-fastpip (Segmentation Based On Turning Points) 2 | ===================================================== 3 | 4 | The “Perceptually Important Points” algorithm 5 | gives a method for dimensionality reduction and a mechanism to automatically extract 6 | the most important points from a human observer perspective, favouring compression and 7 | a good visualization of time series with high dimensionality. 8 | 9 | Example 10 | ------- 11 | 12 | ```python 13 | >>> from fastpip import pip 14 | >>> pip([(0, 0), (1, 1), (2, 2), (3, 1), (4, 0), (5, 1), (6, 2), (7, 1), (8, 0)], 5) 15 | [(0, 0), (2, 2), (4, 0), (6, 2), (8, 0)] 16 | ``` 17 | 18 | 19 | References 20 | ---------- 21 | - https://github.com/intelie/fastpip-js 22 | - http://www.academia.edu/1578716/Aplica%C3%A7%C3%A3o_do_algoritmo_Perceptually_Important_Points_em_s%C3%A9ries_temporais_de_datacenters 23 | - http://thescipub.com/PDF/jcssp.2010.1389.1395.pdf 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-fastpip (Segmentation Based On Turning Points) 2 | ===================================================== 3 | 4 | The “Perceptually Important Points” algorithm 5 | gives a method for dimensionality reduction and a mechanism to automatically extract 6 | the most important points from a human observer perspective, favouring compression and 7 | a good visualization of time series with high dimensionality. 8 | 9 | Example 10 | ------- 11 | 12 | :: 13 | 14 | >>> from fastpip import pip 15 | >>> pip([(0, 0), (1, 1), (2, 2), (3, 1), (4, 0), (5, 1), (6, 2), (7, 1), (8, 0)], 5) 16 | [(0, 0), (2, 2), (4, 0), (6, 2), (8, 0)] 17 | 18 | 19 | References 20 | ---------- 21 | - https://github.com/intelie/fastpip-js 22 | - http://www.academia.edu/1578716/Aplica%C3%A7%C3%A3o_do_algoritmo_Perceptually_Important_Points_em_s%C3%A9ries_temporais_de_datacenters 23 | - http://thescipub.com/PDF/jcssp.2010.1389.1395.pdf 24 | -------------------------------------------------------------------------------- /fastpip/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .fastpip import pip # NOQA 4 | -------------------------------------------------------------------------------- /fastpip/fastpip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging as default_logging 3 | import sys 4 | import math 5 | 6 | __all__ = ['pip'] 7 | 8 | logging = default_logging.getLogger('simentarweb:utils:charts') 9 | 10 | INFINITY = float('inf') 11 | 12 | 13 | class PipItem(object): 14 | 15 | def __init__(self, index, parent, **kwargs): 16 | self.index = index 17 | self.parent = parent 18 | self.cache = kwargs.get('cache', INFINITY) 19 | self.value = kwargs.get('value', None) 20 | self.order = kwargs.get('order', None) 21 | self.left = kwargs.get('left', None) 22 | self.right = kwargs.get('right', None) 23 | 24 | def update_cache(self): 25 | if None in (self.left, self.right): 26 | self.cache = INFINITY 27 | else: 28 | self.cache = self.parent.distance(self.left.value, self.value, self.right.value) 29 | self.parent.notify_change(self.index) 30 | 31 | def put_after(self, tail): 32 | if tail is not None: 33 | tail.right = self 34 | tail.update_cache() 35 | self.left = tail 36 | self.update_cache() 37 | return self 38 | 39 | def recycle(self): 40 | if self.left is None: 41 | self.parent.head = self.right 42 | else: 43 | self.left.right = self.right 44 | self.left.update_cache() 45 | 46 | if self.right is None: 47 | self.parent.tail = self.left 48 | else: 49 | self.right.left = self.left 50 | self.right.update_cache() 51 | 52 | return self.clear() 53 | 54 | def clear(self): 55 | self.order = 0 56 | self.left = None 57 | self.right = None 58 | self.cache = INFINITY 59 | 60 | ret = self.value 61 | self.value = None 62 | return ret 63 | 64 | 65 | class PipHeap(object): 66 | 67 | def __init__(self, distance, **kwargs): 68 | self.distance = distance 69 | self.heap = self.create_heap(512) 70 | self.head = None 71 | self.tail = None 72 | self.size = 0 73 | self.global_order = 0 74 | 75 | def create_heap(self, size): 76 | return [PipItem(i, self) for i in range(size)] 77 | 78 | def ensure_heap(self, size): 79 | new_elements = [PipItem(i, self) for i in range(len(self.heap), size+1)] 80 | self.heap.extend(new_elements) 81 | 82 | def acquire_item(self, value): 83 | self.ensure_heap(self.size) 84 | item = self.heap[self.size] 85 | item.value = value 86 | 87 | self.size += 1 88 | self.global_order += 1 89 | item.order = self.global_order 90 | return item 91 | 92 | def add(self, value): 93 | self.tail = self.acquire_item(value).put_after(self.tail) 94 | if self.head is None: 95 | self.head = self.tail 96 | 97 | @property 98 | def min_value(self): 99 | return self.heap[0].cache 100 | 101 | def remove_min(self): 102 | return self.remove_at(0) 103 | 104 | def remove_at(self, index): 105 | self.size -= 1 106 | self.swap(index, self.size) 107 | self.bubble_down(index) 108 | return self.heap[self.size].recycle() 109 | 110 | def notify_change(self, index): 111 | return self.bubble_down(self.bubble_up(index)) 112 | 113 | def bubble_up(self, n): 114 | while (n != 0) and self.less(n, (n-1)/2): 115 | n = self.swap(n, (n-1)/2) 116 | return n 117 | 118 | def bubble_down(self, n): 119 | get_k = lambda n: self.min(n, n*2+1, n*2+2) 120 | 121 | k = get_k(n) 122 | while (k != n) and (k < self.size): 123 | n = self.swap(n, k) 124 | k = get_k(n) 125 | return n 126 | 127 | def min(self, i, j, k=None): 128 | if k is not None: 129 | result = self.min(i, self.min(j, k)) 130 | else: 131 | result = i if self.less(i, j) else j 132 | 133 | return result 134 | 135 | def less(self, i, j): 136 | def i_smaller_than_j(heap, i, j): 137 | i, j = int(i), int(j) 138 | if heap[i].cache != heap[j].cache: 139 | result = heap[i].cache < heap[j].cache 140 | else: 141 | result = heap[i].order < heap[j].order 142 | return result 143 | 144 | heap = self.heap 145 | return ((i < self.size) and (j >= self.size or i_smaller_than_j(heap, i, j))) 146 | 147 | def swap(self, i, j): 148 | i, j = int(i), int(j) 149 | self.heap[i].index, self.heap[j].index = j, i 150 | self.heap[i], self.heap[j] = self.heap[j], self.heap[i] 151 | return j 152 | 153 | def __iter__(self): 154 | current = self.head 155 | while current is not None: 156 | yield current.value 157 | current = current.right 158 | 159 | 160 | def vertical_distance(left, current, right): 161 | EPSILON = 1e-06 162 | a_x, a_y = left 163 | b_x, b_y = current 164 | c_x, c_y = right 165 | 166 | if (abs(a_x - b_x) < EPSILON) or (abs(b_x - c_x) < EPSILON): 167 | result = 0 168 | elif (c_x - a_x) == 0: 169 | # Otherwise we could have a ZeroDivisionError 170 | result = INFINITY 171 | else: 172 | result = abs(((a_y + (c_y - a_y) * (b_x - a_x) / (c_x - a_x) - b_y)) * (c_x - a_x)) 173 | 174 | return result 175 | 176 | 177 | def dist(x1, x2, y1, y2): 178 | return math.sqrt((x1-x2)**2 + (y1-y2)**2) 179 | 180 | 181 | def euclidean_distance(left, current, right): 182 | left_current = dist(left[0], current[0], left[1], current[1]) 183 | rightcurrent = dist(right[0], current[0], right[1], current[1]) 184 | return (left_current + rightcurrent) * (right[0] - left[0]) 185 | 186 | 187 | def pip(data, k, fast=True, stream_mode=True, distance='vertical'): 188 | distance_functions = { 189 | 'vertical': vertical_distance, 190 | 'euclidean': euclidean_distance, 191 | } 192 | distance_function = distance_functions[distance] 193 | 194 | if fast: 195 | result = fastpip(data, k, stream_mode=stream_mode, distance_function=distance_function) 196 | else: 197 | result = simplepip(data, k, distance_function=distance_function) 198 | 199 | return result 200 | 201 | 202 | def fastpip(data, k, stream_mode=True, distance_function=vertical_distance): 203 | 204 | if len(data) >= k: 205 | heap = PipHeap(distance_function) 206 | 207 | for element in data: 208 | heap.add(element) 209 | 210 | if stream_mode and (heap.size > k): 211 | heap.remove_min() 212 | 213 | if not stream_mode: 214 | while heap.size > k: 215 | heap.remove_min() 216 | 217 | ret = list(heap) 218 | else: 219 | ret = data 220 | 221 | logging.debug("pip: started with {} points, returned {} points".format(len(data), len(ret))) 222 | return ret 223 | 224 | 225 | def simplepip(data, k, distance_function=vertical_distance): 226 | ret = [] 227 | 228 | for (idx, value) in enumerate(data): 229 | ret.append(value) 230 | if len(ret) <= k: 231 | continue 232 | 233 | miniv = sys.maxsize 234 | minij = 0 235 | 236 | for j in range(1, len(ret) - 1): 237 | d = distance_function(ret[j - 1], ret[j], ret[j + 1]) 238 | if d < miniv: 239 | miniv = d 240 | minij = j 241 | 242 | del ret[minij] 243 | 244 | return ret 245 | -------------------------------------------------------------------------------- /fastpip/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .piptest import * # NOQA 4 | -------------------------------------------------------------------------------- /fastpip/tests/piptest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from unittest import TestCase, expectedFailure 3 | import os 4 | import json 5 | from functools import partial 6 | 7 | from fastpip import pip 8 | 9 | __all__ = ['TestFastPip', 'TestSimplePip'] 10 | 11 | 12 | class BaseTestPip(TestCase): 13 | 14 | __samples_cache__ = {} 15 | 16 | def setUp(self): 17 | super(BaseTestPip, self).setUp() 18 | self.sample_1 = lambda: self.load_sample('sample_1') 19 | self.sample_2 = lambda: self.load_sample('sample_2') 20 | self.sample_3 = lambda: self.load_sample('sample_3') 21 | 22 | def load_sample(self, name): 23 | if name not in self.__samples_cache__: 24 | current_dir = os.path.dirname(os.path.abspath(__file__)) 25 | filename = os.path.join(current_dir, name) 26 | with open(filename) as handle: 27 | self.__samples_cache__[name] = json.load(handle) 28 | 29 | return self.__samples_cache__[name] 30 | 31 | def assertAlmostEqualCurves(self, curve_a, curve_b, msg=None): 32 | with_seven_places = lambda value: round(value, 7) 33 | curve_with_seven_places = lambda curve: [ 34 | (with_seven_places(item[0]), with_seven_places(item[1])) 35 | for item in curve 36 | ] 37 | 38 | return self.assertEqual( 39 | curve_with_seven_places(curve_a), curve_with_seven_places(curve_b), msg=msg 40 | ) 41 | 42 | def assertNotAlmostEqualCurves(self, curve_a, curve_b, msg=None): 43 | with_seven_places = lambda value: round(value, 7) 44 | curve_with_seven_places = lambda curve: [ 45 | (with_seven_places(item[0]), with_seven_places(item[1])) 46 | for item in curve 47 | ] 48 | 49 | return self.assertNotEqual( 50 | curve_with_seven_places(curve_a), curve_with_seven_places(curve_b), msg=msg 51 | ) 52 | 53 | def test_empty_input(self): 54 | data = [] 55 | self.assertEqual(self.pip(data, 50), data) 56 | 57 | def test_nothing_is_changed_when_data_has_less_points_than_expected(self): 58 | data = list(zip(range(0, 10), 'qwertasdfg')) 59 | self.assertEqual(self.pip(data, 50), data) 60 | 61 | @expectedFailure 62 | def test_values_for_X_must_always_be_increasing(self): 63 | data = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (3, 3), (2, 2), (1, 1), (0, 0)] 64 | expected = [(0, 0), (4, 4), (0, 0)] 65 | self.assertEqual(self.pip(data, 3), expected) 66 | 67 | def test_smallest_set_of_significant_points(self): 68 | data = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 3), (6, 2), (7, 1), (8, 0)] 69 | expected = [(0, 0), (4, 4), (8, 0)] 70 | self.assertEqual(self.pip(data, 3), expected) 71 | 72 | def test_four_significant_points(self): 73 | data = [(0, 0), (1, 1), (2, 0), (3, -1), (4, -2), (5, -3), (6, -2), (7, -1), (8, 0)] 74 | expected = [(0, 0), (1, 1), (5, -3), (8, 0)] 75 | self.assertEqual(self.pip(data, 4), expected) 76 | 77 | def test_five_significant_points(self): 78 | data = [(0, 0), (1, 3), (2, 2), (3, 1), (4, 0), (5, 1), (6, 2), (7, 3), (8, 0)] 79 | expected = [(0, 0), (1, 3), (4, 0), (7, 3), (8, 0)] 80 | self.assertEqual(self.pip(data, 5), expected) 81 | 82 | def test_result_should_always_be_the_same_for_a_given_input(self): 83 | data = [(item.get('x'), item.get('y')) for item in self.sample_1()] 84 | data_length = len(data) 85 | 86 | first_result = pip(data, data_length/10) 87 | for i in range(3): 88 | self.assertAlmostEqualCurves( 89 | self.pip(data, data_length/10), first_result, "Result has changed on iteration {}".format(i+1) 90 | ) 91 | 92 | def test_behaviour_changes_according_to_the_distance_function_used_1(self): 93 | data = [(item.get('x'), item.get('y')) for item in self.sample_1()] 94 | 95 | vertical_result = pip(data, 300, distance='vertical') 96 | euclidean_result = pip(data, 300, distance='euclidean') 97 | self.assertNotAlmostEqualCurves(vertical_result, euclidean_result) 98 | 99 | def test_behaviour_changes_according_to_the_distance_function_used_2(self): 100 | data = self.sample_2() 101 | 102 | vertical_result = pip(data, 300, distance='vertical') 103 | euclidean_result = pip(data, 300, distance='euclidean') 104 | self.assertNotAlmostEqualCurves(vertical_result, euclidean_result) 105 | 106 | def test_behaviour_changes_according_to_the_distance_function_used_3(self): 107 | data = self.sample_3() 108 | 109 | vertical_result = pip(data, 300, distance='vertical') 110 | euclidean_result = pip(data, 300, distance='euclidean') 111 | self.assertNotAlmostEqualCurves(vertical_result, euclidean_result) 112 | 113 | def test_behaviour_may_change_with_batch_or_stream_processing_using_vertical_distance_1(self): 114 | data = [(item.get('x'), item.get('y')) for item in self.sample_1()] 115 | 116 | stream_result = pip(data, 300, distance='vertical', stream_mode=True) 117 | batch_result = pip(data, 300, distance='vertical', stream_mode=False) 118 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 119 | 120 | def test_behaviour_may_change_with_batch_or_stream_processing_using_vertical_distance_2(self): 121 | data = self.sample_2() 122 | 123 | stream_result = pip(data, 300, distance='vertical', stream_mode=True) 124 | batch_result = pip(data, 300, distance='vertical', stream_mode=False) 125 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 126 | 127 | def test_behaviour_may_change_with_batch_or_stream_processing_using_vertical_distance_3(self): 128 | data = self.sample_3() 129 | 130 | stream_result = pip(data, 300, distance='vertical', stream_mode=True) 131 | batch_result = pip(data, 300, distance='vertical', stream_mode=False) 132 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 133 | 134 | def test_behaviour_may_change_with_batch_or_stream_processing_using_euclidean_distance_1(self): 135 | data = [(item.get('x'), item.get('y')) for item in self.sample_1()] 136 | 137 | stream_result = pip(data, 300, distance='euclidean', stream_mode=True) 138 | batch_result = pip(data, 300, distance='euclidean', stream_mode=False) 139 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 140 | 141 | def test_behaviour_may_change_with_batch_or_stream_processing_using_euclidean_distance_2(self): 142 | data = self.sample_2() 143 | 144 | stream_result = pip(data, 300, distance='euclidean', stream_mode=True) 145 | batch_result = pip(data, 300, distance='euclidean', stream_mode=False) 146 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 147 | 148 | def test_behaviour_may_change_with_batch_or_stream_processing_using_euclidean_distance_3(self): 149 | data = self.sample_3() 150 | 151 | stream_result = pip(data, 300, distance='euclidean', stream_mode=True) 152 | batch_result = pip(data, 300, distance='euclidean', stream_mode=False) 153 | self.assertNotAlmostEqualCurves(stream_result, batch_result) 154 | 155 | 156 | class TestFastPip(BaseTestPip): 157 | 158 | def setUp(self): 159 | super(TestFastPip, self).setUp() 160 | self.pip = partial(pip, fast=True) 161 | 162 | @expectedFailure 163 | def test_values_for_X_must_always_be_increasing_in_all_modes(self): 164 | data = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (3, 3), (2, 2), (1, 1), (0, 0)] 165 | expected = [(0, 0), (4, 4), (0, 0)] 166 | self.assertEqual(self.pip(data, 3, stream_mode=False), expected) 167 | 168 | def test_smallest_set_of_significant_points_in_batch_mode(self): 169 | data = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 3), (6, 2), (7, 1), (8, 0)] 170 | expected = [(0, 0), (4, 4), (8, 0)] 171 | self.assertEqual(self.pip(data, 3, stream_mode=False), expected) 172 | 173 | def test_four_significant_points_in_batch_mode(self): 174 | data = [(0, 0), (1, 1), (2, 0), (3, -1), (4, -2), (5, -3), (6, -2), (7, -1), (8, 0)] 175 | expected = [(0, 0), (1, 1), (5, -3), (8, 0)] 176 | self.assertEqual(self.pip(data, 4, stream_mode=False), expected) 177 | 178 | def test_five_significant_points_in_batch_mode(self): 179 | data = [(0, 0), (1, 3), (2, 2), (3, 1), (4, 0), (5, 1), (6, 2), (7, 3), (8, 0)] 180 | expected = [(0, 0), (1, 3), (4, 0), (7, 3), (8, 0)] 181 | self.assertEqual(self.pip(data, 5, stream_mode=False), expected) 182 | 183 | 184 | class TestSimplePip(BaseTestPip): 185 | 186 | def setUp(self): 187 | super(TestSimplePip, self).setUp() 188 | self.pip = partial(pip, fast=False) 189 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from fastpip.tests import * 4 | 5 | if __name__ == '__main__': 6 | unittest.main() 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | from setuptools import setup, find_packages 6 | from setuptools.command.test import test as TestCommand 7 | 8 | 9 | class Tox(TestCommand): 10 | user_options = TestCommand.user_options + [ 11 | ('environment=', 'e', "Run 'test_suite' in specified environment") 12 | ] 13 | environment = None 14 | 15 | def finalize_options(self): 16 | TestCommand.finalize_options(self) 17 | self.test_args = [] 18 | self.test_suite = True 19 | 20 | def run_tests(self): 21 | #import here, cause outside the eggs aren't loaded 22 | import tox 23 | if self.environment: 24 | self.test_args.append('-e{0}'.format(self.environment)) 25 | errno = tox.cmdline(self.test_args) 26 | sys.exit(errno) 27 | 28 | 29 | def read_file(filename): 30 | """Read a file into a string""" 31 | open_kwargs = {} 32 | if sys.version_info.major == 3: 33 | open_kwargs = {'encoding': 'utf-8'} 34 | 35 | path = os.path.abspath(os.path.dirname(__file__)) 36 | filepath = os.path.join(path, filename) 37 | with open(filepath, **open_kwargs) as filecontents: 38 | return filecontents.read() 39 | 40 | 41 | def get_readme(): 42 | """Return the README file contents. Supports text,rst, and markdown""" 43 | for name in ('README', 'README.rst', 'README.md'): 44 | if os.path.exists(name): 45 | return read_file(name) 46 | return '' 47 | 48 | 49 | setup( 50 | name="python-fastpip", 51 | version="1.2", 52 | url='https://github.com/intelie/python-fastpip', 53 | author='Vitor M. A. da Cruz', 54 | author_email='vitor.mazzi@intelie.com.br', 55 | description='Segmentation Based On Turning Points', 56 | long_description=get_readme(), 57 | packages=find_packages(exclude=["example", "*.tests", "*.tests.*", "tests.*", "tests"]), 58 | include_package_data=True, 59 | install_requires='', 60 | tests_require=['virtualenv>=1.11.2', 'tox>=1.6.1', ], 61 | cmdclass={'test': Tox}, 62 | test_suite='fastpip.tests', 63 | classifiers=[ 64 | 'Topic :: Scientific/Engineering :: Visualization', 65 | 'License :: OSI Approved :: Apache Software License', 66 | ], 67 | license="Apache 2", 68 | ) 69 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_missing_interpreters = true 3 | envlist = {py27,py34,py35} 4 | 5 | [testenv] 6 | basepython = 7 | py27: python2.7 8 | py34: python3.4 9 | py35: python3.5 10 | commands = 11 | {envpython} runtests.py {posargs} 12 | --------------------------------------------------------------------------------