apmtools 0.2.16__tar.gz → 0.2.18__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.16 → apmtools-0.2.18}/PKG-INFO +1 -1
- {apmtools-0.2.16 → apmtools-0.2.18}/pyproject.toml +1 -1
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/shiny/summary.py +139 -164
- {apmtools-0.2.16 → apmtools-0.2.18}/.gitignore +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/LICENSE +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/README.md +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.18}/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.018
|
|
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>
|
|
@@ -9,10 +9,11 @@ import pandas as pd
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
PLOT_TYPES = {
|
|
12
|
-
"line": "Line",
|
|
13
12
|
"bar": "Bar",
|
|
14
|
-
"histogram": "Histogram",
|
|
15
13
|
"box": "Boxplot",
|
|
14
|
+
"histogram": "Histogram",
|
|
15
|
+
"line": "Line",
|
|
16
|
+
"scatter": "Scatter",
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
# ``None`` means row-level plotting. The other values are passed directly to
|
|
@@ -28,13 +29,13 @@ FREQUENCIES = {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
AGGREGATES = {
|
|
32
|
+
"Count": "count",
|
|
33
|
+
"Maximum": "max",
|
|
31
34
|
"Mean": "mean",
|
|
32
35
|
"Median": "median",
|
|
33
|
-
"Sum": "sum",
|
|
34
36
|
"Minimum": "min",
|
|
35
|
-
"Maximum": "max",
|
|
36
|
-
"Count": "count",
|
|
37
37
|
"Standard deviation": "std",
|
|
38
|
+
"Sum": "sum",
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
def _default_x(data: pd.DataFrame) -> str:
|
|
@@ -98,6 +99,7 @@ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
|
|
|
98
99
|
key=str.casefold,
|
|
99
100
|
)
|
|
100
101
|
|
|
102
|
+
|
|
101
103
|
def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
|
|
102
104
|
"""Validate and normalise an initial Y selection."""
|
|
103
105
|
numeric = _numeric_columns(data)
|
|
@@ -177,16 +179,6 @@ def _aggregate(data, value_columns, frequency, grouping_by, aggregate):
|
|
|
177
179
|
return result
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
def _aggregated_value_columns(data, value_columns, aggregate):
|
|
181
|
-
"""Return the columns produced by Summary.group_stat()."""
|
|
182
|
-
columns = []
|
|
183
|
-
for column in value_columns:
|
|
184
|
-
aggregated = f"{column}_{aggregate}"
|
|
185
|
-
if aggregated in data.columns:
|
|
186
|
-
columns.append(aggregated)
|
|
187
|
-
return columns
|
|
188
|
-
|
|
189
|
-
|
|
190
182
|
def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
|
|
191
183
|
"""Convert a timestamp aggregation frequency to its expected spacing."""
|
|
192
184
|
if not frequency or frequency == "weekday":
|
|
@@ -299,6 +291,10 @@ def create_summary_app(
|
|
|
299
291
|
column_map = {str(c): c for c in summary.columns}
|
|
300
292
|
numeric_columns = _alphabetical(_numeric_columns(summary))
|
|
301
293
|
categorical_columns = _categorical_columns(summary)
|
|
294
|
+
datetime_columns = _datetime_columns(summary)
|
|
295
|
+
group_columns = _alphabetical(
|
|
296
|
+
c for c in columns if not pd.api.types.is_numeric_dtype(summary[column_map[c]])
|
|
297
|
+
)
|
|
302
298
|
|
|
303
299
|
x = _default_x(summary) if x is None else str(x)
|
|
304
300
|
if x not in column_map:
|
|
@@ -315,6 +311,8 @@ def create_summary_app(
|
|
|
315
311
|
unknown_groups = [c for c in initial_groups if c not in column_map]
|
|
316
312
|
if unknown_groups:
|
|
317
313
|
raise ValueError(f"Unknown group_by column(s): {unknown_groups!r}")
|
|
314
|
+
# Numeric columns are not offered by the Group by selector.
|
|
315
|
+
initial_groups = [c for c in initial_groups if c in group_columns]
|
|
318
316
|
|
|
319
317
|
if plot_type not in PLOT_TYPES:
|
|
320
318
|
raise ValueError(
|
|
@@ -331,7 +329,7 @@ def create_summary_app(
|
|
|
331
329
|
ui.output_ui("y_control"),
|
|
332
330
|
ui.input_selectize(
|
|
333
331
|
"group_by", "Group by",
|
|
334
|
-
choices={c: c for c in
|
|
332
|
+
choices={c: c for c in group_columns},
|
|
335
333
|
selected=initial_groups,
|
|
336
334
|
multiple=True,
|
|
337
335
|
options={"placeholder": "Select one or more grouping columns"},
|
|
@@ -364,8 +362,11 @@ def create_summary_app(
|
|
|
364
362
|
"datetime_end", "End datetime",
|
|
365
363
|
value="", placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS",
|
|
366
364
|
),
|
|
367
|
-
ui.
|
|
368
|
-
|
|
365
|
+
ui.panel_conditional(
|
|
366
|
+
"input.plot_type === 'histogram'",
|
|
367
|
+
ui.input_numeric("bins", "Histogram bins",
|
|
368
|
+
value=30, min=1, max=200, step=1),
|
|
369
|
+
),
|
|
369
370
|
ui.input_text("title", "Plot title", value=title),
|
|
370
371
|
ui.help_text(
|
|
371
372
|
"Subset and datetime filters are applied before aggregation. "
|
|
@@ -382,39 +383,53 @@ def create_summary_app(
|
|
|
382
383
|
def server(input, output, session):
|
|
383
384
|
@render.ui
|
|
384
385
|
def x_control():
|
|
385
|
-
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
386
|
+
kind = input.plot_type()
|
|
387
|
+
if kind in ("box", "bar"):
|
|
388
|
+
choices = categorical_columns
|
|
389
|
+
multiple = True
|
|
390
|
+
elif kind == "line":
|
|
391
|
+
choices = datetime_columns
|
|
392
|
+
multiple = False
|
|
393
|
+
else:
|
|
394
|
+
choices = numeric_columns
|
|
395
|
+
multiple = False
|
|
396
|
+
|
|
397
|
+
if not choices:
|
|
398
|
+
return ui.help_text(f"No compatible X variables for {kind}.")
|
|
399
|
+
selected = [x] if x in choices else [choices[0]]
|
|
400
|
+
if multiple:
|
|
401
|
+
return ui.input_selectize(
|
|
402
|
+
"x", "X-axis / categories",
|
|
403
|
+
choices={c: c for c in choices},
|
|
404
|
+
selected=selected, multiple=True,
|
|
396
405
|
)
|
|
397
406
|
return ui.input_select(
|
|
398
|
-
"x", "X-axis /
|
|
399
|
-
choices={c: c for c in
|
|
400
|
-
selected=
|
|
407
|
+
"x", "X-axis / variable",
|
|
408
|
+
choices={c: c for c in choices},
|
|
409
|
+
selected=selected[0],
|
|
401
410
|
)
|
|
402
411
|
|
|
403
412
|
@render.ui
|
|
404
413
|
def y_control():
|
|
405
|
-
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
|
|
414
|
+
kind = input.plot_type()
|
|
415
|
+
if kind == "histogram":
|
|
416
|
+
return ui.div()
|
|
417
|
+
if not numeric_columns:
|
|
418
|
+
return ui.help_text("No numeric Y variables available.")
|
|
419
|
+
selected = next(
|
|
420
|
+
(v for v in initial_y if v in numeric_columns), numeric_columns[0])
|
|
421
|
+
if kind in ("box", "bar", "scatter"):
|
|
409
422
|
return ui.input_select(
|
|
410
423
|
"y", "Y-axis / variable",
|
|
411
424
|
choices={c: c for c in numeric_columns},
|
|
412
425
|
selected=selected,
|
|
413
426
|
)
|
|
414
427
|
return ui.input_selectize(
|
|
415
|
-
"y", "Y-axis /
|
|
428
|
+
"y", "Y-axis / variables",
|
|
416
429
|
choices={c: c for c in numeric_columns},
|
|
417
|
-
selected=initial_y
|
|
430
|
+
selected=[v for v in initial_y if v in numeric_columns] or [
|
|
431
|
+
selected],
|
|
432
|
+
multiple=True,
|
|
418
433
|
)
|
|
419
434
|
|
|
420
435
|
@render.ui
|
|
@@ -474,89 +489,92 @@ def create_summary_app(
|
|
|
474
489
|
@reactive.calc
|
|
475
490
|
def selected_data():
|
|
476
491
|
current = filtered_summary()
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
492
|
+
kind = input.plot_type()
|
|
493
|
+
x_input = input.x()
|
|
494
|
+
selected_x = ([x_input] if isinstance(
|
|
495
|
+
x_input, str) else list(x_input or []))
|
|
496
|
+
if not selected_x:
|
|
497
|
+
return pd.DataFrame()
|
|
498
|
+
|
|
499
|
+
if kind == "histogram":
|
|
500
|
+
value_names = selected_x
|
|
501
|
+
else:
|
|
502
|
+
y_input = input.y()
|
|
503
|
+
value_names = ([y_input] if isinstance(
|
|
504
|
+
y_input, str) else list(y_input or []))
|
|
505
|
+
if not value_names:
|
|
481
506
|
return pd.DataFrame()
|
|
482
507
|
|
|
483
|
-
y_columns = [column_map[c] for c in selected_y]
|
|
484
508
|
selected_groups = list(input.group_by())
|
|
485
509
|
grouping_by = [column_map[c] for c in selected_groups]
|
|
486
510
|
frequency = FREQUENCIES[input.frequency()]
|
|
487
511
|
aggregate = AGGREGATES[input.aggregate()]
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
# All selected grouping columns are passed directly to
|
|
500
|
-
# Summary.group_stat() when a frequency is selected.
|
|
512
|
+
value_columns = [column_map[c] for c in value_names]
|
|
513
|
+
|
|
514
|
+
if kind in ("box", "bar"):
|
|
515
|
+
for c in selected_x:
|
|
516
|
+
if column_map[c] not in grouping_by:
|
|
517
|
+
grouping_by.append(column_map[c])
|
|
518
|
+
if kind == "scatter" and frequency is not None:
|
|
519
|
+
# Both numerical axes must be aggregated on the same rows.
|
|
520
|
+
value_columns = list(dict.fromkeys(
|
|
521
|
+
[column_map[selected_x[0]], *value_columns]
|
|
522
|
+
))
|
|
501
523
|
if frequency is not None:
|
|
502
|
-
current = _aggregate(
|
|
503
|
-
|
|
504
|
-
|
|
524
|
+
current = _aggregate(
|
|
525
|
+
current, value_columns, frequency, grouping_by, aggregate
|
|
526
|
+
)
|
|
505
527
|
return current
|
|
506
528
|
|
|
507
529
|
@render_plotly
|
|
508
530
|
def plot():
|
|
509
531
|
plot_df = selected_data()
|
|
510
532
|
selected_plot = input.plot_type()
|
|
511
|
-
|
|
512
|
-
|
|
533
|
+
x_input = input.x()
|
|
534
|
+
selected_x = ([x_input] if isinstance(
|
|
535
|
+
x_input, str) else list(x_input or []))
|
|
536
|
+
y_input = None if selected_plot == "histogram" else input.y()
|
|
513
537
|
selected_y = ([y_input] if isinstance(
|
|
514
538
|
y_input, str) else list(y_input or []))
|
|
515
539
|
selected_groups = list(input.group_by())
|
|
516
540
|
frequency = FREQUENCIES[input.frequency()]
|
|
517
541
|
|
|
518
|
-
if plot_df.empty or not
|
|
542
|
+
if plot_df.empty or not selected_x or (
|
|
543
|
+
selected_plot != "histogram" and not selected_y
|
|
544
|
+
):
|
|
519
545
|
return px.scatter(title="No data for the selected filters")
|
|
520
546
|
|
|
521
|
-
|
|
522
|
-
# produced by group_stat(). For example, selecting ``pm25`` and
|
|
523
|
-
# hourly means produces ``pm25_mean`` in the aggregated result.
|
|
524
|
-
selected_y_columns = [column_map[c] for c in selected_y]
|
|
547
|
+
aggregate = AGGREGATES[input.aggregate()]
|
|
525
548
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
# Therefore the aggregated frequency column replaces the
|
|
530
|
-
# originally selected X-axis column.
|
|
531
|
-
if frequency is not None:
|
|
532
|
-
aggregate = AGGREGATES[input.aggregate()]
|
|
533
|
-
y_columns = _aggregated_value_columns(
|
|
534
|
-
plot_df, selected_y_columns, aggregate
|
|
535
|
-
)
|
|
536
|
-
if selected_plot == "box":
|
|
537
|
-
if selected_x not in column_map:
|
|
538
|
-
return px.scatter(title="Select a categorical X variable for the boxplot")
|
|
539
|
-
x_column = column_map[selected_x]
|
|
540
|
-
else:
|
|
541
|
-
x_column = frequency
|
|
549
|
+
def mapped(column):
|
|
550
|
+
original = column_map[column]
|
|
551
|
+
return f"{original}_{aggregate}" if frequency is not None else original
|
|
542
552
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
553
|
+
if selected_plot == "histogram":
|
|
554
|
+
x_column = mapped(selected_x[0])
|
|
555
|
+
y_columns = []
|
|
556
|
+
elif selected_plot in ("box", "bar"):
|
|
557
|
+
x_columns = [column_map[c] for c in selected_x]
|
|
558
|
+
x_column = x_columns[0]
|
|
559
|
+
y_columns = [mapped(selected_y[0])]
|
|
560
|
+
elif selected_plot == "scatter":
|
|
561
|
+
x_column = mapped(selected_x[0])
|
|
562
|
+
y_columns = [mapped(selected_y[0])]
|
|
550
563
|
else:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
564
|
+
x_column = frequency if frequency is not None else column_map[selected_x[0]]
|
|
565
|
+
y_columns = [mapped(c) for c in selected_y]
|
|
566
|
+
|
|
567
|
+
required = [x_column, *y_columns]
|
|
568
|
+
if selected_plot in ("box", "bar"):
|
|
569
|
+
required.extend(x_columns)
|
|
570
|
+
if any(c not in plot_df.columns for c in required):
|
|
571
|
+
return px.scatter(title="Selected variables are unavailable after aggregation")
|
|
572
|
+
|
|
573
|
+
if selected_plot in ("box", "bar"):
|
|
574
|
+
plot_df = plot_df.copy()
|
|
575
|
+
plot_df["__plot_category"] = plot_df[x_columns].astype(
|
|
576
|
+
str).agg(" | ".join, axis=1)
|
|
577
|
+
x_column = "__plot_category"
|
|
560
578
|
|
|
561
579
|
# Multiple grouping columns are supported by group_stat(). For
|
|
562
580
|
# plotting, Plotly needs one color/category column, so combine the
|
|
@@ -570,74 +588,31 @@ def create_summary_app(
|
|
|
570
588
|
)
|
|
571
589
|
|
|
572
590
|
if selected_plot == "histogram":
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
id_vars=[group_column],
|
|
582
|
-
var_name="variable",
|
|
583
|
-
value_name="value",
|
|
584
|
-
)
|
|
585
|
-
fig = px.histogram(
|
|
586
|
-
long_df,
|
|
587
|
-
x="value",
|
|
588
|
-
color=group_column,
|
|
589
|
-
nbins=int(input.bins()),
|
|
590
|
-
barmode="overlay",
|
|
591
|
-
opacity=0.45,
|
|
592
|
-
facet_row=("variable" if len(y_columns) > 1 else None),
|
|
593
|
-
)
|
|
594
|
-
# Keep the groups as the legend entries rather than the
|
|
595
|
-
# selected Y variables.
|
|
596
|
-
fig.update_traces(marker_line_width=0)
|
|
597
|
-
else:
|
|
598
|
-
long_df = plot_df[y_columns].melt(
|
|
599
|
-
var_name="variable", value_name="value"
|
|
600
|
-
)
|
|
601
|
-
fig = px.histogram(
|
|
602
|
-
long_df,
|
|
603
|
-
x="value",
|
|
604
|
-
color="variable",
|
|
605
|
-
nbins=int(input.bins()),
|
|
606
|
-
barmode="overlay" if len(
|
|
607
|
-
y_columns) > 1 else "relative",
|
|
608
|
-
opacity=0.65 if len(y_columns) > 1 else 1.0,
|
|
609
|
-
)
|
|
591
|
+
fig = px.histogram(
|
|
592
|
+
plot_df,
|
|
593
|
+
x=x_column,
|
|
594
|
+
color="__plot_group" if group_columns else None,
|
|
595
|
+
nbins=int(input.bins()),
|
|
596
|
+
barmode="overlay",
|
|
597
|
+
opacity=0.45 if group_columns else 1.0,
|
|
598
|
+
)
|
|
610
599
|
elif selected_plot == "box":
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
title="Boxplots require a categorical variable on the X axis"
|
|
617
|
-
)
|
|
618
|
-
y_column = y_columns[0]
|
|
619
|
-
if group_columns:
|
|
620
|
-
fig = px.box(
|
|
621
|
-
plot_df, x=x_column, y=y_column,
|
|
622
|
-
color="__plot_group", points="outliers",
|
|
623
|
-
)
|
|
624
|
-
else:
|
|
625
|
-
fig = px.box(
|
|
626
|
-
plot_df, x=x_column, y=y_column, points="outliers"
|
|
627
|
-
)
|
|
600
|
+
fig = px.box(
|
|
601
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
602
|
+
color="__plot_group" if group_columns else None,
|
|
603
|
+
points="outliers",
|
|
604
|
+
)
|
|
628
605
|
elif selected_plot == "bar":
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
facet_row="variable" if len(y_columns) > 1 else None, barmode="group",
|
|
640
|
-
)
|
|
606
|
+
fig = px.bar(
|
|
607
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
608
|
+
color="__plot_group" if group_columns else None,
|
|
609
|
+
barmode="group",
|
|
610
|
+
)
|
|
611
|
+
elif selected_plot == "scatter":
|
|
612
|
+
fig = px.scatter(
|
|
613
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
614
|
+
color="__plot_group" if group_columns else None,
|
|
615
|
+
)
|
|
641
616
|
else:
|
|
642
617
|
# ``identifier`` identifies an individual input monitor. It
|
|
643
618
|
# must therefore be used as Plotly's line-group key even when
|
|
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
|