acteval 0.1.0__py3-none-any.whl

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/filters.py ADDED
@@ -0,0 +1,16 @@
1
+ from pandas import DataFrame
2
+
3
+ from acteval.creativity.features import creativity
4
+
5
+
6
+ def no_filter(scenario: DataFrame, base: DataFrame) -> DataFrame:
7
+ return scenario
8
+
9
+
10
+ def filter_novel(scenario: DataFrame, base: DataFrame) -> DataFrame:
11
+ base_hashed = creativity.hash_population(base)
12
+ # Vectorized: hash all synthetic schedules at once
13
+ act_hash = scenario.act.astype(str) + scenario.duration.astype(str)
14
+ synth_hashes = act_hash.groupby(scenario.pid).agg("".join)
15
+ novel_pids = synth_hashes.index[~synth_hashes.isin(base_hashed)]
16
+ return scenario[scenario.pid.isin(novel_pids)].reset_index(drop=True)
acteval/jobs.py ADDED
@@ -0,0 +1,215 @@
1
+ import sys
2
+ from functools import partial
3
+ from pathlib import Path
4
+
5
+ if sys.version_info >= (3, 11):
6
+ import tomllib
7
+ else:
8
+ import tomli as tomllib
9
+
10
+ from acteval.density.features import participation, times, transitions
11
+ from acteval.distance import emd
12
+ from acteval.ops import average, average2d, feature_weight
13
+ from acteval.structural.features import structural
14
+
15
+ _DEFAULT_CONFIG = Path(__file__).parent / "config.toml"
16
+
17
+
18
+ def load_config(path=None) -> dict:
19
+ if path is None:
20
+ path = _DEFAULT_CONFIG
21
+ with open(path, "rb") as f:
22
+ return tomllib.load(f)
23
+
24
+
25
+ def build_density_jobs(cfg: dict) -> list[tuple]:
26
+ """Returns active jobs for participations, transitions, and timing domains.
27
+
28
+ Each job is a 4-tuple of ``(feature, size, description_job, distance_job)``
29
+ where *feature* is a 3-tuple ``(name, fn, per_pid_fn | None)``.
30
+ The per-pid function, when present, returns a ``PidFeatures`` instance.
31
+ """
32
+ ngram_min_count = cfg.get("ngrams", {}).get("min_count", 3)
33
+ participations_cfg = cfg.get("jobs", {}).get("participations", {})
34
+ transitions_cfg = cfg.get("jobs", {}).get("transitions", {})
35
+ timing_cfg = cfg.get("jobs", {}).get("timing", {})
36
+
37
+ participation_rate_jobs = []
38
+ if participations_cfg.get("lengths", True):
39
+ participation_rate_jobs.append(
40
+ (
41
+ (
42
+ "lengths",
43
+ structural.sequence_lengths,
44
+ structural.sequence_lengths_per_pid,
45
+ ),
46
+ (feature_weight),
47
+ ("length.", average),
48
+ ("EMD", emd),
49
+ )
50
+ )
51
+ if participations_cfg.get("rates", True):
52
+ participation_rate_jobs.append(
53
+ (
54
+ (
55
+ "participation rate",
56
+ participation.participation_rates_by_act,
57
+ participation.participation_rates_by_act_per_pid,
58
+ ),
59
+ (feature_weight),
60
+ ("av. rate", average),
61
+ ("EMD", emd),
62
+ )
63
+ )
64
+ if participations_cfg.get("pair_rates", True):
65
+ participation_rate_jobs.append(
66
+ (
67
+ (
68
+ "pair participation rate",
69
+ participation.joint_participation_rate,
70
+ participation.joint_participation_rate_per_pid,
71
+ ),
72
+ (feature_weight),
73
+ ("av rate.", average),
74
+ ("EMD", emd),
75
+ )
76
+ )
77
+
78
+ transition_jobs = []
79
+ if transitions_cfg.get("2-gram", True):
80
+ transition_jobs.append(
81
+ (
82
+ (
83
+ "2-gram",
84
+ partial(transitions.transitions_by_act, min_count=ngram_min_count),
85
+ partial(
86
+ transitions.transitions_by_act_per_pid,
87
+ min_count=ngram_min_count,
88
+ ),
89
+ ),
90
+ (feature_weight),
91
+ ("av. rate", average),
92
+ ("EMD", emd),
93
+ )
94
+ )
95
+ if transitions_cfg.get("3-gram", True):
96
+ transition_jobs.append(
97
+ (
98
+ (
99
+ "3-gram",
100
+ partial(
101
+ transitions.transition_3s_by_act, min_count=ngram_min_count
102
+ ),
103
+ partial(
104
+ transitions.transition_3s_by_act_per_pid,
105
+ min_count=ngram_min_count,
106
+ ),
107
+ ),
108
+ (feature_weight),
109
+ ("av. rate", average),
110
+ ("EMD", emd),
111
+ )
112
+ )
113
+ if transitions_cfg.get("4-gram", True):
114
+ transition_jobs.append(
115
+ (
116
+ (
117
+ "4-gram",
118
+ partial(
119
+ transitions.transition_4s_by_act, min_count=ngram_min_count
120
+ ),
121
+ partial(
122
+ transitions.transition_4s_by_act_per_pid,
123
+ min_count=ngram_min_count,
124
+ ),
125
+ ),
126
+ (feature_weight),
127
+ ("av. rate", average),
128
+ ("EMD", emd),
129
+ )
130
+ )
131
+
132
+ time_jobs = []
133
+ if timing_cfg.get("start_times", True):
134
+ time_jobs.append(
135
+ (
136
+ (
137
+ "start times",
138
+ times.start_times_by_act_plan_enum,
139
+ times.start_times_by_act_plan_enum_per_pid,
140
+ ),
141
+ (feature_weight),
142
+ ("average", average),
143
+ ("EMD", emd),
144
+ )
145
+ )
146
+ if timing_cfg.get("durations", True):
147
+ time_jobs.append(
148
+ (
149
+ (
150
+ "durations",
151
+ times.durations_by_act_plan_enum,
152
+ times.durations_by_act_plan_enum_per_pid,
153
+ ),
154
+ (feature_weight),
155
+ ("average", average),
156
+ ("EMD", emd),
157
+ )
158
+ )
159
+ if timing_cfg.get("start_durations", True):
160
+ time_jobs.append(
161
+ (
162
+ (
163
+ "start-durations",
164
+ times.start_and_duration_by_act_bins,
165
+ times.start_and_duration_by_act_bins_per_pid,
166
+ ),
167
+ (feature_weight),
168
+ ("average", average2d),
169
+ ("EMD", emd),
170
+ )
171
+ )
172
+ if timing_cfg.get("joint_durations", True):
173
+ time_jobs.append(
174
+ (
175
+ (
176
+ "joint-durations",
177
+ times.joint_durations_by_act_bins,
178
+ times.joint_durations_by_act_bins_per_pid,
179
+ ),
180
+ (feature_weight),
181
+ ("average", average2d),
182
+ ("EMD", emd),
183
+ )
184
+ )
185
+
186
+ return [
187
+ ("participations", participation_rate_jobs),
188
+ ("transitions", transition_jobs),
189
+ ("timing", time_jobs),
190
+ ]
191
+
192
+
193
+ def build_creativity_jobs(cfg: dict) -> bool:
194
+ """Returns whether to run creativity evaluation."""
195
+ return cfg.get("jobs", {}).get("creativity", {}).get("enabled", True)
196
+
197
+
198
+ def build_structural_jobs(cfg: dict) -> bool:
199
+ """Returns whether to run structural/feasibility evaluation."""
200
+ return cfg.get("jobs", {}).get("structural", {}).get("enabled", True)
201
+
202
+
203
+ def get_jobs(config_path=None):
204
+ """Load config and return active job specification.
205
+
206
+ Returns:
207
+ tuple of (density_domain_jobs, run_creativity, run_structural)
208
+ where density_domain_jobs is [(domain_name, [jobs])].
209
+ """
210
+ cfg = load_config(config_path)
211
+ return (
212
+ build_density_jobs(cfg),
213
+ build_creativity_jobs(cfg),
214
+ build_structural_jobs(cfg),
215
+ )
acteval/ops.py ADDED
@@ -0,0 +1,49 @@
1
+ import numpy as np
2
+ from numpy import ndarray
3
+ from pandas import Series
4
+
5
+
6
+ def actual(features: dict[str, float]) -> Series:
7
+ return Series(features)
8
+
9
+
10
+ def feature_value(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
11
+ return Series({k: v[0] for k, (v, w) in features.items()}, dtype=int)
12
+
13
+
14
+ def feature_length(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
15
+ return Series({k: len(v) for k, (v, w) in features.items()}, dtype=int)
16
+
17
+
18
+ def feature_weight(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
19
+ return Series({k: w.sum() for k, (v, w) in features.items()}, dtype=int)
20
+
21
+
22
+ def average_weight(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
23
+ return Series({k: w.mean() for k, (v, w) in features.items()}, dtype=float)
24
+
25
+
26
+ def average_density(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
27
+ total = sum(w.sum() for _, w in features.values())
28
+ return Series({k: w.sum() / total for k, (v, w) in features.items()}, dtype=float)
29
+
30
+
31
+ def average(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
32
+ weighted_average = {}
33
+ for k, (v, w) in features.items():
34
+ if w.sum() > 0:
35
+ weighted_average[k] = np.average(v, axis=0, weights=w).sum()
36
+ else:
37
+ weighted_average[k] = 0
38
+ return Series(weighted_average, dtype=float)
39
+
40
+
41
+ def average2d(features: dict[str, tuple[ndarray, ndarray]]) -> Series:
42
+ return Series(
43
+ {
44
+ k: np.average(v, axis=0, weights=w).sum().sum()
45
+ for k, (v, w) in features.items()
46
+ if w.sum() > 0
47
+ },
48
+ dtype=float,
49
+ )
acteval/population.py ADDED
@@ -0,0 +1,165 @@
1
+ """Population: precomputed numpy representation of a schedule population DataFrame."""
2
+
3
+ import numpy as np
4
+ from numpy import ndarray
5
+ from pandas import DataFrame
6
+
7
+ from acteval.density.features.utils import _cumcount
8
+
9
+
10
+ class Population:
11
+ """Precomputed numpy representation of a schedule population DataFrame.
12
+
13
+ Converts a DataFrame once and caches all derived quantities to avoid
14
+ redundant computation across feature extraction functions.
15
+
16
+ Eager attributes (always computed):
17
+ acts: activity strings.
18
+ act_codes: integer-encoded activities (0, 1, ...).
19
+ starts: start times.
20
+ ends: end times.
21
+ durations: durations.
22
+ pids: dense integer-encoded pids (0, 1, ...).
23
+ unique_acts: sorted unique activity strings.
24
+ n_act_types: number of unique activity types.
25
+ act_to_int: dict mapping activity string → int code.
26
+ int_to_act: ndarray indexed by int code → activity string.
27
+ n: number of unique persons.
28
+ pid_counts: activity count per pid.
29
+ pid_starts: first row index per pid.
30
+ pid_ends: exclusive end row index per pid.
31
+ pid_boundaries: row indices where pid changes (== pid_starts[1:]).
32
+ first_idx: first row index per pid (== pid_starts).
33
+ last_idx: last row index per pid (== pid_ends - 1).
34
+
35
+ Lazy properties (computed on first access):
36
+ act_enum_key: cumcount within (pid, act), e.g. "home0", "work0", "home1".
37
+ seq_key: cumcount within pid, e.g. "0home", "1work", "2home".
38
+ count_matrix: shape (n, n_act_types) — activity counts per pid.
39
+ """
40
+
41
+ def __init__(self, df: DataFrame):
42
+ """Construct a Population from a schedule DataFrame.
43
+
44
+ Args:
45
+ df: DataFrame with columns pid, act, start, end, duration.
46
+ Rows are assumed sorted by pid; a defensive sort is applied if not.
47
+ The act, start, end, and duration columns are optional — absent
48
+ columns result in empty arrays.
49
+ """
50
+ if len(df) == 0:
51
+ self._init_empty()
52
+ return
53
+
54
+ raw_pids = df["pid"].values
55
+
56
+ # Sort by pid if needed (defensive)
57
+ if len(raw_pids) > 1 and not np.all(raw_pids[:-1] <= raw_pids[1:]):
58
+ order = np.argsort(raw_pids, kind="stable")
59
+ raw_pids = raw_pids[order]
60
+ else:
61
+ order = None
62
+
63
+ def _col(name, default_dtype=float):
64
+ if name not in df.columns:
65
+ return np.empty(0, dtype=default_dtype)
66
+ vals = df[name].values
67
+ return vals[order] if order is not None else vals
68
+
69
+ self.acts = _col("act", object)
70
+ self.starts = _col("start", float)
71
+ self.ends = _col("end", float)
72
+ self.durations = _col("duration", float)
73
+
74
+ # Activity encoding
75
+ if len(self.acts) > 0:
76
+ self.unique_acts, self.act_codes = np.unique(self.acts, return_inverse=True)
77
+ else:
78
+ self.unique_acts = np.array([], dtype=object)
79
+ self.act_codes = np.array([], dtype=np.int64)
80
+ self.n_act_types = len(self.unique_acts)
81
+ self.act_to_int = {a: i for i, a in enumerate(self.unique_acts)}
82
+ self.int_to_act = self.unique_acts # indexed by int code → act string
83
+
84
+ # Pid grouping
85
+ self.unique_pids_original, self.pids, self.pid_counts = np.unique(
86
+ raw_pids, return_inverse=True, return_counts=True
87
+ )
88
+ self.n = len(self.pid_counts)
89
+ self.pid_starts = np.concatenate([[0], np.cumsum(self.pid_counts)[:-1]])
90
+ self.pid_ends = self.pid_starts + self.pid_counts
91
+ self.pid_boundaries = self.pid_starts[1:] # free — no extra computation
92
+ self.first_idx = self.pid_starts
93
+ self.last_idx = self.pid_ends - 1
94
+
95
+ self._lazy_act_enum_key = None
96
+ self._lazy_seq_key = None
97
+ self._lazy_count_matrix = None
98
+
99
+ def _init_empty(self) -> None:
100
+ """Initialise all arrays to empty for an empty DataFrame."""
101
+ self.acts = np.array([], dtype=object)
102
+ self.starts = np.empty(0, dtype=float)
103
+ self.ends = np.empty(0, dtype=float)
104
+ self.durations = np.empty(0, dtype=float)
105
+ self.unique_acts = np.array([], dtype=object)
106
+ self.act_codes = np.array([], dtype=np.int64)
107
+ self.n_act_types = 0
108
+ self.act_to_int = {}
109
+ self.int_to_act = np.array([], dtype=object)
110
+ self.unique_pids_original = np.array([], dtype=object)
111
+ self.pids = np.array([], dtype=np.int64)
112
+ self.pid_counts = np.array([], dtype=np.int64)
113
+ self.n = 0
114
+ self.pid_starts = np.array([], dtype=np.int64)
115
+ self.pid_ends = np.array([], dtype=np.int64)
116
+ self.pid_boundaries = np.array([], dtype=np.int64)
117
+ self.first_idx = np.array([], dtype=np.int64)
118
+ self.last_idx = np.array([], dtype=np.int64)
119
+ self._lazy_act_enum_key = None
120
+ self._lazy_seq_key = None
121
+ self._lazy_count_matrix = None
122
+
123
+ def __len__(self) -> int:
124
+ """Return total number of activity rows."""
125
+ return len(self.acts)
126
+
127
+ def __bool__(self) -> bool:
128
+ """Return False for an empty population."""
129
+ return len(self.acts) > 0
130
+
131
+ @property
132
+ def is_empty(self) -> bool:
133
+ """True when the population contains no rows."""
134
+ return len(self.acts) == 0
135
+
136
+ @property
137
+ def act_enum_key(self) -> ndarray:
138
+ """Cumcount within (pid, act) group, e.g. ["home0", "work0", "home1", ...]."""
139
+ if self._lazy_act_enum_key is None:
140
+ compound = self.pids * self.n_act_types + self.act_codes
141
+ cumcounts = _cumcount(compound)
142
+ self._lazy_act_enum_key = np.array(
143
+ [str(a) + str(c) for a, c in zip(self.acts, cumcounts)], dtype=object
144
+ )
145
+ return self._lazy_act_enum_key
146
+
147
+ @property
148
+ def seq_key(self) -> ndarray:
149
+ """Cumcount within pid group, e.g. ["0home", "1work", "2home", ...]."""
150
+ if self._lazy_seq_key is None:
151
+ cumcounts = _cumcount(self.pids)
152
+ self._lazy_seq_key = np.array(
153
+ [str(c) + str(a) for c, a in zip(cumcounts, self.acts)], dtype=object
154
+ )
155
+ return self._lazy_seq_key
156
+
157
+ @property
158
+ def count_matrix(self) -> ndarray:
159
+ """Activity count matrix, shape (n, n_act_types)."""
160
+ if self._lazy_count_matrix is None:
161
+ matrix = np.zeros((self.n, self.n_act_types), dtype=np.int64)
162
+ if self.n > 0 and self.n_act_types > 0:
163
+ np.add.at(matrix, (self.pids, self.act_codes), 1)
164
+ self._lazy_count_matrix = matrix
165
+ return self._lazy_count_matrix
@@ -0,0 +1,2 @@
1
+ """Structural / feasibility evaluation — checks schedule validity constraints
2
+ (home-based sequences, no consecutive duplicate activities)."""
File without changes
@@ -0,0 +1,210 @@
1
+ from typing import List
2
+
3
+ import numpy as np
4
+ from numpy import array, ndarray
5
+ from pandas import DataFrame, MultiIndex, Series
6
+
7
+ from acteval.density.features.pid_features import PidFeatures
8
+ from acteval.density.features.utils import (
9
+ _grouped_sum,
10
+ weighted_features,
11
+ )
12
+ from acteval.population import Population
13
+
14
+
15
+ def feasibility_eval(population: Population, name: str) -> tuple[Series, Series]:
16
+ index = MultiIndex.from_tuples(
17
+ [
18
+ ("feasibility", "invalid", "all"),
19
+ ("feasibility", "not home based", "all"),
20
+ ("feasibility", "not home based", "starts"),
21
+ ("feasibility", "not home based", "ends"),
22
+ ("feasibility", "consecutive", "all"),
23
+ ("feasibility", "consecutive", "home"),
24
+ ("feasibility", "consecutive", "work"),
25
+ ("feasibility", "consecutive", "education"),
26
+ ],
27
+ names=["domain", "feature", "segment"],
28
+ )
29
+
30
+ if population.is_empty:
31
+ print(f"Warning: {name} has no novel schedules for quality evaluation.")
32
+ weights = Series([0] * len(index), index=index, name=f"{name}__weight")
33
+ metrics = Series([0] * len(index), index=index, name=name)
34
+ return weights, metrics
35
+
36
+ pids = population.pids
37
+ acts = population.acts
38
+ unique_pids = np.arange(population.n)
39
+
40
+ # home based feasibility
41
+ first_acts = acts[population.first_idx]
42
+ last_acts = acts[population.last_idx]
43
+
44
+ not_start_at_home = first_acts != "home"
45
+ not_end_at_home = last_acts != "home"
46
+ not_home_based = not_start_at_home | not_end_at_home
47
+
48
+ # consecutive feasibility
49
+ consecutive_home = _get_consecutives(pids, acts, unique_pids, "home")
50
+ consecutive_work = _get_consecutives(pids, acts, unique_pids, "work")
51
+ consecutive_education = _get_consecutives(pids, acts, unique_pids, "education")
52
+
53
+ consecutive = consecutive_home | consecutive_work | consecutive_education
54
+
55
+ # combined
56
+ invalid = not_home_based | consecutive
57
+
58
+ n = population.n
59
+
60
+ metrics = Series(
61
+ [
62
+ invalid.sum() / n,
63
+ not_home_based.sum() / n,
64
+ not_start_at_home.sum() / n,
65
+ not_end_at_home.sum() / n,
66
+ consecutive.sum() / n,
67
+ consecutive_home.sum() / n,
68
+ consecutive_work.sum() / n,
69
+ consecutive_education.sum() / n,
70
+ ],
71
+ index=index,
72
+ name=name,
73
+ dtype=float,
74
+ )
75
+ weights = Series([n] * len(index), index=index, name=f"{name}__weight", dtype=int)
76
+ return weights, metrics
77
+
78
+
79
+ def _get_consecutives(
80
+ pids: ndarray, acts: ndarray, unique_pids: ndarray, target: str
81
+ ) -> ndarray:
82
+ """Check which pids have consecutive occurrences of target activity.
83
+
84
+ Args:
85
+ pids: 1D array of pid values (in schedule order).
86
+ acts: 1D array of activity values (in schedule order).
87
+ unique_pids: 1D array of unique pid values.
88
+ target: Activity to check for consecutive occurrences.
89
+
90
+ Returns:
91
+ Boolean array of length len(unique_pids), True if pid has consecutive target.
92
+ """
93
+ is_target = acts == target
94
+ # Previous row is same pid and also target
95
+ prev_same_pid = np.empty(len(pids), dtype=bool)
96
+ prev_same_pid[0] = False
97
+ prev_same_pid[1:] = pids[1:] == pids[:-1]
98
+
99
+ prev_is_target = np.empty(len(is_target), dtype=bool)
100
+ prev_is_target[0] = False
101
+ prev_is_target[1:] = is_target[:-1]
102
+
103
+ consecutive = is_target & prev_is_target & prev_same_pid
104
+ if consecutive.any():
105
+ bad_pids = np.unique(pids[consecutive])
106
+ return np.isin(unique_pids, bad_pids)
107
+ return np.zeros(len(unique_pids), dtype=bool)
108
+
109
+
110
+ def get_consecutives(population: DataFrame, act: str) -> Series:
111
+ """Legacy wrapper for backwards compatibility."""
112
+ pids = population.pid.values
113
+ acts = population.act.values
114
+ unique_pids = np.unique(pids)
115
+ result = _get_consecutives(pids, acts, unique_pids, act)
116
+ return Series(result, index=unique_pids, name="consecutive")
117
+
118
+
119
+ def start_and_end_acts(
120
+ population: Population, target: str = "home"
121
+ ) -> dict[str, tuple[ndarray, ndarray]]:
122
+ acts = population.acts
123
+ n = population.n
124
+ first = int((acts[population.first_idx] == target).sum())
125
+ last = int((acts[population.last_idx] == target).sum())
126
+ return {
127
+ f"first act {target}": (array([0, 1]), array([(n - first), first])),
128
+ f"last act {target}": (array([0, 1]), array([(n - last), last])),
129
+ }
130
+
131
+
132
+ def act_consecutive(
133
+ population: Population, targets: List[str] = ["home", "work", "education"]
134
+ ) -> dict[str, tuple[ndarray, ndarray]]:
135
+ pids = population.pids
136
+ acts = population.acts
137
+ unique_pids = np.arange(population.n)
138
+ n = population.n
139
+ result = {}
140
+ for target in targets:
141
+ consecutive_mask = _get_consecutives(pids, acts, unique_pids, target)
142
+ c = int(consecutive_mask.sum())
143
+ result[f"consecutive {target}"] = (
144
+ array([0, 1]),
145
+ array([n - c, c]),
146
+ )
147
+ return result
148
+
149
+
150
+ def contains_consecutive(schedule: DataFrame, act: str) -> int:
151
+ mask = schedule.act.eq(act)
152
+ consecutive = mask == mask.shift(1)
153
+ return (mask & consecutive).sum() > 0
154
+
155
+
156
+ def time_consistency(
157
+ population: Population, target: int = 1440
158
+ ) -> dict[str, tuple[ndarray, ndarray]]:
159
+ pids = population.pids
160
+ n = population.n
161
+
162
+ starts_val = population.starts
163
+ ends_val = population.ends
164
+ durations_val = population.durations
165
+
166
+ starts = int((starts_val[population.first_idx] == 0).sum())
167
+ ends = int((ends_val[population.last_idx] == target).sum())
168
+
169
+ _, dur_sums = _grouped_sum(pids, durations_val)
170
+ duration = int((dur_sums == target).sum())
171
+
172
+ return {
173
+ "starts at 0": (array([0, 1]), array([(n - starts), starts])),
174
+ f"ends at {target}": (array([0, 1]), array([(n - ends), ends])),
175
+ f"duration is {target}": (
176
+ array([0, 1]),
177
+ array([(n - duration), duration]),
178
+ ),
179
+ }
180
+
181
+
182
+ def duration_consistency(
183
+ population: Population, factor: int = 1440
184
+ ) -> dict[str, tuple[ndarray, ndarray]]:
185
+ _, dur_sums = _grouped_sum(population.pids, population.durations)
186
+ return weighted_features({"total duration": dur_sums / factor})
187
+
188
+
189
+ def sequence_lengths(
190
+ population: Population,
191
+ ) -> dict[str, tuple[ndarray, ndarray]]:
192
+ lengths, length_counts = np.unique(population.pid_counts, return_counts=True)
193
+ return {"sequence lengths": (lengths, length_counts)}
194
+
195
+
196
+ def sequence_lengths_per_pid(
197
+ population: Population,
198
+ ) -> PidFeatures:
199
+ pids = np.arange(population.n)
200
+ return PidFeatures(
201
+ data={"sequence lengths": (population.pid_counts, pids)},
202
+ bin_size=None,
203
+ factor=1,
204
+ )
205
+
206
+
207
+ def trip_consistency(
208
+ population: Population,
209
+ ) -> dict[str, tuple[ndarray, ndarray]]:
210
+ raise NotImplementedError
@@ -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'