joaopimenta commited on
Commit
959f7ba
·
verified ·
1 Parent(s): a01ffab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -105
app.py CHANGED
@@ -5,138 +5,161 @@ import plotly.graph_objects as go
5
  import os
6
 
7
  # =========================================================
8
- # 1. FUNÇÃO DE PARSING PERSONALIZADO (MOTOR DATA)
9
  # =========================================================
10
  def load_motordata_csv(filepath):
11
- """
12
- Lê o CSV específico da MotorData, limpando caracteres especiais
13
- e corrigindo a estrutura de colunas.
14
- """
15
  rows = []
16
  max_cols = 0
17
  lines = []
18
 
19
- # 1. Detetar Encoding
20
- for enc in ["latin1", "utf-8", "cp1252"]:
21
  try:
22
  with open(filepath, "r", encoding=enc) as f:
23
  lines = f.readlines()
24
- print(f"✔ Encoding detetado: {enc}")
25
  break
26
  except:
27
  continue
28
 
29
  if not lines:
 
30
  return pd.DataFrame()
31
 
32
- # 2. Remover a primeira linha (que contém apenas filtros/metadata)
 
33
  if len(lines) > 1:
34
- lines = lines[1:]
 
 
35
 
36
  # 3. Processar linha a linha
37
  for line in lines:
38
- # Limpar lixo excel: ="VAL" → VAL e remover aspas extras
39
  clean = line.replace('="', '').replace('"', '').strip()
40
-
41
- # Separar por ponto e vírgula
42
  parts = clean.split(";")
43
-
44
  rows.append(parts)
45
  max_cols = max(max_cols, len(parts))
46
 
47
- # 4. Normalizar colunas (preencher linhas curtas com vazio)
48
  rows = [r + [""] * (max_cols - len(r)) for r in rows]
49
 
50
  if not rows:
51
  return pd.DataFrame()
52
 
53
- # 5. Definir Header e Dados
54
- header = rows[0] # A primeira linha processada é o cabeçalho
55
- data = rows[1:] # O resto são dados
56
-
57
- # Criar DataFrame
58
- # (Limpa nomes das colunas para garantir que não têm espaços extras)
59
- clean_header = [h.strip() for h in header]
60
  df = pd.DataFrame(data, columns=clean_header)
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  return df
63
 
64
  # =========================================================
65
- # 2. CARREGAMENTO E PREPARAÇÃO
66
  # =========================================================
67
  def load_data():
68
  file_path = "dados_vendas.csv"
69
  df = pd.DataFrame()
70
 
71
- # --- TENTA CARREGAR O FICHEIRO REAL ---
72
  if os.path.exists(file_path):
73
  print("📂 A processar dados reais...")
74
  try:
75
  df = load_motordata_csv(file_path)
76
  except Exception as e:
77
- print(f"❌ Erro ao processar CSV: {e}")
 
 
 
 
 
 
 
 
78
 
79
- # --- SE FALHAR OU NÃO EXISTIR, USA SIMULAÇÃO ---
80
- if df.empty:
81
- print("⚠️ Usando dados simulados (Ficheiro vazio ou inexistente)...")
82
- data = {
83
- 'Marca': ['VALTRA', 'KIOTI', 'JOHN DEERE', 'NEW HOLLAND', 'KUBOTA'] * 10,
84
- 'Regiao': ['Norte', 'Centro', 'Alentejo', 'Norte', 'Algarve'] * 10,
85
- 'Tipo': ['AGRICOLA'] * 50,
86
- 'Potencia kW': ['75', '19', '88', '55', '70'] * 10,
 
 
 
 
 
 
87
  }
88
- df = pd.DataFrame(data)
89
 
90
- # ----------------------------------------------------------
91
- # === LIMPEZA E TIPAGEM DE DADOS ===
92
- # ----------------------------------------------------------
93
 
94
- # 1. Filtrar por TIPO (se a coluna existir)
95
- if 'Tipo' in df.columns:
96
- # Normalizar para maiúsculas e remover espaços
97
- df['Tipo'] = df['Tipo'].astype(str).str.upper().str.strip()
98
- df = df[df['Tipo'] == 'AGRICOLA'].copy()
 
 
99
 
