Initial commit

This commit is contained in:
Jakub Kaczmarek 2022-06-18 14:42:39 +02:00
commit 36e622dbd9
10 changed files with 153368 additions and 0 deletions

15
assets/otomoto.png Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

152944
car_prices.csv Normal file

File diff suppressed because it is too large Load Diff

25
generate_wordcloud.py Normal file
View 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
View 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
View File

13
utils/data.py Normal file
View 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
View 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