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.
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ from numpy import arange, ndarray
3
+ from pandas import DataFrame
4
+
5
+ from acteval.density.features._discretise import descretise_population
6
+
7
+
8
+ def count_schedules(population: DataFrame) -> int:
9
+ count = population.pid.nunique()
10
+ return {"all": (np.array([count]), np.array([1]))}
11
+
12
+
13
+ def binned_activity_count(
14
+ population: DataFrame, class_map: dict, duration: int = 1440, step: int = 15
15
+ ) -> ndarray:
16
+ return descretise_population(
17
+ population, duration=duration, step_size=step, class_map=class_map
18
+ ).sum(0)[0, :, :]
19
+
20
+
21
+ def binned_activity_density(
22
+ population: DataFrame, class_map: dict, duration: int = 1440, step: int = 15
23
+ ) -> ndarray:
24
+ return descretise_population(
25
+ population, duration=duration, step_size=step, class_map=class_map
26
+ ).mean(0)[0, :, :]
27
+
28
+
29
+ def activity_frequencies(
30
+ population: DataFrame, duration: int = 1440, step: int = 15
31
+ ) -> dict[str, tuple[ndarray, ndarray]]:
32
+ index_to_acts = {i: a for i, a in enumerate(population.act.unique())}
33
+ class_map = {a: i for i, a in index_to_acts.items()}
34
+ bins = binned_activity_count(
35
+ population=population, class_map=class_map, duration=duration, step=step
36
+ )
37
+ support = arange(0, 1, step / duration)
38
+ freqs = {act: (support, bins[:, i]) for act, i in class_map.items()}
39
+ return freqs
40
+
41
+
42
+ def activity_densities(
43
+ population: DataFrame, duration: int = 1440, step: int = 15
44
+ ) -> dict[str, tuple[ndarray, ndarray]]:
45
+ index_to_acts = {i: a for i, a in enumerate(population.act.unique())}
46
+ class_map = {a: i for i, a in index_to_acts.items()}
47
+ bins = binned_activity_density(
48
+ population=population, class_map=class_map, duration=duration, step=step
49
+ )
50
+ support = arange(0, 1, step / duration)
51
+ return {act: (support, bins[:, i]) for act, i in class_map.items()}
@@ -0,0 +1,192 @@
1
+ import numpy as np
2
+ from numpy import array, ndarray
3
+
4
+ from acteval.density.features.pid_features import PidFeatures
5
+ from acteval.density.features.utils import (
6
+ _count_matrix,
7
+ weighted_features,
8
+ )
9
+ from acteval.population import Population
10
+
11
+
12
+ def participation_prob_by_act(
13
+ population: Population,
14
+ ) -> dict[str, tuple[ndarray, ndarray]]:
15
+ """Calculate the participations by activity for a given population.
16
+
17
+ Args:
18
+ population (Population): The population data.
19
+
20
+ Returns:
21
+ dict[str, tuple[array, array]]: A dictionary containing the participation for each activity.
22
+ """
23
+ matrix = population.count_matrix
24
+ participated = (matrix > 0).sum(axis=0)
25
+ n = population.n
26
+ return {
27
+ act: (array([0, 1]), array([n - participated[j], participated[j]]))
28
+ for j, act in enumerate(population.unique_acts)
29
+ }
30
+
31
+
32
+ def participation_rates(
33
+ population: Population,
34
+ ) -> dict[str, tuple[ndarray, ndarray]]:
35
+ return weighted_features({"all": population.pid_counts})
36
+
37
+
38
+ def participation_rates_by_act(
39
+ population: Population,
40
+ ) -> dict[str, tuple[ndarray, ndarray]]:
41
+ matrix = population.count_matrix
42
+ return weighted_features(
43
+ {act: matrix[:, j] for j, act in enumerate(population.unique_acts)}
44
+ )
45
+
46
+
47
+ def participation_rates_by_seq_act(
48
+ population: Population,
49
+ ) -> dict[str, tuple[ndarray, ndarray]]:
50
+ matrix, _, unique_keys = _count_matrix(population.pids, population.seq_key)
51
+ return weighted_features({k: matrix[:, j] for j, k in enumerate(unique_keys)})
52
+
53
+
54
+ def participation_rates_by_act_enum(
55
+ population: Population,
56
+ ) -> dict[str, tuple[ndarray, ndarray]]:
57
+ matrix, _, unique_keys = _count_matrix(population.pids, population.act_enum_key)
58
+ return weighted_features({k: matrix[:, j] for j, k in enumerate(unique_keys)})
59
+
60
+
61
+ def calc_pair_prob(act_counts, pair):
62
+ a, b = pair
63
+ if a == b:
64
+ return (act_counts[a] > 1).sum()
65
+ return ((act_counts[a] > 0) & (act_counts[b] > 0)).sum()
66
+
67
+
68
+ def calc_pair_rate(act_counts, pair):
69
+ a, b = pair
70
+ if a == b:
71
+ return ((act_counts[a] / 2).astype(int)).value_counts().to_dict()
72
+ return ((act_counts[[a, b]].min(axis=1) / 2).astype(int)).value_counts().to_dict()
73
+
74
+
75
+ def combinations_with_replacement(
76
+ targets: list, length: int, prev_array=[]
77
+ ) -> list[list]:
78
+ """Returns all possible combinations of elements in the input array with replacement,
79
+ where each combination has a length of tuple_length.
80
+
81
+ Args:
82
+ targets (list): The input array to generate combinations from.
83
+ length (int): The length of each combination.
84
+ prev_array (list, optional): The previous array generated in the recursion. Defaults to [].
85
+
86
+ Returns:
87
+ list: A list of all possible combinations of elements in the input array with replacement.
88
+ """
89
+ if len(prev_array) == length:
90
+ return [prev_array]
91
+ combs = []
92
+ for i, val in enumerate(targets):
93
+ prev_array_extended = prev_array.copy()
94
+ prev_array_extended.append(val)
95
+ combs += combinations_with_replacement(targets[i:], length, prev_array_extended)
96
+ return combs
97
+
98
+
99
+ def joint_participation_prob(
100
+ population: Population,
101
+ ) -> dict[str, tuple[ndarray, ndarray]]:
102
+ """Calculate the participation prob for all pairs of activities in the given population.
103
+
104
+ Args:
105
+ population (Population): A Population containing the population data.
106
+
107
+ Returns:
108
+ dict: A dictionary containing the participation probability for all pairs of activities.
109
+ """
110
+ matrix = population.count_matrix
111
+ unique_acts = population.unique_acts
112
+ act_list = list(unique_acts)
113
+ act_idx = {a: i for i, a in enumerate(act_list)}
114
+ n = matrix.shape[0]
115
+ pairs = combinations_with_replacement(act_list, 2)
116
+ metric = {}
117
+ for pair in pairs:
118
+ ai, bi = act_idx[pair[0]], act_idx[pair[1]]
119
+ if pair[0] == pair[1]:
120
+ p = int((matrix[:, ai] > 1).sum())
121
+ else:
122
+ p = int(((matrix[:, ai] > 0) & (matrix[:, bi] > 0)).sum())
123
+ metric["+".join(pair)] = (array([0, 1]), array([n - p, p]))
124
+ return metric
125
+
126
+
127
+ def joint_participation_rate(
128
+ population: Population,
129
+ ) -> dict[str, tuple[ndarray, ndarray]]:
130
+ """Calculate the participation rate for all pairs of activities in the given population.
131
+
132
+ Args:
133
+ population (Population): A Population containing the population data.
134
+
135
+ Returns:
136
+ dict: A dictionary containing the participation rate for all pairs of activities.
137
+ """
138
+ matrix = population.count_matrix
139
+ unique_acts = population.unique_acts
140
+ act_list = list(unique_acts)
141
+ act_idx = {a: i for i, a in enumerate(act_list)}
142
+ pairs = combinations_with_replacement(act_list, 2)
143
+ metric = {}
144
+ for pair in pairs:
145
+ ai, bi = act_idx[pair[0]], act_idx[pair[1]]
146
+ if pair[0] == pair[1]:
147
+ vals = matrix[:, ai] // 2
148
+ else:
149
+ vals = np.minimum(matrix[:, ai], matrix[:, bi]) // 2
150
+ keys, counts = np.unique(vals, return_counts=True)
151
+ metric["+".join(pair)] = (keys, counts)
152
+ return metric
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Per-pid variants — return PidFeatures for efficient subsetting
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ def participation_rates_by_act_per_pid(
161
+ population: Population,
162
+ ) -> PidFeatures:
163
+ """Per-pid participation rates by activity.
164
+
165
+ Each key maps to ``(count_per_person, pids)`` where *pids* is shared
166
+ across all keys (the dense pid range).
167
+ """
168
+ matrix = population.count_matrix
169
+ pids = np.arange(population.n)
170
+ data = {act: (matrix[:, j], pids) for j, act in enumerate(population.unique_acts)}
171
+ return PidFeatures(data=data, bin_size=None, factor=1)
172
+
173
+
174
+ def joint_participation_rate_per_pid(
175
+ population: Population,
176
+ ) -> PidFeatures:
177
+ """Per-pid joint participation rates for all activity pairs."""
178
+ matrix = population.count_matrix
179
+ unique_acts = population.unique_acts
180
+ act_list = list(unique_acts)
181
+ act_idx = {a: i for i, a in enumerate(act_list)}
182
+ pids = np.arange(population.n)
183
+ pairs = combinations_with_replacement(act_list, 2)
184
+ data = {}
185
+ for pair in pairs:
186
+ ai, bi = act_idx[pair[0]], act_idx[pair[1]]
187
+ if pair[0] == pair[1]:
188
+ vals = matrix[:, ai] // 2
189
+ else:
190
+ vals = np.minimum(matrix[:, ai], matrix[:, bi]) // 2
191
+ data["+".join(pair)] = (vals, pids)
192
+ return PidFeatures(data=data, bin_size=None, factor=1)
@@ -0,0 +1,73 @@
1
+ """PidFeatures: pre-compression feature data with pid tracking for efficient subsetting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ import numpy as np
8
+ from numpy import ndarray
9
+
10
+ from acteval.density.features.utils import compress_feature
11
+
12
+
13
+ @dataclass
14
+ class PidFeatures:
15
+ """Per-pid feature data that can be subset and aggregated.
16
+
17
+ Stores raw (pre-compression) values alongside pid arrays so that features
18
+ can be efficiently filtered to a subset of persons and then compressed
19
+ into the standard ``{key: (values, weights)}`` format.
20
+
21
+ Args:
22
+ data: ``{key: (values, pids)}`` where *values* is a 1-D (or 2-D)
23
+ array and *pids* is a same-length array of dense pid ints.
24
+ bin_size: Optional bin size forwarded to ``compress_feature``.
25
+ factor: Divisor forwarded to ``compress_feature``.
26
+ """
27
+
28
+ data: dict[str, tuple[ndarray, ndarray]]
29
+ bin_size: int | None = None
30
+ factor: int = 1
31
+
32
+ # Internal cache: maps ``id(pid_array)`` → mask for a given pid set,
33
+ # so that per-person features sharing one pid array compute the mask once.
34
+ _mask_cache: dict[int, ndarray] = field(default_factory=dict, repr=False)
35
+
36
+ def subset(self, pid_set: ndarray) -> PidFeatures:
37
+ """Return a new PidFeatures containing only values for *pid_set*.
38
+
39
+ Args:
40
+ pid_set: 1-D array of dense pid ints to keep.
41
+ """
42
+ pid_set_unique = np.unique(pid_set)
43
+ new_data: dict[str, tuple[ndarray, ndarray]] = {}
44
+ mask_cache: dict[int, ndarray] = {}
45
+
46
+ for key, (values, pids) in self.data.items():
47
+ pid_id = id(pids)
48
+ if pid_id not in mask_cache:
49
+ mask_cache[pid_id] = np.isin(pids, pid_set_unique)
50
+ mask = mask_cache[pid_id]
51
+ new_data[key] = (values[mask], pids[mask])
52
+
53
+ return PidFeatures(
54
+ data=new_data,
55
+ bin_size=self.bin_size,
56
+ factor=self.factor,
57
+ )
58
+
59
+ def aggregate(self) -> dict[str, tuple[ndarray, ndarray]]:
60
+ """Compress per-pid data into ``{key: (unique_values, counts)}``.
61
+
62
+ Produces identical output to the existing pipeline's
63
+ ``weighted_features`` / ``compress_feature`` calls.
64
+ """
65
+ result: dict[str, tuple[ndarray, ndarray]] = {}
66
+ for key, (values, _pids) in self.data.items():
67
+ if len(values) == 0:
68
+ result[key] = (np.array([]), np.array([], dtype=np.int64))
69
+ else:
70
+ result[key] = compress_feature(
71
+ values, bin_size=self.bin_size, factor=self.factor
72
+ )
73
+ return result
@@ -0,0 +1,171 @@
1
+ import numpy as np
2
+ from numpy import array, ndarray
3
+
4
+ from acteval.density.features.pid_features import PidFeatures
5
+ from acteval.density.features.utils import (
6
+ _collect_by_group,
7
+ _collect_by_group_with_pids,
8
+ weighted_features,
9
+ )
10
+ from acteval.population import Population
11
+
12
+
13
+ def start_times_by_act(
14
+ population: Population, bin_size: int = 15, factor: int = 1440
15
+ ) -> dict[str, tuple[ndarray, ndarray]]:
16
+ features = _collect_by_group(population.acts, population.starts)
17
+ return weighted_features(features, bin_size=bin_size, factor=factor)
18
+
19
+
20
+ def end_times_by_act(
21
+ population: Population, bin_size: int = 15, factor: int = 1440
22
+ ) -> dict[str, tuple[ndarray, ndarray]]:
23
+ features = _collect_by_group(population.acts, population.ends)
24
+ return weighted_features(features, bin_size=bin_size, factor=factor)
25
+
26
+
27
+ def durations_by_act(
28
+ population: Population, bin_size: int = 15, factor: int = 1440
29
+ ) -> dict[str, tuple[ndarray, ndarray]]:
30
+ features = _collect_by_group(population.acts, population.durations)
31
+ return weighted_features(features, bin_size=bin_size, factor=factor)
32
+
33
+
34
+ def start_durations_by_act(population: Population) -> dict[str, ndarray]:
35
+ if population.is_empty:
36
+ return {a: array([]) for a in population.unique_acts}
37
+ pairs = np.column_stack([population.starts, population.durations])
38
+ return _collect_by_group(population.acts, pairs)
39
+
40
+
41
+ def start_and_duration_by_act_bins(
42
+ population: Population, bin_size: int = 15, factor: int = 1440
43
+ ) -> dict[str, tuple[ndarray, ndarray]]:
44
+ features = start_durations_by_act(population)
45
+ return weighted_features(features, bin_size=bin_size, factor=factor)
46
+
47
+
48
+ def joint_durations_by_act_bins(
49
+ population: Population, bin_size: int = 15, factor: int = 1440
50
+ ) -> dict[str, tuple[ndarray, ndarray]]:
51
+ if population.is_empty:
52
+ return {a: array([]) for a in population.unique_acts}
53
+ pids = population.pids
54
+ acts = population.acts.astype(str)
55
+ durations = population.durations.astype(float)
56
+
57
+ # Identify rows that are not the last in their pid group
58
+ same_pid_next = np.empty(len(pids), dtype=bool)
59
+ same_pid_next[:-1] = pids[:-1] == pids[1:]
60
+ same_pid_next[-1] = False
61
+
62
+ # For valid rows, pair current duration with next duration
63
+ valid_acts = acts[same_pid_next]
64
+ valid_durs = durations[same_pid_next]
65
+ next_durs = durations[1:][same_pid_next[:-1]]
66
+ valid_pairs = np.column_stack([valid_durs, next_durs])
67
+
68
+ features = _collect_by_group(valid_acts, valid_pairs)
69
+ return weighted_features(features, bin_size=bin_size, factor=factor)
70
+
71
+
72
+ def start_times_by_act_plan_seq(
73
+ population: Population,
74
+ ) -> dict[str, tuple[ndarray, ndarray]]:
75
+ features = _collect_by_group(population.seq_key, population.starts)
76
+ return weighted_features(features, factor=1440)
77
+
78
+
79
+ def start_times_by_act_plan_enum(
80
+ population: Population,
81
+ ) -> dict[str, tuple[ndarray, ndarray]]:
82
+ features = _collect_by_group(population.act_enum_key, population.starts)
83
+ return weighted_features(features, factor=1440)
84
+
85
+
86
+ def end_times_by_act_plan_seq(
87
+ population: Population,
88
+ ) -> dict[str, tuple[ndarray, ndarray]]:
89
+ features = _collect_by_group(population.seq_key, population.ends)
90
+ return weighted_features(features, factor=1440)
91
+
92
+
93
+ def end_times_by_act_plan_enum(
94
+ population: Population,
95
+ ) -> dict[str, tuple[ndarray, ndarray]]:
96
+ features = _collect_by_group(population.act_enum_key, population.ends)
97
+ return weighted_features(features, factor=1440)
98
+
99
+
100
+ def durations_by_act_plan_seq(
101
+ population: Population,
102
+ ) -> dict[str, tuple[ndarray, ndarray]]:
103
+ features = _collect_by_group(population.seq_key, population.durations)
104
+ return weighted_features(features, factor=1440)
105
+
106
+
107
+ def durations_by_act_plan_enum(
108
+ population: Population,
109
+ ) -> dict[str, tuple[ndarray, ndarray]]:
110
+ features = _collect_by_group(population.act_enum_key, population.durations)
111
+ return weighted_features(features, factor=1440)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Per-pid variants — return PidFeatures for efficient subsetting
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ def start_times_by_act_plan_enum_per_pid(
120
+ population: Population,
121
+ ) -> PidFeatures:
122
+ features = _collect_by_group_with_pids(
123
+ population.act_enum_key, population.starts, population.pids
124
+ )
125
+ return PidFeatures(data=features, bin_size=None, factor=1440)
126
+
127
+
128
+ def durations_by_act_plan_enum_per_pid(
129
+ population: Population,
130
+ ) -> PidFeatures:
131
+ features = _collect_by_group_with_pids(
132
+ population.act_enum_key, population.durations, population.pids
133
+ )
134
+ return PidFeatures(data=features, bin_size=None, factor=1440)
135
+
136
+
137
+ def start_and_duration_by_act_bins_per_pid(
138
+ population: Population,
139
+ bin_size: int = 15,
140
+ factor: int = 1440,
141
+ ) -> PidFeatures:
142
+ if population.is_empty:
143
+ return PidFeatures(data={}, bin_size=bin_size, factor=factor)
144
+ pairs = np.column_stack([population.starts, population.durations])
145
+ features = _collect_by_group_with_pids(population.acts, pairs, population.pids)
146
+ return PidFeatures(data=features, bin_size=bin_size, factor=factor)
147
+
148
+
149
+ def joint_durations_by_act_bins_per_pid(
150
+ population: Population,
151
+ bin_size: int = 15,
152
+ factor: int = 1440,
153
+ ) -> PidFeatures:
154
+ if population.is_empty:
155
+ return PidFeatures(data={}, bin_size=bin_size, factor=factor)
156
+ pids = population.pids
157
+ acts = population.acts.astype(str)
158
+ durations = population.durations.astype(float)
159
+
160
+ same_pid_next = np.empty(len(pids), dtype=bool)
161
+ same_pid_next[:-1] = pids[:-1] == pids[1:]
162
+ same_pid_next[-1] = False
163
+
164
+ valid_acts = acts[same_pid_next]
165
+ valid_pids = pids[same_pid_next]
166
+ valid_durs = durations[same_pid_next]
167
+ next_durs = durations[1:][same_pid_next[:-1]]
168
+ valid_pairs = np.column_stack([valid_durs, next_durs])
169
+
170
+ features = _collect_by_group_with_pids(valid_acts, valid_pairs, valid_pids)
171
+ return PidFeatures(data=features, bin_size=bin_size, factor=factor)