Entendendo o conceito de eixo em NumPy e Pandas: Como trabalhar com dados em Python

feature_axis_eixo_numpy_pandas

Ambos o NumPy e o Pandas têm o conceito de eixo (ou axis em inglês), mas eles são usados de maneiras ligeiramente diferentes.

No NumPy, o eixo refere-se à dimensão ao longo da qual uma determinada operação é realizada. Já no Pandas, o parâmetro axis tem um significado ligeiramente diferente, referindo-se à direção ao longo da qual uma operação deve ser realizada.

Neste artigo, veremos exemplos e consequências deste conceito em cada biblioteca.

Como escrito no início do artigo, no NumPy o eixo refere-se à dimensão ao longo da qual uma determinada operação é realizada. Por exemplo, ao calcular a soma de uma matriz NumPy, você pode especificar o eixo ao longo do qual a soma deve ser calculada. Se a matriz tiver a forma (3, 4), então o parâmetro de eixo pode ser definido como 0 ou 1. Se axis=0, a soma é calculada ao longo da primeira dimensão, resultando em um array 1D de comprimento 4. Se axis=1, a soma é calculada ao longo da segunda dimensão, resultando em um array 1D de comprimento 3.

Veremos este exemplo com duas dimensões, e depois vamos expandir para exemplos mais complexos, com mais dimensões.

Duas dimensões

Comecemos importando a biblioteca e criando um array bidimensional, conferindo o comprimento de suas dimensões com o atributo shape. Caso tenha dúvidas sobre os conceitos de dimensão e shape (forma) no contexto do NumPy, veja este artigo.

import numpy as np

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
arr
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])
arr.shape
(3, 4)

Como explicado anteriormente, o axis define em qual dimensão determinada operação será feita. Logo, façamos uma soma dos valores ao longo da primeira dimensão:

sum_axis_0 = np.sum(arr, axis=0)
sum_axis_0
array([15, 18, 21, 24])

Obtivemos 4 valores em um array unidimensional. Cada um deles é a soma dos valores ao longo da dimensão de tamanho 3. Como o array original arr é bidimensional, você pode visualizar mais facilmente se imaginar que os valores são as somas de cada coluna de uma matriz bidimensional. Mas não se apegue muito a esta visualização, pois ela não será muito útil quando aumentarmos a quantidade de dimensões.

Vejamos a soma ao longo da segunda dimensão:

sum_axis_1 = np.sum(arr, axis=1)
sum_axis_1
array([10, 26, 42])

Aqui, temos um array unidimensional de 3 valores. Cada um deles é a soma dos valores ao longo da dimensão de tamanho 4. Seguindo nossa analogia visual, você pode enxergar como sendo a soma de cada linha de nossa matriz bidimensional.

É importante notar que o parâmetro de eixo pode ser usado com muitos outros métodos NumPy, não apenas com a soma. Por exemplo, ao calcular a média, a mediana ou o desvio padrão de uma matriz, o parâmetro de eixo pode ser usado para especificar ao longo de qual dimensão o cálculo deve ser feito. Isso é especialmente útil quando se trabalha com matrizes de alta dimensão, onde as operações podem se tornar complexas. Daí o parâmetro de eixo ser uma das principais ferramentas que os cientistas de dados usam para manipular e transformar dados em matrizes NumPy.

As operações de slice continuam sendo válidas, de forma que também podemos realizar operações em partes de nossa matriz, indicando o eixo desejado:

# Somando as duas primeiras linhas ao longo do eixo 0
sum_rows = np.sum(arr[:2, :], axis=0)
sum_rows
array([ 6,  8, 10, 12])
# Somando todas as linhas das primeira e terceira colunas ao longo do eixo 0
sum_cols = np.sum(arr[:, [0, 2]], axis=0)
sum_cols
array([15, 21])

Três dimensões

Para três dimensões, a linha de raciocínio é a mesma, mas a analogia de linhas e colunas que usamos acima pode não ajudar muito. Aqui utilizaremos recursos visuais para efetivamente compreender a ideia de eixo de uma operação. Primeiro, criemos um array 3D com inteiros aleatórios:

# Definindo uma seed fixa, de forma que seja possível reproduzir o artigo igualmente
rng = np.random.default_rng(seed=42)

# Criando um array 3D com valores inteiros aleatórios de 0 a 9
arr_3d = rng.integers(low=0, high=9, size=(2, 3, 4), endpoint=True)

Conferindo o shape:

arr_3d.shape
(2, 3, 4)

Vejamos como o NumPy nos apresenta tal array:

arr_3d
array([[[0, 7, 6, 4],
        [4, 8, 0, 6],
        [2, 0, 5, 9]],

       [[7, 7, 7, 7],
        [5, 1, 8, 4],
        [5, 3, 1, 9]]])

Observe que ele apresenta como se fossem dois conjuntos de dados. Inclusive, podemos pegar cada conjunto e verificar seu shape:

arr_3d[0]
array([[0, 7, 6, 4],
       [4, 8, 0, 6],
       [2, 0, 5, 9]])
arr_3d[0].shape
(3, 4)
arr_3d[1]
array([[7, 7, 7, 7],
       [5, 1, 8, 4],
       [5, 3, 1, 9]])
arr_3d[1].shape
(3, 4)

Ou seja, temos duas matrizes bidimensionais com 3 linhas e 4 colunas, arranjadas ao longo de uma dimensão de comprimento 2. Confuso? Veja a seguinte imagem:

numpy_axes

Com auxílio da imagem, o que acontece, então, se solicitamos a soma ao longo do eixo 0? Ora, é como se estivéssemos empurrando a matriz da frente contra a matriz de trás. Logo, deve ser a soma item a item de uma matriz com a outra, e o shape resultante deve ser igual ao shape da matriz resultante:

sum_axis_0 = arr_3d.sum(axis=0)
display(sum_axis_0)
display(sum_axis_0.shape)
array([[ 7, 14, 13, 11],
       [ 9,  9,  8, 10],
       [ 7,  3,  6, 18]])
(3, 4)

Confira o resultado e veja que é realmente o que foi descrito. E aqui já temos uma primeira dica que é muito útil quando analisamos resultados de dimensões muito elevadas, de difícil visualização: o shape resultante é igual ao original retirando-se a dimensão na qual se está aplicando a operação. No caso, nosso array original tem shape (2, 3, 4), estamos realizando a operação de soma ao longo da dimensão de comprimento 2, de forma que o shape resultante exclui esta dimensão, ficando (3, 4). Guarde esta dica para analisar os exemplos seguintes.

No caso específico desta soma que fizemos, o resultado é o mesmo de adicionar explicitamente cada matriz bidimensional presente em cada posição do primeiro eixo (axis=0):

arr_3d[0] + arr_3d[1]
array([[ 7, 14, 13, 11],
       [ 9,  9,  8, 10],
       [ 7,  3,  6, 18]])

Mas, claro, é mais eficiente e sucinto utilizar a notação de eixo.

Modifiquemos agora nosso exemplo, solicitando a soma ao longo do eixo 1:

sum_axis_1 = arr_3d.sum(axis=1)
display(sum_axis_1)
display(sum_axis_1.shape)
array([[ 6, 15, 11, 19],
       [17, 11, 16, 20]])
(2, 4)

Veja que o shape é o esperado conforme a dica que passamos anteriormente. Como estamos olhando segunda dimensão, de comprimento 3, o shape resultante é dado pelos comprimentos das demais dimensões, sendo (2, 4).

Os valores nada mais são que as somas ao longo do eixo 1, que pode ser entendido como as somas de cada coluna de cada matriz bidimensional, conforme a figura apresentada. Como são duas matrizes, cada uma com quatro colunas, temos como resultado uma matriz bidimensional 2 x 4.

Perceba como a sintaxe do NumPy facilita muito esta operação. Além de a realizar eficientemente quanto a memória e velocidade de execução. Se fossemos realizar tal soma manualmente, a partir de uma função própria, precisaríamos iterar por cada matriz de nosso array tridimensional e, em cada matriz, iterar por cada coluna armazenando as somas de cada uma. Apenas a título de exercício, podemos criar tal função e realizar a soma ao longo do eixo 1:

