Initial commit
This commit is contained in:
commit
36e622dbd9
15
assets/otomoto.png
Normal file
15
assets/otomoto.png
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 94 13">
|
||||||
|
<path fill="#142355" d="M14.23.71L13.8 3.1h3.74l-1.46 8.34H19l1.46-8.34h3.8l.43-2.38z"/>
|
||||||
|
<path fill="#C82814" d="M50.7.71L47.18 4.3 44.9.71h-2.7l-1.88 10.73h2.77l1.14-6.56 1.71 2.99h1.36l2.66-2.96-1.13 6.53h2.76L53.47.7z"/>
|
||||||
|
<path fill="#142355" d="M69.49.71l-.44 2.38h3.75l-1.46 8.34h2.9L75.7 3.1h3.8l.43-2.38z"/>
|
||||||
|
<g transform="translate(.5 -.1)">
|
||||||
|
<mask id="b" fill="#fff">
|
||||||
|
<path id="a" d="M0 .23v10.16h10.16V.23H0z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill="#C82814" d="M1.74 10.39a5.96 5.96 0 010-8.43 5.96 5.96 0 018.42.01l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z" mask="url(#b)"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#142355" d="M10.67 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm17.21 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.43 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#C82814" d="M36.31 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.42 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm21.18 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.42 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#142355" d="M65.92 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm17.21 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.43 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#C82814" d="M91.56 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.23 0 2.99 2.99 0 000-4.22l2.1-2.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
15
assets/otomoto.svg
Normal file
15
assets/otomoto.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 94 13">
|
||||||
|
<path fill="#142355" d="M14.23.71L13.8 3.1h3.74l-1.46 8.34H19l1.46-8.34h3.8l.43-2.38z"/>
|
||||||
|
<path fill="#C82814" d="M50.7.71L47.18 4.3 44.9.71h-2.7l-1.88 10.73h2.77l1.14-6.56 1.71 2.99h1.36l2.66-2.96-1.13 6.53h2.76L53.47.7z"/>
|
||||||
|
<path fill="#142355" d="M69.49.71l-.44 2.38h3.75l-1.46 8.34h2.9L75.7 3.1h3.8l.43-2.38z"/>
|
||||||
|
<g transform="translate(.5 -.1)">
|
||||||
|
<mask id="b" fill="#fff">
|
||||||
|
<path id="a" d="M0 .23v10.16h10.16V.23H0z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill="#C82814" d="M1.74 10.39a5.96 5.96 0 010-8.43 5.96 5.96 0 018.42.01l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z" mask="url(#b)"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#142355" d="M10.67 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm17.21 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.43 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#C82814" d="M36.31 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.42 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm21.18 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.42 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#142355" d="M65.92 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.22 0 2.99 2.99 0 000-4.22l2.1-2.1zm17.21 8.42a5.96 5.96 0 010-8.42 5.96 5.96 0 018.43 0l-2.1 2.1a2.98 2.98 0 00-4.22 0 2.99 2.99 0 000 4.22l-2.1 2.1z"/>
|
||||||
|
<path fill="#C82814" d="M91.56 1.86a5.96 5.96 0 010 8.43 5.96 5.96 0 01-8.43 0l2.1-2.11a2.99 2.99 0 004.23 0 2.99 2.99 0 000-4.22l2.1-2.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
67
assets/style.css
Normal file
67
assets/style.css
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16rem;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-left: 18rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-wrapper {
|
||||||
|
max-width: 650px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphs-left {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphs-right {
|
||||||
|
width: 55%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine_vol_histogram {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price_boxplot {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordcloud {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-table {
|
||||||
|
font-family: system-ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header-name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: #a3a3a3 !important;
|
||||||
|
}
|
BIN
assets/wordcloud.png
Normal file
BIN
assets/wordcloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
152944
car_prices.csv
Normal file
152944
car_prices.csv
Normal file
File diff suppressed because it is too large
Load Diff
25
generate_wordcloud.py
Normal file
25
generate_wordcloud.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from wordcloud import WordCloud
|
||||||
|
import wordcloud
|
||||||
|
|
||||||
|
cars_df = pd.read_csv("car_prices.csv")
|
||||||
|
cars_df = cars_df.drop(["title", "link"], axis=1)
|
||||||
|
cars_df = cars_df[cars_df["fuel"] != "LPG"]
|
||||||
|
cars_df = cars_df[cars_df["vol_engine"] > 500]
|
||||||
|
cars_df = cars_df[cars_df["price"] < 2_500_000]
|
||||||
|
cars_df = cars_df[cars_df["year"] > 1990]
|
||||||
|
cars_df["vol_engine"] = cars_df["vol_engine"] / 1000
|
||||||
|
cars_df.loc[cars_df["year"] == 2023, "year"] = 2022
|
||||||
|
cars_df["mark"] = cars_df["mark"].apply(lambda x: x.capitalize())
|
||||||
|
cars_df.loc[cars_df["mark"]=="Mercedes-benz"] = "MercedesBenz"
|
||||||
|
cars_df.loc[cars_df["mark"]=="Alfa-romeo"] = "AlfaRomeo"
|
||||||
|
|
||||||
|
|
||||||
|
WordCloud (
|
||||||
|
background_color = 'white',
|
||||||
|
width = 512,
|
||||||
|
height = 250,
|
||||||
|
collocations=False
|
||||||
|
).generate_from_text(' '.join(cars_df["mark"])).to_file('assets/wordcloud.png')
|
||||||
|
|
||||||
|
|
244
main.py
Normal file
244
main.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import dash
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
import plotly.express as px
|
||||||
|
from dash.dependencies import Input, Output
|
||||||
|
from dash import dcc, html, dash_table
|
||||||
|
from wordcloud import WordCloud
|
||||||
|
from utils.graphs import create_engine_vol_histogram, create_price_boxplot
|
||||||
|
from utils.data import cleanup
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PAGE_SIZE = 15
|
||||||
|
|
||||||
|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
|
||||||
|
app.config.suppress_callback_exceptions = True
|
||||||
|
|
||||||
|
df = pd.read_csv("car_prices.csv")
|
||||||
|
df = cleanup(df)
|
||||||
|
|
||||||
|
engine_vol_fig = create_engine_vol_histogram(df)
|
||||||
|
|
||||||
|
price_boxplot_fig = px.box(df, x='year', y='price')
|
||||||
|
total_cars = df.shape[0]
|
||||||
|
average_price = int(df['price'].mean())
|
||||||
|
average_mileage = int(df['mileage'].mean())
|
||||||
|
|
||||||
|
sidebar = html.Div(
|
||||||
|
[
|
||||||
|
html.Img(src=app.get_asset_url("otomoto.svg"), width=222),
|
||||||
|
html.Hr(),
|
||||||
|
dbc.Nav(
|
||||||
|
[
|
||||||
|
dbc.NavLink("Main Dashboard", href="/", active="exact"),
|
||||||
|
dbc.NavLink("Data Explorer", href="/page-1", active="exact"),
|
||||||
|
dbc.NavLink("Documentation", href="/page-2", active="exact"),
|
||||||
|
],
|
||||||
|
vertical=True,
|
||||||
|
pills=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="sidebar"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = html.Div(id="page-content", children=[], className="content")
|
||||||
|
|
||||||
|
app.layout = html.Div([
|
||||||
|
dcc.Location(id="url"),
|
||||||
|
sidebar,
|
||||||
|
content
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("page-content", "children"),
|
||||||
|
[Input("url", "pathname")]
|
||||||
|
)
|
||||||
|
def render_page_content(pathname):
|
||||||
|
if pathname == "/":
|
||||||
|
return [
|
||||||
|
dbc.Card(
|
||||||
|
dbc.CardBody([
|
||||||
|
dbc.Button([
|
||||||
|
"Total cars",
|
||||||
|
dbc.Badge(total_cars, color="light", text_color="primary", className="ms-1"),
|
||||||
|
],
|
||||||
|
color="primary",
|
||||||
|
),
|
||||||
|
dbc.Button(
|
||||||
|
[
|
||||||
|
"Average price",
|
||||||
|
dbc.Badge(average_price, color="light", text_color="primary", className="ms-1"),
|
||||||
|
],
|
||||||
|
color="primary",
|
||||||
|
),
|
||||||
|
dbc.Button(
|
||||||
|
[
|
||||||
|
"Average mileage",
|
||||||
|
dbc.Badge(average_mileage, color="light", text_color="primary", className="ms-1"),
|
||||||
|
],
|
||||||
|
color="primary",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="summary"),
|
||||||
|
className="summary-wrapper"),
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div([
|
||||||
|
html.Img(
|
||||||
|
id="word_cloud",
|
||||||
|
src=app.get_asset_url("wordcloud.png"),
|
||||||
|
className="wordcloud"
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
id='bargraph',
|
||||||
|
figure=engine_vol_fig,
|
||||||
|
className="engine_vol_histogram"
|
||||||
|
)
|
||||||
|
], className="graphs-left"),
|
||||||
|
html.Div([
|
||||||
|
dbc.Select(
|
||||||
|
id="select",
|
||||||
|
options=[{'label': 'All', 'value': 'All' }] + [{ 'label': x, 'value': x } for x in df['mark'].unique()]
|
||||||
|
),
|
||||||
|
dcc.Graph(id='price_boxplot', className='price-boxplot')
|
||||||
|
], className="graphs-right")
|
||||||
|
],
|
||||||
|
className="graphs"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
elif pathname == "/page-1":
|
||||||
|
return [
|
||||||
|
html.H1('Data explorer'),
|
||||||
|
dash_table.DataTable(
|
||||||
|
id='data-explorer',
|
||||||
|
columns=[
|
||||||
|
{'name': i, 'id': i, 'deletable': True} for i in sorted(df.columns)
|
||||||
|
],
|
||||||
|
page_current= 0,
|
||||||
|
page_size= PAGE_SIZE,
|
||||||
|
page_action='custom',
|
||||||
|
|
||||||
|
filter_action='custom',
|
||||||
|
filter_query='',
|
||||||
|
|
||||||
|
sort_action='custom',
|
||||||
|
sort_mode='multi',
|
||||||
|
sort_by=[],
|
||||||
|
|
||||||
|
style_header={
|
||||||
|
'backgroundColor': 'rgb(30, 30, 30)',
|
||||||
|
'color': 'white'
|
||||||
|
},
|
||||||
|
style_filter={
|
||||||
|
'backgroundColor': 'rgb(30, 30, 30)',
|
||||||
|
'color': 'white'
|
||||||
|
},
|
||||||
|
style_data={
|
||||||
|
'backgroundColor': 'rgb(50, 50, 50)',
|
||||||
|
'color': 'white'
|
||||||
|
},
|
||||||
|
style_cell={
|
||||||
|
'color': 'white'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
elif pathname == "/page-2":
|
||||||
|
return [
|
||||||
|
html.H1('High School in Iran',
|
||||||
|
style={'textAlign':'center'}),
|
||||||
|
dcc.Graph(id='bargraph',
|
||||||
|
figure=px.bar(df, barmode='group', x='Years',
|
||||||
|
y=['Girls High School', 'Boys High School']))
|
||||||
|
]
|
||||||
|
# If the user tries to reach a different page, return a 404 message
|
||||||
|
return dbc.Jumbotron(
|
||||||
|
[
|
||||||
|
html.H1("404: Not found", className="text-danger"),
|
||||||
|
html.Hr(),
|
||||||
|
html.P(f"The pathname {pathname} was not recognised..."),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output('price_boxplot', 'figure'),
|
||||||
|
Input('select', 'value')
|
||||||
|
)
|
||||||
|
def update_output(value):
|
||||||
|
filtered_df = df
|
||||||
|
if (value != 'All' and value != None):
|
||||||
|
filtered_df = df.loc[df['mark'] == value]
|
||||||
|
|
||||||
|
return create_price_boxplot(filtered_df)
|
||||||
|
|
||||||
|
operators = [['ge ', '>='],
|
||||||
|
['le ', '<='],
|
||||||
|
['lt ', '<'],
|
||||||
|
['gt ', '>'],
|
||||||
|
['ne ', '!='],
|
||||||
|
['eq ', '='],
|
||||||
|
['contains '],
|
||||||
|
['datestartswith ']]
|
||||||
|
|
||||||
|
|
||||||
|
def split_filter_part(filter_part):
|
||||||
|
for operator_type in operators:
|
||||||
|
for operator in operator_type:
|
||||||
|
if operator in filter_part:
|
||||||
|
name_part, value_part = filter_part.split(operator, 1)
|
||||||
|
name = name_part[name_part.find('{') + 1: name_part.rfind('}')]
|
||||||
|
|
||||||
|
value_part = value_part.strip()
|
||||||
|
v0 = value_part[0]
|
||||||
|
if (v0 == value_part[-1] and v0 in ("'", '"', '`')):
|
||||||
|
value = value_part[1: -1].replace('\\' + v0, v0)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = float(value_part)
|
||||||
|
except ValueError:
|
||||||
|
value = value_part
|
||||||
|
|
||||||
|
# word operators need spaces after them in the filter string,
|
||||||
|
# but we don't want these later
|
||||||
|
return name, operator_type[0].strip(), value
|
||||||
|
|
||||||
|
return [None] * 3
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output('data-explorer', 'data'),
|
||||||
|
Input('data-explorer', "page_current"),
|
||||||
|
Input('data-explorer', "page_size"),
|
||||||
|
Input('data-explorer', 'sort_by'),
|
||||||
|
Input('data-explorer', 'filter_query'))
|
||||||
|
def update_table(page_current, page_size, sort_by, filter):
|
||||||
|
filtering_expressions = filter.split(' && ')
|
||||||
|
dff = df
|
||||||
|
for filter_part in filtering_expressions:
|
||||||
|
col_name, operator, filter_value = split_filter_part(filter_part)
|
||||||
|
|
||||||
|
if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'):
|
||||||
|
# these operators match pandas series operator method names
|
||||||
|
dff = dff.loc[getattr(dff[col_name], operator)(filter_value)]
|
||||||
|
elif operator == 'contains':
|
||||||
|
dff = dff.loc[dff[col_name].str.contains(filter_value)]
|
||||||
|
elif operator == 'datestartswith':
|
||||||
|
# this is a simplification of the front-end filtering logic,
|
||||||
|
# only works with complete fields in standard format
|
||||||
|
dff = dff.loc[dff[col_name].str.startswith(filter_value)]
|
||||||
|
|
||||||
|
if len(sort_by):
|
||||||
|
dff = dff.sort_values(
|
||||||
|
[col['column_id'] for col in sort_by],
|
||||||
|
ascending=[
|
||||||
|
col['direction'] == 'asc'
|
||||||
|
for col in sort_by
|
||||||
|
],
|
||||||
|
inplace=False
|
||||||
|
)
|
||||||
|
|
||||||
|
page = page_current
|
||||||
|
size = page_size
|
||||||
|
return dff.iloc[page * size: (page + 1) * size].to_dict('records')
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
app.run_server(debug=True, port=3000)
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
13
utils/data.py
Normal file
13
utils/data.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
def cleanup(df):
|
||||||
|
df = df.drop(["title", "link"], axis=1)
|
||||||
|
df = df[df["fuel"] != "LPG"]
|
||||||
|
df = df[df["vol_engine"] > 500]
|
||||||
|
df = df[df["price"] < 2_500_000]
|
||||||
|
df = df[df["year"] > 1990]
|
||||||
|
df["vol_engine"] = df["vol_engine"] / 1000
|
||||||
|
df.loc[df["year"] == 2023, "year"] = 2022
|
||||||
|
df["mark"] = df["mark"].apply(lambda x: x.capitalize())
|
||||||
|
df.loc[df["mark"]=="Mercedes-benz", 'mark'] = "MercedesBenz"
|
||||||
|
df.loc[df["mark"]=="Alfa-romeo", 'mark'] = "AlfaRomeo"
|
||||||
|
|
||||||
|
return df
|
45
utils/graphs.py
Normal file
45
utils/graphs.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import plotly.express as px
|
||||||
|
|
||||||
|
def create_engine_vol_histogram(df):
|
||||||
|
fig = px.histogram(df, x="vol_engine", nbins=40)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
xaxis = {
|
||||||
|
'title': 'Engine volume',
|
||||||
|
'visible': True,
|
||||||
|
'showticklabels': True
|
||||||
|
},
|
||||||
|
yaxis = {
|
||||||
|
'title': '',
|
||||||
|
'visible': True,
|
||||||
|
'showticklabels': True
|
||||||
|
},
|
||||||
|
margin = {
|
||||||
|
'l': 0, # left
|
||||||
|
'r': 10, # right
|
||||||
|
't': 50, # top
|
||||||
|
'b': 10, # bottom
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def create_price_boxplot(df):
|
||||||
|
fig = px.box(df, x='year', y='price')
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
xaxis = {
|
||||||
|
'title': 'Year'
|
||||||
|
},
|
||||||
|
yaxis = {
|
||||||
|
'title': 'Price'
|
||||||
|
},
|
||||||
|
margin = {
|
||||||
|
'l': 0,
|
||||||
|
'r': 0,
|
||||||
|
't': 50,
|
||||||
|
'b': 10
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
Loading…
Reference in New Issue
Block a user