scdata 1.3.2__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 (99) hide show
  1. {scdata-1.3.2/scdata.egg-info → scdata-1.5.0}/PKG-INFO +20 -10
  2. {scdata-1.3.2 → scdata-1.5.0}/requirements.txt +3 -12
  3. {scdata-1.3.2 → scdata-1.5.0}/scdata/__init__.py +1 -1
  4. {scdata-1.3.2 → scdata-1.5.0}/scdata/_config/config.py +18 -78
  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.5.0/scdata/device/device.py +1136 -0
  11. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/__init__.py +2 -3
  12. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/alphasense.py +335 -29
  13. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/error_codes.py +1 -0
  14. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/params.py +3 -2
  15. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/timeseries.py +101 -19
  16. {scdata-1.3.2 → scdata-1.5.0}/scdata/io/device_api.py +0 -1
  17. {scdata-1.3.2 → scdata-1.5.0}/scdata/io/device_file.py +208 -63
  18. scdata-1.5.0/scdata/models/__init__.py +1 -0
  19. {scdata-1.3.2 → scdata-1.5.0}/scdata/models/models.py +19 -2
  20. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/__init__.py +15 -8
  21. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/box_plot.py +12 -6
  22. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/heatmap_iplot.py +14 -5
  23. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/heatmap_plot.py +13 -6
  24. scdata-1.3.2/scdata/test/plot/ts_uplot.py → scdata-1.5.0/scdata/plot/heatmap_uplot.py +16 -11
  25. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/maps.py +13 -11
  26. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_dispersion_grid.py +6 -4
  27. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_iplot.py +8 -5
  28. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_plot.py +15 -8
  29. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/target_diagram.py +19 -16
  30. scdata-1.3.2/scdata/test/plot/plot_tools.py → scdata-1.5.0/scdata/plot/tools.py +176 -25
  31. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dendrogram.py +7 -6
  32. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_grid.py +6 -4
  33. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_plot.py +8 -7
  34. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_uplot.py +10 -9
  35. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_iplot.py +15 -8
  36. scdata-1.5.0/scdata/plot/ts_panel.py +377 -0
  37. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_plot.py +16 -11
  38. {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_scatter.py +16 -11
  39. scdata-1.5.0/scdata/plot/ts_uplot.py +326 -0
  40. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/checks/checks.py +4 -6
  41. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/test.py +82 -28
  42. scdata-1.5.0/scdata/tools/merging.py +323 -0
  43. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/series.py +3 -2
  44. scdata-1.5.0/scdata/tools/tree.py +41 -0
  45. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/units.py +2 -0
  46. {scdata-1.3.2 → scdata-1.5.0/scdata.egg-info}/PKG-INFO +20 -10
  47. {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/SOURCES.txt +27 -19
  48. {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/requires.txt +19 -8
  49. {scdata-1.3.2 → scdata-1.5.0}/setup.py +20 -1
  50. scdata-1.3.2/scdata/device/device.py +0 -933
  51. scdata-1.3.2/scdata/device/process/baseline.py +0 -295
  52. scdata-1.3.2/scdata/models/__init__.py +0 -1
  53. {scdata-1.3.2 → scdata-1.5.0}/LICENSE +0 -0
  54. {scdata-1.3.2 → scdata-1.5.0}/MANIFEST.in +0 -0
  55. {scdata-1.3.2 → scdata-1.5.0}/README.md +0 -0
  56. {scdata-1.3.2 → scdata-1.5.0}/scdata/_config/__init__.py +0 -0
  57. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/__init__.py +0 -0
  58. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/plot/__init__.py +0 -0
  59. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/formulae.py +0 -0
  60. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/geoseries.py +0 -0
  61. {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/regression.py +0 -0
  62. {scdata-1.3.2 → scdata-1.5.0}/scdata/io/__init__.py +0 -0
  63. {scdata-1.3.2 → scdata-1.5.0}/scdata/io/model.py +0 -0
  64. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/__init__.py +0 -0
  65. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/checks/__init__.py +0 -0
  66. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/dispersion/__init__.py +0 -0
  67. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/dispersion/dispersion.py +0 -0
  68. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/__init__.py +0 -0
  69. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/templates/sc_template.html +0 -0
  70. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/to_file.py +0 -0
  71. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/__init__.py +0 -0
  72. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/combine.py +0 -0
  73. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/history.py +0 -0
  74. {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/prepare.py +0 -0
  75. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/__init__.py +0 -0
  76. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/cleaning.py +0 -0
  77. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/custom_logger.py +0 -0
  78. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/date.py +0 -0
  79. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/dictmerge.py +0 -0
  80. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/find.py +0 -0
  81. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/gets.py +0 -0
  82. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/interim/example.csv +0 -0
  83. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/interim/geodata.csv +0 -0
  84. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/lazy.py +0 -0
  85. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/location.py +0 -0
  86. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/report.py +0 -0
  87. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/stats.py +0 -0
  88. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/example_upload_1.json +0 -0
  89. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/example_zenodo_upload.yaml +0 -0
  90. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/report.pdf +0 -0
  91. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/url_check.py +0 -0
  92. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo.py +0 -0
  93. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/README.md +0 -0
  94. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_dataset.json +0 -0
  95. {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_publication.json +0 -0
  96. {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/dependency_links.txt +0 -0
  97. {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/not-zip-safe +0 -0
  98. {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/top_level.txt +0 -0
  99. {scdata-1.3.2 → 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.3.2
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
@@ -18,27 +18,36 @@ Classifier: Programming Language :: Python :: 3
18
18
  Requires-Python: >=3.9
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: branca~=0.4.0
22
- Requires-Dist: Flask~=2.2.2
23
- Requires-Dist: folium~=0.12.1
24
21
  Requires-Dist: geopy~=1.21.0
25
22
  Requires-Dist: Jinja2~=3.1.2
26
23
  Requires-Dist: matplotlib
27
24
  Requires-Dist: pandas~=2.2.2
28
- Requires-Dist: pydantic
29
- Requires-Dist: pytest
25
+ Requires-Dist: pydantic~=2.8
30
26
  Requires-Dist: PyYAML~=6.0.1
31
27
  Requires-Dist: requests
32
28
  Requires-Dist: scipy
33
29
  Requires-Dist: scikit-learn
34
30
  Requires-Dist: seaborn
35
- Requires-Dist: smartcitizen-connector
31
+ Requires-Dist: smartcitizen-connector>=1.5.0
36
32
  Requires-Dist: termcolor==1.1.0
37
- Requires-Dist: tqdm~=4.50.2
38
33
  Requires-Dist: timezonefinder~=6.1.9
39
34
  Requires-Dist: urllib3
40
- Requires-Dist: boto3
41
- Requires-Dist: awswrangler
35
+ Requires-Dist: Flask~=2.2.2
36
+ Provides-Extra: plotting
37
+ Requires-Dist: bokeh; extra == "plotting"
38
+ Requires-Dist: panel; extra == "plotting"
39
+ Requires-Dist: branca~=0.4.0; extra == "plotting"
40
+ Requires-Dist: folium~=0.12.1; extra == "plotting"
41
+ Requires-Dist: hvplot; extra == "plotting"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest; extra == "dev"
44
+ Requires-Dist: bokeh; extra == "dev"
45
+ Requires-Dist: panel; extra == "dev"
46
+ Requires-Dist: branca~=0.4.0; extra == "dev"
47
+ Requires-Dist: folium~=0.12.1; extra == "dev"
48
+ Requires-Dist: awswrangler; extra == "dev"
49
+ Requires-Dist: hvplot; extra == "dev"
50
+ Requires-Dist: boto3; extra == "dev"
42
51
  Dynamic: author
43
52
  Dynamic: classifier
44
53
  Dynamic: description
@@ -48,6 +57,7 @@ Dynamic: keywords
48
57
  Dynamic: license
49
58
  Dynamic: license-file
50
59
  Dynamic: project-url
60
+ Dynamic: provides-extra
51
61
  Dynamic: requires-dist
52
62
  Dynamic: requires-python
53
63
  Dynamic: summary
@@ -1,27 +1,18 @@
1
1
  # TODO To be updated?
2
- branca~=0.4.0
3
- # TODO Add once finished with file reports
4
- Flask~=2.2.2
5
- # TODO To be updated?
6
- folium~=0.12.1
7
- # TODO To be updated?
8
2
  geopy~=1.21.0
9
3
  # TODO To be updated?
10
4
  Jinja2~=3.1.2
11
5
  matplotlib
12
6
  pandas~=2.2.2
13
- pydantic
14
- pytest
7
+ pydantic~=2.8
15
8
  # TODO To be updated?
16
9
  PyYAML~=6.0.1
17
10
  requests
18
11
  scipy
19
12
  scikit-learn
20
13
  seaborn
21
- smartcitizen-connector
14
+ smartcitizen-connector>=1.5.0
22
15
  termcolor==1.1.0
23
- tqdm~=4.50.2
24
16
  timezonefinder~=6.1.9
25
17
  urllib3
26
- boto3
27
- awswrangler
18
+ Flask~=2.2.2
@@ -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.3.2'
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
 
@@ -29,10 +29,6 @@ class Config(object):
29
29
 
30
30
  # Framework option
31
31
  # For renderer plots and config files
32
- # Options:
33
- # - 'script': no plots in jupyter, updates config
34
- # - 'jupyterlab': for plots, updates config
35
- # - 'chupiflow': no plots in jupyter, does not update config
36
32
  framework = 'script'
37
33
 
38
34
  if 'IPython' in sys.modules: _ipython_avail = True
@@ -99,7 +95,6 @@ class Config(object):
99
95
  # f'{_base_postprocessing_url}blueprints/sck_15.{_default_file_type}',
100
96
  # f'{_base_postprocessing_url}blueprints/sck_20.{_default_file_type}',
101
97
  f'{_base_postprocessing_url}blueprints/sc_air.{_default_file_type}',
102
- f'{_base_postprocessing_url}blueprints/sc_water.{_default_file_type}',
103
98
  # f'{_base_postprocessing_url}blueprints/sck_21_sps30.{_default_file_type}',
104
99
  # f'{_base_postprocessing_url}blueprints/sck_21_sen5x.{_default_file_type}',
105
100
  # f'{_base_postprocessing_url}blueprints/sck_21_gps.{_default_file_type}',
@@ -410,73 +405,18 @@ class Config(object):
410
405
  ### ------------VALUES-CHECK---------------
411
406
  ### ---------------------------------------
412
407
 
413
- _default_sampling_rate = {
414
- 'AMS AS7731 - UVA': 1,
415
- 'AMS AS7731 - UVB': 1,
416
- 'AMS AS7731 - UVC': 1,
417
- 'LIGHT': 1,
418
- 'BATT': 1,
419
- 'NOISE_A': 1,
420
- 'SCD30_CO2': 1,
421
- 'SCD30_HUM': 1,
422
- 'SCD30_TEMP': 1,
423
- 'SD-card': 1,
424
- 'ST LPS33 - Barometric Pressure': 1,
425
- 'PRESS': 1,
426
- 'PMS5003_PM_1': 5,
427
- 'PMS5003_PM_25': 5,
428
- 'PMS5003_PM_10': 5,
429
- 'PMS5003_PN_03': 5,
430
- 'PMS5003_PN_03': 5,
431
- 'PMS5003_PN_05':5,
432
- 'PMS5003_PN_1':5,
433
- 'PMS5003_PN_10':5,
434
- 'PMS5003_PN_25':5,
435
- 'PMS5003_PN_5':5,
436
- 'SEN5X_HUM': 5,
437
- 'SEN5X_PM_1': 5,
438
- 'SEN5X_PM_10': 5,
439
- 'SEN5X_PM_25': 5,
440
- 'SEN5X_PM_40': 5,
441
- 'SEN5X_PN_05': 5,
442
- 'SEN5X_PN_1': 5,
443
- 'SEN5X_PN_10': 5,
444
- 'SEN5X_PN_25': 5,
445
- 'SEN5X_PN_40': 5,
446
- 'SEN5X_TPS': 5,
447
- 'SEN5X_TEMP': 5,
448
- 'SFA30_HCHO': 1,
449
- 'SFA30_HUM': 1,
450
- 'SFA30_TEMP': 1,
451
- 'ADC_48_0': 1,
452
- 'ADC_48_1': 1,
453
- 'ADC_48_2': 1,
454
- 'ADC_48_3': 1,
455
- 'ADC_49_0': 1,
456
- 'ADC_49_1': 1,
457
- 'ADC_49_2': 1,
458
- 'ADC_49_3': 1,
459
- 'CCS811_VOCS': 1,
460
- 'CCS811_ECO2': 1,
461
- 'HUM': 1,
462
- 'TEMP': 1,
463
- 'RSSI': 1,
464
- 'NO2': 1,
465
- 'O3': 1,
466
- 'AS_TEMP': 1,
467
- 'AS_PH': 1,
468
- 'AS_COND': 1,
469
- 'AS_DO_SAT': 1,
470
- 'AS_DO': 1
471
- }
408
+ _default_gap_size_minutes = 5
472
409
 
473
- _default_unplausible_values = {
410
+ # TODO - Review limits
411
+ _default_implausible_values = {
412
+ 'CCS811_VOCS': [0, 30000],
413
+ 'CCS811_ECO2': [400, 30000],
474
414
  'NOISE_A': [20, 99],
475
- 'SCD30_CO2': [300, 2000],
415
+ 'SCD30_CO2': [300, 9500], # Sensor limit is 10000ppm
476
416
  'SCD30_HUM': [20, 99],
477
417
  'SCD30_TEMP': [-20, 50],
478
418
  'BATT': [0, 100],
479
- 'ST LPS33 - Barometric Pressure': [50, 110],
419
+ 'LPS33_PRESS': [50, 110],
480
420
  'PRESS': [50, 110],
481
421
  'PMS5003_PM_1': [0, 500],
482
422
  'PMS5003_PM_25': [0, 500],
@@ -487,19 +427,19 @@ class Config(object):
487
427
  'SEN5X_PM_25': [0, 500],
488
428
  'SEN5X_PM_40': [0, 500],
489
429
  'SEN5X_TEMP': [-20, 50],
490
- 'SFA30_HCHO': [00, 1000],
430
+ 'SFA30_HCHO': [0, 4500], # Sensor saturates at 5000ppb, standard output is at 1000ppb
491
431
  'SFA30_HUM': [20, 99],
492
432
  'SFA30_TEMP': [-20, 50],
493
- 'ADC_48_0': [0, 3],
494
- 'ADC_48_1': [0, 3],
495
- 'ADC_48_2': [0, 3],
496
- 'ADC_48_3': [0, 3],
497
- 'ADC_49_0': [0, 3],
498
- 'ADC_49_1': [0, 3],
499
- 'ADC_49_2': [0, 3],
500
- '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],
501
441
  'HUM': [20, 99],
502
- 'TEMP': [-20, 50],
442
+ 'TEMP': [-40, 60],
503
443
  'NO2': [0, 1000],
504
444
  'O3': [0, 1000],
505
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