def sum_column(matrix):
    result = []
    for column in range(len(matrix[0])):
        total = 0
        for row in matrix:
            total += row[column]
        result.append(total)
    return result


sum_axis_1_function = []
for matrix in arr_3d:
    sum_axis_1_function.append(sum_column(matrix))

np.array(sum_axis_1_function)
array([[ 6, 15, 11, 19],
       [17, 11, 16, 20]])

O resultado é o mesmo obtido pelo NumPy, mas o código é certamente mais difícil de manter e ineficiente do ponto de vista de memória e velocidade. Esta é a beleza do NumPy e suas operações vetorizadas. Ou seja, ele é otimizado para aplicar algoritmos usualmente elaborados para operações elemento a elemento, como nossa função acima, em conjuntos de valores. O ganho de performance e semântica é significativo.

Ainda temos mais uma dimensão em nosso array. Seguindo a mesma lógica, sabemos que o array resultante da operação de soma ao longo desse eixo terá seu shape (2, 3), pois estamos olhando a dimensão de comprimento 4 de nosso array original. E, pela figura, pode ser entendido como as somas de cada linha de cada matriz bidimensional. Como são duas matrizes, cada uma com três linhas, temos como resultado uma matriz bidimensional 2 x 3. Conferindo:

sum_axis_2 = arr_3d.sum(axis=2)
display(sum_axis_2)
display(sum_axis_2.shape)
array([[17, 18, 16],
       [28, 18, 18]])
(2, 3)

Assim como feito anteriormente, apenas para fins didáticos, podemos verificar que este é exatamente o mesmo resultado que seria obtido criando uma função para iterar por cada linha de cada matriz realizando a soma:

def sum_row(matrix):
    result = []
    for row in matrix:
        total = sum(row)
        result.append(total)
    return result


sum_axis_2_function = []
for matrix in arr_3d:
    sum_axis_2_function.append(sum_row(matrix))

np.array(sum_axis_2_function)
array([[17, 18, 16],
       [28, 18, 18]])

Novamente, é muito mais eficiente utilizar as operações vetorizadas do NumPy. Mas deixo as funções neste artigo para facilitar a compreensão e visualização dos resultados obtidos.

Mais de três dimensões

Até 3 dimensões, figuras e esquemas nos ajudam a ter uma ideia visual. No entanto, o grande poder do NumPy é possibilitar trabalhar em problemas que, conceitualmente, podem envolver múltiplas dimensões. Se a lógica apresentada até aqui foi compreendida por você, os resultados para dimensões superiores serão bastante naturais, mesmo que nossa capacidade de visualização seja prejudicada pela nossa limitação visual a três dimensões.

Geremos um array de números inteiros de 4 dimensões:

arr_4d = rng.integers(low=0, high=9, size=(2, 2, 3, 4), endpoint=True)
arr_4d
array([[[[7, 6, 4, 8],
         [5, 4, 4, 2],
         [0, 5, 8, 0]],

        [[8, 8, 2, 6],
         [1, 7, 7, 3],
         [0, 9, 4, 8]]],


       [[[6, 7, 7, 1],
         [3, 4, 4, 0],
         [5, 1, 7, 6]],

        [[9, 7, 3, 9],
         [4, 3, 9, 3],
         [0, 4, 7, 1]]]])

Veja que geramos um array de shape (2, 2, 3, 4). Ora, isto significa que temos a primeira dimensão com comprimento 2, cada posição desta dimensão armazenando um array tridimensional de forma (2, 3, 4). Não por acaso, vemos na maneira que o NumPy apresenta o array dois conjuntos claros de arrays. E, dentro de cada um destes, outros dois conjuntos que podemos visualizar como arrays bidimensionais de forma (3, 4). Tudo que descrevemos neste parágrafo segue a mesma linha de raciocínio que adotamos em 3 dimensões, apenas expandimos para uma dimensão adicional.

Podemos visualizar cada posição da primeira dimensão de nosso array quadridimensional:

arr_4d[0]
array([[[7, 6, 4, 8],
        [5, 4, 4, 2],
        [0, 5, 8, 0]],

       [[8, 8, 2, 6],
        [1, 7, 7, 3],
        [0, 9, 4, 8]]])
