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.
- {scdata-1.3.2/scdata.egg-info → scdata-1.5.0}/PKG-INFO +20 -10
- {scdata-1.3.2 → scdata-1.5.0}/requirements.txt +3 -12
- {scdata-1.3.2 → scdata-1.5.0}/scdata/__init__.py +1 -1
- {scdata-1.3.2 → scdata-1.5.0}/scdata/_config/config.py +18 -78
- scdata-1.5.0/scdata/device/check/__init__.py +4 -0
- scdata-1.5.0/scdata/device/check/flats.py +61 -0
- scdata-1.5.0/scdata/device/check/gaps.py +94 -0
- scdata-1.5.0/scdata/device/check/implausible.py +25 -0
- scdata-1.5.0/scdata/device/check/outliers.py +210 -0
- scdata-1.5.0/scdata/device/device.py +1136 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/__init__.py +2 -3
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/alphasense.py +335 -29
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/error_codes.py +1 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/params.py +3 -2
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/timeseries.py +101 -19
- {scdata-1.3.2 → scdata-1.5.0}/scdata/io/device_api.py +0 -1
- {scdata-1.3.2 → scdata-1.5.0}/scdata/io/device_file.py +208 -63
- scdata-1.5.0/scdata/models/__init__.py +1 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/models/models.py +19 -2
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/__init__.py +15 -8
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/box_plot.py +12 -6
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/heatmap_iplot.py +14 -5
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/heatmap_plot.py +13 -6
- scdata-1.3.2/scdata/test/plot/ts_uplot.py → scdata-1.5.0/scdata/plot/heatmap_uplot.py +16 -11
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/maps.py +13 -11
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_dispersion_grid.py +6 -4
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_iplot.py +8 -5
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/scatter_plot.py +15 -8
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/target_diagram.py +19 -16
- scdata-1.3.2/scdata/test/plot/plot_tools.py → scdata-1.5.0/scdata/plot/tools.py +176 -25
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dendrogram.py +7 -6
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_grid.py +6 -4
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_plot.py +8 -7
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_dispersion_uplot.py +10 -9
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_iplot.py +15 -8
- scdata-1.5.0/scdata/plot/ts_panel.py +377 -0
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_plot.py +16 -11
- {scdata-1.3.2/scdata/test → scdata-1.5.0/scdata}/plot/ts_scatter.py +16 -11
- scdata-1.5.0/scdata/plot/ts_uplot.py +326 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/checks/checks.py +4 -6
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/test.py +82 -28
- scdata-1.5.0/scdata/tools/merging.py +323 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/series.py +3 -2
- scdata-1.5.0/scdata/tools/tree.py +41 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/units.py +2 -0
- {scdata-1.3.2 → scdata-1.5.0/scdata.egg-info}/PKG-INFO +20 -10
- {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/SOURCES.txt +27 -19
- {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/requires.txt +19 -8
- {scdata-1.3.2 → scdata-1.5.0}/setup.py +20 -1
- scdata-1.3.2/scdata/device/device.py +0 -933
- scdata-1.3.2/scdata/device/process/baseline.py +0 -295
- scdata-1.3.2/scdata/models/__init__.py +0 -1
- {scdata-1.3.2 → scdata-1.5.0}/LICENSE +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/MANIFEST.in +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/README.md +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/_config/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/plot/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/formulae.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/geoseries.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/device/process/regression.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/io/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/io/model.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/checks/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/dispersion/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/dispersion/dispersion.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/templates/sc_template.html +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/export/to_file.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/combine.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/history.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/test/tools/prepare.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/__init__.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/cleaning.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/custom_logger.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/date.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/dictmerge.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/find.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/gets.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/interim/example.csv +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/interim/geodata.csv +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/lazy.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/location.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/report.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/stats.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/example_upload_1.json +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/example_zenodo_upload.yaml +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/uploads/report.pdf +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/url_check.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo.py +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/README.md +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_dataset.json +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata/tools/zenodo_templates/template_zenodo_publication.json +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/dependency_links.txt +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/not-zip-safe +0 -0
- {scdata-1.3.2 → scdata-1.5.0}/scdata.egg-info/top_level.txt +0 -0
- {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
|
+
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:
|
|
41
|
-
|
|
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
|
-
|
|
27
|
-
awswrangler
|
|
18
|
+
Flask~=2.2.2
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
'
|
|
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': [
|
|
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,
|
|
494
|
-
'ADC_48_1': [0,
|
|
495
|
-
'ADC_48_2': [0,
|
|
496
|
-
'ADC_48_3': [0,
|
|
497
|
-
'ADC_49_0': [0,
|
|
498
|
-
'ADC_49_1': [0,
|
|
499
|
-
'ADC_49_2': [0,
|
|
500
|
-
'ADC_49_3': [0,
|
|
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': [-
|
|
442
|
+
'TEMP': [-40, 60],
|
|
503
443
|
'NO2': [0, 1000],
|
|
504
444
|
'O3': [0, 1000],
|
|
505
445
|
'AS_TEMP': [-20, 50],
|
|
@@ -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
|