100
- # 2. Tratar Potência (Converter string "88" para número 88.0)
101
- if 'Potencia kW' in df.columns:
102
- # Remove qualquer caracter não numérico que tenha sobrado
103
- df["Potencia kW"] = pd.to_numeric(df["Potencia kW"], errors='coerce')
104
- df = df.dropna(subset=["Potencia kW"])
105
-
106
- # Criar Segmentos (Bins)
107
- bins = [0, 25, 50, 100, 500]
108
- labels = ['< 25 kW (Compactos)', '25 - 50 kW', '50 - 100 kW', '> 100 kW (Alta)']
109
- df['Cluster Potencia'] = pd.cut(df['Potencia kW'], bins=bins, labels=labels)
110
- df['Cluster Potencia'] = df['Cluster Potencia'].astype(str)
111
 
112
  return df
113
 
114
- # Carregar dados ao iniciar a aplicação
115
  df_global = load_data()
116
 
117
- # Definir limites para os sliders (Global variables)
118
- if not df_global.empty and "Potencia kW" in df_global.columns:
119
  min_kw_global = int(df_global["Potencia kW"].min())
120
  max_kw_global = int(df_global["Potencia kW"].max())
121
- # Limpa regiões vazias
122
- all_regions = sorted([x for x in df_global["Regiao"].unique().tolist() if x and str(x).strip() != ''])
123
  else:
124
  min_kw_global, max_kw_global = 0, 100
125
- all_regions = []
126
 
127
  # =========================================================
128
- # 3. LÓGICA DO DASHBOARD (CALLBACK)
129
  # =========================================================
130
  def update_dashboard(val_min, val_max, selected_regions):
131
- # Se não houver dados
132
  if df_global.empty:
133
  return "0", "0%", "0%", None, None, None, pd.DataFrame()
134
 