arr_4d[1]
array([[[6, 7, 7, 1],
        [3, 4, 4, 0],
        [5, 1, 7, 6]],

       [[9, 7, 3, 9],
        [4, 3, 9, 3],
        [0, 4, 7, 1]]])

Agora, façamos a soma ao longo dessa dimensão, ao longo de axis=0:

sum_axis_0_4d = arr_4d.sum(axis=0)
display(sum_axis_0_4d)
display(sum_axis_0_4d.shape)
array([[[13, 13, 11,  9],
        [ 8,  8,  8,  2],
        [ 5,  6, 15,  6]],

       [[17, 15,  5, 15],
        [ 5, 10, 16,  6],
        [ 0, 13, 11,  9]]])
(2, 3, 4)

O shape é o resultado que esperamos seguindo a dica dada mais acima no artigo e a discussão feita anteriormente. E os valores são o resultado de somar, item a item, os arrays 3D presentes no eixo.

Para axis=1 temos:

display(arr_4d.sum(axis=1))
display(arr_4d.sum(axis=1).shape)
array([[[15, 14,  6, 14],
        [ 6, 11, 11,  5],
        [ 0, 14, 12,  8]],

       [[15, 14, 10, 10],
        [ 7,  7, 13,  3],
        [ 5,  5, 14,  7]]])
(2, 3, 4)

Aqui é a dimensão onde entramos no array 3D presente em cada posição do eixo 0. Assim, estamos somando item a item as matrizes bidimensionais em cada array 3D.

Para axis=2:

display(arr_4d.sum(axis=2))
display(arr_4d.sum(axis=2).shape)
array([[[12, 15, 16, 10],
        [ 9, 24, 13, 17]],

       [[14, 12, 18,  7],
        [13, 14, 19, 13]]])
(2, 2, 4)

Seguindo a lógica, é a soma de cada coluna (são 4 colunas) dentro de cada matriz bidimensional, dentro de cada array 3D (são 2 arrays), dentro de cada posição de nosso array 4D (são 2 posições).

Por fim, para axis=3:

display(arr_4d.sum(axis=3))
display(arr_4d.sum(axis=3).shape)
array([[[25, 15, 13],
        [24, 18, 21]],

       [[21, 11, 19],
        [28, 19, 12]]])
(2, 2, 3)

Analogamente, é a soma de cada linha (são 3 linhas) dentro de cada matriz bidimensional, dentro de cada array 3D (são 2 arrays), dentro de cada posição de nosso array 4D (são 2 posições).

Se tiver dificuldade, leia novamente, faça contas em um pedaço de papel, ou faça esquemas. Eu mesmo fiz isto para entender quando tive contato com estas operações. Mas uma vez entendido, nunca mais você terá dificuldade em entender o resultado de uma operação envolvendo eixos no NumPy!

Pandas

escrevemos aqui no site sobre como as estruturas básicas do Pandas, Series e DataFrames, são construídas sobre os arrays do NumPy. No entanto, os dataframes do Pandas são, por definição, feitos para serem similares a estruturas tabulares, bidimensionais, com colunas e linhas. Desta forma, precisamos entender como o conceito de eixo deve ser entendido nesta biblioteca.

DataFrames simples

Acredito que o exemplo mais simples seja transformar nosso array bidimensional que serviu de primeiro exemplo do NumPy em um dataframe do Pandas. Lembrando, o array era:

arr
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])

Transformado em dataframe:

import pandas as pd

arr_to_df = pd.DataFrame(arr)
arr_to_df
0 1 2 3
0 1 2 3 4
1 5 6 7 8
2 9 10 11 12

Vejamos como será o resultado de solicitar a soma ao longo de axis=0:

arr_to_df.sum(axis=0, numeric_only=True)
0    15
1    18
2    21
3    24
dtype: int64

Ora, nada mais temos que o resultado da soma de cada coluna. E os valores são os mesmos que obtivemos, na forma de array, quando fizemos a soma ao longo de axis=0 no array original do NumPy.

Analogamente, para axis=1:

arr_to_df.sum(axis=1, numeric_only=True)
0    10
1    26
2    42
dtype: int64

