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.
Files changed (53) hide show
  1. acteval-0.1.0/.claude/settings.local.json +16 -0
  2. acteval-0.1.0/.github/workflows/ci.yml +34 -0
  3. acteval-0.1.0/.github/workflows/publish.yml +36 -0
  4. acteval-0.1.0/.gitignore +45 -0
  5. acteval-0.1.0/CLAUDE.md +60 -0
  6. acteval-0.1.0/PKG-INFO +12 -0
  7. acteval-0.1.0/README.md +118 -0
  8. acteval-0.1.0/pyproject.toml +40 -0
  9. acteval-0.1.0/src/acteval/__init__.py +10 -0
  10. acteval-0.1.0/src/acteval/config.toml +24 -0
  11. acteval-0.1.0/src/acteval/creativity/__init__.py +2 -0
  12. acteval-0.1.0/src/acteval/creativity/features/__init__.py +0 -0
  13. acteval-0.1.0/src/acteval/creativity/features/creativity.py +88 -0
  14. acteval-0.1.0/src/acteval/density/__init__.py +2 -0
  15. acteval-0.1.0/src/acteval/density/describe/__init__.py +0 -0
  16. acteval-0.1.0/src/acteval/density/describe/frequency.py +101 -0
  17. acteval-0.1.0/src/acteval/density/describe/times.py +269 -0
  18. acteval-0.1.0/src/acteval/density/describe/transitions.py +93 -0
  19. acteval-0.1.0/src/acteval/density/features/__init__.py +0 -0
  20. acteval-0.1.0/src/acteval/density/features/_discretise.py +97 -0
  21. acteval-0.1.0/src/acteval/density/features/frequency.py +51 -0
  22. acteval-0.1.0/src/acteval/density/features/participation.py +192 -0
  23. acteval-0.1.0/src/acteval/density/features/pid_features.py +73 -0
  24. acteval-0.1.0/src/acteval/density/features/times.py +171 -0
  25. acteval-0.1.0/src/acteval/density/features/transitions.py +241 -0
  26. acteval-0.1.0/src/acteval/density/features/utils.py +197 -0
  27. acteval-0.1.0/src/acteval/distance/__init__.py +16 -0
  28. acteval-0.1.0/src/acteval/distance/scalar.py +68 -0
  29. acteval-0.1.0/src/acteval/distance/wasserstein.py +200 -0
  30. acteval-0.1.0/src/acteval/evaluate.py +811 -0
  31. acteval-0.1.0/src/acteval/filters.py +16 -0
  32. acteval-0.1.0/src/acteval/jobs.py +215 -0
  33. acteval-0.1.0/src/acteval/ops.py +49 -0
  34. acteval-0.1.0/src/acteval/population.py +165 -0
  35. acteval-0.1.0/src/acteval/structural/__init__.py +2 -0
  36. acteval-0.1.0/src/acteval/structural/features/__init__.py +0 -0
  37. acteval-0.1.0/src/acteval/structural/features/structural.py +210 -0
  38. acteval-0.1.0/tests/test_compare.py +69 -0
  39. acteval-0.1.0/tests/test_creativity.py +136 -0
  40. acteval-0.1.0/tests/test_describe_features.py +56 -0
  41. acteval-0.1.0/tests/test_distance_emd.py +75 -0
  42. acteval-0.1.0/tests/test_distance_scalar.py +33 -0
  43. acteval-0.1.0/tests/test_features_participation.py +147 -0
  44. acteval-0.1.0/tests/test_features_structural.py +202 -0
  45. acteval-0.1.0/tests/test_features_times.py +215 -0
  46. acteval-0.1.0/tests/test_features_transitions.py +79 -0
  47. acteval-0.1.0/tests/test_features_utils.py +21 -0
  48. acteval-0.1.0/tests/test_filter.py +76 -0
  49. acteval-0.1.0/tests/test_frequency.py +105 -0
  50. acteval-0.1.0/tests/test_pid_features.py +292 -0
  51. acteval-0.1.0/tests/test_population.py +138 -0
  52. acteval-0.1.0/tests/test_report.py +124 -0
  53. 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
@@ -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
@@ -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'
@@ -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,10 @@
1
+ from acteval.evaluate import (
2
+ Evaluator as Evaluator,
3
+ )
4
+ from acteval.evaluate import (
5
+ compare as compare,
6
+ )
7
+ from acteval.evaluate import (
8
+ compare_splits as compare_splits,
9
+ )
10
+ from acteval.population import Population as Population
@@ -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
@@ -0,0 +1,2 @@
1
+ """Creativity metrics — diversity, novelty, conservatism, and homogeneity of
2
+ synthetic schedules relative to the observed population."""
@@ -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)
@@ -0,0 +1,2 @@
1
+ """Density estimation jobs — measures how well synthetic activity distributions
2
+ (participation rates, timing, n-gram transitions) match the observed population."""
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