Skip to content

Demographics

Demographics

Bases: DemographicsBase

This class is a container of data necessary to produce a EMOD-valid demographics input file. It can be initialized from an existing valid demographics.joson type file or from an array of valid Nodes.

Source code in emod_api/demographics/Demographics.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Demographics(DemographicsBase):
    """
    This class is a container of data necessary to produce a EMOD-valid demographics input file. It can be initialized
    from an existing valid demographics.joson type file or from an array of valid Nodes.
    """
    def __init__(self, nodes: List[Node], idref: str = "Gridded world grump2.5arcmin", base_file: str = None,
                 default_node: Node = None):
        """
        A class to create demographics.
        :param nodes: list of Nodes
        :param idref: A name/reference
        :param base_file: A demographics file in json format
        :default_node: An optional node to use for default settings.
        """
        super().__init__(nodes=nodes, idref=idref, default_node=default_node)

        # HIV is expected to pass a default node. Malaria is not (for now).
        if default_node is None:
            if base_file:
                with open(base_file, "rb") as src:
                    self.raw = json.load(src)
            else:
                # adding and using this default configuration (True) as malaria may use it; I don't know. HIV does not.
                self.SetMinimalNodeAttributes()
                DT.NoInitialPrevalence(self)  # does this need to be called?
                DT.InitAgeUniform(self)

    def to_dict(self) -> Dict:
        self.verify_demographics_integrity()
        self.raw["Nodes"] = [node.to_dict() for node in self.nodes]
        self.raw["Metadata"]["NodeCount"] = len(self.nodes)
        return self.raw

    def generate_file(self, name="demographics.json"):
        """
        Write the contents of the instance to an EMOD-compatible (JSON) file.
        """
        with open(name, "w") as output:
            json.dump(self.to_dict(), output, indent=3, sort_keys=True)
        return name

__init__(nodes, idref='Gridded world grump2.5arcmin', base_file=None, default_node=None)

A class to create demographics. :param nodes: list of Nodes :param idref: A name/reference :param base_file: A demographics file in json format :default_node: An optional node to use for default settings.

Source code in emod_api/demographics/Demographics.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def __init__(self, nodes: List[Node], idref: str = "Gridded world grump2.5arcmin", base_file: str = None,
             default_node: Node = None):
    """
    A class to create demographics.
    :param nodes: list of Nodes
    :param idref: A name/reference
    :param base_file: A demographics file in json format
    :default_node: An optional node to use for default settings.
    """
    super().__init__(nodes=nodes, idref=idref, default_node=default_node)

    # HIV is expected to pass a default node. Malaria is not (for now).
    if default_node is None:
        if base_file:
            with open(base_file, "rb") as src:
                self.raw = json.load(src)
        else:
            # adding and using this default configuration (True) as malaria may use it; I don't know. HIV does not.
            self.SetMinimalNodeAttributes()
            DT.NoInitialPrevalence(self)  # does this need to be called?
            DT.InitAgeUniform(self)

generate_file(name='demographics.json')

Write the contents of the instance to an EMOD-compatible (JSON) file.

Source code in emod_api/demographics/Demographics.py
307
308
309
310
311
312
313
def generate_file(self, name="demographics.json"):
    """
    Write the contents of the instance to an EMOD-compatible (JSON) file.
    """
    with open(name, "w") as output:
        json.dump(self.to_dict(), output, indent=3, sort_keys=True)
    return name

from_csv(input_file, res=30 / 3600, id_ref='from_csv')

Create an EMOD-compatible :py:class:Demographics instance from a csv population-by-node file.

Parameters:

Name Type Description Default
input_file str

Filename

required
res float

Resolution of the nodes in arc-seconds

30 / 3600
id_ref str

Description of the source of the file.

