popgen3 3.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- popgen3-3.0.1/PKG-INFO +95 -0
- popgen3-3.0.1/README.md +63 -0
- popgen3-3.0.1/popgen/__init__.py +7 -0
- popgen3-3.0.1/popgen/cli.py +23 -0
- popgen3-3.0.1/popgen/config.py +121 -0
- popgen3-3.0.1/popgen/data.py +132 -0
- popgen3-3.0.1/popgen/draw.py +101 -0
- popgen3-3.0.1/popgen/ipf.py +353 -0
- popgen3-3.0.1/popgen/output.py +487 -0
- popgen3-3.0.1/popgen/project.py +265 -0
- popgen3-3.0.1/popgen/reweighting.py +185 -0
- popgen3-3.0.1/popgen3.egg-info/PKG-INFO +95 -0
- popgen3-3.0.1/popgen3.egg-info/SOURCES.txt +18 -0
- popgen3-3.0.1/popgen3.egg-info/dependency_links.txt +1 -0
- popgen3-3.0.1/popgen3.egg-info/entry_points.txt +2 -0
- popgen3-3.0.1/popgen3.egg-info/requires.txt +4 -0
- popgen3-3.0.1/popgen3.egg-info/top_level.txt +1 -0
- popgen3-3.0.1/pyproject.toml +3 -0
- popgen3-3.0.1/setup.cfg +4 -0
- popgen3-3.0.1/setup.py +37 -0
popgen3-3.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: popgen3
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: A population synthesis and sample weighting tool for transportation planning.
|
|
5
|
+
Home-page: https://github.com/chnfanyu/PopGen3
|
|
6
|
+
Author: Fan Yu
|
|
7
|
+
Author-email: fanyu4@asu.edu
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: numpy>=1.21.6
|
|
21
|
+
Requires-Dist: pandas>=1.3.0
|
|
22
|
+
Requires-Dist: scipy>=1.10.1
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# PopGen3
|
|
34
|
+
|
|
35
|
+
## Introduction
|
|
36
|
+
|
|
37
|
+
**PopGen** is a state-of-the-art open-source synthetic population generator for advanced travel demand modeling. Developed under the guidance of Professor Ram M. Pendyala at Arizona State University, PopGen utilizes a heuristic algorithm (IPU) to control and match both household-level and person-level attribute distributions.
|
|
38
|
+
|
|
39
|
+
## Key Features
|
|
40
|
+
|
|
41
|
+
- Advanced control of household and person variables across multiple geographic resolutions (region and geo).
|
|
42
|
+
- Command-line interface for simplified usage and enhanced computational efficiency.
|
|
43
|
+
- Fully compatible with Python 3, with updated dependencies.
|
|
44
|
+
|
|
45
|
+
## Run PopGen3 Online
|
|
46
|
+
- [](https://colab.research.google.com/drive/1j1Stb8IA8OfaoPRh232kId8hqi3dUtur?usp=sharing)
|
|
47
|
+
- [](https://mybinder.org/v2/gh/chnfanyu/PopGen3/HEAD)
|
|
48
|
+
|
|
49
|
+
## Contents
|
|
50
|
+
|
|
51
|
+
### Data Inputs
|
|
52
|
+
|
|
53
|
+
#### Survey sample data:
|
|
54
|
+
`household_sample.csv`
|
|
55
|
+
`person_sample.csv`
|
|
56
|
+
|
|
57
|
+
#### Population marginal data:
|
|
58
|
+
|
|
59
|
+
- **geo-level marginals:**
|
|
60
|
+
`person_marginals.csv`
|
|
61
|
+
`household_marginals.csv`
|
|
62
|
+
|
|
63
|
+
- **region-level marginals:**
|
|
64
|
+
`region_person_marginals.csv`
|
|
65
|
+
`region_household_marginals.csv`
|
|
66
|
+
|
|
67
|
+
#### Multi-Geographic Resolution Mapping Data:
|
|
68
|
+
|
|
69
|
+
`region_geo_mapping.csv`
|
|
70
|
+
`geo_sample_mapping.csv`
|
|
71
|
+
|
|
72
|
+
Example of Multi-Geographic Resolution Levels:
|
|
73
|
+
- <small><i>Region Level:</i> Set as census county subdivision</small>
|
|
74
|
+
- <small><i>Geo Level:</i> Set as census tracts</small>
|
|
75
|
+
- <small><i>Sample Geo Level:</i> Set as Public Use Microdata Areas (PUMAs)</small>
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
### Configuration File
|
|
79
|
+
|
|
80
|
+
The `configuration.yaml` file contains several key sections for the PopGen setup.
|
|
81
|
+
|
|
82
|
+
#### Designed Input Keys
|
|
83
|
+
|
|
84
|
+
| **Project Wide Setting** | **Input Data Files** | **Scenario Settings** |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| - synthesize<br>- name<br>- location | - entities<br>- column_names<br>- location | - description<br>- apply_region_controls<br>- control_variables<br>- parameters<br>- geos_to_synthesize<br>- outputs |
|
|
87
|
+
|
|
88
|
+
### Key Outputs
|
|
89
|
+
|
|
90
|
+
#### Sample Weighting Results
|
|
91
|
+
- `weights.csv`
|
|
92
|
+
#### Synthetic Population Generated
|
|
93
|
+
- `housing_synthetic.csv`
|
|
94
|
+
- `person_synthetic.csv`
|
|
95
|
+
|
popgen3-3.0.1/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# PopGen3
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
**PopGen** is a state-of-the-art open-source synthetic population generator for advanced travel demand modeling. Developed under the guidance of Professor Ram M. Pendyala at Arizona State University, PopGen utilizes a heuristic algorithm (IPU) to control and match both household-level and person-level attribute distributions.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- Advanced control of household and person variables across multiple geographic resolutions (region and geo).
|
|
10
|
+
- Command-line interface for simplified usage and enhanced computational efficiency.
|
|
11
|
+
- Fully compatible with Python 3, with updated dependencies.
|
|
12
|
+
|
|
13
|
+
## Run PopGen3 Online
|
|
14
|
+
- [](https://colab.research.google.com/drive/1j1Stb8IA8OfaoPRh232kId8hqi3dUtur?usp=sharing)
|
|
15
|
+
- [](https://mybinder.org/v2/gh/chnfanyu/PopGen3/HEAD)
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
### Data Inputs
|
|
20
|
+
|
|
21
|
+
#### Survey sample data:
|
|
22
|
+
`household_sample.csv`
|
|
23
|
+
`person_sample.csv`
|
|
24
|
+
|
|
25
|
+
#### Population marginal data:
|
|
26
|
+
|
|
27
|
+
- **geo-level marginals:**
|
|
28
|
+
`person_marginals.csv`
|
|
29
|
+
`household_marginals.csv`
|
|
30
|
+
|
|
31
|
+
- **region-level marginals:**
|
|
32
|
+
`region_person_marginals.csv`
|
|
33
|
+
`region_household_marginals.csv`
|
|
34
|
+
|
|
35
|
+
#### Multi-Geographic Resolution Mapping Data:
|
|
36
|
+
|
|
37
|
+
`region_geo_mapping.csv`
|
|
38
|
+
`geo_sample_mapping.csv`
|
|
39
|
+
|
|
40
|
+
Example of Multi-Geographic Resolution Levels:
|
|
41
|
+
- <small><i>Region Level:</i> Set as census county subdivision</small>
|
|
42
|
+
- <small><i>Geo Level:</i> Set as census tracts</small>
|
|
43
|
+
- <small><i>Sample Geo Level:</i> Set as Public Use Microdata Areas (PUMAs)</small>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Configuration File
|
|
47
|
+
|
|
48
|
+
The `configuration.yaml` file contains several key sections for the PopGen setup.
|
|
49
|
+
|
|
50
|
+
#### Designed Input Keys
|
|
51
|
+
|
|
52
|
+
| **Project Wide Setting** | **Input Data Files** | **Scenario Settings** |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| - synthesize<br>- name<br>- location | - entities<br>- column_names<br>- location | - description<br>- apply_region_controls<br>- control_variables<br>- parameters<br>- geos_to_synthesize<br>- outputs |
|
|
55
|
+
|
|
56
|
+
### Key Outputs
|
|
57
|
+
|
|
58
|
+
#### Sample Weighting Results
|
|
59
|
+
- `weights.csv`
|
|
60
|
+
#### Synthetic Population Generated
|
|
61
|
+
- `housing_synthetic.csv`
|
|
62
|
+
- `person_synthetic.csv`
|
|
63
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import logging
|
|
4
|
+
from popgen.project import popgen_run
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""Command-line interface for running PopGen."""
|
|
8
|
+
parser = argparse.ArgumentParser(description="Run PopGen with a specified configuration file.")
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"config_path",
|
|
11
|
+
type=str,
|
|
12
|
+
help="Path to the configuration YAML file (e.g., configuration_arizona.yaml)"
|
|
13
|
+
)
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
if not os.path.exists(args.config_path):
|
|
17
|
+
logging.error(f"Configuration file '{args.config_path}' not found.")
|
|
18
|
+
exit(1)
|
|
19
|
+
|
|
20
|
+
popgen_run(args.config_path)
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
#logging.basicConfig(level=logging.INFO)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def wrap_config_value(value):
|
|
12
|
+
"""Wrap YAML elements as Config objects to allow attribute access.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
If YAML defines:
|
|
16
|
+
attribute1:
|
|
17
|
+
attribute2: 'Value'
|
|
18
|
+
|
|
19
|
+
Then, x.attribute1.attribute2 can be used to access "Value".
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(value, str):
|
|
22
|
+
return value
|
|
23
|
+
try:
|
|
24
|
+
return value + 0
|
|
25
|
+
except TypeError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
return Config(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
"""Config class for handling YAML configuration as attribute-accessible objects."""
|
|
33
|
+
|
|
34
|
+
DEFAULT_PARAMETERS = {
|
|
35
|
+
"ipf": {
|
|
36
|
+
"tolerance": 0.0001,
|
|
37
|
+
"iterations": 250,
|
|
38
|
+
"zero_marginal_correction": 0.00001,
|
|
39
|
+
"rounding_procedure": "bucket",
|
|
40
|
+
"archive_performance_frequency": 1
|
|
41
|
+
},
|
|
42
|
+
"reweighting": {
|
|
43
|
+
"procedure": "ipu",
|
|
44
|
+
"tolerance": 0.0001,
|
|
45
|
+
"inner_iterations": 1,
|
|
46
|
+
"outer_iterations": 50,
|
|
47
|
+
"archive_performance_frequency": 1
|
|
48
|
+
},
|
|
49
|
+
"draws": {
|
|
50
|
+
"pvalue_tolerance": 0.9999,
|
|
51
|
+
"iterations": 25,
|
|
52
|
+
"seed": 0
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def __init__(self, data):
|
|
57
|
+
self._data = data or {}
|
|
58
|
+
|
|
59
|
+
def __setitem__(self, key, value):
|
|
60
|
+
self._data[key] = value
|
|
61
|
+
|
|
62
|
+
def __setattr__(self, key, value):
|
|
63
|
+
if key == "_data":
|
|
64
|
+
super().__setattr__(key, value)
|
|
65
|
+
else:
|
|
66
|
+
self._data[key] = value
|
|
67
|
+
|
|
68
|
+
def __getattr__(self, key):
|
|
69
|
+
value = self.return_value(key)
|
|
70
|
+
return wrap_config_value(value) if value is not None else None
|
|
71
|
+
|
|
72
|
+
def __getitem__(self, key):
|
|
73
|
+
value = self.return_value(key)
|
|
74
|
+
return wrap_config_value(value)
|
|
75
|
+
|
|
76
|
+
def __getstate__(self):
|
|
77
|
+
"""Ensure `yaml.dump()` works correctly with Config objects."""
|
|
78
|
+
return self.__dict__
|
|
79
|
+
|
|
80
|
+
def write_to_file(self, filepath):
|
|
81
|
+
with open(filepath, 'w') as file:
|
|
82
|
+
yaml.dump(self._data, file, default_flow_style=False)
|
|
83
|
+
|
|
84
|
+
def return_value(self, key):
|
|
85
|
+
"""Retrieve value from config data safely."""
|
|
86
|
+
try:
|
|
87
|
+
return self._data[key]
|
|
88
|
+
except KeyError:
|
|
89
|
+
logging.warning(f"Key '{key}' not found in configuration.")
|
|
90
|
+
return None # Return None instead of raising an error
|
|
91
|
+
|
|
92
|
+
def __len__(self):
|
|
93
|
+
return len(self._data)
|
|
94
|
+
|
|
95
|
+
def __repr__(self):
|
|
96
|
+
return repr(self._data)
|
|
97
|
+
|
|
98
|
+
def return_list(self):
|
|
99
|
+
"""Return a list of top-level keys in the configuration."""
|
|
100
|
+
data_list = []
|
|
101
|
+
for i in self._data:
|
|
102
|
+
data_list.append(i)
|
|
103
|
+
return data_list
|
|
104
|
+
|
|
105
|
+
def return_dict(self):
|
|
106
|
+
"""Convert nested Config objects to pure dictionaries."""
|
|
107
|
+
|
|
108
|
+
def convert(value):
|
|
109
|
+
if isinstance(value, Config):
|
|
110
|
+
return value.return_dict()
|
|
111
|
+
elif isinstance(value, list):
|
|
112
|
+
return [convert(v) for v in value]
|
|
113
|
+
elif isinstance(value, dict):
|
|
114
|
+
return {k: convert(v) for k, v in value.items()}
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
return convert(self._data)
|
|
118
|
+
|
|
119
|
+
def write_to_open(self, filepath):
|
|
120
|
+
with open(filepath, 'w') as outfile:
|
|
121
|
+
yaml.dump(self._data, outfile, default_flow_style=False)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import logging
|
|
5
|
+
from .config import ConfigError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DB:
|
|
9
|
+
"""Handles all PopGen input data files and maintains necessary mappings and datasets."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, config):
|
|
12
|
+
self.config = config
|
|
13
|
+
self.sample = {}
|
|
14
|
+
self.geo_marginals = {}
|
|
15
|
+
self.region_marginals = {}
|
|
16
|
+
self.geo = {}
|
|
17
|
+
self.geo_ids = None
|
|
18
|
+
self.region_ids = None
|
|
19
|
+
self.sample_geo_ids = None
|
|
20
|
+
self._inputs_config = self.config.project.inputs
|
|
21
|
+
self.location = os.path.abspath(self.config.project.location)
|
|
22
|
+
|
|
23
|
+
def load_data(self):
|
|
24
|
+
"""Loads all required data while handling missing data gracefully."""
|
|
25
|
+
logging.info("Loading database data...")
|
|
26
|
+
|
|
27
|
+
# Load geo correspondence mapping
|
|
28
|
+
geo_corr_mapping_config = getattr(self._inputs_config.location, "geo_corr_mapping", None)
|
|
29
|
+
self.geo = self.get_data(geo_corr_mapping_config) if geo_corr_mapping_config else {}
|
|
30
|
+
|
|
31
|
+
# Load sample data
|
|
32
|
+
sample_config = getattr(self._inputs_config.location, "sample", None)
|
|
33
|
+
self.sample = self.get_data(sample_config) if sample_config else {}
|
|
34
|
+
|
|
35
|
+
# Load marginals
|
|
36
|
+
marginals_config = getattr(self._inputs_config.location, "marginals", None)
|
|
37
|
+
self.geo_marginals = self.get_data(getattr(marginals_config, "geo", None),
|
|
38
|
+
header=[0, 1]) if marginals_config and hasattr(marginals_config,
|
|
39
|
+
"geo") else {}
|
|
40
|
+
self.region_marginals = self.get_data(getattr(marginals_config, "region", None),
|
|
41
|
+
header=[0, 1]) if marginals_config and hasattr(marginals_config,
|
|
42
|
+
"region") else {}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
self._enumerate_geo_ids()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logging.warning(f"_enumerate_geo_ids failed due to {e}. Defaulting to empty lists.")
|
|
48
|
+
self.geo_ids = []
|
|
49
|
+
self.region_ids = []
|
|
50
|
+
|
|
51
|
+
def get_data(self, config, header=0):
|
|
52
|
+
"""Loads data while handling missing files gracefully."""
|
|
53
|
+
config_dict = config.return_dict() if config else {}
|
|
54
|
+
data_dict = {}
|
|
55
|
+
|
|
56
|
+
for item, filename in config_dict.items():
|
|
57
|
+
if filename is None:
|
|
58
|
+
logging.warning(f"{item} has no filename specified. Skipping.")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
full_location = os.path.join(self.location, filename)
|
|
62
|
+
if os.path.exists(full_location):
|
|
63
|
+
try:
|
|
64
|
+
data_dict[item] = pd.read_csv(full_location, index_col=0, header=header)
|
|
65
|
+
if data_dict[item].index.name:
|
|
66
|
+
data_dict[item].loc[:, data_dict[item].index.name] = data_dict[item].index.values
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logging.warning(f"Failed to load {filename} due to {e}. Skipping {item}.")
|
|
69
|
+
else:
|
|
70
|
+
logging.warning(f"{filename} not found. Skipping {item}.")
|
|
71
|
+
|
|
72
|
+
return data_dict
|
|
73
|
+
|
|
74
|
+
def _enumerate_geo_ids(self):
|
|
75
|
+
"""Ensures proper initialization of geo and region IDs."""
|
|
76
|
+
self.geo_ids_all = []
|
|
77
|
+
self.region_ids_all = []
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
if "geo_to_sample" in self.geo:
|
|
81
|
+
self.geo_ids_all = self.geo["geo_to_sample"].index.tolist()
|
|
82
|
+
|
|
83
|
+
if "region_to_geo" in self.geo:
|
|
84
|
+
self.region_ids_all = np.unique(self.geo["region_to_geo"].index.values).tolist()
|
|
85
|
+
elif "region_to_sample" in self.geo:
|
|
86
|
+
self.region_ids_all = np.unique(self.geo["region_to_sample"].index.values).tolist()
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logging.warning(f"Failed to enumerate geo IDs due to {e}. Defaulting to empty lists.")
|
|
89
|
+
self.geo_ids_all = []
|
|
90
|
+
self.region_ids_all = []
|
|
91
|
+
|
|
92
|
+
def get_geo_ids_for_region(self, region_id):
|
|
93
|
+
"""Retrieves geo IDs corresponding to a given region ID."""
|
|
94
|
+
geo_name = self._inputs_config.column_names.geo
|
|
95
|
+
geo_list = self.geo["region_to_geo"].loc[region_id, geo_name]
|
|
96
|
+
return [int(geo_list)] if isinstance(geo_list, (int, np.integer)) else list(geo_list)
|
|
97
|
+
|
|
98
|
+
def enumerate_geo_ids_for_scenario(self, scenario_config):
|
|
99
|
+
"""Ensures geo enumeration is handled even when missing data is encountered."""
|
|
100
|
+
try:
|
|
101
|
+
self.region_ids = getattr(scenario_config.geos_to_synthesize.region, "ids", self.region_ids_all)
|
|
102
|
+
self.geo_ids = []
|
|
103
|
+
|
|
104
|
+
if "region_to_geo" in self.geo:
|
|
105
|
+
for region_id in self.region_ids:
|
|
106
|
+
geo_list = self.get_geo_ids_for_region(region_id)
|
|
107
|
+
if geo_list:
|
|
108
|
+
self.geo_ids += geo_list
|
|
109
|
+
else:
|
|
110
|
+
print("⚠️ No region_to_geo mapping. Using only region data.")
|
|
111
|
+
self.geo_ids = self.region_ids if self.region_ids else []
|
|
112
|
+
except ConfigError as e:
|
|
113
|
+
print(f"⚠️ KeyError: {e}. Defaulting to all geo and region IDs.")
|
|
114
|
+
self.geo_ids = self.geo_ids_all if self.geo_ids_all else []
|
|
115
|
+
self.region_ids = self.region_ids_all if self.region_ids_all else []
|
|
116
|
+
|
|
117
|
+
def return_variables_cats(self, entity, variable_names):
|
|
118
|
+
"""Returns unique categories for each variable in an entity dataset."""
|
|
119
|
+
return {var: self.return_variable_cats(entity, var) for var in variable_names}
|
|
120
|
+
|
|
121
|
+
def return_variable_cats(self, entity, variable_name):
|
|
122
|
+
"""Returns unique values for a specific variable in an entity dataset."""
|
|
123
|
+
return np.unique(self.sample[entity][variable_name].values).tolist()
|
|
124
|
+
|
|
125
|
+
def check_data(self):
|
|
126
|
+
"""Runs data consistency checks."""
|
|
127
|
+
self.check_sample_marginals_consistency()
|
|
128
|
+
self.check_marginals()
|
|
129
|
+
|
|
130
|
+
def check(self):
|
|
131
|
+
"""Placeholder for additional data consistency checks."""
|
|
132
|
+
pass
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import logging
|
|
4
|
+
from scipy import stats
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Draw_Population:
|
|
8
|
+
"""Handles the process of drawing synthetic populations based on weighted sampling."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, scenario_config, geo_ids, geo_row_idx, geo_frequencies,
|
|
11
|
+
geo_constraints, geo_stacked, region_sample_weights):
|
|
12
|
+
self.scenario_config = scenario_config
|
|
13
|
+
self.geo_ids = geo_ids
|
|
14
|
+
self.geo_row_idx = geo_row_idx
|
|
15
|
+
self.geo_frequencies = geo_frequencies
|
|
16
|
+
self.geo_constraints = geo_constraints
|
|
17
|
+
self.geo_stacked = geo_stacked
|
|
18
|
+
self.region_sample_weights = region_sample_weights
|
|
19
|
+
|
|
20
|
+
self.iterations = self.scenario_config.parameters.draws.iterations
|
|
21
|
+
self.seed = self.scenario_config.parameters.draws.seed
|
|
22
|
+
self.pvalue_tolerance = self.scenario_config.parameters.draws.pvalue_tolerance
|
|
23
|
+
|
|
24
|
+
self.geo_id_rows_syn_dict = {}
|
|
25
|
+
self.performance_columns = ["p_value", "iterations", "chi_sq_stat"]
|
|
26
|
+
self.draws_performance = pd.DataFrame(index=self.geo_ids, columns=self.performance_columns)
|
|
27
|
+
|
|
28
|
+
def draw_population(self):
|
|
29
|
+
"""Executes the drawing of households for each geographical ID."""
|
|
30
|
+
np.random.seed(self.seed)
|
|
31
|
+
logging.info("Starting synthetic population drawing.")
|
|
32
|
+
|
|
33
|
+
for geo_id in self.geo_ids:
|
|
34
|
+
logging.info(f"Processing geo ID: {geo_id}")
|
|
35
|
+
geo_sample_weights = self.region_sample_weights.loc[:, geo_id]
|
|
36
|
+
geo_cumulative_weights = self._return_cumulative_probability(geo_sample_weights)
|
|
37
|
+
geo_id_frequencies = self.geo_frequencies.loc[geo_id, :]
|
|
38
|
+
geo_id_constraints = self.geo_constraints.loc[geo_id, :]
|
|
39
|
+
|
|
40
|
+
if geo_id_frequencies.sum() == 0:
|
|
41
|
+
logging.warning(f"Skipping geo_id {geo_id}: Zero frequency sum.")
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
p_value_max, geo_id_rows_syn_max, iter_max, stat_max = -1, None, None, None
|
|
45
|
+
|
|
46
|
+
for iter in range(self.iterations):
|
|
47
|
+
seed = self.seed + iter
|
|
48
|
+
geo_id_rows_syn = self._pick_households(geo_id_frequencies, geo_cumulative_weights)
|
|
49
|
+
stat, p_value = self._measure_match(geo_id_rows_syn, geo_id_constraints)
|
|
50
|
+
|
|
51
|
+
if p_value > self.pvalue_tolerance:
|
|
52
|
+
logging.info(f"Converged at iteration {iter} for geo ID {geo_id}.")
|
|
53
|
+
p_value_max, geo_id_rows_syn_max, iter_max, stat_max = p_value, geo_id_rows_syn, iter, stat
|
|
54
|
+
break
|
|
55
|
+
elif p_value > p_value_max:
|
|
56
|
+
p_value_max, geo_id_rows_syn_max, iter_max, stat_max = p_value, geo_id_rows_syn, iter, stat
|
|
57
|
+
|
|
58
|
+
self.draws_performance.loc[geo_id, self.performance_columns] = (p_value_max, iter_max, stat_max)
|
|
59
|
+
self.geo_id_rows_syn_dict[geo_id] = geo_id_rows_syn_max
|
|
60
|
+
|
|
61
|
+
def _return_cumulative_probability(self, geo_sample_weights):
|
|
62
|
+
"""Computes cumulative probabilities for weighted sampling."""
|
|
63
|
+
geo_cumulative_weights = {}
|
|
64
|
+
|
|
65
|
+
for column in self.geo_frequencies.columns:
|
|
66
|
+
rows = self.geo_row_idx[column]
|
|
67
|
+
weights = geo_sample_weights.take(rows)
|
|
68
|
+
geo_cumulative_weights[column] = (weights / weights.sum()).cumsum()
|
|
69
|
+
|
|
70
|
+
return geo_cumulative_weights
|
|
71
|
+
|
|
72
|
+
def _pick_households(self, geo_id_frequencies, geo_cumulative_weights):
|
|
73
|
+
"""Selects households based on cumulative probabilities."""
|
|
74
|
+
last = 0
|
|
75
|
+
rand_numbers = np.random.random(int(geo_id_frequencies.sum()))
|
|
76
|
+
list_rows_syn_subpop = []
|
|
77
|
+
|
|
78
|
+
for column in self.geo_frequencies.columns:
|
|
79
|
+
rows = self.geo_row_idx[column]
|
|
80
|
+
column_frequency = int(geo_id_frequencies[column])
|
|
81
|
+
column_bins = np.searchsorted(geo_cumulative_weights[column], rand_numbers[last:last + column_frequency],
|
|
82
|
+
side="right")
|
|
83
|
+
last += column_frequency
|
|
84
|
+
list_rows_syn_subpop.append(rows.take(column_bins))
|
|
85
|
+
|
|
86
|
+
return np.sort(np.concatenate(list_rows_syn_subpop))
|
|
87
|
+
|
|
88
|
+
def _measure_match(self, geo_id_rows_syn, geo_id_constraints):
|
|
89
|
+
"""Evaluates the match between synthetic and expected constraints using chi-square test."""
|
|
90
|
+
geo_id_constraints.name = "constraint"
|
|
91
|
+
geo_id_synthetic = self.geo_stacked.take(geo_id_rows_syn).sum()
|
|
92
|
+
geo_id_synthetic = pd.DataFrame(geo_id_synthetic, columns=["synthetic_count"])
|
|
93
|
+
geo_id_synthetic = geo_id_synthetic.join(geo_id_constraints, how="inner")
|
|
94
|
+
|
|
95
|
+
observed, expected = geo_id_synthetic["synthetic_count"], geo_id_synthetic["constraint"]
|
|
96
|
+
observed_sum, expected_sum = observed.sum(), expected.sum()
|
|
97
|
+
|
|
98
|
+
if abs(observed_sum - expected_sum) / expected_sum > 1e-08:
|
|
99
|
+
expected *= observed_sum / expected_sum
|
|
100
|
+
|
|
101
|
+
return stats.chisquare(observed, expected)
|