boris-behav-obs 9.6.3__tar.gz → 9.6.5__tar.gz

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.
Files changed (129) hide show
  1. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/PKG-INFO +13 -10
  2. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/README.TXT +20 -22
  3. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/README.md +4 -1
  4. boris_behav_obs-9.6.5/boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  5. boris_behav_obs-9.6.5/boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  6. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/irr_weighted_cohen_kappa.py +47 -10
  7. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +47 -10
  8. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/config_file.py +0 -2
  9. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/export_events.py +5 -5
  10. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plugins.py +41 -3
  11. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/preferences.py +23 -7
  12. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/version.py +2 -2
  13. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris_behav_obs.egg-info/PKG-INFO +13 -10
  14. boris_behav_obs-9.6.5/boris_behav_obs.egg-info/requires.txt +18 -0
  15. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/pyproject.toml +9 -9
  16. boris_behav_obs-9.6.3/boris/analysis_plugins/irr_cohen_kappa.py +0 -72
  17. boris_behav_obs-9.6.3/boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +0 -77
  18. boris_behav_obs-9.6.3/boris_behav_obs.egg-info/requires.txt +0 -18
  19. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/LICENSE.TXT +0 -0
  20. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/MANIFEST.in +0 -0
  21. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/__init__.py +0 -0
  22. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/__main__.py +0 -0
  23. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/about.py +0 -0
  24. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/add_modifier.py +0 -0
  25. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/add_modifier_ui.py +0 -0
  26. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/advanced_event_filtering.py +0 -0
  27. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/__init__.py +0 -0
  28. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/_latency.py +0 -0
  29. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/list_of_dataframe_columns.py +0 -0
  30. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/number_of_occurences.py +0 -0
  31. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  32. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/analysis_plugins/time_budget.py +0 -0
  33. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/behav_coding_map_creator.py +0 -0
  34. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/behavior_binary_table.py +0 -0
  35. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/behaviors_coding_map.py +0 -0
  36. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/boris_cli.py +0 -0
  37. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/cmd_arguments.py +0 -0
  38. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/coding_pad.py +0 -0
  39. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/config.py +0 -0
  40. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/connections.py +0 -0
  41. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/converters.py +0 -0
  42. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/converters_ui.py +0 -0
  43. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/cooccurence.py +0 -0
  44. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/core.py +0 -0
  45. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/core_qrc.py +0 -0
  46. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/core_ui.py +0 -0
  47. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/db_functions.py +0 -0
  48. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/dev.py +0 -0
  49. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/dialog.py +0 -0
  50. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/duration_widget.py +0 -0
  51. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/edit_event.py +0 -0
  52. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/edit_event_ui.py +0 -0
  53. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/event_operations.py +0 -0
  54. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/events_cursor.py +0 -0
  55. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/events_snapshots.py +0 -0
  56. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/exclusion_matrix.py +0 -0
  57. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/export_observation.py +0 -0
  58. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/external_processes.py +0 -0
  59. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/geometric_measurement.py +0 -0
  60. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/gui_utilities.py +0 -0
  61. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/image_overlay.py +0 -0
  62. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/import_observations.py +0 -0
  63. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/irr.py +0 -0
  64. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/latency.py +0 -0
  65. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/measurement_widget.py +0 -0
  66. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/media_file.py +0 -0
  67. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/menu_options.py +0 -0
  68. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/modifier_coding_map_creator.py +0 -0
  69. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/modifiers_coding_map.py +0 -0
  70. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/mpv-1.0.3.py +0 -0
  71. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/mpv.py +0 -0
  72. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/mpv2.py +0 -0
  73. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/observation.py +0 -0
  74. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/observation_operations.py +0 -0
  75. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/observation_ui.py +0 -0
  76. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/observations_list.py +0 -0
  77. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/otx_parser.py +0 -0
  78. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/param_panel.py +0 -0
  79. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/param_panel_ui.py +0 -0
  80. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/player_dock_widget.py +0 -0
  81. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plot_data_module.py +0 -0
  82. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plot_events.py +0 -0
  83. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plot_events_rt.py +0 -0
  84. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plot_spectrogram_rt.py +0 -0
  85. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/plot_waveform_rt.py +0 -0
  86. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/__init__.py +0 -0
  87. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/const.py +0 -0
  88. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/dict.py +0 -0
  89. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/func.py +0 -0
  90. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/interval.py +0 -0
  91. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/portion/io.py +0 -0
  92. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/preferences_ui.py +0 -0
  93. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/project.py +0 -0
  94. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/project_functions.py +0 -0
  95. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/project_import_export.py +0 -0
  96. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/project_ui.py +0 -0
  97. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/qrc_boris.py +0 -0
  98. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/qrc_boris5.py +0 -0
  99. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/select_modifiers.py +0 -0
  100. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/select_observations.py +0 -0
  101. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/select_subj_behav.py +0 -0
  102. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/state_events.py +0 -0
  103. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/subjects_pad.py +0 -0
  104. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/synthetic_time_budget.py +0 -0
  105. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/time_budget_functions.py +0 -0
  106. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/time_budget_widget.py +0 -0
  107. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/transitions.py +0 -0
  108. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/utilities.py +0 -0
  109. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/video_equalizer.py +0 -0
  110. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/video_equalizer_ui.py +0 -0
  111. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/video_operations.py +0 -0
  112. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/view_df.py +0 -0
  113. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/view_df_ui.py +0 -0
  114. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris/write_event.py +0 -0
  115. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris_behav_obs.egg-info/SOURCES.txt +0 -0
  116. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  117. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  118. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/boris_behav_obs.egg-info/top_level.txt +0 -0
  119. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/setup.cfg +0 -0
  120. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_db_functions.py +0 -0
  121. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_export_observation.py +0 -0
  122. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_irr.py +0 -0
  123. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_observation_gui.py +0 -0
  124. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_otx_parser.py +0 -0
  125. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_preferences_gui.py +0 -0
  126. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_project_functions.py +0 -0
  127. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_time_budget.py +0 -0
  128. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_utilities.py +0 -0
  129. {boris_behav_obs-9.6.3 → boris_behav_obs-9.6.5}/tests/test_utilities2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.6.3
