acteval 0.1.2__tar.gz → 0.2.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.
- {acteval-0.1.2 → acteval-0.2.0}/.github/workflows/benchmark.yml +1 -1
- acteval-0.2.0/LICENSE +21 -0
- {acteval-0.1.2 → acteval-0.2.0}/PKG-INFO +6 -2
- acteval-0.2.0/README.md +621 -0
- acteval-0.2.0/assets/density-estimation.png +0 -0
- acteval-0.2.0/llms.txt +122 -0
- {acteval-0.1.2 → acteval-0.2.0}/pyproject.toml +5 -3
- {acteval-0.1.2 → acteval-0.2.0}/scripts/generate_blog_plots.py +29 -41
- acteval-0.2.0/src/acteval/__init__.py +50 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/_aggregation.py +31 -116
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/_jobs.py +128 -14
- acteval-0.2.0/src/acteval/_pipeline.py +393 -0
- acteval-0.2.0/src/acteval/_progress.py +99 -0
- acteval-0.2.0/src/acteval/_report.py +5 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/_result_frame.py +24 -79
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/_splits.py +1 -2
- acteval-0.2.0/src/acteval/cli.py +514 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/config.toml +9 -4
- acteval-0.2.0/src/acteval/evaluate.py +809 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/creativity.py +22 -10
- acteval-0.2.0/src/acteval/features/structural.py +208 -0
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/frequency.py +8 -3
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/plot.py +1 -1
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/results.py +2 -2
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/times.py +1 -1
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/transitions.py +1 -1
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/population.py +15 -2
- acteval-0.2.0/src/acteval/results.py +528 -0
- acteval-0.2.0/tests/test_cli.py +626 -0
- acteval-0.2.0/tests/test_compare.py +93 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_creativity.py +53 -10
- acteval-0.2.0/tests/test_disable_config.py +136 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_structural.py +75 -30
- acteval-0.2.0/tests/test_filter_cmd.py +161 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_integration.py +94 -15
- acteval-0.2.0/tests/test_jobs.py +80 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_pid_features.py +43 -19
- acteval-0.2.0/tests/test_results.py +202 -0
- acteval-0.2.0/uv.lock +1619 -0
- acteval-0.1.2/README.md +0 -319
- acteval-0.1.2/llms.txt +0 -68
- acteval-0.1.2/src/acteval/__init__.py +0 -35
- acteval-0.1.2/src/acteval/_filters.py +0 -16
- acteval-0.1.2/src/acteval/_pipeline.py +0 -395
- acteval-0.1.2/src/acteval/_report.py +0 -97
- acteval-0.1.2/src/acteval/cli.py +0 -206
- acteval-0.1.2/src/acteval/evaluate.py +0 -794
- acteval-0.1.2/src/acteval/features/structural.py +0 -202
- acteval-0.1.2/tests/test_cli.py +0 -280
- acteval-0.1.2/tests/test_compare.py +0 -39
- acteval-0.1.2/tests/test_filter.py +0 -76
- acteval-0.1.2/tests/test_jobs.py +0 -39
- acteval-0.1.2/uv.lock +0 -3817
- {acteval-0.1.2 → acteval-0.2.0}/.github/workflows/ci.yml +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/.github/workflows/publish.yml +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/.gitignore +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/__main__.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/_compat.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/distance/__init__.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/distance/scalar.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/distance/wasserstein.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/__init__.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/_discretise.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/_pid_features.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/_utils.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/catalogue.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/frequency.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/participation.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/times.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/features/transitions.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/src/acteval/pairwise.py +0 -0
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/__init__.py +0 -0
- {acteval-0.1.2/src/acteval/describe → acteval-0.2.0/src/acteval/plot}/utils.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/conftest.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_bench_evaluate.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_bench_pairwise.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_describe_features.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_distance_emd.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_distance_scalar.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_catalogue.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_participation.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_times.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_transitions.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_features_utils.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_frequency.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_missing_features.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_pairwise.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_polars_compat.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_population.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_report.py +0 -0
- {acteval-0.1.2 → acteval-0.2.0}/tests/test_validation.py +0 -0
acteval-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fred Shone
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: acteval
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Standalone library for evaluating activity sequences
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
5
7
|
Requires-Python: >=3.10
|
|
6
8
|
Requires-Dist: matplotlib
|
|
7
9
|
Requires-Dist: numpy
|
|
8
10
|
Requires-Dist: pandas
|
|
9
11
|
Requires-Dist: pot
|
|
10
12
|
Requires-Dist: scipy
|
|
13
|
+
Requires-Dist: seaborn
|
|
11
14
|
Requires-Dist: tabulate
|
|
12
15
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
16
|
+
Requires-Dist: tqdm
|
|
13
17
|
Requires-Dist: tslearn
|
acteval-0.2.0/README.md
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
# Acteval
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fredshone/acteval/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/acteval/)
|
|
5
|
+
[](https://pypi.org/project/acteval/)
|
|
6
|
+
[](https://github.com/fredshone/acteval/blob/main/LICENSE)
|
|
7
|
+
[](https://fredshone.github.io/acteval/dev/bench/)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
[](https://github.com/psf/black)
|
|
10
|
+
[](https://github.com/astral-sh/uv)
|
|
11
|
+
|
|
12
|
+
**Act**ivity schedule **eval**uation. A density estimation framework, and supplementary metrics, for comprehensively comparing or evaluating samples of activity schedules and associated attributes. CLI and python API.
|
|
13
|
+
|
|
14
|
+
**Why Acteval?**
|
|
15
|
+
|
|
16
|
+
- **Agnostic** - Acteval provides a thorough evaluation framework agnostic of downstream application.
|
|
17
|
+
- **Explainable** - High-level quantitative evaluation is composed of a hierarchy of interpretable metrics.
|
|
18
|
+
|
|
19
|
+
> **Work in progress.** Acteval has been in use since 2024, and included in various publications. But is still under active development. We therefore do not commit to backward compatibility of outputs until version 1 is released.
|
|
20
|
+
|
|
21
|
+
Skip to:
|
|
22
|
+
- [About](#about)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Data Formats](#data-formats)
|
|
25
|
+
- [API](#api)
|
|
26
|
+
- [CLI](#cli)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Benchmarks](#benchmarks)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install acteval # or uv pip install...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or for your own python project (I recommend using [uv](https://github.com/astral-sh/uv)):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add acteval
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## About
|
|
43
|
+
|
|
44
|
+
### Density estimation
|
|
45
|
+
|
|
46
|
+
Activity schedules are complex, high-dimensional, sequences of activity participations and times. Comparing the (probability) densities of high dimensional data samples is hard. Acteval tackles density estimation through comprehensive *slicing*. An example of a single *slice* might be a histogram of trips rates. The histograms of trips rates, between an **observed** or target sample, can be compared to a candidate or **synthetic** sample using earth movers distance (EMD) such that lower distances show a closer match.
|
|
47
|
+
|
|
48
|
+
To give a comprehensive estimation of density numerous slices are considered, for example:
|
|
49
|
+
|
|
50
|
+
- The participation rates of shop activities
|
|
51
|
+
- The transition rates of changing from home to work activities
|
|
52
|
+
- The start times of education activities.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
### Domains
|
|
57
|
+
|
|
58
|
+
The default [configuration](https://github.com/fredshone/acteval/blob/main/src/acteval/config.toml) of Acteval disaggregates slices by activity types and includes joint distributions such as start times and durations of an activity type. This results in *a lot* of slices. For convenience, these are aggregated (using weighted averages) into top-level **domain** metrics and mid-level **group** metrics, as follows:
|
|
59
|
+
|
|
60
|
+
- **Participations** *- do activities take place the correct number of times?*:
|
|
61
|
+
- **Trip rates** *- are sequences the correct length?*
|
|
62
|
+
- **Activity rates** *- how often do people go to e.g. work?*
|
|
63
|
+
- **Joint activity rates** *- how often do people go to e.g. work and shop?*
|
|
64
|
+
- **Transitions** *- do activities take place in the correct order?*:
|
|
65
|
+
- **Activity 2-grams** *- how often do people transition from e.g. work to shop?*
|
|
66
|
+
- **Activity 3-grams** *- how often do people transition from e.g. home to work to home?*
|
|
67
|
+
- **Activity 4-grams** *- how often do people transition from e.g. home to shop to work to home?*
|
|
68
|
+
- **Timings** *- do activities take place at the correct times and for the correct durations?*:
|
|
69
|
+
- **Activity start times** *- when do people start e.g. work?*
|
|
70
|
+
- **Activity durations** *- how long do people e.g. shop?*
|
|
71
|
+
- **Activity joint start times and durations** *- when do people start and how long do they e.g. work?*
|
|
72
|
+
- **Activity 2-gram durations** *- how long do people e.g. shop and work?*
|
|
73
|
+
|
|
74
|
+
### Joint attributes-schedules density estimation
|
|
75
|
+
|
|
76
|
+
The above metrics consider the density estimates of samples of activity schedules. These can be further sliced based on associated attributes or labels so that the joint distribution of samples and schedules with associated attributes, such as *the income of the person undertaking the schedule*, or *the day of the week the schedule took place on*, can be considered.
|
|
77
|
+
|
|
78
|
+
This is achieved by additionally supplying **observed** and **synthetic** attributes that can be joined to their corresponding schedules using unique `pid` identifiers. As with the regular density estimations, category-level distances are typically aggregated to attribute-level and then domain-level.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### Supplementary metrics
|
|
82
|
+
|
|
83
|
+
We also supplement these density estimation distances with creativity and feasibility metrics for the synthetic sample as follows:
|
|
84
|
+
|
|
85
|
+
- **Creativity** *- is my model realistically diverse and does it avoid memorisation?*:
|
|
86
|
+
- **Diversity** *- are generated samples unique from each other?*
|
|
87
|
+
- **Novelty** *- are generated samples unique from the observed samples?*
|
|
88
|
+
- **Feasibility** *- does my model avoid structural zeros i.e. unrealistic schedules?*:
|
|
89
|
+
- **Home-based** *- do samples start and end at home?*
|
|
90
|
+
- **Sequences** *- do samples include consecutive work, education or home activities?*
|
|
91
|
+
|
|
92
|
+
In all cases we calculate probabilities and report the complementary, so that smaller is better. Note that the feasibility metrics are highly opinionated and may not suit all use cases.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
There is both a command line interface and python interface:
|
|
98
|
+
|
|
99
|
+
### CLI in brief
|
|
100
|
+
|
|
101
|
+
Once installed, run the following on your command line, for example using `uv run` or install as an executable using `uv tool install acteval`.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Compare one model to observed activity schedules
|
|
105
|
+
acteval compare observed.csv -m my_model synthetic.csv
|
|
106
|
+
|
|
107
|
+
# Compare multiple models side-by-side
|
|
108
|
+
acteval compare observed.csv -m model_a synthetic_a.csv -m model_b synthetic_b.csv
|
|
109
|
+
|
|
110
|
+
# Save results to a specific output dir (the default is ./results/)
|
|
111
|
+
acteval compare observed.csv -m my_model synthetic.csv -o my_results/
|
|
112
|
+
|
|
113
|
+
# Split evaluation by attributes (e.g. gender and age)
|
|
114
|
+
acteval compare observed.csv --target-attrs target_attrs.csv \
|
|
115
|
+
-m model_a synthetic_a.csv synth_attrs_a.csv \
|
|
116
|
+
-m model_b synthetic_b.csv synth_attrs_b.csv \
|
|
117
|
+
--split-on gender age
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Explore the CLI further using `acteval --help` or refer to more details [below](#api), such as batch mode.
|
|
121
|
+
|
|
122
|
+
### Python API in brief
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import pandas as pd
|
|
126
|
+
from acteval import compare
|
|
127
|
+
|
|
128
|
+
observed = pd.DataFrame([
|
|
129
|
+
{"pid": 0, "act": "home", "start": 0, "end": 8},
|
|
130
|
+
{"pid": 0, "act": "work", "start": 8, "end": 16},
|
|
131
|
+
{"pid": 0, "act": "home", "start": 16, "end": 24},
|
|
132
|
+
{"pid": 1, "act": "home", "start": 0, "end": 12},
|
|
133
|
+
{"pid": 1, "act": "shop", "start": 12, "end": 13},
|
|
134
|
+
{"pid": 1, "act": "home", "start": 13, "end": 24},
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
synthetic = pd.DataFrame([
|
|
138
|
+
{"pid": 0, "act": "home", "start": 0, "end": 9},
|
|
139
|
+
{"pid": 0, "act": "work", "start": 9, "end": 17},
|
|
140
|
+
{"pid": 0, "act": "home", "start": 17, "end": 24},
|
|
141
|
+
{"pid": 1, "act": "home", "start": 0, "end": 8},
|
|
142
|
+
{"pid": 1, "act": "work", "start": 8, "end": 16},
|
|
143
|
+
{"pid": 1, "act": "home", "start": 16, "end": 24},
|
|
144
|
+
{"pid": 2, "act": "home", "start": 0, "end": 8},
|
|
145
|
+
{"pid": 2, "act": "home", "start": 8, "end": 24},
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
result = compare(observed, synthetic)
|
|
149
|
+
print(result.summary())
|
|
150
|
+
# synthetic
|
|
151
|
+
# domain
|
|
152
|
+
# creativity 0.166667
|
|
153
|
+
# feasibility 0.333333
|
|
154
|
+
# participations 0.162037
|
|
155
|
+
# timing 0.082728
|
|
156
|
+
# transitions 0.380952
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The `compare()` function also supports **batch** comparisons using `{name: DataFrame}` dict to compare several samples:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
result = compare(
|
|
163
|
+
observed,
|
|
164
|
+
{
|
|
165
|
+
"model_a": synthetic_a,
|
|
166
|
+
"model_b": synthetic_b
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The `compare()` function also supports **joint** attribute-schedule comparisons:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
result = compare(
|
|
176
|
+
target_schedules: observed,
|
|
177
|
+
target_attributes=target_attrs,
|
|
178
|
+
synthetic_schedules: synthetic,
|
|
179
|
+
synthetic_attributes=synthetic_attributes,
|
|
180
|
+
split_on=["gender", "weather"],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
...and **batching** of **joint** attribute-schedule comparisons:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
result = compare(
|
|
189
|
+
target_schedules: observed,
|
|
190
|
+
target_attributes=target_attrs,
|
|
191
|
+
synthetic_schedules: {
|
|
192
|
+
"model_a": synthetic_a,
|
|
193
|
+
"model_b": synthetic_b
|
|
194
|
+
},
|
|
195
|
+
synthetic_attributes={
|
|
196
|
+
"model_a": attributes_a,
|
|
197
|
+
"model_b": attributes_b
|
|
198
|
+
},
|
|
199
|
+
split_on=["gender", "weather"],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`result` is an `EvalResult`. See [Reading the results](#reading-the-results) for more detail.
|
|
205
|
+
|
|
206
|
+
## Data formats
|
|
207
|
+
|
|
208
|
+
Data is passed as a pandas (or [polars](https://pola.rs)) DataFrames with the API. The CLI will accept both .csv and .parquet.
|
|
209
|
+
|
|
210
|
+
### Activity schedules
|
|
211
|
+
|
|
212
|
+
A DataFrame with one row per activity:
|
|
213
|
+
|
|
214
|
+
| column | type | description |
|
|
215
|
+
|--------|------|-------------|
|
|
216
|
+
| `pid` | int/str | Person identifier |
|
|
217
|
+
| `act` | str | Activity label (e.g. `"home"`, `"work"`, `"shop"`) |
|
|
218
|
+
| `start` | numeric | Start time (any consistent unit, e.g. hours) |
|
|
219
|
+
| `end` | numeric | End time |
|
|
220
|
+
| `duration` | numeric | Duration (`end - start`) |
|
|
221
|
+
|
|
222
|
+
Any two of `start`, `end`, and `duration` are sufficient — the third is derived automatically. A polars DataFrame in the same shape works anywhere a pandas one does; it's converted internally.
|
|
223
|
+
|
|
224
|
+
### Attributes
|
|
225
|
+
|
|
226
|
+
Attributes can be as a pandas (or polars) DataFrame with one row per activity schedule. These should be joinable using a `pid` column. Attributes themselves can be arbitrarily named and used, though we suggest restricting yourself string column names and to categorical variables.
|
|
227
|
+
|
|
228
|
+
## API
|
|
229
|
+
|
|
230
|
+
### Comparing populations
|
|
231
|
+
|
|
232
|
+
#### `compare(target_schedules, synthetic_schedules: DataFrame, **kwargs)`
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
result = compare(observed, synthetic)
|
|
236
|
+
```
|
|
237
|
+
`result` is an `EvalResult` object. See [Reading the results](#reading-the-results) for how to access distances and descriptions at feature, group, and domain level.
|
|
238
|
+
|
|
239
|
+
#### `compare(target_schedules, synthetic_schedules: dict[str, DataFrame], **kwargs)`
|
|
240
|
+
|
|
241
|
+
Name a synthetic sample, or provide multiple named samples using a dict:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
result = compare(observed, {"model_a": synthetic_a, "model_b": synthetic_b})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `compare(target_schedules, synthetic_schedules, target_attributes: DataFrame, synthetic_attributes: dict[str, DataFrame], split_on: list[str], **kwargs)`
|
|
248
|
+
|
|
249
|
+
Pass `target_attributes` and `synthetic_attributes` and `split_on` to evaluate separately within each category of an attribute (e.g. gender) instead of over the whole population:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
target_attrs = pd.DataFrame({
|
|
253
|
+
"pid": [0, 1],
|
|
254
|
+
"gender": ["M", "F"]
|
|
255
|
+
})
|
|
256
|
+
synthetic_attributes = pd.DataFrame({
|
|
257
|
+
"pid": [0, 1],
|
|
258
|
+
"gender": ["F", "M"]
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
result = compare(
|
|
262
|
+
target_schedules=observed, # <- target or observed activity schedules
|
|
263
|
+
synthetic_schedules={"my_model": synthetic}, # <- dict of synthetic activity schedules
|
|
264
|
+
target_attributes=target_attrs, # <- target or observed attributes
|
|
265
|
+
synthetic_attributes={"my_model": synthetic_attributes}, # <- dict of synthetic attributes
|
|
266
|
+
split_on=["gender"],
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
This makes the `by_attribute`/`by_category` splits available at every level via `result.at(level, split)` — see [Reading the results](#reading-the-results) for how to read them.
|
|
271
|
+
|
|
272
|
+
> **Numeric split columns are auto-binned.** If a `split_on` column is numeric
|
|
273
|
+
> (float, or an integer with more than 10 unique values), it's automatically
|
|
274
|
+
> bucketed into up to 5 ordinal bins (`"lowest"`...`"highest"`) via `pd.qcut`,
|
|
275
|
+
> with a `UserWarning` noting the bin edges chosen. Encode the column as a
|
|
276
|
+
> categorical/string beforehand (e.g. your own age bands) to control the
|
|
277
|
+
> buckets yourself and suppress the warning.
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
#### `Evaluator`
|
|
281
|
+
|
|
282
|
+
`compare` is a wrapper for `Evaluator` — it computes and caches the observed features as required.
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from acteval import Evaluator
|
|
286
|
+
|
|
287
|
+
evaluator = Evaluator(observed)
|
|
288
|
+
|
|
289
|
+
result_v1 = evaluator.compare({"v1": synthetic_v1})
|
|
290
|
+
result_v2 = evaluator.compare({"v2": synthetic_v2})
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### `**kwargs`
|
|
294
|
+
|
|
295
|
+
Pass `progress=True` to either `compare()` or `Evaluator(...)` to show tqdm progress bars.
|
|
296
|
+
|
|
297
|
+
Pass `disable` with a list of dotted `section.key` paths matching `config.toml` to switch off individual metrics without writing a custom config file:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
result = compare(
|
|
301
|
+
...
|
|
302
|
+
disable=["jobs.creativity.novelty", "jobs.transitions.4-gram"],
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Call `list_disable_keys()` to see every valid dotted path up front, instead of
|
|
307
|
+
reading `config.toml` or triggering the `ValueError` an unknown key raises (which
|
|
308
|
+
also lists the valid paths). `disable` also works on `Evaluator(observed,
|
|
309
|
+
disable=[...])` and the CLI's `--disable` flag.
|
|
310
|
+
|
|
311
|
+
For anything more involved than a metric or two, pass your own configuration file
|
|
312
|
+
rather than using the [default](https://github.com/fredshone/acteval/blob/main/src/acteval/config.toml),
|
|
313
|
+
e.g. `compare(observed, synthetic, config_path=PATH)` (also accepted by
|
|
314
|
+
`compare_grid()`, `compare_many()`, `Evaluator(...)` and the CLI's `--config`).
|
|
315
|
+
`disable` is applied on top of whatever `config_path` loads. `Evaluator` also
|
|
316
|
+
accepts a pre-built `jobs` (`EvalConfig`), which cannot be combined with
|
|
317
|
+
`config_path` or `disable`.
|
|
318
|
+
|
|
319
|
+
#### `compare_grid()` — every target × every model
|
|
320
|
+
|
|
321
|
+
To compare the *same* synthetic models against several targets (e.g. several
|
|
322
|
+
observed populations) and see them side-by-side, use `compare_grid()`:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from acteval import compare_grid
|
|
326
|
+
|
|
327
|
+
result = compare_grid(
|
|
328
|
+
{"target_a": observed_a, "target_b": observed_b},
|
|
329
|
+
{"model_1": synthetic_1, "model_2": synthetic_2},
|
|
330
|
+
)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
##### `compare_many()` — targets and models paired one-to-one
|
|
334
|
+
|
|
335
|
+
To compare each target against just its corresponding synthetic model — e.g.
|
|
336
|
+
model 1 was fit against target A, model 2 against target B, and you want each
|
|
337
|
+
scored only against its own target — use `compare_many()`. The two dicts are
|
|
338
|
+
paired up positionally (first with first, second with second, ...) and must be
|
|
339
|
+
the same length:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from acteval import compare_many
|
|
343
|
+
|
|
344
|
+
result = compare_many(
|
|
345
|
+
{"target_a": observed_a, "target_b": observed_b},
|
|
346
|
+
{"model_a": synthetic_a, "model_b": synthetic_b},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
print(result.model_names)
|
|
350
|
+
# ['target_a::model_a', 'target_b::model_b']
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Both helpers accept `attributes_a`/`attributes_b` (per-target/per-model
|
|
354
|
+
attributes DataFrames) and `split_on`, and pass any other keyword arguments
|
|
355
|
+
(e.g. `config_path`, `disable`, `progress`) through to `compare()`.
|
|
356
|
+
|
|
357
|
+
For per-target attributes/`split_on` with more control, or to inspect
|
|
358
|
+
intermediate per-target results before merging, call `compare()` yourself in a
|
|
359
|
+
loop and pass the results to `acteval.results.combine()`:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from acteval.results import combine
|
|
363
|
+
|
|
364
|
+
results = {
|
|
365
|
+
"target_a": compare(observed_a, synthetic),
|
|
366
|
+
"target_b": compare(observed_b, synthetic),
|
|
367
|
+
}
|
|
368
|
+
result = combine(results)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
> **Known limitation:** the shared `target`/`unit` values underlying feature →
|
|
372
|
+
> group → domain aggregation are taken from the *first* result only, so
|
|
373
|
+
> re-aggregating a non-first target's columns uses that first target's weights
|
|
374
|
+
> rather than its own. Each model's distances are still computed against its own
|
|
375
|
+
> target — this only affects aggregation weighting, and will be resolved by a
|
|
376
|
+
> future refactor to carry one weight base per source.
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
#### `pairwise_distances(schedules, specs=None)`
|
|
380
|
+
|
|
381
|
+
> **Work in progress only!**
|
|
382
|
+
|
|
383
|
+
Compute a single NxN distance matrix between individual schedules. Useful for clustering, outlier detection, or directly comparing a small batch of schedules — a standalone code path, independent of `compare()`/`Evaluator`/`config.toml`.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from acteval import pairwise_distances
|
|
387
|
+
|
|
388
|
+
result = pairwise_distances(schedules)
|
|
389
|
+
result.matrix # numpy array, shape (N, N)
|
|
390
|
+
result.pids # original pid values, length N
|
|
391
|
+
df = result.to_dataframe() # labeled DataFrame, original pid values as index/columns
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The result matrix is symmetric with zeros on the diagonal. All values are in **0–1**.
|
|
395
|
+
|
|
396
|
+
By default, three equal-weight semantic-distance specs are used (participations,
|
|
397
|
+
transitions, timing via MAE). Pass a custom `specs` list to change the metrics or
|
|
398
|
+
their relative weights, e.g. `pairwise_distances(schedules, specs=[chamfer_spec()])`.
|
|
399
|
+
Each spec defines a `feature_fn` (extracts a `(N, ...)` array from the population)
|
|
400
|
+
and a `distance_fn` (computes the `(N, N)` matrix); the final matrix is a weighted
|
|
401
|
+
average across all active specs. See `acteval.pairwise` for the available factories —
|
|
402
|
+
`default_pairwise_specs()`, `chamfer_spec(max_len, weight)`, `soft_dtw_spec(max_len, gamma, weight)`.
|
|
403
|
+
|
|
404
|
+
`Population` is the internal numpy-precomputation layer `compare()`/`pairwise_distances()` are built on — most users never construct it directly; see its docstring (`acteval.population.Population`) if you need it.
|
|
405
|
+
|
|
406
|
+
#### `acteval.plot`
|
|
407
|
+
|
|
408
|
+
Matplotlib plotting helpers for interactive/notebook use — Gantt charts, time-use
|
|
409
|
+
and participation-rate breakdowns, bigram heatmaps, start/end/duration histogram
|
|
410
|
+
grids, sequence-probability waterfalls, and heatmap/bar-chart views of a
|
|
411
|
+
`compare()` result. Not imported by `compare()`/`Evaluator`/`pairwise_distances`,
|
|
412
|
+
so it never runs unless you call into it. Submodules: `frequency`, `times`,
|
|
413
|
+
`transitions`, `plot` (schedule-level charts), `results` (result-level charts) —
|
|
414
|
+
see each module's docstring for the full function list.
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
from acteval.plot.plot import gantt
|
|
418
|
+
|
|
419
|
+
gantt(observed)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Reading the results
|
|
423
|
+
|
|
424
|
+
#### `EvalResult`
|
|
425
|
+
|
|
426
|
+
`compare` and `Evaluator` return large amounts of metrics. These are exposed via an `EvalResult` object, which you can use, for example, to extract quick domain summaries with `summary`:
|
|
427
|
+
|
|
428
|
+
#### `EvalResult.summary()`
|
|
429
|
+
|
|
430
|
+
For comparing models against each other, `summary()` is usually all you need:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
# Domain-level summary table
|
|
434
|
+
print(result.summary())
|
|
435
|
+
# model_a model_b
|
|
436
|
+
# domain
|
|
437
|
+
# creativity 0.166667 0.333333
|
|
438
|
+
# feasibility 0.333333 1.000000
|
|
439
|
+
# participations 0.162037 0.988889
|
|
440
|
+
# timing 0.082728 0.669676
|
|
441
|
+
# transitions 0.380952 0.500000
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
> **Known limitation:** The density estimation domain distances and supplementary metrics have different units, supports and typical values.
|
|
447
|
+
We generally therefore do not aggregate them further into a "meta score". However you can access `rank_models()` and `best_model()` which do simply sum metrics to allow meta comparison. Use with care. For safer comparison we suggest normalisinng metrics against a baseline, for example, to report % improvement my each metric.
|
|
448
|
+
|
|
449
|
+
#### `EvalResult.save()`
|
|
450
|
+
|
|
451
|
+
Save all (low-level features, via groups to top-level domains) to CSV with `result.save("output_dir/")`.
|
|
452
|
+
|
|
453
|
+
#### `EvalResult.at(level, split)`
|
|
454
|
+
|
|
455
|
+
For anything more detailed than the summary table — a specific aggregation level, or a specific split — `result.at(...)`:
|
|
456
|
+
|
|
457
|
+
```python
|
|
458
|
+
result.at() # domains × combined (result.at().distances == result.summary())
|
|
459
|
+
result.at("groups") # groups × combined
|
|
460
|
+
result.at("features", "by_attribute") # features × by_attribute (requires split_on)
|
|
461
|
+
result.at("domains", "by_category") # domains × by_category (requires split_on)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- `level` is one of `"features"`, `"groups"`, `"domains"`
|
|
465
|
+
- `split` is one of `"combined"`, `"by_attribute"`, `"by_category"`
|
|
466
|
+
|
|
467
|
+
Each call returns an `AggregatedResult` with `.distances` and `.descriptions` DataFrames.
|
|
468
|
+
|
|
469
|
+
#### `EvalResult.raw`
|
|
470
|
+
|
|
471
|
+
`EvalResult.raw` exposes the pre-aggregation data (one `ResultFrame` each for descriptions and distances) that every level above is aggregated from — `distances` covers models only (there's no such thing as the target's distance to itself); pair it with `result.target_distance_weights` if you're building custom distance aggregations of your own.
|
|
472
|
+
|
|
473
|
+
> **Note on timing features:** A distance of `1.0` for a timing feature means the activity is *entirely absent* from the synthetic population — not just timed differently. This is treated as a maximum-penalty missing feature rather than a distributional difference.
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
## CLI
|
|
477
|
+
|
|
478
|
+
`acteval` ships with a command-line interface for comparing models without writing
|
|
479
|
+
Python. It has two subcommands: `compare` (the primary one) and `filter`.
|
|
480
|
+
|
|
481
|
+
#### `acteval compare`
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Compare one model to observed data
|
|
486
|
+
acteval compare observed.csv -m my_model synthetic.csv
|
|
487
|
+
|
|
488
|
+
# Compare multiple models side-by-side
|
|
489
|
+
acteval compare observed.csv -m model_a synthetic_a.csv -m model_b synthetic_b.csv
|
|
490
|
+
|
|
491
|
+
# Save results to CSV files
|
|
492
|
+
acteval compare observed.csv -m my_model synthetic.csv -o results/
|
|
493
|
+
|
|
494
|
+
# Show group-level detail instead of domain summary
|
|
495
|
+
acteval compare observed.csv -m my_model synthetic.csv -l groups
|
|
496
|
+
|
|
497
|
+
# Use a custom config file
|
|
498
|
+
acteval compare observed.csv -m my_model synthetic.csv -c custom.toml
|
|
499
|
+
|
|
500
|
+
# Disable specific metrics without a custom config file
|
|
501
|
+
acteval compare observed.csv -m my_model synthetic.csv --disable jobs.creativity.novelty
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Input files can be CSV or Parquet (detected by extension). Run `acteval compare --help` for the full option list.
|
|
505
|
+
|
|
506
|
+
As per the API it is also possible to do joint density estimation by providing attributes:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
# Split evaluation by attribute (e.g. gender and age)
|
|
510
|
+
# Per-model attrs are the third argument to -m
|
|
511
|
+
# Attributes must be provided for the target and ALL models, or not at all
|
|
512
|
+
acteval compare observed.csv --target-attrs target_attrs.csv \
|
|
513
|
+
-m model_a synthetic_a.csv synth_attrs_a.csv \
|
|
514
|
+
-m model_b synthetic_b.csv synth_attrs_b.csv \
|
|
515
|
+
-s gender age
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Attribute rules:**
|
|
519
|
+
- Attributes must be provided for **all** inputs (target + every model) or **none**. Partial specification raises an error.
|
|
520
|
+
- `--split-on` (`-s`) and `--target-attrs` must be specified together.
|
|
521
|
+
- In batch mode, if any model subdirectory contains an attributes file, all subdirectories must contain one.
|
|
522
|
+
|
|
523
|
+
For the lazy - there is also an auto-discovery mode for batch experiments, but use with care:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
# Batch mode: auto-discover model subdirectories
|
|
527
|
+
# Each subdir becomes a model (name = dir name); schedule and attrs files are
|
|
528
|
+
# classified by their columns (pid + act → schedule; pid + other cols → attrs)
|
|
529
|
+
acteval compare observed.csv --batch models/
|
|
530
|
+
|
|
531
|
+
# Batch mode with attribute splitting
|
|
532
|
+
acteval compare observed.csv --target-attrs target_attrs.csv --batch models/ --split-on gender
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
**Batch directory layout:**
|
|
537
|
+
```
|
|
538
|
+
models/
|
|
539
|
+
model_a/
|
|
540
|
+
schedules.csv ← has pid, act → classified as schedule
|
|
541
|
+
attributes.csv ← has pid + other cols, no act → classified as attrs
|
|
542
|
+
model_b/
|
|
543
|
+
...
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### `acteval filter`
|
|
547
|
+
|
|
548
|
+
Filter a schedule file down to persons with a specific structural issue — useful for
|
|
549
|
+
spot-checking a synthetic population before running `compare`.
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
# Schedules that don't start and end at home
|
|
553
|
+
acteval filter non-home-based synthetic.csv -o flagged.csv
|
|
554
|
+
|
|
555
|
+
# Schedules with consecutive duplicate activities (default: home, work, education)
|
|
556
|
+
acteval filter consecutive synthetic.csv --act home shop
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Without `-o/--output`, filtered rows are printed to stdout as CSV. Run
|
|
560
|
+
`acteval filter --help` for the full option list.
|
|
561
|
+
|
|
562
|
+
## Development
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
# Install dependencies (including dev tools)
|
|
566
|
+
uv sync
|
|
567
|
+
|
|
568
|
+
# Run tests (benchmarks excluded by default)
|
|
569
|
+
uv run pytest tests/
|
|
570
|
+
|
|
571
|
+
# Run tests with coverage
|
|
572
|
+
uv run pytest --cov=src/acteval tests/
|
|
573
|
+
|
|
574
|
+
# Lint with ruff
|
|
575
|
+
ruff check src/ tests/
|
|
576
|
+
|
|
577
|
+
# Auto-fix lint issues
|
|
578
|
+
ruff check --fix src/ tests/
|
|
579
|
+
|
|
580
|
+
# Check formatting with black
|
|
581
|
+
black --check src/ tests/
|
|
582
|
+
|
|
583
|
+
# Auto-format
|
|
584
|
+
black src/ tests/
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Benchmarks
|
|
588
|
+
|
|
589
|
+
Two benchmark suites are included, both using [pytest-benchmark](https://pytest-benchmark.readthedocs.io/).
|
|
590
|
+
|
|
591
|
+
### Population evaluation (`compare`)
|
|
592
|
+
|
|
593
|
+
Tests `compare()` at 1k, 20k, and 100k rows (observed + synthetic populations):
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
uv run pytest tests/test_bench_evaluate.py --benchmark-only -v
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Pairwise distances (`pairwise_distances`)
|
|
600
|
+
|
|
601
|
+
Tests `pairwise_distances()` at 256, 512, and 1024 schedules:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
uv run pytest tests/test_bench_pairwise.py --benchmark-only -v
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Both suites together
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only -v
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Comparing runs
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
# Save a baseline
|
|
617
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only --benchmark-save=baseline
|
|
618
|
+
|
|
619
|
+
# Compare against it after making changes
|
|
620
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only --benchmark-compare=baseline
|
|
621
|
+
```
|
|
Binary file
|