scdata 1.4.0__tar.gz → 1.5.0__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 (97) hide show
  1. {scdata-1.4.0/scdata.egg-info → scdata-1.5.0}/PKG-INFO +5 -3
  2. {scdata-1.4.0 → scdata-1.5.0}/requirements.txt +2 -2
  3. {scdata-1.4.0 → scdata-1.5.0}/scdata/__init__.py +1 -1
  4. {scdata-1.4.0 → scdata-1.5.0}/scdata/_config/config.py +17 -73
  5. scdata-1.5.0/scdata/device/check/__init__.py +4 -0
  6. scdata-1.5.0/scdata/device/check/flats.py +61 -0
  7. scdata-1.5.0/scdata/device/check/gaps.py +94 -0
  8. scdata-1.5.0/scdata/device/check/implausible.py +25 -0
  9. scdata-1.5.0/scdata/device/check/outliers.py +210 -0
  10. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/device.py +474 -402
  11. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/alphasense.py +221 -56
  12. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/error_codes.py +1 -0
  13. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/params.py +2 -2
  14. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/timeseries.py +35 -18
  15. {scdata-1.4.0 → scdata-1.5.0}/scdata/io/device_file.py +206 -63
  16. scdata-1.5.0/scdata/models/__init__.py +1 -0
  17. {scdata-1.4.0 → scdata-1.5.0}/scdata/models/models.py +18 -2
  18. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/tools.py +4 -4
  19. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_panel.py +114 -38
  20. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/test.py +7 -5
  21. scdata-1.5.0/scdata/tools/merging.py +323 -0
  22. {scdata-1.4.0 → scdata-1.5.0/scdata.egg-info}/PKG-INFO +5 -3
  23. {scdata-1.4.0 → scdata-1.5.0}/scdata.egg-info/SOURCES.txt +6 -0
  24. {scdata-1.4.0 → scdata-1.5.0}/scdata.egg-info/requires.txt +4 -2
  25. {scdata-1.4.0 → scdata-1.5.0}/setup.py +3 -1
  26. scdata-1.4.0/scdata/models/__init__.py +0 -1
  27. {scdata-1.4.0 → scdata-1.5.0}/LICENSE +0 -0
  28. {scdata-1.4.0 → scdata-1.5.0}/MANIFEST.in +0 -0
  29. {scdata-1.4.0 → scdata-1.5.0}/README.md +0 -0
  30. {scdata-1.4.0 → scdata-1.5.0}/scdata/_config/__init__.py +0 -0
  31. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/__init__.py +0 -0
  32. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/plot/__init__.py +0 -0
  33. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/__init__.py +0 -0
  34. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/formulae.py +0 -0
  35. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/geoseries.py +0 -0
  36. {scdata-1.4.0 → scdata-1.5.0}/scdata/device/process/regression.py +0 -0
  37. {scdata-1.4.0 → scdata-1.5.0}/scdata/io/__init__.py +0 -0
  38. {scdata-1.4.0 → scdata-1.5.0}/scdata/io/device_api.py +0 -0
  39. {scdata-1.4.0 → scdata-1.5.0}/scdata/io/model.py +0 -0
  40. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/__init__.py +0 -0
  41. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/box_plot.py +0 -0
  42. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/heatmap_iplot.py +0 -0
  43. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/heatmap_plot.py +0 -0
  44. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/heatmap_uplot.py +0 -0
  45. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/maps.py +0 -0
  46. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/scatter_dispersion_grid.py +0 -0
  47. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/scatter_iplot.py +0 -0
  48. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/scatter_plot.py +0 -0
  49. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/target_diagram.py +0 -0
  50. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_dendrogram.py +0 -0
  51. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_dispersion_grid.py +0 -0
  52. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_dispersion_plot.py +0 -0
  53. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_dispersion_uplot.py +0 -0
  54. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_iplot.py +0 -0
  55. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_plot.py +0 -0
  56. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_scatter.py +0 -0
  57. {scdata-1.4.0 → scdata-1.5.0}/scdata/plot/ts_uplot.py +0 -0
  58. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/__init__.py +0 -0
  59. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/checks/__init__.py +0 -0
  60. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/checks/checks.py +0 -0
  61. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/dispersion/__init__.py +0 -0
  62. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/dispersion/dispersion.py +0 -0
  63. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/export/__init__.py +0 -0
  64. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/export/templates/sc_template.html +0 -0
  65. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/export/to_file.py +0 -0
  66. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/tools/__init__.py +0 -0
  67. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/tools/combine.py +0 -0
  68. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/tools/history.py +0 -0
  69. {scdata-1.4.0 → scdata-1.5.0}/scdata/test/tools/prepare.py +0 -0
  70. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/__init__.py +0 -0
  71. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/cleaning.py +0 -0
  72. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/custom_logger.py +0 -0
  73. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/date.py +0 -0
  74. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/dictmerge.py +0 -0
  75. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/find.py +0 -0
  76. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/gets.py +0 -0
  77. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/interim/example.csv +0 -0
  78. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/interim/geodata.csv +0 -0
  79. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/lazy.py +0 -0
  80. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/location.py +0 -0
  81. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/report.py +0 -0
  82. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/series.py +0 -0
  83. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/stats.py +0 -0
  84. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/tree.py +0 -0
  85. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/units.py +0 -0
  86. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/uploads/example_upload_1.json +0 -0
  87. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/uploads/example_zenodo_upload.yaml +0 -0
  88. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/uploads/report.pdf +0 -0
  89. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/url_check.py +0 -0
  90. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/zenodo.py +0 -0
  91. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/zenodo_templates/README.md +0 -0
  92. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_dataset.json +0 -0
  93. {scdata-1.4.0 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_publication.json +0 -0
  94. {scdata-1.4.0 → scdata-1.5.0}/scdata.egg-info/dependency_links.txt +0 -0
  95. {scdata-1.4.0 → scdata-1.5.0}/scdata.egg-info/not-zip-safe +0 -0
  96. {scdata-1.4.0 → scdata-1.5.0}/scdata.egg-info/top_level.txt +0 -0
  97. {scdata-1.4.0 → scdata-1.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scdata
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Analysis of sensors and time series data
5
5
  Home-page: https://github.com/fablabbcn/smartcitizen-data
6
6
  Author: oscgonfer
@@ -22,13 +22,13 @@ Requires-Dist: geopy~=1.21.0
22
22
  Requires-Dist: Jinja2~=3.1.2
23
23
  Requires-Dist: matplotlib
24
24
  Requires-Dist: pandas~=2.2.2
25
- Requires-Dist: pydantic
25
+ Requires-Dist: pydantic~=2.8
26
26
  Requires-Dist: PyYAML~=6.0.1
27
27
  Requires-Dist: requests
28
28
  Requires-Dist: scipy
29
29
  Requires-Dist: scikit-learn
30
30
  Requires-Dist: seaborn
31
- Requires-Dist: smartcitizen-connector
31
+ Requires-Dist: smartcitizen-connector>=1.5.0
32
32
  Requires-Dist: termcolor==1.1.0
33
33
  Requires-Dist: timezonefinder~=6.1.9
34
34
  Requires-Dist: urllib3
@@ -38,6 +38,7 @@ Requires-Dist: bokeh; extra == "plotting"
38
38
  Requires-Dist: panel; extra == "plotting"
39
39
  Requires-Dist: branca~=0.4.0; extra == "plotting"
40
40
  Requires-Dist: folium~=0.12.1; extra == "plotting"
41
+ Requires-Dist: hvplot; extra == "plotting"
41
42
  Provides-Extra: dev
42
43
  Requires-Dist: pytest; extra == "dev"
43
44
  Requires-Dist: bokeh; extra == "dev"
@@ -45,6 +46,7 @@ Requires-Dist: panel; extra == "dev"
45
46
  Requires-Dist: branca~=0.4.0; extra == "dev"
46
47
  Requires-Dist: folium~=0.12.1; extra == "dev"
47
48
  Requires-Dist: awswrangler; extra == "dev"
49
+ Requires-Dist: hvplot; extra == "dev"
48
50
  Requires-Dist: boto3; extra == "dev"
49
51
  Dynamic: author
50
52
  Dynamic: classifier
@@ -4,14 +4,14 @@ geopy~=1.21.0
4
4
  Jinja2~=3.1.2
5
5
  matplotlib
6
6
  pandas~=2.2.2
7
- pydantic
7
+ pydantic~=2.8
8
8
  # TODO To be updated?
9
9
  PyYAML~=6.0.1
10
10
  requests
11
11
  scipy
12
12
  scikit-learn
13
13
  seaborn
14
- smartcitizen-connector
14
+ smartcitizen-connector>=1.5.0
15
15
  termcolor==1.1.0
16
16
  timezonefinder~=6.1.9
17
17
  urllib3
@@ -3,4 +3,4 @@ from .device import Device
3
3
  from .test import Test
4
4
  from .models import Source, TestOptions, DeviceOptions, APIParams, FileParams, CSVParams
5
5
 
6
- __version__ = '1.4.0'
6
+ __version__ = '1.5.0'
@@ -17,7 +17,7 @@ import json
17
17
  from pydantic import TypeAdapter
18
18
  from typing import List
19
19
 
20
- from scdata.models import Name, Blueprint, Metric
20
+ from scdata.models import Name, Blueprint
21
21
  from scdata.tools.dictmerge import dict_fmerge
22
22
  from scdata.tools.gets import get_json_from_url
23
23
 
@@ -95,7 +95,6 @@ class Config(object):
95
95
  # f'{_base_postprocessing_url}blueprints/sck_15.{_default_file_type}',
96
96
  # f'{_base_postprocessing_url}blueprints/sck_20.{_default_file_type}',
97
97
  f'{_base_postprocessing_url}blueprints/sc_air.{_default_file_type}',
98
- f'{_base_postprocessing_url}blueprints/sc_water.{_default_file_type}',
99
98
  # f'{_base_postprocessing_url}blueprints/sck_21_sps30.{_default_file_type}',
100
99
  # f'{_base_postprocessing_url}blueprints/sck_21_sen5x.{_default_file_type}',
101
100
  # f'{_base_postprocessing_url}blueprints/sck_21_gps.{_default_file_type}',
@@ -406,69 +405,14 @@ class Config(object):
406
405
  ### ------------VALUES-CHECK---------------
407
406
  ### ---------------------------------------
408
407
 
409
- _default_sampling_rate = {
410
- 'AMS AS7731 - UVA': 1,
411
- 'AMS AS7731 - UVB': 1,
412
- 'AMS AS7731 - UVC': 1,
413
- 'LIGHT': 1,
414
- 'BATT': 1,
415
- 'NOISE_A': 1,
416
- 'SCD30_CO2': 1,
417
- 'SCD30_HUM': 1,
418
- 'SCD30_TEMP': 1,
419
- 'SD-card': 1,
420
- 'LPS33_PRESS': 1,
421
- 'PRESS': 1,
422
- 'PMS5003_PM_1': 5,
423
- 'PMS5003_PM_25': 5,
424
- 'PMS5003_PM_10': 5,
425
- 'PMS5003_PN_03': 5,
426
- 'PMS5003_PN_03': 5,
427
- 'PMS5003_PN_05':5,
428
- 'PMS5003_PN_1':5,
429
- 'PMS5003_PN_10':5,
430
- 'PMS5003_PN_25':5,
431
- 'PMS5003_PN_5':5,
432
- 'SEN5X_HUM': 5,
433
- 'SEN5X_PM_1': 5,
434
- 'SEN5X_PM_10': 5,
435
- 'SEN5X_PM_25': 5,
436
- 'SEN5X_PM_40': 5,
437
- 'SEN5X_PN_05': 5,
438
- 'SEN5X_PN_1': 5,
439
- 'SEN5X_PN_10': 5,
440
- 'SEN5X_PN_25': 5,
441
- 'SEN5X_PN_40': 5,
442
- 'SEN5X_TPS': 5,
443
- 'SEN5X_TEMP': 5,
444
- 'SFA30_HCHO': 1,
445
- 'SFA30_HUM': 1,
446
- 'SFA30_TEMP': 1,
447
- 'ADC_48_0': 1,
448
- 'ADC_48_1': 1,
449
- 'ADC_48_2': 1,
450
- 'ADC_48_3': 1,
451
- 'ADC_49_0': 1,
452
- 'ADC_49_1': 1,
453
- 'ADC_49_2': 1,
454
- 'ADC_49_3': 1,
455
- 'CCS811_VOCS': 1,
456
- 'CCS811_ECO2': 1,
457
- 'HUM': 1,
458
- 'TEMP': 1,
459
- 'RSSI': 1,
460
- 'NO2': 1,
461
- 'O3': 1,
462
- 'AS_TEMP': 1,
463
- 'AS_PH': 1,
464
- 'AS_COND': 1,
465
- 'AS_DO_SAT': 1,
466
- 'AS_DO': 1
467
- }
408
+ _default_gap_size_minutes = 5
468
409
 
469
- _default_unplausible_values = {
410
+ # TODO - Review limits
411
+ _default_implausible_values = {
412
+ 'CCS811_VOCS': [0, 30000],
413
+ 'CCS811_ECO2': [400, 30000],
470
414
  'NOISE_A': [20, 99],
471
- 'SCD30_CO2': [300, 2000],
415
+ 'SCD30_CO2': [300, 9500], # Sensor limit is 10000ppm
472
416
  'SCD30_HUM': [20, 99],
473
417
  'SCD30_TEMP': [-20, 50],
474
418
  'BATT': [0, 100],
@@ -483,19 +427,19 @@ class Config(object):
483
427
  'SEN5X_PM_25': [0, 500],
484
428
  'SEN5X_PM_40': [0, 500],
485
429
  'SEN5X_TEMP': [-20, 50],
486
- 'SFA30_HCHO': [00, 1000],
430
+ 'SFA30_HCHO': [0, 4500], # Sensor saturates at 5000ppb, standard output is at 1000ppb
487
431
  'SFA30_HUM': [20, 99],
488
432
  'SFA30_TEMP': [-20, 50],
489
- 'ADC_48_0': [0, 3],
490
- 'ADC_48_1': [0, 3],
491
- 'ADC_48_2': [0, 3],
492
- 'ADC_48_3': [0, 3],
493
- 'ADC_49_0': [0, 3],
494
- 'ADC_49_1': [0, 3],
495
- 'ADC_49_2': [0, 3],
496
- 'ADC_49_3': [0, 3],
433
+ 'ADC_48_0': [0, 5.1],
434
+ 'ADC_48_1': [0, 5.1],
435
+ 'ADC_48_2': [0, 5.1],
436
+ 'ADC_48_3': [0, 5.1],
437
+ 'ADC_49_0': [0, 5.1],
438
+ 'ADC_49_1': [0, 5.1],
439
+ 'ADC_49_2': [0, 5.1],
440
+ 'ADC_49_3': [0, 5.1],
497
441
  'HUM': [20, 99],
498
- 'TEMP': [-20, 50],
442
+ 'TEMP': [-40, 60],
499
443
  'NO2': [0, 1000],
500
444
  'O3': [0, 1000],
501
445
  'AS_TEMP': [-20, 50],
@@ -0,0 +1,4 @@
1
+ from .flats import find_flat_values
2
+ from .implausible import find_implausible_values
3
+ from .gaps import find_gaps
4
+ from .outliers import find_outliers_isolation_forest, find_outliers
@@ -0,0 +1,61 @@
1
+ from pandas import DatetimeIndex, Timedelta
2
+
3
+ from scdata.tools.custom_logger import logger
4
+ from scdata.device.process.error_codes import StatusCode, ProcessResult
5
+
6
+ def find_flat_values(dataframe, **kwargs):
7
+ '''
8
+ Flags values where the rolling standard deviation is below limit_rolling_std
9
+
10
+ Parameters
11
+ ----------
12
+ flat_window_minutes: int
13
+ 1000
14
+ Window in minutes. Requires a DatetimeIndex. Values are only flagged
15
+ once a full window of data is available
16
+ flat_sensor_window: int
17
+ None
18
+ Window in rows. Used instead of flat_window_minutes if set
19
+ limit_rolling_std: float
20
+ 1e-5
21
+ Standard deviation below which values are flat
22
+ columns: list
23
+ All columns
24
+ Columns to check
25
+ '''
26
+
27
+ flat_window_minutes = kwargs.get('flat_window_minutes', 1000)
28
+ flat_sensor_window = kwargs.get('flat_sensor_window', None)
29
+ limit_rolling_std = kwargs.get('limit_rolling_std', 1e-5)
30
+ columns = kwargs.get('columns', list(dataframe.columns))
31
+
32
+ df = dataframe.copy()
33
+ cols = []
34
+
35
+ if flat_sensor_window is not None:
36
+ window = flat_sensor_window
37
+ full_window = None
38
+ logger.info(f'Flat window size: {flat_sensor_window} rows. STD Limit: {limit_rolling_std}')
39
+ elif isinstance(df.index, DatetimeIndex):
40
+ window = f'{flat_window_minutes}min'
41
+ # Time based windows are computed with partial data at the start
42
+ full_window = df.index >= df.index.min() + Timedelta(minutes=flat_window_minutes)
43
+ logger.info(f'Flat window size: {flat_window_minutes} minutes. STD Limit: {limit_rolling_std}')
44
+ else:
45
+ logger.error('flat_window_minutes requires a DatetimeIndex')
46
+ return ProcessResult(None, StatusCode.ERROR_WRONG_INDEX)
47
+
48
+ for col in columns:
49
+ if '__' in col: continue # Internal code for healthchecks
50
+ if col not in df.columns:
51
+ logger.warning(f'{col} not in columns. Skipping')
52
+ continue
53
+
54
+ logger.info (f'Calculating flat values for {col}')
55
+ df[f'__{col}'] = df[col].rolling(window=window).std() < limit_rolling_std
56
+ if full_window is not None:
57
+ df[f'__{col}'] &= full_window
58
+
59
+ cols.append(f'__{col}')
60
+
61
+ return ProcessResult(df[cols], StatusCode.SUCCESS)
@@ -0,0 +1,94 @@
1
+ from scdata._config import config
2
+ from scdata.tools.custom_logger import logger
3
+ from scdata.device.process.error_codes import StatusCode, ProcessResult
4
+ from pandas import Timedelta, DataFrame
5
+
6
+ import pandas as pd
7
+
8
+ # def find_gap_in_column(dataframe, frequency):
9
+ # Attempt to avoid having to resample -> Works, but to determine gap size only
10
+
11
+ # df = dataframe.copy()
12
+ # df = df.dropna()
13
+ # df['time'] = df.index
14
+ # df['time_lag'] = df['time'].shift(1)
15
+ # df['time_delta'] = df['time'] - df['time_lag']
16
+ # df['gap'] = df['time_delta'] > Timedelta(minutes=gap_size)
17
+ # df['gap_size'] = df.loc[df['gap'], 'time_delta']
18
+
19
+ # return df['gap']
20
+
21
+ # def find_gap_in_column(
22
+ # s: pd.Series,
23
+ # frequency: int | None = None,
24
+ # gap_size: int = 5,
25
+ # jitter_tolerance_sec: int = 5
26
+ # ):
27
+ # """
28
+ # Detect gaps in timeseries:
29
+ # - frequency: expected frequency in minutes
30
+ # - gap_size: minimum gap size in minutes
31
+ # - jitter_tolerance_sec: tolerance to jitter in seconds
32
+ # """
33
+
34
+ # s = s.sort_index()
35
+ # s = s[~s.index.duplicated()]
36
+
37
+ # if len(s) < 2:
38
+ # return pd.Series(False, index=s.index)
39
+
40
+ # dt = s.index.to_series().diff().dropna()
41
+ # freq_sec = frequency * 60
42
+ # gap_threshold = freq_sec + jitter_tolerance_sec
43
+
44
+ # gap_events = dt > pd.Timedelta(seconds=gap_threshold)
45
+ # gap_mask = pd.Series(False, index=s.index)
46
+
47
+ # for idx in dt.index[gap_events]:
48
+ # start = idx - dt.loc[idx]
49
+ # end = idx
50
+
51
+ # gap_duration_min = dt.loc[idx].total_seconds() / 60
52
+
53
+ # if gap_duration_min >= gap_size:
54
+ # gap_mask.loc[start:end] = True
55
+
56
+ # return gap_mask
57
+
58
+ def find_gaps(dataframe, **kwargs):
59
+
60
+ # default_gap_size_minutes = kwargs.get('default_gap_size_minutes',
61
+ # config._default_gap_size_minutes)
62
+ # gap_sizes = kwargs.get('gap_sizes', None)
63
+
64
+ # default_frequency_minutes = kwargs.get('default_frequency_minutes', 1)
65
+ # frequencies = kwargs.get('frequencies', None)
66
+
67
+ # gaps = []
68
+ df = dataframe.copy()
69
+
70
+ cols = []
71
+ for col in df.columns:
72
+ # gap_size = default_gap_size_minutes
73
+ # frequency = default_frequency_minutes
74
+ if '__' in col: continue # Internal code for healthchecks
75
+
76
+ # if gap_sizes is not None:
77
+ # for gap_size_group in gap_sizes:
78
+ # if col in gap_size_group["columns"]:
79
+ # gap_size = gap_size_group["gap_size_minutes"]
80
+ # break
81
+
82
+ # if frequencies is not None:
83
+ # for frequency_group in frequencies:
84
+ # if col in frequency_group["columns"]:
85
+ # frequency = frequency_group["frequency_minutes"]
86
+ # break
87
+
88
+ # logger.info (f'Calculating gaps for {col}, using {frequency} minutes. Gap size: {gap_size} minutes')
89
+ logger.info (f'Calculating gaps for {col}')
90
+ # df[f'__{col}'] = find_gap_in_column(df[col], frequency, gap_size)
91
+ df[f'__{col}'] = df.loc[:, col].isna()
92
+ cols.append(f'__{col}')
93
+
94
+ return ProcessResult(df[cols], StatusCode.SUCCESS)
@@ -0,0 +1,25 @@
1
+ from scdata._config import config
2
+ from scdata.tools.custom_logger import logger
3
+ from scdata.device.process.error_codes import StatusCode, ProcessResult
4
+
5
+ def find_implausible_values(dataframe, **kwargs):
6
+
7
+ implausible_values = kwargs.get('implausible_values', config._default_implausible_values)
8
+
9
+ df = dataframe.copy()
10
+ cols = []
11
+
12
+ for item in implausible_values:
13
+ col = item['column']
14
+ min_val = item['limits'][0]
15
+ max_val = item['limits'][1]
16
+ if col not in df.columns:
17
+ logger.warning(f'{col} not in columns. Skipping')
18
+ continue
19
+
20
+ logger.info (f'Calculating implausible values for {col} using [{min_val}, {max_val}]')
21
+ df[f'__{col}'] = (df.loc[:, col] < min_val) | (df.loc[:, col] > max_val)
22
+
23
+ cols.append(f'__{col}')
24
+
25
+ return ProcessResult(df[cols], StatusCode.SUCCESS)
@@ -0,0 +1,210 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from sklearn.ensemble import IsolationForest
4
+ from scipy.stats import zscore
5
+
6
+ from scdata._config import config
7
+ from scdata.tools.custom_logger import logger
8
+ from scdata.device.process.error_codes import StatusCode, ProcessResult
9
+
10
+ def find_outliers(dataframe, **kwargs):
11
+ columns = kwargs.get('columns', list(dataframe.columns))
12
+ zscore_value = kwargs.get('zscore', 3)
13
+ window = kwargs.get('window', 60)
14
+
15
+ df = dataframe.copy()
16
+ cols = []
17
+
18
+ for col in columns:
19
+ if '__' in col: continue # Internal code for healthchecks
20
+ if col not in df.columns:
21
+ logger.warning(f'{col} not in columns. Skipping')
22
+ continue
23
+
24
+ logger.info (f'Calculating outliers for {col}')
25
+ df[f'{col}_z_score'] = zscore(df[col].rolling(window=window).mean(), nan_policy='omit')
26
+
27
+ df.loc[:, f'__{col}'] = False
28
+ df.loc[(df[f'{col}_z_score'] > zscore_value) | (df[f'{col}_z_score'] < -zscore_value), f'__{col}'] = True
29
+
30
+ cols.append(f'__{col}')
31
+
32
+
33
+ return ProcessResult(df[cols], StatusCode.SUCCESS)
34
+
35
+ def find_outliers_isolation_forest(dataframe, **kwargs):
36
+ columns = kwargs.get('columns', list(dataframe.columns))
37
+ detector = kwargs.get('detector', None)
38
+
39
+ if detector is None:
40
+ logger.error(f'Detector cant be null. Aborting')
41
+ return ProcessResult(None, StatusCode.ERROR_MISSING_INPUTS)
42
+
43
+ df = dataframe.copy()
44
+ cols = []
45
+ prediction = detector.predict(df)
46
+
47
+ for col in columns:
48
+ if '__' in col: continue # Internal code for healthchecks
49
+ if col not in df.columns:
50
+ logger.warning(f'{col} not in columns. Skipping')
51
+ continue
52
+ if f'{col}_OUTL' not in prediction.columns:
53
+ logger.warning(f'{col} not predicted by detector. Skipping')
54
+ continue
55
+
56
+ logger.info (f'Calculating outliers for {col}')
57
+ # Rows without features (i.e. dropped NaN) are not flagged
58
+ df[f'__{col}'] = prediction[f'{col}_OUTL'].reindex(df.index).eq(1)
59
+
60
+ cols.append(f'__{col}')
61
+
62
+ return ProcessResult(df[cols], StatusCode.SUCCESS)
63
+
64
+ class MultiDeviceIForest:
65
+
66
+ def __init__(
67
+ self,
68
+ sensor_cols,
69
+ lags=(1, 2, 3, 5, 10),
70
+ contamination=0.01,
71
+ normalize_per_device=True,
72
+ custom_checks_fn=None
73
+ ):
74
+ self.sensor_cols = sensor_cols
75
+ self.lags = lags
76
+ self.contamination = contamination
77
+ self.normalize_per_device = normalize_per_device
78
+ self.custom_checks_fn = custom_checks_fn
79
+
80
+ self.models = {}
81
+ self.feature_map = {}
82
+
83
+ def _build_features(self, df):
84
+
85
+ df = df.copy()
86
+ df = df[~df.index.duplicated(keep='first')]
87
+ df = df.dropna()
88
+ # df = df.resample('5Min').mean()
89
+
90
+ for sensor in self.sensor_cols:
91
+ if sensor not in df.columns:
92
+ # logger.warn(f"Device doesn't have {sensor}")
93
+ continue
94
+ if self.normalize_per_device:
95
+ mean = df[sensor].mean()
96
+ std = df[sensor].std() + 1e-6
97
+ df[f"{sensor}_norm"] = (df[sensor] - mean) / std
98
+ base = f"{sensor}_norm"
99
+ else:
100
+ base = sensor
101
+
102
+ # Lags
103
+ for lag in self.lags:
104
+ df[f"{base}_lag_{lag}"] = df[base].shift(lag)
105
+
106
+ # Rolling
107
+ df[f"{base}_roll_mean_10"] = df[base].rolling(10).mean()
108
+ df[f"{base}_roll_std_10"] = df[base].rolling(10).std()
109
+
110
+ # Time features
111
+ df["hour"] = df.index.hour
112
+ df["dayofweek"] = df.index.dayofweek
113
+
114
+ if self.custom_checks_fn:
115
+ df = self.custom_checks_fn(df)
116
+
117
+ df = df.dropna()
118
+
119
+ return df
120
+
121
+ def fit(self, devices, train_device_ids, sample_frac=0.1):
122
+
123
+ samples = []
124
+
125
+ for device in devices:
126
+ if device.id not in train_device_ids:
127
+ continue
128
+ logger.info(f"Adding {device.id} to training")
129
+ df = self._build_features(device.data)
130
+
131
+ if len(df) == 0:
132
+ continue
133
+
134
+ sample = df.sample(frac=sample_frac)
135
+ samples.append(sample)
136
+
137
+ if not samples:
138
+ raise ValueError("No training data collected")
139
+
140
+ train_df = pd.concat(samples)
141
+
142
+ for sensor in self.sensor_cols:
143
+ logger.info(f"Training {sensor}")
144
+
145
+ base = f"{sensor}_norm" if self.normalize_per_device else sensor
146
+
147
+ feature_cols = [
148
+ c for c in train_df.columns
149
+ if c.startswith(base + "_lag_")
150
+ or c.startswith(base + "_roll_")
151
+ ] + ["hour", "dayofweek"]
152
+
153
+ X = train_df[feature_cols]
154
+
155
+ model = IsolationForest(
156
+ contamination=self.contamination,
157
+ n_estimators=150,
158
+ n_jobs=-1,
159
+ random_state=42
160
+ )
161
+
162
+ model.fit(X)
163
+
164
+ self.models[sensor] = model
165
+ self.feature_map[sensor] = feature_cols
166
+
167
+ return self
168
+
169
+ def predict(self, dataframe):
170
+
171
+ df = self._build_features(dataframe)
172
+ if df is None:
173
+ return None
174
+ all_new_cols = []
175
+
176
+ for sensor in self.sensor_cols:
177
+ #logger.info(f"Predicting {sensor}")
178
+ if sensor not in dataframe.columns:
179
+ # logger.warn(f"\tOutliers prediction: {sensor} not in data columns")
180
+ continue
181
+ model = self.models[sensor]
182
+ feature_cols = self.feature_map[sensor]
183
+
184
+ X = df[feature_cols]
185
+
186
+ labels = model.predict(X)
187
+ scores = model.decision_function(X)
188
+
189
+ prefix = f"{sensor}"
190
+
191
+ cols = [
192
+ f"{prefix}_LABEL",
193
+ f"{prefix}_SCORE",
194
+ f"{prefix}_OUTL",
195
+ ]
196
+
197
+ dataframe = dataframe.drop(columns=cols, errors="ignore")
198
+
199
+ new_cols = pd.DataFrame({
200
+ f"{prefix}_LABEL": labels,
201
+ f"{prefix}_SCORE": scores,
202
+ f"{prefix}_OUTL": (labels == -1).astype(np.float64),
203
+ }, index=df.index)
204
+
205
+ all_new_cols.append(new_cols)
206
+
207
+ dataframe = pd.concat([dataframe] + all_new_cols, axis=1)
208
+ dataframe = dataframe.copy()
209
+
210
+ return dataframe