apmtools 0.2.7__tar.gz → 0.2.9__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.7 → apmtools-0.2.9}/PKG-INFO +1 -1
- {apmtools-0.2.7 → apmtools-0.2.9}/pyproject.toml +1 -1
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/classes.py +44 -47
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/shiny/summary.py +126 -34
- {apmtools-0.2.7 → apmtools-0.2.9}/.gitignore +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/LICENSE +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/README.md +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.7 → apmtools-0.2.9}/src/apmtools/units.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.009
|
|
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>
|
|
@@ -884,60 +884,57 @@ class Summary(pd.DataFrame):
|
|
|
884
884
|
return self
|
|
885
885
|
|
|
886
886
|
def subset(self, filter_dict={}, filter_style='all'):
|
|
887
|
+
"""Return rows matching values specified in ``filter_dict``.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
filter_dict : dict
|
|
892
|
+
Mapping of column names to iterables of accepted values.
|
|
893
|
+
filter_style : {'all', 'any', 'negative all', 'negative any'}
|
|
894
|
+
``all`` keeps rows matching every supplied condition; ``any`` keeps
|
|
895
|
+
rows matching at least one condition. ``negative all`` keeps rows
|
|
896
|
+
that match none of the supplied conditions, and ``negative any``
|
|
897
|
+
keeps rows that do not match any individual condition.
|
|
898
|
+
|
|
899
|
+
Notes
|
|
900
|
+
-----
|
|
901
|
+
Filtering is performed positionally rather than assuming that the
|
|
902
|
+
DataFrame index is ``0..n-1``. This is important because ``Summary``
|
|
903
|
+
objects commonly acquire non-contiguous indices after previous
|
|
904
|
+
filtering operations.
|
|
887
905
|
"""
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
attrib is an attribute of the elements of dictionary, and attrib_value is a list
|
|
891
|
-
of the values of such attrib that the elements of returned dictionary can have
|
|
892
|
-
specify filter_style='all' if all conditions should be met to be included in the return dictionary, specify filter_style='any' for including when any condition is met. Default is 'all'.
|
|
893
|
-
"""
|
|
894
|
-
if type(filter_dict) != type(dict()):
|
|
906
|
+
|
|
907
|
+
if not isinstance(filter_dict, dict):
|
|
895
908
|
print("subset function error: type filter_dict should be dict")
|
|
896
909
|
return
|
|
910
|
+
|
|
897
911
|
return_dict = copy.deepcopy(self)
|
|
898
912
|
a = {}
|
|
899
913
|
|
|
900
|
-
if filter_style
|
|
901
|
-
|
|
902
|
-
for i, j in filter_dict.items():
|
|
903
|
-
if (i in return_dict.columns):
|
|
904
|
-
for z in range(len(return_dict)):
|
|
905
|
-
if return_dict.loc[z, i] in j:
|
|
906
|
-
filtering_list[z] = True
|
|
907
|
-
else:
|
|
908
|
-
filtering_list[z] = False
|
|
909
|
-
return_dict = return_dict.loc[filtering_list]
|
|
914
|
+
if filter_style not in {'all', 'any', 'negative all', 'negative any'}:
|
|
915
|
+
return return_dict
|
|
910
916
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
filtering_list = [True]*(len(return_dict))
|
|
933
|
-
for i, j in filter_dict.items():
|
|
934
|
-
if (i in return_dict.columns):
|
|
935
|
-
for z in range(len(return_dict)):
|
|
936
|
-
if return_dict.loc[z, i] in j:
|
|
937
|
-
filtering_list[z] = False
|
|
938
|
-
return_dict = return_dict.loc[filtering_list]
|
|
939
|
-
|
|
940
|
-
return return_dict
|
|
917
|
+
# Only conditions referring to existing columns can be evaluated.
|
|
918
|
+
conditions = []
|
|
919
|
+
for column, values in filter_dict.items():
|
|
920
|
+
if column in return_dict.columns:
|
|
921
|
+
conditions.append(return_dict[column].isin(values).to_numpy())
|
|
922
|
+
|
|
923
|
+
if not conditions:
|
|
924
|
+
return return_dict
|
|
925
|
+
|
|
926
|
+
conditions = pd.DataFrame(conditions, dtype=bool)
|
|
927
|
+
|
|
928
|
+
if filter_style == 'all':
|
|
929
|
+
mask = conditions.all(axis=0).to_numpy()
|
|
930
|
+
elif filter_style == 'any':
|
|
931
|
+
mask = conditions.any(axis=0).to_numpy()
|
|
932
|
+
elif filter_style == 'negative all':
|
|
933
|
+
mask = (~conditions).all(axis=0).to_numpy()
|
|
934
|
+
else: # negative any
|
|
935
|
+
mask = (~conditions).any(axis=0).to_numpy()
|
|
936
|
+
|
|
937
|
+
return return_dict.loc[mask]
|
|
941
938
|
|
|
942
939
|
def show(self, number=0, key=None):
|
|
943
940
|
"""
|
|
@@ -83,15 +83,22 @@ def _subset_choices(data: pd.DataFrame, column: str) -> tuple[dict[str, str], di
|
|
|
83
83
|
return choices, value_map
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _apply_filters(data,
|
|
87
|
-
"""Apply Summary.subset() and
|
|
86
|
+
def _apply_filters(data, subset_filters, datetime_start=None, datetime_end=None):
|
|
87
|
+
"""Apply multiple ``Summary.subset()`` filters and datetime filtering.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
subset_filters : dict
|
|
92
|
+
Mapping of column names to the original values that should be retained.
|
|
93
|
+
Conditions for different columns are combined with ``filter_style="all"``.
|
|
94
|
+
Values within one column are OR'd by :meth:`Summary.subset`.
|
|
95
|
+
"""
|
|
88
96
|
filtered = data
|
|
89
97
|
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
filtered = filtered.subset({subset_column: values})
|
|
98
|
+
if subset_filters:
|
|
99
|
+
# Summary.subset() handles non-contiguous DataFrame indices
|
|
100
|
+
# positionally, so no index manipulation is required here.
|
|
101
|
+
filtered = filtered.subset(subset_filters, filter_style="all")
|
|
95
102
|
|
|
96
103
|
if datetime_start or datetime_end:
|
|
97
104
|
filtered = filtered.dtfilter(
|
|
@@ -182,7 +189,7 @@ def create_summary_app(
|
|
|
182
189
|
raise ValueError(
|
|
183
190
|
f"Unknown plot_type {plot_type!r}. Choose one of: {', '.join(PLOT_TYPES)}")
|
|
184
191
|
|
|
185
|
-
subset_columns =
|
|
192
|
+
subset_columns = columns
|
|
186
193
|
|
|
187
194
|
app_ui = ui.page_sidebar(
|
|
188
195
|
ui.sidebar(
|
|
@@ -214,12 +221,12 @@ def create_summary_app(
|
|
|
214
221
|
),
|
|
215
222
|
ui.hr(),
|
|
216
223
|
ui.h5("Subset"),
|
|
217
|
-
ui.input_select("subset_column", "Column",
|
|
218
|
-
choices=subset_columns, selected="None"),
|
|
219
224
|
ui.input_selectize(
|
|
220
|
-
"
|
|
221
|
-
|
|
225
|
+
"subset_columns", "Columns", choices={c: c for c in subset_columns},
|
|
226
|
+
selected=[], multiple=True,
|
|
227
|
+
options={"placeholder": "Select one or more columns"},
|
|
222
228
|
),
|
|
229
|
+
ui.output_ui("subset_filters"),
|
|
223
230
|
ui.hr(),
|
|
224
231
|
ui.h5("Datetime filter"),
|
|
225
232
|
ui.input_text(
|
|
@@ -246,29 +253,59 @@ def create_summary_app(
|
|
|
246
253
|
)
|
|
247
254
|
|
|
248
255
|
def server(input, output, session):
|
|
249
|
-
@
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
@render.ui
|
|
257
|
+
def subset_filters():
|
|
258
|
+
"""Render one value selector for every selected subset column."""
|
|
259
|
+
selected_columns = list(input.subset_columns())
|
|
260
|
+
if not selected_columns:
|
|
261
|
+
return ui.help_text(
|
|
262
|
+
"Select one or more columns above to choose their values."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
controls = []
|
|
266
|
+
for i, selected_column in enumerate(selected_columns):
|
|
267
|
+
if selected_column not in column_map:
|
|
268
|
+
continue
|
|
255
269
|
choices, _ = _subset_choices(
|
|
256
270
|
summary, column_map[selected_column])
|
|
257
|
-
|
|
271
|
+
input_id = f"subset_values_{i}"
|
|
272
|
+
controls.append(
|
|
273
|
+
ui.input_selectize(
|
|
274
|
+
input_id,
|
|
275
|
+
f"{selected_column} values",
|
|
276
|
+
choices=choices,
|
|
277
|
+
selected=[],
|
|
278
|
+
multiple=True,
|
|
279
|
+
options={
|
|
280
|
+
"placeholder": f"Select {selected_column} values"},
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
return ui.div(*controls)
|
|
258
284
|
|
|
259
285
|
@reactive.calc
|
|
260
286
|
def filtered_summary():
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
selected_columns = list(input.subset_columns())
|
|
288
|
+
subset_filters = {}
|
|
289
|
+
|
|
290
|
+
for i, selected_column in enumerate(selected_columns):
|
|
291
|
+
if selected_column not in column_map:
|
|
292
|
+
continue
|
|
293
|
+
selected_keys = list(input[f"subset_values_{i}"]())
|
|
294
|
+
if not selected_keys:
|
|
295
|
+
# An explicitly selected column with no selected values
|
|
296
|
+
# should not remove all data; it simply contributes no
|
|
297
|
+
# filtering condition until values are chosen.
|
|
298
|
+
continue
|
|
299
|
+
_, value_map = _subset_choices(
|
|
300
|
+
summary, column_map[selected_column])
|
|
301
|
+
values = [value_map[key]
|
|
302
|
+
for key in selected_keys if key in value_map]
|
|
303
|
+
if values:
|
|
304
|
+
subset_filters[column_map[selected_column]] = values
|
|
305
|
+
|
|
263
306
|
start = input.datetime_start().strip()
|
|
264
307
|
end = input.datetime_end().strip()
|
|
265
|
-
return _apply_filters(
|
|
266
|
-
summary,
|
|
267
|
-
column_map[subset_column] if subset_column != "None" else "None",
|
|
268
|
-
subset_values,
|
|
269
|
-
start,
|
|
270
|
-
end,
|
|
271
|
-
)
|
|
308
|
+
return _apply_filters(summary, subset_filters, start, end)
|
|
272
309
|
|
|
273
310
|
@reactive.calc
|
|
274
311
|
def selected_data():
|
|
@@ -415,17 +452,72 @@ def create_summary_app(
|
|
|
415
452
|
facet_row="variable" if len(y_columns) > 1 else None, barmode="group",
|
|
416
453
|
)
|
|
417
454
|
else:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
455
|
+
# ``identifier`` identifies an individual input monitor. It
|
|
456
|
+
# must therefore be used as Plotly's line-group key even when
|
|
457
|
+
# it is not one of the user-selected colour/group columns.
|
|
458
|
+
# This prevents observations from two different monitors
|
|
459
|
+
# being connected by a line merely because they share the
|
|
460
|
+
# same selected group. The identifier is deliberately NOT
|
|
461
|
+
# used for colour, so monitors in the same selected group
|
|
462
|
+
# retain the same colour.
|
|
463
|
+
identifier_column = (
|
|
464
|
+
column_map["identifier"]
|
|
465
|
+
if "identifier" in column_map
|
|
466
|
+
else None
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if group_columns:
|
|
421
470
|
group_column = "__plot_group"
|
|
422
|
-
|
|
423
|
-
|
|
471
|
+
line_columns = [x_column, group_column, *y_columns]
|
|
472
|
+
if identifier_column is not None:
|
|
473
|
+
line_columns.append(identifier_column)
|
|
474
|
+
long_df = plot_df[line_columns].melt(
|
|
475
|
+
id_vars=[
|
|
476
|
+
x_column,
|
|
477
|
+
group_column,
|
|
478
|
+
*([identifier_column]
|
|
479
|
+
if identifier_column is not None else []),
|
|
480
|
+
],
|
|
481
|
+
var_name="variable",
|
|
482
|
+
value_name="value",
|
|
424
483
|
)
|
|
425
484
|
fig = px.line(
|
|
426
|
-
long_df,
|
|
485
|
+
long_df,
|
|
486
|
+
x=x_column,
|
|
487
|
+
y="value",
|
|
488
|
+
color=group_column,
|
|
489
|
+
line_group=identifier_column if identifier_column is not None else None,
|
|
427
490
|
facet_row="variable" if len(y_columns) > 1 else None,
|
|
428
491
|
)
|
|
492
|
+
elif identifier_column is not None:
|
|
493
|
+
# With no explicit grouping, retain the normal Y-variable
|
|
494
|
+
# colouring while using identifier to keep each monitor's
|
|
495
|
+
# time series separate.
|
|
496
|
+
if len(y_columns) == 1:
|
|
497
|
+
fig = px.line(
|
|
498
|
+
plot_df,
|
|
499
|
+
x=x_column,
|
|
500
|
+
y=y_columns[0],
|
|
501
|
+
line_group=identifier_column,
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
line_columns = [
|
|
505
|
+
x_column, identifier_column, *y_columns]
|
|
506
|
+
long_df = plot_df[line_columns].melt(
|
|
507
|
+
id_vars=[x_column, identifier_column],
|
|
508
|
+
var_name="variable",
|
|
509
|
+
value_name="value",
|
|
510
|
+
)
|
|
511
|
+
fig = px.line(
|
|
512
|
+
long_df,
|
|
513
|
+
x=x_column,
|
|
514
|
+
y="value",
|
|
515
|
+
color="variable",
|
|
516
|
+
line_group=identifier_column,
|
|
517
|
+
facet_row="variable",
|
|
518
|
+
)
|
|
519
|
+
else:
|
|
520
|
+
fig = px.line(plot_df, x=x_column, y=y_columns)
|
|
429
521
|
|
|
430
522
|
fig.update_layout(
|
|
431
523
|
title=input.title(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|