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/__init__.py +10 -0
- acteval/config.toml +24 -0
- acteval/creativity/__init__.py +2 -0
- acteval/creativity/features/__init__.py +0 -0
- acteval/creativity/features/creativity.py +88 -0
- acteval/density/__init__.py +2 -0
- acteval/density/describe/__init__.py +0 -0
- acteval/density/describe/frequency.py +101 -0
- acteval/density/describe/times.py +269 -0
- acteval/density/describe/transitions.py +93 -0
- acteval/density/features/__init__.py +0 -0
- acteval/density/features/_discretise.py +97 -0
- acteval/density/features/frequency.py +51 -0
- acteval/density/features/participation.py +192 -0
- acteval/density/features/pid_features.py +73 -0
- acteval/density/features/times.py +171 -0
- acteval/density/features/transitions.py +241 -0
- acteval/density/features/utils.py +197 -0
- acteval/distance/__init__.py +16 -0
- acteval/distance/scalar.py +68 -0
- acteval/distance/wasserstein.py +200 -0
- acteval/evaluate.py +811 -0
- acteval/filters.py +16 -0
- acteval/jobs.py +215 -0
- acteval/ops.py +49 -0
- acteval/population.py +165 -0
- acteval/structural/__init__.py +2 -0
- acteval/structural/features/__init__.py +0 -0
- acteval/structural/features/structural.py +210 -0
- acteval-0.1.0.dist-info/METADATA +12 -0
- acteval-0.1.0.dist-info/RECORD +32 -0
- acteval-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy import ndarray
|
|
3
|
+
from pandas import DataFrame, MultiIndex, Series
|
|
4
|
+
|
|
5
|
+
from acteval.density.features.pid_features import PidFeatures
|
|
6
|
+
from acteval.density.features.utils import weighted_features
|
|
7
|
+
from acteval.population import Population
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _build_ngrams(
|
|
11
|
+
population: Population, n: int, min_count: int = 0
|
|
12
|
+
) -> dict[str, tuple[ndarray, ndarray]]:
|
|
13
|
+
"""Build n-gram transition features from a Population.
|
|
14
|
+
|
|
15
|
+
Uses integer encoding to avoid string operations in the inner loop.
|
|
16
|
+
Activity codes are packed into a single integer per n-gram, then
|
|
17
|
+
converted back to string labels at the end.
|
|
18
|
+
"""
|
|
19
|
+
codes = population.act_codes
|
|
20
|
+
pids = population.pids
|
|
21
|
+
base = population.n_act_types
|
|
22
|
+
|
|
23
|
+
# Build integer-encoded n-grams using vectorised arithmetic
|
|
24
|
+
# Each n-gram is encoded as code[0]*base^(n-1) + code[1]*base^(n-2) + ... + code[n-1]
|
|
25
|
+
powers = base ** np.arange(n - 1, -1, -1)
|
|
26
|
+
ngram_codes = np.zeros(len(codes), dtype=np.int64)
|
|
27
|
+
for i in range(n):
|
|
28
|
+
shifted = np.roll(codes, -i)
|
|
29
|
+
ngram_codes += shifted * powers[i]
|
|
30
|
+
|
|
31
|
+
# Identify valid positions (not crossing pid boundaries)
|
|
32
|
+
valid_mask = np.ones(len(codes), dtype=bool)
|
|
33
|
+
# Exclude trailing positions within last pid
|
|
34
|
+
if n > 1:
|
|
35
|
+
valid_mask[-(n - 1) :] = False
|
|
36
|
+
# Mark positions near pid boundaries
|
|
37
|
+
pid_boundaries = population.pid_boundaries
|
|
38
|
+
if len(pid_boundaries) > 0:
|
|
39
|
+
for offset in range(-(n - 1), 0):
|
|
40
|
+
positions = pid_boundaries + offset
|
|
41
|
+
positions = positions[(positions >= 0) & (positions < len(codes))]
|
|
42
|
+
valid_mask[positions] = False
|
|
43
|
+
|
|
44
|
+
valid_ngrams = ngram_codes[valid_mask]
|
|
45
|
+
valid_pids = pids[valid_mask]
|
|
46
|
+
|
|
47
|
+
# Early return if no valid n-grams
|
|
48
|
+
if len(valid_ngrams) == 0:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
# Count n-grams per pid using numpy
|
|
52
|
+
unique_ngrams, ngram_indices = np.unique(valid_ngrams, return_inverse=True)
|
|
53
|
+
unique_pids, pid_indices = np.unique(valid_pids, return_inverse=True)
|
|
54
|
+
|
|
55
|
+
# Build count matrix (pids x ngrams)
|
|
56
|
+
count_matrix = np.zeros((len(unique_pids), len(unique_ngrams)), dtype=int)
|
|
57
|
+
np.add.at(count_matrix, (pid_indices, ngram_indices), 1)
|
|
58
|
+
|
|
59
|
+
# Filter rare n-grams
|
|
60
|
+
if min_count > 0:
|
|
61
|
+
col_totals = count_matrix.sum(axis=0)
|
|
62
|
+
keep = col_totals >= min_count
|
|
63
|
+
count_matrix = count_matrix[:, keep]
|
|
64
|
+
unique_ngrams = unique_ngrams[keep]
|
|
65
|
+
|
|
66
|
+
# Decode integer n-grams back to string labels
|
|
67
|
+
int_to_act = population.int_to_act
|
|
68
|
+
|
|
69
|
+
def _decode_ngram(code):
|
|
70
|
+
labels = []
|
|
71
|
+
for i in range(n):
|
|
72
|
+
labels.append(int_to_act[code // powers[i]])
|
|
73
|
+
code %= powers[i]
|
|
74
|
+
return ">".join(str(label) for label in labels)
|
|
75
|
+
|
|
76
|
+
# Build result dict with pid counts as lists (matching original format)
|
|
77
|
+
# todo: maybe don't need these by pid?
|
|
78
|
+
result = {}
|
|
79
|
+
for j, ng_code in enumerate(unique_ngrams):
|
|
80
|
+
label = _decode_ngram(ng_code)
|
|
81
|
+
result[label] = count_matrix[:, j].tolist()
|
|
82
|
+
|
|
83
|
+
return weighted_features(result)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def transitions_by_act(
|
|
87
|
+
population: Population, min_count: int = 0
|
|
88
|
+
) -> dict[str, tuple[ndarray, ndarray]]:
|
|
89
|
+
return _build_ngrams(population, 2, min_count=min_count)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def transition_3s_by_act(
|
|
93
|
+
population: Population, min_count: int = 0
|
|
94
|
+
) -> dict[str, tuple[ndarray, ndarray]]:
|
|
95
|
+
return _build_ngrams(population, 3, min_count=min_count)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def transition_4s_by_act(
|
|
99
|
+
population: Population, min_count: int = 0
|
|
100
|
+
) -> dict[str, tuple[ndarray, ndarray]]:
|
|
101
|
+
return _build_ngrams(population, 4, min_count=min_count)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_ngrams_per_pid(
|
|
105
|
+
population: Population, n: int, min_count: int = 0
|
|
106
|
+
) -> PidFeatures:
|
|
107
|
+
"""Build per-pid n-gram transition features returning PidFeatures.
|
|
108
|
+
|
|
109
|
+
Uses the same integer-encoding logic as ``_build_ngrams`` but keeps
|
|
110
|
+
per-person counts with pid tracking instead of compressing immediately.
|
|
111
|
+
``min_count`` filtering is applied on the full population to keep a
|
|
112
|
+
consistent feature space across subsets.
|
|
113
|
+
"""
|
|
114
|
+
codes = population.act_codes
|
|
115
|
+
pids = population.pids
|
|
116
|
+
base = population.n_act_types
|
|
117
|
+
|
|
118
|
+
powers = base ** np.arange(n - 1, -1, -1)
|
|
119
|
+
ngram_codes = np.zeros(len(codes), dtype=np.int64)
|
|
120
|
+
for i in range(n):
|
|
121
|
+
shifted = np.roll(codes, -i)
|
|
122
|
+
ngram_codes += shifted * powers[i]
|
|
123
|
+
|
|
124
|
+
valid_mask = np.ones(len(codes), dtype=bool)
|
|
125
|
+
if n > 1:
|
|
126
|
+
valid_mask[-(n - 1) :] = False
|
|
127
|
+
pid_boundaries = population.pid_boundaries
|
|
128
|
+
if len(pid_boundaries) > 0:
|
|
129
|
+
for offset in range(-(n - 1), 0):
|
|
130
|
+
positions = pid_boundaries + offset
|
|
131
|
+
positions = positions[(positions >= 0) & (positions < len(codes))]
|
|
132
|
+
valid_mask[positions] = False
|
|
133
|
+
|
|
134
|
+
valid_ngrams = ngram_codes[valid_mask]
|
|
135
|
+
valid_pids = pids[valid_mask]
|
|
136
|
+
|
|
137
|
+
if len(valid_ngrams) == 0:
|
|
138
|
+
return PidFeatures(data={}, bin_size=None, factor=1)
|
|
139
|
+
|
|
140
|
+
unique_ngrams, ngram_indices = np.unique(valid_ngrams, return_inverse=True)
|
|
141
|
+
unique_pids, pid_indices = np.unique(valid_pids, return_inverse=True)
|
|
142
|
+
|
|
143
|
+
count_matrix = np.zeros((len(unique_pids), len(unique_ngrams)), dtype=int)
|
|
144
|
+
np.add.at(count_matrix, (pid_indices, ngram_indices), 1)
|
|
145
|
+
|
|
146
|
+
if min_count > 0:
|
|
147
|
+
col_totals = count_matrix.sum(axis=0)
|
|
148
|
+
keep = col_totals >= min_count
|
|
149
|
+
count_matrix = count_matrix[:, keep]
|
|
150
|
+
unique_ngrams = unique_ngrams[keep]
|
|
151
|
+
|
|
152
|
+
int_to_act = population.int_to_act
|
|
153
|
+
|
|
154
|
+
def _decode_ngram(code):
|
|
155
|
+
labels = []
|
|
156
|
+
for i in range(n):
|
|
157
|
+
labels.append(int_to_act[code // powers[i]])
|
|
158
|
+
code %= powers[i]
|
|
159
|
+
return ">".join(str(label) for label in labels)
|
|
160
|
+
|
|
161
|
+
# Build PidFeatures: each key maps to (per-person counts, pids)
|
|
162
|
+
# All keys share the same pid array for efficient mask caching.
|
|
163
|
+
result_data: dict[str, tuple[ndarray, ndarray]] = {}
|
|
164
|
+
for j, ng_code in enumerate(unique_ngrams):
|
|
165
|
+
label = _decode_ngram(ng_code)
|
|
166
|
+
result_data[label] = (count_matrix[:, j], unique_pids)
|
|
167
|
+
|
|
168
|
+
return PidFeatures(data=result_data, bin_size=None, factor=1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def transitions_by_act_per_pid(
|
|
172
|
+
population: Population, min_count: int = 0
|
|
173
|
+
) -> PidFeatures:
|
|
174
|
+
return _build_ngrams_per_pid(population, 2, min_count=min_count)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def transition_3s_by_act_per_pid(
|
|
178
|
+
population: Population, min_count: int = 0
|
|
179
|
+
) -> PidFeatures:
|
|
180
|
+
return _build_ngrams_per_pid(population, 3, min_count=min_count)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def transition_4s_by_act_per_pid(
|
|
184
|
+
population: Population, min_count: int = 0
|
|
185
|
+
) -> PidFeatures:
|
|
186
|
+
return _build_ngrams_per_pid(population, 4, min_count=min_count)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def tour(acts: Series) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Extracts the tour from the given Series of activities.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
acts (Series): A Series containing the activities.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
str: A string representation of the tour.
|
|
198
|
+
"""
|
|
199
|
+
return ">".join(acts.str[0])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def full_sequences(population: DataFrame) -> dict[str, tuple[ndarray, ndarray]]:
|
|
203
|
+
transitions = population.reset_index()
|
|
204
|
+
transitions = transitions.set_index(["index", "pid"])
|
|
205
|
+
transitions.act = transitions.act.astype(str)
|
|
206
|
+
transitions = transitions.groupby("pid").act.apply(tour)
|
|
207
|
+
transitions = (
|
|
208
|
+
transitions.groupby("pid")
|
|
209
|
+
.value_counts()
|
|
210
|
+
.unstack()
|
|
211
|
+
.fillna(0)
|
|
212
|
+
.astype(int)
|
|
213
|
+
.to_dict(orient="list")
|
|
214
|
+
)
|
|
215
|
+
return weighted_features(transitions)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def collect_sequence(acts: Series) -> str:
|
|
219
|
+
return ">".join(acts)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def sequence_probs(population: DataFrame) -> DataFrame:
|
|
223
|
+
"""
|
|
224
|
+
Calculates the sequence probabilities in the given population DataFrame.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
population (DataFrame): A DataFrame containing the population data.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
DataFrame: A DataFrame containing the probability of each sequence.
|
|
231
|
+
"""
|
|
232
|
+
metrics = (
|
|
233
|
+
population.groupby("pid")
|
|
234
|
+
.act.apply(collect_sequence)
|
|
235
|
+
.value_counts(normalize=True)
|
|
236
|
+
)
|
|
237
|
+
metrics = metrics.sort_values(ascending=False)
|
|
238
|
+
metrics.index = MultiIndex.from_tuples(
|
|
239
|
+
[("sequence rate", acts) for acts in metrics.index]
|
|
240
|
+
)
|
|
241
|
+
return metrics
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy import array, ndarray, unique
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _collect_by_group(keys, values):
|
|
8
|
+
"""Group values by keys into a dict of arrays.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
keys: 1D array of group keys.
|
|
12
|
+
values: 1D or 2D array of values to group.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
dict mapping each unique key to the corresponding array of values.
|
|
16
|
+
"""
|
|
17
|
+
order = np.argsort(keys, kind="stable")
|
|
18
|
+
sorted_keys = keys[order]
|
|
19
|
+
sorted_vals = values[order]
|
|
20
|
+
unique_keys, start_idx = np.unique(sorted_keys, return_index=True)
|
|
21
|
+
splits = np.split(sorted_vals, start_idx[1:])
|
|
22
|
+
return {k: v for k, v in zip(unique_keys, splits)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _collect_by_group_with_pids(keys, values, pids):
|
|
26
|
+
"""Group values by keys, tracking which pid each value belongs to.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
keys: 1D array of group keys.
|
|
30
|
+
values: 1D or 2D array of values to group.
|
|
31
|
+
pids: 1D array of pid ints (same length as keys/values).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict mapping each unique key to ``(grouped_values, grouped_pids)``.
|
|
35
|
+
"""
|
|
36
|
+
order = np.argsort(keys, kind="stable")
|
|
37
|
+
sorted_keys = keys[order]
|
|
38
|
+
sorted_vals = values[order]
|
|
39
|
+
sorted_pids = pids[order]
|
|
40
|
+
unique_keys, start_idx = np.unique(sorted_keys, return_index=True)
|
|
41
|
+
val_splits = np.split(sorted_vals, start_idx[1:])
|
|
42
|
+
pid_splits = np.split(sorted_pids, start_idx[1:])
|
|
43
|
+
return {k: (v, p) for k, v, p in zip(unique_keys, val_splits, pid_splits)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _count_matrix(row_keys, col_keys):
|
|
47
|
+
"""Build a count matrix (crosstab) from row and column key arrays.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
row_keys: 1D array of row group keys.
|
|
51
|
+
col_keys: 1D array of column group keys.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
tuple of (matrix, unique_rows, unique_cols).
|
|
55
|
+
"""
|
|
56
|
+
unique_rows, row_codes = np.unique(row_keys, return_inverse=True)
|
|
57
|
+
unique_cols, col_codes = np.unique(col_keys, return_inverse=True)
|
|
58
|
+
matrix = np.zeros((len(unique_rows), len(unique_cols)), dtype=np.int64)
|
|
59
|
+
np.add.at(matrix, (row_codes, col_codes), 1)
|
|
60
|
+
return matrix, unique_rows, unique_cols
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cumcount(group_keys):
|
|
64
|
+
"""Compute cumulative count within each group (like groupby().cumcount()).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
group_keys: 1D array of group keys.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
1D int array with the within-group ordinal for each element.
|
|
71
|
+
"""
|
|
72
|
+
n = len(group_keys)
|
|
73
|
+
order = np.argsort(group_keys, kind="stable")
|
|
74
|
+
sorted_keys = group_keys[order]
|
|
75
|
+
# Find group start positions in the sorted array
|
|
76
|
+
change_positions = np.concatenate(
|
|
77
|
+
[[0], np.where(sorted_keys[1:] != sorted_keys[:-1])[0] + 1]
|
|
78
|
+
)
|
|
79
|
+
counts = np.diff(np.concatenate([change_positions, [n]]))
|
|
80
|
+
group_starts = np.repeat(change_positions, counts)
|
|
81
|
+
sorted_cumcounts = np.arange(n) - group_starts
|
|
82
|
+
# Map back to original order
|
|
83
|
+
result = np.empty(n, dtype=np.int64)
|
|
84
|
+
result[order] = sorted_cumcounts
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _grouped_sum(keys, values):
|
|
89
|
+
"""Sum values per group key.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
keys: 1D array of group keys.
|
|
93
|
+
values: 1D numeric array.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
tuple of (unique_keys, sums).
|
|
97
|
+
"""
|
|
98
|
+
order = np.argsort(keys, kind="stable")
|
|
99
|
+
sorted_keys = keys[order]
|
|
100
|
+
sorted_vals = values[order]
|
|
101
|
+
unique_keys, start_idx = np.unique(sorted_keys, return_index=True)
|
|
102
|
+
sums = np.add.reduceat(sorted_vals, start_idx)
|
|
103
|
+
return unique_keys, sums
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _first_last_per_group(keys):
|
|
107
|
+
"""Find the original indices of the first and last element per group.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
keys: 1D array of group keys.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
tuple of (unique_keys, first_indices, last_indices) where indices
|
|
114
|
+
refer to positions in the original array.
|
|
115
|
+
"""
|
|
116
|
+
order = np.argsort(keys, kind="stable")
|
|
117
|
+
sorted_keys = keys[order]
|
|
118
|
+
unique_keys, start_idx = np.unique(sorted_keys, return_index=True)
|
|
119
|
+
end_idx = np.empty_like(start_idx)
|
|
120
|
+
end_idx[:-1] = start_idx[1:] - 1
|
|
121
|
+
end_idx[-1] = len(keys) - 1
|
|
122
|
+
return unique_keys, order[start_idx], order[end_idx]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def equals(
|
|
126
|
+
a: dict[str, tuple[ndarray, ndarray]], b: dict[str, tuple[ndarray, ndarray]]
|
|
127
|
+
) -> bool:
|
|
128
|
+
if set(a.keys()) != set(b.keys()):
|
|
129
|
+
return False
|
|
130
|
+
for k in a.keys():
|
|
131
|
+
if not len(a[k][0]) == len(b[k][0]):
|
|
132
|
+
return False
|
|
133
|
+
if not len(a[k][1]) == len(b[k][1]):
|
|
134
|
+
return False
|
|
135
|
+
if not (a[k][0] == b[k][0]).all():
|
|
136
|
+
return False
|
|
137
|
+
if not (a[k][1] == b[k][1]).all():
|
|
138
|
+
return False
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def bin_values(values: array, bin_size: Union[int, float]) -> ndarray:
|
|
143
|
+
"""
|
|
144
|
+
Bins the input values based on the given bin size.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
values (array): Input values to be binned.
|
|
148
|
+
bin_size (int, float): Size of each bin.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
array: Binned values.
|
|
152
|
+
"""
|
|
153
|
+
return (values // bin_size * bin_size) + (bin_size / 2)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def compress_feature(
|
|
157
|
+
feature: list, bin_size: Optional[int] = None, factor: int = 1440
|
|
158
|
+
) -> tuple[ndarray, ndarray]:
|
|
159
|
+
"""
|
|
160
|
+
Compresses a feature by optionally binning its values and returning unique values with counts.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
feature (list): The feature to compress.
|
|
164
|
+
bin_size (int, optional): The size of each bin. If None, no binning is performed.
|
|
165
|
+
factor (int): Factor to apply to convert output values.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
tuple: A tuple containing two arrays and the total weight. The first array contains the unique
|
|
169
|
+
values, and the second array contains the counts of each value.
|
|
170
|
+
"""
|
|
171
|
+
s = array(feature)
|
|
172
|
+
if bin_size is not None:
|
|
173
|
+
s = bin_values(s, bin_size)
|
|
174
|
+
ks, ws = unique(s, axis=0, return_counts=True)
|
|
175
|
+
ks = ks / factor
|
|
176
|
+
return ks, ws
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def weighted_features(
|
|
180
|
+
features: dict[str, ndarray],
|
|
181
|
+
bin_size: Optional[int] = None,
|
|
182
|
+
factor: int = 1,
|
|
183
|
+
) -> dict[str, tuple[ndarray, ndarray]]:
|
|
184
|
+
"""
|
|
185
|
+
Apply optional binning and value counting to dictionary of features.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
features (dict[array): A dictionary of features to compress.
|
|
189
|
+
bin_size (Optional[int]): The size of the bin to use for compression. Defaults to None.
|
|
190
|
+
factor (int): Factor to apply to convert output values.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
dict[str, tuple[array, array[int]]]: A dictionary of features and weights.
|
|
194
|
+
"""
|
|
195
|
+
return {
|
|
196
|
+
k: compress_feature(values, bin_size, factor) for k, values in features.items()
|
|
197
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from acteval.distance.scalar import (
|
|
2
|
+
abs_av_diff as abs_av_diff,
|
|
3
|
+
)
|
|
4
|
+
from acteval.distance.scalar import (
|
|
5
|
+
mae as mae,
|
|
6
|
+
)
|
|
7
|
+
from acteval.distance.scalar import (
|
|
8
|
+
mape as mape,
|
|
9
|
+
)
|
|
10
|
+
from acteval.distance.scalar import (
|
|
11
|
+
mape_scalar as mape_scalar,
|
|
12
|
+
)
|
|
13
|
+
from acteval.distance.scalar import (
|
|
14
|
+
mse as mse,
|
|
15
|
+
)
|
|
16
|
+
from acteval.distance.wasserstein import emd as emd
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mape(a: tuple[np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray]) -> float:
|
|
5
|
+
"""Calculate mean average percentage error between distributions a and b.
|
|
6
|
+
|
|
7
|
+
Clipped at 1.0.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
a (tuple[np.ndarray, np.ndarray]): Distribution a.
|
|
11
|
+
b (tuple[np.ndarray, np.ndarray]): Distribution b.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
float: MAPE.
|
|
15
|
+
"""
|
|
16
|
+
# TODO test this
|
|
17
|
+
# unpack
|
|
18
|
+
ak, aw = a
|
|
19
|
+
bk, bw = b
|
|
20
|
+
# calc weighted average
|
|
21
|
+
akw = (ak * aw).sum() / aw.sum()
|
|
22
|
+
bkw = (bk * bw).sum() / bw.sum()
|
|
23
|
+
diff = np.abs(akw - bkw)
|
|
24
|
+
if diff == 0:
|
|
25
|
+
return 0.0
|
|
26
|
+
if bkw == 0:
|
|
27
|
+
return clamp(diff / akw)
|
|
28
|
+
return clamp(diff / bkw)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clamp(x):
|
|
32
|
+
if x > 1.0:
|
|
33
|
+
return 1.0
|
|
34
|
+
return x
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mape_scalar(a, b):
|
|
38
|
+
return np.abs((a - b) / a).mean()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mse(a: tuple[np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray]) -> float:
|
|
42
|
+
# requires and b have same support.
|
|
43
|
+
# unpack
|
|
44
|
+
_, aw = a
|
|
45
|
+
_, bw = b
|
|
46
|
+
return ((aw - bw) ** 2).mean()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def mae(a: tuple[np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray]) -> float:
|
|
50
|
+
# TODO test this
|
|
51
|
+
# requires and b have same support.
|
|
52
|
+
# unpack
|
|
53
|
+
_, aw = a
|
|
54
|
+
_, bw = b
|
|
55
|
+
return (np.abs(aw - bw)).mean()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def abs_av_diff(
|
|
59
|
+
a: tuple[np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray]
|
|
60
|
+
) -> float:
|
|
61
|
+
# TODO test this
|
|
62
|
+
# unpack
|
|
63
|
+
ak, aw = a
|
|
64
|
+
bk, bw = b
|
|
65
|
+
a_average = (ak * aw).sum() / aw.sum()
|
|
66
|
+
b_average = (bk * bw).sum() / bw.sum()
|
|
67
|
+
|
|
68
|
+
return np.abs(a_average - b_average)
|