pynhts 0.1.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.
Files changed (61) hide show
  1. pynhts-0.1.1/.github/workflows/ci.yml +18 -0
  2. pynhts-0.1.1/.github/workflows/release.yml +36 -0
  3. pynhts-0.1.1/.gitignore +28 -0
  4. pynhts-0.1.1/CHANGELOG.md +24 -0
  5. pynhts-0.1.1/CITATION.cff +17 -0
  6. pynhts-0.1.1/CONTRIBUTING.md +18 -0
  7. pynhts-0.1.1/LICENSE +29 -0
  8. pynhts-0.1.1/PKG-INFO +130 -0
  9. pynhts-0.1.1/README.md +92 -0
  10. pynhts-0.1.1/docs/data_notes.md +67 -0
  11. pynhts-0.1.1/docs/index.md +53 -0
  12. pynhts-0.1.1/docs/methods.md +57 -0
  13. pynhts-0.1.1/docs/roadmap.md +24 -0
  14. pynhts-0.1.1/docs/validation.md +48 -0
  15. pynhts-0.1.1/mkdocs.yml +11 -0
  16. pynhts-0.1.1/paper/paper.bib +74 -0
  17. pynhts-0.1.1/paper/paper.md +100 -0
  18. pynhts-0.1.1/pyproject.toml +78 -0
  19. pynhts-0.1.1/src/pynhts/__init__.py +7 -0
  20. pynhts-0.1.1/src/pynhts/__main__.py +7 -0
  21. pynhts-0.1.1/src/pynhts/_registry/2017.yaml +62 -0
  22. pynhts-0.1.1/src/pynhts/_schema/2017/household.yaml +2924 -0
  23. pynhts-0.1.1/src/pynhts/_schema/2017/person.yaml +6306 -0
  24. pynhts-0.1.1/src/pynhts/_schema/2017/trip.yaml +4527 -0
  25. pynhts-0.1.1/src/pynhts/_schema/2017/vehicle.yaml +5123 -0
  26. pynhts-0.1.1/src/pynhts/cli.py +93 -0
  27. pynhts-0.1.1/src/pynhts/core/__init__.py +1 -0
  28. pynhts-0.1.1/src/pynhts/core/data.py +389 -0
  29. pynhts-0.1.1/src/pynhts/core/schema.py +112 -0
  30. pynhts-0.1.1/src/pynhts/io/__init__.py +27 -0
  31. pynhts-0.1.1/src/pynhts/io/converter.py +289 -0
  32. pynhts-0.1.1/src/pynhts/io/fetcher.py +237 -0
  33. pynhts-0.1.1/src/pynhts/io/registry.py +71 -0
  34. pynhts-0.1.1/src/pynhts/ml/__init__.py +197 -0
  35. pynhts-0.1.1/src/pynhts/py.typed +0 -0
  36. pynhts-0.1.1/src/pynhts/recipes/__init__.py +68 -0
  37. pynhts-0.1.1/src/pynhts/recipes/base.py +91 -0
  38. pynhts-0.1.1/src/pynhts/recipes/data/stt2017.yaml +106 -0
  39. pynhts-0.1.1/src/pynhts/recipes/stt2017.py +197 -0
  40. pynhts-0.1.1/src/pynhts/stats/__init__.py +21 -0
  41. pynhts-0.1.1/src/pynhts/stats/distributions.py +87 -0
  42. pynhts-0.1.1/src/pynhts/stats/jackknife.py +43 -0
  43. pynhts-0.1.1/src/pynhts/stats/survey.py +221 -0
  44. pynhts-0.1.1/tests/data/conftest.py +8 -0
  45. pynhts-0.1.1/tests/data/test_convert_real.py +104 -0
  46. pynhts-0.1.1/tests/data/test_loader_golden.py +111 -0
  47. pynhts-0.1.1/tests/data/test_network.py +20 -0
  48. pynhts-0.1.1/tests/data/test_real_files.py +45 -0
  49. pynhts-0.1.1/tests/data/test_recipes.py +51 -0
  50. pynhts-0.1.1/tests/data/test_schema_vs_csv.py +24 -0
  51. pynhts-0.1.1/tests/data/test_stats_golden.py +50 -0
  52. pynhts-0.1.1/tests/unit/test_cli.py +34 -0
  53. pynhts-0.1.1/tests/unit/test_convert.py +172 -0
  54. pynhts-0.1.1/tests/unit/test_data.py +146 -0
  55. pynhts-0.1.1/tests/unit/test_fetch.py +152 -0
  56. pynhts-0.1.1/tests/unit/test_ml.py +74 -0
  57. pynhts-0.1.1/tests/unit/test_recipes_unit.py +55 -0
  58. pynhts-0.1.1/tests/unit/test_schema.py +46 -0
  59. pynhts-0.1.1/tests/unit/test_stats.py +189 -0
  60. pynhts-0.1.1/tools/build_schema.py +140 -0
  61. pynhts-0.1.1/tools/make_validation_report.py +60 -0
