Por IMAM em 30 de julho de 2024
O projeto é construir este resultado
Introdução
Python é uma linguagem com inúmeros recursos. Dentre eles o Dash, uma biblioteca incrível que nos permite com relativa facilidade elaborar excelentes dashboards.
Vamos fazer um pequeno projeto para entendermos como funciona a biblioteca Dash.
O funcionamento básico da biblioteca é o seguinte:
Cria-se um layout. Como se estivesse criando um esqueleto de um website. Depois cria-se funções que recebem valores dos filtros, geram gráficos com esses filtros, e a saída da função (o gráfico) vai para o lugar desejado do layout.
É possível, inclusive, criar um efeito semelhante ao do Power BI – quando uma barra de um gráfico é clicada outros gráficos são filtrados.
Sim, a ideia é bem simples. Só demanda umas tantas calorias.
Base de Dados
A base que escolhemos para nosso projeto foi a “Brazilian E-Commerce Public Dataset by Olist” encontrada no site da Kaggle pelo link: Brazilian E-Commerce Public Dataset by Olist (kaggle.com). Nela temos 100 mil ordens de venda com a relação de produtos, valores, frete, data de emissão e de entrega, entre outras informações.
• olist_customers_dataset.csv
∘ customer_id
∘ customer_unique_id
∘ customer_zip_code_prefix
• olist_order_items_dataset.csv
∘ order_id
∘ order_item_id
∘ product_id
• olist_orders_dataset.csv
∘ order_id
∘ customer_id
∘ order_status
∘ order_purchase_timestamp
• olist_products_dataset.csv
∘ product_id
∘ product_category_name
∘ product_name_lenght
Bibliotecas utilizadas
Pandas e Numpy para carregamento e tratamento dos dados:
Plotly para gerar os gráficos.
Dash para gerar o dashboard.
1
2
3
4
5
6
|
import pandas as pd import numpy as np from dash import Dash, dcc, html, Input , Output, State, dash_table, callback import dash_bootstrap_components as dbc from plotly import graph_objects as go import plotly.subplots as sp |
Carregamento dos dados
A tabela base dos gráficos será a “olist_order_items_dataset” (no código chamaremos de ‘f_order_items’), porém ela não contém todas as colunas necessárias.
Vamos buscar o restante das colunas nas outras tabelas do dataset.
A tabela de itens vendidos é a olist_order_items:
Depois de adicionar a data de entrega, cidade e estado do cliente e categoria do produto as colunas ficaram assim:
Como os dados estão mais consistentes no período entre 01/02/2017 e 31/08/2018 vamos deixar os dados filtrados desde o início.
O código de toda esta etapa é o seguinte:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
app = Dash(__name__, external_stylesheets = [dbc.themes.BOOTSTRAP]) local_dados = "C:/Users/danil/OneDrive/Danilo_Back-up/Kaggle/Brasilian_Ecommerce_Olist/" d_product = pd.read_csv(f '{local_dados}products.csv' , sep = "," , decimal = "." ) d_customer = pd.read_csv(f '{local_dados}customers.csv' , sep = "," , decimal = "." ) f_orders = pd.read_csv(f '{local_dados}orders.csv' , sep = "," , decimal = "." ) f_order_items = pd.read_csv(f '{local_dados}order_items.csv' ,sep = "," , decimal = "." ) f_order_items = f_order_items.merge( f_orders[[ 'order_id' , 'customer_id' , 'order_delivered_customer_date' ]], how = 'left' , on = 'order_id' ).merge( d_product[[ 'product_id' , 'product_category_name' ]], how = 'left' , on = 'product_id' ).merge( d_customer[[ 'customer_id' , 'customer_city' , 'customer_state' ]], how = 'left' , on = 'customer_id' ).drop(columns = [ 'order_item_id' , 'shipping_limit_date' ]) f_order_items = f_order_items.assign( order_id = f_order_items[ 'order_id' ].astype( 'category' ), product_id = f_order_items[ 'product_id' ].astype( 'category' ), seller_id = f_order_items[ 'seller_id' ].astype( 'category' ), customer_id = f_order_items[ 'customer_id' ].astype( 'category' ), order_delivered_customer_date = f_order_items[ 'order_delivered_customer_date' ].astype( 'datetime64[ns]' ), product_category_name = f_order_items[ 'product_category_name' ].astype( 'category' ), customer_city = f_order_items[ 'customer_city' ].astype( 'category' ), customer_state = f_order_items[ 'customer_state' ].astype( 'category' ), ) f_order_items = f_order_items.loc[ (f_order_items[ 'order_delivered_customer_date' ] > = pd.to_datetime( '2017-02-01' )) & (f_order_items[ 'order_delivered_customer_date' ] < = pd.to_datetime( '2018-08-31' )) ] |
Definições gerais de Layout
Abaixo estão uma série de definições para a fonte e formatação dos textos, margens e cores de fundo.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
header_font_size = 28 header_font_family = 'consolas' header_color = '#6F432A' row_sep_height = 5 gap = 'g-1' body_height = 888 # divisível por 3 body_font = 17 body_color = '#D6CABA' body_font_family = 'consolas' body_margin_left = 4 body_margin_right = 4 |
Layout
Vamos dividir nosso layout em 3 linhas e 3 colunas.
A primeira linha terá o título do dashboard.
A segunda terá os filtros de data e categoria de produto.
A terceira terá os gráficos.
A linha dos gráficos será dividida em 3 colunas: a primeira com as vendas por mês e as vendas por estado, a segunda com as vendas por cidade, e a terceira com a contagem acumulada de clientes, vendedores e produtos.
O código para o layout ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
app.layout = dbc.Container([ # ======================= Header – 1ª linha ============================== dbc.Row([ dbc.Col([ html.Div( "Dashboard - Olist - Visão Geral" , id = "texto_header" , style = { 'font-size' :header_font_size, 'color' : 'white' , 'font-weight' : 'bold' , 'font-family' :header_font_family } ), ], width = { "size" : 12 , "order" : 1 , "offset" : 0 }, md = 0 , lg = 0 ) ], style = { 'marginLeft' : 0 , 'marginRight' : 0 , 'background' :header_color}), # ========================= Filtros – 2ª linha ============================ dbc.Row([ dbc.Col([ dcc.DatePickerRange( id = 'date_range' ), ], width = 5 ), dbc.Col([ dcc.Dropdown( id = 'category_dropdown' , ), ], width = 3 ), ], style = { 'marginLeft' : body_margin_left, 'marginRight' : body_margin_right, 'height' : 53 }, className = gap), # =========================== Body – 3ª linha ============================== dbc.Row([ # ================ Coluna 1 ======================== dbc.Col([ dbc.Row([ dcc.Graph( id = "graph_vendas_geral" , clear_on_unhover = True ) ], style = { 'height' :body_height / 3 * 2 - row_sep_height}), dbc.Row([], style = { 'height' :row_sep_height }), dbc.Row([ dcc.Graph( id = "graph_vendas_state" , clear_on_unhover = True ) ], style = { 'height' :body_height / 3 - row_sep_height}), ], width = 5 ), # ================ Coluna 2 ======================== dbc.Col([ dbc.Row([ dcc.Graph( id = "graph_vendas_city" , clear_on_unhover = True ) ], style = { 'height' :body_height - row_sep_height}), ], width = 3 ), # ================ Coluna 3 ======================== dbc.Col([ dbc.Row([ dcc.Graph( id = "graph_qtd_customer" ) ], style = { 'height' :body_height / 3 - row_sep_height}), dbc.Row([], style = { 'height' :row_sep_height }), dbc.Row([ dcc.Graph( id = "graph_qtd_seller" ) ], style = { 'height' :body_height / 3 - row_sep_height}), dbc.Row([], style = { 'height' :row_sep_height }), dbc.Row([ dcc.Graph( id = "graph_qtd_category" ) ], style = { 'height' :body_height / 3 - row_sep_height}), ], width = 4 ), ], style = { 'marginLeft' : body_margin_left, 'marginRight' : body_margin_right, 'height' :body_height + 10 }, className = gap), ], style = { 'marginLeft' : 0 , 'marginRight' : 0 , 'background-color' :body_color, 'overflowX' : 'hidden' , 'padding' : 0 }, fluid = True ) if __name__ = = "__main__" : app.run_server(port = 8050 , debug = True ) |
O resultado do Código é o seguinte:
Linha 2 – Filtros de Data e Categoria
Na linha 2 colocaremos dois objetos. O primeiro irá receber duas datas do usuário conforme for clicado – a primeira data será a inicial dos gráficos e a segunda será a data final. O segundo objeto é uma lista em que o usuário poderá escolher um elemento dela. Essa lista terá as categorias de produto.
O código ficou conforme abaixo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# ========================= Filtros – 2ª linha ============================ dbc.Row([ dbc.Col([ dcc.DatePickerRange( min_date_allowed = pd.to_datetime( min (f_order_items[ 'order_delivered_customer_date' ]) ), max_date_allowed = pd.to_datetime( max (f_order_items[ 'order_delivered_customer_date' ]) ), start_date = pd.to_datetime( min (f_order_items[ 'order_delivered_customer_date' ]) ), end_date = pd.to_datetime( max (f_order_items[ 'order_delivered_customer_date' ]) ), display_format = 'DD/MM/YYYY' , style = { 'font-family' :body_font_family}, id = 'date_range' ), ], width = 5 ), dbc.Col([ dcc.Dropdown( list (f_order_items[ 'product_category_name' ].unique()), placeholder = "Selecionar category" , id = 'category_dropdown' , style = { 'font-family' : 'consolas' , 'font-size' :body_font} ), ], width = 3 ), ], style = { 'marginLeft' : body_margin_left, 'marginRight' : body_margin_right, 'height' : 53 }, className = gap), |
É assim que ficaram os filtros da 2ª linha:
Linha 3 – Gráficos
1ª Coluna
Para o gráfico de vendas por mês a função receberá as datas e a categoria de produtos, gerará um gráfico de colunas e enviará para a posição no layout.
Para o gráfico de vendas por estado a função receberá as datas e a categoria de produtos, gerará um gráfico de colunas – cada coluna sendo um estado –, e enviará para o a posição no layout (em baixa do de vendas mensais.
O código ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
# ===================================================================== # def f_order_items_filtro_data( start_date, end_date ): f_order_items2 = f_order_items.loc[ ( f_order_items[ 'order_delivered_customer_date' ] > = start_date ) & ( f_order_items[ 'order_delivered_customer_date' ] < = end_date ) ] return f_order_items2 # --------------------------------------------------------------------- # # ===================================================================== # # ============================ Coluna 1 =============================== # # ===================================================================== # @callback ( Output(component_id = "graph_vendas_state" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ), Input (component_id = "category_dropdown" , component_property = "value" ) ) def update_graph_vendas_state( start_date, end_date, category_dropdown ): f_order_items2 = f_order_items_filtro_data( start_date, end_date ) if category_dropdown = = None : f_order_items2 else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'product_category_name' ].isin([category_dropdown]) ] f_order_items2 = f_order_items2.groupby([ 'customer_state' ], observed = True ).agg( total_price = ( 'price' , 'sum' ) ).sort_values( 'total_price' , ascending = False ).reset_index() fig = go.Figure().add_trace( go.Bar( x = f_order_items2[ 'customer_state' ], y = f_order_items2[ 'total_price' ], name = "total_price" , orientation = 'v' ) ).update_layout( margin = dict (l = 30 , r = 10 , b = 5 , t = 35 ), plot_bgcolor = "white" , bargap = 0.1 , font = dict (family = body_font_family, size = body_font, color = 'black' ), hoverlabel = dict ( font_family = body_font_family, font_size = body_font ), title = f "<b>Vendas por customer_state</b>" , title_font = dict (size = body_font, color = 'black' , family = body_font_family), ).update_xaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , ).update_yaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , range = [ 0 , f_order_items2[ 'total_price' ]. max () ] ) return fig # --------------------------------------------------------------------- # @callback ( Output(component_id = "graph_vendas_geral" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ), Input (component_id = "category_dropdown" , component_property = "value" ), Input (component_id = "graph_vendas_state" , component_property = "hoverData" ), Input (component_id = "graph_vendas_city" , component_property = "hoverData" ) ) def update_graph_vendas_geral( start_date, end_date, category_dropdown, graph_vendas_state, graph_vendas_city ): f_order_items2 = f_order_items_filtro_data( start_date, end_date ) if category_dropdown = = None : pass else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'product_category_name' ].isin([category_dropdown]) ] f_order_items2[ 'customer_date' ] = f_order_items2[ 'order_delivered_customer_date' ].dt.to_period( 'M' ).dt.to_timestamp() if graph_vendas_state = = None : pass else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'customer_state' ] = = graph_vendas_state[ 'points' ][ 0 ][ 'x' ] ] if graph_vendas_city = = None : pass else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'customer_city' ] = = graph_vendas_city[ 'points' ][ 0 ][ 'y' ] ] f_order_items2 = f_order_items2.groupby([ 'customer_date' ], observed = True ).agg( total_price = ( 'price' , 'sum' ) ).reset_index() fig = go.Figure().add_trace( go.Bar( x = f_order_items2[ 'customer_date' ], y = f_order_items2[ 'total_price' ], name = "total_price" , orientation = 'v' , xperiodalignment = "start" , ) ).update_layout( margin = dict (l = 35 , r = 10 , b = 30 , t = 45 ), plot_bgcolor = "white" , bargap = 0.1 , font = dict (family = body_font_family, size = body_font, color = 'black' ), hoverlabel = dict (font_family = body_font_family, font_size = body_font ), title = f "<b>Vendas por Mês</b>" , title_font = dict (size = body_font + 1 , color = 'black' , family = body_font_family), ).update_xaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , dtick = "M2" , tickformat = "%b\n%Y" , range = [ pd.to_datetime(start_date) - pd.to_timedelta( 14 , 'D' ), end_date ], #end_date ).update_yaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , range = [ 0 , f_order_items2[ 'total_price' ]. max () ] ) return fig # ===================================================================== # |
2ª Coluna
Para o gráfico de vendas por cidade a função receberá as datas e a categoria de produtos, gerará um gráfico de colunas – cada coluna sendo um estado –, e enviará para o a posição no layout.
O código ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
# ===================================================================== # # ============================ Coluna 2 =============================== # # ===================================================================== # @callback ( Output(component_id = "graph_vendas_city" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ), Input (component_id = "category_dropdown" , component_property = "value" ), Input (component_id = "graph_vendas_state" , component_property = "hoverData" ) ) def update_graph_vendas_city( start_date, end_date, category_dropdown, hoverData ): f_order_items2 = f_order_items_filtro_data( start_date, end_date ) if category_dropdown = = None : f_order_items2 else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'product_category_name' ].isin([category_dropdown]) ] if hoverData = = None : f_order_items2 = f_order_items2.groupby([ 'customer_city' ], observed = True ).agg( total_price = ( 'price' , 'sum' ) ).sort_values( 'total_price' , ascending = True ).reset_index() else : f_order_items2 = f_order_items2.loc[ f_order_items2[ 'customer_state' ] = = hoverData[ 'points' ][ 0 ][ 'x' ] ].groupby([ 'customer_city' ], observed = True ).agg( total_price = ( 'price' , 'sum' ) ).sort_values( 'total_price' , ascending = True ).reset_index() f_order_items2 = f_order_items2.tail( 40 ) fig = go.Figure().add_trace( go.Bar( x = f_order_items2[ 'total_price' ], y = f_order_items2[ 'customer_city' ], name = "total_price" , orientation = 'h' ) ).update_layout( margin = dict (l = 25 , r = 10 , b = 5 , t = 35 ), plot_bgcolor = "white" , bargap = 0.1 , font = dict (family = body_font_family, size = body_font - 2 , color = 'black' ), hoverlabel = dict (font_family = body_font_family, font_size = body_font ), title = f "<b>Vendas por customer_city</b>" , title_font = dict (size = body_font, color = 'black' , family = body_font_family), ).update_xaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , range = [ 0 , f_order_items2[ 'total_price' ]. max () ] ).update_yaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , ) return fig # ===================================================================== # |
3ª Coluna
Serão 3 gráficos semelhantes. No eixo ‘x’ teremos a data e no ‘y’ teremos, para o primeiro, a quantidade distinta acumulada de clientes, para o segundo, a quantidade distinta acumulada de vendedores, e para o terceiro, a quantidade distinta acumulada de produtos.
O propósito da 3ª coluna do nosso dashboard é observar se a plataforma está crescendo, ou seja, se está gerando engajamento da comunidade.
O código ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
# ===================================================================== # def grafico_contagem_acumulada( start_date, end_date, variavel ): f_order_items2 = f_order_items_filtro_data( start_date, end_date ) f_order_items2 = f_order_items2.sort_values( 'order_delivered_customer_date' ) f_order_items2 = f_order_items2[[ 'order_delivered_customer_date' , variavel ]].drop_duplicates( variavel ) f_order_items2[ variavel ] = 1 f_order_items2[ 'cumcount' ] = f_order_items2[ variavel ].cumsum() f_order_items2 = pd.concat([ f_order_items2, pd.DataFrame( [{ 'order_delivered_customer_date' :end_date, variavel:variavel, 'cumcount' :f_order_items2[ 'cumcount' ]. max () }] ) ], ignore_index = True ) f_order_items2.reset_index(drop = True ) fig = go.Figure().add_trace( go.Scatter( x = f_order_items2[ 'order_delivered_customer_date' ], y = f_order_items2[ 'cumcount' ], fill = 'tonexty' , marker_color = "darkblue" , fillcolor = "rgba(164,219,232, 0.25)" , line = dict (width = 2.5 ), name = variavel ) ).update_layout( margin = dict (l = 40 , r = 10 , b = 3 , t = 40 ), plot_bgcolor = "white" , bargap = 0 , font = dict (family = body_font_family, size = body_font - 1 , color = 'black' ), hoverlabel = dict ( font_family = body_font_family, font_size = body_font ), title = f "<b>Qtde distinta de {variavel}</b>" , title_font = dict (size = body_font, color = 'black' , family = body_font_family), ).update_xaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , tickformat = "%Y-%m" , range = [ start_date, end_date ] ).update_yaxes( showgrid = True , gridwidth = 1 , gridcolor = 'lightgray' , range = [ 0 , f_order_items2[ 'cumcount' ]. max () ] ) return fig # ===================================================================== # # ===================================================================== # # ============================ Coluna 3 =============================== # # ===================================================================== # @callback ( Output(component_id = "graph_qtd_customer" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ) ) def update_graph_qtd_customer( start_date, end_date ): fig = grafico_contagem_acumulada(start_date = start_date, end_date = end_date, variavel = 'customer_id' ) return fig # --------------------------------------------------------------------- # @callback ( Output(component_id = "graph_qtd_seller" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ) ) def update_graph_qtd_seller( start_date, end_date ): fig = grafico_contagem_acumulada(start_date = start_date, end_date = end_date, variavel = 'seller_id' ) return fig # --------------------------------------------------------------------- # @callback ( Output(component_id = "graph_qtd_category" , component_property = "figure" ), Input (component_id = "date_range" , component_property = "start_date" ), Input (component_id = "date_range" , component_property = "end_date" ) ) def update_graph_qtd_category( start_date, end_date ): fig = grafico_contagem_acumulada(start_date = start_date, end_date = end_date, variavel = 'product_id' ) return fig # ===================================================================== # |
Para dar um efeito parecido com o do Power BI utilizamos o texto que aparece quando colocamos o mouse em cima das colunas dos gráficos de vendas por estado e por cidade para servir de filtro.
Considerações
Está é uma pequena amostra do que é possível fazer com os dashboards da biblioteca Dash e a linguagem Python. Seria possível incluir cadastramento de itens, de usuários, gerar um executável para outras pessoas acessarem, e muito mais.
Muito obrigado por ler o artigo.