labda 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labda-0.0.2 → labda-0.0.3}/PKG-INFO +4 -2
- labda-0.0.3/README.md +27 -0
- {labda-0.0.2 → labda-0.0.3}/labda/__init__.py +3 -0
- labda-0.0.3/labda/assets/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/assets/__pycache__/counts_cut_points.cpython-311.pyc +0 -0
- labda-0.0.3/labda/assets/__pycache__/transportation_cut_points.cpython-311.pyc +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/assets/transportation_cut_points.py +1 -1
- labda-0.0.3/labda/expanders/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/expanders/__pycache__/accelerometer.cpython-311.pyc +0 -0
- labda-0.0.3/labda/expanders/__pycache__/extras.cpython-311.pyc +0 -0
- labda-0.0.3/labda/expanders/__pycache__/spatial.cpython-311.pyc +0 -0
- labda-0.0.3/labda/logging.py +54 -0
- labda-0.0.3/labda/parallel.py +41 -0
- labda-0.0.3/labda/parsers/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/actigraph.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/bulk.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/gadgetbridge.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/qstarz.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/utils.cpython-311.pyc +0 -0
- labda-0.0.3/labda/parsers/__pycache__/vendors.cpython-311.pyc +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/parsers/actigraph.py +23 -29
- labda-0.0.3/labda/parsers/bulk.py +39 -0
- {labda-0.0.2 → labda-0.0.3}/labda/parsers/qstarz.py +59 -48
- labda-0.0.3/labda/parsers/utils.py +43 -0
- labda-0.0.3/labda/processing/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/__pycache__/bouts.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/__pycache__/domains.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/__pycache__/manipulations.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/accelerometer/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/accelerometer/__pycache__/activity_intensity.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/accelerometer/__pycache__/cut_points.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/accelerometer/__pycache__/steps.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/accelerometer/__pycache__/wear.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/spatial/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/spatial/__pycache__/manipulations.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/spatial/__pycache__/timeline.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/spatial/__pycache__/transportation.cpython-311.pyc +0 -0
- labda-0.0.3/labda/processing/spatial/__pycache__/trips.cpython-311.pyc +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/manipulations.py +1 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/timeline.py +12 -2
- {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/trips.py +72 -22
- labda-0.0.3/labda/structure/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/collection.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/domains.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/linkage.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/merging.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/resampling.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/__pycache__/subject.cpython-311.pyc +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/collection.py +65 -22
- {labda-0.0.2 → labda-0.0.3}/labda/structure/merging.py +21 -5
- {labda-0.0.2 → labda-0.0.3}/labda/structure/subject.py +29 -13
- labda-0.0.3/labda/structure/validation/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/validation/__pycache__/domains.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/validation/__pycache__/linkage.cpython-311.pyc +0 -0
- labda-0.0.3/labda/structure/validation/__pycache__/subject.cpython-311.pyc +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/subject.py +2 -1
- {labda-0.0.2 → labda-0.0.3}/labda/utils.py +34 -0
- labda-0.0.3/labda/visualisation/__pycache__/__init__.cpython-311.pyc +0 -0
- labda-0.0.3/labda/visualisation/__pycache__/spatial.cpython-311.pyc +0 -0
- labda-0.0.3/labda/visualisation/spatial.py +93 -0
- {labda-0.0.2 → labda-0.0.3}/pyproject.toml +7 -17
- labda-0.0.2/README.md +0 -26
- labda-0.0.2/labda/parsers/bulk.py +0 -31
- labda-0.0.2/labda/processing/spatial/environment.py +0 -50
- {labda-0.0.2 → labda-0.0.3}/CHANGELOG.md +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/assets/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/assets/counts_cut_points.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/expanders/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/expanders/accelerometer.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/expanders/extras.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/expanders/spatial.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/parsers/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/parsers/gadgetbridge.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/parsers/garmin.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/activity_intensity.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/steps.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/wear.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/bouts.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/domains.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/manipulations.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/transportation.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/processing/wear_validity.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/domains.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/linkage.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/resampling.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/__init__.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/domains.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/linkage.py +0 -0
- {labda-0.0.2 → labda-0.0.3}/labda/visualisation/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: labda
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Library for Advanced Behavioural Data Analysis
|
|
5
5
|
Home-page: https://labda.josefheidler.cz
|
|
6
6
|
License: CC-BY-SA-4.0
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering
|
|
22
22
|
Requires-Dist: geopandas (>=0.14.2,<0.15.0)
|
|
23
|
+
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
23
24
|
Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
|
|
24
25
|
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
25
26
|
Requires-Dist: pandera (>=0.18.0,<0.19.0)
|
|
@@ -44,7 +45,8 @@ TO-DO...
|
|
|
44
45
|
TO-DO...
|
|
45
46
|
|
|
46
47
|
## License
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
[Library for Advanced Behavioural Data Analysis](https://labda.josefheidler.cz/) by [Josef Heidler](https://labda.josefheidler.cz/) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
|
48
50
|
|
|
49
51
|
## Documentation
|
|
50
52
|
|
labda-0.0.3/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# LABDA: Library for Advanced Behavioural Data Analysis
|
|
2
|
+
|
|
3
|
+
**LABDA is currently under development and may undergo significant API changes that could result in breaking changes.**
|
|
4
|
+
|
|
5
|
+
## What is it?
|
|
6
|
+
|
|
7
|
+
TO-DO...
|
|
8
|
+
|
|
9
|
+
## Main Features
|
|
10
|
+
|
|
11
|
+
TO-DO...
|
|
12
|
+
|
|
13
|
+
## License
|
|
14
|
+
|
|
15
|
+
[Library for Advanced Behavioural Data Analysis](https://labda.josefheidler.cz/) by [Josef Heidler](https://labda.josefheidler.cz/) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
|
16
|
+
|
|
17
|
+
## Documentation
|
|
18
|
+
|
|
19
|
+
TO-DO...
|
|
20
|
+
|
|
21
|
+
## Getting Help, Discussion and Development
|
|
22
|
+
|
|
23
|
+
TO-DO...
|
|
24
|
+
|
|
25
|
+
## Contributing to labda
|
|
26
|
+
|
|
27
|
+
TO-DO..
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from .structure.subject import Subject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def color_level(record):
|
|
10
|
+
match record["level"].name:
|
|
11
|
+
case "ERROR":
|
|
12
|
+
color = "red"
|
|
13
|
+
case "SUCCESS":
|
|
14
|
+
color = "green"
|
|
15
|
+
case _:
|
|
16
|
+
color = "white"
|
|
17
|
+
|
|
18
|
+
return color
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def formatter(record):
|
|
22
|
+
color = color_level(record)
|
|
23
|
+
return (
|
|
24
|
+
f"<{color}>"
|
|
25
|
+
+ "{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}\n"
|
|
26
|
+
+ f"</{color}>"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def configure_logger(level: str):
|
|
31
|
+
logger.remove()
|
|
32
|
+
logger.add(
|
|
33
|
+
sys.stderr,
|
|
34
|
+
format=formatter,
|
|
35
|
+
colorize=True,
|
|
36
|
+
level=level,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def success_parsing_log(
|
|
41
|
+
subject: Subject,
|
|
42
|
+
path: Path,
|
|
43
|
+
func: str,
|
|
44
|
+
):
|
|
45
|
+
meta = subject.metadata
|
|
46
|
+
sampling_frequency = (
|
|
47
|
+
f"SF: {meta.sampling_frequency}s" if meta.sampling_frequency else ""
|
|
48
|
+
)
|
|
49
|
+
timezone = f", TZ: {meta.timezone}" if meta.timezone else ""
|
|
50
|
+
crs = f", CRS: {meta.crs}" if meta.crs else ""
|
|
51
|
+
|
|
52
|
+
logger.success(
|
|
53
|
+
f"{func} | {path} | Parsed {len(subject.df)} records ({sampling_frequency}{timezone}{crs})"
|
|
54
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from .logging import logger
|
|
6
|
+
from .structure.subject import Subject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def process_obj(obj: Any, func: Callable, **kwargs) -> Any | None:
|
|
10
|
+
try:
|
|
11
|
+
if isinstance(obj, Subject):
|
|
12
|
+
func(obj, **kwargs)
|
|
13
|
+
return obj
|
|
14
|
+
|
|
15
|
+
return func(obj, **kwargs)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
logger.error(f"{func.__module__}:{func.__name__} | {obj} | {e}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parallel_processing(
|
|
21
|
+
func: Callable,
|
|
22
|
+
objs: list[Any],
|
|
23
|
+
n_cores: int | str = "max",
|
|
24
|
+
**kwargs,
|
|
25
|
+
) -> list[Any]:
|
|
26
|
+
max_cores = mp.cpu_count()
|
|
27
|
+
|
|
28
|
+
if n_cores == "max":
|
|
29
|
+
n_cores = max_cores
|
|
30
|
+
elif isinstance(n_cores, int) and n_cores > max_cores:
|
|
31
|
+
raise ValueError(f"Maximum number of cores is {max_cores}. Received {n_cores}.")
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f"Invalid value for n_cores: {n_cores}.")
|
|
34
|
+
|
|
35
|
+
with mp.Pool(n_cores) as pool:
|
|
36
|
+
results = pool.map(
|
|
37
|
+
partial(process_obj, func=func, **kwargs),
|
|
38
|
+
objs,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return results
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Tuple
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
from dateutil.parser import parse as parse_dt
|
|
7
7
|
|
|
8
|
+
from ..logging import success_parsing_log
|
|
8
9
|
from ..structure.subject import Metadata, Sensor, Subject, Vendor
|
|
9
10
|
from ..structure.validation.subject import SCHEMA, Column
|
|
10
|
-
from ..utils import
|
|
11
|
+
from ..utils import (
|
|
12
|
+
align_datetimes,
|
|
13
|
+
filter_datetime,
|
|
14
|
+
get_sampling_frequency,
|
|
15
|
+
)
|
|
16
|
+
from .utils import set_timezone
|
|
11
17
|
|
|
12
18
|
# TODO: Add param for column mapping if there is no header.
|
|
13
19
|
# TODO: It is not perfect, but it is a start and it should be able to parse automatically almost all ActiGraph files from SDU.
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
def filter_datetime(
|
|
17
|
-
df: pd.DataFrame,
|
|
18
|
-
start: datetime | None = None,
|
|
19
|
-
end: datetime | None = None,
|
|
20
|
-
) -> pd.DataFrame:
|
|
21
|
-
if start and end:
|
|
22
|
-
if end < start:
|
|
23
|
-
raise ValueError("The end time must be after the start time.")
|
|
24
|
-
|
|
25
|
-
if start:
|
|
26
|
-
df = df[df[Column.DATETIME] >= start]
|
|
27
|
-
|
|
28
|
-
if end:
|
|
29
|
-
df = df[df[Column.DATETIME] <= end]
|
|
30
|
-
|
|
31
|
-
return df
|
|
32
|
-
|
|
33
|
-
|
|
34
22
|
def get_metadata(metadata: list[str]) -> dict[str, Any]:
|
|
35
23
|
model = metadata[0].split("ActiGraph")[-1].split()[0].strip()
|
|
36
24
|
firmware = metadata[0].split("Firmware")[-1].split()[0].strip()
|
|
@@ -137,15 +125,15 @@ def from_csv(
|
|
|
137
125
|
metadata: bool = True,
|
|
138
126
|
*,
|
|
139
127
|
subject_id: str | None = None,
|
|
140
|
-
sensor_id: str | None = None,
|
|
128
|
+
sensor_id: str | None = None, # type: ignore
|
|
141
129
|
serial_number: str | None = None,
|
|
142
130
|
model: str | None = None,
|
|
143
131
|
vendor: Vendor = Vendor.ACTIGRAPH,
|
|
144
132
|
firmware_version: str | None = None,
|
|
145
133
|
start: datetime | None = None,
|
|
146
134
|
end: datetime | None = None,
|
|
147
|
-
timezone: str | None = None,
|
|
148
|
-
|
|
135
|
+
timezone: str | None | Tuple[str, str] = None,
|
|
136
|
+
datetime_format: str | None = None,
|
|
149
137
|
) -> Subject:
|
|
150
138
|
if isinstance(path, str):
|
|
151
139
|
path = Path(path)
|
|
@@ -183,17 +171,18 @@ def from_csv(
|
|
|
183
171
|
Column.PERPENDICULAR_COUNTS,
|
|
184
172
|
]
|
|
185
173
|
|
|
186
|
-
df = handle_datetime(df, parsed_metadata,
|
|
187
|
-
df = filter_datetime(df, start, end)
|
|
174
|
+
df = handle_datetime(df, parsed_metadata, datetime_format)
|
|
188
175
|
|
|
189
176
|
df.set_index(Column.DATETIME, inplace=True)
|
|
190
|
-
|
|
177
|
+
df, timezone = set_timezone(df, timezone)
|
|
191
178
|
|
|
192
|
-
df
|
|
179
|
+
df = filter_datetime(df, start, end)
|
|
193
180
|
|
|
181
|
+
sampling_frequency = parsed_metadata.get("sampling_frequency")
|
|
194
182
|
if not sampling_frequency:
|
|
195
183
|
sampling_frequency = get_sampling_frequency(df)
|
|
196
|
-
|
|
184
|
+
|
|
185
|
+
df = align_datetimes(df, sampling_frequency)
|
|
197
186
|
|
|
198
187
|
columns = [col.value for col in Column]
|
|
199
188
|
ordered_columns = [col for col in columns if col in df.columns]
|
|
@@ -229,4 +218,9 @@ def from_csv(
|
|
|
229
218
|
timezone=timezone,
|
|
230
219
|
) # type: ignore
|
|
231
220
|
|
|
232
|
-
|
|
221
|
+
subject = Subject(metadata=metadata, df=df) # type: ignore
|
|
222
|
+
|
|
223
|
+
func = f"{__name__}:from_csv"
|
|
224
|
+
success_parsing_log(subject, path, func)
|
|
225
|
+
|
|
226
|
+
return subject
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from ..parallel import parallel_processing, process_obj
|
|
5
|
+
from ..structure import Collection
|
|
6
|
+
|
|
7
|
+
# TODO: Add bulk parsing with linkage file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def bulk_parser(
|
|
11
|
+
folder: str | Path,
|
|
12
|
+
parser: Callable,
|
|
13
|
+
id: str | None = None,
|
|
14
|
+
parallel: bool = True,
|
|
15
|
+
n_cores: int | str = "max",
|
|
16
|
+
**kwargs,
|
|
17
|
+
):
|
|
18
|
+
if isinstance(folder, str):
|
|
19
|
+
folder = Path(folder)
|
|
20
|
+
|
|
21
|
+
if not folder.is_dir():
|
|
22
|
+
raise ValueError(f"'{folder}' is not a valid folder.")
|
|
23
|
+
|
|
24
|
+
files = list(Path(folder).glob("*"))
|
|
25
|
+
|
|
26
|
+
if id is None:
|
|
27
|
+
id = folder.name
|
|
28
|
+
|
|
29
|
+
if parallel:
|
|
30
|
+
results = parallel_processing(parser, files, n_cores, **kwargs)
|
|
31
|
+
else:
|
|
32
|
+
results = [process_obj(file, parser, **kwargs) for file in files]
|
|
33
|
+
|
|
34
|
+
# Remove None values from results
|
|
35
|
+
results = list(filter(None, results))
|
|
36
|
+
|
|
37
|
+
collection = Collection(id=id, subjects=results)
|
|
38
|
+
|
|
39
|
+
return collection
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Tuple
|
|
3
4
|
|
|
5
|
+
import numpy as np
|
|
4
6
|
import pandas as pd
|
|
5
7
|
|
|
8
|
+
from ..logging import success_parsing_log
|
|
6
9
|
from ..structure.subject import Metadata, Sensor, Subject, Vendor
|
|
7
10
|
from ..structure.validation.subject import SCHEMA, Column
|
|
8
11
|
from ..utils import (
|
|
9
12
|
align_datetimes,
|
|
10
13
|
columns_exists,
|
|
11
|
-
|
|
12
|
-
convert_to_geodataframe,
|
|
13
|
-
get_crs,
|
|
14
|
+
filter_datetime,
|
|
14
15
|
get_sampling_frequency,
|
|
15
|
-
get_timezone,
|
|
16
16
|
)
|
|
17
|
+
from .utils import set_crs, set_timezone
|
|
17
18
|
|
|
18
19
|
# TODO: Can be refactored.,
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect_environment_from_gps_signal(
|
|
23
|
+
df: pd.DataFrame,
|
|
24
|
+
method: str,
|
|
25
|
+
limit: int | float,
|
|
26
|
+
) -> pd.DataFrame:
|
|
27
|
+
df = df.copy()
|
|
28
|
+
|
|
29
|
+
methods = [
|
|
30
|
+
Column.SNR_VIEWED, # 260
|
|
31
|
+
Column.NSAT_RATIO, # 70
|
|
32
|
+
]
|
|
33
|
+
if method not in methods:
|
|
34
|
+
raise ValueError(f"Method muset be one of {methods}")
|
|
35
|
+
|
|
36
|
+
if method not in df.columns:
|
|
37
|
+
raise ValueError(f"Column '{method}' does not exist in dataframe")
|
|
38
|
+
|
|
39
|
+
mask = df[method].notna()
|
|
40
|
+
df.loc[mask, Column.ENVIRONMENT] = "outdoor"
|
|
41
|
+
df.loc[mask & (df[method] <= limit), Column.ENVIRONMENT] = "indoor"
|
|
42
|
+
|
|
43
|
+
return df
|
|
20
44
|
|
|
21
45
|
|
|
22
46
|
def parse_snr(df: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -162,37 +186,10 @@ def parse_elevation(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
162
186
|
return df
|
|
163
187
|
|
|
164
188
|
|
|
165
|
-
def filter_datetime(
|
|
166
|
-
df: pd.DataFrame,
|
|
167
|
-
start: datetime | None = None,
|
|
168
|
-
end: datetime | None = None,
|
|
169
|
-
) -> pd.DataFrame:
|
|
170
|
-
if start and end:
|
|
171
|
-
if end < start:
|
|
172
|
-
raise ValueError("The end time must be after the start time.")
|
|
173
|
-
|
|
174
|
-
if start:
|
|
175
|
-
df = df[df[Column.DATETIME] >= start]
|
|
176
|
-
|
|
177
|
-
if end:
|
|
178
|
-
df = df[df[Column.DATETIME] <= end]
|
|
179
|
-
|
|
180
|
-
return df
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def handle_crs(df: pd.DataFrame, crs: str) -> pd.DataFrame:
|
|
184
|
-
gdf = convert_to_geodataframe(df, crs="WGS84")
|
|
185
|
-
gdf.to_crs(crs, inplace=True)
|
|
186
|
-
df = convert_to_dataframe(gdf)
|
|
187
|
-
|
|
188
|
-
return df
|
|
189
|
-
|
|
190
|
-
|
|
191
189
|
def parse_datetime(df: pd.DataFrame, dt_format: str | None = None) -> pd.DataFrame:
|
|
192
190
|
df = df.copy()
|
|
193
191
|
column = None
|
|
194
192
|
|
|
195
|
-
# FIXME: Fix this...
|
|
196
193
|
if "DATE" in df.columns and "TIME" in df.columns:
|
|
197
194
|
date = "DATE"
|
|
198
195
|
time = "TIME"
|
|
@@ -228,8 +225,15 @@ def parse_coordinates(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
228
225
|
[Column.LATITUDE, Column.LONGITUDE]
|
|
229
226
|
].apply(pd.to_numeric)
|
|
230
227
|
|
|
231
|
-
df
|
|
232
|
-
df
|
|
228
|
+
df["N/S"] = df["N/S"].str.strip()
|
|
229
|
+
df[Column.LATITUDE] = np.where(
|
|
230
|
+
df["N/S"] == "S", -df[Column.LATITUDE], df[Column.LATITUDE]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
df["E/W"] = df["E/W"].str.strip()
|
|
234
|
+
df[Column.LONGITUDE] = np.where(
|
|
235
|
+
df["E/W"] == "W", -df[Column.LONGITUDE], df[Column.LONGITUDE]
|
|
236
|
+
)
|
|
233
237
|
|
|
234
238
|
return df
|
|
235
239
|
|
|
@@ -279,9 +283,11 @@ def from_csv(
|
|
|
279
283
|
start: datetime | None = None,
|
|
280
284
|
end: datetime | None = None,
|
|
281
285
|
sampling_frequency: float | None = None,
|
|
282
|
-
crs: str | None = None,
|
|
283
|
-
timezone: str | None = None,
|
|
284
|
-
|
|
286
|
+
crs: str | None | Tuple[str, str] = None,
|
|
287
|
+
timezone: str | None | Tuple[str, str] = None,
|
|
288
|
+
datetime_format: str | None = None,
|
|
289
|
+
nsat_ratio: float = 70,
|
|
290
|
+
snr_viewed: int = 260,
|
|
285
291
|
) -> Subject:
|
|
286
292
|
if isinstance(path, str):
|
|
287
293
|
path = Path(path)
|
|
@@ -299,15 +305,7 @@ def from_csv(
|
|
|
299
305
|
remove_repeated_headers(df)
|
|
300
306
|
|
|
301
307
|
df = parse_coordinates(df)
|
|
302
|
-
df = parse_datetime(df,
|
|
303
|
-
|
|
304
|
-
if not crs:
|
|
305
|
-
crs = get_crs(df)
|
|
306
|
-
|
|
307
|
-
df = handle_crs(df, crs)
|
|
308
|
-
|
|
309
|
-
if not timezone:
|
|
310
|
-
timezone = get_timezone(df)
|
|
308
|
+
df = parse_datetime(df, datetime_format)
|
|
311
309
|
|
|
312
310
|
df.drop_duplicates(
|
|
313
311
|
subset=[Column.DATETIME], inplace=True
|
|
@@ -317,7 +315,6 @@ def from_csv(
|
|
|
317
315
|
if "VALID" in df.columns:
|
|
318
316
|
df = df[~(df["VALID"] == "Estimated (dead reckoning)")]
|
|
319
317
|
|
|
320
|
-
df = filter_datetime(df, start, end)
|
|
321
318
|
df = parse_distance(df)
|
|
322
319
|
df = parse_speed(df)
|
|
323
320
|
df = parse_elevation(df)
|
|
@@ -330,7 +327,16 @@ def from_csv(
|
|
|
330
327
|
(df[Column.NSAT_USED] * 100) / df[Column.NSAT_VIEWED]
|
|
331
328
|
).round(2)
|
|
332
329
|
|
|
330
|
+
if Column.SNR_VIEWED in df.columns and snr_viewed:
|
|
331
|
+
df = detect_environment_from_gps_signal(df, Column.SNR_VIEWED, snr_viewed)
|
|
332
|
+
elif Column.NSAT_RATIO in df.columns and nsat_ratio:
|
|
333
|
+
df = detect_environment_from_gps_signal(df, Column.NSAT_RATIO, nsat_ratio)
|
|
334
|
+
|
|
333
335
|
df.set_index(Column.DATETIME, inplace=True)
|
|
336
|
+
df, timezone = set_timezone(df, timezone)
|
|
337
|
+
df, crs = set_crs(df, crs)
|
|
338
|
+
|
|
339
|
+
df = filter_datetime(df, start, end)
|
|
334
340
|
|
|
335
341
|
if not sampling_frequency:
|
|
336
342
|
sampling_frequency = get_sampling_frequency(df)
|
|
@@ -363,4 +369,9 @@ def from_csv(
|
|
|
363
369
|
timezone=timezone,
|
|
364
370
|
)
|
|
365
371
|
|
|
366
|
-
|
|
372
|
+
subject = Subject(metadata=metadata, df=df)
|
|
373
|
+
|
|
374
|
+
func = f"{__name__}:from_csv"
|
|
375
|
+
success_parsing_log(subject, path, func)
|
|
376
|
+
|
|
377
|
+
return subject
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from ..logging import logger
|
|
6
|
+
from ..utils import change_crs, change_timezone, get_crs, get_timezone
|
|
7
|
+
|
|
8
|
+
DEFAULT_CRS_SENSOR = "EPSG:4326"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_timezone(
|
|
12
|
+
df: pd.DataFrame,
|
|
13
|
+
timezone: str | None | Tuple[str, str] = None,
|
|
14
|
+
) -> Tuple[pd.DataFrame, str]:
|
|
15
|
+
if isinstance(timezone, tuple) and len(timezone) == 2:
|
|
16
|
+
source_tz = timezone[0]
|
|
17
|
+
target_tz = timezone[1]
|
|
18
|
+
df = change_timezone(df, source_tz, target_tz)
|
|
19
|
+
timezone = target_tz
|
|
20
|
+
elif not timezone:
|
|
21
|
+
timezone = get_timezone(df)
|
|
22
|
+
|
|
23
|
+
return df, timezone
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_crs(
|
|
27
|
+
df: pd.DataFrame,
|
|
28
|
+
crs: str | None | Tuple[str, str] = None,
|
|
29
|
+
) -> Tuple[pd.DataFrame, str]:
|
|
30
|
+
if isinstance(crs, tuple) and len(crs) == 2:
|
|
31
|
+
source_crs = crs[0]
|
|
32
|
+
target_crs = crs[1]
|
|
33
|
+
df = change_crs(df, source_crs, target_crs)
|
|
34
|
+
crs = target_crs
|
|
35
|
+
elif isinstance(crs, str):
|
|
36
|
+
df = change_crs(df, DEFAULT_CRS_SENSOR, crs)
|
|
37
|
+
elif not crs:
|
|
38
|
+
crs = get_crs(df)
|
|
39
|
+
df = change_crs(df, DEFAULT_CRS_SENSOR, crs)
|
|
40
|
+
else:
|
|
41
|
+
logger.error(f"Invalid value for crs: {crs}")
|
|
42
|
+
|
|
43
|
+
return df, crs
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,6 +11,7 @@ def get_information(df: pd.DataFrame, crs: str) -> pd.Series:
|
|
|
11
11
|
start = df.index.min()
|
|
12
12
|
end = df.index.max()
|
|
13
13
|
duration = end - start
|
|
14
|
+
segment_id = df["segment_id"].iloc[0]
|
|
14
15
|
|
|
15
16
|
distance_col = get_unique_column_name(df)
|
|
16
17
|
df = add_distance(df, crs=crs, name=distance_col)
|
|
@@ -28,7 +29,11 @@ def get_information(df: pd.DataFrame, crs: str) -> pd.Series:
|
|
|
28
29
|
location_id = df["stationary_id"].iloc[0]
|
|
29
30
|
else:
|
|
30
31
|
status = "transport"
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
if Column.TRIP_MODE in df.columns:
|
|
34
|
+
mode = df[Column.TRIP_MODE].iloc[0]
|
|
35
|
+
else:
|
|
36
|
+
mode = pd.NA
|
|
32
37
|
location_id = pd.NA
|
|
33
38
|
else:
|
|
34
39
|
status = "stationary"
|
|
@@ -44,6 +49,7 @@ def get_information(df: pd.DataFrame, crs: str) -> pd.Series:
|
|
|
44
49
|
|
|
45
50
|
return pd.Series(
|
|
46
51
|
{
|
|
52
|
+
"segment_id": segment_id,
|
|
47
53
|
"trip_id": trip_id,
|
|
48
54
|
"stationary_id": location_id,
|
|
49
55
|
"start": start,
|
|
@@ -87,7 +93,11 @@ def get_timeline(df: pd.DataFrame, crs: str) -> pd.DataFrame:
|
|
|
87
93
|
lambda x: get_information(x, crs=crs)
|
|
88
94
|
)
|
|
89
95
|
|
|
90
|
-
summary =
|
|
96
|
+
summary = (
|
|
97
|
+
pd.concat([trips, stops], ignore_index=True)
|
|
98
|
+
.sort_values(["start", "end"])
|
|
99
|
+
.reset_index(drop=True)
|
|
100
|
+
)
|
|
91
101
|
|
|
92
102
|
return summary.astype(
|
|
93
103
|
{
|