@@ -0,0 +1,18 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ${{ matrix.os }}
6
+ strategy:
7
+ matrix:
8
+ os: [ubuntu-latest, windows-latest]
9
+ python: ["3.10", "3.13"]
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: ${{ matrix.python }}
15
+ - run: pip install -e ".[dev]"
16
+ - run: ruff check src tests
17
+ - run: mypy
18
+ - run: pytest # data/network tests are excluded by default (no microdata in CI)
@@ -0,0 +1,36 @@
1
+ name: release
2
+ # Publishes to PyPI with Trusted Publishing (OIDC): no token or password is stored anywhere.
3
+ # Trigger: publishing a GitHub release, or running the workflow by hand (workflow_dispatch).
4
+ on:
5
+ release:
6
+ types: [published]
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: python -m pip install build twine
18
+ - run: python -m build
19
+ - run: python -m twine check dist/*
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ # data and caches: never commit microdata or third-party copyrighted PDFs
2
+ local_data/
3
+ data_raw/
4
+ *.csv
5
+ *.parquet
6
+ *.zip
7
+ *.feather
8
+ *.arrow
9
+ *.parquet.*
10
+ *.sas7bdat
11
+ *.xpt
12
+ *.dbf
13
+ *.RData
14
+ *.rds
15
+ lit/*.pdf
16
+ .cache/
17
+ # python
18
+ __pycache__/
19
+ *.py[cod]
20
+ .venv/
21
+ .pytest_cache/
22
+ .mypy_cache/
23
+ .ruff_cache/
24
+ dist/
25
+ build/
26
+ *.egg-info/
27
+ .coverage
28
+ htmlcov/
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-10-17)
4
+
5
+ First version published on PyPI (0.1.0 exists only as a GitHub release). README links made absolute so
6
+ that they work on the PyPI page; no code changes.
7
+
8
+ ## 0.1.0 (2026-10-17)
9
+
10
+ First release. Scope: NHTS **2017** only.
11
+
12
+ * `pynhts.io`: SHA-256-pinned download of the official archives, offline mode, adoption of local
13
+ files, deterministic and idempotent CSV to Parquet conversion, replicate weights as float32.
14
+ * `pynhts.core`: codebook-derived typed schema (hierarchical, label-aware column ownership);
15
+ `NHTSData` loader with lazy columns, validated many-to-one joins and per-column masking of
16
+ survey codes (`missing="nan"|"keep"`, `skip="keep"|"nan"`); `decode_labels`.
17
+ * `pynhts.stats`: stratified jackknife (JKn, 98 replicates) totals, means, proportions and ratios
18
+ with standard error, margin of error, confidence interval, domains and groups;
19
+ `ratio_across` for rates built from two weight systems; Student-t quantile without SciPy.
20
+ * `pynhts.recipes`: replication of 24 figures of FHWA's *Summary of Travel Trends: 2017 NHTS*
21
+ (20 reproduced, 2 estimate-only, 2 not reproduced; gaps documented, none tuned away).
22
+ * `pynhts.ml`: NumPy arrays with sample weights, PyTorch tensors, household graph edges.
23
+ * Command line: `pynhts fetch | register | info | validate`.
24
+ * Documentation: methods, data pitfalls, generated validation report. JOSS paper draft.
@@ -0,0 +1,17 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use pynhts, please cite it as below."
3
+ title: "pynhts: survey-weight-aware access to US National Household Travel Survey microdata"
4
+ version: 0.1.1
5
+ date-released: 2026-10-17
6
+ license: BSD-3-Clause
7
+ repository-code: "https://github.com/sabernaseralavi-60/pynhts"
8
+ authors:
9
+ - family-names: Naseralavi
10
+ given-names: Seyed Saber
11
+ orcid: "https://orcid.org/0000-0002-5392-910X"
12
+ affiliation: "Department of Civil Engineering, Shahid Bahonar University of Kerman, Kerman, Iran"
13
+ keywords: [NHTS, travel survey, transportation, replicate weights, jackknife]
14
+ abstract: >-
15
+ Python library that downloads and verifies the 2017 NHTS public-use files, converts them to
16
+ typed Parquet, merges the tables without silent errors, computes jackknife (JKn) standard errors
17
+ and margins of error, and reproduces published FHWA tables.
@@ -0,0 +1,18 @@
1
+ # Contributing
2
+
3
+ Thanks for helping. Short rules:
4
+
5
+ 1. **Set up:** `uv venv && uv pip install -e ".[dev]"`.
6
+ 2. **Checks before a pull request:** `ruff check src tests`, `mypy`, `pytest`. The default test
7
+ run needs no data. Tests that need the real files carry `@pytest.mark.data`
8
+ (`pytest -m data`, with the files registered via `pynhts register <folder>`); the live
9
+ download test is `@pytest.mark.network` and is manual.
10
+ 3. **Never commit microdata** or third-party PDFs (`.gitignore` already excludes them).
11
+ 4. **Do not tune the library to match a published figure.** If a figure does not reproduce,
12
+ record it with a note in `src/pynhts/recipes/data/*.yaml` and set its `expected` status
13
+ honestly. Explanations of the documented gaps are very welcome as issues.
14
+ 5. **New waves:** add a registry file (`_registry/<wave>.yaml`), generate the schema with
15
+ `tools/build_schema.py` from the official codebook, and add golden tests before any
16
+ estimator is touched.
17
+ 6. Regenerate `docs/validation.md` with `python tools/make_validation_report.py` after any change
18
+ that can move an estimate.
pynhts-0.1.1/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, pynhts developers
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pynhts-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.5
2
+ Name: pynhts
3
+ Version: 0.1.1
4
+ Summary: Typed, survey-weight-aware access to US National Household Travel Survey microdata.
5
+ Project-URL: Homepage, https://github.com/sabernaseralavi-60/pynhts
6
+ Project-URL: Issues, https://github.com/sabernaseralavi-60/pynhts/issues
7
+ Project-URL: Changelog, https://github.com/sabernaseralavi-60/pynhts/blob/main/CHANGELOG.md
8
+ License: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: NHTS,jackknife,replicate weights,transportation,travel survey
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: pandas>=2.0
20
+ Requires-Dist: platformdirs>=3.10
21
+ Requires-Dist: pooch>=1.8
22
+ Requires-Dist: pyarrow>=14
23
+ Requires-Dist: pyyaml>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: openpyxl; extra == 'dev'
27
+ Requires-Dist: pandas-stubs; extra == 'dev'
28
+ Requires-Dist: pytest-cov; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: types-pyyaml; extra == 'dev'
32
+ Provides-Extra: graph
33
+ Requires-Dist: torch-geometric>=2.4; extra == 'graph'
34
+ Requires-Dist: torch>=2.0; extra == 'graph'
35
+ Provides-Extra: torch
36
+ Requires-Dist: torch>=2.0; extra == 'torch'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # pynhts
40
+
41
+ [![ci](https://github.com/sabernaseralavi-60/pynhts/actions/workflows/ci.yml/badge.svg)](https://github.com/sabernaseralavi-60/pynhts/actions/workflows/ci.yml)
42
+ ![status](https://img.shields.io/badge/status-alpha-orange)
43
+ ![python](https://img.shields.io/badge/python-3.10%2B-blue)
44
+ ![license](https://img.shields.io/badge/license-BSD--3--Clause-green)
45
+
46
+ Typed, survey-weight-aware Python access to US National Household Travel Survey (NHTS)
47
+ microdata.
48
+
49
+ > **Scope and status (0.1.0, alpha).** Only the **2017** wave is supported; other waves come
50
+ > after this one is solid. The API may still change. Results are validated
51
+ > against FHWA's published tables: 20 of 24 figures reproduce completely and 4 gaps are reported
52
+ > openly in [docs/validation.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md).
53
+
54
+ ## Install
55
+
56
+ ```
57
+ pip install pynhts
58
+ pynhts fetch --replicates # download, verify (SHA-256) and convert; about 0.4 GB of Parquet, plus 1.8 GB of raw CSV that you may delete afterwards
59
+ ```
60
+
61
+ Already have the NHTS 2017 CSV files? `pynhts register <folder>` adopts them after checking
62
+ their hashes.
63
+
64
+ ## What it does
65
+
66
+ ```python
67
+ import pynhts
68
+ data = pynhts.load(2017)
69
+
70
+ # lazy, validated tables: columns are joined from the table that owns them
71
+ trips = data.trips(["TRPMILES", "TRPTRANS", "HHFAMINC", "R_AGE"])
72
+
73
+ # weighted statistics with jackknife (JKn) standard errors and 95 % margins of error
74
+ s = data.survey("person", ["R_AGE_IMP", "DRIVER"])
75
+ s.total(domain=s.rows["DRIVER"] == "01") # 223,277 thousand drivers, MOE 827 thousand
76
+ data.survey("trip", ["TRPMILES"]).mean("TRPMILES") # 10.70 miles, MOE 0.40
77
+
78
+ # replicate a published table and see every gap
79
+ from pynhts import recipes
80
+ print(recipes.to_markdown(recipes.run("stt2017", data)))
81
+
82
+ # machine-learning bridge
83
+ from pynhts import ml
84
+ arr = ml.to_arrays(trips, ["TRPMILES", "R_AGE"], target="TRPTRANS", weight="WTTRDFIN")
85
+ ```
86
+
87
+ | Module | Purpose |
88
+ |---|---|
89
+ | `pynhts.io` | verified download, offline mode, deterministic Parquet cache |
90
+ | `pynhts.core` | codebook-derived schema, `NHTSData` loader, missing-code handling |
91
+ | `pynhts.stats` | JKn totals, means, proportions, ratios; margins of error |
92
+ | `pynhts.recipes` | replication of published tables with honest status per figure |
93
+ | `pynhts.ml` | NumPy / PyTorch / graph export (PyTorch optional) |
94
+
95
+ ## Why a library
96
+
97
+ Correct NHTS analysis has many silent traps (a data server that returns HTTP 200 for missing
98
+ files, string codes with leading zeros, `-1` skips that are not missing data, 107 copied
99
+ parent columns in the child tables, a vehicle `PERSONID` that is not the person key, ...).
100
+ They are documented in [docs/data_notes.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/data_notes.md) and each is covered by a test.
101
+
102
+ ## Validation
103
+
104
+ * All 3,807 coded values of the official codebook (counts and weighted totals) reproduce.
105
+ * 24 figures from Tables 1 and 3 of FHWA's *Summary of Travel Trends: 2017 NHTS*, with margins
106
+ of error: 20 reproduce completely, 2 reproduce the estimate but not the margin of error,
107
+ 2 do not reproduce. See [docs/validation.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md).
108
+
109
+ ## Documentation
110
+
111
+ [Methods](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/methods.md) · [Data notes](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/data_notes.md) · [Validation](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md)
112
+ · [Roadmap](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/roadmap.md) · [Contributing](https://github.com/sabernaseralavi-60/pynhts/blob/main/CONTRIBUTING.md) · [Changelog](https://github.com/sabernaseralavi-60/pynhts/blob/main/CHANGELOG.md)
113
+
114
+ ## Data
115
+
116
+ The NHTS microdata are public data of the US Federal Highway Administration
117
+ (<https://nhts.ornl.gov>). They are **never** bundled with or committed to this repository.
118
+
119
+ ## Development
120
+
121
+ ```
122
+ uv venv && uv pip install -e ".[dev]"
123
+ pytest # unit tests, no data needed
124
+ pytest -m data # real-data tests (files registered in the pynhts cache)
125
+ pytest -m network -o addopts=-ra # live ORNL download, manual only
126
+ ```
127
+
128
+ ## Citation
129
+
130
+ See [CITATION.cff](https://github.com/sabernaseralavi-60/pynhts/blob/main/CITATION.cff). License: BSD-3-Clause.
pynhts-0.1.1/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # pynhts
2
+
3
+ [![ci](https://github.com/sabernaseralavi-60/pynhts/actions/workflows/ci.yml/badge.svg)](https://github.com/sabernaseralavi-60/pynhts/actions/workflows/ci.yml)
4
+ ![status](https://img.shields.io/badge/status-alpha-orange)
5
+ ![python](https://img.shields.io/badge/python-3.10%2B-blue)
6
+ ![license](https://img.shields.io/badge/license-BSD--3--Clause-green)
7
+
8
+ Typed, survey-weight-aware Python access to US National Household Travel Survey (NHTS)
9
+ microdata.
10
+
11
+ > **Scope and status (0.1.0, alpha).** Only the **2017** wave is supported; other waves come
12
+ > after this one is solid. The API may still change. Results are validated
13
+ > against FHWA's published tables: 20 of 24 figures reproduce completely and 4 gaps are reported
14
+ > openly in [docs/validation.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md).
15
+
16
+ ## Install
17
+
18
+ ```
19
+ pip install pynhts
20
+ pynhts fetch --replicates # download, verify (SHA-256) and convert; about 0.4 GB of Parquet, plus 1.8 GB of raw CSV that you may delete afterwards
21
+ ```
22
+
23
+ Already have the NHTS 2017 CSV files? `pynhts register <folder>` adopts them after checking
24
+ their hashes.
25
+
26
+ ## What it does
27
+
28
+ ```python
29
+ import pynhts
30
+ data = pynhts.load(2017)
31
+
32
+ # lazy, validated tables: columns are joined from the table that owns them
33
+ trips = data.trips(["TRPMILES", "TRPTRANS", "HHFAMINC", "R_AGE"])
34
+
35
+ # weighted statistics with jackknife (JKn) standard errors and 95 % margins of error
36
+ s = data.survey("person", ["R_AGE_IMP", "DRIVER"])
37
+ s.total(domain=s.rows["DRIVER"] == "01") # 223,277 thousand drivers, MOE 827 thousand
38
+ data.survey("trip", ["TRPMILES"]).mean("TRPMILES") # 10.70 miles, MOE 0.40
39
+
40
+ # replicate a published table and see every gap
41
+ from pynhts import recipes
42
+ print(recipes.to_markdown(recipes.run("stt2017", data)))
43
+
44
+ # machine-learning bridge
45
+ from pynhts import ml
46
+ arr = ml.to_arrays(trips, ["TRPMILES", "R_AGE"], target="TRPTRANS", weight="WTTRDFIN")
47
+ ```
48
+
49
+ | Module | Purpose |
50
+ |---|---|
51
+ | `pynhts.io` | verified download, offline mode, deterministic Parquet cache |
52
+ | `pynhts.core` | codebook-derived schema, `NHTSData` loader, missing-code handling |
53
+ | `pynhts.stats` | JKn totals, means, proportions, ratios; margins of error |
54
+ | `pynhts.recipes` | replication of published tables with honest status per figure |
55
+ | `pynhts.ml` | NumPy / PyTorch / graph export (PyTorch optional) |
56
+
57
+ ## Why a library
58
+
59
+ Correct NHTS analysis has many silent traps (a data server that returns HTTP 200 for missing
60
+ files, string codes with leading zeros, `-1` skips that are not missing data, 107 copied
61
+ parent columns in the child tables, a vehicle `PERSONID` that is not the person key, ...).
62
+ They are documented in [docs/data_notes.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/data_notes.md) and each is covered by a test.
63
+
64
+ ## Validation
65
+
66
+ * All 3,807 coded values of the official codebook (counts and weighted totals) reproduce.
67
+ * 24 figures from Tables 1 and 3 of FHWA's *Summary of Travel Trends: 2017 NHTS*, with margins
68
+ of error: 20 reproduce completely, 2 reproduce the estimate but not the margin of error,
69
+ 2 do not reproduce. See [docs/validation.md](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md).
70
+
71
+ ## Documentation
72
+
73
+ [Methods](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/methods.md) · [Data notes](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/data_notes.md) · [Validation](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/validation.md)
74
+ · [Roadmap](https://github.com/sabernaseralavi-60/pynhts/blob/main/docs/roadmap.md) · [Contributing](https://github.com/sabernaseralavi-60/pynhts/blob/main/CONTRIBUTING.md) · [Changelog](https://github.com/sabernaseralavi-60/pynhts/blob/main/CHANGELOG.md)
75
+
76
+ ## Data
77
+
78
+ The NHTS microdata are public data of the US Federal Highway Administration
79
+ (<https://nhts.ornl.gov>). They are **never** bundled with or committed to this repository.
80
+
81
+ ## Development
82
+
83
+ ```
84
+ uv venv && uv pip install -e ".[dev]"
85
+ pytest # unit tests, no data needed
86
+ pytest -m data # real-data tests (files registered in the pynhts cache)
87
+ pytest -m network -o addopts=-ra # live ORNL download, manual only
88
+ ```
89
+
90
+ ## Citation
91
+
92
+ See [CITATION.cff](https://github.com/sabernaseralavi-60/pynhts/blob/main/CITATION.cff). License: BSD-3-Clause.
@@ -0,0 +1,67 @@
1
+ # NHTS 2017: pitfalls found while building pynhts
2
+
3
+ Every item below was checked against the files or the official documents, and each is covered
4
+ by a test. They are the reason the library exists.
5
+
6
+ ## Download and files
7
+
8
+ * **The ORNL server answers HTTP 200 with an HTML page for files that do not exist.** A status
9
+ check proves nothing. pynhts pins SHA-256 hashes for both archives and every member
10
+ (`src/pynhts/_registry/2017.yaml`).
11
+ * The files dated 2019 and the archive currently served (modified 2023) are byte-identical:
12
+ all four core CSV hashes match.
13
+ * There is **no trip-level replicate file**. `perwgt.csv` holds two blocks, the person weight
14
+ (`WTPERFIN`, `WTPERFIN1..98`) and the travel-day weight (`WTTRDFIN`, `WTTRDFIN1..98`); trips
15
+ get replicates by joining on `HOUSEID` + `PERSONID`.
16
+
17
+ ## Codes
18
+
19
+ * Coded columns are **strings with leading zeros** (`"01"`). Letting a reader infer types
20
+ silently turns them into integers. pynhts types every column from the codebook.
21
+ * Four different kinds of non-answer exist: `-9` not ascertained, `-8` don't know, `-7`
22
+ refused, and `-1` **appropriate skip** (the question did not apply; not missing). `-88`/`-77`
23
+ are "don't know"/"prefer not to answer" in numeric fields where -8/-7 could be real values.
24
+ * The codebook counts `-88` together with the `-8` rows (and `-77` with `-7`) in `YEARMILE` and
25
+ `OD_READ`: 149 and 144 rows hold -8/-7 although only -88/-77 are listed. Masking only the
26
+ listed codes would leave them in as negative numbers. pynhts masks them too.
27
+ * The codebook lists only the codes **observed in each table**, so the same variable can
28
+ declare different codes in the household and trip files. pynhts masks per column from the
29
+ schema of the table the column is read from, and a test proves that the real data contain no
30
+ undeclared missing or skip code.
31
+
32
+ ## Tables and keys
33
+
34
+ * **Child files carry copies of parent columns** (107 of them: 35 in the person file, 35 in
35
+ the vehicle file, 37 in the trip file). A naive merge creates `_x`/`_y` columns or picks the
36
+ wrong weight. The loader reads copies from the child table and joins only genuinely missing
37
+ columns, with a validated many-to-one join. A test proves that every copy equals its parent.
38
+ * **The same name can be a different variable.** In the vehicle file `PERSONID` is the person
39
+ who mainly drives the vehicle, not the person key, and `VEHID` is the vehicle's own key.
40
+ Ownership is therefore decided by name *and* label, and only toward ancestors.
41
+ * Vehicles use the **household** weight `WTHHFIN`.
42
+
43
+ ## Weights and variance
44
+
45
+ * The 2017 weighting report builds 98 jackknife replicates (14 variance strata of 7 units), so
46
+ Var = 6/7 * sum of squared replicate deviations. FHWA's margin of error equals the standard
47
+ error times t(0.975, 98) = 1.984 (checked against the published margins of error).
48
+ * `WTTRDFIN` equals 365 x `WTPERFIN`, in the full weights and in every replicate (to 1e-7,
49
+ float32 storage). All trips of one person share one weight, which lets trip estimates run on
50
+ 264,234 person-days instead of a 923,572 x 98 matrix.
51
+ * Replicates are stored as float32; they are cast to float64 before any squaring.
52
+ * Rates *per household* published by FHWA are person-weighted totals divided by
53
+ household-weighted households. They are ratios across two weight systems, which
54
+ `pynhts.stats.ratio_across` supports.
55
+
56
+ ## Variables
57
+
58
+ * `R_AGE` leaves 496 persons without an age; the report's age bands use the imputed age
59
+ `R_AGE_IMP`.
60
+ * The public person file starts at age 5. The report's printed total of persons (321,419
61
+ thousand) exceeds the sum of its own age bands (301,599) by 19,820 thousand; the weights
62
+ reproduce the sum of the bands exactly. The cause is not established.
63
+ * 656 trips have `TRPMILES = -9`. They count as trips but add no miles.
64
+ * The 2017 survey changed how trip distance is obtained (shortest route between geocoded
65
+ points instead of self-report). The report therefore prints "original" and "adjusted"
66
+ 2017 values; pynhts reproduces the **original** ones, which are what the public variable
67
+ `TRPMILES` contains.
@@ -0,0 +1,53 @@
1
+ # pynhts
2
+
3
+ Typed, survey-weight-aware Python access to US National Household Travel Survey (NHTS)
4
+ microdata. Phase 1 covers the **2017** wave.
5
+
6
+ ## Install
7
+
8
+ ```
9
+ pip install pynhts
10
+ ```
11
+
12
+ ## Five-minute tour
13
+
14
+ ```python
15
+ import pynhts
16
+
17
+ data = pynhts.load(2017) # downloads, verifies (SHA-256) and converts on first use
18
+
19
+ # 1. Lazy, validated tables; columns are joined from the table that owns them
20
+ trips = data.trips(["TRPMILES", "TRPTRANS", "HHFAMINC", "R_AGE"])
21
+
22
+ # 2. Correct weighted statistics with margins of error (jackknife, 98 replicate weights)
23
+ s = data.survey("person", ["R_AGE_IMP", "DRIVER"])
24
+ s.total(domain=s.rows["DRIVER"] == "01") # 223,277 thousand drivers, MOE 827 thousand
25
+
26
+ t = data.survey("trip", ["TRPMILES"])
27
+ t.mean("TRPMILES") # 10.70 miles, MOE 0.40
28
+
29
+ # 3. Replicate published tables and see every gap
30
+ from pynhts import recipes
31
+ print(recipes.to_markdown(recipes.run("stt2017", data)))
32
+
33
+ # 4. Hand data to machine-learning code
34
+ from pynhts import ml
35
+ arr = ml.to_arrays(trips, ["TRPMILES", "R_AGE"], target="TRPTRANS", weight="WTTRDFIN")
36
+ ```
37
+
38
+ Command line: `pynhts fetch --replicates`, `pynhts register <folder-with-your-CSVs>`,
39
+ `pynhts info`, `pynhts validate`.
40
+
41
+ ## Where the data live
42
+
43
+ Raw CSV and Parquet files go to `$PYNHTS_CACHE_DIR` or the user cache directory. They are
44
+ never part of the package or of the git repository. Files you already have can be adopted
45
+ with `pynhts register`, after their hashes are checked. After conversion the raw CSVs can be
46
+ deleted: the Parquet files are enough.
47
+
48
+ ## Pages
49
+
50
+ * [Validation against published tables](validation.md): what reproduces and what does not.
51
+ * [Methods](methods.md): weights, jackknife variance, margins of error.
52
+ * [Data notes](data_notes.md): the NHTS 2017 pitfalls found while building the library.
53
+ * [Roadmap](roadmap.md)
@@ -0,0 +1,57 @@
1
+ # Methods
2
+
3
+ ## Estimators
4
+
5
+ `data.survey(table, columns)` returns a `Survey`: the rows of a table bound to the 98 replicate
6
+ weights of the unit that carries the weight.
7
+
8
+ | Table | Weight | Replicate unit |
9
+ |---|---|---|
10
+ | household, vehicle | `WTHHFIN` | household |
11
+ | person | `WTPERFIN` | person |
12
+ | trip | `WTTRDFIN` | person-day |
13
+
14
+ Supported estimators, each with optional `domain=` (subpopulation) and `by=` (groups):
15
+
16
+ * `total(y)`: weighted total (`y=None` counts rows),
17
+ * `mean(y)`: ratio of the weighted total of `y` to the weighted count of rows with a value,
18
+ * `proportion(condition)`: mean of a boolean,
19
+ * `ratio(numerator, denominator)`: ratio of two weighted totals,
20
+ * `ratio_across(survey_a, y_a, survey_b, y_b)`: ratio between two surveys that share the
21
+ replicate index (person-weighted trips per household-weighted household).
22
+
23
+ Every result row has `estimate`, `se`, `moe`, `ci_low`, `ci_high`, `cv` and `n` (rows
24
+ contributing).
25
+
26
+ ## Variance
27
+
28
+ For an estimate `theta` with replicate estimates `theta_1 ... theta_98`:
29
+
30
+ Var(theta) = (6/7) * sum_r (theta_r - theta)^2 (stratified jackknife, JKn)
31
+ MOE = t(0.975, 98) * sqrt(Var) (95 %)
32
+
33
+ The factor 6/7 = (n_h - 1)/n_h comes from the 14 variance strata of 7 variance units described
34
+ in the 2017 NHTS weighting report. For ratios, the ratio is recomputed under every replicate
35
+ weight, so the variance reflects the variability of numerator and denominator jointly.
36
+ `level=` changes the confidence level; the degrees of freedom default to 98 as in FHWA's
37
+ tables.
38
+
39
+ ## Rows and units
40
+
41
+ All rows of one unit share one weight, so `sum_rows w_row * y_row = sum_units W_u * (sum of y
42
+ in unit u)`. The engine aggregates row values to the unit once (`numpy.bincount`) and works
43
+ on a matrix of at most 264,234 x 98 doubles, even for 923,572 trips.
44
+
45
+ ## Missing values
46
+
47
+ A row whose analysis variable is NA is dropped from numerator and denominator (complete cases
48
+ inside the domain), like `na.rm = TRUE` in R's `survey` package. Load tables with
49
+ `missing="nan"` (default) so that survey codes become NA. This is a statement about the
50
+ estimator, not about nonresponse bias: item nonresponse can bias an estimate and pynhts does not
51
+ adjust for it.
52
+
53
+ ## What is not provided
54
+
55
+ Variance for regression coefficients and other model-based estimates is not implemented yet;
56
+ the replicate weights are available (`data.replicate_weights`) for users who refit a model once
57
+ per replicate. Only the 2017 wave is supported.
@@ -0,0 +1,24 @@
1
+ # Roadmap
2
+
3
+ ## Done (version 0.1.0, NHTS 2017)
4
+
5
+ * Hash-pinned download, offline mode, adoption of local files (`pynhts.io`).
6
+ * Codebook-derived typed schema, deterministic idempotent Parquet cache with float32 replicate
7
+ weights (`pynhts.io`, `pynhts.core.schema`).
8
+ * `NHTSData` loader with lazy columns, validated joins and per-column missing-code masking
9
+ (`pynhts.core`).
10
+ * Jackknife estimators with margins of error and a cross-survey ratio (`pynhts.stats`).
11
+ * Recipe that reproduces 24 figures of FHWA's *Summary of Travel Trends: 2017 NHTS*
12
+ (`pynhts.recipes`); 20 reproduce completely, 4 gaps are documented.
13
+ * Arrays, PyTorch tensors and household graph edges (`pynhts.ml`); command line interface.
14
+
15
+ ## Not done yet
16
+
17
+ * **Other waves** (2022 NextGen, 2009, 2001, 1995). Deliberately postponed until the 2017
18
+ schema, merge logic and weighting were validated. The registry and schema layers are
19
+ per-wave, so a new wave adds files, not code paths.
20
+ * Variance of regression coefficients (refit once per replicate weight).
21
+ * More recipes: other tables of the Summary of Travel Trends and seminal NHTS papers.
22
+ * A hosted documentation site and a PyPI release.
23
+ * Explaining the four documented gaps (see [validation](validation.md)); reports from users
24
+ who can are welcome.
@@ -0,0 +1,48 @@
1
+ # Validation against published NHTS 2017 tables
2
+
3
+ *Generated by `tools/make_validation_report.py` on 2026-10-17 with pynhts 0.1.1. Do not edit by hand.*
4
+
5
+ **Source:** McGuckin, N. and Fucci, A. (2018). Summary of Travel Trends: 2017 National Household Travel Survey. Federal Highway Administration.
6
+
7
+ **Scorecard (24 published figures):** 20 reproduced, 2 estimate reproduced but margin of error differs, 2 not reproduced.
8
+
9
+ Status rule: the estimate must match the printed figure to half a unit of its last printed digit; the 95 % margin of error (MOE = t(0.975, 98) x JKn standard error) must match to the same rounding or 0.1 %, whichever is larger. Figures are in the report's units (thousands for Table 1, daily rates for Table 3). The *MOE diff* column is (ours - published) / published.
10
+
11
+ | Table | Quantity | Published | MOE pub. | Estimate | MOE | MOE diff | Status |
12
+ |---|---|---:|---:|---:|---:|---:|---|
13
+ | 1a | Households, all (thousands) | 118,208 | | 118,208 | 0 | | reproduced |
14
+ | 1a | Households, 1 person (thousands) | 32,952 | | 32,952 | 0 | | reproduced |
15
+ | 1a | Households, 2 persons (thousands) | 40,056 | | 40,056 | 0 | | reproduced |
16
+ | 1a | Households, 3 persons (thousands) | 18,521 | 97 | 18,521 | 97 | +0.4% | reproduced |
17
+ | 1a | Households, 4+ persons (thousands) | 26,679 | 97 | 26,679 | 97 | +0.4% | reproduced |
18
+ | 1b | Persons, all (thousands) | 321,419 | 0 | 301,599 | 0 | | not_reproduced |
19
+ | 1b | Persons under 16 (thousands) | 45,498 | 756 | 45,498 | 756 | +0.0% | reproduced |
20
+ | 1b | Persons 16-19 (thousands) | 17,755 | 945 | 17,755 | 945 | +0.0% | reproduced |
21
+ | 1b | Persons 20-34 (thousands) | 64,339 | 954 | 64,339 | 954 | -0.0% | reproduced |
22
+ | 1b | Persons 35-64 (thousands) | 126,350 | 985 | 126,350 | 986 | +0.1% | reproduced |
23
+ | 1b | Persons 65+ (thousands) | 47,657 | 0 | 47,657 | 0 | | reproduced |
24
+ | 1b | Persons 16+ (thousands) | 256,101 | 756 | 256,101 | 756 | +0.0% | reproduced |
25
+ | 1c | Drivers (thousands) | 223,277 | 827 | 223,277 | 827 | +0.0% | reproduced |
26
+ | 1c | Workers (thousands) | 156,988 | 1,012 | 156,988 | 1,012 | -0.0% | reproduced |
27
+ | 3a | Daily person trips per household | 8.60 | 0.10 | 8.60 | 0.10 | +1.9% | reproduced |
28
+ | 3a | Daily person miles per household | 92.02 | 3.50 | 92.02 | 3.50 | -0.1% | reproduced |
29
+ | 3a | Daily vehicle trips per household | 5.11 | 0.06 | 5.11 | 0.06 | -1.1% | reproduced |
30
+ | 3a | Daily vehicle miles per household | 48.81 | 2.04 | 48.81 | 2.04 | +0.1% | reproduced |
31
+ | 3b | Daily person trips per person | 3.37 | 0.04 | 3.37 | 0.04 | -0.2% | reproduced |
32
+ | 3b | Daily person miles per person | 36.07 | 1.47 | 36.07 | 1.37 | -6.8% | estimate_only |
33
+ | 3b | Average person trip length (miles) | 10.70 | 0.40 | 10.70 | 0.40 | -0.6% | reproduced |
34
+ | 3b | Daily vehicle trips per driver | 2.70 | 0.03 | 2.71 | 0.03 | -1.6% | not_reproduced |
35
+ | 3b | Daily vehicle miles per driver | 25.84 | 1.04 | 25.84 | 1.06 | +1.4% | estimate_only |
36
+ | 3b | Average vehicle trip length (miles) | 9.55 | 0.37 | 9.55 | 0.37 | +1.1% | reproduced |
37
+
38
+ ## Known gaps
39
+
40
+ * **Persons, all (thousands)** (`persons_all`, Table 1b, status `not_reproduced`): The printed total exceeds the sum of the report's own five age bands (301,599) by 19,820 thousand; the weights reproduce that sum exactly and the public file has no persons under age 5. Cause not established (a population-control baseline is a plausible but unverified explanation).
41
+
42
+ * **Daily person miles per person** (`pmt_per_person`, Table 3b, status `estimate_only`): Estimate reproduces (36.07) but the JKn margin of error is 1.37, 6.7 percent below the printed 1.47, while the MOE of person miles per household (3.50) and of trip length (0.40) reproduce. WTTRDFIN equals 365 x WTPERFIN in the full weights and in all 98 replicates (relative difference below 1.1e-7), so the weight system is not the cause. Not established.
43
+
44
+ * **Daily vehicle trips per driver** (`vtrips_per_driver`, Table 3b, status `not_reproduced`): We obtain 2.7064, which rounds to 2.71 against the printed 2.70 (0.2 percent). The MOE (0.0295) matches. Vehicle trips are defined here as trips on which the respondent drove (DRVR_FLG = 01); a slightly different definition in the report is the likely cause but is not established.
45
+
46
+ * **Daily vehicle miles per driver** (`vmt_per_driver`, Table 3b, status `estimate_only`): Estimate reproduces (25.84); JKn MOE is 1.055 against the printed 1.04 (1.4 percent).
47
+
48
+ These gaps are reported, not tuned away: no rule in the library was changed to make a figure match. If you can explain one, please open an issue.