135
- # Troca se min > max
136
- if val_min > val_max:
137
- val_min, val_max = val_max, val_min
138
-
139
- # Filtragem
140
  mask = (
141
  (df_global["Potencia kW"] >= val_min) &
142
  (df_global["Potencia kW"] <= val_max) &
@@ -147,29 +170,25 @@ def update_dashboard(val_min, val_max, selected_regions):
147
  if df_filtered.empty:
148
  return "0", "0%", "0%", None, None, None, df_filtered
149
 
150
- # Cálculos KPIs
151
  total = len(df_filtered)
152
- v_valtra = len(df_filtered[df_filtered['Marca'].astype(str).str.upper() == 'VALTRA'])
153
- v_kioti = len(df_filtered[df_filtered['Marca'].astype(str).str.upper() == 'KIOTI'])
154
 
155
  share_valtra = (v_valtra / total * 100) if total > 0 else 0
156
  share_kioti = (v_kioti / total * 100) if total > 0 else 0
157
 
158
- # Textos KPI
159
- kpi_total_txt = f"{total} Unidades"
160
- kpi_valtra_txt = f"{share_valtra:.1f}% ({v_valtra})"
161
- kpi_kioti_txt = f"{share_kioti:.1f}% ({v_kioti})"
162
-
163
- # Gráfico 1: Ranking
164
  top_marcas = df_filtered['Marca'].value_counts().reset_index().head(15)
165
  top_marcas.columns = ['Marca', 'Vendas']
166
 
167
  colors = []
168
- for marca in top_marcas['Marca']:
169
- m = str(marca).upper()
170
- if m == 'VALTRA': colors.append('#d62728') # Vermelho
171
- elif m == 'KIOTI': colors.append('#ff7f0e') # Laranja
172
- else: colors.append('#cccccc') # Cinza
173
 
174
  fig_rank = go.Figure(data=[go.Bar(
175
  x=top_marcas['Marca'], y=top_marcas['Vendas'],
@@ -177,63 +196,52 @@ def update_dashboard(val_min, val_max, selected_regions):
177
  )])
178
  fig_rank.update_layout(title="Ranking de Mercado", template="plotly_white", height=400)
179
 
180
- # Gráfico 2: Potência
181
  if 'Cluster Potencia' in df_filtered.columns:
182
- vendas_cluster = df_filtered['Cluster Potencia'].value_counts().reset_index()
183
- vendas_cluster.columns = ['Cluster', 'Vendas']
184
- fig_pie = px.pie(vendas_cluster, values='Vendas', names='Cluster',
185
- title='Mix de Potência', hole=0.4)
186
  else:
187
  fig_pie = go.Figure()
188
 
189
- # Gráfico 3: Segmentos Valtra vs Kioti
190
- nossas = df_filtered[df_filtered['Marca'].astype(str).str.upper().isin(['VALTRA', 'KIOTI'])]
191
- if not nossas.empty and 'Cluster Potencia' in nossas.columns:
192
  fig_seg = px.histogram(nossas, x="Cluster Potencia", color="Marca",
193
- barmode="group", title="Onde competimos?",
194
  color_discrete_map={'VALTRA': '#d62728', 'KIOTI': '#ff7f0e'})
195
  else:
196
- fig_seg = go.Figure().add_annotation(text="Sem dados das marcas", showarrow=False)
197
 
198
- return kpi_total_txt, kpi_valtra_txt, kpi_kioti_txt, fig_rank, fig_pie, fig_seg, df_filtered
199
 
200
  # =========================================================
201
- # 4. INTERFACE GRÁFICA (LAYOUT)
202
  # =========================================================
203
  with gr.Blocks(title="Dashboard Valtra & Kioti", theme=gr.themes.Soft()) as demo:
204
-
205
  gr.Markdown("# 🚜 Dashboard Executivo: Valtra & KIOTI")
206
- gr.Markdown(f"_Dados carregados: {len(df_global)} registos_")
207
 
208
  with gr.Row():
209
- # Sidebar Filtros
210
  with gr.Column(scale=1):
211
  gr.Label("⚙️ Filtros")
212
-
213
  s_min = gr.Slider(minimum=min_kw_global, maximum=max_kw_global, value=min_kw_global, step=1, label="Min kW")
214
  s_max = gr.Slider(minimum=min_kw_global, maximum=max_kw_global, value=max_kw_global, step=1, label="Max kW")
215
-
216
  chk_reg = gr.CheckboxGroup(choices=all_regions, value=all_regions, label="Regiões")
217
  btn = gr.Button("Atualizar", variant="primary")
218
 
219
- # Área Principal
220
  with gr.Column(scale=4):
221
  with gr.Row():
222
- # KPIs
223
  k1 = gr.Text(label="Mercado Total")
224
  k2 = gr.Text(label="Share VALTRA")
225
  k3 = gr.Text(label="Share KIOTI")
226
-
227
- # Gráficos
228
  plot_rank = gr.Plot(label="Ranking")
229
  with gr.Row():
230
  plot_pie = gr.Plot(label="Potência")
231
  plot_seg = gr.Plot(label="Segmentos")
232
 
233
- with gr.Accordion("📂 Ver Tabela de Dados", open=False):
234
  tbl = gr.Dataframe()
235
 
236
- # Eventos
237
  inputs = [s_min, s_max, chk_reg]
238
  outputs = [k1, k2, k3, plot_rank, plot_pie, plot_seg, tbl]
239
 
@@ -241,8 +249,6 @@ with gr.Blocks(title="Dashboard Valtra & Kioti", theme=gr.themes.Soft()) as demo
241
  s_max.change(update_dashboard, inputs, outputs)
242
  chk_reg.change(update_dashboard, inputs, outputs)
243
  btn.click(update_dashboard, inputs, outputs)
244
-
245
- # Iniciar
246
  demo.load(update_dashboard, inputs, outputs)
247
 
248
  if __name__ == "__main__":
 
5
  import os
6
 
7
  # =========================================================
8
+ # 1. FUNÇÃO DE PARSING ROBUSTA (MOTOR DATA)
9
  # =========================================================
10
  def load_motordata_csv(filepath):
 
 
 
 
11
  rows = []
12
  max_cols = 0
13
  lines = []
14
 
15
+ # 1. Tentar vários encodings
16
+ for enc in ["latin1", "utf-8", "cp1252", "ISO-8859-1"]:
17
  try:
18
  with open(filepath, "r", encoding=enc) as f:
19
  lines = f.readlines()
20
+ print(f"✔ Encoding detetado com sucesso: {enc}")
21
  break
22
  except:
23
  continue
24
 
25
  if not lines:
26
+ print("❌ Ficheiro vazio ou ilegível.")
27
  return pd.DataFrame()
28
 
29
+ # 2. Limpeza inteligente do cabeçalho
30
+ # Removemos linhas iniciais se não parecerem dados (ex: filtros)
31
  if len(lines) > 1:
32
+ # Se a primeira linha tiver menos de 3 colunas, é lixo
33
+ if len(lines[0].split(';')) < 3:
34
+ lines = lines[1:]
35
 
36
  # 3. Processar linha a linha
37
  for line in lines:
38
+ # Limpar lixo Excel (="Valor")
39
  clean = line.replace('="', '').replace('"', '').strip()
 
 
40
  parts = clean.split(";")
 
41
  rows.append(parts)
42
  max_cols = max(max_cols, len(parts))
43
 
44
+ # 4. Normalizar colunas
45
  rows = [r + [""] * (max_cols - len(r)) for r in rows]
46
 
47
  if not rows:
48
  return pd.DataFrame()
49
 
50
+ # 5. Criar DataFrame
51
+ header = rows[0]
52
+ data = rows[1:]
53
+
54
+ # IMPORTANTE: Limpar espaços dos nomes das colunas (strip)
55
+ clean_header = [str(h).strip() for h in header]
56
+
57
  df = pd.DataFrame(data, columns=clean_header)
58
 
59
+ # === [CRÍTICO] CORREÇÃO DE NOMES DAS COLUNAS ===
60
+ print(f"🔍 Colunas encontradas no CSV (RAW): {df.columns.tolist()}")
61
+
62
+ # Renomeação forçada para padronizar
63
+ for col in df.columns:
64
+ c_low = col.lower()
65
+
66
+ # Procura qualquer variação de Potência (com/sem acento, com/sem parenteses)
67
+ if ("pot" in c_low and "kw" in c_low):
68
+ print(f"✅ Coluna de Potência encontrada: '{col}' -> Renomeando para 'Potencia kW'")
69
+ df.rename(columns={col: 'Potencia kW'}, inplace=True)
70
+
71
+ elif "regi" in c_low: # Regiao, Região, Region...
72
+ df.rename(columns={col: 'Regiao'}, inplace=True)
73
+
74
+ elif "marca" in c_low:
75
+ df.rename(columns={col: 'Marca'}, inplace=True)
76
+
77
  return df
78
 
79
  # =========================================================
80
+ # 2. CARREGAMENTO E LÓGICA PRINCIPAL
81
  # =========================================================
82
  def load_data():
83
  file_path = "dados_vendas.csv"
84
  df = pd.DataFrame()
85
 
 
86
  if os.path.exists(file_path):
87
  print("📂 A processar dados reais...")
88
  try:
89
  df = load_motordata_csv(file_path)
90
  except Exception as e:
91
+ print(f"❌ Erro crítico ao ler CSV: {e}")
92
+ else:
93
+ print("⚠️ Ficheiro 'dados_vendas.csv' não encontrado.")
94
+
95
+ # --- SEGURANÇA ANTI-CRASH ---
96
+ # Se a coluna não existir (mesmo depois da renomeação), criamos uma vazia
97
+ if "Potencia kW" not in df.columns:
98
+ print("⚠️ AVISO: Coluna 'Potencia kW' não encontrada! Criando coluna dummy a zero.")
99
+ df["Potencia kW"] = 0
100
 
101
+ if "Regiao" not in df.columns:
102
+ df["Regiao"] = "Desconhecido"
103
+
104
+ if "Marca" not in df.columns:
105
+ df["Marca"] = "Outros"
106
+
107
+ # Se o dataframe estiver vazio, carrega simulação para não dar erro visual
108
+ if df.empty or len(df) < 2:
109
+ print("⚠️ DataFrame vazio. Usando dados simulados.")
110
+ data_sim = {
111
+ 'Marca': ['VALTRA', 'KIOTI', 'JOHN DEERE', 'NEW HOLLAND'] * 10,
112
+ 'Regiao': ['Norte', 'Centro', 'Sul'] * 14,
113
+ 'Tipo': ['AGRICOLA'] * 42,
114
+ 'Potencia kW': [75, 25, 100, 50] * 10 + [0, 0], # Garante números
115
  }
116
+ df = pd.DataFrame(data_sim)
117
 
118
+ # --- LIMPEZA E TIPAGEM ---
 
 
119
 
120
+ # 1. Filtrar Tipo (Agrícola)
121
+ # Procura coluna Tipo de forma flexível
122
+ tipo_col = next((c for c in df.columns if "tipo" in c.lower()), None)
123
+ if tipo_col:
124
+ df['Tipo_clean'] = df[tipo_col].astype(str).str.upper().str.strip()
125
+ # Filtra onde aparece AGRICOLA
126
+ df = df[df['Tipo_clean'].str.contains('AGRICOLA', na=False)].copy()
127
 
128
+ # 2. Converter Potência para Números
129
+ # Forçamos conversão (erros viram NaN)
130
+ df["Potencia kW"] = pd.to_numeric(df["Potencia kW"], errors='coerce').fillna(0)
131
+
132
+ # 3. Criar Segmentos (Clusters)
133
+ bins = [-1, 25, 50, 100, 1000] # Começa em -1 para apanhar o 0
134
+ labels = ['< 25 kW (Compactos)', '25 - 50 kW', '50 - 100 kW', '> 100 kW (Alta)']
135
+ df['Cluster Potencia'] = pd.cut(df['Potencia kW'], bins=bins, labels=labels).astype(str)
 
 
 
136
 
137
  return df
138
 
139
+ # Carregar dados (Variáveis Globais)
140
  df_global = load_data()
141
 
142
+ # Calcular limites para os filtros
143
+ if not df_global.empty:
144
  min_kw_global = int(df_global["Potencia kW"].min())
145
  max_kw_global = int(df_global["Potencia kW"].max())
146
+ # Lista de regiões (sem vazios e NaN)
147
+ all_regions = sorted([str(x) for x in df_global["Regiao"].unique() if str(x).lower() != 'nan' and str(x).strip() != ''])
148
  else:
149
  min_kw_global, max_kw_global = 0, 100
150
+ all_regions = ["Norte", "Sul"]
151
 
152
  # =========================================================
153
+ # 3. DASHBOARD (GRADIO)
154
  # =========================================================
155
  def update_dashboard(val_min, val_max, selected_regions):
156
+ # Prevenir erros se dataframe estiver vazio
157
  if df_global.empty:
158
  return "0", "0%", "0%", None, None, None, pd.DataFrame()
159
 
160
+ if val_min > val_max: val_min, val_max = val_max, val_min
161
+
162
+ # Filtro Principal
 
 
163
  mask = (
164
  (df_global["Potencia kW"] >= val_min) &
165
  (df_global["Potencia kW"] <= val_max) &
 
170
  if df_filtered.empty:
171
  return "0", "0%", "0%", None, None, None, df_filtered
172
 
173
+ # KPIs
174
  total = len(df_filtered)
175
+ v_valtra = len(df_filtered[df_filtered['Marca'].str.upper() == 'VALTRA'])
176
+ v_kioti = len(df_filtered[df_filtered['Marca'].str.upper() == 'KIOTI'])
177
 
178
  share_valtra = (v_valtra / total * 100) if total > 0 else 0
179
  share_kioti = (v_kioti / total * 100) if total > 0 else 0
180
 
181
+ # Gráficos
182
+ # 1. Ranking
 
 
 
 
183
  top_marcas = df_filtered['Marca'].value_counts().reset_index().head(15)
184
  top_marcas.columns = ['Marca', 'Vendas']
185
 
186
  colors = []
187
+ for m in top_marcas['Marca']:
188
+ m_str = str(m).upper()
189
+ if 'VALTRA' in m_str: colors.append('#d62728')
190
+ elif 'KIOTI' in m_str: colors.append('#ff7f0e')
191
+ else: colors.append('#cccccc')
192
 
193
  fig_rank = go.Figure(data=[go.Bar(
194
  x=top_marcas['Marca'], y=top_marcas['Vendas'],
 
196
  )])
197
  fig_rank.update_layout(title="Ranking de Mercado", template="plotly_white", height=400)
198
 
199
+ # 2. Pizza Potência
200
  if 'Cluster Potencia' in df_filtered.columns:
201
+ v_cluster = df_filtered['Cluster Potencia'].value_counts().reset_index()
202
+ v_cluster.columns = ['Cluster', 'Vendas']
203
+ fig_pie = px.pie(v_cluster, values='Vendas', names='Cluster', title='Mix de Potência', hole=0.4)
 
204
  else:
205
  fig_pie = go.Figure()
206
 
207
+ # 3. Histograma Segmentos
208
+ nossas = df_filtered[df_filtered['Marca'].str.upper().isin(['VALTRA', 'KIOTI'])]
209
+ if not nossas.empty:
210
  fig_seg = px.histogram(nossas, x="Cluster Potencia", color="Marca",
211
+ barmode="group", title="Comparativo Direto",
212
  color_discrete_map={'VALTRA': '#d62728', 'KIOTI': '#ff7f0e'})
213
  else:
214
+ fig_seg = go.Figure().add_annotation(text="Sem dados Valtra/Kioti", showarrow=False)
215
 
216
+ return f"{total}", f"{share_valtra:.1f}% ({v_valtra})", f"{share_kioti:.1f}% ({v_kioti})", fig_rank, fig_pie, fig_seg, df_filtered
217
 
218
  # =========================================================
219
+ # 4. INTERFACE
220
  # =========================================================
221
  with gr.Blocks(title="Dashboard Valtra & Kioti", theme=gr.themes.Soft()) as demo:
 
222
  gr.Markdown("# 🚜 Dashboard Executivo: Valtra & KIOTI")
 
223
 
224
  with gr.Row():
 
225
  with gr.Column(scale=1):
226
  gr.Label("⚙️ Filtros")
 
227
  s_min = gr.Slider(minimum=min_kw_global, maximum=max_kw_global, value=min_kw_global, step=1, label="Min kW")
228
  s_max = gr.Slider(minimum=min_kw_global, maximum=max_kw_global, value=max_kw_global, step=1, label="Max kW")
 
229
  chk_reg = gr.CheckboxGroup(choices=all_regions, value=all_regions, label="Regiões")
230
  btn = gr.Button("Atualizar", variant="primary")
231
 
 
232
  with gr.Column(scale=4):
233
  with gr.Row():
 
234
  k1 = gr.Text(label="Mercado Total")
235
  k2 = gr.Text(label="Share VALTRA")
236
  k3 = gr.Text(label="Share KIOTI")
 
 
237
  plot_rank = gr.Plot(label="Ranking")
238
  with gr.Row():
239
  plot_pie = gr.Plot(label="Potência")
240
  plot_seg = gr.Plot(label="Segmentos")
241
 
242
+ with gr.Accordion("📂 Dados Detalhados", open=False):
243
  tbl = gr.Dataframe()
244
 
 
245
  inputs = [s_min, s_max, chk_reg]
246
  outputs = [k1, k2, k3, plot_rank, plot_pie, plot_seg, tbl]
247
 
 
249
  s_max.change(update_dashboard, inputs, outputs)
250
  chk_reg.change(update_dashboard, inputs, outputs)
251
  btn.click(update_dashboard, inputs, outputs)
 
 
252
  demo.load(update_dashboard, inputs, outputs)
253
 
254
  if __name__ == "__main__":