3
+ Version: 9.6.5
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License-Expression: GPL-3.0-only
@@ -18,16 +18,16 @@ Classifier: Topic :: Scientific/Engineering
18
18
  Requires-Python: >=3.12
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.TXT
21
- Requires-Dist: exifread>=3.0.0
21
+ Requires-Dist: exifread==3.5.1
22
22
  Requires-Dist: numpy==2.3.2
23
- Requires-Dist: matplotlib>=3.3.3
24
- Requires-Dist: pandas>=2.2.2
25
- Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
26
- Requires-Dist: pyreadr
23
+ Requires-Dist: matplotlib==3.10.5
24
+ Requires-Dist: pandas==2.3.2
25
+ Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]==3.8.0
26
+ Requires-Dist: pyreadr==0.5.3
27
27
  Requires-Dist: pyside6==6.9
28
- Requires-Dist: hachoir>=3.3.0
29
- Requires-Dist: scipy>=1.15.3
30
- Requires-Dist: scikit-learn>=1.7.1
28
+ Requires-Dist: hachoir==3.3.0
29
+ Requires-Dist: scipy==1.16.1
30
+ Requires-Dist: scikit-learn==1.7.1
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: ruff; extra == "dev"
33
33
  Requires-Dist: pytest; extra == "dev"
@@ -49,7 +49,10 @@ You can not longer run BORIS natively on MacOS (since v.8). Some alternatives to
49
49
 
50
50
  It provides also some analysis tools like time budget and some plotting functions.
51
51
 
52
- The BORIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications.
52
+ <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
53
+
54
+
55
+ The BORIS paper has more than 2336 citations in peer-reviewed scientific publications.
53
56
 
54
57
 
55
58
 
@@ -1,22 +1,20 @@
1
- BORIS
2
- Behavioral Observation Research Interactive Software
3
-
4
- Copyright 2012-2023 Olivier Friard
5
-
6
- BORIS 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 3 of the License, or
9
- any later version.
10
-
11
- BORIS 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
-
1
+ BORIS
2
+ Behavioral Observation Research Interactive Software
3
+
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ BORIS 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 3 of the License, or
9
+ any later version.
10
+
11
+ BORIS 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
+
@@ -11,7 +11,10 @@ You can not longer run BORIS natively on MacOS (since v.8). Some alternatives to
11
11
 
12
12
  It provides also some analysis tools like time budget and some plotting functions.
13
13
 
14
- The BORIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications.
14
+ <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
15
+
16
+
17
+ The BORIS paper has more than 2336 citations in peer-reviewed scientific publications.
15
18
 
