Commit 531ea622 authored by Samuël Weber's avatar Samuël Weber
Browse files

add app estimate

parent 04712192
......@@ -585,201 +585,52 @@ def get_about_app_results_component():
)
return component
def about_graph_source_contribution():
station_order = ["REV", "MRS", "PdB", "PROV", "NIC", "TAL", "POI", "LEN",
"NGT", "ROU", "LY", "GRE", "CHAM", "RBX", "STRAS"]
conn = sqlite3.connect("./DB_SOURCES.db")
df = pd.read_sql(
"SELECT * FROM profiles_constrained WHERE specie IN ('PM10');",
con=conn
)
conn.close()
df.drop("index", axis=1, inplace=True)
df.set_index(["station", "specie"], inplace=True)
# dfn = (df.T / df.sum(axis=1)).T * 100
sources = [
"Nitrate rich", "Biomass burning", "Primary traffic", "Sulfate rich",
"Dust", "Aged seasalt", "Primary biogenic", "Seasalt", "Marine SOA"
]
sources.sort()
# other_sources = [s for s in df.columns if s not in sources]
traces = []
# for axe, DF in zip(["y1", "y2"], [dfn, df]):
for axe, DF in zip(["y1"], [df]):
for source in sources:
traces.append(
go.Bar(
x=DF.index.get_level_values("station"),
y=DF.loc[(slice(None), "PM10"), source],
name=source,
marker=dict(
color=get_sourceColor(source)
),
xaxis='x',
yaxis=axe,
showlegend=True if axe == "y1" else False,
legendgroup=source
def get_options_estimate_component():
"""TODO: Docstring for get_options_estimate.
:returns: TODO
"""
component = dbc.Row(
dbc.Col(
id="options",
children=[
dbc.RadioItems(
id='option-OPtype',
className="options-items",
options=[
{"label": "OP DTTv", "value": "DTTv"},
{"label": "OP AAv", "value": "AAv"},
],
value="DTTv"
),
dbc.RadioItems(
id='option-temporality',
className="options-items",
options=[
{"label": "Raw (same as input)", "value": "raw"},
{"label": "Monthly", "value": "monthly"},
],
value="monthly"
)
)
# traces.append(
# go.Bar(
# x=DF.index.get_level_values("station"),
# y=DF.loc[(slice(None), "PM10"), other_sources].sum(axis=1),
# name="Other",
# marker=dict(
# color="grey"
# ),
# xaxis='x',
# yaxis=axe,
# showlegend=True if axe == "y1" else False,
# legendgroup="Other"
# )
# )
layout = go.Layout(
xaxis={
'categoryorder': 'array',
'categoryarray': station_order,
},
yaxis1={
'title': 'Mean annual contribution (µg m⁻³)',
# 'domain': [0.00, 0.48]
},
# yaxis1={
# 'title': 'Normalized contribution (%)',
# 'domain': [0.00, 0.48]
# },
# yaxis2={
# 'title': 'Mean annual contribution (µg m⁻³)',
# 'domain': [0.52, 1]
# },
barmode="stack",
height=400,
# width=1200,
margin=go.layout.Margin(t=20, b=30)
]
),
)
return component
graph = dcc.Graph(
id='contrib-graph',
className='graph_main_page',
figure={'data': traces, 'layout': layout}
)
return graph
def about_graph_similarities():
traces = []
layout = go.Layout(
hovermode="closest",
yaxis={
"title": "PD",
"range": [0, 1],
"scaleratio": 1
},
xaxis={
"title": "SID",
"range": [0, 1.5],
"scaleanchor": "y"
},
showlegend=True,
margin=go.layout.Margin(t=20, b=20),
legend=dict(orientation="v"),
shapes=[
{
'type': 'rect',
'x0': 0,
'y0': 0,
'x1': 1,
'y1': 0.4,
'line': {
'width': 0,
},
'fillcolor': 'rgba(0, 255, 0, 0.1)',
},
]
)
def get_estimate_warning_component():
"""TODO: Docstring for get_estimate_warning_component.
:returns: TODO
DBPATH = "./DB_SOURCES.db"
conn = sqlite3.connect(DBPATH)
SID = pd.read_sql(
"SELECT * FROM SID;",
con=conn,
)
PD = pd.read_sql(
"SELECT * FROM PD;",
con=conn,
"""
component = dbc.Row(
dbc.Col(
width=12, sm=12, md=11, lg=10, xl=9,
children=dcc.Markdown(open("estimate.mdwn", "r").read(), dangerously_allow_html=True)
),
justify="center",
)
conn.close()
SID.set_index(["source", "station"], inplace=True)
PD.set_index(["source", "station"], inplace=True)
if "index" in SID.columns:
SID.drop("index", axis=1, inplace=True)
if "index" in PD.columns:
PD.drop("index", axis=1, inplace=True)
# sources = SID.index.get_level_values("source").unique()
sources = [
"Nitrate rich", "Biomass burning", "Primary traffic", "Sulfate rich",
"Dust", "Aged seasalt", "Primary biogenic", "Seasalt", "Marine SOA"
]
sources.sort()
x = SID.loc[(sources, slice(None)), :].reset_index()\
.melt(id_vars=["source", "station"])\
.set_index(["source", "station"])\
.loc[sources]
y = PD.loc[(sources, slice(None)), :].reset_index()\
.melt(id_vars=["source", "station"])\
.set_index(["source", "station"])\
.loc[sources]
x = x[x["value"] > 0].round(3)
y = y[y["value"] > 0].round(3)
for source in sources: # get a second loop for the Z order of the plots
if len(x.xs(source, level="source").dropna()["variable"].unique()) <= 1:
continue
traces.append(
go.Scatter(
x=[x.xs(source, level="source")["value"].mean().round(3)],
y=[y.xs(source, level="source")["value"].mean().round(3)],
error_x=dict(
visible=True,
type="constant",
symmetric=True,
value=x.xs(source, level="source")["value"].std().round(3)
),
error_y=dict(
visible=True,
type="constant",
symmetric=True,
value=y.xs(source, level="source")["value"].std().round(3)
),
mode="markers",
marker=go.scatter.Marker(
color=get_sourceColor(source),
size=18,
line=dict(
color='#aaaaaa',
width=2
)
),
hovertext="Mean±std",
name=source,
)
)
graph = dcc.Graph(
id='similarity-graph',
className='graph_main_page',
figure={'data': traces, 'layout': layout}
)
return graph
return component
def plot_ts(df, station, var, groupby):
"""Set a trace for plotly of the timeserie var in df for the given station,
......
import dash_core_components as dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
import dash_html_components as html
import dash_table
import plotly.express as px
import io
import base64
import pandas as pd
from app_main import app
import apps.app_components as ac
import settings
import pyOPestimator
NCLICKED = 0
layout = dbc.Container(
id="container-results",
children=[
dbc.Row([
dbc.Col(
id="upload-col",
children=[
dcc.Upload(
id='upload-data',
children=html.Div([
dcc.Markdown("""
[Select Files](#ref) (`.csv` or `.xls(x)` with a `Date` and `PM10` column)
""",
id="select-file"
),
]),
style={
'width': '100%',
'height': '60px',
'lineHeight': '60px',
'borderWidth': '1px',
'borderStyle': 'dashed',
'borderRadius': '5px',
'textAlign': 'center',
'margin': '10px'
},
# Allow multiple files to be uploaded
multiple=False
),
html.Div(id='flashinfo'),
dbc.Row(dbc.Col(dbc.Button("Get example data",
id="get_data"))),
html.Div(
id='output-data-upload',
children=[
dash_table.DataTable(
id="datatable",
sort_action="native",
sort_mode="multi",
filter_action='native',
page_size=20,
style_cell={
'overflow': 'hidden',
'textOverflow': 'ellipsis',
'maxWidth': 0,
},
),
]
),
]
),
dbc.Col(
id="graph-col",
width=9,
children=[
ac.get_options_estimate_component(),
dcc.Loading(dcc.Graph(id="graph")),
ac.get_estimate_warning_component()
]
)
])
],
fluid=True,
# end container
)
def parse_contents(contents, filename):
content_type, content_string = contents.split(',')
decoded = base64.b64decode(content_string)
errors = []
data = []
columns = []
if 'csv' in filename or 'xls' in filename:
try:
if 'csv' in filename:
# Assume that the user uploaded a CSV file
df = pd.read_csv(
io.StringIO(decoded.decode('utf-8')))
elif 'xls' in filename:
# Assume that the user uploaded an excel file
df = pd.read_excel(io.BytesIO(decoded))
data = df.to_dict('records')
columns = [{'name': i, 'id': i} for i in df.columns]
except Exception as e:
errors.append(e)
else:
errors.append("It should be a 'csv' or 'xls(x)' file.")
return (data, columns, errors)
@app.callback([
Output('datatable', 'data'),
Output('datatable', 'columns'),
Output('flashinfo', 'children'),
],
[
Input('upload-data', 'contents'),
Input('upload-data', 'filename'),
Input('get_data', 'n_clicks'),
],
)
def update_output(content, name, clicked):
errors = []
global NCLICKED
if clicked > NCLICKED:
df = pyOPestimator.load_dataset("atmoaura_GRE-fr_pm10")
data = df.to_dict('records')
columns = [{'name': i, 'id': i} for i in df.columns]
NCLICKED += 1
else:
if content is not None:
data, columns, errors = parse_contents(content, name)
colnames = [item['name'] for item in columns]
if "Date" not in colnames:
errors.append("The input data must have a 'Date' column")
if "PM10" not in colnames:
errors.append("The input data must have a 'PM10' column")
if len(errors) == 0:
flash = dbc.Alert(
"File uploaded succesfully",
color="primary",
dismissable=True,
)
else:
flash = dbc.Alert(
[
"An error occured."
] + [html.Ul([html.Li(e) for e in errors])],
color="danger",
dismissable=True,
)
return (data, columns, flash)
@app.callback(Output('graph', 'figure'),
[Input('datatable', 'derived_virtual_data'),
Input('option-OPtype', 'value'),
Input('option-temporality', 'value'),
])
def update_graph(dfpm, OPtype, temporality):
"""update the estimated PM OP graph
:dfpm: TODO
:returns: TODO
"""
if dfpm is None:
raise PreventUpdate
dfpm = pd.DataFrame(dfpm)
dfpm["Date"] = pd.to_datetime(dfpm["Date"])
dfop = pyOPestimator.get_op_from_pm10(dfpm, OPtype)
factors = list(dfop.drop(["Date", "month", "PM10", "totalOP"], axis=1).columns)
if temporality == "monthly":
dfop = dfop.groupby(
pd.Grouper(key="Date", freq="M")
).mean()
dfop = dfop.shift(-15, freq="D").reset_index()
dfop = dfop.melt(id_vars=["Date", "totalOP", "month", "PM10"], var_name="Factor",
value_name="OP")
colors = {}
for factor in factors:
colors[factor] = ac.get_sourceColor(source=factor)
fig = px.area(
dfop,
x="Date",
y="OP",
color="Factor",
color_discrete_map=colors
)
return fig
......@@ -6,11 +6,11 @@
margin-top: 10px;
}
#main h2 {
#main, h2 {
color: darkmagenta;
}
#main h3 {
#main, #graph-col h3 {
color: darkolivegreen;
}
......@@ -31,6 +31,10 @@
padding-top: 20px;
}
#select-file p {
text-align: center;
}
#boxplot-options {
/* display: flex; */
display: block;
......@@ -66,3 +70,7 @@ p {
.list-group-item.active {
z-index: 0 !important;
}
#upload-col {
max-width: 30%;
}
## How does it work?
### Method
Gathering several research program in France metropolitan area succeed to estimate a
climatology of the PM10 sources contribution to the ambient PM10 concentration.
We are then able to estimate a roughly monthly mean relative contribution of each sources to the PM10.
Then, for a given day of PM10 measurement, we can estimate the relative
contribution of a set of common sources found in the metropolitan territory.
We should keep in mind that this is a crude approximation since it does not
take into account for local specificities nor for variation over year of the
sources contributions.
Finally, thanks to the recent development of the scientific community, and notably
Weber et al. 2020 for the France area, we can attribute an oxidative
potential (OP) of a set of sources. A simple multiplication end up with the
sources contribution to the oxidative potential of PM.
### Pitfall
This method is a crude, first order approximation of the sources contribution
to the ambient PM10 concentration. The learning set is representative of
urbanized area over France, for year between 2013 to 2018.
The oxidative potential of the sources does present some variation for a given source
at different location. In this method we only take into account for the mean value of them.
For this two reason, this method should be used cautiously and only give a first
idea of what could be the OP of your PM10 timeserie.
......@@ -4,7 +4,7 @@ import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
from app_main import app
from apps import app_contact, app_results, app_deconvolOP
from apps import app_contact, app_results, app_deconvolOP, app_estimateOP
server = app.server
......@@ -18,6 +18,8 @@ app.layout = html.Div([
[
dbc.NavLink("Results", href="/results",
className="btn btn-info"),
dbc.NavLink("Estimate your OP", href="/estimate",
className="btn btn-secondary"),
dbc.NavLink("Question & contact", href="/contact"),
],
navbar=True,
......@@ -88,6 +90,8 @@ def display_page(pathname):
return app_deconvolOP.layout
elif pathname[:len('/results')] == '/results':
return app_results.layout
elif pathname[:len('/estimate')] == '/estimate':
return app_estimateOP.layout
elif pathname[:len('/contact')] == '/contact':
return app_contact.layout
else:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment