pax_global_header00006660000000000000000000000064145070733410014516gustar00rootroot0000000000000052 comment=162fdb9bcf32c8748c264b513d15eb9390b498a6 numpy-groupies-0.10.2/000077500000000000000000000000001450707334100146015ustar00rootroot00000000000000numpy-groupies-0.10.2/.gitattributes000066400000000000000000000000501450707334100174670ustar00rootroot00000000000000numpy_groupies/_version.py export-subst numpy-groupies-0.10.2/.github/000077500000000000000000000000001450707334100161415ustar00rootroot00000000000000numpy-groupies-0.10.2/.github/workflows/000077500000000000000000000000001450707334100201765ustar00rootroot00000000000000numpy-groupies-0.10.2/.github/workflows/ci.yaml000066400000000000000000000035551450707334100214650ustar00rootroot00000000000000name: CI on: push: branches: - "*" pull_request: branches: - "*" schedule: - cron: "0 0 * * *" # Daily “At 00:00” workflow_dispatch: # allows you to trigger manually concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: name: Build (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} defaults: run: shell: bash -l {0} strategy: fail-fast: false matrix: os: ["ubuntu-latest"] python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 with: fetch-depth: 10 # Fetch all history for all branches and tags. - name: Set environment variables run: | echo "CONDA_ENV_FILE=ci/environment.yml" >> $GITHUB_ENV echo "PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV - name: Setup micromamba uses: mamba-org/provision-with-micromamba@v15 with: environment-file: ${{ env.CONDA_ENV_FILE }} environment-name: xarray-tests cache-env: true cache-env-key: "${{runner.os}}-${{runner.arch}}-py${{matrix.python-version}}-${{hashFiles(env.CONDA_ENV_FILE)}}" extra-specs: | python=${{matrix.python-version}} conda # We only want to install this on one run, because otherwise we'll have # duplicate annotations. - name: Install error reporter if: ${{ matrix.os }} == 'ubuntu-latest' and ${{ matrix.python-version }} == '3.10' run: | python -m pip install pytest-github-actions-annotate-failures - name: Set up conda environment shell: bash -l {0} run: | python -m pip install -e .[dev] conda list - name: Run Tests shell: bash -l {0} run: | pytest numpy-groupies-0.10.2/.github/workflows/pypi-release.yaml000066400000000000000000000046161450707334100234700ustar00rootroot00000000000000name: Build and Upload numpy-groupies to PyPI on: release: types: - published push: tags: - 'v*' jobs: build-artifacts: runs-on: ubuntu-latest if: github.repository == 'ml31415/numpy-groupies' steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-python@v4 name: Install Python with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install build twine - name: Build tarball and wheels run: | git clean -xdf git restore -SW . python -m build - name: Check built artifacts run: | python -m twine check --strict dist/* pwd if [ -f dist/numpy-groupies-0.0.0.tar.gz ]; then echo "❌ INVALID VERSION NUMBER" exit 1 else echo "✅ Looks good" fi - uses: actions/upload-artifact@v3 with: name: releases path: dist test-built-dist: needs: build-artifacts runs-on: ubuntu-latest steps: - uses: actions/setup-python@v4 name: Install Python with: python-version: "3.10" - uses: actions/download-artifact@v3 with: name: releases path: dist - name: List contents of built dist run: | ls -ltrh ls -ltrh dist - name: Verify the built dist/wheel is valid if: github.event_name == 'push' run: | python -m pip install --upgrade pip python -m pip install dist/numpy-groupies*.tar.gz - name: Publish package to TestPyPI if: github.event_name == 'push' uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.TESTPYPI_TOKEN }} repository-url: https://test.pypi.org/legacy/ verbose: true upload-to-pypi: needs: test-built-dist if: github.event_name == 'release' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 with: name: releases path: dist - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} verbose: truenumpy-groupies-0.10.2/.gitignore000066400000000000000000000006401450707334100165710ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Dev stuff .mr.developer.cfg .idea .project .pydevproject .settings/ .cache/ __pycache__/ .eggs/ .hypothesis/ *~ *.ini # Dynamic versioning numpy_groupies/_version.py numpy-groupies-0.10.2/LICENSE.txt000066400000000000000000000024371450707334100164320ustar00rootroot00000000000000Copyright (c) 2016, numpy-groupies developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. numpy-groupies-0.10.2/README.md000066400000000000000000000361311450707334100160640ustar00rootroot00000000000000[![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/ml31415/numpy-groupies/ci.yaml?branch=master&logo=github&style=flat)](https://github.com/ml31415/numpy-groupies/actions) [![PyPI](https://img.shields.io/pypi/v/numpy-groupies.svg?style=flat)](https://pypi.org/project/numpy-groupies/) [![Conda-forge](https://img.shields.io/conda/vn/conda-forge/numpy_groupies.svg?style=flat)](https://anaconda.org/conda-forge/numpy_groupies) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # numpy-groupies This package consists of a small library of optimised tools for doing things that can roughly be considered "group-indexing operations". The most prominent tool is `aggregate`, which is described in detail further down the page. ## Installation If you have `pip`, then simply: ``` pip install numpy_groupies ``` Note that `numpy_groupies` doesn't have any compulsory dependencies (even `numpy` is optional) so you should be able to install it fairly easily even without a package manager. If you just want one particular implementation of `aggregate` (e.g. `aggregate_numpy.py`), you can download that one file, and copy-paste the contents of `utils.py` into the top of that file (replacing the `from .utils import (...)` line). ## aggregate ![aggregate_diagram](/diagrams/aggregate.png) ```python import numpy as np import numpy_groupies as npg group_idx = np.array([ 3, 0, 0, 1, 0, 3, 5, 5, 0, 4]) a = np.array([13.2, 3.5, 3.5,-8.2, 3.0,13.4,99.2,-7.1, 0.0,53.7]) npg.aggregate(group_idx, a, func='sum', fill_value=0) # >>> array([10.0, -8.2, 0.0, 26.6, 53.7, 92.1]) ``` `aggregate` takes an array of values, and an array giving the group number for each of those values. It then returns the sum (or mean, or std, or any, ...etc.) of the values in each group. You have probably come across this idea before - see [Matlab's `accumarray` function](http://uk.mathworks.com/help/matlab/ref/accumarray.html?refresh=true), or [`pandas` groupby concept](http://pandas.pydata.org/pandas-docs/dev/groupby.html), or [MapReduce paradigm](http://en.wikipedia.org/wiki/MapReduce), or simply the [basic histogram](https://en.wikipedia.org/wiki/Histogram). A couple of implemented functions do not reduce the data, instead it calculates values cumulatively while iterating over the data or permutates them. The output size matches the input size. ```python group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) npg.aggregate(group_idx, a, func='cumsum') # >>> array([3, 4, 5, 6,15, 9,15,22, 7, 0,15,17, 6,14,31,39]) ``` ### Inputs The function accepts various different combinations of inputs, producing various different shapes of output. We give a brief description of the general meaning of the inputs and then go over the different combinations in more detail: * `group_idx` - array of non-negative integers to be used as the "labels" with which to group the values in `a`. * `a` - array of values to be aggregated. * `func='sum'` - the function to use for aggregation. See the section below for more details. * `size=None` - the shape of the output array. If `None`, the maximum value in `group_idx` will set the size of the output. * `fill_value=0` - value to use for output groups that do not appear anywhere in the `group_idx` input array. * `order='C'` - for multidimensional output, this controls the layout in memory, can be `'F'` for fortran-style. * `dtype=None` - the`dtype` of the output. `None` means choose a sensible type for the given `a`, `func`, and `fill_value`. * `axis=None` - explained below. * `ddof=0` - passed through into calculations of variance and standard deviation (see section on functions). ![aggregate_dims_diagram](/diagrams/aggregate_dims.png) * Form 1 is the simplest, taking `group_idx` and `a` of matching 1D lengths, and producing a 1D output. * Form 2 is similar to Form 1, but takes a scalar `a`, which is broadcast out to the length of `group_idx`. Note that this is generally not that useful. * Form 3 is more complicated. `group_idx` is the same length as the `a.shape[axis]`. The groups are broadcast out along the other axis/axes of `a`, thus the output is of shape `n_groups x a.shape[0] x ... x a.shape[axis-1] x a.shape[axis+1] x ... a.shape[-1]`, i.e. the output has two or more dimensions. * Form 4 also produces output with two or more dimensions, but for very different reasons to Form 3. Here `a` is 1D and `group_idx` is exactly `2D`, whereas in Form 3 `a` is `ND`, `group_idx` is `1D`, and we provide a value for `axis`. The length of `a` must match `group_idx.shape[1]`, the value of `group_idx.shape[0]` determines the number of dimensions in the output, i.e. `group_idx[:,99]` gives the `(x,y,z)` group indices for the `a[99]`. * Form 5 is the same as Form 4 but with scalar `a`. As with Form 2, this is rarely that helpful. **Note on performance.** The `order` of the output is unlikely to affect performance of `aggregate` (although it may affect your downstream usage of that output), however the order of multidimensional `a` or `group_idx` can affect performance: in Form 4 it is best if columns are contiguous in memory within `group_idx`, i.e. `group_idx[:, 99]` corresponds to a contiguous chunk of memory; in Form 3 it's best if all the data in `a` for `group_idx[i]` is contiguous, e.g. if `axis=1` then we want `a[:, 55]` to be contiguous. ### Available functions By default, `aggregate` assumes you want to sum the values within each group, however you can specify another function using the `func` kwarg. This `func` can be any custom callable, however you will likely want one of the following optimized functions. Note that not all functions might be provided by all implementations. * `'sum'` - sum of items within each group (see example above). * `'prod'` - product of items within each group * `'mean'` - mean of items within each group * `'var'`- variance of items within each group. Use `ddof` kwarg for degrees of freedom. The divisor used in calculations is `N - ddof`, where `N` represents the number of elements. By default `ddof` is zero. * `'std'` - standard deviation of items within each group. Use `ddof` kwarg for degrees of freedom (see `var` above). * `'min'` - minimum value of items within each group. * `'max'` - maximum value of items within each group. * `'first'` - first item in `a` from each group. * `'last'` - last item in `a` from each group. * `'argmax'` - the index in `a` of the maximum value in each group. * `'argmin'` - the index in `a` of the minimum value in each group. The above functions also have a `nan`-form, which skip the `nan` values instead of propagating them to the result of the calculation: * `'nansum'`, `'nanprod'`, `'nanmean'`, `'nanvar'`, `'nanstd'`, `'nanmin'`, `'nanmax'`, `'nanfirst'`, `'nanlast'`, `'nanargmax'`, `'nanargmin'` The following functions are slightly different in that they always return boolean values. Their treatment of nans is also different from above: * `'all'` - `True` if all items within a group are truethy. Note that `np.all(nan)` is `True`, i.e. `nan` is actually truethy. * `'any'` - `True` if any items within a group are truethy. * `'allnan'` - `True` if all items within a group are `nan`. * `'anynan'` - `True` if any items within a group are `nan`. The following functions don't reduce the data, but instead produce an output matching the size of the input: * `'cumsum'` - cumulative sum of items within each group. * `'cumprod'` - cumulative product of items within each group. (numba only) * `'cummin'` - cumulative minimum of items within each group. (numba only) * `'cummax'` - cumulative maximum of items within each group. (numba only) * `'sort'` - sort the items within each group in ascending order, use reverse=True to invert the order. Finally, there are three functions which don't reduce each group to a single value, instead they return the full set of items within the group: * `'array'` - simply returns the grouped items, using the same order as appeared in `a`. (numpy only) ### Examples Compute sums of consecutive integers, and then compute products of those consecutive integers. ```python group_idx = np.arange(5).repeat(3) # group_idx: array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) a = np.arange(group_idx.size) # a: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) x = npg.aggregate(group_idx, a) # sum is default # x: array([ 3, 12, 21, 30, 39]) x = npg.aggregate(group_idx, a, 'prod') # x: array([ 0, 60, 336, 990, 2184]) ``` Get variance ignoring nans, setting all-nan groups to `nan`. ```python x = npg.aggregate(group_idx, a, func='nanvar', fill_value=nan) ``` Count the number of elements in each group. Note that this is equivalent to doing `np.bincount(group_idx)`, indeed that is how the numpy implementation does it. ```python x = npg.aggregate(group_idx, 1) ``` Sum 1000 values into a three-dimensional cube of size 15x15x15. Note that in this example all three dimensions have the same size, but that doesn't have to be the case. ```python group_idx = np.random.randint(0, 15, size=(3, 1000)) a = np.random.random(group_idx.shape[1]) x = npg.aggregate(group_idx, a, func="sum", size=(15,15,15), order="F") # x.shape: (15, 15, 15) # np.isfortran(x): True ``` Use a custom function to generate some strings. ```python group_idx = np.array([1, 0, 1, 4, 1]) a = np.array([12.0, 3.2, -15, 88, 12.9]) x = npg.aggregate(group_idx, a, func=lambda g: ' or maybe '.join(str(gg) for gg in g), fill_value='') # x: ['3.2', '12.0 or maybe -15.0 or maybe 12.9', '', '', '88.0'] ``` Use the `axis` arg in order to do a sum-aggregation on three rows simultaneously. ```python a = np.array([[99, 2, 11, 14, 20], [33, 76, 12, 100, 71], [67, 10, -8, 1, 9]]) group_idx = np.array([[3, 3, 7, 0, 0]]) x = npg.aggregate(group_idx, a, axis=1) # x : [[ 34, 0, 0, 101, 0, 0, 0, 11], # [171, 0, 0, 109, 0, 0, 0, 12], # [ 10, 0, 0, 77, 0, 0, 0, -8]] ``` ### Multiple implementations There are multiple implementations of `aggregate` provided. If you use `from numpy_groupies import aggregate`, the best available implementation will automatically be selected. Otherwise you can pick a specific version directly like `from numpy_groupies import aggregate_nb as aggregate` or by importing aggregate from the implementing module `from numpy_groupies.aggregate_weave import aggregate`. Currently the following implementations exist: * **numpy** - This is the default implementation. It uses plain `numpy`, mainly relying on `np.bincount` and basic indexing magic. It comes without other dependencies except `numpy` and shows reasonable performance for the occasional usage. * **numba** - This is the most performant implementation, based on jit compilation provided by numba and LLVM. * **pure python** - This implementation has no dependencies and uses only the standard library. It's horribly slow and should only be used, if there is no numpy available. * **numpy ufunc** - *Only for benchmarking.* This implementation uses the `.at` method of numpy's `ufunc`s (e.g. `add.at`), which would appear to be designed for performing exactly the same calculation that `aggregate` executes, however the numpy implementation is rather incomplete. * **pandas** - *Only for reference.* The pandas' `groupby` concept is the same as the task performed by `aggregate`. However, `pandas` is not actually faster than the default `numpy` implementation. Also, note that there may be room for improvement in the way that `pandas` is utilized here. Most notably, when computing multiple aggregations of the same data (e.g. `'min'` and `'max'`) pandas could potentially be used more efficiently. All implementations have the same calling syntax and produce the same outputs, to within some floating-point error. However some implementations only support a subset of the valid inputs and will sometimes throw `NotImplementedError`. ### Benchmarks Scripts for testing and benchmarking are included in this repository. For benchmarking, run `python -m numpy_groupies.benchmarks.generic` from the root of this repository. Below we are using `500,000` indices uniformly picked from `[0, 1000)`. The values of `a` are uniformly picked from the interval `[0,1)`, with anything less than `0.2` then set to 0 (in order to serve as falsy values in boolean operations). For `nan-` operations another 20% of the values are set to nan, leaving the remainder on the interval `[0.2,0.8)`. The benchmarking results are given in ms for an i7-7560U running at 2.40GHz: | function | ufunc | numpy | numba | pandas | |-----------|---------|---------|---------|---------| | sum | 1.950 | 1.728 | 0.708 | 11.832 | | prod | 2.279 | 2.349 | 0.709 | 11.649 | | min | 2.472 | 2.489 | 0.716 | 11.686 | | max | 2.457 | 2.480 | 0.745 | 11.598 | | len | 1.481 | 1.270 | 0.635 | 10.932 | | all | 37.186 | 3.054 | 0.892 | 12.587 | | any | 35.278 | 5.157 | 0.890 | 12.845 | | anynan | 5.783 | 2.126 | 0.762 | 144.740 | | allnan | 7.971 | 4.367 | 0.774 | 144.507 | | mean | ---- | 2.500 | 0.825 | 13.284 | | std | ---- | 4.528 | 0.965 | 12.193 | | var | ---- | 4.269 | 0.969 | 12.657 | | first | ---- | 1.847 | 0.811 | 11.584 | | last | ---- | 1.309 | 0.581 | 11.842 | | argmax | ---- | 3.504 | 1.411 | 293.640 | | argmin | ---- | 6.996 | 1.347 | 290.977 | | nansum | ---- | 5.388 | 1.569 | 15.239 | | nanprod | ---- | 5.707 | 1.546 | 15.004 | | nanmin | ---- | 5.831 | 1.700 | 14.292 | | nanmax | ---- | 5.847 | 1.731 | 14.927 | | nanlen | ---- | 3.170 | 1.529 | 14.529 | | nanall | ---- | 6.499 | 1.640 | 15.931 | | nanany | ---- | 8.041 | 1.656 | 15.839 | | nanmean | ---- | 5.636 | 1.583 | 15.185 | | nanvar | ---- | 7.514 | 1.682 | 15.643 | | nanstd | ---- | 7.292 | 1.666 | 15.104 | | nanfirst | ---- | 5.318 | 2.096 | 14.432 | | nanlast | ---- | 4.943 | 1.473 | 14.637 | | nanargmin | ---- | 7.977 | 1.779 | 298.911 | | nanargmax | ---- | 5.869 | 1.802 | 301.022 | | cumsum | ---- | 71.713 | 1.119 | 8.864 | | cumprod | ---- | ---- | 1.123 | 12.100 | | cummax | ---- | ---- | 1.062 | 12.133 | | cummin | ---- | ---- | 0.973 | 11.908 | | arbitrary | ---- | 147.853 | 46.690 | 129.779 | | sort | ---- | 167.699 | ---- | ---- | _Linux(x86_64), Python 3.10.12, Numpy 1.25.2, Numba 0.58.0, Pandas 2.0.2_ ## Development This project was started by @ml31415 and the `numba` and `weave` implementations are by him. The pure python and `numpy` implementations were written by @d1manson. The authors hope that `numpy`'s `ufunc.at` methods or some other implementation of `aggregate` within `numpy` or `scipy` will eventually be fast enough, to make this package redundant. Numpy 1.25 actually contained major [improvements on ufunc speed](https://numpy.org/doc/stable/release/1.25.0-notes.html), which reduced the speed gap between numpy and the numba implementation a lot. numpy-groupies-0.10.2/ci/000077500000000000000000000000001450707334100151745ustar00rootroot00000000000000numpy-groupies-0.10.2/ci/environment.yml000066400000000000000000000002041450707334100202570ustar00rootroot00000000000000name: npg-tests channels: - conda-forge - nodefaults dependencies: - numpy - pandas - numba - pytest - numpy_groupies numpy-groupies-0.10.2/conftest.py000066400000000000000000000013531450707334100170020ustar00rootroot00000000000000""" pytest configuration to silently discard test items with invalid parameter combinations See: https://github.com/pytest-dev/pytest/issues/3730#issuecomment-567142496 """ def pytest_configure(config): config.addinivalue_line("markers", "deselect_if(func): function to deselect tests from parametrization") def pytest_collection_modifyitems(config, items): removed = [] kept = [] for item in items: m = item.get_closest_marker("deselect_if") if m: func = m.kwargs["func"] if func(**item.callspec.params): removed.append(item) continue kept.append(item) if removed: config.hook.pytest_deselected(items=removed) items[:] = kept numpy-groupies-0.10.2/diagrams/000077500000000000000000000000001450707334100163705ustar00rootroot00000000000000numpy-groupies-0.10.2/diagrams/aggregate.png000066400000000000000000000220561450707334100210310ustar00rootroot00000000000000PNG  IHDR sRGBgAMA a pHYsod#IDATx^oLg۾خ6ͩʞ +6Rϗna 4YusK,F!%IJiWHA4HX+ѴI): EOtU)tO{g챱L~4J晙 晼h4J?(!!BC @7N7ou먧GZ + "xg(;Y&Xyo`FHGaQHڐ! QU!NdsVO?si']+sVsV݇V:ֈ9Ĝ|gM@"q'RX^(ͨ٪F1υ4J;DI.rlM!<{iAPclnJ]‹O;,O"{~>hcd#& РY "A+ىڥM "moYi)'DNMuQ.]{<* Iۇ$@`*P+%ו)4T`+sK+$], P´䬧)%g,Ӿn؋}c^ 4FJyH6. $S/?%\RI5omvȋG9H0Rv镧FT5Ԙɝ w&([֬W#by=~z]~wȡVleбֻlݮ|&>>dw~ ?J1SwM/aP/ K[RT$rBHMb!XT量"byåDG4J?b4aeSֱֻ= )ӈkXlR[}&f͉#$w( u%VT-E!%).CEW ZL:кU.^vѕrl,wRS~&m< {zF5طK/Ih^IHCFi_YHjl8 vG~*v]VJt^2uDsiijj^oQ /`o eO`yl.\/L'vo/%ћ>q9Gnuk(L.y{mOֽ B"5H;QI @ӼMS}.LRvf6}E`C9ec߻Sjh mh4zx&B!O xܳ/P1uV%1֞{3G<50k$o ӾWV_ 4o`8"/2kXؚ]&\JZgy}6m BQo;nGռڡ:;}dr$ܘT6 Qt?NSźRVT)xQF{Y`&B,:-$J5m'0z"lYxO]J!4N|R aӎHӣ}b YkoŊ*2V5gnʞU&dy;{2{6;G9r4ia1b62y2ioΟ4U^-vVvOTTVtbɿZ a`#:DkL$rɁT*(4Jڏln>on2Ҋ n;R0Ut)~/biRUY3$6:ؿӝO{fOkXZ]젫#DL;ogӛs颲9j%UI{HcXRzuۘXko8_uU;D߰bޙkϿ!IFGMba9tg 7x+]yExkAS+]6fGc0)YObX ;6/캺GjT0VYӱ^G~ꙶ>>VWyFZӱԱֻ}alrP#a:[}-h4*f9u*P#oaڹ6IƈoaR ã?@AEw?UYQYwZZ[#,bsV5 < mHI}_I2J 帠eAd@z- A/˖=ezٲ l B[rQʟf2mHzKgSz V+n!}3,Q1.OG⤴9;ܣ;ni851)&_M՗rn9$)OI=VYb6w;c_5}QΦNܜw~-$L?*U TQ'cu6$ȪObQ=c!.kMʌNœm{n3]l\V͞5srFH 0({ߢ!* ]H_ J%z~> W#N.^0TDR0L?^wҧq8l.LDxb1%)Xio'Bj[l3O\мH8'{{#g70K/4ϵa]DaK*U3>tO$sNœy gy=ǹ[D1-eI6OQ[ @lnsyHSɞ k)?Ds&4NTn (_҅,i=-r'J=k#O;lJS- TO4=BT/myùvRzv}s!dZԃ Q~998RISZ0KB;|oWgԀrilDH%ds;ؖP6XtHu"};}].k4N|ө~[E9ݲS/Y rd ?VٞV I턻w^CRҐ걝WjɰiGQejK]6~ҝAk_頞 jl65Kʰ[2ҖQ~*=}ISWlIMW] {gb13ߣ}.q׿-]RoHNBR:4lX]4w*;"Ϳy1ibIu>ULWn(-{;ݻ=vYSzљ]O=VW_ImVNayv/9޼f2eEvC!ĝ~l#%c9>S~v5sk.k􏜡6(lV2l_emG>{m|(Q!靄uꮯmuDY?]¸).'*;oKڥ<'I) w9^|g|u}!&׻KIWu vCMq4VǂOd_G; ޘ£T~NK`خx۞ɃMӣ"q<^ܿ'yF=rx4ךހD+M؀ĥw.۩ &*(0Nh׎%7 1Ԇd=3*R&nw6SĄ;4Fago#1؋xHu渗M5sVn%֍'m у=>)pR9Myea+WhwSZ+myy-OS;?2Qdf/ns|sWu2GFWT)#' {ו/ػl[%Ln>>r`2n{qjZgU|$g}eW%< ۜJ~ԍxwW.3ӚkϷզ"yReJ HӶ,R BoyYQi=e&vE8ӲQ4o7/ϳS]bT\G^͐f`sjJjDzƉ[b:kFt]fӓ)CHƌwxcy<GG>ugVk˹k6޺keRwħ, ޱKRŪS[Y 2..7IH(*Y$( W땺|ɠE[w,;VEU6IrtbO?H378-Dn@JƷEEQ1CU}9#dџ~ ӢOX)1sVsV݇V:ֈ9Ĝ|g2`Ʒ(S C!!ByT{)#!l OG:]o/DH^ÃQX`m=ACNoI{eN9 IHieڒ2}{GB'Yk4 f+H&x$.ޱf%f5-us HJIX!pR#>UBrsj#kf4mJ.w 9YQF7!BZlvqQ.,Re;LO]bx$i[iY=_mZx"]m%&ʰ]FmD0k+ę#ng{ufrrҡf" ǾvY @"$=DHz<'5)de(?sb OX)ǭt7/Ŝ7'tޮC C^#0Q:5ŲR+e 5cZ':ʖ|u5r&,ى)S7=FQ?iK(֨^'H^rI $zYXr!5:YBcnQdXhQ\P,=Mv\|mzYQVaqM>g+Uj- ?k멯]So H~aT]3*X89@]Op'Zë́?EiމG47vcAi-:^+Ja2GZx¿Tc$rQT$r(~B964S[ UUAyU2&Q7F8D23Bz83%YgJh歟1ӾC'n字]t\\Lz2`U.i[*>Ŭ{qi-kj~ f)t6xY.1PG=g;|OV:a8~ZJps'5 b!ʬ{mK~퉪F{L4z-X*k]]͎n9wyzE<=#ICz͵$BAσoJ[(͕vt -th{GJ4|B#'JVL.gD?^N"Jqdn߿8IVL<#7u0@'Z|ҼdНws;EhvZ &Ëg3k:^Xv9/uQ 6XA4u_${2U쐚䇸xq^@/aBT/.oӾWV_ 4o`8wz2aˡ+{Fwٔ%.ȞԘ ي E[O,:[8p'jeLhuR9ͅUn-mPzRrRI%!ŋxE3Dj@\Q/s:&D,"mHGMg 0JjuӨة(i5IKxJx VW 4SP;9mSTMjɰiGQ>ʈ}_?JNH?拻w^CLA['ύ0pdҫd{ߣhuiCW_Mp'Zi>:I;JS <}}oBNd{ڌ/tP5zhgsSC3hFH67^ ;&.P;/-FaJ(~eEMiljXM@bqv(\l.6zj`w{ֳF3z~6*8BFO%ǛL /r3PpN+5-,ZKS}7h;ȴmnf2ptӫ<(kKTS<%7хӳ@js֎mH%׻ JV Hro]<,g_=}9 HwEƩЦ9,h)iGrt5Rs>xj}ã_pѽIM1`"yVz &*(04h׎L6 7yjqs}o:ãVX" }fLq%[^6\t#ƒѫ+-CyL&9Q& -⛭ˮ{ѱ&OCo]IV+KvmHQ^z9N%O4yGIgg4Eϒ)a*I7ԼW%/)z76rdzS1R(GN/&/* ?4IJxBo|3DD9|1R@R+I;hd$*/Xv 'cCM@EQ1 91g'VzG|RY~b wJ*3(@mHz!! @mHz!!BC &>9IENDB`numpy-groupies-0.10.2/diagrams/aggregate_dims.png000066400000000000000000001301351450707334100220430ustar00rootroot00000000000000PNG  IHDR6W sBIT|d pHYsaa?itEXtSoftwarewww.inkscape.org< IDATxy\Uϯӝ}%+{@ҕDP@H#.(<8288:>.(:&%$! ! ~V]]}+]շWU[{=ǀvg662#"""T`Vdv$FaTDTgF`ko'"""R!0^ R ㉈Hq w,fv,%.]EHt:-""""""hTDĤ"ZDDDDDD$|H_`t""dNC*EDDD %Hy -""""""z+`BiT_,H: ) }YV}8v/ǘ 6἞t:""{TDq"p<:#1Dx ({-"""Zc8buZn=Ig6UT-–3K~*Vm1q[q\UqB"PdER-@vh. q⼜;oA9mcӁa9GGc vh9z-@t}3Tl %;Q`cN'`fʙIjM"\hlXވ+Vڿ<ԃ""Gztttf!W vvA4+p~83P ÜYoG0F(8Oliyx8wJJegzS.~`y~$'1FG7&|TWqC0̸/3WǴ?\q֖,?ILOф ES ]U \L[Kߵp-_t^DD$rYkw6^WfD7!{m-cXٛ߇b80V""2bRU|{h=ۭD9@]%Wu$h22ℛvw/}R]7Ku݆kw![sxj%""2^(z ً.ZeWee/ȥZk႟s~hc;Nv.nB:{Ruu7|<{w TWCE}w g΍E³BO3F!y}J=!摄2G[0cP#;{KfM6', ql sʑsft80RAѣLs({=%g[}D`aa&, ,²: \'0\̈́W4t = 7? \DN(Fr<"}p<c7º,  p:>eOx!yHtS !B%8 p`0aS;_l#|1y0}s u֛"HXj$. xeg+r52qDup{l }:^B$4`lqjt| a inusZ턞b<nm_c8繒(""hg éCV ÑO 亂+mx2j~Hpu5hBk?hqa %v.""4;/c"<\ li͙H#bPwdD{btbxgU/E֧ …}ow;oi|ycxEE_zP0l}# -!%.""_n4` &\wќ$aBV"=aFE݇qK(&7uZ[i5Gw9B؎a躜^ߞXDf aXRBO88p{8ڿ.]M ZHFnr1voe]7п]3q0磄n0gx|{gNmBx)殲s/-pT^5 |KyMq.3Lo{~3r.WΊ{P)GY3V܃r1^DwUtEtr)'=ὝQQ "pg sq#vUVV=}q?by;.""H&JSJmEDwbHS"Z>ŤSRI' """"""_IEHL*EDDDDDDbR-""""""hTDĤ"ZDDDDDD$ޕ6N'HYlK: ) ѕ. ~t"""R*EDDDa {jn 9l L˕+\}0q;xG̐h~$C{/f6 }{Db+gտ]ogfwSb"""""""1'Z}'Ǯ=X~uQ5-Gr嫸叭叭卛/vÙ=qO%""">,m fQ`%FgG Ny(I{S]|qŮbt1~HEtvpo6.-EDDDDDLt< |xp*p>p0eڨM,; >ѹaq^NMDDDDD"'I ѦLGDDDDD'K  dP'%"""""RTDaica c9`pPXr""""""HEtv' FgKvG8""""""JEt6O6:;NIDDDDDibwSѐ^EDDDDDDPݗ-Z1I5iش $DG5:ƿR^&$s|VZ ưƉ:w1q[qrjs2,Ϲ#Iɥj G \|8S1_൥HM6w7ܼr.W[؊[؊[޸bW1ޱs04g*w_793; ixgK:  ,ά 3WVf1b_L&+ʥ ӱ'i4nmlx8b7$gƮbVK f%M߭3q{o#;6SO:D6DKaƠZcPm*F+}%&Vq߸)V+?$DD*HsUDU&W"""""""1't""Q]a&H"""""""1IEHL*EDDDDDDbR-""""""hTDĤuEDDDqpMyTj"ZDDDt""dpw 4[DDDDDD$&"""""""1i8Hk }nᵣXq~![qq;ƙpLC""""%7a1v4e_6 M'*Wl-orVVh~"h ):?& n +θgN:?9D΅8` 20wQ7Yh""#*EDDDJ糃ѧ WU+N`^> \\z""S-"""R" r h۝6E/OK -)!DXq p4`p|k|bIHI)IYg """"%0ۘ~ 8e0 58DDT]aԟ M:7Мt""!3yoٖPR""Rr*+ONB*d`cIsMHYi8Hil~8n-"""Rw9\\=Rpx'sH?>h6/MӼ¤sxw>ds '""%>3fqP5+Ku[_`hݨѬ8-i27Nb37.pmEDw2:dR`$ܳ *ؙt""3* #u P"uk3E;nEyv=G1q[q7Mq㊈ d yPNњ}qvR c8秋)\q[q[qchD1^R؊[޸匭叭{;3ǝێ4} ,SrMDDDDDDz_EDϦqõ5o[H'.x/p@fO ~> {f1|wn4?go.➧gRlS4̰kpo]G=0xG ~! w*v"u~? m ^36-}T.tbt 0|c# OoKzxhUGΗaQ*o`m@Sv_*؟t""ۿUDg9~ǯ1R vy3 87ecq'pA\ L[ٶ,]>T6:}*L,] iR!ävYDOߒ0O> Hv]-V`l.%'8}mwnDD3 fwD+3Mޜt>"""RzZD>I 6Xfr9O@ǧdH;o6-^_D_2qS yߝLOc)6\DDD *I~S_w5T8ܗ~=Unﴈp<`^ff̌<!"""ltG]J>ֹ\YDӵ);w\ {q."DZwotdpdIHPq|mo9qnd|LˀpSMT,3 R̆D{&``P v{[xh{SZ3ͬS\ lpL0^w=E9OTص[pݝhssi$ }avWlmÇ7uw ZArHŽ!L& gQ1{pTc[lo6 2ǀqGkڀs=f<0f2) ̞s3F`cw*EDDT Lr+͝>}B5lL>cGqnP3g嶭c+O/ꍉ ;6¤[ٟz =2`1pwvTv48Pyr!Qf`5Q hf*` yszEDDDsSK?nT=GX03m/gHXy~ Zo) >`s7cd~VGwSXya7dhmH31R3 /%(DDJ_3݄#Z}i~3DRx<4̶iB1w7W`ή0"x X}f&,q7)}gEyL """"Qc!.sg&p1 >GC4joTM[: 8aokd6l_p؇>[oC.kȋ'0lS_1=m-7,2n9c+ny3c94K¤d 9v<&a^w_\E:gXl"Rbi꫁McFL: π+:l[ ǀ0OsKö]ɕU+;+CçŻh{l9e2rjr:>X BY1 D;Sp)ϝ+Pb˖p(S2dV.Ws` d4@Qɍ4l쾙T3 OH:컐0CiN 3;Ph/wf6hw4ODDMEMNֹ{+,Y0Ү68H_8iM{bsMi $F>t"M'̟r@ED]rvnoD?]ϟv,fc4!5T;y̬P@C;R&E#By%9cG4$-w20Mv{_SZbaqbQI!"UTO=ff̈́ٹ;N V"ԟ +Ь3kw:dsNhݴ(lX5=tyWYuծ٠#.< j+a·5AEt幄 Ho؜to}VUyi'"}P{znlaef687ك IDAT| Gwxc:)TQm!"<|_NEDPD-c/̎ ND~Äۻr/p}# y1azf|w9yeF^:)_=ч:҈ؤSgTDH}m",aum L&6; ٵ3; >2ꭂ0zeIS?<) O(p]# y4B߀dz%o2'1윱"2@NXg Lh`rmhZ~I,4>c; -=D!F2֓ ݜND""+~;V#7rN^~wT`j`uo"1הoWn\+_5Ґw#,{5>\y{W;}o91>\"HE)Q13EkH1"疍5xgDE~&p+piWm9Öe-/Ѻi&o_wmgf }3|NJYxikFRLXжi%d?PD """".M4WUokWp2Wע/Kl(3 +f)㥩 )̸mζcMh~%#"2+E?""""2C {V_{UGMn(pͳ[l8ES-"""s3()PNJAÞ'unK{pËlu1-!|Ow4ng.&f⥩?(bbF0RDjtLDD$I;N@zj4`㫁N DYaSjײ _Zƶ~V[g ̺ỘFŨ&VG?oݟL: .F:c6vi/# W>6m;[[|}fO[p00Xoy5lBўn(p$БU̒4vȾ̈́{4hm4[DDDv`QBI= ahxvr;&Pvww,~бQx/5o秛զ7?7ҠYD *EDDD4=ϗh pk# uifkSx9=t.IH"դc'鄖bWirGDTDH5wQE ;8~7Yg[gS h]ǘ3w;>#c+/Ms~4 6i5ϡ \ض}ow\)z3pUP-4D!鄉MeeHÚ;lisk|7v(x)ch;'W3)pގpC# 8ND*h铪isi&żvf1GUtoƮgG'Ԑi;ڰhiǀTo&i !gjd~[D78ݜMu{,MdWWS;ݝixSf5/ ntjTWLÿuΚ#F8o>k" "ZDDD4Ӫ'w;S n6nN=@wRlZa`t_~ciYkN`WX-66 oi4ckOC 4}P>q7vsZUxaS_|dϓy/?WZq""*EDD/۲{?XFlNggpjy E'R?S5=_K4ÜuDE߹(zi{  k)N3GxvzF7| RQߞƌFwsރ4 ߹۽iejhd¬"O>jAUc)ؖe1-T0#>W/HS!3  NSuW߷ti6BRê^r%5S>%;"uMl2-Z崉mP w)1"""ҧy43;/Qy/L§6ߧ_Ut<=˾d} 0.:r,4k`ynƯ-g˷WiʷvY|oUD"""D@ H28\SE=o{r&@[Ŧ.ÔO3]Vw'R-"""wT53ܾc"pl/ { O6Îw8_'rQI3Օ[?Zƛ_e}So>CvwZJLD""",4k3Xhw6ؽ3tiŤ618U8 X//p/I"{G]DDD%'tX.<+kbqs=cH(~h'o uHMbIOe*E*zEDD_r+gsZjoz[kGP}Fܔ6łO/,dz1IFi"/v =wfL>v}Xn0+C{cs WpӲGz昏Y]$c9cnwǶwq*3hqzbIhqϫiOG/FP= `.dHSx9-sݿÈ뀣hY~cL !Qng=5Z֩MĘ:4Iq5խ?^bw풌Y5zPS |фh8I[5!4&Š)KӰv7hN^=&l?0xұw@էBN4fۧH퉶="~q9@'lfwS!"'ZDDDFyxKw Y}ۢn-ۻ}PNOs+fa# J8TDt`fc '!*EDDDD"f6p 0*tDR-""""̪ #"}hhfNVsOETf4pz҉H%DDDD,. h)h+ǤNJ:  7& $oI'!""RV*Ec' 'd.iD3 W`ߒtmfUI""=`""""2Pd71ia,tDDm?@"""RzN@dݗ$Mn>^dWD?+z= `fCCd 0%sC[v}ؚ 4l&*3Lq4U `l}ӇmSYY-1z."h~hW?~qw.%,8%KMGDzhV7?&f#"NEH_fp[$aI!"RApI: 7o䩈  2'-w25*8 y>5%""""q6w_t2"N`NJ:~`WtE=Xe-"""" p?t"".g*4bzl%\լL2Uq3: w{EDRH0lଋڍ wލjm0Xm!6UΛ~Kxx5N'g1؅þA$ x182zg_d?6jc QqL1$YG4EqN&Xk19kχH9ymauWta\Ww('f%8s4e,qwY#R!z6;eǘqƙe-O8C|>ٖUL֥E3`) 9&*_4h]uhFDk="8Sp̈́y0AFCEDDDR4wZbg ജ; ^B(rq6c,fFkC! c% ~<} 7azzwq E(vxgcyY֣r7l,`,ar?I? 8O邙 |xфk?"0u0P8NеsCg0^#r)9 y>P,KY˳lsx४r)1#SlE_ym ?N׏H*EOxâfخ>+{)f +3l()`THT"0q'wt^Ωp6t""""R^fV \ LN8J1F0JXu`Wr_G{=""""R"fNVK)4byky?ɳVN:  Ry:DvӄeOEDb)Y54DDDD ""L*DDDDDzJ*`1dDR-""""&RU'mtfv~fw$-""ZDDDD4w$2zW%.poI* 3R-""""EpI* 3|T"R*EDDDd ݗ$谤R-"""mak*d;۝*3$]q4U ’TwlCADzh-^t""һTDW18̧,9DDDDlρ ɦ""IP]Ye>O)9DDDDzܽLVT""""""] |J:IhZ{Eim y^D}Ƥ68aCM 69,nt֗4?+Q.""""I&]*%k#ZOGGtZKp'റ9$Zw DDSG90`T 3__^O6K̪!CEt _[ u'=m < """"E3+[%tgS`!<4GhDDDD'3;̞~ t>"?'c mSWu:  W("""R \p*+SC&CCDz@=} IDAT^}HXђ$Ii' , M᮹CH#ϯlp=T?ђ$I).~c!7Ժ=H[b]D-:hy9H/hI$ w+clG9n+cZ#Ig-Ij-E1 G-n..rW=4p,%I4l \c|QXK \W<$ sgf&7RSesr8ђ$ICЪXKxwhd'RAUE¬*M&-U hҖT8sUgaYTj-땋Ƴ$IhvC~\Sej ̑? sO0jr꫱Ld+06 |@˳Cb-I$=k,>y0;s/¨s~|@NRYDK$I@4RH+n$ "ђ$I&vy߾ziLwhe`24YDK$iDj4 JM+JM f-I'G×i+5{~ILҠg-I#G灓+ЫVe@&ih$IҰ#p 6 |@ILҐc-Ia+G~2:hyzR4DYDK$iɑ^Ғۿ"-`@4YDK$iȑ|0B[HU5yI>,%I4ȏ Y @˰R* BřS; !m1WRa<cݎsBcH q^;N~T-+iw(`'ԅk |."M \#?FZ],_hL2{zx%Y庾$ih)4F1^HuB :40'~yq~np8fjΎm1>-~`iU1!ci<`:0 !,1>{dbq`QBD<iͩUD ɑxgv|9&ݏygȟQ-`9ޖ=]I3x铌75!ڥrǀhLn%$IZ)[bB@*kH ´%1ƒFw!@ŵw6t;$iy3iI!VR )si0sT5/*>T/1.>4H+ -{^s_ \ xcDz͑U_ȟI*7gh7k75"5Io-9羇$Ij&B8 5Ƹ[H@pIqw ݮ;x!n-iˬRIӫWca6iVY_{SeM xjGE"ZR<# FI P 89;b]|$G~|-в:G@ˊZŖ$I!DmΤ1 ![|9['Ƹ2R=ʼ"M#_[.ZO\k5nhuqTBJ܇~?Һx:ݓǣsVhG|3p=4u`aq7UI&֒h˂ QcmG:*]nx|\tT㡗6IkN2to@JWw6hY# U xjK҈} |2iR%[d( !L%/6fҭCO$IAmn unmbR~.U,x(֊-3!}6i+7q"IY` iA :3Fi{ͤB'K&Q8Cq)kFྞeʞz9jQ8'G~lhY@٪/ϑGI*'dwDLf⧼ bVKb[>$-Y-“1F%(IRl9?-{P6RbaF\Q يF*<;[ɤ=ʪL84Ɛ>l+)˵m!Lf590̹Q!>c|,~t?U -Ip{nzRVG53Zr|"))'ϑ pcU9Ӏ>@K;Hz!G)/>{{wב62=4.IJa_ҽcB/Ƹ6k7FqC1M%cd$҇Ǔ>)A)1\ai)l<c\]ArLo %IJb!GCBϓܗ4`e]@˟s|T0f7e/F#JGw\༬KOc*r[#Q򯗜8EtDӸ4M?Մ㦍m}hm}T[߸g.]TBC XI=4E!{bkk"n!;? x]=N!T@΍ˮɅn1vF#N$ ;1ل9N"}(=TЇGn*caGr$I /%Iqy'fOt;ڽ~y*9{˭ [d1q{+I6[4ȄZx|B}Q߽NA6d !~$I "zdYIh1.J+:{k@ !Afc%D}:%IRYD ZntT*Ƹ9A4GTa'\G&mI4|xO$i0(ʜ֭ݠGa-I҈H$i0xg~!jV4(g*oľп$IR?XDK'ͣ1W{Ź{ QcKqI"Zp1B3p4iu҅hDnU)*toI4YDKJ+peV1&SIV/6>U0ݐ\OIʕ.\E6.IvE$i@d9MϾ*+Y|qssb*15ն$IÃsK$IT%hI$Id-I$IR,%I$IE$I$IUiuI!]<IFjĒ$ITOEٗ$I$I*tnI$Id-I$IR,%I$IR&7: IF1ƶj\Tc\$$IRզ{4: IF*sK$IT%hI$I>ђ$i^ht$ Sc ,%IZc4: Iǜ-I$IR,%I$IE$I$IU$I$J.,&I4XmUXIR]uYWE$Iv[NBFyѕ8[$I*YDK$IT%hI$I=ђ$id 4g\5YXnM;L`oKk~}D -m_3q[[wNNjBnb}@$5NH`Cc74*a!0({k"+ v"1ma񌧕ޮ\cSU!\WeFUǭgl7n=c۷?\W[ !QhI4EزIg#$ ~ђ$id4$ICE$I#U`"0 Vf`$ XHGIQNt`#=[FXz>Vokߋ_㶯kumpf"m= :lW$iI i `49kIE8sIyi_w&]`m^nvlY%SتTiF7gyݕ-^&IFVZ?SE1979uUv_{_cןP{<.d!}-z{ӲI`pETOǧԤsXiͤ{!-$wHSI^$I>o],2Hf|Qarq 5')R57ާ+#l[4furI$IҀo]_U6f.ӦnCrzrWG^ Wrl12I$Io],2wPV/> eZuGk]DR}F'=-$I$i~XMZumuX=ݙsOX-ޏ=m4:0pencO7\X'nWp}k$I$ M7 E`wR7؅+#XTxgFjU>GH Ƒ ;biD4 7gkk}qI$IR tdq\` Q`9i:rim 쫣$GI]¶ =HdoR] Xy>EaR~"[x ؿzȳ% }b5S$ITVm#UX=+"`QgJGn|+.}u%I_DV~*Ӹx|`O#vB/+mX|o˵g_2Ќ[ƭl7Ė$I@-F]V_Dڊ)we[< E}J I$IZI< mQ3[喁HH$I4buդ7:I$IS4En!-Iٛ^4 w/q۸[ƭl-! {0h#I^&Zcz‘9L6oU~d4$iG <%I$I(hI$Id-I$IR\XL$iׯ$ 5$I"OO48 IR sK$IT%hI$Id-I$IR,%I$IE$I$IU$I$Jђ$I$U}{ \=l!\㵳|]$IHڱ~'+s"ܾ|yIjh\j:wBd}a"w?̾ooD913$i$98}$E!Wm?u_$IU݋<8X,~Z,Q?{fW"/i?b "xEv.Z uk{4J`5--Dk(Db.$-I$Iꛁ̹O"_A? }4 xY!,.>ЭW"?vEt.-D]@!r}Z,a qfz6ncqǞɇ؍T{# ~?=\0KNi`,8\9{b.N>|ِ -p rb*"_<[ہIi%m_B-ȝ}h_ !Q孝ݯ/ǸC3n=c[߸cGЍcn*,x.2*IR"é{r=wsEt_(r%m |T^ K*|t*m\(q\;%m_\܇r h-I$IRC&FrCI_wc#YNW"w8B1BdHZ؇$i^L̸@ Ⱥ'I)sQ!-5[54"`/#-0VB5t|Z  El[TKc~H L\wITri-x1p%pM =-Ir {e /Ī iFl>ҏgk$IC/HL+D"SCo$I7#_^JZoKϑ>}߅HLB:O/'OWH+|>)E{Z8c^$ %= |O؁JTKQD/+- ! &xC)Df1%q&FҔg֑ժֆޛH.kҽgiXBk 4u=4_=Hw"O>q>Ҫ^G-Xf"ϗ+D_'чWҔqI4H;yC0 IjD"]lݺtHEh.pLOiUܗ=9*ܹ\0=;}bo/D6"?~OZ{\#s{Y$ #n!r LMTC1o. z/ g] "~n=7 [Kp^.pQ!2M>|&xX DZyҖY5 ~z4-)DNU$i 8+ q,\ JTSu_4UQRѾpiˁ[תcfތ+ [Fswح8H4} ҇gwBdI5yW"Y@ZVg H%IR${Vi-I^dB\`,9nO}< 8ݟKjC">X| jP$IƘq2Hg@Bd};r]Գ$IC-Qߓ ,ɞƥ%I-t щH$H>|x{r$ؠ*ծ?+D$IR 1e)@i\~/I#ޠ* 5{Xўiqxz6ncqĸAe>T5K ۱c6"W.02>z Zԭ1GB8PqIz6ncq;\[!2[I[0-1>Ї܎$ Q1xCkh@kizVK !QzlPDkpRsU*wĭgl?qޱ%IV}%I$I.$I $i[DK$IT%hI$Id-I$IR,%I$IE$I$IU$I$Jђ$I$U"Z$I*YDK$ITQN@$I#G,<ѹ hY$$U"Z$Iu#? 3|IHE$I&G~p0 >]AE4XDK$rG>t&NˑUѩHE$Ij&G>o> tF'":ђ$IW,צ}Kw8О?OqS(4Oß:돃!^1zw{η X@KCE$I%GŤűN-צk]G׆{w~#/cCx!xg/C|~4OKW[<[mCIE$IvHQs˵m]q]o>r++ ! \\KO?1v6:^ޛYȨ]ҕ"z >"p}!^^e[&}:jq|iߙ|ˤ+G=oBOmbGdk4͋Nŕ&TT+xMoޝ>u0cu.iv;Yz,08 bUajA;:jqݡ]٤!/G~6iwRlzSoRLc{1|9Ƹn0ě+04YDK$%?˵Otn͕>Lд &s!ޤ;1CxR߶]̸hO&6ZIE$Iz#?x?a`Zvm ׯT9k* !G*wдtKŧbOxrS0]*tӃ4\60ͻPf-Imȏ \*n_h{l%yǷ9|(oOW:$p0{Df_1i]ݸegcᅾ](iP$I9MH[]W/#O=oxN S׀}HxBG'χPb3sݿx: B߆7޳egMegK,%IDy]_Ȫ{5B_eξB8"齤=#p)^blm]5X֗$ 1ђ$I#Xirmbg\7xr٥Y㥤~TieزzI#?؇)F3,"V}w)#RICE$I# irS!ҺO? {TX(uK\BN[-{.ip5f-ȟAJ[ZJF,%I1^@W؅\m_hg7t^9ٺPrڟn}t[%dqٺYx9Ǔ~i/O%Z؁k% Cђ$I587f`;»k|7j/9K!eF&}P/1%fM}լYwcz'hv4YDK$ 9M[H+US]׆eaL;_h\@8B{I+[/"xf} f\'ie-!;-]54XDKA.ʆ4_= p3"2-q5vTtw}6H| :ZWn=ʹoy彴l,@;0k͵{8NǞ[s++6T?r'|X6]:oWh`MB'ӿ,9]ԁ7j1VGϑohM"Z$ Jmlz)4ߟ…c8Mw:B8W~(0("to^wNk !C:s;`ߋ4Ҝ2.nmOSO)1]Efy<tQ=ʑK*({ umdTi|W l*.p(it$i[n<&bޟ܏}veZbI#E$IJ!@̾8W^ @V#_c,;R;&v}2jJ[Eo/wV<2ջ@ˊ>],iD$I u0a/8,›!gν{۳7+i/>/ᮝ`ThNJ㸏v2]J~~sqwOnj |x-;ksw7N!$->R/4f\'.?1{U"zZWv2e5ƅ^Ue[χ6w$P{/|7VuRgϾ=n@ Ĩ7/C598q]Aj -k޶NM|5Um=kXv|\MV>z@b=­p>\B*3 [= cV].p i'Ο<vۤ ;g^ ΅0{ @祦m6cYs321J[Ib,%IҐDη)y-t^g*;9~#}jg64ޛVj1r^W;sIg-Iv]r`V$no溻*#'x `{c] }[vz^L5B;+>+8]U//_cBr'I!)Cް#ůVhp;@]/N-gv&UUA|u;ų_|#+zd 6}?G%IҐhLLCr޼5kvbhxd Iۿ 6s(KqB⫏oWqSMn%D2T\_㮍e9vm M\GMc̱i qyW똣7fƛصsI_IfBS]bKsI e-I{]9 F/!0/@'pbo_q>0\(w;@;M5Q't1 cZ#/@w}K-^C&}@·fo2bQ{wU}qsfDD dVںRc.VEZSu!T-j]&6 dLywpdL&zߜ{fr[s|'/3+..vt2"""mqa ~L2^p}{ lp[9Lgz3fUMsl$0 )w|rkGe4|9;yK3p&a{9)""""%Eݳ+kG1_ #1b/Wm\?4FzoWl1#cK.f9R8W 8sS@þN|tf6ph5VZզh~r{z .̎&;:TDd1Q4 IDwQ-""""^Ϡ`E*EDDDW3Amh]oihbfv=AE6iV`>"""݌^n7 Ͷ316߇7v^Rܮq3CDN+vB"*HG_Ǥ9v["R4{]MHOV6>1e"ݜh鉖qKSwe5TE6;^hp9kwO;3;!0عHP݋0y*Zzw__dhj"""LEtk|ݗ۸l a||sИh*d h3nQ-ңNH7Yسָ{~G<=O~s'n؝xv. 7% *EDDD`c06f+}v{b7+W  \+n:"ҙԝ[DDDD$?Tw_ ZѝhwD"iJdNnqb'#"]h::w_[dDkR-""""]#p pSdDkS-""""U_,v2"=jO/v"ҽhvnUl]عH"ZDDDDz3cf/9%ԝ[DDDDz43.v/b*n4PZDd;NXfv 1*EDDD1l6p0Ǹ1^Wf;^fv;POq eY>]Hϣ"ZDu_؉Dj fu@[ _,']8~Op/Nuz؉|{"8"~sHg #JaYÍJ7/:yO2{ [Ɛ缘+10Xr_\2Y//q6L2JV,!6aYf5vMNͰz>]`vVWʸYe |^4gf J.-Ȉ^]wy-~|.{uÊ9β%Z當5‚ ]4Ef$yuB ;b0= '`2?C=Ȕ3aG:*Jh6Nrq]mP8c0/^MEt RX,'߭9(t8.y1rh*[w"ٓr^8,r `tau3n8[vgn=n~cVӗb2X̘caY7|-bq-!GvNƄggNn:sQ5j=nwl/t}f6 8عH"zJ 36})Ċf1+~d4>*`bp﫹㞺[ql'x3szf2|lTƗnf*qWq0b }@Ӵؽc_'Ex5Lε& ɮg>mq Ct}Fw%[/~0k9l5&Vey@, ].UVֈLzv'}B,u> uΪwwewݮ2>VF18x0ʼ7-ECF h~@x,VtDE40MؿR'DPNfľ\#2=s7iarOyz &/0c|z>Lnyxz ME=NDٹrqtəLV ;j<'ja`l9:/iY`(5 3=Kan˜0Ѡ0Z8zN /VNxsIqc;CMGՇ 93E #,SZ "?nSֱ-,4XUsZ-ܸ}ND ζ} WE[i|}9oe?*x'r?'8jRDޔмYDOBDKD]6:gU} <9HxL᷵΂c* 1Y뜟V'ޙ]ep:kk8L6g =Y18>'i;wM 4óqa1) chÉRΣ X+@,)p7cQq8$hg?cbpKUD;-{<:֭y/Hu3#bpC歩w>8'bBg'ffNx}$HQ"`u:gT*|>(컘&ؑib"yߋQ"R"Z{۞'/&\`n;Wٿu^',0mʷι'lg5-\繖ۢᰕӳ=D9_J9=wCnј- Gۙ-6_-Jn:dŘZgm͢n9zHn%;"""L.۰.- ڴ%8o*;ATϸ۝[j e!ߚm`b,-"]9a}VD&lƴDmuf6;g`0WU=5I]p䈈t/3PcO Xavn!w?ݷkImef}|c1"\o0>F,Hq:JQLWπ_^MQ:c@b\dbj Zjwd LK 1c<'_IwpX;6Q~!7.=yd^WL_U$&UlOl_ѱG2rG,%ݽa!ffqeACsEWz`?5v2}m40GGߣq|vDCl2kn ly_q1G9[)Y>Ynh^QΆ3ācv2NhXQ&*T^H|8ePHqL9/үӾ +|[DwY@,${+h#}-S&|M'MH0yLMSv ?#l; fxߙ~ĭ~r"i k0> Grg~g )ft nJ}6/ޖW;v8Nݮh.7؞8#RI8[1G<&E͟+ i,Y,t8̎6]b "+sS.H~1uk 78}COU|a.;C9o57A)`8,7?0- ીa)jWBD\{` 4nV `VQT3ޠޣێWQݭy\#WvE[PkݽCl 3\ _Dze b/qR* if7*(_Ȋf*~fp)YO0}8N:# -0`o ps]~~ ~2$T1峆ĆLK?Lcnhl Xʉ}{^Ia+`ūjpn Y_k4yVS>aw}y`}: `\3I~ձ !\3xu\Ts; gudPmإW&**c ]Gh/8~Y=3_ i >ŌGHbt&A)jn|{@5G?Ŭj9~8Rn>J0(.8 έ'6/FWKt%H~,0*=k=5ߊ}ɰg8bjY-YǝhZ׷QsKB7&Hy4S.^ Zv|il Sld =@E߇I0<ƈ^=Ϸhzph )%rM,S˽Obr֏؀RҵY6Ef5s:ӛ 5R:"7XV ) l fT3et3:M >؟ 8@XT``W8<~\ 4?+=#giX`eN#a!< J3wF~:a/ L9lv1R̜DGK H)e4;ѰHGٲOccH4RG6u.MotymiS4m%ýg9v\ ;~=c8n<\3 +*^URrFa?ٰ/FL6|` |,5ULyx?=Ӱל/+ aof;u̸2~|S oz=D3#gc6p<nf)_CcQ㊷Py'U@.ńˊtYDo]^BwYm78"]YKe4H ɺfJoT\dSfu23)E98 9~Gͥ0ETиa |5؃)fl꒗`>`rU 1Ox,nk)ݿacyΥJbF 8̉ g_ C92 pFoy-eF}59 8>Vf}ڜ 9 9 &p>c' ; =V6Bntu=ŜTg!޽~sG; [6b_dm 瞕 ,;ҁf0 IJ'[)x4;Q?m9vI4@3B H;夽yo2Ly x[ ݿ:v0e34@D npp5Ƀ0d8/&h~$sʭ|z)χ6i~)s @00NK3t6- rKS{`6#92 ^JDuTDHQU߀' 2 !\K= no.7IfS{9!-*_喦*Nl7 %Ti>7}/oȑ kWK7ۊaT<5(oK6f<'xm9|q so%|&ح`fZ8vz 1xױۓt 9ZtDTDro\QvUcWXQnfúHq (]|҉y N׽=<;n|{q <^ˉbiZ$pq$G:NDٓnm.,۩40҆ض @3Nlhpo=5̮&sowZ}D˛GXnmm .ox~alY [l{,_XM037Ϟ7|)jE]p>*J(AXn9)V/p=k MIxıe'ZkpeHaVDou}d=͛>Z#hT\3|% 0=0<|Rz=a&v]wR̼yC=3#湈t.3|8Hw"ZDDDDz43OIDAT'4l"VK.KݹEDDD2SWI۝ ND6""""f6 YTcw+b"Aԝ[DDDDz 3\ {%EED:hl SKW2r.b"R *EDDD[[@YX\ \,҃*巾fMFs#ax:"y4;H̬xZѝhvY 8pҰ^ߤA筛P7vkb+na:Hgf^n]6mIJfv70!Pw63-PLl:ā;L)EDD<3;prEDҖ!y]&n!cӾaDDD.3L&;zTDf3h^ihlp!pP^c$H+D|<*EDDDE8@d9pps!!"KE6iV`>B A2|{m4m,Ep4CDDDDz p8H\^@|9HN{] 9pI#h?OQP\DpTDHOVGX!w_^Vb ^VVzCD""""3#gc6rN?#fv~'9CGc?Y`Ǐe#)C[-. EP-""" y4[koe> 3A2vЫjp5MhH^un?IQs2BEznWOvϭ aw;-9RTDro\Qvccg l-hN$>qEDD)A'L;nGb;jzpAtq*EDDDH,.Amz8/E:'3.TDH > ػ SDQ-""""=Z4`t^.~3IEH #hp5 tk*EDDDGI<6~\fU$&"=htff@_ ltENIDz=ˁ3 n.OQfg&ݙYЇwRѨNÀkftד^uk8 i&H8ak \IK3 ڼx8m3N:δ/Eh;پ|Znf]wD oj; 濝HOaf%!@voZx*ED3 }YQ3ew_#`.0`0V(ҫ(v""w[.}mUDH0u~t%ڶ̖r`'b$'=۰{!JLʒ ۋuI#W7{|(f|y#EMO/2a4M]UDHY;Fxvpx4o{c.J8;rw_"^ oEv)Їiw_ff;F Yvgk>/w큙bOdkۿF08F]P15W"St> m*7=g1ӃJsj1ś]3]ωƨ@sJ3˜;3򖻯T ީө0\ c|E٬F1֘٪q;E룓Y< `}{ wϔMc2#g-"j5c')j;d^4Y6?f5zw_m?¢)3=zl,77\=I03{j;uwr<“w5<Xh8Xط;0pB9-Gx7ewϾ[XJb9Llq2^t[qټzwpkr$HQ"mlT"zO;KEwxVcѶ Dݹ}ynj',-KxѬ3+!<>ϝd3Ixb{)>Q -""]5_MXm}[f t+/A3pt}o7G;{NHeǭl!Ǿm9w kW˱h{@thLѺf}Zٿ5:yov ǁd,"""=A|y1]Z}}QkY{U*EDҼ_C*;-YY+uDDDۙN79w{oܽR[NBztњ gCmw-_2 6[t?qkwl3g.hɮYaTD*;VΊ;-WY5u%]FWs} ""dC,cݣ]jG3E]sL8: \6U-""]._{.Y}Hh3=vfv ̈́kTґ&ˋ5 image/svg+xml group_idx a result group_idx a, axis=1 result group_idx a result group_idx a result group_idx a result form 1 form 2 form 3 form 4 form 5 note ndim(a) can be >2 and ndim(result) = ndim(a) numpy-groupies-0.10.2/diagrams/diagram.docx000066400000000000000000001162711450707334100206630ustar00rootroot00000000000000PK!)e[Content_Types].xml (Tn0?CSu^jdxdGprb"6_q1x*㗥˿ IJ{@q3aGK"ߤDPK!N _rels/.rels (JA a}7 "Hw"w̤ھ P^O֛;<aYՠ؛`GkxmPY[g Gΰino/<<1ⳆA$>"f3\ȾTI SWY ig@X6_]7~ fˉao.b*lIrj),l0%b 6iD_, |uZ^t٢yǯ;!Y,}{C/h>PK!4Gword/_rels/document.xml.rels (N0 HC;M7`Ch.+5K6IDֵGV~T7xΦl',+]mEHrRd*A(XLYIT?r#0v5ؐQA!i̸ze3Z)EٮOޡSKgSJ˽ʑvY)O(eg82~cDž'',]hSAa#Nc24!xdrgx85lL,ebSuJC#6o@NI;ݘ xFqPw?PK!{+Qbʆword/document.xml}ڸtyt6;_x7aߒ :tOIvƖT*J?J_b[XuO]FwTW3x$2A(;lp׃]Qs֝0$ԇUz\.PW-lS5Ok\0U=s}-G$( `d[)P~էI{8[p]&l:g2?ƚKˆ'];W39B;jFpx]6-._ JKełH&$3( 슠lAHF䤻SRY7 ~һHDz>)iWqеJ,[wHXP]18<܁" OVB$V]˅!.G{gt$T}urʁjB'0] )]q%8$XwQW,QQ+%)TNHO/s|rcSG6M*@ bY^.}}):v+фP)IypoJ$T}SC|hmC- ʩ' |pf Szu(~ءIn? }_\`&~˓yb]\Z9"s0 mxXH<]+z =?%,[+q;ǝwqp\JK@ JT9}RӾ85?g5y\F ׉K}G=K80+P:DN94HcMtGe]+5L i)+~]Aƞ$^1fD,̴eX3cm+SKk|^XY+ߤ~B?PkܬEڭ`S~ӝj 5x+`v(S(\03}QT^Qvԕg43Gx`FC0~у=K@6ot(8*@*t40qFU7z,yFau$Q2drw]XXpa L'x=53DɌX!/ys2 .p"zE LdՕ|~ y< 2~xM\Q<ɾeoa̳/`z2B;X@PEajXI3!0'y=u`M߇[ϘD09نif͒R:v*jJ> 'i O,Ͽ_ `TYLaw-]Rq֢05[T\L Fx"-rVKm#Iyc*_NFR&*D2.")_g-ظ0R3 iRL%Cz|6&42<24fI9G02PGs*.1!SU2T.[_>iVްtmiAzP?Be>X \#/3&ďsG#jx. w!g|5bEQHRN. QXtLSu-SXHٙZ*2pY"p"z#uw/:K*Ai<*q#Kk6߃5ePލ '~΅T>K QOuo65$$K#@,^ ʥR_(-}Zan 7J PĊ3?cRvYGk9Pi q.ZicMbHnZڌfkQMQu.U%i='wjΒFm s82ؕ]hּ4z mCvKuXMMY:Vvsl_:1JAߨ4|.ǢSy=a<:6Kc`-N˜]s][&7HY>'膥qiXRn6mi;U-U]bGfvMTcݖ|O>fz+c%+gVmNT qcefyi,iu"Մ1@S(櫭%巶b.6ɴkn E͑/Sy;"-2i"lzw,,>wơE 1{Q`"Y~PfLUi3ژ8= "vt·>Ho.oKelZ K֕P2ܦҖj7^g8U>3Yv.fKƇiX IUN^Lh2 ˒Tɘ 6W%77Bmb*t5'SfJe(fm- 6U0VZ2Y~k jd.ddV 岭fw"Pn.t6LF_rBmy{n1A9*f݆\O;|5ҝR-ܗUH<_5gzgDt;im'^;h; 5r9cn_zFb c8\%p<[sw֕M[ %Zk1rXw*9L^&r[zߖG嶮7 r1="54i4ͥکMcf%TՍwzM[}gWd++a+]эtUkK'+\pJAKvȴstQ ah5* (W7Qrm[uq$4bd%m2IOvGj/akmqh4rp\c˰2)88 cz]C"txL5m{Cva*Y-e";ɽR;Nؘv34sBYJ-a \-bEYZ']*Bn7 E=VDf4'օ 愚1%Us[R_ Wk锍M u͸I9/4<]3ry4>Lv$ui?Lj꼺#áCD 9yQ\%/ŐbΠ-)L q&LaM65;a'!+PA)$bl3\r4tՓ#*|Bcc`&E~#%AZ@7 >qIۖ<^} 8,|>BuBRiqOA=ϋ=˩ >E0ss3hC&c:^}Jކ8r@\n=LaC<橧S~ƓeDM_nnr~E4zL& ;hc5O-=Jb?E'сEb#0vM&@)lLmT+wc!øB&HqK\:#@|D,|Đ1d\̘16Ӿ5FRveG|7$b!ކx q݇A#/>e;r\r-,jV^FRD.A|ۇUnfS(V&=- أ7Ĥ3 p}P<qvlܨՠNB)ĵYXɔ4>2F\kI|2hHZu)+>.Dߪ[v55kRJE'G^2`,DSfqqY|,S*~\k9R7c#`&Zlx=0Qav|p(Ti.dӂ"c'8/E84ù292 _5k:.5\9'N̙D( z!K\G0_jdxp(v>UkW +tLpTg|azraƁ8iOI ][r|KgI4j,olaPXָW#q"CRxZ'Mctzz/ra WyͬZ:sLy*.fq g=68bzJiϔb ޣޒM;JWJB5ɚQMLZ̎4~gw> 96[}P5Ψy%AFF{. 6Щ.a:TIatk}v&m̋QW,=PJ΂1$Q1Lzc`/cJIҪF8c v'zsk15&ձZOG:0v#Fh 빊Q7; 7&Ukhq~p }NM'Ѡۜ5 'W%{`V@r#g1nl>/̒8H^oTiQR*TyhMsDȪe2iUn+h1]rArSn@)]w_Xoqdd/]֠t|iX=pԒ5&Kc Ay0ʉr] Bv vm>>TzM{.T[}\&H ؁N}Rܬ8 KakCxr@&~ G~:Bv NSt6F`bq3bqh= d^,!H+AQ.UX {K$ש}>闟J^ނxf/_65*z~8}@y uDꗊqC ƻegW,w.KWiրm+?kKě k7-`GH _F`QYGP:xk*| ZC1?4f xߋ%lʠh8 h^1c^(d]IՑ^?QP[oں?[L{xǷ_ ړ}}w|WXQ~Iza} ](޴P?=uħW0)7(XSyy+_^n__u)T//KH݀y (\_O5ӄ^o~*{P8a+E"J"3*8j@2U~z<^'UrXӈk@,,gKTXITG81|i*)\*bI*NCU n=TDoPU/~Ѩv%?0p+=|vO| Ǜ±`vA'P<&DXaD}*@4e&pMVySNx~AoD(4P] \SDPg"nw FCkZ%N d6޴k^K\:d }1Juk%|y`q8 ,X?+*VUh:VaƨV%Ak3fMv$Dkњuŝ3y͝zfS6)[hz]J!O]\9[S Uoy?%D͖y<.0=fØ6լQ$t(f fcy7ڵ:%~:X1lwƽ:\~nwhZRUvKo V-.Ԁa\fۣckY*AcR$@ۉ۶ Dz6pz-v;S36Ys H9a?bW|P#zFQS_Ę ʞtUP>VEq sV9=lf!8k AON)T5!r(2N] >#HQ>oҚۥlp I+Ӂ\E@hT֌_X "NZ"X*vb(fz&eR Ek~>= a'ЫN[iyW5Ď(*A[RFzǾoXYVL|]kle9}lVFjh1$ 4.:#-^6Рm|:# ؈4ჃlNV=V4P/INHPI3{+ mf#W(\V ߳$P:#˱;)wGK꾀T/?%x 2(FubԈU~ϬY˲|*WD;*:!%ZiN ә j/]c,:tsyၟw)ݤC;0Oj0/m5{w B}GЧ$t9 Bۋ!vތEg7 W C nKy?W;+eV8§῾mo8_(D?O0$NvaR auvOIZ~3є>nW3gE(, ^9#Y,$3e%y$_rz}I57Z+*Qh8ySduo;uDJt[N 74K3\ܞ^u3.`(.oc}d,/{LK?17h;<_' ѽ˫8%Z!W]߬*g赫 ؆4lwR( Vit.h<^AE]7^[E䉼~ybI,%/+Ε,!PhՃsm{nI+M=G=70Cuz.Gy\42<24 h,雏>s~C,xh2ιOU2T.[_VǘLpmR[M_Uw;WCfX%ڲY6G-%J(nSiKrm/3DVSǹ뇫~}f<\0jkև5)(gJE_阯\ٗ17EQTD*;겺ꮚ*1M|NasN %J +_¸\Û(8VTn!HDMUnq,[-CnccDmcc7[~!NKãuN-< ̉'6K©@2 HL(haxe 7/ zpwg+CMߋq`DSՖWڕGd(!դw: *d^ 2:.Nh) G :R.d[Necp6sJ@tw{ij;:iUY w'EgDm$`(_ LȨx~+U72f2rpCT# `6?'(a)M40wf_ A֚cy[i.Prbr1gb88 .^CXKZ)6>'31OlȻSi r9tNLߐ!6t)/[LϢ98Z`. وEuYl)(\7zCߧxN< [qˀIʗFcڎ]u:T>Y'k"MnZ,>h܀DE4@q'Z3ywxI75<{./Cw)E%O2bHԚ 2t^H*JHY\KA!f(iy'V82N;iΘQ_" ˅ 1?yg }n]i A=En=.!<@ۜk~)vb\IG~N&UGj,%B=@\76~YSF]}mbĊQ RXiEcF>UAsNߖ$_Ͳ'߈?&K<7Yhw$7geF/ Pq15 f}W1⯀zDAj9FJ$qv8LNmo–l}h 7XƧ_bV6$|p]uHcE;ȇ5Z?j C =cV`j\w3 ?#Og$jX1J=\3d#FݜoD]=3wƬh >9ylG!O@~GSCx0%pۖ\Pmjet9l_h_\f6rN>=킢ޅz]NLޝȪoaPL&u!\긣 4H_WfHNSG˼*Tc`UѸ:TKWf+,ՄřDE*CSUղ7B0Da7%ywq|<1HcbF$X,M rsZִdjq.ԑ+:ࢥTKdr 6&C PۡA5qiH8DHKp(<Wر4:;h8!wXZ ,l,L;\ulzYMxKGb|ʤQ6q.#tmʚz"3bO1WzQLxK' xq0R\j|؛RUul!uMQ0B+ϯ K=}އ :_^_e=+s\.z4SI"8QaǕ z98O6te\S0dr,z?Զ%? h~x#*\\9 8/7f 蠫CM 碗Hby] 䌾h> h.ru 8u|8A 1}Enc%КՌ ?H4bDxqQ@'SPO^[ad p!f@>M]FS-ݗ gp~HWMyVx`vm ÖUqS6 Mz` җkyz=.aYz Vnc%tX?|s?myEZhᵸ4o!ҿD?Qwy}sg^)|R'"{֕kWYl]P]3Lk @uǿ= l𽔘oθgMnd*]xCĠ`x RήGk]R?k+W%-Gv/^r,~`> ,k|v`8(65fw@7~47pnufK_a&W7O -eï`5;oૼ_54O7t͒PoJnCܐgW |1!}gK&_tjs_Zw.8KPF+A]Xὸ/؈AxMɆXwPϺ䑟fYJ~DfKV6ԯ7]LL ׿4`E+\7)w=zo$v { ov^ݶ{>7\r~@kWKq _X!V=#?bi ~ɽVΗGk>6򛲋Dqp1  +Q v1qH[c+k(jD.PWI V_*Ş;~`5 w/bo&:rLG֑\C j8Z}%Q#JƢݝ-8Q}(lϏJ4 ls$a`7/@l`0`V1ӏZ#k$nĨ p{VCG֑ud` *2e NlipcY]놸e=yͳ$0:i$g Ȧ 2қf%|@gQ)1IElSbbt3Q.w=1i1fǥlى8&gN7++8*)pJg4v=M̡_T-U k),~@ ;9(BJZ8A{&d`J.o=ԶYR&D?'_NEj닣٩Ns?OS<'.'/śN6f=HCd+g9<9TCq"R{Yp΍pp.b $FKǵ)n|_D֏,ي'caș, ]U%Oc;Q.H>K؝̚Γ\qsQX.qܨ|[au@IvUSuS%#-Wij] 1ܯӼ3{vTӊFpYW#^.j@aOS=zA9 &IO(eY iCε$DO et%mѳ|G4RèeJ@>~m<4݁oxn: @l8mo.>إb j0[(V-_٬r8BYH1_=+ âf{z M 8 ^ ;@bdq}d;':p)N3^HS4lRyBC6b٣K2 ]sIh ';ãjM.5S1fMzsEP1=*B@#pÈrM^vK/|XD3Rח:3,}&3d`$(t?bh*}Q7/h8Ѭ2W.`}|_4-5|gz²R l'Mv?zQeYbzF1>NYk jYJq6ģIB()3k:FiC<*w" p~ !{XkzհE~_{҉_ߦo@:; A|X!|Z)hltE[Y:S;rmm;g{Mp~DsIX ٠2Bͷ>XA+έ cY+5[}L~Mͺ^4+ 7-RU}'F=|'v&$P8P,H, tK'`i%8?ڣfׯ؇WS0a  ob͵7~8L ( _Oa0POQ~Jh$R?'N tj .oQCt" sMH ;z0v"EJz97M7Ɠt͘NқTL#;*RWCQY~1-G2U:F*68852= EM<S'⪜g=W$5gRIB](m!jvJ*ߐ@г~+d f&PDP=u/G}03\#?S{]͏Giۅbk~. GI@ SD4+yp49xǶuåQPas/Bٴ@u$e[7񹳀4 ow,7s/6"pN J{7ޅk#lul'V[Ɠu(|.= hZ_%Qk='͘Zdyry`={b"6u9(yJtI:6\5ǭE} 3<;O_b"2wU)V܄h:Auc36H 52tePU]#w5t Y|`~>jSA۵I;uy߹S?/. v]t>W<,m ͨ@{k]{vyŸhjځTK_aNWg 2 ] n进'yJ(}s94us~# VHZ3V*#fnۙO(KxtXF˽(N/CX/ p߇C^ % 5'ȶ@JUC 0u @ {r(q8U/[]7aNPAN%v-i^ ij.UJ˗!>[z1zaaĀ5oۼ^sVj:pAu <\|\ϥ4vϴz6zW=*PUзlm(hoRjҠ{N>;a Ot=GG?W 7խglz8nd-gM zgs#t]Mp[ur7Ǐ]^?vsqtԻDa_t3>ܔPgh<NoǶ?˧oɁGVw/.M0P?'-5?|P/f*>Ցi3s!.yM˶LB4t؟F\8>. qUN;W[9"@Ke3.N#{drOFjy=2v#2E֕)Ŷ1cSe@Gfd$hpn=2"v! -p_CEa )Ѕr @qJV< >ɶ~H>쳔K#:>Mya LR*T{zW!4w,r|YJiV!$Wq1QQ{)T(VZ(F)1H۹haeZ%Q EQN'8Z} B_y0'SD⬴ACa!'YB?4`ICiȞW[kEmӽeDSKs(&1;x(#xhZzqG+V!evXd7<}_?GBBA {3)Qq6P[9#N#;Zr@xfv}PXVRC\:f"!ESnG55b! nbR0)d'%Hv8Nk\Z Ayhm ɮfE蛠<$28I$r*!1}Qzs8t_&kjX*YˌDYW@>Pƺ .x]HljJ?N?D5*QgQiB[K7l~22Eg Q8啷Hb^S`% i*< ;ɳuz;*3lYܞ =Dz q/u?d3,Szo+S>kp?SEXbWfsު.)3n5>!!oCyrnt>2nxM~2ĀR`ڥ`\% D<װa5<`~A '岼m^YiW,u:k1ze=aJF~e#Ϟ8/lff0E ;B}b3l&[]4*]Jß<}76}mbz{ zv+>X{ɐ{ٸ^/ =OVz 5h˼-4| +:ۀMNtPݔ2ǷAˇ>4 i3l)hƭѧ7&%a4bмzQ? (M2vp?Py8Msu@HNz3؞'hG 0|EM# #^h+b$X= utj^> #$RX_@ E$`iz9\n ï_S+KAǿ/u@ksP.m[{d̆>k."n~D} 0Jxt3joǼ4́(Զ>/_멦xw C],0 8 tOch>(xR|DVaU:_S'⪜g=W$5gRIB](%VΰS-V!qbbSGqҎ V5V%i dc&.>6̂8 rW@ 4P!}<]E'h sU]rk OHIN[t2;Qek~?rer߾k3N,&]INҝILwpDb[Ůq~h0Z=jx=ciRm0q {2ԓA"fVmV]x@NpJ&$ tG+e|df!Jʱ9GB.a]1zbؒudM4>(_,A=4 ` Ѣ8 u8*;I~*=H\8LHû ɀfkz5pJ͊Pnz˲DO% >U[I&0]uzΪ /F08z<5^@X >NRP )nk4&[Cs&:Il&֨yjkħHD&f 褋4.rtۚlDFdՕ y2qu=Sջ+}K @Z%Wa8=CถWD/7(6 嗛88ͨϾ~Al`Lx/6:;Ȫ~|pڙxR2ɯ%f^-/ }>/>TGQK/_GO c? זMȨWED+ޫPA _֢`:B.ɞڙ 3g`mo5{U@kv_(Whk6--rfFi  1I |ِ51>~P6AQ!nldC$L }}wVI=2$sGJTHNY)OS޷߼噱JU5[hoxd )s\z++9%6e7?]п;}Ts_v́C|+O52l }V`lȌ~peV%HMa8QoP(Y9e@GQastct^蚃9j*enXaJ߀)'+,*۫ e,^br-өoeTuҮ#3\I}\?V8Ahp"g{j ?U:_=AawPxm߈l= `bow4rH";2݌ t.~6:"anyp'<@ z_F Ⱦp7T[ލ9B"nQc4 Eo0_E_DΊMo :'\U%]Q֧@yƳ5RAs1ⶳH4~ɃzC_/qT "^:7 Z x`q4_ wBkCJ _HJ҄jb}oFyB7ڛC!ÉܬXYۧ`[$f  "p ,x})oA.@W39]F}?7$ ݡD()Kg}{\&_k[t?yt1 A [ݳVw/hT {wgMGtűA@׵ul%4{ڸմQwiOfA(kow7iYsw?bƬ ||9=9M3T١Vgb;2x^R`Q0\qTRk |zɔb5JѦ/dW5%)~fkXw;SbJߕ:pv 5JTsJUznHW˳2|ИsbONŝU,(5[ԘJ(#RcF:RmjpږTb[ka;H%{,WӪ1p2\#4rew} DEfgl]# pwSm]$U-㠤cG~M擩n?6M3qh'7#:rMQJBsqcwP84-bAJ^~[r8[ FQ-36Ldڥ:U*ǖC q+%^;(`Dw'4KuWݡ%"DV%sXCf(ҕZw&ӕA۬LZx뻜Da&"D4ih t'ΨizJiV=p'*om>l:,b{&)? e{/f¼ _&:x^y?8xꃍ3Q=L+yMkThO괯f{uk;>߫^S=J~~ģ QPr=˥Q̱jK3G]~J~E}{GaX>? 9G>2BI?z3tY!w!*DaGsY7[#,֮W' = qĐ+wp9A-xm2/@̜p^=D۬,_ZJ Tq닧_PjB.9,/ [(~p~`}_GNW1_AE 5 }Q>} xMb [R(P>Ly?_,O_.h 9-_4'BO(*KPC;`ґ?p\mĥRFπbo=yA_9I>Ӵgw={cdɶ $@ ىQ6jn=?#8J)> :gLNے]6@$8cފ7w_9#K{ E8ܻY{~0WGz  ͎m2iC*#zzr)86gNEKSp(N S?q]҄KX郱JV,a+ͬ` Y1>,-oDs#NE}RA#F!H܀:e8<`Zc=AY\jG pnEV1Xven]=T$FQ5FȌGJ2o (bDVW#Fv&X\Ӵvǩ -̝nLk3E!;-FBQX~ Ywjּ`9Qi=kccg+giTc.Ĝ`hCJH"U8Bّ{La%$~2Kح؝m@ـ.쨴dfWnT@7jO*BUYy akY m_sC0jw+n}qJO;q CQElWJ.(#/8_Sd{Z?"?"CQ}b=|(OE!YV̊ ?i )ֳOYnQf[Kxa>*8c 6+ ?U'V3[x +(?=KBuޱA~owހӐ>s)y1 ģw ѿP~\ޒ*_2_-_Xv|}W( e_c"O]'ܗS@`qF m/ 2ۜ}F :':]|a}qB_x%$ !)O^rC$y@/yH*񄴽)޵߻UDb`}"qۋJחX^)I`nEp)liV[]1M<OP6r=zgbIguSebORD۫qu gZo~ٺlAplxpT0+[}`jzAV2Fi@qv֬5\|ʜ̭NleXdsjcs7f W+Ն7`g ȘJj|h(KD- dXiJ؇(x$( :;˹! I_TS 1?E??ZBΪmU/?~xY'y5g&΋/ɋ>GMGeD3Vq%'#q$8K)fw9:ĵ x}rxwr:\TZaG*y8IjbRc|XŻǿI u3KGnD1NIBs RuK>V.EL+M2#'fi ~V vl{u8zH *:(W☕ ~JTe\O*tHGHY}KNP*ݾ˦TѼ9/#A7qZ$*c?qUnwN%Oi4 =3N)cbJ uV4(Tn 7_?m-ٛ{UBwznʜ"Z xJZp; {/<P;,)''KQk5qpN8KGbe Sd̛\17 pa>SR! 3K4'+rzQ TTIIvt]Kc⫲K#v5+|D~O@%\w_nN[L9KqgVhn R!y+Un;*&/HrT >>\ t=.Tġ S; Z~!P9giCڧ!# B,;X=ۻ,I2UWV9$lk=Aj;{AP79|s*Y;̠[MCۿhf]o{oY=1kyVV5E8Vk+֜\80X4D)!!?*|fv u"xA@T_q64)kڬuV7 t '%;i9s9x,ڎ-45xd8?ǘd/Y|t &LILJ`& -Gt/PK!2ա% word/settings.xmlVmo6>`9,Ɏ8ElG]dPmHʊwŨ^ԠXO"; V0`^:)^m3_i36=\gZGcd6WBb^( \aƐznL"CJB9az5b4g)#Z~O*Nr'an:3) >6_AG^'F=_qBկ?JTXk(.\U%o R=sgVGaw ssk 姳9F4OƺJGPx f0j a|`˻² |,}/]"ql1$/(|I<C([rAwQeڌicv(.12 x6$.xTnƫQm$Gϗ|;[&w.k2;T7dԄǸETM8)yCK M!Js8fYM}>"uj .;D#V!7%Ip@, IY!=mf`wO?n,++ #+hPr8 ~w)w,]Pe#`@=-h_2ROK௣͎0,gh{Ahq'7$}DC]"Я$=9e)Ij^`x=7Egј ^YfyA Ju!ܵ|iWڱ8rXxWqJ)hu[yPK!ab eGword/stylesWithEffects.xml\[s~ߪ(3NReg89Oo2-<!׭Zqo8"\أCb+<,OgN9~g*I}X Lv7i] a|Db.D ?EpYڭ9 DBZ8p䃈_n39)_q {8ňëL3\r~+Gp;n8:0hoƩM[oEiOL]{qG6S_ޫea1GEC\s7K|6DU˒iipW4-1PIƉmK(bgFԒi{O?c?>0Mݕ?06aN^'9r#g}&iA =O {ρ\g XQ,$|@ֵon85ŞG PݡP0;D{#T`uZ c޸ش Kfk:OjvJ Vgq QQB$a mRyT,T3qgԭu+@5Q5ҽ8`Ӳbvdf32+=M&{cZ7 (MwJLMVouUS5}TTfHC~țyw;HM2ͩE&_5P @ܠ.({(țb*yPSG,b %,Mu~țy!oP?Mꇼ @ɻ?&`s"yAɛSL(ycr&;JcUR X*ai&``Ȱ0Mꇼ C~țyw;HM2ͩE&Ӄ*7Ș7&/'oM@1NP5TMxL r*E7~țyw;HM2ͩE&Ӄ*7Ș7/'oM@1NP5yTTGꇼ @ɛSN,2qS?M&u'vțe SM2 T$o17{p_|=uT{2ITlb*dC: ք!^-IMt-B#dI׭u`*0o@P]ۓd虾Gв7˥4h}]Y >@CP#>MU06Cπ P\j.;Hxݽ \s+ٷdjfg(5fީ ޠ3o# (V,TMCpW-f!]֝p(x| il!VHSԯ8jrLCQ(pX]7 IY90$]ךPtn:trkeu(oTr/-郯9T h,-ˠ#z2SoZOYT?o8<h؅rs*nUvug.? vCW޷2騨:iVxbmj-W)#{CYhɐ,t7Lo/ T;n/1S9 eլǒw&of)cIe 'Yr@+*U Yt"ݗ&0dfmu3&ٴez~1lq>lt6h:[4O[4 kt4B'6F݆j.j6K-S&s f)9+y!hY:%A Vlc}iN_ާx""pr9WE)hXA?35YhƲ= `7*\OKwe)Ϻ/SeF= 3+,dV9d"1ϒQХFqn}埒PK!t?9z(customXml/_rels/item1.xml.rels (1 ;ܝxxYt23iS(O+,1 ?¬S4T5(zG?)'2=l,D60& +Jd2:Yw#u]otm@aCo J6 wE0X(\|̔6(`x k PK!5]9 tDword/styles.xml\Ks8ol(S~LƮJ<=S$dBZi4H"!2=E_C2:# ,IuX싀ǯ م뤙^(bpY*C: N"nl{5E^AlY /" A%v3_D[/+}0gn.&H5ٝw3\?HXEn6-Q$&gi FGy8K+/9-Yq]xü4NwF:Opw ӟ .^Zϛ& ^rhr-Y[2w{`<<*[go1 )42su'e"AV XYlYXE,|yxJH HĄ%=&%{7<`ݰ%e~OD_ ԟ1 4϶2lAtI?8*{m@W@l'3A]glʵu]Ĵ"0xż]=btfWWއeC(j]Q i]Q h]Qsx늚[WٸQ4 %3B&7Ш#y5G֪Mdܭ2H2KDں#PeDۍr8%l?SgƒVs|5`r=6" X<ʣTV:3dr%lftN(y{ИL3)mI>, .*p)>psUlޢtQ=Zʅ (*.)Rt|p(ٿLs_ZRzͭsV"Y"Zanfuk$[g}:׾(qj=ZXC`mvJFY;5Ƶ@֤}71b,Ϛ<1 cQQb$e mb<*ZOY[V-B C|<&AG ,kZU Îskf@v%I8 I@vPnPSenz,C0̩6FY2> ,ꇼ @7&u'vțe SM)6_5P @ܠ.ͨ{(mM@vP (17 DBKS&C~țy!oPwn鏼 Xܠ9L kz@e&n8Jޘ (7;BՇT*X X8&r, n!oE7&CyAsj @M憣ɛb:yPS!Ts,kU4y0^:7 dcQ?M&CyAsj @M憣9ɛb:yPS!TM,kU4!ofg&0ly,ꇼ @ɻ?&`Ys2yAɛd -%_OzϠ@D ,&+~;#`a!<&͡] W!xo&Nn{S[!uxB؞$@} -;f B+ohzb*ƿqaQ Pw} W Q}KFf~;~Rh6ɛ :M=rpj]AhB4Bb,&ArfS- /6ebkuގX+V"Dd^q2QaxX^ {(dNÐTw] @in:trkeu)oT/-郯9T h,*ˠt2SoL-yT?o8<hZs*nUv.? vC_޷2騨:iVxbm/j?-W)#Q!U,2$'=CAM&ϜjǍ)a/;cǒw& UBRֳ&-N.&<VTyBA"LiN]Lr4wid66L/3Χ-ΦMqia-C0mhxY8et UYm2I7KQ] yFsMЧй/ "9_bpK{do2>#`*"/c~m[sSӟ|,o߃.}vf88_-Ky֭gx*k4쩠RȏY&fQ&! )Ս,]jw&C~PK!tz+U(customXml/itemProps1.xml $( j }6&.1BXkiWטDԔwaagC4 `{ H;{F/\I^ié=4}<2ɘ|פ- 9?Ӣ(SVRY ^zo@Y(`Ji9դ{hVa$~ҝW.GJ+֬ovvx[5Zn6?L@چQm|PK!'RdocProps/core.xml (QK0C{u:Bہ=9(n 6iH{nu >/ܴU:$Eh7%zY-)gZP84 n(o,<ƀ\HQnJP߂b. uch7064 <3c3 )4cAoܟ2r*&t:?{`l6i>FOK )|c9hɴktGJŚ9 _KKoC7ca'Mpmx7(ǖg5R2IMW$4}z?Sw=1 ? PK!\(customXml/item1.xml $( I /-JN-VNIM.IM .IUq pԋQR %bJ 9yVIJ%%Vzy@ (]??--39%?475DL?)3)'3?( jUч{Ǝ PK! 8word/fontTable.xmlێ0+;D_hjKԛ^T`y Y޾;d/j0c<=hiME %0ܮTuyH̬FT %=s(ϧtI|Dܒwհ9i{EۡGd\9.kgծМѦ$|idSV, c+(F$  6S(b}L"5īG#x|涭/4?Ay8v΁zՎgt`h`r\DSAr-Ӑ;3;r7l16l!M/+c?iv萿QI'iK|۳NB1s,:}L(M耥~mI0$~ P;䯌< " 7K8[8A$B]N\u X$qؒҥWK[c$]tdD!߅? J<0y9DZWcSˤۗXAӝ_PK!p|docProps/app.xml (Sn0 ?7JڤFŐba[mϚL'lIX׏vO4H[Sg-h]4jW[OŗyIR*?`w c 16=_ '\hqvUxktz+ЖX^aw\MK\< P`kE('9tԀX(0 )C(g z/.Q~Gޫ4rXhEftpU=v.d`g_dBn@ ZոJA x@FQBK5E{g?Td*oU0õdaޜ̓\0N\Nߔ!v6i襎p]׮ d w6h%9+SZ΋0Kߙ=P5Ssl\\ȣ}?dT}Rb4m֌aZ?6+ؘ׷C3LMqȌI|,R32\KI͎!"+3V7AIʾMX#~fPEQKqha%ũIs2Y2wsMj`K&cKF/b.n^8RknX:ccy\ЉCr*S;W'b:BVԴ!U S\$pE/Dۥ莉jId6h--Okh{ץAGd^]YCudX$hky;)0~Med$]`[wgh8;hQZ2&.Ak|Ej@k/'\)1^z=aԑA"ޘo VL|24rG :?g&.'?b}ߘIG=ehL;t0)m皸n y);.\l1@X%/19Vebˎ6 c Q%H퇙 6EN,Imlk v5><3{0omY;X]M 1JN.ȰHѦ옩)ԊAA) ?$;~MQ)fG&^5*UFֻ _LZJqfg57NV?S cRU>l.6G< x"_̋ttңsI["=zʔ$V|*'K:h{|;mOcE&8۷n3W$4]p'>v h}_Kdu|7ѵ~Ǝu+;}׳+{P⺴G&S7ozk }h;lwk3Lk8F$OՀ&u?62ีK'l?VcM7][3mH;fvɴkNǧ׹J*&.Dor ]"L.]i//p^쬔9Q|`Ce웎#6xG\RߴM>WNʢ{K}Ħ ިI{`w^}Ϯ3ǚ~]Js9y-]'oݒ[^9m֌K%{S_*~S A}UuE8}-xG\VC?V=վ*Rɪ| O5q# r`˛O}A\О|]@3n޼)Ǐlٲ{}da!Mkˣ&Q1[9'?wn#r~b<_oӈM=O yZd+/ٍ ./Lpo!9W@&&;cg_bTyD~hhHyy׽ǎm۶%ocUhx.]g,9 18r#u2R',-&NwT?f.cË́ 3g;Jvс7l /zEqbbQ~_N8n =NN<ԏns&618?hդe_pg2 p>{2yǪfޛZg&e6hg3k?Tyd]T0~o Kh;6d;m-2ێTavz)qi1y RVS΍>+o:1G[EJ$<<@nּдq#\zOc8p{O'HK+u;w\oNjW˷#%{64O G/p)qXt²=ζ >5hZ|ۄȸ{X|tGT>k^kyV$Py}&ώ{Qߩ~K}Xx'?22"gϞw}'H[Se(;ȼ9qS},Jo$)n59dggHs5bTqYk Ra$ y&Cjq<֒NjtCycw%TR\lm^rd|uşOFmNtS<d> n#.Y\VvM+{H?ޫVk}lǾgKr;z9uuȉie2oN0]5bTqiy-8ĥ>bקi+ͩ$^u ~yX.X ꕶ Zyod߬۷oۥVX9CtPQk,^s1{nt]j>Z?.3}cW=weW\C\k|tߧR O+1vʿ+nWEdA~EoOnһסGtkZW+m$^{<_ln}v:M:+o#6>xĥ),,9!.`>ִ㮺q|'yfδEEy$UZ{LK[Xkos-lown<7Yh/$$< B"@ Z@"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$4ѮK˃ݍz2ȉD"dLK0#?EU4tnrߥ^줣EYj\\,R2XSq9JIΎQXܤ,$pV3rzc^C5XY0дaщ`8s6(w?K38NO:ץhW'-Ek|n-e:۞k݃:rv\'<%h .bJ_bs8m:JKP>A 3MMAzmXRLtxMk=7n7~&_f%'~rmyFuD"C옩)ԊAA) ?$;~MQ)fG&^5Iʂ+#ZY]7k:q[LG9Hᐔ xȴYV*f$ga<+hw]$_Ip<[QJf#@~ H>L2=oGiOu jly^Vna؊2Mf|Y:fA9LepHd%k];mz(>.AɌ$`1 YfɌGjo܎2'*T)qz Y1U+6&]9TGn&.Mp}"sEbz }hi8d*R*3x8p;[\L&ǣ4Kr}ԑCy -$< ҢD(!aJeZh@ch AZ4ٝQ"oD;sm?l.hܩ]s_,-Dݟ~ByUż~yt]mVm7%o?|gwϛ>v7~m6ɚd߮7I{ 0/.IMr?/Mhe{*ګ1W_ui\uw[LL2'|36QďGfv]0knB$8V7n~ry{u-ݭ XD(2i3nA_^_̠H\ze|'rr׵t~o-Jɝ Ly\#68z?lOmMpD~wB7lq}ZAL{nl#1eDv1*]hv͛E'ڭ+in9֓{T$ (#6q<#.u\'=*[>*:K RY/@!F>.٘]sqYק\;!WJ{I^_n皥{M`r[ONߺ%rڬJ0MT$u<{ZBep<#.[篮,r~tA~/GlcT~tttxe˖- /l_WOȦM7o{mfAT0>}s޲w$չ)={#yyd<*#f9Nލ^9-ߟf7ClxG\Wrlm?jgtaA R|ih.[n#.p}Z≗Ⱦ7cv[; %)v$rgk\Lr]g_wM䇆矗_+|1ٶm[lҭ׿կV%_xѮ4i߭n^>o֢woc[-E9=(^'KW>=lWi1ٷ>>|kcÆ o!/Wt/722R׮I mפ]5OSYo7sPi(/$88n a+xG\VrG>jBsqT\GB=yJ}zQoﻊۥeIu"U|Ibez.{IGE3χ&Id3{ZmI/ch"%jNvbQCHoo v>55~^pf{ ;m-2ێTOK~v99cמre9fyIfhC\|\]k{q^#ϋ/TZ{2f[5o#ɢ>MCσDZ=C#ϖܵ߱vt,|xoqS.S~;| fz!]:]si/H xG\dG"ϝvqDmФn7(6[uO)FKkN12 t̚mф8qIrc⦆)us﹛}XxM5A>{8mu_ qZ/':_?3L淦vQvys⺕XHRV4]cFeF|]^3_)\A\>շ^-?;^K}['g߉OqWh4Yڵ}9U_ckƱNh˼^)o׿elz #,\~Gmt Y-Rק{ : x{UJt_rbPS<].p}oIU(Ԕy}ohNnyyRD… ޲:nuW]]]sM}mp붠\$w{olܸۮ vuTy3<Ng}o߶K(G{ós8rXݢCbNϜ+1Upv8ykl.B{OqhS5wU?”{]ZgG#/$*ئc=Mu&Q:|0v> 'Ji$烿mW|3v}Fovڪ*/ܒ쪥uthM?W>+ڳEu͛7-˻~5^d;ֶ߰ȷ#Z"jU

