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,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import pathlib
|
|
23
|
+
import re
|
|
24
|
+
import statistics
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import tablib
|
|
28
|
+
from PySide6.QtCore import Qt
|
|
29
|
+
from PySide6.QtGui import QIcon
|
|
30
|
+
from PySide6.QtWidgets import (
|
|
31
|
+
QDialog,
|
|
32
|
+
QFileDialog,
|
|
33
|
+
QHBoxLayout,
|
|
34
|
+
QLabel,
|
|
35
|
+
QLineEdit,
|
|
36
|
+
QListWidget,
|
|
37
|
+
QMessageBox,
|
|
38
|
+
QPushButton,
|
|
39
|
+
QRadioButton,
|
|
40
|
+
QSizePolicy,
|
|
41
|
+
QSpacerItem,
|
|
42
|
+
QTableWidget,
|
|
43
|
+
QTableWidgetItem,
|
|
44
|
+
QVBoxLayout,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from . import config as cfg
|
|
48
|
+
from . import db_functions, dialog, observation_operations
|
|
49
|
+
from . import portion as Interval
|
|
50
|
+
from . import project_functions, select_observations, select_subj_behav
|
|
51
|
+
from . import utilities as util
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def icc(i: list):
|
|
55
|
+
"""
|
|
56
|
+
create a closed-closed interval
|
|
57
|
+
"""
|
|
58
|
+
return Interval.closed(i[0], i[1])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ico(i: list):
|
|
62
|
+
"""
|
|
63
|
+
create a closed-open interval
|
|
64
|
+
"""
|
|
65
|
+
return Interval.closedopen(i[0], i[1])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def io(i: list):
|
|
69
|
+
"""
|
|
70
|
+
create a open interval
|
|
71
|
+
"""
|
|
72
|
+
return Interval.open(i[0], i[1])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Advanced_event_filtering_dialog(QDialog):
|
|
76
|
+
"""
|
|
77
|
+
Dialog for visualizing advanced event filtering results
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
summary_header: tuple = (
|
|
81
|
+
"Observation id",
|
|
82
|
+
"Number of occurences",
|
|
83
|
+
"Total duration (s)",
|
|
84
|
+
"Duration mean (s)",
|
|
85
|
+
"Std Dev",
|
|
86
|
+
)
|
|
87
|
+
details_header: tuple = ("Observation id", "Comment", "Start time", "Stop time", "Duration (s)")
|
|
88
|
+
|
|
89
|
+
def __init__(self, events):
|
|
90
|
+
super().__init__()
|
|
91
|
+
|
|
92
|
+
self.events = events
|
|
93
|
+
self.out = []
|
|
94
|
+
self.setWindowTitle("Advanced event filtering")
|
|
95
|
+
|
|
96
|
+
vbox = QVBoxLayout()
|
|
97
|
+
|
|
98
|
+
self.lb_time_interval = QLabel()
|
|
99
|
+
vbox.addWidget(self.lb_time_interval)
|
|
100
|
+
|
|
101
|
+
hbox = QHBoxLayout()
|
|
102
|
+
hbox.addWidget(QLabel("Filter"))
|
|
103
|
+
self.logic = QLineEdit("")
|
|
104
|
+
hbox.addWidget(self.logic)
|
|
105
|
+
self.pb_filter = QPushButton("Filter events", clicked=self.filter)
|
|
106
|
+
hbox.addWidget(self.pb_filter)
|
|
107
|
+
self.pb_clear = QPushButton("Clear", clicked=self.logic.clear)
|
|
108
|
+
self.pb_clear.setIcon(QIcon.fromTheme("edit-clear"))
|
|
109
|
+
hbox.addWidget(self.pb_clear)
|
|
110
|
+
vbox.addLayout(hbox)
|
|
111
|
+
|
|
112
|
+
hbox = QHBoxLayout()
|
|
113
|
+
self.rb_summary = QRadioButton("Summary", toggled=self.filter)
|
|
114
|
+
self.rb_summary.setChecked(True)
|
|
115
|
+
hbox.addWidget(self.rb_summary)
|
|
116
|
+
self.rb_details = QRadioButton("Details", toggled=self.filter)
|
|
117
|
+
hbox.addWidget(self.rb_details)
|
|
118
|
+
vbox.addLayout(hbox)
|
|
119
|
+
|
|
120
|
+
hbox = QHBoxLayout()
|
|
121
|
+
vbox2 = QVBoxLayout()
|
|
122
|
+
vbox2.addWidget(QLabel("Subjects"))
|
|
123
|
+
self.lw1 = QListWidget()
|
|
124
|
+
vbox2.addWidget(self.lw1)
|
|
125
|
+
hbox.addLayout(vbox2)
|
|
126
|
+
|
|
127
|
+
vbox2 = QVBoxLayout()
|
|
128
|
+
vbox2.addWidget(QLabel("Behaviors"))
|
|
129
|
+
self.lw2 = QListWidget()
|
|
130
|
+
vbox2.addWidget(self.lw2)
|
|
131
|
+
hbox.addLayout(vbox2)
|
|
132
|
+
self.add_subj_behav_button = QPushButton("", clicked=self.add_subj_behav)
|
|
133
|
+
self.add_subj_behav_button.setIcon(QIcon.fromTheme("go-up"))
|
|
134
|
+
hbox.addWidget(self.add_subj_behav_button)
|
|
135
|
+
|
|
136
|
+
vbox2 = QVBoxLayout()
|
|
137
|
+
vbox2.addWidget(QLabel("Logical operators"))
|
|
138
|
+
self.lw3 = QListWidget()
|
|
139
|
+
self.lw3.addItems(["AND", "OR"])
|
|
140
|
+
vbox2.addWidget(self.lw3)
|
|
141
|
+
hbox.addLayout(vbox2)
|
|
142
|
+
self.add_logic_button = QPushButton("", clicked=self.add_logic)
|
|
143
|
+
self.add_logic_button.setIcon(QIcon.fromTheme("go-up"))
|
|
144
|
+
hbox.addWidget(self.add_logic_button)
|
|
145
|
+
|
|
146
|
+
vbox.addLayout(hbox)
|
|
147
|
+
|
|
148
|
+
self.lb_results = QLabel("Results")
|
|
149
|
+
vbox.addWidget(self.lb_results)
|
|
150
|
+
|
|
151
|
+
self.tw = QTableWidget(self)
|
|
152
|
+
vbox.addWidget(self.tw)
|
|
153
|
+
|
|
154
|
+
hbox = QHBoxLayout()
|
|
155
|
+
hbox.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
156
|
+
self.pb_save = QPushButton("Save results", clicked=self.save_results)
|
|
157
|
+
hbox.addWidget(self.pb_save)
|
|
158
|
+
self.pb_close = QPushButton(cfg.CLOSE, clicked=self.close)
|
|
159
|
+
hbox.addWidget(self.pb_close)
|
|
160
|
+
vbox.addLayout(hbox)
|
|
161
|
+
|
|
162
|
+
self.setLayout(vbox)
|
|
163
|
+
|
|
164
|
+
subjects_list, behaviors_list = [], []
|
|
165
|
+
for obs_id in events:
|
|
166
|
+
for subj_behav in events[obs_id]:
|
|
167
|
+
subj, behav = subj_behav.split("|")
|
|
168
|
+
subjects_list.append(subj)
|
|
169
|
+
behaviors_list.append(behav)
|
|
170
|
+
subjects_set = sorted(set(subjects_list))
|
|
171
|
+
behaviors_set = sorted(set(behaviors_list))
|
|
172
|
+
|
|
173
|
+
self.lw1.addItems(subjects_set)
|
|
174
|
+
self.lw2.addItems(behaviors_set)
|
|
175
|
+
|
|
176
|
+
self.resize(640, 640)
|
|
177
|
+
|
|
178
|
+
def add_subj_behav(self):
|
|
179
|
+
"""
|
|
180
|
+
add subject|behavior of selected listwidgets items in lineEdit
|
|
181
|
+
"""
|
|
182
|
+
if self.lw1.currentItem() and self.lw2.currentItem():
|
|
183
|
+
self.logic.insert(f'"{self.lw1.currentItem().text()}|{self.lw2.currentItem().text()}" ')
|
|
184
|
+
else:
|
|
185
|
+
QMessageBox.warning(self, cfg.programName, "Select a subject and a behavior")
|
|
186
|
+
|
|
187
|
+
def add_logic(self):
|
|
188
|
+
"""
|
|
189
|
+
add selected logic operaton to lineedit
|
|
190
|
+
"""
|
|
191
|
+
if self.lw3.currentItem():
|
|
192
|
+
text = ""
|
|
193
|
+
if self.lw3.currentItem().text() == "AND":
|
|
194
|
+
text = " & "
|
|
195
|
+
if self.lw3.currentItem().text() == "OR":
|
|
196
|
+
text = " | "
|
|
197
|
+
if text:
|
|
198
|
+
self.logic.insert(text)
|
|
199
|
+
else:
|
|
200
|
+
QMessageBox.warning(self, cfg.programName, "Select a logical operator")
|
|
201
|
+
|
|
202
|
+
def filter(self):
|
|
203
|
+
"""
|
|
204
|
+
filter events
|
|
205
|
+
"""
|
|
206
|
+
if not self.logic.text():
|
|
207
|
+
return
|
|
208
|
+
if self.logic.text().count('"') % 2:
|
|
209
|
+
QMessageBox.warning(self, cfg.programName, 'Wrong number of double quotes (")')
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
sb_list = re.findall('"([^"]*)"', self.logic.text())
|
|
213
|
+
|
|
214
|
+
self.out = []
|
|
215
|
+
flag_error = False
|
|
216
|
+
for obs_id in self.events:
|
|
217
|
+
logic = self.logic.text()
|
|
218
|
+
for sb in set(sb_list):
|
|
219
|
+
logic = logic.replace(f'"{sb}"', f'self.events[obs_id]["{sb}"]')
|
|
220
|
+
if sb not in self.events[obs_id]:
|
|
221
|
+
self.events[obs_id][sb] = io([0, 0])
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
eval_result = eval(logic)
|
|
225
|
+
for i in eval_result:
|
|
226
|
+
if not i.empty:
|
|
227
|
+
self.out.append([obs_id, "", f"{i.lower}", f"{i.upper}", f"{i.upper - i.lower:.3f}"])
|
|
228
|
+
except KeyError:
|
|
229
|
+
self.out.append([obs_id, "subject / behavior not found", cfg.NA, cfg.NA, cfg.NA])
|
|
230
|
+
except Exception:
|
|
231
|
+
error_type, _, _ = util.error_info(sys.exc_info())
|
|
232
|
+
self.out.append([obs_id, f"Error in {self.logic.text()}: {error_type} ", cfg.NA, cfg.NA, cfg.NA])
|
|
233
|
+
flag_error = True
|
|
234
|
+
|
|
235
|
+
self.tw.clear()
|
|
236
|
+
|
|
237
|
+
if flag_error or self.rb_details.isChecked():
|
|
238
|
+
self.lb_results.setText(f"Results ({len(self.out)} event{'s' * (len(self.out) > 1)})")
|
|
239
|
+
|
|
240
|
+
self.tw.setRowCount(len(self.out))
|
|
241
|
+
self.tw.setColumnCount(len(self.details_header)) # obs_id, comment, start, stop, duration
|
|
242
|
+
self.tw.setHorizontalHeaderLabels(self.details_header)
|
|
243
|
+
|
|
244
|
+
if not flag_error and self.rb_summary.isChecked():
|
|
245
|
+
summary = {}
|
|
246
|
+
for row in self.out:
|
|
247
|
+
obs_id, _, start, stop, duration = row
|
|
248
|
+
if obs_id not in summary:
|
|
249
|
+
summary[obs_id] = []
|
|
250
|
+
summary[obs_id].append(float(duration))
|
|
251
|
+
|
|
252
|
+
self.out = []
|
|
253
|
+
for obs_id in summary:
|
|
254
|
+
self.out.append(
|
|
255
|
+
[
|
|
256
|
+
obs_id,
|
|
257
|
+
str(len(summary[obs_id])),
|
|
258
|
+
str(round(sum(summary[obs_id]), 3)),
|
|
259
|
+
str(round(statistics.mean(summary[obs_id]), 3)),
|
|
260
|
+
str(round(statistics.stdev(summary[obs_id]), 3)) if len(summary[obs_id]) > 1 else "NA",
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
self.lb_results.setText(f"Results ({len(summary)} observation{'s' * (len(summary) > 1)})")
|
|
265
|
+
self.tw.setRowCount(len(summary))
|
|
266
|
+
self.tw.setColumnCount(len(self.summary_header)) # obs_id, mean, stdev
|
|
267
|
+
self.tw.setHorizontalHeaderLabels(self.summary_header)
|
|
268
|
+
|
|
269
|
+
for r in range(len(self.out)):
|
|
270
|
+
for c in range(self.tw.columnCount()):
|
|
271
|
+
item = QTableWidgetItem()
|
|
272
|
+
item.setText(self.out[r][c])
|
|
273
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
274
|
+
self.tw.setItem(r, c, item)
|
|
275
|
+
|
|
276
|
+
def save_results(self):
|
|
277
|
+
"""
|
|
278
|
+
save results
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
file_formats = [
|
|
282
|
+
cfg.TSV,
|
|
283
|
+
cfg.CSV,
|
|
284
|
+
cfg.ODS,
|
|
285
|
+
cfg.XLSX,
|
|
286
|
+
cfg.XLS,
|
|
287
|
+
cfg.HTML,
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
file_name, filter_ = QFileDialog().getSaveFileName(None, "Save results", "", ";;".join(file_formats))
|
|
291
|
+
if not file_name:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
output_format = cfg.FILE_NAME_SUFFIX[filter_]
|
|
295
|
+
|
|
296
|
+
if pathlib.Path(file_name).suffix != "." + output_format:
|
|
297
|
+
file_name = str(pathlib.Path(file_name)) + "." + output_format
|
|
298
|
+
# check if file with new extension already exists
|
|
299
|
+
if pathlib.Path(file_name).is_file():
|
|
300
|
+
if (
|
|
301
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
302
|
+
== cfg.CANCEL
|
|
303
|
+
):
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
if self.rb_details.isChecked():
|
|
307
|
+
tablib_dataset = tablib.Dataset(headers=self.details_header)
|
|
308
|
+
if self.rb_summary.isChecked():
|
|
309
|
+
tablib_dataset = tablib.Dataset(headers=self.summary_header)
|
|
310
|
+
tablib_dataset.title = util.safe_xl_worksheet_title(self.logic.text(), output_format)
|
|
311
|
+
|
|
312
|
+
[tablib_dataset.append(x) for x in self.out]
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
with open(file_name, "wb") as f:
|
|
316
|
+
if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML):
|
|
317
|
+
f.write(str.encode(tablib_dataset.export(output_format)))
|
|
318
|
+
if filter_ in (cfg.ODS, cfg.XLSX, cfg.XLS):
|
|
319
|
+
f.write(tablib_dataset.export(output_format))
|
|
320
|
+
|
|
321
|
+
except Exception:
|
|
322
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def event_filtering(self):
|
|
326
|
+
"""
|
|
327
|
+
advanced event filtering
|
|
328
|
+
the portion module is used to do operations on intervals (intersection, union)
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
_, selected_observations = select_observations.select_observations2(
|
|
332
|
+
self, cfg.MULTIPLE, "Select observations for advanced event filtering"
|
|
333
|
+
)
|
|
334
|
+
if not selected_observations:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
338
|
+
if not_ok or not selected_observations:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
342
|
+
# exit with message if events do not have timestamp
|
|
343
|
+
if start_coding.is_nan():
|
|
344
|
+
QMessageBox.critical(
|
|
345
|
+
None,
|
|
346
|
+
cfg.programName,
|
|
347
|
+
("This function is not available for observations with events that do not have a timestamp"),
|
|
348
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
349
|
+
QMessageBox.NoButton,
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
354
|
+
|
|
355
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
356
|
+
|
|
357
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
358
|
+
self,
|
|
359
|
+
selected_observations,
|
|
360
|
+
start_coding=start_coding,
|
|
361
|
+
end_coding=end_coding,
|
|
362
|
+
# start_interval=start_interval,
|
|
363
|
+
# end_interval=end_interval,
|
|
364
|
+
start_interval=None,
|
|
365
|
+
end_interval=None,
|
|
366
|
+
maxTime=max_media_duration_all_obs,
|
|
367
|
+
show_include_modifiers=False,
|
|
368
|
+
show_exclude_non_coded_behaviors=False,
|
|
369
|
+
by_category=False,
|
|
370
|
+
n_observations=len(selected_observations),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if not parameters:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
377
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
_, _, db_connector = db_functions.load_aggregated_events_in_db(
|
|
381
|
+
self.pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
cursor = db_connector.cursor()
|
|
385
|
+
|
|
386
|
+
if parameters[cfg.TIME_INTERVAL] in (cfg.TIME_EVENTS, cfg.TIME_FULL_OBS):
|
|
387
|
+
cursor.execute("SELECT MIN(start), MAX(stop) FROM aggregated_events")
|
|
388
|
+
min_time, max_time = cursor.fetchone()
|
|
389
|
+
|
|
390
|
+
if parameters[cfg.TIME_INTERVAL] in (cfg.TIME_ARBITRARY_INTERVAL, cfg.TIME_OBS_INTERVAL):
|
|
391
|
+
min_time = float(parameters[cfg.START_TIME])
|
|
392
|
+
max_time = float(parameters[cfg.END_TIME])
|
|
393
|
+
|
|
394
|
+
cursor.execute(
|
|
395
|
+
"UPDATE aggregated_events SET start = ? WHERE start < ? AND stop BETWEEN ? AND ?",
|
|
396
|
+
(
|
|
397
|
+
min_time,
|
|
398
|
+
min_time,
|
|
399
|
+
min_time,
|
|
400
|
+
max_time,
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
cursor.execute(
|
|
404
|
+
"UPDATE aggregated_events SET stop = ? WHERE stop > ? AND start BETWEEN ? AND ?",
|
|
405
|
+
(
|
|
406
|
+
max_time,
|
|
407
|
+
max_time,
|
|
408
|
+
min_time,
|
|
409
|
+
max_time,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
cursor.execute(
|
|
413
|
+
"UPDATE aggregated_events SET start = ?, stop = ? WHERE start < ? AND stop > ?",
|
|
414
|
+
(
|
|
415
|
+
min_time,
|
|
416
|
+
max_time,
|
|
417
|
+
min_time,
|
|
418
|
+
max_time,
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
cursor.execute(
|
|
423
|
+
"DELETE FROM aggregated_events WHERE (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
|
|
424
|
+
(
|
|
425
|
+
min_time,
|
|
426
|
+
min_time,
|
|
427
|
+
max_time,
|
|
428
|
+
max_time,
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# create intervals from DB
|
|
433
|
+
cursor.execute("SELECT observation, subject, behavior, start, stop FROM aggregated_events")
|
|
434
|
+
|
|
435
|
+
events: dict = {}
|
|
436
|
+
for row in cursor.fetchall():
|
|
437
|
+
obs, subj, behav, start, stop = row
|
|
438
|
+
if obs not in events:
|
|
439
|
+
events[obs] = {}
|
|
440
|
+
|
|
441
|
+
# use function in base at event (state or point)
|
|
442
|
+
interval_func = icc if start == stop else ico
|
|
443
|
+
|
|
444
|
+
if f"{subj}|{behav}" not in events[obs]:
|
|
445
|
+
# create new interval
|
|
446
|
+
events[obs][f"{subj}|{behav}"] = interval_func([start, stop])
|
|
447
|
+
else:
|
|
448
|
+
# append to existing interval
|
|
449
|
+
events[obs][f"{subj}|{behav}"] |= interval_func([start, stop])
|
|
450
|
+
|
|
451
|
+
w = Advanced_event_filtering_dialog(events)
|
|
452
|
+
w.lb_time_interval.setText(
|
|
453
|
+
(f"Time interval: {util.smart_time_format(min_time, self.timeFormat)} - {util.smart_time_format(max_time, self.timeFormat)}")
|
|
454
|
+
)
|
|
455
|
+
w.exec_()
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS plugin
|
|
3
|
+
|
|
4
|
+
number of occurences of behaviors
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.1"
|
|
10
|
+
__version_date__ = "2025-04-10"
|
|
11
|
+
__plugin_name__ = "Behavior latencyxxx"
|
|
12
|
+
__author__ = "Olivier Friard - University of Torino - Italy"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import itertools
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(df: pd.DataFrame):
|
|
19
|
+
"""
|
|
20
|
+
Latency of a behavior after another.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
df["start_time"] = pd.to_datetime(df["Start (s)"])
|
|
24
|
+
df["end_time"] = pd.to_datetime(df["Stop (s)"])
|
|
25
|
+
|
|
26
|
+
latency_by_subject: dict = {}
|
|
27
|
+
|
|
28
|
+
for subject, group in df.groupby("subject"):
|
|
29
|
+
behaviors = group["behavior"].tolist()
|
|
30
|
+
# combinations = []
|
|
31
|
+
# Utiliser itertools pour créer des combinaisons 2 à 2 des comportements
|
|
32
|
+
for comb in itertools.combinations(behaviors, 2):
|
|
33
|
+
# combinations.append(comb)
|
|
34
|
+
|
|
35
|
+
last_A_end_time = None
|
|
36
|
+
|
|
37
|
+
# Liste pour stocker les latences de chaque sujet
|
|
38
|
+
subject_latency = []
|
|
39
|
+
|
|
40
|
+
for index, row in group.iterrows():
|
|
41
|
+
if row["behavior"] == comb[0]:
|
|
42
|
+
# Si on rencontre un comportement A, on réinitialise le temps de fin du comportement A
|
|
43
|
+
last_A_end_time = row["end_time"]
|
|
44
|
+
subject_latency.append(None) # Pas de latence pour A
|
|
45
|
+
elif row["behavior"] == comb[1] and last_A_end_time is not None:
|
|
46
|
+
# Si on rencontre un comportement B et qu'on a déjà vu un A avant
|
|
47
|
+
latency_time = row["start_time"] - last_A_end_time
|
|
48
|
+
subject_latency.append(latency_time)
|
|
49
|
+
else:
|
|
50
|
+
# Si on rencontre un B mais sans A avant
|
|
51
|
+
subject_latency.append(None)
|
|
52
|
+
|
|
53
|
+
# Ajout des latences calculées au DataFrame
|
|
54
|
+
df.loc[group.index, f"latency {comb[1]} after {comb[0]}"] = subject_latency
|
|
55
|
+
|
|
56
|
+
# Calcul de la latence totale ou moyenne par sujet
|
|
57
|
+
latency_by_subject[(subject, comb)] = df.groupby("subject")["latency"].agg(["sum", "mean"])
|
|
58
|
+
|
|
59
|
+
return str(latency_by_subject)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS plugin
|
|
3
|
+
|
|
4
|
+
Inter Rater Reliability (IRR) Unweighted Cohen's Kappa
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from typing import Dict, Tuple
|
|
9
|
+
|
|
10
|
+
from sklearn.metrics import cohen_kappa_score
|
|
11
|
+
from PySide6.QtWidgets import QInputDialog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__version__ = "0.0.3"
|
|
15
|
+
__version_date__ = "2025-09-02"
|
|
16
|
+
__plugin_name__ = "Inter Rater Reliability - Unweighted Cohen's Kappa"
|
|
17
|
+
__author__ = "Olivier Friard - University of Torino - Italy"
|
|
18
|
+
__description__ = """
|
|
19
|
+
This plugin calculates Cohen's Kappa to measure inter-rater reliability between two observers who code categorical behaviors over time intervals.
|
|
20
|
+
Unlike the weighted version, this approach does not take into account the duration of the intervals.
|
|
21
|
+
Each segment of time is treated equally, regardless of how long it lasts.
|
|
22
|
+
This plugin does not take into account the modifiers.
|
|
23
|
+
|
|
24
|
+
How it works:
|
|
25
|
+
|
|
26
|
+
Time segmentation
|
|
27
|
+
The program identifies all the time boundaries (start and end points) used by both observers.
|
|
28
|
+
These boundaries are merged into a common timeline, which is then divided into a set of non-overlapping elementary intervals.
|
|
29
|
+
|
|
30
|
+
Assigning codes
|
|
31
|
+
For each elementary interval, the program determines which behavior was coded by each observer.
|
|
32
|
+
|
|
33
|
+
Comparison of codes
|
|
34
|
+
The program builds two parallel lists of behavior codes, one for each observer.
|
|
35
|
+
Each elementary interval is counted as one unit of observation, no matter how long the interval actually lasts.
|
|
36
|
+
|
|
37
|
+
Cohen's Kappa calculation
|
|
38
|
+
Using these two lists, the program computes Cohen's Kappa using the cohen_kappa_score function of the sklearn package.
|
|
39
|
+
(see https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html for details)
|
|
40
|
+
This coefficient measures how much the observers agree on their coding, adjusted for the amount of agreement that would be expected by chance.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run(df: pd.DataFrame) -> pd.DataFrame:
|
|
46
|
+
"""
|
|
47
|
+
Calculate the Inter Rater Reliability - Unweighted Cohen's Kappa
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Attribute all active codes for each interval
|
|
51
|
+
def get_code(t_start, obs):
|
|
52
|
+
active_codes = [seg[2] for seg in obs if seg[0] <= t_start < seg[1]]
|
|
53
|
+
if not active_codes:
|
|
54
|
+
return ""
|
|
55
|
+
# Sort to ensure deterministic representation (e.g., "A+B" instead of "B+A")
|
|
56
|
+
return "+".join(sorted(active_codes))
|
|
57
|
+
|
|
58
|
+
# ask user for the number of decimal places for rounding (can be negative)
|
|
59
|
+
round_decimals, ok = QInputDialog.getInt(
|
|
60
|
+
None, "Rounding", "Enter the number of decimal places for rounding (can be negative)", value=3, minValue=-5, maxValue=3, step=1
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# round times
|
|
64
|
+
df["Start (s)"] = df["Start (s)"].round(round_decimals)
|
|
65
|
+
df["Stop (s)"] = df["Stop (s)"].round(round_decimals)
|
|
66
|
+
|
|
67
|
+
# Get unique values
|
|
68
|
+
unique_obs_list = df["Observation id"].unique().tolist()
|
|
69
|
+
|
|
70
|
+
# Convert to tuples grouped by observation
|
|
71
|
+
grouped = {
|
|
72
|
+
obs: [
|
|
73
|
+
(row[0], row[1], row[2] + "|" + row[3]) # concatenate subject and behavior with |
|
|
74
|
+
for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"]].itertuples(index=False, name=None)
|
|
75
|
+
]
|
|
76
|
+
for obs, group in df.groupby("Observation id")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ck_results: Dict[Tuple[str, str], str] = {}
|
|
80
|
+
for idx1, obs_id1 in enumerate(unique_obs_list):
|
|
81
|
+
obs1 = grouped[obs_id1]
|
|
82
|
+
|
|
83
|
+
# Perfect agreement with itself
|
|
84
|
+
ck_results[(obs_id1, obs_id1)] = "1.000"
|
|
85
|
+
|
|
86
|
+
for obs_id2 in unique_obs_list[idx1 + 1 :]:
|
|
87
|
+
obs2 = grouped[obs_id2]
|
|
88
|
+
|
|
89
|
+
# get all the break points
|
|
90
|
+
time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
|
|
91
|
+
|
|
92
|
+
# elementary intervals
|
|
93
|
+
elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
|
|
94
|
+
|
|
95
|
+
obs1_codes = [get_code(t[0], obs1) for t in elementary_intervals]
|
|
96
|
+
|
|
97
|
+
obs2_codes = [get_code(t[0], obs2) for t in elementary_intervals]
|
|
98
|
+
|
|
99
|
+
# Cohen's Kappa
|
|
100
|
+
kappa = cohen_kappa_score(obs1_codes, obs2_codes)
|
|
101
|
+
print(f"{obs_id1} - {obs_id2}: Cohen's Kappa : {kappa:.3f}")
|
|
102
|
+
|
|
103
|
+
ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
|
|
104
|
+
ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
|
|
105
|
+
|
|
106
|
+
# DataFrame conversion
|
|
107
|
+
df_results = pd.Series(ck_results).unstack()
|
|
108
|
+
|
|
109
|
+
return df_results
|