apmtools 0.2.58__tar.gz → 0.2.59__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.58 → apmtools-0.2.59}/PKG-INFO +1 -1
- {apmtools-0.2.58 → apmtools-0.2.59}/pyproject.toml +1 -1
- apmtools-0.2.59/src/apmtools/shiny/quality_filters.py +76 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/quality_view.py +26 -4
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/tab_initialization.py +20 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/ui.py +157 -20
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/view_schema.py +1 -1
- {apmtools-0.2.58 → apmtools-0.2.59}/.gitignore +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/LICENSE +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/README.md +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/completeness.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/plot_app.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/plot_configs.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/appearance.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/color_editor.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/columns.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/config.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/data.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/derived_views.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/elements.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/export.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/filter_config.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/histogram_multi.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/inherited_source.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/legend_identity.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/matplotlib_renderer.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/multi_axis.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/multi_stat_plot.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/multi_y.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/profile_report.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/profiling.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/state.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/summary.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/text_editors.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/transformations.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/view_aggregation.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/view_controller.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/view_history.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/src/apmtools/shiny/workspace_controller.py +0 -0
- {apmtools-0.2.58 → apmtools-0.2.59}/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.59
|
|
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>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Reusable, Shiny-independent subset filtering for quality dashboards."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, time, timedelta
|
|
8
|
+
from .data import _parse_datetime_filter, _subset_choices
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def filter_quality_data(frame, settings):
|
|
12
|
+
"""Apply the same staged categorical, numeric and date filters as tables.
|
|
13
|
+
|
|
14
|
+
The configured selected values are Shiny selector *keys*, resolved against
|
|
15
|
+
the source dataframe. Missing numeric values remain unchanged, consistent
|
|
16
|
+
with the existing table filtering policy.
|
|
17
|
+
"""
|
|
18
|
+
current = frame.copy()
|
|
19
|
+
for step in settings.get('subset_chain', []):
|
|
20
|
+
column = step.get('column')
|
|
21
|
+
if column not in current.columns:
|
|
22
|
+
continue
|
|
23
|
+
_, value_map = _subset_choices(frame, column)
|
|
24
|
+
values = [value_map[key] for key in step.get('values', []) if key in value_map]
|
|
25
|
+
if not values:
|
|
26
|
+
continue
|
|
27
|
+
style = step.get('style', 'all')
|
|
28
|
+
if hasattr(current, 'subset'):
|
|
29
|
+
current = current.subset({column: values}, filter_style=style)
|
|
30
|
+
else:
|
|
31
|
+
matches = current[column].isin([v for v in values if v is not None])
|
|
32
|
+
if any(v is None for v in values):
|
|
33
|
+
matches |= current[column].isna()
|
|
34
|
+
if style in ('negative all', 'negative any'):
|
|
35
|
+
matches = ~matches
|
|
36
|
+
current = current.loc[matches].copy()
|
|
37
|
+
# Derived plot/table frames are often ordinary pandas DataFrames (without
|
|
38
|
+
# Summary.dtfilter). Filter their actual datetime column without assuming
|
|
39
|
+
# that the source column is literally named "datetime".
|
|
40
|
+
time_col = settings.get('time_col')
|
|
41
|
+
if time_col not in current.columns or not pd.api.types.is_datetime64_any_dtype(current[time_col]):
|
|
42
|
+
time_col = next((col for col in current.columns
|
|
43
|
+
if pd.api.types.is_datetime64_any_dtype(current[col])), None)
|
|
44
|
+
if time_col is not None:
|
|
45
|
+
timestamps = pd.to_datetime(current[time_col], errors='coerce')
|
|
46
|
+
mask = pd.Series(True, index=current.index)
|
|
47
|
+
for field, lower in (('datetime_start', True), ('datetime_end', False)):
|
|
48
|
+
kind, value = _parse_datetime_filter(settings.get(field))
|
|
49
|
+
if kind == 'date':
|
|
50
|
+
bound = datetime.combine(value, time.min)
|
|
51
|
+
if lower:
|
|
52
|
+
mask &= timestamps.ge(bound)
|
|
53
|
+
else:
|
|
54
|
+
mask &= timestamps.lt(bound + timedelta(days=1))
|
|
55
|
+
elif kind == 'datetime':
|
|
56
|
+
mask &= timestamps.ge(value) if lower else timestamps.lt(value)
|
|
57
|
+
elif kind == 'time':
|
|
58
|
+
times = timestamps.dt.time
|
|
59
|
+
mask &= times.ge(value) if lower else times.lt(value)
|
|
60
|
+
current = current.loc[mask.fillna(False)].copy()
|
|
61
|
+
for column in settings.get('numeric_subset_columns', []):
|
|
62
|
+
if column not in current.columns or not pd.api.types.is_numeric_dtype(current[column]):
|
|
63
|
+
continue
|
|
64
|
+
values = pd.to_numeric(current[column], errors='coerce')
|
|
65
|
+
for comparison in ('gt', 'lt'):
|
|
66
|
+
raw = settings.get('numeric_subset_bounds', {}).get(column, {}).get(comparison, '')
|
|
67
|
+
try:
|
|
68
|
+
bound = float(str(raw).strip())
|
|
69
|
+
except (ValueError, TypeError):
|
|
70
|
+
continue
|
|
71
|
+
if not np.isfinite(bound):
|
|
72
|
+
continue
|
|
73
|
+
mask = values.gt(bound) if comparison == 'gt' else values.lt(bound)
|
|
74
|
+
current = current.loc[mask.fillna(False) | values.isna()].copy()
|
|
75
|
+
values = values.loc[current.index]
|
|
76
|
+
return current
|
|
@@ -56,15 +56,37 @@ def selected_heatmap_variable(value_cols, selected):
|
|
|
56
56
|
return selected if selected in choices else (choices[0] if choices else None)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def quality_heatmap_data(report, variable):
|
|
60
|
-
"""
|
|
59
|
+
def quality_heatmap_data(report, variable, *, mode='coverage'):
|
|
60
|
+
"""Return interval-by-identifier heatmap values on a 0–1 scale.
|
|
61
|
+
|
|
62
|
+
``mode='coverage'`` shows the measured observed/expected fraction, without
|
|
63
|
+
reference to the completeness threshold. ``mode='threshold'`` is the
|
|
64
|
+
historical binary classification (1=complete, 0=incomplete).
|
|
65
|
+
``mode='threshold_coverage'`` shows 1 for bins meeting the threshold, and
|
|
66
|
+
the *actual* fraction for bins failing it. This retains the continuous
|
|
67
|
+
shades for incomplete bins without losing the threshold classification.
|
|
68
|
+
Undefined expected counts remain NaN in every mode.
|
|
69
|
+
"""
|
|
70
|
+
if mode not in ('coverage', 'threshold', 'threshold_coverage'):
|
|
71
|
+
raise ValueError("heatmap mode must be 'coverage', 'threshold', or 'threshold_coverage'")
|
|
61
72
|
if report.empty:
|
|
62
73
|
return pd.DataFrame()
|
|
63
74
|
key = report.columns[0]
|
|
64
|
-
portion = report.loc[report['variable'].eq(variable)
|
|
75
|
+
portion = report.loc[report['variable'].eq(variable)].copy()
|
|
65
76
|
if portion.empty:
|
|
66
77
|
return pd.DataFrame()
|
|
67
|
-
|
|
78
|
+
if mode == 'threshold':
|
|
79
|
+
portion['_heatmap_value'] = portion['is_complete'].astype(float).where(
|
|
80
|
+
portion['expected'].notna())
|
|
81
|
+
elif mode == 'threshold_coverage':
|
|
82
|
+
portion['_heatmap_value'] = portion['completeness'].where(
|
|
83
|
+
~portion['is_complete'].astype(bool), 1.0)
|
|
84
|
+
portion['_heatmap_value'] = portion['_heatmap_value'].where(
|
|
85
|
+
portion['expected'].notna())
|
|
86
|
+
else:
|
|
87
|
+
portion['_heatmap_value'] = portion['completeness']
|
|
88
|
+
return portion.pivot_table(index=key, columns='interval',
|
|
89
|
+
values='_heatmap_value', aggfunc='mean', dropna=False)
|
|
68
90
|
|
|
69
91
|
|
|
70
92
|
def missing_interval_runs(report):
|
|
@@ -66,6 +66,26 @@ class TabInitializer:
|
|
|
66
66
|
frame, steps = prepare_table_source(self.summary)
|
|
67
67
|
return TableInitialization(f'Table {number}', frame, None, steps)
|
|
68
68
|
|
|
69
|
+
def new_quality(self, selected, number):
|
|
70
|
+
"""Create a quality view from the current plot/table's transformed data.
|
|
71
|
+
|
|
72
|
+
The stored lineage replays the source transformation on restoration;
|
|
73
|
+
no potentially stale dataframe snapshots are retained in saved views.
|
|
74
|
+
"""
|
|
75
|
+
kind = self.workspace.kind(selected)
|
|
76
|
+
if kind == 'table':
|
|
77
|
+
frame, steps = prepare_table_source(
|
|
78
|
+
self.summary, parent_kind='table',
|
|
79
|
+
parent_steps=self.workspace.table_lineage.get(selected, []),
|
|
80
|
+
parent_settings=self.workspace.table_settings[selected]())
|
|
81
|
+
elif kind == 'plot':
|
|
82
|
+
frame, steps = prepare_table_source(
|
|
83
|
+
self.summary, parent_kind='plot',
|
|
84
|
+
parent_settings=self.workspace.plot_snapshots[selected]())
|
|
85
|
+
else:
|
|
86
|
+
frame, steps = prepare_table_source(self.summary)
|
|
87
|
+
return TableInitialization(f'Quality {number}', frame, {'source_steps': steps}, steps)
|
|
88
|
+
|
|
69
89
|
def restored_table(self, preset_id, number):
|
|
70
90
|
preset = self.views.preset(preset_id)
|
|
71
91
|
_source, settings = self.views.restore_table(preset_id)
|
|
@@ -15,6 +15,8 @@ from .config import VARIABLE_UNITS, PLOT_TYPES, FREQUENCIES, AGGREGATES, _MPL_PA
|
|
|
15
15
|
from .columns import _alphabetical, _categorical_columns, _datetime_columns, _default_group, _default_x, _normalise_y, _numeric_columns, _timedelta_columns
|
|
16
16
|
from .data import _aggregate, _apply_filters, _composite_labels, _subset_choices
|
|
17
17
|
from .view_aggregation import normalize_statistics
|
|
18
|
+
from .quality_filters import filter_quality_data
|
|
19
|
+
from .derived_views import prepare_table_source
|
|
18
20
|
from .quality_view import quality_report, quality_summary, quality_heatmap_data, missing_interval_runs, QUALITY_FREQUENCIES, selected_heatmap_variable
|
|
19
21
|
from .matplotlib_renderer import _axis_bounds_from_spec, _format_panel_bounds, _matplotlib_defaults, _matplotlib_direct_figure, _pad_axis_bounds, _panel_axis_bounds, _panel_partitions, _panel_trace_defaults
|
|
20
22
|
from .elements import element_keys, filter_elements, default_visible_keys
|
|
@@ -2033,16 +2035,54 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2033
2035
|
|
|
2034
2036
|
# Quality dashboards have independent controls and do not modify plot data.
|
|
2035
2037
|
@module.ui
|
|
2036
|
-
def quality_tab_ui(settings=None):
|
|
2038
|
+
def quality_tab_ui(frame, settings=None):
|
|
2037
2039
|
settings = settings or {}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2040
|
+
available = _alphabetical(frame.columns)
|
|
2041
|
+
numeric = _alphabetical(_numeric_columns(frame))
|
|
2042
|
+
id_choices = _alphabetical([c for c in available if c not in numeric])
|
|
2043
|
+
times = _datetime_columns(frame)
|
|
2044
|
+
selected_values = [v for v in settings.get("value_cols", numeric[:1]) if v in numeric]
|
|
2041
2045
|
selected_heatmap = selected_heatmap_variable(selected_values, settings.get("heatmap_variable"))
|
|
2046
|
+
stages = max(2, len(available) + 1)
|
|
2042
2047
|
return ui.div(
|
|
2043
2048
|
ui.layout_sidebar(
|
|
2044
2049
|
ui.sidebar(
|
|
2045
|
-
ui.
|
|
2050
|
+
ui.div(ui.navset_tab(
|
|
2051
|
+
ui.nav_panel("Filters",
|
|
2052
|
+
*[
|
|
2053
|
+
ui.div(
|
|
2054
|
+
ui.input_select(f"subset_column_{i}", f"Subset column {i + 1}",
|
|
2055
|
+
choices={"": "Select a column", **{c: c for c in available}},
|
|
2056
|
+
selected=(settings.get("subset_chain", [])[i].get("column", "") if i < len(settings.get("subset_chain", [])) else "")),
|
|
2057
|
+
ui.output_ui(f"subset_values_ui_{i}"),
|
|
2058
|
+
ui.input_select(f"subset_style_{i}", "Filter style",
|
|
2059
|
+
choices={k: k for k in ("all", "any", "negative all", "negative any")},
|
|
2060
|
+
selected=(settings.get("subset_chain", [])[i].get("style", "all") if i < len(settings.get("subset_chain", [])) else "all")),
|
|
2061
|
+
class_="mb-3",
|
|
2062
|
+
) if i == 0 else ui.panel_conditional(
|
|
2063
|
+
" && ".join(f"input.subset_column_{j} !== ''" for j in range(i)),
|
|
2064
|
+
ui.div(
|
|
2065
|
+
ui.input_select(f"subset_column_{i}", f"Subset column {i + 1}",
|
|
2066
|
+
choices={"": "Select a column", **{c: c for c in available}},
|
|
2067
|
+
selected=(settings.get("subset_chain", [])[i].get("column", "") if i < len(settings.get("subset_chain", [])) else "")),
|
|
2068
|
+
ui.output_ui(f"subset_values_ui_{i}"),
|
|
2069
|
+
ui.input_select(f"subset_style_{i}", "Filter style",
|
|
2070
|
+
choices={k: k for k in ("all", "any", "negative all", "negative any")},
|
|
2071
|
+
selected=(settings.get("subset_chain", [])[i].get("style", "all") if i < len(settings.get("subset_chain", [])) else "all")),
|
|
2072
|
+
class_="mb-3",
|
|
2073
|
+
),
|
|
2074
|
+
) for i in range(stages)
|
|
2075
|
+
],
|
|
2076
|
+
ui.input_selectize("numeric_subset_columns", "Numerical subset",
|
|
2077
|
+
choices={c: c for c in numeric}, multiple=True,
|
|
2078
|
+
selected=settings.get("numeric_subset_columns", [])),
|
|
2079
|
+
ui.output_ui("numeric_subset_bounds"),
|
|
2080
|
+
ui.input_text("datetime_start", "Start datetime", value=settings.get("datetime_start", ""),
|
|
2081
|
+
placeholder="YYYY-MM-DD, HH:MM:SS, or YYYY-MM-DD HH:MM:SS"),
|
|
2082
|
+
ui.input_text("datetime_end", "End datetime", value=settings.get("datetime_end", ""),
|
|
2083
|
+
placeholder="YYYY-MM-DD, HH:MM:SS, or YYYY-MM-DD HH:MM:SS"),
|
|
2084
|
+
),
|
|
2085
|
+
ui.nav_panel("Coverage analysis",
|
|
2046
2086
|
ui.input_select("id_col", "Monitor / identifier column",
|
|
2047
2087
|
choices=id_choices,
|
|
2048
2088
|
selected=settings.get("id_col", "identifier") if settings.get("id_col", "identifier") in id_choices else (id_choices[0] if id_choices else None)),
|
|
@@ -2050,7 +2090,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2050
2090
|
choices=times,
|
|
2051
2091
|
selected=settings.get("time_col", "datetime") if settings.get("time_col", "datetime") in times else (times[0] if times else None)),
|
|
2052
2092
|
ui.input_selectize("value_cols", "Measurements",
|
|
2053
|
-
choices=
|
|
2093
|
+
choices=numeric, multiple=True,
|
|
2054
2094
|
selected=selected_values),
|
|
2055
2095
|
ui.input_select("frequency", "Aggregation frequency",
|
|
2056
2096
|
choices={v: v.capitalize() for v in QUALITY_FREQUENCIES},
|
|
@@ -2069,6 +2109,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2069
2109
|
"Intervals outside the recording span cannot be inferred.",
|
|
2070
2110
|
class_="small text-muted"),
|
|
2071
2111
|
ui.download_button("download_quality_csv", "Export interval coverage CSV"),
|
|
2112
|
+
), id="quality_sidebar_tabs"), class_="summary-sidebar-tabs"),
|
|
2072
2113
|
title="Data quality", position="left", open="open", width="320px",
|
|
2073
2114
|
),
|
|
2074
2115
|
ui.div(
|
|
@@ -2087,8 +2128,93 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2087
2128
|
)
|
|
2088
2129
|
|
|
2089
2130
|
@module.server
|
|
2090
|
-
def quality_tab_server(input, output, session, settings=None):
|
|
2131
|
+
def quality_tab_server(input, output, session, frame, settings=None):
|
|
2091
2132
|
settings = settings or {}
|
|
2133
|
+
available = _alphabetical(frame.columns)
|
|
2134
|
+
numeric = _alphabetical(_numeric_columns(frame))
|
|
2135
|
+
stages = max(2, len(available) + 1)
|
|
2136
|
+
choice_cache = {}
|
|
2137
|
+
|
|
2138
|
+
def choices_for(name):
|
|
2139
|
+
if name not in choice_cache:
|
|
2140
|
+
choice_cache[name] = _subset_choices(frame, name)
|
|
2141
|
+
return choice_cache[name]
|
|
2142
|
+
|
|
2143
|
+
def make_values_ui(stage):
|
|
2144
|
+
def values_ui():
|
|
2145
|
+
try:
|
|
2146
|
+
name = input[f"subset_column_{stage}"]()
|
|
2147
|
+
except Exception:
|
|
2148
|
+
return ui.div()
|
|
2149
|
+
if name not in frame.columns:
|
|
2150
|
+
return ui.div()
|
|
2151
|
+
choices, _ = choices_for(name)
|
|
2152
|
+
selected = (settings.get("subset_chain", [])[stage].get("values", [])
|
|
2153
|
+
if stage < len(settings.get("subset_chain", [])) else [])
|
|
2154
|
+
return ui.input_selectize(f"subset_values_{stage}", f"{name} values",
|
|
2155
|
+
choices=choices, multiple=True, selected=selected)
|
|
2156
|
+
values_ui.__name__ = f"subset_values_ui_{stage}"
|
|
2157
|
+
return render.ui(values_ui)
|
|
2158
|
+
|
|
2159
|
+
for stage in range(stages):
|
|
2160
|
+
output(make_values_ui(stage))
|
|
2161
|
+
|
|
2162
|
+
@render.ui
|
|
2163
|
+
def numeric_subset_bounds():
|
|
2164
|
+
controls = []
|
|
2165
|
+
for name in list(input.numeric_subset_columns() or []):
|
|
2166
|
+
if name not in numeric:
|
|
2167
|
+
continue
|
|
2168
|
+
index = numeric.index(name)
|
|
2169
|
+
controls.append(ui.div(
|
|
2170
|
+
ui.tags.strong(name),
|
|
2171
|
+
ui.input_text(f"numeric_gt_{index}", "Greater than", placeholder="No lower bound",
|
|
2172
|
+
value=settings.get("numeric_subset_bounds", {}).get(name, {}).get("gt", "")),
|
|
2173
|
+
ui.input_text(f"numeric_lt_{index}", "Less than", placeholder="No upper bound",
|
|
2174
|
+
value=settings.get("numeric_subset_bounds", {}).get(name, {}).get("lt", "")),
|
|
2175
|
+
))
|
|
2176
|
+
return ui.div(*controls)
|
|
2177
|
+
|
|
2178
|
+
@reactive.calc
|
|
2179
|
+
def filter_settings():
|
|
2180
|
+
chain = []
|
|
2181
|
+
for stage in range(stages):
|
|
2182
|
+
try:
|
|
2183
|
+
column = input[f"subset_column_{stage}"]()
|
|
2184
|
+
except Exception:
|
|
2185
|
+
break
|
|
2186
|
+
if column not in frame.columns:
|
|
2187
|
+
break
|
|
2188
|
+
try:
|
|
2189
|
+
values = list(input[f"subset_values_{stage}"]() or [])
|
|
2190
|
+
except Exception:
|
|
2191
|
+
values = (settings.get("subset_chain", [])[stage].get("values", [])
|
|
2192
|
+
if stage < len(settings.get("subset_chain", [])) else [])
|
|
2193
|
+
try:
|
|
2194
|
+
style = input[f"subset_style_{stage}"]() or "all"
|
|
2195
|
+
except Exception:
|
|
2196
|
+
style = "all"
|
|
2197
|
+
chain.append({"column": column, "values": values, "style": style})
|
|
2198
|
+
bounds = {}
|
|
2199
|
+
for name in list(input.numeric_subset_columns() or []):
|
|
2200
|
+
if name not in numeric:
|
|
2201
|
+
continue
|
|
2202
|
+
index = numeric.index(name)
|
|
2203
|
+
bounds[name] = {}
|
|
2204
|
+
for suffix in ("gt", "lt"):
|
|
2205
|
+
try:
|
|
2206
|
+
bounds[name][suffix] = input[f"numeric_{suffix}_{index}"]() or ""
|
|
2207
|
+
except Exception:
|
|
2208
|
+
bounds[name][suffix] = ""
|
|
2209
|
+
return {"subset_chain": chain,
|
|
2210
|
+
"numeric_subset_columns": list(input.numeric_subset_columns() or []),
|
|
2211
|
+
"numeric_subset_bounds": bounds,
|
|
2212
|
+
"datetime_start": input.datetime_start() or "",
|
|
2213
|
+
"datetime_end": input.datetime_end() or ""}
|
|
2214
|
+
|
|
2215
|
+
@reactive.calc
|
|
2216
|
+
def filtered_data():
|
|
2217
|
+
return filter_quality_data(frame, {**filter_settings(), "time_col": input.time_col()})
|
|
2092
2218
|
|
|
2093
2219
|
@reactive.calc
|
|
2094
2220
|
def report_settings():
|
|
@@ -2116,7 +2242,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2116
2242
|
@reactive.calc
|
|
2117
2243
|
def quality_settings():
|
|
2118
2244
|
conf = report_settings()
|
|
2119
|
-
return {**conf, "heatmap_variable": selected_heatmap_variable(
|
|
2245
|
+
return {**conf, **filter_settings(), "source_steps": settings.get("source_steps", []), "heatmap_variable": selected_heatmap_variable(
|
|
2120
2246
|
conf["value_cols"], input.heatmap_variable())}
|
|
2121
2247
|
|
|
2122
2248
|
@reactive.calc
|
|
@@ -2124,7 +2250,10 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2124
2250
|
conf = report_settings()
|
|
2125
2251
|
if not conf["value_cols"] or not conf["id_col"] or not conf["time_col"]:
|
|
2126
2252
|
return pd.DataFrame()
|
|
2127
|
-
|
|
2253
|
+
data = filtered_data()
|
|
2254
|
+
if data.empty:
|
|
2255
|
+
return pd.DataFrame()
|
|
2256
|
+
return quality_report(data, conf)
|
|
2128
2257
|
|
|
2129
2258
|
@render.ui
|
|
2130
2259
|
def quality_status():
|
|
@@ -2134,7 +2263,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2134
2263
|
return ui.p("Select a measurement to analyse coverage.", class_="text-muted")
|
|
2135
2264
|
missing_bins = int((result["observed"] == 0).sum())
|
|
2136
2265
|
return ui.p(f"{len(result):,} measurement intervals; "
|
|
2137
|
-
f"{int(result['is_complete'].sum()):,} complete; "
|
|
2266
|
+
f"{int(result['is_complete'].sum()):,} complete at ≥ {report_settings()['min_completeness']:.0%}; "
|
|
2138
2267
|
f"{missing_bins:,} entirely missing.", class_="text-muted")
|
|
2139
2268
|
except (ValueError, KeyError) as exc:
|
|
2140
2269
|
return ui.p(str(exc), class_="text-danger")
|
|
@@ -2163,7 +2292,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2163
2292
|
import matplotlib.pyplot as plt
|
|
2164
2293
|
from matplotlib.colors import Normalize
|
|
2165
2294
|
conf = quality_settings()
|
|
2166
|
-
matrix = quality_heatmap_data(report(), conf["heatmap_variable"])
|
|
2295
|
+
matrix = quality_heatmap_data(report(), conf["heatmap_variable"], mode="threshold_coverage")
|
|
2167
2296
|
fig, ax = plt.subplots(figsize=(10, 4))
|
|
2168
2297
|
if matrix.empty:
|
|
2169
2298
|
ax.text(0.5, 0.5, "No data for selected measurement", ha="center", va="center")
|
|
@@ -2180,8 +2309,9 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
2180
2309
|
ax.set_xticklabels([str(matrix.columns[i]) for i in ticks], rotation=35, ha="right", fontsize=8)
|
|
2181
2310
|
ax.set_xlabel("Aggregation interval")
|
|
2182
2311
|
ax.set_ylabel(conf["id_col"])
|
|
2183
|
-
ax.set_title(f"{conf['heatmap_variable']} coverage")
|
|
2184
|
-
fig.colorbar(image, ax=ax,
|
|
2312
|
+
ax.set_title(f"{conf['heatmap_variable']} — complete at ≥ {conf['min_completeness']:.0%} coverage")
|
|
2313
|
+
fig.colorbar(image, ax=ax, ticks=[0, .25, .5, .75, 1],
|
|
2314
|
+
label="Coverage (fraction; passing bins displayed as 100%)")
|
|
2185
2315
|
fig.tight_layout()
|
|
2186
2316
|
return fig
|
|
2187
2317
|
|
|
@@ -3010,9 +3140,10 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
3010
3140
|
number = next_quality.get() + 1
|
|
3011
3141
|
tab_id = f"quality_{number}"
|
|
3012
3142
|
settings = views.restore_quality(preset_id)
|
|
3143
|
+
frame, _ = prepare_table_source(summary, restored_settings=settings)
|
|
3013
3144
|
panel = ui.nav_panel(preset.get('name') or f"Quality {number}",
|
|
3014
|
-
quality_tab_ui(tab_id, settings), value=tab_id)
|
|
3015
|
-
snapshot = quality_tab_server(tab_id, settings)
|
|
3145
|
+
quality_tab_ui(tab_id, frame, settings), value=tab_id)
|
|
3146
|
+
snapshot = quality_tab_server(tab_id, frame, settings)
|
|
3016
3147
|
workspace.add_quality(tab_id, snapshot)
|
|
3017
3148
|
begin_history(tab_id, settings, restored=True)
|
|
3018
3149
|
register_quality_history(tab_id)
|
|
@@ -3059,10 +3190,15 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
3059
3190
|
def add_quality():
|
|
3060
3191
|
number = next_quality.get() + 1
|
|
3061
3192
|
tab_id = f"quality_{number}"
|
|
3062
|
-
|
|
3063
|
-
|
|
3193
|
+
selected = input.plot_tabs()
|
|
3194
|
+
with reactive.isolate():
|
|
3195
|
+
initialized = initializer.new_quality(selected, number)
|
|
3196
|
+
frame, steps = initialized.frame, initialized.lineage
|
|
3197
|
+
initial_settings = {"source_steps": steps}
|
|
3198
|
+
panel = ui.nav_panel(f"Quality {number}", quality_tab_ui(tab_id, frame, initial_settings), value=tab_id)
|
|
3199
|
+
settings = quality_tab_server(tab_id, frame, initial_settings)
|
|
3064
3200
|
workspace.add_quality(tab_id, settings)
|
|
3065
|
-
begin_history(tab_id,
|
|
3201
|
+
begin_history(tab_id, initial_settings)
|
|
3066
3202
|
register_quality_history(tab_id)
|
|
3067
3203
|
next_quality.set(number)
|
|
3068
3204
|
active_tabs.set(workspace.active_tabs)
|
|
@@ -3107,8 +3243,9 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
3107
3243
|
number = next_quality.get() + 1
|
|
3108
3244
|
next_quality.set(number)
|
|
3109
3245
|
replacement = f"quality_{number}"
|
|
3110
|
-
|
|
3111
|
-
|
|
3246
|
+
frame, _ = prepare_table_source(summary, restored_settings=settings)
|
|
3247
|
+
panel = ui.nav_panel(f"Quality {number}", quality_tab_ui(replacement, frame, settings), value=replacement)
|
|
3248
|
+
quality_snapshot = quality_tab_server(replacement, frame, settings)
|
|
3112
3249
|
workspace.add_quality(replacement, quality_snapshot)
|
|
3113
3250
|
register_quality_history(replacement)
|
|
3114
3251
|
elif kind == "plot":
|
|
@@ -98,7 +98,7 @@ def inspect_saved_views(views, columns=None):
|
|
|
98
98
|
continue
|
|
99
99
|
if value not in available:
|
|
100
100
|
issues.append(ViewIssue(identifier, f'settings.{field}', 'warning', f'Column not found: {value}'))
|
|
101
|
-
chain = settings.get('source_steps', []) if view['type']
|
|
101
|
+
chain = settings.get('source_steps', []) if view['type'] in ('table', 'quality') else []
|
|
102
102
|
if not isinstance(chain, list):
|
|
103
103
|
issues.append(ViewIssue(identifier, 'settings.source_steps', 'error', 'Expected a list of transformation steps'))
|
|
104
104
|
elif any(not isinstance(step, dict) for step in chain):
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|