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.
- 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
|
@@ -0,0 +1,1039 @@
|
|
|
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 logging
|
|
24
|
+
import os
|
|
25
|
+
import pathlib as pl
|
|
26
|
+
from decimal import Decimal as dec
|
|
27
|
+
from io import StringIO
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import pyreadr
|
|
33
|
+
|
|
34
|
+
flag_pyreadr_loaded = True
|
|
35
|
+
except ModuleNotFoundError:
|
|
36
|
+
flag_pyreadr_loaded = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
import tablib
|
|
40
|
+
from PySide6.QtCore import Qt
|
|
41
|
+
from PySide6.QtWidgets import (
|
|
42
|
+
QFileDialog,
|
|
43
|
+
QHBoxLayout,
|
|
44
|
+
QInputDialog,
|
|
45
|
+
QLabel,
|
|
46
|
+
QListWidget,
|
|
47
|
+
QMessageBox,
|
|
48
|
+
QPushButton,
|
|
49
|
+
QSizePolicy,
|
|
50
|
+
QSpacerItem,
|
|
51
|
+
QTableWidget,
|
|
52
|
+
QTableWidgetItem,
|
|
53
|
+
QVBoxLayout,
|
|
54
|
+
QWidget,
|
|
55
|
+
QApplication,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
from . import config as cfg
|
|
59
|
+
from . import (
|
|
60
|
+
db_functions,
|
|
61
|
+
dialog,
|
|
62
|
+
gui_utilities,
|
|
63
|
+
observation_operations,
|
|
64
|
+
project_functions,
|
|
65
|
+
select_observations,
|
|
66
|
+
select_subj_behav,
|
|
67
|
+
time_budget_functions,
|
|
68
|
+
)
|
|
69
|
+
from . import utilities as util
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class timeBudgetResults(QWidget):
|
|
73
|
+
"""
|
|
74
|
+
class for displaying time budget results in new window
|
|
75
|
+
a function for exporting data in TSV, CSV, XLS and ODS formats is implemented
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
pj (dict): BORIS project
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, pj, config_param):
|
|
82
|
+
super().__init__()
|
|
83
|
+
|
|
84
|
+
self.pj = pj
|
|
85
|
+
self.config_param = config_param
|
|
86
|
+
|
|
87
|
+
hbox = QVBoxLayout(self)
|
|
88
|
+
|
|
89
|
+
self.label = QLabel("")
|
|
90
|
+
hbox.addWidget(self.label)
|
|
91
|
+
|
|
92
|
+
self.lw = QListWidget()
|
|
93
|
+
# self.lw.setEnabled(False)
|
|
94
|
+
self.lw.setMaximumHeight(100)
|
|
95
|
+
hbox.addWidget(self.lw)
|
|
96
|
+
|
|
97
|
+
self.lbTotalObservedTime = QLabel("")
|
|
98
|
+
hbox.addWidget(self.lbTotalObservedTime)
|
|
99
|
+
|
|
100
|
+
# behaviors excluded from total time
|
|
101
|
+
self.excluded_behaviors_list = QLabel("")
|
|
102
|
+
hbox.addWidget(self.excluded_behaviors_list)
|
|
103
|
+
|
|
104
|
+
self.twTB = QTableWidget()
|
|
105
|
+
hbox.addWidget(self.twTB)
|
|
106
|
+
|
|
107
|
+
hbox2 = QHBoxLayout()
|
|
108
|
+
|
|
109
|
+
self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
|
|
110
|
+
hbox2.addWidget(self.pbSave)
|
|
111
|
+
|
|
112
|
+
spacerItem = QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
113
|
+
hbox2.addItem(spacerItem)
|
|
114
|
+
|
|
115
|
+
self.pbClose = QPushButton(cfg.CLOSE, clicked=self.close_clicked)
|
|
116
|
+
hbox2.addWidget(self.pbClose)
|
|
117
|
+
|
|
118
|
+
hbox.addLayout(hbox2)
|
|
119
|
+
|
|
120
|
+
self.setWindowTitle("Time budget")
|
|
121
|
+
|
|
122
|
+
def close_clicked(self):
|
|
123
|
+
"""
|
|
124
|
+
save geometry of widget and close it
|
|
125
|
+
"""
|
|
126
|
+
gui_utilities.save_geometry(self, "time budget")
|
|
127
|
+
self.close()
|
|
128
|
+
|
|
129
|
+
def pbSave_clicked(self):
|
|
130
|
+
"""
|
|
131
|
+
save time budget analysis results in TSV, CSV, ODS, XLS format
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def complete(lst: list, max_: int) -> list:
|
|
135
|
+
"""
|
|
136
|
+
complete list with empty string until len = max
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
lst (list): list to complete
|
|
140
|
+
max_ (int): length of the returned list
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
list: completed list
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
lst.extend([""] * (max_ - len(lst)))
|
|
147
|
+
return lst
|
|
148
|
+
|
|
149
|
+
logging.debug("save time budget results to file")
|
|
150
|
+
|
|
151
|
+
file_formats = (cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML, cfg.TEXT_FILE, cfg.PANDAS_DF, cfg.RDS)
|
|
152
|
+
|
|
153
|
+
file_name, filter_ = QFileDialog().getSaveFileName(self, "Save Time budget analysis", "", ";;".join(file_formats))
|
|
154
|
+
|
|
155
|
+
if not file_name:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# add correct file extension if not present
|
|
159
|
+
if pl.Path(file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
|
|
160
|
+
if cfg.FILE_NAME_SUFFIX[filter_] != "cli":
|
|
161
|
+
file_name = str(pl.Path(file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
|
|
162
|
+
else:
|
|
163
|
+
file_name = str(pl.Path(file_name))
|
|
164
|
+
# check if file with new extension already exists
|
|
165
|
+
if pl.Path(file_name).is_file():
|
|
166
|
+
if (
|
|
167
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
168
|
+
== cfg.CANCEL
|
|
169
|
+
):
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
rows: list = []
|
|
173
|
+
|
|
174
|
+
header: list = ["Observation id", "Observation date", "Description"]
|
|
175
|
+
# indep var labels
|
|
176
|
+
header.extend([self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in self.pj[cfg.INDEPENDENT_VARIABLES]])
|
|
177
|
+
header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
|
|
178
|
+
|
|
179
|
+
for idx in range(self.twTB.columnCount()):
|
|
180
|
+
header.append(self.twTB.horizontalHeaderItem(idx).text())
|
|
181
|
+
rows.append(header)
|
|
182
|
+
|
|
183
|
+
col1: list = []
|
|
184
|
+
# add obs id
|
|
185
|
+
if self.lw.count() == 1:
|
|
186
|
+
col1.append(self.lw.item(0).text())
|
|
187
|
+
else:
|
|
188
|
+
col1.append("NA, observations grouped")
|
|
189
|
+
|
|
190
|
+
# add obs date
|
|
191
|
+
if self.lw.count() == 1:
|
|
192
|
+
col1.append(self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get("date", "").replace("T", " "))
|
|
193
|
+
else:
|
|
194
|
+
# TODO: check if date is the same for all selected obs
|
|
195
|
+
col1.append("NA, observations grouped")
|
|
196
|
+
|
|
197
|
+
# description
|
|
198
|
+
if self.lw.count() == 1:
|
|
199
|
+
col1.append(util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get(cfg.DESCRIPTION, "")))
|
|
200
|
+
else:
|
|
201
|
+
col1.append("NA, observations grouped")
|
|
202
|
+
|
|
203
|
+
# indep var values
|
|
204
|
+
for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
|
|
205
|
+
if self.lw.count() == 1:
|
|
206
|
+
# var has value in obs?
|
|
207
|
+
if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get(
|
|
208
|
+
cfg.INDEPENDENT_VARIABLES, []
|
|
209
|
+
):
|
|
210
|
+
col1.append(
|
|
211
|
+
self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
|
|
212
|
+
self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
col1.append("")
|
|
217
|
+
else:
|
|
218
|
+
# TODO: check if var value is the same for all selected obs
|
|
219
|
+
col1.append("NA, observations grouped")
|
|
220
|
+
|
|
221
|
+
if self.time_interval == cfg.TIME_ARBITRARY_INTERVAL:
|
|
222
|
+
col1.extend([f"{self.min_time:0.3f}", f"{self.max_time:0.3f}", f"{self.max_time - self.min_time:0.3f}"])
|
|
223
|
+
|
|
224
|
+
if self.time_interval == cfg.TIME_FULL_OBS:
|
|
225
|
+
col1.extend(["Full observation", "Full observation", "Full observation"])
|
|
226
|
+
|
|
227
|
+
if self.time_interval == cfg.TIME_EVENTS:
|
|
228
|
+
col1.extend(["Limited to coded events", "Limited to coded events", "Limited to coded events"])
|
|
229
|
+
|
|
230
|
+
for row_idx in range(self.twTB.rowCount()):
|
|
231
|
+
values = []
|
|
232
|
+
for col_idx in range(self.twTB.columnCount()):
|
|
233
|
+
values.append(util.intfloatstr(self.twTB.item(row_idx, col_idx).text()))
|
|
234
|
+
rows.append(col1 + values)
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
else:
|
|
238
|
+
# observations list
|
|
239
|
+
obs_header = ["Observation id", "Observation date", "Description"]
|
|
240
|
+
|
|
241
|
+
# indep var
|
|
242
|
+
obs_header.extend(
|
|
243
|
+
[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in self.pj[cfg.INDEPENDENT_VARIABLES]]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
obs_header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
|
|
247
|
+
|
|
248
|
+
obs_rows = []
|
|
249
|
+
obs_rows.append(obs_header)
|
|
250
|
+
for idx in range(self.lw.count()):
|
|
251
|
+
row = []
|
|
252
|
+
# obs id
|
|
253
|
+
row.append(self.lw.item(idx).text())
|
|
254
|
+
row.append(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get("date", ""))
|
|
255
|
+
row.append(util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get(cfg.DESCRIPTION, "")))
|
|
256
|
+
|
|
257
|
+
for idx2 in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
|
|
258
|
+
# var has value in obs?
|
|
259
|
+
if self.pj[cfg.INDEPENDENT_VARIABLES][idx2]["label"] in self.pj[cfg.OBSERVATIONS][
|
|
260
|
+
self.lw.item(idx).text()
|
|
261
|
+
].get(cfg.INDEPENDENT_VARIABLES, []):
|
|
262
|
+
row.append(
|
|
263
|
+
self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES][
|
|
264
|
+
self.pj[cfg.INDEPENDENT_VARIABLES][idx2]["label"]
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
row.append("")
|
|
269
|
+
# start stop duration
|
|
270
|
+
row.extend([f"{self.min_time:0.3f}", f"{self.max_time:0.3f}", f"{self.max_time - self.min_time:0.3f}"])
|
|
271
|
+
|
|
272
|
+
obs_rows.append(row)
|
|
273
|
+
|
|
274
|
+
# write file with observations information
|
|
275
|
+
data = tablib.Dataset()
|
|
276
|
+
data.title = "Time budget - Observations information"
|
|
277
|
+
|
|
278
|
+
for row in obs_rows:
|
|
279
|
+
data.append(complete(row, max([len(r) for r in obs_rows])))
|
|
280
|
+
|
|
281
|
+
with open(pl.Path(file_name).with_suffix(f".observations_info.{cfg.FILE_NAME_SUFFIX[filter_]}"), "wb") as f:
|
|
282
|
+
if filter_ in [cfg.TSV, cfg.CSV, cfg.HTML]:
|
|
283
|
+
f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
|
|
284
|
+
if filter_ in [cfg.ODS, cfg.XLSX, cfg.XLS]:
|
|
285
|
+
f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
rows.append(["Observations:"])
|
|
290
|
+
for idx in range(self.lw.count()):
|
|
291
|
+
rows.append([""])
|
|
292
|
+
rows.append(["Observation id", self.lw.item(idx).text()])
|
|
293
|
+
rows.append(["Observation date", self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get("date", "")])
|
|
294
|
+
rows.append(
|
|
295
|
+
[
|
|
296
|
+
"Description",
|
|
297
|
+
util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get(cfg.DESCRIPTION, "")),
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if cfg.INDEPENDENT_VARIABLES in self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()]:
|
|
302
|
+
rows.append(["Independent variables:"])
|
|
303
|
+
for var in self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES]:
|
|
304
|
+
rows.append(
|
|
305
|
+
[var, self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES][var]]
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if self.excluded_behaviors_list.text():
|
|
309
|
+
s1, s2 = self.excluded_behaviors_list.text().split(": ")
|
|
310
|
+
rows.extend([[""], [s1] + s2.split(", ")])
|
|
311
|
+
|
|
312
|
+
rows.extend([[""], [""], ["Time budget:"]])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# write header
|
|
316
|
+
|
|
317
|
+
header = [self.twTB.horizontalHeaderItem(col_idx).text() for col_idx in range(self.twTB.columnCount())]
|
|
318
|
+
rows.append(header)
|
|
319
|
+
|
|
320
|
+
for row in range(self.twTB.rowCount()):
|
|
321
|
+
values = []
|
|
322
|
+
for col_idx in range(self.twTB.columnCount()):
|
|
323
|
+
values.append(util.intfloatstr(self.twTB.item(row, col_idx).text()))
|
|
324
|
+
rows.append(values)
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
data = tablib.Dataset()
|
|
328
|
+
data.title = "Time budget"
|
|
329
|
+
|
|
330
|
+
for row in rows:
|
|
331
|
+
data.append(complete(row, max([len(r) for r in rows])))
|
|
332
|
+
|
|
333
|
+
if filter_ in (cfg.PANDAS_DF, cfg.RDS):
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
# build pandas dataframe from the tsv export of tablib dataset
|
|
337
|
+
dtype = {
|
|
338
|
+
"Observation id": str,
|
|
339
|
+
"Observation date": str,
|
|
340
|
+
"Description": str,
|
|
341
|
+
"Time budget start": str,
|
|
342
|
+
"Time budget stop": str,
|
|
343
|
+
"Time budget duration": str,
|
|
344
|
+
"Subject": str,
|
|
345
|
+
"Behavior": str,
|
|
346
|
+
"Modifiers": str,
|
|
347
|
+
"Total number of occurences": float,
|
|
348
|
+
"Total duration (s)": float,
|
|
349
|
+
"Duration mean (s)": float,
|
|
350
|
+
"Duration std dev": float,
|
|
351
|
+
"inter-event intervals mean (s)": float,
|
|
352
|
+
"inter-event intervals std dev": float,
|
|
353
|
+
"% of total length ": float,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# indep var values
|
|
357
|
+
for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
|
|
358
|
+
if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "numeric":
|
|
359
|
+
dtype[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = float
|
|
360
|
+
else:
|
|
361
|
+
dtype[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = str
|
|
362
|
+
|
|
363
|
+
df = pd.read_csv(
|
|
364
|
+
StringIO(data.export("tsv")),
|
|
365
|
+
sep="\t",
|
|
366
|
+
dtype=dtype,
|
|
367
|
+
parse_dates=[1],
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if filter_ == cfg.PANDAS_DF:
|
|
371
|
+
df.to_pickle(file_name)
|
|
372
|
+
|
|
373
|
+
if flag_pyreadr_loaded and filter_ == cfg.RDS:
|
|
374
|
+
pyreadr.write_rds(file_name, df)
|
|
375
|
+
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# write results
|
|
379
|
+
with open(file_name, "wb") as f:
|
|
380
|
+
if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML, cfg.TEXT_FILE):
|
|
381
|
+
f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
|
|
382
|
+
if filter_ in (cfg.ODS, cfg.XLSX, cfg.XLS):
|
|
383
|
+
f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def time_budget(self, mode: str, mode2: str = "list"):
|
|
387
|
+
"""
|
|
388
|
+
time budget (by behavior or category)
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
mode (str): ["by_behavior", "by_category"]
|
|
392
|
+
mode2 (str): must be in ["list", "current"]
|
|
393
|
+
"current" time budget of current observation
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
if mode2 == "current":
|
|
397
|
+
if self.observationId:
|
|
398
|
+
selected_observations = [self.observationId]
|
|
399
|
+
else:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
if mode2 == "list":
|
|
403
|
+
_, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
|
|
404
|
+
|
|
405
|
+
if not selected_observations:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
# check if coded behaviors are defined in ethogram
|
|
409
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# check if state events are paired
|
|
413
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
414
|
+
if not_ok or not selected_observations:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
flagGroup: bool = False
|
|
418
|
+
if len(selected_observations) > 1:
|
|
419
|
+
flagGroup = (
|
|
420
|
+
dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
|
|
421
|
+
== cfg.YES
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
max_media_duration_all_obs, total_media_duration_all_obs = observation_operations.media_duration(
|
|
425
|
+
self.pj[cfg.OBSERVATIONS], selected_observations
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
|
|
429
|
+
|
|
430
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
431
|
+
|
|
432
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
433
|
+
|
|
434
|
+
parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
|
|
435
|
+
self,
|
|
436
|
+
selected_observations,
|
|
437
|
+
start_coding=start_coding,
|
|
438
|
+
end_coding=end_coding,
|
|
439
|
+
# start_interval=start_interval,
|
|
440
|
+
# end_interval=end_interval,
|
|
441
|
+
start_interval=None,
|
|
442
|
+
end_interval=None,
|
|
443
|
+
maxTime=max_media_duration_all_obs,
|
|
444
|
+
by_category=(mode == "by_category"),
|
|
445
|
+
n_observations=len(selected_observations),
|
|
446
|
+
show_exclude_non_coded_modifiers=True,
|
|
447
|
+
)
|
|
448
|
+
if parameters == {}:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
452
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
logging.debug(f"{parameters=}")
|
|
456
|
+
|
|
457
|
+
# ask for excluding behaviors durations from total time
|
|
458
|
+
if start_coding is not None and not start_coding.is_nan():
|
|
459
|
+
cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
|
|
460
|
+
title="Select behaviors to exclude from the total time",
|
|
461
|
+
text=("The duration of the selected behaviors will be subtracted from the total time"),
|
|
462
|
+
table="",
|
|
463
|
+
behavior_type=cfg.STATE_EVENT_TYPES,
|
|
464
|
+
)
|
|
465
|
+
if cancel_pressed:
|
|
466
|
+
return
|
|
467
|
+
else:
|
|
468
|
+
parameters[cfg.EXCLUDED_BEHAVIORS] = []
|
|
469
|
+
|
|
470
|
+
self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
|
|
471
|
+
QApplication.processEvents()
|
|
472
|
+
|
|
473
|
+
# check if time_budget window must be used
|
|
474
|
+
if flagGroup or len(selected_observations) == 1:
|
|
475
|
+
t0 = time.time()
|
|
476
|
+
|
|
477
|
+
cursor = db_functions.load_events_in_db(
|
|
478
|
+
self.pj,
|
|
479
|
+
parameters[cfg.SELECTED_SUBJECTS],
|
|
480
|
+
selected_observations,
|
|
481
|
+
parameters[cfg.SELECTED_BEHAVIORS],
|
|
482
|
+
time_interval=cfg.TIME_FULL_OBS,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
total_observation_time = 0
|
|
486
|
+
for obsId in selected_observations:
|
|
487
|
+
obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
|
|
488
|
+
|
|
489
|
+
if obs_length == dec(-1): # media length not available
|
|
490
|
+
parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
|
|
491
|
+
|
|
492
|
+
if obs_length == dec(-2): # images obs without time
|
|
493
|
+
parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
|
|
494
|
+
|
|
495
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
|
|
496
|
+
min_time = float(0)
|
|
497
|
+
# check if the last event is recorded after media file length
|
|
498
|
+
try:
|
|
499
|
+
if float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) > float(obs_length):
|
|
500
|
+
max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
|
|
501
|
+
else:
|
|
502
|
+
max_time = float(obs_length)
|
|
503
|
+
except Exception:
|
|
504
|
+
max_time = float(obs_length)
|
|
505
|
+
|
|
506
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_OBS_INTERVAL:
|
|
507
|
+
obs_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
|
|
508
|
+
offset = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
|
|
509
|
+
min_time = float(obs_interval[0]) + offset
|
|
510
|
+
# Use max media duration for max time if no interval is defined (=0)
|
|
511
|
+
max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(obs_length)
|
|
512
|
+
|
|
513
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS: # events duration
|
|
514
|
+
try:
|
|
515
|
+
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
|
|
516
|
+
except Exception:
|
|
517
|
+
min_time = float(0)
|
|
518
|
+
try:
|
|
519
|
+
max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) # last event
|
|
520
|
+
except Exception:
|
|
521
|
+
# TODO: set to 0 if no events ?
|
|
522
|
+
max_time = float(obs_length)
|
|
523
|
+
|
|
524
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
|
|
525
|
+
min_time = float(parameters[cfg.START_TIME])
|
|
526
|
+
max_time = float(parameters[cfg.END_TIME])
|
|
527
|
+
|
|
528
|
+
# check intervals
|
|
529
|
+
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
530
|
+
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
531
|
+
# if cfg.POINT in self.eventType(behav).upper():
|
|
532
|
+
if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
# extract modifiers
|
|
536
|
+
|
|
537
|
+
cursor.execute(
|
|
538
|
+
"SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
539
|
+
(obsId, subj, behav),
|
|
540
|
+
)
|
|
541
|
+
distinct_modifiers = list(cursor.fetchall())
|
|
542
|
+
|
|
543
|
+
# logging.debug("distinct_modifiers: {}".format(distinct_modifiers))
|
|
544
|
+
|
|
545
|
+
for modifier in distinct_modifiers:
|
|
546
|
+
# logging.debug("modifier #{}#".format(modifier[0]))
|
|
547
|
+
|
|
548
|
+
# insert events at boundaries of time interval
|
|
549
|
+
if (
|
|
550
|
+
len(
|
|
551
|
+
cursor.execute(
|
|
552
|
+
(
|
|
553
|
+
"SELECT * FROM events "
|
|
554
|
+
"WHERE observation = ? AND subject = ? AND code = ? AND modifiers = ? "
|
|
555
|
+
"AND occurence < ?"
|
|
556
|
+
),
|
|
557
|
+
(obsId, subj, behav, modifier[0], min_time),
|
|
558
|
+
).fetchall()
|
|
559
|
+
)
|
|
560
|
+
% 2
|
|
561
|
+
):
|
|
562
|
+
cursor.execute(
|
|
563
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
564
|
+
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if (
|
|
568
|
+
len(
|
|
569
|
+
cursor.execute(
|
|
570
|
+
(
|
|
571
|
+
"SELECT * FROM events WHERE observation = ? AND subject = ? AND code = ? "
|
|
572
|
+
"AND modifiers = ? AND occurence > ?"
|
|
573
|
+
),
|
|
574
|
+
(obsId, subj, behav, modifier[0], max_time),
|
|
575
|
+
).fetchall()
|
|
576
|
+
)
|
|
577
|
+
% 2
|
|
578
|
+
):
|
|
579
|
+
cursor.execute(
|
|
580
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
581
|
+
(obsId, subj, behav, "STATE", modifier[0], max_time),
|
|
582
|
+
)
|
|
583
|
+
try:
|
|
584
|
+
cursor.execute("COMMIT")
|
|
585
|
+
except Exception:
|
|
586
|
+
pass
|
|
587
|
+
|
|
588
|
+
total_observation_time += max_time - min_time
|
|
589
|
+
|
|
590
|
+
# delete all events out of time interval from db
|
|
591
|
+
cursor.execute(
|
|
592
|
+
"DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
|
|
593
|
+
(obsId, min_time, max_time),
|
|
594
|
+
)
|
|
595
|
+
try:
|
|
596
|
+
cursor.execute("COMMIT")
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
out, categories = time_budget_functions.time_budget_analysis(
|
|
601
|
+
self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# check excluded behaviors
|
|
605
|
+
excl_behaviors_total_time = {}
|
|
606
|
+
for element in out:
|
|
607
|
+
if element["subject"] not in excl_behaviors_total_time:
|
|
608
|
+
excl_behaviors_total_time[element["subject"]] = 0
|
|
609
|
+
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
610
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
|
|
611
|
+
|
|
612
|
+
# widget for results visualization
|
|
613
|
+
self.tb = timeBudgetResults(self.pj, self.config_param)
|
|
614
|
+
|
|
615
|
+
# add min and max time
|
|
616
|
+
self.tb.time_interval = parameters[cfg.TIME_INTERVAL]
|
|
617
|
+
self.tb.min_time = min_time
|
|
618
|
+
self.tb.max_time = max_time
|
|
619
|
+
|
|
620
|
+
# observations list
|
|
621
|
+
self.tb.label.setText("Selected observations")
|
|
622
|
+
for obs_id in selected_observations:
|
|
623
|
+
# self.tb.lw.addItem(f"{obs_id} {self.pj[OBSERVATIONS][obs_id]['date']} {self.pj[OBSERVATIONS][obs_id]['description']}")
|
|
624
|
+
self.tb.lw.addItem(obs_id)
|
|
625
|
+
|
|
626
|
+
# media length
|
|
627
|
+
if len(selected_observations) > 1:
|
|
628
|
+
if total_observation_time:
|
|
629
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
630
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
|
|
631
|
+
if self.timeFormat == cfg.S:
|
|
632
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
|
|
633
|
+
else:
|
|
634
|
+
self.tb.lbTotalObservedTime.setText("Total observation length: not available")
|
|
635
|
+
else:
|
|
636
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
637
|
+
self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
|
|
638
|
+
if self.timeFormat == cfg.S:
|
|
639
|
+
self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
|
|
640
|
+
|
|
641
|
+
# behaviors excluded from total time
|
|
642
|
+
if parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
643
|
+
self.tb.excluded_behaviors_list.setText(
|
|
644
|
+
"Behaviors excluded from total time: " + (", ".join(parameters[cfg.EXCLUDED_BEHAVIORS]))
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
self.tb.excluded_behaviors_list.setVisible(False)
|
|
648
|
+
|
|
649
|
+
self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
|
|
650
|
+
logging.debug("Time budget generated")
|
|
651
|
+
|
|
652
|
+
if mode == "by_behavior":
|
|
653
|
+
tb_fields = [
|
|
654
|
+
"Subject",
|
|
655
|
+
"Behavior",
|
|
656
|
+
"Modifiers",
|
|
657
|
+
"Total number of occurences",
|
|
658
|
+
"Total duration (s)",
|
|
659
|
+
"Duration mean (s)",
|
|
660
|
+
"Duration std dev",
|
|
661
|
+
"inter-event intervals mean (s)",
|
|
662
|
+
"inter-event intervals std dev",
|
|
663
|
+
"% of total length",
|
|
664
|
+
]
|
|
665
|
+
fields = [
|
|
666
|
+
"subject",
|
|
667
|
+
"behavior",
|
|
668
|
+
"modifiers",
|
|
669
|
+
"number",
|
|
670
|
+
"duration",
|
|
671
|
+
"duration_mean",
|
|
672
|
+
"duration_stdev",
|
|
673
|
+
"inter_duration_mean",
|
|
674
|
+
"inter_duration_stdev",
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
self.tb.twTB.setColumnCount(len(tb_fields))
|
|
678
|
+
self.tb.twTB.setHorizontalHeaderLabels(tb_fields)
|
|
679
|
+
|
|
680
|
+
for row in out:
|
|
681
|
+
self.tb.twTB.setRowCount(self.tb.twTB.rowCount() + 1)
|
|
682
|
+
column = 0
|
|
683
|
+
for field in fields:
|
|
684
|
+
if isinstance(row[field], float):
|
|
685
|
+
item = QTableWidgetItem(f"{row[field]:.3f}")
|
|
686
|
+
else:
|
|
687
|
+
item = QTableWidgetItem(str(row[field]).replace(" ()", ""))
|
|
688
|
+
# no modif allowed
|
|
689
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
690
|
+
self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
|
|
691
|
+
column += 1
|
|
692
|
+
|
|
693
|
+
# % of total time
|
|
694
|
+
if row["duration"] in (0, cfg.NA):
|
|
695
|
+
item = QTableWidgetItem(str(row["duration"]))
|
|
696
|
+
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
697
|
+
tot_time = float(total_observation_time)
|
|
698
|
+
# substract time of excluded behaviors from the total for the subject
|
|
699
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
700
|
+
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
701
|
+
item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
|
|
702
|
+
|
|
703
|
+
else:
|
|
704
|
+
item = QTableWidgetItem("-")
|
|
705
|
+
|
|
706
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
707
|
+
self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
|
|
708
|
+
|
|
709
|
+
if mode == "by_category":
|
|
710
|
+
tb_fields = ["Subject", "Category", "Total number", "Total duration (s)"]
|
|
711
|
+
fields = ["number", "duration"]
|
|
712
|
+
|
|
713
|
+
self.tb.twTB.setColumnCount(len(tb_fields))
|
|
714
|
+
self.tb.twTB.setHorizontalHeaderLabels(tb_fields)
|
|
715
|
+
|
|
716
|
+
for subject in categories:
|
|
717
|
+
for category in categories[subject]:
|
|
718
|
+
self.tb.twTB.setRowCount(self.tb.twTB.rowCount() + 1)
|
|
719
|
+
|
|
720
|
+
column = 0
|
|
721
|
+
item = QTableWidgetItem(subject)
|
|
722
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
723
|
+
self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
|
|
724
|
+
|
|
725
|
+
column = 1
|
|
726
|
+
if category == "":
|
|
727
|
+
item = QTableWidgetItem("No category")
|
|
728
|
+
else:
|
|
729
|
+
item = QTableWidgetItem(category)
|
|
730
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
731
|
+
self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
|
|
732
|
+
|
|
733
|
+
for field in fields:
|
|
734
|
+
column += 1
|
|
735
|
+
|
|
736
|
+
if field == "duration":
|
|
737
|
+
try:
|
|
738
|
+
item = QTableWidgetItem(f"{categories[subject][category][field]:0.3f}")
|
|
739
|
+
except Exception:
|
|
740
|
+
item = QTableWidgetItem(categories[subject][category][field])
|
|
741
|
+
else:
|
|
742
|
+
item = QTableWidgetItem(str(categories[subject][category][field]))
|
|
743
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
744
|
+
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
745
|
+
self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
|
|
746
|
+
|
|
747
|
+
self.tb.twTB.resizeColumnsToContents()
|
|
748
|
+
|
|
749
|
+
gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
|
|
750
|
+
|
|
751
|
+
self.tb.show()
|
|
752
|
+
|
|
753
|
+
if not flagGroup and len(selected_observations) > 1:
|
|
754
|
+
output_format, ok = QInputDialog.getItem(
|
|
755
|
+
self,
|
|
756
|
+
"Time budget analysis format",
|
|
757
|
+
"Available formats",
|
|
758
|
+
(
|
|
759
|
+
cfg.TSV,
|
|
760
|
+
cfg.CSV,
|
|
761
|
+
cfg.ODS,
|
|
762
|
+
cfg.ODS_WB,
|
|
763
|
+
cfg.XLSX,
|
|
764
|
+
cfg.XLSX_WB,
|
|
765
|
+
cfg.HTML,
|
|
766
|
+
cfg.XLS,
|
|
767
|
+
),
|
|
768
|
+
0,
|
|
769
|
+
False,
|
|
770
|
+
)
|
|
771
|
+
if not ok:
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
extension = cfg.FILE_NAME_SUFFIX[output_format]
|
|
775
|
+
|
|
776
|
+
if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
|
|
777
|
+
workbook = tablib.Databook()
|
|
778
|
+
|
|
779
|
+
wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
|
|
780
|
+
if not wb_file_name:
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
if pl.Path(wb_file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
|
|
784
|
+
wb_file_name = str(pl.Path(wb_file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
|
|
785
|
+
# check if file with new extension already exists
|
|
786
|
+
if pl.Path(wb_file_name).is_file():
|
|
787
|
+
if (
|
|
788
|
+
dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
789
|
+
== cfg.CANCEL
|
|
790
|
+
):
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
else: # not workbook
|
|
794
|
+
exportDir = QFileDialog(self).getExistingDirectory(
|
|
795
|
+
self,
|
|
796
|
+
"Choose a directory to save the time budget analysis",
|
|
797
|
+
os.path.expanduser("~"),
|
|
798
|
+
options=QFileDialog.ShowDirsOnly,
|
|
799
|
+
)
|
|
800
|
+
if not exportDir:
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
if mode == "by_behavior":
|
|
804
|
+
tb_fields = [
|
|
805
|
+
"Subject",
|
|
806
|
+
"Behavior",
|
|
807
|
+
"Modifiers",
|
|
808
|
+
"Total number of occurences",
|
|
809
|
+
"Total duration (s)",
|
|
810
|
+
"Duration mean (s)",
|
|
811
|
+
"Duration std dev",
|
|
812
|
+
"inter-event intervals mean (s)",
|
|
813
|
+
"inter-event intervals std dev",
|
|
814
|
+
"% of total length",
|
|
815
|
+
]
|
|
816
|
+
fields = [
|
|
817
|
+
"subject",
|
|
818
|
+
"behavior",
|
|
819
|
+
"modifiers",
|
|
820
|
+
"number",
|
|
821
|
+
"duration",
|
|
822
|
+
"duration_mean",
|
|
823
|
+
"duration_stdev",
|
|
824
|
+
"inter_duration_mean",
|
|
825
|
+
"inter_duration_stdev",
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
if mode == "by_category":
|
|
829
|
+
tb_fields = ["Subject", "Category", "Total number of occurences", "Total duration (s)"]
|
|
830
|
+
fields = ["subject", "category", "number", "duration"]
|
|
831
|
+
|
|
832
|
+
mem_command = ""
|
|
833
|
+
for obsId in selected_observations:
|
|
834
|
+
cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
|
|
835
|
+
|
|
836
|
+
obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
|
|
837
|
+
|
|
838
|
+
if obs_length == -1:
|
|
839
|
+
obs_length = 0
|
|
840
|
+
|
|
841
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
|
|
842
|
+
min_time = float(0)
|
|
843
|
+
# check if the last event is recorded after media file length
|
|
844
|
+
try:
|
|
845
|
+
if float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) > float(obs_length):
|
|
846
|
+
max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
|
|
847
|
+
else:
|
|
848
|
+
max_time = float(obs_length)
|
|
849
|
+
except Exception:
|
|
850
|
+
max_time = float(obs_length)
|
|
851
|
+
|
|
852
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
|
|
853
|
+
try:
|
|
854
|
+
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
|
|
855
|
+
except Exception:
|
|
856
|
+
min_time = float(0)
|
|
857
|
+
try:
|
|
858
|
+
max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
|
|
859
|
+
except Exception:
|
|
860
|
+
max_time = float(obs_length)
|
|
861
|
+
|
|
862
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
|
|
863
|
+
min_time = float(parameters[cfg.START_TIME])
|
|
864
|
+
max_time = float(parameters[cfg.END_TIME])
|
|
865
|
+
|
|
866
|
+
# check intervals
|
|
867
|
+
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
868
|
+
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
869
|
+
if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
|
|
870
|
+
continue
|
|
871
|
+
|
|
872
|
+
cursor.execute(
|
|
873
|
+
"SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
874
|
+
(obsId, subj, behav),
|
|
875
|
+
)
|
|
876
|
+
distinct_modifiers = list(cursor.fetchall())
|
|
877
|
+
|
|
878
|
+
for modifier in distinct_modifiers:
|
|
879
|
+
if (
|
|
880
|
+
len(
|
|
881
|
+
cursor.execute(
|
|
882
|
+
(
|
|
883
|
+
"SELECT * FROM events "
|
|
884
|
+
"WHERE observation = ? AND subject = ? "
|
|
885
|
+
"AND code = ? AND modifiers = ? AND occurence < ?"
|
|
886
|
+
),
|
|
887
|
+
(obsId, subj, behav, modifier[0], min_time),
|
|
888
|
+
).fetchall()
|
|
889
|
+
)
|
|
890
|
+
% 2
|
|
891
|
+
):
|
|
892
|
+
cursor.execute(
|
|
893
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
894
|
+
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
895
|
+
)
|
|
896
|
+
if (
|
|
897
|
+
len(
|
|
898
|
+
cursor.execute(
|
|
899
|
+
(
|
|
900
|
+
"SELECT * FROM events WHERE observation = ? AND subject = ? AND code = ?"
|
|
901
|
+
" AND modifiers = ? AND occurence > ?"
|
|
902
|
+
),
|
|
903
|
+
(obsId, subj, behav, modifier[0], max_time),
|
|
904
|
+
).fetchall()
|
|
905
|
+
)
|
|
906
|
+
% 2
|
|
907
|
+
):
|
|
908
|
+
cursor.execute(
|
|
909
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
910
|
+
(obsId, subj, behav, cfg.STATE, modifier[0], max_time),
|
|
911
|
+
)
|
|
912
|
+
try:
|
|
913
|
+
cursor.execute("COMMIT")
|
|
914
|
+
except Exception:
|
|
915
|
+
pass
|
|
916
|
+
|
|
917
|
+
cursor.execute(
|
|
918
|
+
"DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
|
|
919
|
+
(obsId, min_time, max_time),
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
out, categories = time_budget_functions.time_budget_analysis(
|
|
923
|
+
self.pj[cfg.ETHOGRAM], cursor, [obsId], parameters, by_category=(mode == "by_category")
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
# check excluded behaviors
|
|
927
|
+
excl_behaviors_total_time = {}
|
|
928
|
+
for element in out:
|
|
929
|
+
if element["subject"] not in excl_behaviors_total_time:
|
|
930
|
+
excl_behaviors_total_time[element["subject"]] = 0
|
|
931
|
+
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
932
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
|
|
933
|
+
|
|
934
|
+
rows: list = []
|
|
935
|
+
col1: list = []
|
|
936
|
+
# observation id
|
|
937
|
+
col1.append(obsId)
|
|
938
|
+
col1.append(self.pj[cfg.OBSERVATIONS][obsId].get("date", "").replace("T", ""))
|
|
939
|
+
col1.append(util.eol2space(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DESCRIPTION, "")))
|
|
940
|
+
header = ["Observation id", "Observation date", "Description"]
|
|
941
|
+
|
|
942
|
+
indep_var_label: list = []
|
|
943
|
+
indep_var_values: list = []
|
|
944
|
+
for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
|
|
945
|
+
indep_var_label.append(v["label"])
|
|
946
|
+
indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
|
|
947
|
+
|
|
948
|
+
header.extend(indep_var_label)
|
|
949
|
+
col1.extend(indep_var_values)
|
|
950
|
+
|
|
951
|
+
# interval analysis
|
|
952
|
+
if dec(min_time).is_nan(): # check if observation has timestamp
|
|
953
|
+
col1.extend([cfg.NA, cfg.NA, cfg.NA])
|
|
954
|
+
else:
|
|
955
|
+
col1.extend([f"{min_time:0.3f}", f"{max_time:0.3f}", f"{max_time - min_time:0.3f}"])
|
|
956
|
+
header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
|
|
957
|
+
|
|
958
|
+
if mode == "by_behavior":
|
|
959
|
+
# header
|
|
960
|
+
rows.append(header + tb_fields)
|
|
961
|
+
|
|
962
|
+
for row in out:
|
|
963
|
+
values = []
|
|
964
|
+
for field in fields:
|
|
965
|
+
values.append(str(row[field]).replace(" ()", ""))
|
|
966
|
+
# % of total time
|
|
967
|
+
if row["duration"] in (0, cfg.NA):
|
|
968
|
+
values.append(row["duration"])
|
|
969
|
+
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
970
|
+
tot_time = float(max_time - min_time)
|
|
971
|
+
# substract duration of excluded behaviors from total time for each subject
|
|
972
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
973
|
+
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
974
|
+
# % of tot time
|
|
975
|
+
values.append(round(row["duration"] / tot_time * 100, 1) if tot_time > 0 else cfg.NA)
|
|
976
|
+
else:
|
|
977
|
+
values.append("-")
|
|
978
|
+
|
|
979
|
+
rows.append(col1 + values)
|
|
980
|
+
|
|
981
|
+
if mode == "by_category":
|
|
982
|
+
rows.append(header + tb_fields)
|
|
983
|
+
|
|
984
|
+
for subject in categories:
|
|
985
|
+
for category in categories[subject]:
|
|
986
|
+
values = []
|
|
987
|
+
values.append(subject)
|
|
988
|
+
if category == "":
|
|
989
|
+
values.append("No category")
|
|
990
|
+
else:
|
|
991
|
+
values.append(category)
|
|
992
|
+
|
|
993
|
+
values.append(categories[subject][category]["number"])
|
|
994
|
+
try:
|
|
995
|
+
values.append(f"{categories[subject][category]['duration']:0.3f}")
|
|
996
|
+
except Exception:
|
|
997
|
+
values.append(categories[subject][category]["duration"])
|
|
998
|
+
|
|
999
|
+
rows.append(col1 + values)
|
|
1000
|
+
|
|
1001
|
+
data = tablib.Dataset()
|
|
1002
|
+
data.title = obsId
|
|
1003
|
+
for row in rows:
|
|
1004
|
+
data.append(util.complete(row, max([len(r) for r in rows])))
|
|
1005
|
+
|
|
1006
|
+
# check worksheet/workbook title for forbidden char (excel)
|
|
1007
|
+
data.title = util.safe_xl_worksheet_title(data.title, extension)
|
|
1008
|
+
|
|
1009
|
+
if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
|
|
1010
|
+
workbook.add_sheet(data)
|
|
1011
|
+
|
|
1012
|
+
else:
|
|
1013
|
+
file_name = f"{pl.Path(exportDir) / pl.Path(util.safeFileName(obsId))}.{extension}"
|
|
1014
|
+
if mem_command != cfg.OVERWRITE_ALL and pl.Path(file_name).is_file():
|
|
1015
|
+
if mem_command == "Skip all":
|
|
1016
|
+
continue
|
|
1017
|
+
mem_command = dialog.MessageDialog(
|
|
1018
|
+
cfg.programName,
|
|
1019
|
+
f"The file {file_name} already exists.",
|
|
1020
|
+
[cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
|
|
1021
|
+
)
|
|
1022
|
+
if mem_command == cfg.CANCEL:
|
|
1023
|
+
return
|
|
1024
|
+
if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
|
|
1025
|
+
continue
|
|
1026
|
+
|
|
1027
|
+
with open(file_name, "wb") as f:
|
|
1028
|
+
if output_format in (cfg.TSV, cfg.CSV, cfg.HTML):
|
|
1029
|
+
f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[output_format])))
|
|
1030
|
+
|
|
1031
|
+
if output_format in (cfg.ODS, cfg.XLSX, cfg.XLS):
|
|
1032
|
+
f.write(data.export(cfg.FILE_NAME_SUFFIX[output_format]))
|
|
1033
|
+
|
|
1034
|
+
if output_format == cfg.XLSX_WB:
|
|
1035
|
+
with open(wb_file_name, "wb") as f:
|
|
1036
|
+
f.write(workbook.xlsx)
|
|
1037
|
+
if output_format == cfg.ODS_WB:
|
|
1038
|
+
with open(wb_file_name, "wb") as f:
|
|
1039
|
+
f.write(workbook.ods)
|