Data-Warehouse-Power-BI-Sales-Dashboard

Sales Data Warehouse & Power BI Dashboard

Executive Summary Análise Temporal
Análise Produtos Análise Geográfica
Análise Demográfica Análise Preditiva e Insights

1. Resumo Executivo

Este repositório documenta, ponta a ponta, a construção de um mini data warehouse para a cadeia fictícia TechSolutions (retalho de eletrónica) e a entrega de dashboards em Power BI. Inclui geração de dados sintéticos (20 lojas, ~1,5k clientes, ~200 produtos, ~1,3M linhas de vendas 2020-2025), infraestrutura em Docker com SQL Server, pipeline ETL/ELT em Python + T-SQL, modelo dimensional em estrela e artefactos de Power BI (medidas DAX, cálculos visuais, scripts Python avançados e ficheiros .pbix).

2. Índice

3. Contexto de Negócio

4. Objetivos

5. Stakeholders e Perfis de Utilizador

6. Porque este DW importa

7. Arquitetura da Solução

8. Inventário de Fontes de Dados

9. Destaques de Modelação Dimensional

10. Estratégia de Snapshots e KPIs

11. Pipeline ETL / ELT

1) Gerar dados: python data/10804Proj3.py (gera ou regenera CSVs com seeds fixos). 2) Subir infra: docker compose up -d --build a partir de docker/ (usa variáveis de docker/.env). 3) Ingestão para staging: no container loader, correr python etl/ingest_csv.py (cria BD se não existir, varre DATA_PATH, grava tabelas sanitizadas com nomes derivados do caminho). 4) Normalização staging: sql/01_staging.sql tipa colunas e cria índices. 5) Dimensões: sql/02_dimensions.sql cria DIM_DATA, DIM_LOJA, DIM_CLIENTE, DIM_PRODUTO, trata desconhecidos e faixas etárias. 6) Fato: sql/03_facts.sql popula dw.FACT_VENDAS, recalcula valor_total se nulo e associa stock_inicial. 7) Views: sql/04_views.sql expõe joins frequentes e métricas agregadas por cliente/transação.

12. Layout do Repositório

13. Entregáveis Power BI

14. Primeiros Passos (Getting Started)

Pré-requisitos: Docker + Docker Compose, Python 3.11, ODBC Driver 17 para SQL Server (no host ou dentro do container loader), Power BI Desktop para abrir .pbix.

1) Clonar o repositório e posicionar-se na pasta sales-dashbaord/. 2) (Opcional) Regenerar dados: python data/10804Proj3.py. 3) Subir infra: cd docker && docker compose up -d --build. 4) Instalar dependências no loader: docker compose exec loader pip install -r etl/requirements.txt. 5) Correr ingestão: docker compose exec loader python etl/ingest_csv.py. 6) Executar scripts SQL na ordem 0004 (via SSMS, Azure Data Studio ou sqlcmd):

   docker compose exec mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $MSSQL_PASSWORD -i /sql/00_init_schema.sql
   # repetir para 01_staging.sql, 02_dimensions.sql, 03_facts.sql, 04_views.sql

7) Abrir o .pbix no Power BI Desktop, apontar a ligação para o servidor/BD locais (SalesDW) e atualizar as tabelas.

15. Validação e Testes

16. Operação e Manutenção

17. Roteiro (Roadmap)

18. Medidas DAX

Vendas Totais = SUM ( 'dw FACT_VENDAS'[valor_total] )
Quantidade Vendida = SUM ( 'dw FACT_VENDAS'[quantidade] )
Numero de Transacoes = DISTINCTCOUNT ( 'dw FACT_VENDAS'[transacao_id] )
Transacao Media = DIVIDE ( [Vendas Totais], [Numero de Transacoes] )
Margem de Lucro =
SUMX ( 'dw FACT_VENDAS', 'dw FACT_VENDAS'[quantidade] * ( 'dw FACT_VENDAS'[preco_unitario] - 'dw FACT_VENDAS'[custo_unitario] ) )

Crescimento YoY % =
VAR AnoSelecionado = CALCULATE( MAX('Calendario'[Ano]), ALLSELECTED('Calendario') )
VAR AnoAnterior    = AnoSelecionado - 1
VAR VendasAtual    = CALCULATE( [Vendas Totais], 'Calendario'[Ano] = AnoSelecionado )
VAR VendasAnterior = CALCULATE( [Vendas Totais], 'Calendario'[Ano] = AnoAnterior )
RETURN IF( NOT ISBLANK(VendasAtual) && NOT ISBLANK(VendasAnterior), DIVIDE(VendasAtual - VendasAnterior, VendasAnterior) )

Media Movel 3M =
VAR UltimaData = MAX ( 'dw DIM_DATA'[data] )
RETURN CALCULATE ( AVERAGEX ( DATESINPERIOD ( 'dw DIM_DATA'[data], UltimaData, -3, MONTH ), [Vendas Totais] ), REMOVEFILTERS ( 'dw DIM_DATA'[data] ) )

Media Movel 3Y =
VAR UltimaData = MAX ( 'dw DIM_DATA'[data] )
RETURN CALCULATE ( AVERAGEX ( DATESINPERIOD ( 'dw DIM_DATA'[data], UltimaData, -3, YEAR ), [Vendas Totais] ), REMOVEFILTERS ( 'dw DIM_DATA'[data] ) )

Vendas Ano Anterior = CALCULATE( [Vendas Totais], SAMEPERIODLASTYEAR(Calendario[Date]) )

Clientes = DISTINCTCOUNT ( 'dw DIM_CLIENTE'[cliente_id] )

Potencial Cidade = CALCULATE( SUM( 'Cidade'[Habitantes] ), TREATAS( VALUES( 'dw DIM_LOJA'[cidade] ), 'Cidade'[cidade] ) )

% penetracao cidade = DIVIDE('Medidas Clientes'[Clientes], [Potencial Cidade])

19. Métodos Avançados em Python

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

# Estilo para fundo claro
plt.style.use('default')
plt.rcParams['figure.facecolor'] = 'white'
plt.rcParams['axes.facecolor'] = 'white'
plt.rcParams['axes.edgecolor'] = 'black'
plt.rcParams['axes.labelcolor'] = 'black'
plt.rcParams['xtick.color'] = 'black'
plt.rcParams['ytick.color'] = 'black'
plt.rcParams['text.color'] = 'black'
plt.rcParams['legend.edgecolor'] = 'black'
plt.rcParams['legend.labelcolor'] = 'black'
plt.rcParams['axes.titlepad'] = 15

# 1) Carregar dataset do Power BI
df = dataset.copy()
df = df.dropna(subset=['Vendas Totais', 'faixa_etaria_valor', 'genero_valor'])

# 2) Seleção de features
X = df[['Vendas Totais', 'faixa_etaria_valor', 'genero_valor']].values

# 3) Normalizar
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 4) Calcular métricas para k = 2..10
k_values = range(2, 11)
inertias = []
silhouette_scores = []

for k in k_values:
    kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
    labels = kmeans.fit_predict(X_scaled)
    inertias.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, labels))

# 5) Ponto de cotovelo
x1, y1 = k_values[0], inertias[0]
x2, y2 = k_values[-1], inertias[-1]

distances = []
for i, k in enumerate(k_values):
    x0, y0 = k, inertias[i]
    num = abs((y2 - y1)*x0 - (x2 - x1)*y0 + x2*y1 - y2*x1)
    den = np.sqrt((y2 - y1)**2 + (x2 - x1)**2)
    distances.append(num / den)

elbow_k = k_values[np.argmax(distances)]
elbow_inertia = inertias[np.argmax(distances)]

# 6) Melhor silhouette
best_silhouette_k = k_values[np.argmax(silhouette_scores)]
best_silhouette_score = max(silhouette_scores)

# 7) Gráfico combinado
fig, ax1 = plt.subplots(figsize=(10, 6))
ax1.plot(k_values, inertias, 'bo-', linewidth=2, markersize=8, label='Inertia (Elbow)')
ax1.set_xlabel('Number of Clusters (k)', fontsize=12)
ax1.set_ylabel('Inertia', fontsize=12, color='black')
ax1.tick_params(axis='y', labelcolor='black')
ax1.scatter(elbow_k, elbow_inertia, s=220, c='red', edgecolor='black', linewidth=1.5, label=f'Elbow = k={elbow_k}')
ax1.grid(True, linestyle='--', linewidth=0.6, alpha=0.5, color='gray')

ax2 = ax1.twinx()
ax2.plot(k_values, silhouette_scores, 'g^-', linewidth=2, markersize=9, label='Silhouette Score')
ax2.set_ylabel('Silhouette Score', fontsize=12, color='black')
ax2.tick_params(axis='y', labelcolor='black')
ax2.scatter(best_silhouette_k, best_silhouette_score, s=220, c='blue', edgecolor='black', linewidth=1.5, label=f'Best Silhouette = k={best_silhouette_k}')

plt.title('Elbow Method + Silhouette Score (Combined Analysis)', fontsize=15, color='black')
lns1, labs1 = ax1.get_legend_handles_labels()
lns2, labs2 = ax2.get_legend_handles_labels()
plt.legend(lns1 + lns2, labs1 + labs2, facecolor='white', edgecolor='black', labelcolor='black', loc='best')
plt.tight_layout()
plt.savefig("combined_elbow_silhouette_white.png", dpi=300, bbox_inches='tight')
plt.show()
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans

plt.rcParams['axes.facecolor'] = 'white'
plt.rcParams['axes.edgecolor'] = 'black'
plt.rcParams['axes.labelcolor'] = 'black'
plt.rcParams['xtick.color'] = 'black'
plt.rcParams['ytick.color'] = 'black'
plt.rcParams['text.color'] = 'black'
plt.rcParams['legend.edgecolor'] = 'black'
plt.rcParams['legend.labelcolor'] = 'black'
plt.rcParams['axes.titlepad'] = 15

cluster_palette = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]

df = dataset.copy()
df = df[['valor_total', 'faixa_etaria_valor', 'genero_valor']].dropna()

scaler = StandardScaler()
X_scaled = scaler.fit_transform(df)

BEST_K = 4
kmeans = KMeans(n_clusters=BEST_K, init='k-means++', n_init=10, random_state=42)
df['cluster'] = kmeans.fit_predict(X_scaled)

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
loadings = pca.components_.T * np.sqrt(pca.explained_variance_)

pca_df = pd.DataFrame(X_pca, columns=['PCA1', 'PCA2'])
pca_df['cluster'] = df['cluster']

feature_names = ['valor_total', 'faixa_etaria_valor', 'genero_valor']

fig, ax = plt.subplots(figsize=(14, 10))

sns.scatterplot(
    data=pca_df,
    x='PCA1', y='PCA2',
    hue='cluster',
    palette=cluster_palette,
    s=140, alpha=0.9,
    edgecolor="none",
    ax=ax
)

arrow_scale = 2.3
for i, feature in enumerate(feature_names):
    ax.arrow(0, 0, loadings[i, 0] * arrow_scale, loadings[i, 1] * arrow_scale, head_width=0.12, head_length=0.12, fc='black', ec='black', lw=2.5)
    ax.text(loadings[i, 0] * (arrow_scale + 0.3), loadings[i, 1] * (arrow_scale + 0.3), feature, fontsize=12, color='black', weight='bold')

var1 = pca.explained_variance_ratio_[0] * 100
var2 = pca.explained_variance_ratio_[1] * 100

ax.set_title(f"PCA Projection with Loadings ({var1:.1f}% + {var2:.1f}% Variance Explained)", fontsize=16, color='black')
ax.set_xlabel(f"PCA1 ({var1:.1f}% var)", fontsize=12)
ax.set_ylabel(f"PCA2 ({var2:.1f}% var)", fontsize=12)
ax.axhline(0, color='grey', linestyle='--', alpha=0.6)
ax.axvline(0, color='grey', linestyle='--', alpha=0.6)
ax.grid(True, linestyle='--', alpha=0.35, color='lightgrey')

legend = plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', frameon=True)
legend.get_frame().set_facecolor('white')
legend.get_frame().set_edgecolor('black')

plt.tight_layout()
plt.show()
from prophet import Prophet
import pandas as pd
import matplotlib.pyplot as plt

df = dataset.rename(columns={'data': 'ds', 'valor_total': 'y'})
df['ds'] = pd.to_datetime(df['ds'])

df_daily = df.groupby('ds', as_index=False).sum()
df_daily = df_daily[df_daily['y'] < df_daily['y'].quantile(0.95)]

model = Prophet()
model.fit(df_daily)

future = model.make_future_dataframe(periods=120)
forecast = model.predict(future)

fig, ax = plt.subplots(figsize=(12, 7))
ax.scatter(df_daily['ds'], df_daily['y'], label="Historical Data", color='blue', alpha=0.6)
ax.plot(forecast['ds'], forecast['yhat'], label="Forecast", color='red', linewidth=2)
ax.fill_between(forecast['ds'], forecast['yhat_lower'], forecast['yhat_upper'], color='red', alpha=0.2, label="Confidence Interval")
ax.grid(True, linestyle='--', alpha=0.5)
ax.set_title("Prophet Forecast", fontsize=14)
ax.set_xlabel("Date")
ax.set_ylabel("Sales")
ax.legend()
plt.tight_layout()
plt.show()
import pandas as pd
from prophet import Prophet
import matplotlib.pyplot as plt

df = dataset.rename(columns={'data': 'ds', 'valor_total': 'y'})
df['ds'] = pd.to_datetime(df['ds'])
df = df[df['y'] < df['y'].quantile(0.95)]

model = Prophet()
model.fit(df)

future = model.make_future_dataframe(periods=90)
forecast = model.predict(future)

fig = model.plot(forecast)
plt.title("Actual Sales and Forecasted Sales (90 Days)")
plt.xlabel("Date")
plt.ylabel("Sales Units")

fig2 = model.plot_components(forecast)
plt.show()
Purchased Both Products (Cell) = 
VAR ProdutoLinha  = SELECTEDVALUE('dw DIM_PRODUTO'[nome])
VAR ProdutoColuna = SELECTEDVALUE('Comparaçao Produtos'[nome])
VAR TransacoesLinha =
    CALCULATETABLE(
        VALUES('dw FACT_VENDAS'[transacao_id]),
        'dw FACT_VENDAS',
        'dw DIM_PRODUTO'[nome] = ProdutoLinha
    )
VAR TransacoesColuna =
    CALCULATETABLE(
        VALUES('dw FACT_VENDAS'[transacao_id]),
        TREATAS({ProdutoColuna}, 'dw DIM_PRODUTO'[nome])
    )
RETURN
IF(
    HASONEVALUE('dw DIM_PRODUTO'[nome]) &&
    HASONEVALUE('Comparaçao Produtos'[nome]) &&
    ProdutoLinha <> ProdutoColuna,
    COUNTROWS(INTERSECT(TransacoesLinha, TransacoesColuna))
)

Purchased Both Products = 
VAR Result =
    IF(
        ISINSCOPE('dw DIM_PRODUTO'[nome]) &&
        ISINSCOPE('Comparaçao Produtos'[nome]),
        [Purchased Both Products (Cell)],
        SUMX(
            SUMMARIZECOLUMNS(
                'dw DIM_PRODUTO'[nome],
                'Comparaçao Produtos'[nome]
            ),
            [Purchased Both Products (Cell)]
        )
    )
RETURN Result
Percent of grand total = DIVIDE([Vendas Totais], COLLAPSEALL([Vendas Totais], ROWS))
Running sum = RUNNINGSUM([Percent of grand total],ORDERBY([Vendas Totais],DESC))
Greenline = IF([Pareto]<=.8,[Pareto],BLANK())
Redline = IF([Pareto]>.8,[Pareto],BLANK())

20. Licença