'from_csv'
Source code in emod_api/demographics/Demographics.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def from_csv(input_file,
             res=30 / 3600,
             id_ref="from_csv"):
    """
    Create an EMOD-compatible :py:class:`Demographics` instance from a csv population-by-node file.

    Args:
        input_file (str): Filename
        res (float, optional): Resolution of the nodes in arc-seconds
        id_ref (str, optional): Description of the source of the file.
    """
    def get_value(row, headers):
        for h in headers:
            if row.get(h) is not None:
                return float(row.get(h))
        return None

    if not os.path.exists(input_file):
        print(f"{input_file} not found.")
        return

    print(f"{input_file} found and being read for demographics.json file creation.")
    node_info = pd.read_csv(input_file, encoding='iso-8859-1')
    out_nodes = []
    for index, row in node_info.iterrows():
        if 'under5_pop' in row:
            pop = int(6 * row['under5_pop'])
            if pop < 25000:
                continue
        else:
            pop = int(row['pop'])

        latitude_headers = ["lat", "latitude", "LAT", "LATITUDE", "Latitude", "Lat"]
        lat = get_value(row, latitude_headers)

        longitude_headers = ["lon", "longitude", "LON", "LONGITUDE", "Longitude", "Lon"]
        lon = get_value(row, longitude_headers)

        birth_rate_headers = ["birth", "Birth", "birth_rate", "birthrate", "BirthRate", "Birth_Rate", "BIRTH",
                              "birth rate", "Birth Rate"]
        birth_rate = get_value(row, birth_rate_headers)
        if birth_rate is not None and birth_rate < 0.0:
            raise ValueError("Birth rate defined in " + input_file + " must be greater 0.")

        node_id = row.get('node_id')
        if node_id is not None and int(node_id) == 0:
            raise ValueError("Node ids can not be '0'.")

        forced_id = int(_node_id_from_lat_lon_res(lat=lat, lon=lon, res=res)) if node_id is None else int(node_id)

        if 'loc' in row:
            place_name = str(row['loc'])
        else:
            place_name = None
        meta = {}
        """
        meta = {'dot_name': (row['ADM0_NAME']+':'+row['ADM1_NAME']+':'+row['ADM2_NAME']),
                'GUID': row['GUID'],
                'density': row['under5_pop_weighted_density']}
        """
        node_attributes = NodeAttributes(name=place_name, birth_rate=birth_rate)
        node = Node(lat, lon, pop,
                    node_attributes=node_attributes,
                    forced_id=forced_id, meta=meta)

        out_nodes.append(node)
    return Demographics(nodes=out_nodes, idref=id_ref)

from_file(base_file)

Create a :py:class:Demographics instance from an existing demographics file.

Source code in emod_api/demographics/Demographics.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def from_file(base_file):
    """
    Create a :py:class:`Demographics` instance from an existing demographics file.
    """
    with open(base_file, "rb") as src:
        raw = json.load(src)
        nodes = []

        # Load the nodes
        for node in raw["Nodes"]:
            nodes.append(Node.from_data(node))

        # Load the idref
        idref = raw["Metadata"]["IdReference"]

    # Create the file
    return Demographics(nodes, idref, base_file)

from_params(tot_pop=1000000, num_nodes=100, frac_rural=0.3, id_ref='from_params', random_2d_grid=False)

Create an EMOD-compatible :py:class:Demographics object with the population and numbe of nodes specified.

Parameters:

Name Type Description Default
tot_pop int

The total population.

1000000
num_nodes int

Number of nodes. Can be defined as a two-dimensional grid of nodes [longitude, latitude]. The distance to the next neighbouring node is 1.

100
frac_rural float

Determines what fraction of the population gets put in the 'rural' nodes, which means all nodes besides node 1. Node 1 is the 'urban' node.

0.3
id_ref str

Facility name

'from_params'
random_2d_grid bool

Create a random distanced grid with num_nodes nodes.

False

Returns:

Type Description
Demographics

New Demographics object