Temos que o resultado da soma de cada linha. E os valores são os mesmos que obtivemos, na forma de array, quando realizamos a soma ao longo de axis=1 no array original do NumPy.

Mas, se os resultados são os mesmos, há, então, alguma diferença?

Neste caso específico, não. Mas vamos considerar um exemplo mais usual de dataframe que irá ajudar a perceber algumas particularidades da biblioteca e servirá de modelo básico para as próximas discussões.

Como dito no início desta seção, o Pandas foi pensado para lidar com dados tabulares. Isto significa que as colunas representam as variáveis, cada uma com um nome exclusivo e com um tipo de dado específico, e as linhas representam as observações ou registros no conjunto de dados. Considere o dataframe a seguir, com nome, idade e salário para três pessoas fictícias:

data = {
    "Nome": ["Alice", "Bruno", "Carlos"],
    "Idade": [25, 30, 35],
    "Salário": [50000, 60000, 70000],
}
df = pd.DataFrame(data)
df
Nome Idade Salário
0 Alice 25 50000
1 Bruno 30 60000
2 Carlos 35 70000

Agora temos mais significado para nossos dados. Fica bem claro que, se quisermos a média de idade e de salário das pessoas, estamos olhando para as colunas, o que significa axis=0 no Pandas:

mean_axis_0 = df.mean(axis=0, numeric_only=True)
mean_axis_0
Idade         30.0
Salário    60000.0
dtype: float64

Como a biblioteca espera sempre um formato tabular, onde cada coluna tem diversos registros de um mesmo tipo, o parâmetro axis=0 costuma ser o padrão nos métodos do Pandas. Desta forma, seria suficiente:

df.mean(numeric_only=True)
Idade         30.0
Salário    60000.0
dtype: float64

Os mais habituados com a biblioteca provavelmente têm o costume de aplicar um filtro selecionando a(s) coluna(s) desejada(s). Assim, poderíamos calcular a média apenas para a coluna Idade, por exemplo:

df["Idade"].mean()
30.0

Desta forma, fica explícito que a operação está atuando sobre uma coluna. Também poderíamos fazer como abaixo, para as duas colunas:

df[["Idade", "Salário"]].mean()
Idade         30.0
Salário    60000.0
dtype: float64

Evidentemente, poderíamos solicitar a média por linha com axis=1:

mean_axis_1 = df.mean(axis=1, numeric_only=True)
mean_axis_1
0    25012.5
1    30015.0
2    35017.5
dtype: float64

Mas qual seria o significado destes valores? Não consigo enxergar nenhum. Claro que podem haver contextos onde aplicar operações em linhas façam sentido, veremos alguns a seguir, mas a ideia é mostrar que, como o Pandas trabalha com a ideia de dados tabulares, as operações são pensadas primariamente para aplicação em colunas. Da mesma forma que lidamos com planilhas em programas como o Excel.

Pelo discutido, talvez o leitor já tenha inferido que não faz sentido axis=2 ou valores superiores no Pandas. Afinal, se sempre somos reduzidos a dados tabulares, teremos ou colunas (axis=0), ou linhas (axis=1). Porém, isto não significa que não podemos ter mais dimensões, só precisamos entender o que elas significam no contexto da biblioteca.

DataFrames com MultiIndex

Assim como na seção anterior transformamos nosso array bidimensional estudado com o NumPy em um dataframe, façamos o mesmo agora com nosso array tridimensional. A questão é como passar a ideia das dimensões. Para isso podemos trabalhar com dataframes com múltiplos índices.

index = pd.MultiIndex.from_product(
    [range(s) for s in arr_3d.shape], names=["dim1", "dim2", "dim3"]
)

df_multi = pd.DataFrame(
    {"dados": arr_3d.flatten()},
    index=index,
)

df_multi
dados
dim1 dim2 dim3
0 0 0 0
1 7
2 6
3 4
1 0 4
1 8
2 0
3 6
2 0 2
1 0
2 5
3 9
1 0 0 7
1 7
2 7
3 7
1 0 5
1 1
2 8
3 4
2 0 5
1 3
2 1
3 9

