apmtools 0.2.13__tar.gz → 0.2.15__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.13 → apmtools-0.2.15}/PKG-INFO +1 -1
- {apmtools-0.2.13 → apmtools-0.2.15}/pyproject.toml +1 -1
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/classes.py +103 -29
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/shiny/summary.py +103 -25
- {apmtools-0.2.13 → apmtools-0.2.15}/.gitignore +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/LICENSE +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/README.md +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.13 → apmtools-0.2.15}/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.015
|
|
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>
|
|
@@ -39,9 +39,9 @@ class DictionaryPlus(dict):
|
|
|
39
39
|
"""
|
|
40
40
|
Return a subset of a DictionaryPlus, specified in the parameter filter_dict (itself a dictionary) or condition (a function that takes at minimum a value from the dictionary as an input parameter, and return True/False if some condition specified in the function is met. Typically a lambda function of the form lambda x: True if condition else False)
|
|
41
41
|
filter_dict is {attrib:["attrib_value_x","attrib_value_y",..]}, where
|
|
42
|
-
attrib is an attribute of the elements of dictionary, and attrib_value is a list
|
|
42
|
+
attrib is an attribute of the elements of dictionary, and attrib_value is a value or list
|
|
43
43
|
of the values of such attrib that the elements of returned dictionary can have
|
|
44
|
-
specify filter_style='all' if all conditions should be met to be included in the return dictionary,
|
|
44
|
+
specify filter_style='all' if all conditions should be met to be included in the return dictionary, other filters allowed are 'any', 'negative all', 'negative any'.
|
|
45
45
|
"""
|
|
46
46
|
if type(filter_dict) != type(dict()):
|
|
47
47
|
print("subset function error: type filter_dict should be dict")
|
|
@@ -49,14 +49,17 @@ class DictionaryPlus(dict):
|
|
|
49
49
|
return_dict = copy.deepcopy(self)
|
|
50
50
|
a = {}
|
|
51
51
|
|
|
52
|
+
if filter_style not in {'all', 'any', 'negative all', 'negative any'}:
|
|
53
|
+
return return_dict
|
|
54
|
+
|
|
52
55
|
if filter_style == 'all':
|
|
53
56
|
a = {key: value for key, value in return_dict.items()}
|
|
54
57
|
for key, value in return_dict.items():
|
|
55
58
|
for i, j in filter_dict.items():
|
|
56
59
|
if hasattr(value, i):
|
|
57
60
|
try:
|
|
58
|
-
if type(j)
|
|
59
|
-
if
|
|
61
|
+
if type(j) is not list:
|
|
62
|
+
if getattr(value, i) not in [j]:
|
|
60
63
|
del a[key]
|
|
61
64
|
break
|
|
62
65
|
else:
|
|
@@ -67,8 +70,8 @@ class DictionaryPlus(dict):
|
|
|
67
70
|
pass
|
|
68
71
|
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
69
72
|
try:
|
|
70
|
-
if type(j)
|
|
71
|
-
if
|
|
73
|
+
if type(j) is not list:
|
|
74
|
+
if getattr(value, 'm')[i] not in [j]:
|
|
72
75
|
del a[key]
|
|
73
76
|
break
|
|
74
77
|
else:
|
|
@@ -86,8 +89,8 @@ class DictionaryPlus(dict):
|
|
|
86
89
|
for i, j in filter_dict.items():
|
|
87
90
|
if hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
88
91
|
try:
|
|
89
|
-
if type(j)
|
|
90
|
-
if
|
|
92
|
+
if type(j) is not list:
|
|
93
|
+
if getattr(value, 'm')[i] in [j]:
|
|
91
94
|
a[key] = value
|
|
92
95
|
break
|
|
93
96
|
else:
|
|
@@ -98,8 +101,8 @@ class DictionaryPlus(dict):
|
|
|
98
101
|
pass
|
|
99
102
|
else:
|
|
100
103
|
try:
|
|
101
|
-
if type(j)
|
|
102
|
-
if
|
|
104
|
+
if type(j) is not list:
|
|
105
|
+
if getattr(value, i) in [j]:
|
|
103
106
|
a[key] = value
|
|
104
107
|
break
|
|
105
108
|
else:
|
|
@@ -109,14 +112,14 @@ class DictionaryPlus(dict):
|
|
|
109
112
|
except:
|
|
110
113
|
pass
|
|
111
114
|
|
|
112
|
-
if filter_style == 'negative':
|
|
115
|
+
if filter_style == 'negative all':
|
|
113
116
|
a = {key: value for key, value in return_dict.items()}
|
|
114
117
|
for key, value in return_dict.items():
|
|
115
118
|
for i, j in filter_dict.items():
|
|
116
119
|
if hasattr(value, i):
|
|
117
120
|
try:
|
|
118
|
-
if type(j)
|
|
119
|
-
if
|
|
121
|
+
if type(j) is not list:
|
|
122
|
+
if getattr(value, i) in [j]:
|
|
120
123
|
del a[key]
|
|
121
124
|
break
|
|
122
125
|
else:
|
|
@@ -127,8 +130,8 @@ class DictionaryPlus(dict):
|
|
|
127
130
|
pass
|
|
128
131
|
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
129
132
|
try:
|
|
130
|
-
if type(j)
|
|
131
|
-
if
|
|
133
|
+
if type(j) is not list:
|
|
134
|
+
if getattr(value, 'm')[i] in [j]:
|
|
132
135
|
del a[key]
|
|
133
136
|
break
|
|
134
137
|
else:
|
|
@@ -140,6 +143,40 @@ class DictionaryPlus(dict):
|
|
|
140
143
|
else:
|
|
141
144
|
break
|
|
142
145
|
|
|
146
|
+
if filter_style == 'negative any':
|
|
147
|
+
b = {key: value for key, value in return_dict.items()}
|
|
148
|
+
for key, value in return_dict.items():
|
|
149
|
+
for i, j in filter_dict.items():
|
|
150
|
+
if hasattr(value, i):
|
|
151
|
+
try:
|
|
152
|
+
if type(j) is not list:
|
|
153
|
+
if getattr(value, i) not in [j]:
|
|
154
|
+
del b[key]
|
|
155
|
+
break
|
|
156
|
+
else:
|
|
157
|
+
if getattr(value, i) not in j:
|
|
158
|
+
del b[key]
|
|
159
|
+
break
|
|
160
|
+
except:
|
|
161
|
+
pass
|
|
162
|
+
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
163
|
+
try:
|
|
164
|
+
if type(j) is not list:
|
|
165
|
+
if getattr(value, 'm')[i] not in [j]:
|
|
166
|
+
del b[key]
|
|
167
|
+
break
|
|
168
|
+
else:
|
|
169
|
+
if getattr(value, 'm')[i] not in j:
|
|
170
|
+
del b[key]
|
|
171
|
+
break
|
|
172
|
+
except:
|
|
173
|
+
pass
|
|
174
|
+
else:
|
|
175
|
+
del b[key]
|
|
176
|
+
break
|
|
177
|
+
a = {key: value for key, value in return_dict.items()
|
|
178
|
+
if key not in b.keys()}
|
|
179
|
+
|
|
143
180
|
if condition != None:
|
|
144
181
|
if a == {}:
|
|
145
182
|
for key, value in return_dict.items():
|
|
@@ -264,14 +301,17 @@ class Dataset(DictionaryPlus):
|
|
|
264
301
|
return_dict = copy.deepcopy(self)
|
|
265
302
|
a = {}
|
|
266
303
|
|
|
304
|
+
if filter_style not in {'all', 'any', 'negative all', 'negative any'}:
|
|
305
|
+
return return_dict
|
|
306
|
+
|
|
267
307
|
if filter_style == 'all':
|
|
268
308
|
a = {key: value for key, value in return_dict.items()}
|
|
269
309
|
for key, value in return_dict.items():
|
|
270
310
|
for i, j in filter_dict.items():
|
|
271
311
|
if hasattr(value, i):
|
|
272
312
|
try:
|
|
273
|
-
if type(j)
|
|
274
|
-
if
|
|
313
|
+
if type(j) is not list:
|
|
314
|
+
if getattr(value, i) not in [j]:
|
|
275
315
|
del a[key]
|
|
276
316
|
break
|
|
277
317
|
else:
|
|
@@ -282,8 +322,8 @@ class Dataset(DictionaryPlus):
|
|
|
282
322
|
pass
|
|
283
323
|
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
284
324
|
try:
|
|
285
|
-
if type(j)
|
|
286
|
-
if
|
|
325
|
+
if type(j) is not list:
|
|
326
|
+
if getattr(value, 'm')[i] not in [j]:
|
|
287
327
|
del a[key]
|
|
288
328
|
break
|
|
289
329
|
else:
|
|
@@ -301,8 +341,8 @@ class Dataset(DictionaryPlus):
|
|
|
301
341
|
for i, j in filter_dict.items():
|
|
302
342
|
if hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
303
343
|
try:
|
|
304
|
-
if type(j)
|
|
305
|
-
if
|
|
344
|
+
if type(j) is not list:
|
|
345
|
+
if getattr(value, 'm')[i] in [j]:
|
|
306
346
|
a[key] = value
|
|
307
347
|
break
|
|
308
348
|
else:
|
|
@@ -313,8 +353,8 @@ class Dataset(DictionaryPlus):
|
|
|
313
353
|
pass
|
|
314
354
|
else:
|
|
315
355
|
try:
|
|
316
|
-
if type(j)
|
|
317
|
-
if
|
|
356
|
+
if type(j) is not list:
|
|
357
|
+
if getattr(value, i) in [j]:
|
|
318
358
|
a[key] = value
|
|
319
359
|
break
|
|
320
360
|
else:
|
|
@@ -324,14 +364,14 @@ class Dataset(DictionaryPlus):
|
|
|
324
364
|
except:
|
|
325
365
|
pass
|
|
326
366
|
|
|
327
|
-
if filter_style == 'negative':
|
|
367
|
+
if filter_style == 'negative all':
|
|
328
368
|
a = {key: value for key, value in return_dict.items()}
|
|
329
369
|
for key, value in return_dict.items():
|
|
330
370
|
for i, j in filter_dict.items():
|
|
331
371
|
if hasattr(value, i):
|
|
332
372
|
try:
|
|
333
|
-
if type(j)
|
|
334
|
-
if
|
|
373
|
+
if type(j) is not list:
|
|
374
|
+
if getattr(value, i) in [j]:
|
|
335
375
|
del a[key]
|
|
336
376
|
break
|
|
337
377
|
else:
|
|
@@ -342,8 +382,8 @@ class Dataset(DictionaryPlus):
|
|
|
342
382
|
pass
|
|
343
383
|
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
344
384
|
try:
|
|
345
|
-
if type(j)
|
|
346
|
-
if
|
|
385
|
+
if type(j) is not list:
|
|
386
|
+
if getattr(value, 'm')[i] in [j]:
|
|
347
387
|
del a[key]
|
|
348
388
|
break
|
|
349
389
|
else:
|
|
@@ -355,6 +395,39 @@ class Dataset(DictionaryPlus):
|
|
|
355
395
|
else:
|
|
356
396
|
break
|
|
357
397
|
|
|
398
|
+
if filter_style == 'negative any':
|
|
399
|
+
b = {key: value for key, value in return_dict.items()}
|
|
400
|
+
for key, value in return_dict.items():
|
|
401
|
+
for i, j in filter_dict.items():
|
|
402
|
+
if hasattr(value, i):
|
|
403
|
+
try:
|
|
404
|
+
if type(j) is not list:
|
|
405
|
+
if getattr(value, i) not in [j]:
|
|
406
|
+
del b[key]
|
|
407
|
+
break
|
|
408
|
+
else:
|
|
409
|
+
if getattr(value, i) not in j:
|
|
410
|
+
del b[key]
|
|
411
|
+
break
|
|
412
|
+
except:
|
|
413
|
+
pass
|
|
414
|
+
elif hasattr(value, 'm') & (type(value.m) == type({})) & (i in value.m.keys()):
|
|
415
|
+
try:
|
|
416
|
+
if type(j) is not list:
|
|
417
|
+
if getattr(value, 'm')[i] not in [j]:
|
|
418
|
+
del b[key]
|
|
419
|
+
break
|
|
420
|
+
else:
|
|
421
|
+
if getattr(value, 'm')[i] not in j:
|
|
422
|
+
del b[key]
|
|
423
|
+
break
|
|
424
|
+
except:
|
|
425
|
+
pass
|
|
426
|
+
else:
|
|
427
|
+
del b[key]
|
|
428
|
+
break
|
|
429
|
+
a = {key:value for key,value in return_dict.items() if key not in b.keys()}
|
|
430
|
+
|
|
358
431
|
if condition != None:
|
|
359
432
|
if a == {}:
|
|
360
433
|
for key, value in return_dict.items():
|
|
@@ -703,7 +776,6 @@ class Dataset(DictionaryPlus):
|
|
|
703
776
|
return pd.concat([out[k] for k in out.keys()]).reset_index()
|
|
704
777
|
return out
|
|
705
778
|
|
|
706
|
-
|
|
707
779
|
def save_data(self, directory="./saved/", levels=[]):
|
|
708
780
|
def get_specific_level(v, j):
|
|
709
781
|
if hasattr(v, j):
|
|
@@ -919,6 +991,8 @@ class Summary(pd.DataFrame):
|
|
|
919
991
|
# Only conditions referring to existing columns can be evaluated.
|
|
920
992
|
conditions = []
|
|
921
993
|
for column, values in filter_dict.items():
|
|
994
|
+
if type(values) is not list:
|
|
995
|
+
values = [values]
|
|
922
996
|
if column in return_dict.columns:
|
|
923
997
|
conditions.append(return_dict[column].isin(values).to_numpy())
|
|
924
998
|
|
|
@@ -44,7 +44,10 @@ def _default_x(data: pd.DataFrame) -> str:
|
|
|
44
44
|
for column in data.columns
|
|
45
45
|
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
46
46
|
]
|
|
47
|
-
|
|
47
|
+
if "time" in datetime_columns:
|
|
48
|
+
return "time"
|
|
49
|
+
else:
|
|
50
|
+
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
48
51
|
|
|
49
52
|
def _default_group(data: pd.DataFrame) -> str | None:
|
|
50
53
|
"""Return ``identifier`` when available."""
|
|
@@ -58,7 +61,6 @@ def _numeric_columns(data: pd.DataFrame) -> list[str]:
|
|
|
58
61
|
if pd.api.types.is_numeric_dtype(data[column])
|
|
59
62
|
]
|
|
60
63
|
|
|
61
|
-
|
|
62
64
|
def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
|
|
63
65
|
"""Validate and normalise an initial Y selection."""
|
|
64
66
|
numeric = _numeric_columns(data)
|
|
@@ -71,6 +73,25 @@ def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> l
|
|
|
71
73
|
return [str(column) for column in values]
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
def _categorical_columns(data: pd.DataFrame) -> list[str]:
|
|
77
|
+
"""Return columns suitable for a boxplot categorical X axis."""
|
|
78
|
+
return sorted(
|
|
79
|
+
[
|
|
80
|
+
str(column)
|
|
81
|
+
for column in data.columns
|
|
82
|
+
if not pd.api.types.is_numeric_dtype(data[column])
|
|
83
|
+
and not pd.api.types.is_datetime64_any_dtype(data[column])
|
|
84
|
+
],
|
|
85
|
+
key=str.casefold,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _alphabetical(values) -> list[str]:
|
|
90
|
+
"""Return string values sorted alphabetically, case-insensitively."""
|
|
91
|
+
return sorted((str(value) for value in values), key=str.casefold)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
74
95
|
def _subset_choices(data: pd.DataFrame, column: str) -> tuple[dict[str, str], dict[str, object]]:
|
|
75
96
|
"""Build stable Shiny choices and their corresponding original values."""
|
|
76
97
|
values = pd.unique(data[column].dropna())
|
|
@@ -250,9 +271,10 @@ def create_summary_app(
|
|
|
250
271
|
raise ValueError(
|
|
251
272
|
"Cannot create a Shiny application from an empty Summary.")
|
|
252
273
|
|
|
253
|
-
columns =
|
|
274
|
+
columns = _alphabetical(summary.columns)
|
|
254
275
|
column_map = {str(c): c for c in summary.columns}
|
|
255
|
-
numeric_columns = _numeric_columns(summary)
|
|
276
|
+
numeric_columns = _alphabetical(_numeric_columns(summary))
|
|
277
|
+
categorical_columns = _categorical_columns(summary)
|
|
256
278
|
|
|
257
279
|
x = _default_x(summary) if x is None else str(x)
|
|
258
280
|
if x not in column_map:
|
|
@@ -281,12 +303,8 @@ def create_summary_app(
|
|
|
281
303
|
ui.h4("Plot controls"),
|
|
282
304
|
ui.input_select("plot_type", "Plot type",
|
|
283
305
|
choices=PLOT_TYPES, selected=plot_type),
|
|
284
|
-
ui.
|
|
285
|
-
|
|
286
|
-
ui.input_selectize(
|
|
287
|
-
"y", "Y-axis / variable", choices=numeric_columns,
|
|
288
|
-
selected=initial_y, multiple=True,
|
|
289
|
-
),
|
|
306
|
+
ui.output_ui("x_control"),
|
|
307
|
+
ui.output_ui("y_control"),
|
|
290
308
|
ui.input_selectize(
|
|
291
309
|
"group_by", "Group by",
|
|
292
310
|
choices={c: c for c in columns},
|
|
@@ -338,6 +356,43 @@ def create_summary_app(
|
|
|
338
356
|
)
|
|
339
357
|
|
|
340
358
|
def server(input, output, session):
|
|
359
|
+
@render.ui
|
|
360
|
+
def x_control():
|
|
361
|
+
"""Use categorical X choices for boxplots; all columns otherwise."""
|
|
362
|
+
if input.plot_type() == "box":
|
|
363
|
+
if not categorical_columns:
|
|
364
|
+
return ui.help_text(
|
|
365
|
+
"Boxplots require at least one categorical column for the X axis."
|
|
366
|
+
)
|
|
367
|
+
selected = x if x in categorical_columns else categorical_columns[0]
|
|
368
|
+
return ui.input_select(
|
|
369
|
+
"x", "X-axis / category",
|
|
370
|
+
choices={c: c for c in categorical_columns},
|
|
371
|
+
selected=selected,
|
|
372
|
+
)
|
|
373
|
+
return ui.input_select(
|
|
374
|
+
"x", "X-axis / category",
|
|
375
|
+
choices={c: c for c in columns},
|
|
376
|
+
selected=x,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
@render.ui
|
|
380
|
+
def y_control():
|
|
381
|
+
"""Boxplots accept one numeric Y variable; other plots accept many."""
|
|
382
|
+
if input.plot_type() == "box":
|
|
383
|
+
selected = initial_y[0] if initial_y else (
|
|
384
|
+
numeric_columns[0] if numeric_columns else None)
|
|
385
|
+
return ui.input_select(
|
|
386
|
+
"y", "Y-axis / variable",
|
|
387
|
+
choices={c: c for c in numeric_columns},
|
|
388
|
+
selected=selected,
|
|
389
|
+
)
|
|
390
|
+
return ui.input_selectize(
|
|
391
|
+
"y", "Y-axis / variable",
|
|
392
|
+
choices={c: c for c in numeric_columns},
|
|
393
|
+
selected=initial_y, multiple=True,
|
|
394
|
+
)
|
|
395
|
+
|
|
341
396
|
@render.ui
|
|
342
397
|
def subset_filters():
|
|
343
398
|
"""Render one value selector for every selected subset column."""
|
|
@@ -395,7 +450,9 @@ def create_summary_app(
|
|
|
395
450
|
@reactive.calc
|
|
396
451
|
def selected_data():
|
|
397
452
|
current = filtered_summary()
|
|
398
|
-
|
|
453
|
+
y_input = input.y()
|
|
454
|
+
selected_y = ([y_input] if isinstance(
|
|
455
|
+
y_input, str) else list(y_input or []))
|
|
399
456
|
if not selected_y:
|
|
400
457
|
return pd.DataFrame()
|
|
401
458
|
|
|
@@ -405,9 +462,18 @@ def create_summary_app(
|
|
|
405
462
|
frequency = FREQUENCIES[input.frequency()]
|
|
406
463
|
aggregate = AGGREGATES[input.aggregate()]
|
|
407
464
|
|
|
465
|
+
# A boxplot's categorical X must survive group_stat(). Include it
|
|
466
|
+
# as a statistical grouping key when frequency aggregation is used,
|
|
467
|
+
# without turning it into a colour/group selection in the UI.
|
|
468
|
+
if input.plot_type() == "box":
|
|
469
|
+
selected_x = input.x()
|
|
470
|
+
if selected_x in column_map:
|
|
471
|
+
box_x = column_map[selected_x]
|
|
472
|
+
if box_x not in grouping_by:
|
|
473
|
+
grouping_by = [*grouping_by, box_x]
|
|
474
|
+
|
|
408
475
|
# All selected grouping columns are passed directly to
|
|
409
|
-
# Summary.group_stat() when a frequency is selected.
|
|
410
|
-
# data they remain plotting-only grouping columns.
|
|
476
|
+
# Summary.group_stat() when a frequency is selected.
|
|
411
477
|
if frequency is not None:
|
|
412
478
|
current = _aggregate(current, y_columns,
|
|
413
479
|
frequency, grouping_by, aggregate)
|
|
@@ -419,7 +485,9 @@ def create_summary_app(
|
|
|
419
485
|
plot_df = selected_data()
|
|
420
486
|
selected_plot = input.plot_type()
|
|
421
487
|
selected_x = input.x()
|
|
422
|
-
|
|
488
|
+
y_input = input.y()
|
|
489
|
+
selected_y = ([y_input] if isinstance(
|
|
490
|
+
y_input, str) else list(y_input or []))
|
|
423
491
|
selected_groups = list(input.group_by())
|
|
424
492
|
frequency = FREQUENCIES[input.frequency()]
|
|
425
493
|
|
|
@@ -441,7 +509,12 @@ def create_summary_app(
|
|
|
441
509
|
y_columns = _aggregated_value_columns(
|
|
442
510
|
plot_df, selected_y_columns, aggregate
|
|
443
511
|
)
|
|
444
|
-
|
|
512
|
+
if selected_plot == "box":
|
|
513
|
+
if selected_x not in column_map:
|
|
514
|
+
return px.scatter(title="Select a categorical X variable for the boxplot")
|
|
515
|
+
x_column = column_map[selected_x]
|
|
516
|
+
else:
|
|
517
|
+
x_column = frequency
|
|
445
518
|
|
|
446
519
|
if x_column not in plot_df.columns:
|
|
447
520
|
return px.scatter(
|
|
@@ -511,18 +584,23 @@ def create_summary_app(
|
|
|
511
584
|
opacity=0.65 if len(y_columns) > 1 else 1.0,
|
|
512
585
|
)
|
|
513
586
|
elif selected_plot == "box":
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
587
|
+
# Boxplots are distributions of one numerical variable across
|
|
588
|
+
# categories. X is therefore always the selected categorical
|
|
589
|
+
# column and Y is exactly one numerical variable.
|
|
590
|
+
if x_column not in categorical_columns:
|
|
591
|
+
return px.scatter(
|
|
592
|
+
title="Boxplots require a categorical variable on the X axis"
|
|
593
|
+
)
|
|
594
|
+
y_column = y_columns[0]
|
|
595
|
+
if group_columns:
|
|
596
|
+
fig = px.box(
|
|
597
|
+
plot_df, x=x_column, y=y_column,
|
|
598
|
+
color="__plot_group", points="outliers",
|
|
599
|
+
)
|
|
519
600
|
else:
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
id_vars=[group_column], var_name="variable", value_name="value",
|
|
601
|
+
fig = px.box(
|
|
602
|
+
plot_df, x=x_column, y=y_column, points="outliers"
|
|
523
603
|
)
|
|
524
|
-
fig = px.box(long_df, x="variable", y="value",
|
|
525
|
-
color=group_column, points="outliers")
|
|
526
604
|
elif selected_plot == "bar":
|
|
527
605
|
if not group_columns:
|
|
528
606
|
fig = px.bar(plot_df, x=x_column,
|
|
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
|