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.
Files changed (93) hide show
  1. {labda-0.0.2 → labda-0.0.3}/PKG-INFO +4 -2
  2. labda-0.0.3/README.md +27 -0
  3. {labda-0.0.2 → labda-0.0.3}/labda/__init__.py +3 -0
  4. labda-0.0.3/labda/assets/__pycache__/__init__.cpython-311.pyc +0 -0
  5. labda-0.0.3/labda/assets/__pycache__/counts_cut_points.cpython-311.pyc +0 -0
  6. labda-0.0.3/labda/assets/__pycache__/transportation_cut_points.cpython-311.pyc +0 -0
  7. {labda-0.0.2 → labda-0.0.3}/labda/assets/transportation_cut_points.py +1 -1
  8. labda-0.0.3/labda/expanders/__pycache__/__init__.cpython-311.pyc +0 -0
  9. labda-0.0.3/labda/expanders/__pycache__/accelerometer.cpython-311.pyc +0 -0
  10. labda-0.0.3/labda/expanders/__pycache__/extras.cpython-311.pyc +0 -0
  11. labda-0.0.3/labda/expanders/__pycache__/spatial.cpython-311.pyc +0 -0
  12. labda-0.0.3/labda/logging.py +54 -0
  13. labda-0.0.3/labda/parallel.py +41 -0
  14. labda-0.0.3/labda/parsers/__pycache__/__init__.cpython-311.pyc +0 -0
  15. labda-0.0.3/labda/parsers/__pycache__/actigraph.cpython-311.pyc +0 -0
  16. labda-0.0.3/labda/parsers/__pycache__/bulk.cpython-311.pyc +0 -0
  17. labda-0.0.3/labda/parsers/__pycache__/gadgetbridge.cpython-311.pyc +0 -0
  18. labda-0.0.3/labda/parsers/__pycache__/qstarz.cpython-311.pyc +0 -0
  19. labda-0.0.3/labda/parsers/__pycache__/utils.cpython-311.pyc +0 -0
  20. labda-0.0.3/labda/parsers/__pycache__/vendors.cpython-311.pyc +0 -0
  21. {labda-0.0.2 → labda-0.0.3}/labda/parsers/actigraph.py +23 -29
  22. labda-0.0.3/labda/parsers/bulk.py +39 -0
  23. {labda-0.0.2 → labda-0.0.3}/labda/parsers/qstarz.py +59 -48
  24. labda-0.0.3/labda/parsers/utils.py +43 -0
  25. labda-0.0.3/labda/processing/__pycache__/__init__.cpython-311.pyc +0 -0
  26. labda-0.0.3/labda/processing/__pycache__/bouts.cpython-311.pyc +0 -0
  27. labda-0.0.3/labda/processing/__pycache__/domains.cpython-311.pyc +0 -0
  28. labda-0.0.3/labda/processing/__pycache__/manipulations.cpython-311.pyc +0 -0
  29. labda-0.0.3/labda/processing/accelerometer/__pycache__/__init__.cpython-311.pyc +0 -0
  30. labda-0.0.3/labda/processing/accelerometer/__pycache__/activity_intensity.cpython-311.pyc +0 -0
  31. labda-0.0.3/labda/processing/accelerometer/__pycache__/cut_points.cpython-311.pyc +0 -0
  32. labda-0.0.3/labda/processing/accelerometer/__pycache__/steps.cpython-311.pyc +0 -0
  33. labda-0.0.3/labda/processing/accelerometer/__pycache__/wear.cpython-311.pyc +0 -0
  34. labda-0.0.3/labda/processing/spatial/__pycache__/__init__.cpython-311.pyc +0 -0
  35. labda-0.0.3/labda/processing/spatial/__pycache__/manipulations.cpython-311.pyc +0 -0
  36. labda-0.0.3/labda/processing/spatial/__pycache__/timeline.cpython-311.pyc +0 -0
  37. labda-0.0.3/labda/processing/spatial/__pycache__/transportation.cpython-311.pyc +0 -0
  38. labda-0.0.3/labda/processing/spatial/__pycache__/trips.cpython-311.pyc +0 -0
  39. {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/manipulations.py +1 -0
  40. {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/timeline.py +12 -2
  41. {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/trips.py +72 -22
  42. labda-0.0.3/labda/structure/__pycache__/__init__.cpython-311.pyc +0 -0
  43. labda-0.0.3/labda/structure/__pycache__/collection.cpython-311.pyc +0 -0
  44. labda-0.0.3/labda/structure/__pycache__/domains.cpython-311.pyc +0 -0
  45. labda-0.0.3/labda/structure/__pycache__/linkage.cpython-311.pyc +0 -0
  46. labda-0.0.3/labda/structure/__pycache__/merging.cpython-311.pyc +0 -0
  47. labda-0.0.3/labda/structure/__pycache__/resampling.cpython-311.pyc +0 -0
  48. labda-0.0.3/labda/structure/__pycache__/subject.cpython-311.pyc +0 -0
  49. {labda-0.0.2 → labda-0.0.3}/labda/structure/collection.py +65 -22
  50. {labda-0.0.2 → labda-0.0.3}/labda/structure/merging.py +21 -5
  51. {labda-0.0.2 → labda-0.0.3}/labda/structure/subject.py +29 -13
  52. labda-0.0.3/labda/structure/validation/__pycache__/__init__.cpython-311.pyc +0 -0
  53. labda-0.0.3/labda/structure/validation/__pycache__/domains.cpython-311.pyc +0 -0
  54. labda-0.0.3/labda/structure/validation/__pycache__/linkage.cpython-311.pyc +0 -0
  55. labda-0.0.3/labda/structure/validation/__pycache__/subject.cpython-311.pyc +0 -0
  56. {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/subject.py +2 -1
  57. {labda-0.0.2 → labda-0.0.3}/labda/utils.py +34 -0
  58. labda-0.0.3/labda/visualisation/__pycache__/__init__.cpython-311.pyc +0 -0
  59. labda-0.0.3/labda/visualisation/__pycache__/spatial.cpython-311.pyc +0 -0
  60. labda-0.0.3/labda/visualisation/spatial.py +93 -0
  61. {labda-0.0.2 → labda-0.0.3}/pyproject.toml +7 -17
  62. labda-0.0.2/README.md +0 -26
  63. labda-0.0.2/labda/parsers/bulk.py +0 -31
  64. labda-0.0.2/labda/processing/spatial/environment.py +0 -50
  65. {labda-0.0.2 → labda-0.0.3}/CHANGELOG.md +0 -0
  66. {labda-0.0.2 → labda-0.0.3}/labda/assets/__init__.py +0 -0
  67. {labda-0.0.2 → labda-0.0.3}/labda/assets/counts_cut_points.py +0 -0
  68. {labda-0.0.2 → labda-0.0.3}/labda/expanders/__init__.py +0 -0
  69. {labda-0.0.2 → labda-0.0.3}/labda/expanders/accelerometer.py +0 -0
  70. {labda-0.0.2 → labda-0.0.3}/labda/expanders/extras.py +0 -0
  71. {labda-0.0.2 → labda-0.0.3}/labda/expanders/spatial.py +0 -0
  72. {labda-0.0.2 → labda-0.0.3}/labda/parsers/__init__.py +0 -0
  73. {labda-0.0.2 → labda-0.0.3}/labda/parsers/gadgetbridge.py +0 -0
  74. {labda-0.0.2 → labda-0.0.3}/labda/parsers/garmin.py +0 -0
  75. {labda-0.0.2 → labda-0.0.3}/labda/processing/__init__.py +0 -0
  76. {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/__init__.py +0 -0
  77. {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/activity_intensity.py +0 -0
  78. {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/steps.py +0 -0
  79. {labda-0.0.2 → labda-0.0.3}/labda/processing/accelerometer/wear.py +0 -0
  80. {labda-0.0.2 → labda-0.0.3}/labda/processing/bouts.py +0 -0
  81. {labda-0.0.2 → labda-0.0.3}/labda/processing/domains.py +0 -0
  82. {labda-0.0.2 → labda-0.0.3}/labda/processing/manipulations.py +0 -0
  83. {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/__init__.py +0 -0
  84. {labda-0.0.2 → labda-0.0.3}/labda/processing/spatial/transportation.py +0 -0
  85. {labda-0.0.2 → labda-0.0.3}/labda/processing/wear_validity.py +0 -0
  86. {labda-0.0.2 → labda-0.0.3}/labda/structure/__init__.py +0 -0
  87. {labda-0.0.2 → labda-0.0.3}/labda/structure/domains.py +0 -0
  88. {labda-0.0.2 → labda-0.0.3}/labda/structure/linkage.py +0 -0
  89. {labda-0.0.2 → labda-0.0.3}/labda/structure/resampling.py +0 -0
  90. {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/__init__.py +0 -0
  91. {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/domains.py +0 -0
  92. {labda-0.0.2 → labda-0.0.3}/labda/structure/validation/linkage.py +0 -0
  93. {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.2
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
- <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
+ [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..
@@ -1 +1,4 @@
1
+ from .logging import configure_logger
1
2
  from .structure import Collection, Column, Subject, merge_collections, merge_subjects
3
+
4
+ configure_logger("ERROR")
@@ -8,7 +8,7 @@ CUT_POINTS = [
8
8
  "required_data": Column.SPEED,
9
9
  "units": "km/h",
10
10
  "cut_points": [
11
- {"name": "walking/running", "max": 9},
11
+ {"name": "walk/run", "max": 9},
12
12
  {"name": "bicycle", "max": 35},
13
13
  {"name": "vehicle", "max": float("inf")},
14
14
  ],
@@ -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
@@ -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 get_sampling_frequency
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
- dt_format: str | None = None,
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, dt_format)
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
- sampling_frequency = parsed_metadata.get("sampling_frequency")
177
+ df, timezone = set_timezone(df, timezone)
191
178
 
192
- df.dropna(how="all", inplace=True)
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
- df = df.resample(f"{sampling_frequency}s").nearest()
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
- return Subject(metadata=metadata, df=df) # type: ignore
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
- convert_to_dataframe,
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
- # TODO: Remove all spaces from the column names, so there is no need to check if there is space or not.
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.loc[df["N/S"] == "S", Column.LATITUDE] = -df[Column.LATITUDE]
232
- df.loc[df["E/W"] == "W", Column.LONGITUDE] = -df[Column.LONGITUDE]
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
- dt_format: str | None = None,
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, dt_format)
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
- return Subject(metadata=metadata, df=df)
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
@@ -41,6 +41,7 @@ def stop_splitter(
41
41
 
42
42
  columns_not_exists(df, [name], overwrite=overwrite)
43
43
 
44
+ # TODO: How to handle NA values, maybe just filter to have only valid points (with coordinates).
44
45
  df = df.copy()
45
46
  df[name] = False
46
47
 
@@ -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
- mode = df["trip_mode"].iloc[0]
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 = pd.concat([trips, stops], ignore_index=True).sort_values(["start", "end"])
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
  {