boris-behav-obs 9.7.7__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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/plot_events.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import datetime as dt
|
|
24
|
+
import pathlib as pl
|
|
25
|
+
|
|
26
|
+
import matplotlib
|
|
27
|
+
|
|
28
|
+
matplotlib.use("QtAgg")
|
|
29
|
+
|
|
30
|
+
import matplotlib.dates
|
|
31
|
+
|
|
32
|
+
import matplotlib.pyplot as plt
|
|
33
|
+
import numpy as np
|
|
34
|
+
from matplotlib.dates import (
|
|
35
|
+
DateFormatter,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from . import config as cfg
|
|
39
|
+
from . import db_functions, project_functions, observation_operations
|
|
40
|
+
from . import utilities as util
|
|
41
|
+
|
|
42
|
+
# matplotlib.pyplot.switch_backend("Qt5Agg")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def default_value(ethogram, behavior, parameter):
|
|
46
|
+
"""
|
|
47
|
+
return value for duration in case of point event
|
|
48
|
+
"""
|
|
49
|
+
default_value_ = 0
|
|
50
|
+
if project_functions.event_type(behavior, ethogram) in cfg.POINT_EVENT_TYPES and parameter in ["duration"]:
|
|
51
|
+
default_value_ = "NA"
|
|
52
|
+
return default_value_
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def init_behav_modif(
|
|
56
|
+
ethogram: dict,
|
|
57
|
+
selected_subjects: list,
|
|
58
|
+
distinct_behav_modif,
|
|
59
|
+
include_modifiers,
|
|
60
|
+
parameters,
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
initialize dictionary with subject, behaviors and modifiers
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
behaviors = {}
|
|
67
|
+
for subj in selected_subjects:
|
|
68
|
+
behaviors[subj] = {}
|
|
69
|
+
for behav_modif in distinct_behav_modif:
|
|
70
|
+
behav, modif = behav_modif
|
|
71
|
+
behav_modif_str = "|".join(behav_modif) if modif else behav
|
|
72
|
+
|
|
73
|
+
if behav_modif_str not in behaviors[subj]:
|
|
74
|
+
behaviors[subj][behav_modif_str] = {}
|
|
75
|
+
|
|
76
|
+
for parameter in parameters:
|
|
77
|
+
behaviors[subj][behav_modif_str][parameter[0]] = default_value(ethogram, behav_modif_str, parameter[0])
|
|
78
|
+
|
|
79
|
+
return behaviors
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def init_behav(ethogram: dict, selected_subjects: list, distinct_behaviors, parameters) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
initialize dictionary with subject, behaviors and modifiers
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
behaviors: dict = {}
|
|
88
|
+
for subj in selected_subjects:
|
|
89
|
+
behaviors[subj] = {}
|
|
90
|
+
for behavior in distinct_behaviors:
|
|
91
|
+
if behavior not in behaviors[subj]:
|
|
92
|
+
behaviors[subj][behavior] = {}
|
|
93
|
+
for parameter in parameters:
|
|
94
|
+
behaviors[subj][behavior][parameter] = default_value(ethogram, behavior, parameter)
|
|
95
|
+
return behaviors
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_behaviors_bar_plot(
|
|
99
|
+
pj: dict,
|
|
100
|
+
selected_observations: list,
|
|
101
|
+
param: dict,
|
|
102
|
+
plot_directory: str,
|
|
103
|
+
output_format: str,
|
|
104
|
+
plot_colors: list = cfg.BEHAVIORS_PLOT_COLORS,
|
|
105
|
+
) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
time budget bar plot
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
pj (dict): project
|
|
111
|
+
param (dict): parameters
|
|
112
|
+
plot_directory (str): path of directory
|
|
113
|
+
output_format (str): image format
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
dict:
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
selected_subjects = param[cfg.SELECTED_SUBJECTS]
|
|
120
|
+
selected_behaviors = param[cfg.SELECTED_BEHAVIORS]
|
|
121
|
+
start_time = param[cfg.START_TIME]
|
|
122
|
+
end_time = param[cfg.END_TIME]
|
|
123
|
+
|
|
124
|
+
parameters = ["duration", "number of occurences"]
|
|
125
|
+
|
|
126
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(pj, selected_subjects, selected_observations, selected_behaviors)
|
|
127
|
+
|
|
128
|
+
if not ok:
|
|
129
|
+
return {"error": True, "message": msg}
|
|
130
|
+
|
|
131
|
+
# extract all behaviors from ethogram for colors in plot
|
|
132
|
+
all_behaviors = util.all_behaviors(pj[cfg.ETHOGRAM])
|
|
133
|
+
|
|
134
|
+
for obs_id in selected_observations:
|
|
135
|
+
cursor = db_connector.cursor()
|
|
136
|
+
# distinct behaviors
|
|
137
|
+
cursor.execute(
|
|
138
|
+
"SELECT distinct behavior FROM aggregated_events WHERE observation = ?",
|
|
139
|
+
(obs_id,),
|
|
140
|
+
)
|
|
141
|
+
distinct_behav = [rows["behavior"] for rows in cursor.fetchall()]
|
|
142
|
+
|
|
143
|
+
# add selected behaviors that are not observed
|
|
144
|
+
"""
|
|
145
|
+
if not param[EXCLUDE_BEHAVIORS]:
|
|
146
|
+
for behavior in selected_behaviors:
|
|
147
|
+
if [x for x in distinct_behav if x == behavior] == []:
|
|
148
|
+
distinct_behav.append(behavior)
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# distinct subjects
|
|
152
|
+
cursor.execute(
|
|
153
|
+
"SELECT distinct subject FROM aggregated_events WHERE observation = ?",
|
|
154
|
+
(obs_id,),
|
|
155
|
+
)
|
|
156
|
+
distinct_subjects = [rows["subject"] for rows in cursor.fetchall()]
|
|
157
|
+
|
|
158
|
+
behaviors = init_behav(pj[cfg.ETHOGRAM], distinct_subjects, distinct_behav, parameters)
|
|
159
|
+
|
|
160
|
+
# plot creation
|
|
161
|
+
if len(distinct_subjects) > 1:
|
|
162
|
+
fig, axs = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
|
|
163
|
+
fig2, axs2 = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
fig, ax = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
|
|
167
|
+
axs = np.ndarray(shape=(1), dtype=type(ax))
|
|
168
|
+
axs[0] = ax
|
|
169
|
+
|
|
170
|
+
fig2, ax2 = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
|
|
171
|
+
axs2 = np.ndarray(shape=(1), dtype=type(ax2))
|
|
172
|
+
axs2[0] = ax2
|
|
173
|
+
|
|
174
|
+
fig.suptitle("Durations of behaviors")
|
|
175
|
+
fig2.suptitle("Number of occurences of behaviors")
|
|
176
|
+
|
|
177
|
+
# if modifiers not to be included set modifiers to ""
|
|
178
|
+
cursor.execute("UPDATE aggregated_events SET modifiers = ''")
|
|
179
|
+
|
|
180
|
+
# time
|
|
181
|
+
obs_length = observation_operations.observation_total_length(pj[cfg.OBSERVATIONS][obs_id])
|
|
182
|
+
if obs_length == -1:
|
|
183
|
+
obs_length = 0
|
|
184
|
+
|
|
185
|
+
if param["time"] == cfg.TIME_FULL_OBS:
|
|
186
|
+
min_time = float(0)
|
|
187
|
+
max_time = float(obs_length)
|
|
188
|
+
|
|
189
|
+
if param["time"] == cfg.TIME_EVENTS:
|
|
190
|
+
try:
|
|
191
|
+
min_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0])
|
|
192
|
+
except Exception:
|
|
193
|
+
min_time = float(0)
|
|
194
|
+
try:
|
|
195
|
+
max_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0])
|
|
196
|
+
except Exception:
|
|
197
|
+
max_time = float(obs_length)
|
|
198
|
+
|
|
199
|
+
if param["time"] in (cfg.TIME_ARBITRARY_INTERVAL, cfg.TIME_OBS_INTERVAL):
|
|
200
|
+
min_time = float(start_time)
|
|
201
|
+
max_time = float(end_time)
|
|
202
|
+
|
|
203
|
+
cursor.execute(
|
|
204
|
+
"UPDATE aggregated_events SET start = ? WHERE observation = ? AND start < ? AND stop BETWEEN ? AND ?",
|
|
205
|
+
(
|
|
206
|
+
min_time,
|
|
207
|
+
obs_id,
|
|
208
|
+
min_time,
|
|
209
|
+
min_time,
|
|
210
|
+
max_time,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
cursor.execute(
|
|
214
|
+
"UPDATE aggregated_events SET stop = ? WHERE observation = ? AND stop > ? AND start BETWEEN ? AND ?",
|
|
215
|
+
(
|
|
216
|
+
max_time,
|
|
217
|
+
obs_id,
|
|
218
|
+
max_time,
|
|
219
|
+
min_time,
|
|
220
|
+
max_time,
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
cursor.execute(
|
|
224
|
+
"UPDATE aggregated_events SET start = ?, stop = ? WHERE observation = ? AND start < ? AND stop > ?",
|
|
225
|
+
(
|
|
226
|
+
min_time,
|
|
227
|
+
max_time,
|
|
228
|
+
obs_id,
|
|
229
|
+
min_time,
|
|
230
|
+
max_time,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
for ax_idx, subject in enumerate(sorted(distinct_subjects)):
|
|
235
|
+
for behavior in distinct_behav:
|
|
236
|
+
# number of occurences
|
|
237
|
+
cursor.execute(
|
|
238
|
+
("SELECT COUNT(*) AS count FROM aggregated_events WHERE observation = ? AND subject = ? AND behavior = ?"),
|
|
239
|
+
(
|
|
240
|
+
obs_id,
|
|
241
|
+
subject,
|
|
242
|
+
behavior,
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
for row in cursor.fetchall():
|
|
246
|
+
behaviors[subject][behavior]["number of occurences"] = 0 if row["count"] is None else row["count"]
|
|
247
|
+
|
|
248
|
+
# total duration
|
|
249
|
+
if project_functions.event_type(behavior, pj[cfg.ETHOGRAM]) in cfg.STATE_EVENT_TYPES:
|
|
250
|
+
cursor.execute(
|
|
251
|
+
(
|
|
252
|
+
"SELECT SUM(stop - start) AS duration FROM aggregated_events "
|
|
253
|
+
"WHERE observation = ? AND subject = ? AND behavior = ?"
|
|
254
|
+
),
|
|
255
|
+
(
|
|
256
|
+
obs_id,
|
|
257
|
+
subject,
|
|
258
|
+
behavior,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
for row in cursor.fetchall():
|
|
262
|
+
behaviors[subject][behavior]["duration"] = 0 if row["duration"] is None else row["duration"]
|
|
263
|
+
|
|
264
|
+
(
|
|
265
|
+
durations,
|
|
266
|
+
n_occurences,
|
|
267
|
+
colors,
|
|
268
|
+
x_labels,
|
|
269
|
+
colors_duration,
|
|
270
|
+
x_labels_duration,
|
|
271
|
+
) = ([], [], [], [], [], [])
|
|
272
|
+
|
|
273
|
+
for behavior in sorted(distinct_behav):
|
|
274
|
+
if param[cfg.EXCLUDE_BEHAVIORS] and behaviors[subject][behavior]["number of occurences"] == 0:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
n_occurences.append(behaviors[subject][behavior]["number of occurences"])
|
|
278
|
+
x_labels.append(behavior)
|
|
279
|
+
|
|
280
|
+
# color
|
|
281
|
+
behav_idx = [k for k in pj[cfg.ETHOGRAM] if pj[cfg.ETHOGRAM][k]["code"] == behavior][0]
|
|
282
|
+
col = None
|
|
283
|
+
if cfg.COLOR in pj[cfg.ETHOGRAM][behav_idx]:
|
|
284
|
+
col = util.behavior_user_color(pj[cfg.ETHOGRAM], behavior)
|
|
285
|
+
if col is not None:
|
|
286
|
+
colors.append(col)
|
|
287
|
+
else:
|
|
288
|
+
try:
|
|
289
|
+
colors.append(util.behavior_color(plot_colors, all_behaviors.index(behavior)))
|
|
290
|
+
except Exception:
|
|
291
|
+
colors.append("darkgray")
|
|
292
|
+
|
|
293
|
+
if project_functions.event_type(behavior, pj[cfg.ETHOGRAM]) in cfg.STATE_EVENT_TYPES:
|
|
294
|
+
durations.append(behaviors[subject][behavior]["duration"])
|
|
295
|
+
x_labels_duration.append(behavior)
|
|
296
|
+
|
|
297
|
+
col = None
|
|
298
|
+
if cfg.COLOR in pj[cfg.ETHOGRAM][behav_idx]:
|
|
299
|
+
col = util.behavior_user_color(pj[cfg.ETHOGRAM], behavior)
|
|
300
|
+
if col is not None:
|
|
301
|
+
colors_duration.append(col)
|
|
302
|
+
else:
|
|
303
|
+
try:
|
|
304
|
+
colors_duration.append(util.behavior_color(plot_colors, all_behaviors.index(behavior)))
|
|
305
|
+
except Exception:
|
|
306
|
+
colors_duration.append("darkgray")
|
|
307
|
+
|
|
308
|
+
# width = 0.35 # the width of the bars: can also be len(x) sequence
|
|
309
|
+
|
|
310
|
+
axs2[ax_idx].bar(
|
|
311
|
+
np.arange(len(n_occurences)),
|
|
312
|
+
n_occurences,
|
|
313
|
+
# width,
|
|
314
|
+
color=colors,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
axs[ax_idx].bar(
|
|
318
|
+
np.arange(len(durations)),
|
|
319
|
+
durations,
|
|
320
|
+
# width,
|
|
321
|
+
color=colors_duration,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if ax_idx == 0:
|
|
325
|
+
axs[ax_idx].set_ylabel("Duration (s)")
|
|
326
|
+
axs[ax_idx].set_xlabel("Behaviors")
|
|
327
|
+
axs[ax_idx].set_title(f"{subject}")
|
|
328
|
+
|
|
329
|
+
axs[ax_idx].set_xticks(np.arange(len(durations)))
|
|
330
|
+
axs[ax_idx].set_xticklabels(x_labels_duration, rotation="vertical", fontsize=8)
|
|
331
|
+
|
|
332
|
+
if ax_idx == 0:
|
|
333
|
+
axs2[ax_idx].set_ylabel("Number of occurences")
|
|
334
|
+
axs2[ax_idx].set_xlabel("Behaviors")
|
|
335
|
+
axs2[ax_idx].set_title(f"{subject}")
|
|
336
|
+
|
|
337
|
+
axs2[ax_idx].set_xticks(np.arange(len(n_occurences)))
|
|
338
|
+
axs2[ax_idx].set_xticklabels(x_labels, rotation="vertical", fontsize=8)
|
|
339
|
+
|
|
340
|
+
fig.align_labels()
|
|
341
|
+
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
|
|
342
|
+
|
|
343
|
+
fig2.align_labels()
|
|
344
|
+
fig2.tight_layout(rect=[0, 0.03, 1, 0.95])
|
|
345
|
+
|
|
346
|
+
if plot_directory:
|
|
347
|
+
# output_file_name = f"{pathlib.Path(plot_directory) / utilities.safeFileName(obs_id)}.{output_format}"
|
|
348
|
+
fig.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.duration.{output_format}")
|
|
349
|
+
fig2.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.number_of_occurences.{output_format}")
|
|
350
|
+
plt.close()
|
|
351
|
+
else:
|
|
352
|
+
fig.show()
|
|
353
|
+
fig2.show()
|
|
354
|
+
|
|
355
|
+
return {}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create_events_plot(
|
|
359
|
+
self,
|
|
360
|
+
selected_observations,
|
|
361
|
+
parameters,
|
|
362
|
+
plot_colors=cfg.BEHAVIORS_PLOT_COLORS,
|
|
363
|
+
plot_directory="",
|
|
364
|
+
file_format="png",
|
|
365
|
+
):
|
|
366
|
+
"""
|
|
367
|
+
create a time diagram plot (like a gantt chart)
|
|
368
|
+
with matplotlib barh function (https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.barh.html)
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
selected_subjects = parameters[cfg.SELECTED_SUBJECTS]
|
|
372
|
+
selected_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
|
|
373
|
+
include_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
|
|
374
|
+
interval = parameters[cfg.TIME_INTERVAL]
|
|
375
|
+
start_time = parameters[cfg.START_TIME]
|
|
376
|
+
end_time = parameters[cfg.END_TIME]
|
|
377
|
+
|
|
378
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(self.pj, selected_subjects, selected_observations, selected_behaviors)
|
|
379
|
+
|
|
380
|
+
if not ok:
|
|
381
|
+
return False, msg, None
|
|
382
|
+
|
|
383
|
+
cursor = db_connector.cursor()
|
|
384
|
+
|
|
385
|
+
# if modifiers not to be included set modifiers to ""
|
|
386
|
+
if not include_modifiers:
|
|
387
|
+
cursor.execute("UPDATE aggregated_events SET modifiers = ''")
|
|
388
|
+
|
|
389
|
+
cursor.execute("SELECT DISTINCT behavior, modifiers FROM aggregated_events")
|
|
390
|
+
distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
|
|
391
|
+
|
|
392
|
+
# add selected behaviors that are not observed
|
|
393
|
+
for behav in selected_behaviors:
|
|
394
|
+
if [x for x in distinct_behav_modif if x[0] == behav] == []:
|
|
395
|
+
distinct_behav_modif.append([behav, "-"])
|
|
396
|
+
|
|
397
|
+
distinct_behav_modif = sorted(distinct_behav_modif)
|
|
398
|
+
max_len = len(distinct_behav_modif)
|
|
399
|
+
|
|
400
|
+
all_behaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]
|
|
401
|
+
|
|
402
|
+
par1 = 1
|
|
403
|
+
bar_height = 0.5
|
|
404
|
+
epoch_date = dt.datetime(2017, 1, 1)
|
|
405
|
+
|
|
406
|
+
for obs_id in selected_observations:
|
|
407
|
+
if len(selected_subjects) > 1:
|
|
408
|
+
fig, axs = plt.subplots(figsize=(20, 8), nrows=len(selected_subjects), ncols=1, sharex=True)
|
|
409
|
+
else:
|
|
410
|
+
fig, ax = plt.subplots(figsize=(20, 8), nrows=len(selected_subjects), ncols=1, sharex=True)
|
|
411
|
+
axs = np.ndarray(shape=(1), dtype=type(ax))
|
|
412
|
+
axs[0] = ax
|
|
413
|
+
|
|
414
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(self.pj, selected_subjects, [obs_id], selected_behaviors)
|
|
415
|
+
|
|
416
|
+
cursor = db_connector.cursor()
|
|
417
|
+
# if modifiers not to be included set modifiers to ""
|
|
418
|
+
if not include_modifiers:
|
|
419
|
+
cursor.execute("UPDATE aggregated_events SET modifiers = ''")
|
|
420
|
+
cursor = db_connector.cursor()
|
|
421
|
+
|
|
422
|
+
cursor.execute("SELECT DISTINCT behavior, modifiers FROM aggregated_events")
|
|
423
|
+
distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
|
|
424
|
+
|
|
425
|
+
# add selected behaviors that are not observed
|
|
426
|
+
if not parameters["exclude behaviors"]:
|
|
427
|
+
for behav in selected_behaviors:
|
|
428
|
+
if [x for x in distinct_behav_modif if x[0] == behav] == []:
|
|
429
|
+
distinct_behav_modif.append([behav, "-"])
|
|
430
|
+
|
|
431
|
+
distinct_behav_modif = sorted(distinct_behav_modif)
|
|
432
|
+
max_len = len(distinct_behav_modif)
|
|
433
|
+
|
|
434
|
+
# time
|
|
435
|
+
obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id])
|
|
436
|
+
if obs_length == -1: # media length not available
|
|
437
|
+
interval = cfg.TIME_EVENTS
|
|
438
|
+
|
|
439
|
+
if interval == cfg.TIME_FULL_OBS:
|
|
440
|
+
min_time = 0.0
|
|
441
|
+
max_time = float(obs_length)
|
|
442
|
+
|
|
443
|
+
if interval == cfg.TIME_OBS_INTERVAL:
|
|
444
|
+
obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
|
|
445
|
+
offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
|
|
446
|
+
min_time = float(obs_interval[0]) + offset
|
|
447
|
+
# Use max media duration for max time if no interval is defined (=0)
|
|
448
|
+
max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(obs_length)
|
|
449
|
+
|
|
450
|
+
if interval == cfg.TIME_EVENTS:
|
|
451
|
+
try:
|
|
452
|
+
min_time = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0]) # first event
|
|
453
|
+
except Exception:
|
|
454
|
+
min_time = 0.0
|
|
455
|
+
try:
|
|
456
|
+
max_time = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0]) # last event
|
|
457
|
+
except Exception:
|
|
458
|
+
max_time = float(obs_length)
|
|
459
|
+
|
|
460
|
+
if interval == cfg.TIME_ARBITRARY_INTERVAL:
|
|
461
|
+
min_time = float(start_time)
|
|
462
|
+
max_time = float(end_time)
|
|
463
|
+
|
|
464
|
+
# adjust start if start < init
|
|
465
|
+
cursor.execute(
|
|
466
|
+
"UPDATE aggregated_events SET start = ? WHERE start < ? AND stop BETWEEN ? AND ?",
|
|
467
|
+
(
|
|
468
|
+
min_time,
|
|
469
|
+
min_time,
|
|
470
|
+
min_time,
|
|
471
|
+
max_time,
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
# adjust stop if stop > end
|
|
475
|
+
cursor.execute(
|
|
476
|
+
"UPDATE aggregated_events SET stop = ? WHERE stop > ? AND start BETWEEN ? AND ?",
|
|
477
|
+
(
|
|
478
|
+
max_time,
|
|
479
|
+
max_time,
|
|
480
|
+
min_time,
|
|
481
|
+
max_time,
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
# adjust start and stop if start < init and stop > end
|
|
485
|
+
cursor.execute(
|
|
486
|
+
"UPDATE aggregated_events SET start = ?, stop = ? WHERE start < ? AND stop > ?",
|
|
487
|
+
(
|
|
488
|
+
min_time,
|
|
489
|
+
max_time,
|
|
490
|
+
min_time,
|
|
491
|
+
max_time,
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# delete events outside time interval
|
|
496
|
+
cursor.execute(
|
|
497
|
+
"DELETE FROM aggregated_events WHERE (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
|
|
498
|
+
(min_time, min_time, max_time, max_time),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
ylabels = [" ".join(x) for x in distinct_behav_modif]
|
|
502
|
+
for ax_idx, subject in enumerate(selected_subjects):
|
|
503
|
+
if parameters["exclude behaviors"]:
|
|
504
|
+
cursor.execute(
|
|
505
|
+
"SELECT DISTINCT behavior, modifiers FROM aggregated_events WHERE subject = ?",
|
|
506
|
+
(subject,),
|
|
507
|
+
)
|
|
508
|
+
distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
|
|
509
|
+
|
|
510
|
+
# add selected behaviors that are not observed
|
|
511
|
+
if not parameters["exclude behaviors"]:
|
|
512
|
+
for behav in selected_behaviors:
|
|
513
|
+
if [x for x in distinct_behav_modif if x[0] == behav] == []:
|
|
514
|
+
distinct_behav_modif.append([behav, "-"])
|
|
515
|
+
|
|
516
|
+
distinct_behav_modif = sorted(distinct_behav_modif)
|
|
517
|
+
max_len = len(distinct_behav_modif)
|
|
518
|
+
ylabels = [" ".join(x) for x in distinct_behav_modif]
|
|
519
|
+
|
|
520
|
+
if not ax_idx:
|
|
521
|
+
axs[ax_idx].set_title(f"Observation {obs_id}\n{subject}", fontsize=14)
|
|
522
|
+
else:
|
|
523
|
+
axs[ax_idx].set_title(subject, fontsize=14)
|
|
524
|
+
bars = {}
|
|
525
|
+
i = 0
|
|
526
|
+
for behavior_modifiers in distinct_behav_modif:
|
|
527
|
+
behavior, modifiers = behavior_modifiers
|
|
528
|
+
behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior
|
|
529
|
+
bars[behavior_modifiers_str] = []
|
|
530
|
+
|
|
531
|
+
# total duration
|
|
532
|
+
cursor.execute(
|
|
533
|
+
("SELECT start, stop FROM aggregated_events WHERE subject = ? AND behavior = ? AND modifiers = ?"),
|
|
534
|
+
(
|
|
535
|
+
subject,
|
|
536
|
+
behavior,
|
|
537
|
+
modifiers,
|
|
538
|
+
),
|
|
539
|
+
)
|
|
540
|
+
for row in cursor.fetchall():
|
|
541
|
+
bars[behavior_modifiers_str].append((row["start"], row["stop"]))
|
|
542
|
+
|
|
543
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
544
|
+
start_date = matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=row["start"]))
|
|
545
|
+
end_date = matplotlib.dates.date2num(
|
|
546
|
+
epoch_date + dt.timedelta(seconds=row["stop"] + cfg.POINT_EVENT_PLOT_DURATION * (row["stop"] == row["start"]))
|
|
547
|
+
)
|
|
548
|
+
if self.timeFormat == cfg.S:
|
|
549
|
+
start_date = row["start"]
|
|
550
|
+
end_date = row["stop"]
|
|
551
|
+
|
|
552
|
+
# color
|
|
553
|
+
behav_idx = [k for k in self.pj[cfg.ETHOGRAM] if self.pj[cfg.ETHOGRAM][k]["code"] == behavior][0]
|
|
554
|
+
col = None
|
|
555
|
+
if cfg.COLOR in self.pj[cfg.ETHOGRAM][behav_idx]:
|
|
556
|
+
col = util.behavior_user_color(self.pj[cfg.ETHOGRAM], behavior)
|
|
557
|
+
if col is not None:
|
|
558
|
+
bar_color = col
|
|
559
|
+
else:
|
|
560
|
+
try:
|
|
561
|
+
bar_color = util.behavior_color(plot_colors, all_behaviors.index(behavior))
|
|
562
|
+
except Exception:
|
|
563
|
+
bar_color = "darkgray"
|
|
564
|
+
bar_color = cfg.POINT_EVENT_PLOT_COLOR if row["stop"] == row["start"] else bar_color
|
|
565
|
+
|
|
566
|
+
# sage colors removed from matplotlib colors list
|
|
567
|
+
if bar_color in ("sage", "darksage", "lightsage"):
|
|
568
|
+
bar_color = {
|
|
569
|
+
"darksage": "#598556",
|
|
570
|
+
"lightsage": "#bcecac",
|
|
571
|
+
"sage": "#87ae73",
|
|
572
|
+
}[bar_color]
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
axs[ax_idx].barh(
|
|
576
|
+
(i * par1) + par1,
|
|
577
|
+
end_date - start_date,
|
|
578
|
+
left=start_date,
|
|
579
|
+
height=bar_height,
|
|
580
|
+
align="center",
|
|
581
|
+
edgecolor=bar_color,
|
|
582
|
+
color=bar_color,
|
|
583
|
+
alpha=1,
|
|
584
|
+
)
|
|
585
|
+
except Exception:
|
|
586
|
+
axs[ax_idx].barh(
|
|
587
|
+
(i * par1) + par1,
|
|
588
|
+
end_date - start_date,
|
|
589
|
+
left=start_date,
|
|
590
|
+
height=bar_height,
|
|
591
|
+
align="center",
|
|
592
|
+
edgecolor="darkgray",
|
|
593
|
+
color="darkgray",
|
|
594
|
+
alpha=1,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
i += 1
|
|
598
|
+
|
|
599
|
+
axs[ax_idx].set_ylim(bottom=0, top=(max_len * par1) + par1)
|
|
600
|
+
pos = np.arange(par1, max_len * par1 + par1 + 1, par1)
|
|
601
|
+
axs[ax_idx].set_yticks(pos[: len(ylabels)])
|
|
602
|
+
|
|
603
|
+
axs[ax_idx].set_yticklabels(ylabels, fontdict={"fontsize": 10})
|
|
604
|
+
|
|
605
|
+
axs[ax_idx].set_ylabel(
|
|
606
|
+
"Behaviors" + " (modifiers)" * include_modifiers,
|
|
607
|
+
fontdict={"fontsize": 10},
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
611
|
+
axs[ax_idx].set_xlim(
|
|
612
|
+
left=matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=min_time)),
|
|
613
|
+
right=matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=max_time)),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
axs[ax_idx].grid(color="g", linestyle=":")
|
|
617
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
618
|
+
axs[ax_idx].xaxis_date()
|
|
619
|
+
axs[ax_idx].xaxis.set_major_formatter(DateFormatter("%H:%M:%S"))
|
|
620
|
+
axs[ax_idx].set_xlabel("Time (HH:MM:SS)", fontdict={"fontsize": 12})
|
|
621
|
+
if self.timeFormat == cfg.S:
|
|
622
|
+
axs[ax_idx].set_xlabel("Time (s)", fontdict={"fontsize": 12})
|
|
623
|
+
|
|
624
|
+
axs[ax_idx].invert_yaxis()
|
|
625
|
+
|
|
626
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
627
|
+
fig.autofmt_xdate()
|
|
628
|
+
|
|
629
|
+
plt.tight_layout()
|
|
630
|
+
|
|
631
|
+
if len(selected_observations) > 1:
|
|
632
|
+
plt.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.{file_format}")
|
|
633
|
+
else:
|
|
634
|
+
plt.show()
|