Source code for laser_core.demographics.spatialpops

# Some functions for distributing a given population of agents heterogeneously across a number of nodes according to a simple distribution

import numpy as np


[docs] def distribute_population_skewed(tot_pop, num_nodes, frac_rural=0.3): """ Calculate the population distribution across a number of nodes based on a total population, the number of nodes, and the fraction of the population assigned to rural nodes. The function generates a list of node populations distributed according to a simple exponential random distribution, with adjustments to ensure the sum matches the total population and the specified fraction of rural population is respected. Parameters ---------- tot_pop : int The total population to be distributed across the nodes. num_nodes : int The total number of nodes among which the population will be distributed. frac_rural : float The fraction of the total population to be assigned to rural nodes (value between 0 and 1). Defaults to 0.3. The 0 node is the single urban node and has (1-frac_rural) of the population. Returns ------- list of int A list of integers representing the population at each node. The sum of the list equals `tot_pop`. Notes ----- - The population distribution is weighted using an exponential random distribution to create heterogeneity among node populations. - Adjustments are made to ensure the total fraction assigned to rural nodes adheres to `frac_rural`. Examples -------- >>> from laser_core.demographics.spatialpops import distribute_population_skewed >>> np.random.seed(42) # For reproducibility >>> tot_pop = 1000 >>> num_nodes = 5 >>> frac_rural = 0.3 >>> distribute_population_skewed(tot_pop, num_nodes, frac_rural) [700, 154, 64, 54, 28] >>> tot_pop = 500 >>> num_nodes = 3 >>> frac_rural = 0.4 >>> distribute_population_skewed(tot_pop, num_nodes, frac_rural) [300, 136, 64] """ # Valid input data checks if tot_pop <= 0: raise ValueError("Total population must be greater than 0.") if num_nodes <= 0: raise ValueError("Number of nodes must be greater than 0.") if not (0 <= frac_rural <= 1): raise ValueError("Fraction of rural population must be between 0 and 1.") # Generate node sizes nsizes = np.exp(-np.log(np.random.rand(num_nodes - 1))) nsizes = frac_rural * nsizes / np.sum(nsizes) nsizes = np.minimum(nsizes, 100 / tot_pop) nsizes = frac_rural * nsizes / np.sum(nsizes) nsizes = np.insert(nsizes, 0, 1 - frac_rural) # Calculate populations and round to integers npops = ((np.round(tot_pop * nsizes, 0)).astype(int)).tolist() # Ensure total population matches tot_pop difference = tot_pop - sum(npops) npops[1] += difference # Adjust the second node return np.array(npops, dtype=np.uint32)
[docs] def distribute_population_tapered(tot_pop, num_nodes): """ Distribute a total population heterogeneously across a given number of nodes. The distribution follows a logarithmic-like decay pattern where the first node (Node 0) receives the largest share of the population, approximately half the total population. Subsequent nodes receive progressively smaller populations, ensuring that even the smallest node has a non-negligible share. The function ensures the sum of the distributed populations matches the `tot_pop` exactly by adjusting the largest node if rounding introduces discrepancies. Parameters ---------- tot_pop : int The total population to distribute. Must be a positive integer. num_nodes : int The number of nodes to distribute the population across. Must be a positive integer. Returns ------- numpy.ndarray A 1D array of integers where each element represents the population assigned to a specific node. The length of the array is equal to `num_nodes`. Raises ------ ValueError If `tot_pop` or `num_nodes` is not greater than 0. Notes ----- - The logarithmic-like distribution ensures that Node 0 has the highest population, and subsequent nodes receive progressively smaller proportions. - The function guarantees that the sum of the returned array equals `tot_pop`. Examples -------- Distribute a total population of 1000 across 5 nodes: >>> from laser_core.demographics.spatialpops import distribution_population_tapered >>> distribute_population_tapered(1000, 5) array([500, 250, 125, 75, 50]) Distribute a total population of 1200 across 3 nodes: >>> distribute_population_tapered(1200, 3) array([600, 400, 200]) Handling a small total population with more nodes: >>> distribute_population_tapered(10, 4) array([5, 3, 2, 0]) Ensuring the distribution adds up to the total population: >>> pop = distribute_population_tapered(1000, 5) >>> pop.sum() 1000 """ if num_nodes <= 0 or tot_pop <= 0: raise ValueError("Both tot_pop and num_nodes must be greater than 0.") # Generate a logarithmic-like declining distribution weights = np.logspace(0, -1, num=num_nodes, base=10) # Declines logarithmically weights = weights / weights.sum() # Normalize weights to sum to 1 # Scale weights to the total population and round to integers population_distribution = np.round(weights * tot_pop).astype(int) # Ensure the sum matches the tot_pop by adjusting the largest node difference = tot_pop - population_distribution.sum() population_distribution[0] += difference # Adjust Node 0 (largest) to make up the difference return population_distribution