w?vfa}|=.6"-]!]PF4)*h7Љnnܽ0ÆkPF4)*hz6ȏ2b^6%=o s3Y2*#˧fލgE.QF,g[͵h}VLql˦oG볢Oۗw k[iX\qzj%{էH4ʈM E.Vh w(#F{;k='zeK.31-k;LR2b*f>3Rt>ter9ߜfgؓ[ây9vg]6D5az%x Z~!y ;IZ'\<azjs{CgteROm?;3#E3ɱU:,ݗgz}fKdSn{ oH4K}-6{ (CO.p& tvdTX.@mwte" OJ|ctX*F.'26{n["zx*#Lmo?8Z)tF1?oH*Iu)ʈ%qnFIUF, ¿Ɩ @'hs?UMLql $~(#ǭ ;9XHjhݧ2bVN4Vu)an^4g&ʈ%a@ LR-\2bN<:#oHѥz(#f$~kDNKQ*%~kDNQFɗK!~2X-$lh (# PF4) Jh<}HCG (#CQF4D ¾eʈ#ECGvҡ(#xa2PeD㑢ʈ#E;A~G<тoz2HPeD㑢th?ʈ#hA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@؛άc.kB1]jGFvNec62(t |b[nvK(`ZE( K~:6tiu-/v[C:թ4{||@_WC7aCG|?N5vXWjpYX]+4ÍA juK\96:M"uaC f p3VGT.j ~ lpT; r7nQmM/vTe&MST]6npR' &6کH'V- EnJUa\f]@@zh.վm`z]XmWa8ja_gY؟_& Nvfɤa>;W]m֚+'}Yj jM6V-ZsSdN9ʩ5 )-{5-Z IWR)T}vy lQ!C Fxd5ݙR{4[wؙoI!lrcH(ǐQF4y p^L+k], !;+Mvv|Wۋ:uF{E9&+Peg 5l_ۇM妝*|rۃŃM奝-#?ԐO|}Cl?m`*(Xv/]"19 :L^vJ6^[IJ }0_azYjE,ۺOi.>l~۠@d5F {U?@}/ݫݩ@נv9:5dg$LXstƝۢ,e+d$LXunYd\;LXuao}d\;Luȯ?cnM~d5NB@-KXe7&g!{|:dջq**qPY=~x7{A~x7VϹ-ƃn>f߿ON6J'偿{/u'E+xJԐz7v۶RE_7(Ǭ n]M v~7&yvd5 {<Xo^R`Yt?iw5d4>ϳ%^8:kPY=i}0gsK<;i}\婨 #45rLV◐v a˗v a'C95I\NWv..S IF {CI e VMⰈGK(&}qX{wG17ى7Sd5>t6} 5dMr )R0ʈ##vMN/;j&R01`GF^ ʬtjԐM691`cH(#@PC6(ǐQ!h<2a8a8a8aK.OiIENDB`numpy-groupies-0.10.2/diagrams/multi_cumsum.png000066400000000000000000000203511450707334100216220ustar00rootroot00000000000000PNG  IHDRzTDcsRGBgAMA a pHYsod ~IDATx^_lǝӗ@<IU '9ն*rT90[*I& DhNT]ETlNTlQHH=Ix3Rl33˝#HH;w( xy*jm5 nZWi>^fq-`LܹC6PI _52dp19UȩEיf0F8\b56{%|C %ȴGḕNkQ%A#n)J:x`E(rw w^|&ȁ Ѵ>zy!84l4 I~_a*vPqX*\ycu8Ô;KS^5_JUK_d7ygt|Q+%Qy%Q1"H* wשܨtM267_^!Mƛ>t=`mNmWgv8} 3tW9utjybu漕7|M1beYz _cT9hʌBZJu=}S^۹zOmAy1;M6ҼaKXK~,ok󸨫M^ݷX*Oҫe]zW,w(muZFkt^Ïͷ/OF$+1DZ.,˔k5#TT7>}Ֆ2~2 -JBQ(0̦s硻S&vrz?BSEzBզSkOKlMxIAzVLkGYzT(jӫV evts{koX- t0uĨ:^l#b=X e- żMf%YK'G̪jF۷nɡOJӫrgO:q8޺cUk:jxK9r+KBizѫz6i8Ō~s&j45jVׇdƢ-1\s\k2tϔ灇e-(l1WQ(ޛǖ ߠ|4o~D~Bč\#[bN&Z2\@SZoge&3ctqI?:Nk|9=|Na*6&eh[*vLbAEx՞::",%k,6x%a䔬Y2-X3,f{v? . R8Ya)-*5%  > OilY%֨Lu]]䡳>̛3GFspME=ce*M{\o-Mǟ9mb-fmhTjz̿|R|IE;EY*Bb}rρO~ٱsX0=ZYkhj1CCҞ55K1+\7u \tiOϥݩQә)fEK:'8gřM o dܕuFצze&>tC|fĂnљn73'Uw^dɩͭš.ZU^i&_/0%ۊYP'X8:1iu{kz6@E+U| Hq6mN,54;y,jyu$ Q\Ο.\;(8ģn MQmҶKznjRҶʹ Mm Eg,z}}l&''E)#.fXƇWClMPyCYhPmx\gN4med.qI>2Lrk$k͆w>G2YhGD)#$RE̼~QYp+YYqEd.kmf}dB27%`܄)27a M`e8nJ;O?K @}$VNgL7oޫ;XU;c7E< ݮ(W#"|gzRccOt 3]tC:Ag:d &H - ng[h@# v1} +'ˎ7FKjJkZU] [n.Ś?<.겟gGZ5_SbZ/Jk-_f+Dѣby2"0X@ R"A|$ux"%mRgmkj|39d-oFV˚7]M=S‰st9:8[zRGCD]e#͛+nβϲNŠ[|9WsJHDU֚KnXDQaD{ġ{TN+88rn_xE$nJ:VH]KZzjQ}ؘ*Lc߳gjVսXjV3LZPR*,˨֧M"ּFƚw7Ud[]<3Ŭ9Z?}+7>r=Jy ~_ϗb >t&{^^xw>LyE9[ ē$7_mO܌YZ?vST=5}n ϧqc@w)硡^?;7Fz\31y|qɞSQSoHP.x}|dvD!xpbAwH}8ܫr&7uRҮ҃}WOoMG FڧYkM}M Z#:WˁX=8 EI}lB;ӣќ.|9%M@=Bb7 zl+@Q9*kIG8kb{"ԴTrеty/oF>6q;tuV(Ohۮ t1PJv)|sн&:qp NՇBNڼ@6|$^Rh֨+ƹu{!H~<2>ؽ4yN$vDffEs?7ʊ>X" 0bBغXU]:Bu'^)UwXH;,BG-S8y,f)xw73aE.$cCMGۼ76ߎRǩtzbMGAmT4oJE]mκK#cW|Ydz b@'npፈ;kuW3t_Wxyڷ7\2_Uɧ󥧆`*,˚}T۩D);N[VY][Unjp(̅+osp/#v']}76N;(;Pln~|j_?ՖnU4<9BG_݉bv5͆;>;ź;x9UKcͿRxdp|B2zXX{Vڟz;;Y͠;~/͹ǚ9Tf̡p1M>nv>$jpv;s:׊abBWvC(ʹ/񼳽_G:wdg ~7.n|O Vk=G]CNd>$y) ޜcYMC2DjRwXR: )n.wnxo#Ԁ5Sgw&X-H$8_~FTt3.u['bXtTtsxC;UIȢ3&&_ jG07XۦR%ZDqnޤ*^.)?4&''EI_}%4D 2e8neZ`{(_Xa=8hAqsQ&7Aܔq7E1UT 3$b &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H 9(.qwP(D.UjAܴ;FWҾ}&[`Йns6l kd&H !nD &H !nDp+UM-iRիe&HWkѡk7!& Q_ī)j BE6ZP:bA^,5A"Zh`]'k j 7!/RfVZ>V3,ΖrBiz\ծl%r^+V,5Aͮ zu:tMMeBFb&N|(ѥ 3u)Q^!.V y&`lMȩTTppA4TD *~ᦵyv2IuЍx쑱TMbrtێU//Dƶ;{m{ߩ݂" ~{>'+,e~oB{zk:pMMiw薵1L\Ƿv3FuuY9=sqezʴ갨.D(NÙ-+o-ł^s:  |(udZ2\@SЕ8=Sq",mn5O +r<5~CɐWfLN1Ca7}}HnJ>I*3G[,sqdP$2(o4.jO"ǩ-6yF[.sRtHiAܜIQW9D 2qQL{t[Y$%Xt<"J:xVR!nDͲ|9pwbafs󈨛kEedvd}ƨxI)f;6! j\ [n.2f#I27+ƻ٣YWk不~r_kcmL iw;Zn[SKvۼvQ<zI,MȒBzBTJ3LY/UdO=!)O%=QvZst9:8[zRGCD=eg?U"qjv-kKEM#&r\jﻚ E=rtVB9[kK^K6j0/Nr,Cw$l9=[kÖd@܄lپYZF۷nɡOdy.nei{l_ڪW WͪF۞ (Oݎ:Φfl DMEzepsTԹW, AiyX1ҝ,D57ZiY]"o|z:جɀCbG}LbQ* bbvLj]3mkymk׌ L^}+ ME(Ɯ[sMQͪrNŘw_Znb`DbXG,;nfݡ [vq6v4Ҽ[Xak}|l[R GEn;|Zr#l{DIy<+:r寻:ze)аiفSd }UQRԕf,,ߒk"8tӍ|F@ nFk<_4!D} Ķ@(>bQ(.v$f١έۧ]q] oeBL{ˌ[}ty/=l҃};:N=%*`uV(9eZMix3ؙzezvFkjW U-w8z5kʿ!/߇LY֒Z_' ehܺFkЖpgm~9)ҷ1ͩ6|ͻfO??)ѧJsero5Q.n|Oos51TD̍ٚli,)dӘmSVunB6cͿ1>>e<|tSZ L?W{vg"ID"!s699)J:;(A9n Ji.q+˝עKރGDIRt 1: if unravel_shape is not None: # argreductions only mask = ret == fill_value ret[mask] = 0 ret = np.unravel_index(ret, unravel_shape)[axis] ret[mask] = fill_value ret = ret.reshape(size, order=order) return ret @classmethod def _initialize(cls, flat_size, fill_value, dtype, input_dtype, input_size): if cls.forced_fill_value is None: ret = np.full(flat_size, fill_value, dtype=dtype) else: ret = np.full(flat_size, cls.forced_fill_value, dtype=dtype) counter = mean = outer = None if cls.counter_fill_value is not None: counter = np.full_like(ret, cls.counter_fill_value, dtype=cls.counter_dtype) if cls.mean_fill_value is not None: dtype = cls.mean_dtype if cls.mean_dtype else input_dtype mean = np.full_like(ret, cls.mean_fill_value, dtype=dtype) if cls.outer: outer = np.full(input_size, fill_value, dtype=dtype) return ret, counter, mean, outer @classmethod def _finalize(cls, ret, counter, fill_value): if cls.forced_fill_value is not None and fill_value != cls.forced_fill_value: if cls.counter_dtype == bool: ret[counter] = fill_value else: ret[~counter.astype(bool)] = fill_value @classmethod def callable(cls, nans=False, reverse=False, scalar=False): """Compile a jitted function doing the hard part of the job""" _valgetter = cls._valgetter_scalar if scalar else cls._valgetter valgetter = nb.njit(_valgetter) outersetter = nb.njit(cls._outersetter) if not nans: inner = nb.njit(cls._inner) else: cls_inner = nb.njit(cls._inner) cls_nan_check = nb.njit(cls._nan_check) @nb.njit def inner(ri, val, ret, counter, mean, fill_value): if not cls_nan_check(val): cls_inner(ri, val, ret, counter, mean, fill_value) @nb.njit def loop(group_idx, a, ret, counter, mean, outer, fill_value, ddof): # ddof needs to be present for being exchangeable with loop_2pass size = len(ret) rng = range(len(group_idx) - 1, -1, -1) if reverse else range(len(group_idx)) for i in rng: ri = group_idx[i] if ri < 0: raise ValueError("negative indices not supported") if ri >= size: raise ValueError("one or more indices in group_idx are too large") val = valgetter(a, i) inner(ri, val, ret, counter, mean, fill_value) outersetter(outer, i, ret[ri]) return loop @staticmethod def _valgetter(a, i): return a[i] @staticmethod def _valgetter_scalar(a, i): return a @staticmethod def _nan_check(val): return val != val @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): raise NotImplementedError("subclasses need to overwrite _inner") @staticmethod def _outersetter(outer, i, val): pass class Aggregate2pass(AggregateOp): """Base class for everything that needs to process the data twice like mean, var and std.""" @classmethod def callable(cls, nans=False, reverse=False, scalar=False): # Careful, cls needs to be passed, so that the overwritten methods remain available in # AggregateOp.callable loop_1st = super().callable(nans=nans, reverse=reverse, scalar=scalar) _2pass_inner = nb.njit(cls._2pass_inner) @nb.njit def loop_2nd(ret, counter, mean, fill_value, ddof): for ri in range(len(ret)): if counter[ri] > ddof: ret[ri] = _2pass_inner(ri, ret, counter, mean, ddof) else: ret[ri] = fill_value @nb.njit def loop_2pass(group_idx, a, ret, counter, mean, outer, fill_value, ddof): loop_1st(group_idx, a, ret, counter, mean, outer, fill_value, ddof) loop_2nd(ret, counter, mean, fill_value, ddof) return loop_2pass @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): raise NotImplementedError("subclasses need to overwrite _2pass_inner") @classmethod def _finalize(cls, ret, counter, fill_value): """Copying the fill value is already done in the 2nd pass""" pass class AggregateNtoN(AggregateOp): """Base class for cumulative functions, where the output size matches the input size.""" outer = True @staticmethod def _outersetter(outer, i, val): outer[i] = val class AggregateGeneric(AggregateOp): """Base class for jitting arbitrary functions.""" counter_fill_value = None def __init__(self, func, **kwargs): self.func = func self.__dict__.update(kwargs) self._jitfunc = self.callable(self.nans) def __call__( self, group_idx, a, size=None, fill_value=0, order="C", dtype=None, axis=None, ddof=0, ): iv = input_validation(group_idx, a, size=size, order=order, axis=axis, check_bounds=False) group_idx, a, flat_size, ndim_idx, size, _ = iv # TODO: The typecheck should be done by the class itself, not by check_dtype dtype = check_dtype(dtype, self.func, a, len(group_idx)) check_fill_value(fill_value, dtype, func=self.func) input_dtype = type(a) if np.isscalar(a) else a.dtype ret, _, _, _ = self._initialize(flat_size, fill_value, dtype, input_dtype, group_idx.size) group_idx = np.ascontiguousarray(group_idx) sortidx = np.argsort(group_idx, kind="mergesort") self._jitfunc(sortidx, group_idx, a, ret) # Deal with ndimensional indexing if ndim_idx > 1: ret = ret.reshape(size, order=order) return ret def callable(self, nans=False): """Compile a jitted function and loop it over the sorted data.""" func = nb.njit(self.func) @nb.njit def loop(sortidx, group_idx, a, ret): size = len(ret) group_idx_srt = group_idx[sortidx] a_srt = a[sortidx] indices = step_indices(group_idx_srt) for i in range(len(indices) - 1): start_idx, stop_idx = indices[i], indices[i + 1] ri = group_idx_srt[start_idx] if ri < 0: raise ValueError("negative indices not supported") if ri >= size: raise ValueError("one or more indices in group_idx are too large") ret[ri] = func(a_srt[start_idx:stop_idx]) return loop class Sum(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] += val class Prod(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] *= val class Len(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] += 1 class All(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] &= bool(val) class Any(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] |= bool(val) class Last(AggregateOp): counter_fill_value = None @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): ret[ri] = val class First(Last): reverse = True class AllNan(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] &= val != val class AnyNan(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] |= val != val class Max(AggregateOp): @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): if counter[ri]: ret[ri] = val counter[ri] = 0 elif ret[ri] < val: ret[ri] = val class Min(AggregateOp): @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): if counter[ri]: ret[ri] = val counter[ri] = 0 elif ret[ri] > val: ret[ri] = val class ArgMax(AggregateOp): mean_fill_value = np.nan @staticmethod def _valgetter(a, i): return a[i], i @staticmethod def _nan_check(val): return val[0] != val[0] @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): cmp_val, arg = val if counter[ri]: # start of a new group counter[ri] = 0 mean[ri] = cmp_val if cmp_val == cmp_val: # Don't point on nans ret[ri] = arg elif mean[ri] < cmp_val: # larger valid value found mean[ri] = cmp_val ret[ri] = arg elif cmp_val != cmp_val: # nan found, reset group mean[ri] = cmp_val ret[ri] = fill_value class ArgMin(ArgMax): @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): cmp_val, arg = val if counter[ri]: # start of a new group counter[ri] = 0 mean[ri] = cmp_val if cmp_val == cmp_val: # Don't point on nans ret[ri] = arg elif mean[ri] > cmp_val: # larger valid value found mean[ri] = cmp_val ret[ri] = arg elif cmp_val != cmp_val: # nan found, reset group mean[ri] = cmp_val ret[ri] = fill_value class SumOfSquares(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] = 0 ret[ri] += val * val class Mean(Aggregate2pass): forced_fill_value = 0 counter_fill_value = 0 counter_dtype = int @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] += 1 ret[ri] += val @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): return ret[ri] / counter[ri] class Std(Mean): mean_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean, fill_value): counter[ri] += 1 mean[ri] += val ret[ri] += val * val @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): mean2 = mean[ri] * mean[ri] return np.sqrt((ret[ri] - mean2 / counter[ri]) / (counter[ri] - ddof)) class Var(Std): @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): mean2 = mean[ri] * mean[ri] return (ret[ri] - mean2 / counter[ri]) / (counter[ri] - ddof) class CumSum(AggregateNtoN, Sum): pass class CumProd(AggregateNtoN, Prod): pass class CumMax(AggregateNtoN, Max): pass class CumMin(AggregateNtoN, Min): pass def get_funcs(): funcs = dict() for op in ( Sum, Prod, Len, All, Any, Last, First, AllNan, AnyNan, Min, Max, ArgMin, ArgMax, Mean, Std, Var, SumOfSquares, CumSum, CumProd, CumMax, CumMin, ): funcname = op.__name__.lower() funcs[funcname] = op(funcname) if funcname not in funcs_no_separate_nan: funcname = "nan" + funcname funcs[funcname] = op(funcname, nans=True) return funcs _impl_dict = get_funcs() _default_cache = {} def aggregate( group_idx, a, func="sum", size=None, fill_value=0, order="C", dtype=None, axis=None, cache=True, **kwargs ): func = get_func(func, aliasing, _impl_dict) if not isinstance(func, str): if cache in (None, False): # Keep None and False in order to accept empty dictionaries aggregate_op = AggregateGeneric(func) else: if cache is True: cache = _default_cache aggregate_op = cache.setdefault(func, AggregateGeneric(func)) return aggregate_op(group_idx, a, size, fill_value, order, dtype, axis, **kwargs) else: func = _impl_dict[func] return func(group_idx, a, size, fill_value, order, dtype, axis, **kwargs) aggregate.__doc__ = ( """ This is the numba implementation of aggregate. """ + aggregate_common_doc ) @nb.njit def step_count(group_idx): """Return the amount of index changes within group_idx.""" cmp_pos = 0 steps = 1 if len(group_idx) < 1: return 0 for i in range(len(group_idx)): if group_idx[cmp_pos] != group_idx[i]: cmp_pos = i steps += 1 return steps @nb.njit def step_indices(group_idx): """Return the edges of areas within group_idx, which are filled with the same value.""" ilen = step_count(group_idx) + 1 indices = np.empty(ilen, np.int64) indices[0] = 0 indices[-1] = group_idx.size cmp_pos = 0 ri = 1 for i in range(len(group_idx)): if group_idx[cmp_pos] != group_idx[i]: cmp_pos = i indices[ri] = i ri += 1 return indices numpy-groupies-0.10.2/numpy_groupies/aggregate_numpy.py000066400000000000000000000303461450707334100234240ustar00rootroot00000000000000import numpy as np from .utils import ( aggregate_common_doc, aliasing, check_boolean, check_dtype, check_fill_value, funcs_no_separate_nan, get_func, input_validation, iscomplexobj, maxval, minimum_dtype, minimum_dtype_scalar, minval, ) def _sum(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) if np.ndim(a) == 0: ret = np.bincount(group_idx, minlength=size).astype(dtype, copy=False) if a != 1: ret *= a else: if iscomplexobj(a): ret = np.empty(size, dtype=dtype) ret.real = np.bincount(group_idx, weights=a.real, minlength=size) ret.imag = np.bincount(group_idx, weights=a.imag, minlength=size) else: ret = np.bincount(group_idx, weights=a, minlength=size).astype(dtype, copy=False) if fill_value != 0: _fill_untouched(group_idx, ret, fill_value) return ret def _prod(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 1: ret[group_idx] = 1 # product starts from 1 np.multiply.at(ret, group_idx, a) return ret def _len(group_idx, a, size, fill_value, dtype=None): return _sum(group_idx, 1, size, fill_value, dtype=int) def _last(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) ret = np.full(size, fill_value, dtype=dtype) # repeated indexing gives last value, see: # the phrase "leaving behind the last value" on this page: # http://wiki.scipy.org/Tentative_NumPy_Tutorial ret[group_idx] = a return ret def _first(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) ret = np.full(size, fill_value, dtype=dtype) ret[group_idx[::-1]] = a[::-1] # same trick as _last, but in reverse return ret def _all(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if not fill_value: ret[group_idx] = True ret[group_idx.compress(np.logical_not(a))] = False return ret def _any(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if fill_value: ret[group_idx] = False ret[group_idx.compress(a)] = True return ret def _min(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) dmax = maxval(fill_value, dtype) with np.errstate(invalid="ignore"): ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmax: ret[group_idx] = dmax # min starts from maximum with np.errstate(invalid="ignore"): np.minimum.at(ret, group_idx, a) return ret def _max(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) dmin = minval(fill_value, dtype) with np.errstate(invalid="ignore"): ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmin: ret[group_idx] = dmin # max starts from minimum with np.errstate(invalid="ignore"): np.maximum.at(ret, group_idx, a) return ret def _argmax(group_idx, a, size, fill_value, dtype=int, _nansqueeze=False): a_ = np.where(np.isnan(a), -np.inf, a) if _nansqueeze else a group_max = _max(group_idx, a_, size, np.nan) # nan should never be maximum, so use a and not a_ is_max = a == group_max[group_idx] ret = np.full(size, fill_value, dtype=dtype) group_idx_max = group_idx[is_max] (argmax,) = is_max.nonzero() ret[group_idx_max[::-1]] = argmax[::-1] # reverse to ensure first value for each group wins return ret def _argmin(group_idx, a, size, fill_value, dtype=int, _nansqueeze=False): a_ = np.where(np.isnan(a), np.inf, a) if _nansqueeze else a group_min = _min(group_idx, a_, size, np.nan) # nan should never be minimum, so use a and not a_ is_min = a == group_min[group_idx] ret = np.full(size, fill_value, dtype=dtype) group_idx_min = group_idx[is_min] (argmin,) = is_min.nonzero() ret[group_idx_min[::-1]] = argmin[::-1] # reverse to ensure first value for each group wins return ret def _mean(group_idx, a, size, fill_value, dtype=np.dtype(np.float64)): if np.ndim(a) == 0: raise ValueError("cannot take mean with scalar a") counts = np.bincount(group_idx, minlength=size) if iscomplexobj(a): dtype = a.dtype # TODO: this is a bit clumsy sums = np.empty(size, dtype=dtype) sums.real = np.bincount(group_idx, weights=a.real, minlength=size) sums.imag = np.bincount(group_idx, weights=a.imag, minlength=size) else: sums = np.bincount(group_idx, weights=a, minlength=size).astype(dtype, copy=False) with np.errstate(divide="ignore", invalid="ignore"): ret = sums.astype(dtype, copy=False) / counts if not np.isnan(fill_value): ret[counts == 0] = fill_value return ret def _sum_of_squres(group_idx, a, size, fill_value, dtype=np.dtype(np.float64)): ret = np.bincount(group_idx, weights=a * a, minlength=size) if fill_value != 0: counts = np.bincount(group_idx, minlength=size) ret[counts == 0] = fill_value return ret def _var(group_idx, a, size, fill_value, dtype=np.dtype(np.float64), sqrt=False, ddof=0): if np.ndim(a) == 0: raise ValueError("cannot take variance with scalar a") counts = np.bincount(group_idx, minlength=size) sums = np.bincount(group_idx, weights=a, minlength=size) with np.errstate(divide="ignore", invalid="ignore"): means = sums.astype(dtype, copy=False) / counts counts = np.where(counts > ddof, counts - ddof, 0) ret = np.bincount(group_idx, (a - means[group_idx]) ** 2, minlength=size) / counts if sqrt: ret = np.sqrt(ret) # this is now std not var if not np.isnan(fill_value): ret[counts == 0] = fill_value return ret def _std(group_idx, a, size, fill_value, dtype=np.dtype(np.float64), ddof=0): return _var(group_idx, a, size, fill_value, dtype=dtype, sqrt=True, ddof=ddof) def _allnan(group_idx, a, size, fill_value, dtype=bool): return _all(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _anynan(group_idx, a, size, fill_value, dtype=bool): return _any(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _sort(group_idx, a, size=None, fill_value=None, dtype=None, reverse=False): sortidx = np.lexsort((-a if reverse else a, group_idx)) # Reverse sorting back to into grouped order, but preserving groupwise sorting revidx = np.argsort(np.argsort(group_idx, kind="mergesort"), kind="mergesort") return a[sortidx][revidx] def _array(group_idx, a, size, fill_value, dtype=None): """groups a into separate arrays, keeping the order intact.""" if fill_value is not None and not (np.isscalar(fill_value) or len(fill_value) == 0): raise ValueError("fill_value must be None, a scalar or an empty sequence") order_group_idx = np.argsort(group_idx, kind="mergesort") counts = np.bincount(group_idx, minlength=size) ret = np.split(a[order_group_idx], np.cumsum(counts)[:-1]) ret = np.asanyarray(ret, dtype="object") if fill_value is None or np.isscalar(fill_value): _fill_untouched(group_idx, ret, fill_value) return ret def _generic_callable(group_idx, a, size, fill_value, dtype=None, func=lambda g: g, **kwargs): """groups a by inds, and then applies foo to each group in turn, placing the results in an array.""" groups = _array(group_idx, a, size, ()) ret = np.full(size, fill_value, dtype=dtype or np.float64) for i, grp in enumerate(groups): if np.ndim(grp) == 1 and len(grp) > 0: ret[i] = func(grp) return ret def _cumsum(group_idx, a, size, fill_value=None, dtype=None): """ N to N aggregate operation of cumsum. Perform cumulative sum for each group. group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) _cumsum(group_idx, a, np.max(group_idx) + 1) >>> array([ 3, 4, 5, 6, 15, 9, 15, 22, 7, 0, 15, 17, 6, 14, 31, 39]) """ sortidx = np.argsort(group_idx, kind="mergesort") invsortidx = np.argsort(sortidx, kind="mergesort") group_idx_srt = group_idx[sortidx] a_srt = a[sortidx] a_srt_cumsum = np.cumsum(a_srt, dtype=dtype) increasing = np.arange(len(a), dtype=int) group_starts = _min(group_idx_srt, increasing, size, fill_value=0)[group_idx_srt] a_srt_cumsum += -a_srt_cumsum[group_starts] + a_srt[group_starts] return a_srt_cumsum[invsortidx] def _nancumsum(group_idx, a, size, fill_value=None, dtype=None): a_nonans = np.where(np.isnan(a), 0, a) group_idx_nonans = np.where(np.isnan(group_idx), np.nanmax(group_idx) + 1, group_idx) return _cumsum(group_idx_nonans, a_nonans, size, fill_value=fill_value, dtype=dtype) _impl_dict = dict( min=_min, max=_max, sum=_sum, prod=_prod, last=_last, first=_first, all=_all, any=_any, mean=_mean, std=_std, var=_var, anynan=_anynan, allnan=_allnan, sort=_sort, array=_array, argmax=_argmax, argmin=_argmin, len=_len, cumsum=_cumsum, sumofsquares=_sum_of_squres, generic=_generic_callable, ) _impl_dict.update(("nan" + k, v) for k, v in list(_impl_dict.items()) if k not in funcs_no_separate_nan) _impl_dict["nancumsum"] = _nancumsum def _aggregate_base( group_idx, a, func="sum", size=None, fill_value=0, order="C", dtype=None, axis=None, _impl_dict=_impl_dict, is_pandas=False, **kwargs, ): iv = input_validation(group_idx, a, size=size, order=order, axis=axis, func=func) group_idx, a, flat_size, ndim_idx, size, unravel_shape = iv if group_idx.dtype == np.dtype("uint64"): # Force conversion to signed int, to avoid issues with bincount etc later group_idx = group_idx.astype(int) func = get_func(func, aliasing, _impl_dict) if not isinstance(func, str): # do simple grouping and execute function in loop ret = _impl_dict.get("generic", _generic_callable)( group_idx, a, flat_size, fill_value, func=func, dtype=dtype, **kwargs ) else: # deal with nans and find the function if func.startswith("nan"): if np.ndim(a) == 0: raise ValueError("nan-version not supported for scalar input.") if "nan" in func: if "arg" in func: kwargs["_nansqueeze"] = True elif "cum" in func: pass else: good = ~np.isnan(a) if "len" not in func or is_pandas: # a is not needed for len, nanlen! a = a[good] group_idx = group_idx[good] dtype = check_dtype(dtype, func, a, flat_size) check_fill_value(fill_value, dtype, func=func) func = _impl_dict[func] ret = func(group_idx, a, flat_size, fill_value=fill_value, dtype=dtype, **kwargs) # deal with ndimensional indexing if ndim_idx > 1: if unravel_shape is not None: # A negative fill_value cannot, and should not, be unraveled. mask = ret == fill_value ret[mask] = 0 ret = np.unravel_index(ret, unravel_shape)[axis] ret[mask] = fill_value ret = ret.reshape(size, order=order) return ret def aggregate(group_idx, a, func="sum", size=None, fill_value=0, order="C", dtype=None, axis=None, **kwargs): return _aggregate_base( group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, **kwargs, ) aggregate.__doc__ = ( """ This is the pure numpy implementation of aggregate. """ + aggregate_common_doc ) def _fill_untouched(idx, ret, fill_value): """any elements of ret not indexed by idx are set to fill_value.""" untouched = np.ones_like(ret, dtype=bool) untouched[idx] = False ret[untouched] = fill_value numpy-groupies-0.10.2/numpy_groupies/aggregate_numpy_ufunc.py000066400000000000000000000070071450707334100246220ustar00rootroot00000000000000import numpy as np from .aggregate_numpy import _aggregate_base from .utils import ( aggregate_common_doc, aliasing, check_boolean, get_func, maxval, minimum_dtype, minimum_dtype_scalar, minval, ) def _anynan(group_idx, a, size, fill_value, dtype=None): return _any(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _allnan(group_idx, a, size, fill_value, dtype=None): return _all(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _any(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if fill_value: ret[group_idx] = False # any-test should start from False np.logical_or.at(ret, group_idx, a) return ret def _all(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if not fill_value: ret[group_idx] = True # all-test should start from True np.logical_and.at(ret, group_idx, a) return ret def _sum(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 0: ret[group_idx] = 0 # sums should start at 0 np.add.at(ret, group_idx, a) return ret def _len(group_idx, a, size, fill_value, dtype=None): return _sum(group_idx, 1, size, fill_value, dtype=int) def _prod(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 1: ret[group_idx] = 1 # product should start from 1 np.multiply.at(ret, group_idx, a) return ret def _min(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype(fill_value, dtype or a.dtype) dmax = maxval(fill_value, dtype) ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmax: ret[group_idx] = dmax # min starts from maximum np.minimum.at(ret, group_idx, a) return ret def _max(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype(fill_value, dtype or a.dtype) dmin = minval(fill_value, dtype) ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmin: ret[group_idx] = dmin # max starts from minimum np.maximum.at(ret, group_idx, a) return ret _impl_dict = dict( min=_min, max=_max, sum=_sum, prod=_prod, all=_all, any=_any, allnan=_allnan, anynan=_anynan, len=_len, ) def aggregate(group_idx, a, func="sum", size=None, fill_value=0, order="C", dtype=None, axis=None, **kwargs): func = get_func(func, aliasing, _impl_dict) if not isinstance(func, str): raise NotImplementedError("No such ufunc available") return _aggregate_base( group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, **kwargs, ) aggregate.__doc__ = ( """ Unlike ``aggregate_numpy``, which in most cases does some custom optimisations, this version simply uses ``numpy``'s ``ufunc.at``. As of version 1.14 this gives fairly poor performance. There should normally be no need to use this version, it is intended to be used in testing and benchmarking only. """ + aggregate_common_doc ) numpy-groupies-0.10.2/numpy_groupies/aggregate_pandas.py000066400000000000000000000044261450707334100235220ustar00rootroot00000000000000from functools import partial import numpy as np import pandas as pd from .aggregate_numpy import _aggregate_base from .utils import ( aggregate_common_doc, allnan, anynan, check_dtype, funcs_no_separate_nan, ) def _wrapper(group_idx, a, size, fill_value, func="sum", dtype=None, ddof=0, **kwargs): funcname = func.__name__ if callable(func) else func kwargs = dict() if funcname in ("var", "std"): kwargs["ddof"] = ddof df = pd.DataFrame({"group_idx": group_idx, "a": a}) if func == "sort": grouped = df.groupby("group_idx", sort=True) else: grouped = df.groupby("group_idx", sort=False).aggregate(func, **kwargs) dtype = check_dtype(dtype, getattr(func, "__name__", funcname), a, size) if funcname.startswith("cum"): ret = grouped.values[:, 0] else: ret = np.full(size, fill_value, dtype=dtype) with np.errstate(invalid="ignore"): ret[grouped.index] = grouped.values[:, 0] return ret _supported_funcs = "sum prod all any min max mean var std first last cumsum cumprod cummax cummin".split() _impl_dict = {fn: partial(_wrapper, func=fn) for fn in _supported_funcs} _impl_dict.update( ("nan" + fn, partial(_wrapper, func=fn)) for fn in _supported_funcs if fn not in funcs_no_separate_nan ) _impl_dict.update( allnan=partial(_wrapper, func=allnan), anynan=partial(_wrapper, func=anynan), len=partial(_wrapper, func="count"), nanlen=partial(_wrapper, func="count"), argmax=partial(_wrapper, func="idxmax"), argmin=partial(_wrapper, func="idxmin"), nanargmax=partial(_wrapper, func="idxmax"), nanargmin=partial(_wrapper, func="idxmin"), generic=_wrapper, ) def aggregate(group_idx, a, func="sum", size=None, fill_value=0, order="C", dtype=None, axis=None, **kwargs): return _aggregate_base( group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, is_pandas=True, **kwargs, ) aggregate.__doc__ = ( """ This is the pandas implementation of aggregate. It makes use of `pandas`'s groupby machienery and is mainly used for reference and benchmarking. """ + aggregate_common_doc ) numpy-groupies-0.10.2/numpy_groupies/aggregate_purepy.py000066400000000000000000000105631450707334100235770ustar00rootroot00000000000000import itertools import math import operator from .utils import aggregate_common_doc from .utils import aliasing_py as aliasing from .utils import funcs_no_separate_nan, get_func # min, max, sum, all, any - builtin def _last(x): return x[-1] def _first(x): return x[0] def _array(x): return x def _mean(x): return sum(x) / len(x) def _var(x, ddof=0): mean = _mean(x) return sum((xx - mean) ** 2 for xx in x) / (len(x) - ddof) def _std(x, ddof=0): return math.sqrt(_var(x, ddof=ddof)) def _prod(x): r = x[0] for xx in x[1:]: r *= xx return r def _anynan(x): return any(math.isnan(xx) for xx in x) def _allnan(x): return all(math.isnan(xx) for xx in x) def _argmax(x_and_idx): return max(x_and_idx, key=operator.itemgetter(1))[0] _argmax.x_and_idx = True # tell aggregate what to use as first arg def _argmin(x_and_idx): return min(x_and_idx, key=operator.itemgetter(1))[0] _argmin.x_and_idx = True # tell aggregate what to use as first arg def _sort(group_idx, a, reverse=False): def _argsort(unordered): return sorted(range(len(unordered)), key=lambda k: unordered[k]) sortidx = _argsort(list((gi, aj) for gi, aj in zip(group_idx, -a if reverse else a))) revidx = _argsort(_argsort(group_idx)) a_srt = [a[si] for si in sortidx] return [a_srt[ri] for ri in revidx] _impl_dict = dict( min=min, max=max, sum=sum, prod=_prod, last=_last, first=_first, all=all, any=any, mean=_mean, std=_std, var=_var, anynan=_anynan, allnan=_allnan, sort=_sort, array=_array, argmax=_argmax, argmin=_argmin, len=len, ) _impl_dict.update(("nan" + k, v) for k, v in list(_impl_dict.items()) if k not in funcs_no_separate_nan) def aggregate(group_idx, a, func="sum", size=None, fill_value=0, order=None, dtype=None, axis=None, **kwargs): if axis is not None: raise NotImplementedError("axis arg not supported in purepy implementation.") # Check for 2d group_idx if size is None: try: size = 1 + int(max(group_idx)) except (TypeError, ValueError): raise NotImplementedError("pure python implementation doesn't accept ndim idx input.") for i in group_idx: try: i = int(i) except (TypeError, ValueError): if isinstance(i, (list, tuple)): raise NotImplementedError("pure python implementation doesn't accept ndim idx input.") else: try: len(i) except TypeError: raise ValueError(f"invalid value found in group_idx: {i}") else: raise NotImplementedError("pure python implementation doesn't accept ndim indexed input.") else: if i < 0: raise ValueError("group_idx contains negative value") func = get_func(func, aliasing, _impl_dict) if isinstance(a, (int, float)): if func not in ("sum", "prod", "len"): raise ValueError("scalar inputs are supported only for 'sum', 'prod' and 'len'") a = [a] * len(group_idx) elif len(group_idx) != len(a): raise ValueError("group_idx and a must be of the same length") if isinstance(func, str): if func.startswith("nan"): func = func[3:] # remove nans group_idx, a = zip(*((ix, val) for ix, val in zip(group_idx, a) if not math.isnan(val))) func = _impl_dict[func] if func is _sort: return _sort(group_idx, a, reverse=kwargs.get("reverse", False)) # sort data and evaluate function on groups ret = [fill_value] * size if not getattr(func, "x_and_idx", False): data = sorted(zip(group_idx, a), key=operator.itemgetter(0)) for ix, group in itertools.groupby(data, key=operator.itemgetter(0)): ret[ix] = func(list(val for _, val in group), **kwargs) else: data = sorted(zip(range(len(a)), group_idx, a), key=operator.itemgetter(1)) for ix, group in itertools.groupby(data, key=operator.itemgetter(1)): ret[ix] = func(list((val_idx, val) for val_idx, _, val in group), **kwargs) return ret aggregate.__doc__ = ( """ This is the pure python implementation of aggregate. It is terribly slow. Using the numpy version is highly recommended. """ + aggregate_common_doc ) numpy-groupies-0.10.2/numpy_groupies/benchmarks/000077500000000000000000000000001450707334100220035ustar00rootroot00000000000000numpy-groupies-0.10.2/numpy_groupies/benchmarks/__init__.py000066400000000000000000000000001450707334100241020ustar00rootroot00000000000000numpy-groupies-0.10.2/numpy_groupies/benchmarks/generic.py000066400000000000000000000102571450707334100237760ustar00rootroot00000000000000#!/usr/bin/python -B import platform import sys import timeit from operator import itemgetter import numpy as np from numpy_groupies.tests import _implementations, aggregate_numpy from numpy_groupies.utils import allnan, anynan, nanfirst, nanlast def aggregate_grouploop(*args, **kwargs): """wraps func in lambda which prevents aggregate_numpy from recognising and optimising it. Instead it groups and loops.""" extrafuncs = { "allnan": allnan, "anynan": anynan, "first": itemgetter(0), "last": itemgetter(-1), "nanfirst": nanfirst, "nanlast": nanlast, } func = kwargs.pop("func") func = extrafuncs.get(func, func) if isinstance(func, str): raise NotImplementedError("Grouploop needs to be called with a function") return aggregate_numpy.aggregate(*args, func=lambda x: func(x), **kwargs) def arbitrary(iterator): tmp = 0 for i, x in enumerate(iterator, 1): tmp += x**i return tmp func_list = ( np.sum, np.prod, np.min, np.max, len, np.all, np.any, "anynan", "allnan", np.mean, np.std, np.var, "first", "last", "argmax", "argmin", np.nansum, np.nanprod, np.nanmin, np.nanmax, "nanlen", "nanall", "nanany", np.nanmean, np.nanvar, np.nanstd, "nanfirst", "nanlast", "nanargmin", "nanargmax", "cumsum", "cumprod", "cummax", "cummin", arbitrary, "sort", ) def benchmark_data(size=5e5, seed=100): rnd = np.random.RandomState(seed=seed) group_idx = rnd.randint(0, int(1e3), int(size)) a = rnd.random_sample(group_idx.size) a[a > 0.8] = 0 nana = a.copy() nana[(nana < 0.2) & (nana != 0)] = np.nan nan_share = np.mean(np.isnan(nana)) assert 0.15 < nan_share < 0.25, f"{nan_share * 100:3f}% nans" return a, nana, group_idx def benchmark(implementations, repeat=5, size=5e5, seed=100, raise_errors=False): a, nana, group_idx = benchmark_data(size=size, seed=seed) print("function" + "".join(impl.__name__.rsplit("_", 1)[1].rjust(14) for impl in implementations)) print("-" * (9 + 14 * len(implementations))) for func in func_list: func_name = getattr(func, "__name__", func) print(func_name.ljust(9), end="") results = [] used_a = nana if "nan" in func_name else a for impl in implementations: if impl is None: print("----".rjust(14), end="") continue aggregatefunc = impl.aggregate try: res = aggregatefunc(group_idx, used_a, func=func) except NotImplementedError: print("----".rjust(14), end="") continue except Exception: if raise_errors: raise print("ERROR".rjust(14), end="") else: results.append(res) try: np.testing.assert_array_almost_equal(res, results[0]) except AssertionError: print("FAIL".rjust(14), end="") else: t0 = min( timeit.Timer(lambda: aggregatefunc(group_idx, used_a, func=func)).repeat( repeat=repeat, number=1 ) ) print(f"{t0 * 1000:.3f}".rjust(14), end="") sys.stdout.flush() print() implementation_names = [impl.__name__.rsplit("_", 1)[1] for impl in implementations] postfix = "" if "numba" in implementation_names: import numba postfix += f", Numba {numba.__version__}" if "pandas" in implementation_names: import pandas postfix += f", Pandas {pandas.__version__}" print( f"{platform.system()}({platform.machine()}), Python {sys.version.split()[0]}, Numpy {np.version.version}" f"{postfix}" ) if __name__ == "__main__": implementations = _implementations if "--purepy" in sys.argv else _implementations[1:] implementations = implementations if "--pandas" in sys.argv else implementations[:-1] benchmark(implementations, raise_errors=False) numpy-groupies-0.10.2/numpy_groupies/benchmarks/simple.py000066400000000000000000000103651450707334100236530ustar00rootroot00000000000000#!/usr/bin/python -B import timeit import numpy as np from numpy_groupies import aggregate_np, aggregate_py, aggregate_ufunc from numpy_groupies.aggregate_pandas import aggregate as aggregate_pd from numpy_groupies.utils import aliasing def aggregate_group_loop(*args, **kwargs): """wraps func in lambda which prevents aggregate_numpy from recognising and optimising it. Instead it groups and loops.""" func = kwargs["func"] del kwargs["func"] return aggregate_np(*args, func=lambda x: func(x), **kwargs) print("-----simple examples----------") test_a = np.array([12.0, 3.2, -15, 88, 12.9]) test_group_idx = np.array([1, 0, 1, 4, 1]) print("test_a: ", test_a) print("test_group_idx: ", test_group_idx) print("aggregate(test_group_idx, test_a):") print(aggregate_np(test_group_idx, test_a)) # group vals by idx and sum # array([3.2, 9.9, 0., 0., 88.]) print("aggregate(test_group_idx, test_a, sz=8, func='min', fill_value=np.nan):") print(aggregate_np(test_group_idx, test_a, size=8, func="min", fill_value=np.nan)) # array([3.2, -15., nan, 88., nan, nan, nan, nan]) print("aggregate_py(test_group_idx, test_a, sz=5, func=lambda x: ' + '.join(str(xx) for xx in x),fill_value='')") print( aggregate_py( test_group_idx, test_a, size=5, func=lambda x: " + ".join(str(xx) for xx in x), fill_value="", ) ) print("") print("---------testing--------------") print("compare against group-and-loop with numpy") testable_funcs = {aliasing[f]: f for f in (np.sum, np.prod, np.any, np.all, np.min, np.max, np.std, np.var, np.mean)} test_group_idx = np.random.randint(0, int(1e3), int(1e5)) test_a = np.random.rand(int(1e5)) * 100 - 50 test_a[test_a > 25] = 0 # for use with bool functions for name, f in testable_funcs.items(): numpy_loop_group = aggregate_group_loop(test_group_idx, test_a, func=f) for acc_func, acc_name in [ (aggregate_np, "np-optimised"), (aggregate_ufunc, "np-ufunc-at"), (aggregate_py, "purepy"), (aggregate_pd, "pandas"), ]: try: test_out = acc_func(test_group_idx, test_a, func=name) test_out = np.asarray(test_out) if not np.allclose(test_out, numpy_loop_group.astype(test_out.dtype)): print( name, acc_name, "FAILED test, output: [" + acc_name + "; correct]...", ) print(np.vstack((test_out, numpy_loop_group))) else: print(name, acc_name, "PASSED test") except NotImplementedError: print(name, acc_name, "NOT IMPLEMENTED") print("") print("----------benchmarking-------------") print("Note that the actual observed speedup depends on a variety of properties of the input.") print("Here we are using 100,000 indices uniformly picked from [0, 1000).") print("Specifically, about 25% of the values are 0 (for use with bool operations),") print("the remainder are uniformly distributed on [-50,25).") print("Times are scaled to 10 repetitions (actual number of reps used may not be 10).") print( "".join( [ "function".rjust(8), "pure-py".rjust(14), "np-grouploop".rjust(14), "np-ufuncat".rjust(14), "np-optimised".rjust(14), "pandas".rjust(14), "ratio".rjust(15), ] ) ) for name, f in testable_funcs.items(): print(name.rjust(8), end="") times = [None] * 5 for ii, acc_func in enumerate( [ aggregate_py, aggregate_group_loop, aggregate_ufunc, aggregate_np, aggregate_pd, ] ): try: func = f if acc_func is aggregate_group_loop else name reps = 3 if acc_func is aggregate_py else 20 times[ii] = ( timeit.Timer(lambda: acc_func(test_group_idx, test_a, func=func)).timeit(number=reps) / reps * 10 ) print(f"{times[ii] * 1000:.1f}ms".rjust(13), end="") except NotImplementedError: print("no-impl".rjust(13), end="") denom = min(t for t in times if t is not None) ratios = [("-".center(4) if t is None else str(round(t / denom, 1))).center(5) for t in times] print(" ", (":".join(ratios))) numpy-groupies-0.10.2/numpy_groupies/tests/000077500000000000000000000000001450707334100210305ustar00rootroot00000000000000numpy-groupies-0.10.2/numpy_groupies/tests/__init__.py000066400000000000000000000041551450707334100231460ustar00rootroot00000000000000from functools import wraps import pytest from .. import aggregate_numpy, aggregate_numpy_ufunc, aggregate_purepy try: from .. import aggregate_numba except ImportError: aggregate_numba = None try: from .. import aggregate_pandas except ImportError: aggregate_pandas = None _implementations = [ aggregate_purepy, aggregate_numpy_ufunc, aggregate_numpy, aggregate_numba, aggregate_pandas, ] _implementations = [i for i in _implementations if i is not None] def _impl_name(impl): if not impl or type(impl).__name__ == "NotSetType": return return impl.__name__.rsplit("aggregate_", 1)[1].rsplit("_", 1)[-1] _not_implemented_by_impl_name = { "numpy": ("cumprod", "cummax", "cummin"), "purepy": ("cumsum", "cumprod", "cummax", "cummin", "sumofsquares"), "numba": ("array", "list", "sort"), "pandas": ("array", "list", "sort", "sumofsquares", "nansumofsquares"), "ufunc": "NO_CHECK", } def _wrap_notimplemented_skip(impl, name=None): """Some implementations lack some functionality. That's ok, let's skip that instead of raising errors.""" @wraps(impl) def try_skip(*args, **kwargs): try: return impl(*args, **kwargs) except NotImplementedError as e: impl_name = impl.__module__.split("_")[-1] func = kwargs.pop("func", None) if callable(func): func = func.__name__ not_implemented_ok = _not_implemented_by_impl_name.get(impl_name, []) if not_implemented_ok == "NO_CHECK" or func in not_implemented_ok: pytest.skip("Functionality not implemented") else: raise e if name: try_skip.__name__ = name return try_skip func_list = ( "sum", "prod", "min", "max", "all", "any", "mean", "std", "var", "len", "argmin", "argmax", "anynan", "allnan", "cumsum", "sumofsquares", "nansum", "nanprod", "nanmin", "nanmax", "nanmean", "nanstd", "nanvar", "nanlen", "nanargmin", "nanargmax", "nansumofsquares", ) numpy-groupies-0.10.2/numpy_groupies/tests/test_compare.py000066400000000000000000000116771450707334100241030ustar00rootroot00000000000000""" In this test, aggregate_numpy is taken as a reference implementation and this results are compared against the results of the other implementations. Implementations may throw NotImplementedError in order to show missing functionality without throwing test errors. """ from itertools import product import numpy as np import pytest from . import ( _impl_name, _wrap_notimplemented_skip, aggregate_numba, aggregate_numpy, aggregate_numpy_ufunc, aggregate_pandas, aggregate_purepy, func_list, ) class AttrDict(dict): __getattr__ = dict.__getitem__ TEST_PAIRS = ["np/py", "ufunc/np", "numba/np", "pandas/np"] @pytest.fixture(params=TEST_PAIRS, scope="module") def aggregate_cmp(request, seed=100): test_pair = request.param if test_pair == "np/py": # Some functions in purepy are not implemented func_ref = _wrap_notimplemented_skip(aggregate_purepy.aggregate) func = aggregate_numpy.aggregate group_cnt = 100 else: group_cnt = 1000 func_ref = aggregate_numpy.aggregate if "ufunc" in request.param: impl = aggregate_numpy_ufunc elif "numba" in request.param: impl = aggregate_numba elif "pandas" in request.param: impl = aggregate_pandas else: impl = None if not impl: pytest.skip("Implementation not available") name = _impl_name(impl) func = _wrap_notimplemented_skip(impl.aggregate, "aggregate_" + name) rnd = np.random.RandomState(seed=seed) # Gives 100000 duplicates of size 10 each group_idx = np.repeat(np.arange(group_cnt), 2) rnd.shuffle(group_idx) group_idx = np.repeat(group_idx, 10) a = rnd.randn(group_idx.size) nana = a.copy() nana[::3] = np.nan nana[: (len(nana) // 2)] = np.nan somea = a.copy() somea[somea < 0.3] = 0 somea[::31] = np.nan return AttrDict(locals()) def _deselect_purepy(aggregate_cmp, *args, **kwargs): # purepy implementation does not handle ndim arrays # This is a won't fix and should be deselected instead of skipped return aggregate_cmp.endswith("py") def _deselect_purepy_nanfuncs(aggregate_cmp, func, *args, **kwargs): # purepy implementation does not handle nan values correctly # This is a won't fix and should be deselected instead of skipped return "nan" in getattr(func, "__name__", func) and aggregate_cmp.endswith("py") def func_arbitrary(iterator): tmp = 0 for x in iterator: tmp += x * x return tmp def func_preserve_order(iterator): tmp = 0 for i, x in enumerate(iterator, 1): tmp += x**i return tmp @pytest.mark.filterwarnings("ignore:numpy.ufunc size changed") @pytest.mark.deselect_if(func=_deselect_purepy_nanfuncs) @pytest.mark.parametrize("fill_value", [0, 1, np.nan]) @pytest.mark.parametrize("func", func_list, ids=lambda x: getattr(x, "__name__", x)) def test_cmp(aggregate_cmp, func, fill_value, decimal=10): is_nanfunc = "nan" in getattr(func, "__name__", func) a = aggregate_cmp.nana if is_nanfunc else aggregate_cmp.a try: ref = aggregate_cmp.func_ref(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) except ValueError: with pytest.raises(ValueError): aggregate_cmp.func(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) else: try: res = aggregate_cmp.func(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) except ValueError: if np.isnan(fill_value) and aggregate_cmp.test_pair.endswith("py"): pytest.skip( "pure python version uses lists and does not raise ValueErrors when inserting nan into integers" ) else: raise if isinstance(ref, np.ndarray): assert res.dtype == ref.dtype try: np.testing.assert_allclose(res, ref, rtol=10**-decimal) except AssertionError: if "arg" in func and aggregate_cmp.test_pair.startswith("pandas"): pytest.skip("pandas doesn't fill indices for all-nan groups with fill_value, but with -inf instead") else: raise @pytest.mark.deselect_if(func=_deselect_purepy) @pytest.mark.parametrize(["ndim", "order"], product([2, 3], ["C", "F"])) def test_cmp_ndim(aggregate_cmp, ndim, order, outsize=100, decimal=14): nindices = int(outsize**ndim) outshape = tuple([outsize] * ndim) group_idx = np.random.randint(0, outsize, size=(ndim, nindices)) a = np.random.random(group_idx.shape[1]) res = aggregate_cmp.func(group_idx, a, size=outshape, order=order) ref = aggregate_cmp.func_ref(group_idx, a, size=outshape, order=order) if ndim > 1 and order == "F": # 1d arrays always return False here assert np.isfortran(res) else: assert not np.isfortran(res) assert res.shape == outshape np.testing.assert_array_almost_equal(res, ref, decimal=decimal) numpy-groupies-0.10.2/numpy_groupies/tests/test_generic.py000066400000000000000000000464571450707334100240750ustar00rootroot00000000000000""" Tests, that are run against all implemented versions of aggregate. """ import itertools import warnings import numpy as np import pytest from . import _impl_name, _implementations, _wrap_notimplemented_skip, func_list @pytest.fixture(params=_implementations, ids=_impl_name) def aggregate_all(request): impl = request.param if impl is None: pytest.skip("Implementation not available") name = _impl_name(impl) return _wrap_notimplemented_skip(impl.aggregate, "aggregate_" + name) def _deselect_purepy(aggregate_all, *args, **kwargs): # purepy implementations does not handle nan values and ndim correctly. # So it needs to be excluded from several tests.""" return aggregate_all.__name__.endswith("purepy") def _deselect_purepy_and_pandas(aggregate_all, *args, **kwargs): # purepy and pandas implementation handle some nan cases differently. # So they need to be excluded from several tests.""" return aggregate_all.__name__.endswith(("pandas", "purepy")) def _deselect_purepy_and_invalid_axis(aggregate_all, size, axis, *args, **kwargs): if axis >= len(size): return True if aggregate_all.__name__.endswith("purepy"): # purepy does not handle axis parameter return True def test_preserve_missing(aggregate_all): res = aggregate_all(np.array([0, 1, 3, 1, 3]), np.arange(101, 106, dtype=int)) np.testing.assert_array_equal(res, np.array([101, 206, 0, 208])) if not isinstance(res, list): assert "int" in res.dtype.name @pytest.mark.parametrize("group_idx_type", [int, "uint32", "uint64"]) def test_uint_group_idx(aggregate_all, group_idx_type): group_idx = np.array([1, 1, 2, 2, 2, 2, 4, 4], dtype=group_idx_type) res = aggregate_all(group_idx, np.ones(group_idx.size), dtype=int) np.testing.assert_array_equal(res, np.array([0, 2, 4, 0, 2])) if not isinstance(res, list): assert "int" in res.dtype.name def test_start_with_offset(aggregate_all): group_idx = np.array([1, 1, 2, 2, 2, 2, 4, 4]) res = aggregate_all(group_idx, np.ones(group_idx.size), dtype=int) np.testing.assert_array_equal(res, np.array([0, 2, 4, 0, 2])) if not isinstance(res, list): assert "int" in res.dtype.name @pytest.mark.parametrize("floatfunc", [np.std, np.var, np.mean], ids=lambda x: x.__name__) def test_float_enforcement(aggregate_all, floatfunc): group_idx = np.arange(10).repeat(3) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, floatfunc) if not isinstance(res, list): assert "float" in res.dtype.name assert np.all(np.array(res) > 0) def test_start_with_offset_prod(aggregate_all): group_idx = np.array([2, 2, 4, 4, 4, 7, 7, 7]) res = aggregate_all(group_idx, group_idx, func=np.prod, dtype=int) np.testing.assert_array_equal(res, np.array([0, 0, 4, 0, 64, 0, 0, 343])) def test_no_negative_indices(aggregate_all): for pos in (0, 10, -1): group_idx = np.arange(5).repeat(5) group_idx[pos] = -1 pytest.raises(ValueError, aggregate_all, group_idx, np.arange(len(group_idx))) def test_parameter_missing(aggregate_all): pytest.raises(TypeError, aggregate_all, np.arange(5)) def test_shape_mismatch(aggregate_all): pytest.raises(ValueError, aggregate_all, np.array((1, 2, 3)), np.array((1, 2))) def test_create_lists(aggregate_all): res = aggregate_all(np.array([0, 1, 3, 1, 3]), np.arange(101, 106, dtype=int), func=list) np.testing.assert_array_equal(np.array(res[0]), np.array([101])) assert res[2] == 0 np.testing.assert_array_equal(np.array(res[3]), np.array([103, 105])) def test_item_counting(aggregate_all): group_idx = np.array([0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 6, 5, 4, 3, 8, 8]) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=lambda x: len(x) > 1) np.testing.assert_array_equal(res, np.array([0, 0, 0, 1, 1, 1, 0, 0, 1])) @pytest.mark.parametrize(["func", "fill_value"], [(np.array, None), (np.sum, -1)], ids=["array", "sum"]) def test_fill_value(aggregate_all, func, fill_value): group_idx = np.array([0, 2, 2], dtype=int) res = aggregate_all( group_idx, np.arange(len(group_idx), dtype=int), func=func, fill_value=fill_value, ) assert res[1] == fill_value @pytest.mark.parametrize("order", ["C", "F"]) def test_array_ordering(aggregate_all, order, size=10): mat = np.zeros((size, size), order=order, dtype=float) mat.flat[:] = np.arange(size * size) assert aggregate_all(np.zeros(size, dtype=int), mat[0, :], order=order)[0] == sum(range(size)) @pytest.mark.deselect_if(func=_deselect_purepy) @pytest.mark.parametrize("size", [None, (10, 2)]) def test_ndim_group_idx(aggregate_all, size): group_idx = np.vstack((np.repeat(np.arange(10), 10), np.repeat([0, 1], 50))) aggregate_all(group_idx, 1, size=size) @pytest.mark.deselect_if(func=_deselect_purepy) @pytest.mark.parametrize(["ndim", "order"], itertools.product([1, 2, 3], ["C", "F"])) def test_ndim_indexing(aggregate_all, ndim, order, outsize=10): nindices = int(outsize**ndim) outshape = tuple([outsize] * ndim) group_idx = np.random.randint(0, outsize, size=(ndim, nindices)) a = np.random.random(group_idx.shape[1]) res = aggregate_all(group_idx, a, size=outshape, order=order) if ndim > 1 and order == "F": # 1d arrays always return False here assert np.isfortran(res) else: assert not np.isfortran(res) assert res.shape == outshape def test_len(aggregate_all, group_size=5): group_idx = np.arange(0, 100, 2, dtype=int).repeat(group_size) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func="len") ref = aggregate_all(group_idx, 1, func="sum") if isinstance(res, np.ndarray): assert issubclass(res.dtype.type, np.integer) else: assert isinstance(res[0], int) np.testing.assert_array_equal(res, ref) group_idx = np.arange(0, 100, dtype=int).repeat(group_size) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=len) if isinstance(res, np.ndarray): assert np.all(res == group_size) else: assert all(x == group_size for x in res) def test_nan_len(aggregate_all): group_idx = np.arange(0, 20, 2, dtype=int).repeat(5) a = np.random.random(group_idx.size) a[::4] = np.nan a[::5] = np.nan res = aggregate_all(group_idx, a, func="nanlen") ref = aggregate_all(group_idx[~np.isnan(a)], 1, func="sum") if isinstance(res, np.ndarray): assert issubclass(res.dtype.type, np.integer) else: assert isinstance(res[0], int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("first_last", ["first", "last"]) def test_first_last(aggregate_all, first_last): group_idx = np.arange(0, 100, 2, dtype=int).repeat(5) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=first_last, fill_value=-1) ref = np.zeros(np.max(group_idx) + 1) ref.fill(-1) ref[::2] = np.arange(0 if first_last == "first" else 4, group_idx.size, 5, dtype=int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize(["first_last", "nanoffset"], itertools.product(["nanfirst", "nanlast"], [0, 2, 4])) def test_nan_first_last(aggregate_all, first_last, nanoffset): group_idx = np.arange(0, 100, 2, dtype=int).repeat(5) a = np.arange(group_idx.size, dtype=float) a[nanoffset::5] = np.nan res = aggregate_all(group_idx, a, func=first_last, fill_value=-1) ref = np.zeros(np.max(group_idx) + 1) ref.fill(-1) if first_last == "nanfirst": ref_offset = 1 if nanoffset == 0 else 0 else: ref_offset = 3 if nanoffset == 4 else 4 ref[::2] = np.arange(ref_offset, group_idx.size, 5, dtype=int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize(["func", "ddof"], itertools.product(["var", "std"], [0, 1, 2])) def test_ddof(aggregate_all, func, ddof, size=20): group_idx = np.zeros(20, dtype=int) a = np.random.random(group_idx.size) res = aggregate_all(group_idx, a, func, ddof=ddof) ref_func = {"std": np.std, "var": np.var}.get(func) ref = ref_func(a, ddof=ddof) assert abs(res[0] - ref) < 1e-10 @pytest.mark.parametrize("func", ["sum", "prod", "mean", "var", "std"]) def test_scalar_input(aggregate_all, func): group_idx = np.arange(0, 100, dtype=int).repeat(5) if func not in ("sum", "prod"): pytest.raises((ValueError, NotImplementedError), aggregate_all, group_idx, 1, func=func) else: res = aggregate_all(group_idx, 1, func=func) ref = aggregate_all(group_idx, np.ones_like(group_idx, dtype=int), func=func) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("func", ["sum", "prod", "mean", "var", "std", "all", "any"]) def test_nan_input(aggregate_all, func, groups=100): if aggregate_all.__name__.endswith("pandas"): pytest.skip("pandas always skips nan values") group_idx = np.arange(0, groups, dtype=int).repeat(5) a = np.random.random(group_idx.size) a[::2] = np.nan if func in ("all", "any"): ref = np.ones(groups, dtype=bool) else: ref = np.full(groups, np.nan, dtype=float) res = aggregate_all(group_idx, a, func=func) np.testing.assert_array_equal(res, ref) def test_nan_input_len(aggregate_all, groups=100, group_size=5): if aggregate_all.__name__.endswith("pandas"): pytest.skip("pandas always skips nan values") group_idx = np.arange(0, groups, dtype=int).repeat(group_size) a = np.random.random(len(group_idx)) a[::2] = np.nan ref = np.full(groups, group_size, dtype=int) res = aggregate_all(group_idx, a, func=len) np.testing.assert_array_equal(res, ref) def test_argmin_argmax_nonans(aggregate_all): group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.array([4, 4, 3, 1, 10, 9, 9, 11]) res = aggregate_all(group_idx, a, func="argmax", fill_value=-1) np.testing.assert_array_equal(res, [0, -1, -1, 7]) res = aggregate_all(group_idx, a, func="argmin", fill_value=-1) np.testing.assert_array_equal(res, [3, -1, -1, 5]) @pytest.mark.deselect_if(func=_deselect_purepy) def test_argmin_argmax_nans(aggregate_all): if aggregate_all.__name__.endswith("pandas"): pytest.skip("pandas always ignores nans") group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.array([4, 4, 3, 1, np.nan, 1, 2, 3]) res = aggregate_all(group_idx, a, func="argmax", fill_value=-1) np.testing.assert_array_equal(res, [0, -1, -1, -1]) res = aggregate_all(group_idx, a, func="argmin", fill_value=-1) np.testing.assert_array_equal(res, [3, -1, -1, -1]) @pytest.mark.deselect_if(func=_deselect_purepy) def test_nanargmin_nanargmax_nans(aggregate_all): if aggregate_all.__name__.endswith("pandas"): pytest.skip("pandas doesn't fill indices for all-nan groups with fill_value but with -inf instead") group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.array([4, 4, np.nan, 1, np.nan, np.nan, np.nan, np.nan]) res = aggregate_all(group_idx, a, func="nanargmax", fill_value=-1) np.testing.assert_array_equal(res, [0, -1, -1, -1]) res = aggregate_all(group_idx, a, func="nanargmin", fill_value=-1) np.testing.assert_array_equal(res, [3, -1, -1, -1]) def test_nanargmin_nanargmax_nonans(aggregate_all): group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.array([4, 4, 3, 1, 10, 9, 9, 11]) res = aggregate_all(group_idx, a, func="nanargmax", fill_value=-1) np.testing.assert_array_equal(res, [0, -1, -1, 7]) res = aggregate_all(group_idx, a, func="nanargmin", fill_value=-1) np.testing.assert_array_equal(res, [3, -1, -1, 5]) def test_min_max_inf(aggregate_all): # https://github.com/ml31415/numpy-groupies/issues/40 res = aggregate_all( np.array([0, 1, 2, 0, 1, 2]), np.array([-np.inf, 0, -np.inf, -np.inf, 0, 0]), func="max", ) np.testing.assert_array_equal(res, [-np.inf, 0, 0]) res = aggregate_all( np.array([0, 1, 2, 0, 1, 2]), np.array([np.inf, 0, np.inf, np.inf, 0, 0]), func="min", ) np.testing.assert_array_equal(res, [np.inf, 0, 0]) def test_argmin_argmax_inf(aggregate_all): # https://github.com/ml31415/numpy-groupies/issues/40 res = aggregate_all( np.array([0, 1, 2, 0, 1, 2]), np.array([-np.inf, 0, -np.inf, -np.inf, 0, 0]), func="argmax", fill_value=-1, ) np.testing.assert_array_equal(res, [0, 1, 5]) res = aggregate_all( np.array([0, 1, 2, 0, 1, 2]), np.array([np.inf, 0, np.inf, np.inf, 0, 0]), func="argmin", fill_value=-1, ) np.testing.assert_array_equal(res, [0, 1, 5]) def test_mean(aggregate_all): group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.arange(len(group_idx)) res = aggregate_all(group_idx, a, func="mean") np.testing.assert_array_equal(res, [1.5, 0, 0, 5.5]) def test_cumsum(aggregate_all): group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) ref = np.array([3, 4, 5, 6, 15, 9, 15, 22, 7, 0, 15, 17, 6, 14, 31, 39]) res = aggregate_all(group_idx, a, func="cumsum") np.testing.assert_array_equal(res, ref) @pytest.mark.deselect_if(func=_deselect_purepy_and_pandas) def test_nancumsum(aggregate_all): # https://github.com/ml31415/numpy-groupies/issues/79 group_idx = [0, 0, 0, 1, 1, 0, 0] a = [2, 2, np.nan, 2, 2, 2, 2] ref = [2., 4., 4., 2., 4., 6., 8.] res = aggregate_all(group_idx, a, func="nancumsum") np.testing.assert_array_equal(res, ref) def test_cummax(aggregate_all): group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) ref = np.array([3, 4, 4, 3, 9, 9, 9, 9, 7, 0, 8, 9, 4, 8, 9, 9]) res = aggregate_all(group_idx, a, func="cummax") np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("order", ["normal", "reverse"]) def test_list_ordering(aggregate_all, order): group_idx = np.repeat(np.arange(5), 4) a = np.arange(group_idx.size) if order == "reverse": a = a[::-1] ref = a[:4] res = aggregate_all(group_idx, a, func=list) np.testing.assert_array_equal(np.array(res[0]), ref) @pytest.mark.parametrize("order", ["normal", "reverse"]) def test_sort(aggregate_all, order): group_idx = np.array([3, 3, 3, 2, 2, 2, 1, 1, 1]) a = np.array([3, 2, 1, 3, 4, 5, 5, 10, 1]) ref_normal = np.array([1, 2, 3, 3, 4, 5, 1, 5, 10]) ref_reverse = np.array([3, 2, 1, 5, 4, 3, 10, 5, 1]) reverse = order == "reverse" ref = ref_reverse if reverse else ref_normal res = aggregate_all(group_idx, a, func="sort", reverse=reverse) np.testing.assert_array_equal(res, ref) @pytest.mark.deselect_if(func=_deselect_purepy_and_invalid_axis) @pytest.mark.parametrize("axis", (0, 1)) @pytest.mark.parametrize("size", ((12,), (12, 5))) @pytest.mark.parametrize("func", func_list) def test_along_axis(aggregate_all, func, size, axis): group_idx = np.zeros(size[axis], dtype=int) a = np.random.randn(*size) # add some NaNs to test out nan-skipping if "nan" in func and "nanarg" not in func: a[[1, 4, 5], ...] = np.nan elif "nanarg" in func and a.ndim > 1: a[[1, 4, 5], 1] = np.nan if func in ["any", "all"]: a = a > 0.5 # construct expected values for all cases if func == "len": expected = np.array(size[axis]) elif func == "nanlen": expected = np.array((~np.isnan(a)).sum(axis=axis)) elif func == "anynan": expected = np.isnan(a).any(axis=axis) elif func == "allnan": expected = np.isnan(a).all(axis=axis) elif func == "sumofsquares": expected = np.sum(a * a, axis=axis) elif func == "nansumofsquares": expected = np.nansum(a * a, axis=axis) else: with warnings.catch_warnings(): # Filter expected warnings: # - RuntimeWarning: All-NaN slice encountered # - RuntimeWarning: Mean of empty slice # - RuntimeWarning: Degrees of freedom <= 0 for slice. warnings.simplefilter("ignore", RuntimeWarning) expected = getattr(np, func)(a, axis=axis) # The default fill_value is 0, the following makes the output match numpy fill_value = { "nanprod": 1, "nanvar": np.nan, "nanstd": np.nan, "nanmax": np.nan, "nanmin": np.nan, "nanmean": np.nan, }.get(func, 0) actual = aggregate_all(group_idx, a, axis=axis, func=func, fill_value=fill_value) assert actual.ndim == a.ndim # argmin, argmax don't support keepdims, so we can't use that to construct expected # instead we squeeze out the extra dims in actual. np.testing.assert_allclose(actual.squeeze(), expected) @pytest.mark.deselect_if(func=_deselect_purepy) def test_not_last_axis_reduction(aggregate_all): group_idx = np.array([1, 2, 2, 0, 1]) a = np.array([[1.0, 2.0], [4.0, 4.0], [5.0, 2.0], [np.nan, 3.0], [8.0, 7.0]]) func = "nanmax" fill_value = np.nan axis = 0 actual = aggregate_all(group_idx, a, axis=axis, func=func, fill_value=fill_value) expected = np.array([[np.nan, 3.0], [8.0, 7.0], [5.0, 4.0]]) np.testing.assert_allclose(expected, actual) @pytest.mark.deselect_if(func=_deselect_purepy) def test_custom_callable(aggregate_all): def custom_callable(x): return x.sum() size = (10,) axis = -1 group_idx = np.zeros(size, dtype=int) a = np.random.randn(*size) expected = a.sum(axis=axis, keepdims=True) actual = aggregate_all(group_idx, a, axis=axis, func=custom_callable, fill_value=0) assert actual.ndim == a.ndim np.testing.assert_allclose(actual, expected) @pytest.mark.deselect_if(func=_deselect_purepy) def test_argreduction_nD_array_1D_idx(aggregate_all): # https://github.com/ml31415/numpy-groupies/issues/41 group_idx = np.array([0, 0, 2, 2, 2, 1, 1, 2, 2, 1, 1, 0], dtype=int) a = np.array([[1] * 12, [1] * 12]) actual = aggregate_all(group_idx, a, axis=-1, func="argmax") expected = np.array([[0, 5, 2], [0, 5, 2]]) np.testing.assert_equal(actual, expected) @pytest.mark.deselect_if(func=_deselect_purepy) def test_argreduction_negative_fill_value(aggregate_all): if aggregate_all.__name__.endswith("pandas"): pytest.skip("pandas always skips nan values") group_idx = np.array([0, 0, 2, 2, 2, 1, 1, 2, 2, 1, 1, 0], dtype=int) a = np.array([[1] * 12, [np.nan] * 12]) actual = aggregate_all(group_idx, a, axis=-1, fill_value=-1, func="argmax") expected = np.array([[0, 5, 2], [-1, -1, -1]]) np.testing.assert_equal(actual, expected) @pytest.mark.deselect_if(func=_deselect_purepy) @pytest.mark.parametrize("nan_inds", (None, tuple([[1, 4, 5], Ellipsis]), tuple((1, (0, 1, 2, 3))))) @pytest.mark.parametrize("ddof", (0, 1)) @pytest.mark.parametrize("func", ("nanvar", "nanstd")) def test_var_with_nan_fill_value(aggregate_all, ddof, nan_inds, func): a = np.ones((12, 5)) group_idx = np.zeros(a.shape[-1:], dtype=int) if nan_inds is not None: a[nan_inds] = np.nan with warnings.catch_warnings(): # Filter RuntimeWarning: Degrees of freedom <= 0 for slice. warnings.simplefilter("ignore", RuntimeWarning) expected = getattr(np, func)(a, keepdims=True, axis=-1, ddof=ddof) actual = aggregate_all(group_idx, a, axis=-1, fill_value=np.nan, func=func, ddof=ddof) np.testing.assert_equal(actual, expected) numpy-groupies-0.10.2/numpy_groupies/tests/test_indices.py000066400000000000000000000017711450707334100240650ustar00rootroot00000000000000import numpy as np import pytest from . import _impl_name, aggregate_numba _implementations = [aggregate_numba] _implementations = [i for i in _implementations if i is not None] @pytest.fixture(params=_implementations, ids=_impl_name) def aggregate_nb_wv(request): if request.param is None: pytest.skip("Implementation not available") return request.param def test_step_indices_length(aggregate_nb_wv): group_idx = np.array([1, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2], dtype=int) for _ in range(20): np.random.shuffle(group_idx) step_cnt_ref = np.count_nonzero(np.diff(group_idx)) assert aggregate_nb_wv.step_count(group_idx) == step_cnt_ref + 1 assert len(aggregate_nb_wv.step_indices(group_idx)) == step_cnt_ref + 2 def test_step_indices_fields(aggregate_nb_wv): group_idx = np.array([1, 1, 1, 2, 2, 3, 3, 4, 5, 2, 2], dtype=int) steps = aggregate_nb_wv.step_indices(group_idx) np.testing.assert_array_equal(steps, np.array([0, 3, 5, 7, 8, 9, 11])) numpy-groupies-0.10.2/numpy_groupies/tests/test_utils.py000066400000000000000000000014731450707334100236060ustar00rootroot00000000000000import numpy as np from ..utils import check_dtype, unpack def test_check_dtype(): dtype = check_dtype(None, "mean", np.arange(10, dtype=int), 10) assert np.issubdtype(dtype, np.floating) def test_unpack(): """Keep this test, in case unpack might get reimplemented again at some point.""" group_idx = np.arange(10) np.random.shuffle(group_idx) group_idx = np.repeat(group_idx, 3) vals = np.random.randn(np.max(group_idx) + 1) np.testing.assert_array_equal(unpack(group_idx, vals), vals[group_idx]) def test_unpack_long(): group_idx = np.repeat(np.arange(10000), 20) a = np.arange(group_idx.size, dtype=int) a = np.random.randn(np.max(group_idx) + 1) vals = np.random.randn(np.max(group_idx) + 1) np.testing.assert_array_equal(unpack(group_idx, vals), vals[group_idx]) numpy-groupies-0.10.2/numpy_groupies/utils.py000066400000000000000000000536411450707334100214110ustar00rootroot00000000000000"""Common functionality for all aggregate implementations.""" import numpy as np aggregate_common_doc = """ See readme file at https://github.com/ml31415/numpy-groupies for a full description. Below we reproduce the "Full description of inputs" section from that readme, note that the text below makes references to other portions of the readme that are not shown here. group_idx: this is an array of non-negative integers, to be used as the "labels" with which to group the values in ``a``. Although we have so far assumed that ``group_idx`` is one-dimensional, and the same length as ``a``, it can in fact be two-dimensional (or some form of nested sequences that can be converted to 2D). When ``group_idx`` is 2D, the size of the 0th dimension corresponds to the number of dimensions in the output, i.e. ``group_idx[i,j]`` gives the index into the ith dimension in the output for ``a[j]``. Note that ``a`` should still be 1D (or scalar), with length matching ``group_idx.shape[1]``. a: this is the array of values to be aggregated. See above for a simple demonstration of what this means. ``a`` will normally be a one-dimensional array, however it can also be a scalar in some cases. func: default='sum' the function to use for aggregation. See the section above for details. Note that the simplest way to specify the function is using a string (e.g. ``func='max'``) however a number of aliases are also defined (e.g. you can use the ``func=np.max``, or even ``func=max``, where ``max`` is the builtin function). To check the available aliases see ``utils.py``. size: default=None the shape of the output array. If ``None``, the maximum value in ``group_idx`` will set the size of the output. Note that for multidimensional output you need to list the size of each dimension here, or give ``None``. fill_value: default=0 in the example above, group 2 does not have any data, so requires some kind of filling value - in this case the default of ``0`` is used. If you had set ``fill_value=nan`` or something else, that value would appear instead of ``0`` for the 2 element in the output. Note that there are some subtle interactions between what is permitted for ``fill_value`` and the input/output ``dtype`` - exceptions should be raised in most cases to alert the programmer if issue arrise. order: default='C' this is relevant only for multimensional output. It controls the layout of the output array in memory, can be ``'F'`` for fortran-style. dtype: default=None the ``dtype`` of the output. By default something sensible is chosen based on the input, aggregation function, and ``fill_value``. ddof: default=0 passed through into calculations of variance and standard deviation (see above). """ funcs_common = ( "first last len mean var std allnan anynan max min argmax argmin sumofsquares cumsum cumprod cummax cummin".split() ) funcs_no_separate_nan = frozenset(["sort", "rsort", "array", "allnan", "anynan"]) _alias_str = { "or": "any", "and": "all", "add": "sum", "count": "len", "plus": "sum", "multiply": "prod", "product": "prod", "times": "prod", "amax": "max", "maximum": "max", "amin": "min", "minimum": "min", "split": "array", "splice": "array", "sorted": "sort", "asort": "sort", "asorted": "sort", "rsorted": "sort", "dsort": "sort", "dsorted": "rsort", } _alias_builtin = { all: "all", any: "any", len: "len", max: "max", min: "min", sum: "sum", sorted: "sort", slice: "array", list: "array", } _alias_numpy = { np.add: "sum", np.sum: "sum", np.any: "any", np.all: "all", np.multiply: "prod", np.prod: "prod", np.amin: "min", np.min: "min", np.minimum: "min", np.amax: "max", np.max: "max", np.maximum: "max", np.argmax: "argmax", np.argmin: "argmin", np.mean: "mean", np.std: "std", np.var: "var", np.array: "array", np.asarray: "array", np.sort: "sort", np.cumsum: "cumsum", np.cumprod: "cumprod", np.nansum: "nansum", np.nanprod: "nanprod", np.nanmean: "nanmean", np.nanvar: "nanvar", np.nanmax: "nanmax", np.nanmin: "nanmin", np.nanstd: "nanstd", np.nanargmax: "nanargmax", np.nanargmin: "nanargmin", np.nancumsum: "nancumsum", } def get_aliasing(*extra): """ Assembles a dictionary that maps both strings and functions to a list of supported function names. Examples: alias['add'] = 'sum' alias[sorted] = 'sort' This function should only be called during import. """ alias = dict((k, k) for k in funcs_common) alias.update(_alias_str) alias.update((fn, fn) for fn in _alias_builtin.values()) alias.update(_alias_builtin) for d in extra: alias.update(d) alias.update((k, k) for k in set(alias.values())) # Treat nan-functions as firstclass member and add them directly for key in set(alias.values()): if key not in funcs_no_separate_nan and not key.startswith("nan"): key = "nan" + key alias[key] = key return alias aliasing_py = get_aliasing() aliasing = get_aliasing(_alias_numpy) def get_func(func, aliasing, implementations): """Return the key of a found implementation or the func itself""" try: func_str = aliasing[func] except KeyError: if callable(func): return func else: if func_str in implementations: return func_str if func_str.startswith("nan") and func_str[3:] in funcs_no_separate_nan: raise ValueError(f"{func_str[3:]} does not have a nan-version") else: raise NotImplementedError("No such function available") raise ValueError(f"func {func} is neither a valid function string nor a callable object") def check_boolean(x): if x not in (0, 1): raise ValueError("Value not boolean") _next_int_dtype = dict( bool=np.int8, uint8=np.int16, int8=np.int16, uint16=np.int32, int16=np.int32, uint32=np.int64, int32=np.int64, ) _next_float_dtype = dict( float16=np.float32, float32=np.float64, float64=np.complex64, complex64=np.complex128, ) def minimum_dtype(x, dtype=np.bool_): """ Returns the "most basic" dtype which represents `x` properly, which provides at least the same value range as the specified dtype. """ def check_type(x, dtype): try: with np.errstate(invalid="ignore"): converted = np.array(x).astype(dtype) except (ValueError, OverflowError, RuntimeWarning): return False # False if some overflow has happened return converted == x or np.isnan(x) def type_loop(x, dtype, dtype_dict, default=None): while True: try: dtype = np.dtype(dtype_dict[dtype.name]) if check_type(x, dtype): return np.dtype(dtype) except KeyError: if default is not None: return np.dtype(default) raise ValueError(f"Can not determine dtype of {x!r}") dtype = np.dtype(dtype) if check_type(x, dtype): return dtype if np.issubdtype(dtype, np.inexact): return type_loop(x, dtype, _next_float_dtype) else: return type_loop(x, dtype, _next_int_dtype, default=np.float32) def minimum_dtype_scalar(x, dtype, a): if dtype is None: dtype = np.dtype(type(a)) if isinstance(a, (int, float)) else a.dtype return minimum_dtype(x, dtype) _forced_types = { "array": object, "all": bool, "any": bool, "nanall": bool, "nanany": bool, "len": np.int64, "nanlen": np.int64, "allnan": bool, "anynan": bool, "argmax": np.int64, "argmin": np.int64, "nanargmin": np.int64, "nanargmax": np.int64, } _forced_float_types = {"mean", "var", "std", "nanmean", "nanvar", "nanstd"} _forced_same_type = { "min", "max", "first", "last", "nanmin", "nanmax", "nanfirst", "nanlast", } def check_dtype(dtype, func_str, a, n): if np.isscalar(a) or not a.shape: if func_str not in ("sum", "prod", "len"): raise ValueError("scalar inputs are supported only for 'sum', 'prod' and 'len'") a_dtype = np.dtype(type(a)) else: a_dtype = a.dtype if dtype is not None: # dtype set by the user # Careful here: np.bool != np.bool_ ! if np.issubdtype(dtype, np.bool_) and not ("all" in func_str or "any" in func_str): raise TypeError(f"function {func_str} requires a more complex datatype than bool") if not np.issubdtype(dtype, np.integer) and func_str in ("len", "nanlen"): raise TypeError(f"function {func_str} requires an integer datatype") # TODO: Maybe have some more checks here return np.dtype(dtype) else: try: return np.dtype(_forced_types[func_str]) except KeyError: if func_str in _forced_float_types: if np.issubdtype(a_dtype, np.floating): return a_dtype else: return np.dtype(np.float64) else: if func_str == "sum": # Try to guess the minimally required int size if np.issubdtype(a_dtype, np.int64): # It's not getting bigger anymore # TODO: strictly speaking it might need float return np.dtype(np.int64) elif np.issubdtype(a_dtype, np.integer): maxval = np.iinfo(a_dtype).max * n return minimum_dtype(maxval, a_dtype) elif np.issubdtype(a_dtype, np.bool_): return minimum_dtype(n, a_dtype) else: # floating, inexact, whatever return a_dtype elif func_str in _forced_same_type: return a_dtype else: if isinstance(a_dtype, np.integer): return np.dtype(np.int64) else: return a_dtype def minval(fill_value, dtype): dtype = minimum_dtype(fill_value, dtype) if issubclass(dtype.type, np.floating): return -np.inf if issubclass(dtype.type, np.integer): return np.iinfo(dtype).min return np.finfo(dtype).min def maxval(fill_value, dtype): dtype = minimum_dtype(fill_value, dtype) if issubclass(dtype.type, np.floating): return np.inf if issubclass(dtype.type, np.integer): return np.iinfo(dtype).max return np.finfo(dtype).max def check_fill_value(fill_value, dtype, func=None): if func in ("all", "any", "allnan", "anynan"): check_boolean(fill_value) else: try: return dtype.type(fill_value) except ValueError: raise ValueError(f"fill_value must be convertible into {dtype.type.__name__}") def check_group_idx(group_idx, a=None, check_min=True): if a is not None and group_idx.size != a.size: raise ValueError("The size of group_idx must be the same as a.size") if not issubclass(group_idx.dtype.type, np.integer): raise TypeError("group_idx must be of integer type") if check_min and np.min(group_idx) < 0: raise ValueError("group_idx contains negative indices") def _ravel_group_idx(group_idx, a, axis, size, order, method="ravel"): ndim_a = a.ndim # Create the broadcast-ready multidimensional indexing. # Note the user could do this themselves, so this is # very much just a convenience. size_in = int(np.max(group_idx)) + 1 if size is None else size group_idx_in = group_idx group_idx = [] size = [] for ii, s in enumerate(a.shape): if method == "ravel": ii_idx = group_idx_in if ii == axis else np.arange(s) ii_shape = [1] * ndim_a ii_shape[ii] = s group_idx.append(ii_idx.reshape(ii_shape)) size.append(size_in if ii == axis else s) # Use the indexing, and return. It's a bit simpler than # using trying to keep all the logic below happy if method == "ravel": group_idx = np.ravel_multi_index(group_idx, size, order=order, mode="raise") elif method == "offset": group_idx = offset_labels(group_idx_in, a.shape, axis, order, size_in) return group_idx, size def offset_labels(group_idx, inshape, axis, order, size): """ Offset group labels by dimension. This is used when we reduce over a subset of the dimensions of group_idx. It assumes that the reductions dimensions have been flattened in the last dimension Copied from https://stackoverflow.com/questions/46256279/bin-elements-per-row-vectorized-2d-bincount-for-numpy """ newaxes = tuple(ax for ax in range(len(inshape)) if ax != axis) group_idx = np.broadcast_to(np.expand_dims(group_idx, newaxes), inshape) if axis not in (-1, len(inshape) - 1): group_idx = np.moveaxis(group_idx, axis, -1) newshape = group_idx.shape[:-1] + (-1,) group_idx = group_idx + np.arange(np.prod(newshape[:-1]), dtype=int).reshape(newshape) * size if axis not in (-1, len(inshape) - 1): return np.moveaxis(group_idx, -1, axis) else: return group_idx def input_validation( group_idx, a, size=None, order="C", axis=None, ravel_group_idx=True, check_bounds=True, func=None, ): """ Do some fairly extensive checking of group_idx and a, trying to give the user as much help as possible with what is wrong. Also, convert ndim-indexing to 1d indexing. """ if not isinstance(a, (int, float, complex)) and not is_duck_array(a): a = np.asanyarray(a) if not is_duck_array(group_idx): group_idx = np.asanyarray(group_idx) if not np.issubdtype(group_idx.dtype, np.integer): raise TypeError("group_idx must be of integer type") # This check works for multidimensional indexing as well if check_bounds and np.any(group_idx < 0): raise ValueError("negative indices not supported") ndim_idx = np.ndim(group_idx) ndim_a = np.ndim(a) # Deal with the axis arg: if present, then turn 1d indexing into # multi-dimensional indexing along the specified axis. if axis is None: if ndim_a > 1: raise ValueError("a must be scalar or 1 dimensional, use .ravel to flatten. Alternatively specify axis.") elif axis >= ndim_a or axis < -ndim_a: raise ValueError("axis arg too large for np.ndim(a)") else: axis = axis if axis >= 0 else ndim_a + axis # negative indexing if ndim_idx > 1: # TODO: we could support a sequence of axis values for multiple # dimensions of group_idx. raise NotImplementedError("only 1d indexing currently supported with axis arg.") elif a.shape[axis] != len(group_idx): raise ValueError("a.shape[axis] doesn't match length of group_idx.") elif size is not None and not np.isscalar(size): raise NotImplementedError("when using axis arg, size must be None or scalar.") else: is_form_3 = group_idx.ndim == 1 and a.ndim > 1 and axis is not None orig_shape = a.shape if is_form_3 else group_idx.shape if isinstance(func, str) and "arg" in func: unravel_shape = orig_shape else: unravel_shape = None method = "offset" if axis == ndim_a - 1 else "ravel" group_idx, size = _ravel_group_idx(group_idx, a, axis, size, order, method=method) flat_size = np.prod(size) ndim_idx = ndim_a size = orig_shape if is_form_3 and not callable(func) and "cum" in func else size return ( group_idx.ravel(), a.ravel(), flat_size, ndim_idx, size, unravel_shape, ) if ndim_idx == 1: if size is None: size = int(np.max(group_idx)) + 1 else: if not np.isscalar(size): raise ValueError("output size must be scalar or None") if check_bounds and np.any(group_idx > size - 1): raise ValueError(f"one or more indices are too large for size {size}") flat_size = size else: if size is None: size = np.max(group_idx, axis=1).astype(int) + 1 elif np.isscalar(size): raise ValueError(f"output size must be of length {len(group_idx)}") elif len(size) != len(group_idx): raise ValueError(f"{len(size)} sizes given, but {len(group_idx)} output dimensions specified in index") if ravel_group_idx: group_idx = np.ravel_multi_index(group_idx, size, order=order, mode="raise") flat_size = np.prod(size) if not (np.ndim(a) == 0 or len(a) == group_idx.size): raise ValueError("group_idx and a must be of the same length, or a can be scalar") return group_idx, a, flat_size, ndim_idx, size, None # General tools def unpack(group_idx, ret): """ Take an aggregate packed array and uncompress it to the size of group_idx. This is equivalent to ret[group_idx]. """ return ret[group_idx] def allnan(x): return np.all(np.isnan(x)) def anynan(x): return np.any(np.isnan(x)) def nanfirst(x): return x[~np.isnan(x)][0] def nanlast(x): return x[~np.isnan(x)][-1] def multi_arange(n): """By example: # 0 1 2 3 4 5 6 7 8 n = [0, 0, 3, 0, 0, 2, 0, 2, 1] res = [0, 1, 2, 0, 1, 0, 1, 0] That is it is equivalent to something like this : hstack((arange(n_i) for n_i in n)) This version seems quite a bit faster, at least for some possible inputs, and at any rate it encapsulates a task in a function. """ if n.ndim != 1: raise ValueError("n is supposed to be 1d array.") n_mask = n.astype(bool) n_cumsum = np.cumsum(n) ret = np.ones(n_cumsum[-1] + 1, dtype=int) ret[n_cumsum[n_mask]] -= n[n_mask] ret[0] -= 1 return np.cumsum(ret)[:-1] def label_contiguous_1d(X): """ WARNING: API for this function is not liable to change!!! By example: X = [F T T F F T F F F T T T] result = [0 1 1 0 0 2 0 0 0 3 3 3] Or: X = [0 3 3 0 0 5 5 5 1 1 0 2] result = [0 1 1 0 0 2 2 2 3 3 0 4] The ``0`` or ``False`` elements of ``X`` are labeled as ``0`` in the output. If ``X`` is a boolean array, each contiguous block of ``True`` is given an integer label, if ``X`` is not boolean, then each contiguous block of identical values is given an integer label. Integer labels are 1, 2, 3, ..... (i.e. start a 1 and increase by 1 for each block with no skipped numbers.) """ if X.ndim != 1: raise ValueError("this is for 1d masks only.") is_start = np.empty(len(X), dtype=bool) is_start[0] = X[0] # True if X[0] is True or non-zero if X.dtype.kind == "b": is_start[1:] = ~X[:-1] & X[1:] M = X else: M = X.astype(bool) is_start[1:] = X[:-1] != X[1:] is_start[~M] = False L = np.cumsum(is_start) L[~M] = 0 return L def relabel_groups_unique(group_idx): """ See also ``relabel_groups_masked``. keep_group: [0 3 3 3 0 2 5 2 0 1 1 0 3 5 5] ret: [0 3 3 3 0 2 4 2 0 1 1 0 3 4 4] Description of above: unique groups in input was ``1,2,3,5``, i.e. ``4`` was missing, so group 5 was relabled to be ``4``. Relabeling maintains order, just "compressing" the higher numbers to fill gaps. """ keep_group = np.zeros(np.max(group_idx) + 1, dtype=bool) keep_group[0] = True keep_group[group_idx] = True return relabel_groups_masked(group_idx, keep_group) def relabel_groups_masked(group_idx, keep_group): """ group_idx: [0 3 3 3 0 2 5 2 0 1 1 0 3 5 5] 0 1 2 3 4 5 keep_group: [0 1 0 1 1 1] ret: [0 2 2 2 0 0 4 0 0 1 1 0 2 4 4] Description of above in words: remove group 2, and relabel group 3,4, and 5 to be 2, 3 and 4 respectively, in order to fill the gap. Note that group 4 was never used in the input group_idx, but the user supplied mask said to keep group 4, so group 5 is only moved up by one place to fill the gap created by removing group 2. That is, the mask describes which groups to remove, the remaining groups are relabled to remove the gaps created by the falsy elements in ``keep_group``. Note that ``keep_group[0]`` has no particular meaning because it refers to the zero group which cannot be "removed". ``keep_group`` should be bool and ``group_idx`` int. Values in ``group_idx`` can be any order. """ keep_group = keep_group.astype(bool, copy=not keep_group[0]) if not keep_group[0]: # ensuring keep_group[0] is True makes life easier keep_group[0] = True relabel = np.zeros(keep_group.size, dtype=group_idx.dtype) relabel[keep_group] = np.arange(np.count_nonzero(keep_group)) return relabel[group_idx] def is_duck_array(value): """This function was copied from xarray/core/utils.py under the terms of Xarray's Apache-2 license.""" if isinstance(value, np.ndarray): return True return ( hasattr(value, "ndim") and hasattr(value, "shape") and hasattr(value, "dtype") and hasattr(value, "__array_function__") and hasattr(value, "__array_ufunc__") ) def iscomplexobj(x): """Copied from np.iscomplexobj so that we place fewer requirements on duck array types.""" try: dtype = x.dtype type_ = dtype.type except AttributeError: type_ = np.asarray(x).dtype.type return issubclass(type_, np.complexfloating) numpy-groupies-0.10.2/pyproject.toml000066400000000000000000000031301450707334100175120ustar00rootroot00000000000000[build-system] requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "numpy-groupies" description = "Optimised tools for group-indexing operations: aggregated sum and more." dynamic = ["version"] readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE.txt"} authors = [ {name = "Michael Löffler", email = "ml@occam.com.ua"}, {name = "Daniel Manson", email = "danielmanson.uk@gmail.com"} ] maintainers = [ {name = "Deepak Cherian", email = "dcherian@ucar.edu"} ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: BSD License", ] keywords = ["accumarray", "aggregate", "groupby", "grouping", "indexing"] requires-python = ">=3.9" dependencies = ["numpy"] [project.optional-dependencies] fast = [ "numba", ] dev = [ "pytest", "numba", "pandas", ] [project.urls] source = "https://github.com/ml31415/numpy-groupies" tracker = "https://github.com/ml31415/numpy-groupies/issues" [tool.black] line-length = 120 [tool.isort] profile = "black" honor_noqa = true [tool.setuptools.packages.find] include = ["numpy_groupies*"] [tool.setuptools_scm] write_to = "numpy_groupies/_version.py"