acteval 0.1.0__tar.gz → 0.1.2__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/.github/workflows/benchmark.yml +40 -0
- {acteval-0.1.0 → acteval-0.1.2}/.github/workflows/ci.yml +6 -3
- {acteval-0.1.0 → acteval-0.1.2}/PKG-INFO +4 -3
- acteval-0.1.2/README.md +319 -0
- acteval-0.1.2/llms.txt +68 -0
- {acteval-0.1.0 → acteval-0.1.2}/pyproject.toml +20 -6
- acteval-0.1.2/scripts/generate_blog_plots.py +460 -0
- acteval-0.1.2/src/acteval/__init__.py +35 -0
- acteval-0.1.2/src/acteval/__main__.py +4 -0
- acteval-0.1.2/src/acteval/_aggregation.py +265 -0
- acteval-0.1.2/src/acteval/_compat.py +34 -0
- acteval-0.1.0/src/acteval/filters.py → acteval-0.1.2/src/acteval/_filters.py +1 -1
- acteval-0.1.2/src/acteval/_jobs.py +266 -0
- acteval-0.1.2/src/acteval/_pipeline.py +395 -0
- acteval-0.1.2/src/acteval/_report.py +97 -0
- acteval-0.1.2/src/acteval/_result_frame.py +178 -0
- acteval-0.1.2/src/acteval/_splits.py +67 -0
- acteval-0.1.2/src/acteval/cli.py +206 -0
- acteval-0.1.2/src/acteval/config.toml +43 -0
- acteval-0.1.2/src/acteval/describe/__init__.py +13 -0
- {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/frequency.py +29 -20
- acteval-0.1.2/src/acteval/describe/plot.py +603 -0
- acteval-0.1.2/src/acteval/describe/results.py +124 -0
- {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/times.py +43 -38
- {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/transitions.py +15 -12
- acteval-0.1.2/src/acteval/describe/utils.py +142 -0
- acteval-0.1.2/src/acteval/distance/__init__.py +33 -0
- {acteval-0.1.0 → acteval-0.1.2}/src/acteval/distance/wasserstein.py +0 -6
- acteval-0.1.2/src/acteval/evaluate.py +794 -0
- acteval-0.1.2/src/acteval/features/__init__.py +28 -0
- acteval-0.1.0/src/acteval/density/features/pid_features.py → acteval-0.1.2/src/acteval/features/_pid_features.py +1 -1
- acteval-0.1.2/src/acteval/features/catalogue.py +191 -0
- {acteval-0.1.0/src/acteval/creativity → acteval-0.1.2/src/acteval}/features/creativity.py +17 -2
- {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/features/frequency.py +1 -1
- acteval-0.1.2/src/acteval/features/participation.py +79 -0
- acteval-0.1.2/src/acteval/features/structural.py +202 -0
- acteval-0.1.2/src/acteval/features/times.py +96 -0
- acteval-0.1.2/src/acteval/features/transitions.py +117 -0
- acteval-0.1.2/src/acteval/pairwise.py +447 -0
- {acteval-0.1.0 → acteval-0.1.2}/src/acteval/population.py +68 -9
- acteval-0.1.2/tests/conftest.py +30 -0
- acteval-0.1.2/tests/test_bench_evaluate.py +96 -0
- acteval-0.1.2/tests/test_bench_pairwise.py +72 -0
- acteval-0.1.2/tests/test_cli.py +280 -0
- acteval-0.1.2/tests/test_compare.py +39 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_creativity.py +1 -1
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_describe_features.py +12 -25
- acteval-0.1.2/tests/test_features_catalogue.py +75 -0
- acteval-0.1.2/tests/test_features_participation.py +72 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_structural.py +33 -82
- acteval-0.1.2/tests/test_features_times.py +163 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_transitions.py +15 -18
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_utils.py +1 -1
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_filter.py +1 -1
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_frequency.py +2 -2
- acteval-0.1.2/tests/test_integration.py +232 -0
- acteval-0.1.2/tests/test_jobs.py +39 -0
- acteval-0.1.2/tests/test_missing_features.py +58 -0
- acteval-0.1.2/tests/test_pairwise.py +526 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_pid_features.py +161 -108
- acteval-0.1.2/tests/test_polars_compat.py +150 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_population.py +2 -2
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_report.py +14 -13
- acteval-0.1.2/tests/test_validation.py +256 -0
- {acteval-0.1.0 → acteval-0.1.2}/uv.lock +905 -33
- acteval-0.1.0/.claude/settings.local.json +0 -16
- acteval-0.1.0/CLAUDE.md +0 -60
- acteval-0.1.0/README.md +0 -118
- acteval-0.1.0/src/acteval/__init__.py +0 -10
- acteval-0.1.0/src/acteval/config.toml +0 -24
- acteval-0.1.0/src/acteval/creativity/__init__.py +0 -2
- acteval-0.1.0/src/acteval/creativity/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/density/__init__.py +0 -2
- acteval-0.1.0/src/acteval/density/describe/__init__.py +0 -0
- acteval-0.1.0/src/acteval/density/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/density/features/participation.py +0 -192
- acteval-0.1.0/src/acteval/density/features/times.py +0 -171
- acteval-0.1.0/src/acteval/density/features/transitions.py +0 -241
- acteval-0.1.0/src/acteval/distance/__init__.py +0 -16
- acteval-0.1.0/src/acteval/evaluate.py +0 -811
- acteval-0.1.0/src/acteval/jobs.py +0 -215
- acteval-0.1.0/src/acteval/ops.py +0 -49
- acteval-0.1.0/src/acteval/structural/__init__.py +0 -2
- acteval-0.1.0/src/acteval/structural/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/structural/features/structural.py +0 -210
- acteval-0.1.0/tests/test_compare.py +0 -69
- acteval-0.1.0/tests/test_features_participation.py +0 -147
- acteval-0.1.0/tests/test_features_times.py +0 -215
- {acteval-0.1.0 → acteval-0.1.2}/.github/workflows/publish.yml +0 -0
- {acteval-0.1.0 → acteval-0.1.2}/.gitignore +0 -0
- {acteval-0.1.0 → acteval-0.1.2}/src/acteval/distance/scalar.py +0 -0
- {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/features/_discretise.py +0 -0
- /acteval-0.1.0/src/acteval/density/features/utils.py → /acteval-0.1.2/src/acteval/features/_utils.py +0 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_distance_emd.py +0 -0
- {acteval-0.1.0 → acteval-0.1.2}/tests/test_distance_scalar.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Benchmark
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
benchmark:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: astral-sh/setup-uv@v4
|
|
17
|
+
|
|
18
|
+
- name: Install Python 3.13
|
|
19
|
+
run: uv python install 3.13
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: uv sync
|
|
23
|
+
|
|
24
|
+
- name: Run benchmarks
|
|
25
|
+
run: >
|
|
26
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py
|
|
27
|
+
--benchmark-only
|
|
28
|
+
--benchmark-json output.json
|
|
29
|
+
|
|
30
|
+
- name: Store benchmark results
|
|
31
|
+
uses: benchmark-action/github-action-benchmark@v1
|
|
32
|
+
with:
|
|
33
|
+
tool: pytest
|
|
34
|
+
output-file-path: output.json
|
|
35
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
auto-push: true
|
|
37
|
+
alert-threshold: "120%"
|
|
38
|
+
comment-on-alert: true
|
|
39
|
+
fail-on-alert: true
|
|
40
|
+
benchmark-data-dir-path: dev/bench
|
|
@@ -7,11 +7,12 @@ on:
|
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
9
|
test:
|
|
10
|
-
runs-on:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
11
|
strategy:
|
|
12
12
|
fail-fast: false
|
|
13
13
|
matrix:
|
|
14
|
-
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
16
|
|
|
16
17
|
steps:
|
|
17
18
|
- uses: actions/checkout@v4
|
|
@@ -25,10 +26,12 @@ jobs:
|
|
|
25
26
|
run: uv sync
|
|
26
27
|
|
|
27
28
|
- name: Lint
|
|
29
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
|
28
30
|
run: uv run ruff check src/ tests/
|
|
29
31
|
|
|
30
32
|
- name: Check formatting
|
|
33
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
|
31
34
|
run: uv run black --check src/ tests/
|
|
32
35
|
|
|
33
36
|
- name: Run tests
|
|
34
|
-
run: uv run pytest tests/ --cov=src/acteval
|
|
37
|
+
run: uv run pytest tests/ --ignore=tests/test_bench_evaluate.py --cov=src/acteval
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acteval
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Standalone library for evaluating activity sequences
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: matplotlib
|
|
7
7
|
Requires-Dist: numpy
|
|
8
8
|
Requires-Dist: pandas
|
|
9
9
|
Requires-Dist: pot
|
|
10
|
-
Requires-Dist: scipy
|
|
10
|
+
Requires-Dist: scipy
|
|
11
11
|
Requires-Dist: tabulate
|
|
12
12
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
13
|
+
Requires-Dist: tslearn
|
acteval-0.1.2/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# acteval
|
|
2
|
+
|
|
3
|
+
A Python library for evaluating synthetic activity schedules by comparing them to observed data. Given a population of daily activity sequences (who did what and when), `acteval` measures how well a synthetic population reproduces the observed distribution across multiple dimensions: activity frequencies, timing, transitions, participation rates, and novelty.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install acteval
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add acteval
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
`acteval` ships with a command-line interface for comparing models without writing Python.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Compare one model to observed data
|
|
23
|
+
acteval observed.csv --model my_model synthetic.csv
|
|
24
|
+
|
|
25
|
+
# Compare multiple models side-by-side
|
|
26
|
+
acteval observed.csv \
|
|
27
|
+
--model model_a synthetic_a.csv \
|
|
28
|
+
--model model_b synthetic_b.csv
|
|
29
|
+
|
|
30
|
+
# Save full results to CSV files
|
|
31
|
+
acteval observed.csv --model my_model synthetic.csv --output results/
|
|
32
|
+
|
|
33
|
+
# Show group-level detail instead of domain summary
|
|
34
|
+
acteval observed.csv --model my_model synthetic.csv --level groups
|
|
35
|
+
|
|
36
|
+
# Split evaluation by attribute (e.g. gender)
|
|
37
|
+
acteval observed.csv \
|
|
38
|
+
--target-attrs target_attrs.csv --split-on gender \
|
|
39
|
+
--model my_model synthetic.csv --attrs my_model synth_attrs.csv
|
|
40
|
+
|
|
41
|
+
# Use a custom config file
|
|
42
|
+
acteval observed.csv --model my_model synthetic.csv --config custom.toml
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Input files can be CSV or Parquet (detected by extension). Run `acteval --help` for the full option list.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import pandas as pd
|
|
51
|
+
from acteval import compare
|
|
52
|
+
|
|
53
|
+
observed = pd.DataFrame([
|
|
54
|
+
{"pid": 0, "act": "home", "start": 0, "end": 8, "duration": 8},
|
|
55
|
+
{"pid": 0, "act": "work", "start": 8, "end": 16, "duration": 8},
|
|
56
|
+
{"pid": 0, "act": "home", "start": 16, "end": 24, "duration": 8},
|
|
57
|
+
{"pid": 1, "act": "home", "start": 0, "end": 12, "duration": 12},
|
|
58
|
+
{"pid": 1, "act": "shop", "start": 12, "end": 13, "duration": 1},
|
|
59
|
+
{"pid": 1, "act": "home", "start": 13, "end": 24, "duration": 11},
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
synthetic = pd.DataFrame([
|
|
63
|
+
{"pid": 0, "act": "home", "start": 0, "end": 9, "duration": 9},
|
|
64
|
+
{"pid": 0, "act": "work", "start": 9, "end": 17, "duration": 8},
|
|
65
|
+
{"pid": 0, "act": "home", "start": 17, "end": 24, "duration": 7},
|
|
66
|
+
{"pid": 1, "act": "home", "start": 0, "end": 8, "duration": 8},
|
|
67
|
+
{"pid": 1, "act": "work", "start": 8, "end": 16, "duration": 8},
|
|
68
|
+
{"pid": 1, "act": "home", "start": 16, "end": 24, "duration": 8},
|
|
69
|
+
{"pid": 2, "act": "home", "start": 0, "end": 8, "duration": 8},
|
|
70
|
+
{"pid": 2, "act": "home", "start": 8, "end": 24, "duration": 16},
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
result = compare(observed, {"my_model": synthetic})
|
|
74
|
+
print(result.summary())
|
|
75
|
+
# creativity 0.166667
|
|
76
|
+
# feasibility 0.500000
|
|
77
|
+
# participations 0.162037
|
|
78
|
+
# timing 0.082728
|
|
79
|
+
# transitions 0.380952
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Input format
|
|
83
|
+
|
|
84
|
+
Data is passed as a pandas DataFrame with one row per activity episode:
|
|
85
|
+
|
|
86
|
+
| column | type | description |
|
|
87
|
+
|--------|------|-------------|
|
|
88
|
+
| `pid` | int/str | Person identifier |
|
|
89
|
+
| `act` | str | Activity label (e.g. `"home"`, `"work"`, `"shop"`) |
|
|
90
|
+
| `start` | numeric | Start time (any consistent unit, e.g. hours) |
|
|
91
|
+
| `end` | numeric | End time |
|
|
92
|
+
| `duration` | numeric | Duration (`end - start`); can be omitted when both `start` and `end` are provided |
|
|
93
|
+
|
|
94
|
+
Any two of `start`, `end`, and `duration` are sufficient — the third is derived automatically.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import pandas as pd
|
|
99
|
+
|
|
100
|
+
observed = pd.DataFrame([
|
|
101
|
+
{"pid": 0, "act": "home", "start": 0, "end": 6, "duration": 6},
|
|
102
|
+
{"pid": 0, "act": "work", "start": 6, "end": 14, "duration": 8},
|
|
103
|
+
{"pid": 0, "act": "home", "start": 14, "end": 24, "duration": 10},
|
|
104
|
+
{"pid": 1, "act": "home", "start": 0, "end": 10, "duration": 10},
|
|
105
|
+
{"pid": 1, "act": "work", "start": 10, "end": 24, "duration": 14},
|
|
106
|
+
])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
### `compare(observed, synthetic, **kwargs)`
|
|
112
|
+
|
|
113
|
+
Compare one or more synthetic populations to an observed population.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from acteval import compare
|
|
117
|
+
|
|
118
|
+
# Single synthetic population
|
|
119
|
+
result = compare(observed, synthetic)
|
|
120
|
+
|
|
121
|
+
# Multiple models side-by-side
|
|
122
|
+
result = compare(observed, {"model_a": synthetic_a, "model_b": synthetic_b})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`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.
|
|
126
|
+
|
|
127
|
+
### `Evaluator`
|
|
128
|
+
|
|
129
|
+
Use `Evaluator` when comparing multiple synthetic populations against the same observed data — it computes and caches the observed features once. Each `compare()` call is independent.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from acteval import Evaluator
|
|
133
|
+
|
|
134
|
+
evaluator = Evaluator(observed)
|
|
135
|
+
|
|
136
|
+
result_v1 = evaluator.compare({"v1": synthetic_v1})
|
|
137
|
+
result_v2 = evaluator.compare({"v2": synthetic_v2})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `pairwise_distances(schedules, specs=None)`
|
|
141
|
+
|
|
142
|
+
Compute a single NxN distance matrix between individual schedules. Useful for clustering, outlier detection, or directly comparing a small batch of schedules.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from acteval import pairwise_distances
|
|
146
|
+
|
|
147
|
+
result = pairwise_distances(schedules)
|
|
148
|
+
result.matrix # numpy array, shape (N, N)
|
|
149
|
+
result.pids # original pid values, length N
|
|
150
|
+
|
|
151
|
+
# Get a labeled DataFrame with original pid values as index/columns
|
|
152
|
+
df = result.to_dataframe()
|
|
153
|
+
|
|
154
|
+
# Example: find the two most similar schedules
|
|
155
|
+
import numpy as np
|
|
156
|
+
dist = result.to_dataframe()
|
|
157
|
+
dist.values[np.arange(len(dist)), np.arange(len(dist))] = np.inf
|
|
158
|
+
i, j = np.unravel_index(dist.values.argmin(), dist.shape)
|
|
159
|
+
print(f"Most similar: {dist.index[i]} and {dist.columns[j]}")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The result matrix is symmetric with zeros on the diagonal. All values are in **0–1**.
|
|
163
|
+
|
|
164
|
+
#### Pluggable distance specs
|
|
165
|
+
|
|
166
|
+
By default, three equal-weight semantic-distance specs are used (participations, transitions, timing via MAE). Pass a custom `specs` list to change the metrics or their relative weights:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from acteval.pairwise import chamfer_spec, soft_dtw_spec, default_pairwise_specs
|
|
170
|
+
|
|
171
|
+
# Chamfer distance on EOS-padded activity sequences
|
|
172
|
+
result = pairwise_distances(schedules, specs=[chamfer_spec()])
|
|
173
|
+
|
|
174
|
+
# Soft-DTW on EOS-padded activity sequences
|
|
175
|
+
result = pairwise_distances(schedules, specs=[soft_dtw_spec(gamma=1.0)])
|
|
176
|
+
|
|
177
|
+
# Mix metrics with custom weights
|
|
178
|
+
result = pairwise_distances(schedules, specs=[
|
|
179
|
+
*default_pairwise_specs(), # weight=1.0 each
|
|
180
|
+
chamfer_spec(weight=1.0),
|
|
181
|
+
soft_dtw_spec(weight=2.0),
|
|
182
|
+
])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each spec defines a `feature_fn` (extracts a `(N, ...)` array from the population) and a `distance_fn` (computes the `(N, N)` matrix). The final matrix is a weighted average across all active specs.
|
|
186
|
+
|
|
187
|
+
| Factory | Description |
|
|
188
|
+
|---------|-------------|
|
|
189
|
+
| `default_pairwise_specs()` | MAE on participation counts, bi-gram counts, mean durations |
|
|
190
|
+
| `chamfer_spec(max_len, weight)` | Chamfer distance on EOS-padded `(N, L, 2)` sequences |
|
|
191
|
+
| `soft_dtw_spec(max_len, gamma, weight)` | Soft-DTW on EOS-padded `(N, L, 2)` sequences |
|
|
192
|
+
|
|
193
|
+
### `Population`
|
|
194
|
+
|
|
195
|
+
For direct access to the underlying data structure:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from acteval import Population
|
|
199
|
+
|
|
200
|
+
pop = Population(observed)
|
|
201
|
+
print(pop.acts) # activity labels per episode
|
|
202
|
+
print(pop.durations) # durations as numpy array
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Reading the results
|
|
206
|
+
|
|
207
|
+
### Accessing results
|
|
208
|
+
|
|
209
|
+
`compare()` returns an `EvalResult` with distances and descriptions at three levels of aggregation. Each level is accessed via a property that returns a view object with `.distances` and `.descriptions` DataFrames:
|
|
210
|
+
|
|
211
|
+
| Property | Index | Content |
|
|
212
|
+
|----------|-------|---------|
|
|
213
|
+
| `result.features.combined.distances` | `(domain, feature, segment)` | Per-feature distances — lower is closer to observed |
|
|
214
|
+
| `result.features.combined.descriptions` | `(domain, feature, segment)` | Per-feature descriptive statistics (e.g. average start time) |
|
|
215
|
+
| `result.groups.combined.distances` | `(domain, feature)` | Distances averaged across segments |
|
|
216
|
+
| `result.groups.combined.descriptions` | `(domain, feature)` | Descriptions averaged across segments |
|
|
217
|
+
| `result.domains.combined.distances` | `(domain,)` | Distances averaged across features — one row per domain |
|
|
218
|
+
| `result.domains.combined.descriptions` | `(domain,)` | Descriptions averaged across features |
|
|
219
|
+
|
|
220
|
+
Save all levels to CSV at once with `result.save("output_dir/")`.
|
|
221
|
+
|
|
222
|
+
Distances are in the range **0–1** (lower is better). A distance of `0.0` means the synthetic distribution perfectly matches observed; `1.0` is the maximum penalty.
|
|
223
|
+
|
|
224
|
+
> **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.
|
|
225
|
+
|
|
226
|
+
### Evaluation domains
|
|
227
|
+
|
|
228
|
+
| Domain | What it measures |
|
|
229
|
+
|--------|-----------------|
|
|
230
|
+
| `participations` | Who does what and how often — participation rates, joint participation, sequence lengths |
|
|
231
|
+
| `transitions` | Activity sequences — 2-, 3-, and 4-gram transition patterns |
|
|
232
|
+
| `timing` | When and how long — start times, durations, and their joint distributions |
|
|
233
|
+
| `creativity` | How novel and diverse the synthetic schedules are relative to observed |
|
|
234
|
+
| `feasibility` | Structural validity — home-based schedules, no consecutive duplicate activities |
|
|
235
|
+
|
|
236
|
+
### Ranking models
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
result = compare(observed, {"model_a": df_a, "model_b": df_b})
|
|
240
|
+
|
|
241
|
+
# Mean domain distance per model (lower is better)
|
|
242
|
+
print(result.rank_models())
|
|
243
|
+
# model_a 0.12
|
|
244
|
+
# model_b 0.19
|
|
245
|
+
|
|
246
|
+
# Best model
|
|
247
|
+
print(result.best_model) # "model_a"
|
|
248
|
+
|
|
249
|
+
# Domain-level summary table
|
|
250
|
+
print(result.summary())
|
|
251
|
+
# model_a model_b
|
|
252
|
+
# domain
|
|
253
|
+
# creativity 0.08 0.14
|
|
254
|
+
# feasibility 0.03 0.05
|
|
255
|
+
# participations 0.11 0.18
|
|
256
|
+
# timing 0.16 0.22
|
|
257
|
+
# transitions 0.22 0.35
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Install dependencies (including dev tools)
|
|
264
|
+
uv sync
|
|
265
|
+
|
|
266
|
+
# Run tests (benchmarks excluded by default)
|
|
267
|
+
uv run pytest tests/
|
|
268
|
+
|
|
269
|
+
# Run tests with coverage
|
|
270
|
+
uv run pytest --cov=src/acteval tests/
|
|
271
|
+
|
|
272
|
+
# Lint with ruff
|
|
273
|
+
ruff check src/ tests/
|
|
274
|
+
|
|
275
|
+
# Auto-fix lint issues
|
|
276
|
+
ruff check --fix src/ tests/
|
|
277
|
+
|
|
278
|
+
# Check formatting with black
|
|
279
|
+
black --check src/ tests/
|
|
280
|
+
|
|
281
|
+
# Auto-format
|
|
282
|
+
black src/ tests/
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Benchmarks
|
|
286
|
+
|
|
287
|
+
Two benchmark suites are included, both using [pytest-benchmark](https://pytest-benchmark.readthedocs.io/).
|
|
288
|
+
|
|
289
|
+
### Population evaluation (`compare`)
|
|
290
|
+
|
|
291
|
+
Tests `compare()` at 1k, 20k, and 100k rows (observed + synthetic populations):
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
uv run pytest tests/test_bench_evaluate.py --benchmark-only -v
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Pairwise distances (`pairwise_distances`)
|
|
298
|
+
|
|
299
|
+
Tests `pairwise_distances()` at 256, 512, and 1024 schedules:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
uv run pytest tests/test_bench_pairwise.py --benchmark-only -v
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Both suites together
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only -v
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Comparing runs
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Save a baseline
|
|
315
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only --benchmark-save=baseline
|
|
316
|
+
|
|
317
|
+
# Compare against it after making changes
|
|
318
|
+
uv run pytest tests/test_bench_evaluate.py tests/test_bench_pairwise.py --benchmark-only --benchmark-compare=baseline
|
|
319
|
+
```
|
acteval-0.1.2/llms.txt
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# acteval
|
|
2
|
+
|
|
3
|
+
> Python library for evaluating synthetic activity schedules: given daily sequences of who-did-what-and-when, measures how well a synthetic population reproduces observed distributions across frequencies, timing, transitions, participation rates, and novelty. Distances are 0–1 (lower is better).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
pip install acteval
|
|
8
|
+
|
|
9
|
+
## Input format
|
|
10
|
+
|
|
11
|
+
pandas DataFrame, one row per activity episode:
|
|
12
|
+
- pid — person identifier (int or str)
|
|
13
|
+
- act — activity label (str, e.g. "home", "work", "shop")
|
|
14
|
+
- start — episode start time (numeric, any consistent unit)
|
|
15
|
+
- end — episode end time (numeric)
|
|
16
|
+
- duration — episode duration (numeric)
|
|
17
|
+
|
|
18
|
+
## Key API
|
|
19
|
+
|
|
20
|
+
- compare(observed, synthetic, **kwargs) → EvalResult
|
|
21
|
+
Compare one or more synthetic DataFrames to observed.
|
|
22
|
+
synthetic can be a single DataFrame or {"name": df, ...} dict.
|
|
23
|
+
|
|
24
|
+
- Evaluator(observed) — cache observed features for repeated comparisons.
|
|
25
|
+
evaluator.compare({"v1": df_v1}) re-uses cached observed features.
|
|
26
|
+
|
|
27
|
+
- pairwise_distances(schedules, specs=None) → PairwiseResult
|
|
28
|
+
NxN distance matrix between individual schedules.
|
|
29
|
+
result.matrix (numpy), result.to_dataframe() (labeled).
|
|
30
|
+
|
|
31
|
+
## EvalResult structure
|
|
32
|
+
|
|
33
|
+
Six DataFrames at three aggregation levels:
|
|
34
|
+
- distances / descriptions — per-feature (index: domain, feature, segment)
|
|
35
|
+
- group_distances / group_descriptions — per feature group (index: domain, feature)
|
|
36
|
+
- domain_distances / domain_descriptions — per domain (index: domain)
|
|
37
|
+
|
|
38
|
+
Convenience: result.summary(), result.rank_models(), result.best_model
|
|
39
|
+
|
|
40
|
+
## Evaluation domains
|
|
41
|
+
|
|
42
|
+
- participations — who does what and how often; participation rates, joint participation
|
|
43
|
+
- transitions — activity sequence patterns (2-, 3-, 4-gram bigrams)
|
|
44
|
+
- timing — start times and durations (EMD via Wasserstein distance)
|
|
45
|
+
- creativity — novelty and diversity of synthetic schedules vs observed
|
|
46
|
+
- feasibility — structural validity: home-based, no consecutive duplicate activities
|
|
47
|
+
|
|
48
|
+
Missing activity penalty: 1.0 (maximum) for timing; None (compute on available data) for participation/transitions.
|
|
49
|
+
|
|
50
|
+
## Pairwise distance specs
|
|
51
|
+
|
|
52
|
+
from acteval.pairwise import chamfer_spec, soft_dtw_spec, default_pairwise_specs
|
|
53
|
+
- default_pairwise_specs() — MAE on participation counts, bigram counts, mean durations
|
|
54
|
+
- chamfer_spec(max_len, weight) — Chamfer on EOS-padded (N, L, 2) sequences
|
|
55
|
+
- soft_dtw_spec(max_len, gamma, weight) — Soft-DTW on EOS-padded sequences
|
|
56
|
+
|
|
57
|
+
## Source layout
|
|
58
|
+
|
|
59
|
+
src/acteval/
|
|
60
|
+
- evaluate.py — public API: compare(), Evaluator, evaluate()
|
|
61
|
+
- _pipeline.py — evaluation engine; builds features → groups → domains output
|
|
62
|
+
- population.py — Population: core data structure, integer-encodes activities/persons
|
|
63
|
+
- _jobs.py — JobSpec dataclass, get_jobs(); reads config.toml
|
|
64
|
+
- pairwise.py — pairwise_distances(), PairwiseResult, spec factories
|
|
65
|
+
- features/ — structural, frequency, times, transitions, participation, creativity
|
|
66
|
+
- distance/ — wasserstein.py (EMD via POT), scalar.py (MAE/MSE/MAPE)
|
|
67
|
+
- describe/ — descriptive stats alongside distance metrics
|
|
68
|
+
- _filters.py — filter_novel(): removes sequences seen in base population
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acteval"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Standalone library for evaluating activity sequences"
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"pandas",
|
|
8
8
|
"numpy",
|
|
9
|
-
"scipy
|
|
9
|
+
"scipy",
|
|
10
10
|
"pot",
|
|
11
11
|
"matplotlib",
|
|
12
12
|
"tabulate",
|
|
13
13
|
"tomli>=1.1.0; python_version < '3.11'",
|
|
14
|
+
"tslearn",
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
[tool.uv]
|
|
17
|
-
dev-dependencies = [
|
|
18
|
+
dev-dependencies = [
|
|
19
|
+
"pytest",
|
|
20
|
+
"pytest-cov",
|
|
21
|
+
"pytest-benchmark",
|
|
22
|
+
"ruff",
|
|
23
|
+
"black",
|
|
24
|
+
"matplotlib>=3.7.5",
|
|
25
|
+
"seaborn>=0.13.2",
|
|
26
|
+
"polars>=1.8.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
acteval = "acteval.cli:main"
|
|
18
31
|
|
|
19
32
|
[build-system]
|
|
20
33
|
requires = ["hatchling"]
|
|
@@ -25,9 +38,10 @@ packages = ["src/acteval"]
|
|
|
25
38
|
|
|
26
39
|
[tool.pytest.ini_options]
|
|
27
40
|
testpaths = ["tests"]
|
|
41
|
+
addopts = "--ignore=tests/test_bench_evaluate.py --ignore=tests/test_bench_pairwise.py --benchmark-columns=mean,stddev,rounds --benchmark-sort=name"
|
|
28
42
|
|
|
29
43
|
[tool.ruff]
|
|
30
|
-
target-version = "
|
|
44
|
+
target-version = "py310"
|
|
31
45
|
|
|
32
46
|
[tool.ruff.lint]
|
|
33
47
|
select = ["E", "F", "I", "W"]
|
|
@@ -37,4 +51,4 @@ ignore = ["E501"]
|
|
|
37
51
|
"tests/test_distance_emd.py" = ["E402"]
|
|
38
52
|
|
|
39
53
|
[tool.black]
|
|
40
|
-
target-version = ["
|
|
54
|
+
target-version = ["py310"]
|