Vamos entender o que fizemos acima. Os valores de nosso array foram listados na coluna dados, mas foram hierarquizados em níveis com auxílio dos índices dim1, dim2, dim3. Veja como os índices criam a ideia de níveis, grupos, categorias, ou hierarquia dentro de nosso dataframe. Você verá estas palavras que listei com frequência no estudo de dataframes com múltiplos índices. Perceba, também, que, embora estejamos numa estrutura bidimensional, os índices agregam dimensões semânticas aos nossos dados. Isto ficará mais evidente nos próximos exemplos mas, antes, vejamos algumas consequências.

Como dito, os índices criam uma ideia de grupos em níveis. Assim, podemos usar o método groupby e agrupar por um determinado nível. E, com os dados agrupados, passar alguma função como, por exemplo, sum para somar os dados de um determinado nível:

df_multi.groupby(level="dim1").sum()
dados
dim1
0 51
1 64
df_multi.groupby(level="dim2").sum()
dados
dim2
0 45
1 36
2 34

E nada impede que sejam passados mais de um nível:

df_multi.groupby(level=["dim1", "dim2"]).sum()
dados
dim1 dim2
0 0 17
1 18
2 16
1 0 28
1 18
2 18

Talvez este exemplo com índices de nomes genéricos dim1, dim2, e dim3 esteja abstrato demais para ficar claro o potencial do que estamos fazendo aqui. Assim, vamos adicionar mais semântica ao exemplo, mantendo a base no nosso array tridimensional:

dates = pd.date_range("2022-01", periods=arr_3d.shape[0], freq="M", name="Data")
stores = ["Loja1", "Loja2", "Loja3"]
products = ["ProdutoA", "ProdutoB", "ProdutoC", "ProdutoD"]
multi_index = pd.MultiIndex.from_product(
    [dates, stores, products], names=["Data", "Loja", "Produto"]
)

df_vendas = pd.DataFrame({"Vendas": arr_3d.flatten()}, index=multi_index)

df_vendas
Vendas
Data Loja Produto
2022-01-31 Loja1 ProdutoA 0
ProdutoB 7
ProdutoC 6
ProdutoD 4
Loja2 ProdutoA 4
ProdutoB 8
ProdutoC 0
ProdutoD 6
Loja3 ProdutoA 2
ProdutoB 0
ProdutoC 5
ProdutoD 9
2022-02-28 Loja1 ProdutoA 7
ProdutoB 7
ProdutoC 7
ProdutoD 7
Loja2 ProdutoA 5
ProdutoB 1
ProdutoC 8
ProdutoD 4
Loja3 ProdutoA 5
ProdutoB 3
ProdutoC 1
ProdutoD 9

Os valores são exatamente os mesmos de nosso dataframe anterior. No entanto, agora vemos que há níveis para nossos dados: produto a que se referem, lojas destes produtos, mês a que os valores se referem. E os valores representam o total de unidades vendidas de cada produto.

Os agrupamentos agora vão ter muito mais significado para nós. Agrupando por data e solicitando a soma, teremos o total de unidades vendidas por mês:

df_vendas.groupby(level="Data").sum()
Vendas
Data
2022-01-31 51
2022-02-28 64

Podemos detalhar por mês e por loja:

df_vendas.groupby(level=["Data", "Loja"]).sum()
Vendas
Data Loja
2022-01-31 Loja1 17
Loja2 18
Loja3 16
2022-02-28 Loja1 28
Loja2 18
Loja3 18

Compare os resultados com o obtido na seção referente ao NumPy, quando a soma foi feita ao longo de axis=2.

Também podemos detalhar por mês e produto:

df_vendas.groupby(level=["Data", "Produto"]).sum()
Vendas
Data Produto
2022-01-31 ProdutoA 6
ProdutoB 15
ProdutoC 11
ProdutoD 19
2022-02-28 ProdutoA 17
ProdutoB 11
ProdutoC 16
ProdutoD 20

Compare os resultados com o obtido na seção referente ao NumPy, quando a soma foi feita ao longo de axis=1.

