labda 0.0.2__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.
- CHANGELOG.md +16 -0
- labda/__init__.py +1 -0
- labda/assets/__init__.py +2 -0
- labda/assets/counts_cut_points.py +35 -0
- labda/assets/transportation_cut_points.py +16 -0
- labda/expanders/__init__.py +8 -0
- labda/expanders/accelerometer.py +29 -0
- labda/expanders/extras.py +33 -0
- labda/expanders/spatial.py +242 -0
- labda/parsers/__init__.py +4 -0
- labda/parsers/actigraph.py +232 -0
- labda/parsers/bulk.py +31 -0
- labda/parsers/gadgetbridge.py +232 -0
- labda/parsers/garmin.py +89 -0
- labda/parsers/qstarz.py +366 -0
- labda/processing/__init__.py +1 -0
- labda/processing/accelerometer/__init__.py +2 -0
- labda/processing/accelerometer/activity_intensity.py +57 -0
- labda/processing/accelerometer/steps.py +24 -0
- labda/processing/accelerometer/wear.py +38 -0
- labda/processing/bouts.py +85 -0
- labda/processing/domains.py +32 -0
- labda/processing/manipulations.py +22 -0
- labda/processing/spatial/__init__.py +4 -0
- labda/processing/spatial/environment.py +50 -0
- labda/processing/spatial/manipulations.py +186 -0
- labda/processing/spatial/timeline.py +99 -0
- labda/processing/spatial/transportation.py +80 -0
- labda/processing/spatial/trips.py +551 -0
- labda/processing/wear_validity.py +69 -0
- labda/structure/__init__.py +6 -0
- labda/structure/collection.py +103 -0
- labda/structure/domains.py +29 -0
- labda/structure/linkage.py +28 -0
- labda/structure/merging.py +133 -0
- labda/structure/resampling.py +82 -0
- labda/structure/subject.py +294 -0
- labda/structure/validation/__init__.py +0 -0
- labda/structure/validation/domains.py +37 -0
- labda/structure/validation/linkage.py +28 -0
- labda/structure/validation/subject.py +323 -0
- labda/utils.py +240 -0
- labda/visualisation/__init__.py +0 -0
- labda-0.0.2.dist-info/METADATA +59 -0
- labda-0.0.2.dist-info/RECORD +46 -0
- labda-0.0.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pandera import Column, DataFrameSchema
|
|
2
|
+
|
|
3
|
+
# TODO: Refactor...
|
|
4
|
+
|
|
5
|
+
SCHEMA = DataFrameSchema(
|
|
6
|
+
columns={
|
|
7
|
+
"subject_id": Column(
|
|
8
|
+
dtype="string",
|
|
9
|
+
description="Unique ID of the dataframe (participant)",
|
|
10
|
+
required=False,
|
|
11
|
+
nullable=True,
|
|
12
|
+
),
|
|
13
|
+
"domain": Column(
|
|
14
|
+
dtype="string",
|
|
15
|
+
description="Name of the domain",
|
|
16
|
+
),
|
|
17
|
+
"start": Column(
|
|
18
|
+
dtype="datetime64[ns]",
|
|
19
|
+
description="Start time of the domain",
|
|
20
|
+
required=False,
|
|
21
|
+
nullable=True,
|
|
22
|
+
),
|
|
23
|
+
"end": Column(
|
|
24
|
+
dtype="datetime64[ns]",
|
|
25
|
+
description="End time of the domain",
|
|
26
|
+
required=False,
|
|
27
|
+
nullable=True,
|
|
28
|
+
),
|
|
29
|
+
"geometry": Column(
|
|
30
|
+
dtype="geometry",
|
|
31
|
+
description="Geometry of the domain",
|
|
32
|
+
required=False,
|
|
33
|
+
nullable=True,
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
coerce=True,
|
|
37
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from pandera import Check, Column, DataFrameSchema
|
|
2
|
+
|
|
3
|
+
# TODO: Maybe just one linkage also for domains (timetable, geometries), information about the subjects etc.
|
|
4
|
+
# TODO: Refactor...
|
|
5
|
+
|
|
6
|
+
SCHEMA = DataFrameSchema(
|
|
7
|
+
columns={
|
|
8
|
+
"subject_id": Column(
|
|
9
|
+
dtype="string",
|
|
10
|
+
description="Unique ID of the dataframe (subject)",
|
|
11
|
+
),
|
|
12
|
+
"sensor_id": Column(
|
|
13
|
+
dtype="string",
|
|
14
|
+
description="ID of the device (e.g. MAC address, serial number)",
|
|
15
|
+
),
|
|
16
|
+
"start": Column(
|
|
17
|
+
dtype="datetime64[ns]",
|
|
18
|
+
description="Start time of the recording",
|
|
19
|
+
required=False,
|
|
20
|
+
),
|
|
21
|
+
"end": Column(
|
|
22
|
+
dtype="datetime64[ns]",
|
|
23
|
+
description="End time of the recording",
|
|
24
|
+
required=False,
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
coerce=True,
|
|
28
|
+
)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
import pandera as pr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Column(StrEnum):
|
|
7
|
+
SUBJECT_ID = "subject_id"
|
|
8
|
+
DATETIME = "datetime"
|
|
9
|
+
WEAR = "wear"
|
|
10
|
+
TIMEDELTA = "timedelta"
|
|
11
|
+
# ---
|
|
12
|
+
VERTICAL_COUNTS = "vertical_counts"
|
|
13
|
+
HORIZONTAL_COUNTS = "horizontal_counts"
|
|
14
|
+
PERPENDICULAR_COUNTS = "perpendicular_counts"
|
|
15
|
+
VM_COUNTS = "vm_counts"
|
|
16
|
+
POSITION = "position"
|
|
17
|
+
STEPS = "steps"
|
|
18
|
+
# ---
|
|
19
|
+
LATITUDE = "latitude"
|
|
20
|
+
LONGITUDE = "longitude"
|
|
21
|
+
NSAT_VIEWED = "nsat_viewed"
|
|
22
|
+
NSAT_USED = "nsat_used"
|
|
23
|
+
NSAT_RATIO = "nsat_ratio"
|
|
24
|
+
SNR_VIEWED = "snr_viewed"
|
|
25
|
+
SNR_USED = "snr_used"
|
|
26
|
+
PDOP = "pdop"
|
|
27
|
+
HDOP = "hdop"
|
|
28
|
+
VDOP = "vdop"
|
|
29
|
+
# ---
|
|
30
|
+
DISTANCE = "distance"
|
|
31
|
+
ELEVATION = "elevation"
|
|
32
|
+
SPEED = "speed"
|
|
33
|
+
ACCELERATION = "acceleration"
|
|
34
|
+
DIRECTION = "direction"
|
|
35
|
+
ENVIRONMENT = "environment"
|
|
36
|
+
# ---
|
|
37
|
+
SEGMENT_ID = "segment_id"
|
|
38
|
+
TRIP_ID = "trip_id"
|
|
39
|
+
TRIP_STATUS = "trip_status"
|
|
40
|
+
TRIP_MODE = "trip_mode"
|
|
41
|
+
STATIONARY_ID = "stationary_id"
|
|
42
|
+
# ---
|
|
43
|
+
HEART_RATE = "heart_rate"
|
|
44
|
+
LUX = "lux"
|
|
45
|
+
# ---
|
|
46
|
+
ACTIVITY_INTENSITY = "activity_intensity"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
SCHEMA = pr.DataFrameSchema(
|
|
50
|
+
index=pr.Index(
|
|
51
|
+
name=Column.DATETIME,
|
|
52
|
+
dtype="datetime64[ns]",
|
|
53
|
+
description="Datetime in timezone where data was collected.",
|
|
54
|
+
),
|
|
55
|
+
columns={
|
|
56
|
+
Column.SUBJECT_ID: pr.Column(
|
|
57
|
+
dtype="string",
|
|
58
|
+
description="Unique ID of the subject.",
|
|
59
|
+
required=False,
|
|
60
|
+
nullable=False,
|
|
61
|
+
),
|
|
62
|
+
Column.WEAR: pr.Column(
|
|
63
|
+
dtype="boolean",
|
|
64
|
+
description="Devices worn or not.",
|
|
65
|
+
required=False,
|
|
66
|
+
nullable=True,
|
|
67
|
+
),
|
|
68
|
+
Column.TIMEDELTA: pr.Column(
|
|
69
|
+
dtype="timedelta",
|
|
70
|
+
description="Time since last fix.",
|
|
71
|
+
required=False,
|
|
72
|
+
nullable=True,
|
|
73
|
+
),
|
|
74
|
+
Column.VERTICAL_COUNTS: pr.Column(
|
|
75
|
+
dtype="UInt16",
|
|
76
|
+
description="Vertical counts.",
|
|
77
|
+
required=False,
|
|
78
|
+
nullable=True,
|
|
79
|
+
),
|
|
80
|
+
Column.HORIZONTAL_COUNTS: pr.Column(
|
|
81
|
+
dtype="UInt16",
|
|
82
|
+
description="Horizontal counts.",
|
|
83
|
+
required=False,
|
|
84
|
+
nullable=True,
|
|
85
|
+
),
|
|
86
|
+
Column.PERPENDICULAR_COUNTS: pr.Column(
|
|
87
|
+
dtype="UInt16",
|
|
88
|
+
description="Perpendicular counts.",
|
|
89
|
+
required=False,
|
|
90
|
+
nullable=True,
|
|
91
|
+
),
|
|
92
|
+
Column.VM_COUNTS: pr.Column(
|
|
93
|
+
dtype="Float32",
|
|
94
|
+
description="Vector magnitude based on axes counts.",
|
|
95
|
+
required=False,
|
|
96
|
+
nullable=True,
|
|
97
|
+
),
|
|
98
|
+
Column.POSITION: pr.Column(
|
|
99
|
+
dtype="category",
|
|
100
|
+
description="Position of subject.",
|
|
101
|
+
required=False,
|
|
102
|
+
nullable=True,
|
|
103
|
+
checks=[
|
|
104
|
+
pr.Check(
|
|
105
|
+
lambda s: s[s.notna()].isin(
|
|
106
|
+
[
|
|
107
|
+
"standing",
|
|
108
|
+
"sitting",
|
|
109
|
+
"lying",
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
Column.STEPS: pr.Column(
|
|
116
|
+
dtype="UInt16",
|
|
117
|
+
description="Steps since last fix.",
|
|
118
|
+
required=False,
|
|
119
|
+
nullable=True,
|
|
120
|
+
),
|
|
121
|
+
Column.LATITUDE: pr.Column(
|
|
122
|
+
dtype="Float64",
|
|
123
|
+
description="Latitude in appropriate coordinate system where data was collected.",
|
|
124
|
+
required=False,
|
|
125
|
+
nullable=True,
|
|
126
|
+
# checks=[Check.ge(-90), Check.le(90)],
|
|
127
|
+
),
|
|
128
|
+
Column.LONGITUDE: pr.Column(
|
|
129
|
+
dtype="Float64",
|
|
130
|
+
description="Longitude in appropriate coordinate system where data was collected.",
|
|
131
|
+
required=False,
|
|
132
|
+
nullable=True,
|
|
133
|
+
# checks=[Check.ge(-180), Check.le(180)],
|
|
134
|
+
),
|
|
135
|
+
Column.NSAT_VIEWED: pr.Column(
|
|
136
|
+
dtype="UInt8",
|
|
137
|
+
description="Number of satellites viewed by device.",
|
|
138
|
+
required=False,
|
|
139
|
+
nullable=True,
|
|
140
|
+
),
|
|
141
|
+
Column.NSAT_USED: pr.Column(
|
|
142
|
+
dtype="UInt8",
|
|
143
|
+
description="Number of satellites used by device.",
|
|
144
|
+
required=False,
|
|
145
|
+
nullable=True,
|
|
146
|
+
),
|
|
147
|
+
Column.NSAT_RATIO: pr.Column(
|
|
148
|
+
dtype="Float32",
|
|
149
|
+
description="Satellites ratio (used/viewed).",
|
|
150
|
+
required=False,
|
|
151
|
+
nullable=True,
|
|
152
|
+
),
|
|
153
|
+
Column.SNR_VIEWED: pr.Column(
|
|
154
|
+
dtype="UInt16",
|
|
155
|
+
description="Sum of satellites SNR.",
|
|
156
|
+
required=False,
|
|
157
|
+
nullable=True,
|
|
158
|
+
),
|
|
159
|
+
Column.SNR_USED: pr.Column(
|
|
160
|
+
dtype="UInt16",
|
|
161
|
+
description="Sum of used satellites SNR.",
|
|
162
|
+
required=False,
|
|
163
|
+
nullable=True,
|
|
164
|
+
),
|
|
165
|
+
Column.PDOP: pr.Column(
|
|
166
|
+
dtype="Float32",
|
|
167
|
+
description="GNSS PDOP (Position of Dilution of Precision).",
|
|
168
|
+
required=False,
|
|
169
|
+
nullable=True,
|
|
170
|
+
),
|
|
171
|
+
Column.HDOP: pr.Column(
|
|
172
|
+
dtype="Float32",
|
|
173
|
+
description="GNSS HDOP (Horizontal Dilution of Precision).",
|
|
174
|
+
required=False,
|
|
175
|
+
nullable=True,
|
|
176
|
+
),
|
|
177
|
+
Column.VDOP: pr.Column(
|
|
178
|
+
dtype="Float32",
|
|
179
|
+
description="GNSS VDOP (Vertical Dilution of Precision).",
|
|
180
|
+
required=False,
|
|
181
|
+
nullable=True,
|
|
182
|
+
),
|
|
183
|
+
Column.ACCELERATION: pr.Column(
|
|
184
|
+
dtype="Float32",
|
|
185
|
+
description="Acceleration in m/s².",
|
|
186
|
+
nullable=True,
|
|
187
|
+
required=False,
|
|
188
|
+
),
|
|
189
|
+
Column.DIRECTION: pr.Column(
|
|
190
|
+
dtype="Float32",
|
|
191
|
+
description="Direction (bearing) in degrees",
|
|
192
|
+
required=False,
|
|
193
|
+
nullable=True,
|
|
194
|
+
),
|
|
195
|
+
Column.DISTANCE: pr.Column(
|
|
196
|
+
dtype="Float32",
|
|
197
|
+
description="Distance taken since last fix in metres.",
|
|
198
|
+
required=False,
|
|
199
|
+
nullable=True,
|
|
200
|
+
),
|
|
201
|
+
Column.ELEVATION: pr.Column(
|
|
202
|
+
dtype="Float32",
|
|
203
|
+
description="Elevation in metres",
|
|
204
|
+
required=False,
|
|
205
|
+
nullable=True,
|
|
206
|
+
),
|
|
207
|
+
Column.ENVIRONMENT: pr.Column(
|
|
208
|
+
dtype="category",
|
|
209
|
+
description="Type of environment",
|
|
210
|
+
required=False,
|
|
211
|
+
nullable=True,
|
|
212
|
+
checks=[
|
|
213
|
+
pr.Check(
|
|
214
|
+
lambda s: s[s.notna()].isin(
|
|
215
|
+
[
|
|
216
|
+
"indoor",
|
|
217
|
+
"outdoor",
|
|
218
|
+
"vehicle",
|
|
219
|
+
"mixed",
|
|
220
|
+
]
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
],
|
|
224
|
+
),
|
|
225
|
+
Column.SPEED: pr.Column(
|
|
226
|
+
dtype="Float32",
|
|
227
|
+
description="Speed in km/h.",
|
|
228
|
+
required=False,
|
|
229
|
+
nullable=True,
|
|
230
|
+
),
|
|
231
|
+
Column.SEGMENT_ID: pr.Column(
|
|
232
|
+
dtype="UInt32",
|
|
233
|
+
description="ID of segment based on missing data or other criteria.",
|
|
234
|
+
required=False,
|
|
235
|
+
nullable=False,
|
|
236
|
+
),
|
|
237
|
+
Column.TRIP_ID: pr.Column(
|
|
238
|
+
dtype="UInt16",
|
|
239
|
+
description="ID of trip.",
|
|
240
|
+
required=False,
|
|
241
|
+
nullable=True,
|
|
242
|
+
),
|
|
243
|
+
Column.TRIP_MODE: pr.Column(
|
|
244
|
+
dtype="category",
|
|
245
|
+
description="Type of transportation mode.",
|
|
246
|
+
required=False,
|
|
247
|
+
nullable=True,
|
|
248
|
+
checks=[
|
|
249
|
+
pr.Check(
|
|
250
|
+
lambda s: s[s.notna()].isin(
|
|
251
|
+
[
|
|
252
|
+
"walk",
|
|
253
|
+
"run",
|
|
254
|
+
"bicycle",
|
|
255
|
+
"scooter",
|
|
256
|
+
"vehicle",
|
|
257
|
+
"train",
|
|
258
|
+
]
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
],
|
|
262
|
+
),
|
|
263
|
+
Column.TRIP_STATUS: pr.Column(
|
|
264
|
+
dtype="category",
|
|
265
|
+
description="Status of trip",
|
|
266
|
+
required=False,
|
|
267
|
+
nullable=True,
|
|
268
|
+
checks=[
|
|
269
|
+
pr.Check(
|
|
270
|
+
lambda s: s[s.notna()].isin(
|
|
271
|
+
[
|
|
272
|
+
"start",
|
|
273
|
+
"end",
|
|
274
|
+
"start-end",
|
|
275
|
+
"transport",
|
|
276
|
+
"pause",
|
|
277
|
+
"stationary",
|
|
278
|
+
]
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
],
|
|
282
|
+
),
|
|
283
|
+
Column.STATIONARY_ID: pr.Column(
|
|
284
|
+
dtype="UInt16",
|
|
285
|
+
description="ID of stationary.",
|
|
286
|
+
required=False,
|
|
287
|
+
nullable=True,
|
|
288
|
+
),
|
|
289
|
+
Column.HEART_RATE: pr.Column(
|
|
290
|
+
dtype="UInt8",
|
|
291
|
+
description="Heart rate in beats per minute.",
|
|
292
|
+
required=False,
|
|
293
|
+
nullable=True,
|
|
294
|
+
),
|
|
295
|
+
Column.LUX: pr.Column(
|
|
296
|
+
dtype="UInt16",
|
|
297
|
+
description="Ambient light in lux.",
|
|
298
|
+
required=False,
|
|
299
|
+
nullable=True,
|
|
300
|
+
),
|
|
301
|
+
Column.ACTIVITY_INTENSITY: pr.Column(
|
|
302
|
+
dtype="category",
|
|
303
|
+
description="Intensity of activity.",
|
|
304
|
+
required=False,
|
|
305
|
+
nullable=True,
|
|
306
|
+
checks=[
|
|
307
|
+
pr.Check(
|
|
308
|
+
lambda s: s[s.notna()].isin(
|
|
309
|
+
[
|
|
310
|
+
"sedentary",
|
|
311
|
+
"light",
|
|
312
|
+
"moderate",
|
|
313
|
+
"moderate-vigorous",
|
|
314
|
+
"vigorous",
|
|
315
|
+
]
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
],
|
|
319
|
+
),
|
|
320
|
+
},
|
|
321
|
+
coerce=True,
|
|
322
|
+
strict=False,
|
|
323
|
+
)
|
labda/utils.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pyproj
|
|
6
|
+
from timezonefinder import TimezoneFinder
|
|
7
|
+
|
|
8
|
+
from .structure.validation.subject import Column
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def columns_exists(
|
|
12
|
+
df: pd.DataFrame,
|
|
13
|
+
columns: list[Column | str],
|
|
14
|
+
) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Checks if the given columns exist in the DataFrame.
|
|
17
|
+
|
|
18
|
+
This function iterates over the list of columns and checks if each column exists in the DataFrame.
|
|
19
|
+
If a column does not exist, it raises a ValueError.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
df (pd.DataFrame): The DataFrame to check.
|
|
23
|
+
columns (list[str]): A list of column names to check for existence in the DataFrame.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If a column does not exist in df.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
missing_columns = [col for col in columns if col not in df.columns]
|
|
30
|
+
|
|
31
|
+
if missing_columns:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"The following columns do not exist in the DataFrame: {', '.join(missing_columns)}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def columns_not_exists(
|
|
38
|
+
df: pd.DataFrame,
|
|
39
|
+
columns: list[Column | str],
|
|
40
|
+
*,
|
|
41
|
+
overwrite: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Checks if the specified columns do not exist in the DataFrame. If 'overwrite' is True,
|
|
45
|
+
existing columns will be dropped. If 'overwrite' is False, an error will be raised for existing columns.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
df (pd.DataFrame): The DataFrame to check.
|
|
49
|
+
columns (List[str]): A list of column names to check.
|
|
50
|
+
overwrite (bool, optional): Whether to drop existing columns. Defaults to False.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If 'overwrite' is False and any of the specified columns exist in the DataFrame.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
"""
|
|
58
|
+
existing_columns = [col for col in columns if col in df.columns]
|
|
59
|
+
|
|
60
|
+
if existing_columns:
|
|
61
|
+
if overwrite:
|
|
62
|
+
df.drop(columns=existing_columns, inplace=True)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"The following columns already exist in the DataFrame: {', '.join(existing_columns)}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_unique_column_name(df: pd.DataFrame) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Generates a unique column name that is not present in the given list of columns.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
df (pd.DataFrame): The DataFrame to check.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: A unique column name.
|
|
78
|
+
"""
|
|
79
|
+
hex = 4
|
|
80
|
+
colname = secrets.token_hex(hex)
|
|
81
|
+
while colname in df.columns.tolist():
|
|
82
|
+
colname = secrets.token_hex(hex)
|
|
83
|
+
|
|
84
|
+
return colname
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_sampling_frequency(df: pd.DataFrame) -> float:
|
|
88
|
+
# TODO: Check that index is a datetime index.
|
|
89
|
+
|
|
90
|
+
frequencies = df.index.to_series().diff().value_counts()
|
|
91
|
+
dominant_frequency = frequencies.idxmax()
|
|
92
|
+
|
|
93
|
+
if len(frequencies) > 1 and frequencies[dominant_frequency] == frequencies.iloc[1]:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"We are uncertain about the frequency sampling. Please verify the data manually."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
seconds = dominant_frequency.total_seconds() # type: ignore
|
|
99
|
+
if seconds < 1:
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
"Sub-second sampling frequencies are not supported yet."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return seconds
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_to_geodataframe(
|
|
108
|
+
df: pd.DataFrame,
|
|
109
|
+
*,
|
|
110
|
+
crs: str | None = None,
|
|
111
|
+
) -> gpd.GeoDataFrame:
|
|
112
|
+
"""
|
|
113
|
+
Converts a DataFrame to a GeoDataFrame by creating a geometry column from latitude and longitude columns.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
df (pd.DataFrame): The DataFrame to be converted.
|
|
117
|
+
crs (str | None, optional): The coordinate reference system (CRS) of the GeoDataFrame. Defaults to None.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
gpd.GeoDataFrame: The converted GeoDataFrame.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
columns_exists(df, [Column.LATITUDE, Column.LONGITUDE])
|
|
124
|
+
|
|
125
|
+
return gpd.GeoDataFrame(
|
|
126
|
+
df, geometry=gpd.points_from_xy(df.longitude, df.latitude), crs=crs
|
|
127
|
+
) # type: ignore
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def convert_to_dataframe(gdf: gpd.GeoDataFrame) -> pd.DataFrame:
|
|
131
|
+
"""
|
|
132
|
+
Converts a GeoDataFrame to a DataFrame by extracting the latitude and longitude
|
|
133
|
+
coordinates from the geometry column. The geometry column is dropped in the process.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
gdf (gpd.GeoDataFrame): The input GeoDataFrame.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
pd.DataFrame: The converted DataFrame with latitude and longitude columns.
|
|
140
|
+
"""
|
|
141
|
+
gdf[Column.LATITUDE] = gdf.geometry.y
|
|
142
|
+
gdf[Column.LONGITUDE] = gdf.geometry.x
|
|
143
|
+
gdf.drop(columns=["geometry"], inplace=True)
|
|
144
|
+
|
|
145
|
+
return pd.DataFrame(gdf)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_crs(df: pd.DataFrame) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Guesses the coordinate reference system (CRS) of a DataFrame containing spatial data. The CRS is estimated using the WGS84 ellipsoid.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
df (pd.DataFrame): The DataFrame containing spatial data.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
str: The estimated CRS of the DataFrame.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
gdf = convert_to_geodataframe(df, crs="WGS84") # type: ignore
|
|
160
|
+
estimated_crs = gdf.geometry.estimate_utm_crs()
|
|
161
|
+
|
|
162
|
+
return str(estimated_crs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_timezone(df: pd.DataFrame, sample_size: int = 5) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Guesses the dominant timezone based on latitude and longitude coordinates in a DataFrame. The timezone is estimated using the TimezoneFinder package.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
df (pd.DataFrame): The DataFrame containing latitude and longitude columns.
|
|
171
|
+
sample_size (int, optional): The number of coordinates to sample for timezone estimation. Defaults to 5.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If there are multiple timezones with the same highest count.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
str: The dominant timezone as a string.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
columns_exists(df, [Column.LATITUDE, Column.LONGITUDE])
|
|
181
|
+
|
|
182
|
+
coords = df[df[Column.LATITUDE].notna() & df[Column.LONGITUDE].notna()]
|
|
183
|
+
sample = coords.sample(sample_size)
|
|
184
|
+
|
|
185
|
+
tz_finder = TimezoneFinder()
|
|
186
|
+
sample["timezone"] = sample.apply(
|
|
187
|
+
lambda row: tz_finder.timezone_at(lat=row.latitude, lng=row.longitude), # type: ignore
|
|
188
|
+
axis=1,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
timezones = sample["timezone"].value_counts()
|
|
192
|
+
dominant_timezone = timezones.idxmax()
|
|
193
|
+
if len(timezones) >= 2 and timezones[dominant_timezone] == timezones.iloc[1]:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
"We are uncertain about the timezone. Please verify the data manually."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return str(dominant_timezone)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def convert_distance_parameter(param: int | float, sampling_frequency: float) -> float:
|
|
202
|
+
"""
|
|
203
|
+
Converts a distance parameter to meters.
|
|
204
|
+
|
|
205
|
+
This function accepts a distance parameter as an integer or float in meters per minute.
|
|
206
|
+
The function then converts the distance to proper value based on sampling frequency.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
distance (int | float): The distance in meters per minute to convert.
|
|
210
|
+
sampling_frequency (float): The sampling frequency in seconds.
|
|
211
|
+
Returns:
|
|
212
|
+
float: The converted distance in meters per minute based on sampling frequency.
|
|
213
|
+
"""
|
|
214
|
+
meters = param / (60 / sampling_frequency)
|
|
215
|
+
return meters
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def align_datetimes(df: pd.DataFrame, sampling_frequency: float) -> pd.DataFrame:
|
|
219
|
+
df = df.copy()
|
|
220
|
+
# TODO: Add docstring
|
|
221
|
+
diff = get_unique_column_name(df)
|
|
222
|
+
rounded = get_unique_column_name(df)
|
|
223
|
+
|
|
224
|
+
df[rounded] = df.index.round(f"{sampling_frequency}s") # type: ignore
|
|
225
|
+
df[diff] = (df.index - df[rounded]).dt.total_seconds().abs() # type: ignore
|
|
226
|
+
df = df.sort_values([rounded, diff])
|
|
227
|
+
df.drop(columns=[diff], inplace=True)
|
|
228
|
+
|
|
229
|
+
df = df.drop_duplicates(subset=rounded, keep="first")
|
|
230
|
+
df.set_index(rounded, inplace=True, drop=True)
|
|
231
|
+
df.index.name = Column.DATETIME
|
|
232
|
+
|
|
233
|
+
return df
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_crs_units(crs: str) -> str:
|
|
237
|
+
crs = pyproj.CRS(crs) # type: ignore
|
|
238
|
+
units = crs.axis_info[0].unit_name # type: ignore
|
|
239
|
+
|
|
240
|
+
return units
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: labda
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Library for Advanced Behavioural Data Analysis
|
|
5
|
+
Home-page: https://labda.josefheidler.cz
|
|
6
|
+
License: CC-BY-SA-4.0
|
|
7
|
+
Keywords: analysis,health,behaviour,data,spatial,temporal
|
|
8
|
+
Author: Josef Heidler
|
|
9
|
+
Author-email: jheidler@health.sdu.dk
|
|
10
|
+
Maintainer: Josef Heidler
|
|
11
|
+
Maintainer-email: jheidler@health.sdu.dk
|
|
12
|
+
Requires-Python: >=3.11,<4.0
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: License :: Other/Proprietary License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Requires-Dist: geopandas (>=0.14.2,<0.15.0)
|
|
23
|
+
Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
|
|
24
|
+
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
25
|
+
Requires-Dist: pandera (>=0.18.0,<0.19.0)
|
|
26
|
+
Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
|
|
27
|
+
Requires-Dist: pydeck (>=0.8.0,<0.9.0)
|
|
28
|
+
Requires-Dist: sqlalchemy (>=2.0.25,<3.0.0)
|
|
29
|
+
Requires-Dist: timezonefinder (>=6.2.0,<7.0.0)
|
|
30
|
+
Project-URL: Documentation, https://labda.josefheidler.cz
|
|
31
|
+
Project-URL: Repository, https://github.com/josefheidler/labda
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# LABDA: Library for Advanced Behavioural Data Analysis
|
|
35
|
+
|
|
36
|
+
**LABDA is currently under development and may undergo significant API changes that could result in breaking changes.**
|
|
37
|
+
|
|
38
|
+
## What is it?
|
|
39
|
+
|
|
40
|
+
TO-DO...
|
|
41
|
+
|
|
42
|
+
## Main Features
|
|
43
|
+
|
|
44
|
+
TO-DO...
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://labda.josefheidler.cz/">Library for Advanced Behavioural Data Analysis</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://josefheidler.cz/">Josef Heidler</a> is licensed under <a href="http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"></a></p>
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
TO-DO...
|
|
52
|
+
|
|
53
|
+
## Getting Help, Discussion and Development
|
|
54
|
+
|
|
55
|
+
TO-DO...
|
|
56
|
+
|
|
57
|
+
## Contributing to labda
|
|
58
|
+
|
|
59
|
+
TO-DO..
|