# acteval

> 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).

## Install

pip install acteval

## Input format

pandas (or polars) DataFrame, one row per activity episode:
- pid — person identifier (int or str)
- act — activity label (str, e.g. "home", "work", "shop")
- start — episode start time (numeric, any consistent unit)
- end — episode end time (numeric)
- duration — episode duration (numeric)

Any two of start/end/duration are enough — the third is derived.

## Key API

- compare(observed, synthetic, **kwargs) → EvalResult
  Compare one or more synthetic DataFrames to observed.
  synthetic can be a single DataFrame (result column "synthetic") or {"name": df, ...} dict.
  kwargs: attributes, target_attributes, split_on (attribute-based splitting),
  config_path (custom config.toml), disable (dotted config.toml keys to switch
  off, applied on top of config_path), progress (tqdm bars), verbose.

- Evaluator(observed, target_attributes=None, split_on=None, config_path=None,
  disable=None, jobs=None, ...) — caches observed features for repeated
  comparisons. evaluator.compare({"v1": df_v1}) re-uses them. jobs (a pre-built
  EvalConfig) cannot be combined with config_path/disable (ValueError).
  Low-level: evaluator.compare_population(model, schedule, attributes=None) +
  evaluator.report() for one-model-at-a-time accumulation.

- compare_grid(schedules_a, schedules_b, attributes_a=None, attributes_b=None,
  split_on=None, **kwargs) → EvalResult
  Compare every synthetic model in schedules_b against every target in
  schedules_a ({name: df} dicts). Runs compare() once per target and combine()s
  the results; model columns are named "{target_name}::{model_name}".
  **kwargs (e.g. config_path, disable, progress) pass through to compare().

- compare_many(schedules_a, schedules_b, ...same args) → EvalResult
  Like compare_grid but pairwise: dicts are paired by position (must be the same
  length); model columns are named "{name_a}::{name_b}".

- acteval.results.combine(results: dict[str, EvalResult]) → EvalResult
  Merge several already-computed EvalResults (e.g. one per target) into one,
  namespacing model columns by source key. Used internally by compare_grid()/compare_many().
  Known limitation: the target/unit values used for feature→group→domain
  aggregation come from the first result only.

- pairwise_distances(schedules, specs=None) → PairwiseResult
  NxN distance matrix between individual schedules (not population vs population).
  result.matrix (numpy), result.pids, result.to_dataframe() (labeled).

## EvalResult structure

Three aggregation levels, each further split three ways:

- result.features / .groups / .domains → ScheduleView (most → least granular;
  index is (domain, feature, segment), (domain, feature), (domain) respectively)
- each ScheduleView: .combined / .by_attribute / .by_category → AggregatedResult
  (the latter two require split_on, else raise SplitNotAvailableError)
- each AggregatedResult: .distances and .descriptions DataFrames.
  distances has one column per model; descriptions also has a "target" column
  (the observed population's own value, for comparison).

Dispatcher: result.at(level="domains", split="combined") reaches any of the 9
combinations; result.at() == result.at("domains", "combined") == result.summary()
as a distances-only shortcut.

Convenience: result.summary(), result.rank_models(), result.best_model,
result.save(path) (writes every level/split to CSV).

Advanced: result.raw → {"descriptions": ResultFrame, "distances": ResultFrame}
(pre-aggregation values/weights/units, before any tier collapse); pair with
result.target_distance_weights to replicate the weighted aggregation yourself.

## Evaluation domains

- participations — who does what and how often; participation rates, joint participation, sequence lengths
- transitions    — activity sequence patterns (2-, 3-, 4-gram)
- timing         — start times and durations, incl. joint distributions (EMD via Wasserstein distance)
- creativity     — novelty and diversity of synthetic schedules vs observed
- feasibility    — structural validity: home-based, no consecutive duplicate activities
- sequences      — full abbreviated tour-string distributions (e.g. "h>w>h"); off by default

Missing activity penalty: 1.0 (maximum) for timing; None (compute on available data) for participation/transitions.

list_features() enumerates every feature function with its domain/config key.
list_disable_keys() enumerates every dotted disable=[...] path (e.g. "jobs.creativity.novelty").

## CLI

acteval compare TARGET -m NAME SCHEDULE [ATTRS] [-m ...] [--target-attrs PATH
--split-on COL] [-c CONFIG] [--disable KEY] [-o DIR] [-l features|groups|domains] [--batch DIR]

acteval filter non-home-based|consecutive SCHEDULE [-o OUT] — spot-check a
synthetic population for structural issues before running compare.

## Pairwise distance specs

from acteval.pairwise import chamfer_spec, soft_dtw_spec, default_pairwise_specs
- default_pairwise_specs() — three equal-weight MAE specs: participations, transitions, timing
- chamfer_spec(max_len, weight) — Chamfer on EOS-padded (N, L, 2) sequences
- soft_dtw_spec(max_len, gamma, weight) — Soft-DTW on EOS-padded sequences

## Source layout

src/acteval/
- evaluate.py       — comparison entry points: compare(), Evaluator, compare_many()
- results.py        — EvalResult/ScheduleView/AggregatedResult/SplitNotAvailableError, combine()
- _pipeline.py       — evaluation engine; builds per-feature rows, three-tier aggregation
- _aggregation.py, _result_frame.py — aggregation math, values/weights/units separation (ResultFrame)
- population.py      — Population: core data structure, integer-encodes activities/persons
- _jobs.py           — JobSpec/EvalConfig, get_jobs(); reads config.toml; list_disable_keys()
- pairwise.py         — pairwise_distances(), PairwiseResult, spec factories
- features/           — structural, frequency, times, transitions, participation, creativity; catalogue.py (list_features())
- distance/           — wasserstein.py (EMD via POT), scalar.py (MAE/MSE/MAPE)
- plot/               — matplotlib helpers (not imported by compare()/Evaluator)
- cli.py              — `acteval` command (compare, filter subcommands)
