acteval 0.1.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.0/.claude/settings.local.json +16 -0
- acteval-0.1.0/.github/workflows/ci.yml +34 -0
- acteval-0.1.0/.github/workflows/publish.yml +36 -0
- acteval-0.1.0/.gitignore +45 -0
- acteval-0.1.0/CLAUDE.md +60 -0
- acteval-0.1.0/PKG-INFO +12 -0
- acteval-0.1.0/README.md +118 -0
- acteval-0.1.0/pyproject.toml +40 -0
- acteval-0.1.0/src/acteval/__init__.py +10 -0
- acteval-0.1.0/src/acteval/config.toml +24 -0
- acteval-0.1.0/src/acteval/creativity/__init__.py +2 -0
- acteval-0.1.0/src/acteval/creativity/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/creativity/features/creativity.py +88 -0
- acteval-0.1.0/src/acteval/density/__init__.py +2 -0
- acteval-0.1.0/src/acteval/density/describe/__init__.py +0 -0
- acteval-0.1.0/src/acteval/density/describe/frequency.py +101 -0
- acteval-0.1.0/src/acteval/density/describe/times.py +269 -0
- acteval-0.1.0/src/acteval/density/describe/transitions.py +93 -0
- acteval-0.1.0/src/acteval/density/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/density/features/_discretise.py +97 -0
- acteval-0.1.0/src/acteval/density/features/frequency.py +51 -0
- acteval-0.1.0/src/acteval/density/features/participation.py +192 -0
- acteval-0.1.0/src/acteval/density/features/pid_features.py +73 -0
- acteval-0.1.0/src/acteval/density/features/times.py +171 -0
- acteval-0.1.0/src/acteval/density/features/transitions.py +241 -0
- acteval-0.1.0/src/acteval/density/features/utils.py +197 -0
- acteval-0.1.0/src/acteval/distance/__init__.py +16 -0
- acteval-0.1.0/src/acteval/distance/scalar.py +68 -0
- acteval-0.1.0/src/acteval/distance/wasserstein.py +200 -0
- acteval-0.1.0/src/acteval/evaluate.py +811 -0
- acteval-0.1.0/src/acteval/filters.py +16 -0
- acteval-0.1.0/src/acteval/jobs.py +215 -0
- acteval-0.1.0/src/acteval/ops.py +49 -0
- acteval-0.1.0/src/acteval/population.py +165 -0
- acteval-0.1.0/src/acteval/structural/__init__.py +2 -0
- acteval-0.1.0/src/acteval/structural/features/__init__.py +0 -0
- acteval-0.1.0/src/acteval/structural/features/structural.py +210 -0
- acteval-0.1.0/tests/test_compare.py +69 -0
- acteval-0.1.0/tests/test_creativity.py +136 -0
- acteval-0.1.0/tests/test_describe_features.py +56 -0
- acteval-0.1.0/tests/test_distance_emd.py +75 -0
- acteval-0.1.0/tests/test_distance_scalar.py +33 -0
- acteval-0.1.0/tests/test_features_participation.py +147 -0
- acteval-0.1.0/tests/test_features_structural.py +202 -0
- acteval-0.1.0/tests/test_features_times.py +215 -0
- acteval-0.1.0/tests/test_features_transitions.py +79 -0
- acteval-0.1.0/tests/test_features_utils.py +21 -0
- acteval-0.1.0/tests/test_filter.py +76 -0
- acteval-0.1.0/tests/test_frequency.py +105 -0
- acteval-0.1.0/tests/test_pid_features.py +292 -0
- acteval-0.1.0/tests/test_population.py +138 -0
- acteval-0.1.0/tests/test_report.py +124 -0
- acteval-0.1.0/uv.lock +2945 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(find /home/fred/projects/acteval -maxdepth 2 -type f -name \"*.md\" -o -name \"*.toml\" -o -name \"*.txt\" -o -name \"Makefile\" 2>/dev/null | grep -v \".venv\")",
|
|
5
|
+
"Bash(uv sync:*)",
|
|
6
|
+
"Bash(pytest tests/ 2>&1)",
|
|
7
|
+
"Bash(python -c \"import acteval; print\\('ok'\\)\")",
|
|
8
|
+
"Bash(which python:*)",
|
|
9
|
+
"Bash(uv run:*)",
|
|
10
|
+
"Bash(python -m pytest tests/ -v 2>&1)",
|
|
11
|
+
"Bash(python -m pytest tests/ -x -q 2>&1)",
|
|
12
|
+
"Bash(find /home/fred/projects/acteval -type f -name \"*.toml\" -o -type f -name \"*.yaml\" -o -type f -name \"*.yml\" | grep -E \"\\\\.\\(toml|ya?ml\\)$\" | head -20)",
|
|
13
|
+
"Bash(python3 --version && python3 -m pip list | grep -E \"pytest|ruff|black|mypy\" | head -10)"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v4
|
|
20
|
+
|
|
21
|
+
- name: Install Python ${{ matrix.python-version }}
|
|
22
|
+
run: uv python install ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: uv run ruff check src/ tests/
|
|
29
|
+
|
|
30
|
+
- name: Check formatting
|
|
31
|
+
run: uv run black --check src/ tests/
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: uv run pytest tests/ --cov=src/acteval
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: astral-sh/setup-uv@v4
|
|
14
|
+
|
|
15
|
+
- name: Build
|
|
16
|
+
run: uv build
|
|
17
|
+
|
|
18
|
+
- uses: actions/upload-artifact@v4
|
|
19
|
+
with:
|
|
20
|
+
name: dist
|
|
21
|
+
path: dist/
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
needs: build
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/download-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
- name: Publish to PyPI
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
acteval-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
ss
|
|
2
|
+
|
|
3
|
+
# OS-specific
|
|
4
|
+
.DS_Store
|
|
5
|
+
|
|
6
|
+
# Editors
|
|
7
|
+
*~
|
|
8
|
+
\#*\#
|
|
9
|
+
**/.vscode
|
|
10
|
+
.spyderproject
|
|
11
|
+
.spyproject
|
|
12
|
+
.idea/
|
|
13
|
+
|
|
14
|
+
# Byte-compiled files
|
|
15
|
+
__pycache__
|
|
16
|
+
*.pyc
|
|
17
|
+
|
|
18
|
+
# Build output
|
|
19
|
+
build/
|
|
20
|
+
*.egg-info
|
|
21
|
+
dist/
|
|
22
|
+
.cruft.json
|
|
23
|
+
tmp/
|
|
24
|
+
|
|
25
|
+
# Virtual environment
|
|
26
|
+
venv/
|
|
27
|
+
.venv
|
|
28
|
+
|
|
29
|
+
# Reports
|
|
30
|
+
.coverage
|
|
31
|
+
.coverage.*
|
|
32
|
+
coverage.xml
|
|
33
|
+
*.cover
|
|
34
|
+
reports/
|
|
35
|
+
logs
|
|
36
|
+
|
|
37
|
+
# Caches
|
|
38
|
+
.*[cC]ache*
|
|
39
|
+
|
|
40
|
+
# Documentation
|
|
41
|
+
.docs
|
|
42
|
+
mike-*.yml
|
|
43
|
+
|
|
44
|
+
# Jupyter notebooks
|
|
45
|
+
.ipynb_checkpoints
|
acteval-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
uv sync
|
|
10
|
+
|
|
11
|
+
# Run tests
|
|
12
|
+
pytest tests/
|
|
13
|
+
|
|
14
|
+
# Run a single test file
|
|
15
|
+
pytest tests/test_compare.py
|
|
16
|
+
|
|
17
|
+
# Run with coverage
|
|
18
|
+
pytest --cov=src/acteval tests/
|
|
19
|
+
|
|
20
|
+
# Build
|
|
21
|
+
uv build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
`acteval` is a Python library for evaluating synthetic activity schedules (daily time-use data) by comparing them to observed data.
|
|
27
|
+
|
|
28
|
+
### Data Flow
|
|
29
|
+
|
|
30
|
+
1. **Input**: pandas DataFrames with columns `pid` (person ID), `act` (activity), `start`, `end`, `duration`
|
|
31
|
+
2. **`Population`** (`population.py`): Converts DataFrames to numpy arrays once at construction; uses lazy properties for expensive derived quantities (act_enum_key, count_matrix, seq_key). Integer-encodes activities and person IDs for vectorized operations.
|
|
32
|
+
3. **Feature extraction** (`features/`): Each function takes a `Population` and returns `dict[str, tuple[ndarray, ndarray]]` — `{label: (values, weights)}`. Weights enable weighted aggregation across persons.
|
|
33
|
+
4. **Distance metrics** (`distance/`): `wasserstein.py` uses the POT library for EMD/optimal transport; `scalar.py` provides MAE, MSE, MAPE for scalar values.
|
|
34
|
+
5. **Evaluation pipeline** (`evaluate.py`): Organized into "jobs" — tuples of `(feature_fn, size_fn, description_job, distance_job)`. Target features are computed once; multiple synthetic populations are compared against them.
|
|
35
|
+
6. **Output**: Nested DataFrames with individual features, feature groups, and domain summaries, with per-synthetic-model statistics.
|
|
36
|
+
|
|
37
|
+
### Key Modules
|
|
38
|
+
|
|
39
|
+
- **`evaluate.py`**: Main engine — `evaluate()`, `compare()`, `Evaluator` class, `subsample_and_evaluate()`. The `Evaluator` class caches target features for repeated comparisons.
|
|
40
|
+
- **`population.py`**: Core data structure. Pre-computes pid groupings and person boundaries.
|
|
41
|
+
- **`features/`**: `structural.py` (sequence properties, feasibility), `frequency.py` (activity densities), `times.py` (start/end times, durations), `transitions.py` (n-gram patterns up to 4-grams), `participation.py` (activity rates), `creativity.py` (novelty detection via hashing).
|
|
42
|
+
- **`describe/`**: Aggregate statistics for frequency, timing, and transition patterns — used for reporting alongside distance metrics.
|
|
43
|
+
- **`filters.py`**: `filter_novel()` removes sequences already seen in the base population.
|
|
44
|
+
- **`ops.py`**: Aggregation operations (`average`, `feature_weight`).
|
|
45
|
+
|
|
46
|
+
### Public API
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from acteval import compare, evaluate, Evaluator
|
|
50
|
+
|
|
51
|
+
# Single comparison
|
|
52
|
+
result = compare(observed_df, synthetic_df, report_stats=True)
|
|
53
|
+
|
|
54
|
+
# Multiple models
|
|
55
|
+
result = compare(observed_df, {"model_a": df_a, "model_b": df_b})
|
|
56
|
+
|
|
57
|
+
# Reuse target features across comparisons
|
|
58
|
+
evaluator = Evaluator(observed_df)
|
|
59
|
+
result = evaluator.compare({"model_a": df_a})
|
|
60
|
+
```
|
acteval-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acteval
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone library for evaluating activity sequences
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: matplotlib
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: pandas
|
|
9
|
+
Requires-Dist: pot
|
|
10
|
+
Requires-Dist: scipy<=1.13
|
|
11
|
+
Requires-Dist: tabulate
|
|
12
|
+
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
acteval-0.1.0/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
## Development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install dependencies (including dev tools)
|
|
21
|
+
uv sync
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
pytest tests/
|
|
25
|
+
|
|
26
|
+
# Run tests with coverage
|
|
27
|
+
pytest --cov=src/acteval tests/
|
|
28
|
+
|
|
29
|
+
# Lint with ruff
|
|
30
|
+
ruff check src/ tests/
|
|
31
|
+
|
|
32
|
+
# Auto-fix lint issues
|
|
33
|
+
ruff check --fix src/ tests/
|
|
34
|
+
|
|
35
|
+
# Check formatting with black
|
|
36
|
+
black --check src/ tests/
|
|
37
|
+
|
|
38
|
+
# Auto-format
|
|
39
|
+
black src/ tests/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Input format
|
|
43
|
+
|
|
44
|
+
Data is passed as a pandas DataFrame with one row per activity episode:
|
|
45
|
+
|
|
46
|
+
| column | type | description |
|
|
47
|
+
|--------|------|-------------|
|
|
48
|
+
| `pid` | int/str | Person identifier |
|
|
49
|
+
| `act` | str | Activity label (e.g. `"home"`, `"work"`, `"shop"`) |
|
|
50
|
+
| `start` | numeric | Start time (any consistent unit, e.g. hours) |
|
|
51
|
+
| `end` | numeric | End time |
|
|
52
|
+
| `duration` | numeric | Duration (`end - start`) |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import pandas as pd
|
|
56
|
+
|
|
57
|
+
observed = pd.DataFrame([
|
|
58
|
+
{"pid": 0, "act": "home", "start": 0, "end": 6, "duration": 6},
|
|
59
|
+
{"pid": 0, "act": "work", "start": 6, "end": 14, "duration": 8},
|
|
60
|
+
{"pid": 0, "act": "home", "start": 14, "end": 24, "duration": 10},
|
|
61
|
+
{"pid": 1, "act": "home", "start": 0, "end": 10, "duration": 10},
|
|
62
|
+
{"pid": 1, "act": "work", "start": 10, "end": 24, "duration": 14},
|
|
63
|
+
])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `compare(observed, synthetic, **kwargs)`
|
|
69
|
+
|
|
70
|
+
Compare one or more synthetic populations to an observed population.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from acteval import compare
|
|
74
|
+
|
|
75
|
+
# Single synthetic population
|
|
76
|
+
result = compare(observed, synthetic)
|
|
77
|
+
|
|
78
|
+
# Multiple models side-by-side
|
|
79
|
+
result = compare(observed, {"model_a": synthetic_a, "model_b": synthetic_b})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`result` is a dict with six DataFrames:
|
|
83
|
+
|
|
84
|
+
| key | description |
|
|
85
|
+
|-----|-------------|
|
|
86
|
+
| `"distances"` | Per-feature distances |
|
|
87
|
+
| `"group_distances"` | Distances aggregated by feature group |
|
|
88
|
+
| `"domain_distances"` | Distances aggregated by domain (participations, transitions, timing) |
|
|
89
|
+
| `"descriptions"` | Per-feature descriptive statistics |
|
|
90
|
+
| `"group_descriptions"` | Group-level descriptive statistics |
|
|
91
|
+
| `"domain_descriptions"` | Domain-level descriptive statistics |
|
|
92
|
+
|
|
93
|
+
Pass `report_stats=True` to include additional statistics in the output.
|
|
94
|
+
|
|
95
|
+
### `Evaluator`
|
|
96
|
+
|
|
97
|
+
Use `Evaluator` when comparing multiple synthetic populations against the same observed data — it computes and caches the observed features once.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from acteval import Evaluator
|
|
101
|
+
|
|
102
|
+
evaluator = Evaluator(observed)
|
|
103
|
+
|
|
104
|
+
result_v1 = evaluator.compare({"v1": synthetic_v1})
|
|
105
|
+
result_v2 = evaluator.compare({"v2": synthetic_v2})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `Population`
|
|
109
|
+
|
|
110
|
+
For direct access to the underlying data structure:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from acteval import Population
|
|
114
|
+
|
|
115
|
+
pop = Population(observed)
|
|
116
|
+
print(pop.acts) # activity labels per episode
|
|
117
|
+
print(pop.durations) # durations as numpy array
|
|
118
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "acteval"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Standalone library for evaluating activity sequences"
|
|
5
|
+
requires-python = ">=3.8"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"pandas",
|
|
8
|
+
"numpy",
|
|
9
|
+
"scipy<=1.13",
|
|
10
|
+
"pot",
|
|
11
|
+
"matplotlib",
|
|
12
|
+
"tabulate",
|
|
13
|
+
"tomli>=1.1.0; python_version < '3.11'",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.uv]
|
|
17
|
+
dev-dependencies = ["pytest", "pytest-cov", "ruff", "black"]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/acteval"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
target-version = "py39"
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E", "F", "I", "W"]
|
|
34
|
+
ignore = ["E501"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint.per-file-ignores]
|
|
37
|
+
"tests/test_distance_emd.py" = ["E402"]
|
|
38
|
+
|
|
39
|
+
[tool.black]
|
|
40
|
+
target-version = ["py39"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[ngrams]
|
|
2
|
+
min_count = 3
|
|
3
|
+
|
|
4
|
+
[jobs.participations]
|
|
5
|
+
lengths = true
|
|
6
|
+
rates = true
|
|
7
|
+
pair_rates = true
|
|
8
|
+
|
|
9
|
+
[jobs.transitions]
|
|
10
|
+
"2-gram" = true
|
|
11
|
+
"3-gram" = true
|
|
12
|
+
"4-gram" = true
|
|
13
|
+
|
|
14
|
+
[jobs.timing]
|
|
15
|
+
start_times = true
|
|
16
|
+
durations = true
|
|
17
|
+
start_durations = true
|
|
18
|
+
joint_durations = true
|
|
19
|
+
|
|
20
|
+
[jobs.creativity]
|
|
21
|
+
enabled = true
|
|
22
|
+
|
|
23
|
+
[jobs.structural]
|
|
24
|
+
enabled = true
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from pandas import DataFrame
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def hash_schedule(schedule: DataFrame) -> list[str]:
|
|
5
|
+
"""Hash a schedule. We first create strings of combined activities and durations.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
schedule (DataFrame): Input schedule.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
str: hashed schedule.
|
|
12
|
+
"""
|
|
13
|
+
act_hash = schedule.act.astype(str) + schedule.duration.astype(str)
|
|
14
|
+
return "".join(act_hash)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def hash_population(population: DataFrame) -> set[str]:
|
|
18
|
+
"""Hash a population of sequences. We first create strings of combined activities and durations.
|
|
19
|
+
Then create a python set of these strings. This will remove duplicates.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
population (DataFrame): Input population of sequences.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
set[str]: set of hashed sequences.
|
|
26
|
+
"""
|
|
27
|
+
act_hash = population.act.astype(str) + population.duration.astype(str)
|
|
28
|
+
return set(act_hash.groupby(population.pid).apply("".join))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def diversity(population: DataFrame, hashed: set[str]) -> float:
|
|
32
|
+
"""Measure the internal diversity of a population of sequences. This is the ratio of unique
|
|
33
|
+
sequences to the total number of sequences.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
population (DataFrame): Input population of sequences.
|
|
37
|
+
hashed (set[str]): Hashed population of sequences.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
float: Diversity of the population.
|
|
41
|
+
"""
|
|
42
|
+
n = population.pid.nunique()
|
|
43
|
+
if n == 0:
|
|
44
|
+
return 0
|
|
45
|
+
unique = len(hashed)
|
|
46
|
+
return unique / n
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def homogeneity(population: DataFrame, hashed: set[str]) -> float:
|
|
50
|
+
"""Measure the internal homogeneity of a population of sequences. This is 1-diversity.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
population (DataFrame): Input population of sequences.
|
|
54
|
+
hashed (set[str]): Hashed population of sequences.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
float: Homogeneity of the synthetic sample.
|
|
58
|
+
"""
|
|
59
|
+
return 1 - diversity(population, hashed)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def novelty(observed_hashed: set[str], synthetic_hashed: set[str]) -> float:
|
|
63
|
+
"""Measure the novelty of a population by comparing it to an observed population.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
observed_hashed (set[str]): Hashed observed population.
|
|
67
|
+
synthetic_hashed (set[str]): Hashed synthetic population.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
float: Novelty of the synthetic population.
|
|
71
|
+
"""
|
|
72
|
+
n = len(synthetic_hashed)
|
|
73
|
+
if n == 0:
|
|
74
|
+
return 0
|
|
75
|
+
return len(synthetic_hashed - observed_hashed) / n
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def conservatism(observed_hashed: set[str], synthetic_hashed: set[str]) -> float:
|
|
79
|
+
"""Measure the conservatism of a population as 1-novelty.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
observed_hashed (set[str]): Hashed observed population.
|
|
83
|
+
synthetic_hashed (set[str]): Hashed synthetic population.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
float: Conservatism of the synthetic population.
|
|
87
|
+
"""
|
|
88
|
+
return 1 - novelty(observed_hashed, synthetic_hashed)
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from matplotlib import pyplot as plt
|
|
5
|
+
from matplotlib.patches import Patch
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
|
|
8
|
+
from acteval.density.features.frequency import binned_activity_density
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def frequency_plots(observed: DataFrame, ys: Optional[dict[DataFrame]], **kwargs):
|
|
12
|
+
if ys is None:
|
|
13
|
+
ys = dict()
|
|
14
|
+
acts = list(observed.act.value_counts(ascending=False).index)
|
|
15
|
+
class_map = {n: i for i, n in enumerate(acts)}
|
|
16
|
+
|
|
17
|
+
n_plots = len(ys) + 2
|
|
18
|
+
ratios = [1 for _ in range(n_plots)]
|
|
19
|
+
ratios[-1] = 0.3
|
|
20
|
+
|
|
21
|
+
cmap = kwargs.pop("cmap", None)
|
|
22
|
+
if cmap is None:
|
|
23
|
+
cmap = plt.cm.Set3
|
|
24
|
+
colors = cmap.colors
|
|
25
|
+
factor = (len(acts) // len(colors)) + 1
|
|
26
|
+
cmap = dict(zip(acts, colors * factor))
|
|
27
|
+
|
|
28
|
+
fig, axs = plt.subplots(
|
|
29
|
+
sharex=True,
|
|
30
|
+
sharey=True,
|
|
31
|
+
nrows=1,
|
|
32
|
+
ncols=n_plots,
|
|
33
|
+
constrained_layout=True,
|
|
34
|
+
figsize=kwargs.pop("figsize", (15, 4)),
|
|
35
|
+
gridspec_kw={"width_ratios": ratios},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
name = kwargs.pop("observed_title", "Observed")
|
|
39
|
+
|
|
40
|
+
plot_agg_acts(name, observed, class_map, ax=axs[0], legend=False, **kwargs)
|
|
41
|
+
|
|
42
|
+
# now deal with ys
|
|
43
|
+
for i, (name, y) in enumerate(ys.items()):
|
|
44
|
+
ax = axs[i + 1]
|
|
45
|
+
plot_agg_acts(name, y, class_map, ax=ax, legend=False, **kwargs)
|
|
46
|
+
|
|
47
|
+
# legend
|
|
48
|
+
elements = [Patch(facecolor=cmap[act], label=act.title()) for act in acts]
|
|
49
|
+
axs[-1].axis("off")
|
|
50
|
+
axs[-1].legend(handles=elements, loc="center left", frameon=False)
|
|
51
|
+
|
|
52
|
+
return fig
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plot_agg_acts(
|
|
56
|
+
name: str,
|
|
57
|
+
population: DataFrame,
|
|
58
|
+
class_map: dict,
|
|
59
|
+
duration: int = 1440,
|
|
60
|
+
step: int = 10,
|
|
61
|
+
ax=None,
|
|
62
|
+
legend=True,
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
interval = kwargs.pop("interval", 240)
|
|
66
|
+
bins = binned_activity_density(
|
|
67
|
+
population, duration=duration, step=step, class_map=class_map
|
|
68
|
+
)
|
|
69
|
+
columns = list(class_map.keys())
|
|
70
|
+
totals = bins.sum(0)
|
|
71
|
+
sorted_cols = [x for _, x in sorted(zip(totals, columns))]
|
|
72
|
+
df = DataFrame(bins, columns=columns)[sorted_cols]
|
|
73
|
+
df.index = [
|
|
74
|
+
datetime(2021, 11, 1, 0) + timedelta(minutes=i * step)
|
|
75
|
+
for i in range(len(df.index))
|
|
76
|
+
]
|
|
77
|
+
fig = df.plot(kind="bar", stacked=True, width=1, ax=ax, legend=legend, **kwargs)
|
|
78
|
+
if legend:
|
|
79
|
+
ax.legend(loc="upper right")
|
|
80
|
+
ax = fig.axes
|
|
81
|
+
labels = ["" for _ in range(len(df.index))]
|
|
82
|
+
|
|
83
|
+
labels[:: int(interval / step)] = [x.strftime("%H:%M") for x in df.index][
|
|
84
|
+
:: int(interval / step)
|
|
85
|
+
]
|
|
86
|
+
ax.set_xticks(
|
|
87
|
+
[i / step for i in [0, 240, 480, 720, 960, 1200, 1440]],
|
|
88
|
+
labels=["00:00", "04:00", "08:00", "12:00", "16:00", "20:00", "24:00"],
|
|
89
|
+
rotation=90,
|
|
90
|
+
fontsize=8,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
ax.spines["top"].set_visible(False)
|
|
94
|
+
ax.spines["right"].set_visible(False)
|
|
95
|
+
ax.spines["bottom"].set_visible(False)
|
|
96
|
+
ax.spines["left"].set_visible(False)
|
|
97
|
+
|
|
98
|
+
ax.set_xlabel("Time of day")
|
|
99
|
+
ax.set_ylabel("Activity Proportion")
|
|
100
|
+
ax.set_title(name)
|
|
101
|
+
return ax
|