Conseguiu entender as comparações? Obtivemos os mesmos resultados numéricos, mas agora com um significado atrelado, dados pelos índices do Pandas. São os índices que agregam mais dimensões ao dataframe, sendo dimensões do ponto de vista semântico, o dataframe continua sendo uma estrutura tabular de linhas e colunas.

Podemos fazer o mesmo com nosso array quadridimensional criado na seção de NumPy:

dates = pd.date_range("2022-01", periods=arr_4d.shape[0], freq="M", name="Data")
stores = ["Loja1", "Loja2", "Loja3"]
products = ["ProdutoA", "ProdutoB", "ProdutoC", "ProdutoD"]
multi_index = pd.MultiIndex.from_product(
    [dates, stores, products], names=["Data", "Loja", "Produto"]
)

# Create a DataFrame from the 3D NumPy array and the MultiIndex
df_vendas_preco_unit = pd.DataFrame(
    arr_4d.reshape(-1, 2),
    index=multi_index,
    columns=["UnidadesVendidas", "PrecoUnidade"],
)

# Print the DataFrame
df_vendas_preco_unit
UnidadesVendidas PrecoUnidade
Data Loja Produto
2022-01-31 Loja1 ProdutoA 7 6
ProdutoB 4 8
ProdutoC 5 4
ProdutoD 4 2
Loja2 ProdutoA 0 5
ProdutoB 8 0
ProdutoC 8 8
ProdutoD 2 6
Loja3 ProdutoA 1 7
ProdutoB 7 3
ProdutoC 0 9
ProdutoD 4 8
2022-02-28 Loja1 ProdutoA 6 7
ProdutoB 7 1
ProdutoC 3 4
ProdutoD 4 0
Loja2 ProdutoA 5 1
ProdutoB 7 6
ProdutoC 9 7
ProdutoD 3 9
Loja3 ProdutoA 4 3
ProdutoB 9 3
ProdutoC 0 4
ProdutoD 7 1

Observe, agora, que temos duas colunas de valores. Uma indica a quantidade de unidades vendidas para cada produto, de cada loja, por cada mês. A outra, o preço de cada unidade de produto, para cada loja, a cada mês. Pense que é um relatório de uma média ou grande empresa, que possui lojas com os mesmos produtos, mas em regiões geográficas distintas. Assim, cada região pode ter preços distintos para um mesmo produto a depender das relações oferta/demanda oriundas da situação socioeconômica de cada região.

Neste contexto, faz sentido uma operação de multiplicação a cada linha, para obter a receita de venda, que é a multiplicação da quantidade de unidades vendidas pelo preço de cada unidade. O método prod nos possibilita realizar esta operação facilmente:

df_vendas_preco_unit.prod(axis=1)
Data        Loja   Produto 
2022-01-31  Loja1  ProdutoA    42
                   ProdutoB    32
                   ProdutoC    20
                   ProdutoD     8
            Loja2  ProdutoA     0
                   ProdutoB     0
                   ProdutoC    64
                   ProdutoD    12
            Loja3  ProdutoA     7
                   ProdutoB    21
                   ProdutoC     0
                   ProdutoD    32
2022-02-28  Loja1  ProdutoA    42
                   ProdutoB     7
                   ProdutoC    12
                   ProdutoD     0
            Loja2  ProdutoA     5
                   ProdutoB    42
                   ProdutoC    63
                   ProdutoD    27
            Loja3  ProdutoA    12
                   ProdutoB    27
                   ProdutoC     0
                   ProdutoD     7
dtype: int64

O retorno não é um dataframe, como podemos verificar com a função type:

type(_)
pandas.core.series.Series

O que obtivemos é uma série do Pandas. Já discutimos a diferença entre série e dataframe aqui no site anteriormente.

Podemos alocar esta série em uma coluna do nosso dataframe de início:

