humanleague

PyPI - Python Version PyPI version CRAN_Status_Badge

DOI status

python (pip) build r-cmd-check

Codacy Badge codecov

Introduction

Please note ongoing development is for the python version only. R development is currently maintenance-only due to resource constraints.

humanleague is a python and an R package for microsynthesising populations from marginal and (optionally) seed data. The package is implemented in C++ for performance.

The package contains algorithms that use a number of different microsynthesis techniques:

The latter provides a bridge between deterministic reweighting and combinatorial optimisation, offering advantages of both techniques:

The algorithms:

The package also contains the following utilities:

Version 1.0.1 reflects the work described in the Quasirandom Integer Sampling (QIS) paper.

Installation

Python

Requires Python 3.13 or newer. The package can be installed using pip, e.g.

pip install humanleague

Development

uv is highly recommended for managing environments.

uv sync --dev
uv build
uv run pytest

Nanobind docs suggest a dev workflow where the build happens directly in the dev env - first manually install the build deps (required after every uv sync)

uv pip install nanobind scikit-build-core

Then build with

uv pip install --no-build-isolation -ve .

R

Official release:

> install.packages("humanleague")

For a development version

> # devtools::install_github("virgesmith/humanleague")
install.packages("humanleague", repos = c('https://virgesmith.r-universe.dev', 'https://cloud.r-project.org'))

Or, for the legacy version

> devtools::install_github("virgesmith/humanleague@1.0.1")

Documentation and Examples

R

Consult the package documentation, e.g.

> library(humanleague)
> ?humanleague

Python

The package now contains type annotations and your IDE should automatically display this, e.g.:

help

The type stubs are hand-maintained, using nanobind.stubgen to detect signature changes — see doc/type-stubs.md.

Multidimensional integerisation

Building on the one-dimensionl integerise function - which given a discrete probability distribution and a count, returns the closest integer population to the distribution that sums to the count - a multidimensional equivalent integerise is introduced. In one dimension, for example this:

>>> import humanleague
>>> p = [0.1, 0.2, 0.3, 0.4]
>>> result, stats = humanleague.integerise(p, 11)
>>> result
array([1, 2, 3, 5], dtype=int32)
>>> stats
{'rmse': 0.3535533905932736}

produces the optimal (i.e. closest possible) integer population to the discrete distribution.

The integerise function generalises this problem and applies it to higher dimensions: given an n-dimensional array of real numbers where the 1-d marginal sums in every dimension are integral (and thus the total population is too), it attempts to find an integral array that also satisfies these constraints.

The QISI algorithm is repurposed to this end. As it is a sampling algorithm it cannot guarantee that a solution is found, and if so, whether the solution is optimal. If it fails this does not prove that a solution does not exist for the given input.

>>> import numpy as np
>>> a = np.array([[ 0.3,  1.2,  2. ,  1.5],
                  [ 0.6,  2.4,  4. ,  3. ],
                  [ 1.5,  6. , 10. ,  7.5],
                  [ 0.6,  2.4,  4. ,  3. ]])
# marginal sums
>>> a.sum(axis=0)
array([ 3., 12., 20., 15.])
>>> a.sum(axis=1)
array([ 5., 10., 25., 10.])
# perform integerisation
>>> result, stats = humanleague.integerise(a)
>>> stats
{'conv': True, 'rmse': 0.5766281297335398}
>>> result
array([[ 0,  2,  2,  1],
       [ 0,  3,  4,  3],
       [ 2,  6, 10,  7],
       [ 1,  1,  4,  4]])
# check marginals are preserved
>>> (result.sum(axis=0) == a.sum(axis=0)).all()
True
>>> (result.sum(axis=1) == a.sum(axis=1)).all()
True