Analisi dell'E-Commerce Brasiliano – Olist¶

Analisi Retail e Clienti (2016–2018)¶

Emilio Nahuel Pattini
Buenos Aires, Argentina
1 febbraio 2026

Indice dei Contenuti¶

  • 1. Comprensione del Business e Domande
  • 2. Caricamento e Panoramica dei Dati
    • 2.1 Caricamento dei Dati
    • 2.2 Panoramica dei Dati e Controlli Iniziali
  • 3. Pulizia e Preparazione dei Dati
    • 3.1 Pulizia Iniziale dei Dati
    • 3.2 Feature Engineering e Trasformazioni di Business
  • 4. Integrazione dei Dati e Primi Insight di Business
    • 4.1 Salvataggio dei Dati Puliti
    • 4.2 Primo Merge – Creazione di una Tabella Base di Lavoro
    • 4.3 Primo Insight di Business: Ricavi e Performance di Consegna per Stato
  • 5. Analisi di Prodotti e Categorie
    • 5.1 Caricamento di Tabelle Aggiuntive
    • 5.2 Merge delle Informazioni sui Prodotti nella Tabella Base
    • 5.3 Visualizzazione e Insight sulle Performance per Categoria
    • 5.4 Performance per Categoria e Stato
    • 5.5 Visualizzazione: Partecipazione dei Ricavi per Categoria e Stato
  • 6. Segmentazione Clienti – Analisi RFM
    • 6.1 Calcolo RFM
    • 6.2 Scoring RFM e Segmentazione Clienti
    • 6.3 Visualizzazione dei Segmenti RFM e Raccomandazioni Azionabili
    • 6.4 Esportazione dei Risultati RFM
  • 7. Analisi di Cohorts – Retention dei Clienti nel Tempo
    • 7.1 Setup e Calcolo dei Cohorts
    • 7.2 Tabella e Heatmap di Retention
    • 7.3 Insight sui Cohorts e Raccomandazioni per la Retention
  • 8. Previsione Base – Predizione dei Ricavi Futuri con Prophet
    • 8.1 Setup della Previsione con Prophet
    • 8.2 Insight di Previsione e Raccomandazioni di Business
  • 9. Finalizzazione e Presentazione
    • 9.1 Esportazione delle Tabelle Chiave
    • 9.2 Riassunto del Progetto e Insight Chiave
    • 9.3 Dashboard in Power BI
    • 9.4 Conclusione del Progetto e Prossimi Passi
    • 9.5 Report Pubblicato e Download

Introduzione¶

Obiettivo del Progetto
Analisi orientata al business: vendite, comportamento del cliente, RFM, cohorts, CLV, previsione di base + raccomandazioni actionable.

Dataset
Dataset Pubblico Olist – ~100k ordini (Kaggle)

Tech Stack

  • Python: pandas, seaborn, plotly, matplotlib, prophet
  • Dashboard: Power BI

1. Comprensione del Business e Domande ¶

Domande chiave:

  • Quali sono le categorie / prodotti top per ricavi?
  • Retention e recurrenza dei clienti?
  • Segmentazione RFM?
  • Retention dei cohorts nel tempo?
  • Performance di consegna per regione?
  • Impatto dei metodi di pagamento?
  • Opportunità di cross-sell?
  • Previsione per le categorie top?

2. Caricamento e Panoramica dei Dati ¶

Questa sezione copre l'ingestione iniziale dei file CSV raw dal dataset Olist e fornisce una panoramica di alto livello sulla struttura, dimensione e contenuto di ciascuna tabella. L'obiettivo è confermare il caricamento riuscito, identificare le relazioni chiave tra le tabelle e individuare eventuali segnali immediati di qualità dei dati prima di procedere con la pulizia e l'analisi.

2.1. Caricamento dei Dati ¶

Carico le tabelle più rilevanti dal dataset Olist (orders, order_items, customers, payments, reviews) utilizzando pandas.
In questa fase vengono caricate solo le tabelle essenziali per mantenere basso l'uso della memoria e concentrarsi sulle entità principali necessarie per l'analisi di vendite, clienti e logistica.

In [33]:
# Imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import os
import plotly.graph_objects as go
from datetime import datetime
from prophet import Prophet
import plotly.io as pio

# set Plotly renderer so charts embed as interactive HTML instead of static images
pio.renderers.default = "notebook_connected"

# Settings for better visualizations
pd.set_option('display.max_columns', None)             # Show all columns
pd.set_option('display.max_rows', 100)                 # Show more rows
pd.set_option('display.float_format', '{:2f}'.format)  # Number formatting

sns.set_style("whitegrid")                             # Plot style
plt.rcParams['figure.figsize'] = (10, 6)               # Default figure size

print("Execution environment initialized successfully.")
print(f"• Pandas version: {pd.__version__}")
Execution environment initialized successfully.
• Pandas version: 2.3.3
In [34]:
# Base path to the data folder
data_path = './data/'

# Load key tables
orders = pd.read_csv(data_path + 'raw/olist_orders_dataset.csv')
order_items = pd.read_csv(data_path + 'raw/olist_order_items_dataset.csv')
customers = pd.read_csv(data_path + 'raw/olist_customers_dataset.csv')
payments = pd.read_csv(data_path + 'raw/olist_order_payments_dataset.csv')
reviews = pd.read_csv(data_path + 'raw/olist_order_reviews_dataset.csv')

2.2. Panoramica dei Dati e Controlli Iniziali ¶

Eseguo un'ispezione rapida di ciascuna tabella caricata per comprendere:

  • Numero di righe e colonne
  • Tipi di dati
  • Presenza di valori mancanti
  • Righe di esempio

Questo passo aiuta a mappare lo schema del dataset e decidere le priorità di pulizia successive.

In [35]:
# Quick look at each one
def quick_overview(df, name):
    print(f"\n=== {name.upper()} ===")
    print(f"Rows: {df.shape[0]:,}")
    print(f"Columns: {df.shape[1]}")
    print("\nData types and missing values:")
    print(df.info(verbose=False))
    print("\nFirst three rows:")
    display(df.head(3))  # display works better than print in Jupyter

quick_overview(orders, "Orders")
quick_overview(order_items, "Order Items")
quick_overview(customers, "Customers")
quick_overview(payments, "Payments")
quick_overview(reviews, "Reviews")
=== ORDERS ===
Rows: 99,441
Columns: 8

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 99441 entries, 0 to 99440
Columns: 8 entries, order_id to order_estimated_delivery_date
dtypes: object(8)
memory usage: 6.1+ MB
None

First three rows:
order_id customer_id order_status order_purchase_timestamp order_approved_at order_delivered_carrier_date order_delivered_customer_date order_estimated_delivery_date
0 e481f51cbdc54678b7cc49136f2d6af7 9ef432eb6251297304e76186b10a928d delivered 2017-10-02 10:56:33 2017-10-02 11:07:15 2017-10-04 19:55:00 2017-10-10 21:25:13 2017-10-18 00:00:00
1 53cdb2fc8bc7dce0b6741e2150273451 b0830fb4747a6c6d20dea0b8c802d7ef delivered 2018-07-24 20:41:37 2018-07-26 03:24:27 2018-07-26 14:31:00 2018-08-07 15:27:45 2018-08-13 00:00:00
2 47770eb9100c2d0c44946d9cf07ec65d 41ce2a54c0b03bf3443c3d931a367089 delivered 2018-08-08 08:38:49 2018-08-08 08:55:23 2018-08-08 13:50:00 2018-08-17 18:06:29 2018-09-04 00:00:00
=== ORDER ITEMS ===
Rows: 112,650
Columns: 7

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 112650 entries, 0 to 112649
Columns: 7 entries, order_id to freight_value
dtypes: float64(2), int64(1), object(4)
memory usage: 6.0+ MB
None

First three rows:
order_id order_item_id product_id seller_id shipping_limit_date price freight_value
0 00010242fe8c5a6d1ba2dd792cb16214 1 4244733e06e7ecb4970a6e2683c13e61 48436dade18ac8b2bce089ec2a041202 2017-09-19 09:45:35 58.900000 13.290000
1 00018f77f2f0320c557190d7a144bdd3 1 e5f2d52b802189ee658865ca93d83a8f dd7ddc04e1b6c2c614352b383efe2d36 2017-05-03 11:05:13 239.900000 19.930000
2 000229ec398224ef6ca0657da4fc703e 1 c777355d18b72b67abbeef9df44fd0fd 5b51032eddd242adc84c38acab88f23d 2018-01-18 14:48:30 199.000000 17.870000
=== CUSTOMERS ===
Rows: 99,441
Columns: 5

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 99441 entries, 0 to 99440
Columns: 5 entries, customer_id to customer_state
dtypes: int64(1), object(4)
memory usage: 3.8+ MB
None

First three rows:
customer_id customer_unique_id customer_zip_code_prefix customer_city customer_state
0 06b8999e2fba1a1fbc88172c00ba8bc7 861eff4711a542e4b93843c6dd7febb0 14409 franca SP
1 18955e83d337fd6b2def6b18a428ac77 290c77bc529b7ac935b93aa66c333dc3 9790 sao bernardo do campo SP
2 4e7b3e00288586ebd08712fdd0374a03 060e732b5b29e8181a18229c7b0b2b5e 1151 sao paulo SP
=== PAYMENTS ===
Rows: 103,886
Columns: 5

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 103886 entries, 0 to 103885
Columns: 5 entries, order_id to payment_value
dtypes: float64(1), int64(2), object(2)
memory usage: 4.0+ MB
None

First three rows:
order_id payment_sequential payment_type payment_installments payment_value
0 b81ef226f3fe1789b1e8b2acac839d17 1 credit_card 8 99.330000
1 a9810da82917af2d9aefd1278f1dcfa0 1 credit_card 1 24.390000
2 25e8ea4e93396b6fa0d3dd708e76c1bd 1 credit_card 1 65.710000
=== REVIEWS ===
Rows: 99,224
Columns: 7

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 99224 entries, 0 to 99223
Columns: 7 entries, review_id to review_answer_timestamp
dtypes: int64(1), object(6)
memory usage: 5.3+ MB
None

First three rows:
review_id order_id review_score review_comment_title review_comment_message review_creation_date review_answer_timestamp
0 7bc2406110b926393aa56f80a40eba40 73fc7af87114b39712e6da79b0a377eb 4 NaN NaN 2018-01-18 00:00:00 2018-01-18 21:46:59
1 80e641a11e56f04c1ad469d5645fdfde a548910a1c6147796b98fdf73dbeba33 5 NaN NaN 2018-03-10 00:00:00 2018-03-11 03:05:13
2 228ce5500dc1d8e020d8d1322874b6f0 f9e4b658b201a9f2ecdecbb34bed034b 5 NaN NaN 2018-02-17 00:00:00 2018-02-18 14:36:24