df_vendas_preco_unit["ReceitaTotal"] = df_vendas_preco_unit.prod(axis=1)
df_vendas_preco_unit
UnidadesVendidas PrecoUnidade ReceitaTotal
Data Loja Produto
2022-01-31 Loja1 ProdutoA 7 6 42
ProdutoB 4 8 32
ProdutoC 5 4 20
ProdutoD 4 2 8
Loja2 ProdutoA 0 5 0
ProdutoB 8 0 0
ProdutoC 8 8 64
ProdutoD 2 6 12
Loja3 ProdutoA 1 7 7
ProdutoB 7 3 21
ProdutoC 0 9 0
ProdutoD 4 8 32
2022-02-28 Loja1 ProdutoA 6 7 42
ProdutoB 7 1 7
ProdutoC 3 4 12
ProdutoD 4 0 0
Loja2 ProdutoA 5 1 5
ProdutoB 7 6 42
ProdutoC 9 7 63
ProdutoD 3 9 27
Loja3 ProdutoA 4 3 12
ProdutoB 9 3 27
ProdutoC 0 4 0
ProdutoD 7 1 7

Agora, temos em um coluna uma informação obtida por operações linha a linha. Estando em uma coluna, pode ser utilizada em agrupamentos. Por exemplo, total de unidades e de receita por mês:

df_vendas_preco_unit.groupby(level="Data")[["UnidadesVendidas", "ReceitaTotal"]].sum()
UnidadesVendidas ReceitaTotal
Data
2022-01-31 50 238
2022-02-28 64 244

Repare que não faz sentido incluir a coluna de preço de cada unidade, daí a seleção de colunas. O agrupamento também poderia ter sido feito por loja, considerando todo o período de tempo de nossa base de dados:

df_vendas_preco_unit.groupby(level="Loja")[["UnidadesVendidas", "ReceitaTotal"]].sum()
UnidadesVendidas ReceitaTotal
Loja
Loja1 40 163
Loja2 42 213
Loja3 32 106

Ou, por produto:

df_vendas_preco_unit.groupby(level="Produto")[
    ["UnidadesVendidas", "ReceitaTotal"]
].sum()
UnidadesVendidas ReceitaTotal
Produto
ProdutoA 23 108
ProdutoB 42 129
ProdutoC 25 159
ProdutoD 24 86

Ou, ainda, por loja a cada mês:

df_vendas_preco_unit.groupby(level=["Data", "Loja"])[
    ["UnidadesVendidas", "ReceitaTotal"]
].sum()
UnidadesVendidas ReceitaTotal
Data Loja
2022-01-31 Loja1 20 102
Loja2 18 76
Loja3 12 60
2022-02-28 Loja1 20 61
Loja2 24 137
Loja3 20 46

Por produto a cada mês:

df_vendas_preco_unit.groupby(level=["Data", "Produto"])[
    ["UnidadesVendidas", "ReceitaTotal"]
].sum()
UnidadesVendidas ReceitaTotal
Data Produto
2022-01-31 ProdutoA 8 49
ProdutoB 19 53
ProdutoC 13 84
ProdutoD 10 52
2022-02-28 ProdutoA 15 59
ProdutoB 23 76
ProdutoC 12 75
ProdutoD 14 34

O tipo de agrupamento vai depender do interesse, assim como a operação de agrupamento (soma, média, etc). O foco aqui é entender quando se usa axis=0 ou axis=1 e entender que, no Pandas, a ideia de dimensões superiores é dada pelos índices.

Conclusão

A compreensão do conceito de eixo (axis) é essencial para o trabalho com dados em bibliotecas como NumPy e Pandas. No NumPy, o eixo é usado para indicar a dimensão ao longo da qual uma operação deve ser aplicada em um array. Já no Pandas, que é uma biblioteca voltada para dados tabulares, o eixo é usado para operações em colunas ou linhas.

Vimos que o NumPy pode lidar com arrays de diferentes dimensões, desde 1D até 4D ou mais, enquanto o Pandas é voltado para dados tabulares 2D. No entanto, o Pandas permite criar índices com múltiplos níveis, o que permite organizar os dados em mais dimensões sem a necessidade de estruturas de dados complexas.

Esperamos que este artigo tenha ajudado a esclarecer as diferenças entre os conceitos de eixo no NumPy e Pandas e como eles podem ser aplicados no trabalho com dados em Python.

Deixem um comentário abaixo para compartilhar suas dúvidas ou sugestões, e não deixem de conferir outros artigos relacionados a dados aqui no Ciência Programada, vendo a categoria Data Science.

Até a próxima!

Compartilhe:

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima