apmtools 0.2.21__tar.gz → 0.2.22__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.21 → apmtools-0.2.22}/PKG-INFO +1 -1
- {apmtools-0.2.21 → apmtools-0.2.22}/pyproject.toml +1 -1
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/shiny/summary.py +39 -24
- {apmtools-0.2.21 → apmtools-0.2.22}/.gitignore +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/LICENSE +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/README.md +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.21 → apmtools-0.2.22}/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.022
|
|
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>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
"""Interactive Plotly/Shiny application for apmtools
|
|
1
|
+
"""Interactive Plotly/Shiny application for :class:`apmtools.Summary`."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import threading
|
|
6
6
|
from collections.abc import Sequence
|
|
7
|
+
|
|
7
8
|
import numpy as np
|
|
8
9
|
import pandas as pd
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
PLOT_TYPES = {
|
|
12
12
|
"bar": "Bar",
|
|
13
13
|
"box": "Boxplot",
|
|
@@ -19,11 +19,11 @@ PLOT_TYPES = {
|
|
|
19
19
|
# ``None`` means row-level plotting. The other values are passed directly to
|
|
20
20
|
# Summary.group_stat(frequency=...).
|
|
21
21
|
FREQUENCIES = {
|
|
22
|
+
"Day": "day",
|
|
23
|
+
"Hour": "hour",
|
|
24
|
+
"Minute": "minute",
|
|
22
25
|
"None (raw data)": None,
|
|
23
26
|
"Second": "second",
|
|
24
|
-
"Minute": "minute",
|
|
25
|
-
"Hour": "hour",
|
|
26
|
-
"Day": "day",
|
|
27
27
|
"Week": "week",
|
|
28
28
|
"Weekday": "weekday",
|
|
29
29
|
}
|
|
@@ -38,22 +38,24 @@ AGGREGATES = {
|
|
|
38
38
|
"Sum": "sum",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
def _default_x(data: pd.DataFrame) -> str:
|
|
42
|
-
"""Return
|
|
43
|
+
"""Return ``time`` when available, otherwise the first column."""
|
|
44
|
+
if "time" in data.columns:
|
|
45
|
+
return "time"
|
|
43
46
|
datetime_columns = [
|
|
44
47
|
column
|
|
45
48
|
for column in data.columns
|
|
46
49
|
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
47
50
|
]
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
else:
|
|
51
|
-
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
51
|
+
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
52
|
+
|
|
52
53
|
|
|
53
54
|
def _default_group(data: pd.DataFrame) -> str | None:
|
|
54
55
|
"""Return ``identifier`` when available."""
|
|
55
56
|
return "identifier" if "identifier" in data.columns else None
|
|
56
57
|
|
|
58
|
+
|
|
57
59
|
def _numeric_columns(data: pd.DataFrame) -> list[str]:
|
|
58
60
|
"""Return numeric column names as strings."""
|
|
59
61
|
return sorted([
|
|
@@ -100,6 +102,11 @@ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
|
|
|
100
102
|
)
|
|
101
103
|
|
|
102
104
|
|
|
105
|
+
def _alphabetical(values) -> list[str]:
|
|
106
|
+
"""Return string values sorted alphabetically, case-insensitively."""
|
|
107
|
+
return sorted((str(value) for value in values), key=str.casefold)
|
|
108
|
+
|
|
109
|
+
|
|
103
110
|
def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
|
|
104
111
|
"""Validate and normalise an initial Y selection."""
|
|
105
112
|
numeric = _numeric_columns(data)
|
|
@@ -112,15 +119,10 @@ def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> l
|
|
|
112
119
|
return [str(column) for column in values]
|
|
113
120
|
|
|
114
121
|
|
|
115
|
-
def _alphabetical(values) -> list[str]:
|
|
116
|
-
"""Return string values sorted alphabetically, case-insensitively."""
|
|
117
|
-
return sorted((str(value) for value in values), key=str.casefold)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
122
|
def _subset_choices(data: pd.DataFrame, column: str) -> tuple[dict[str, str], dict[str, object]]:
|
|
122
123
|
"""Build stable Shiny choices and their corresponding original values."""
|
|
123
|
-
values = pd.unique(data[column].dropna())
|
|
124
|
+
values = sorted(pd.unique(data[column].dropna()),
|
|
125
|
+
key=lambda value: str(value).casefold())
|
|
124
126
|
choices: dict[str, str] = {}
|
|
125
127
|
value_map: dict[str, object] = {}
|
|
126
128
|
for i, value in enumerate(values):
|
|
@@ -179,6 +181,16 @@ def _aggregate(data, value_columns, frequency, grouping_by, aggregate):
|
|
|
179
181
|
return result
|
|
180
182
|
|
|
181
183
|
|
|
184
|
+
def _aggregated_value_columns(data, value_columns, aggregate):
|
|
185
|
+
"""Return the columns produced by Summary.group_stat()."""
|
|
186
|
+
columns = []
|
|
187
|
+
for column in value_columns:
|
|
188
|
+
aggregated = f"{column}_{aggregate}"
|
|
189
|
+
if aggregated in data.columns:
|
|
190
|
+
columns.append(aggregated)
|
|
191
|
+
return columns
|
|
192
|
+
|
|
193
|
+
|
|
182
194
|
def _histogram_figure(
|
|
183
195
|
data: pd.DataFrame,
|
|
184
196
|
x_column: str,
|
|
@@ -294,6 +306,7 @@ def _histogram_figure(
|
|
|
294
306
|
)
|
|
295
307
|
return fig
|
|
296
308
|
|
|
309
|
+
|
|
297
310
|
def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
|
|
298
311
|
"""Convert a timestamp aggregation frequency to its expected spacing."""
|
|
299
312
|
if not frequency or frequency == "weekday":
|
|
@@ -436,7 +449,8 @@ def create_summary_app(
|
|
|
436
449
|
subset_columns = columns
|
|
437
450
|
|
|
438
451
|
app_ui = ui.page_sidebar(
|
|
439
|
-
ui.
|
|
452
|
+
ui.div(
|
|
453
|
+
ui.tags.style("""
|
|
440
454
|
/* Allow the main pane to consume all width remaining after the sidebar. */
|
|
441
455
|
.bslib-sidebar-layout > .main,
|
|
442
456
|
.bslib-sidebar-layout > main,
|
|
@@ -465,7 +479,7 @@ def create_summary_app(
|
|
|
465
479
|
max-width: none !important;
|
|
466
480
|
}
|
|
467
481
|
"""),
|
|
468
|
-
|
|
482
|
+
ui.tags.script("""
|
|
469
483
|
// Plotly sometimes retains the pixel width measured before the
|
|
470
484
|
// Shiny sidebar and widget have finished laying out.
|
|
471
485
|
(() => {
|
|
@@ -495,7 +509,12 @@ def create_summary_app(
|
|
|
495
509
|
}
|
|
496
510
|
})();
|
|
497
511
|
"""),
|
|
498
|
-
|
|
512
|
+
ui.div(
|
|
513
|
+
output_widget("plot", height="100%", width="100%"),
|
|
514
|
+
class_="summary-plot-container",
|
|
515
|
+
),
|
|
516
|
+
),
|
|
517
|
+
sidebar=ui.sidebar(
|
|
499
518
|
ui.h4("Plot controls"),
|
|
500
519
|
ui.input_select("plot_type", "Plot type",
|
|
501
520
|
choices=PLOT_TYPES, selected=plot_type),
|
|
@@ -559,10 +578,6 @@ def create_summary_app(
|
|
|
559
578
|
"Summary.group_stat()."
|
|
560
579
|
),
|
|
561
580
|
),
|
|
562
|
-
ui.div(
|
|
563
|
-
output_widget("plot", height="100%", width="100%"),
|
|
564
|
-
class_="summary-plot-container",
|
|
565
|
-
),
|
|
566
581
|
)
|
|
567
582
|
|
|
568
583
|
def server(input, output, session):
|
|
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
|