boris-behav-obs 9.6.1__py2.py3-none-any.whl → 9.6.3__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.
Files changed (43) hide show
  1. boris/add_modifier.py +1 -5
  2. boris/analysis_plugins/irr_cohen_kappa.py +72 -0
  3. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +77 -0
  4. boris/analysis_plugins/irr_weighted_cohen_kappa.py +120 -0
  5. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +125 -0
  6. boris/analysis_plugins/time_budget.py +0 -4
  7. boris/behav_coding_map_creator.py +0 -1
  8. boris/boris_cli.py +1 -1
  9. boris/coding_pad.py +0 -2
  10. boris/core.py +11 -28
  11. boris/core_qrc.py +3 -0
  12. boris/db_functions.py +4 -4
  13. boris/edit_event.py +0 -9
  14. boris/exclusion_matrix.py +1 -1
  15. boris/export_events.py +63 -71
  16. boris/gui_utilities.py +0 -1
  17. boris/irr.py +10 -22
  18. boris/menu_options.py +2 -0
  19. boris/modifier_coding_map_creator.py +0 -2
  20. boris/observation_operations.py +2 -4
  21. boris/param_panel.py +0 -4
  22. boris/plot_spectrogram_rt.py +2 -1
  23. boris/plot_waveform_rt.py +2 -1
  24. boris/portion/__init__.py +18 -8
  25. boris/portion/const.py +35 -18
  26. boris/portion/dict.py +5 -5
  27. boris/portion/func.py +2 -2
  28. boris/portion/interval.py +21 -41
  29. boris/portion/io.py +41 -32
  30. boris/project_functions.py +2 -1
  31. boris/state_events.py +1 -1
  32. boris/time_budget_functions.py +0 -9
  33. boris/transitions.py +1 -1
  34. boris/version.py +2 -2
  35. boris/video_equalizer.py +0 -2
  36. boris/view_df.py +0 -2
  37. boris/write_event.py +4 -13
  38. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/METADATA +12 -8
  39. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/RECORD +43 -39
  40. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/WHEEL +0 -0
  41. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/entry_points.txt +0 -0
  42. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/licenses/LICENSE.TXT +0 -0
  43. {boris_behav_obs-9.6.1.dist-info → boris_behav_obs-9.6.3.dist-info}/top_level.txt +0 -0
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(
@@ -1021,15 +1021,6 @@ def time_budget_analysis(
1021
1021
  continue
1022
1022
 
1023
1023
  if len(rows) % 2: # unpaired events
1024
- """
1025
- print()
1026
- print(f"{subject=}")
1027
- print(f"{behavior=}")
1028
- print()
1029
- for row in rows:
1030
- print(f"{row['observation']=} {row['occurence']=}")
1031
- print()
1032
- """
1033
1024
  out_cat.append(
1034
1025
  {
1035
1026
  "subject": subject,
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.3"
24
+ __version_date__ = "2025-08-26"
boris/video_equalizer.py CHANGED
@@ -60,8 +60,6 @@ class Video_equalizer(QDialog, Ui_Equalizer):
60
60
  if n_player not in self.equalizer:
61
61
  return
62
62
 
63
- print(self.equalizer)
64
-
65
63
  self.hs_brightness.setValue(self.equalizer[n_player]["hs_brightness"])
66
64
  self.lb_brightness.setText(str(self.equalizer[n_player]["hs_brightness"]))
67
65
 
boris/view_df.py CHANGED
@@ -49,8 +49,6 @@ class View_df(QWidget, Ui_Form):
49
49
  self.lb_plugin_info.setText(f"{plugin_name} v. {plugin_version}")
50
50
  self.setWindowTitle(f"{plugin_name} v. {plugin_version}")
51
51
 
52
- # print(f"{self.df=}")
53
-
54
52
  self.pb_close.clicked.connect(self.close)
55
53
  self.pb_save.clicked.connect(self.save)
56
54
 
boris/write_event.py CHANGED
@@ -68,8 +68,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
68
68
  )
69
69
  return 1
70
70
 
71
- print(f"{mem_time=}")
72
-
73
71
  if mem_time < self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]:
74
72
  _ = dialog.MessageDialog(
75
73
  cfg.programName,
@@ -104,12 +102,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
104
102
 
105
103
  # add media creation date/time
106
104
 
107
- """
108
- print(f"{media_file_name=}")
109
- print(f"{mem_time=}")
110
- """
111
- print(f"{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]=}")
112
-
113
105
  mem_time += dec(
114
106
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_file_name_posix]
115
107
  )
@@ -413,7 +405,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
413
405
  r = modifiers_selector.exec_()
414
406
  if r:
415
407
  selected_modifiers = modifiers_selector.get_modifiers()
416
- # print(f"{selected_modifiers=}")
417
408
 
418
409
  behavior_to_stop_modifier_str: str = ""
419
410
  for idx in util.sorted_keys(selected_modifiers):
@@ -472,8 +463,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
472
463
  comment,
473
464
  frame_idx,
474
465
  ]
475
- # order by image index ASC
476
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
466
+ # order events list using time, subject, behavior
467
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
477
468
 
478
469
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
479
470
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
@@ -483,8 +474,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
483
474
  modifier_str,
484
475
  comment,
485
476
  ]
486
- # order by image index ASC
487
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
477
+ # order events list using time, subject, behavior
478
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
488
479
 
489
480
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
490
481
  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.3
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
@@ -19,7 +19,7 @@ Requires-Python: >=3.12
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.TXT
21
21
  Requires-Dist: exifread>=3.0.0
22
- Requires-Dist: numpy>=1.26.4
22
+ Requires-Dist: numpy==2.3.2
23
23
  Requires-Dist: matplotlib>=3.3.3
24
24
  Requires-Dist: pandas>=2.2.2
25
25
  Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
@@ -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"
@@ -48,7 +49,7 @@ You can not longer run BORIS natively on MacOS (since v.8). Some alternatives to
48
49
 
49
50
  It provides also some analysis tools like time budget and some plotting functions.
50
51
 
51
- The BORIS paper has more than [![BORIS citations counter](http://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications.
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
53
 
53
54
 
54
55
 
@@ -58,13 +59,17 @@ See the official [BORIS web site](https://www.boris.unito.it).
58
59
  [![Python web site](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org)
59
60
  ![Python versions](https://img.shields.io/pypi/pyversions/boris-behav-obs)
60
61
  ![BORIS license](https://img.shields.io/pypi/l/boris-behav-obs)
62
+ [![PyPI version](https://img.shields.io/pypi/v/boris-behav-obs.svg)](https://pypi.org/project/boris-behav-obs/)
63
+
61
64
  [![Number of downloads](https://static.pepy.tech/personalized-badge/boris-behav-obs?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/boris-behav-obs)
62
65
  ![commit-activity](https://img.shields.io/github/commit-activity/m/olivierfriard/BORIS)
63
- [![PyPI version](https://img.shields.io/pypi/v/boris-behav-obs.svg)](https://pypi.org/project/boris-behav-obs/)
64
- ![BORIS scopus citations badge](http://penelope.unito.it/friard/boris_scopus_citations.svg)
66
+ ![GitHub last commit](https://img.shields.io/github/last-commit/olivierfriard/BORIS)
65
67
 
68
+ ![BORIS scopus citations badge](https://penelope.unito.it/friard/boris_scopus_citations.svg)
66
69
 
67
70
 
71
+ ![GitHub Repo stars](https://img.shields.io/github/stars/olivierfriard/BORIS?style=flat&label=Stars)
72
+ [![Please Star](https://img.shields.io/badge/⭐-Star%20this%20repo-blue?style=flat-square)](https://github.com/olivierfriard/BORIS/stargazers)
68
73
 
69
74
  # Documentation
70
75
 
@@ -81,8 +86,7 @@ Some [video tutorials](https://www.boris.unito.it/video_tutorials/) are availabl
81
86
  # Bug reports and feature requests
82
87
 
83
88
 
84
- To search for bugs, report them or request a feature, please use the github tracker:
85
- https://github.com/olivierfriard/BORIS/issues
89
+ To search for bugs, report them or request a feature, please use the [GitHub issues tracker](https://github.com/olivierfriard/BORIS/issues)
86
90
 
87
91
 
88
92
 
@@ -128,7 +132,7 @@ GNU General Public License for more details.
128
132
 
129
133
  Distributed with a [GPL v.3 license](LICENSE.TXT).
130
134
 
131
- Copyright (C) 2012-2024 Olivier Friard
135
+ Copyright (C) 2012-2025 Olivier Friard
132
136
 
133
137
 
134
138