16
19
 
17
20
 
@@ -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
@@ -0,0 +1,112 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Inter Rater Reliability (IRR) Unweighted Cohen's Kappa with modifiers
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ from sklearn.metrics import cohen_kappa_score
10
+ from PySide6.QtWidgets import QInputDialog
11
+
12
+ __version__ = "0.0.3"
13
+ __version_date__ = "2025-09-02"
14
+ __plugin_name__ = "Inter Rater Reliability - Unweighted Cohen's Kappa with modifiers"
15
+ __author__ = "Olivier Friard - University of Torino - Italy"
16
+ __description__ = """
17
+ This plugin calculates Cohen's Kappa to measure inter-rater reliability between two observers who code categorical behaviors over time intervals.
18
+ Unlike the weighted version, this approach does not take into account the duration of the intervals.
19
+ Each segment of time is treated equally, regardless of how long it lasts.
20
+ This plugin takes into account the modifiers.
21
+
22
+
23
+ How it works:
24
+
25
+ Time segmentation
26
+ The program identifies all the time boundaries (start and end points) used by both observers.
27
+ These boundaries are merged into a common timeline, which is then divided into a set of non-overlapping elementary intervals.
28
+
29
+ Assigning codes
30
+ For each elementary interval, the program determines which behavior was coded by each observer.
31
+
32
+ Comparison of codes
33
+ The program builds two parallel lists of behavior codes, one for each observer.
34
+ Each elementary interval is counted as one unit of observation, no matter how long the interval actually lasts.
35
+
36
+ Cohen's Kappa calculation
37
+ Using these two lists, the program computes Cohen's Kappa using the cohen_kappa_score function of the sklearn package.
38
+ (see https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html for details)
39
+ This coefficient measures how much the observers agree on their coding, adjusted for the amount of agreement that would be expected by chance.
40
+
41
+ """
42
+
43
+
44
+ def run(df: pd.DataFrame):
45
+ """
46
+ Calculate the Inter Rater Reliability - Unweighted Cohen's Kappa with modifiers
47
+ """
48
+
49
+ # Attribute all active codes for each interval
50
+ def get_code(t_start, obs):
51
+ active_codes = [seg[2] for seg in obs if seg[0] <= t_start < seg[1]]
52
+ if not active_codes:
53
+ return ""
54
+ # Sort to ensure deterministic representation (e.g., "A+B" instead of "B+A")
55
+ return "+".join(sorted(active_codes))
56
+
57
+ # ask user for the number of decimal places for rounding (can be negative)
58
+ round_decimals, ok = QInputDialog.getInt(
59
+ None, "Rounding", "Enter the number of decimal places for rounding (can be negative)", value=3, minValue=-5, maxValue=3, step=1
60
+ )
61
+
62
+ # round times
63
+ df["Start (s)"] = df["Start (s)"].round(round_decimals)
64
+ df["Stop (s)"] = df["Stop (s)"].round(round_decimals)
65
+
66
+ # Get unique values
67
+ unique_obs_list = df["Observation id"].unique().tolist()
68
+
69
+ # Convert to tuples grouped by observation
70
+ grouped: dict = {}
71
+ modifiers: list = []
72
+ for col in df.columns:
73
+ if isinstance(col, tuple):
74
+ modifiers.append(col)
75
+
76
+ for obs, group in df.groupby("Observation id"):
77
+ o: list = []
78
+ for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"] + modifiers].itertuples(index=False, name=None):
79
+ modif_list = [row[i] for idx, i in enumerate(range(4, 4 + len(modifiers))) if modifiers[idx][0] == row[3]]
80
+ o.append((row[0], row[1], row[2] + "|" + row[3] + "|" + ",".join(modif_list)))
81
+ grouped[obs] = o
82
+
83
+ ck_results: dict = {}
84
+ for idx1, obs_id1 in enumerate(unique_obs_list):
85
+ obs1 = grouped[obs_id1]
86
+
87
+ ck_results[(obs_id1, obs_id1)] = "1.000"
88
+
89
+ for obs_id2 in unique_obs_list[idx1 + 1 :]:
90
+ obs2 = grouped[obs_id2]
91
+
92
+ # get all the break points
93
+ time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
94
+
95
+ # elementary intervals
96
+ elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
97
+
98
+ obs1_codes = [get_code(t[0], obs1) for t in elementary_intervals]
99
+
100
+ obs2_codes = [get_code(t[0], obs2) for t in elementary_intervals]
101
+
102
+ # Cohen's Kappa
103
+ kappa = cohen_kappa_score(obs1_codes, obs2_codes)
104
+ print(f"{obs_id1} - {obs_id2}: Cohen's Kappa : {kappa:.3f}")
105
+
106
+ ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
107
+ ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
108
+
109
+ # DataFrame conversion
110
+ df_results = pd.Series(ck_results).unstack()
111
+
112
+ return df_results
@@ -1,21 +1,49 @@
1
1
  """
2
2
  BORIS plugin
3
3
 
4
- Inter Rater Reliability (IRR) Weighted Cohen Kappa
4
+ Inter Rater Reliability (IRR) Weighted Cohen's Kappa
5
5
  """
6
6
 
7
7
  import pandas as pd
8
8
  from typing import List, Tuple, Dict, Optional
9
9
 
10
- __version__ = "0.0.1"
11
- __version_date__ = "2025-08-25"
12
- __plugin_name__ = "Inter Rater Reliability - Weighted Cohen Kappa"
10
+ from PySide6.QtWidgets import QInputDialog
11
+
12
+ __version__ = "0.0.3"
13
+ __version_date__ = "2025-09-02"
14
+ __plugin_name__ = "Inter Rater Reliability - Weighted Cohen's Kappa"
13
15
  __author__ = "Olivier Friard - University of Torino - Italy"
16
+ __description__ = """
17
+ This plugin calculates Cohen's Kappa to measure inter-rater reliability between two observers who code categorical behaviors over time intervals.
18
+ Unlike the unweighted version, this approach takes into account the duration of each coded interval, giving more weight to longer intervals in the agreement calculation.
19
+ This plugin does not take into account the modifiers.
20
+
21
+ How it works:
22
+
23
+ Time segmentation
24
+ The program collects all the time boundaries from both observers and merges them into a unified set of time points.
25
+ These define a set of non-overlapping elementary intervals covering the entire observed period.
26
+
27
+ Assigning codes
28
+ For each elementary interval, the program identifies the behavior category assigned by each observer.
29
+
30
+ Weighted contingency table
31
+ Instead of treating each interval equally, the program assigns a weight equal to the duration of the interval.
32
+ These durations are accumulated in a contingency table that records how much time was spent in each combination of categories across the two observers.
33
+
34
+ Agreement calculation
35
+
36
+ Observed agreement (po): The proportion of total time where both observers assigned the same category.
37
+
38
+ Expected agreement (pe): The proportion of agreement expected by chance, based on the time-weighted marginal distributions of each observer's coding.
39
+
40
+ Cohen's Kappa (κ): Computed from the weighted observed and expected agreements.
41
+ """
14
42
 
15
43
 
16
44
  def run(df: pd.DataFrame):
17
45
  """
18
- Calculate the Inter Rater Reliability - Weighted Cohen Kappa
46
+ Calculate the Inter Rater Reliability - Weighted Cohen's Kappa
19
47
  """
20
48
 
