buildingdata 0.7.0__tar.gz → 0.9.0__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.
- {buildingdata-0.7.0 → buildingdata-0.9.0}/PKG-INFO +34 -14
- {buildingdata-0.7.0 → buildingdata-0.9.0}/README.md +30 -13
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/__init__.py +2 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/__init__.py +2 -0
- buildingdata-0.9.0/buildingdata/reference/activity_library.py +241 -0
- buildingdata-0.9.0/buildingdata/reference/census_individuals.py +118 -0
- buildingdata-0.9.0/buildingdata/simulation/_epw.py +470 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/simulation/era5.py +99 -19
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/simulation/era5_bulk.py +13 -2
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_bulk.py +89 -0
- buildingdata-0.9.0/buildingdata/tests/test_pipeline_census_individuals.py +219 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_pipeline_diagnosis.py +7 -5
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_public_api.py +2 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_reference.py +415 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_simulation.py +119 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/PKG-INFO +34 -14
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/SOURCES.txt +3 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/requires.txt +4 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/pyproject.toml +8 -1
- buildingdata-0.7.0/buildingdata/simulation/_epw.py +0 -231
- {buildingdata-0.7.0 → buildingdata-0.9.0}/LICENSE +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/_cli.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/bulk.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/cache.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/config.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/exceptions.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/gcs.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/census.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/diagnosis.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/districts.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/elecdom.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/elmas.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/enedis.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/gas_network.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/occupant_diaries.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/reference/ore.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/simulation/__init__.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/simulation/bdtopo.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/simulation/bdtopo_bulk.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/__init__.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/conftest.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_cache.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_config.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/tests/test_gcs.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata/validation.py +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/dependency_links.txt +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/entry_points.txt +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/buildingdata.egg-info/top_level.txt +0 -0
- {buildingdata-0.7.0 → buildingdata-0.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildingdata
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -22,9 +22,12 @@ Requires-Dist: zarr>=2.18; extra == "era5"
|
|
|
22
22
|
Requires-Dist: dask>=2024.1; extra == "era5"
|
|
23
23
|
Provides-Extra: bulk
|
|
24
24
|
Requires-Dist: py7zr>=0.20; extra == "bulk"
|
|
25
|
+
Provides-Extra: timezone
|
|
26
|
+
Requires-Dist: timezonefinder>=6.0; extra == "timezone"
|
|
25
27
|
Provides-Extra: pipeline
|
|
26
28
|
Requires-Dist: snakemake>=8.0; extra == "pipeline"
|
|
27
29
|
Requires-Dist: openpyxl>=3.0; extra == "pipeline"
|
|
30
|
+
Requires-Dist: fastexcel>=0.11; extra == "pipeline"
|
|
28
31
|
Requires-Dist: py7zr>=0.20; extra == "pipeline"
|
|
29
32
|
Provides-Extra: docs
|
|
30
33
|
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
@@ -66,6 +69,9 @@ gas = bd.get_gas_network() # GRDF gas network routes -> geopandas GeoDat
|
|
|
66
69
|
# On-demand datasets (fetched live from public APIs)
|
|
67
70
|
buildings = bd.get_bdtopo("751010101") # per-IRIS building geometry (IGN WFS)
|
|
68
71
|
epw = bd.get_era5_climate(48.85, 2.35, 2020) # ERA5 weather -> synthetic EPW
|
|
72
|
+
# hour axis is UTC; pass
|
|
73
|
+
# timezone="Europe/Paris"
|
|
74
|
+
# for local standard time
|
|
69
75
|
```
|
|
70
76
|
|
|
71
77
|
You can also configure from the command line:
|
|
@@ -76,19 +82,32 @@ buildingdata configure --bucket my-bucket --cache-dir ~/.cache/buildingdata
|
|
|
76
82
|
|
|
77
83
|
## What it provides
|
|
78
84
|
|
|
79
|
-
| Function | Source | Returns | Consumed by |
|
|
80
|
-
| --- | --- | --- | --- |
|
|
81
|
-
| `get_census()` | INSEE census (GCS) | polars `DataFrame` | buildingmodel — dwelling inference |
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
85
|
+
| Function | Source | Licence | Returns | Consumed by |
|
|
86
|
+
| --- | --- | --- | --- | --- |
|
|
87
|
+
| `get_census()` | INSEE census (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — dwelling inference |
|
|
88
|
+
| `get_census_individuals()` | INSEE census individuals, activitymodel vocabulary (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — occupant inference (planned) |
|
|
89
|
+
| `get_districts()` | IRIS geometries (GCS) | Licence Ouverte 2.0 | geopandas `GeoDataFrame` | buildingmodel — geocoding; building_eload — district lookup |
|
|
90
|
+
| `get_diagnosis()` | ADEME energy performance diagnoses (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — envelope & system inference |
|
|
91
|
+
| `get_gas_network()` | GRDF gas network routes (GCS) | Licence Ouverte (Etalab) | geopandas `GeoDataFrame` | buildingmodel — gas-connection inference |
|
|
92
|
+
| `get_ore()` | Agence ORE annual consumption per IRIS (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | building_eload — calibration target |
|
|
93
|
+
| `get_enedis_national()` / `get_enedis_regional()` | Enedis conso-inf36 measured consumption (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | building_eload — validation reference |
|
|
94
|
+
| `get_elmas(table)` | ELMAS non-residential load curves (GCS) | **CC BY 4.0** | polars `DataFrame` | building_eload — non-residential model |
|
|
95
|
+
| `get_occupant_diaries()` | synthetic occupant activity calendar (GCS) | maintainers' synthetic artifact; derived from a **restricted-access** survey | polars `DataFrame` | building_eload — occupancy models |
|
|
96
|
+
| `get_activity_library(table)` | activitymodel diary library from the INSEE time-use survey (GCS) | maintainers' derived artifact, no survey identifier; derived from a **restricted-access** survey | polars `DataFrame` | building_eload — occupant calendars via activitymodel (planned) |
|
|
97
|
+
| `get_elecdom()` | Enedis panel Elecdom end-use load curves (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | provenance only — source of building_eload constants |
|
|
98
|
+
| `get_bdtopo(iris_code)` | IGN Géoplateforme WFS (live) or bulk cache | Licence Ouverte 2.0 | geopandas `GeoDataFrame` | building_eload → buildingmodel — input geometry |
|
|
99
|
+
| `get_era5_climate(lat, lon, year)` | Copernicus CDS (live) or bulk cache | **CC BY 4.0** (CDS account required) | path to EPW file (hour axis **UTC** by default, see `timezone=`) | buildingmodel — weather boundary condition |
|
|
100
|
+
|
|
101
|
+
The MIT `LICENSE` covers the **code only**. Each dataset keeps its upstream
|
|
102
|
+
licence, and every derived blob inherits the licence of the source it came
|
|
103
|
+
from — there is no separate "buildingdata data licence". Two rows are not
|
|
104
|
+
Licence Ouverte, and both matter for a publication: ELMAS is CC BY 4.0 (cite the
|
|
105
|
+
paper), and ERA5 is CC BY 4.0 behind a personal Copernicus account, so an
|
|
106
|
+
ERA5-dependent workflow is not anonymously reproducible. The occupant diaries
|
|
107
|
+
and the activity library derive from the INSEE *Enquête Emploi du temps
|
|
108
|
+
2009-2010*, which is obtained on request and is not redistributed here. The *Datasets* documentation
|
|
109
|
+
page carries the per-dataset attribution string to reproduce, the exact upstream
|
|
110
|
+
vintage, and the verification date.
|
|
92
111
|
|
|
93
112
|
Reference datasets are pulled from a public Google Cloud Storage bucket and
|
|
94
113
|
cached locally with generation-based freshness checks. French geospatial data
|
|
@@ -162,6 +181,7 @@ Because `~/.config/buildingdata/config.ini` is stored in your home directory, co
|
|
|
162
181
|
```bash
|
|
163
182
|
pip install "buildingdata[era5]" # ERA5 weather (cdsapi, xarray, pvlib, ...)
|
|
164
183
|
pip install "buildingdata[bulk]" # BDTOPO bulk prefetch (py7zr)
|
|
184
|
+
pip install "buildingdata[timezone]" # ERA5 timezone="local_standard" (timezonefinder)
|
|
165
185
|
pip install "buildingdata[docs]" # build the Sphinx documentation
|
|
166
186
|
```
|
|
167
187
|
|
|
@@ -30,6 +30,9 @@ gas = bd.get_gas_network() # GRDF gas network routes -> geopandas GeoDat
|
|
|
30
30
|
# On-demand datasets (fetched live from public APIs)
|
|
31
31
|
buildings = bd.get_bdtopo("751010101") # per-IRIS building geometry (IGN WFS)
|
|
32
32
|
epw = bd.get_era5_climate(48.85, 2.35, 2020) # ERA5 weather -> synthetic EPW
|
|
33
|
+
# hour axis is UTC; pass
|
|
34
|
+
# timezone="Europe/Paris"
|
|
35
|
+
# for local standard time
|
|
33
36
|
```
|
|
34
37
|
|
|
35
38
|
You can also configure from the command line:
|
|
@@ -40,19 +43,32 @@ buildingdata configure --bucket my-bucket --cache-dir ~/.cache/buildingdata
|
|
|
40
43
|
|
|
41
44
|
## What it provides
|
|
42
45
|
|
|
43
|
-
| Function | Source | Returns | Consumed by |
|
|
44
|
-
| --- | --- | --- | --- |
|
|
45
|
-
| `get_census()` | INSEE census (GCS) | polars `DataFrame` | buildingmodel — dwelling inference |
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
46
|
+
| Function | Source | Licence | Returns | Consumed by |
|
|
47
|
+
| --- | --- | --- | --- | --- |
|
|
48
|
+
| `get_census()` | INSEE census (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — dwelling inference |
|
|
49
|
+
| `get_census_individuals()` | INSEE census individuals, activitymodel vocabulary (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — occupant inference (planned) |
|
|
50
|
+
| `get_districts()` | IRIS geometries (GCS) | Licence Ouverte 2.0 | geopandas `GeoDataFrame` | buildingmodel — geocoding; building_eload — district lookup |
|
|
51
|
+
| `get_diagnosis()` | ADEME energy performance diagnoses (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | buildingmodel — envelope & system inference |
|
|
52
|
+
| `get_gas_network()` | GRDF gas network routes (GCS) | Licence Ouverte (Etalab) | geopandas `GeoDataFrame` | buildingmodel — gas-connection inference |
|
|
53
|
+
| `get_ore()` | Agence ORE annual consumption per IRIS (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | building_eload — calibration target |
|
|
54
|
+
| `get_enedis_national()` / `get_enedis_regional()` | Enedis conso-inf36 measured consumption (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | building_eload — validation reference |
|
|
55
|
+
| `get_elmas(table)` | ELMAS non-residential load curves (GCS) | **CC BY 4.0** | polars `DataFrame` | building_eload — non-residential model |
|
|
56
|
+
| `get_occupant_diaries()` | synthetic occupant activity calendar (GCS) | maintainers' synthetic artifact; derived from a **restricted-access** survey | polars `DataFrame` | building_eload — occupancy models |
|
|
57
|
+
| `get_activity_library(table)` | activitymodel diary library from the INSEE time-use survey (GCS) | maintainers' derived artifact, no survey identifier; derived from a **restricted-access** survey | polars `DataFrame` | building_eload — occupant calendars via activitymodel (planned) |
|
|
58
|
+
| `get_elecdom()` | Enedis panel Elecdom end-use load curves (GCS) | Licence Ouverte 2.0 | polars `DataFrame` | provenance only — source of building_eload constants |
|
|
59
|
+
| `get_bdtopo(iris_code)` | IGN Géoplateforme WFS (live) or bulk cache | Licence Ouverte 2.0 | geopandas `GeoDataFrame` | building_eload → buildingmodel — input geometry |
|
|
60
|
+
| `get_era5_climate(lat, lon, year)` | Copernicus CDS (live) or bulk cache | **CC BY 4.0** (CDS account required) | path to EPW file (hour axis **UTC** by default, see `timezone=`) | buildingmodel — weather boundary condition |
|
|
61
|
+
|
|
62
|
+
The MIT `LICENSE` covers the **code only**. Each dataset keeps its upstream
|
|
63
|
+
licence, and every derived blob inherits the licence of the source it came
|
|
64
|
+
from — there is no separate "buildingdata data licence". Two rows are not
|
|
65
|
+
Licence Ouverte, and both matter for a publication: ELMAS is CC BY 4.0 (cite the
|
|
66
|
+
paper), and ERA5 is CC BY 4.0 behind a personal Copernicus account, so an
|
|
67
|
+
ERA5-dependent workflow is not anonymously reproducible. The occupant diaries
|
|
68
|
+
and the activity library derive from the INSEE *Enquête Emploi du temps
|
|
69
|
+
2009-2010*, which is obtained on request and is not redistributed here. The *Datasets* documentation
|
|
70
|
+
page carries the per-dataset attribution string to reproduce, the exact upstream
|
|
71
|
+
vintage, and the verification date.
|
|
56
72
|
|
|
57
73
|
Reference datasets are pulled from a public Google Cloud Storage bucket and
|
|
58
74
|
cached locally with generation-based freshness checks. French geospatial data
|
|
@@ -126,6 +142,7 @@ Because `~/.config/buildingdata/config.ini` is stored in your home directory, co
|
|
|
126
142
|
```bash
|
|
127
143
|
pip install "buildingdata[era5]" # ERA5 weather (cdsapi, xarray, pvlib, ...)
|
|
128
144
|
pip install "buildingdata[bulk]" # BDTOPO bulk prefetch (py7zr)
|
|
145
|
+
pip install "buildingdata[timezone]" # ERA5 timezone="local_standard" (timezonefinder)
|
|
129
146
|
pip install "buildingdata[docs]" # build the Sphinx documentation
|
|
130
147
|
```
|
|
131
148
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
from .activity_library import get_activity_library
|
|
2
3
|
from .census import get_census
|
|
4
|
+
from .census_individuals import get_census_individuals
|
|
3
5
|
from .diagnosis import get_diagnosis
|
|
4
6
|
from .districts import get_districts
|
|
5
7
|
from .elecdom import get_elecdom
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
|
|
6
|
+
from ..exceptions import SchemaValidationError
|
|
7
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
8
|
+
from ..validation import require_columns, require_dtype
|
|
9
|
+
|
|
10
|
+
# The activity diary library built by ``activitymodel`` from the INSEE Enquete
|
|
11
|
+
# Emploi du temps 2009-2010 ships as four tables, one parquet blob each (the
|
|
12
|
+
# ELMAS pattern). Consumers assemble them with
|
|
13
|
+
# ``activitymodel.DiaryLibrary.from_frames``.
|
|
14
|
+
_BLOB_NAMES = {
|
|
15
|
+
"days": "activity_days_latest.parquet",
|
|
16
|
+
"clusters": "activity_clusters_latest.parquet",
|
|
17
|
+
"patterns": "activity_patterns_latest.parquet",
|
|
18
|
+
"manifest": "activity_manifest_latest.parquet",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_DATASET = "activity diary library"
|
|
22
|
+
|
|
23
|
+
# Per-table contract, transcribed from activitymodel 0.1's ``library`` module
|
|
24
|
+
# (DAY_META_SCHEMA, CLUSTER_SCHEMA, PATTERN_VALUE_SCHEMA and the manifest
|
|
25
|
+
# key/value frame). Transcribed rather than imported because activitymodel is
|
|
26
|
+
# optional here. The library schema is activitymodel's API: a change bumps its
|
|
27
|
+
# minor version and must be mirrored here in the same release cycle.
|
|
28
|
+
_SCHEMAS = {
|
|
29
|
+
"days": {
|
|
30
|
+
"day_id": pl.UInt32,
|
|
31
|
+
"day_type": pl.String,
|
|
32
|
+
"cluster_id": pl.Int16,
|
|
33
|
+
"weight": pl.Float64,
|
|
34
|
+
},
|
|
35
|
+
"clusters": {
|
|
36
|
+
"cluster_id": pl.Int16,
|
|
37
|
+
"day_type": pl.String,
|
|
38
|
+
"cluster_index": pl.Int16,
|
|
39
|
+
"n_days": pl.UInt32,
|
|
40
|
+
"weight_sum": pl.Float64,
|
|
41
|
+
},
|
|
42
|
+
"patterns": {"weight": pl.Float64, "n_persons": pl.UInt32},
|
|
43
|
+
"manifest": {"key": pl.String, "value": pl.String},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# One detailed activity code per 10-minute slot from 00:00, Europe/Paris wall
|
|
47
|
+
# clock. building_eload hardcodes 144 steps per day.
|
|
48
|
+
_SLOT_COLUMNS = tuple(f"slot_{i:03d}" for i in range(144))
|
|
49
|
+
|
|
50
|
+
# ``patterns`` links each socio-demographic cell to one cluster per day type,
|
|
51
|
+
# in columns ``cluster_<day_type>``.
|
|
52
|
+
_PATTERN_CLUSTER_PREFIX = "cluster_"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _table_dataset(table):
|
|
56
|
+
return f"{_DATASET} ({table!r} table)"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_days(df, dataset):
|
|
60
|
+
slots = {c for c in df.columns if c.startswith("slot_")}
|
|
61
|
+
missing = [c for c in _SLOT_COLUMNS if c not in slots]
|
|
62
|
+
unexpected = sorted(slots - set(_SLOT_COLUMNS))
|
|
63
|
+
if missing or unexpected:
|
|
64
|
+
raise SchemaValidationError(
|
|
65
|
+
f"{dataset}: expected the 144 slot columns slot_000..slot_143, found "
|
|
66
|
+
f"{len(slots)}; missing {missing[:5]}, unexpected {unexpected[:5]}. "
|
|
67
|
+
f"building_eload hardcodes 144 ten-minute steps per day, so a day of "
|
|
68
|
+
f"any other length would be silently misplaced on the timeline."
|
|
69
|
+
)
|
|
70
|
+
require_dtype(df, _SLOT_COLUMNS, pl.UInt8, dataset)
|
|
71
|
+
|
|
72
|
+
# The contract lists every column of ``days`` so that it provably holds no
|
|
73
|
+
# survey microdata: a survey identifier or a socio-demographic attribute
|
|
74
|
+
# next to the activity sequence would re-attach the day to a person.
|
|
75
|
+
extra = [c for c in df.columns if c not in _SCHEMAS["days"] and c not in slots]
|
|
76
|
+
if extra:
|
|
77
|
+
raise SchemaValidationError(
|
|
78
|
+
f"{dataset}: column(s) {extra[:10]} are not part of the contract. The "
|
|
79
|
+
f"published pool is activity sequences detached from any person or "
|
|
80
|
+
f"household; an identifier or socio-demographic column must not be "
|
|
81
|
+
f"published, and no consumer may rely on one."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_patterns(df, dataset):
|
|
86
|
+
cluster_columns = [c for c in df.columns if c.startswith(_PATTERN_CLUSTER_PREFIX)]
|
|
87
|
+
if not cluster_columns:
|
|
88
|
+
raise SchemaValidationError(
|
|
89
|
+
f"{dataset}: no '{_PATTERN_CLUSTER_PREFIX}<day_type>' column found in "
|
|
90
|
+
f"{df.columns}; without them the patterns link people to no cluster."
|
|
91
|
+
)
|
|
92
|
+
require_dtype(df, cluster_columns, pl.Int16, dataset)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_manifest(df, dataset):
|
|
96
|
+
duplicated = df.filter(pl.col("key").is_duplicated())["key"].unique().sort().to_list()
|
|
97
|
+
if duplicated:
|
|
98
|
+
raise SchemaValidationError(
|
|
99
|
+
f"{dataset}: duplicated key(s) {duplicated}. The manifest is read into a "
|
|
100
|
+
f"dict, where the last duplicate would silently win."
|
|
101
|
+
)
|
|
102
|
+
for key, value in df.select("key", "value").iter_rows():
|
|
103
|
+
try:
|
|
104
|
+
json.loads(value)
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
raise SchemaValidationError(
|
|
107
|
+
f"{dataset}: the value of {key!r} is not JSON-encoded: {value!r:.80}."
|
|
108
|
+
) from None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_table(table, df):
|
|
112
|
+
"""Assert one table carries its documented columns and dtypes."""
|
|
113
|
+
dataset = _table_dataset(table)
|
|
114
|
+
schema = _SCHEMAS[table]
|
|
115
|
+
require_columns(df, schema, dataset)
|
|
116
|
+
for column, dtype in schema.items():
|
|
117
|
+
require_dtype(df, (column,), dtype, dataset)
|
|
118
|
+
if table == "days":
|
|
119
|
+
_check_days(df, dataset)
|
|
120
|
+
elif table == "patterns":
|
|
121
|
+
_check_patterns(df, dataset)
|
|
122
|
+
elif table == "manifest":
|
|
123
|
+
_check_manifest(df, dataset)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _load_activitymodel():
|
|
127
|
+
"""Return ``(activitymodel, LibraryValidationError)``, or None when not installed."""
|
|
128
|
+
try:
|
|
129
|
+
import activitymodel
|
|
130
|
+
from activitymodel.exceptions import LibraryValidationError
|
|
131
|
+
except ModuleNotFoundError as exc:
|
|
132
|
+
# Only a missing activitymodel means "not installed": an installed one
|
|
133
|
+
# whose own import fails is broken and must say so.
|
|
134
|
+
if exc.name != "activitymodel":
|
|
135
|
+
raise
|
|
136
|
+
return None
|
|
137
|
+
return activitymodel, LibraryValidationError
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _read(table, refresh):
|
|
141
|
+
dest = ensure_blob_cached(
|
|
142
|
+
_BLOB_NAMES[table], refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
143
|
+
)
|
|
144
|
+
return pl.read_parquet(dest)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _validate(table, df, refresh):
|
|
148
|
+
"""Check ``table``; with activitymodel installed, check the whole library too."""
|
|
149
|
+
_check_table(table, df)
|
|
150
|
+
|
|
151
|
+
loaded = _load_activitymodel()
|
|
152
|
+
if loaded is None:
|
|
153
|
+
return df
|
|
154
|
+
activitymodel, library_validation_error = loaded
|
|
155
|
+
|
|
156
|
+
# DiaryLibrary.validate() is cross-table (a day's cluster must exist and
|
|
157
|
+
# share its day type, the manifest taxonomy must match the installed
|
|
158
|
+
# activitymodel's), so the sibling tables are read too.
|
|
159
|
+
frames = {name: df if name == table else _read(name, refresh) for name in _BLOB_NAMES}
|
|
160
|
+
for name, frame in frames.items():
|
|
161
|
+
if name != table:
|
|
162
|
+
_check_table(name, frame)
|
|
163
|
+
try:
|
|
164
|
+
activitymodel.DiaryLibrary.from_frames(**frames, validate=True)
|
|
165
|
+
except library_validation_error as exc:
|
|
166
|
+
raise SchemaValidationError(
|
|
167
|
+
f"{_DATASET}: the four tables fail activitymodel "
|
|
168
|
+
f"{activitymodel.__version__}'s DiaryLibrary.validate(): {exc}. If the "
|
|
169
|
+
f"blobs were restaged, call again with refresh=True so every table is "
|
|
170
|
+
f"re-read at its live generation; otherwise the library and the "
|
|
171
|
+
f"installed activitymodel disagree on the contract."
|
|
172
|
+
) from exc
|
|
173
|
+
return df
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_activity_library(table="days", refresh=False):
|
|
177
|
+
"""Return one table of the activity diary library (polars).
|
|
178
|
+
|
|
179
|
+
The library is built by ``activitymodel`` from the INSEE *Enquête Emploi du
|
|
180
|
+
temps 2009-2010* (PROGEDO-ADISP) and holds no survey microdata: ``days``
|
|
181
|
+
are activity sequences detached from any person, ``patterns`` are weighted
|
|
182
|
+
counts over socio-demographic cells. Each table is its own GCS blob;
|
|
183
|
+
``building_eload`` assembles all four with::
|
|
184
|
+
|
|
185
|
+
from activitymodel import DiaryLibrary
|
|
186
|
+
from activitymodel.library import TABLE_NAMES
|
|
187
|
+
|
|
188
|
+
library = DiaryLibrary.from_frames(
|
|
189
|
+
**{name: get_activity_library(name) for name in TABLE_NAMES}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
* ``"days"`` (default) -> ``activity_days_latest.parquet``
|
|
193
|
+
The pool of survey days: ``day_id`` (``UInt32``), ``day_type``
|
|
194
|
+
(``weekday`` / ``saturday`` / ``sunday``), ``cluster_id`` (``Int16``),
|
|
195
|
+
``weight`` (``Float64``, survey diary weight) and ``slot_000`` ..
|
|
196
|
+
``slot_143`` (``UInt8``), the detailed activity code of each 10-minute
|
|
197
|
+
slot from 00:00, Europe/Paris wall clock. Codes index the manifest's
|
|
198
|
+
``activities``. No other column.
|
|
199
|
+
* ``"clusters"`` -> ``activity_clusters_latest.parquet``
|
|
200
|
+
``cluster_id``, ``day_type``, ``cluster_index`` (``Int16``, 0..k-1 by
|
|
201
|
+
decreasing size within the day type), ``n_days`` (``UInt32``),
|
|
202
|
+
``weight_sum`` (``Float64``).
|
|
203
|
+
* ``"patterns"`` -> ``activity_patterns_latest.parquet``
|
|
204
|
+
One ``String`` column per matching key (``sex``, ``age_class``, ...),
|
|
205
|
+
one ``Int16`` ``cluster_<day_type>`` column per day type (null when the
|
|
206
|
+
cell kept no diary of that day type), ``weight`` (``Float64``, summed
|
|
207
|
+
person weight) and ``n_persons`` (``UInt32``).
|
|
208
|
+
* ``"manifest"`` -> ``activity_manifest_latest.parquet``
|
|
209
|
+
``key`` / ``value`` strings, values JSON-encoded: taxonomy, day types,
|
|
210
|
+
matching keys, clustering parameters, provenance.
|
|
211
|
+
|
|
212
|
+
Every call checks the requested table's columns and dtypes, including the
|
|
213
|
+
exact 144 ``UInt8`` slot columns and no column beyond the contract in
|
|
214
|
+
``days``. When ``activitymodel`` is installed, the other three tables are
|
|
215
|
+
read as well and the four are handed to ``DiaryLibrary.validate()``, which
|
|
216
|
+
adds the cross-table and value checks (cluster references, day types,
|
|
217
|
+
code range, weights, taxonomy of the installed version).
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
table (str): which table to return, one of ``"days"``, ``"clusters"``,
|
|
221
|
+
``"patterns"``, ``"manifest"``. Defaults to ``"days"``.
|
|
222
|
+
refresh (bool): force re-download even if the cache is warm. Also
|
|
223
|
+
applies to the sibling tables read for the cross-table check, so a
|
|
224
|
+
refreshed table is never checked against a stale one. Defaults to
|
|
225
|
+
False.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
polars.DataFrame: the requested table.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ValueError: if ``table`` is not a library table.
|
|
232
|
+
RemoteNotAvailableError: if the blob (or, with activitymodel
|
|
233
|
+
installed, a sibling blob) is not found in the GCS bucket.
|
|
234
|
+
SchemaValidationError: if the table, or the library as a whole,
|
|
235
|
+
breaks the contract.
|
|
236
|
+
"""
|
|
237
|
+
if table not in _BLOB_NAMES:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"Unknown activity library table {table!r}; expected one of {sorted(_BLOB_NAMES)}."
|
|
240
|
+
)
|
|
241
|
+
return _validate(table, _read(table, refresh), refresh)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
5
|
+
from ..validation import require_columns, require_dtype
|
|
6
|
+
|
|
7
|
+
_BLOB_NAME = "census_individuals_latest.parquet"
|
|
8
|
+
|
|
9
|
+
_DATASET = "INSEE census individuals"
|
|
10
|
+
|
|
11
|
+
# The published schema, mirrored from the pipeline's
|
|
12
|
+
# ``process_census_individuals.py::OUTPUT_SCHEMA``. The person columns are
|
|
13
|
+
# activitymodel's harmonised vocabulary; the dwelling columns use
|
|
14
|
+
# census_latest's labels, so buildingmodel can place a household on a dwelling
|
|
15
|
+
# by joining on them. Every categorical column is a plain ``String``.
|
|
16
|
+
_SCHEMA = {
|
|
17
|
+
# geography: district/city are null for people the census locates only to
|
|
18
|
+
# their canton-ou-ville; city_group is null when that canton spans several
|
|
19
|
+
"district": pl.String,
|
|
20
|
+
"city": pl.String,
|
|
21
|
+
"canton_ou_ville": pl.String,
|
|
22
|
+
"city_group": pl.String,
|
|
23
|
+
"city_group_candidates": pl.List(pl.String),
|
|
24
|
+
"department": pl.String,
|
|
25
|
+
# household
|
|
26
|
+
"household_key": pl.String,
|
|
27
|
+
"household_position": pl.String,
|
|
28
|
+
"ipondi": pl.Float64,
|
|
29
|
+
# person (activitymodel vocabulary)
|
|
30
|
+
"sex": pl.String,
|
|
31
|
+
"age": pl.Int64,
|
|
32
|
+
"age_class": pl.String,
|
|
33
|
+
"activity_status": pl.String,
|
|
34
|
+
"csp_group": pl.String,
|
|
35
|
+
"employment_status": pl.String,
|
|
36
|
+
"couple_status": pl.String,
|
|
37
|
+
"household_size": pl.Int64,
|
|
38
|
+
"household_size_class": pl.String,
|
|
39
|
+
# dwelling (census_latest vocabulary)
|
|
40
|
+
"residential_type": pl.String,
|
|
41
|
+
"construction_year_class_simplified": pl.String,
|
|
42
|
+
"heating_system": pl.String,
|
|
43
|
+
"occupancy_type": pl.String,
|
|
44
|
+
"occupant_count": pl.Int32,
|
|
45
|
+
"living_area_class": pl.String,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_census_individuals(city_group_list=None, refresh=False):
|
|
50
|
+
"""Return the INSEE census individuals (polars), optionally pre-filtered.
|
|
51
|
+
|
|
52
|
+
One row per person of an ordinary household living in a house or an
|
|
53
|
+
apartment in metropolitan France, from the RP2022 *Individus localisés au
|
|
54
|
+
canton-ou-ville* file (~16.5 M rows nationally), harmonised to the
|
|
55
|
+
activitymodel vocabulary. It is the input of
|
|
56
|
+
buildingmodel's occupant inference, which draws one census household per
|
|
57
|
+
dwelling. Downloaded from ``census_individuals_latest.parquet`` on GCS on
|
|
58
|
+
first call. Filtering uses a polars lazy scan, so only the requested rows
|
|
59
|
+
are materialised.
|
|
60
|
+
|
|
61
|
+
Columns:
|
|
62
|
+
|
|
63
|
+
* geography: ``district`` (IRIS), ``city``, ``canton_ou_ville`` (INSEE
|
|
64
|
+
pseudo-canton), ``city_group`` (EPCI, in ``get_districts``'s coding),
|
|
65
|
+
``city_group_candidates`` (list) and ``department``. The census locates a
|
|
66
|
+
person to the IRIS only in IRIS-split communes. Everyone else (41.6 % of
|
|
67
|
+
the weighted population) has a null ``district``/``city``, and
|
|
68
|
+
``city_group_candidates`` lists the city groups of their canton's
|
|
69
|
+
unsplit communes; ``city_group`` is set only when there is exactly one
|
|
70
|
+
(null for 27.4 % of the population).
|
|
71
|
+
* household: ``household_key`` (canton-ou-ville + INSEE household number),
|
|
72
|
+
``household_position`` (``reference_person``, ``partner``, ``child``,
|
|
73
|
+
``other``), ``ipondi`` (the INSEE person weight).
|
|
74
|
+
* person, activitymodel vocabulary: ``sex``, ``age``, ``age_class``,
|
|
75
|
+
``activity_status``, ``csp_group`` (null outside employment),
|
|
76
|
+
``employment_status``, ``couple_status``, ``household_size``,
|
|
77
|
+
``household_size_class``.
|
|
78
|
+
* dwelling, census_latest vocabulary: ``residential_type``,
|
|
79
|
+
``construction_year_class_simplified`` (the census file only has the
|
|
80
|
+
regrouped construction period, whose 2006+ classes do not split at 2012
|
|
81
|
+
like ``construction_year_class``), ``heating_system``,
|
|
82
|
+
``occupancy_type`` (always ``primary residence``), ``occupant_count``
|
|
83
|
+
(equal to ``household_size``) and ``living_area_class``.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
city_group_list (list of str, optional): EPCI codes to filter on. A
|
|
87
|
+
person is kept when any of their ``city_group_candidates`` is in the
|
|
88
|
+
list: someone located only to a canton that straddles two EPCIs is
|
|
89
|
+
returned for both. Filter on ``district`` being non-null to keep
|
|
90
|
+
IRIS-located people only. If None, the full national table is
|
|
91
|
+
returned. Defaults to None.
|
|
92
|
+
refresh (bool): force re-download even if the cache is warm.
|
|
93
|
+
Defaults to False.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
polars.DataFrame: one row per person, columns as listed above.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
100
|
+
SchemaValidationError: if the blob lacks a documented column or one
|
|
101
|
+
has another dtype.
|
|
102
|
+
"""
|
|
103
|
+
dest = ensure_blob_cached(
|
|
104
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
lf = pl.scan_parquet(dest)
|
|
108
|
+
schema = lf.collect_schema()
|
|
109
|
+
require_columns(schema, _SCHEMA, _DATASET)
|
|
110
|
+
for column, dtype in _SCHEMA.items():
|
|
111
|
+
require_dtype(schema, (column,), dtype, _DATASET)
|
|
112
|
+
|
|
113
|
+
if city_group_list is not None:
|
|
114
|
+
codes = [str(code) for code in city_group_list]
|
|
115
|
+
lf = lf.filter(
|
|
116
|
+
pl.col("city_group_candidates").list.eval(pl.element().is_in(codes)).list.any()
|
|
117
|
+
)
|
|
118
|
+
return lf.collect()
|