apmtools 0.2.16__tar.gz → 0.2.17__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.17}/PKG-INFO +1 -1
- {apmtools-0.2.16 → apmtools-0.2.17}/pyproject.toml +1 -1
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/shiny/summary.py +138 -156
- {apmtools-0.2.16 → apmtools-0.2.17}/.gitignore +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/LICENSE +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/README.md +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.16 → apmtools-0.2.17}/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.017
|
|
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,12 +9,14 @@ 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
|
|
|
19
|
+
|
|
18
20
|
# ``None`` means row-level plotting. The other values are passed directly to
|
|
19
21
|
# Summary.group_stat(frequency=...).
|
|
20
22
|
FREQUENCIES = {
|
|
@@ -28,13 +30,13 @@ FREQUENCIES = {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
AGGREGATES = {
|
|
33
|
+
"Count": "count",
|
|
34
|
+
"Maximum": "max",
|
|
31
35
|
"Mean": "mean",
|
|
32
36
|
"Median": "median",
|
|
33
|
-
"Sum": "sum",
|
|
34
37
|
"Minimum": "min",
|
|
35
|
-
"Maximum": "max",
|
|
36
|
-
"Count": "count",
|
|
37
38
|
"Standard deviation": "std",
|
|
39
|
+
"Sum": "sum",
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
def _default_x(data: pd.DataFrame) -> str:
|
|
@@ -98,7 +100,7 @@ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
|
|
|
98
100
|
key=str.casefold,
|
|
99
101
|
)
|
|
100
102
|
|
|
101
|
-
def
|
|
103
|
+
def _default_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)
|
|
104
106
|
if y is None:
|
|
@@ -298,13 +300,16 @@ def create_summary_app(
|
|
|
298
300
|
columns = _alphabetical(summary.columns)
|
|
299
301
|
column_map = {str(c): c for c in summary.columns}
|
|
300
302
|
numeric_columns = _alphabetical(_numeric_columns(summary))
|
|
301
|
-
categorical_columns = _categorical_columns(summary)
|
|
303
|
+
categorical_columns = _alphabetical(_categorical_columns(summary))
|
|
304
|
+
datetime_columns = _alphabetical(_datetime_columns(summary))
|
|
305
|
+
timedelta_columns = _alphabetical(_timedelta_columns(summary))
|
|
306
|
+
|
|
302
307
|
|
|
303
308
|
x = _default_x(summary) if x is None else str(x)
|
|
304
309
|
if x not in column_map:
|
|
305
310
|
raise ValueError(f"Unknown x column: {x!r}")
|
|
306
311
|
|
|
307
|
-
initial_y =
|
|
312
|
+
initial_y = _default_y(summary, x, y)
|
|
308
313
|
if group_by is None:
|
|
309
314
|
initial_groups = [_default_group(summary)] if _default_group(
|
|
310
315
|
summary) is not None else []
|
|
@@ -364,8 +369,11 @@ def create_summary_app(
|
|
|
364
369
|
"datetime_end", "End datetime",
|
|
365
370
|
value="", placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS",
|
|
366
371
|
),
|
|
367
|
-
ui.
|
|
368
|
-
|
|
372
|
+
ui.panel_conditional(
|
|
373
|
+
"input.plot_type === 'histogram'",
|
|
374
|
+
ui.input_numeric("bins", "Histogram bins",
|
|
375
|
+
value=30, min=1, max=200, step=1),
|
|
376
|
+
),
|
|
369
377
|
ui.input_text("title", "Plot title", value=title),
|
|
370
378
|
ui.help_text(
|
|
371
379
|
"Subset and datetime filters are applied before aggregation. "
|
|
@@ -382,39 +390,53 @@ def create_summary_app(
|
|
|
382
390
|
def server(input, output, session):
|
|
383
391
|
@render.ui
|
|
384
392
|
def x_control():
|
|
385
|
-
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
kind = input.plot_type()
|
|
394
|
+
if kind in ("box", "bar"):
|
|
395
|
+
choices = categorical_columns
|
|
396
|
+
multiple = True
|
|
397
|
+
elif kind == "line":
|
|
398
|
+
choices = datetime_columns
|
|
399
|
+
multiple = False
|
|
400
|
+
else:
|
|
401
|
+
choices = numeric_columns
|
|
402
|
+
multiple = False
|
|
403
|
+
|
|
404
|
+
if not choices:
|
|
405
|
+
return ui.help_text(f"No compatible X variables for {kind}.")
|
|
406
|
+
selected = [x] if x in choices else [choices[0]]
|
|
407
|
+
if multiple:
|
|
408
|
+
return ui.input_selectize(
|
|
409
|
+
"x", "X-axis / categories",
|
|
410
|
+
choices={c: c for c in choices},
|
|
411
|
+
selected=selected, multiple=True,
|
|
396
412
|
)
|
|
397
413
|
return ui.input_select(
|
|
398
|
-
"x", "X-axis /
|
|
399
|
-
choices={c: c for c in
|
|
400
|
-
selected=
|
|
414
|
+
"x", "X-axis / variable",
|
|
415
|
+
choices={c: c for c in choices},
|
|
416
|
+
selected=selected[0],
|
|
401
417
|
)
|
|
402
418
|
|
|
403
419
|
@render.ui
|
|
404
420
|
def y_control():
|
|
405
|
-
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
|
|
421
|
+
kind = input.plot_type()
|
|
422
|
+
if kind == "histogram":
|
|
423
|
+
return ui.div()
|
|
424
|
+
if not numeric_columns:
|
|
425
|
+
return ui.help_text("No numeric Y variables available.")
|
|
426
|
+
selected = next(
|
|
427
|
+
(v for v in initial_y if v in numeric_columns), numeric_columns[0])
|
|
428
|
+
if kind in ("box", "bar", "scatter"):
|
|
409
429
|
return ui.input_select(
|
|
410
430
|
"y", "Y-axis / variable",
|
|
411
431
|
choices={c: c for c in numeric_columns},
|
|
412
432
|
selected=selected,
|
|
413
433
|
)
|
|
414
434
|
return ui.input_selectize(
|
|
415
|
-
"y", "Y-axis /
|
|
435
|
+
"y", "Y-axis / variables",
|
|
416
436
|
choices={c: c for c in numeric_columns},
|
|
417
|
-
selected=initial_y
|
|
437
|
+
selected=[v for v in initial_y if v in numeric_columns] or [
|
|
438
|
+
selected],
|
|
439
|
+
multiple=True,
|
|
418
440
|
)
|
|
419
441
|
|
|
420
442
|
@render.ui
|
|
@@ -474,89 +496,92 @@ def create_summary_app(
|
|
|
474
496
|
@reactive.calc
|
|
475
497
|
def selected_data():
|
|
476
498
|
current = filtered_summary()
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
499
|
+
kind = input.plot_type()
|
|
500
|
+
x_input = input.x()
|
|
501
|
+
selected_x = ([x_input] if isinstance(
|
|
502
|
+
x_input, str) else list(x_input or []))
|
|
503
|
+
if not selected_x:
|
|
504
|
+
return pd.DataFrame()
|
|
505
|
+
|
|
506
|
+
if kind == "histogram":
|
|
507
|
+
value_names = selected_x
|
|
508
|
+
else:
|
|
509
|
+
y_input = input.y()
|
|
510
|
+
value_names = ([y_input] if isinstance(
|
|
511
|
+
y_input, str) else list(y_input or []))
|
|
512
|
+
if not value_names:
|
|
481
513
|
return pd.DataFrame()
|
|
482
514
|
|
|
483
|
-
y_columns = [column_map[c] for c in selected_y]
|
|
484
515
|
selected_groups = list(input.group_by())
|
|
485
516
|
grouping_by = [column_map[c] for c in selected_groups]
|
|
486
517
|
frequency = FREQUENCIES[input.frequency()]
|
|
487
518
|
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.
|
|
519
|
+
value_columns = [column_map[c] for c in value_names]
|
|
520
|
+
|
|
521
|
+
if kind in ("box", "bar"):
|
|
522
|
+
for c in selected_x:
|
|
523
|
+
if column_map[c] not in grouping_by:
|
|
524
|
+
grouping_by.append(column_map[c])
|
|
525
|
+
if kind == "scatter" and frequency is not None:
|
|
526
|
+
# Both numerical axes must be aggregated on the same rows.
|
|
527
|
+
value_columns = list(dict.fromkeys(
|
|
528
|
+
[column_map[selected_x[0]], *value_columns]
|
|
529
|
+
))
|
|
501
530
|
if frequency is not None:
|
|
502
|
-
current = _aggregate(
|
|
503
|
-
|
|
504
|
-
|
|
531
|
+
current = _aggregate(
|
|
532
|
+
current, value_columns, frequency, grouping_by, aggregate
|
|
533
|
+
)
|
|
505
534
|
return current
|
|
506
535
|
|
|
507
536
|
@render_plotly
|
|
508
537
|
def plot():
|
|
509
538
|
plot_df = selected_data()
|
|
510
539
|
selected_plot = input.plot_type()
|
|
511
|
-
|
|
512
|
-
|
|
540
|
+
x_input = input.x()
|
|
541
|
+
selected_x = ([x_input] if isinstance(
|
|
542
|
+
x_input, str) else list(x_input or []))
|
|
543
|
+
y_input = None if selected_plot == "histogram" else input.y()
|
|
513
544
|
selected_y = ([y_input] if isinstance(
|
|
514
545
|
y_input, str) else list(y_input or []))
|
|
515
546
|
selected_groups = list(input.group_by())
|
|
516
547
|
frequency = FREQUENCIES[input.frequency()]
|
|
517
548
|
|
|
518
|
-
if plot_df.empty or not
|
|
549
|
+
if plot_df.empty or not selected_x or (
|
|
550
|
+
selected_plot != "histogram" and not selected_y
|
|
551
|
+
):
|
|
519
552
|
return px.scatter(title="No data for the selected filters")
|
|
520
553
|
|
|
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]
|
|
554
|
+
aggregate = AGGREGATES[input.aggregate()]
|
|
525
555
|
|
|
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
|
|
556
|
+
def mapped(column):
|
|
557
|
+
original = column_map[column]
|
|
558
|
+
return f"{original}_{aggregate}" if frequency is not None else original
|
|
542
559
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
560
|
+
if selected_plot == "histogram":
|
|
561
|
+
x_column = mapped(selected_x[0])
|
|
562
|
+
y_columns = []
|
|
563
|
+
elif selected_plot in ("box", "bar"):
|
|
564
|
+
x_columns = [column_map[c] for c in selected_x]
|
|
565
|
+
x_column = x_columns[0]
|
|
566
|
+
y_columns = [mapped(selected_y[0])]
|
|
567
|
+
elif selected_plot == "scatter":
|
|
568
|
+
x_column = mapped(selected_x[0])
|
|
569
|
+
y_columns = [mapped(selected_y[0])]
|
|
550
570
|
else:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
571
|
+
x_column = frequency if frequency is not None else column_map[selected_x[0]]
|
|
572
|
+
y_columns = [mapped(c) for c in selected_y]
|
|
573
|
+
|
|
574
|
+
required = [x_column, *y_columns]
|
|
575
|
+
if selected_plot in ("box", "bar"):
|
|
576
|
+
required.extend(x_columns)
|
|
577
|
+
if any(c not in plot_df.columns for c in required):
|
|
578
|
+
return px.scatter(title="Selected variables are unavailable after aggregation")
|
|
579
|
+
|
|
580
|
+
if selected_plot in ("box", "bar"):
|
|
581
|
+
plot_df = plot_df.copy()
|
|
582
|
+
plot_df["__plot_category"] = plot_df[x_columns].astype(
|
|
583
|
+
str).agg(" | ".join, axis=1)
|
|
584
|
+
x_column = "__plot_category"
|
|
560
585
|
|
|
561
586
|
# Multiple grouping columns are supported by group_stat(). For
|
|
562
587
|
# plotting, Plotly needs one color/category column, so combine the
|
|
@@ -570,74 +595,31 @@ def create_summary_app(
|
|
|
570
595
|
)
|
|
571
596
|
|
|
572
597
|
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
|
-
)
|
|
598
|
+
fig = px.histogram(
|
|
599
|
+
plot_df,
|
|
600
|
+
x=x_column,
|
|
601
|
+
color="__plot_group" if group_columns else None,
|
|
602
|
+
nbins=int(input.bins()),
|
|
603
|
+
barmode="overlay",
|
|
604
|
+
opacity=0.45 if group_columns else 1.0,
|
|
605
|
+
)
|
|
610
606
|
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
|
-
)
|
|
607
|
+
fig = px.box(
|
|
608
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
609
|
+
color="__plot_group" if group_columns else None,
|
|
610
|
+
points="outliers",
|
|
611
|
+
)
|
|
628
612
|
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
|
-
)
|
|
613
|
+
fig = px.bar(
|
|
614
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
615
|
+
color="__plot_group" if group_columns else None,
|
|
616
|
+
barmode="group",
|
|
617
|
+
)
|
|
618
|
+
elif selected_plot == "scatter":
|
|
619
|
+
fig = px.scatter(
|
|
620
|
+
plot_df, x=x_column, y=y_columns[0],
|
|
621
|
+
color="__plot_group" if group_columns else None,
|
|
622
|
+
)
|
|
641
623
|
else:
|
|
642
624
|
# ``identifier`` identifies an individual input monitor. It
|
|
643
625
|
# 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
|