3. Pulizia e Preparazione dei Dati ¶

3.1. Pulizia Iniziale dei Dati ¶

In questa fase eseguo controlli fondamentali di qualità dei dati: conversione delle colonne timestamp in formato datetime appropriato, verifica dell'unicità degli identificatori chiave (order_id, customer_id), e ispezione dei valori mancanti nelle colonne critiche. L'obiettivo è garantire che i dati raw siano affidabili e pronti per l'analisi senza introdurre errori nei calcoli o nei join.

Conversioni di tipo¶

Converto le stringhe timestamp in oggetti datetime per poter eseguire calcoli basati sul tempo (delta, raggruppamenti per mese, ecc.) in modo accurato.

In [36]:
# 3.1.1. Convert date columns to datetime

date_columns = [
    'order_purchase_timestamp',
    'order_approved_at',
    'order_delivered_carrier_date',
    'order_delivered_customer_date',
    'order_estimated_delivery_date'
]

for col in date_columns:
    orders[col] = pd.to_datetime(orders[col], errors='coerce')

print("Date columns converted:")
print(orders[date_columns].dtypes)
Date columns converted:
order_purchase_timestamp         datetime64[ns]
order_approved_at                datetime64[ns]
order_delivered_carrier_date     datetime64[ns]
order_delivered_customer_date    datetime64[ns]
order_estimated_delivery_date    datetime64[ns]
dtype: object

Controlli di Duplicati¶

Verifico che le chiavi primarie (order_id in orders, customer_id in customers) non abbiano duplicati, evitando conteggi gonfiati durante i merge o le aggregazioni.

In [37]:
# 3.1.2. Check duplicates in key tables

print("\nDuplicates:")
print("orders order_id duplicated:", orders['order_id'].duplicated().sum())
print("customers customer_id duplicated:", customers['customer_id'].duplicated().sum())
print("order_items (should be 0):", order_items.duplicated().sum())
Duplicates:
orders order_id duplicated: 0
customers customer_id duplicated: 0
order_items (should be 0): 0

Ispezione dei Valori Mancanti¶

Identifico e comprendo i pattern di dati mancanti, in particolare nei timestamp relativi alle consegne e nei commenti delle recensioni, per decidere strategie di gestione appropriate.

In [38]:
# 3.1.3. Missing values summary (focus on critical columns)

print("\nMissing values in orders:")
print(orders.isnull().sum()[orders.isnull().sum() > 0])

print("\nMissing values in reviews (expected high in comments):")
print(reviews.isnull().sum()[reviews.isnull().sum() > 0])
Missing values in orders:
order_approved_at                 160
order_delivered_carrier_date     1783
order_delivered_customer_date    2965
dtype: int64

Missing values in reviews (expected high in comments):
review_comment_title      87656
review_comment_message    58247
dtype: int64

3.2. Feature Engineering e Trasformazioni di Business ¶

Flag di Stato¶

Creo colonne booleane (is_delivered, is_approved) per filtrare facilmente ordini completati ed evitare problemi con NaN in metriche basate sul tempo.

In [39]:
# 3.2.1. Fill / handle missing timestamps in a business-friendly way

# Create flags (is_delivered, is_approved) for filtering later (e.g., only analyze delivered orders for delivery performance)
orders['is_delivered'] = orders['order_delivered_customer_date'].notna()
orders['is_approved'] = orders['order_approved_at'].notna()

print("Order status breakdown after cleaning:")
print(orders['order_status'].value_counts(normalize=True).round(3) * 100) # Use proportions (fractions that sum to 1), round to 3 d.p., and convert to %
Order status breakdown after cleaning:
order_status
delivered     97.000000
shipped        1.100000
canceled       0.600000
unavailable    0.600000
invoiced       0.300000
processing     0.300000
created        0.000000
approved       0.000000
Name: proportion, dtype: float64

Calcoli del Tempo di Consegna¶

Calcolo actual_delivery_time_days: i giorni di calendario reali dall'acquisto alla consegna al cliente — chiave per comprendere l'esperienza del cliente e la velocità logistica.

In [40]:
# 3.2.2. Calculate actual delivery time (from purchase to delivered) | how long the customer actually waited (purchase → delivered)
orders['actual_delivery_time_days'] = (
    orders['order_delivered_customer_date'] - orders['order_purchase_timestamp']
).dt.days
In [41]:
# 3.2.3. Calculate estimated vs actual delivery time difference | → positive = arrived late, negative = arrived early
orders['actual_minus_estimated_delivery_days'] = (
    orders['order_delivered_customer_date'] - orders['order_estimated_delivery_date']
).dt.days

Metriche di Ritardo e Performance¶

Calcolo actual_minus_estimated_delivery_days: quanto prima o dopo è arrivata l'ordine rispetto alla data promessa (negativo = anticipato, positivo = in ritardo) — essenziale per valutare l'accuratezza della promessa di consegna di Olist e il suo impatto sulla soddisfazione del cliente.

In [42]:
# Filter extreme outliers

reasonable_delivery = orders['actual_delivery_time_days'].between(0, 60)  # realistic range
print("\n% of orders with reasonable delivery time (0–60 days):", reasonable_delivery.mean().round(3) * 100)
% of orders with reasonable delivery time (0–60 days): 96.7
In [43]:
# Quick summary of new features

print("\nDelivery time stats (only delivered orders):")  
delivered_orders = orders[orders['is_delivered']]             # Only consider delivered orders: Use mask 'orders['is_delivered']' to keep only rows where mask == True
print(delivered_orders[['actual_delivery_time_days', 'actual_minus_estimated_delivery_days']].describe())
Delivery time stats (only delivered orders):
       actual_delivery_time_days  actual_minus_estimated_delivery_days
count               96476.000000                          96476.000000
mean                   12.094086                            -11.876881
std                     9.551746                             10.183854
min                     0.000000                           -147.000000
25%                     6.000000                            -17.000000
50%                    10.000000                            -12.000000
75%                    15.000000                             -7.000000
max                   209.000000                            188.000000

4. Integrazione dei Dati e Primi Insight di Business ¶

4.1. Salvataggio dei Dati Puliti ¶

Salvo il DataFrame orders pulito e arricchito (con nuove feature) in una cartella di dati processati.

Questo segue le best practice: non sovrascrivere mai i dati raw e creare file intermedi riproducibili.

In [44]:
# 4.1 Saving Cleaned Data

# Create 'processed' subfolder if it doesn't exist
processed_dir = './data/processed/'
os.makedirs(processed_dir, exist_ok=True)

# Save the enriched orders table
orders.to_csv(processed_dir + 'cleaned_orders_with_features.csv', index=False)

print("Cleaned & enriched orders saved to:")
print(processed_dir + 'cleaned_orders_with_features.csv')
Cleaned & enriched orders saved to:
./data/processed/cleaned_orders_with_features.csv

4.2. Primo Merge – Creazione di una Tabella Base di Lavoro ¶

Combino le tabelle principali (orders + customers + payments) in un unico DataFrame master.

Questo fornisce una tabella unica con posizione del cliente, valore totale di pagamento e dettagli dell'ordine — ideale per analisi dei ricavi, segmentazione clienti e insight geografici.

In [45]:
# 4.2 First Merge – Creating a Base Working Table

# Step 1: Aggregate payments per order (some orders have multiple payments)
# I want total payment value per order_id
payments_total = payments.groupby('order_id')['payment_value'].sum().reset_index()
payments_total = payments_total.rename(columns={'payment_value': 'total_order_value'})

# Step 2: Merge orders + customers (get state and unique customer id)
base_df = orders.merge(
    customers[['customer_id', 'customer_unique_id', 'customer_state', 'customer_city']],
    on='customer_id',
    how='left'
)

# Step 3: Add total payment value
base_df = base_df.merge(
    payments_total,
    on='order_id',
    how='left'
)

# Quality check after merge
print("Shape before merges:", orders.shape)
print("Shape after merges:", base_df.shape)
print("\nMissing total_order_value after merge:", base_df['total_order_value'].isnull().sum())

# Show first few rows of our new base table
print("\nFirst 3 rows of base working table:")
display(base_df[['order_id', 'customer_unique_id', 'customer_state', 'order_status', 
                 'actual_delivery_time_days', 'total_order_value']].head(3))
Shape before merges: (99441, 12)
Shape after merges: (99441, 16)

Missing total_order_value after merge: 1

First 3 rows of base working table:
order_id customer_unique_id customer_state order_status actual_delivery_time_days total_order_value
0 e481f51cbdc54678b7cc49136f2d6af7 7c396fd4830fd04220f754e42b4e5bff SP delivered 8.000000 38.710000
1 53cdb2fc8bc7dce0b6741e2150273451 af07308b275d755c9edb36a90c618231 BA delivered 13.000000 141.460000
2 47770eb9100c2d0c44946d9cf07ec65d 3a653a41f6f9fc3d2a113cf8398680e8 GO delivered 9.000000 179.120000

4.3. Primo Insight di Business: Ricavi e Performance di Consegna per Stato ¶

Aggrego la tabella base per calcolare il ricavo totale e il tempo medio di consegna per stato del cliente.

Questo fornisce una visione iniziale delle performance geografiche — identificando regioni ad alto valore e potenziali colli di bottiglia logistici.

In [46]:
# 4.3 First Business Insight: Revenue & Delivery Performance by State

# Filter only delivered orders (to make delivery metrics meaningful)
delivered_base = base_df[base_df['is_delivered']]

# Aggregate key metrics by state
state_summary = delivered_base.groupby('customer_state').agg(
    total_revenue=('total_order_value', 'sum'),
    avg_delivery_days=('actual_delivery_time_days', 'mean'),
    median_delivery_days=('actual_delivery_time_days', 'median'),
    order_count=('order_id', 'count'),
    avg_delay_days=('actual_minus_estimated_delivery_days', 'mean')
).reset_index()

# Format numbers for readability
state_summary['total_revenue'] = state_summary['total_revenue'].round(2)
state_summary['avg_delivery_days'] = state_summary['avg_delivery_days'].round(1)
state_summary['median_delivery_days'] = state_summary['median_delivery_days'].round(1)
state_summary['avg_delay_days'] = state_summary['avg_delay_days'].round(1)

# Add the average revenue per order for each state
state_summary['avg_revenue_per_order'] = (state_summary['total_revenue'] / state_summary['order_count']).round(2)

# Sort by total revenue descending
state_summary = state_summary.sort_values('total_revenue', ascending=False)

# Show top 10 states
print("Top 10 states by total revenue (delivered orders only):")
display(
    state_summary.head(10).style.format({
        'total_revenue': '{:,.2f}',
        'avg_delivery_days': '{:.1f}',
        'median_delivery_days': '{:.1f}',
        'order_count': '{:,}',
        'avg_delay_days': '{:.1f}',
        'avg_revenue_per_order': '{:,.2f}',
    })
)
Top 10 states by total revenue (delivered orders only):
  customer_state total_revenue avg_delivery_days median_delivery_days order_count avg_delay_days avg_revenue_per_order
25 SP 5,769,221.49 8.3 7.0 40,495 -11.1 142.47
18 RJ 2,056,101.21 14.8 12.0 12,353 -11.8 166.45
10 MG 1,819,321.70 11.5 10.0 11,355 -13.2 160.22
22 RS 861,608.40 14.8 13.0 5,344 -13.9 161.23
17 PR 781,919.55 11.5 10.0 4,923 -13.3 158.83
23 SC 595,361.91 14.5 13.0 3,547 -11.5 167.85
4 BA 591,270.60 18.9 16.0 3,256 -10.8 181.59
6 DF 346,146.17 12.5 11.0 2,080 -12.0 166.42
8 GO 334,294.22 15.2 13.0 1,957 -12.2 170.82
7 ES 317,682.65 15.3 13.0 1,995 -10.5 159.24
In [47]:
# Interactive bar chart with Plotly

fig = px.bar(
    state_summary.head(10),
    x='customer_state',
    y='total_revenue',
    color='avg_delivery_days',
    color_continuous_scale='RdYlGn_r',   # Red = late, Green = early
    title='Top 10 States by Revenue (color = avg actual delivery days)',
    labels={
        'customer_state': 'State',
        'total_revenue': 'Total Revenue (BRL)',
        'avg_delivery_days': 'Avg Delivery Time (days)'
    },
    hover_data=['order_count', 'median_delivery_days', 'avg_delay_days']
)

fig.update_layout(
    xaxis_title="State (UF)",
    yaxis_title="Total Revenue (BRL)",
    coloraxis_colorbar_title="Avg Delivery (days)"
)

fig.show()

Salvataggio del Riassunto per Stato¶

Esporto le metriche aggregate di performance per stato in un file processato per uso futuro (ad esempio, dashboarding in Power BI o visualizzazioni aggiuntive).

In [48]:
# 4.3.1 Saving State-Level Summary

# Reuse the same processed directory I created earlier
processed_dir = './data/processed/'
os.makedirs(processed_dir, exist_ok=True) 

# Save the summary
state_summary.to_csv(
    processed_dir + 'state_performance_summary.csv',
    index=False
)

print("State summary saved successfully to:")
print(processed_dir + 'state_performance_summary.csv')
State summary saved successfully to:
./data/processed/state_performance_summary.csv

5. Analisi di Prodotti e Categorie ¶

5.1. Caricamento di Tabelle Aggiuntive ¶

Carico le tabelle relative ai prodotti per abilitare insight a livello di categoria.

  • products: attributi dei prodotti (categoria, dimensioni)
  • product_category_name_translation: traduzione in inglese dei nomi delle categorie in portoghese
  • order_items: collega ordini ai prodotti (quantità, prezzo, spedizione) e già caricata precedentemente.
In [49]:
# 5.1 Loading Additional Tables

# Load the two new tables
products = pd.read_csv(data_path + 'raw/olist_products_dataset.csv')
category_translation = pd.read_csv(data_path + 'raw/product_category_name_translation.csv')

# Quick check
print("order_items shape:", order_items.shape)
print("products shape:", products.shape)
print("category_translation shape:", category_translation.shape)

# Preview both tables
quick_overview(products, "Products")
quick_overview(category_translation, "Category Translation")
order_items shape: (112650, 7)
products shape: (32951, 9)
category_translation shape: (71, 2)

=== PRODUCTS ===
Rows: 32,951
Columns: 9

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32951 entries, 0 to 32950
Columns: 9 entries, product_id to product_width_cm
dtypes: float64(7), object(2)
memory usage: 2.3+ MB
None

First three rows:
product_id product_category_name product_name_lenght product_description_lenght product_photos_qty product_weight_g product_length_cm product_height_cm product_width_cm
0 1e9e8ef04dbcff4541ed26657ea517e5 perfumaria 40.000000 287.000000 1.000000 225.000000 16.000000 10.000000 14.000000
1 3aa071139cb16b67ca9e5dea641aaa2f artes 44.000000 276.000000 1.000000 1000.000000 30.000000 18.000000 20.000000
2 96bd76ec8810374ed1b65e291975717f esporte_lazer 46.000000 250.000000 1.000000 154.000000 18.000000 9.000000 15.000000
=== CATEGORY TRANSLATION ===
Rows: 71
Columns: 2

Data types and missing values:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 71 entries, 0 to 70
Columns: 2 entries, product_category_name to product_category_name_english
dtypes: object(2)
memory usage: 1.2+ KB
None

First three rows:
product_category_name product_category_name_english
0 beleza_saude health_beauty
1 informatica_acessorios computers_accessories
2 automotivo auto

5.2. Merge delle Informazioni sui Prodotti nella Tabella Base ¶

Unisco order_items con products e le traduzioni delle categorie, poi aggrego per ottenere metriche di performance per categoria (ricavi, conteggio ordini, prezzo medio, ecc.).

In [50]:
# 5.2 Merging Product Information

# Step 1: Translate category names (left join to keep all products)
products_en = products.merge(
    category_translation,
    on='product_category_name',
    how='left'
)

# Step 2: Merge order_items with products_en (add category, price, etc.)
items_with_category = order_items.merge(
    products_en[['product_id', 'product_category_name_english', 'product_weight_g']],
    on='product_id',
    how='left'
)

# Handle missing English category names
items_with_category['product_category_name_english'] = items_with_category['product_category_name_english'].fillna('uncategorized')

# Quick check for missing English names (should be very few)
print("Missing English category names:", 
      items_with_category['product_category_name_english'].isnull().sum())

# Step 3: Aggregate by category (revenue, count, avg price, etc.)
category_performance = items_with_category.groupby('product_category_name_english').agg(
    total_revenue=('price', 'sum'),
    total_freight=('freight_value', 'sum'),
    order_items_count=('order_id', 'count'),
    unique_orders=('order_id', 'nunique'),
    avg_price=('price', 'mean'),
    avg_weight_g=('product_weight_g', 'mean')
).reset_index()

# Add revenue share %
total_revenue_all = category_performance['total_revenue'].sum()
category_performance['revenue_share_pct'] = (
    category_performance['total_revenue'] / total_revenue_all * 100
).round(2)

# Sort by revenue descending
category_performance = category_performance.sort_values('total_revenue', ascending=False)

# Show top 15 categories
print("\nTop 15 categories by total revenue:")
display(
    category_performance.head(15).style.format({
        'total_revenue': '{:,.2f}',
        'total_freight': '{:,.2f}',
        'avg_price': '{:,.2f}',
        'avg_weight_g': '{:.0f}',
        'revenue_share_pct': '{:.2f}%'
    })
)
Missing English category names: 0

Top 15 categories by total revenue:
  product_category_name_english total_revenue total_freight order_items_count unique_orders avg_price avg_weight_g revenue_share_pct
43 health_beauty 1,258,681.34 182,566.73 9670 8836 130.16 1049 9.26%
71 watches_gifts 1,205,005.68 100,535.93 5991 5624 201.14 581 8.87%
7 bed_bath_table 1,036,988.68 204,693.04 11115 9417 93.30 2117 7.63%
65 sports_leisure 988,048.97 168,607.51 8641 7720 114.34 1743 7.27%
15 computers_accessories 911,954.32 147,318.08 7827 6689 116.51 903 6.71%
39 furniture_decor 729,762.49 172,749.30 8334 6449 87.56 2653 5.37%
20 cool_stuff 635,290.85 84,039.10 3796 3632 167.36 2549 4.67%
49 housewares 632,248.66 146,149.11 6964 5884 90.79 3215 4.65%
5 auto 592,720.11 92,664.21 4235 3897 139.96 2594 4.36%
42 garden_tools 485,256.46 98,962.75 4347 3518 111.63 2824 3.57%
69 toys 483,946.60 77,425.95 4117 3886 117.55 1857 3.56%
6 baby 411,764.89 68,353.11 3065 2885 134.34 3273 3.03%
59 perfumery 399,124.87 54,213.84 3419 3162 116.74 480 2.94%
68 telephony 323,667.53 71,215.79 4545 4199 71.21 262 2.38%
57 office_furniture 273,960.70 68,571.95 1691 1273 162.01 11390 2.02%

5.3. Visualizzazione della Performance per Categoria e Insight ¶

Visualizzo le categorie top per ricavi e genero insight actionable su pricing, spedizione e opportunità.

In [51]:
# Top 10 categories for clean visualization
top_categories = category_performance.head(10)

# Bar chart: Revenue by category
fig_bar = px.bar(
    top_categories,
    x='product_category_name_english',
    y='total_revenue',
    color='avg_price',
    color_continuous_scale='YlOrRd',
    title='Top 10 Product Categories by Total Revenue',
    labels={
        'product_category_name_english': 'Category',
        'total_revenue': 'Total Revenue (BRL)',
        'avg_price': 'Average Unit Price (BRL)'
    },
    hover_data=['order_items_count', 'unique_orders', 'total_freight', 'revenue_share_pct']
)

fig_bar.update_layout(
    xaxis_title="Category",
    yaxis_title="Total Revenue (BRL)",
    xaxis_tickangle=-45,
    coloraxis_colorbar_title="Avg Price"
)

fig_bar.show()
In [52]:
# Pie chart: Revenue share (top 10 + others)
others_revenue = category_performance.iloc[10:]['total_revenue'].sum()

pie_data = pd.concat([
    top_categories[['product_category_name_english', 'total_revenue']],
    pd.DataFrame({'product_category_name_english': ['Others'], 'total_revenue': [others_revenue]})
])

fig_pie = px.pie(
    pie_data,
    values='total_revenue',
    names='product_category_name_english',
    title='Revenue Share - Top 10 Categories vs Others',
    hole=0.4  # donut style
)

fig_pie.show()

Osservazioni Chiave e Raccomandazioni Iniziali:

  • Salute e Bellezza guida con ~10 % del revenue totale: alto volume combinato con un prezzo medio solido → ideale per campagne di massa, promozioni per volume e sforzi di marketing ampi.

  • Letto, Bagno e Tavola e Arredamento e Decorazione mostrano costi di spedizione significativamente alti rispetto al prezzo → opportunità per rivedere il pricing (aumentare i margini) o negoziare tariffe logistiche migliori con i corrieri.

  • Orologi e Regali ha il ticket medio più alto (~201 BRL) → forte potenziale per upselling, bundle premium, raccomandazioni personalizzate e programmi fedeltà rivolti a clienti ad alto valore.

  • Le top 5 categorie rappresentano più del 40 % del revenue totale → alto rischio di concentrazione; considerare di diversificare promuovendo categorie emergenti o sottoperformanti.

  • 1.627 articoli rimangono non categorizzati (~1–2 % del revenue) → vale la pena una revisione manuale per creare nuove categorie, migliorare la discoverability dei prodotti e potenziare gli algoritmi di raccomandazione.

5.4. Performance per Categoria e Stato ¶

Riunisco le informazioni sulle categorie con la tabella base dei clienti per analizzare quali categorie di prodotti performano meglio in ciascun stato brasiliano.

Questo aiuta a identificare preferenze regionali, opportunità localizzate e potenziali aggiustamenti logistici/pricing per regione.

In [53]:
# 5.4 Category Performance by State

# Step 1: Keep only delivered orders for meaningful metrics
delivered_base = base_df[base_df['is_delivered']].copy()

# Step 2: Add category info to delivered orders
# First, aggregate items by order_id (one row per order with main category)
# For simplicity, I take the category of the most expensive item per order (common approach)
order_main_category = (
    items_with_category.loc[
        items_with_category.groupby('order_id')['price'].idxmax()
    ]
    [['order_id', 'product_category_name_english']]
    .reset_index(drop=True)
)

# Merge main category into delivered_base
delivered_with_category = delivered_base.merge(
    order_main_category,
    on='order_id',
    how='left'
)

# Fill any missing categories (rare after previous fix)
delivered_with_category['product_category_name_english'] = delivered_with_category['product_category_name_english'].fillna('uncategorized')

# Step 3: Aggregate: revenue & order count per state + category
state_category_summary = delivered_with_category.groupby(
    ['customer_state', 'product_category_name_english']
).agg(
    total_revenue=('total_order_value', 'sum'),
    order_count=('order_id', 'nunique'),
    avg_ticket=('total_order_value', 'mean')
).reset_index()

# Add revenue share within each state
state_category_summary['state_total_revenue'] = state_category_summary.groupby('customer_state')['total_revenue'].transform('sum')
state_category_summary['revenue_share_in_state_pct'] = (
    state_category_summary['total_revenue'] / state_category_summary['state_total_revenue'] * 100
).round(2)

# Sort by state and then by revenue descending within state
state_category_summary = state_category_summary.sort_values(
    ['customer_state', 'total_revenue'], ascending=[True, False]
)

# Show top categories for the top 5 revenue states (SP, RJ, MG, RS, PR)
top_states = ['SP', 'RJ', 'MG', 'RS', 'PR']
for state in top_states:
    print(f"\nTop 10 categories in {state} by revenue:")
    display(
        state_category_summary[state_category_summary['customer_state'] == state]
        .head(10)
        .style.format({
            'total_revenue': '{:,.2f}',
            'avg_ticket': '{:,.2f}',
            'revenue_share_in_state_pct': '{:.2f}%',
            'state_total_revenue': '{:.2f}'
        })
    )
Top 10 categories in SP by revenue:
  customer_state product_category_name_english total_revenue order_count avg_ticket state_total_revenue revenue_share_in_state_pct
1272 SP bed_bath_table 549,408.91 4307 127.56 5769221.49 9.52%
1308 SP health_beauty 509,859.18 3693 138.10 5769221.49 8.84%
1335 SP watches_gifts 449,135.06 2083 215.62 5769221.49 7.79%
1329 SP sports_leisure 427,734.06 3203 133.54 5769221.49 7.41%
1280 SP computers_accessories 386,706.97 2609 148.22 5769221.49 6.70%
1304 SP furniture_decor 331,287.25 2618 126.54 5769221.49 5.74%
1314 SP housewares 323,729.96 2693 120.21 5769221.49 5.61%
1270 SP auto 235,440.59 1579 149.11 5769221.49 4.08%
1285 SP cool_stuff 230,410.92 1279 180.15 5769221.49 3.99%
1333 SP toys 205,513.18 1568 131.07 5769221.49 3.56%
Top 10 categories in RJ by revenue:
  customer_state product_category_name_english total_revenue order_count avg_ticket state_total_revenue revenue_share_in_state_pct
986 RJ watches_gifts 188,485.58 784 240.42 2056101.21 9.17%
924 RJ bed_bath_table 175,594.31 1342 130.85 2056101.21 8.54%
960 RJ health_beauty 159,174.66 935 170.24 2056101.21 7.74%
980 RJ sports_leisure 140,578.56 889 158.13 2056101.21 6.84%
932 RJ computers_accessories 138,232.02 832 166.14 2056101.21 6.72%
956 RJ furniture_decor 118,425.00 809 146.38 2056101.21 5.76%
966 RJ housewares 93,000.20 709 131.17 2056101.21 4.52%
937 RJ cool_stuff 91,488.42 478 191.40 2056101.21 4.45%
959 RJ garden_tools 85,899.27 522 164.56 2056101.21 4.18%
984 RJ toys 83,263.44 539 154.48 2056101.21 4.05%
Top 10 categories in MG by revenue:
  customer_state product_category_name_english total_revenue order_count avg_ticket state_total_revenue revenue_share_in_state_pct
514 MG health_beauty 175,305.23 987 177.61 1819321.70 9.64%
480 MG bed_bath_table 155,527.65 1108 140.37 1819321.70 8.55%
541 MG watches_gifts 132,117.43 598 220.93 1819321.70 7.26%
535 MG sports_leisure 130,027.02 845 153.88 1819321.70 7.15%
487 MG computers_accessories 126,693.85 857 147.83 1819321.70 6.96%
510 MG furniture_decor 97,409.77 701 138.96 1819321.70 5.35%
520 MG housewares 92,826.66 676 137.32 1819321.70 5.10%
478 MG auto 82,521.85 458 180.18 1819321.70 4.54%
492 MG cool_stuff 79,890.81 422 189.31 1819321.70 4.39%
513 MG garden_tools 72,488.16 478 151.65 1819321.70 3.98%
Top 10 categories in RS by revenue:
  customer_state product_category_name_english total_revenue order_count avg_ticket state_total_revenue revenue_share_in_state_pct
1098 RS bed_bath_table 73,416.22 532 138.00 861608.40 8.52%
1127 RS furniture_decor 65,638.11 425 154.44 861608.40 7.62%
1106 RS computers_accessories 61,275.72 385 159.16 861608.40 7.11%
1151 RS sports_leisure 60,578.49 411 147.39 861608.40 7.03%
1130 RS health_beauty 59,453.00 388 153.23 861608.40 6.90%
1157 RS watches_gifts 51,874.17 222 233.67 861608.40 6.02%
1111 RS cool_stuff 49,747.79 251 198.20 861608.40 5.77%
1136 RS housewares 48,260.70 340 141.94 861608.40 5.60%
1129 RS garden_tools 38,871.63 219 177.50 861608.40 4.51%
1155 RS toys 33,347.80 200 166.74 861608.40 3.87%
Top 10 categories in PR by revenue:
  customer_state product_category_name_english total_revenue order_count avg_ticket state_total_revenue revenue_share_in_state_pct
911 PR sports_leisure 66,731.65 419 159.26 781919.55 8.53%
917 PR watches_gifts 62,263.22 265 234.96 781919.55 7.96%
891 PR health_beauty 61,366.50 375 163.64 781919.55 7.85%
888 PR furniture_decor 60,326.83 382 157.92 781919.55 7.72%
859 PR bed_bath_table 55,499.30 395 140.50 781919.55 7.10%
866 PR computers_accessories 50,583.03 333 151.90 781919.55 6.47%
871 PR cool_stuff 41,963.34 201 208.77 781919.55 5.37%
897 PR housewares 38,546.88 279 138.16 781919.55 4.93%
857 PR auto 32,421.59 202 160.50 781919.55 4.15%
915 PR toys 27,313.06 198 137.94 781919.55 3.49%

5.5. Visualizzazione: Partecipazione dei Ricavi per Categoria e Stato ¶

Un heatmap mostra l'importanza relativa di ciascuna categoria all'interno degli stati principali, evidenziando preferenze regionali.

In [54]:
# 5.4.1 Visualization: Heatmap of Revenue Share by State and Category

# Filter to top 5 states and top 10 categories for readability
top_states = ['SP', 'RJ', 'MG', 'RS', 'PR']
top_cats = category_performance.head(10)['product_category_name_english'].tolist()

heatmap_df = state_category_summary[
    (state_category_summary['customer_state'].isin(top_states)) &
    (state_category_summary['product_category_name_english'].isin(top_cats))
]

# Pivot: states as rows, categories as columns, revenue share as values
pivot_table = heatmap_df.pivot(
    index='customer_state',      # rows
    columns='product_category_name_english',
    values='revenue_share_in_state_pct'
).fillna(0)

# Plot heatmap
plt.figure(figsize=(14, 6))

hm = sns.heatmap(
        pivot_table,
        annot=True,                # show numbers in cells
        fmt=".1f",
        annot_kws={"size": 12},    # font size of the numbers inside cells
        cmap="YlGnBu",
        linewidths=0.5,
        cbar_kws={'label': 'Revenue Share in State (%)'}
)

# Style the colorbar label
cbar = hm.collections[0].colorbar
cbar.set_label(
    'Revenue Share in State (%)',
    fontsize=16,               # ← adjust this number (16 is noticeably larger)
    fontweight='bold',
    labelpad=15
)
cbar.ax.tick_params(labelsize=12) # larger ticks on colorbar

plt.title('Revenue Share (%) – Top Categories in Top 5 States', fontsize=20, fontweight='bold', pad=25)
plt.xlabel('Category', fontsize=16, fontweight='bold', labelpad=10)
plt.ylabel('State (UF)', fontsize=16, fontweight='bold', labelpad=25)
plt.xticks(rotation=45, ha='right', fontsize=14)
plt.yticks(fontsize=14)
plt.tight_layout()
plt.show()
No description has been provided for this image

Insight Regionali per Categoria e Raccomandazioni:

  • São Paulo (SP): Dominato da letto_bagno_tavola (~9.5%), salute_bellezza (8.8%) e orologi_regali → mercato maturo con domanda diversificata; priorizzare bundle su articoli per la casa + bellezza e annunci mirati in queste categorie.

  • Rio de Janeiro (RJ) e Minas Gerais (MG): Maggiore quota relativa in decorazione_mobili e articoli_casa → preferenza regionale per articoli domestici; considerare promozioni di spedizione gratuita o pricing localizzato per compensare la sensibilità al costo di spedizione.

  • Stati del Sud (RS, PR): Più equilibrato verso sport_ricreazione, giocattoli e cose_cool → possibile influenza stagionale/culturale; esplorare campagne estive o promozioni focalizzate sui bambini.

  • Opportunità generale: Personalizzare raccomandazioni di prodotti e marketing per stato (es. focus su bellezza in SP, mobili in MG/RJ) → potenziale aumento di conversione e valore medio dell'ordine.


6. Segmentazione Clienti – Analisi RFM ¶

6.1. Calcolo RFM ¶

Calcolo le metriche classiche RFM per ciascun cliente unico:

  • Recency: Giorni dall'ultimo acquisto (minore = più recente)
  • Frequency: Numero di ordini effettuati
  • Monetary: Ricavo totale generato dal cliente

Questo costituisce la base per segmentare i clienti in gruppi (es. VIP, a rischio, nuovi, persi) e derivare strategie di retention e upselling.

In [55]:
# 6.1 RFM Calculation

# Use only delivered orders for accurate purchase behavior
rfm_base = base_df[base_df['is_delivered']].copy()

# Current reference date (use the max purchase date + 1 day for realism)
reference_date = rfm_base['order_purchase_timestamp'].max() + pd.Timedelta(days=1)

# Aggregate per customer_unique_id
rfm = rfm_base.groupby('customer_unique_id').agg(
    recency=('order_purchase_timestamp', lambda x: (reference_date - x.max()).days), # Days since last purchase (lower number = more recent/active customer)
    frequency=('order_id', 'nunique'), # Number of distinct orders (higher = more loyal/repeat buyer)
    monetary=('total_order_value', 'sum') # Total BRL spent (higher = higher lifetime value)
).reset_index()

# Quick overview
print("RFM table shape:", rfm.shape)
print("\nRFM descriptive stats:")
print(rfm[['recency', 'frequency', 'monetary']].describe().round(2))

# Show top 10 customers by monetary value (VIP preview)
print("\nTop 10 customers by total spend:")
display(rfm.sort_values('monetary', ascending=False).head(10).style.format({
    'monetary': '{:,.2f}',
    'recency': '{:,}'
}))
RFM table shape: (93356, 4)

RFM descriptive stats:
           recency    frequency     monetary
count 93356.000000 93356.000000 93356.000000
mean    237.970000     1.030000   165.190000
std     152.620000     0.210000   226.320000
min       1.000000     1.000000     0.000000
25%     114.000000     1.000000    63.050000
50%     219.000000     1.000000   107.780000
75%     346.000000     1.000000   182.540000
max     714.000000    15.000000 13664.080000

Top 10 customers by total spend:
  customer_unique_id recency frequency monetary
3724 0a0a92112bd4c708ca5fde585afaa872 334 1 13,664.08
79634 da122df9eeddfedc1dc1f5349a1a690c 515 2 7,571.63
43166 763c8b1c9c68a0229c42c9fc6f662b93 46 1 7,274.88
80461 dc4802a71eae9be1dd28f5d788ceb526 563 1 6,929.31
25432 459bef486812aa25204be022145caa62 35 1 6,922.21
93079 ff4159b92c40ebe40454e3e6a7c35ed6 462 1 6,726.66
23407 4007669dec559734d6f53e029e360987 279 1 6,081.54
87145 eebb5dda148d3893cdaf5b5ca3040ccb 498 1 4,764.34
26636 48e1ac109decbb87765a3eade6854098 69 1 4,681.78
73126 c8460e4251689ba205045f3ea17884a1 22 4 4,655.91

6.2. Scoring RFM e Segmentazione Clienti ¶

Assegno punteggi (4 = migliore, 1 = peggiore) a Recency (minore = migliore), Frequency e Monetary.

  • Recency e Monetary usano scoring basato sui quartili (pd.qcut)
  • Frequency usa soglie personalizzate a causa dell'estrema skewness (97 % dei clienti acquista solo una volta)

Poi combino i punteggi in segmenti clienti actionable per strategie di retention, re-engagement e upselling.

In [56]:
# 6.2 RFM Scoring & Customer Segmentation

# 6.2.1. Create scores (quartiles)

# Recency: use qcut (works fine because recency is more spread out)
rfm['R_score'] = pd.qcut(rfm['recency'], q=4, labels=[4, 3, 2, 1])  # 4 = most recent

# Monetary: use qcut (also spread out)
rfm['M_score'] = pd.qcut(rfm['monetary'], q=4, labels=[1, 2, 3, 4])  # 4 = highest spend

# Frequency: custom thresholds because of heavy skew
def frequency_score(f):
    if f == 1:
        return 1
    elif f == 2:
        return 2
    elif 3 <= f <= 4:
        return 3
    else:  # f >= 5
        return 4

rfm['F_score'] = rfm['frequency'].apply(frequency_score)

# Combine scores
rfm['RFM_score'] = rfm['R_score'].astype(str) + rfm['F_score'].astype(str) + rfm['M_score'].astype(str)

print("Frequency score distribution:")
print(rfm['F_score'].value_counts(normalize=True).sort_index() * 100)

# 6.2.2. Define meaningful business segments
def get_segment(row):
    r = int(row['R_score'])
    f = int(row['F_score'])
    m = int(row['M_score'])
    
    if r >= 3 and f >= 3 and m >= 3:
        return 'Champions'                  # Best customers – recent, frequent, high value
    elif r >= 3 and f >= 2:
        return 'Loyal Customers'            # Recent & frequent/loyal
    elif r >= 4 and f == 1 and m >= 2:
        return 'Recent High-Value New'      # New but spent a lot recently
    elif r <= 2 and f >= 3:
        return 'At Risk'                    # Good past behavior, but inactive now
    elif r <= 2 and f <= 2 and m <= 2:
        return 'Lost / Hibernating'         # Inactive, low value
    else:
        return 'Other / Potential'          # Middle ground

rfm['segment'] = rfm.apply(get_segment, axis=1)

# 6.2.3. Summary table by segment
segment_summary = rfm.groupby('segment').agg(
    customer_count=('customer_unique_id', 'count'),
    avg_recency_days=('recency', 'mean'),
    avg_frequency=('frequency', 'mean'),
    avg_monetary=('monetary', 'mean'),
    total_revenue=('monetary', 'sum')
).reset_index()

# Add percentage columns
total_customers = segment_summary['customer_count'].sum()
total_revenue_all = segment_summary['total_revenue'].sum()

segment_summary['customer_share_pct'] = (segment_summary['customer_count'] / total_customers * 100).round(2)
segment_summary['revenue_share_pct'] = (segment_summary['total_revenue'] / total_revenue_all * 100).round(2)

# Sort by revenue descending
segment_summary = segment_summary.sort_values('total_revenue', ascending=False)

print("\nRFM Segments Summary:")
display(
    segment_summary.style.format({
        'avg_recency_days': '{:.1f}',
        'avg_frequency': '{:.2f}',
        'avg_monetary': '{:,.2f}',
        'total_revenue': '{:,.2f}',
        'customer_share_pct': '{:.2f}%',
        'revenue_share_pct': '{:.2f}%'
    })
)
Frequency score distribution:
F_score
1   96.999657
2    2.756116
3    0.223874
4    0.020352
Name: proportion, dtype: float64

RFM Segments Summary:
  segment customer_count avg_recency_days avg_frequency avg_monetary total_revenue customer_share_pct revenue_share_pct
4 Other / Potential 51147 241.7 1.02 193.65 9,904,644.95 54.79% 64.22%
5 Recent High-Value New 16824 57.9 1.00 206.31 3,471,006.78 18.02% 22.51%
2 Lost / Hibernating 23750 365.1 1.01 63.98 1,519,629.32 25.44% 9.85%
3 Loyal Customers 1408 113.7 2.00 291.97 411,089.82 1.51% 2.67%
1 Champions 141 105.7 3.55 542.11 76,437.42 0.15% 0.50%
0 At Risk 86 362.9 3.15 453.76 39,023.14 0.09% 0.25%

6.3. Visualizzazione dei Segmenti RFM e Raccomandazioni Azionabili ¶

Visualizziamo la distribuzione di clienti e ricavi per segmento, poi deriviamo strategie concrete di business per ciascun gruppo (retention, re-engagement, upselling, aggiustamenti di pricing, ecc.).

In [57]:
# 6.3 RFM Segment Visualization

# 1. Bar chart: Customer count & Revenue share by segment
fig_bar = px.bar(
    segment_summary,
    x='segment',
    y=['customer_share_pct', 'revenue_share_pct'],
    barmode='group',
    title='<b>Customer Share vs Revenue Share by RFM Segment</b>',
    labels={
        'value': 'Percentage (%)',
        'variable': 'Metric',
        'segment': 'Customer Segment'
    },
    color_discrete_sequence=['#636EFA', '#EF553B']  # blue for customers, red for revenue
)

fig_bar.update_layout(
    title={'font': {'size': 18, 'color': 'black'}},
    xaxis_title="Segment",
    yaxis_title="Percentage (%)",
    legend_title="Metric",
    xaxis_tickangle=-45,
    height=500
)

fig_bar.show()
In [58]:
# 2. Pie chart: Revenue contribution by segment (focus on who drives the money)
fig_pie = px.pie(
    segment_summary,
    values='total_revenue',
    names='segment',
    title='Total Revenue Contribution by RFM Segment',
    hole=0.4,  # donut style
    color_discrete_sequence=px.colors.qualitative.Pastel
)

fig_pie.update_traces(textposition='inside', textinfo='percent+label')
fig_pie.update_layout(
    height=500,
    title={'font': {'size': 18, 'color': 'black'}}
)

fig_pie.show()

Raccomandazioni Azionabili per Segmento:

  • Altri / Potenziale (54.79 % clienti, 64.22 % ricavi)

    Gruppo più grande, motore principale dei ricavi ma con performance media.

    → Concentrarsi sulla conversione verso segmenti superiori: email personalizzate con sconti sul prossimo acquisto, raccomandazioni cross-sell (es. bundle salute_bellezza con letto_bagno_tavola).

  • Nuovi ad Alto Valore Recenti (18.02 % clienti, 22.51 % ricavi)

    Clienti nuovi con primo acquisto di alto valore — molto preziosi!

    → Nutrimento immediato post-acquisto: email di ringraziamento + invito al programma fedeltà, suggerire prodotti complementari (upsell bundle), spedizione gratuita sul secondo ordine per incoraggiare la ripetizione.

  • Persi / Inattivi (25.44 % clienti, 9.85 % ricavi)

    Grande gruppo inattivo con valore passato.

    → Campagne di riattivazione: email win-back con offerte a tempo limitato (es. 20 % off + spedizione gratuita), sondaggio per capire la ragione del churn, annunci mirati su categorie ad alto margine che hanno comprato in passato.

  • Clienti Leali (1.51 % clienti, 2.67 % ricavi)

    Gruppo piccolo ma che ripete acquisti.

    → Benefici VIP: accesso anticipato alle vendite, bundle esclusivi, sistema di punti fedeltà per aumentare frequenza e ticket medio.

  • Champions (0.15 % clienti, 0.50 % ricavi)

    Gruppo élite — recenti, frequenti (per Olist), alto spend.

    → Trattamento premium: contatto personale, supporto dedicato, invito a beta/test di nuovi prodotti, programma referral con alte ricompense.

  • A Rischio (0.09 % clienti, 0.25 % ricavi)

    Precedentemente buoni ma ora inattivi.

    → Riattivazione urgente: offerte personalizzate "ci manchi", sconti di alto valore a tempo limitato su categorie che hanno amato.

Opportunità Generale:

Con il 97 % di clienti one-time, il focus deve essere sull'aumento della frequenza in tutti i segmenti — bundle, abbonamenti (se possibile), programma fedeltà e consegne più rapide negli stati ad alto valore (SP/RJ) per migliorare soddisfazione e tasso di ripetizione.

6.4. Esportazione dei Risultati RFM ¶

Salvo la tabella RFM completa (con punteggi e segmenti) e il riassunto per segmento nella cartella dei dati processati.
Questi file possono essere usati direttamente in Power BI per dashboard interattivi o report aggiuntivi.

In [59]:
# 6.4 Exporting RFM Results

# Ensure processed folder exists
processed_dir = './data/processed/'
os.makedirs(processed_dir, exist_ok=True)

# 1. Save full RFM table (per customer)
rfm.to_csv(
    processed_dir + 'rfm_customers_with_segments.csv',
    index=False
)
print("Full RFM table saved to:")
print(processed_dir + 'rfm_customers_with_segments.csv')

# 2. Save segment summary (aggregated)
segment_summary.to_csv(
    processed_dir + 'rfm_segment_summary.csv',
    index=False
)
print("\nSegment summary saved to:")
print(processed_dir + 'rfm_segment_summary.csv')

# Save a formatted version (with rounded numbers for easy Excel/Power BI use)
segment_summary_formatted = segment_summary.copy()
segment_summary_formatted['avg_recency_days'] = segment_summary_formatted['avg_recency_days'].round(1)
segment_summary_formatted['avg_frequency'] = segment_summary_formatted['avg_frequency'].round(2)
segment_summary_formatted['avg_monetary'] = segment_summary_formatted['avg_monetary'].round(2)
segment_summary_formatted['total_revenue'] = segment_summary_formatted['total_revenue'].round(2)

segment_summary_formatted.to_csv(
    processed_dir + 'rfm_segment_summary_formatted.csv',
    index=False
)
print("\nFormatted segment summary also saved (ready for Power BI/Excel):")
print(processed_dir + 'rfm_segment_summary_formatted.csv')
Full RFM table saved to:
./data/processed/rfm_customers_with_segments.csv

Segment summary saved to:
./data/processed/rfm_segment_summary.csv

Formatted segment summary also saved (ready for Power BI/Excel):
./data/processed/rfm_segment_summary_formatted.csv

7. Analisi dei Cohorts – Retention dei Clienti nel Tempo ¶

7.1. Setup e Calcolo dei Cohorts ¶

Definisco i cohorts in base al mese del primo acquisto di ciascun cliente.

Per ogni cohort, calcolo il tasso di retention — la percentuale di clienti che effettuano un acquisto ripetuto nei mesi successivi.

In [60]:
# Use only delivered orders for accurate purchase behavior
cohort_base = base_df[base_df['is_delivered']].copy()

# 7.1.1. Convert purchase timestamp to period (monthly)
cohort_base['purchase_month'] = cohort_base['order_purchase_timestamp'].dt.to_period('M')

# 7.1.2. Find first purchase month per customer
first_purchase = cohort_base.groupby('customer_unique_id')['purchase_month'].min().reset_index()
first_purchase = first_purchase.rename(columns={'purchase_month': 'cohort_month'})

# 7.1.3. Merge cohort month back to main table
cohort_base = cohort_base.merge(
    first_purchase,
    on='customer_unique_id',
    how='left'
)

# 7.1.4. Create cohort index: months since first purchase
cohort_base['cohort_index'] = (cohort_base['purchase_month'] - cohort_base['cohort_month']).apply(lambda x: x.n)

# 7.1.5. Build cohort table: count unique customers per cohort + period
cohort_table = cohort_base.groupby(['cohort_month', 'cohort_index']).agg(
    customer_count=('customer_unique_id', 'nunique')
).reset_index()

# 7.1.6. Pivot to get retention matrix
cohort_pivot = cohort_table.pivot(
    index='cohort_month',
    columns='cohort_index',
    values='customer_count'
)

# 7.1.7. Calculate retention rates (% of original cohort still buying)
cohort_size = cohort_pivot.iloc[:, 0]  # customers in month 0 (first purchase)
retention_matrix = cohort_pivot.divide(cohort_size, axis=0) * 100  # as percentage

7.2. Tabella e Heatmap di Retention ¶

Costruisco una matrice di retention dei cohorts e la visualizzo come heatmap.

Questo mostra come la retention evolve nel tempo per ciascun cohort di partenza (ad esempio, "clienti che hanno acquistato per la prima volta a gennaio 2017").

In [61]:
# 7.2.1. Filter cohorts with at least 50 customers (a common threshold)
valid_cohorts = cohort_size[cohort_size >= 50].index
retention_matrix_filtered = retention_matrix.loc[valid_cohorts]

# Round for display
retention_matrix = retention_matrix.round(1)

# 7.2.2. Heatmap visualization
plt.figure(figsize=(14, 10))

sns.heatmap(
    retention_matrix_filtered,
    annot=True,
    fmt='.1f',
    cmap='YlGnBu',
    linewidths=0.5,
    cbar_kws={'label': 'Retention Rate (%)'}
)

# Get the current axes and colorbar
ax = plt.gca()
cbar = ax.collections[0].colorbar  # the colorbar object

# Make the colour bar label larger and bolder
cbar.set_label(
    'Retention Rate (%)',           
    fontsize=16,                    
    fontweight='bold',              
    labelpad=15                     
)

# make the percentage numbers/ticks on the colorbar larger
cbar.ax.tick_params(labelsize=12)   # controls the 0, 20, 40... numbers

plt.title('Monthly Cohort Retention Rates (%)', fontsize=20, fontweight='bold', pad=25)
plt.xlabel('Months Since First Purchase', fontsize=16, fontweight='bold' , labelpad=25)
plt.ylabel('Cohort Month (First Purchase)', fontsize=16, fontweight='bold' , labelpad=25)
plt.xticks(rotation=0, fontsize=16)
plt.yticks(rotation=0, fontsize=12)

plt.tight_layout()
plt.show()

# 7.2.3. Print retention table for reference (first 12 months)
print("Cohort Retention Rates (%) - First 12 months:")
display(retention_matrix.iloc[:, :13].style.format('{:.1f}'))
No description has been provided for this image
Cohort Retention Rates (%) - First 12 months:
cohort_index 0 1 2 3 4 5 6 7 8 9 10 11 12
cohort_month                          
2016-09 100.0 nan nan nan nan nan nan nan nan nan nan nan nan
2016-10 100.0 nan nan nan nan nan 0.4 nan nan 0.4 nan 0.4 nan
2016-12 100.0 100.0 nan nan nan nan nan nan nan nan nan nan nan
2017-01 100.0 0.3 0.3 0.1 0.4 0.1 0.4 0.1 0.1 nan 0.4 0.1 0.7
2017-02 100.0 0.2 0.3 0.1 0.4 0.1 0.2 0.2 0.1 0.2 0.1 0.3 0.1
2017-03 100.0 0.4 0.4 0.4 0.4 0.2 0.2 0.3 0.3 0.1 0.4 0.1 0.2
2017-04 100.0 0.6 0.2 0.2 0.3 0.3 0.4 0.3 0.3 0.2 0.3 0.1 0.0
2017-05 100.0 0.5 0.5 0.3 0.3 0.3 0.4 0.1 0.3 0.3 0.3 0.3 0.2
2017-06 100.0 0.5 0.4 0.4 0.3 0.4 0.4 0.2 0.1 0.2 0.3 0.4 0.2
2017-07 100.0 0.5 0.3 0.2 0.3 0.2 0.3 0.1 0.2 0.3 0.2 0.3 0.1
2017-08 100.0 0.7 0.3 0.3 0.3 0.5 0.3 0.3 0.1 0.1 0.2 0.2 0.1
2017-09 100.0 0.7 0.5 0.3 0.4 0.2 0.2 0.2 0.3 0.2 0.2 0.1 nan
2017-10 100.0 0.7 0.3 0.1 0.2 0.2 0.2 0.4 0.3 0.2 0.2 nan nan
2017-11 100.0 0.6 0.4 0.2 0.2 0.2 0.1 0.2 0.1 0.1 nan nan nan
2017-12 100.0 0.2 0.3 0.3 0.3 0.2 0.2 0.0 0.2 nan nan nan nan
2018-01 100.0 0.3 0.4 0.3 0.3 0.2 0.2 0.2 nan nan nan nan nan
2018-02 100.0 0.3 0.4 0.3 0.3 0.2 0.2 nan nan nan nan nan nan
2018-03 100.0 0.4 0.3 0.3 0.1 0.1 nan nan nan nan nan nan nan
2018-04 100.0 0.6 0.3 0.2 0.1 nan nan nan nan nan nan nan nan
2018-05 100.0 0.5 0.3 0.2 nan nan nan nan nan nan nan nan nan
2018-06 100.0 0.4 0.3 nan nan nan nan nan nan nan nan nan nan
2018-07 100.0 0.5 nan nan nan nan nan nan nan nan nan nan nan
2018-08 100.0 nan nan nan nan nan nan nan nan nan nan nan nan

7.3. Insight sui Cohorts e Raccomandazioni di Retention ¶

Il heatmap rivela tassi di riacquisto molto bassi, tipici di un marketplace come Olist con alto comportamento di acquirenti one-time.
Riassumo i pattern chiave e propongo strategie actionable per migliorare la retention in tutti i cohorts.

In [62]:
# Average retention per month (across all cohorts)
avg_retention = retention_matrix.mean().dropna()  # ignore NaN in later months

plt.figure(figsize=(10, 6))
plt.plot(avg_retention.index, avg_retention.values, marker='o', color='#1f77b4')
plt.title('Average Cohort Retention Over Time', fontsize=14, fontweight='bold')
plt.xlabel('Months Since First Purchase')
plt.ylabel('Average Retention Rate (%)')
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
No description has been provided for this image

Insight Chiave sui Cohorts:

  • La retention complessiva è estremamente bassa: La retention nel mese 1 media ~3–6 % nella maggior parte dei cohorts, scendendo a <1 % entro il mese 6–12.
    → Questo conferma il precedente risultato RFM: ~97 % dei clienti acquista solo una volta. La sfida non è l'acquisizione, ma trasformare i compratori one-time in clienti ricorrenti.

  • I cohorts iniziali (2016–inizio 2017) mostrano una retention a lungo termine leggermente migliore (fino a 1–2 % ancora attivi dopo 12+ mesi) rispetto a quelli successivi.
    → Possibili ragioni: più tempo per acquisti ripetuti, o i clienti iniziali erano più fedeli/coinvolti. I cohorts più recenti (2018) hanno meno mesi di dati, quindi i pattern a lungo termine sono incompleti.

  • I cohorts iniziali piccoli (es. 2016-09, 2016-10, 2016-12) mostrano retention rumorosa/intermittente (100 % nel mese 1, poi valori sporadici).
    → Questi sono artefatti di dimensioni di campione molto piccole (spesso <10 clienti). Gli insight da queste righe non sono affidabili — concentrarsi sui cohorts più grandi (2017+ con ≥50–100 clienti).

  • Nessuna forte tendenza al rialzo nella retention nel tempo: I cohorts successivi non trattengono meglio di quelli precedenti.
    → Suggerisce che non ci sono stati miglioramenti significativi nell'esperienza cliente, programmi fedeltà o engagement post-acquisto durante il 2017–2018.

Raccomandazioni Azionabili di Retention:

  1. Aumentare la Retention nel Mese 1 (prima ripetizione critica)

    • Sequenza di email post-acquisto: ringraziamento + 10–20 % off sul prossimo ordine (valido 30 giorni)
    • Spedizione gratuita sul secondo acquisto o suggerimenti di bundle basati sulla categoria del primo ordine
    • Obiettivo: aumentare la retention nel mese 1 da ~4 % a 8–10 % → raddoppia i clienti ricorrenti
  2. Riattivare i Cohorts Inattivi (Lost/Hibernating da RFM)

    • Campagne win-back: email personalizzate per clienti inattivi da 3–6 mesi (es. "Ci manchi! 25 % off sui tuoi preferiti")
    • Usare i dati dei cohorts per tempizzare le offerte: targettizzare i cohorts iniziali con valore a lungo termine provato
    • Testare SMS o notifiche push per categorie ad alto valore (es. bellezza, articoli per la casa)
  3. Aumentare la Frequenza nei Cohorts di Medio Termine

    • Programma fedeltà: punti per ogni acquisto, riscattabili su categorie ad alto margine
    • Modelli tipo abbonamento per consumabili (bellezza, pet, prodotti per neonati)
    • Cross-sell bundle: "Completa il tuo set casa" per acquirenti di letto_bagno_tavola
  4. Focus su Prodotti e Categorie

    • Prioritizzare la retention nelle categorie top per ricavi (salute_bellezza, letto_bagno_tavola, orologi_regali)
    • Offrire perks specifici per categoria: campioni gratuiti per bellezza, garanzia estesa per elettronica
  5. Misurazione e Iterazione

    • Tracciare la retention dei cohorts mensilmente nel dashboard Power BI
    • Effettuare A/B test su tattiche di retention su nuovi cohorts → misurare il lift nella retention mese 1–3

Opportunità Complessiva:
Con tassi di ripetizione così bassi, anche un piccolo aumento nella frequenza (es. da 1.03 a 1.2 ordini medi per cliente) potrebbe aumentare i ricavi totali del 15–20 %.
Concentrarsi sull'esperienza post-acquisto, offerte personalizzate e meccanismi di fedeltà per trasformare i compratori one-time in ricorrenti.


8. Previsione Base – Predizione dei Ricavi Futuri con Prophet ¶

8.1. Previsione con Prophet ¶

Utilizzo Facebook Prophet per prevedere i ricavi mensili futuri basati su trend storici e stagionalità.

Prophet è particolarmente adatto per dati di e-commerce con possibili pattern annuali (es. festività, domanda stagionale).

Per questa introduzione, mantengo il modello semplice (senza regressori esterni né festività personalizzate) per concentrarmi sulle capacità di base.

In [63]:
# 8.1 & 8.2 Basic Forecasting with Prophet

# 8.1.1. Prepare time series data: monthly total revenue

# Use base_df (only delivered orders) for consistency
monthly_revenue = base_df[base_df['is_delivered']].copy()

# Group by month
monthly_revenue['ds'] = monthly_revenue['order_purchase_timestamp'].dt.to_period('M').dt.to_timestamp()

# Aggregate revenue per month
monthly_revenue = monthly_revenue.groupby('ds')['total_order_value'].sum().reset_index()

# Rename for Prophet (Prophet requires 'ds' (date) and 'y' (value))
monthly_revenue = monthly_revenue.rename(columns={'total_order_value': 'y'})

# Quick look at the data
print("Monthly revenue data shape:", monthly_revenue.shape)
print(monthly_revenue.head())

# 8.1.2. Create and fit model with logistic growth
model = Prophet(
    yearly_seasonality=True,
    weekly_seasonality=False,   # daily data not needed
    daily_seasonality=False,
    seasonality_mode='additive'  # better for revenue with growth
)

model.fit(monthly_revenue)

# 8.1.3. Create future dataframe and add the same cap/floor (forecast next 12 months)
future = model.make_future_dataframe(periods=12, freq='MS')  # MS = month start

# 8.1.4. Predict
forecast = model.predict(future)

# Clip any remaining negatives (extra safety at post-processing)
forecast['yhat'] = forecast['yhat'].clip(lower=0)
forecast['yhat_lower'] = forecast['yhat_lower'].clip(lower=0)
forecast['yhat_upper'] = forecast['yhat_upper'].clip(lower=0)

# 8.1.5. Plot forecast with Plotly (interactive)
fig = go.Figure()

# Uncertainty interval (two traces to create the shaded area)
fig.add_trace(go.Scatter(  # Upper bound (invisible line)
    x=forecast['ds'],
    y=forecast['yhat_upper'],
    mode='lines',
    line=dict(color='#27ae60', width=1, dash='dot'),
        name='Upper Uncertainty Bound'
))

fig.add_trace(go.Scatter(  # Lower bound + fill between upper & lower
    x=forecast['ds'],
    y=forecast['yhat_lower'],
    mode='lines',
    #fill='tonexty',         # fill area to the previous trace (upper bound)
    line=dict(color='#27ae60', width=1, dash='dot'),
    name='Lower Uncertainty Bound'
))

# Forecast
fig.add_trace(go.Scatter(      # Adds the forecast line
    x=forecast['ds'],
    y=forecast['yhat'],        # predicted values
    mode='lines',
    name='Forecast',
    line=dict(color='#ff7f0e', dash='dash', width=3.5) # orange + dashed
))

# Historical data
fig.add_trace(go.Scatter(      # Adds the historical line
    x=monthly_revenue['ds'],   # dates on x-axis
    y=monthly_revenue['y'],    # actual revenue values
    mode='lines+markers',      # line with dots at each point
    name='Historical Revenue', # legend label
    line=dict(color='#1f77b4', width=3.5) # blue color
))

# Final styling
fig.update_layout(          
    title='<b>Monthly Revenue Forecast (Next 12 Months)</b>',
    title_x=0.5,
    title_font_size=18,
    xaxis_title='Date',
    yaxis_title='Total Revenue (BRL)',
    hovermode='x unified',  # shows all values when hovering on a date
    height=600,
    template='plotly_white' # clean white background
)

fig.show()

# 6. Print forecast for next 6 months
print("\nForecast for next 6 months:")
future_forecast = forecast[forecast['ds'] > monthly_revenue['ds'].max()][['ds', 'yhat', 'yhat_lower', 'yhat_upper']].head(6)
future_forecast['yhat'] = future_forecast['yhat'].round(2)
future_forecast['yhat_lower'] = future_forecast['yhat_lower'].round(2)
future_forecast['yhat_upper'] = future_forecast['yhat_upper'].round(2)
display(future_forecast)
16:29:19 - cmdstanpy - INFO - Chain [1] start processing
16:29:19 - cmdstanpy - INFO - Chain [1] done processing
Monthly revenue data shape: (23, 2)
          ds             y
0 2016-09-01      0.000000
1 2016-10-01  47271.200000
2 2016-12-01     19.620000
3 2017-01-01 127545.670000
4 2017-02-01 271298.650000
Forecast for next 6 months:
ds yhat yhat_lower yhat_upper
23 2018-09-01 1589425.680000 1519977.880000 1666357.970000
24 2018-10-01 1553858.560000 1483368.020000 1633457.640000
25 2018-11-01 1279389.770000 1205482.610000 1352166.030000
26 2018-12-01 1578930.870000 1501889.590000 1650012.790000
27 2019-01-01 1829107.500000 1753655.530000 1898296.960000
28 2019-02-01 1397814.710000 1321736.580000 1473373.690000

Nota: La linea tratteggiata della previsione e i limiti di incertezza si estendono indietro sul periodo storico per mostrare l'adattamento del modello in-sample.
La previsione futura effettiva inizia dopo l'ultimo punto dati storico (agosto 2018).

8.2. Insight di Previsione e Raccomandazioni di Business ¶

Il modello Prophet (crescita lineare, stagionalità annuale) prevede una crescita moderata continua dei ricavi nei prossimi 12 mesi, con totali mensili probabili nel range 600k–900k BRL.

L'adattamento sui dati storici è forte, supportando fiducia nelle proiezioni a breve termine. Di seguito i principali insight e strategie actionable.

Insight Chiave:

  • Trend ascendente stabile — I ricavi sono cresciuti costantemente dal 2017–2018, e la previsione estende questo pattern nel 2019 con lievi fluttuazioni stagionali (probabilmente picchi in Q4 per festività e spesa post-festiva in Q1).

  • Incertezza stretta a breve termine — I primi 6–9 mesi mostrano intervalli stretti, indicando previsioni affidabili. Orizzonti più lunghi (12+ mesi) hanno intervalli più ampi — normale man mano che l'incertezza si accumula.

  • Stagionalità rilevata — Cicli annuali sottili (es. maggiore in Q4/Q1) si allineano con i pattern dell'e-commerce (festività, ritorno a scuola, ecc.), sebbene meno pronunciati rispetto a dataset più grandi.

  • Validazione dell'adattamento del modello — Le previsioni in-sample seguono da vicino i ricavi storici, confermando che il modello cattura bene trend e stagionalità.

Raccomandazioni Azionabili:

  1. Pianificazione Inventario e Logistica

    • Scalare lo stock per le categorie top (salute_bellezza, letto_bagno_tavola, orologi_regali) del 10–20 % sopra i livelli attuali per il 2019.

    • Prioritizzare capacità in SP, RJ, MG — stati ad alto revenue con possibili picchi stagionali.

  2. Marketing e Promozioni

    • Aumentare budget in Q4 (Black Friday, Natale) e Q1 (vendite post-festive) — target bundle su articoli per la casa, bellezza e regali per capitalizzare la stagionalità rilevata.

    • Lanciare campagne focalizzate sulla retention (es. "Sconto sul Secondo Acquisto") all'inizio del 2019 per aumentare i tassi di ripetizione e superare la previsione.

  3. Gestione del Rischio

    • Usare i limiti inferiori come obiettivi di budget conservativi.

    • Monitorare reale vs previsione mensilmente — se sotto il limite inferiore, indagare churn (collegare al segmento "Lost / Hibernating" di RFM) o fattori esterni.

  4. Sinergia con Retention

    • Combinare con cohort/RFM: focalizzarsi su "Recent High-Value New" e "At Risk" — un lift del 2–3 % nella retention mese 1 potrebbe spingere i ricavi 2019 del 15–20 % sopra il baseline previsto.

Opportunità Complessiva:

Il modello proietta una crescita solida assumendo che le tendenze attuali continuino.

Il vero upside sta nel migliorare la retention (attualmente ~3–6 % nel mese 1) — anche piccoli guadagni nelle ripetizioni supererebbero significativamente questo baseline.


9. Finalizzazione e Presentazione ¶

9.1. Esportazione delle Tabelle Chiave ¶

Tutte le tabelle processate vengono salvate in ./data/processed/ per un facile import in Power BI o altri tool.

Questo garantisce riproducibilità e abilita dashboard interattivi (ad esempio, ricavi per stato/categoria, segmenti RFM, retention dei cohorts, trend di previsione).

In [64]:
# 9.1 Exporting Key Tables

processed_dir = './data/processed/'
os.makedirs(processed_dir, exist_ok=True)

# 9.1.1. Cleaned orders with features
orders.to_csv(processed_dir + 'cleaned_orders_with_features.csv', index=False)

# 9.1.2. Base working table (orders + customers + payments)
base_df.to_csv(processed_dir + 'base_working_table.csv', index=False)

# 9.1.3. State performance summary
state_summary.to_csv(processed_dir + 'state_performance_summary.csv', index=False)

# 9.1.4. Category performance
category_performance.to_csv(processed_dir + 'category_performance.csv', index=False)

# 9.1.5. State-category breakdown
state_category_summary.to_csv(processed_dir + 'state_category_summary.csv', index=False)

# 9.1.6. RFM customers with scores & segments
rfm.to_csv(processed_dir + 'rfm_customers_with_segments.csv', index=False)

# 9.1.7. RFM segment summary
segment_summary.to_csv(processed_dir + 'rfm_segment_summary.csv', index=False)

# 9.1.8. Cohort retention matrix
retention_matrix.to_csv(processed_dir + 'cohort_retention_matrix.csv')

# 9.1.9. Monthly revenue + forecast
monthly_revenue.to_csv(processed_dir + 'monthly_historical_revenue.csv', index=False)
forecast.to_csv(processed_dir + 'monthly_revenue_forecast.csv', index=False)

print("All key tables exported successfully to:")
print(processed_dir)
print("Ready for Power BI import!")
All key tables exported successfully to:
./data/processed/
Ready for Power BI import!

9.2. Riassunto del Progetto e Insight Chiave ¶

Riassunto Principali Insight:

  • Concentrazione delle Vendite: Le top 5 categorie rappresentano ~40–50 % dei ricavi; SP genera ~60 % delle vendite totali → alta dipendenza geografica e per categoria.

  • Maggioranza di Acquirenti One-Time: ~97 % dei clienti acquista solo una volta (mediana frequency in RFM = 1.03) → enorme opportunità per aumentare il tasso di ripetizione.

  • Retention Molto Bassa: Retention mese 1 ~3–6 %, scende sotto l'1 % dopo 6–12 mesi (analisi cohorts) → focus urgente sull'esperienza post-acquisto e riattivazione.

  • Performance di Consegna: Media ~12 giorni prima del previsto, ma con outliers e variazione regionale → opportunità per ottimizzare la logistica negli stati più lenti.

  • Previsione 2019: Crescita moderata attesa (~600k–900k BRL/mese), con picchi stagionali sottili → preparare inventario e campagne per Q4/Q1.

Impatto Potenziale: Migliorare la retention di appena 2–3 % (es. mese 1 da ~4 % a 8 %) potrebbe aumentare i ricavi totali del 15–25 % senza cambiare l'acquisizione.

Questo progetto dimostra competenze di analisi dati end-to-end: dai dati raw alle raccomandazioni di business, con forti capacità in Python/SQL/visualizzazione.

9.3. Dashboard in Power BI ¶

Per rendere l'analisi più interattiva e pronta per l'uso business, ho creato un dashboard in Power BI utilizzando le tabelle esportate dalla cartella processed.

Caratteristiche Principali del Dashboard:

Pagina Overview ("Olist Overview")

Riassunto di alto livello con KPI globali e visual chiave:

  • Card semplici per metriche core: Ricavi Totali, Clienti Unici Totali, % Share Ricavi, % Share Clienti, Spesa Media per Cliente, Acquisti Medi per Cliente.
  • Grafico a linee: Ricavi Storici + Previsione 12 Mesi
  • Matrice/Heatmap: Retention dei Cohorts (Mesi dalla prima acquisto)
  • Grafico a barre raggruppate: % Share Ricavi vs % Share Clienti per Segmento RFM
  • Mappa Bubble: Ricavi per Stato Brasiliano
  • Grafico a barre orizzontali: Top 10 Categorie per Ricavi

Nessun slicer su questa pagina per mantenerla come snapshot globale pulito.

Pagina Customer Deep Dive

Focalizzata su analisi dettagliata dei clienti con interattività:

  • Scatter Plot RFM: Recency vs Monetary (dimensione per Frequency, colore per Segmento)
  • Tabella dinamica clienti: Filtrata e ordinata per segmento selezionato (monetary decrescente)
  • Card semplici e Gauges per KPI specifici per segmento (es. % Repeat Customers, Spesa Media, ecc.)
  • Multipli slicer: Segmento RFM, Range Recency, Range Frequency, Range Monetary — per filtraggio profondo e esplorazione di gruppi clienti.

Screenshots:

Pagina Olist Overview:

Pagina Olist Overview – KPI di alto livello, linea previsione, heatmap cohorts, barra RFM, mappa bubble per stato, top categorie

Pagina Customer Deep Dive:

Pagina Customer Deep Dive – Scatter RFM, tabella clienti filtrata, KPI e Gauges per segmento

Link al Dashboard:
Power BI Service – Olist Analytics Dashboard

Il dashboard è pubblicato su Power BI Service (account personale gratuito) e può essere condiviso via link per visualizzazione interattiva.

9.4. Conclusione del Progetto e Prossimi Passi ¶

Riassunto dei Risultati:

  • Caricato e pulito il dataset di E-Commerce Brasiliano Olist (~100k ordini, 9 tabelle).
  • Eseguito EDA profondo: vendite per stato/categoria, preferenze regionali, performance di consegna.
  • Consegnata segmentazione RFM con gruppi clienti actionable e raccomandazioni.
  • Analizzata retention dei cohorts → evidenziate tassi di ripetizione molto bassi e strategie di miglioramento.
  • Previsione dei ricavi futuri con Prophet → identificati trend di crescita e opportunità stagionali.
  • Creati visual interattivi (Plotly) e esportate tabelle per dashboarding.

Lezioni Chiave Apprese:

  • Transizione riuscita da R a Python: pandas per manipolazione dati, Prophet per forecasting, Plotly per visual interattivi.
  • Realtà dell'e-commerce: tasso di riacquisto molto basso (~3 %), alta dipendenza da compratori one-time.
  • Importanza del contesto business: ogni analisi ha portato a raccomandazioni concrete.
  • Peculiarità di Power BI: relazioni e slicer necessitano setup attento per cross-filtering.

Miglioramenti Futuri e Idee:

  • Incorporare dati esterni (es. festività brasiliane, indicatori economici) per forecasting più ricco.
  • Esplorare impatto dei metodi di pagamento su segmenti e retention (boleto vs carta vs rate).
  • Eseguire A/B test reali su tattiche di retention (es. email win-back, sconti) se dati live disponibili.
  • Scalare a modelli più avanzati (es. deep learning per serie temporali, ML per customer lifetime value).

Questo progetto dimostra competenze di analisi dati end-to-end: dai dati raw alle raccomandazioni di business, con forti capacità in Python/SQL/visualizzazione.

Grazie per avermi seguito!

9.5 Report Pubblicato e Download ¶

L'analisi interattiva completa è disponibile online sul mio sito web personale:

Visualizza Report Interattivo (HTML)
(Raccomandato – interattività completa, TOC cliccabile, celle codice espandibili, grafici Plotly)

Scarica Versione PDF:
Report Olist Analytics – PDF
(Export statico per lettura offline o stampa – generato dal notebook)

Entrambe le versioni si basano sullo stesso codice sorgente del Jupyter notebook disponibile nel mio repository.

Emilio Nahuel Pattini – Buenos Aires, 1 febbraio 2026