boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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 +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +266 -144
- boris/advanced_event_filtering.py +23 -29
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_export_to_feral.py +225 -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 +235 -236
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +19 -36
- boris/config.py +109 -50
- boris/config_file.py +58 -67
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2174 -1303
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +27 -7
- boris/dialog.py +461 -242
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +405 -281
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +180 -203
- boris/export_observation.py +60 -73
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +427 -218
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +16 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv2.py +128 -35
- boris/observation.py +501 -211
- boris/observation_operations.py +1037 -393
- boris/observation_ui.py +573 -363
- boris/observations_list.py +51 -58
- boris/otx_parser.py +74 -68
- boris/param_panel.py +45 -59
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +91 -56
- boris/plot_data_module.py +20 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +83 -56
- boris/plot_waveform_rt.py +27 -49
- boris/plugins.py +468 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +307 -123
- boris/preferences_ui.py +686 -227
- boris/project.py +294 -271
- boris/project_functions.py +626 -537
- boris/project_import_export.py +204 -213
- boris/project_ui.py +673 -441
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +62 -90
- boris/select_observations.py +19 -197
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +51 -33
- boris/subjects_pad.py +7 -9
- boris/synthetic_time_budget.py +42 -26
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +77 -89
- boris/transitions.py +41 -41
- boris/utilities.py +594 -226
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +86 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +240 -136
- boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -37
- boris/core.ui +0 -1571
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -982
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1074
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
- boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
boris/time_budget_widget.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -26,6 +26,7 @@ import pathlib as pl
|
|
|
26
26
|
from decimal import Decimal as dec
|
|
27
27
|
from io import StringIO
|
|
28
28
|
import pandas as pd
|
|
29
|
+
import time
|
|
29
30
|
|
|
30
31
|
try:
|
|
31
32
|
import pyreadr
|
|
@@ -36,8 +37,8 @@ except ModuleNotFoundError:
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
import tablib
|
|
39
|
-
from
|
|
40
|
-
from
|
|
40
|
+
from PySide6.QtCore import Qt
|
|
41
|
+
from PySide6.QtWidgets import (
|
|
41
42
|
QFileDialog,
|
|
42
43
|
QHBoxLayout,
|
|
43
44
|
QInputDialog,
|
|
@@ -51,6 +52,7 @@ from PyQt5.QtWidgets import (
|
|
|
51
52
|
QTableWidgetItem,
|
|
52
53
|
QVBoxLayout,
|
|
53
54
|
QWidget,
|
|
55
|
+
QApplication,
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
from . import config as cfg
|
|
@@ -110,7 +112,7 @@ class timeBudgetResults(QWidget):
|
|
|
110
112
|
spacerItem = QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
111
113
|
hbox2.addItem(spacerItem)
|
|
112
114
|
|
|
113
|
-
self.pbClose = QPushButton(
|
|
115
|
+
self.pbClose = QPushButton(cfg.CLOSE, clicked=self.close_clicked)
|
|
114
116
|
hbox2.addWidget(self.pbClose)
|
|
115
117
|
|
|
116
118
|
hbox.addLayout(hbox2)
|
|
@@ -146,24 +148,23 @@ class timeBudgetResults(QWidget):
|
|
|
146
148
|
|
|
147
149
|
logging.debug("save time budget results to file")
|
|
148
150
|
|
|
149
|
-
file_formats =
|
|
151
|
+
file_formats = (cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML, cfg.TEXT_FILE, cfg.PANDAS_DF, cfg.RDS)
|
|
150
152
|
|
|
151
|
-
file_name, filter_ = QFileDialog().getSaveFileName(
|
|
152
|
-
self, "Save Time budget analysis", "", ";;".join(file_formats)
|
|
153
|
-
)
|
|
153
|
+
file_name, filter_ = QFileDialog().getSaveFileName(self, "Save Time budget analysis", "", ";;".join(file_formats))
|
|
154
154
|
|
|
155
155
|
if not file_name:
|
|
156
156
|
return
|
|
157
157
|
|
|
158
158
|
# add correct file extension if not present
|
|
159
159
|
if pl.Path(file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
|
|
160
|
-
|
|
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))
|
|
161
164
|
# check if file with new extension already exists
|
|
162
165
|
if pl.Path(file_name).is_file():
|
|
163
166
|
if (
|
|
164
|
-
dialog.MessageDialog(
|
|
165
|
-
cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
|
|
166
|
-
)
|
|
167
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
167
168
|
== cfg.CANCEL
|
|
168
169
|
):
|
|
169
170
|
return
|
|
@@ -203,9 +204,9 @@ class timeBudgetResults(QWidget):
|
|
|
203
204
|
for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
|
|
204
205
|
if self.lw.count() == 1:
|
|
205
206
|
# var has value in obs?
|
|
206
|
-
if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
):
|
|
209
210
|
col1.append(
|
|
210
211
|
self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
|
|
211
212
|
self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
|
|
@@ -376,7 +377,7 @@ class timeBudgetResults(QWidget):
|
|
|
376
377
|
|
|
377
378
|
# write results
|
|
378
379
|
with open(file_name, "wb") as f:
|
|
379
|
-
if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML):
|
|
380
|
+
if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML, cfg.TEXT_FILE):
|
|
380
381
|
f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
|
|
381
382
|
if filter_ in (cfg.ODS, cfg.XLSX, cfg.XLS):
|
|
382
383
|
f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
|
|
@@ -389,6 +390,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
389
390
|
Args:
|
|
390
391
|
mode (str): ["by_behavior", "by_category"]
|
|
391
392
|
mode2 (str): must be in ["list", "current"]
|
|
393
|
+
"current" time budget of current observation
|
|
392
394
|
"""
|
|
393
395
|
|
|
394
396
|
if mode2 == "current":
|
|
@@ -415,9 +417,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
415
417
|
flagGroup: bool = False
|
|
416
418
|
if len(selected_observations) > 1:
|
|
417
419
|
flagGroup = (
|
|
418
|
-
dialog.MessageDialog(
|
|
419
|
-
cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO]
|
|
420
|
-
)
|
|
420
|
+
dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
|
|
421
421
|
== cfg.YES
|
|
422
422
|
)
|
|
423
423
|
|
|
@@ -425,20 +425,25 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
425
425
|
self.pj[cfg.OBSERVATIONS], selected_observations
|
|
426
426
|
)
|
|
427
427
|
|
|
428
|
-
logging.debug(
|
|
429
|
-
f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
|
|
430
|
-
)
|
|
428
|
+
logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
|
|
431
429
|
|
|
432
430
|
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
433
431
|
|
|
434
|
-
|
|
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
435
|
self,
|
|
436
436
|
selected_observations,
|
|
437
437
|
start_coding=start_coding,
|
|
438
438
|
end_coding=end_coding,
|
|
439
|
+
# start_interval=start_interval,
|
|
440
|
+
# end_interval=end_interval,
|
|
441
|
+
start_interval=None,
|
|
442
|
+
end_interval=None,
|
|
439
443
|
maxTime=max_media_duration_all_obs,
|
|
440
444
|
by_category=(mode == "by_category"),
|
|
441
445
|
n_observations=len(selected_observations),
|
|
446
|
+
show_exclude_non_coded_modifiers=True,
|
|
442
447
|
)
|
|
443
448
|
if parameters == {}:
|
|
444
449
|
return
|
|
@@ -447,21 +452,28 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
447
452
|
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
448
453
|
return
|
|
449
454
|
|
|
455
|
+
logging.debug(f"{parameters=}")
|
|
456
|
+
|
|
450
457
|
# ask for excluding behaviors durations from total time
|
|
451
|
-
if not start_coding.is_nan():
|
|
458
|
+
if start_coding is not None and not start_coding.is_nan():
|
|
452
459
|
cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
|
|
453
460
|
title="Select behaviors to exclude from the total time",
|
|
454
|
-
text=("The duration of the selected behaviors will
|
|
461
|
+
text=("The duration of the selected behaviors will be subtracted from the total time"),
|
|
455
462
|
table="",
|
|
456
|
-
behavior_type=
|
|
463
|
+
behavior_type=cfg.STATE_EVENT_TYPES,
|
|
457
464
|
)
|
|
458
465
|
if cancel_pressed:
|
|
459
466
|
return
|
|
460
467
|
else:
|
|
461
468
|
parameters[cfg.EXCLUDED_BEHAVIORS] = []
|
|
462
469
|
|
|
470
|
+
self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
|
|
471
|
+
QApplication.processEvents()
|
|
472
|
+
|
|
463
473
|
# check if time_budget window must be used
|
|
464
474
|
if flagGroup or len(selected_observations) == 1:
|
|
475
|
+
t0 = time.time()
|
|
476
|
+
|
|
465
477
|
cursor = db_functions.load_events_in_db(
|
|
466
478
|
self.pj,
|
|
467
479
|
parameters[cfg.SELECTED_SUBJECTS],
|
|
@@ -480,7 +492,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
480
492
|
if obs_length == dec(-2): # images obs without time
|
|
481
493
|
parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
|
|
482
494
|
|
|
483
|
-
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
|
|
495
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
|
|
484
496
|
min_time = float(0)
|
|
485
497
|
# check if the last event is recorded after media file length
|
|
486
498
|
try:
|
|
@@ -491,7 +503,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
491
503
|
except Exception:
|
|
492
504
|
max_time = float(obs_length)
|
|
493
505
|
|
|
494
|
-
if parameters[cfg.TIME_INTERVAL] == cfg.
|
|
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
|
|
495
514
|
try:
|
|
496
515
|
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
|
|
497
516
|
except Exception:
|
|
@@ -509,8 +528,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
509
528
|
# check intervals
|
|
510
529
|
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
511
530
|
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
512
|
-
if cfg.POINT in self.eventType(behav).upper():
|
|
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:
|
|
513
533
|
continue
|
|
534
|
+
|
|
514
535
|
# extract modifiers
|
|
515
536
|
|
|
516
537
|
cursor.execute(
|
|
@@ -539,10 +560,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
539
560
|
% 2
|
|
540
561
|
):
|
|
541
562
|
cursor.execute(
|
|
542
|
-
(
|
|
543
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
544
|
-
"VALUES (?,?,?,?,?,?)"
|
|
545
|
-
),
|
|
563
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
546
564
|
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
547
565
|
)
|
|
548
566
|
|
|
@@ -559,10 +577,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
559
577
|
% 2
|
|
560
578
|
):
|
|
561
579
|
cursor.execute(
|
|
562
|
-
(
|
|
563
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
564
|
-
"VALUES (?,?,?,?,?,?)"
|
|
565
|
-
),
|
|
580
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
566
581
|
(obsId, subj, behav, "STATE", modifier[0], max_time),
|
|
567
582
|
)
|
|
568
583
|
try:
|
|
@@ -577,6 +592,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
577
592
|
"DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
|
|
578
593
|
(obsId, min_time, max_time),
|
|
579
594
|
)
|
|
595
|
+
try:
|
|
596
|
+
cursor.execute("COMMIT")
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
580
599
|
|
|
581
600
|
out, categories = time_budget_functions.time_budget_analysis(
|
|
582
601
|
self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
|
|
@@ -588,9 +607,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
588
607
|
if element["subject"] not in excl_behaviors_total_time:
|
|
589
608
|
excl_behaviors_total_time[element["subject"]] = 0
|
|
590
609
|
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
591
|
-
excl_behaviors_total_time[element["subject"]] += (
|
|
592
|
-
element["duration"] if not isinstance(element["duration"], str) else 0
|
|
593
|
-
)
|
|
610
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
|
|
594
611
|
|
|
595
612
|
# widget for results visualization
|
|
596
613
|
self.tb = timeBudgetResults(self.pj, self.config_param)
|
|
@@ -610,20 +627,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
610
627
|
if len(selected_observations) > 1:
|
|
611
628
|
if total_observation_time:
|
|
612
629
|
if self.timeFormat == cfg.HHMMSS:
|
|
613
|
-
self.tb.lbTotalObservedTime.setText(
|
|
614
|
-
f"Total observation length: {util.seconds2time(total_observation_time)}"
|
|
615
|
-
)
|
|
630
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
|
|
616
631
|
if self.timeFormat == cfg.S:
|
|
617
|
-
self.tb.lbTotalObservedTime.setText(
|
|
618
|
-
f"Total observation length: {float(total_observation_time):0.3f}"
|
|
619
|
-
)
|
|
632
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
|
|
620
633
|
else:
|
|
621
634
|
self.tb.lbTotalObservedTime.setText("Total observation length: not available")
|
|
622
635
|
else:
|
|
623
636
|
if self.timeFormat == cfg.HHMMSS:
|
|
624
|
-
self.tb.lbTotalObservedTime.setText(
|
|
625
|
-
f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}"
|
|
626
|
-
)
|
|
637
|
+
self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
|
|
627
638
|
if self.timeFormat == cfg.S:
|
|
628
639
|
self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
|
|
629
640
|
|
|
@@ -635,6 +646,9 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
635
646
|
else:
|
|
636
647
|
self.tb.excluded_behaviors_list.setVisible(False)
|
|
637
648
|
|
|
649
|
+
self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
|
|
650
|
+
logging.debug("Time budget generated")
|
|
651
|
+
|
|
638
652
|
if mode == "by_behavior":
|
|
639
653
|
tb_fields = [
|
|
640
654
|
"Subject",
|
|
@@ -682,10 +696,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
682
696
|
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
683
697
|
tot_time = float(total_observation_time)
|
|
684
698
|
# substract time of excluded behaviors from the total for the subject
|
|
685
|
-
if
|
|
686
|
-
row["subject"] in excl_behaviors_total_time
|
|
687
|
-
and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
|
|
688
|
-
):
|
|
699
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
689
700
|
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
690
701
|
item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
|
|
691
702
|
|
|
@@ -735,7 +746,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
735
746
|
|
|
736
747
|
self.tb.twTB.resizeColumnsToContents()
|
|
737
748
|
|
|
738
|
-
gui_utilities.restore_geometry(self.tb, "time budget", (
|
|
749
|
+
gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
|
|
739
750
|
|
|
740
751
|
self.tb.show()
|
|
741
752
|
|
|
@@ -765,9 +776,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
765
776
|
if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
|
|
766
777
|
workbook = tablib.Databook()
|
|
767
778
|
|
|
768
|
-
wb_file_name, filter_ = QFileDialog(self).getSaveFileName(
|
|
769
|
-
self, "Save Time budget analysis", "", output_format
|
|
770
|
-
)
|
|
779
|
+
wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
|
|
771
780
|
if not wb_file_name:
|
|
772
781
|
return
|
|
773
782
|
|
|
@@ -776,9 +785,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
776
785
|
# check if file with new extension already exists
|
|
777
786
|
if pl.Path(wb_file_name).is_file():
|
|
778
787
|
if (
|
|
779
|
-
dialog.MessageDialog(
|
|
780
|
-
cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
|
|
781
|
-
)
|
|
788
|
+
dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
782
789
|
== cfg.CANCEL
|
|
783
790
|
):
|
|
784
791
|
return
|
|
@@ -824,16 +831,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
824
831
|
|
|
825
832
|
mem_command = ""
|
|
826
833
|
for obsId in selected_observations:
|
|
827
|
-
cursor = db_functions.load_events_in_db(
|
|
828
|
-
self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
|
|
829
|
-
)
|
|
834
|
+
cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
|
|
830
835
|
|
|
831
836
|
obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
|
|
832
837
|
|
|
833
838
|
if obs_length == -1:
|
|
834
839
|
obs_length = 0
|
|
835
840
|
|
|
836
|
-
if parameters[
|
|
841
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
|
|
837
842
|
min_time = float(0)
|
|
838
843
|
# check if the last event is recorded after media file length
|
|
839
844
|
try:
|
|
@@ -844,7 +849,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
844
849
|
except Exception:
|
|
845
850
|
max_time = float(obs_length)
|
|
846
851
|
|
|
847
|
-
if parameters[
|
|
852
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
|
|
848
853
|
try:
|
|
849
854
|
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
|
|
850
855
|
except Exception:
|
|
@@ -854,19 +859,15 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
854
859
|
except Exception:
|
|
855
860
|
max_time = float(obs_length)
|
|
856
861
|
|
|
857
|
-
if parameters[
|
|
862
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
|
|
858
863
|
min_time = float(parameters[cfg.START_TIME])
|
|
859
864
|
max_time = float(parameters[cfg.END_TIME])
|
|
860
865
|
|
|
861
866
|
# check intervals
|
|
862
867
|
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
863
868
|
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
864
|
-
if cfg.
|
|
865
|
-
behav, self.pj[cfg.ETHOGRAM]
|
|
866
|
-
): # self.eventType(behav).upper():
|
|
869
|
+
if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
|
|
867
870
|
continue
|
|
868
|
-
# extract modifiers
|
|
869
|
-
# if plot_parameters["include modifiers"]:
|
|
870
871
|
|
|
871
872
|
cursor.execute(
|
|
872
873
|
"SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
@@ -889,10 +890,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
889
890
|
% 2
|
|
890
891
|
):
|
|
891
892
|
cursor.execute(
|
|
892
|
-
(
|
|
893
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
894
|
-
"VALUES (?,?,?,?,?,?)"
|
|
895
|
-
),
|
|
893
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
896
894
|
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
897
895
|
)
|
|
898
896
|
if (
|
|
@@ -908,10 +906,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
908
906
|
% 2
|
|
909
907
|
):
|
|
910
908
|
cursor.execute(
|
|
911
|
-
(
|
|
912
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
913
|
-
"VALUES (?,?,?,?,?,?)"
|
|
914
|
-
),
|
|
909
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
915
910
|
(obsId, subj, behav, cfg.STATE, modifier[0], max_time),
|
|
916
911
|
)
|
|
917
912
|
try:
|
|
@@ -934,9 +929,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
934
929
|
if element["subject"] not in excl_behaviors_total_time:
|
|
935
930
|
excl_behaviors_total_time[element["subject"]] = 0
|
|
936
931
|
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
937
|
-
excl_behaviors_total_time[element["subject"]] +=
|
|
938
|
-
element["duration"] if element["duration"] != "NA" else 0
|
|
939
|
-
)
|
|
932
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
|
|
940
933
|
|
|
941
934
|
rows: list = []
|
|
942
935
|
col1: list = []
|
|
@@ -950,9 +943,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
950
943
|
indep_var_values: list = []
|
|
951
944
|
for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
|
|
952
945
|
indep_var_label.append(v["label"])
|
|
953
|
-
indep_var_values.append(
|
|
954
|
-
self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], "")
|
|
955
|
-
)
|
|
946
|
+
indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
|
|
956
947
|
|
|
957
948
|
header.extend(indep_var_label)
|
|
958
949
|
col1.extend(indep_var_values)
|
|
@@ -978,10 +969,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
978
969
|
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
979
970
|
tot_time = float(max_time - min_time)
|
|
980
971
|
# substract duration of excluded behaviors from total time for each subject
|
|
981
|
-
if
|
|
982
|
-
row["subject"] in excl_behaviors_total_time
|
|
983
|
-
and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
|
|
984
|
-
):
|
|
972
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
985
973
|
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
986
974
|
# % of tot time
|
|
987
975
|
values.append(round(row["duration"] / tot_time * 100, 1) if tot_time > 0 else cfg.NA)
|
boris/transitions.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -24,8 +24,9 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import subprocess
|
|
26
26
|
import tempfile
|
|
27
|
+
from pathlib import Path
|
|
27
28
|
|
|
28
|
-
from
|
|
29
|
+
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
|
29
30
|
|
|
30
31
|
from . import config as cfg
|
|
31
32
|
from . import dialog, export_observation, select_subj_behav
|
|
@@ -134,7 +135,7 @@ def create_transitions_gv_from_matrix(matrix, cutoff_all=0, cutoff_behavior=0, e
|
|
|
134
135
|
else:
|
|
135
136
|
transitions[row.split("\t")[0]][behaviours[idx]] = int(r)
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
"""transitions_total_number = sum([sum(transitions[x].values()) for x in transitions])"""
|
|
138
139
|
|
|
139
140
|
out = "digraph G { \n"
|
|
140
141
|
|
|
@@ -170,6 +171,8 @@ def transitions_matrix(self, mode):
|
|
|
170
171
|
* number
|
|
171
172
|
* frequencies_after_behaviors
|
|
172
173
|
"""
|
|
174
|
+
logging.debug("flag transitions_matrix function")
|
|
175
|
+
|
|
173
176
|
# ask user observations to analyze
|
|
174
177
|
_, selected_observations = select_observations.select_observations2(
|
|
175
178
|
self, cfg.MULTIPLE, windows_title="Select observations for transitions matrix"
|
|
@@ -181,8 +184,8 @@ def transitions_matrix(self, mode):
|
|
|
181
184
|
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
182
185
|
self,
|
|
183
186
|
selected_observations,
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
show_include_modifiers=True,
|
|
188
|
+
show_exclude_non_coded_behaviors=False,
|
|
186
189
|
n_observations=len(selected_observations),
|
|
187
190
|
)
|
|
188
191
|
|
|
@@ -194,20 +197,20 @@ def transitions_matrix(self, mode):
|
|
|
194
197
|
|
|
195
198
|
flagMulti = False
|
|
196
199
|
if len(parameters[cfg.SELECTED_SUBJECTS]) == 1:
|
|
197
|
-
|
|
200
|
+
file_name, _ = QFileDialog().getSaveFileName(
|
|
198
201
|
None,
|
|
199
202
|
"Create matrix of transitions " + mode,
|
|
200
203
|
"",
|
|
201
204
|
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
202
205
|
)
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
if not file_name:
|
|
207
|
+
return
|
|
205
208
|
else:
|
|
206
|
-
exportDir = QFileDialog
|
|
209
|
+
exportDir = QFileDialog.getExistingDirectory(
|
|
207
210
|
self,
|
|
208
211
|
"Choose a directory to save the transitions matrices",
|
|
209
|
-
|
|
210
|
-
options=QFileDialog
|
|
212
|
+
str(Path.home()),
|
|
213
|
+
options=QFileDialog.ShowDirsOnly,
|
|
211
214
|
)
|
|
212
215
|
if not exportDir:
|
|
213
216
|
return
|
|
@@ -220,9 +223,7 @@ def transitions_matrix(self, mode):
|
|
|
220
223
|
strings_list = []
|
|
221
224
|
for obs_id in selected_observations:
|
|
222
225
|
strings_list.append(
|
|
223
|
-
export_observation.events_to_behavioral_sequences(
|
|
224
|
-
self.pj, obs_id, subject, parameters, self.behav_seq_separator
|
|
225
|
-
)
|
|
226
|
+
export_observation.events_to_behavioral_sequences(self.pj, obs_id, subject, parameters, self.behav_seq_separator)
|
|
226
227
|
)
|
|
227
228
|
|
|
228
229
|
sequences, observed_behaviors = behavioral_strings_analysis(strings_list, self.behav_seq_separator)
|
|
@@ -258,11 +259,11 @@ def transitions_matrix(self, mode):
|
|
|
258
259
|
QMessageBox.critical(self, cfg.programName, f"The file {nf} can not be saved")
|
|
259
260
|
else:
|
|
260
261
|
try:
|
|
261
|
-
with open(
|
|
262
|
+
with open(file_name, "w") as outfile:
|
|
262
263
|
outfile.write(observed_matrix)
|
|
263
264
|
|
|
264
265
|
except Exception:
|
|
265
|
-
QMessageBox.critical(self, cfg.programName, f"The file {
|
|
266
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
|
|
266
267
|
|
|
267
268
|
|
|
268
269
|
def transitions_dot_script():
|
|
@@ -270,21 +271,18 @@ def transitions_dot_script():
|
|
|
270
271
|
create dot script (graphviz language) from transitions frequencies matrix
|
|
271
272
|
"""
|
|
272
273
|
|
|
273
|
-
|
|
274
|
+
file_names, _ = QFileDialog().getOpenFileNames(
|
|
274
275
|
None,
|
|
275
276
|
"Select one or more transitions matrix files",
|
|
276
277
|
"",
|
|
277
278
|
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
278
279
|
)
|
|
279
|
-
fileNames = fn[0] if type(fn) is tuple else fn
|
|
280
280
|
|
|
281
281
|
out = ""
|
|
282
282
|
|
|
283
|
-
for
|
|
284
|
-
with open(
|
|
285
|
-
result, gv = create_transitions_gv_from_matrix(
|
|
286
|
-
infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node"
|
|
287
|
-
)
|
|
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")
|
|
288
286
|
if result:
|
|
289
287
|
QMessageBox.critical(
|
|
290
288
|
None,
|
|
@@ -293,16 +291,24 @@ def transitions_dot_script():
|
|
|
293
291
|
)
|
|
294
292
|
return
|
|
295
293
|
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
298
304
|
|
|
299
|
-
out += f"<b>{
|
|
305
|
+
out += f"<b>{file_name}.gv</b> created<br>"
|
|
300
306
|
|
|
301
307
|
if out:
|
|
302
308
|
QMessageBox.information(
|
|
303
309
|
None,
|
|
304
310
|
cfg.programName,
|
|
305
|
-
(f"{out}<br><br>The DOT scripts can be used with Graphviz or WebGraphviz
|
|
311
|
+
(f"{out}<br><br>The DOT scripts can be used with the Graphviz package or WebGraphviz to generate diagram"),
|
|
306
312
|
)
|
|
307
313
|
|
|
308
314
|
|
|
@@ -326,20 +332,17 @@ def transitions_flow_diagram():
|
|
|
326
332
|
)
|
|
327
333
|
return
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
file_names, _ = QFileDialog().getOpenFileNames(
|
|
330
336
|
None,
|
|
331
337
|
"Select one or more transitions matrix files",
|
|
332
338
|
"",
|
|
333
339
|
"Transitions matrix files (*.txt *.tsv);;All files (*)",
|
|
334
340
|
)
|
|
335
|
-
fileNames = fn[0] if type(fn) is tuple else fn
|
|
336
341
|
|
|
337
342
|
out = ""
|
|
338
|
-
for
|
|
339
|
-
with open(
|
|
340
|
-
result, gv = create_transitions_gv_from_matrix(
|
|
341
|
-
infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node"
|
|
342
|
-
)
|
|
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")
|
|
343
346
|
if result:
|
|
344
347
|
QMessageBox.critical(
|
|
345
348
|
None,
|
|
@@ -348,18 +351,15 @@ def transitions_flow_diagram():
|
|
|
348
351
|
)
|
|
349
352
|
return
|
|
350
353
|
|
|
351
|
-
with open(tempfile.gettempdir() + os.sep + os.path.basename(
|
|
354
|
+
with open(tempfile.gettempdir() + os.sep + os.path.basename(file_name) + ".tmp.gv", "w") as f:
|
|
352
355
|
f.write(gv)
|
|
353
356
|
result = subprocess.getoutput(
|
|
354
|
-
(
|
|
355
|
-
f'dot -Tpng -o "{fileName}.png" '
|
|
356
|
-
f'"{tempfile.gettempdir() + os.sep + os.path.basename(fileName)}.tmp.gv"'
|
|
357
|
-
)
|
|
357
|
+
(f'dot -Tpng -o "{file_name}.png" "{tempfile.gettempdir() + os.sep + os.path.basename(file_name)}.tmp.gv"')
|
|
358
358
|
)
|
|
359
359
|
if not result:
|
|
360
|
-
out += f"<b>{
|
|
360
|
+
out += f"<b>{file_name}.png</b> created<br>"
|
|
361
361
|
else:
|
|
362
|
-
out += f"Problem with <b>{
|
|
362
|
+
out += f"Problem with <b>{file_name}</b><br>"
|
|
363
363
|
|
|
364
364
|
if out:
|
|
365
365
|
QMessageBox.information(None, cfg.programName, out)
|