Spaces:
Sleeping
Sleeping
GMARTINEZMILLA
commited on
Commit
•
84f39ab
1
Parent(s):
e099be1
feat: update filter per manufacturer
Browse files
app.py
CHANGED
@@ -300,11 +300,12 @@ elif page == "🕵️ Análisis de Cliente":
|
|
300 |
</p>
|
301 |
""", unsafe_allow_html=True)
|
302 |
|
303 |
-
#
|
304 |
customer_code = st.selectbox(
|
305 |
"Escribe o selecciona el código de tu cliente",
|
306 |
-
df['CLIENTE'].unique(),
|
307 |
-
|
|
|
308 |
help="Start typing to search for a specific customer code"
|
309 |
)
|
310 |
|
@@ -313,187 +314,189 @@ elif page == "🕵️ Análisis de Cliente":
|
|
313 |
with st.spinner("Estamos identificando el grupo del cliente..."):
|
314 |
# Find Customer's Cluster
|
315 |
customer_match = customer_clusters[customer_clusters['cliente_id'] == customer_code]
|
|
|
|
|
316 |
if not customer_match.empty:
|
317 |
cluster = customer_match['cluster_id'].values[0]
|
318 |
|
319 |
with st.spinner(f"Seleccionando el modelo predictivo..."):
|
|
|
320 |
model_path = f'models/modelo_cluster_{cluster}.txt'
|
321 |
gbm = lgb.Booster(model_file=model_path)
|
322 |
|
323 |
with st.spinner("Preparando los datos..."):
|
324 |
# Load predict data for that cluster
|
325 |
predict_data = pd.read_csv(f'predicts/predict_cluster_{cluster}.csv')
|
|
|
|
|
326 |
predict_data['cliente_id'] = predict_data['cliente_id'].astype(str)
|
327 |
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
if not customer_data.empty:
|
333 |
-
st.success(f"Datos para el cliente {customer_code_str} encontrados.")
|
334 |
-
results = customer_data.copy()
|
335 |
-
|
336 |
-
# Manufacturer selection after customer analysis
|
337 |
-
manufacturer_options = ['Todos'] + sorted(nombres_proveedores['nombre'].unique())
|
338 |
-
selected_manufacturer = st.selectbox("Selecciona el fabricante", manufacturer_options)
|
339 |
-
|
340 |
-
if selected_manufacturer != 'Todos':
|
341 |
-
manufacturer_code = nombres_proveedores[nombres_proveedores['nombre'] == selected_manufacturer]['codigo'].values[0]
|
342 |
-
results_filtered = results[results['marca_id_encoded'] == manufacturer_code]
|
343 |
-
historical_data_filtered = historical_data[historical_data['marca_id_encoded'] == manufacturer_code]
|
344 |
-
else:
|
345 |
-
results_filtered = results
|
346 |
-
historical_data_filtered = historical_data
|
347 |
-
|
348 |
-
with st.spinner("Generando predicciones de venta..."):
|
349 |
-
if not results_filtered.empty:
|
350 |
-
# Define features consistently with the training process
|
351 |
-
lag_features = [f'precio_total_lag_{lag}' for lag in range(1, 25)]
|
352 |
-
features = lag_features + ['mes', 'marca_id_encoded', 'año', 'cluster_id']
|
353 |
-
|
354 |
-
# Prepare data for prediction
|
355 |
-
X_predict = results_filtered[features]
|
356 |
-
|
357 |
-
# Convert categorical features to 'category' dtype
|
358 |
-
categorical_features = ['mes', 'marca_id_encoded', 'cluster_id']
|
359 |
-
for feature in categorical_features:
|
360 |
-
X_predict[feature] = X_predict[feature].astype('category')
|
361 |
-
|
362 |
-
# Make Prediction for the selected customer
|
363 |
-
y_pred = gbm.predict(X_predict, num_iteration=gbm.best_iteration)
|
364 |
-
|
365 |
-
# Reassemble the results
|
366 |
-
results_filtered['ventas_predichas'] = y_pred
|
367 |
-
|
368 |
-
# Load actual data from df_agg_2024
|
369 |
-
actual_sales = df_agg_2024[df_agg_2024['cliente_id'] == customer_code_str]
|
370 |
-
|
371 |
-
if not actual_sales.empty:
|
372 |
-
# Merge predictions with actual sales
|
373 |
-
results_filtered = results_filtered.merge(
|
374 |
-
actual_sales[['cliente_id', 'marca_id_encoded', 'fecha_mes', 'precio_total']],
|
375 |
-
on=['cliente_id', 'marca_id_encoded', 'fecha_mes'],
|
376 |
-
how='left'
|
377 |
-
)
|
378 |
-
results_filtered.rename(columns={'precio_total': 'ventas_reales'}, inplace=True)
|
379 |
-
else:
|
380 |
-
# If no actual sales data for 2024, create the column with 0s
|
381 |
-
results_filtered['ventas_reales'] = 0
|
382 |
|
383 |
-
|
384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
|
386 |
-
|
387 |
-
|
388 |
-
|
|
|
|
|
|
|
|
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
399 |
|
400 |
-
|
401 |
-
|
402 |
-
datos_historicos.rename(columns={'precio_total': 'ventas_historicas'}, inplace=True)
|
403 |
-
else:
|
404 |
-
# Si los datos históricos están vacíos, generar fechas de 2023 con ventas_historicas = 0
|
405 |
-
fechas_2023 = pd.date_range(start='2023-01-01', end='2023-12-31', freq='M')
|
406 |
-
datos_historicos = pd.DataFrame({'fecha_mes': fechas_2023, 'ventas_historicas': [0] * len(fechas_2023)})
|
407 |
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
|
|
413 |
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
x=datos_combinados['fecha_mes'],
|
435 |
-
y=datos_combinados['ventas_historicas'],
|
436 |
-
mode='lines+markers',
|
437 |
-
name='Ventas Históricas',
|
438 |
-
line=dict(color='blue')
|
439 |
-
))
|
440 |
-
|
441 |
-
# Graficar ventas predichas
|
442 |
-
fig.add_trace(go.Scatter(
|
443 |
-
x=datos_combinados['fecha_mes'],
|
444 |
-
y=datos_combinados['ventas_predichas'],
|
445 |
-
mode='lines+markers',
|
446 |
-
name='Ventas Predichas',
|
447 |
-
line=dict(color='orange')
|
448 |
-
))
|
449 |
-
|
450 |
-
# Graficar ventas reales
|
451 |
-
fig.add_trace(go.Scatter(
|
452 |
-
x=datos_combinados['fecha_mes'],
|
453 |
-
y=datos_combinados['ventas_reales'],
|
454 |
-
mode='lines+markers',
|
455 |
-
name='Ventas Reales',
|
456 |
-
line=dict(color='green')
|
457 |
-
))
|
458 |
-
|
459 |
-
# Personalizar el layout para enfocarse en 2023 y 2024
|
460 |
-
fig.update_layout(
|
461 |
-
title=f"Ventas Históricas, Predichas y Reales para Cliente {customer_code}",
|
462 |
-
xaxis_title="Fecha",
|
463 |
-
yaxis_title="Ventas (€)",
|
464 |
-
height=600,
|
465 |
-
xaxis_range=[fecha_inicio_2023, pd.to_datetime("2024-09-30")], # Ajustar el rango del eje x a 2023-2024
|
466 |
-
legend_title="Tipo de Ventas",
|
467 |
-
hovermode="x unified"
|
468 |
-
)
|
469 |
-
|
470 |
-
# Mostrar la gráfica en Streamlit
|
471 |
-
st.plotly_chart(fig)
|
472 |
-
|
473 |
-
# Calculate metrics for 2024 data
|
474 |
-
datos_2024 = datos_combinados[datos_combinados['fecha_mes'].dt.year == 2024]
|
475 |
-
actual = datos_2024['ventas_reales']
|
476 |
-
predicted = datos_2024['ventas_predichas']
|
477 |
-
|
478 |
-
def calculate_mape(y_true, y_pred):
|
479 |
-
mask = y_true != 0
|
480 |
-
return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
|
481 |
-
|
482 |
-
mae = mean_absolute_error(actual, predicted)
|
483 |
-
mse = mean_squared_error(actual, predicted)
|
484 |
-
rmse = np.sqrt(mse)
|
485 |
-
mape = calculate_mape(actual, predicted)
|
486 |
-
smape = np.mean(2 * np.abs(actual - predicted) / (np.abs(actual) + np.abs(predicted))) * 100
|
487 |
-
|
488 |
-
# Display metrics
|
489 |
-
st.subheader("Métricas de Predicción (2024)")
|
490 |
-
col1, col2, col3, col4 = st.columns(4)
|
491 |
-
col1.metric("MAE", f"{mae:.2f} €", help="Promedio de la diferencia absoluta entre las predicciones y los valores reales.")
|
492 |
-
col2.metric("MAPE", f"{mape:.2f}%", help="Porcentaje promedio de error en las predicciones.")
|
493 |
-
col3.metric("RMSE", f"{rmse:.2f} €", help="Medida de la desviación estándar de los residuos de predicción.")
|
494 |
-
col4.metric("SMAPE", f"{smape:.2f}%", help="Alternativa al MAPE que maneja mejor los valores cercanos a cero.")
|
495 |
else:
|
496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
|
498 |
# Split space into two columns
|
499 |
col1, col2 = st.columns(2)
|
@@ -864,4 +867,4 @@ elif page == "💡 Recomendación de Artículos":
|
|
864 |
# else:
|
865 |
# st.warning("⚠️ No recommendations found for the provided basket.")
|
866 |
# else:
|
867 |
-
# st.warning("⚠️ Please select at least one article and set its quantity.")
|
|
|
300 |
</p>
|
301 |
""", unsafe_allow_html=True)
|
302 |
|
303 |
+
# Combine text input and dropdown into a single searchable selectbox
|
304 |
customer_code = st.selectbox(
|
305 |
"Escribe o selecciona el código de tu cliente",
|
306 |
+
df['CLIENTE'].unique(), # All customer codes
|
307 |
+
|
308 |
+
format_func=lambda x: str(x), # Ensures the values are displayed as strings
|
309 |
help="Start typing to search for a specific customer code"
|
310 |
)
|
311 |
|
|
|
314 |
with st.spinner("Estamos identificando el grupo del cliente..."):
|
315 |
# Find Customer's Cluster
|
316 |
customer_match = customer_clusters[customer_clusters['cliente_id'] == customer_code]
|
317 |
+
time.sleep(1)
|
318 |
+
|
319 |
if not customer_match.empty:
|
320 |
cluster = customer_match['cluster_id'].values[0]
|
321 |
|
322 |
with st.spinner(f"Seleccionando el modelo predictivo..."):
|
323 |
+
# Load the Corresponding Model
|
324 |
model_path = f'models/modelo_cluster_{cluster}.txt'
|
325 |
gbm = lgb.Booster(model_file=model_path)
|
326 |
|
327 |
with st.spinner("Preparando los datos..."):
|
328 |
# Load predict data for that cluster
|
329 |
predict_data = pd.read_csv(f'predicts/predict_cluster_{cluster}.csv')
|
330 |
+
|
331 |
+
# Convert cliente_id to string
|
332 |
predict_data['cliente_id'] = predict_data['cliente_id'].astype(str)
|
333 |
|
334 |
+
with st.spinner("Filtrando data..."):
|
335 |
+
# Filter for the specific customer
|
336 |
+
customer_code_str = str(customer_code)
|
337 |
+
customer_data = predict_data[predict_data['cliente_id'] == customer_code_str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
|
339 |
+
with st.spinner("Geneerando predicciones de venta..."):
|
340 |
+
if not customer_data.empty:
|
341 |
+
# Define features consistently with the training process
|
342 |
+
lag_features = [f'precio_total_lag_{lag}' for lag in range(1, 25)]
|
343 |
+
features = lag_features + ['mes', 'marca_id_encoded', 'año', 'cluster_id']
|
344 |
+
|
345 |
+
# Prepare data for prediction
|
346 |
+
X_predict = customer_data[features]
|
347 |
|
348 |
+
# Convert categorical features to 'category' dtype
|
349 |
+
categorical_features = ['mes', 'marca_id_encoded', 'cluster_id']
|
350 |
+
for feature in categorical_features:
|
351 |
+
X_predict[feature] = X_predict[feature].astype('category')
|
352 |
+
|
353 |
+
# Make Prediction for the selected customer
|
354 |
+
y_pred = gbm.predict(X_predict, num_iteration=gbm.best_iteration)
|
355 |
|
356 |
+
# Reassemble the results
|
357 |
+
results = customer_data[['cliente_id', 'marca_id_encoded', 'fecha_mes']].copy()
|
358 |
+
results['ventas_predichas'] = y_pred
|
359 |
|
360 |
+
# Load actual data from df_agg_2024
|
361 |
+
actual_sales = df_agg_2024[df_agg_2024['cliente_id'] == customer_code_str]
|
362 |
+
|
363 |
+
if not actual_sales.empty:
|
364 |
+
# Merge predictions with actual sales
|
365 |
+
results = results.merge(actual_sales[['cliente_id', 'marca_id_encoded', 'fecha_mes', 'precio_total']],
|
366 |
+
on=['cliente_id', 'marca_id_encoded', 'fecha_mes'],
|
367 |
+
how='left')
|
368 |
+
results.rename(columns={'precio_total': 'ventas_reales'}, inplace=True)
|
369 |
+
else:
|
370 |
+
# If no actual sales data for 2024, fill 'ventas_reales' with 0
|
371 |
+
results['ventas_reales'] = 0
|
372 |
|
373 |
+
# Ensure any missing sales data is filled with 0
|
374 |
+
results['ventas_reales'].fillna(0, inplace=True)
|
|
|
|
|
|
|
|
|
|
|
375 |
|
376 |
+
# Define the cutoff date for the last 12 months
|
377 |
+
fecha_inicio = pd.to_datetime("2023-01-01")
|
378 |
+
fecha_corte = pd.to_datetime("2024-09-01")
|
379 |
+
|
380 |
+
# Convertir fecha_mes a datetime en el DataFrame historical_data
|
381 |
+
historical_data['fecha_mes'] = pd.to_datetime(historical_data['fecha_mes'], errors='coerce')
|
382 |
|
383 |
+
# Ensure cliente_id is of type string and strip any leading/trailing whitespace
|
384 |
+
historical_data['cliente_id'] = historical_data['cliente_id'].astype(str).str.strip()
|
385 |
+
customer_code_str = str(customer_code).strip() # Ensure the customer code is also properly formatted
|
386 |
+
|
387 |
+
filtered_historical_data = historical_data[historical_data['cliente_id'] == customer_code_str]
|
388 |
+
|
389 |
+
|
390 |
+
# Filtrar los datos históricos por cliente y por el rango de fechas (2023)
|
391 |
+
fecha_inicio_2023 = pd.to_datetime("2023-01-01")
|
392 |
+
fecha_fin_2023 = pd.to_datetime("2023-12-31")
|
393 |
+
|
394 |
+
datos_historicos = historical_data[
|
395 |
+
(historical_data['cliente_id'] == customer_code_str) &
|
396 |
+
(historical_data['fecha_mes'] >= fecha_inicio_2023) &
|
397 |
+
(historical_data['fecha_mes'] <= fecha_fin_2023)
|
398 |
+
].groupby('fecha_mes')['precio_total'].sum().reset_index()
|
399 |
+
|
400 |
+
# Renombrar la columna 'precio_total' a 'ventas_historicas' si no está vacía
|
401 |
+
if not datos_historicos.empty:
|
402 |
+
datos_historicos.rename(columns={'precio_total': 'ventas_historicas'}, inplace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
else:
|
404 |
+
# Si los datos históricos están vacíos, generar fechas de 2023 con ventas_historicas = 0
|
405 |
+
fechas_2023 = pd.date_range(start='2023-01-01', end='2023-12-31', freq='M')
|
406 |
+
datos_historicos = pd.DataFrame({'fecha_mes': fechas_2023, 'ventas_historicas': [0] * len(fechas_2023)})
|
407 |
+
|
408 |
+
# Filtrar los datos de predicciones y ventas reales para 2024
|
409 |
+
datos_cliente_total = results.groupby('fecha_mes').agg({
|
410 |
+
'ventas_reales': 'sum',
|
411 |
+
'ventas_predichas': 'sum'
|
412 |
+
}).reset_index()
|
413 |
+
|
414 |
+
# Asegurarnos de que fecha_mes en datos_cliente_total es datetime
|
415 |
+
datos_cliente_total['fecha_mes'] = pd.to_datetime(datos_cliente_total['fecha_mes'], errors='coerce')
|
416 |
+
|
417 |
+
# Generar un rango de fechas para 2024 si no hay predicciones
|
418 |
+
fechas_2024 = pd.date_range(start='2024-01-01', end='2024-12-31', freq='M')
|
419 |
+
fechas_df_2024 = pd.DataFrame({'fecha_mes': fechas_2024})
|
420 |
+
|
421 |
+
# Asegurarnos de que fecha_mes en fechas_df_2024 es datetime
|
422 |
+
fechas_df_2024['fecha_mes'] = pd.to_datetime(fechas_df_2024['fecha_mes'], errors='coerce')
|
423 |
+
|
424 |
+
# Combinar datos históricos con predicciones y ventas reales usando un merge
|
425 |
+
# Usamos how='outer' para asegurarnos de incluir todas las fechas de 2023 y 2024
|
426 |
+
datos_combinados = pd.merge(datos_historicos, datos_cliente_total, on='fecha_mes', how='outer').sort_values('fecha_mes')
|
427 |
+
|
428 |
+
# Rellenar los NaN: 0 en ventas_historicas donde faltan predicciones, y viceversa
|
429 |
+
datos_combinados['ventas_historicas'].fillna(0, inplace=True)
|
430 |
+
datos_combinados['ventas_predichas'].fillna(0, inplace=True)
|
431 |
+
datos_combinados['ventas_reales'].fillna(0, inplace=True)
|
432 |
+
|
433 |
+
# Crear la gráfica con Plotly
|
434 |
+
fig = go.Figure()
|
435 |
+
|
436 |
+
# Graficar ventas históricas
|
437 |
+
fig.add_trace(go.Scatter(
|
438 |
+
x=datos_combinados['fecha_mes'],
|
439 |
+
y=datos_combinados['ventas_historicas'],
|
440 |
+
mode='lines+markers',
|
441 |
+
name='Ventas Históricas',
|
442 |
+
line=dict(color='blue')
|
443 |
+
))
|
444 |
+
|
445 |
+
# Graficar ventas predichas
|
446 |
+
fig.add_trace(go.Scatter(
|
447 |
+
x=datos_combinados['fecha_mes'],
|
448 |
+
y=datos_combinados['ventas_predichas'],
|
449 |
+
mode='lines+markers',
|
450 |
+
name='Ventas Predichas',
|
451 |
+
line=dict(color='orange')
|
452 |
+
))
|
453 |
+
|
454 |
+
# Graficar ventas reales
|
455 |
+
fig.add_trace(go.Scatter(
|
456 |
+
x=datos_combinados['fecha_mes'],
|
457 |
+
y=datos_combinados['ventas_reales'],
|
458 |
+
mode='lines+markers',
|
459 |
+
name='Ventas Reales',
|
460 |
+
line=dict(color='green')
|
461 |
+
))
|
462 |
+
|
463 |
+
# Personalizar el layout para enfocarse en 2023 y 2024
|
464 |
+
fig.update_layout(
|
465 |
+
title=f"Ventas Históricas, Predichas y Reales para Cliente {customer_code}",
|
466 |
+
xaxis_title="Fecha",
|
467 |
+
yaxis_title="Ventas (€)",
|
468 |
+
height=600,
|
469 |
+
xaxis_range=[fecha_inicio_2023, pd.to_datetime("2024-09-30")], # Ajustar el rango del eje x a 2023-2024
|
470 |
+
legend_title="Tipo de Ventas",
|
471 |
+
hovermode="x unified"
|
472 |
+
)
|
473 |
+
|
474 |
+
# Mostrar la gráfica en Streamlit
|
475 |
+
st.plotly_chart(fig)
|
476 |
+
|
477 |
+
# Calculate metrics for 2024 data
|
478 |
+
datos_2024 = datos_combinados[datos_combinados['fecha_mes'].dt.year == 2024]
|
479 |
+
actual = datos_2024['ventas_reales']
|
480 |
+
predicted = datos_2024['ventas_predichas']
|
481 |
+
|
482 |
+
def calculate_mape(y_true, y_pred):
|
483 |
+
mask = y_true != 0
|
484 |
+
return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
|
485 |
+
|
486 |
+
mae = mean_absolute_error(actual, predicted)
|
487 |
+
mse = mean_squared_error(actual, predicted)
|
488 |
+
rmse = np.sqrt(mse)
|
489 |
+
mape = calculate_mape(actual, predicted)
|
490 |
+
smape = np.mean(2 * np.abs(actual - predicted) / (np.abs(actual) + np.abs(predicted))) * 100
|
491 |
+
|
492 |
+
# Display metrics
|
493 |
+
st.subheader("Métricas de Predicción (2024)")
|
494 |
+
col1, col2, col3, col4 = st.columns(4)
|
495 |
+
col1.metric("MAE", f"{mae:.2f} €",help="Promedio de la diferencia absoluta entre las predicciones y los valores reales.")
|
496 |
+
col2.metric("MAPE", f"{mape:.2f}%",help="Porcentaje promedio de error en las predicciones.")
|
497 |
+
col3.metric("RMSE", f"{rmse:.2f} €",help="Medida de la desviación estándar de los residuos de predicción.")
|
498 |
+
col4.metric("SMAPE", f"{smape:.2f}%",help="Alternativa al MAPE que maneja mejor los valores cercanos a cero.")
|
499 |
+
|
500 |
|
501 |
# Split space into two columns
|
502 |
col1, col2 = st.columns(2)
|
|
|
867 |
# else:
|
868 |
# st.warning("⚠️ No recommendations found for the provided basket.")
|
869 |
# else:
|
870 |
+
# st.warning("⚠️ Please select at least one article and set its quantity.")
|