Source code in emod_api/demographics/Demographics.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def from_params(tot_pop: int = 1000000,
                num_nodes: int = 100,
                frac_rural: float = 0.3,
                id_ref: str = "from_params",
                random_2d_grid: bool = False):
    """
    Create an EMOD-compatible :py:class:`Demographics` object with the population and numbe of nodes specified.

    Args:
        tot_pop: The total population.
        num_nodes: Number of nodes. Can be defined as a two-dimensional grid  of nodes [longitude, latitude].
            The distance to the next neighbouring node is 1.
        frac_rural: Determines what fraction of the population gets put in the 'rural' nodes, which means all nodes
            besides node 1. Node 1 is the 'urban' node.
        id_ref:  Facility name
        random_2d_grid: Create a random distanced grid with num_nodes nodes.

    Returns:
        (Demographics): New Demographics object
    """
    if frac_rural > 1.0:
        raise ValueError("frac_rural can't be greater than 1.0")
    if frac_rural < 0.0:
        raise ValueError("frac_rural can't be less than 0")
    if frac_rural == 0.0:
        frac_rural = 1e-09

    if random_2d_grid:
        total_nodes = num_nodes
        ucellb = np.array([[1.0, 0.0], [-0.5, 0.86603]])
        nlocs = np.random.rand(num_nodes, 2)
        nlocs[0, :] = 0.5
        nlocs = np.round(np.matmul(nlocs, ucellb), 4)
    else:
        if isinstance(num_nodes, int):
            lon_grid = num_nodes
            lat_grid = 1
        else:
            lon_grid = num_nodes[0]  # east/west
            lat_grid = num_nodes[1]  # north/south

        total_nodes = lon_grid * lat_grid
        nlocs = [[i, j] for i in range(lon_grid) for j in range(lat_grid)]

    nodes = []
    npops = get_node_pops_from_params(tot_pop, total_nodes, frac_rural)

    # Add nodes to demographics
    for idx, lat_lon in enumerate(nlocs):
        nodes.append(Node(lat=lat_lon[0], lon=lat_lon[1], pop=npops[idx], forced_id=idx + 1))

    return Demographics(nodes=nodes, idref=id_ref)

from_pop_csv(pop_filename_in, res=1 / 120, id_ref='from_raster', pop_filename_out='spatial_gridded_pop_dir', site='No_Site')

Deprecated. Please use from_pop_raster_csv.

Source code in emod_api/demographics/Demographics.py
263
264
265
266
267
268
269
270
271
def from_pop_csv(pop_filename_in,
                 res=1 / 120,
                 id_ref="from_raster",
                 pop_filename_out="spatial_gridded_pop_dir",
                 site="No_Site"):
    """
        Deprecated. Please use from_pop_raster_csv.
    """
    return from_pop_raster_csv(pop_filename_in, res, id_ref, pop_filename_out, site)

from_pop_raster_csv(pop_filename_in, res=1 / 120, id_ref='from_raster', pop_filename_out='spatial_gridded_pop_dir', site='No_Site')

1
2
3
4
5
Take a csv of a population-counts raster and build a grid for use with EMOD simulations.
Grid size is specified by grid resolution in arcs or in kilometers. The population counts
from the raster csv are then assigned to their nearest grid center and a new intermediate
grid file is generated with latitude, longitude and population. This file is then fed to
from_csv to generate a demographics object.

Parameters:

Name Type Description Default
pop_filename_in str

The filename of the population-counts raster in CSV format.

required
res float

The grid resolution in arcs or kilometers. Default is 1/120.

1 / 120
id_ref str

Identifier reference for the grid. Default is "from_raster".

'from_raster'
pop_filename_out str

The output filename for the intermediate grid file. Default is "spatial_gridded_pop_dir".

'spatial_gridded_pop_dir'
site str

The site name or identifier. Default is "No_Site".

'No_Site'

Returns:

Type Description
Demographics

New Demographics object based on the grid file.

Raises:

Source code in emod_api/demographics/Demographics.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def from_pop_raster_csv(pop_filename_in,
                        res=1 / 120,
                        id_ref="from_raster",
                        pop_filename_out="spatial_gridded_pop_dir",
                        site="No_Site"):
    """
        Take a csv of a population-counts raster and build a grid for use with EMOD simulations.
        Grid size is specified by grid resolution in arcs or in kilometers. The population counts
        from the raster csv are then assigned to their nearest grid center and a new intermediate
        grid file is generated with latitude, longitude and population. This file is then fed to
        from_csv to generate a demographics object.

    Args:
        pop_filename_in (str): The filename of the population-counts raster in CSV format.
        res (float, optional): The grid resolution in arcs or kilometers. Default is 1/120.
        id_ref (str, optional): Identifier reference for the grid. Default is "from_raster".
        pop_filename_out (str, optional): The output filename for the intermediate grid file.
            Default is "spatial_gridded_pop_dir".
        site (str, optional): The site name or identifier. Default is "No_Site".

    Returns:
        (Demographics): New Demographics object based on the grid file.

    Raises:

    """
    grid_file_path = service._create_grid_files(pop_filename_in, pop_filename_out, site)
    print(f"{grid_file_path} grid file created.")
    return from_csv(grid_file_path, res, id_ref)

from_template_node(lat=0, lon=0, pop=1000000, name='Erewhon', forced_id=1)

Create a single-node :py:class:Demographics instance from a few parameters.

Source code in emod_api/demographics/Demographics.py
27
28
29
30
31
32
33
34
35
36
def from_template_node(lat=0,
                       lon=0,
                       pop=1000000,
                       name="Erewhon",
                       forced_id=1):
    """
    Create a single-node :py:class:`Demographics` instance from a few parameters.
    """
    new_nodes = [Node(lat, lon, pop, forced_id=forced_id, name=name)]
    return Demographics(nodes=new_nodes)

get_node_ids_from_file(demographics_file)

Get a list of node ids from a demographics file.

Source code in emod_api/demographics/Demographics.py
58
59
60
61
62
63
def get_node_ids_from_file(demographics_file):
    """
    Get a list of node ids from a demographics file.
    """
    d = from_file(demographics_file)
    return sorted(d.node_ids)

get_node_pops_from_params(tot_pop, num_nodes, frac_rural)

Get a list of node populations from the params used to create a sparsely parameterized multi-node :py:class:Demographics instance. The first population in the list is the "urban" population and remaning populations are roughly drawn from a log-uniform distribution.

Parameters:

Name Type Description Default
tot_pop int

Sum of all node populations (not guaranteed)

required
num_nodes int

The total number of nodes.

required
frac_rural float

The fraction of the total population that is to be distributed across the num_nodes-1 "rural" nodes.

required

Returns:

Type Description
list

A list containing the urban node population followed by the rural nodes.

Source code in emod_api/demographics/Demographics.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def get_node_pops_from_params(tot_pop,
                              num_nodes,
                              frac_rural) -> list:
    """
    Get a list of node populations from the params used to create a sparsely
    parameterized multi-node :py:class:`Demographics` instance. The first population
    in the list is the "urban" population and remaning populations are roughly drawn from a
    log-uniform distribution.

    Args:
        tot_pop (int): Sum of all node populations (not guaranteed)
        num_nodes (int): The total number of nodes.
        frac_rural (float): The fraction of the total population that is to be distributed across the
            `num_nodes`-1 "rural" nodes.

    Returns:
        A list containing the urban node population followed by the rural nodes.
    """

    # Draw from a log-uniform or reciprocal distribution (support from (1, \infty))
    nsizes = np.exp(-np.log(np.random.rand(num_nodes - 1)))
    # normalize to frac_rural
    nsizes = frac_rural * nsizes / np.sum(nsizes)
    # require atleast 100 people
    nsizes = np.minimum(nsizes, 100 / tot_pop)
    # normalize to frac_rural
    nsizes = frac_rural * nsizes / np.sum(nsizes)
    # add the urban node
    nsizes = np.insert(nsizes, 0, 1 - frac_rural)
    # round the populations to the nearest integer and change type to list
    npops = ((np.round(tot_pop * nsizes, 0)).astype(int)).tolist()
    return npops