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.
Files changed (95) hide show
  1. acteval-0.1.2/.github/workflows/benchmark.yml +40 -0
  2. {acteval-0.1.0 → acteval-0.1.2}/.github/workflows/ci.yml +6 -3
  3. {acteval-0.1.0 → acteval-0.1.2}/PKG-INFO +4 -3
  4. acteval-0.1.2/README.md +319 -0
  5. acteval-0.1.2/llms.txt +68 -0
  6. {acteval-0.1.0 → acteval-0.1.2}/pyproject.toml +20 -6
  7. acteval-0.1.2/scripts/generate_blog_plots.py +460 -0
  8. acteval-0.1.2/src/acteval/__init__.py +35 -0
  9. acteval-0.1.2/src/acteval/__main__.py +4 -0
  10. acteval-0.1.2/src/acteval/_aggregation.py +265 -0
  11. acteval-0.1.2/src/acteval/_compat.py +34 -0
  12. acteval-0.1.0/src/acteval/filters.py → acteval-0.1.2/src/acteval/_filters.py +1 -1
  13. acteval-0.1.2/src/acteval/_jobs.py +266 -0
  14. acteval-0.1.2/src/acteval/_pipeline.py +395 -0
  15. acteval-0.1.2/src/acteval/_report.py +97 -0
  16. acteval-0.1.2/src/acteval/_result_frame.py +178 -0
  17. acteval-0.1.2/src/acteval/_splits.py +67 -0
  18. acteval-0.1.2/src/acteval/cli.py +206 -0
  19. acteval-0.1.2/src/acteval/config.toml +43 -0
  20. acteval-0.1.2/src/acteval/describe/__init__.py +13 -0
  21. {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/frequency.py +29 -20
  22. acteval-0.1.2/src/acteval/describe/plot.py +603 -0
  23. acteval-0.1.2/src/acteval/describe/results.py +124 -0
  24. {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/times.py +43 -38
  25. {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/describe/transitions.py +15 -12
  26. acteval-0.1.2/src/acteval/describe/utils.py +142 -0
  27. acteval-0.1.2/src/acteval/distance/__init__.py +33 -0
  28. {acteval-0.1.0 → acteval-0.1.2}/src/acteval/distance/wasserstein.py +0 -6
  29. acteval-0.1.2/src/acteval/evaluate.py +794 -0
  30. acteval-0.1.2/src/acteval/features/__init__.py +28 -0
  31. acteval-0.1.0/src/acteval/density/features/pid_features.py → acteval-0.1.2/src/acteval/features/_pid_features.py +1 -1
  32. acteval-0.1.2/src/acteval/features/catalogue.py +191 -0
  33. {acteval-0.1.0/src/acteval/creativity → acteval-0.1.2/src/acteval}/features/creativity.py +17 -2
  34. {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/features/frequency.py +1 -1
  35. acteval-0.1.2/src/acteval/features/participation.py +79 -0
  36. acteval-0.1.2/src/acteval/features/structural.py +202 -0
  37. acteval-0.1.2/src/acteval/features/times.py +96 -0
  38. acteval-0.1.2/src/acteval/features/transitions.py +117 -0
  39. acteval-0.1.2/src/acteval/pairwise.py +447 -0
  40. {acteval-0.1.0 → acteval-0.1.2}/src/acteval/population.py +68 -9
  41. acteval-0.1.2/tests/conftest.py +30 -0
  42. acteval-0.1.2/tests/test_bench_evaluate.py +96 -0
  43. acteval-0.1.2/tests/test_bench_pairwise.py +72 -0
  44. acteval-0.1.2/tests/test_cli.py +280 -0
  45. acteval-0.1.2/tests/test_compare.py +39 -0
  46. {acteval-0.1.0 → acteval-0.1.2}/tests/test_creativity.py +1 -1
  47. {acteval-0.1.0 → acteval-0.1.2}/tests/test_describe_features.py +12 -25
  48. acteval-0.1.2/tests/test_features_catalogue.py +75 -0
  49. acteval-0.1.2/tests/test_features_participation.py +72 -0
  50. {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_structural.py +33 -82
  51. acteval-0.1.2/tests/test_features_times.py +163 -0
  52. {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_transitions.py +15 -18
  53. {acteval-0.1.0 → acteval-0.1.2}/tests/test_features_utils.py +1 -1
  54. {acteval-0.1.0 → acteval-0.1.2}/tests/test_filter.py +1 -1
  55. {acteval-0.1.0 → acteval-0.1.2}/tests/test_frequency.py +2 -2
  56. acteval-0.1.2/tests/test_integration.py +232 -0
  57. acteval-0.1.2/tests/test_jobs.py +39 -0
  58. acteval-0.1.2/tests/test_missing_features.py +58 -0
  59. acteval-0.1.2/tests/test_pairwise.py +526 -0
  60. {acteval-0.1.0 → acteval-0.1.2}/tests/test_pid_features.py +161 -108
  61. acteval-0.1.2/tests/test_polars_compat.py +150 -0
  62. {acteval-0.1.0 → acteval-0.1.2}/tests/test_population.py +2 -2
  63. {acteval-0.1.0 → acteval-0.1.2}/tests/test_report.py +14 -13
  64. acteval-0.1.2/tests/test_validation.py +256 -0
  65. {acteval-0.1.0 → acteval-0.1.2}/uv.lock +905 -33
  66. acteval-0.1.0/.claude/settings.local.json +0 -16
  67. acteval-0.1.0/CLAUDE.md +0 -60
  68. acteval-0.1.0/README.md +0 -118
  69. acteval-0.1.0/src/acteval/__init__.py +0 -10
  70. acteval-0.1.0/src/acteval/config.toml +0 -24
  71. acteval-0.1.0/src/acteval/creativity/__init__.py +0 -2
  72. acteval-0.1.0/src/acteval/creativity/features/__init__.py +0 -0
  73. acteval-0.1.0/src/acteval/density/__init__.py +0 -2
  74. acteval-0.1.0/src/acteval/density/describe/__init__.py +0 -0
  75. acteval-0.1.0/src/acteval/density/features/__init__.py +0 -0
  76. acteval-0.1.0/src/acteval/density/features/participation.py +0 -192
  77. acteval-0.1.0/src/acteval/density/features/times.py +0 -171
  78. acteval-0.1.0/src/acteval/density/features/transitions.py +0 -241
  79. acteval-0.1.0/src/acteval/distance/__init__.py +0 -16
  80. acteval-0.1.0/src/acteval/evaluate.py +0 -811
  81. acteval-0.1.0/src/acteval/jobs.py +0 -215
  82. acteval-0.1.0/src/acteval/ops.py +0 -49
  83. acteval-0.1.0/src/acteval/structural/__init__.py +0 -2
  84. acteval-0.1.0/src/acteval/structural/features/__init__.py +0 -0
  85. acteval-0.1.0/src/acteval/structural/features/structural.py +0 -210
  86. acteval-0.1.0/tests/test_compare.py +0 -69
  87. acteval-0.1.0/tests/test_features_participation.py +0 -147
  88. acteval-0.1.0/tests/test_features_times.py +0 -215
  89. {acteval-0.1.0 → acteval-0.1.2}/.github/workflows/publish.yml +0 -0
  90. {acteval-0.1.0 → acteval-0.1.2}/.gitignore +0 -0
  91. {acteval-0.1.0 → acteval-0.1.2}/src/acteval/distance/scalar.py +0 -0
  92. {acteval-0.1.0/src/acteval/density → acteval-0.1.2/src/acteval}/features/_discretise.py +0 -0
  93. /acteval-0.1.0/src/acteval/density/features/utils.py → /acteval-0.1.2/src/acteval/features/_utils.py +0 -0
  94. {acteval-0.1.0 → acteval-0.1.2}/tests/test_distance_emd.py +0 -0
  95. {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: ubuntu-latest
10
+ runs-on: ${{ matrix.os }}
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
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.0
3
+ Version: 0.1.2
4
4
  Summary: Standalone library for evaluating activity sequences
5
- Requires-Python: >=3.8
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<=1.13
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
@@ -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.0"
3
+ version = "0.1.2"
4
4
  description = "Standalone library for evaluating activity sequences"
5
- requires-python = ">=3.8"
5
+ requires-python = ">=3.10"
6
6
  dependencies = [
7
7
  "pandas",
8
8
  "numpy",
9
- "scipy<=1.13",
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 = ["pytest", "pytest-cov", "ruff", "black"]
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 = "py39"
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 = ["py39"]
54
+ target-version = ["py310"]