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
boris/transitions.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
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 subprocess
|
|
26
|
+
import tempfile
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
|
30
|
+
|
|
31
|
+
from . import config as cfg
|
|
32
|
+
from . import dialog, export_observation, select_subj_behav
|
|
33
|
+
from . import select_observations
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def behavioral_strings_analysis(strings, behav_seq_separator):
|
|
37
|
+
"""
|
|
38
|
+
Analyze behavioral strings
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
rows = strings[:]
|
|
42
|
+
sequences = []
|
|
43
|
+
for row in rows:
|
|
44
|
+
if behav_seq_separator:
|
|
45
|
+
r = row.strip().split(behav_seq_separator)
|
|
46
|
+
else:
|
|
47
|
+
r = list(row.strip())
|
|
48
|
+
|
|
49
|
+
sequences.append(r)
|
|
50
|
+
|
|
51
|
+
# extract unique behaviors
|
|
52
|
+
unique_behaviors = []
|
|
53
|
+
for seq in sequences:
|
|
54
|
+
for c in seq:
|
|
55
|
+
if c not in unique_behaviors:
|
|
56
|
+
unique_behaviors.append(c)
|
|
57
|
+
|
|
58
|
+
unique_behaviors.sort()
|
|
59
|
+
|
|
60
|
+
return sequences, unique_behaviors
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def observed_transitions_matrix(sequences, behaviours, mode="frequency") -> str:
|
|
64
|
+
"""
|
|
65
|
+
create the normalized matrix of observed transitions
|
|
66
|
+
mode:
|
|
67
|
+
* frequency:
|
|
68
|
+
* number
|
|
69
|
+
* frequencies_after_behaviors
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
logging.debug("function: observed_transitions_matrix")
|
|
73
|
+
logging.debug(f"behaviours: {behaviours}")
|
|
74
|
+
|
|
75
|
+
if "" in behaviours:
|
|
76
|
+
behaviours.remove("")
|
|
77
|
+
|
|
78
|
+
transitions = {}
|
|
79
|
+
for behaviour in behaviours:
|
|
80
|
+
if not behaviour:
|
|
81
|
+
continue
|
|
82
|
+
transitions[behaviour] = {}
|
|
83
|
+
for behaviour2 in behaviours:
|
|
84
|
+
transitions[behaviour][behaviour2] = 0
|
|
85
|
+
|
|
86
|
+
for seq in sequences:
|
|
87
|
+
for i in range(len(seq) - 1):
|
|
88
|
+
if not seq[i]:
|
|
89
|
+
continue
|
|
90
|
+
if seq[i] in behaviours and seq[i + 1] in behaviours:
|
|
91
|
+
transitions[seq[i]][seq[i + 1]] += 1
|
|
92
|
+
|
|
93
|
+
transitions_total_number = sum([sum(transitions[x].values()) for x in transitions])
|
|
94
|
+
|
|
95
|
+
if not transitions_total_number:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
out = "\t" + "\t".join(list(behaviours)) + "\n"
|
|
99
|
+
for behaviour in behaviours:
|
|
100
|
+
out += "{}\t".format(behaviour)
|
|
101
|
+
for behaviour2 in behaviours:
|
|
102
|
+
if mode == "frequency":
|
|
103
|
+
out += "{}\t".format(transitions[behaviour][behaviour2] / transitions_total_number)
|
|
104
|
+
elif mode == "number":
|
|
105
|
+
out += "{}\t".format(transitions[behaviour][behaviour2])
|
|
106
|
+
elif mode == "frequencies_after_behaviors":
|
|
107
|
+
if sum(transitions[behaviour].values()):
|
|
108
|
+
out += "{}\t".format(transitions[behaviour][behaviour2] / sum(transitions[behaviour].values()))
|
|
109
|
+
else:
|
|
110
|
+
out += "{}\t".format(transitions[behaviour][behaviour2])
|
|
111
|
+
out = out[:-1] + "\n"
|
|
112
|
+
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def create_transitions_gv_from_matrix(matrix, cutoff_all=0, cutoff_behavior=0, edge_label="percent_node") -> tuple:
|
|
117
|
+
"""
|
|
118
|
+
create code for GraphViz
|
|
119
|
+
matrix: matrix of frequency
|
|
120
|
+
edge_label: (percent_node, fraction_node)
|
|
121
|
+
return string containing graphviz code
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
behaviours = matrix.split("\n")[0].strip().split("\t")
|
|
125
|
+
transitions = {}
|
|
126
|
+
|
|
127
|
+
for row in matrix.split("\n")[1:]:
|
|
128
|
+
if not row:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
transitions[row.split("\t")[0]] = {}
|
|
132
|
+
for idx, r in enumerate(row.split("\t")[1:]):
|
|
133
|
+
if "." in r:
|
|
134
|
+
transitions[row.split("\t")[0]][behaviours[idx]] = float(r)
|
|
135
|
+
else:
|
|
136
|
+
transitions[row.split("\t")[0]][behaviours[idx]] = int(r)
|
|
137
|
+
|
|
138
|
+
"""transitions_total_number = sum([sum(transitions[x].values()) for x in transitions])"""
|
|
139
|
+
|
|
140
|
+
out = "digraph G { \n"
|
|
141
|
+
|
|
142
|
+
for behaviour1 in behaviours:
|
|
143
|
+
for behaviour2 in behaviours:
|
|
144
|
+
if behaviour1 not in transitions or behaviour2 not in transitions:
|
|
145
|
+
return True, "Error: the file does not seem a transition matrix"
|
|
146
|
+
if transitions[behaviour1][behaviour2]:
|
|
147
|
+
if edge_label == "percent_node":
|
|
148
|
+
if transitions[behaviour1][behaviour2] > cutoff_all:
|
|
149
|
+
out += '"{behaviour1}" -> "{behaviour2}" [label="{label:0.3f}"];\n'.format(
|
|
150
|
+
behaviour1=behaviour1, behaviour2=behaviour2, label=transitions[behaviour1][behaviour2]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if edge_label == "fraction_node":
|
|
154
|
+
transition_sum = sum(transitions[behaviour1].values())
|
|
155
|
+
if transitions[behaviour1][behaviour2] / transition_sum > cutoff_behavior:
|
|
156
|
+
out += """"{behaviour1}" -> "{behaviour2}" [label="{label}%"];\n""".format(
|
|
157
|
+
behaviour1=behaviour1,
|
|
158
|
+
behaviour2=behaviour2,
|
|
159
|
+
label=round(transitions[behaviour1][behaviour2] / transition_sum * 100, 1),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
out += "\n}"
|
|
163
|
+
return False, out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def transitions_matrix(self, mode):
|
|
167
|
+
"""
|
|
168
|
+
create transitions frequencies matrix with selected observations, subjects and behaviors
|
|
169
|
+
mode:
|
|
170
|
+
* frequency
|
|
171
|
+
* number
|
|
172
|
+
* frequencies_after_behaviors
|
|
173
|
+
"""
|
|
174
|
+
logging.debug("flag transitions_matrix function")
|
|
175
|
+
|
|
176
|
+
# ask user observations to analyze
|
|
177
|
+
_, selected_observations = select_observations.select_observations2(
|
|
178
|
+
self, cfg.MULTIPLE, windows_title="Select observations for transitions matrix"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not selected_observations:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
185
|
+
self,
|
|
186
|
+
selected_observations,
|
|
187
|
+
show_include_modifiers=True,
|
|
188
|
+
show_exclude_non_coded_behaviors=False,
|
|
189
|
+
n_observations=len(selected_observations),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if parameters == {}:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
flagMulti = False
|
|
199
|
+
if len(parameters[cfg.SELECTED_SUBJECTS]) == 1:
|
|
200
|
+
file_name, _ = QFileDialog().getSaveFileName(
|
|
201
|
+
None,
|
|
202
|
+
"Create matrix of transitions " + mode,
|
|
203
|
+
"",
|
|
204
|
+
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
205
|
+
)
|
|
206
|
+
if not file_name:
|
|
207
|
+
return
|
|
208
|
+
else:
|
|
209
|
+
exportDir = QFileDialog.getExistingDirectory(
|
|
210
|
+
self,
|
|
211
|
+
"Choose a directory to save the transitions matrices",
|
|
212
|
+
str(Path.home()),
|
|
213
|
+
options=QFileDialog.ShowDirsOnly,
|
|
214
|
+
)
|
|
215
|
+
if not exportDir:
|
|
216
|
+
return
|
|
217
|
+
flagMulti = True
|
|
218
|
+
|
|
219
|
+
flag_overwrite_all = False
|
|
220
|
+
for subject in parameters[cfg.SELECTED_SUBJECTS]:
|
|
221
|
+
logging.debug(f"subjects: {subject}")
|
|
222
|
+
|
|
223
|
+
strings_list = []
|
|
224
|
+
for obs_id in selected_observations:
|
|
225
|
+
strings_list.append(
|
|
226
|
+
export_observation.events_to_behavioral_sequences(self.pj, obs_id, subject, parameters, self.behav_seq_separator)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
sequences, observed_behaviors = behavioral_strings_analysis(strings_list, self.behav_seq_separator)
|
|
230
|
+
|
|
231
|
+
observed_matrix = observed_transitions_matrix(
|
|
232
|
+
sequences, sorted(list(set(observed_behaviors + parameters[cfg.SELECTED_BEHAVIORS]))), mode=mode
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not observed_matrix:
|
|
236
|
+
QMessageBox.warning(self, cfg.programName, f"No transitions found for <b>{subject}</b>")
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
logging.debug(f"observed_matrix {mode}:\n{observed_matrix}")
|
|
240
|
+
|
|
241
|
+
if flagMulti:
|
|
242
|
+
try:
|
|
243
|
+
nf = f"{exportDir}{os.sep}{subject}_transitions_{mode}_matrix.tsv"
|
|
244
|
+
|
|
245
|
+
if os.path.isfile(nf) and not flag_overwrite_all:
|
|
246
|
+
answer = dialog.MessageDialog(
|
|
247
|
+
cfg.programName,
|
|
248
|
+
f"A file with same name already exists.<br><b>{nf}</b>",
|
|
249
|
+
["Overwrite", "Overwrite all", cfg.CANCEL],
|
|
250
|
+
)
|
|
251
|
+
if answer == cfg.CANCEL:
|
|
252
|
+
continue
|
|
253
|
+
if answer == "Overwrite all":
|
|
254
|
+
flag_overwrite_all = True
|
|
255
|
+
|
|
256
|
+
with open(nf, "w") as outfile:
|
|
257
|
+
outfile.write(observed_matrix)
|
|
258
|
+
except Exception:
|
|
259
|
+
QMessageBox.critical(self, cfg.programName, f"The file {nf} can not be saved")
|
|
260
|
+
else:
|
|
261
|
+
try:
|
|
262
|
+
with open(file_name, "w") as outfile:
|
|
263
|
+
outfile.write(observed_matrix)
|
|
264
|
+
|
|
265
|
+
except Exception:
|
|
266
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def transitions_dot_script():
|
|
270
|
+
"""
|
|
271
|
+
create dot script (graphviz language) from transitions frequencies matrix
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
file_names, _ = QFileDialog().getOpenFileNames(
|
|
275
|
+
None,
|
|
276
|
+
"Select one or more transitions matrix files",
|
|
277
|
+
"",
|
|
278
|
+
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
out = ""
|
|
282
|
+
|
|
283
|
+
for file_name in file_names:
|
|
284
|
+
with open(file_name, "r") as infile:
|
|
285
|
+
result, gv = create_transitions_gv_from_matrix(infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node")
|
|
286
|
+
if result:
|
|
287
|
+
QMessageBox.critical(
|
|
288
|
+
None,
|
|
289
|
+
cfg.programName,
|
|
290
|
+
gv,
|
|
291
|
+
)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
with open(file_name + ".gv", "w") as file_out:
|
|
296
|
+
file_out.write(gv)
|
|
297
|
+
except Exception:
|
|
298
|
+
QMessageBox.critical(
|
|
299
|
+
None,
|
|
300
|
+
cfg.programName,
|
|
301
|
+
("Error saving the file"),
|
|
302
|
+
)
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
out += f"<b>{file_name}.gv</b> created<br>"
|
|
306
|
+
|
|
307
|
+
if out:
|
|
308
|
+
QMessageBox.information(
|
|
309
|
+
None,
|
|
310
|
+
cfg.programName,
|
|
311
|
+
(f"{out}<br><br>The DOT scripts can be used with the Graphviz package or WebGraphviz to generate diagram"),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def transitions_flow_diagram():
|
|
316
|
+
"""
|
|
317
|
+
create flow diagram with graphviz (if installed) from transitions matrix
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
# check if dot present in path
|
|
321
|
+
result = subprocess.getoutput("dot -V")
|
|
322
|
+
if "graphviz" not in result:
|
|
323
|
+
QMessageBox.critical(
|
|
324
|
+
None,
|
|
325
|
+
cfg.programName,
|
|
326
|
+
(
|
|
327
|
+
"The GraphViz package is not installed.<br>"
|
|
328
|
+
"The <b>dot</b> program was not found in the path.<br><br>"
|
|
329
|
+
'Go to <a href="http://www.graphviz.org">'
|
|
330
|
+
"http://www.graphviz.org</a> for information"
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
file_names, _ = QFileDialog().getOpenFileNames(
|
|
336
|
+
None,
|
|
337
|
+
"Select one or more transitions matrix files",
|
|
338
|
+
"",
|
|
339
|
+
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
out = ""
|
|
343
|
+
for file_name in file_names:
|
|
344
|
+
with open(file_name, "r") as infile:
|
|
345
|
+
result, gv = create_transitions_gv_from_matrix(infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node")
|
|
346
|
+
if result:
|
|
347
|
+
QMessageBox.critical(
|
|
348
|
+
None,
|
|
349
|
+
cfg.programName,
|
|
350
|
+
gv,
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
with open(tempfile.gettempdir() + os.sep + os.path.basename(file_name) + ".tmp.gv", "w") as f:
|
|
355
|
+
f.write(gv)
|
|
356
|
+
result = subprocess.getoutput(
|
|
357
|
+
(f'dot -Tpng -o "{file_name}.png" "{tempfile.gettempdir() + os.sep + os.path.basename(file_name)}.tmp.gv"')
|
|
358
|
+
)
|
|
359
|
+
if not result:
|
|
360
|
+
out += f"<b>{file_name}.png</b> created<br>"
|
|
361
|
+
else:
|
|
362
|
+
out += f"Problem with <b>{file_name}</b><br>"
|
|
363
|
+
|
|
364
|
+
if out:
|
|
365
|
+
QMessageBox.information(None, cfg.programName, out)
|