21
49
  def cohen_kappa_weighted_by_time(
@@ -41,12 +69,12 @@ def run(df: pd.DataFrame):
41
69
  # 2. Build elementary intervals (non-overlapping time bins)
42
70
  elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
43
71
 
44
- # 3. Helper: get the active code for an observer at a given time
72
+ # 3. # Attribute all active codes for each interval
45
73
  def get_code(t: float, obs: List[Tuple[float, float, str]]) -> Optional[str]:
46
- for seg in obs:
47
- if seg[0] <= t < seg[1]:
48
- return seg[2]
49
- return None # in case no segment covers this time
74
+ active_codes = [seg[2] for seg in obs if seg[0] <= t < seg[1]]
75
+ if not active_codes:
76
+ return None
77
+ return "+".join(sorted(active_codes))
50
78
 
51
79
  # 4. Build weighted contingency table (durations instead of counts)
52
80
  contingency: Dict[Tuple[Optional[str], Optional[str]], float] = {}
@@ -78,6 +106,15 @@ def run(df: pd.DataFrame):
78
106
 
79
107
  return kappa, po, pe, contingency
80
108
 
109
+ # ask user for the number of decimal places for rounding (can be negative)
110
+ round_decimals, ok = QInputDialog.getInt(
111
+ None, "Rounding", "Enter the number of decimal places for rounding (can be negative)", value=3, minValue=-5, maxValue=3, step=1
112
+ )
113
+
114
+ # round times
115
+ df["Start (s)"] = df["Start (s)"].round(round_decimals)
116
+ df["Stop (s)"] = df["Stop (s)"].round(round_decimals)
117
+
81
118
  # Get unique values as a numpy array
82
119
  unique_obs = df["Observation id"].unique()
83
120
 
@@ -1,21 +1,49 @@
1
1
  """
2
2
  BORIS plugin
3
3
 
4
- Inter Rater Reliability (IRR) Weighted Cohen Kappa with modifiers
4
+ Inter Rater Reliability (IRR) Weighted Cohen's Kappa with modifiers
5
5
  """
6
6
 
7
7
  import pandas as pd
8
8
  from typing import List, Tuple, Dict, Optional
9
9
 
10
- __version__ = "0.0.1"
11
- __version_date__ = "2025-08-25"
12
- __plugin_name__ = "Inter Rater Reliability - Weighted Cohen Kappa with modifiers"
10
+ from PySide6.QtWidgets import QInputDialog
11
+
12
+ __version__ = "0.0.3"
13
+ __version_date__ = "2025-09-02"
14
+ __plugin_name__ = "Inter Rater Reliability - Weighted Cohen's Kappa with modifiers"
13
15
  __author__ = "Olivier Friard - University of Torino - Italy"
16
+ __description__ = """
17
+ This plugin calculates Cohen's Kappa to measure inter-rater reliability between two observers who code categorical behaviors over time intervals.
18
+ Unlike the unweighted version, this approach takes into account the duration of each coded interval, giving more weight to longer intervals in the agreement calculation.
19
+ This plugin takes into account the modifiers.
20
+
21
+ How it works:
22
+
23
+ Time segmentation
24
+ The program collects all the time boundaries from both observers and merges them into a unified set of time points.
25
+ These define a set of non-overlapping elementary intervals covering the entire observed period.
26
+
27
+ Assigning codes
28
+ For each elementary interval, the program identifies the behavior category assigned by each observer.
29
+
30
+ Weighted contingency table
31
+ Instead of treating each interval equally, the program assigns a weight equal to the duration of the interval.
32
+ These durations are accumulated in a contingency table that records how much time was spent in each combination of categories across the two observers.
33
+
34
+ Agreement calculation
35
+
36
+ Observed agreement (po): The proportion of total time where both observers assigned the same category.
37
+
38
+ Expected agreement (pe): The proportion of agreement expected by chance, based on the time-weighted marginal distributions of each observer's coding.
39
+
40
+ Cohen's Kappa (κ): Computed from the weighted observed and expected agreements.
41
+ """
14
42
 
15
43
 
16
44
  def run(df: pd.DataFrame):
17
45
  """
18
- Calculate the Inter Rater Reliability - Weighted Cohen Kappa with modifiers
46
+ Calculate the Inter Rater Reliability - Weighted Cohen's Kappa with modifiers
19
47
  """
20
48
 
21
49
  def cohen_kappa_weighted_by_time(
@@ -41,12 +69,12 @@ def run(df: pd.DataFrame):
41
69
  # 2. Build elementary intervals (non-overlapping time bins)
42
70
  elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
43
71
 
44
- # 3. Helper: get the active code for an observer at a given time
72
+ # 3. Attribute all active codes for each interval
45
73
  def get_code(t: float, obs: List[Tuple[float, float, str]]) -> Optional[str]:
46
- for seg in obs:
47
- if seg[0] <= t < seg[1]:
48
- return seg[2]
49
- return None # in case no segment covers this time
74
+ active_codes = [seg[2] for seg in obs if seg[0] <= t < seg[1]]
75
+ if not active_codes:
76
+ return None
77
+ return "+".join(sorted(active_codes))
50
78
 
51
79
  # 4. Build weighted contingency table (durations instead of counts)
52
80
  contingency: Dict[Tuple[Optional[str], Optional[str]], float] = {}
@@ -78,6 +106,15 @@ def run(df: pd.DataFrame):
78
106
 
79
107
  return kappa, po, pe, contingency
80
108
 
109
+ # ask user for the number of decimal places for rounding (can be negative)
110
+ round_decimals, ok = QInputDialog.getInt(
111
+ None, "Rounding", "Enter the number of decimal places for rounding (can be negative)", value=3, minValue=-5, maxValue=3, step=1
112
+ )
113
+
114
+ # round times
115
+ df["Start (s)"] = df["Start (s)"].round(round_decimals)
116
+ df["Stop (s)"] = df["Stop (s)"].round(round_decimals)
117
+
81
118
  # Get unique values as a numpy array
82
119
  unique_obs = df["Observation id"].unique()
83
120
 
@@ -169,8 +169,6 @@ def read(self):
169
169
  # check for new version
170
170
  self.checkForNewVersion = False
171
171
 
172
- # print(f"{self.no_first_launch_dialog=}")
173
-
174
172
  if not self.no_first_launch_dialog:
175
173
  try:
176
174
  if settings.value("check_for_new_version") is None:
@@ -979,12 +979,12 @@ def export_events_as_textgrid(self) -> None:
979
979
  continue
980
980
 
981
981
  # check if file already exists
982
- if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid").is_file():
982
+ if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid").is_file():
983
983
  if mem_command == cfg.SKIP_ALL:
984
984
  continue
985
985
  mem_command = dialog.MessageDialog(
986
986
  cfg.programName,
987
- f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid</b> already exists.",
987
+ f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid</b> already exists.",
988
988
  [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
989
989
  )
990
990
  if mem_command == cfg.CANCEL:
@@ -993,13 +993,13 @@ def export_events_as_textgrid(self) -> None:
993
993
  continue
994
994
 
995
995
  try:
996
- with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid", "w") as f:
996
+ with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid", "w") as f:
997
997
  f.write(out)
998
998
  file_count += 1
999
- self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid was created.")
999
+ self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid was created.")
1000
1000
  QApplication.processEvents()
1001
1001
  except Exception:
1002
- self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid can not be created.")
1002
+ self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid can not be created.")
1003
1003
  QApplication.processEvents()
1004
1004
 
1005
1005
  self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")
@@ -105,9 +105,11 @@ def get_r_plugin_description(plugin_path: str) -> str | None:
105
105
 
106
106
  def load_plugins(self):
107
107
  """
108
- load selected plugins in analysis menu
108
+ load selected plugins in config_param
109
109
  """
110
110
 
111
+ logging.debug("Loading plugins")
112
+
111
113
  def msg():
112
114
  QMessageBox.warning(
113
115
  self,
@@ -124,7 +126,25 @@ def load_plugins(self):
124
126
  for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
125
127
  if file_.name.startswith("_"):
126
128
  continue
127
- plugin_name = get_plugin_name(file_)
129
+
130
+ logging.debug(f"Loading plugin: {Path(file_).stem}")
131
+
132
+ # test module
133
+ module_name = Path(file_).stem
134
+ spec = importlib.util.spec_from_file_location(module_name, file_)
135
+ plugin_module = importlib.util.module_from_spec(spec)
136
+ spec.loader.exec_module(plugin_module)
137
+ attributes_list = dir(plugin_module)
138
+
139
+ if "__plugin_name__" in attributes_list:
140
+ plugin_name = plugin_module.__plugin_name__
141
+ else:
142
+ continue
143
+
144
+ if "run" not in attributes_list:
145
+ continue
146
+
147
+ # plugin_name = get_plugin_name(file_)
128
148
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
129
149
  # check if plugin with same name already loaded
130
150
  if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
@@ -138,7 +158,25 @@ def load_plugins(self):
138
158
  for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
139
159
  if file_.name.startswith("_"):
140
160
  continue
141
- plugin_name = get_plugin_name(file_)
161
+
162
+ logging.debug(f"Loading personal plugin: {Path(file_).stem}")
163
+
164
+ # test module
165
+ module_name = Path(file_).stem
166
+ spec = importlib.util.spec_from_file_location(module_name, file_)
167
+ plugin_module = importlib.util.module_from_spec(spec)
168
+ spec.loader.exec_module(plugin_module)
169
+ attributes_list = dir(plugin_module)
170
+
171
+ if "__plugin_name__" in attributes_list:
172
+ plugin_name = plugin_module.__plugin_name__
173
+ else:
174
+ continue
175
+
176
+ if "run" not in attributes_list:
177
+ continue
178
+
179
+ # plugin_name = get_plugin_name(file_)
142
180
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
143
181
  # check if plugin with same name already loaded
144
182
  if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]: