apmtools 0.2.46__tar.gz → 0.2.48__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.
- {apmtools-0.2.46 → apmtools-0.2.48}/PKG-INFO +1 -1
- {apmtools-0.2.46 → apmtools-0.2.48}/pyproject.toml +1 -1
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/classes.py +21 -40
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/appearance.py +3 -2
- apmtools-0.2.48/src/apmtools/shiny/color_editor.py +64 -0
- apmtools-0.2.48/src/apmtools/shiny/elements.py +96 -0
- apmtools-0.2.48/src/apmtools/shiny/histogram_multi.py +32 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/matplotlib_renderer.py +147 -19
- apmtools-0.2.48/src/apmtools/shiny/multi_axis.py +150 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/plotly_renderer.py +32 -5
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/ui.py +577 -97
- apmtools-0.2.48/src/apmtools/units.py +190 -0
- apmtools-0.2.46/src/apmtools/units.py +0 -390
- {apmtools-0.2.46 → apmtools-0.2.48}/.gitignore +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/LICENSE +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/README.md +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/columns.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/config.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/data.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/export.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/multi_y.py +0 -0
- {apmtools-0.2.46 → apmtools-0.2.48}/src/apmtools/shiny/summary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apmtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.48
|
|
4
4
|
Summary: A collection of tools for processing air pollution monitoring data
|
|
5
5
|
Project-URL: Homepage, https://github.com/federlorenz/apmtools
|
|
6
6
|
Author-email: Federico Lorenzetti <lorenzetti.federico@gmail.com>
|
|
@@ -574,7 +574,7 @@ class Dataset(DictionaryPlus):
|
|
|
574
574
|
self.scan_folder(directory=f"{directory}{j}/", levels=levels,
|
|
575
575
|
level=level+1, monitor=monitor, levels_dict=levels_dict, utc_timezone_shift=utc_timezone_shift, interpolate=interpolate, add_m=add_m)
|
|
576
576
|
|
|
577
|
-
def save_summary(self, save_csv=True, filename="./",
|
|
577
|
+
def save_summary(self, save_csv=True, filename="./", all_variables=False, merged=True):
|
|
578
578
|
types_in = list(set(type(v) for v in self.values()))
|
|
579
579
|
|
|
580
580
|
def match_class(x):
|
|
@@ -619,13 +619,9 @@ class Dataset(DictionaryPlus):
|
|
|
619
619
|
"start", "end", "length", "datetime","timedelta"]
|
|
620
620
|
columns2 = list(set(d.meta()).difference(
|
|
621
621
|
set(["filter", "header", "parameters"])))
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
columns3 = ["pm25"]+columns["upas"] if columns["upas"] != "all" else ["pm25"]+list(
|
|
626
|
-
d.set_attrib("columns"))
|
|
627
|
-
columns4 = ["pm25correctedrh",
|
|
628
|
-
"pm25correctedrhandgrav", "filterid", "grav_not", "grav"]
|
|
622
|
+
columns3 = ["upas_pm25"] if not all_variables else ["upas_"+i for i in list(d.set_attrib("columns"))]+["upas_pm25"]
|
|
623
|
+
columns4 = ["upas_pm25_RHcorrected",
|
|
624
|
+
"upas_pm25_RHgravcorrected", "upas_filterid", "upas_gravimetric", "upas_gravimetric_blankcorrected"]
|
|
629
625
|
buffer = StringIO()
|
|
630
626
|
with buffer as file:
|
|
631
627
|
df = writer(file, delimiter=",")
|
|
@@ -658,15 +654,13 @@ class Dataset(DictionaryPlus):
|
|
|
658
654
|
app.append(v.m[m] if v.m[m] != None else "")
|
|
659
655
|
else:
|
|
660
656
|
app.append("")
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
else:
|
|
669
|
-
app.append(None)
|
|
657
|
+
|
|
658
|
+
for col in columns3[:-1]:
|
|
659
|
+
if col[5:] in v.columns:
|
|
660
|
+
app.append(v[col[5:]].iloc[i])
|
|
661
|
+
else:
|
|
662
|
+
app.append(None)
|
|
663
|
+
app.append(v["PM2_5MC"].iloc[i])
|
|
670
664
|
xx = (a*v["PM2_5MC"].iloc[i])+(b*v["AtmoRH"].iloc[i])+c
|
|
671
665
|
app.append(xx)
|
|
672
666
|
if grav != "":
|
|
@@ -695,7 +689,7 @@ class Dataset(DictionaryPlus):
|
|
|
695
689
|
columns1 = ["identifier", "monitor",
|
|
696
690
|
"start", "end", "length", "datetime", "timedelta"]
|
|
697
691
|
columns2 = list(set(d.meta()))
|
|
698
|
-
columns3 = ["
|
|
692
|
+
columns3 = ["lascar_CO(ppm)"]
|
|
699
693
|
|
|
700
694
|
buffer = StringIO()
|
|
701
695
|
with buffer as file:
|
|
@@ -743,9 +737,9 @@ class Dataset(DictionaryPlus):
|
|
|
743
737
|
columns1 = ["identifier", "monitor",
|
|
744
738
|
"start", "end", "length", "datetime", "timedelta"]
|
|
745
739
|
columns2 = list(set(d.meta()))
|
|
746
|
-
columns3 = ["
|
|
747
|
-
|
|
748
|
-
columns4 = ["
|
|
740
|
+
columns3 = ["lcm_sensorName", "lcm_deviceId", "lcm_resolution",
|
|
741
|
+
"lcm_PM2.5", "lcm_PM10", "lcm_CO2", "lcm_NO2", "lcm_Temperature", "lcm_Humidity"] if all_variables else ["lcm_PM2.5"]
|
|
742
|
+
columns4 = ["lcm_PM2.5correctedrh"]
|
|
749
743
|
|
|
750
744
|
buffer = StringIO()
|
|
751
745
|
with buffer as file:
|
|
@@ -774,7 +768,7 @@ class Dataset(DictionaryPlus):
|
|
|
774
768
|
else:
|
|
775
769
|
app.append("")
|
|
776
770
|
for m in columns3:
|
|
777
|
-
app.append(v[m].iloc[i])
|
|
771
|
+
app.append(v[m[4:]].iloc[i])
|
|
778
772
|
xx = (a*v["PM2.5"].iloc[i])+(b*v["Humidity"].iloc[i])+c
|
|
779
773
|
app.append(xx)
|
|
780
774
|
df.writerow(list(map(str, app)))
|
|
@@ -793,13 +787,8 @@ class Dataset(DictionaryPlus):
|
|
|
793
787
|
columns1 = ["identifier", "monitor",
|
|
794
788
|
"start", "end", "length", "datetime", "timedelta"]
|
|
795
789
|
columns2 = list(set(d.meta()))
|
|
796
|
-
columns3 = ["
|
|
797
|
-
columns4 = []
|
|
798
|
-
if "purple" not in columns.keys():
|
|
799
|
-
columns4 = []
|
|
800
|
-
else:
|
|
801
|
-
columns4 = columns["purple"] if columns["purple"] != "all" else list(
|
|
802
|
-
d.set_attrib("columns"))
|
|
790
|
+
columns3 = ["purple_pm25", "purple_pm25_RHcorrected"]
|
|
791
|
+
columns4 = [] if not all_variables else ["purple_"+i for i in list(d.set_attrib("columns"))]
|
|
803
792
|
buffer = StringIO()
|
|
804
793
|
with buffer as file:
|
|
805
794
|
df = writer(file, delimiter=",")
|
|
@@ -829,17 +818,9 @@ class Dataset(DictionaryPlus):
|
|
|
829
818
|
app.append(
|
|
830
819
|
((v["pm2_5_cf_1"]+v["pm2_5_cf_1_b"])/2).iloc[i])
|
|
831
820
|
app.append(v["pm_adj"].iloc[i])
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if col in v.columns:
|
|
836
|
-
app.append(v[col].iloc[i])
|
|
837
|
-
else:
|
|
838
|
-
app.append(None)
|
|
839
|
-
else:
|
|
840
|
-
for col in columns["purple"]:
|
|
841
|
-
if col in v.columns:
|
|
842
|
-
app.append(v[col].iloc[i])
|
|
821
|
+
for col in columns4:
|
|
822
|
+
if col[6:] in v.columns:
|
|
823
|
+
app.append(v[col[6:]].iloc[i])
|
|
843
824
|
else:
|
|
844
825
|
app.append(None)
|
|
845
826
|
df.writerow(list(map(str, app)))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Figure labels and appearance controls."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
+
import re
|
|
3
4
|
import threading
|
|
4
5
|
from datetime import date, datetime, time, timedelta
|
|
5
6
|
from collections.abc import Sequence
|
|
@@ -38,7 +39,7 @@ def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
|
|
|
38
39
|
elif plot_type in {"box", "bar"}:
|
|
39
40
|
categories = category_columns or [x_column]
|
|
40
41
|
fig.update_xaxes(title_text=" | ".join(_axis_label(c) for c in categories))
|
|
41
|
-
fig.update_yaxes(title_text=_axis_label(y_columns
|
|
42
|
+
fig.update_yaxes(title_text="<br>".join(_axis_label(y) for y in y_columns))
|
|
42
43
|
elif plot_type == "line" and len(y_columns) > 1:
|
|
43
44
|
fig.update_xaxes(title_text=_axis_label(x_column))
|
|
44
45
|
fig.update_yaxes(title_text="<br>".join(_axis_label(y) for y in y_columns))
|
|
@@ -59,7 +60,7 @@ def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
|
|
|
59
60
|
def _parse_label_map(value):
|
|
60
61
|
"""Parse semicolon-separated original=replacement entries."""
|
|
61
62
|
result = {}
|
|
62
|
-
for entry in (value or "")
|
|
63
|
+
for entry in re.split(r"[;\r\n]+", value or ""):
|
|
63
64
|
if "=" in entry:
|
|
64
65
|
key, replacement = entry.split("=", 1)
|
|
65
66
|
if key.strip() and replacement.strip():
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Stable colour-editor keys for the actual prepared plotting data."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
_HEX = re.compile(r"#[0-9a-fA-F]{6}\Z")
|
|
7
|
+
|
|
8
|
+
def valid_hex_color(value):
|
|
9
|
+
return isinstance(value, str) and _HEX.fullmatch(value.strip()) is not None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def active_color_keys(spec, *, mode="group", panel_by=()):
|
|
13
|
+
"""Return colour dimensions with nonempty plotted observations.
|
|
14
|
+
|
|
15
|
+
Faceting removes its grouping columns. For multi-Y plots, 'both' uses
|
|
16
|
+
combinations, while group/variable/none use the corresponding colour
|
|
17
|
+
dimension (the same keys accepted by multi_y_styles).
|
|
18
|
+
"""
|
|
19
|
+
if "error" in spec or spec["data"].empty:
|
|
20
|
+
return ()
|
|
21
|
+
if spec.get("hist_variables"):
|
|
22
|
+
return tuple(dict.fromkeys(spec["data"].loc[
|
|
23
|
+
spec["data"][spec["x"]].notna(), "__hist_series"].astype(str)))
|
|
24
|
+
from .matplotlib_renderer import _panel_partitions
|
|
25
|
+
from .multi_axis import records
|
|
26
|
+
parts = (_panel_partitions(spec, panel_by) if panel_by
|
|
27
|
+
else [(None, spec["data"], spec["groups"])])
|
|
28
|
+
keys = []
|
|
29
|
+
for _, frame, remaining in parts:
|
|
30
|
+
if frame.empty:
|
|
31
|
+
continue
|
|
32
|
+
sub = {**spec, "data": frame, "groups": remaining,
|
|
33
|
+
"_original_groups": spec["groups"]} if panel_by else {**spec, "data": frame, "groups": remaining}
|
|
34
|
+
if spec.get("multi_axis"):
|
|
35
|
+
pairs = [(str(y), str(g) if remaining else None)
|
|
36
|
+
for _, g, y, _, _, _ in records(sub)]
|
|
37
|
+
else:
|
|
38
|
+
ys = list(sub["ys"])
|
|
39
|
+
groups = (list(pd.unique(frame["__plot_group"].astype(str)))
|
|
40
|
+
if remaining and "__plot_group" in frame else [None])
|
|
41
|
+
pairs = []
|
|
42
|
+
for g in groups:
|
|
43
|
+
rows = (frame.loc[frame["__plot_group"].astype(str) == g]
|
|
44
|
+
if g is not None else frame)
|
|
45
|
+
for y in ys:
|
|
46
|
+
if y in rows and rows[y].notna().any():
|
|
47
|
+
pairs.append((str(y), g))
|
|
48
|
+
multi = bool(spec.get("multi_axis") or len(sub["ys"]) > 1)
|
|
49
|
+
for y, g in pairs:
|
|
50
|
+
if not multi or mode in ("group", "none"):
|
|
51
|
+
key = g if g is not None else y
|
|
52
|
+
elif mode == "variable":
|
|
53
|
+
key = y
|
|
54
|
+
else:
|
|
55
|
+
key = f"{g}, {y}" if g is not None else y
|
|
56
|
+
if key not in keys and key != "All observations":
|
|
57
|
+
keys.append(key)
|
|
58
|
+
return tuple(keys)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def color_defaults(spec, keys, palette="Default"):
|
|
62
|
+
from .config import _MPL_PALETTES
|
|
63
|
+
values = _MPL_PALETTES.get(palette, _MPL_PALETTES["Default"])
|
|
64
|
+
return {key: values[i % len(values)] for i, key in enumerate(keys)}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Selectable plotted elements, independent of the rendering backend."""
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _group_labels(spec):
|
|
6
|
+
frame = spec['data']
|
|
7
|
+
if spec.get('groups') and '__plot_group' in frame:
|
|
8
|
+
return frame['__plot_group'].astype(str)
|
|
9
|
+
return pd.Series('All observations', index=frame.index)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def element_keys(spec):
|
|
13
|
+
"""Selectable element identifiers with at least one valid plotted value.
|
|
14
|
+
|
|
15
|
+
Elements with exclusively missing observations are not selectable, rather
|
|
16
|
+
than merely being unchecked by default.
|
|
17
|
+
"""
|
|
18
|
+
return default_visible_keys(spec)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def filter_elements(spec, selected):
|
|
22
|
+
"""Remove hidden elements without changing the underlying Summary or filters.
|
|
23
|
+
|
|
24
|
+
For multi-Y lines, individual group/variable combinations are removed by
|
|
25
|
+
masking only their Y values. Panels with no remaining values disappear.
|
|
26
|
+
"""
|
|
27
|
+
if 'error' in spec or selected is None:
|
|
28
|
+
return spec
|
|
29
|
+
allowed = set(selected)
|
|
30
|
+
if spec.get('hist_variables'):
|
|
31
|
+
frame = spec['data'].loc[spec['data']['__hist_series'].isin(allowed)].copy()
|
|
32
|
+
return {**spec, 'data': frame, '_visible_elements': allowed}
|
|
33
|
+
if spec.get('multi_axis'):
|
|
34
|
+
from .multi_axis import records
|
|
35
|
+
filtered = {**spec, '_visible_elements': allowed}
|
|
36
|
+
keep = pd.Index([])
|
|
37
|
+
for _, _, _, _, _, rows in records(filtered):
|
|
38
|
+
keep = keep.union(rows.index, sort=False)
|
|
39
|
+
return {**filtered, 'data': spec['data'].loc[spec['data'].index.isin(keep)].copy()}
|
|
40
|
+
frame = spec['data'].copy()
|
|
41
|
+
kind = spec['kind']
|
|
42
|
+
groups = _group_labels(spec)
|
|
43
|
+
if kind == 'line':
|
|
44
|
+
keep_any = pd.Series(False, index=frame.index)
|
|
45
|
+
for y in spec['ys']:
|
|
46
|
+
keys = groups.map(lambda g: f'{g} | {y}' if len(spec['ys']) > 1 else g)
|
|
47
|
+
permitted = keys.isin(allowed)
|
|
48
|
+
frame.loc[~permitted, y] = float('nan')
|
|
49
|
+
keep_any |= permitted & frame[y].notna()
|
|
50
|
+
frame = frame.loc[keep_any].copy()
|
|
51
|
+
elif kind in ('box', 'bar'):
|
|
52
|
+
keys = [f'{c} | {g}' if spec['groups'] else c
|
|
53
|
+
for c, g in zip(frame[spec['x']].astype(str), groups)]
|
|
54
|
+
frame = frame.loc[pd.Series(keys, index=frame.index).isin(allowed)].copy()
|
|
55
|
+
else:
|
|
56
|
+
frame = frame.loc[groups.isin(allowed)].copy()
|
|
57
|
+
return {**spec, 'data': frame, '_visible_elements': allowed}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def default_visible_keys(spec):
|
|
61
|
+
"""Elements with at least one nonmissing plotted value (not merely rows)."""
|
|
62
|
+
if 'error' in spec:
|
|
63
|
+
return []
|
|
64
|
+
if spec.get('multi_axis'):
|
|
65
|
+
from .multi_axis import all_keys
|
|
66
|
+
return all_keys(spec, nonmissing=True)
|
|
67
|
+
frame, kind, x = spec['data'], spec['kind'], spec['x']
|
|
68
|
+
if spec.get('hist_variables'):
|
|
69
|
+
return list(dict.fromkeys(frame.loc[frame[x].notna(), '__hist_series'].astype(str)))
|
|
70
|
+
groups = _group_labels(spec)
|
|
71
|
+
if kind == 'line':
|
|
72
|
+
return list(dict.fromkeys(
|
|
73
|
+
f'{group} | {y}' if len(spec['ys']) > 1 else group
|
|
74
|
+
for y in spec['ys'] if y in frame
|
|
75
|
+
for group in groups[frame[y].notna()]
|
|
76
|
+
))
|
|
77
|
+
if kind in ('box', 'bar'):
|
|
78
|
+
y = next((y for y in spec['ys'] if y in frame), None)
|
|
79
|
+
if y is None:
|
|
80
|
+
return []
|
|
81
|
+
valid = frame[y].notna()
|
|
82
|
+
return list(dict.fromkeys(
|
|
83
|
+
f'{category} | {group}' if spec['groups'] else str(category)
|
|
84
|
+
for category, group in zip(frame.loc[valid, x].astype(str), groups.loc[valid])
|
|
85
|
+
))
|
|
86
|
+
# Scatter requires both coordinates; histogram requires its numeric x.
|
|
87
|
+
if kind == 'scatter':
|
|
88
|
+
valid = frame[x].notna()
|
|
89
|
+
for y in spec['ys']:
|
|
90
|
+
if y in frame:
|
|
91
|
+
valid &= frame[y].notna()
|
|
92
|
+
else:
|
|
93
|
+
valid = frame[x].notna()
|
|
94
|
+
return list(dict.fromkeys(groups.loc[valid]))
|
|
95
|
+
|
|
96
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Long-format data preparation for overlaid multi-variable histograms."""
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from .data import _composite_labels
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def histogram_long_data(frame, variables, *, group_columns=()):
|
|
7
|
+
"""Create one histogram series per variable/group using a shared numeric axis.
|
|
8
|
+
|
|
9
|
+
Non-histogram columns are retained so faceting, export, and filtering work.
|
|
10
|
+
"""
|
|
11
|
+
variables = list(dict.fromkeys(variables))
|
|
12
|
+
if not variables:
|
|
13
|
+
raise ValueError("At least one histogram variable is required")
|
|
14
|
+
missing = [name for name in variables if name not in frame]
|
|
15
|
+
if missing:
|
|
16
|
+
raise KeyError(f"Missing histogram variables: {missing}")
|
|
17
|
+
groups = list(group_columns)
|
|
18
|
+
for col in groups:
|
|
19
|
+
if col not in frame:
|
|
20
|
+
raise KeyError(col)
|
|
21
|
+
chunks = []
|
|
22
|
+
for name in variables:
|
|
23
|
+
part = frame.copy()
|
|
24
|
+
part["__hist_value"] = pd.to_numeric(part[name], errors="coerce")
|
|
25
|
+
part["__hist_variable"] = name
|
|
26
|
+
part["__hist_series"] = (name + " | " + _composite_labels(part, groups)
|
|
27
|
+
if groups else name)
|
|
28
|
+
part = part.loc[part["__hist_value"].notna()]
|
|
29
|
+
chunks.append(part)
|
|
30
|
+
if not chunks:
|
|
31
|
+
return frame.iloc[0:0].copy()
|
|
32
|
+
return pd.concat(chunks, ignore_index=True)
|