├── .github └── workflows │ └── continuous-deployment.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── census_data_aggregator ├── __init__.py └── exceptions.py ├── setup.cfg ├── setup.py └── test.py /.github/workflows/continuous-deployment.yaml: -------------------------------------------------------------------------------- 1 | name: Testing and deployment 2 | on: 3 | push: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | lint-python: 8 | name: Lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - id: setup-python 15 | name: Setup Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.9' 19 | cache: 'pipenv' 20 | 21 | - id: install-pipenv 22 | name: Install pipenv 23 | run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python 24 | shell: bash 25 | 26 | - id: install-python-dependencies 27 | name: Install Python dependencies 28 | run: pipenv install --dev --python=`which python` 29 | shell: bash 30 | 31 | - id: run 32 | name: Run 33 | run: pipenv run flake8 census_data_aggregator 34 | 35 | test-python: 36 | strategy: 37 | matrix: 38 | python: ['3.7', '3.8', '3.9', '3.10'] 39 | name: Test 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | 45 | - id: setup-python 46 | name: Setup Python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: '3.9' 50 | cache: 'pipenv' 51 | 52 | - id: install-pipenv 53 | name: Install pipenv 54 | run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python 55 | shell: bash 56 | 57 | - id: install-python-dependencies 58 | name: Install Python dependencies 59 | run: pipenv install --dev --python=`which python` 60 | shell: bash 61 | 62 | - id: run 63 | name: Run 64 | run: pipenv run python test.py 65 | shell: bash 66 | 67 | test-build: 68 | name: Build Python package 69 | runs-on: ubuntu-latest 70 | needs: [test-python] 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v3 74 | 75 | - id: setup-python 76 | name: Setup Python 77 | uses: actions/setup-python@v4 78 | with: 79 | python-version: '3.9' 80 | cache: 'pipenv' 81 | 82 | - id: install-pipenv 83 | name: Install pipenv 84 | run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python 85 | shell: bash 86 | 87 | - id: install-python-dependencies 88 | name: Install Python dependencies 89 | run: pipenv install --dev --python=`which python` 90 | shell: bash 91 | 92 | - id: build 93 | name: Build release 94 | run: | 95 | pipenv run python setup.py sdist 96 | pipenv run python setup.py bdist_wheel 97 | ls -l dist 98 | 99 | - id: check 100 | name: Check release 101 | run: | 102 | pipenv run twine check dist/* 103 | 104 | - id: save 105 | name: Save artifact 106 | uses: actions/upload-artifact@v2 107 | with: 108 | name: test-release-${{ github.run_number }} 109 | path: ./dist 110 | if-no-files-found: error 111 | 112 | tag-release: 113 | name: Tagged PyPI release 114 | runs-on: ubuntu-latest 115 | needs: [test-build] 116 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 117 | steps: 118 | - uses: actions/setup-python@v3 119 | with: 120 | python-version: '3.9' 121 | 122 | - id: fetch 123 | name: Fetch artifact 124 | uses: actions/download-artifact@v2 125 | with: 126 | name: test-release-${{ github.run_number }} 127 | path: ./dist 128 | 129 | - id: publish 130 | name: Publish release 131 | uses: pypa/gh-action-pypi-publish@release/v1 132 | with: 133 | user: __token__ 134 | password: ${{ secrets.PYPI_API_TOKEN }} 135 | verbose: true 136 | verify_metadata: false 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | args: ['--maxkb=10000'] 12 | - id: check-case-conflict 13 | - id: mixed-line-ending 14 | 15 | - repo: https://github.com/psf/black 16 | rev: 22.3.0 17 | hooks: 18 | - id: black 19 | 20 | - repo: https://github.com/asottile/blacken-docs 21 | rev: v1.12.1 22 | hooks: 23 | - id: blacken-docs 24 | additional_dependencies: [black] 25 | 26 | - repo: https://github.com/timothycrosley/isort 27 | rev: 5.10.1 28 | hooks: 29 | - id: isort 30 | args: ["--profile", "black", "--filter-files"] 31 | 32 | - repo: https://gitlab.com/pycqa/flake8 33 | rev: 3.9.2 34 | hooks: 35 | - id: flake8 36 | 37 | - repo: https://github.com/asottile/pyupgrade 38 | rev: v2.31.0 39 | hooks: 40 | - id: pyupgrade 41 | args: [--py37-plus] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Los Angeles Times Data Desk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | twine = "*" 8 | flake8 = "*" 9 | setuptools-scm = "*" 10 | pre-commit = "*" 11 | sphinx = "*" 12 | myst-parser = "*" 13 | 14 | [packages] 15 | numpy = "*" 16 | 17 | [requires] 18 | python_version = "3.9" 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3912e25097bd3914c15d5e5e278852429322a32546e5841fcd043e1d9ae618b8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "numpy": { 20 | "hashes": [ 21 | "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d", 22 | "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07", 23 | "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df", 24 | "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9", 25 | "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d", 26 | "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a", 27 | "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719", 28 | "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2", 29 | "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280", 30 | "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa", 31 | "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387", 32 | "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1", 33 | "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43", 34 | "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f", 35 | "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398", 36 | "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63", 37 | "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de", 38 | "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8", 39 | "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481", 40 | "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0", 41 | "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d", 42 | "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e", 43 | "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96", 44 | "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb", 45 | "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6", 46 | "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d", 47 | "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a", 48 | "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135" 49 | ], 50 | "index": "pypi", 51 | "version": "==1.23.5" 52 | } 53 | }, 54 | "develop": { 55 | "alabaster": { 56 | "hashes": [ 57 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 58 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 59 | ], 60 | "version": "==0.7.12" 61 | }, 62 | "babel": { 63 | "hashes": [ 64 | "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe", 65 | "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6" 66 | ], 67 | "markers": "python_version >= '3.6'", 68 | "version": "==2.11.0" 69 | }, 70 | "bleach": { 71 | "hashes": [ 72 | "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", 73 | "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" 74 | ], 75 | "markers": "python_version >= '3.7'", 76 | "version": "==5.0.1" 77 | }, 78 | "certifi": { 79 | "hashes": [ 80 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 81 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 82 | ], 83 | "index": "pypi", 84 | "version": "==2022.12.7" 85 | }, 86 | "cffi": { 87 | "hashes": [ 88 | "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", 89 | "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", 90 | "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", 91 | "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", 92 | "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", 93 | "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", 94 | "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", 95 | "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", 96 | "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", 97 | "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", 98 | "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", 99 | "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", 100 | "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", 101 | "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", 102 | "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", 103 | "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", 104 | "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", 105 | "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", 106 | "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", 107 | "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", 108 | "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", 109 | "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", 110 | "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", 111 | "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", 112 | "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", 113 | "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", 114 | "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", 115 | "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", 116 | "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", 117 | "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", 118 | "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", 119 | "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", 120 | "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", 121 | "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", 122 | "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", 123 | "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", 124 | "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", 125 | "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", 126 | "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", 127 | "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", 128 | "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", 129 | "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", 130 | "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", 131 | "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", 132 | "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", 133 | "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", 134 | "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", 135 | "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", 136 | "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", 137 | "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", 138 | "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", 139 | "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", 140 | "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", 141 | "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", 142 | "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", 143 | "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", 144 | "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", 145 | "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", 146 | "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", 147 | "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", 148 | "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", 149 | "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", 150 | "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", 151 | "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" 152 | ], 153 | "version": "==1.15.1" 154 | }, 155 | "cfgv": { 156 | "hashes": [ 157 | "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", 158 | "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" 159 | ], 160 | "markers": "python_full_version >= '3.6.1'", 161 | "version": "==3.3.1" 162 | }, 163 | "charset-normalizer": { 164 | "hashes": [ 165 | "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", 166 | "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" 167 | ], 168 | "markers": "python_version >= '3.6'", 169 | "version": "==2.1.1" 170 | }, 171 | "commonmark": { 172 | "hashes": [ 173 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", 174 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" 175 | ], 176 | "version": "==0.9.1" 177 | }, 178 | "cryptography": { 179 | "hashes": [ 180 | "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", 181 | "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", 182 | "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", 183 | "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", 184 | "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", 185 | "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", 186 | "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", 187 | "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", 188 | "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", 189 | "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", 190 | "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", 191 | "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", 192 | "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", 193 | "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", 194 | "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", 195 | "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", 196 | "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", 197 | "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", 198 | "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", 199 | "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", 200 | "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", 201 | "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", 202 | "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", 203 | "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", 204 | "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", 205 | "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" 206 | ], 207 | "markers": "python_version >= '3.6'", 208 | "version": "==38.0.4" 209 | }, 210 | "distlib": { 211 | "hashes": [ 212 | "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", 213 | "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" 214 | ], 215 | "version": "==0.3.6" 216 | }, 217 | "docutils": { 218 | "hashes": [ 219 | "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", 220 | "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" 221 | ], 222 | "markers": "python_version >= '3.7'", 223 | "version": "==0.19" 224 | }, 225 | "filelock": { 226 | "hashes": [ 227 | "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2", 228 | "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c" 229 | ], 230 | "markers": "python_version >= '3.7'", 231 | "version": "==3.8.2" 232 | }, 233 | "flake8": { 234 | "hashes": [ 235 | "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", 236 | "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" 237 | ], 238 | "index": "pypi", 239 | "version": "==6.0.0" 240 | }, 241 | "identify": { 242 | "hashes": [ 243 | "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f", 244 | "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d" 245 | ], 246 | "markers": "python_version >= '3.7'", 247 | "version": "==2.5.9" 248 | }, 249 | "idna": { 250 | "hashes": [ 251 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 252 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 253 | ], 254 | "markers": "python_version >= '3.5'", 255 | "version": "==3.4" 256 | }, 257 | "imagesize": { 258 | "hashes": [ 259 | "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", 260 | "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" 261 | ], 262 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 263 | "version": "==1.4.1" 264 | }, 265 | "importlib-metadata": { 266 | "hashes": [ 267 | "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b", 268 | "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313" 269 | ], 270 | "markers": "python_version < '3.10'", 271 | "version": "==5.1.0" 272 | }, 273 | "jaraco.classes": { 274 | "hashes": [ 275 | "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", 276 | "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" 277 | ], 278 | "markers": "python_version >= '3.7'", 279 | "version": "==3.2.3" 280 | }, 281 | "jeepney": { 282 | "hashes": [ 283 | "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", 284 | "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" 285 | ], 286 | "markers": "sys_platform == 'linux'", 287 | "version": "==0.8.0" 288 | }, 289 | "jinja2": { 290 | "hashes": [ 291 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 292 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 293 | ], 294 | "markers": "python_version >= '3.7'", 295 | "version": "==3.1.2" 296 | }, 297 | "keyring": { 298 | "hashes": [ 299 | "sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e", 300 | "sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361" 301 | ], 302 | "markers": "python_version >= '3.7'", 303 | "version": "==23.11.0" 304 | }, 305 | "markdown-it-py": { 306 | "hashes": [ 307 | "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27", 308 | "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da" 309 | ], 310 | "markers": "python_version >= '3.7'", 311 | "version": "==2.1.0" 312 | }, 313 | "markupsafe": { 314 | "hashes": [ 315 | "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", 316 | "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", 317 | "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", 318 | "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", 319 | "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", 320 | "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", 321 | "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", 322 | "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", 323 | "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", 324 | "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", 325 | "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", 326 | "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", 327 | "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", 328 | "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", 329 | "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", 330 | "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", 331 | "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", 332 | "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", 333 | "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", 334 | "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", 335 | "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", 336 | "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", 337 | "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", 338 | "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", 339 | "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", 340 | "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", 341 | "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", 342 | "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", 343 | "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", 344 | "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", 345 | "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", 346 | "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", 347 | "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", 348 | "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", 349 | "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", 350 | "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", 351 | "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", 352 | "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", 353 | "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", 354 | "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" 355 | ], 356 | "markers": "python_version >= '3.7'", 357 | "version": "==2.1.1" 358 | }, 359 | "mccabe": { 360 | "hashes": [ 361 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 362 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 363 | ], 364 | "markers": "python_version >= '3.6'", 365 | "version": "==0.7.0" 366 | }, 367 | "mdit-py-plugins": { 368 | "hashes": [ 369 | "sha256:36d08a29def19ec43acdcd8ba471d3ebab132e7879d442760d963f19913e04b9", 370 | "sha256:5cfd7e7ac582a594e23ba6546a2f406e94e42eb33ae596d0734781261c251260" 371 | ], 372 | "markers": "python_version >= '3.7'", 373 | "version": "==0.3.3" 374 | }, 375 | "mdurl": { 376 | "hashes": [ 377 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 378 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 379 | ], 380 | "markers": "python_version >= '3.7'", 381 | "version": "==0.1.2" 382 | }, 383 | "more-itertools": { 384 | "hashes": [ 385 | "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41", 386 | "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab" 387 | ], 388 | "markers": "python_version >= '3.7'", 389 | "version": "==9.0.0" 390 | }, 391 | "myst-parser": { 392 | "hashes": [ 393 | "sha256:61b275b85d9f58aa327f370913ae1bec26ebad372cc99f3ab85c8ec3ee8d9fb8", 394 | "sha256:79317f4bb2c13053dd6e64f9da1ba1da6cd9c40c8a430c447a7b146a594c246d" 395 | ], 396 | "index": "pypi", 397 | "version": "==0.18.1" 398 | }, 399 | "nodeenv": { 400 | "hashes": [ 401 | "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", 402 | "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" 403 | ], 404 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 405 | "version": "==1.7.0" 406 | }, 407 | "packaging": { 408 | "hashes": [ 409 | "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", 410 | "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" 411 | ], 412 | "markers": "python_version >= '3.7'", 413 | "version": "==22.0" 414 | }, 415 | "pkginfo": { 416 | "hashes": [ 417 | "sha256:ac03e37e4d601aaee40f8087f63fc4a2a6c9814dda2c8fa6aab1b1829653bdfa", 418 | "sha256:d580059503f2f4549ad6e4c106d7437356dbd430e2c7df99ee1efe03d75f691e" 419 | ], 420 | "markers": "python_version >= '3.6'", 421 | "version": "==1.9.2" 422 | }, 423 | "platformdirs": { 424 | "hashes": [ 425 | "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", 426 | "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" 427 | ], 428 | "markers": "python_version >= '3.7'", 429 | "version": "==2.6.0" 430 | }, 431 | "pre-commit": { 432 | "hashes": [ 433 | "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7", 434 | "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959" 435 | ], 436 | "index": "pypi", 437 | "version": "==2.20.0" 438 | }, 439 | "pycodestyle": { 440 | "hashes": [ 441 | "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", 442 | "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" 443 | ], 444 | "markers": "python_version >= '3.6'", 445 | "version": "==2.10.0" 446 | }, 447 | "pycparser": { 448 | "hashes": [ 449 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 450 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 451 | ], 452 | "version": "==2.21" 453 | }, 454 | "pyflakes": { 455 | "hashes": [ 456 | "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", 457 | "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" 458 | ], 459 | "markers": "python_version >= '3.6'", 460 | "version": "==3.0.1" 461 | }, 462 | "pygments": { 463 | "hashes": [ 464 | "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", 465 | "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" 466 | ], 467 | "markers": "python_version >= '3.6'", 468 | "version": "==2.13.0" 469 | }, 470 | "pytz": { 471 | "hashes": [ 472 | "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", 473 | "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" 474 | ], 475 | "version": "==2022.6" 476 | }, 477 | "pyyaml": { 478 | "hashes": [ 479 | "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", 480 | "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", 481 | "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", 482 | "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", 483 | "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", 484 | "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", 485 | "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", 486 | "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", 487 | "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", 488 | "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", 489 | "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", 490 | "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", 491 | "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", 492 | "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", 493 | "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", 494 | "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", 495 | "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", 496 | "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", 497 | "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", 498 | "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", 499 | "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", 500 | "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", 501 | "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", 502 | "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", 503 | "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", 504 | "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", 505 | "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", 506 | "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", 507 | "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", 508 | "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", 509 | "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", 510 | "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", 511 | "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", 512 | "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", 513 | "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", 514 | "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", 515 | "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", 516 | "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", 517 | "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", 518 | "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" 519 | ], 520 | "markers": "python_version >= '3.6'", 521 | "version": "==6.0" 522 | }, 523 | "readme-renderer": { 524 | "hashes": [ 525 | "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", 526 | "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" 527 | ], 528 | "markers": "python_version >= '3.7'", 529 | "version": "==37.3" 530 | }, 531 | "requests": { 532 | "hashes": [ 533 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 534 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 535 | ], 536 | "markers": "python_version >= '3.7' and python_version < '4'", 537 | "version": "==2.28.1" 538 | }, 539 | "requests-toolbelt": { 540 | "hashes": [ 541 | "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7", 542 | "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d" 543 | ], 544 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 545 | "version": "==0.10.1" 546 | }, 547 | "rfc3986": { 548 | "hashes": [ 549 | "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", 550 | "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" 551 | ], 552 | "markers": "python_version >= '3.7'", 553 | "version": "==2.0.0" 554 | }, 555 | "rich": { 556 | "hashes": [ 557 | "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e", 558 | "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0" 559 | ], 560 | "markers": "python_version < '4' and python_full_version >= '3.6.3'", 561 | "version": "==12.6.0" 562 | }, 563 | "secretstorage": { 564 | "hashes": [ 565 | "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", 566 | "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" 567 | ], 568 | "markers": "sys_platform == 'linux'", 569 | "version": "==3.3.3" 570 | }, 571 | "setuptools": { 572 | "hashes": [ 573 | "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", 574 | "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" 575 | ], 576 | "markers": "python_version >= '3.7'", 577 | "version": "==65.6.3" 578 | }, 579 | "setuptools-scm": { 580 | "hashes": [ 581 | "sha256:031e13af771d6f892b941adb6ea04545bbf91ebc5ce68c78aaf3fff6e1fb4844", 582 | "sha256:7930f720905e03ccd1e1d821db521bff7ec2ac9cf0ceb6552dd73d24a45d3b02" 583 | ], 584 | "index": "pypi", 585 | "version": "==7.0.5" 586 | }, 587 | "six": { 588 | "hashes": [ 589 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 590 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 591 | ], 592 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 593 | "version": "==1.16.0" 594 | }, 595 | "snowballstemmer": { 596 | "hashes": [ 597 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 598 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 599 | ], 600 | "version": "==2.2.0" 601 | }, 602 | "sphinx": { 603 | "hashes": [ 604 | "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d", 605 | "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5" 606 | ], 607 | "index": "pypi", 608 | "version": "==5.3.0" 609 | }, 610 | "sphinxcontrib-applehelp": { 611 | "hashes": [ 612 | "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", 613 | "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" 614 | ], 615 | "markers": "python_version >= '3.5'", 616 | "version": "==1.0.2" 617 | }, 618 | "sphinxcontrib-devhelp": { 619 | "hashes": [ 620 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 621 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 622 | ], 623 | "markers": "python_version >= '3.5'", 624 | "version": "==1.0.2" 625 | }, 626 | "sphinxcontrib-htmlhelp": { 627 | "hashes": [ 628 | "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", 629 | "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" 630 | ], 631 | "markers": "python_version >= '3.6'", 632 | "version": "==2.0.0" 633 | }, 634 | "sphinxcontrib-jsmath": { 635 | "hashes": [ 636 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 637 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 638 | ], 639 | "markers": "python_version >= '3.5'", 640 | "version": "==1.0.1" 641 | }, 642 | "sphinxcontrib-qthelp": { 643 | "hashes": [ 644 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 645 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 646 | ], 647 | "markers": "python_version >= '3.5'", 648 | "version": "==1.0.3" 649 | }, 650 | "sphinxcontrib-serializinghtml": { 651 | "hashes": [ 652 | "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", 653 | "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" 654 | ], 655 | "markers": "python_version >= '3.5'", 656 | "version": "==1.1.5" 657 | }, 658 | "toml": { 659 | "hashes": [ 660 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 661 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 662 | ], 663 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 664 | "version": "==0.10.2" 665 | }, 666 | "tomli": { 667 | "hashes": [ 668 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 669 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 670 | ], 671 | "markers": "python_version >= '3.7'", 672 | "version": "==2.0.1" 673 | }, 674 | "twine": { 675 | "hashes": [ 676 | "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e", 677 | "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0" 678 | ], 679 | "index": "pypi", 680 | "version": "==4.0.1" 681 | }, 682 | "typing-extensions": { 683 | "hashes": [ 684 | "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", 685 | "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" 686 | ], 687 | "markers": "python_version >= '3.7'", 688 | "version": "==4.4.0" 689 | }, 690 | "urllib3": { 691 | "hashes": [ 692 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", 693 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" 694 | ], 695 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 696 | "version": "==1.26.13" 697 | }, 698 | "virtualenv": { 699 | "hashes": [ 700 | "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4", 701 | "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058" 702 | ], 703 | "markers": "python_version >= '3.6'", 704 | "version": "==20.17.1" 705 | }, 706 | "webencodings": { 707 | "hashes": [ 708 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 709 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 710 | ], 711 | "version": "==0.5.1" 712 | }, 713 | "zipp": { 714 | "hashes": [ 715 | "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa", 716 | "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766" 717 | ], 718 | "markers": "python_version >= '3.7'", 719 | "version": "==3.11.0" 720 | } 721 | } 722 | } 723 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # census-data-aggregator 2 | 3 | Combine U.S. census data responsibly 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pipenv install census-data-aggregator 9 | ``` 10 | 11 | ## Usage 12 | 13 | Import the library. 14 | 15 | ```python 16 | import census_data_aggregator 17 | ``` 18 | 19 | ### Approximating sums 20 | 21 | Total together estimates from the U.S. Census Bureau and approximate the combined margin of error. Follows the bureau's [official guidelines](https://www.documentcloud.org/documents/6162551-20180418-MOE.html) for how to calculate a new margin of error when totaling multiple values. Useful for aggregating census categories and geographies. 22 | 23 | Accepts an open-ended set of paired lists, each expected to provide an estimate followed by its margin of error. 24 | 25 | ```python 26 | males_under_5, males_under_5_moe = 10154024, 3778 27 | females_under_5, females_under_5_moe = 9712936, 3911 28 | census_data_aggregator.approximate_sum( 29 | (males_under_5, males_under_5_moe), (females_under_5, females_under_5_moe) 30 | ) 31 | 19866960, 5437.757350231803 32 | ``` 33 | 34 | ### Approximating means 35 | 36 | Estimate a mean and approximate the margin of error. 37 | 38 | The Census Bureau guidelines do not provide instructions for approximating a mean using data from the ACS. Instead, we implement our own simulation-based approach. 39 | 40 | Expects a list of dictionaries that divide the full range of data values into continuous categories. Each dictionary should have four keys: 41 | 42 | key|value 43 | ---|----- 44 | min|The minimum value of the range 45 | max|The maximum value of the range 46 | n|The number of people, households or other units in the range 47 | moe|The margin of error for the number of units in the range 48 | 49 | ```python 50 | income = [ 51 | dict(min=0, max=9999, n=7942251, moe=17662), 52 | dict(min=10000, max=14999, n=5768114, moe=16409), 53 | dict(min=15000, max=19999, n=5727180, moe=16801), 54 | dict(min=20000, max=24999, n=5910725, moe=17864), 55 | dict(min=25000, max=29999, n=5619002, moe=16113), 56 | dict(min=30000, max=34999, n=5711286, moe=15891), 57 | dict(min=35000, max=39999, n=5332778, moe=16488), 58 | dict(min=40000, max=44999, n=5354520, moe=15415), 59 | dict(min=45000, max=49999, n=4725195, moe=16890), 60 | dict(min=50000, max=59999, n=9181800, moe=20965), 61 | dict(min=60000, max=74999, n=11818514, moe=30723), 62 | dict(min=75000, max=99999, n=14636046, moe=49159), 63 | dict(min=100000, max=124999, n=10273788, moe=47842), 64 | dict(min=125000, max=149999, n=6428069, moe=37952), 65 | dict(min=150000, max=199999, n=6931136, moe=37236), 66 | dict(min=200000, max=1000000, n=7465517, moe=42206), 67 | ] 68 | approximate_mean(income) 69 | (98045.44530685373, 194.54892406267754) 70 | ``` 71 | 72 | Note that this function expects you to submit a lower bound for the smallest bin and an upper bound for the largest bin. This is often not available for ACS datasets like income. We recommend experimenting with different lower and upper bounds to assess its effect on the resulting mean. 73 | 74 | By default the simulation is run 50 times, which can take as long as a minute. The number of simulations can be changed by setting the `simulation` keyword argument. 75 | 76 | ```python 77 | approximate_mean(income, simulations=10) 78 | ``` 79 | 80 | The simulation assumes a uniform distribution of values within each bin. In some cases, like income, it is common to assume the [Pareto distribution](https://en.wikipedia.org/wiki/Pareto_distribution) in the highest bin. You can employ it here by passing `True` to the `pareto` keyword argument. 81 | 82 | ```python 83 | approximate_mean(income, pareto=True) 84 | (60364.96525340687, 58.60735554621351) 85 | ``` 86 | 87 | Also, due to the stochastic nature of the simulation approach, you will need to set a seed before running this function to ensure replicability. 88 | 89 | ```python 90 | import numpy 91 | 92 | numpy.random.seed(711355) 93 | approximate_mean(income, pareto=True) 94 | (60364.96525340687, 58.60735554621351) 95 | numpy.random.seed(711355) 96 | approximate_mean(income, pareto=True) 97 | (60364.96525340687, 58.60735554621351) 98 | ``` 99 | 100 | ### Approximating medians 101 | 102 | Estimate a median and approximate the margin of error. Follows the U.S. Census Bureau's official guidelines for estimation. Useful for generating medians for measures like household income and age when aggregating census geographies. 103 | 104 | Expects a list of dictionaries that divide the full range of data values into continuous categories. Each dictionary should have three keys: 105 | 106 | key|value 107 | ---|----- 108 | min|The minimum value of the range 109 | max|The maximum value of the range 110 | n|The number of people, households or other units in the range 111 | 112 | ```python 113 | household_income_la_2013_acs1 = [ 114 | dict(min=2499, max=9999, n=1382), 115 | dict(min=10000, max=14999, n=2377), 116 | dict(min=15000, max=19999, n=1332), 117 | dict(min=20000, max=24999, n=3129), 118 | dict(min=25000, max=29999, n=1927), 119 | dict(min=30000, max=34999, n=1825), 120 | dict(min=35000, max=39999, n=1567), 121 | dict(min=40000, max=44999, n=1996), 122 | dict(min=45000, max=49999, n=1757), 123 | dict(min=50000, max=59999, n=3523), 124 | dict(min=60000, max=74999, n=4360), 125 | dict(min=75000, max=99999, n=6424), 126 | dict(min=100000, max=124999, n=5257), 127 | dict(min=125000, max=149999, n=3485), 128 | dict(min=150000, max=199999, n=2926), 129 | dict(min=200000, max=250001, n=4215), 130 | ] 131 | ``` 132 | 133 | For a margin of error to be returned, a sampling percentage must be provided to calculate the standard error. The sampling percentage represents what proportion of the population that participated in the survey. Here are the values for some common census surveys. 134 | 135 | survey|sampling percentage 136 | ------|------------------- 137 | One-year PUMS|1 138 | One-year ACS|2.5 139 | Three-year ACS|7.5 140 | Five-year ACS|12.5 141 | 142 | ```python 143 | census_data_aggregator.approximate_median( 144 | household_income_Los_Angeles_County_2013_acs1, sampling_percentage=2.5 145 | ) 146 | 70065.84266055046, 3850.680465234964 147 | ``` 148 | 149 | If you do not provide the value to the function, no margin of error will be returned. 150 | 151 | ```python 152 | census_data_aggregator.approximate_median(household_income_Los_Angeles_County_2013_acs1) 153 | 70065.84266055046, None 154 | ``` 155 | 156 | If the data being approximated comes from PUMS, an additional design factor must also be provided. The design factor is a statistical input used to tailor the estimate to the variance of the dataset. Find the value for the dataset you are estimating by referring to [the bureau's reference material](https://www.census.gov/programs-surveys/acs/technical-documentation/pums/documentation.html). 157 | 158 | ### Approximating percent change 159 | 160 | Calculates the percent change between two estimates and approximates its margin of error. Follows the bureau's [ACS handbook](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html). 161 | 162 | Accepts two paired lists, each expected to provide an estimate followed by its margin of error. The first input should be the earlier estimate in the comparison. The second input should be the later estimate. 163 | 164 | Returns both values as percentages multiplied by 100. 165 | 166 | ```python 167 | single_women_in_fairfax_before = 135173, 3860 168 | single_women_in_fairfax_after = 139301, 4047 169 | census_data_aggregator.approximate_percentchange( 170 | single_women_in_fairfax_before, single_women_in_fairfax_after 171 | ) 172 | 3.0538643072211165, 4.198069852261231 173 | ``` 174 | 175 | ### Approximating products 176 | 177 | Calculates the product of two estimates and approximates its margin of error. Follows the bureau's [ACS handbook](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html). 178 | 179 | Accepts two paired lists, each expected to provide an estimate followed by its margin of error. 180 | 181 | ```python 182 | owner_occupied_units = 74506512, 228238 183 | single_family_percent = 0.824, 0.001 184 | census_data_aggregator.approximate_product(owner_occupied_units, single_family_percent) 185 | 61393366, 202289 186 | ``` 187 | 188 | ### Approximating proportions 189 | 190 | Calculate an estimate's proportion of another estimate and approximate the margin of error. Follows the bureau's [ACS handbook](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html). Simply multiply the result by 100 for a percentage. Recommended when the first value is smaller than the second. 191 | 192 | Accepts two paired lists, each expected to provide an estimate followed by its margin of error. The numerator goes in first. The denominator goes in second. In cases where the numerator is not a subset of the denominator, the bureau recommends using the approximate_ratio method instead. 193 | 194 | ```python 195 | single_women_in_virginia = 203119, 5070 196 | total_women_in_virginia = 630498, 831 197 | census_data_aggregator.approximate_proportion( 198 | single_women_in_virginia, total_women_in_virginia 199 | ) 200 | 0.322, 0.008 201 | ``` 202 | 203 | ### Approximating ratios 204 | 205 | Calculate the ratio between two estimates and approximate its margin of error. Follows the bureau's [ACS handbook](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html). 206 | 207 | Accepts two paired lists, each expected to provide an estimate followed by its margin of error. The numerator goes in first. The denominator goes in second. In cases where the numerator is a subset of the denominator, the bureau recommends uses the approximate_proportion method. 208 | 209 | ```python 210 | single_men_in_virginia = (226840, 5556) 211 | single_women_in_virginia = (203119, 5070) 212 | census_data_aggregator.approximate_ratio( 213 | single_men_in_virginia, single_women_in_virginia 214 | ) 215 | 1.117, 0.039 216 | ``` 217 | 218 | ## A note from the experts 219 | 220 | The California State Data Center's Demographic Research Unit [notes](https://www.documentcloud.org/documents/6165014-How-to-Recalculate-a-Median.html#document/p4/a508562): 221 | 222 | > The user should be aware that the formulas are actually approximations that overstate the MOE compared to the more precise methods based on the actual survey returns that the Census Bureau uses. Therefore, the calculated MOEs will be higher, or more conservative, than those found in published tabulations for similarly-sized areas. This knowledge may affect the level of error you are willing to accept. 223 | 224 | The American Community Survey's handbook [adds](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html#document/p3/a509993): 225 | 226 | > As the number of estimates involved in a sum or difference increases, the results of the approximation formula become increasingly different from the \[standard error\] derived directly from the ACS microdata. Users are encouraged to work with the fewest number of estimates possible. 227 | 228 | ## References 229 | 230 | This module was designed to conform with the Census Bureau's April 18, 2018, presentation ["Using American Community Survey Estimates and Margin of Error"](https://www.documentcloud.org/documents/6162551-20180418-MOE.html), the bureau's [PUMS Accuracy statement](https://www.documentcloud.org/documents/6165603-2013-2017AccuracyPUMS.html) and the California State Data Center's 2016 edition of ["Recalculating medians and their margins of error for aggregated ACS data."](https://www.documentcloud.org/documents/6165014-How-to-Recalculate-a-Median.html), and the Census Bureau's [ACS 2018 General Handbook Chapter 8, "Calculating Measures of Error for Derived Estimates"](https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html) 231 | 232 | ## Links 233 | 234 | * Issues: [github.com/datadesk/census-data-aggregator/issues](https://github.com/datadesk/census-data-aggregator/issues) 235 | * Packaging: [pypi.python.org/pypi/census-data-aggregator](https://pypi.python.org/pypi/census-data-aggregator) 236 | * Testing: [github.com/datadesk/census-data-aggregator/actions](https://github.com/datadesk/census-data-aggregator/actions) 237 | -------------------------------------------------------------------------------- /census_data_aggregator/__init__.py: -------------------------------------------------------------------------------- 1 | import math 2 | import warnings 3 | 4 | import numpy 5 | 6 | from .exceptions import DataError, SamplingPercentageWarning 7 | 8 | 9 | def approximate_sum(*pairs): 10 | """Sum estimates from the U.S. Census Bureau and approximate the combined margin of error. 11 | 12 | Follows the U.S. Census Bureau's `official guidelines`_ for how to calculate a new margin of error 13 | when totaling multiple values. Useful for aggregating census categories and geographies. 14 | 15 | Args: 16 | *pairs (list): An open-ended set of paired lists, each expected to provide an 17 | estimate followed by its margin of error. 18 | 19 | Returns: 20 | A two-item tuple with the summed total followed by the approximated margin of error. 21 | 22 | (19866960, 5437.757350231803) 23 | 24 | Examples: 25 | Combining the under-five male population with under-five female population 26 | to calculate a grand total of children under five. 27 | 28 | >>> males_under_5, males_under_5_moe = 10154024, 3778 29 | >>> females_under_5, females_under_5_moe = 9712936, 3911 30 | >>> approximate_sum( 31 | (males_under_5, males_under_5_moe), 32 | (females_under_5, females_under_5_moe) 33 | ) 34 | 19866960, 5437.757350231803 35 | 36 | .. _official guidelines: 37 | https://www.documentcloud.org/documents/6162551-20180418-MOE.html 38 | """ 39 | # According to the Census Bureau, when approximating a sum use only the largest zero estimate margin of error, once 40 | # https://www.documentcloud.org/documents/6162551-20180418-MOE.html#document/p52 41 | zeros = [p for p in pairs if p[0] == 0] 42 | # So if there are zeros... 43 | if len(zeros) > 1: 44 | # ... weed them out 45 | max_zero_margin = max(p[1] for p in zeros) 46 | not_zero_margins = [p[1] for p in pairs if p[0] != 0] 47 | margins = [max_zero_margin] + not_zero_margins 48 | # If not, just keep all the input margins 49 | else: 50 | margins = [p[1] for p in pairs] 51 | 52 | # Calculate the margin using the bureau's official formula 53 | margin_of_error = math.sqrt(sum(m**2 for m in margins)) 54 | 55 | # Calculate the total 56 | total = sum(p[0] for p in pairs) 57 | 58 | # Return the results 59 | return total, margin_of_error 60 | 61 | 62 | def approximate_median(range_list, design_factor=1, sampling_percentage=None): 63 | """Estimate a median and approximate the margin of error. 64 | 65 | Follows the U.S. Census Bureau's `official guidelines`_ for estimation using a design factor. 66 | Useful for generating medians for measures like household income and age when aggregating census geographies. 67 | 68 | Args: 69 | range_list (list): A list of dictionaries that divide the full range of data values into continuous categories. 70 | Each dictionary should have three keys: 71 | * min (int): The minimum value of the range 72 | * max (int): The maximum value of the range 73 | * n (int): The number of people, households or other unit in the range 74 | The minimum value in the first range and the maximum value in the last range 75 | can be tailored to the dataset by using the "jam values" provided in 76 | the `American Community Survey's technical documentation`_. 77 | design_factor (float, optional): A statistical input used to tailor the standard error to the 78 | variance of the dataset. This is only needed for data coming from public use microdata sample, 79 | also known as PUMS. You do not need to provide this input if you are approximating 80 | data from the American Community Survey. The design factor for each PUMS 81 | dataset is provided as part of `the bureau's reference material`_. 82 | sampling_percentage (float, optional): A statistical input used to correct for variance linked to 83 | the size of the survey's population sample. This value submitted should be the percentage of 84 | * One-year PUMS: 1 85 | * One-year ACS: 2.5 86 | * Three-year ACS: 7.5 87 | * Five-year ACS: 12.5 88 | If you do not provide this input, a margin of error will not be returned. 89 | 90 | Returns: 91 | A two-item tuple with the median followed by the approximated margin of error. 92 | 93 | (42211.096153846156, 10153.200960954948) 94 | 95 | Examples: 96 | Estimating the median for a range of household incomes. 97 | 98 | >>> household_income_2013_acs5 = [ 99 | dict(min=2499, max=9999, n=186), 100 | dict(min=10000, max=14999, n=78), 101 | dict(min=15000, max=19999, n=98), 102 | dict(min=20000, max=24999, n=287), 103 | dict(min=25000, max=29999, n=142), 104 | dict(min=30000, max=34999, n=90), 105 | dict(min=35000, max=39999, n=107), 106 | dict(min=40000, max=44999, n=104), 107 | dict(min=45000, max=49999, n=178), 108 | dict(min=50000, max=59999, n=106), 109 | dict(min=60000, max=74999, n=177), 110 | dict(min=75000, max=99999, n=262), 111 | dict(min=100000, max=124999, n=77), 112 | dict(min=125000, max=149999, n=100), 113 | dict(min=150000, max=199999, n=58), 114 | dict(min=200000, max=250001, n=18) 115 | ] 116 | 117 | >>> approximate_median(household_income_2013_acs5, sampling_percentage=5*2.5) 118 | (42211.096153846156, 4706.522752733644) 119 | 120 | ... _official guidelines: 121 | https://www.documentcloud.org/documents/6165603-2013-2017AccuracyPUMS.html#document/p18 122 | ... _American Community Survey's technical documentation 123 | https://www.documentcloud.org/documents/6165752-2017-SummaryFile-Tech-Doc.html#document/p20/a508561 124 | ... _the bureau's reference material: 125 | https://www.census.gov/programs-surveys/acs/technical-documentation/pums/documentation.html 126 | """ 127 | # Sort the list 128 | range_list.sort(key=lambda x: x["min"]) 129 | 130 | # For each range calculate its min and max value along the universe's scale 131 | cumulative_n = 0 132 | for range_ in range_list: 133 | range_["n_min"] = cumulative_n 134 | cumulative_n += range_["n"] 135 | range_["n_max"] = cumulative_n 136 | 137 | # What is the total number of observations in the universe? 138 | n = sum(d["n"] for d in range_list) 139 | 140 | # What is the estimated midpoint of the n? 141 | n_midpoint = n / 2.0 142 | 143 | # Now use those to determine which group contains the midpoint. 144 | n_midpoint_range = next( 145 | d for d in range_list if n_midpoint >= d["n_min"] and n_midpoint <= d["n_max"] 146 | ) 147 | 148 | # How many households in the midrange are needed to reach the midpoint? 149 | n_midrange_gap = n_midpoint - n_midpoint_range["n_min"] 150 | 151 | # What is the proportion of the group that would be needed to get the midpoint? 152 | n_midrange_gap_percent = n_midrange_gap / n_midpoint_range["n"] 153 | 154 | # Apply this proportion to the width of the midrange 155 | n_midrange_gap_adjusted = ( 156 | n_midpoint_range["max"] - n_midpoint_range["min"] 157 | ) * n_midrange_gap_percent 158 | 159 | # Estimate the median 160 | estimated_median = n_midpoint_range["min"] + n_midrange_gap_adjusted 161 | 162 | # If there's no sampling percentage, we can't calculate a margin of error 163 | if not sampling_percentage: 164 | # Let's throw a warning, but still return the median 165 | warnings.warn("", SamplingPercentageWarning) 166 | return estimated_median, None 167 | 168 | # Get the standard error for this dataset 169 | standard_error = ( 170 | design_factor 171 | * math.sqrt( 172 | ((100 - sampling_percentage) / (n * sampling_percentage)) * (50**2) 173 | ) 174 | ) / 100 175 | 176 | # Use the standard error to calculate the p values 177 | p_lower = 0.5 - standard_error 178 | p_upper = 0.5 + standard_error 179 | 180 | # Estimate the p_lower and p_upper n values 181 | p_lower_n = n * p_lower 182 | p_upper_n = n * p_upper 183 | 184 | # Find the ranges the p values fall within 185 | try: 186 | p_lower_range_i, p_lower_range = next( 187 | (i, d) 188 | for i, d in enumerate(range_list) 189 | if p_lower_n >= d["n_min"] and p_lower_n <= d["n_max"] 190 | ) 191 | except StopIteration: 192 | raise DataError( 193 | f"The n's lower p value {p_lower_n} does not fall within a data range." 194 | ) 195 | 196 | try: 197 | p_upper_range_i, p_upper_range = next( 198 | (i, d) 199 | for i, d in enumerate(range_list) 200 | if p_upper_n >= d["n_min"] and p_upper_n <= d["n_max"] 201 | ) 202 | except StopIteration: 203 | raise DataError( 204 | f"The n's upper p value {p_upper_n} does not fall within a data range." 205 | ) 206 | 207 | # Use these values to estimate the lower bound of the confidence interval 208 | p_lower_a1 = p_lower_range["min"] 209 | try: 210 | p_lower_a2 = range_list[p_lower_range_i + 1]["min"] 211 | except IndexError: 212 | p_lower_a2 = p_lower_range["max"] 213 | p_lower_c1 = p_lower_range["n_min"] / n 214 | try: 215 | p_lower_c2 = range_list[p_lower_range_i + 1]["n_min"] / n 216 | except IndexError: 217 | p_lower_c2 = p_lower_range["n_max"] / n 218 | lower_bound = ((p_lower - p_lower_c1) / (p_lower_c2 - p_lower_c1)) * ( 219 | p_lower_a2 - p_lower_a1 220 | ) + p_lower_a1 221 | 222 | # Same for the upper bound 223 | p_upper_a1 = p_upper_range["min"] 224 | try: 225 | p_upper_a2 = range_list[p_upper_range_i + 1]["min"] 226 | except IndexError: 227 | p_upper_a2 = p_upper_range["max"] 228 | p_upper_c1 = p_upper_range["n_min"] / n 229 | try: 230 | p_upper_c2 = range_list[p_upper_range_i + 1]["n_min"] / n 231 | except IndexError: 232 | p_upper_c2 = p_upper_range["n_max"] / n 233 | upper_bound = ((p_upper - p_upper_c1) / (p_upper_c2 - p_upper_c1)) * ( 234 | p_upper_a2 - p_upper_a1 235 | ) + p_upper_a1 236 | 237 | # Calculate the standard error of the median 238 | standard_error_median = 0.5 * (upper_bound - lower_bound) 239 | 240 | # Calculate the margin of error at the 90% confidence level 241 | margin_of_error = 1.645 * standard_error_median 242 | 243 | # Return the result 244 | return estimated_median, margin_of_error 245 | 246 | 247 | def approximate_proportion(numerator_pair, denominator_pair): 248 | """Calculate an estimate's proportion of another estimate and approximate the margin of error. 249 | 250 | Follows the U.S. Census Bureau's `official guidelines`_. 251 | 252 | Intended for case where the numerator is a subset of the denominator. 253 | The `approximate_ratio` method should be used in cases where the 254 | denominator is larger. 255 | 256 | Args: 257 | numerator (list): a two-item sequence with a U.S. Census 258 | bureau estimate and its margin of error. 259 | 260 | denominator (list): a two-item sequence with a U.S. Census 261 | bureau estimate and its margin of error. 262 | 263 | Returns: 264 | A two-item sequence containing with the proportion followed by its estimated 265 | margin of error. 266 | 267 | (0.322, 0.008) 268 | 269 | Examples: 270 | The percentage of single women in suburban Virginia. 271 | 272 | >>> approximate_proportion((203119, 5070), (690746, 831)) 273 | (0.322, 0.008) 274 | 275 | ... _official guidelines: 276 | https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html#document/p5 277 | """ 278 | # Pull out the values 279 | numerator_estimate, numerator_moe = numerator_pair 280 | denominator_estimate, denominator_moe = denominator_pair 281 | 282 | # Approximate the proportion 283 | proportion_estimate = numerator_estimate / denominator_estimate 284 | 285 | # Approximate the margin of error 286 | squared_proportion_moe = numerator_moe**2 - ( 287 | proportion_estimate**2 * denominator_moe**2 288 | ) 289 | # Ensure it is greater than zero 290 | if squared_proportion_moe < 0: 291 | raise DataError( 292 | "The margin of error is less than zero. Census experts advise using the approximate_ratio method instead." 293 | ) 294 | proportion_moe = (1.0 / denominator_estimate) * math.sqrt(squared_proportion_moe) 295 | 296 | # Return the result 297 | return proportion_estimate, proportion_moe 298 | 299 | 300 | def approximate_ratio(numerator_pair, denominator_pair): 301 | """Calculate the ratio between two estimates and approximate its margin of error. 302 | 303 | Follows the U.S. Census Bureau's `official guidelines`_. 304 | 305 | Args: 306 | numerator (list): a two-item sequence with a U.S. Census 307 | bureau estimate and its margin of error. 308 | 309 | denominator (list): a two-item sequence with a U.S. Census 310 | bureau estimate and its margin of error. 311 | 312 | Returns: 313 | A two-item sequence containing the ratio and its approximated margin of error. 314 | 315 | (0.9869791666666666, 0.07170047425884142) 316 | 317 | Examples: 318 | >>> approximate_ratio((226840, 5556), (203119, 5070)) 319 | (1.117, 0.039) 320 | 321 | ... _official guidelines: 322 | https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html#document/p7 323 | """ 324 | # Pull out the values 325 | numerator_estimate, numerator_moe = numerator_pair 326 | denominator_estimate, denominator_moe = denominator_pair 327 | 328 | # Approximate the ratio 329 | ratio_estimate = numerator_estimate / denominator_estimate 330 | 331 | # Approximate the margin of error 332 | squared_ratio_moe = numerator_moe**2 + ( 333 | ratio_estimate**2 * denominator_moe**2 334 | ) 335 | ratio_moe = (1.0 / denominator_estimate) * math.sqrt(squared_ratio_moe) 336 | 337 | # Return the result 338 | return ratio_estimate, ratio_moe 339 | 340 | 341 | def approximate_product(pair_one, pair_two): 342 | """Calculates the product of two estimates and approximates its margin of error. 343 | 344 | Follows the U.S. Census Bureau's `official guidelines`_. 345 | 346 | Args: 347 | pair_one (list): a two-item sequence with a U.S. Census 348 | bureau estimate and its margin of error. 349 | 350 | pair_two (list): a two-item sequence with a U.S. Census 351 | bureau estimate and its margin of error. 352 | 353 | Returns: 354 | A two-item sequence containing the estimate and its approximate margin of error. 355 | 356 | (61393366, 202289) 357 | 358 | Examples: 359 | >>> approximate_product((74506512, 228238), (0.824, 0.001)) 360 | (61393366, 202289) 361 | 362 | ... _official guidelines: 363 | https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html#document/p8 364 | """ 365 | # Pull out the values 366 | estimate_one, moe_one = pair_one 367 | estimate_two, moe_two = pair_two 368 | 369 | # Approximate the product 370 | product_estimate = estimate_one * estimate_two 371 | 372 | # Approximate the margin of error 373 | squared_product_moe = (estimate_one**2 * moe_two**2) + ( 374 | estimate_two**2 * moe_one**2 375 | ) 376 | product_moe = math.sqrt(squared_product_moe) 377 | 378 | # Return the results 379 | return product_estimate, product_moe 380 | 381 | 382 | def approximate_percentchange(pair_old, pair_new): 383 | """Calculates the percent change between two estimates and approximates its margin of error. 384 | 385 | Multiplies results by 100. 386 | 387 | Follows the U.S. Census Bureau's `official guidelines`_. 388 | 389 | Args: 390 | pair_old (list): a two-item sequence with an earlier U.S. Census 391 | bureau estimate and its margin of error. 392 | 393 | pair_new (list): a two-item sequence with a later U.S. Census 394 | bureau estimate and its margin of error. 395 | 396 | Returns: 397 | A two-item sequence containing the estimate and its approximate margin of error. 398 | 399 | (61393366, 202289) 400 | 401 | Examples: 402 | The change of percentage of single women in suburban Virginia, 403 | taken from the bureau's official example as well as data `gathered elsewhere`_. 404 | 405 | >>> approximate_percentchange((135173, 3860), (139301, 4047)) 406 | (3.0538643072211165, 4.198069852261231) 407 | 408 | ... _official guidelines: 409 | https://www.documentcloud.org/documents/6177941-Acs-General-Handbook-2018-ch08.html#document/p8 410 | ... _gathered elsewhere: 411 | https://www.fairfaxcounty.gov/demographics/sites/demographics/files/assets/acs/acs2017.pdf 412 | """ 413 | # Pull out the values 414 | estimate_old, moe_old = pair_old 415 | estimate_new, moe_new = pair_new 416 | 417 | # Approximate the percent change 418 | percent_change_estimate = ((estimate_new - estimate_old) / estimate_old) * 100 419 | 420 | # Approximate the margin of error 421 | percent_change_as_ratio = approximate_ratio(pair_new, pair_old) 422 | decimal_change_moe = percent_change_as_ratio[1] 423 | percent_change_moe = 100 * decimal_change_moe 424 | 425 | # Return the results 426 | return percent_change_estimate, percent_change_moe 427 | 428 | 429 | def approximate_mean(range_list, simulations=50, pareto=False): 430 | """Estimate a mean and approximate the margin of error. 431 | 432 | The Census Bureau guidelines do not provide instructions for 433 | approximating a mean using data from the ACS. 434 | 435 | Instead, we implement our own simulation-based approach. 436 | 437 | Due to the stochastic nature of the simulation approach, you will need to set 438 | a seed before running this function to ensure replicability. 439 | 440 | Note that this function expects you to submit a lower bound for the smallest 441 | bin and an upper bound for the largest bin. This is often not available for 442 | ACS datasets like income. We recommend experimenting with different 443 | lower and upper bounds to assess its effect on the resulting mean. 444 | 445 | Args: 446 | range_list (list): A list of dictionaries that divide the full range of data values into continuous categories. 447 | Each dictionary should have four keys: 448 | * min (int): The minimum value of the range 449 | * max (int): The maximum value of the range 450 | * n (int): The number of people, households or other units in the range 451 | * moe (float): The margin of error for n 452 | simulations (int): number of simulations to run, used to estimate margin of error. Defaults to 50. 453 | pareto (logical): Set True to use the Pareto distribution to simulate values in upper bin. 454 | Set False to assume a uniform distribution. Pareto is often appropriate for income. Defaults to False. 455 | 456 | Returns: 457 | A two-item tuple with the mean followed by the approximated margin of error. 458 | 459 | (774578.4565215431, 128.94103705296743) 460 | 461 | Examples: 462 | Estimating the mean for a range of household incomes. 463 | 464 | >>> income = [ 465 | dict(min=0, max=9999, n=7942251, moe=17662), 466 | dict(min=10000, max=14999, n=5768114, moe=16409), 467 | dict(min=15000, max=19999, n=5727180, moe=16801), 468 | dict(min=20000, max=24999, n=5910725, moe=17864), 469 | dict(min=25000, max=29999, n=5619002, moe=16113), 470 | dict(min=30000, max=34999, n=5711286, moe=15891), 471 | dict(min=35000, max=39999, n=5332778, moe=16488), 472 | dict(min=40000, max=44999, n=5354520, moe=15415), 473 | dict(min=45000, max=49999, n=4725195, moe=16890), 474 | dict(min=50000, max=59999, n=9181800, moe=20965), 475 | dict(min=60000, max=74999, n=11818514, moe=30723), 476 | dict(min=75000, max=99999, n=14636046, moe=49159), 477 | dict(min=100000, max=124999, n=10273788, moe=47842), 478 | dict(min=125000, max=149999, n=6428069, moe=37952), 479 | dict(min=150000, max=199999, n=6931136, moe=37236), 480 | dict(min=200000, max=1000000, n=7465517, moe=42206) 481 | ] 482 | >>> approximate_mean(income) 483 | (98045.44530685373, 194.54892406267754) 484 | >>> approximate_mean(income, pareto=True) 485 | (60364.96525340687, 58.60735554621351) 486 | """ 487 | # Sort the list 488 | range_list.sort(key=lambda x: x["min"]) 489 | 490 | if pareto: # need shape parameter if using Pareto distribution 491 | nb1 = range_list[-2]["n"] # number in second to last bin 492 | nb = range_list[-1]["n"] # number in last bin 493 | lb1 = range_list[-2]["min"] # lower bound of second to last bin 494 | lb = range_list[-1]["min"] # lower bound of last bin 495 | alpha_hat = (numpy.log(nb1 + nb) - numpy.log(nb)) / ( 496 | numpy.log(lb) - numpy.log(lb1) 497 | ) # shape parameter for Pareto 498 | 499 | simulation_results = [] 500 | for i in range(simulations): 501 | simulated_values = [] 502 | simulated_n = [] 503 | # loop through every bin except the last one 504 | for range_ in range_list[:-1]: 505 | se = range_["moe"] / 1.645 # convert moe to se 506 | nn = round( 507 | numpy.random.normal(range_["n"], se) 508 | ) # use moe to introduce randomness into number in bin 509 | nn = int(nn) # clean it up 510 | simulated_values.append( 511 | numpy.random.uniform(range_["min"], range_["max"], size=(1, nn)).sum() 512 | ) # draw random values within the bin, assume uniform 513 | simulated_n.append(nn) 514 | # a special case to handle the last bin 515 | if pareto: 516 | last = range_list[-1] 517 | se = last["moe"] / 1.645 # convert moe to se 518 | nn = round( 519 | numpy.random.normal(last["n"], se) 520 | ) # use moe to introduce randomness into number in bin 521 | nn = int(nn) # clean it up 522 | simulated_values.append( 523 | numpy.random.pareto(a=alpha_hat, size=(1, nn)).sum() 524 | ) # draw random values within the bin, assume uniform 525 | simulated_n.append(nn) 526 | # use uniform otherwise 527 | else: 528 | last = range_list[-1] 529 | se = last["moe"] / 1.645 # convert moe to se 530 | nn = round( 531 | numpy.random.normal(last["n"], se) 532 | ) # use moe to introduce randomness into number in bin 533 | nn = int(nn) # clean it up 534 | simulated_values.append( 535 | numpy.random.uniform(last["min"], last["max"], size=(1, nn)).sum() 536 | ) # draw random values within the bin, assume uniform 537 | simulated_n.append(nn) 538 | simulation_results.append( 539 | sum(simulated_values) / sum(simulated_n) 540 | ) # calculate mean for replicate 541 | 542 | estimated_mean = numpy.mean(simulation_results) # calculate overall mean 543 | moe_right = ( 544 | numpy.quantile(simulation_results, 0.95) - estimated_mean 545 | ) # go from confidence interval to margin of error 546 | moe_left = estimated_mean - numpy.quantile( 547 | simulation_results, 0.05 548 | ) # go from confidence interval to margin of error 549 | margin_of_error = max( 550 | moe_left, moe_right 551 | ) # if asymmetrical take bigger one, conservative 552 | 553 | # Return the result 554 | return estimated_mean, margin_of_error 555 | -------------------------------------------------------------------------------- /census_data_aggregator/exceptions.py: -------------------------------------------------------------------------------- 1 | class DataError(Exception): 2 | """Raised by the data submitted to the function is invalid.""" 3 | 4 | pass 5 | 6 | 7 | class SamplingPercentageWarning(Warning): 8 | """Warns that you have not provided a design factor.""" 9 | 10 | def __str__(self): 11 | return """A margin of error cannot be calculated unless you provide a sampling percentage.""" 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 999 6 | ignore = E731,W503 7 | 8 | [metadata] 9 | license-file = LICENSE 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | 6 | def read(fname): 7 | with open(os.path.join(os.path.dirname(__file__), fname)) as f: 8 | return f.read() 9 | 10 | 11 | def version_scheme(version): 12 | """ 13 | Version scheme hack for setuptools_scm. 14 | 15 | Appears to be necessary to due to the bug documented here: https://github.com/pypa/setuptools_scm/issues/342 16 | 17 | If that issue is resolved, this method can be removed. 18 | """ 19 | import time 20 | 21 | from setuptools_scm.version import guess_next_version 22 | 23 | if version.exact: 24 | return version.format_with("{tag}") 25 | else: 26 | _super_value = version.format_next_version(guess_next_version) 27 | now = int(time.time()) 28 | return _super_value + str(now) 29 | 30 | 31 | def local_version(version): 32 | """ 33 | Local version scheme hack for setuptools_scm. 34 | 35 | Appears to be necessary to due to the bug documented here: https://github.com/pypa/setuptools_scm/issues/342 36 | 37 | If that issue is resolved, this method can be removed. 38 | """ 39 | return "" 40 | 41 | 42 | setup( 43 | name="census-data-aggregator", 44 | description="Combine U.S. census data responsibly", 45 | long_description=read("README.md"), 46 | long_description_content_type="text/markdown", 47 | license="MIT", 48 | packages=("census_data_aggregator",), 49 | install_requires=[ 50 | "numpy", 51 | ], 52 | use_scm_version={"version_scheme": version_scheme, "local_scheme": local_version}, 53 | classifiers=[ 54 | "Development Status :: 5 - Production/Stable", 55 | "Programming Language :: Python", 56 | "Programming Language :: Python :: 3", 57 | "Programming Language :: Python :: 3.7", 58 | "Programming Language :: Python :: 3.8", 59 | "Programming Language :: Python :: 3.9", 60 | "Programming Language :: Python :: 3.10", 61 | "License :: OSI Approved :: MIT License", 62 | ], 63 | project_urls={ 64 | "Maintainer": "https://github.com/datadesk", 65 | "Source": "https://github.com/datadesk/census-data-aggregator", 66 | "Tracker": "https://github.com/datadesk/census-data-aggregator/issues", 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import doctest 3 | import unittest 4 | 5 | import numpy 6 | 7 | import census_data_aggregator 8 | from census_data_aggregator.exceptions import DataError, SamplingPercentageWarning 9 | 10 | 11 | class CensusErrorAnalyzerTest(unittest.TestCase): 12 | def test_sum(self): 13 | males_under_5, males_under_5_moe = 10154024, 3778 14 | females_under_5, females_under_5_moe = 9712936, 3911 15 | self.assertEqual( 16 | census_data_aggregator.approximate_sum( 17 | (males_under_5, males_under_5_moe), 18 | (females_under_5, females_under_5_moe), 19 | ), 20 | (19866960, 5437.757350231803), 21 | ) 22 | # With multiple zeros 23 | self.assertEqual( 24 | census_data_aggregator.approximate_sum( 25 | [0.0, 22], [0, 22], [0, 29], [41, 37] 26 | ), 27 | (41, 47.01063709417264), 28 | ) 29 | # From the ACS handbook examples 30 | single_women = ((135173, 3860), (43104, 2642), (24842, 1957)) 31 | self.assertEqual( 32 | census_data_aggregator.approximate_sum(*single_women), 33 | (203119, 5070.4647715963865), 34 | ) 35 | 36 | def test_median(self): 37 | household_income_Los_Angeles_County_2013_acs5 = [ 38 | dict(min=2499, max=9999, n=209050), 39 | dict(min=10000, max=14999, n=190300), 40 | dict(min=15000, max=19999, n=173380), 41 | dict(min=20000, max=24999, n=167740), 42 | dict(min=25000, max=29999, n=154347), 43 | dict(min=30000, max=34999, n=155834), 44 | dict(min=35000, max=39999, n=143103), 45 | dict(min=40000, max=44999, n=140946), 46 | dict(min=45000, max=49999, n=126807), 47 | dict(min=50000, max=59999, n=241482), 48 | dict(min=60000, max=74999, n=303887), 49 | dict(min=75000, max=99999, n=384881), 50 | dict(min=100000, max=124999, n=268689), 51 | dict(min=125000, max=149999, n=169129), 52 | dict(min=150000, max=199999, n=189195), 53 | dict(min=200000, max=250001, n=211613), 54 | ] 55 | 56 | self.assertEqual( 57 | census_data_aggregator.approximate_median( 58 | household_income_Los_Angeles_County_2013_acs5, 59 | sampling_percentage=2.5 * 5, 60 | ), 61 | (56363.58534176461, 161.96723586588095), 62 | ) 63 | 64 | household_income_Los_Angeles_County_2013_acs3 = [ 65 | dict(min=2499, max=9999, n=222966), 66 | dict(min=10000, max=14999, n=197354), 67 | dict(min=15000, max=19999, n=178836), 68 | dict(min=20000, max=24999, n=177895), 69 | dict(min=25000, max=29999, n=155399), 70 | dict(min=30000, max=34999, n=156869), 71 | dict(min=35000, max=39999, n=145396), 72 | dict(min=40000, max=44999, n=141772), 73 | dict(min=45000, max=49999, n=125984), 74 | dict(min=50000, max=59999, n=237511), 75 | dict(min=60000, max=74999, n=303531), 76 | dict(min=75000, max=99999, n=371986), 77 | dict(min=100000, max=124999, n=264049), 78 | dict(min=125000, max=149999, n=164391), 79 | dict(min=150000, max=199999, n=179788), 80 | dict(min=200000, max=250001, n=209815), 81 | ] 82 | 83 | self.assertEqual( 84 | census_data_aggregator.approximate_median( 85 | household_income_Los_Angeles_County_2013_acs3, 86 | sampling_percentage=2.5 * 3, 87 | ), 88 | (54811.92744757085, 218.6913805834877), 89 | ) 90 | 91 | household_income_la_2013_acs1 = [ 92 | dict(min=2499, max=9999, n=1382), 93 | dict(min=10000, max=14999, n=2377), 94 | dict(min=15000, max=19999, n=1332), 95 | dict(min=20000, max=24999, n=3129), 96 | dict(min=25000, max=29999, n=1927), 97 | dict(min=30000, max=34999, n=1825), 98 | dict(min=35000, max=39999, n=1567), 99 | dict(min=40000, max=44999, n=1996), 100 | dict(min=45000, max=49999, n=1757), 101 | dict(min=50000, max=59999, n=3523), 102 | dict(min=60000, max=74999, n=4360), 103 | dict(min=75000, max=99999, n=6424), 104 | dict(min=100000, max=124999, n=5257), 105 | dict(min=125000, max=149999, n=3485), 106 | dict(min=150000, max=199999, n=2926), 107 | dict(min=200000, max=250001, n=4215), 108 | ] 109 | 110 | self.assertEqual( 111 | census_data_aggregator.approximate_median( 112 | household_income_la_2013_acs1, sampling_percentage=2.5 113 | ), 114 | (70065.84266055046, 3850.680465234964), 115 | ) 116 | 117 | with self.assertWarns(SamplingPercentageWarning): 118 | m, moe = census_data_aggregator.approximate_median( 119 | household_income_Los_Angeles_County_2013_acs5, design_factor=1.5 120 | ) 121 | self.assertTrue(moe is None) 122 | # Test a sample size so small the p values fail 123 | with self.assertRaises(DataError): 124 | bad_data = [ 125 | dict(min=0, max=49999, n=5), 126 | dict(min=50000, max=99999, n=5), 127 | dict(min=100000, max=199999, n=5), 128 | dict(min=200000, max=250001, n=5), 129 | ] 130 | census_data_aggregator.approximate_median( 131 | bad_data, design_factor=1.5, sampling_percentage=1 132 | ) 133 | 134 | top_median = [ 135 | dict(min=0, max=49999, n=50), 136 | dict(min=50000, max=99999, n=50), 137 | dict(min=100000, max=199999, n=50), 138 | dict(min=200000, max=250001, n=5000), 139 | ] 140 | census_data_aggregator.approximate_median( 141 | top_median, design_factor=1.5, sampling_percentage=1 142 | ) 143 | 144 | def test_percentchange(self): 145 | estimate, moe = census_data_aggregator.approximate_percentchange( 146 | (135173, 3860), (139301, 4047) 147 | ) 148 | self.assertAlmostEqual(estimate, 3.0538643072211165) 149 | self.assertAlmostEqual(moe, 4.198069852261231) 150 | 151 | def test_sum_ch8(self): 152 | # Never-married female characteristics from Table 8.1 153 | nmf_fairfax = (135173, 3860) 154 | nmf_arlington = (43104, 2642) 155 | nmf_alexandria = (24842, 1957) 156 | 157 | # Calculate aggregate pop and MOE 158 | agg_pop, agg_moe = census_data_aggregator.approximate_sum( 159 | nmf_fairfax, nmf_arlington, nmf_alexandria 160 | ) 161 | 162 | self.assertEqual(agg_pop, 203119) 163 | self.assertAlmostEqual(agg_moe, 5070, places=0) 164 | 165 | def test_proportion_ch8(self): 166 | # Total females aged 15 and older from Table 8.4 167 | tf15_fairfax = (466037, 391) 168 | tf15_arlington = (97360, 572) 169 | tf15_alexandria = (67101, 459) 170 | 171 | # Aggregate the values and MOEs 172 | denominator = census_data_aggregator.approximate_sum( 173 | tf15_fairfax, tf15_arlington, tf15_alexandria 174 | ) 175 | 176 | numerator = (203119, 5070) 177 | 178 | # Calculate the proportion and its MOE 179 | proportion, moe = census_data_aggregator.approximate_proportion( 180 | numerator, denominator 181 | ) 182 | 183 | self.assertAlmostEqual(proportion, 0.322, places=3) 184 | self.assertAlmostEqual(moe, 0.008, places=3) 185 | 186 | with self.assertRaises(DataError): 187 | census_data_aggregator.approximate_proportion(denominator, numerator) 188 | 189 | def test_ratio_ch8(self): 190 | # Never-married Males from table 8.5 191 | nmm_fairfax = (156720, 4222) 192 | nmm_arlington = (44613, 2819) 193 | nmm_alexandria = (25507, 2259) 194 | 195 | # Aggregate the values and MOEs 196 | numerator = census_data_aggregator.approximate_sum( 197 | nmm_fairfax, nmm_arlington, nmm_alexandria 198 | ) 199 | 200 | denominator = (203119, 5070) 201 | 202 | # Calculate the proportion and its MOE 203 | ratio, moe = census_data_aggregator.approximate_ratio(numerator, denominator) 204 | 205 | self.assertAlmostEqual(ratio, 1.117, places=3) 206 | self.assertAlmostEqual(moe, 0.039, places=3) 207 | 208 | def test_product_ch8(self): 209 | # Number of owner-occupied housing units in the United States 210 | oou = (74506512, 228238) 211 | # Percentage of single-unit, owner-occupied housing units in the United States 212 | pct_1unit_det_oou = (0.824, 0.001) 213 | 214 | ( 215 | num_1unit_det_oou_est, 216 | num_1unit_det_oou_moe, 217 | ) = census_data_aggregator.approximate_product(oou, pct_1unit_det_oou) 218 | 219 | self.assertAlmostEqual(num_1unit_det_oou_est, 61393366, places=0) 220 | self.assertAlmostEqual(num_1unit_det_oou_moe, 202289, places=0) 221 | 222 | def test_mean(self): 223 | range_list = [ 224 | dict(min=0, max=9999, n=7942251, moe=17662), 225 | dict(min=10000, max=14999, n=5768114, moe=16409), 226 | dict(min=15000, max=19999, n=5727180, moe=16801), 227 | dict(min=20000, max=24999, n=5910725, moe=17864), 228 | dict(min=25000, max=29999, n=5619002, moe=16113), 229 | dict(min=30000, max=34999, n=5711286, moe=15891), 230 | dict(min=35000, max=39999, n=5332778, moe=16488), 231 | dict(min=40000, max=44999, n=5354520, moe=15415), 232 | dict(min=45000, max=49999, n=4725195, moe=16890), 233 | dict(min=50000, max=59999, n=9181800, moe=20965), 234 | dict(min=60000, max=74999, n=11818514, moe=30723), 235 | dict(min=75000, max=99999, n=14636046, moe=49159), 236 | dict(min=100000, max=124999, n=10273788, moe=47842), 237 | dict(min=125000, max=149999, n=6428069, moe=37952), 238 | dict(min=150000, max=199999, n=6931136, moe=37236), 239 | dict(min=200000, max=1000000, n=7465517, moe=42206), 240 | ] 241 | numpy.random.seed(711355) 242 | # Calculate the mean and its MOE 243 | mean, moe = census_data_aggregator.approximate_mean(range_list) 244 | 245 | self.assertAlmostEqual(mean, 98045.44530685373, places=3) 246 | self.assertAlmostEqual(moe, 194.54892406267754, places=3) 247 | 248 | numpy.random.seed(711355) 249 | 250 | mean, moe = census_data_aggregator.approximate_mean(range_list, pareto=True) 251 | 252 | self.assertAlmostEqual(mean, 60364.96525340687, places=3) 253 | self.assertAlmostEqual(moe, 58.60735554621351, places=3) 254 | 255 | def test_mean_order(self): 256 | range_list = [ 257 | dict(min=50000, max=59999, n=9181800, moe=20965), 258 | dict(min=60000, max=74999, n=11818514, moe=30723), 259 | dict(min=75000, max=99999, n=14636046, moe=49159), 260 | dict(min=100000, max=124999, n=10273788, moe=47842), 261 | dict(min=125000, max=149999, n=6428069, moe=37952), 262 | dict(min=150000, max=199999, n=6931136, moe=37236), 263 | dict(min=200000, max=1000000, n=7465517, moe=42206), 264 | dict(min=0, max=9999, n=7942251, moe=17662), 265 | dict(min=10000, max=14999, n=5768114, moe=16409), 266 | dict(min=15000, max=19999, n=5727180, moe=16801), 267 | dict(min=20000, max=24999, n=5910725, moe=17864), 268 | dict(min=25000, max=29999, n=5619002, moe=16113), 269 | dict(min=30000, max=34999, n=5711286, moe=15891), 270 | dict(min=35000, max=39999, n=5332778, moe=16488), 271 | dict(min=40000, max=44999, n=5354520, moe=15415), 272 | dict(min=45000, max=49999, n=4725195, moe=16890), 273 | ] 274 | numpy.random.seed(711355) 275 | # Calculate the mean and its MOE 276 | mean, moe = census_data_aggregator.approximate_mean(range_list) 277 | 278 | self.assertAlmostEqual(mean, 98045.44530685373, places=3) 279 | self.assertAlmostEqual(moe, 194.54892406267754, places=3) 280 | 281 | numpy.random.seed(711355) 282 | 283 | mean, moe = census_data_aggregator.approximate_mean(range_list, pareto=True) 284 | 285 | self.assertAlmostEqual(mean, 60364.96525340687, places=3) 286 | self.assertAlmostEqual(moe, 58.60735554621351, places=3) 287 | 288 | 289 | if __name__ == "__main__": 290 | unittest.main() 291 | doctest.testmod("census_data_aggregator/__init__.py") 292 | --------------------------------------------------------------------------------