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