Caso esteja se aventurando em ciência de dados, muito provavelmente já viu um reshape em algum código. Talvez com um misterioso valor de -1 em uma das posições como reshape(-1, 1). Mas, afinal, o que significa isto? Por que por vezes precisamos transformar nossos dados para utilizar métodos de modelos no Scikit-Learn? É o que veremos neste artigo.
Para tentar reproduzir uma situação real, vamos pegar uma base dados, armazená-la em um dataframe Pandas, tentar aplicar diretamente um modelo do Scikit-Learn e, então, utilizar o reshape para resolver os erros encontrados.
Tópicos
Importações e dataset de exemplo
Para começar, vamos importar os pacotes necessários ao nosso estudo e escolher uma base de dados (dataset) para utilizar de exemplo:
from sklearn import datasets
from sklearn.linear_model import LinearRegression
import numpy as np
import pandas as pd
A base de dados escolhida para esse artigo faz parte da coleção de datasets fornecida com o pacote Scikit-Learn, sendo um dataset de dados de pacientes com diabetes. Vamos obter as variáveis do dataset:
diabetes_X, diabetes_y = datasets.load_diabetes(return_X_y=True, as_frame=True)
Observe, nas células a seguir, as 5 primeiras entradas de cada variável atentando-se para a forma como são exibidos os resultados:
diabetes_X.head()
age | sex | bmi | bp | s1 | s2 | s3 | s4 | s5 | s6 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.038076 | 0.050680 | 0.061696 | 0.021872 | -0.044223 | -0.034821 | -0.043401 | -0.002592 | 0.019908 | -0.017646 |
1 | -0.001882 | -0.044642 | -0.051474 | -0.026328 | -0.008449 | -0.019163 | 0.074412 | -0.039493 | -0.068330 | -0.092204 |
2 | 0.085299 | 0.050680 | 0.044451 | -0.005671 | -0.045599 | -0.034194 | -0.032356 | -0.002592 | 0.002864 | -0.025930 |
3 | -0.089063 | -0.044642 | -0.011595 | -0.036656 | 0.012191 | 0.024991 | -0.036038 | 0.034309 | 0.022692 | -0.009362 |
4 | 0.005383 | -0.044642 | -0.036385 | 0.021872 | 0.003935 | 0.015596 | 0.008142 | -0.002592 | -0.031991 | -0.046641 |
diabetes_y.head()
0 151.0 1 75.0 2 141.0 3 206.0 4 135.0 Name: target, dtype: float64
Muito embora o objetivo do artigo não seja fazer um estudo detalhado da base de dados, vamos ao menos ter uma descrição do significado de cada coluna.
As características base estão na variável diabetes_X
e os valores alvo na variável diabetes_y
. Abaixo uma descrição tirada da documentação do Scikit-Learn:
age
– idade em anossex
– sexo da pessoabmi
– índice de massa corpórea (IMC)bp
– pressão sanguínea médias1
– tc, colesterol totals2
– ldl, lipoproteínas de baixa densidades3
– hdl, lipoproteínas de alta densidades4
– tch, colesterol total HDLs5
– ltg, log do nível de triglicerídeoss6
– glu, nível de açúcar no sangue- A variável alvo representa uma medida da progressão da doença após um ano de acompanhamento.
Diferença entre série e dataframe
O reshape
é um método do NumPy, que é uma das bibliotecas base do Pandas. Assim, vamos começar verificando as estruturas do Pandas para entendermos como são construídas sobre o NumPy.
Para compreender o significado do reshape
, precisamos entender o que é a dimensão de uma variável e como obter essa característica. E como ela se relaciona com o fato de a variável ser um dataframe ou uma série do Pandas.
Comecemos com as dimensões, com o atributo ndim
:
diabetes_X.ndim
2
diabetes_y.ndim
1
A dimensão 2 para nossos dados em diabetes_X
é fácil de entender: são as linhas e colunas. Já a dimensão 1 para diabetes_y
também é intuitivo, tendo em vista que vimos uma sequência de números, uma única dimensão.
O atributo shape
mostra o número de itens em cada dimensão:
diabetes_X.shape
(442, 10)
Ou seja, em diabetes_X
temos 442 linhas e 10 colunas.
diabetes_y.shape
(442,)
Em diabetes_y
temos 442 valores.
Vejamos o tipo de cada variável:
type(diabetes_X), type(diabetes_y)
(pandas.core.frame.DataFrame, pandas.core.series.Series)
Vemos que diabetes_X
é um DataFrame do Pandas, enquanto que diabetes_y
é uma Series (série). Aqui vale descrever um pouco melhor estas entidades do Pandas.
Pela documentação, um DataFrame é um estrutura bidimensional com colunas de tipos potencialmente diferentes. Já uma Series é um array (vetor) unidimensional capaz de conter qualquer tipo de dados. Ou seja, apenas pela documentação, já poderíamos saber as dimensões de nossas variáveis contanto que soubéssemos seus tipos.
Há, ainda, mais um atributo de interesse para nossas variáveis, o size
, que retorna quantos elementos há em nossa estrutura:
diabetes_X.size
4420
Como há 442 linhas e 10 colunas em diabetes_X
, há 442 x 10 = 4420 elementos. Pode-se fazer analogia com as células em uma planilha com tal quantidade de linhas e colunas.
diabetes_y.size
442
Para diabetes_y
o resultado é evidente, já que é a quantidade de valores de nosso array.
Criando séries e dataframes a partir de uma coluna
O que ocorre se pegarmos uma coluna de nosso dataframe? Qual será seu tipo?
bmi_series = diabetes_X["bmi"]
bmi_series.head()
0 0.061696 1 -0.051474 2 0.044451 3 -0.011595 4 -0.036385 Name: bmi, dtype: float64
type(bmi_series)
pandas.core.series.Series
Ou seja, um DataFrame pode ser pensado como um container de Series. Aliás, no link para a documentação colocado anteriormente, há exatamente esta afirmação ao final da definição de DataFrame.
Assim, ao solicitar o shape
teremos apenas um valor:
bmi_series.shape
(442,)
Agora, preste atenção na célula seguinte. Observe que colocamos um par de colchetes a mais ao redor do nome da coluna:
bmi_df = diabetes_X[["bmi"]]
bmi_df.head()
bmi | |
---|---|
0 | 0.061696 |
1 | -0.051474 |
2 | 0.044451 |
3 | -0.011595 |
4 | -0.036385 |
type(bmi_df)
pandas.core.frame.DataFrame
bmi_df.shape
(442, 1)
Veja que, apenas com essa alteração passamos a ter um DataFrame! Um DataFrame de uma única coluna, é verdade. Mas o objeto não é apenas um array de valores agora, é um DataFrame.
Para entender um pouco melhor, vamos olhar o que há por baixo do Pandas: a biblioteca NumPy.
O NumPy por baixo do Pandas
A estrutura básica do NumPy é a ndarray
, nome que significa array N-dimensional. Podemos pegar nossas variáveis anteriores, de tipos do Pandas, e solicitar que sejam expressas em seus tipos mais básicos, do NumPy, com o método to_numpy
:
diabetes_X_array = diabetes_X.to_numpy()
diabetes_X_array
array([[ 0.03807591, 0.05068012, 0.06169621, ..., -0.00259226, 0.01990842, -0.01764613], [-0.00188202, -0.04464164, -0.05147406, ..., -0.03949338, -0.06832974, -0.09220405], [ 0.08529891, 0.05068012, 0.04445121, ..., -0.00259226, 0.00286377, -0.02593034], ..., [ 0.04170844, 0.05068012, -0.01590626, ..., -0.01107952, -0.04687948, 0.01549073], [-0.04547248, -0.04464164, 0.03906215, ..., 0.02655962, 0.04452837, -0.02593034], [-0.04547248, -0.04464164, -0.0730303 , ..., -0.03949338, -0.00421986, 0.00306441]])
Com printoptions
, podemos visualizar de forma mais conveniente, deixando mais clara a estrutura de nossa nova variável:
with np.printoptions(precision=5, edgeitems=3):
display(diabetes_X_array)
array([[ 0.03808, 0.05068, 0.0617 , ..., -0.00259, 0.01991, -0.01765], [-0.00188, -0.04464, -0.05147, ..., -0.03949, -0.06833, -0.0922 ], [ 0.0853 , 0.05068, 0.04445, ..., -0.00259, 0.00286, -0.02593], ..., [ 0.04171, 0.05068, -0.01591, ..., -0.01108, -0.04688, 0.01549], [-0.04547, -0.04464, 0.03906, ..., 0.02656, 0.04453, -0.02593], [-0.04547, -0.04464, -0.07303, ..., -0.03949, -0.00422, 0.00306]])
Os atributos ndim
e shape
vistos anteriormente são, na realidade, herdados do NumPy:
type(diabetes_X_array)
numpy.ndarray
diabetes_X_array.ndim
2
diabetes_X_array.shape
(442, 10)
Veja, portanto, que a dimensão e o shape são derivados dos objetos NumPy mais básicos.
Podemos verificar o mesmo com nossa variável alvo:
diabetes_y_array = diabetes_y.to_numpy()
with np.printoptions(precision=5, edgeitems=3, threshold=10):
display(diabetes_y_array)
array([151., 75., 141., ..., 132., 220., 57.])
type(diabetes_y_array)
numpy.ndarray
diabetes_y_array.ndim
1
diabetes_y_array.shape
(442,)
Então já sabemos que o Pandas é uma extensão do NumPy. E o reshape? Onde entra nesta história?
As exigências de dimensão do Scikit-Learn
Muitos dos métodos do Scikit-Learn exigem que passemos nossas variáveis base como um array bidimensional. Vejamos isso no nosso exemplo.
Vamos nos recordar da Series que criamos a partir de uma das colunas de nosso DataFrame:
bmi_series = diabetes_X["bmi"]
bmi_series.head()
0 0.061696 1 -0.051474 2 0.044451 3 -0.011595 4 -0.036385 Name: bmi, dtype: float64
type(bmi_series)
pandas.core.series.Series
bmi_series.shape
(442,)
E, também, de nossa variável alvo:
diabetes_y.head()
0 151.0 1 75.0 2 141.0 3 206.0 4 135.0 Name: target, dtype: float64
diabetes_y.ndim
1
diabetes_y.shape
(442,)
Observação: O foco do artigo não é a ciência de dados em si, mas compreender o reshape. Logo, os procedimentos a seguir são para ir direto ao ponto. Em um projeto real, deve-se fazer separação entre dados de treino e de teste, além de uma boa análise exploratória antes de sair aplicando modelos. Aqui pularemos esta parte por não ser nosso foco.
Podemos tentar verificar o quanto o comportamento da variável alvo é linear com relação a Series “bmi”:
lr = LinearRegression()
lr.fit(bmi_series, diabetes_y)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) ValueError: Expected 2D array, got 1D array instead: Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
Veja que ocorre um erro, citando que Expected 2D array, got 1D array instead
, o que já é um indício que não se espera uma Series por ser unidimensional.
A última linha da mensagem é:
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
Vamos tentar seguir a instrução:
bmi_series.reshape(-1, 1)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) AttributeError: 'Series' object has no attribute 'reshape'
Observe que o tipo Series não possui o método reshape
. Mas, como vimos, os tipos do Pandas são construídos em cima dos tipos do NumPy. E o reshape
é um método de arrays do NumPy. Logo, podemos fazer:
bmi_series_numpy_reshape = bmi_series.to_numpy().reshape(-1, 1)
with np.printoptions(precision=5, edgeitems=3, threshold=10):
display(bmi_series_numpy_reshape)
array([[ 0.0617 ], [-0.05147], [ 0.04445], ..., [-0.01591], [ 0.03906], [-0.07303]])
bmi_series_numpy_reshape.ndim
2
lr.fit(bmi_series_numpy_reshape, diabetes_y)
LinearRegression()
lr.coef_, lr.intercept_, lr.score(bmi_series_numpy_reshape, diabetes_y)
(array([949.43526038]), 152.1334841628967, 0.3439237602253803)
Pronto, conseguimos fazer nossa regressão. Mas ainda não sabemos exatamente o que o reshape
faz.
Além disso, não seria muito prático, nem Pythonico, ficar recorrendo aos tipos básicos por baixo do Pandas. Há uma forma melhor de se abordar a questão.
A forma recomendável de fazer
Como vimos, utilizando dois pares de colchetes, obtemos diretamente um DataFrame, que já é um tipo bimensional:
bmi_df = diabetes_X[["bmi"]]
type(bmi_df)
pandas.core.frame.DataFrame
bmi_df.shape
(442, 1)
bmi_df.ndim
2
Agora, podemos passar diretamente para o método fit
:
lr.fit(bmi_df, diabetes_y)
LinearRegression()
lr.coef_, lr.intercept_, lr.score(bmi_df, diabetes_y)
(array([949.43526038]), 152.1334841628967, 0.3439237602253803)
Obtemos o mesmo resultado de antes, mas com muito menos trabalho. E, atenção, sem nem ao menos precisar do reshape
! Sim, muitos códigos utilizam tal método desnecessariamente.
Ótimo! Mas o que exatamente é o reshape
e o que significa os valores passados ao método?
Entendendo o significado de (-1, 1) no reshape
Vamos criar um array unidimensional e conferir seu atributo shape
:
array_1D = np.array((1, 2, 3))
array_1D
array([1, 2, 3])
array_1D.shape
(3,)
Podemos mudar o shape
deste array de forma explícita com o reshape
passando o número de linhas e colunas:
array_2D = array_1D.reshape(3, 1)
array_2D
array([[1], [2], [3]])
array_2D.shape
(3, 1)
array_2D.ndim
2
Agora, o mesmo pode ser feito passando um dos valores como -1. Tal valor simplesmente significa que o NumPy vai completar com base no total de elementos do array e no tamanho das demais dimensões. Ou seja, se já passarmos que uma das dimensões tem tamanho 1 e há 3 elementos, a dimensão restante só pode ter tamanho 3:
array_1D.reshape(-1, 1)
array([[1], [2], [3]])
Da mesma forma, se uma dimensão tem tamanho 3, a outra só pode ter tamanho 1:
array_1D.reshape(3, -1)
array([[1], [2], [3]])
Assim, a grande vantagem de se utilizar uma das dimensões como -1 é que o código fica dinâmico. Você não precisa saber o total de elementos do array, basta dizer qual o tamanho de uma das dimensões que o reshape
faz o resto do trabalho. Isto é muito conveniente para o exemplo do Scikit-Learn, já que só queremos acrescentar uma segunda dimensão, independentemente do tamanho de nossa base de dados.
O reshape
apresentará um erro caso as dimensões passadas não façam sentido:
array_1D.reshape(2, 1)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) /tmp/ipykernel_471207/2649632558.py in <module> ----> 1 array_1D.reshape(2, 1) ValueError: cannot reshape array of size 3 into shape (2,1)
Vamos pegar mais um exemplo, criando um array de 2 linhas e 4 colunas:
array = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
array
array([[1, 2, 3, 4], [5, 6, 7, 8]])
array.shape
(2, 4)
Com o que vimos, fica fácil pedir para que o reshape
transforme em um array com duas colunas:
array.reshape(-1, 2)
array([[1, 2], [3, 4], [5, 6], [7, 8]])
Veja que o reshape
, corretamente, interpretou como 4 o valor de -1 passado.
Como nosso array possui 8 elementos, podemos ter um array de 3 dimensões, com 2 elementos em cada. Abaixo, o -1 é interpretado como 2:
array_3D = array.reshape((-1, 2, 2))
array_3D
array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
Precisamos de três índices para pegar um determinado elemento. Por exemplo, o primeiro elemento do array:
array_3D[0][0][0]
1
Outros métodos interessantes do NumPy
Por fim, vamos nos lembrar de um dos arrays que criamos anteriormente:
array_2D
array([[1], [2], [3]])
Este array foi criado pela transformação de uma array unidimensional em um bidimensional, adicionando uma dimensão de tamanho 1. Podemos remover dimensões de tamanho 1 com o método squeeze:
array_2D.squeeze()
array([1, 2, 3])
Lembrando do nosso array de 8 elementos:
array
array([[1, 2, 3, 4], [5, 6, 7, 8]])
Podemos transformá-lo em um array monodimensional de duas formas:
array_flatten = array.flatten()
array_flatten
array([1, 2, 3, 4, 5, 6, 7, 8])
array_ravel = array.ravel()
array_ravel
array([1, 2, 3, 4, 5, 6, 7, 8])
No entanto, estas formas não são equivalentes. De forma resumida:
ravel | flatten |
---|---|
retorna apenas uma view (referência) do array original | retorna uma cópia do array original |
modificar o array resultante altera o original | modificar o array resultante não altera o original |
mais rápido que flatten | mais lento por alocar espaço em memória |
função a nivel de biblioteca | método do objeto ndarray |
Como verificar se um array é cópia de outro? Podemos usar o atributo base
. Se o retorno for None
, temos ou o array original ou uma cópia; caso contrário, o retorno é o array que serviu de base, de referência:
print(array.base)
print(array_flatten.base)
print(array_ravel.base)
None None [[1 2 3 4] [5 6 7 8]]
Como esperado, apenas o ravel
retornou um array, justamente o array que serviu de base a ele, mostrando ser o array ravel apenas uma view de seu array base.
Conclusão
E aí? Entendeu o tal reshape
? Vimos não apenas o que ele significa com exemplos da biblioteca de origem NumPy, mas mostramos a relação com o Pandas e com modelos do Scikit-Learn. Agora não tem mais como ficar com dúvidas quando encontrar este método em algum código.
Qual outro método você gostaria de ver detalhado aqui no site? Deixe nos comentários.
Para mais artigos da parte de ciência de dados, veja a categoria Data Science.
Até a próxima!