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
acteval/__init__.py
ADDED
acteval/config.toml
ADDED
|
@@ -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
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from matplotlib import colormaps, patches
|
|
4
|
+
from matplotlib import pyplot as plt
|
|
5
|
+
from matplotlib.figure import Axes, Figure
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def times_distributions_plot(
|
|
10
|
+
observed: DataFrame, ys: Optional[dict[str, DataFrame]], **kwargs
|
|
11
|
+
) -> Figure:
|
|
12
|
+
ratios = [1 for _ in range(4)]
|
|
13
|
+
ratios[0] = 0.2
|
|
14
|
+
fig, axs = plt.subplots(
|
|
15
|
+
4,
|
|
16
|
+
observed.act.nunique(),
|
|
17
|
+
figsize=kwargs.pop("figsize", (12, 5)),
|
|
18
|
+
sharex=True,
|
|
19
|
+
sharey=False,
|
|
20
|
+
# tight_layout=True,
|
|
21
|
+
constrained_layout=True,
|
|
22
|
+
gridspec_kw={"height_ratios": ratios},
|
|
23
|
+
)
|
|
24
|
+
acts = list(observed.act.value_counts(ascending=False).index)
|
|
25
|
+
name = kwargs.pop("observed_title", "Observed")
|
|
26
|
+
_times_plot(name, observed, acts, axs=axs)
|
|
27
|
+
if ys is None:
|
|
28
|
+
return fig
|
|
29
|
+
for name, y in ys.items():
|
|
30
|
+
_times_plot(name, y, acts, axs=axs)
|
|
31
|
+
for ax in axs[0]:
|
|
32
|
+
ax.tick_params(axis="x", which="both", length=0.0)
|
|
33
|
+
handles, labels = axs[1][0].get_legend_handles_labels()
|
|
34
|
+
fig.legend(
|
|
35
|
+
handles,
|
|
36
|
+
labels,
|
|
37
|
+
loc="upper center",
|
|
38
|
+
fontsize=9,
|
|
39
|
+
ncol=len(ys) + 1,
|
|
40
|
+
frameon=False,
|
|
41
|
+
)
|
|
42
|
+
return fig
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _times_plot(
|
|
46
|
+
name: str,
|
|
47
|
+
population: DataFrame,
|
|
48
|
+
acts: list[str],
|
|
49
|
+
axs: Axes,
|
|
50
|
+
xmin: int = 0,
|
|
51
|
+
xmax: int = 1440,
|
|
52
|
+
step: int = 30,
|
|
53
|
+
**kwargs,
|
|
54
|
+
) -> Tuple[Figure, Axes]:
|
|
55
|
+
starts = population.groupby("act", observed=False).start
|
|
56
|
+
ends = population.groupby("act", observed=False).end
|
|
57
|
+
durations = population.groupby("act", observed=False).duration
|
|
58
|
+
bins = list(range(xmin, xmax, step))
|
|
59
|
+
|
|
60
|
+
for i, act in enumerate(acts):
|
|
61
|
+
if act not in population.act.unique():
|
|
62
|
+
continue
|
|
63
|
+
axs[0][i].spines["top"].set_visible(False)
|
|
64
|
+
axs[0][i].spines["right"].set_visible(False)
|
|
65
|
+
axs[0][i].spines["bottom"].set_visible(False)
|
|
66
|
+
axs[0][i].spines["left"].set_visible(False)
|
|
67
|
+
axs[0][i].set_xticks([])
|
|
68
|
+
axs[0][i].set_yticks([])
|
|
69
|
+
|
|
70
|
+
axs[1][i].set_title(act.title(), fontsize="small")
|
|
71
|
+
start_group = starts.get_group(act)
|
|
72
|
+
if len(start_group) < 5:
|
|
73
|
+
continue
|
|
74
|
+
axs[1][i].hist(
|
|
75
|
+
starts.get_group(act),
|
|
76
|
+
bins=bins,
|
|
77
|
+
density=True,
|
|
78
|
+
histtype="step",
|
|
79
|
+
label=name,
|
|
80
|
+
linewidth=1.4,
|
|
81
|
+
**kwargs,
|
|
82
|
+
)
|
|
83
|
+
axs[1][i].set_xlim(xmin, xmax)
|
|
84
|
+
axs[1][i].set_yticklabels([])
|
|
85
|
+
axs[1][i].set(ylabel=None)
|
|
86
|
+
axs[1][i].set_xticks([])
|
|
87
|
+
axs[1][i].set_yticks([])
|
|
88
|
+
|
|
89
|
+
axs[2][i].hist(
|
|
90
|
+
ends.get_group(act),
|
|
91
|
+
bins=bins,
|
|
92
|
+
density=True,
|
|
93
|
+
histtype="step",
|
|
94
|
+
label=name,
|
|
95
|
+
linewidth=1.4,
|
|
96
|
+
**kwargs,
|
|
97
|
+
)
|
|
98
|
+
axs[2][i].set_xlim(xmin, xmax)
|
|
99
|
+
axs[2][i].set_yticklabels([])
|
|
100
|
+
axs[2][i].set(ylabel=None)
|
|
101
|
+
axs[2][i].set_xticks([])
|
|
102
|
+
axs[2][i].set_yticks([])
|
|
103
|
+
|
|
104
|
+
axs[3][i].hist(
|
|
105
|
+
durations.get_group(act),
|
|
106
|
+
bins=bins,
|
|
107
|
+
density=True,
|
|
108
|
+
histtype="step",
|
|
109
|
+
label=name,
|
|
110
|
+
linewidth=1.4,
|
|
111
|
+
**kwargs,
|
|
112
|
+
)
|
|
113
|
+
axs[3][i].set_xlim(xmin, xmax)
|
|
114
|
+
axs[3][i].set_yticklabels([])
|
|
115
|
+
axs[3][i].set(ylabel=None)
|
|
116
|
+
axs[3][i].set_xticks(
|
|
117
|
+
[0, 240, 480, 720, 960, 1200, 1440],
|
|
118
|
+
labels=[
|
|
119
|
+
"00:00",
|
|
120
|
+
"04:00",
|
|
121
|
+
"08:00",
|
|
122
|
+
"12:00",
|
|
123
|
+
"16:00",
|
|
124
|
+
"20:00",
|
|
125
|
+
"24:00",
|
|
126
|
+
],
|
|
127
|
+
rotation=90,
|
|
128
|
+
fontsize=8,
|
|
129
|
+
)
|
|
130
|
+
axs[3][i].set_yticks([])
|
|
131
|
+
axs[3][i].set_xlabel("Time/Duration", fontsize=8)
|
|
132
|
+
|
|
133
|
+
axs[1][0].set_ylabel("Start Time\nDensities", fontsize=10)
|
|
134
|
+
axs[2][0].set_ylabel("End Time\nDensities", fontsize=10)
|
|
135
|
+
axs[3][0].set_ylabel("Duration\nDensities", fontsize=10)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def joint_time_distributions_plot(
|
|
139
|
+
observed: DataFrame, ys: Optional[dict[DataFrame]], **kwargs
|
|
140
|
+
) -> Figure:
|
|
141
|
+
if ys is None:
|
|
142
|
+
ys = dict()
|
|
143
|
+
acts = list(observed.act.value_counts(ascending=False).index)
|
|
144
|
+
n_acts = len(acts)
|
|
145
|
+
rows = len(ys) + 2
|
|
146
|
+
ratios = [1 for _ in range(rows)]
|
|
147
|
+
ratios[0] = 0.2
|
|
148
|
+
|
|
149
|
+
cmaps = kwargs.pop("cmaps", {})
|
|
150
|
+
legend = []
|
|
151
|
+
legend_colours = []
|
|
152
|
+
|
|
153
|
+
fig, axs = plt.subplots(
|
|
154
|
+
rows,
|
|
155
|
+
observed.act.nunique(),
|
|
156
|
+
figsize=kwargs.pop("figsize", (12, 5)),
|
|
157
|
+
sharex=False,
|
|
158
|
+
sharey=False,
|
|
159
|
+
constrained_layout=True,
|
|
160
|
+
gridspec_kw={"height_ratios": ratios},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# deal with observed first
|
|
164
|
+
name = kwargs.pop("observed_title", "Observed")
|
|
165
|
+
|
|
166
|
+
legend.append(name)
|
|
167
|
+
cmap = cmaps.get(0, "Blues")
|
|
168
|
+
lcolours = colormaps[cmap]([0, 0.5, 1])
|
|
169
|
+
legend_colours.append(lcolours[int(len(lcolours) / 2)])
|
|
170
|
+
|
|
171
|
+
_joint_time_plot(observed, axs[1], acts, cmap=cmap)
|
|
172
|
+
|
|
173
|
+
# now deal with ys
|
|
174
|
+
for i, (name, y) in enumerate(ys.items()):
|
|
175
|
+
|
|
176
|
+
legend.append(name)
|
|
177
|
+
cmap = cmaps.get(i + 1, "Reds")
|
|
178
|
+
lcolours = colormaps[cmap]([0, 0.5, 1])
|
|
179
|
+
legend_colours.append(lcolours[int(len(lcolours) / 2)])
|
|
180
|
+
|
|
181
|
+
_joint_time_plot(y, axs[i + 2], acts, cmap=cmap)
|
|
182
|
+
|
|
183
|
+
# xlabel on bottom row
|
|
184
|
+
for ax in axs[-1]:
|
|
185
|
+
ax.set_xlabel("Start times", fontsize=8)
|
|
186
|
+
ax.set_xticks(
|
|
187
|
+
[240, 480, 720, 960, 1200, 1440],
|
|
188
|
+
labels=["04:00", "08:00", "12:00", "16:00", "20:00", "24:00"],
|
|
189
|
+
rotation=90,
|
|
190
|
+
fontsize=8,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# acts
|
|
194
|
+
for ax, act in zip(axs[1], acts):
|
|
195
|
+
ax.set_title(act.title(), fontsize=9)
|
|
196
|
+
|
|
197
|
+
# deal with legend
|
|
198
|
+
for i in range(n_acts):
|
|
199
|
+
axs[0][i].spines["top"].set_visible(False)
|
|
200
|
+
axs[0][i].spines["right"].set_visible(False)
|
|
201
|
+
axs[0][i].spines["bottom"].set_visible(False)
|
|
202
|
+
axs[0][i].spines["left"].set_visible(False)
|
|
203
|
+
axs[0][i].set_xticks([])
|
|
204
|
+
axs[0][i].set_yticks([])
|
|
205
|
+
for ax in axs[0]:
|
|
206
|
+
ax.tick_params(axis="x", which="both", length=0.0)
|
|
207
|
+
|
|
208
|
+
handles = [
|
|
209
|
+
patches.Patch(color=c, label=lab) for c, lab in zip(legend_colours, legend)
|
|
210
|
+
]
|
|
211
|
+
fig.legend(
|
|
212
|
+
handles=handles,
|
|
213
|
+
loc="upper center",
|
|
214
|
+
fontsize=9,
|
|
215
|
+
ncol=len(ys) + 1,
|
|
216
|
+
frameon=False,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return fig
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _joint_time_plot(
|
|
223
|
+
population: DataFrame,
|
|
224
|
+
axs: Axes,
|
|
225
|
+
acts: list[str],
|
|
226
|
+
cmap: str,
|
|
227
|
+
xmin: int = 240,
|
|
228
|
+
xmax: int = 1441,
|
|
229
|
+
ymin: int = 0,
|
|
230
|
+
ymax: int = 960,
|
|
231
|
+
xstep: int = 30,
|
|
232
|
+
ystep: int = 30,
|
|
233
|
+
):
|
|
234
|
+
starts = population.groupby("act", observed=False).start
|
|
235
|
+
durations = population.groupby("act", observed=False).duration
|
|
236
|
+
|
|
237
|
+
start_bins = list(range(xmin, xmax, xstep))
|
|
238
|
+
duration_bins = list(range(ymin, ymax, ystep))
|
|
239
|
+
|
|
240
|
+
for i, act in enumerate(acts):
|
|
241
|
+
axs[i].set_xticks([])
|
|
242
|
+
axs[i].set_yticks([])
|
|
243
|
+
if act not in population.act.unique():
|
|
244
|
+
continue
|
|
245
|
+
act_starts = starts.get_group(act)
|
|
246
|
+
act_durations = durations.get_group(act)
|
|
247
|
+
axs[i].hist2d(
|
|
248
|
+
x=act_starts,
|
|
249
|
+
y=act_durations,
|
|
250
|
+
bins=(start_bins, duration_bins),
|
|
251
|
+
cmap=cmap,
|
|
252
|
+
)
|
|
253
|
+
ylabel = "Durations"
|
|
254
|
+
axs[0].set_ylabel(ylabel, fontsize=8)
|
|
255
|
+
axs[0].set_yticks(
|
|
256
|
+
[0, 120, 240, 360, 480, 600, 720, 840, 960],
|
|
257
|
+
labels=[
|
|
258
|
+
"00:00",
|
|
259
|
+
"02:00",
|
|
260
|
+
"04:00",
|
|
261
|
+
"06:00",
|
|
262
|
+
"08:00",
|
|
263
|
+
"10:00",
|
|
264
|
+
"12:00",
|
|
265
|
+
"14:00",
|
|
266
|
+
"16:00",
|
|
267
|
+
],
|
|
268
|
+
fontsize=8,
|
|
269
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from matplotlib import pyplot as plt
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
from matplotlib.colors import ListedColormap as CMap
|
|
6
|
+
from matplotlib.figure import Figure
|
|
7
|
+
from matplotlib.patches import Patch
|
|
8
|
+
from pandas import DataFrame
|
|
9
|
+
|
|
10
|
+
from acteval.density.features.transitions import sequence_probs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sequence_prob_plot(
|
|
14
|
+
observed: DataFrame, ys: Optional[dict[DataFrame]], **kwargs
|
|
15
|
+
) -> Figure:
|
|
16
|
+
acts = list(observed.act.value_counts(ascending=False).index)
|
|
17
|
+
cmap = kwargs.pop("cmap", None)
|
|
18
|
+
if cmap is None:
|
|
19
|
+
cmap = plt.cm.Set3
|
|
20
|
+
colors = cmap.colors
|
|
21
|
+
factor = (len(acts) // len(colors)) + 1
|
|
22
|
+
cmap = dict(zip(acts, colors * factor))
|
|
23
|
+
|
|
24
|
+
n_plots = len(ys) + 2
|
|
25
|
+
ratios = [1 for _ in range(n_plots)]
|
|
26
|
+
ratios[-1] = 0.3
|
|
27
|
+
|
|
28
|
+
fig, axs = plt.subplots(
|
|
29
|
+
1,
|
|
30
|
+
n_plots,
|
|
31
|
+
figsize=kwargs.pop("figsize", (12, 5)),
|
|
32
|
+
sharex=True,
|
|
33
|
+
sharey=True,
|
|
34
|
+
# tight_layout=True,
|
|
35
|
+
constrained_layout=True,
|
|
36
|
+
gridspec_kw={"width_ratios": ratios},
|
|
37
|
+
)
|
|
38
|
+
acts = list(observed.act.value_counts(ascending=False).index)
|
|
39
|
+
name = kwargs.pop("observed_title", "Observed")
|
|
40
|
+
_probs_plot(name, observed, ax=axs[0], cmap=cmap, ylabel=True)
|
|
41
|
+
|
|
42
|
+
if ys is None:
|
|
43
|
+
return fig
|
|
44
|
+
for i, (name, y) in enumerate(ys.items()):
|
|
45
|
+
_probs_plot(name, y, ax=axs[i + 1], cmap=cmap)
|
|
46
|
+
axs[i + 1].set_title(name)
|
|
47
|
+
|
|
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 _probs_plot(
|
|
56
|
+
name: str,
|
|
57
|
+
population: DataFrame,
|
|
58
|
+
cmap: Optional[CMap],
|
|
59
|
+
ax=Axes,
|
|
60
|
+
ylabel=False,
|
|
61
|
+
) -> Tuple[Figure, Axes]:
|
|
62
|
+
probs = sequence_probs(population)
|
|
63
|
+
accumulated = probs[::-1].cumsum()[::-1]
|
|
64
|
+
|
|
65
|
+
ys = []
|
|
66
|
+
widths = []
|
|
67
|
+
heights = []
|
|
68
|
+
lefts = []
|
|
69
|
+
cols = []
|
|
70
|
+
for idx, p, ap in zip(probs.index, probs, accumulated):
|
|
71
|
+
seq = idx[1].split(">")
|
|
72
|
+
width = 1 / len(seq)
|
|
73
|
+
for i, act in enumerate(seq):
|
|
74
|
+
ys.append(ap - p)
|
|
75
|
+
widths.append(width)
|
|
76
|
+
heights.append(p)
|
|
77
|
+
lefts.append(i * width)
|
|
78
|
+
cols.append(cmap[act])
|
|
79
|
+
|
|
80
|
+
ax.barh(y=ys, width=widths, height=heights, left=lefts, color=cols, align="edge")
|
|
81
|
+
ax.hlines(ys, xmin=0, xmax=1, color="white", linewidth=0.1)
|
|
82
|
+
|
|
83
|
+
ax.spines["top"].set_visible(False)
|
|
84
|
+
ax.spines["right"].set_visible(False)
|
|
85
|
+
ax.spines["bottom"].set_visible(False)
|
|
86
|
+
ax.spines["left"].set_visible(False)
|
|
87
|
+
ax.set_xlabel("Activity\nSequence")
|
|
88
|
+
if ylabel:
|
|
89
|
+
ax.set_ylabel("Sequence Proportion")
|
|
90
|
+
ax.set_xticklabels([])
|
|
91
|
+
ax.set_xticks([])
|
|
92
|
+
ax.set_title(name)
|
|
93
|
+
return ax
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Pure-numpy discretisation helpers (no torch dependency)."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def descretise_population(
|
|
10
|
+
data: pd.DataFrame, duration: int, step_size: int, class_map: dict
|
|
11
|
+
) -> np.ndarray:
|
|
12
|
+
"""Convert given population of activity traces into array [P, C, H, W].
|
|
13
|
+
|
|
14
|
+
P is the population size.
|
|
15
|
+
C (channel) is length 1.
|
|
16
|
+
H is time steps.
|
|
17
|
+
W is a one-hot encoding of activity type.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
data (pd.DataFrame): Schedule DataFrame with pid, act, start, end columns.
|
|
21
|
+
duration (int): Total duration in time units.
|
|
22
|
+
step_size (int): Step size for discretisation.
|
|
23
|
+
class_map (dict): Mapping from activity string to integer class index.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
np.ndarray: Array of shape [P, C, H, W].
|
|
27
|
+
"""
|
|
28
|
+
persons = data.pid.nunique()
|
|
29
|
+
num_classes = len(class_map)
|
|
30
|
+
steps = duration // step_size
|
|
31
|
+
encoded = np.zeros((persons, steps, num_classes, 1), dtype=np.float32)
|
|
32
|
+
|
|
33
|
+
for pid, (_, trace) in enumerate(data.groupby("pid")):
|
|
34
|
+
trace_encoding = descretise_trace(
|
|
35
|
+
acts=trace.act,
|
|
36
|
+
starts=trace.start,
|
|
37
|
+
ends=trace.end,
|
|
38
|
+
length=duration,
|
|
39
|
+
class_map=class_map,
|
|
40
|
+
)
|
|
41
|
+
trace_encoding = down_sample(trace_encoding, step_size)
|
|
42
|
+
trace_encoding = one_hot(trace_encoding, num_classes)
|
|
43
|
+
trace_encoding = trace_encoding.reshape(steps, num_classes, 1)
|
|
44
|
+
encoded[pid] = trace_encoding # [B, H, W, C]
|
|
45
|
+
encoded = encoded.transpose(0, 3, 1, 2) # [B, C, H, W]
|
|
46
|
+
return encoded
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def descretise_trace(
|
|
50
|
+
acts: Iterable[str],
|
|
51
|
+
starts: Iterable[int],
|
|
52
|
+
ends: Iterable[int],
|
|
53
|
+
length: int,
|
|
54
|
+
class_map: dict,
|
|
55
|
+
) -> np.ndarray:
|
|
56
|
+
"""Create categorical encoding from ranges with step of 1.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
acts (Iterable[str]): Activity labels.
|
|
60
|
+
starts (Iterable[int]): Start times.
|
|
61
|
+
ends (Iterable[int]): End times.
|
|
62
|
+
length (int): Total length of encoding.
|
|
63
|
+
class_map (dict): Mapping from activity string to integer class index.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
np.ndarray: 1D categorical encoding of length `length`.
|
|
67
|
+
"""
|
|
68
|
+
encoding = np.zeros((length), dtype=np.int8)
|
|
69
|
+
for act, start, end in zip(acts, starts, ends):
|
|
70
|
+
encoding[start:end] = class_map[act]
|
|
71
|
+
return encoding
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def down_sample(array: np.ndarray, step: int) -> np.ndarray:
|
|
75
|
+
"""Down-sample by stepping through given array.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
array (np.ndarray): Input array.
|
|
79
|
+
step (int): Step size.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
np.ndarray: Down-sampled array.
|
|
83
|
+
"""
|
|
84
|
+
return array[::step]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def one_hot(target: np.ndarray, num_classes: int) -> np.ndarray:
|
|
88
|
+
"""One hot encoding of given categorical array.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
target (np.ndarray): Categorical array.
|
|
92
|
+
num_classes (int): Number of classes.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
np.ndarray: One-hot encoded array.
|
|
96
|
+
"""
|
|
97
|
+
return np.eye(num_classes)[target]
|