boris-behav-obs 9.6.1__py2.py3-none-any.whl → 9.6.2__py2.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/add_modifier.py CHANGED
@@ -123,11 +123,7 @@ class addModifierDialog(QDialog, Ui_Dialog):
123
123
  if (
124
124
  dialog.MessageDialog(
125
125
  cfg.programName,
126
- (
127
- "You are working on a behavior.<br>"
128
- "If you close the window it will be lost.<br>"
129
- "Do you want to change modifiers set"
130
- ),
126
+ ("You are working on a behavior.<br>If you close the window it will be lost.<br>Do you want to change modifiers set"),
131
127
  [cfg.CLOSE, cfg.CANCEL],
132
128
  )
133
129
  == cfg.CANCEL
@@ -0,0 +1,72 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Inter Rater Reliability (IRR) Unweighted Cohen Kappa
5
+ """
6
+
7
+ import pandas as pd
8
+ from sklearn.metrics import cohen_kappa_score
9
+
10
+ __version__ = "0.0.1"
11
+ __version_date__ = "2025-08-25"
12
+ __plugin_name__ = "Inter Rater Reliability - Unweighted Cohen Kappa"
13
+ __author__ = "Olivier Friard - University of Torino - Italy"
14
+
15
+
16
+ def run(df: pd.DataFrame):
17
+ """
18
+ Calculate the Inter Rater Reliability - Unweighted Cohen Kappa
19
+ """
20
+
21
+ # attribute a code for each interval
22
+ def get_code(t_start, obs):
23
+ for seg in obs:
24
+ if t_start >= seg[0] and t_start < seg[1]:
25
+ return seg[2]
26
+ return ""
27
+
28
+ # Get unique values as a numpy array
29
+ unique_obs = df["Observation id"].unique()
30
+
31
+ # Convert to a list
32
+ unique_obs_list = unique_obs.tolist()
33
+
34
+ # Convert to tuples grouped by observation
35
+ grouped = {
36
+ obs: [
37
+ (row[0], row[1], row[2] + "|" + row[3]) # concatenate subject and behavior with |
38
+ for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"]].itertuples(index=False, name=None)
39
+ ]
40
+ for obs, group in df.groupby("Observation id")
41
+ }
42
+
43
+ ck_results: dict = {}
44
+ for idx1, obs_id1 in enumerate(unique_obs_list):
45
+ obs1 = grouped[obs_id1]
46
+
47
+ ck_results[(obs_id1, obs_id1)] = "1.000"
48
+
49
+ for obs_id2 in unique_obs_list[idx1 + 1 :]:
50
+ obs2 = grouped[obs_id2]
51
+
52
+ # get all the break points
53
+ time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
54
+
55
+ # elementary intervals
56
+ elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
57
+
58
+ obs1_codes = [get_code(t[0], obs1) for t in elementary_intervals]
59
+
60
+ obs2_codes = [get_code(t[0], obs2) for t in elementary_intervals]
61
+
62
+ # Cohen's Kappa
63
+ kappa = cohen_kappa_score(obs1_codes, obs2_codes)
64
+ print(f"{obs_id1} - {obs_id2}: Cohen's Kappa : {kappa:.3f}")
65
+
66
+ ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
67
+ ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
68
+
69
+ # DataFrame conversion
70
+ df_results = pd.Series(ck_results).unstack()
71
+
72
+ return df_results
@@ -0,0 +1,77 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Inter Rater Reliability (IRR) Unweighted Cohen Kappa with modifiers
5
+ """
6
+
7
+ import pandas as pd
8
+ from sklearn.metrics import cohen_kappa_score
9
+
10
+ __version__ = "0.0.1"
11
+ __version_date__ = "2025-08-25"
12
+ __plugin_name__ = "Inter Rater Reliability - Unweighted Cohen Kappa with modifiers"
13
+ __author__ = "Olivier Friard - University of Torino - Italy"
14
+
15
+
16
+ def run(df: pd.DataFrame):
17
+ """
18
+ Calculate the Inter Rater Reliability - Unweighted Cohen Kappa with modifiers
19
+ """
20
+
21
+ # attribute a code for each interval
22
+ def get_code(t_start, obs):
23
+ for seg in obs:
24
+ if t_start >= seg[0] and t_start < seg[1]:
25
+ return seg[2]
26
+ return ""
27
+
28
+ # Get unique values as a numpy array
29
+ unique_obs = df["Observation id"].unique()
30
+
31
+ # Convert to a list
32
+ unique_obs_list = unique_obs.tolist()
33
+
34
+ # Convert to tuples grouped by observation
35
+ grouped: dict = {}
36
+ modifiers: list = []
37
+ for col in df.columns:
38
+ if isinstance(col, tuple):
39
+ modifiers.append(col)
40
+
41
+ for obs, group in df.groupby("Observation id"):
42
+ o: list = []
43
+ for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"] + modifiers].itertuples(index=False, name=None):
44
+ modif_list = [row[i] for idx, i in enumerate(range(4, 4 + len(modifiers))) if modifiers[idx][0] == row[3]]
45
+ o.append((row[0], row[1], row[2] + "|" + row[3] + "|" + ",".join(modif_list)))
46
+ grouped[obs] = o
47
+
48
+ ck_results: dict = {}
49
+ for idx1, obs_id1 in enumerate(unique_obs_list):
50
+ obs1 = grouped[obs_id1]
51
+
52
+ ck_results[(obs_id1, obs_id1)] = "1.000"
53
+
54
+ for obs_id2 in unique_obs_list[idx1 + 1 :]:
55
+ obs2 = grouped[obs_id2]
56
+
57
+ # get all the break points
58
+ time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
59
+
60
+ # elementary intervals
61
+ elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
62
+
63
+ obs1_codes = [get_code(t[0], obs1) for t in elementary_intervals]
64
+
65
+ obs2_codes = [get_code(t[0], obs2) for t in elementary_intervals]
66
+
67
+ # Cohen's Kappa
68
+ kappa = cohen_kappa_score(obs1_codes, obs2_codes)
69
+ print(f"{obs_id1} - {obs_id2}: Cohen's Kappa : {kappa:.3f}")
70
+
71
+ ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
72
+ ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
73
+
74
+ # DataFrame conversion
75
+ df_results = pd.Series(ck_results).unstack()
76
+
77
+ return df_results
@@ -0,0 +1,120 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Inter Rater Reliability (IRR) Weighted Cohen Kappa
5
+ """
6
+
7
+ import pandas as pd
8
+ from typing import List, Tuple, Dict, Optional
9
+
10
+ __version__ = "0.0.1"
11
+ __version_date__ = "2025-08-25"
12
+ __plugin_name__ = "Inter Rater Reliability - Weighted Cohen Kappa"
13
+ __author__ = "Olivier Friard - University of Torino - Italy"
14
+
15
+
16
+ def run(df: pd.DataFrame):
17
+ """
18
+ Calculate the Inter Rater Reliability - Weighted Cohen Kappa
19
+ """
20
+
21
+ def cohen_kappa_weighted_by_time(
22
+ obs1: List[Tuple[float, float, str]], obs2: List[Tuple[float, float, str]]
23
+ ) -> Tuple[float, float, float, Dict[Tuple[Optional[str], Optional[str]], float]]:
24
+ """
25
+ Compute Cohen's Kappa weighted by time duration.
26
+
27
+ Args:
28
+ obs1: List of (start_time, end_time, code) for observer 1
29
+ obs2: List of (start_time, end_time, code) for observer 2
30
+
31
+ Returns:
32
+ kappa (float): Cohen's Kappa weighted by duration
33
+ po (float): Observed agreement proportion (weighted)
34
+ pe (float): Expected agreement proportion by chance (weighted)
35
+ contingency (dict): Contingency table {(code1, code2): total_duration}
36
+ """
37
+
38
+ # 1. Collect all time boundaries from both observers
39
+ time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
40
+
41
+ # 2. Build elementary intervals (non-overlapping time bins)
42
+ elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
43
+
44
+ # 3. Helper: get the active code for an observer at a given time
45
+ 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
50
+
51
+ # 4. Build weighted contingency table (durations instead of counts)
52
+ contingency: Dict[Tuple[Optional[str], Optional[str]], float] = {}
53
+ total_time = 0.0
54
+
55
+ for start, end in elementary_intervals:
56
+ c1 = get_code(start, obs1)
57
+ c2 = get_code(start, obs2)
58
+ duration = end - start
59
+ total_time += duration
60
+ contingency[(c1, c2)] = contingency.get((c1, c2), 0.0) + duration
61
+
62
+ # 5. Observed agreement (po)
63
+ po = sum(duration for (c1, c2), duration in contingency.items() if c1 == c2) / total_time
64
+
65
+ # Marginal distributions for each observer
66
+ codes1: Dict[Optional[str], float] = {}
67
+ codes2: Dict[Optional[str], float] = {}
68
+ for (c1, c2), duration in contingency.items():
69
+ codes1[c1] = codes1.get(c1, 0.0) + duration
70
+ codes2[c2] = codes2.get(c2, 0.0) + duration
71
+
72
+ # 6. Expected agreement (pe), using marginal proportions
73
+ all_codes = set(codes1) | set(codes2)
74
+ pe = sum((codes1.get(c, 0.0) / total_time) * (codes2.get(c, 0.0) / total_time) for c in all_codes)
75
+
76
+ # 7. Kappa calculation
77
+ kappa = (po - pe) / (1 - pe) if (1 - pe) != 0 else 0.0
78
+
79
+ return kappa, po, pe, contingency
80
+
81
+ # Get unique values as a numpy array
82
+ unique_obs = df["Observation id"].unique()
83
+
84
+ # Convert to a list
85
+ unique_obs_list = unique_obs.tolist()
86
+
87
+ # Convert to tuples grouped by observation
88
+ grouped = {
89
+ obs: [
90
+ (row[0], row[1], row[2] + "|" + row[3]) # concatenate subject and behavior with |
91
+ for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"]].itertuples(index=False, name=None)
92
+ ]
93
+ for obs, group in df.groupby("Observation id")
94
+ }
95
+
96
+ ck_results: dict = {}
97
+ str_results: str = ""
98
+ for idx1, obs_id1 in enumerate(unique_obs_list):
99
+ obs1 = grouped[obs_id1]
100
+
101
+ ck_results[(obs_id1, obs_id1)] = "1.000"
102
+
103
+ for obs_id2 in unique_obs_list[idx1 + 1 :]:
104
+ obs2 = grouped[obs_id2]
105
+
106
+ # Cohen's Kappa
107
+ kappa, po, pe, table = cohen_kappa_weighted_by_time(obs1, obs2)
108
+
109
+ print(f"{obs_id1} - {obs_id2}: Cohen's Kappa: {kappa:.3f} Expected agreement: {pe:.3f} Observed agreement: {po:.3f}")
110
+ str_results += (
111
+ f"{obs_id1} - {obs_id2}: Cohen's Kappa: {kappa:.3f} Expected agreement: {pe:.3f} Observed agreement: {po:.3f}\n"
112
+ )
113
+
114
+ ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
115
+ ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
116
+
117
+ # DataFrame conversion
118
+ df_results = pd.Series(ck_results).unstack()
119
+
120
+ return df_results, str_results
@@ -0,0 +1,125 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Inter Rater Reliability (IRR) Weighted Cohen Kappa with modifiers
5
+ """
6
+
7
+ import pandas as pd
8
+ from typing import List, Tuple, Dict, Optional
9
+
10
+ __version__ = "0.0.1"
11
+ __version_date__ = "2025-08-25"
12
+ __plugin_name__ = "Inter Rater Reliability - Weighted Cohen Kappa with modifiers"
13
+ __author__ = "Olivier Friard - University of Torino - Italy"
14
+
15
+
16
+ def run(df: pd.DataFrame):
17
+ """
18
+ Calculate the Inter Rater Reliability - Weighted Cohen Kappa with modifiers
19
+ """
20
+
21
+ def cohen_kappa_weighted_by_time(
22
+ obs1: List[Tuple[float, float, str]], obs2: List[Tuple[float, float, str]]
23
+ ) -> Tuple[float, float, float, Dict[Tuple[Optional[str], Optional[str]], float]]:
24
+ """
25
+ Compute Cohen's Kappa weighted by time duration with modifiers.
26
+
27
+ Args:
28
+ obs1: List of (start_time, end_time, code) for observer 1
29
+ obs2: List of (start_time, end_time, code) for observer 2
30
+
31
+ Returns:
32
+ kappa (float): Cohen's Kappa weighted by duration
33
+ po (float): Observed agreement proportion (weighted)
34
+ pe (float): Expected agreement proportion by chance (weighted)
35
+ contingency (dict): Contingency table {(code1, code2): total_duration}
36
+ """
37
+
38
+ # 1. Collect all time boundaries from both observers
39
+ time_points = sorted(set([t for seg in obs1 for t in seg[:2]] + [t for seg in obs2 for t in seg[:2]]))
40
+
41
+ # 2. Build elementary intervals (non-overlapping time bins)
42
+ elementary_intervals = [(time_points[i], time_points[i + 1]) for i in range(len(time_points) - 1)]
43
+
44
+ # 3. Helper: get the active code for an observer at a given time
45
+ 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
50
+
51
+ # 4. Build weighted contingency table (durations instead of counts)
52
+ contingency: Dict[Tuple[Optional[str], Optional[str]], float] = {}
53
+ total_time = 0.0
54
+
55
+ for start, end in elementary_intervals:
56
+ c1 = get_code(start, obs1)
57
+ c2 = get_code(start, obs2)
58
+ duration = end - start
59
+ total_time += duration
60
+ contingency[(c1, c2)] = contingency.get((c1, c2), 0.0) + duration
61
+
62
+ # 5. Observed agreement (po)
63
+ po = sum(duration for (c1, c2), duration in contingency.items() if c1 == c2) / total_time
64
+
65
+ # Marginal distributions for each observer
66
+ codes1: Dict[Optional[str], float] = {}
67
+ codes2: Dict[Optional[str], float] = {}
68
+ for (c1, c2), duration in contingency.items():
69
+ codes1[c1] = codes1.get(c1, 0.0) + duration
70
+ codes2[c2] = codes2.get(c2, 0.0) + duration
71
+
72
+ # 6. Expected agreement (pe), using marginal proportions
73
+ all_codes = set(codes1) | set(codes2)
74
+ pe = sum((codes1.get(c, 0.0) / total_time) * (codes2.get(c, 0.0) / total_time) for c in all_codes)
75
+
76
+ # 7. Kappa calculation
77
+ kappa = (po - pe) / (1 - pe) if (1 - pe) != 0 else 0.0
78
+
79
+ return kappa, po, pe, contingency
80
+
81
+ # Get unique values as a numpy array
82
+ unique_obs = df["Observation id"].unique()
83
+
84
+ # Convert to a list
85
+ unique_obs_list = unique_obs.tolist()
86
+
87
+ # Convert to tuples grouped by observation
88
+ grouped: dict = {}
89
+ modifiers: list = []
90
+ for col in df.columns:
91
+ if isinstance(col, tuple):
92
+ modifiers.append(col)
93
+
94
+ for obs, group in df.groupby("Observation id"):
95
+ o = []
96
+ for row in group[["Start (s)", "Stop (s)", "Subject", "Behavior"] + modifiers].itertuples(index=False, name=None):
97
+ modif_list = [row[i] for idx, i in enumerate(range(4, 4 + len(modifiers))) if modifiers[idx][0] == row[3]]
98
+ o.append((row[0], row[1], row[2] + "|" + row[3] + "|" + ",".join(modif_list)))
99
+ grouped[obs] = o
100
+
101
+ ck_results: dict = {}
102
+ str_results: str = ""
103
+ for idx1, obs_id1 in enumerate(unique_obs_list):
104
+ obs1 = grouped[obs_id1]
105
+
106
+ ck_results[(obs_id1, obs_id1)] = "1.000"
107
+
108
+ for obs_id2 in unique_obs_list[idx1 + 1 :]:
109
+ obs2 = grouped[obs_id2]
110
+
111
+ # Cohen's Kappa
112
+ kappa, po, pe, table = cohen_kappa_weighted_by_time(obs1, obs2)
113
+
114
+ print(f"{obs_id1} - {obs_id2}: Cohen's Kappa: {kappa:.3f} Expected agreement: {pe:.3f} Observed agreement: {po:.3f}")
115
+ str_results += (
116
+ f"{obs_id1} - {obs_id2}: Cohen's Kappa: {kappa:.3f} Expected agreement: {pe:.3f} Observed agreement: {po:.3f}\n"
117
+ )
118
+
119
+ ck_results[(obs_id1, obs_id2)] = f"{kappa:.3f}"
120
+ ck_results[(obs_id2, obs_id1)] = f"{kappa:.3f}"
121
+
122
+ # DataFrame conversion
123
+ df_results = pd.Series(ck_results).unstack()
124
+
125
+ return df_results, str_results
boris/boris_cli.py CHANGED
@@ -252,7 +252,7 @@ if args.command:
252
252
 
253
253
  K, out = irr.cohen_kappa(cursor, observations_id_list[0], observations_id_list[1], interval, subjects, include_modifiers)
254
254
 
255
- print(("Cohen's Kappa - Index of Inter-Rater Reliability\n\n" "Interval time: {interval:.3f} s\n").format(interval=interval))
255
+ print(("Cohen's Kappa - Index of Inter-Rater Reliability\n\nInterval time: {interval:.3f} s\n").format(interval=interval))
256
256
 
257
257
  print(out)
258
258
  sys.exit()
boris/core.py CHANGED
@@ -4263,7 +4263,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4263
4263
 
4264
4264
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
4265
4265
  self.project_changed()
4266
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
4266
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
4267
4267
 
4268
4268
  self.load_tw_events(self.observationId)
4269
4269
 
@@ -4361,14 +4361,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4361
4361
 
4362
4362
  events_to_add = project_functions.fix_unpaired_state_events2(self.pj[cfg.ETHOGRAM], events, time_to_stop)
4363
4363
 
4364
- # print(f"{events_to_add=}")
4365
-
4366
4364
  if events_to_add:
4367
4365
  self.statusbar.showMessage("The playlist has finished. Some ongoing state events were stopped automatically", 0)
4368
4366
 
4369
4367
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
4370
4368
  self.project_changed()
4371
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
4369
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
4372
4370
 
4373
4371
  self.load_tw_events(self.observationId)
4374
4372
 
boris/core_qrc.py CHANGED
@@ -15946,10 +15946,13 @@ qt_resource_struct = b"\
15946
15946
  \x00\x00\x01\x95k&\xa4B\
15947
15947
  "
15948
15948
 
15949
+
15949
15950
  def qInitResources():
15950
15951
  QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
15951
15952
 
15953
+
15952
15954
  def qCleanupResources():
15953
15955
  QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
15954
15956
 
15957
+
15955
15958
  qInitResources()
boris/exclusion_matrix.py CHANGED
@@ -42,7 +42,7 @@ class ExclusionMatrix(QDialog):
42
42
 
43
43
  self.label = QLabel()
44
44
  self.label.setText(
45
- ("Check if behaviors are mutually exclusive.\n" "The Point events (displayed on blue background) cannot be excluded)")
45
+ ("Check if behaviors are mutually exclusive.\nThe Point events (displayed on blue background) cannot be excluded)")
46
46
  )
47
47
  hbox.addWidget(self.label)
48
48
 
boris/irr.py CHANGED
@@ -126,7 +126,7 @@ def cohen_kappa(cursor, obsid1: str, obsid2: str, interval: dec, selected_subjec
126
126
  first_event = cursor.execute(
127
127
  (
128
128
  "SELECT min(start) FROM aggregated_events "
129
- f"WHERE observation in (?, ?) AND subject in ({','.join('?'*len(selected_subjects))}) "
129
+ f"WHERE observation in (?, ?) AND subject in ({','.join('?' * len(selected_subjects))}) "
130
130
  ),
131
131
  (obsid1, obsid2) + tuple(selected_subjects),
132
132
  ).fetchone()[0]
@@ -134,21 +134,18 @@ def cohen_kappa(cursor, obsid1: str, obsid2: str, interval: dec, selected_subjec
134
134
  logging.debug(f"first_event: {first_event}")
135
135
 
136
136
  last_event = cursor.execute(
137
- (
138
- "SELECT max(stop) FROM aggregated_events "
139
- f"WHERE observation in (?, ?) AND subject in ({','.join('?'*len(selected_subjects))}) "
140
- ),
137
+ (f"SELECT max(stop) FROM aggregated_events WHERE observation in (?, ?) AND subject in ({','.join('?' * len(selected_subjects))}) "),
141
138
  (obsid1, obsid2) + tuple(selected_subjects),
142
139
  ).fetchone()[0]
143
140
 
144
141
  logging.debug(f"last_event: {last_event}")
145
142
 
146
143
  nb_events1 = cursor.execute(
147
- ("SELECT COUNT(*) FROM aggregated_events " f"WHERE observation = ? AND subject in ({','.join('?'*len(selected_subjects))}) "),
144
+ (f"SELECT COUNT(*) FROM aggregated_events WHERE observation = ? AND subject in ({','.join('?' * len(selected_subjects))}) "),
148
145
  (obsid1,) + tuple(selected_subjects),
149
146
  ).fetchone()[0]
150
147
  nb_events2 = cursor.execute(
151
- ("SELECT COUNT(*) FROM aggregated_events " f"WHERE observation = ? AND subject in ({','.join('?'*len(selected_subjects))}) "),
148
+ (f"SELECT COUNT(*) FROM aggregated_events WHERE observation = ? AND subject in ({','.join('?' * len(selected_subjects))}) "),
152
149
  (obsid2,) + tuple(selected_subjects),
153
150
  ).fetchone()[0]
154
151
 
@@ -201,11 +198,7 @@ def cohen_kappa(cursor, obsid1: str, obsid2: str, interval: dec, selected_subjec
201
198
  logging.debug(f"contingency_table:\n {contingency_table}")
202
199
 
203
200
  template = (
204
- "Observation: {obsid1}\n"
205
- "number of events: {nb_events1}\n\n"
206
- "Observation: {obsid2}\n"
207
- "number of events: {nb_events2:.0f}\n\n"
208
- "K = {K:.3f}"
201
+ "Observation: {obsid1}\nnumber of events: {nb_events1}\n\nObservation: {obsid2}\nnumber of events: {nb_events2:.0f}\n\nK = {K:.3f}"
209
202
  )
210
203
 
211
204
  # out += "Observation length: <b>{:.3f} s</b><br>".format(self.observationTotalMediaLength(obsid1))
@@ -470,7 +463,7 @@ def needleman_wunsch_identity(cursor, obsid1: str, obsid2: str, interval, select
470
463
  first_event = cursor.execute(
471
464
  (
472
465
  "SELECT min(start) FROM aggregated_events "
473
- f"WHERE observation in (?, ?) AND subject in ({','.join('?'*len(selected_subjects))}) "
466
+ f"WHERE observation in (?, ?) AND subject in ({','.join('?' * len(selected_subjects))}) "
474
467
  ),
475
468
  (obsid1, obsid2) + tuple(selected_subjects),
476
469
  ).fetchone()[0]
@@ -483,22 +476,19 @@ def needleman_wunsch_identity(cursor, obsid1: str, obsid2: str, interval, select
483
476
  logging.debug(f"first_event: {first_event}")
484
477
 
485
478
  last_event = cursor.execute(
486
- (
487
- "SELECT max(stop) FROM aggregated_events "
488
- f"WHERE observation in (?, ?) AND subject in ({','.join('?'*len(selected_subjects))}) "
489
- ),
479
+ (f"SELECT max(stop) FROM aggregated_events WHERE observation in (?, ?) AND subject in ({','.join('?' * len(selected_subjects))}) "),
490
480
  (obsid1, obsid2) + tuple(selected_subjects),
491
481
  ).fetchone()[0]
492
482
 
493
483
  logging.debug(f"last_event: {last_event}")
494
484
 
495
485
  nb_events1 = cursor.execute(
496
- ("SELECT COUNT(*) FROM aggregated_events " f"WHERE observation = ? AND subject in ({','.join('?'*len(selected_subjects))}) "),
486
+ (f"SELECT COUNT(*) FROM aggregated_events WHERE observation = ? AND subject in ({','.join('?' * len(selected_subjects))}) "),
497
487
  (obsid1,) + tuple(selected_subjects),
498
488
  ).fetchone()[0]
499
489
 
500
490
  nb_events2 = cursor.execute(
501
- ("SELECT COUNT(*) FROM aggregated_events " f"WHERE observation = ? AND subject in ({','.join('?'*len(selected_subjects))}) "),
491
+ (f"SELECT COUNT(*) FROM aggregated_events WHERE observation = ? AND subject in ({','.join('?' * len(selected_subjects))}) "),
502
492
  (obsid2,) + tuple(selected_subjects),
503
493
  ).fetchone()[0]
504
494
 
@@ -606,9 +596,7 @@ def needleman_wunch(self):
606
596
 
607
597
  cursor = db_connector.cursor()
608
598
  out = (
609
- "Needleman-Wunsch similarity\n\n"
610
- f"Time unit: {interval:.3f} s\n"
611
- f"Selected subjects: {', '.join(parameters[cfg.SELECTED_SUBJECTS])}\n\n"
599
+ f"Needleman-Wunsch similarity\n\nTime unit: {interval:.3f} s\nSelected subjects: {', '.join(parameters[cfg.SELECTED_SUBJECTS])}\n\n"
612
600
  )
613
601
  mem_done = []
614
602
  nws_results = np.ones((len(selected_observations), len(selected_observations)))
boris/menu_options.py CHANGED
@@ -143,6 +143,8 @@ def update_menu(self):
143
143
  self.actionJumpForward,
144
144
  self.actionJumpBackward,
145
145
  self.actionJumpTo,
146
+ self.action_change_time_offset_of_players,
147
+ self.action_deinterlace,
146
148
  self.actionZoom_level,
147
149
  self.actionRotate_current_video,
148
150
  self.actionDisplay_subtitles,
boris/portion/__init__.py CHANGED
@@ -3,18 +3,28 @@ from .interval import Interval, open, closed, openclosed, closedopen, empty, sin
3
3
  from .func import iterate
4
4
  from .io import from_string, to_string, from_data, to_data
5
5
 
6
- # disabled because BORIS does not need IntervalDict
6
+ # disabled because BORIS does not need IntervalDict
7
7
  # so the sortedcontainers module is not required
8
- #from .dict import IntervalDict
8
+ # from .dict import IntervalDict
9
9
 
10
10
 
11
11
  __all__ = [
12
- 'inf', 'CLOSED', 'OPEN',
13
- 'Interval',
14
- 'open', 'closed', 'openclosed', 'closedopen', 'singleton', 'empty',
15
- 'iterate',
16
- 'from_string', 'to_string', 'from_data', 'to_data',
17
- 'IntervalDict',
12
+ "inf",
13
+ "CLOSED",
14
+ "OPEN",
15
+ "Interval",
16
+ "open",
17
+ "closed",
18
+ "openclosed",
19
+ "closedopen",
20
+ "singleton",
21
+ "empty",
22
+ "iterate",
23
+ "from_string",
24
+ "to_string",
25
+ "from_data",
26
+ "to_data",
27
+ "IntervalDict",
18
28
  ]
19
29
 
20
30
  CLOSED = Bound.CLOSED
boris/portion/const.py CHANGED
@@ -5,11 +5,12 @@ class Bound(enum.Enum):
5
5
  """
6
6
  Bound types, either CLOSED for inclusive, or OPEN for exclusive.
7
7
  """
8
+
8
9
  CLOSED = True
9
10
  OPEN = False
10
11
 
11
12
  def __bool__(self):
12
- raise ValueError('The truth value of a bound is ambiguous.')
13
+ raise ValueError("The truth value of a bound is ambiguous.")
13
14
 
14
15
  def __invert__(self):
15
16
  return Bound.CLOSED if self is Bound.OPEN else Bound.OPEN
@@ -21,7 +22,7 @@ class Bound(enum.Enum):
21
22
  return self.name
22
23
 
23
24
 
24
- class _Singleton():
25
+ class _Singleton:
25
26
  __instance = None
26
27
 
27
28
  def __new__(cls, *args, **kwargs):
@@ -35,21 +36,29 @@ class _PInf(_Singleton):
35
36
  Represent positive infinity.
36
37
  """
37
38
 
38
- def __neg__(self): return _NInf()
39
+ def __neg__(self):
40
+ return _NInf()
39
41
 
40
- def __lt__(self, o): return False
42
+ def __lt__(self, o):
43
+ return False
41
44
 
42
- def __le__(self, o): return isinstance(o, _PInf)
45
+ def __le__(self, o):
46
+ return isinstance(o, _PInf)
43
47
 
44
- def __gt__(self, o): return not isinstance(o, _PInf)
48
+ def __gt__(self, o):
49
+ return not isinstance(o, _PInf)
45
50
 
46
- def __ge__(self, o): return True
51
+ def __ge__(self, o):
52
+ return True
47
53
 
48
- def __eq__(self, o): return isinstance(o, _PInf)
54
+ def __eq__(self, o):
55
+ return isinstance(o, _PInf)
49
56
 
50
- def __repr__(self): return '+inf'
57
+ def __repr__(self):
58
+ return "+inf"
51
59
 
52
- def __hash__(self): return hash(float('+inf'))
60
+ def __hash__(self):
61
+ return hash(float("+inf"))
53
62
 
54
63
 
55
64
  class _NInf(_Singleton):
@@ -57,21 +66,29 @@ class _NInf(_Singleton):
57
66
  Represent negative infinity.
58
67
  """
59
68
 
60
- def __neg__(self): return _PInf()
69
+ def __neg__(self):
70
+ return _PInf()
61
71
 
62
- def __lt__(self, o): return not isinstance(o, _NInf)
72
+ def __lt__(self, o):
73
+ return not isinstance(o, _NInf)
63
74
 
64
- def __le__(self, o): return True
75
+ def __le__(self, o):
76
+ return True
65
77
 
66
- def __gt__(self, o): return False
78
+ def __gt__(self, o):
79
+ return False
67
80
 
68
- def __ge__(self, o): return isinstance(o, _NInf)
81
+ def __ge__(self, o):
82
+ return isinstance(o, _NInf)
69
83
 
70
- def __eq__(self, o): return isinstance(o, _NInf)
84
+ def __eq__(self, o):
85
+ return isinstance(o, _NInf)
71
86
 
72
- def __repr__(self): return '-inf'
87
+ def __repr__(self):
88
+ return "-inf"
73
89
 
74
- def __hash__(self): return hash(float('-inf'))
90
+ def __hash__(self):
91
+ return hash(float("-inf"))
75
92
 
76
93
 
77
94
  # Positive infinity
boris/portion/dict.py CHANGED
@@ -28,7 +28,7 @@ class IntervalDict(MutableMapping):
28
28
  number of distinct values (not keys) that are stored.
29
29
  """
30
30
 
31
- __slots__ = ('_storage', )
31
+ __slots__ = ("_storage",)
32
32
 
33
33
  def __init__(self, mapping_or_iterable=None):
34
34
  """
@@ -352,10 +352,10 @@ class IntervalDict(MutableMapping):
352
352
  return key in self.domain()
353
353
 
354
354
  def __repr__(self):
355
- return '{}{}{}'.format(
356
- '{',
357
- ', '.join('{!r}: {!r}'.format(i, v) for i, v in self.items()),
358
- '}',
355
+ return "{}{}{}".format(
356
+ "{",
357
+ ", ".join("{!r}: {!r}".format(i, v) for i, v in self.items()),
358
+ "}",
359
359
  )
360
360
 
361
361
  def __eq__(self, other):
boris/portion/func.py CHANGED
@@ -31,7 +31,7 @@ def iterate(interval, step, *, base=None, reverse=False):
31
31
  :return: a lazy iterator.
32
32
  """
33
33
  if base is None:
34
- base = (lambda x: x)
34
+ base = lambda x: x
35
35
 
36
36
  exclude = operator.lt if not reverse else operator.gt
37
37
  include = operator.le if not reverse else operator.ge
@@ -39,7 +39,7 @@ def iterate(interval, step, *, base=None, reverse=False):
39
39
 
40
40
  value = base(interval.lower if not reverse else interval.upper)
41
41
  if (value == -inf and not reverse) or (value == inf and reverse):
42
- raise ValueError('Cannot start iteration with infinity.')
42
+ raise ValueError("Cannot start iteration with infinity.")
43
43
 
44
44
  for i in interval if not reverse else reversed(interval):
45
45
  value = base(i.lower if not reverse else i.upper)
boris/portion/interval.py CHANGED
@@ -2,7 +2,7 @@ from collections import namedtuple
2
2
  from .const import Bound, inf
3
3
 
4
4
 
5
- Atomic = namedtuple('Atomic', ['left', 'lower', 'upper', 'right'])
5
+ Atomic = namedtuple("Atomic", ["left", "lower", "upper", "right"])
6
6
 
7
7
 
8
8
  def mergeable(a, b):
@@ -96,7 +96,7 @@ class Interval:
96
96
  one of the helpers provided in this module (open, closed, openclosed, etc.)
97
97
  """
98
98
 
99
- __slots__ = ('_intervals',)
99
+ __slots__ = ("_intervals",)
100
100
 
101
101
  def __init__(self, *intervals):
102
102
  """
@@ -111,7 +111,7 @@ class Interval:
111
111
  if not interval.empty:
112
112
  self._intervals.extend(interval._intervals)
113
113
  else:
114
- raise TypeError('Parameters must be Interval instances')
114
+ raise TypeError("Parameters must be Interval instances")
115
115
 
116
116
  if len(self._intervals) == 0:
117
117
  # So we have at least one (empty) interval
@@ -181,10 +181,7 @@ class Interval:
181
181
  """
182
182
  True if interval is empty, False otherwise.
183
183
  """
184
- return (
185
- self.lower > self.upper or
186
- (self.lower == self.upper and (self.left == Bound.OPEN or self.right == Bound.OPEN))
187
- )
184
+ return self.lower > self.upper or (self.lower == self.upper and (self.left == Bound.OPEN or self.right == Bound.OPEN))
188
185
 
189
186
  @property
190
187
  def atomic(self):
@@ -307,7 +304,7 @@ class Interval:
307
304
  elif isinstance(value, tuple):
308
305
  intervals.append(Interval.from_atomic(*value))
309
306
  else:
310
- raise TypeError('Unsupported return type {} for {}'.format(type(value), value))
307
+ raise TypeError("Unsupported return type {} for {}".format(type(value), value))
311
308
 
312
309
  return Interval(*intervals)
313
310
 
@@ -350,7 +347,7 @@ class Interval:
350
347
  return True
351
348
  return False
352
349
  else:
353
- raise TypeError('Unsupported type {} for {}'.format(type(other), other))
350
+ raise TypeError("Unsupported type {} for {}".format(type(other), other))
354
351
 
355
352
  def intersection(self, other):
356
353
  """
@@ -468,14 +465,8 @@ class Interval:
468
465
  if item.empty:
469
466
  return True
470
467
  elif self.atomic:
471
- left = item.lower > self.lower or (
472
- item.lower == self.lower and
473
- (item.left == self.left or self.left == Bound.CLOSED)
474
- )
475
- right = item.upper < self.upper or (
476
- item.upper == self.upper and
477
- (item.right == self.right or self.right == Bound.CLOSED)
478
- )
468
+ left = item.lower > self.lower or (item.lower == self.lower and (item.left == self.left or self.left == Bound.CLOSED))
469
+ right = item.upper < self.upper or (item.upper == self.upper and (item.right == self.right or self.right == Bound.CLOSED))
479
470
  return left and right
480
471
  else:
481
472
  selfiter = iter(self)
@@ -504,13 +495,11 @@ class Interval:
504
495
  def __invert__(self):
505
496
  complements = [
506
497
  Interval.from_atomic(Bound.OPEN, -inf, self.lower, ~self.left),
507
- Interval.from_atomic(~self.right, self.upper, inf, Bound.OPEN)
498
+ Interval.from_atomic(~self.right, self.upper, inf, Bound.OPEN),
508
499
  ]
509
500
 
510
501
  for i, j in zip(self._intervals[:-1], self._intervals[1:]):
511
- complements.append(
512
- Interval.from_atomic(~i.right, i.upper, j.lower, ~j.left)
513
- )
502
+ complements.append(Interval.from_atomic(~i.right, i.upper, j.lower, ~j.left))
514
503
 
515
504
  return Interval(*complements)
516
505
 
@@ -526,12 +515,7 @@ class Interval:
526
515
  return False
527
516
 
528
517
  for a, b in zip(self._intervals, other._intervals):
529
- eq = (
530
- a.left == b.left and
531
- a.lower == b.lower and
532
- a.upper == b.upper and
533
- a.right == b.right
534
- )
518
+ eq = a.left == b.left and a.lower == b.lower and a.upper == b.upper and a.right == b.right
535
519
  if not eq:
536
520
  return False
537
521
  return True
@@ -543,8 +527,7 @@ class Interval:
543
527
  if self.right == Bound.OPEN:
544
528
  return self.upper <= other.lower
545
529
  else:
546
- return self.upper < other.lower or \
547
- (self.upper == other.lower and other.left == Bound.OPEN)
530
+ return self.upper < other.lower or (self.upper == other.lower and other.left == Bound.OPEN)
548
531
  else:
549
532
  return self.upper < other or (self.right == Bound.OPEN and self.upper == other)
550
533
 
@@ -553,8 +536,7 @@ class Interval:
553
536
  if self.left == Bound.OPEN:
554
537
  return self.lower >= other.upper
555
538
  else:
556
- return self.lower > other.upper or \
557
- (self.lower == other.upper and other.right == Bound.OPEN)
539
+ return self.lower > other.upper or (self.lower == other.upper and other.right == Bound.OPEN)
558
540
  else:
559
541
  return self.lower > other or (self.left == Bound.OPEN and self.lower == other)
560
542
 
@@ -563,8 +545,7 @@ class Interval:
563
545
  if self.right == Bound.OPEN:
564
546
  return self.upper <= other.upper
565
547
  else:
566
- return self.upper < other.upper or \
567
- (self.upper == other.upper and other.right == Bound.CLOSED)
548
+ return self.upper < other.upper or (self.upper == other.upper and other.right == Bound.CLOSED)
568
549
  else:
569
550
  return self.lower < other or (self.left == Bound.CLOSED and self.lower == other)
570
551
 
@@ -573,8 +554,7 @@ class Interval:
573
554
  if self.left == Bound.OPEN:
574
555
  return self.lower >= other.lower
575
556
  else:
576
- return self.lower > other.lower or \
577
- (self.lower == other.lower and other.left == Bound.CLOSED)
557
+ return self.lower > other.lower or (self.lower == other.lower and other.left == Bound.CLOSED)
578
558
  else:
579
559
  return self.upper > other or (self.right == Bound.CLOSED and self.upper == other)
580
560
 
@@ -586,16 +566,16 @@ class Interval:
586
566
 
587
567
  for interval in self:
588
568
  if interval.empty:
589
- intervals.append('()')
569
+ intervals.append("()")
590
570
  elif interval.lower == interval.upper:
591
- intervals.append('[{}]'.format(repr(interval.lower)))
571
+ intervals.append("[{}]".format(repr(interval.lower)))
592
572
  else:
593
573
  intervals.append(
594
- '{}{},{}{}'.format(
595
- '[' if interval.left == Bound.CLOSED else '(',
574
+ "{}{},{}{}".format(
575
+ "[" if interval.left == Bound.CLOSED else "(",
596
576
  repr(interval.lower),
597
577
  repr(interval.upper),
598
- ']' if interval.right == Bound.CLOSED else ')',
578
+ "]" if interval.right == Bound.CLOSED else ")",
599
579
  )
600
580
  )
601
- return ' | '.join(intervals)
581
+ return " | ".join(intervals)
boris/portion/io.py CHANGED
@@ -4,9 +4,20 @@ from .const import Bound, inf
4
4
  from .interval import Interval
5
5
 
6
6
 
7
- def from_string(string, conv, *, bound=r'.+?', disj=r' ?\| ?', sep=r', ?',
8
- left_open=r'\(', left_closed=r'\[', right_open=r'\)', right_closed=r'\]',
9
- pinf=r'\+inf', ninf=r'-inf'):
7
+ def from_string(
8
+ string,
9
+ conv,
10
+ *,
11
+ bound=r".+?",
12
+ disj=r" ?\| ?",
13
+ sep=r", ?",
14
+ left_open=r"\(",
15
+ left_closed=r"\[",
16
+ right_open=r"\)",
17
+ right_closed=r"\]",
18
+ pinf=r"\+inf",
19
+ ninf=r"-inf",
20
+ ):
10
21
  """
11
22
  Parse given string and create an Interval instance.
12
23
  A converter function has to be provided to convert a bound (as string) to a value.
@@ -25,11 +36,11 @@ def from_string(string, conv, *, bound=r'.+?', disj=r' ?\| ?', sep=r', ?',
25
36
  :return: an Interval instance.
26
37
  """
27
38
 
28
- re_left_boundary = r'(?P<left>{}|{})'.format(left_open, left_closed)
29
- re_right_boundary = r'(?P<right>{}|{})'.format(right_open, right_closed)
30
- re_bounds = r'(?P<lower>{bound})({sep}(?P<upper>{bound}))?'.format(bound=bound, sep=sep)
31
- re_interval = r'{}(|{}){}'.format(re_left_boundary, re_bounds, re_right_boundary)
32
- re_intervals = r'{}(?P<disj>{})?'.format(re_interval, disj)
39
+ re_left_boundary = r"(?P<left>{}|{})".format(left_open, left_closed)
40
+ re_right_boundary = r"(?P<right>{}|{})".format(right_open, right_closed)
41
+ re_bounds = r"(?P<lower>{bound})({sep}(?P<upper>{bound}))?".format(bound=bound, sep=sep)
42
+ re_interval = r"{}(|{}){}".format(re_left_boundary, re_bounds, re_right_boundary)
43
+ re_intervals = r"{}(?P<disj>{})?".format(re_interval, disj)
33
44
 
34
45
  intervals = []
35
46
  has_more = True
@@ -49,22 +60,23 @@ def from_string(string, conv, *, bound=r'.+?', disj=r' ?\| ?', sep=r', ?',
49
60
  else:
50
61
  group = match.groupdict()
51
62
 
52
- left = Bound.CLOSED if re.match(left_closed + '$', group['left']) else Bound.OPEN
53
- right = Bound.CLOSED if re.match(right_closed + '$', group['right']) else Bound.OPEN
63
+ left = Bound.CLOSED if re.match(left_closed + "$", group["left"]) else Bound.OPEN
64
+ right = Bound.CLOSED if re.match(right_closed + "$", group["right"]) else Bound.OPEN
54
65
 
55
- lower = group.get('lower', None)
56
- upper = group.get('upper', None)
66
+ lower = group.get("lower", None)
67
+ upper = group.get("upper", None)
57
68
  lower = _convert(lower) if lower is not None else inf
58
69
  upper = _convert(upper) if upper is not None else lower
59
70
 
60
71
  intervals.append(Interval.from_atomic(left, lower, upper, right))
61
- string = string[match.end():]
72
+ string = string[match.end() :]
62
73
 
63
74
  return Interval(*intervals)
64
75
 
65
76
 
66
- def to_string(interval, conv=repr, *, disj=' | ', sep=',', left_open='(',
67
- left_closed='[', right_open=')', right_closed=']', pinf='+inf', ninf='-inf'):
77
+ def to_string(
78
+ interval, conv=repr, *, disj=" | ", sep=",", left_open="(", left_closed="[", right_open=")", right_closed="]", pinf="+inf", ninf="-inf"
79
+ ):
68
80
  """
69
81
  Export given interval to string.
70
82
 
@@ -81,7 +93,7 @@ def to_string(interval, conv=repr, *, disj=' | ', sep=',', left_open='(',
81
93
  :return: a string representation for given interval.
82
94
  """
83
95
  if interval.empty:
84
- return '{}{}'.format(left_open, right_open)
96
+ return "{}{}".format(left_open, right_open)
85
97
 
86
98
  def _convert(bound):
87
99
  if bound == inf:
@@ -100,14 +112,14 @@ def to_string(interval, conv=repr, *, disj=' | ', sep=',', left_open='(',
100
112
  upper = _convert(item.upper)
101
113
 
102
114
  if item.lower == item.upper:
103
- exported_intervals.append('{}{}{}'.format(left, lower, right))
115
+ exported_intervals.append("{}{}{}".format(left, lower, right))
104
116
  else:
105
- exported_intervals.append('{}{}{}{}{}'.format(left, lower, sep, upper, right))
117
+ exported_intervals.append("{}{}{}{}{}".format(left, lower, sep, upper, right))
106
118
 
107
119
  return disj.join(exported_intervals)
108
120
 
109
121
 
110
- def from_data(data, conv=None, *, pinf=float('inf'), ninf=float('-inf')):
122
+ def from_data(data, conv=None, *, pinf=float("inf"), ninf=float("-inf")):
111
123
  """
112
124
  Import an interval from a piece of data.
113
125
 
@@ -130,16 +142,18 @@ def from_data(data, conv=None, *, pinf=float('inf'), ninf=float('-inf')):
130
142
 
131
143
  for item in data:
132
144
  left, lower, upper, right = item
133
- intervals.append(Interval.from_atomic(
134
- Bound(left),
135
- _convert(lower),
136
- _convert(upper),
137
- Bound(right),
138
- ))
145
+ intervals.append(
146
+ Interval.from_atomic(
147
+ Bound(left),
148
+ _convert(lower),
149
+ _convert(upper),
150
+ Bound(right),
151
+ )
152
+ )
139
153
  return Interval(*intervals)
140
154
 
141
155
 
142
- def to_data(interval, conv=None, *, pinf=float('inf'), ninf=float('-inf')):
156
+ def to_data(interval, conv=None, *, pinf=float("inf"), ninf=float("-inf")):
143
157
  """
144
158
  Export given interval to a list of 4-uples (left, lower,
145
159
  upper, right).
@@ -163,10 +177,5 @@ def to_data(interval, conv=None, *, pinf=float('inf'), ninf=float('-inf')):
163
177
  return conv(bound)
164
178
 
165
179
  for item in interval:
166
- data.append((
167
- item.left.value,
168
- _convert(item.lower),
169
- _convert(item.upper),
170
- item.right.value
171
- ))
180
+ data.append((item.left.value, _convert(item.lower), _convert(item.upper), item.right.value))
172
181
  return data
@@ -1539,7 +1539,8 @@ def open_project_json(project_file_name: str) -> tuple:
1539
1539
  # sort events by time asc
1540
1540
  for obs_id in pj[cfg.OBSERVATIONS]:
1541
1541
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
1542
- pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort()
1542
+ # sort events list using the first 3 items (time, subject, behavior)
1543
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
1543
1544
 
1544
1545
  return project_file_name, projectChanged, pj, msg
1545
1546
 
boris/state_events.py CHANGED
@@ -136,7 +136,7 @@ def fix_unpaired_events(self, silent_mode: bool = False):
136
136
 
137
137
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
138
138
  self.project_changed()
139
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
139
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
140
140
  self.load_tw_events(self.observationId)
141
141
 
142
142
  index = self.tv_events.model().index(
boris/transitions.py CHANGED
@@ -354,7 +354,7 @@ def transitions_flow_diagram():
354
354
  with open(tempfile.gettempdir() + os.sep + os.path.basename(file_name) + ".tmp.gv", "w") as f:
355
355
  f.write(gv)
356
356
  result = subprocess.getoutput(
357
- (f'dot -Tpng -o "{file_name}.png" ' f'"{tempfile.gettempdir() + os.sep + os.path.basename(file_name)}.tmp.gv"')
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
360
  out += f"<b>{file_name}.png</b> created<br>"
boris/version.py CHANGED
@@ -20,5 +20,5 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- __version__ = "9.6.1"
24
- __version_date__ = "2025-07-17"
23
+ __version__ = "9.6.2"
24
+ __version_date__ = "2025-08-05"
boris/write_event.py CHANGED
@@ -472,8 +472,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
472
472
  comment,
473
473
  frame_idx,
474
474
  ]
475
- # order by image index ASC
476
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
475
+ # order events list using time, subject, behavior
476
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
477
477
 
478
478
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
479
479
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
@@ -483,8 +483,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
483
483
  modifier_str,
484
484
  comment,
485
485
  ]
486
- # order by image index ASC
487
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
486
+ # order events list using time, subject, behavior
487
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
488
488
 
489
489
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
490
490
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.6.1
3
+ Version: 9.6.2
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
@@ -27,6 +27,7 @@ Requires-Dist: pyreadr
27
27
  Requires-Dist: pyside6==6.9
28
28
  Requires-Dist: hachoir>=3.3.0
29
29
  Requires-Dist: scipy>=1.15.3
30
+ Requires-Dist: scikit-learn>=1.7.1
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: ruff; extra == "dev"
32
33
  Requires-Dist: pytest; extra == "dev"
@@ -1,13 +1,13 @@
1
1
  boris/__init__.py,sha256=iAtmVMy22TJpMmxVTMSK_6-wXnCbx1ogvWgfYEcbHzU,773
2
2
  boris/__main__.py,sha256=ANjTbXgXDoz2nB1tCtOIllfIVotCa602iebACX7rXaE,764
3
3
  boris/about.py,sha256=VPa8zeu0bMb1LRXDq8uUSG_7mSbkb2HTk1AtWbzWQwE,5366
4
- boris/add_modifier.py,sha256=DWqxkKDBm21QH_kPvhpnltwLtFvPxne0VmZ1SY26hj8,26340
4
+ boris/add_modifier.py,sha256=l9LSa_9FAV9CnBgm26tJqhMAdnFoBQafZLyt9pTKmac,26240
5
5
  boris/add_modifier_ui.py,sha256=Y7TLO5uS6zW7zpjXmjA4V_VIp_bFDNtjOTbJ9Q6m-mQ,11601
6
6
  boris/advanced_event_filtering.py,sha256=VlvU12mL6xYacZOvJAi5uLpHMcmAw5Pvuvmka-PN29c,15469
7
7
  boris/behav_coding_map_creator.py,sha256=_WmfWTYkKh_a7pZa49h2GtORCi6h8joZTWihud6YDBE,38826
8
8
  boris/behavior_binary_table.py,sha256=bpmRDpEjq0rw3YOCoN_He3kfUe8A_R6E48kQR7KnkH8,12453
9
9
  boris/behaviors_coding_map.py,sha256=xIGJxp2eghrpiGDmYH73eJPERuyc4A_54uT-Got3zTs,7302
10
- boris/boris_cli.py,sha256=n0OiVvZM1gM6E7yKaff9wlgmpAGK4TK052VRi8AabJo,13196
10
+ boris/boris_cli.py,sha256=Bc51DEMcD79ZZfM9pCzpaWU6iT6b8gNwR3n8fr42_4E,13193
11
11
  boris/cmd_arguments.py,sha256=oWb-FvhKLbKJhATlTHy9muWu8XnnUfOZ-3Fmz2M8Yzc,1848
12
12
  boris/coding_pad.py,sha256=fBKdp7DDyupySJosIYtqNd8s2E-GruzCgVhDFsoVWKE,10986
13
13
  boris/config.py,sha256=IbW8PkAFcZIL-8NoSscXSeI82dneLzpywaGXWDcnrWw,17845
@@ -16,8 +16,8 @@ boris/connections.py,sha256=kqc6jaYNzoJe8crPtfwE-fXTW4nTfB8-PojRzbbLEus,19629
16
16
  boris/converters.py,sha256=n6gDM9x2hS-ZOoHLruiifuXxnC7ERsUukiFokhHZPoQ,11678
17
17
  boris/converters_ui.py,sha256=uu7LOBV_fKv2DBdOqsqPwjGsjgONr5ODBoscAA-EP48,9900
18
18
  boris/cooccurence.py,sha256=tVERC-V8MWjWHlGEfDuu08iS94qjt4do-38jwI62QaY,10367
19
- boris/core.py,sha256=nq-GnTz6mdGFSv1GDAp0XJNyC0FW0DEdvqhMrlY_dCg,231016
20
- boris/core_qrc.py,sha256=J0kom27nrmi4-TCDLJZCZbtUAitiXQ4WEDfErawuxt8,639879
19
+ boris/core.py,sha256=xDurFeAulOx1TjcaZbuoymvnP07fjIAUfhElLw4och8,231004
20
+ boris/core_qrc.py,sha256=Hz51Xw70ZIlDbYB281nfGtCm43_ItYhamMu2T5X8Tu8,639882
21
21
  boris/core_ui.py,sha256=--VDOzUfjsA4TJRw3aFk2CeSL29193vPGLgYJRhQfUY,77143
22
22
  boris/db_functions.py,sha256=Uw9wWH_Pe-qNzpV1k21YG_jKsoOmfY_iiK_7ARZHGDc,13352
23
23
  boris/dev.py,sha256=9pUElbjl9g17rFUJXX5aVSu55_iIKIuDxNdrB0DI_d0,3671
@@ -28,7 +28,7 @@ boris/edit_event_ui.py,sha256=qFgt00cejGB6UGC1mFkyZcsIAdvMeYMK0WYjZtJl1T0,9207
28
28
  boris/event_operations.py,sha256=bqUZjgJaJ1Z8oTiidG9wfCp2LLUH1Zf4kBDeg_yjC-o,38514
29
29
  boris/events_cursor.py,sha256=VPY_ygD0fxE5lp25mcd2l00XQXurCR6hprffF4tKRbU,2078
30
30
  boris/events_snapshots.py,sha256=PjWzQvUGQtIcEc_7FDsRphf7fAhhTccQgYc2eQSA65g,27621
31
- boris/exclusion_matrix.py,sha256=ff88xD6aqc8bpIuj9ZCz9ju_HeqqgibQwoaJrIOJ6RI,5272
31
+ boris/exclusion_matrix.py,sha256=K_o8pEMYRQ3curgRQYkn5hPRksLDitICuwjB7mpVRPA,5269
32
32
  boris/export_events.py,sha256=3B336WEA0g_8oW3VDo_kfq5D0ISu-e7z2f-_ROUvU9c,39756
33
33
  boris/export_observation.py,sha256=B8ASj6H70xfcTSUHrbcJa6YOYjih2WD4DSiUtXj5eAk,50764
34
34
  boris/external_processes.py,sha256=PogE2eEiQLVZ2useMamQMOAeDmMUX_TlIpqPKLMm6Ak,13607
@@ -36,11 +36,11 @@ boris/geometric_measurement.py,sha256=4pI-AYpBSFlJBqS-f8dnkgLtj_Z2E5kwwAdh6WwZ4k
36
36
  boris/gui_utilities.py,sha256=2HdWFxo2y0oxC29VJAA3R-TOMxVbOy3FuVwspjrTD6A,5519
37
37
  boris/image_overlay.py,sha256=zZAL8MTt2i2s58CuX81Nym3rJ5pKiTeP4AO8WbIUonM,2527
38
38
  boris/import_observations.py,sha256=zKrkpk1ADxTj2BECactPPOhU6wtrh3TjtOWue2HCT5w,9074
39
- boris/irr.py,sha256=o5QN3B2b-02AUkrklMJCitFGsfiUDtmI0MxUbPv2cBg,22472
39
+ boris/irr.py,sha256=n6Y_Y9iEKOf9_7EE_lDRNei7tq2wkFKk_JVflm9UQdk,22335
40
40
  boris/latency.py,sha256=48z9L_A582-wKCfD0M3h0uyYkeL2ezjlQAS_GzeoOe0,9739
41
41
  boris/measurement_widget.py,sha256=lZV62KtK6TjdoNbKxj3uyNAuL5dfnQnn7mYwzMo-dOM,4480
42
42
  boris/media_file.py,sha256=Wnw-PCyAz6CA00zhjrx0UTgXZ0wmHuNlnElV_TzJ_2M,4818
43
- boris/menu_options.py,sha256=UEB3GxRh6YKNCg67qbhOVhJW1ZOznuPe15bADc_CNTI,7062
43
+ boris/menu_options.py,sha256=uznHFMtpGRWL6Eig10gJ5tOiypgOr9XVyxRiuCbgN9U,7146
44
44
  boris/modifier_coding_map_creator.py,sha256=NQHy_txgxKZnGByXiro_Oy_cq4DrFaFiAYwVp1CWrTs,33281
45
45
  boris/modifiers_coding_map.py,sha256=oT56ZY_PXhEJsMoblEsyNMAPbDpv7ZMOCnvmt7Ibx_Y,4554
46
46
  boris/mpv-1.0.3.py,sha256=EXRtzQqFjOn4wMC6482Ilq3fNQ9N1GRP1VxwLzdeaBY,88077
@@ -63,7 +63,7 @@ boris/plugins.py,sha256=lYR_sLqRMdyGEoSPwaLyVuF50B92k6vR8TlDAE5B4l0,14252
63
63
  boris/preferences.py,sha256=gWkqKvKuAAzjNbL3_NdBeaHfNC5xKQVxVZW4J1OwYRg,20763
64
64
  boris/preferences_ui.py,sha256=wbo51aBNdcQTJni1DmUM5ZQPOwAtKSkEQam7rRzRS5g,34166
65
65
  boris/project.py,sha256=nyXfCDY_rLP3jC1QGv-280jUKgbABqESjOm7I19rJ1U,86432
66
- boris/project_functions.py,sha256=vCs_colyOYtWQFOy5z4aS8mD8f2lZzKU98XIjmGa0BY,80769
66
+ boris/project_functions.py,sha256=o0IOvhGs1cqEjpdeNUeY-qvFfWAQl_7tsUEKxogKRuU,80869
67
67
  boris/project_import_export.py,sha256=oBG1CSXfKISsb3TLNT-8BH8kZPAzxIYSNemlLVH1Lh8,38560
68
68
  boris/project_ui.py,sha256=yB-ewhHt8S8DTTRIk-dNK2tPMNU24lNji9fDW_Xazu8,38805
69
69
  boris/qrc_boris.py,sha256=aH-qUirYY1CGxmTK1SFCPvuZfazIHX4DdUKF1gxZeYM,675008
@@ -71,35 +71,39 @@ boris/qrc_boris5.py,sha256=prnOw7VGXWXRuVCYp_yIrmWhrlG1F9rx-3BQvkPenjY,161608
71
71
  boris/select_modifiers.py,sha256=42uG9F75pfPoPJ-blp-vFgmpBpVJtL42FlIxpNpq9z4,13319
72
72
  boris/select_observations.py,sha256=k7c3FNVQW74YGH9oFmtHXRVCRnpKGhjCVk3cQtyLML8,8027
73
73
  boris/select_subj_behav.py,sha256=ulXbsRY-AIyQRSwXhVlvkNRS_eqWaCvkDKTTyOLqvoE,11742
74
- boris/state_events.py,sha256=Vrxn3CxC3yf8aeFE76l24n03qgaQ1QXpMtprYT0pq64,7771
74
+ boris/state_events.py,sha256=iUrC5ypwIKOnmoq0moDQwtH9-DrgiJ81zL2pMxESucU,7790
75
75
  boris/subjects_pad.py,sha256=lSRRGfLfD10_YpGua8RGVdKhoXlsXawGhNibPkRhuzM,3541
76
76
  boris/synthetic_time_budget.py,sha256=3Eb9onMLmgqCLd50CuxV9L8RV2ESzfaMWvPK_bXUMMk,10489
77
77
  boris/time_budget_functions.py,sha256=y5He8crz0xsTxVfz0jATwFFQVnPAIrNHja_0sF6NtRE,52551
78
78
  boris/time_budget_widget.py,sha256=z-tyITBtIz-KH1H2OdMB5a8x9QQLK7Wu96-zkC6NVDA,43213
79
- boris/transitions.py,sha256=_aZJfJWv3EBrtmQ7qsdTCayQo6uWU7BXqtQQgflEhr4,12250
79
+ boris/transitions.py,sha256=okyDCO-Vn4p_Fixd8cGiSIaUhUxG5ePIOqGSuP52g_c,12246
80
80
  boris/utilities.py,sha256=dD5HpojqlAGLVkr3YnOsaqfbCMHFYroe040ZchB5WnM,56662
81
- boris/version.py,sha256=mDyPxZPO9ciCvBjRw2xZTdaF3TgIW_KL2omBeJ1TGTo,787
81
+ boris/version.py,sha256=fv-Q0KkA5fOHNIN-jnwFQaHABpI1tEKUf19G8ytKUzo,787
82
82
  boris/video_equalizer.py,sha256=FartoGghFK-T53zklP70rPKYqTuzL8qdvfGlsOF2wwc,5854
83
83
  boris/video_equalizer_ui.py,sha256=1CG3s79eM4JAbaCx3i1ILZXLceb41_gGXlOLNfpBgnw,10142
84
84
  boris/video_operations.py,sha256=rXKWndaALaF-yLEPIY_-Z99XRAncZRzRd1sLzwSpbjs,10768
85
85
  boris/view_df.py,sha256=AKScLASX2Uatw7rqPbsnio83eVT4GZYCFhL091eMvlY,3370
86
86
  boris/view_df_ui.py,sha256=CaMeRH_vQ00CTDDFQn73ZZaS-r8BSTWpL-dMCFqzJ_Q,2775
87
- boris/write_event.py,sha256=RN_cFQZNE2jWbM_BiodL4tsyKT0JPREvUy7xgZnrfaM,24041
87
+ boris/write_event.py,sha256=iczoNOHNBZVAy6ZM2bw6dUC1EqoT9kW9umkdU-qo31U,24125
88
88
  boris/analysis_plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  boris/analysis_plugins/_latency.py,sha256=9kCdFDtb5Zdao1xFpioi_exm_IxyGm6RlY3Opn6GUFo,2030
90
+ boris/analysis_plugins/irr_cohen_kappa.py,sha256=UtIZ_y5Wm9UbZpGI67a-AszYnH_5DV1LNcff76ChQYg,2253
91
+ boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py,sha256=olYp-L5DZ71vs9B-k9PyErLY5-O2d-R69TUYx6mAzj4,2563
92
+ boris/analysis_plugins/irr_weighted_cohen_kappa.py,sha256=ZEV9PHq0IfPmGBP3gxgnDlnUyM_NXmEIhqqltUHMGwU,4561
93
+ boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py,sha256=of4EVdUPCpre-HH5l9d1yD-19vj0Judn60OM0Km40EQ,4880
90
94
  boris/analysis_plugins/list_of_dataframe_columns.py,sha256=VEiVhxADtyaIKN4JrfFV02TuTAfWhQ60bf1mHVQp27I,437
91
95
  boris/analysis_plugins/number_of_occurences.py,sha256=IDyDrdezqvSKT3BlD8QWpSYk8X9nnBBLI80OUnFJ3bY,509
92
96
  boris/analysis_plugins/number_of_occurences_by_independent_variable.py,sha256=_7HTKXsyxNfyO69tP8zkQEHzT0C7qHdL1sqBjnUfRQY,1459
93
97
  boris/analysis_plugins/time_budget.py,sha256=C1wNYwd5Jugr8h5z2aXRUBY8dF8pD4n953dPwNHY5VY,2244
94
- boris/portion/__init__.py,sha256=ZBUG4I7YWhRkeWdP-JEpxhxldJlUYQkeaJseTjdhtJE,602
95
- boris/portion/const.py,sha256=hEp26BKcEg1Js4DfZsBHmDtJJts83Tl1HWQ0CNJNwEc,1588
96
- boris/portion/dict.py,sha256=SyHxc7PfDw2ufNLFQycwJtzmRfL48rDp4UrM2KN7IWc,11282
97
- boris/portion/func.py,sha256=3TkQtFKLfsqntwd27HSGHceFhnXHmT-EbNMqktElC5Q,2174
98
- boris/portion/interval.py,sha256=bAdUiJjGeUAPgsBAImwNeviiwfQq5odfhFZccAWzOTA,20299
99
- boris/portion/io.py,sha256=ppNeRpiLNrocF1yzGeuEUIhYMf2LfsR-cji3d0nmvUs,6371
100
- boris_behav_obs-9.6.1.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
101
- boris_behav_obs-9.6.1.dist-info/METADATA,sha256=rOegvZFaLcobCRMuUmpNavzzhWQyX7wdWPrEs7ahGpY,4602
102
- boris_behav_obs-9.6.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
103
- boris_behav_obs-9.6.1.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
104
- boris_behav_obs-9.6.1.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
105
- boris_behav_obs-9.6.1.dist-info/RECORD,,
98
+ boris/portion/__init__.py,sha256=KmMu4q-iIFO5nrVRIP340mEzxzOlZQqaRupiDmS70es,642
99
+ boris/portion/const.py,sha256=JSYZUktIPCekB6qSom1FPfASn-X7w0G5-aNNHKhIZnw,1715
100
+ boris/portion/dict.py,sha256=uNM-LEY52CZ2VNMMW_C9QukoyTvPlQf8vcbGa1lQBHI,11281
101
+ boris/portion/func.py,sha256=mSQr20YS1ug7R1fRqBg8LifjtXDRvJ6Kjc3WOeL9P34,2172
102
+ boris/portion/interval.py,sha256=sOlj3MAGGaB-JxCkigS-n3qw0fY7TANAsXv1pavr8J4,19931
103
+ boris/portion/io.py,sha256=kpq44pw3xnIyAlPwaR5qRHKRdZ72f8HS9YVIWs5k2pk,6367
104
+ boris_behav_obs-9.6.2.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
105
+ boris_behav_obs-9.6.2.dist-info/METADATA,sha256=Y4dUMgXciZdoXdacbzA0NULwV7c0Cn9kEgveFCXVss4,4637
106
+ boris_behav_obs-9.6.2.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
107
+ boris_behav_obs-9.6.2.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
108
+ boris_behav_obs-9.6.2.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
109
+ boris_behav_obs-9.6.2.dist-info/RECORD,,