A elasticidade-preço da demanda é um conceito fundamental para entender como as variações nos preços afetam o comportamento dos consumidores. Em um mercado competitivo, saber quais produtos são mais sensíveis a mudanças de preço pode ser decisivo para otimizar estratégias de precificação e maximizar receita. Neste post, vamos explorar um projeto prático de elasticidade de preço, utilizando dados reais para calcular a sensibilidade dos produtos e visualizar os resultados de forma intuitiva com um dashboard completo em Streamlit. Ao final, você terá insights valiosos sobre como a elasticidade pode guiar decisões estratégicas para aumentar a eficiência do seu negócio.
Caso não tenha visto, temos outros dois posts sobre o tema aqui no blog. Recomendo a leitura de ambos para entender o passo a passo do projeto:
- Elasticidade de Preço: Transformando Dados em Estratégias Lucrativas para descobrir os conceitos da elasticidade-preço da demanda
- Como Calcular Elasticidade-Preço da Demanda: Fórmulas e Exemplos para aprender a calcular a elasticidade-preço e demanda
Agora que você já conhece as conceitos e cálculos da Elasticidade de Preço, podemos começar nosso projeto 🚀
Lembrando que todos os arquivos utilizados estão disponíveis no repositório no meu GitHub. 🐙
Problema de negócio¶
Para entender mais a relação dos produtos da Best Buy com seus concorrentes, os Gerentes de Custos e Orçamentos, Vendas e Marketing desejam analisar os dados reunidos por pesquisa com empresas concorrentes e entender como é a relação dos clientes com os preços de um setor de seus produtos.
O produto de dados será uma análise dos produtos das empresas e um dashboard aplicando elasticidade de preço com os produtos do seguimento Speaker/Portable/Bluetooth.
Planejamento da Solução¶
Entendendo o problema de negócio
Entender a necessidade do time de vendas e responder as questões sobre as empresas do ramo.
Coleta de dados
Coleta de dados de arquivo de pesquisa.
Limpeza dos dados
Colunas renomeadas, limpeza dos dados, transformação dos dados para aplicação em modelos de regressão para análise de elasticidade.
Análise Exploratória de Dados (EDA)
Exploração dos dados para adquirir conhecimento de negócio, analisar vendas por produtos e lojas.
Machine Learning
Aplicação de Regressão Linear nos dados dos produtos da Best Buy para análise de elasticidade de preço, considerando apenas os produtos que apresentaram modelos com nível de significância estatistica menor que 5%.
Dashboard
- Performance de negócio dos produtos considerando acréscimos e descontos dos produtos
- Relatório com dados de elasticidade e estatísticas dos produtos
- Tabela com elasticidade cruzada entre os produtos da categoria analisada
Ferramentas
- Python 3.10.7
- Pandas, Numpy, Seaborn, Matplotlib, Statsmodels
- Git
- Técnicas de Regressão Linear para cálculo da elasticidade
- Streamlit
0.0 Imports¶
Vamos importar os pacotes que serão usados nesse projeto.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import statsmodels.api as sm
0.1 Carregar os dados¶
Vamos carregar os dados que serão usados, remover colunas que não são relevantes para a análise e dar uma olhada rápida nas informações das primeiras linhas.
df_raw = pd.read_csv('../data/raw/df_ready.csv')
# removendo colunas com informações não usadas
df_raw = df_raw.drop(columns={ 'Unnamed: 0', 'Date_imp', 'Cluster', 'condition', 'sourceURLs', 'Date_imp_d.1',
'Zscore_1', 'price_std', 'imageURLs', 'shipping', 'weight', 'currency'})
df_raw.head()
1.0 Descrição dos dados¶
Na primeira etapa, vamos começar a análise com informações básicas, como quantidade de linhas e colunas, qual o tipo de informações nas colunas, procurar com dados faltantes e garantir que as informações de data estão formatados corretamente (alguns tipos de dados com datas podem ser desafiadores de tratar).
df1 = df_raw.copy()
1.1 Renomeando as colunas¶
Primeiro valos renomear as colunas para nomes que sejam mais fáceis de utilizar durante o desenvolvimento do código para análise.
cols_name = ['date_imp', 'category_name', 'name', 'price', 'disc_price',
'merchant', 'disc_percentage', 'is_sale', 'imp_count', 'brand',
'p_description', 'dateadded', 'dateseen', 'dateupdated', 'manufacturer',
'day_n', 'month', 'month_n', 'day', 'week_number']
df1.columns = cols_name
1.2 Dimensão dos dados¶
print(f'Number of rows: {df1.shape[0]}')
print(f'Number of cols: {df1.shape[1]}')
Number of rows: 23151 Number of cols: 20
Nosso conjunto de dados tem 23151 linhas com 20 colunas.
1.3 Tipos dos dados¶
df1.dtypes
date_imp object category_name object name object price float64 disc_price float64 merchant object disc_percentage float64 is_sale object imp_count int64 brand object p_description object dateadded object dateseen object dateupdated object manufacturer object day_n object month int64 month_n object day int64 week_number int64 dtype: object
Temos dados de texto, número flutuantes e números inteiros.
1.4 Checkagem de Valores Nulos¶
df1.isna().sum()
date_imp 0 category_name 0 name 0 price 0 disc_price 0 merchant 0 disc_percentage 0 is_sale 0 imp_count 0 brand 0 p_description 0 dateadded 0 dateseen 0 dateupdated 0 manufacturer 10639 day_n 0 month 0 month_n 0 day 0 week_number 0 dtype: int64
A única coluna com valores nulos é a de fabricante do produto (manufacturer
). No entanto essa informação não é tão relevante para o que vamos analisar.
1.5 Mudança de tipo dos dados¶
Vamos transformar a columa com datas para o tipo específico de data, para que possamos trabalhar de melhor forma com essas informações.
df1['date_imp'] = pd.to_datetime( df1['date_imp'])
1.7 Estatística Descritiva¶
Estatística descritiva é a área da estatística que organiza, resume e apresenta dados de forma clara por meio de medidas numéricas (como média, mediana e desvio padrão) e representações gráficas (como histogramas e boxplots). Seu objetivo é fornecer um panorama geral do conjunto de dados sem tirar conclusões além do que os próprios dados mostram.
# separando os dados númericos e categóricos
num_attributes = df1.select_dtypes( include=['float64', 'int64'])
cat_attributes = df1.select_dtypes( exclude=['float64', 'int64', 'datetime64[ns]'])
1.7.1 Atributos Númericos¶
Vamos calcular as métricas de estatística descritiva para cada variável.
# central tendency
ct1 = pd.DataFrame(num_attributes.apply(np.mean) ).T # média
ct2 = pd.DataFrame(num_attributes.apply(np.median) ).T # mediana
# dispersion
d1 = pd.DataFrame(num_attributes.apply(np.std) ).T # Desvio padrão
d2 = pd.DataFrame(num_attributes.apply(min) ).T # valor mínimo
d3 = pd.DataFrame(num_attributes.apply(max) ).T # valor máximo
d4 = pd.DataFrame(num_attributes.apply( lambda x: x.max() - x.min())).T # Variação entre o mínimo e máxilo valor
d5 = pd.DataFrame(num_attributes.apply( lambda x: x.skew())).T #skewness
d6 = pd.DataFrame(num_attributes.apply( lambda x : x.kurtosis())).T # kurtosis
m = pd.concat( [d2, d3, d4, ct1, ct2, d1, d5, d6]).T.reset_index()
m.columns = ['att', 'min', 'max', 'range', 'mean', 'median', 'std', 'skew', 'kurt']
m
1.7.2 Interpretação dos dados¶
Vamos analisar a tabela gerada acima e extrair informações dos dados:
Variáveis Financeiras:
- price (preço) e disc_price (preço com desconto) possuem uma ampla variação (range ≈ 10.879), indicando uma grande dispersão nos valores.
- Ambas têm média próxima de 500, mas um desvio padrão elevado (~800-850), sugerindo que a maioria dos valores pode estar concentrada em faixas menores, com alguns preços muito altos.
- O alto skewness (> 4) e kurtosis (> 30) indicam uma distribuição altamente assimétrica e com caudas longas, ou seja, existem outliers muito altos.
Descontos:
- disc_percentage (percentual de desconto) tem uma média muito baixa (~1,69%) e desvio padrão pequeno (~0,08), indicando que a maioria dos descontos é bem reduzida.
- A assimetria (skewness = 5.21) sugere que existem poucos descontos altos e a maioria está próxima de zero.
Frequência e Temporalidade:
- imp_count (número de impressões) tem média de 6,56 e mediana de 5, com distribuição levemente assimétrica à direita (skew = 1.28), indicando que a maioria dos produtos tem poucas impressões, mas alguns têm números elevados.
- month, day e week_number apresentam distribuições mais equilibradas, com skewness e curtose próximas de zero, sugerindo que os dados temporais estão relativamente bem distribuídos sem grandes desvios ou outliers.
Conclusão¶
Os dados indicam grande variabilidade nos preços e descontos, com distribuições altamente assimétricas. Já os dados temporais e de impressões são mais equilibrados. Isso sugere que existem poucos produtos muito caros ou com descontos elevados, enquanto a maioria se concentra em faixas menores.
2.0 – Análise Exploratória de Dados (Exploratory Data Analysis – EDA)¶
EDA (Exploratory Data Analysis) é o processo inicial de análise de dados que envolve a exploração visual e estatística para identificar padrões, detectar outliers, entender distribuições e encontrar relações entre variáveis, auxiliando na formulação de hipóteses e na limpeza dos dados.
# copiando os dados para uma nova tabela, para preservar as informações passadas
# útil caso seja necessário modificar algum passo anterior
df2 = df1.copy()
Algumas perguntas interessantes para serem respondidas:
Qual o merchant que mais vendeu?
Qual a categoria mais vendida?
Qual a marca mais vendida?
Quais os dias que mais vendem?
Quais os meses que mais vendem?
Quais as semanas que mais vendem?
Como o foco aqui é aplicarmos os conceitos e técnicas de elasticidade de preço, vou omitir a etapa de exploração. Caso tenha interesse, todas essas perguntas foram respondidas no repositório do GitHub.
3.0 Feature Engineering¶
Feature Engineering é o processo de criar, transformar e selecionar variáveis (features) para melhorar o desempenho de modelos de Machine Learning (Aprendizado de Máquina), extraindo informações relevantes e representativas dos dados.
# copiando os dados da etapa anterior
df3 = df2.copy()
3.1 Categoria mais vendida da Bestuy.com¶
Vamos selecionar a categoria com mais produtos vendidos da BestBuy, que é a categoria Speaker, Portable, Bluetooth
.
# selecionado apenas produtos da categoria para análise da elasticidade
df_category = df_best[df_best['category_name'] == 'speaker, portable, bluetooth']
# check NA
df_category.isna().sum()
date_imp 0 category_name 0 name 0 price 0 disc_price 0 merchant 0 disc_percentage 0 is_sale 0 imp_count 0 brand 0 p_description 0 dateadded 0 dateseen 0 dateupdated 0 manufacturer 320 day_n 0 month 0 month_n 0 day 0 week_number 0 dtype: int64
Note que há poucos dados nulos do fabricante comparado com os dados nulos de todo o conjunto.
3.2 Agregando as vendas¶
Como a maioria dos produtos não apresentam vendas todos os dias, vamos agrupar os produtos vendidos por semana. E para o preço, consideraremos a média dos preços do produto vendidos naquela semana.
# Agrupando por nome e número da semana no ano, e agregando o preço com desconto (valor da venda) e demanda (quantidade vendida)
test = df_category.groupby(['name', 'week_number']).agg({'disc_price': 'mean', 'date_imp': 'count'}).reset_index()
test.head()
name | week_number | disc_price | date_imp | |
---|---|---|---|---|
0 | BOOM 2 Wireless Bluetooth Speaker – Indigo | 9 | 199.99 | 1 |
1 | BOOM 2 Wireless Bluetooth Speaker – Indigo | 10 | 149.99 | 2 |
2 | BOOM 2 Wireless Bluetooth Speaker – Indigo | 13 | 199.99 | 1 |
3 | BOOM 2 Wireless Bluetooth Speaker – Indigo | 18 | 199.99 | 1 |
4 | BOOM 2 Wireless Bluetooth Speaker – Indigo | 19 | 129.99 | 1 |
Note que mesmo agrupando algumas semanas ainda estão ausentes, ou seja, são semanas que não houve vendas.
Para prosseguirmos, precisamos que as informações de preço e demanda estejam dispostas em relação ao número da semana. Ou seja, precisamos inverter os eixos da tabela, e gerar duas tabelas:
- Tabela com valores de preço por semana para cada produto
- Tabela com valores de demanda por semana para cada produto
#criando dataframe dos valores(x) preço
x_price = test.pivot(index= 'week_number' , columns= 'name', values='disc_price')
x_price = pd.DataFrame(x_price.to_records())
x_price.head()
# criando dataframe com valores de demanda em y
y_demand = test.pivot(index= 'week_number' , columns= 'name', values='date_imp')
y_demand = pd.DataFrame(y_demand.to_records())
y_demand.head()
Como nem todas as semanas tinham informações de venda, ao gerarmos as tabelas esses campos foram preenchidos com valores nulos (NaN
). Vamos precisar tratar esses valores mais a frente.
Descrição dos novos dataframes¶
4.0 Descrição dos dados¶
Como geramos novas tabelas, vamos refazer nossa análise descritiva e verificar por novos padrões e insights nos dados.
4.1 Data Dimension¶
print(f'Number of rows: {x_price.shape[0]}')
print(f'Number of columns: {x_price.shape[1]}')
Number of rows: 23 Number of columns: 42
print(f'Number of rows: {y_demand.shape[0]}')
print(f'Number of columns: {y_demand.shape[1]}')
Number of rows: 23 Number of columns: 42
As duas tabelas aprensentam o mesmo número de linhas e colunas, indicando a mesma quantidade de produtos em ambas.
4.2 Substituindo Valores Nulos¶
Na tabela de preços, para as semanas em que não temos informações sobre os valores de vendas dos produtos, vamos considerar o preço médio do produto durante todo o período analisado. Pois mesmo que nenhuma venda do produto tenha sido feita, o produto ainda estava disponível para ser comprado.
# Preenchendo os valores nulos com a média da coluna
a = np.round(x_price.median(), 2)
x_price.fillna(a, inplace=True)
x_price.head()
No caso dos valores nulos da tabela de demanda, vamos substitui-los por 0, considerando que nenhuma venda foi realizada na semana.
y_demand.fillna(0, inplace=True)
y_demand.head()
4.3 Estatística Descritiva¶
# central tendency
ct1_x = pd.DataFrame(x_price.apply(np.mean) ).T # média
ct2_x = pd.DataFrame(x_price.apply(np.median) ).T # mediana
# dispersion
d1_x = pd.DataFrame(x_price.apply(np.std) ).T # Desvio padrão
d2_x = pd.DataFrame(x_price.apply(min) ).T # valor mínimo
d3_x = pd.DataFrame(x_price.apply(max) ).T # valor máximo
d4_x = pd.DataFrame(x_price.apply( lambda x: x.max() - x.min())).T # Variação entre o mínimo e máxilo valor
d5_x = pd.DataFrame(x_price.apply( lambda x: x.skew())).T # skewness
d6_x = pd.DataFrame(x_price.apply( lambda x : x.kurtosis())).T # kurtosis
m_x = pd.concat( [d2_x, d3_x, d4_x, ct1_x, ct2_x, d1_x, d5_x, d6_x]).T.reset_index()
m_x.columns = ['att', 'min', 'max', 'range', 'mean', 'median', 'std', 'skew', 'kurt']
m_x
Vamos analisar nossa nova tabela:
1. Análise Geral dos Preços¶
- Os preços variam bastante, com o mínimo em $9,00 e o máximo em $337,49, sugerindo uma grande diversidade de produtos, desde modelos mais acessíveis até opções premium.
- O desvio padrão varia entre produtos, indicando que alguns têm preços mais estáveis, enquanto outros apresentam maior variação.
2. Assimetria e Curtose¶
- Alguns produtos apresentam skewness (assimetria) negativa, como a Bose Soundlink Color II (-4,79), indicando que os preços são mais concentrados em valores altos, com poucas opções mais baratas.
- Já produtos como o BRAVEN BRV-HD têm skewness positiva (2,83), mostrando que a maioria dos preços está concentrada na faixa mais baixa, com alguns valores elevados puxando a média para cima.
- Curtose alta (> 8) em alguns produtos indica distribuições mais “pontudas”, ou seja, preços que se concentram em uma faixa estreita, com poucos valores extremos.
3. Diferença Entre Mínimo e Máximo (Range)¶
- Produtos como Sony XB7 Extra Bass têm uma grande variação de preço ($149,50), indicando promoções ou diferentes configurações disponíveis.
- Outros, como a Monster SuperStar BackFloat, têm uma variação mínima ($0,04), sugerindo um preço praticamente fixo.
4. Padrões nos Produtos¶
- Alguns produtos têm mediana e média muito próximas, como o JBL Clip2 ($57,38 média e $59,99 mediana), o que sugere uma distribuição simétrica sem valores extremos.
- Outros, como a Tough Portable Bluetooth Speaker ($153,64 média, $146,99 mediana), mostram uma leve assimetria, indicando preços que podem ter variação dependendo da demanda.
# central tendency
ct1_y = pd.DataFrame(y_demand.apply(np.mean) ).T # média
ct2_y = pd.DataFrame(y_demand.apply(np.median) ).T # mediana
# dispersion
d1_y = pd.DataFrame(y_demand.apply(np.std) ).T # Desvio padrão
d2_y = pd.DataFrame(y_demand.apply(min) ).T # valor mínimo
d3_y = pd.DataFrame(y_demand.apply(max) ).T # valor máximo
d4_y = pd.DataFrame(y_demand.apply( lambda x: x.max() - x.min())).T # Variação entre o mínimo e máxilo valor
d5_y = pd.DataFrame(y_demand.apply( lambda x: x.skew())).T #skewness
d6_y = pd.DataFrame(y_demand.apply( lambda x : x.kurtosis())).T # kurtosis
m_y = pd.concat( [d2_y, d3_y, d4_y, ct1_y, ct2_y, d1_y, d5_y, d6_y]).T.reset_index()
m_y.columns = ['att', 'min', 'max', 'range', 'mean', 'median', 'std', 'skew', 'kurt']
m_y
Analisando também a tabela de demanda:
1. Variabilidade das vendas¶
- A coluna week_number mostra que os dados abrangem um período entre as semanas 9 e 41, com média próxima da semana 25.
- Alguns produtos, como o JBL Clip2 Portable Speaker, tiveram um número de vendas significativamente maior (máximo de 10) do que outros, que registraram no máximo 2 ou 3 unidades por semana.
2. Produtos com maior dispersão¶
- O JBL Clip2 Portable Speaker tem um desvio padrão de 3.34, indicando que suas vendas variam bastante entre as semanas.
- O Outdoor Tech Buckshot Pro Bluetooth Speaker tem um máximo de 4 vendas por semana e um desvio padrão de 1.59, indicando alta variabilidade.
3. Assimetria nas vendas¶
- O FUGOO – Sport XL Portable Bluetooth Speaker e Monster SuperStar BackFloat possuem alta assimetria positiva (skew ≈ 2.06), sugerindo que tiveram poucas semanas com vendas altas e muitas semanas com vendas baixas ou nulas.
- O Harman Kardon One Portable Bluetooth Speaker tem uma assimetria negativa de -1.84, indicando que a maioria das semanas teve vendas altas, com poucas semanas de vendas muito baixas.
4. Distribuições achatadas ou com caudas longas¶
- Produtos como Bose Soundlink Color II e Sony SRS-XB40 apresentam curtose negativa, indicando que suas vendas foram relativamente estáveis, sem muitos valores extremos.
- Já FUGOO Sport XL e Monster SuperStar BackFloat possuem curtose alta (≈ 3.8), indicando que algumas semanas tiveram vendas extremamente altas em comparação com a média.
5.0 Machine Learning¶
Agora que temos nossos dados prontos, entraremos na etapa de aprendizado de máquina utlizando Regressão Linear para determinarmos a elasticidade-preço da demanda de cada produto.
5.1 BOOM 2 Wireless Bluetooth Speaker – Indigo¶
Exemplo de aplicação da Regressão Linear, com valores usados para cálculo da elasticidade.
Note que vamos criar uma coluna com valores constantes antes de aplicar a regressão linear. Essa coluna de valores 1 é um requisito do pacote OLS. Na equação de regressão linear temos:
$$\hat{y} = b_0 * x_0 + b_1 * x_1$$Nesse caso, $x_0$ é sempre 1, logo a equação fica:
$$\hat{y} = b_0 * 1 + b_1 * x_1$$# separando as colunas de preço e demanda do produto escolhido
x_item = x_price['BOOM 2 Wireless Bluetooth Speaker - Indigo']
y_item = y_demand['BOOM 2 Wireless Bluetooth Speaker - Indigo']
# adicionando uma coluna com valores 1, necessária para cálculo da regressão linear
X_item = sm.add_constant(x_item)
# criando o modelo de regressão linear
model = sm.OLS(y_item, X_item)
# salvando os dados gerados no arquivo
results = model.fit()
print(results.summary())
OLS Regression Results ====================================================================================================== Dep. Variable: BOOM 2 Wireless Bluetooth Speaker - Indigo R-squared: 0.196 Model: OLS Adj. R-squared: 0.157 Method: Least Squares F-statistic: 5.106 Date: Sun, 08 Oct 2023 Prob (F-statistic): 0.0346 Time: 18:08:11 Log-Likelihood: -25.117 No. Observations: 23 AIC: 54.23 Df Residuals: 21 BIC: 56.50 Df Model: 1 Covariance Type: nonrobust ============================================================================================================== coef std err t P>|t| [0.025 0.975] -------------------------------------------------------------------------------------------------------------- const 3.8816 1.419 2.736 0.012 0.931 6.832 BOOM 2 Wireless Bluetooth Speaker - Indigo -0.0168 0.007 -2.260 0.035 -0.032 -0.001 ============================================================================== Omnibus: 4.549 Durbin-Watson: 2.630 Prob(Omnibus): 0.103 Jarque-Bera (JB): 3.898 Skew: 0.986 Prob(JB): 0.142 Kurtosis: 2.574 Cond. No. 1.72e+03 ============================================================================== Notes: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified. [2] The condition number is large, 1.72e+03. This might indicate that there are strong multicollinearity or other numerical problems.
5.2 Interpretando Summary¶
No início do sumário temos qual e a nossa variável dependente que é ‘BOOM 2 Wireless Bluetooth Speaker – Indigo’ em seguida temos o modelo e método utilizado e ambos remetem ao método dos mínimos quadrados.
A seguir são apresentados o número de observações no dataset, DF Residuals é os graus de liberdade do nosso teste, determinado como o (número e observações – número de variáveis – 1).
Df Model é o número de variáveis preditas, tipo de covariância é a medida de como duas variáveis estão relacionadas, podendo ser positiva ou negativa, pode minimizar ou eliminar variáveis.
R² é o quanto a variável independente é explicada pela nossa variável dependente, expressa percentualmente, no caso é 0,196, ou seja, 19,6% e o valor ajustado de R² é o R² penalizado pelo número de variáveis. F-statistic é o resultado de um teste F de Fisher-Snedeco, para interpretar esse valor é necessário determinar um valor de alpha e usar uma tabela F, esse teste determina a significância estatística do nosso modelo.
Prob (F-statistic) define a acurácia da hipótese nula. Log-likelihood compara os valores dos coeficientes de cada variável na criação do modelo.
AIC e BIC são usados para selecionar as características das variáveis.
const é o coeficiente linear, pode ser interpretado como o valor onde começa a nossa reta, ou ainda, onde o eixo vertical (y) é cortado. Abaixo são apresentadas todas as variáveis independentes do modelo (no caso somente a variável ‘BOOM 2 Wireless Bluetooth Speaker – Indigo’.
coef (coeficiente linear) é a medida de como afeta o comportamento da reta, ou seja, é o coeficiente angular b da reta. std err é o desvio padrão, t é relacionado com quão preciso o coeficiente é medido. P>|t| é uma medida importante que gera o p-valor, que mede a eficácia do modelo em medir a variável. 0,025 e 0,975 estabelecem as medidas de 95% dentro dos nossos dados, seguem a mesma definição clássica para outliers, que define outlier como dados fora de dois desvios padrões.
Omnibus é uma medida de normalidade que usa assimetria (skewness) e curtose, sendo 0 uma curva perfeitamente normal. Prob(Omnibus) é um teste estatístico que mede a normalidade da distribuição, o valor sendo 1 indica uma distribuição perfeitamente normal. Skew mede a assimetria da curva, com 0 temos uma curva perfeitamente simétrica. Kurtosis mede o quão agudo é o pico da nossa curva, altos valores indicam menos outliers.
Durbin-Watson define a homoscedasticidade ou uma distribuição uniforme dos erros dos dados, o ideal é ter valores entre 1 e 2.
Jarque-Bera (JB) e Prob(JB) são métodos alternativos para medir os mesmos valores de Omnibus e Prob(Omnibus).
Cond. No. mede a sensibilidade do modelo comparado com as mudanças nos nossos dados.
5.3 Todas os produtos¶
Agora repetindo processo para todos os produtos, vamos considerar apenas os produtos que alcançarem um p-valor menor que 0,05 que indica estatisticamente que há uma probabilidade menor que 5% de obter os resultados observados devido ao acaso. E por fim armazenando os resultados em dataframe
:
# dicionario para armazenar os resultados obtidos da regressão de cada produto
results_values = {
"name": [],
"price_elasticity": [],
"price_mean": [],
"quantity_mean": [],
"quantity_total": [],
"intercept": [],
"slope": [],
"rsquared": [],
"p_value": []
}
# loop para cálculo de cada regressão
for column in x_price.columns[1:]:
column_points = []
for i in range(len(x_price[column])):
column_points.append((x_price[column][i], y_demand[column][i]))
df = pd.DataFrame(list(column_points), columns=['x_price', 'y_demand'])
x = df['x_price']
y = df['y_demand']
X = sm.add_constant(x)
#machine learning
model = sm.OLS(y, X)
results = model.fit()
# caso o p-valor da regressão calculado para o produto seja menor que 0.05, o mesmo é considerado válido para estudo de elasticidade
if results.f_pvalue < 0.05:
rsquared = results.rsquared
p_value = results.f_pvalue
intercept, slope = results.params
mean_price = np.mean(x)
mean_quantity = np.mean(y)
total_quantity = np.sum(y)
price_elasticity = slope*(mean_price/mean_quantity)
results_values['name'].append(column)
results_values['price_elasticity'].append(price_elasticity)
results_values['price_mean'].append(mean_price)
results_values['quantity_mean'].append(mean_quantity)
results_values['quantity_total'].append(total_quantity)
results_values['intercept'].append(intercept)
results_values['slope'].append(slope)
results_values['rsquared'].append(rsquared)
results_values['p_value'].append(p_value)
# transformando o dicionário em DataFrame
df_elasticity = pd.DataFrame.from_dict(results_values)
5.4 Produtos válidos para análise¶
Somente produtos que atendem a condição de nível de significância inferior a 0.05, note que algumas informações obtidas durante a regressão linear também foram mantidas:
- Elasticidade do preço
- Intercepto ou coeficiente linear (indica onde a reta cruza o eixo)
- Coeficiente angular (indica o inclinação da reta )
- Valor de $R^2$
- p-valor
df_elasticity
Monster SuperStar BackFloat High-Definition apresentou um número extremo para elasticidade de preço. Uma análise futura mais detalha é necessário. No momento a ação será a retirada do produto.
# salvando tabela de dados em csv
df_elasticity.to_csv('../data/processed/elasticity.csv', index=False)
6.0 Elasticidade¶
Com os valores de elasticidade podemos analisar o ranking dos produtos:
# criando um ranking da elasticidade-preço
df_elasticity['ranking'] = df_elasticity.loc[ : ,'price_elasticity'].rank( ascending = True).astype(int)
df_elasticity = df_elasticity.reset_index(drop = True)
# plotando gráfico
plt.figure(figsize = (12,4))
plt.hlines(y = df_elasticity['ranking'] , xmin = 0, xmax = df_elasticity['price_elasticity'], alpha = 0.5, linewidth = 3)
for name, p in zip(df_elasticity['name'], df_elasticity['ranking']):
plt.text(4, p, name)
#Add elasticity labels
for x, y, s in zip(df_elasticity['price_elasticity'], df_elasticity['ranking'], df_elasticity['price_elasticity']):
plt.text(x, y, round(s, 2), horizontalalignment='right' if x < 0 else 'left',
verticalalignment='center',
fontdict={'color':'red' if x < 0 else 'green', 'size':10})
# ajustes no gráfico
plt.gca().set(ylabel= 'Ranking Number', xlabel= 'Price Elasticity')
plt.title('Price Elasticity' , fontdict={'size':13})
plt.grid(linestyle='--')
7.0 Performance de Negócio¶
Para apresentar nossos resultados ao time de negócio responsável pelos preços dos produtos, vamos calcular diferentes cenários para que possam analisar os riscos e vantagens das mudanças de preço para cada produto.
Considerando um desconto de 15% no preço atual médio do produto, calcularemos:
- O cenário atual: faturamento atual (baseado no preço e demanda atuais)
- O pior cenário: faturamento com desconto e demanda atual
- O valor arriscado: possível perda entre o cenário atual e pior cenário
- O cenário esperado: faturamento com desconto e demanda aumentada
- A variação entre o cenário atual e o cenário esperado
- A variação percentual
#dicionario para armazenar resultados
resultado_faturamento = {
'name': [],
'current_revenue': [],
'worstcase_revenue':[],
'risked_revenue':[],
'expected_revenue':[],
'variance':[],
'variance_perc':[]
}
for i in range(len(df_elasticity)):
preco_atual_medio = x_price[df_elasticity['name'][i]].mean()
demanda_atual = y_demand[df_elasticity['name'][i]].sum()
# preço promocional
reducao_preco = preco_atual_medio*0.85
# aumento da demanda considerando o valor de redução do preço (1.0 - 0.9)
aumento_demanda = np.abs(0.15*df_elasticity['price_elasticity'][i])
# cálculo da nova demanda considerando o aumento
demanda_nova = demanda_atual-(aumento_demanda*demanda_atual)
# faturamento atual
faturamento_atual = round(preco_atual_medio*demanda_atual, 2)
# faturamento com preço reduzido e nova demanda
faturamento_novo = round(reducao_preco*demanda_nova, 2)
# caso a demanda não altere, o quanto custa essa redução
faturamento_reducao = round(faturamento_atual*0.9, 2)
# risco de aplicar promoção e não houver aumento
perda_faturamento = round(faturamento_atual-faturamento_reducao, 2)
# variação entre faturamento atual e previsto
variacao_faturamento = round(faturamento_novo-faturamento_atual ,2)
# variação percentual
variacao_percentual = round(((faturamento_novo-faturamento_atual)/faturamento_atual),2)
# agrupando valores
resultado_faturamento['name'].append(df_elasticity['name'][i])
resultado_faturamento['current_revenue'].append(faturamento_atual)
resultado_faturamento['worstcase_revenue'].append(faturamento_reducao)
resultado_faturamento['risked_revenue'].append(perda_faturamento)
resultado_faturamento['expected_revenue'].append(faturamento_novo)
resultado_faturamento['variance'].append(variacao_faturamento)
resultado_faturamento['variance_perc'].append(variacao_percentual)
resultado = pd.DataFrame(resultado_faturamento)
resultado
Apresentar as informações de diferentes cenários para os stakeholders é muito mais interessante do que o próprio valor da elasticidade. Os dados da tabela acima podem ajudar a decidir as próximas ações que serão tomadas na empresa com relação a promoções e aumento de preços.
# salvando arquivo csv
resultado.to_csv('../data/processed/business_performance.csv', index=False)
8.0 Elasticidade de Preço Cruzada¶
Por fim, vamos calcular o impacto de um produto sobre o outro. Como o preço de cada produto pode influenciar na elasticidade dos produtos substitutos.
def crossprice ( df_x, df_y, column_name):
# pegando todos os valores de x_price
new_df = x_price.copy()
# peganado os valores y_demand com o mesmo nome da coluna
new_df['y_value-' + column_name] = y_demand[column_name]
multi_xvalues =new_df.loc[:, new_df.columns[1:-1]]
multi_yvalues = new_df.loc[:, new_df.columns[-1]]
# obter o valor médio do preço do produto
mean_xvalues = np.mean(multi_xvalues)
# obter o valor médio da demanda do produto
mean_yvalues = np.mean(multi_yvalues)
# regressão linear
X = sm.add_constant(multi_xvalues)
model = sm.OLS(multi_yvalues, X, missing='drop')
result = model.fit()
#obtendo os resultados
results_summary = result.summary()
# p-valores para cada coeficiente
pvalue = result.pvalues
# transformando o resultado em uma dataframe
results_as_html = results_summary.tables[1].as_html()
new_dataframe = pd.read_html(results_as_html, header=0, index_col=0)[0]
#adicionando o p-valor ao dataframe
new_dataframe['p_value'] = pvalue
#definindo o nome do produto como indice
new_dataframe.index.name= 'name'
new_dataframe.reset_index()
#calculando a elasticidade cruzada
new_dataframe['mean'] = mean_xvalues
new_dataframe['price_elasticity'] = round((new_dataframe.coef)*(new_dataframe['mean']/mean_yvalues), 2)
new_dataframe = new_dataframe.reset_index()
pvalue_siginicant = new_dataframe['p_value']
#verificando a hipótese nula(inclinação por produto)
new_dataframe[column_name + 'CPE'] = np.where((pvalue_siginicant > 0.05), 'No Effect', new_dataframe['price_elasticity'])
new_dataframe = new_dataframe.dropna()
return new_dataframe[['name', column_name + 'CPE']]
result_df = pd.DataFrame()
for column in x_price.columns[1:]:
result_df[['name', column +'CPE']] = crossprice(x_price, y_demand, column)
result_df = result_df.set_index('name')
# salvando tabela em csv
result_df.to_csv('../data/processed/crossprice.csv')
As informações foram salvas em um arquivo e usadas no dashboard para facilitar a visualização dos dados.
Para acompanhar e testar alterações em diferentes variações de preço dos produtos, desenvolvi um dashboard em Streamlit com três visões.
Na primeira visão é possível escolher acrescentar ou descontar uma porcentagem do preço atual do produto, e assim analisar o retorno calculado em diferentes cenários.
A segunda visão apresenta as informações de elasticidade-preço, intercepto, inclinação da reta, \(R^2\) e p-valor para cada produto, no nome das colunas é possível ver uma descrição da mesma.
Na terceira visão, podemos buscar o valor de elasticidade cruzada entre dois produtos, tanto na tabela inferior com todos os dados ou selecionando o primeiro e segundo produto. Lembrando que a elasticidade cruzada do Produto A em relação ao Produto B é diferente da elasticidade cruzada do Produto B em relação ao Produto A.
Conclusão
A análise da elasticidade de preço nos permitiu entender melhor o comportamento dos consumidores diante das variações de preços, fornecendo insights valiosos para a tomada de decisão estratégica. Ao longo do projeto, exploramos conceitos fundamentais, realizamos análises estatísticas e, por fim, desenvolvemos um dashboard interativo que possibilita testar diferentes cenários de precificação e visualizar impactos potenciais nas vendas e na receita.
Com essa ferramenta, empresas podem simular ajustes nos preços e tomar decisões mais embasadas, minimizando riscos e maximizando oportunidades de lucro. Além disso, esse projeto reforça a importância de uma abordagem baseada em dados para estratégias de precificação, tornando o processo mais eficiente e alinhado com o comportamento do mercado.
Imagem de capa por John de Jong na Unsplash.