activitymodel 0.1.0__py3-none-any.whl
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.
- activitymodel/__init__.py +45 -0
- activitymodel/calendar.py +286 -0
- activitymodel/clustering.py +220 -0
- activitymodel/diagnostics/__init__.py +14 -0
- activitymodel/diagnostics/association.py +93 -0
- activitymodel/diagnostics/distance.py +33 -0
- activitymodel/diagnostics/profiles.py +59 -0
- activitymodel/exceptions.py +21 -0
- activitymodel/harmonise.py +269 -0
- activitymodel/library.py +325 -0
- activitymodel/matching.py +189 -0
- activitymodel/pipeline.py +239 -0
- activitymodel/survey/__init__.py +18 -0
- activitymodel/survey/diaries.py +285 -0
- activitymodel/survey/io.py +98 -0
- activitymodel/taxonomy.py +397 -0
- activitymodel/vocabulary.py +196 -0
- activitymodel-0.1.0.dist-info/METADATA +153 -0
- activitymodel-0.1.0.dist-info/RECORD +22 -0
- activitymodel-0.1.0.dist-info/WHEEL +5 -0
- activitymodel-0.1.0.dist-info/licenses/LICENCE.md +21 -0
- activitymodel-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Assign each occupant a cluster per day type from their socio-demographics.
|
|
2
|
+
|
|
3
|
+
The previous implementation drew a *survey diary* per occupant through
|
|
4
|
+
``buildingmodel.join_random_by_columns`` (repeating every diary row
|
|
5
|
+
``pond_carnet`` times, i.e. tens of millions of rows) and matched weekday and
|
|
6
|
+
weekend independently, losing the fact that the same respondent filled both.
|
|
7
|
+
|
|
8
|
+
Here the draw is over the library's aggregate ``patterns`` table:
|
|
9
|
+
|
|
10
|
+
1. Occupants are grouped by the values of the keys of the current ladder
|
|
11
|
+
level; the pattern rows are summed to the same keys.
|
|
12
|
+
2. Inside each cell one pattern row (a triple ``cluster_weekday``,
|
|
13
|
+
``cluster_saturday``, ``cluster_sunday``, some possibly null) is drawn per
|
|
14
|
+
occupant with probability proportional to the summed person weight. Drawing
|
|
15
|
+
a whole row keeps the weekday/weekend coupling observed on real people.
|
|
16
|
+
3. Occupants whose cell has no pattern fall through to the next, coarser level.
|
|
17
|
+
4. Day types left null by the drawn pattern are filled from the marginal
|
|
18
|
+
distribution of that day type, again walking the ladder from the finest
|
|
19
|
+
level with support.
|
|
20
|
+
|
|
21
|
+
Everything is driven by one ``numpy.random.Generator``; cells are visited in
|
|
22
|
+
sorted order, so a seed gives the same assignment on every machine.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Sequence
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import polars as pl
|
|
31
|
+
|
|
32
|
+
from .exceptions import MatchingError, VocabularyError
|
|
33
|
+
from .library import DiaryLibrary, cluster_column
|
|
34
|
+
from .vocabulary import DEFAULT_MATCHING_LADDER
|
|
35
|
+
|
|
36
|
+
MATCH_LEVEL_COLUMN = "match_level"
|
|
37
|
+
_CELL = "_cell"
|
|
38
|
+
_NULL_TOKEN = "\x00"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cell_expr(keys: Sequence[str]) -> pl.Expr:
|
|
42
|
+
if not keys:
|
|
43
|
+
return pl.lit("")
|
|
44
|
+
return pl.concat_str(
|
|
45
|
+
[pl.col(k).cast(pl.String).fill_null(_NULL_TOKEN) for k in keys], separator="|"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _augment(frame: pl.DataFrame) -> pl.DataFrame:
|
|
50
|
+
"""Derive the coarser ``activity_status`` when only ``employment_status`` exists."""
|
|
51
|
+
if "activity_status" not in frame.columns and "employment_status" in frame.columns:
|
|
52
|
+
frame = frame.with_columns(
|
|
53
|
+
activity_status=pl.when(pl.col("employment_status").str.starts_with("employed"))
|
|
54
|
+
.then(pl.lit("employed"))
|
|
55
|
+
.otherwise(pl.col("employment_status"))
|
|
56
|
+
)
|
|
57
|
+
return frame
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _draw_rows(
|
|
61
|
+
occupants: pl.DataFrame,
|
|
62
|
+
table: pl.DataFrame,
|
|
63
|
+
keys: Sequence[str],
|
|
64
|
+
value_columns: Sequence[str],
|
|
65
|
+
rng: np.random.Generator,
|
|
66
|
+
) -> pl.DataFrame:
|
|
67
|
+
"""Draw one ``table`` row per occupant inside its cell; unmatched occupants are dropped.
|
|
68
|
+
|
|
69
|
+
Returns ``occupants`` columns ``_occ`` plus ``value_columns``.
|
|
70
|
+
"""
|
|
71
|
+
grouped = (
|
|
72
|
+
table.group_by(list(keys) + list(value_columns), maintain_order=False)
|
|
73
|
+
.agg(pl.col("weight").sum())
|
|
74
|
+
.with_columns(_cell_expr(keys).alias(_CELL))
|
|
75
|
+
.sort([_CELL] + list(value_columns), nulls_last=True)
|
|
76
|
+
)
|
|
77
|
+
occ = occupants.with_columns(_cell_expr(keys).alias(_CELL))
|
|
78
|
+
pools = grouped.partition_by(_CELL, as_dict=True, maintain_order=True)
|
|
79
|
+
pieces = []
|
|
80
|
+
for (cell,), members in sorted(occ.partition_by(_CELL, as_dict=True).items()):
|
|
81
|
+
pool = pools.get((cell,))
|
|
82
|
+
if pool is None:
|
|
83
|
+
continue
|
|
84
|
+
weights = pool.get_column("weight").to_numpy()
|
|
85
|
+
picks = rng.choice(pool.height, size=members.height, p=weights / weights.sum())
|
|
86
|
+
pieces.append(
|
|
87
|
+
members.select("_occ").with_columns(
|
|
88
|
+
pool.select(list(value_columns))[picks].get_columns()
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
if not pieces:
|
|
92
|
+
return (
|
|
93
|
+
occupants.select("_occ")
|
|
94
|
+
.head(0)
|
|
95
|
+
.with_columns([pl.lit(None, dtype=table.schema[c]).alias(c) for c in value_columns])
|
|
96
|
+
)
|
|
97
|
+
return pl.concat(pieces)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def assign_clusters(
|
|
101
|
+
library: DiaryLibrary,
|
|
102
|
+
occupants: pl.DataFrame,
|
|
103
|
+
seed: int | np.random.Generator | None = 0,
|
|
104
|
+
ladder: Sequence[Sequence[str]] = DEFAULT_MATCHING_LADDER,
|
|
105
|
+
occupant_id: str = "occupant_id",
|
|
106
|
+
) -> pl.DataFrame:
|
|
107
|
+
"""Assign one cluster per day type to every occupant.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
library: the diary library.
|
|
111
|
+
occupants: one row per occupant with ``occupant_id`` and the harmonised
|
|
112
|
+
key columns used by ``ladder``.
|
|
113
|
+
seed: integer seed or a ``numpy.random.Generator``.
|
|
114
|
+
ladder: key subsets tried from finest to coarsest; every key must be a
|
|
115
|
+
library matching key or ``activity_status``. The last level should
|
|
116
|
+
be empty so nobody is left unmatched.
|
|
117
|
+
occupant_id: name of the occupant identifier column.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Frame with ``occupant_id``, ``cluster_<day_type>`` (``Int16``) for each
|
|
121
|
+
library day type and ``match_level`` (``Int8``, index of the ladder
|
|
122
|
+
level that matched, 0 = finest).
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
VocabularyError: an occupant column required by the ladder is missing.
|
|
126
|
+
MatchingError: occupants remain unmatched after the last level.
|
|
127
|
+
"""
|
|
128
|
+
rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed)
|
|
129
|
+
if occupant_id not in occupants.columns:
|
|
130
|
+
raise VocabularyError(f"occupants lack identifier column {occupant_id!r}")
|
|
131
|
+
if occupants.get_column(occupant_id).n_unique() != occupants.height:
|
|
132
|
+
raise VocabularyError(f"{occupant_id!r} is not unique")
|
|
133
|
+
|
|
134
|
+
patterns = _augment(library.patterns)
|
|
135
|
+
allowed = set(patterns.columns)
|
|
136
|
+
needed = {k for level in ladder for k in level}
|
|
137
|
+
unknown = needed - allowed
|
|
138
|
+
if unknown:
|
|
139
|
+
raise VocabularyError(
|
|
140
|
+
f"ladder uses keys absent from the library patterns: {sorted(unknown)}"
|
|
141
|
+
)
|
|
142
|
+
occ = _augment(occupants)
|
|
143
|
+
missing = needed - set(occ.columns)
|
|
144
|
+
if missing:
|
|
145
|
+
raise VocabularyError(f"occupants lack columns required by the ladder: {sorted(missing)}")
|
|
146
|
+
value_columns = library.cluster_columns
|
|
147
|
+
occ = occ.with_row_index("_occ")
|
|
148
|
+
|
|
149
|
+
# Pass 1: joint draw of a pattern row, walking the ladder.
|
|
150
|
+
remaining = occ
|
|
151
|
+
assigned = []
|
|
152
|
+
for level, keys in enumerate(ladder):
|
|
153
|
+
if remaining.height == 0:
|
|
154
|
+
break
|
|
155
|
+
drawn = _draw_rows(remaining, patterns, keys, value_columns, rng)
|
|
156
|
+
if drawn.height:
|
|
157
|
+
assigned.append(
|
|
158
|
+
drawn.with_columns(pl.lit(level, dtype=pl.Int8).alias(MATCH_LEVEL_COLUMN))
|
|
159
|
+
)
|
|
160
|
+
remaining = remaining.join(drawn.select("_occ"), on="_occ", how="anti")
|
|
161
|
+
if remaining.height:
|
|
162
|
+
raise MatchingError(
|
|
163
|
+
f"{remaining.height} occupants matched no pattern even at the coarsest level; "
|
|
164
|
+
"end the ladder with an empty key set"
|
|
165
|
+
)
|
|
166
|
+
result = pl.concat(assigned).join(occ, on="_occ", how="left")
|
|
167
|
+
|
|
168
|
+
# Pass 2: fill day types the drawn pattern left null from the marginals.
|
|
169
|
+
for day_type in library.day_types:
|
|
170
|
+
column = cluster_column(day_type)
|
|
171
|
+
marginal = patterns.filter(pl.col(column).is_not_null())
|
|
172
|
+
for keys in ladder:
|
|
173
|
+
holes = result.filter(pl.col(column).is_null())
|
|
174
|
+
if holes.height == 0:
|
|
175
|
+
break
|
|
176
|
+
filled = _draw_rows(holes, marginal, keys, [column], rng).rename({column: "_fill"})
|
|
177
|
+
result = (
|
|
178
|
+
result.join(filled, on="_occ", how="left")
|
|
179
|
+
.with_columns(pl.coalesce([pl.col(column), pl.col("_fill")]).alias(column))
|
|
180
|
+
.drop("_fill")
|
|
181
|
+
)
|
|
182
|
+
if result.get_column(column).null_count():
|
|
183
|
+
raise MatchingError(f"no pattern provides a {day_type} cluster at any ladder level")
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
result.sort("_occ")
|
|
187
|
+
.select([occupant_id] + value_columns + [MATCH_LEVEL_COLUMN])
|
|
188
|
+
.with_columns([pl.col(c).cast(pl.Int16) for c in value_columns])
|
|
189
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Build a :class:`~activitymodel.library.DiaryLibrary` from the survey.
|
|
2
|
+
|
|
3
|
+
This is the entry point the buildingdata Snakemake rule calls::
|
|
4
|
+
|
|
5
|
+
from activitymodel.pipeline import LibraryBuildOptions, build_library
|
|
6
|
+
library = build_library(survey_directory, LibraryBuildOptions())
|
|
7
|
+
library.write(output_directory)
|
|
8
|
+
|
|
9
|
+
It can also be run by hand::
|
|
10
|
+
|
|
11
|
+
python -m activitymodel.pipeline /path/to/edt2010 /path/to/output --weekday 16 --saturday 10 --sunday 10
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import datetime as dt
|
|
18
|
+
from dataclasses import asdict, dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import polars as pl
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
from .clustering import ClusteringOptions, cluster_codes
|
|
26
|
+
from .library import (
|
|
27
|
+
CLOCK,
|
|
28
|
+
SLOT_COLUMNS,
|
|
29
|
+
SLOT_MINUTES,
|
|
30
|
+
SLOTS_PER_DAY,
|
|
31
|
+
DiaryLibrary,
|
|
32
|
+
cluster_column,
|
|
33
|
+
)
|
|
34
|
+
from .survey.diaries import DAY_TYPES, DiaryOptions, DiaryTable, build_diaries
|
|
35
|
+
from .survey.io import SurveyFiles, load_survey_tables
|
|
36
|
+
from .taxonomy import DETAILED_ACTIVITIES
|
|
37
|
+
from .vocabulary import DEFAULT_MATCHING_KEYS
|
|
38
|
+
|
|
39
|
+
DEFAULT_SOURCE = {
|
|
40
|
+
"name": "INSEE Enquete Emploi du temps 2009-2010",
|
|
41
|
+
"distributor": "PROGEDO-ADISP",
|
|
42
|
+
"doi": "10.13144/lil-0695",
|
|
43
|
+
"url": "https://data.progedo.fr/studies/doi/10.13144/lil-0695",
|
|
44
|
+
"access": "on request, not redistributable; the library holds no microdata row",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class LibraryBuildOptions:
|
|
50
|
+
"""Everything that shapes a library build, recorded in its manifest."""
|
|
51
|
+
|
|
52
|
+
diaries: DiaryOptions = field(default_factory=DiaryOptions)
|
|
53
|
+
clustering: ClusteringOptions = field(default_factory=ClusteringOptions)
|
|
54
|
+
matching_keys: tuple[str, ...] = DEFAULT_MATCHING_KEYS
|
|
55
|
+
source: dict = field(default_factory=lambda: dict(DEFAULT_SOURCE))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_library(
|
|
59
|
+
survey: str | Path | SurveyFiles | tuple[pl.DataFrame, pl.DataFrame, pl.DataFrame],
|
|
60
|
+
options: LibraryBuildOptions | None = None,
|
|
61
|
+
) -> DiaryLibrary:
|
|
62
|
+
"""Read the survey, code and cluster the diaries, assemble the library.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
survey: a directory holding the ADISP files, a :class:`SurveyFiles`, or
|
|
66
|
+
the three raw frames ``(diaries, individuals, households)``.
|
|
67
|
+
options: build options (defaults to :class:`LibraryBuildOptions`).
|
|
68
|
+
"""
|
|
69
|
+
options = LibraryBuildOptions() if options is None else options
|
|
70
|
+
if isinstance(survey, tuple):
|
|
71
|
+
diaries, individuals, households = survey
|
|
72
|
+
else:
|
|
73
|
+
files = survey if isinstance(survey, SurveyFiles) else SurveyFiles.from_directory(survey)
|
|
74
|
+
diaries, individuals, households = load_survey_tables(files)
|
|
75
|
+
table = build_diaries(diaries, individuals, households, options.diaries)
|
|
76
|
+
labels = cluster_diaries(table, options.clustering)
|
|
77
|
+
return assemble_library(table, labels, options)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cluster_diaries(table: DiaryTable, options: ClusteringOptions) -> dict[str, np.ndarray]:
|
|
81
|
+
"""Cluster each day type's pool. Returns ``day_type -> labels`` (aligned with the table)."""
|
|
82
|
+
rng = np.random.default_rng(options.seed)
|
|
83
|
+
day_types = table.frame.get_column("day_type").to_numpy().astype(object)
|
|
84
|
+
weights = table.frame.get_column("weight").to_numpy()
|
|
85
|
+
labels = {}
|
|
86
|
+
for day_type in DAY_TYPES:
|
|
87
|
+
rows = np.flatnonzero(day_types == day_type)
|
|
88
|
+
if rows.size == 0:
|
|
89
|
+
continue
|
|
90
|
+
k = options.n_clusters.get(day_type)
|
|
91
|
+
if k is None:
|
|
92
|
+
raise ValueError(f"no cluster count configured for day type {day_type!r}")
|
|
93
|
+
out = np.full(table.n_diaries, -1, dtype=np.int32)
|
|
94
|
+
out[rows] = cluster_codes(table.codes[rows], k, options, weights=weights[rows], rng=rng)
|
|
95
|
+
labels[day_type] = out
|
|
96
|
+
return labels
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def assemble_library(
|
|
100
|
+
table: DiaryTable, labels: dict[str, np.ndarray], options: LibraryBuildOptions
|
|
101
|
+
) -> DiaryLibrary:
|
|
102
|
+
"""Turn a coded diary table and its cluster labels into the published tables."""
|
|
103
|
+
day_types = tuple(d for d in DAY_TYPES if d in labels)
|
|
104
|
+
frame = table.frame.with_row_index("_row")
|
|
105
|
+
keys = list(options.matching_keys)
|
|
106
|
+
|
|
107
|
+
# Global cluster ids: contiguous blocks per day type, in DAY_TYPES order.
|
|
108
|
+
cluster_index = np.full(table.n_diaries, -1, dtype=np.int32)
|
|
109
|
+
cluster_id = np.full(table.n_diaries, -1, dtype=np.int32)
|
|
110
|
+
offset = 0
|
|
111
|
+
cluster_rows = []
|
|
112
|
+
for day_type in day_types:
|
|
113
|
+
mask = labels[day_type] >= 0
|
|
114
|
+
k = int(labels[day_type][mask].max()) + 1
|
|
115
|
+
cluster_index[mask] = labels[day_type][mask]
|
|
116
|
+
cluster_id[mask] = offset + labels[day_type][mask]
|
|
117
|
+
for i in range(k):
|
|
118
|
+
cluster_rows.append(
|
|
119
|
+
{"cluster_id": offset + i, "day_type": day_type, "cluster_index": i}
|
|
120
|
+
)
|
|
121
|
+
offset += k
|
|
122
|
+
frame = frame.with_columns(
|
|
123
|
+
pl.Series("cluster_id", cluster_id, dtype=pl.Int16),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
slots = pl.DataFrame(table.codes, schema=list(SLOT_COLUMNS)).cast(pl.UInt8)
|
|
127
|
+
days = frame.select(
|
|
128
|
+
pl.col("_row").cast(pl.UInt32).alias("day_id"),
|
|
129
|
+
pl.col("day_type").cast(pl.String),
|
|
130
|
+
pl.col("cluster_id"),
|
|
131
|
+
pl.col("weight").cast(pl.Float64),
|
|
132
|
+
).with_columns(slots.get_columns())
|
|
133
|
+
|
|
134
|
+
clusters = (
|
|
135
|
+
pl.DataFrame(cluster_rows)
|
|
136
|
+
.cast({"cluster_id": pl.Int16, "cluster_index": pl.Int16})
|
|
137
|
+
.join(
|
|
138
|
+
days.group_by("cluster_id").agg(
|
|
139
|
+
pl.len().cast(pl.UInt32).alias("n_days"), pl.col("weight").sum().alias("weight_sum")
|
|
140
|
+
),
|
|
141
|
+
on="cluster_id",
|
|
142
|
+
how="left",
|
|
143
|
+
)
|
|
144
|
+
.sort("cluster_id")
|
|
145
|
+
.select("cluster_id", "day_type", "cluster_index", "n_days", "weight_sum")
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# One row per person: cluster per day type (first diary of that type),
|
|
149
|
+
# person weight (questionnaire weight, else mean diary weight), keys.
|
|
150
|
+
per_person = (
|
|
151
|
+
frame.sort("diary_id")
|
|
152
|
+
.group_by("person_id", maintain_order=True)
|
|
153
|
+
.agg(
|
|
154
|
+
[pl.col(k).first() for k in keys]
|
|
155
|
+
+ [
|
|
156
|
+
pl.coalesce([pl.col("person_weight").first(), pl.col("weight").mean()]).alias(
|
|
157
|
+
"weight"
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
+ [
|
|
161
|
+
pl.col("cluster_id")
|
|
162
|
+
.filter(pl.col("day_type") == d)
|
|
163
|
+
.first()
|
|
164
|
+
.alias(cluster_column(d))
|
|
165
|
+
for d in day_types
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
patterns = (
|
|
170
|
+
per_person.group_by(keys + [cluster_column(d) for d in day_types])
|
|
171
|
+
.agg(pl.col("weight").sum(), pl.len().cast(pl.UInt32).alias("n_persons"))
|
|
172
|
+
.with_columns([pl.col(k).cast(pl.String) for k in keys])
|
|
173
|
+
.sort(keys + [cluster_column(d) for d in day_types], nulls_last=True)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
manifest = {
|
|
177
|
+
"activitymodel_version": __version__,
|
|
178
|
+
"created_at": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
|
|
179
|
+
"source": dict(options.source),
|
|
180
|
+
"taxonomy_level": "detailed",
|
|
181
|
+
"activities": list(DETAILED_ACTIVITIES),
|
|
182
|
+
"slot_minutes": SLOT_MINUTES,
|
|
183
|
+
"slots_per_day": SLOTS_PER_DAY,
|
|
184
|
+
"clock": CLOCK,
|
|
185
|
+
"day_types": list(day_types),
|
|
186
|
+
"matching_keys": keys,
|
|
187
|
+
"age_class_bounds": list(options.diaries.age_classes.bounds),
|
|
188
|
+
"diary_options": _jsonable(asdict(options.diaries)),
|
|
189
|
+
"clustering": _jsonable(asdict(options.clustering)),
|
|
190
|
+
"n_days": int(days.height),
|
|
191
|
+
"n_persons": int(per_person.height),
|
|
192
|
+
}
|
|
193
|
+
return DiaryLibrary.from_frames(days, clusters, patterns, manifest)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _jsonable(value):
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
return {str(k): _jsonable(v) for k, v in value.items()}
|
|
199
|
+
if isinstance(value, (list, tuple)):
|
|
200
|
+
return [_jsonable(v) for v in value]
|
|
201
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
202
|
+
return value
|
|
203
|
+
if hasattr(value, "bounds"): # AgeClasses
|
|
204
|
+
return list(value.bounds)
|
|
205
|
+
return str(value)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main(argv=None) -> None:
|
|
209
|
+
parser = argparse.ArgumentParser(
|
|
210
|
+
description="Build an activitymodel diary library from the EDT 2010 files."
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"survey_dir", type=Path, help="directory holding carnet/individu/menage (.parquet or .dta)"
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument("output_dir", type=Path, help="directory receiving the library tables")
|
|
216
|
+
parser.add_argument("--weekday", type=int, default=16)
|
|
217
|
+
parser.add_argument("--saturday", type=int, default=10)
|
|
218
|
+
parser.add_argument("--sunday", type=int, default=10)
|
|
219
|
+
parser.add_argument("--method", choices=("ward", "kmeans"), default="ward")
|
|
220
|
+
parser.add_argument("--level", default="detailed", help="taxonomy level used for clustering")
|
|
221
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
222
|
+
args = parser.parse_args(argv)
|
|
223
|
+
options = LibraryBuildOptions(
|
|
224
|
+
clustering=ClusteringOptions(
|
|
225
|
+
n_clusters={"weekday": args.weekday, "saturday": args.saturday, "sunday": args.sunday},
|
|
226
|
+
method=args.method,
|
|
227
|
+
level=args.level,
|
|
228
|
+
seed=args.seed,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
library = build_library(args.survey_dir, options)
|
|
232
|
+
paths = library.write(args.output_dir)
|
|
233
|
+
print(library.summary())
|
|
234
|
+
for name, path in paths.items():
|
|
235
|
+
print(f" wrote {name:9s} -> {path}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Reading and preparing the Enquete Emploi du temps 2009-2010 (pipeline side).
|
|
2
|
+
|
|
3
|
+
Nothing in this sub-package is imported by the matching or calendar modules:
|
|
4
|
+
building_eload never needs it, and it is the only part of activitymodel that
|
|
5
|
+
touches restricted survey microdata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .diaries import DiaryOptions, DiaryTable, build_diaries
|
|
9
|
+
from .io import SurveyFiles, load_survey_tables, read_table
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DiaryOptions",
|
|
13
|
+
"DiaryTable",
|
|
14
|
+
"SurveyFiles",
|
|
15
|
+
"build_diaries",
|
|
16
|
+
"load_survey_tables",
|
|
17
|
+
"read_table",
|
|
18
|
+
]
|