boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.1__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 +24 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +235 -131
- boris/advanced_event_filtering.py +23 -29
- 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 +228 -229
- 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 +16 -34
- boris/config.py +101 -49
- boris/config_file.py +55 -64
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2108 -1275
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +134 -0
- 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 +304 -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 +127 -36
- boris/observation.py +493 -210
- boris/observation_operations.py +1010 -391
- 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 +18 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +80 -56
- boris/plot_waveform_rt.py +23 -48
- boris/plugins.py +431 -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 +298 -123
- boris/preferences_ui.py +664 -225
- boris/project.py +293 -270
- boris/project_functions.py +610 -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 +6 -8
- boris/synthetic_time_budget.py +25 -17
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +71 -86
- boris/transitions.py +41 -41
- boris/utilities.py +562 -222
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +78 -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.1.dist-info/METADATA +140 -0
- boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.1.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.6.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
- boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.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)
|
|
@@ -148,9 +150,7 @@ class timeBudgetResults(QWidget):
|
|
|
148
150
|
|
|
149
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
|
|
@@ -164,9 +164,7 @@ class timeBudgetResults(QWidget):
|
|
|
164
164
|
# check if file with new extension already exists
|
|
165
165
|
if pl.Path(file_name).is_file():
|
|
166
166
|
if (
|
|
167
|
-
dialog.MessageDialog(
|
|
168
|
-
cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
|
|
169
|
-
)
|
|
167
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
170
168
|
== cfg.CANCEL
|
|
171
169
|
):
|
|
172
170
|
return
|
|
@@ -206,9 +204,9 @@ class timeBudgetResults(QWidget):
|
|
|
206
204
|
for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
|
|
207
205
|
if self.lw.count() == 1:
|
|
208
206
|
# var has value in obs?
|
|
209
|
-
if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
):
|
|
212
210
|
col1.append(
|
|
213
211
|
self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
|
|
214
212
|
self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
|
|
@@ -392,6 +390,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
392
390
|
Args:
|
|
393
391
|
mode (str): ["by_behavior", "by_category"]
|
|
394
392
|
mode2 (str): must be in ["list", "current"]
|
|
393
|
+
"current" time budget of current observation
|
|
395
394
|
"""
|
|
396
395
|
|
|
397
396
|
if mode2 == "current":
|
|
@@ -418,9 +417,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
418
417
|
flagGroup: bool = False
|
|
419
418
|
if len(selected_observations) > 1:
|
|
420
419
|
flagGroup = (
|
|
421
|
-
dialog.MessageDialog(
|
|
422
|
-
cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO]
|
|
423
|
-
)
|
|
420
|
+
dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
|
|
424
421
|
== cfg.YES
|
|
425
422
|
)
|
|
426
423
|
|
|
@@ -428,20 +425,25 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
428
425
|
self.pj[cfg.OBSERVATIONS], selected_observations
|
|
429
426
|
)
|
|
430
427
|
|
|
431
|
-
logging.debug(
|
|
432
|
-
f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
|
|
433
|
-
)
|
|
428
|
+
logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
|
|
434
429
|
|
|
435
430
|
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
436
431
|
|
|
437
|
-
|
|
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(
|
|
438
435
|
self,
|
|
439
436
|
selected_observations,
|
|
440
437
|
start_coding=start_coding,
|
|
441
438
|
end_coding=end_coding,
|
|
439
|
+
# start_interval=start_interval,
|
|
440
|
+
# end_interval=end_interval,
|
|
441
|
+
start_interval=None,
|
|
442
|
+
end_interval=None,
|
|
442
443
|
maxTime=max_media_duration_all_obs,
|
|
443
444
|
by_category=(mode == "by_category"),
|
|
444
445
|
n_observations=len(selected_observations),
|
|
446
|
+
show_exclude_non_coded_modifiers=True,
|
|
445
447
|
)
|
|
446
448
|
if parameters == {}:
|
|
447
449
|
return
|
|
@@ -450,21 +452,28 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
450
452
|
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
451
453
|
return
|
|
452
454
|
|
|
455
|
+
logging.debug(f"{parameters=}")
|
|
456
|
+
|
|
453
457
|
# ask for excluding behaviors durations from total time
|
|
454
|
-
if not start_coding.is_nan():
|
|
458
|
+
if start_coding is not None and not start_coding.is_nan():
|
|
455
459
|
cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
|
|
456
460
|
title="Select behaviors to exclude from the total time",
|
|
457
|
-
text=("The duration of the selected behaviors will
|
|
461
|
+
text=("The duration of the selected behaviors will be subtracted from the total time"),
|
|
458
462
|
table="",
|
|
459
|
-
behavior_type=
|
|
463
|
+
behavior_type=cfg.STATE_EVENT_TYPES,
|
|
460
464
|
)
|
|
461
465
|
if cancel_pressed:
|
|
462
466
|
return
|
|
463
467
|
else:
|
|
464
468
|
parameters[cfg.EXCLUDED_BEHAVIORS] = []
|
|
465
469
|
|
|
470
|
+
self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
|
|
471
|
+
QApplication.processEvents()
|
|
472
|
+
|
|
466
473
|
# check if time_budget window must be used
|
|
467
474
|
if flagGroup or len(selected_observations) == 1:
|
|
475
|
+
t0 = time.time()
|
|
476
|
+
|
|
468
477
|
cursor = db_functions.load_events_in_db(
|
|
469
478
|
self.pj,
|
|
470
479
|
parameters[cfg.SELECTED_SUBJECTS],
|
|
@@ -483,7 +492,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
483
492
|
if obs_length == dec(-2): # images obs without time
|
|
484
493
|
parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
|
|
485
494
|
|
|
486
|
-
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
|
|
495
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
|
|
487
496
|
min_time = float(0)
|
|
488
497
|
# check if the last event is recorded after media file length
|
|
489
498
|
try:
|
|
@@ -494,7 +503,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
494
503
|
except Exception:
|
|
495
504
|
max_time = float(obs_length)
|
|
496
505
|
|
|
497
|
-
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
|
|
498
514
|
try:
|
|
499
515
|
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
|
|
500
516
|
except Exception:
|
|
@@ -512,8 +528,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
512
528
|
# check intervals
|
|
513
529
|
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
514
530
|
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
515
|
-
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:
|
|
516
533
|
continue
|
|
534
|
+
|
|
517
535
|
# extract modifiers
|
|
518
536
|
|
|
519
537
|
cursor.execute(
|
|
@@ -542,10 +560,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
542
560
|
% 2
|
|
543
561
|
):
|
|
544
562
|
cursor.execute(
|
|
545
|
-
(
|
|
546
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
547
|
-
"VALUES (?,?,?,?,?,?)"
|
|
548
|
-
),
|
|
563
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
549
564
|
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
550
565
|
)
|
|
551
566
|
|
|
@@ -562,10 +577,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
562
577
|
% 2
|
|
563
578
|
):
|
|
564
579
|
cursor.execute(
|
|
565
|
-
(
|
|
566
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
567
|
-
"VALUES (?,?,?,?,?,?)"
|
|
568
|
-
),
|
|
580
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
569
581
|
(obsId, subj, behav, "STATE", modifier[0], max_time),
|
|
570
582
|
)
|
|
571
583
|
try:
|
|
@@ -580,6 +592,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
580
592
|
"DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
|
|
581
593
|
(obsId, min_time, max_time),
|
|
582
594
|
)
|
|
595
|
+
try:
|
|
596
|
+
cursor.execute("COMMIT")
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
583
599
|
|
|
584
600
|
out, categories = time_budget_functions.time_budget_analysis(
|
|
585
601
|
self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
|
|
@@ -591,9 +607,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
591
607
|
if element["subject"] not in excl_behaviors_total_time:
|
|
592
608
|
excl_behaviors_total_time[element["subject"]] = 0
|
|
593
609
|
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
594
|
-
excl_behaviors_total_time[element["subject"]] += (
|
|
595
|
-
element["duration"] if not isinstance(element["duration"], str) else 0
|
|
596
|
-
)
|
|
610
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
|
|
597
611
|
|
|
598
612
|
# widget for results visualization
|
|
599
613
|
self.tb = timeBudgetResults(self.pj, self.config_param)
|
|
@@ -613,20 +627,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
613
627
|
if len(selected_observations) > 1:
|
|
614
628
|
if total_observation_time:
|
|
615
629
|
if self.timeFormat == cfg.HHMMSS:
|
|
616
|
-
self.tb.lbTotalObservedTime.setText(
|
|
617
|
-
f"Total observation length: {util.seconds2time(total_observation_time)}"
|
|
618
|
-
)
|
|
630
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
|
|
619
631
|
if self.timeFormat == cfg.S:
|
|
620
|
-
self.tb.lbTotalObservedTime.setText(
|
|
621
|
-
f"Total observation length: {float(total_observation_time):0.3f}"
|
|
622
|
-
)
|
|
632
|
+
self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
|
|
623
633
|
else:
|
|
624
634
|
self.tb.lbTotalObservedTime.setText("Total observation length: not available")
|
|
625
635
|
else:
|
|
626
636
|
if self.timeFormat == cfg.HHMMSS:
|
|
627
|
-
self.tb.lbTotalObservedTime.setText(
|
|
628
|
-
f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}"
|
|
629
|
-
)
|
|
637
|
+
self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
|
|
630
638
|
if self.timeFormat == cfg.S:
|
|
631
639
|
self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
|
|
632
640
|
|
|
@@ -638,6 +646,9 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
638
646
|
else:
|
|
639
647
|
self.tb.excluded_behaviors_list.setVisible(False)
|
|
640
648
|
|
|
649
|
+
self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
|
|
650
|
+
logging.debug("Time budget generated")
|
|
651
|
+
|
|
641
652
|
if mode == "by_behavior":
|
|
642
653
|
tb_fields = [
|
|
643
654
|
"Subject",
|
|
@@ -685,10 +696,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
685
696
|
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
686
697
|
tot_time = float(total_observation_time)
|
|
687
698
|
# substract time of excluded behaviors from the total for the subject
|
|
688
|
-
if
|
|
689
|
-
row["subject"] in excl_behaviors_total_time
|
|
690
|
-
and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
|
|
691
|
-
):
|
|
699
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
692
700
|
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
693
701
|
item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
|
|
694
702
|
|
|
@@ -738,7 +746,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
738
746
|
|
|
739
747
|
self.tb.twTB.resizeColumnsToContents()
|
|
740
748
|
|
|
741
|
-
gui_utilities.restore_geometry(self.tb, "time budget", (
|
|
749
|
+
gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
|
|
742
750
|
|
|
743
751
|
self.tb.show()
|
|
744
752
|
|
|
@@ -768,9 +776,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
768
776
|
if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
|
|
769
777
|
workbook = tablib.Databook()
|
|
770
778
|
|
|
771
|
-
wb_file_name, filter_ = QFileDialog(self).getSaveFileName(
|
|
772
|
-
self, "Save Time budget analysis", "", output_format
|
|
773
|
-
)
|
|
779
|
+
wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
|
|
774
780
|
if not wb_file_name:
|
|
775
781
|
return
|
|
776
782
|
|
|
@@ -779,9 +785,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
779
785
|
# check if file with new extension already exists
|
|
780
786
|
if pl.Path(wb_file_name).is_file():
|
|
781
787
|
if (
|
|
782
|
-
dialog.MessageDialog(
|
|
783
|
-
cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
|
|
784
|
-
)
|
|
788
|
+
dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
785
789
|
== cfg.CANCEL
|
|
786
790
|
):
|
|
787
791
|
return
|
|
@@ -827,16 +831,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
827
831
|
|
|
828
832
|
mem_command = ""
|
|
829
833
|
for obsId in selected_observations:
|
|
830
|
-
cursor = db_functions.load_events_in_db(
|
|
831
|
-
self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
|
|
832
|
-
)
|
|
834
|
+
cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
|
|
833
835
|
|
|
834
836
|
obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
|
|
835
837
|
|
|
836
838
|
if obs_length == -1:
|
|
837
839
|
obs_length = 0
|
|
838
840
|
|
|
839
|
-
if parameters[
|
|
841
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
|
|
840
842
|
min_time = float(0)
|
|
841
843
|
# check if the last event is recorded after media file length
|
|
842
844
|
try:
|
|
@@ -847,7 +849,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
847
849
|
except Exception:
|
|
848
850
|
max_time = float(obs_length)
|
|
849
851
|
|
|
850
|
-
if parameters[
|
|
852
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
|
|
851
853
|
try:
|
|
852
854
|
min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
|
|
853
855
|
except Exception:
|
|
@@ -857,19 +859,15 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
857
859
|
except Exception:
|
|
858
860
|
max_time = float(obs_length)
|
|
859
861
|
|
|
860
|
-
if parameters[
|
|
862
|
+
if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
|
|
861
863
|
min_time = float(parameters[cfg.START_TIME])
|
|
862
864
|
max_time = float(parameters[cfg.END_TIME])
|
|
863
865
|
|
|
864
866
|
# check intervals
|
|
865
867
|
for subj in parameters[cfg.SELECTED_SUBJECTS]:
|
|
866
868
|
for behav in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
867
|
-
if cfg.
|
|
868
|
-
behav, self.pj[cfg.ETHOGRAM]
|
|
869
|
-
): # self.eventType(behav).upper():
|
|
869
|
+
if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
|
|
870
870
|
continue
|
|
871
|
-
# extract modifiers
|
|
872
|
-
# if plot_parameters["include modifiers"]:
|
|
873
871
|
|
|
874
872
|
cursor.execute(
|
|
875
873
|
"SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
@@ -892,10 +890,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
892
890
|
% 2
|
|
893
891
|
):
|
|
894
892
|
cursor.execute(
|
|
895
|
-
(
|
|
896
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
897
|
-
"VALUES (?,?,?,?,?,?)"
|
|
898
|
-
),
|
|
893
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
899
894
|
(obsId, subj, behav, "STATE", modifier[0], min_time),
|
|
900
895
|
)
|
|
901
896
|
if (
|
|
@@ -911,10 +906,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
911
906
|
% 2
|
|
912
907
|
):
|
|
913
908
|
cursor.execute(
|
|
914
|
-
(
|
|
915
|
-
"INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
|
|
916
|
-
"VALUES (?,?,?,?,?,?)"
|
|
917
|
-
),
|
|
909
|
+
("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
|
|
918
910
|
(obsId, subj, behav, cfg.STATE, modifier[0], max_time),
|
|
919
911
|
)
|
|
920
912
|
try:
|
|
@@ -937,9 +929,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
937
929
|
if element["subject"] not in excl_behaviors_total_time:
|
|
938
930
|
excl_behaviors_total_time[element["subject"]] = 0
|
|
939
931
|
if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
940
|
-
excl_behaviors_total_time[element["subject"]] +=
|
|
941
|
-
element["duration"] if element["duration"] != "NA" else 0
|
|
942
|
-
)
|
|
932
|
+
excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
|
|
943
933
|
|
|
944
934
|
rows: list = []
|
|
945
935
|
col1: list = []
|
|
@@ -953,9 +943,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
953
943
|
indep_var_values: list = []
|
|
954
944
|
for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
|
|
955
945
|
indep_var_label.append(v["label"])
|
|
956
|
-
indep_var_values.append(
|
|
957
|
-
self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], "")
|
|
958
|
-
)
|
|
946
|
+
indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
|
|
959
947
|
|
|
960
948
|
header.extend(indep_var_label)
|
|
961
949
|
col1.extend(indep_var_values)
|
|
@@ -981,10 +969,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
|
|
|
981
969
|
elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
|
|
982
970
|
tot_time = float(max_time - min_time)
|
|
983
971
|
# substract duration of excluded behaviors from total time for each subject
|
|
984
|
-
if
|
|
985
|
-
row["subject"] in excl_behaviors_total_time
|
|
986
|
-
and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
|
|
987
|
-
):
|
|
972
|
+
if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
|
|
988
973
|
tot_time -= excl_behaviors_total_time[row["subject"]]
|
|
989
974
|
# % of tot time
|
|
990
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)
|