boris-behav-obs 9.7.7__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.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1136 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program 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 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program 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
+ import math
23
+ import statistics
24
+ from decimal import Decimal as dec
25
+ from typing import Tuple
26
+ import tablib
27
+ import logging
28
+ import itertools
29
+ import re
30
+
31
+ from . import config as cfg
32
+ from . import db_functions
33
+ from . import portion as I
34
+ from . import project_functions
35
+ from . import observation_operations
36
+
37
+
38
+ def default_value(ethogram: dict, behavior_code: str, param):
39
+ """
40
+ return value for duration in case of point event
41
+ """
42
+ default_value_ = 0.0
43
+ behavior_type = project_functions.event_type(behavior_code, ethogram)
44
+
45
+ if behavior_type in cfg.POINT_EVENT_TYPES and param in (
46
+ "duration",
47
+ "duration mean",
48
+ "duration stdev",
49
+ "proportion of time",
50
+ ):
51
+ default_value_ = cfg.NA
52
+
53
+ if behavior_type in cfg.STATE_EVENT_TYPES and param in (
54
+ "duration mean",
55
+ "duration stdev",
56
+ ):
57
+ default_value_ = cfg.NA
58
+
59
+ return default_value_
60
+
61
+
62
+ def init_behav_modif(ethogram: dict, selected_subjects: list, distinct_behav_modif: list, parameters: dict) -> dict:
63
+ """
64
+ initialize dictionary with subject, behaviors and modifiers
65
+ """
66
+ behaviors: dict = {}
67
+ for subj in selected_subjects:
68
+ behaviors[subj] = {}
69
+ for behav_modif in distinct_behav_modif:
70
+ behav, modif = behav_modif
71
+ behav_modif_str = "|".join(behav_modif) if modif else behav
72
+
73
+ if behav_modif_str not in behaviors[subj]:
74
+ behaviors[subj][behav_modif_str] = {}
75
+
76
+ for param in parameters:
77
+ behaviors[subj][behav_modif_str][param[0]] = default_value(ethogram, behav, param[0])
78
+
79
+ return behaviors
80
+
81
+
82
+ def init_behav_modif_bin(ethogram: dict, selected_subjects: list, distinct_behav_modif: list, parameters: dict) -> dict:
83
+ """
84
+ initialize dictionary with subject, behaviors and modifiers
85
+ """
86
+ behaviors: dict = {}
87
+ for subj in selected_subjects:
88
+ behaviors[subj] = {}
89
+ for behav_modif in distinct_behav_modif:
90
+ if behav_modif not in behaviors[subj]:
91
+ behaviors[subj][behav_modif] = {}
92
+
93
+ for param in parameters:
94
+ behaviors[subj][behav_modif][param[0]] = default_value(ethogram, behav_modif[0], param[0])
95
+
96
+ return behaviors
97
+
98
+
99
+ class StdevFunc:
100
+ """
101
+ class to enable std dev function in SQL
102
+ """
103
+
104
+ def __init__(self):
105
+ self.M = 0.0
106
+ self.S = 0.0
107
+ self.k = 1
108
+
109
+ def step(self, value):
110
+ if value is None:
111
+ return
112
+ tM = self.M
113
+ self.M += (value - tM) / self.k
114
+ self.S += (value - tM) * (value - self.M)
115
+ self.k += 1
116
+
117
+ def finalize(self):
118
+ if self.k < 3:
119
+ return None
120
+ return math.sqrt(self.S / (self.k - 2))
121
+
122
+
123
+ def synthetic_time_budget_bin(pj: dict, selected_observations: list, parameters_obs: dict):
124
+ """
125
+ create a synthetic time budget divised in time bin
126
+
127
+ Args:
128
+ pj (dict): project dictionary
129
+ selected_observations (list): list of observations to include in time budget
130
+ parameters_obs (dict):
131
+
132
+ Returns:
133
+ bool: True if everything OK
134
+ str: message
135
+ tablib.Dataset: dataset containing synthetic time budget data
136
+ """
137
+
138
+ def interval_len(interval):
139
+ return dec(0) if interval.empty else sum([x.upper - x.lower for x in interval])
140
+
141
+ def interval_number(interval):
142
+ return dec(0) if interval.empty else len(interval)
143
+
144
+ def interval_mean(interval):
145
+ return dec(0) if interval.empty else sum([x.upper - x.lower for x in interval]) / len(interval)
146
+
147
+ def interval_std_dev(interval) -> str:
148
+ if interval.empty:
149
+ return cfg.NA
150
+ else:
151
+ try:
152
+ return f"{statistics.stdev([x.upper - x.lower for x in interval]):.3f}"
153
+ except Exception:
154
+ return cfg.NA
155
+
156
+ selected_behaviors = parameters_obs[cfg.SELECTED_BEHAVIORS]
157
+ time_interval = parameters_obs["time"]
158
+ start_time = parameters_obs[cfg.START_TIME]
159
+ end_time = parameters_obs[cfg.END_TIME]
160
+ time_bin_size = dec(parameters_obs[cfg.TIME_BIN_SIZE])
161
+
162
+ parameters = [
163
+ ["duration", "Total duration"],
164
+ ["number", "Number of occurrences"],
165
+ ["duration mean", "Duration mean"],
166
+ ["duration stdev", "Duration std dev"],
167
+ ["proportion of time", "Proportion of time"],
168
+ ]
169
+
170
+ data_report = tablib.Dataset()
171
+ data_report.title = "Synthetic time budget with time bin"
172
+
173
+ distinct_behav_modif = []
174
+ for obs_id in selected_observations:
175
+ for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
176
+ if parameters_obs[cfg.INCLUDE_MODIFIERS]:
177
+ if parameters_obs[cfg.EXCLUDE_NON_CODED_MODIFIERS]:
178
+ # get coded modifiers
179
+ if (
180
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
181
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
182
+ ) not in distinct_behav_modif:
183
+ distinct_behav_modif.append((event[cfg.EVENT_BEHAVIOR_FIELD_IDX], event[cfg.EVENT_MODIFIER_FIELD_IDX]))
184
+ else:
185
+ # get all modifiers combination
186
+ ms: list = []
187
+ modifiers_list = project_functions.get_modifiers_of_behavior(pj[cfg.ETHOGRAM], event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
188
+ if modifiers_list:
189
+ for modif_set in modifiers_list[0]:
190
+ modif_set.append("None")
191
+ ms.append([re.sub(r" \(.*\)", "", x) for x in modif_set])
192
+ distinct_modifiers = ["|".join(x) for x in itertools.product(*ms)]
193
+ for modifier in distinct_modifiers:
194
+ distinct_behav_modif.append((event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modifier))
195
+
196
+ else:
197
+ if (event[cfg.EVENT_BEHAVIOR_FIELD_IDX], "") not in distinct_behav_modif:
198
+ distinct_behav_modif.append((event[cfg.EVENT_BEHAVIOR_FIELD_IDX], ""))
199
+
200
+ distinct_behav_modif.sort()
201
+
202
+ # add selected behaviors that are not observed
203
+ for behav in selected_behaviors:
204
+ if [x for x in distinct_behav_modif if x[0] == behav] == []:
205
+ distinct_behav_modif.append((behav, ""))
206
+
207
+ param_header = ["Observations id", "Total length (s)", "Time interval (s)"]
208
+ subj_header, behav_header, modif_header = (
209
+ [""] * len(param_header),
210
+ [""] * len(param_header),
211
+ [""] * len(param_header),
212
+ )
213
+ subj_header[1] = "Subjects:"
214
+ behav_header[1] = "Behaviors:"
215
+ modif_header[1] = "Modifiers:"
216
+
217
+ for subj in parameters_obs[cfg.SELECTED_SUBJECTS]:
218
+ for behavior_modifiers in distinct_behav_modif:
219
+ behavior, modifiers = behavior_modifiers
220
+ behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior
221
+ for param in parameters:
222
+ subj_header.append(subj)
223
+ behav_header.append(behavior)
224
+ modif_header.append(modifiers)
225
+ param_header.append(param[1])
226
+
227
+ data_report.append(subj_header)
228
+ data_report.append(behav_header)
229
+ if parameters_obs[cfg.INCLUDE_MODIFIERS]:
230
+ data_report.append(modif_header)
231
+ data_report.append(param_header)
232
+
233
+ state_events_list = [
234
+ pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in pj[cfg.ETHOGRAM] if cfg.STATE in pj[cfg.ETHOGRAM][x][cfg.TYPE].upper()
235
+ ]
236
+ # select time interval
237
+ for obs_id in selected_observations:
238
+ behaviors = init_behav_modif_bin(pj[cfg.ETHOGRAM], parameters_obs[cfg.SELECTED_SUBJECTS], distinct_behav_modif, parameters)
239
+
240
+ obs_length = observation_operations.observation_total_length(pj[cfg.OBSERVATIONS][obs_id])
241
+
242
+ if obs_length == -1:
243
+ obs_length = 0
244
+ if time_interval == cfg.TIME_FULL_OBS:
245
+ min_time = dec(0)
246
+ max_time = dec(obs_length)
247
+
248
+ if time_interval == cfg.TIME_EVENTS:
249
+ try:
250
+ min_time = dec(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0])
251
+ except Exception:
252
+ min_time = dec(0)
253
+ try:
254
+ max_time = dec(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0])
255
+ except Exception:
256
+ max_time = dec(obs_length)
257
+
258
+ if time_interval == cfg.TIME_ARBITRARY_INTERVAL:
259
+ min_time = dec(start_time)
260
+ max_time = dec(end_time)
261
+
262
+ if time_interval == cfg.TIME_OBS_INTERVAL:
263
+ obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
264
+ offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
265
+ min_time = dec(obs_interval[0]) + offset
266
+ # Use max media duration for max time if no interval is defined (=0)
267
+ max_time = dec(obs_interval[1]) + offset if obs_interval[1] != 0 else dec(obs_length)
268
+
269
+ events_interval = {}
270
+ mem_events_interval = {}
271
+
272
+ for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
273
+ if event[cfg.EVENT_SUBJECT_FIELD_IDX] == "":
274
+ current_subject = cfg.NO_FOCAL_SUBJECT
275
+ else:
276
+ current_subject = event[cfg.EVENT_SUBJECT_FIELD_IDX]
277
+
278
+ if current_subject not in parameters_obs[cfg.SELECTED_SUBJECTS]:
279
+ continue
280
+ if current_subject not in events_interval:
281
+ events_interval[current_subject] = {}
282
+ mem_events_interval[current_subject] = {}
283
+
284
+ if parameters_obs[cfg.INCLUDE_MODIFIERS]:
285
+ modif = event[cfg.EVENT_MODIFIER_FIELD_IDX]
286
+ else:
287
+ modif = ""
288
+ if (event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif) not in distinct_behav_modif:
289
+ continue
290
+
291
+ if (event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif) not in events_interval[current_subject]:
292
+ events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)] = I.empty()
293
+ mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)] = []
294
+
295
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_events_list:
296
+ mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)].append(event[cfg.EVENT_TIME_FIELD_IDX])
297
+ if len(mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)]) == 2:
298
+ events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)] |= I.closedopen(
299
+ mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)][0],
300
+ mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)][1],
301
+ )
302
+ mem_events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)] = []
303
+ else:
304
+ events_interval[current_subject][(event[cfg.EVENT_BEHAVIOR_FIELD_IDX], modif)] |= I.singleton(
305
+ event[cfg.EVENT_TIME_FIELD_IDX]
306
+ )
307
+
308
+ time_bin_start = min_time
309
+
310
+ if time_bin_size:
311
+ time_bin_end = time_bin_start + time_bin_size
312
+ if time_bin_end > max_time:
313
+ time_bin_end = max_time
314
+ else:
315
+ time_bin_end = max_time
316
+
317
+ while True:
318
+ for subject in events_interval:
319
+ # check behavior to exclude from total time
320
+ time_to_subtract = 0
321
+ if cfg.EXCLUDED_BEHAVIORS in parameters_obs:
322
+ for behav in events_interval[subject]:
323
+ if behav[0] in parameters_obs.get(cfg.EXCLUDED_BEHAVIORS, []):
324
+ interval_intersec = events_interval[subject][behav] & I.closed(time_bin_start, time_bin_end)
325
+ time_to_subtract += interval_len(interval_intersec)
326
+
327
+ for behav in events_interval[subject]:
328
+ interval_intersec = events_interval[subject][behav] & I.closed(time_bin_start, time_bin_end)
329
+
330
+ nocc = interval_number(interval_intersec)
331
+ behaviors[subject][behav]["number"] = nocc
332
+
333
+ behav_type = project_functions.event_type(behav[0], pj[cfg.ETHOGRAM])
334
+ if behav_type in cfg.STATE_EVENT_TYPES:
335
+ dur = interval_len(interval_intersec)
336
+ behaviors[subject][behav]["duration"] = f"{dur:.3f}"
337
+ behaviors[subject][behav]["duration mean"] = f"{interval_mean(interval_intersec):.3f}"
338
+ behaviors[subject][behav]["duration stdev"] = interval_std_dev(interval_intersec)
339
+
340
+ if behav[0] in parameters_obs.get(cfg.EXCLUDED_BEHAVIORS, []):
341
+ proportion = dur / (time_bin_end - time_bin_start)
342
+ else:
343
+ proportion = dur / ((time_bin_end - time_bin_start) - time_to_subtract)
344
+ behaviors[subject][behav]["proportion of time"] = f"{proportion:.3f}"
345
+
346
+ if behav_type in cfg.POINT_EVENT_TYPES:
347
+ behaviors[subject][behav]["duration"] = cfg.NA
348
+ behaviors[subject][behav]["duration mean"] = cfg.NA
349
+ behaviors[subject][behav]["duration stdev"] = cfg.NA
350
+ behaviors[subject][behav]["proportion of time"] = cfg.NA
351
+
352
+ columns = [obs_id, f"{max_time - min_time:.3f}", f"{time_bin_start:.3f}-{time_bin_end:.3f}"]
353
+ for subject in parameters_obs[cfg.SELECTED_SUBJECTS]:
354
+ for behavior_modifiers in distinct_behav_modif:
355
+ behavior, modifiers = behavior_modifiers
356
+ behavior_modifiers_str = behavior_modifiers
357
+
358
+ for param in parameters:
359
+ columns.append(behaviors[subject][behavior_modifiers_str][param[0]])
360
+
361
+ data_report.append(columns)
362
+
363
+ time_bin_start = time_bin_end
364
+ time_bin_end = time_bin_start + time_bin_size
365
+ if time_bin_end > max_time:
366
+ time_bin_end = max_time
367
+
368
+ if time_bin_start == time_bin_end:
369
+ break
370
+
371
+ return True, data_report
372
+
373
+
374
+ def synthetic_time_budget(pj: dict, selected_observations: list, parameters_obs: dict):
375
+ """
376
+ create a synthetic time budget
377
+
378
+ Args:
379
+ pj (dict): project dictionary
380
+ selected_observations (list): list of observations to include in time budget
381
+ parameters_obs (dict):
382
+
383
+ Returns:
384
+ bool: True if everything OK
385
+ str: message
386
+ tablib.Dataset: dataset containing synthetic time budget data
387
+ """
388
+
389
+ interval = parameters_obs["time"]
390
+ start_time = parameters_obs[cfg.START_TIME]
391
+ end_time = parameters_obs[cfg.END_TIME]
392
+
393
+ parameters = [
394
+ ["duration", "Total duration"],
395
+ ["number", "Number of occurrences"],
396
+ ["duration mean", "Duration mean"],
397
+ ["duration stdev", "Duration std dev"],
398
+ ["proportion of time", "Proportion of time"],
399
+ ]
400
+
401
+ data_report = tablib.Dataset()
402
+ data_report.title = "Synthetic time budget"
403
+
404
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
405
+ pj, parameters_obs[cfg.SELECTED_SUBJECTS], selected_observations, parameters_obs[cfg.SELECTED_BEHAVIORS]
406
+ )
407
+
408
+ if not ok:
409
+ return False, msg, None
410
+
411
+ db_connector.create_aggregate("stdev", 1, StdevFunc)
412
+ cursor = db_connector.cursor()
413
+
414
+ # add selected behaviors that are not observed
415
+ distinct_behav_modif: list = []
416
+ for behavior in parameters_obs[cfg.SELECTED_BEHAVIORS]:
417
+ # modifiers
418
+ if parameters_obs[cfg.INCLUDE_MODIFIERS]:
419
+ if parameters_obs[cfg.EXCLUDE_NON_CODED_MODIFIERS]:
420
+ # get coded modifiers
421
+ cursor.execute("SELECT DISTINCT modifiers FROM aggregated_events WHERE behavior = ?", (behavior,))
422
+ for row in cursor.fetchall():
423
+ distinct_behav_modif.append((behavior, row["modifiers"]))
424
+ else:
425
+ # get all modifiers combination
426
+ ms: list = []
427
+ modifiers_list = project_functions.get_modifiers_of_behavior(pj[cfg.ETHOGRAM], behavior)
428
+ if modifiers_list:
429
+ for modif_set in modifiers_list[0]:
430
+ modif_set.append("None")
431
+ ms.append([re.sub(r" \(.*\)", "", x) for x in modif_set])
432
+ distinct_modifiers = ["|".join(x) for x in itertools.product(*ms)]
433
+ for modifier in distinct_modifiers:
434
+ distinct_behav_modif.append((behavior, modifier))
435
+
436
+ else:
437
+ distinct_behav_modif.append((behavior, ""))
438
+
439
+ # print(f"{distinct_behav_modif=}")
440
+
441
+ param_header = ["Observations id", "Total length (s)"]
442
+ subj_header, behav_header, modif_header = (
443
+ [""] * len(param_header),
444
+ [""] * len(param_header),
445
+ [""] * len(param_header),
446
+ )
447
+ subj_header[1] = "Subjects:"
448
+ behav_header[1] = "Behaviors:"
449
+ modif_header[1] = "Modifiers:"
450
+
451
+ for subj in parameters_obs[cfg.SELECTED_SUBJECTS]:
452
+ for behavior, modifiers in distinct_behav_modif:
453
+ """behavior, modifiers = behavior_modifiers"""
454
+ """behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior"""
455
+ for param in parameters:
456
+ subj_header.append(subj)
457
+ behav_header.append(behavior)
458
+ modif_header.append(modifiers)
459
+ param_header.append(param[1])
460
+
461
+ data_report.append(subj_header)
462
+ data_report.append(behav_header)
463
+ if parameters_obs[cfg.INCLUDE_MODIFIERS]:
464
+ data_report.append(modif_header)
465
+ data_report.append(param_header)
466
+
467
+ # select time interval
468
+ for obs_id in selected_observations:
469
+ behaviors = init_behav_modif(pj[cfg.ETHOGRAM], parameters_obs[cfg.SELECTED_SUBJECTS], distinct_behav_modif, parameters)
470
+
471
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
472
+ pj, parameters_obs[cfg.SELECTED_SUBJECTS], [obs_id], parameters_obs[cfg.SELECTED_BEHAVIORS]
473
+ )
474
+
475
+ if not ok:
476
+ return False, msg, None
477
+
478
+ db_connector.create_aggregate("stdev", 1, StdevFunc)
479
+ cursor = db_connector.cursor()
480
+ # if modifiers not to be included set modifiers to ""
481
+ if not parameters_obs[cfg.INCLUDE_MODIFIERS]:
482
+ cursor.execute("UPDATE aggregated_events SET modifiers = ''")
483
+
484
+ # time
485
+ obs_length = observation_operations.observation_total_length(pj[cfg.OBSERVATIONS][obs_id])
486
+
487
+ if obs_length == dec(-1): # media length not available
488
+ interval = cfg.TIME_EVENTS
489
+
490
+ if obs_length == dec(-2): # images obs without time
491
+ interval = cfg.TIME_EVENTS
492
+
493
+ if interval == cfg.TIME_FULL_OBS:
494
+ min_time = float(0)
495
+ max_time = float(obs_length)
496
+
497
+ if interval == cfg.TIME_EVENTS:
498
+ try:
499
+ min_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0])
500
+ except Exception:
501
+ min_time = float(0)
502
+ try:
503
+ max_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0])
504
+ except Exception:
505
+ max_time = float(obs_length)
506
+
507
+ if interval == cfg.TIME_ARBITRARY_INTERVAL:
508
+ min_time = float(start_time)
509
+ max_time = float(end_time)
510
+
511
+ if obs_length != dec(-2): # # obs not an images obs without time
512
+ # adapt start and stop to the selected time interval
513
+ cursor.execute(
514
+ "UPDATE aggregated_events SET start = ? WHERE observation = ? AND start < ? AND stop BETWEEN ? AND ?",
515
+ (
516
+ min_time,
517
+ obs_id,
518
+ min_time,
519
+ min_time,
520
+ max_time,
521
+ ),
522
+ )
523
+ cursor.execute(
524
+ "UPDATE aggregated_events SET stop = ? WHERE observation = ? AND stop > ? AND start BETWEEN ? AND ?",
525
+ (
526
+ max_time,
527
+ obs_id,
528
+ max_time,
529
+ min_time,
530
+ max_time,
531
+ ),
532
+ )
533
+
534
+ cursor.execute(
535
+ "UPDATE aggregated_events SET start = ?, stop = ? WHERE observation = ? AND start < ? AND stop > ?",
536
+ (
537
+ min_time,
538
+ max_time,
539
+ obs_id,
540
+ min_time,
541
+ max_time,
542
+ ),
543
+ )
544
+
545
+ cursor.execute(
546
+ "DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
547
+ (
548
+ obs_id,
549
+ min_time,
550
+ min_time,
551
+ max_time,
552
+ max_time,
553
+ ),
554
+ )
555
+
556
+ for subject in parameters_obs[cfg.SELECTED_SUBJECTS]:
557
+ # check if behaviors are to exclude from total time
558
+ time_to_subtract = 0
559
+ if obs_length != dec(-2): # obs not an images obs without time
560
+ if cfg.EXCLUDED_BEHAVIORS in parameters_obs:
561
+ for excluded_behav in parameters_obs[cfg.EXCLUDED_BEHAVIORS]:
562
+ cursor.execute(
563
+ ("SELECT SUM(stop-start) FROM aggregated_events WHERE observation = ? AND subject = ? AND behavior = ? "),
564
+ (
565
+ obs_id,
566
+ subject,
567
+ excluded_behav,
568
+ ),
569
+ )
570
+ for row in cursor.fetchall():
571
+ if row[0] is not None:
572
+ time_to_subtract += row[0]
573
+
574
+ for behavior_modifiers in distinct_behav_modif:
575
+ behavior, modifiers = behavior_modifiers
576
+ behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior
577
+
578
+ cursor.execute(
579
+ (
580
+ "SELECT SUM(stop - start) AS duration, "
581
+ "COUNT(*) AS n_occurences, "
582
+ "AVG(stop - start) AS mean, "
583
+ "stdev(stop - start) AS ST_DEV, type "
584
+ "FROM aggregated_events "
585
+ "WHERE observation = ? AND subject = ? AND behavior = ? AND modifiers = ? "
586
+ ),
587
+ (
588
+ obs_id,
589
+ subject,
590
+ behavior,
591
+ modifiers,
592
+ ),
593
+ )
594
+
595
+ for row in cursor.fetchall():
596
+ behaviors[subject][behavior_modifiers_str]["number"] = 0 if row["n_occurences"] is None else row["n_occurences"]
597
+
598
+ if obs_length == dec(-2): # images obs without time
599
+ behaviors[subject][behavior_modifiers_str]["duration"] = cfg.NA
600
+ behaviors[subject][behavior_modifiers_str]["duration mean"] = cfg.NA
601
+ behaviors[subject][behavior_modifiers_str]["duration stdev"] = cfg.NA
602
+ behaviors[subject][behavior_modifiers_str]["proportion of time"] = cfg.NA
603
+
604
+ else:
605
+ if row["type"] == cfg.POINT:
606
+ behaviors[subject][behavior_modifiers_str]["duration"] = cfg.NA
607
+ behaviors[subject][behavior_modifiers_str]["duration mean"] = cfg.NA
608
+ behaviors[subject][behavior_modifiers_str]["duration stdev"] = cfg.NA
609
+ behaviors[subject][behavior_modifiers_str]["proportion of time"] = cfg.NA
610
+
611
+ if row["type"] == cfg.STATE:
612
+ behaviors[subject][behavior_modifiers_str]["duration"] = (
613
+ cfg.NA if row["duration"] is None else f"{row['duration']:.3f}"
614
+ )
615
+ behaviors[subject][behavior_modifiers_str]["duration mean"] = (
616
+ cfg.NA if row["mean"] is None else f"{row['mean']:.3f}"
617
+ )
618
+ behaviors[subject][behavior_modifiers_str]["duration stdev"] = (
619
+ cfg.NA if row["ST_DEV"] is None else f"{row['ST_DEV']:.3f}"
620
+ )
621
+
622
+ if behavior not in parameters_obs[cfg.EXCLUDED_BEHAVIORS]:
623
+ try:
624
+ behaviors[subject][behavior_modifiers_str]["proportion of time"] = (
625
+ cfg.NA
626
+ if row["duration"] is None
627
+ else f"{row['duration'] / ((max_time - min_time) - time_to_subtract):.3f}"
628
+ )
629
+ except ZeroDivisionError:
630
+ behaviors[subject][behavior_modifiers_str]["proportion of time"] = cfg.NA
631
+ else:
632
+ # behavior subtracted
633
+ behaviors[subject][behavior_modifiers_str]["proportion of time"] = (
634
+ cfg.NA if row["duration"] is None else f"{row['duration'] / (max_time - min_time):.3f}"
635
+ )
636
+
637
+ if obs_length == dec(-2):
638
+ columns = [obs_id, cfg.NA]
639
+ else:
640
+ columns = [obs_id, f"{max_time - min_time:0.3f}"]
641
+ for subj in parameters_obs[cfg.SELECTED_SUBJECTS]:
642
+ for behavior_modifiers in distinct_behav_modif:
643
+ behavior, modifiers = behavior_modifiers
644
+ behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior
645
+
646
+ for param in parameters:
647
+ columns.append(behaviors[subj][behavior_modifiers_str][param[0]])
648
+
649
+ data_report.append(columns)
650
+
651
+ return True, msg, data_report
652
+
653
+
654
+ def time_budget_analysis(
655
+ ethogram: dict, cursor, selected_observations: list, parameters: dict, by_category: bool = False
656
+ ) -> Tuple[list, dict]:
657
+ """
658
+ extract number of occurrences, total duration, mean ...
659
+ if start_time = 0 and end_time = 0 all events are extracted
660
+
661
+ Args:
662
+ ethogram (dict): project ethogram
663
+ cursor: cursor on temporary database
664
+ selected_observations (list): selected observations
665
+ parameters (dict): parameters for analysis
666
+ by_category (bool): True for grouping in behavioral category else False
667
+
668
+ Returns:
669
+ list: results
670
+ dict:
671
+ """
672
+
673
+ logging.debug("time_budget_analysis function")
674
+
675
+ logging.debug(f"{selected_observations=}")
676
+ logging.debug(f"{parameters=}")
677
+
678
+ categories: dict = {}
679
+ out: list = []
680
+ for subject in parameters[cfg.SELECTED_SUBJECTS]:
681
+ logging.debug(f"{subject=}")
682
+
683
+ out_cat: list = []
684
+ categories[subject] = {}
685
+
686
+ for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
687
+ logging.debug(f"{behavior=}")
688
+
689
+ if parameters[cfg.INCLUDE_MODIFIERS]: # with modifiers
690
+ if parameters[cfg.EXCLUDE_NON_CODED_MODIFIERS]:
691
+ # get coded modifiers
692
+ cursor.execute("SELECT DISTINCT modifiers FROM events WHERE subject = ? AND code = ?", (subject, behavior))
693
+ distinct_modifiers = [x[0] for x in cursor.fetchall()]
694
+ else:
695
+ # get all modifiers combinations
696
+ ms: list = []
697
+ modifiers_list = project_functions.get_modifiers_of_behavior(ethogram, behavior)
698
+ if modifiers_list:
699
+ for modif_set in modifiers_list[0]:
700
+ modif_set.append("None")
701
+ ms.append([re.sub(r" \(.*\)", "", x) for x in modif_set])
702
+ distinct_modifiers = ["|".join(x) for x in itertools.product(*ms)]
703
+
704
+ if not distinct_modifiers:
705
+ if not parameters[cfg.EXCLUDE_BEHAVIORS]:
706
+ if project_functions.event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES:
707
+ # check if observation from pictures
708
+ if parameters["start time"] == dec("0.000") and parameters["end time"] == dec("0.000"):
709
+ duration = cfg.NA
710
+ else:
711
+ duration = 0.000
712
+
713
+ out_cat.append(
714
+ {
715
+ "subject": subject,
716
+ "behavior": behavior,
717
+ "modifiers": "",
718
+ "duration": duration,
719
+ "duration_mean": cfg.NA,
720
+ "duration_stdev": cfg.NA,
721
+ "number": "0",
722
+ "inter_duration_mean": cfg.NA,
723
+ "inter_duration_stdev": cfg.NA,
724
+ }
725
+ )
726
+ else: # point
727
+ out_cat.append(
728
+ {
729
+ "subject": subject,
730
+ "behavior": behavior,
731
+ "modifiers": "",
732
+ "duration": cfg.NA,
733
+ "duration_mean": cfg.NA,
734
+ "duration_stdev": cfg.NA,
735
+ "number": "0",
736
+ "inter_duration_mean": cfg.NA,
737
+ "inter_duration_stdev": cfg.NA,
738
+ }
739
+ )
740
+ continue
741
+
742
+ if project_functions.event_type(behavior, ethogram) in cfg.POINT_EVENT_TYPES:
743
+ for modifier in distinct_modifiers:
744
+ cursor.execute(
745
+ (
746
+ "SELECT occurence, observation FROM events "
747
+ "WHERE subject = ? "
748
+ "AND code = ? "
749
+ "AND modifiers = ? "
750
+ "ORDER BY observation, occurence"
751
+ ),
752
+ (subject, behavior, modifier),
753
+ )
754
+
755
+ rows = cursor.fetchall()
756
+
757
+ if len(selected_observations) == 1:
758
+ new_rows: list = []
759
+ for occurence, observation in rows:
760
+ if occurence is None:
761
+ new_rows.append([float("NaN"), observation])
762
+ else:
763
+ new_rows.append([occurence, observation])
764
+ rows = list(new_rows)
765
+
766
+ # include behaviors without events
767
+ if len(rows) == 0:
768
+ if not parameters[cfg.EXCLUDE_BEHAVIORS]:
769
+ out_cat.append(
770
+ {
771
+ "subject": subject,
772
+ "behavior": behavior,
773
+ "modifiers": "",
774
+ "duration": cfg.NA,
775
+ "duration_mean": cfg.NA,
776
+ "duration_stdev": cfg.NA,
777
+ "number": 0,
778
+ "inter_duration_mean": cfg.NA,
779
+ "inter_duration_stdev": cfg.NA,
780
+ }
781
+ )
782
+ continue
783
+
784
+ # inter events duration
785
+ all_event_interdurations = []
786
+ for idx, row in enumerate(rows):
787
+ if idx and row[1] == rows[idx - 1][1]:
788
+ all_event_interdurations.append(float(row[0]) - float(rows[idx - 1][0]))
789
+
790
+ if [x for x in all_event_interdurations if math.isnan(x)] or len(all_event_interdurations) == 0:
791
+ inter_duration_mean = cfg.NA
792
+ inter_duration_stdev = cfg.NA
793
+ else:
794
+ inter_duration_mean = round(statistics.mean(all_event_interdurations), 3)
795
+ if len(all_event_interdurations) > 1:
796
+ inter_duration_stdev = round(statistics.stdev(all_event_interdurations), 3)
797
+ else:
798
+ inter_duration_stdev = cfg.NA
799
+
800
+ out_cat.append(
801
+ {
802
+ "subject": subject,
803
+ "behavior": behavior,
804
+ "modifiers": modifier,
805
+ "duration": cfg.NA,
806
+ "duration_mean": cfg.NA,
807
+ "duration_stdev": cfg.NA,
808
+ "number": len(rows),
809
+ "inter_duration_mean": inter_duration_mean,
810
+ "inter_duration_stdev": inter_duration_stdev,
811
+ }
812
+ )
813
+
814
+ if project_functions.event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES:
815
+ for modifier in distinct_modifiers:
816
+ cursor.execute(
817
+ (
818
+ "SELECT occurence, observation FROM events "
819
+ "WHERE subject = ? "
820
+ "AND code = ? "
821
+ "AND modifiers = ? "
822
+ "ORDER BY observation, occurence"
823
+ ),
824
+ (subject, behavior, modifier),
825
+ )
826
+
827
+ rows = list(cursor.fetchall())
828
+
829
+ if len(rows) == 0:
830
+ if not parameters[cfg.EXCLUDE_BEHAVIORS]: # include behaviors without events
831
+ # check if observation from pictures
832
+ if parameters["start time"] == dec("0.000") and parameters["end time"] == dec("0.000"):
833
+ duration = cfg.NA
834
+ else:
835
+ duration: float = 0.000
836
+ out_cat.append(
837
+ {
838
+ "subject": subject,
839
+ "behavior": behavior,
840
+ "modifiers": modifier,
841
+ "duration": duration,
842
+ "duration_mean": cfg.NA,
843
+ "duration_stdev": cfg.NA,
844
+ "number": 0,
845
+ "inter_duration_mean": cfg.NA,
846
+ "inter_duration_stdev": cfg.NA,
847
+ }
848
+ )
849
+ continue
850
+
851
+ if len(rows) % 2:
852
+ out_cat.append(
853
+ {
854
+ "subject": subject,
855
+ "behavior": behavior,
856
+ "modifiers": modifier,
857
+ "duration": cfg.UNPAIRED,
858
+ "duration_mean": cfg.UNPAIRED,
859
+ "duration_stdev": cfg.UNPAIRED,
860
+ "number": cfg.UNPAIRED,
861
+ "inter_duration_mean": cfg.UNPAIRED,
862
+ "inter_duration_stdev": cfg.UNPAIRED,
863
+ }
864
+ )
865
+ else:
866
+ all_event_durations: list = []
867
+ all_event_interdurations: list = []
868
+ for idx, row in enumerate(rows):
869
+ # event
870
+ if idx % 2 == 0:
871
+ if row[0] is not None and rows[idx + 1][0] is not None:
872
+ new_init, new_end = float(row[0]), float(rows[idx + 1][0])
873
+ all_event_durations.append(new_end - new_init)
874
+ else:
875
+ all_event_durations.append(float("NaN"))
876
+
877
+ # inter event if same observation
878
+ if idx % 2 and idx != len(rows) - 1 and row[1] == rows[idx + 1][1]:
879
+ if row[0] is not None and rows[idx + 1][0] is not None:
880
+ # and (
881
+ # parameters["start time"] <= row[0] <= parameters["end time"]
882
+ # and parameters["start time"] <= rows[idx + 1][0] <= parameters["end time"]
883
+ # ):
884
+ all_event_interdurations.append(float(rows[idx + 1][0]) - float(row[0]))
885
+ else:
886
+ all_event_interdurations.append(float("NaN"))
887
+
888
+ # events
889
+ if [x for x in all_event_durations if math.isnan(x)]:
890
+ total_duration = cfg.NA
891
+ else:
892
+ total_duration = round(sum(all_event_durations), 3)
893
+ if [x for x in all_event_durations if math.isnan(x)] or len(all_event_durations) == 0:
894
+ duration_mean = cfg.NA
895
+ duration_stdev = cfg.NA
896
+ else:
897
+ duration_mean = round(statistics.mean(all_event_durations), 3)
898
+ if len(all_event_durations) > 1:
899
+ duration_stdev = round(statistics.stdev(all_event_durations), 3)
900
+ else:
901
+ duration_stdev = cfg.NA
902
+ # interduration
903
+ if [x for x in all_event_interdurations if math.isnan(x)] or len(all_event_interdurations) == 0:
904
+ inter_duration_mean = cfg.NA
905
+ inter_duration_stdev = cfg.NA
906
+ else:
907
+ inter_duration_mean = round(statistics.mean(all_event_interdurations), 3)
908
+ if len(all_event_interdurations) > 1:
909
+ inter_duration_stdev = round(statistics.stdev(all_event_interdurations), 3)
910
+ else:
911
+ inter_duration_stdev = cfg.NA
912
+
913
+ out_cat.append(
914
+ {
915
+ "subject": subject,
916
+ "behavior": behavior,
917
+ "modifiers": modifier,
918
+ "duration": total_duration,
919
+ "duration_mean": duration_mean,
920
+ "duration_stdev": duration_stdev,
921
+ "number": len(all_event_durations),
922
+ "inter_duration_mean": inter_duration_mean,
923
+ "inter_duration_stdev": inter_duration_stdev,
924
+ }
925
+ )
926
+
927
+ else: # no modifiers
928
+ if project_functions.event_type(behavior, ethogram) in cfg.POINT_EVENT_TYPES:
929
+ cursor.execute(
930
+ ("SELECT occurence, observation FROM events WHERE subject = ? AND code = ? ORDER BY observation, occurence"),
931
+ (subject, behavior),
932
+ )
933
+
934
+ rows = list(cursor.fetchall())
935
+
936
+ if len(selected_observations) == 1:
937
+ new_rows: list = []
938
+ for occurence, observation in rows:
939
+ if occurence is None:
940
+ new_rows.append([float("NaN"), observation])
941
+ else:
942
+ new_rows.append([occurence, observation])
943
+ rows = list(new_rows)
944
+
945
+ # include behaviors without events
946
+ if len(rows) == 0:
947
+ if not parameters[cfg.EXCLUDE_BEHAVIORS]:
948
+ out_cat.append(
949
+ {
950
+ "subject": subject,
951
+ "behavior": behavior,
952
+ "modifiers": "",
953
+ "duration": cfg.NA,
954
+ "duration_mean": cfg.NA,
955
+ "duration_stdev": cfg.NA,
956
+ "number": 0,
957
+ "inter_duration_mean": cfg.NA,
958
+ "inter_duration_stdev": cfg.NA,
959
+ }
960
+ )
961
+ continue
962
+
963
+ # inter events duration
964
+ all_event_interdurations = []
965
+ for idx, row in enumerate(rows):
966
+ if idx and row[1] == rows[idx - 1][1]:
967
+ all_event_interdurations.append(float(row[0]) - float(rows[idx - 1][0]))
968
+
969
+ if [x for x in all_event_interdurations if math.isnan(x)] or len(all_event_interdurations) == 0:
970
+ inter_duration_mean = cfg.NA
971
+ inter_duration_stdev = cfg.NA
972
+ else:
973
+ inter_duration_mean = round(statistics.mean(all_event_interdurations), 3)
974
+ if len(all_event_interdurations) > 1:
975
+ inter_duration_stdev = round(statistics.stdev(all_event_interdurations), 3)
976
+ else:
977
+ inter_duration_stdev = cfg.NA
978
+
979
+ out_cat.append(
980
+ {
981
+ "subject": subject,
982
+ "behavior": behavior,
983
+ "modifiers": "",
984
+ "duration": cfg.NA,
985
+ "duration_mean": cfg.NA,
986
+ "duration_stdev": cfg.NA,
987
+ "number": len(rows),
988
+ "inter_duration_mean": inter_duration_mean,
989
+ "inter_duration_stdev": inter_duration_stdev,
990
+ }
991
+ )
992
+
993
+ if project_functions.event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES:
994
+ cursor.execute(
995
+ ("SELECT occurence, observation FROM events WHERE subject = ? AND code = ? ORDER BY observation, occurence"),
996
+ (subject, behavior),
997
+ )
998
+
999
+ rows = list(cursor.fetchall())
1000
+
1001
+ if len(rows) == 0:
1002
+ if not parameters[cfg.EXCLUDE_BEHAVIORS]: # include behaviors without events
1003
+ # check if observation from pictures
1004
+ if parameters["start time"] == dec("0.000") and parameters["end time"] == dec("0.000"):
1005
+ duration = cfg.NA
1006
+ else:
1007
+ duration = 0.000
1008
+ out_cat.append(
1009
+ {
1010
+ "subject": subject,
1011
+ "behavior": behavior,
1012
+ "modifiers": "",
1013
+ "duration": duration,
1014
+ "duration_mean": cfg.NA,
1015
+ "duration_stdev": cfg.NA,
1016
+ "number": 0,
1017
+ "inter_duration_mean": cfg.NA,
1018
+ "inter_duration_stdev": cfg.NA,
1019
+ }
1020
+ )
1021
+ continue
1022
+
1023
+ if len(rows) % 2: # unpaired events
1024
+ out_cat.append(
1025
+ {
1026
+ "subject": subject,
1027
+ "behavior": behavior,
1028
+ "modifiers": "",
1029
+ "duration": cfg.UNPAIRED,
1030
+ "duration_mean": cfg.UNPAIRED,
1031
+ "duration_stdev": cfg.UNPAIRED,
1032
+ "number": cfg.UNPAIRED,
1033
+ "inter_duration_mean": cfg.UNPAIRED,
1034
+ "inter_duration_stdev": cfg.UNPAIRED,
1035
+ }
1036
+ )
1037
+ else:
1038
+ all_event_durations: list = []
1039
+ all_event_interdurations: list = []
1040
+ for idx, row in enumerate(rows):
1041
+ # event
1042
+ if idx % 2 == 0:
1043
+ if row[0] is not None and rows[idx + 1][0] is not None:
1044
+ new_init, new_end = float(row[0]), float(rows[idx + 1][0])
1045
+ all_event_durations.append(new_end - new_init)
1046
+ else:
1047
+ all_event_durations.append(float("NaN"))
1048
+
1049
+ # inter event if same observation
1050
+ if idx % 2 and idx != len(rows) - 1 and row[1] == rows[idx + 1][1]:
1051
+ if row[0] is not None and rows[idx + 1][0] is not None:
1052
+ # and (
1053
+ # parameters["start time"] <= row[0] <= parameters["end time"]
1054
+ # and parameters["start time"] <= rows[idx + 1][0] <= parameters["end time"]
1055
+ # ):
1056
+ all_event_interdurations.append(float(rows[idx + 1][0]) - float(row[0]))
1057
+ else:
1058
+ all_event_interdurations.append(float("NaN"))
1059
+
1060
+ # events
1061
+ if [x for x in all_event_durations if math.isnan(x)]:
1062
+ total_duration = cfg.NA
1063
+ else:
1064
+ total_duration = round(sum(all_event_durations), 3)
1065
+ if [x for x in all_event_durations if math.isnan(x)] or len(all_event_durations) == 0:
1066
+ duration_mean = cfg.NA
1067
+ duration_stdev = cfg.NA
1068
+ else:
1069
+ duration_mean = round(statistics.mean(all_event_durations), 3)
1070
+ if len(all_event_durations) > 1:
1071
+ duration_stdev = round(statistics.stdev(all_event_durations), 3)
1072
+ else:
1073
+ duration_stdev = cfg.NA
1074
+ # interduration
1075
+ if [x for x in all_event_interdurations if math.isnan(x)] or len(all_event_interdurations) == 0:
1076
+ inter_duration_mean = cfg.NA
1077
+ inter_duration_stdev = cfg.NA
1078
+ else:
1079
+ inter_duration_mean = round(statistics.mean(all_event_interdurations), 3)
1080
+ if len(all_event_interdurations) > 1:
1081
+ inter_duration_stdev = round(statistics.stdev(all_event_interdurations), 3)
1082
+ else:
1083
+ inter_duration_stdev = cfg.NA
1084
+
1085
+ out_cat.append(
1086
+ {
1087
+ "subject": subject,
1088
+ "behavior": behavior,
1089
+ "modifiers": "",
1090
+ "duration": total_duration,
1091
+ "duration_mean": duration_mean,
1092
+ "duration_stdev": duration_stdev,
1093
+ "number": len(all_event_durations),
1094
+ "inter_duration_mean": inter_duration_mean,
1095
+ "inter_duration_stdev": inter_duration_stdev,
1096
+ }
1097
+ )
1098
+
1099
+ out += out_cat
1100
+
1101
+ if by_category: # and flagCategories:
1102
+ for behav in out_cat:
1103
+ try:
1104
+ category = [
1105
+ ethogram[x][cfg.BEHAVIOR_CATEGORY]
1106
+ for x in ethogram
1107
+ if cfg.BEHAVIOR_CATEGORY in ethogram[x] and ethogram[x][cfg.BEHAVIOR_CODE] == behav["behavior"]
1108
+ ][0]
1109
+ except Exception:
1110
+ category = ""
1111
+
1112
+ if category not in categories[subject]:
1113
+ categories[subject][category] = {"duration": 0, "number": 0}
1114
+
1115
+ if project_functions.event_type(behav["behavior"], ethogram) in cfg.STATE_EVENT_TYPES:
1116
+ if behav["duration"] not in ("-", cfg.NA) and categories[subject][category]["duration"] not in (
1117
+ "-",
1118
+ cfg.NA,
1119
+ ):
1120
+ # print(f"{categories[subject][category]["duration"]=}")
1121
+ # print(f"{behav["duration"]=}")
1122
+ categories[subject][category]["duration"] += behav["duration"]
1123
+ else:
1124
+ categories[subject][category]["duration"] = cfg.NA
1125
+
1126
+ categories[subject][category]["number"] += behav["number"]
1127
+
1128
+ out_sorted: list = []
1129
+ for subject in parameters[cfg.SELECTED_SUBJECTS]:
1130
+ for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
1131
+ for row in out:
1132
+ if row[cfg.SUBJECT] == subject and row["behavior"] == behavior:
1133
+ out_sorted.append(row)
1134
+
1135
+ # http://stackoverflow.com/questions/673867/python-arbitrary-order-by
1136
+ return (out_sorted, categories)