DEV Community

Cover image for Teste em Dados - Básico
Walter R P Cortes
Walter R P Cortes

Posted on • Originally published at blog.wvcode.com.br on

Teste em Dados - Básico

Olá Devs!

Quando trabalhamos com dados, é necessário que, além de desenvolver todas as transformações necessárias para que os dados estejam prontos para serem utilizados na execução de análises, tenhamos uma maneira de validar a correção e validade dos dados segundo as regras que foram determinadas.

Uma maneira automatizada e eficiente de fazer isso é através da criação de testes unitários que validem os dados de acordo com as regras estabelecidas.

Vamos começar???

Obtendo os Dados

Para este artigo, vamos carregar dados que apresentam alguns erros e vamos construir os testes unitários para valida-los.

import pandas as pd
df = pd.read_csv('https://media.githubusercontent.com/media/labeduc/datasets/main/testes/problematic_data.csv')

Aqui podemos ver uma amostra dos dados:

df.sample(5)
Unnamed: 0 ID Name Age Salary Join_Date Category
16 16 17 Name17 56 4700 2023-05-31 Category C
12 12 13 Name13 74 4300 2023-01-31 Category B
42 42 43 Name43 74 7300 2025-07-31 Category A
13 13 14 Name14 35 4400 2023-02-28 Category B
17 17 18 Name18 35 4800 2023-06-30 Category A

Para iniciar o nosso processo de validação, precisamos realizar a primeira inspeção nos dados. Para isso, a biblioteca Pandas nos dá algumas funções bem interessantes.

# A função info() exibe informações sobre o DataFrame, 
# incluindo o tipo de dados de cada coluna, 
# valores não nulos e uso de memória.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 7 columns):
 # Column Non-Null Count Dtype 
--- ------ -------------- ----- 
 0 Unnamed: 0 50 non-null int64 
 1 ID 50 non-null int64 
 2 Name 40 non-null object
 3 Age 50 non-null object
 4 Salary 50 non-null int64 
 5 Join_Date 50 non-null object
 6 Category 50 non-null object
dtypes: int64(3), object(4)
memory usage: 2.9+ KB

Podemos perceber 2 situações que demandam maior verificação:

  • A coluna Name tem 10 valores nulos, o que pode ser um problema para as regra de negócio;
  • A coluna Age (idade) tem todas as linhas preenchidas, mas seu tipo, em vez de ser um valor do tipo inteiro, é do tipo objeto, o que infica possível problema nos dados.

O que a função info não nos mostra é a presença de valores duplicados, ou até mesmo uma linha inteira duplicada. Vamos então aprender como conseguir essas informações.

Tipos de Validação

Dataset está vazio

A proprieadade empty do DataFrame nos informa se o DataFrame está vazio ou não. Se o DataFrame estiver vazio, a propriedade retornará True, caso contrário, retornará False.

vazio = df.empty

print(f"{'' if vazio else 'Não'} está vazio")
Não está vazio

Coluna está vazia

A propriedade empty do DataFrame nos informa se a coluna está vazia ou não. Se a coluna estiver vazia, a propriedade retornará True, caso contrário, retornará False.

vazio = df['Name'].empty

print(f" Coluna Name {'' if vazio else 'Não'} está vazia")
 Coluna Name Não está vazia

Verificando Valores Nulos

Este teste visa descobrir se existem dados faltando em nosso dataset. Podemos testar de maneira geral ou coluna a coluna. Para isso, utilizamos a função isnull() que retorna True para valores nulos e False para valores não nulos.

# Testando se existe algum valor nulo
valores_nulos = df.isnull().values.any()

print(f"{'' if valores_nulos else 'Náo'} Existem Valores Nulos.")
 Existem Valores Nulos.
# O teste pode ser feito para uma coluna específica
valores_nulos = df['Name'].isnull().values.any()

print(f"A coluna Name {'tem' if valores_nulos else 'não tem'} valores nulos.")
A coluna Name tem valores nulos.
# O teste pode ser feito para uma coluna específica
valores_nulos = df['Age'].isnull().values.any()

print(f"A coluna Age {'tem' if valores_nulos else 'não tem'} valores nulos.")
A coluna Age não tem valores nulos.

Verificando os tipos de dados

Este teste visa verificar se o tipo de dados de uma coluna em todas as linhas do seu dataset é consistente com o objetivo de uso desta coluna. Por exemplo, vimos nos exemplos de dados, que a coluna Age está identificada com o tipo de dados objeto, o que certamente nos causará problemas se quisermos calcular a média de idade de nosso dataset, pois é um indicativo de que em alguma linha desta coluna, o valor não é numérico. Podemos fazer uma inspeção manual, já que estamos falando apenas de 50 linhas, mas vamos aprender a fazer isso de maneira automatizada.

# Testando se a coluna Age é do tipo numérico
# A função isna() retorna um DataFrame de valores booleanos que indicam se um elemento é um número ou não.
eh_numero = df['Age'].isna().values.any()

print(f"A coluna Age {'é' if eh_numero else 'não é'} do tipo numérico.")
A coluna Age não é do tipo numérico.

Mas que valor é este? Vamos usar outra função para descobrir.

# A função unique() retorna uma matriz de valores exclusivos em uma coluna. 
# A função tolist() converte a matriz em uma lista.
print(f" Valores únicos de Age: {df['Age'].unique().tolist()}")
 Valores únicos de Age: ['46', '19', '30', '60', '69', '36', '64', '48', '53', '52', '32', '74', '35', '56', '49', 'Unknown', '57', '44', '54', '28', '41', '39', '62', '21', '71', '42', '38', '22', '59', '55']

Ao usar a função unique(), podemos descobrir quais são os valores únicos de uma coluna. Se a coluna tiver um tipo de dados numérico, a função retornará uma lista de valores únicos. Se a coluna tiver um tipo de dados não numérico, a função retornará uma lista de strings. Existem uma ou mais linhas com o valor ‘Unknown’ na coluna Age, o que causa o comportamento que vimos anteriormente. Esse é mais um dos problemas a serem corrigidos, que o nosso teste unitário vai nos ajudar a identificar.

Outros Tipos de Validação

Veja abaixo alguns outros tipos de validação comum em testes de dados

Teste contra valores

Neste tipo de teste, verificamos se as colunas do nosso dataset respeitam, por exemplo, valores mínimos, máximos, conjunto especificos e limitados de opções, se obedecem a uma lógica dependente de outras colunas, etc.

Apenas maiores de 40 anos

# Primeiro vamos corrigir os unknown

df['Age'] = (df['Age'].apply(lambda x: 40 if x == 'Unknown' else x)).astype(int)


# Agora fazemos a validação

menores_de_quarenta = df.query('Age < 40').empty

print(f"{'Não Temos' if menores_de_quarenta else 'Temos'} menores de quarenta")
Temos menores de quarenta

Verifica contra Lista de Valores

#
categoria_invalida = (df['Category'].apply(lambda x: x not in ['Category A', 'Category B', 'Category C'])).empty

print(f"{'Não tem' if categoria_invalida else 'Tem'} categorias invalidas.")
Tem categorias invalidas.

EDA

EDA é a sigla para Exploratory Data Analysis, que em português significa Análise Exploratória de Dados. Este tipo de teste visa verificar se os dados estão de acordo com o esperado, ou seja, se estão dentro de um intervalo esperado, se não há outliers, se a distribuição dos dados está correta, etc. Ou seja, é uma análise mais aprofundada dos dados, que fazem validações mais complexas e de cunho estatístico.

Para nos ajudar com essa análise, podemos utilizar a função describe() do Pandas, que nos dá um resumo estatístico dos dados.

df.describe()
Unnamed: 0 ID Age Salary
count 50.00000 50.00000 50.000000 50.000000
mean 24.50000 25.50000 46.320000 5310.000000
std 14.57738 14.57738 14.618188 1653.351574
min 0.00000 1.00000 19.000000 2000.000000
25% 12.25000 13.25000 36.500000 4025.000000
50% 24.50000 25.50000 46.000000 5350.000000
75% 36.75000 37.75000 56.000000 6675.000000
max 49.00000 50.00000 74.000000 8000.000000

Como podemos ver, a função describe() nos dá um resumo estatístico dos dados numéricos, como a média, desvio padrão, mínimo, máximo, etc. Com essas informações, podemos fazer validações mais complexas, como verificar se a média de idade está dentro de um intervalo esperado, se a distribuição dos dados está correta, etc. Mas, como podemos ver, a coluna Age não está sendo considerada como numérica, o que nos impede de fazer essas validações. Vamos corrigir isso.

# A correção aplicada foi a substituição dos valores 'Unknown' por 40 e a conversão para inteiro.
# Por que 40? Porque é um valor que não altera a média e a mediana dos dados.

df['Age'] = df['Age'].apply(lambda x: 40 if x == 'Unknown' else x).astype(int)
df.describe()
Unnamed: 0 ID Age Salary
count 50.00000 50.00000 50.000000 50.000000
mean 24.50000 25.50000 46.320000 5310.000000
std 14.57738 14.57738 14.618188 1653.351574
min 0.00000 1.00000 19.000000 2000.000000
25% 12.25000 13.25000 36.500000 4025.000000
50% 24.50000 25.50000 46.000000 5350.000000
75% 36.75000 37.75000 56.000000 6675.000000
max 49.00000 50.00000 74.000000 8000.000000

Bom, agora que temos isso resolvido, vamos ao próximo passo: rodar o EDA. O EDA pode ser feito manualmente, mas vamos aprender a fazer isso de maneira automatizada. Para essa análise mais automatizada, vamos usar três ferramentas: jupyter-summarytools, sweetviz e dtale.

Jupyter-summarytools

É a versão mais bonita do describe(). Ele nos dá um resumo estatístico dos dados, mas de uma maneira mais visual e interativa. Para instalar, basta rodar o comando !pip install jupyter-summarytools no seu Jupyter Notebook.

from summarytools import dfSummary

dfSummary(df)

T_e3ff2 thead>tr>th {

text-align: left;
}

T_e3ff2_row0_col0, #T_e3ff2_row1_col0, #T_e3ff2_row2_col0, #T_e3ff2_row3_col0, #T_e3ff2_row4_col0, #T_e3ff2_row5_col0, #T_e3ff2_row6_col0 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 5%;
max-width: 50px;
min-width: 20px;
}

T_e3ff2_row0_col1, #T_e3ff2_row1_col1, #T_e3ff2_row2_col1, #T_e3ff2_row3_col1, #T_e3ff2_row4_col1, #T_e3ff2_row5_col1, #T_e3ff2_row6_col1 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 15%;
max-width: 200px;
min-width: 100px;
word-break: break-word;
}

T_e3ff2_row0_col2, #T_e3ff2_row1_col2, #T_e3ff2_row2_col2, #T_e3ff2_row3_col2, #T_e3ff2_row4_col2, #T_e3ff2_row5_col2, #T_e3ff2_row6_col2 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 30%;
min-width: 100px;
}

T_e3ff2_row0_col3, #T_e3ff2_row1_col3, #T_e3ff2_row2_col3, #T_e3ff2_row3_col3, #T_e3ff2_row4_col3, #T_e3ff2_row5_col3, #T_e3ff2_row6_col3 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 25%;
min-width: 100px;
}

T_e3ff2_row0_col4, #T_e3ff2_row1_col4, #T_e3ff2_row2_col4, #T_e3ff2_row3_col4, #T_e3ff2_row4_col4, #T_e3ff2_row5_col4, #T_e3ff2_row6_col4 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 20%;
min-width: 150px;
}

T_e3ff2_row0_col5, #T_e3ff2_row1_col5, #T_e3ff2_row2_col5, #T_e3ff2_row3_col5, #T_e3ff2_row4_col5, #T_e3ff2_row5_col5, #T_e3ff2_row6_col5 {

text-align: left;
font-size: 12px;
vertical-align: middle;
width: 10%;
}

Table 1: Data Frame Summary
df
Dimensions: 50 x 7
Duplicates: 0

No Variable Stats / Values Freqs / (% of Valid) Graph Missing
1 Unnamed: 0
[int64]
Mean (sd) : 24.5 (14.6)
min < med < max:
0.0 < 24.5 < 49.0
IQR (CV) : 24.5 (1.7)
50 distinct values 0
(0.0%)
2 ID
[int64]
Mean (sd) : 25.5 (14.6)
min < med < max:
1.0 < 25.5 < 50.0
IQR (CV) : 24.5 (1.7)
50 distinct values 0
(0.0%)
3 Name
[object]
1. nan
2. Name1
3. Name38
4. Name28
5. Name29
6. Name31
7. Name32
8. Name33
9. Name34
10. Name36
11. other
10 (20.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
31 (62.0%)
10
(20.0%)
4 Age
[int64]
Mean (sd) : 46.3 (14.6)
min < med < max:
19.0 < 46.0 < 74.0
IQR (CV) : 19.5 (3.2)
30 distinct values 0
(0.0%)
5 Salary
[int64]
Mean (sd) : 5310.0 (1653.4)
min < med < max:
2000.0 < 5350.0 < 8000.0
IQR (CV) : 2650.0 (3.2)
48 distinct values 0
(0.0%)
6 Join_Date
[object]
1. 2022-01-31
2. 2025-02-28
3. 2024-04-30
4. 2024-05-31
5. 2024-06-30
6. 2024-07-31
7. 2024-08-31
8. 2024-09-30
9. 2024-10-31
10. 2024-11-30
11. other
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
1 (2.0%)
40 (80.0%)
0
(0.0%)
7 Category
[object]
1. Category A
2. Category B
3. Category C
4. No Category
19 (38.0%)
15 (30.0%)
13 (26.0%)
3 (6.0%)
0
(0.0%)

Sweetviz

O Sweetviz é uma ferramenta que nos dá um relatório completo dos dados, com gráficos e tabelas que nos ajudam a entender melhor os dados. Para instalar, basta rodar o comando !pip install sweetviz no seu Jupyter Notebook. Ele é muito fácil de usar, basta rodar o comando sweetviz.analyze([seu_dataframe]) e ele vai gerar um relatório completo dos seus dados.

import sweetviz as sv

my_report = sv.analyze(df)

# Exibindo o relatório no próprio notebook
# Existem outras opções de saída, como HTML e JSON.
my_report.show_notebook()

D-Tale

O D-Tale é uma ferramenta que nos dá um relatório completo dos dados, com gráficos e tabelas que nos ajudam a entender melhor os dados. Para instalar, basta rodar o comando !pip install dtale no seu Jupyter Notebook. Ele é muito fácil de usar, basta rodar o comando dtale.show([seu_dataframe]) e ele vai gerar um relatório completo dos seus dados.

import dtale

import dtale.app as dtale_app

dtale_app.USE_COLAB = True

dtale.show(df)

Infelizmente, não podemos ver o resultado aqui, mas você pode rodar no seu Jupyter Notebook ou Google Colab e ver o resultado.


Criando os testes unitários

Agora que sabemos sobre alguns dos tipos de testes que podemos aplicar aos nossos dados, vamos aprender como organizar isso de uma forma prática.

A idéia é englobar os testes aprendidos em funções que podem ser chamadas a qualquer momento, assim a cada alteração que fazemos no dataset, podemos validar o mesmo.

Em primeiro lugar, englobamos os testes que fizemos em funções.

def teste_nulos(data_frame, coluna=None):
 """Verifica se o DataFrame ou uma Coluna específica possui valores nulos.

 Returns:
 True se houver valores nulos, False caso contrário.
 """
 if coluna is None:
 return data_frame.isnull().values.any()
 else:
 return data_frame[coluna].isnull().values.any()


def teste_eh_numero(data_frame, coluna):
 """Verifica se os valores de uma coluna são numéricos.

 Returns:
 True se algum dos valores não é numérico, False caso contrário.
 """
 from pandas.api.types import is_numeric_dtype

 return is_numeric_dtype(data_frame[coluna])


def teste_vazio(data_frame, coluna=None):
 """Verifica se o DataFrame ou uma Coluna específica está vazio.

 Returns:
 True se estiver vazio, False caso contrário.
 """
 if coluna is None:
 return data_frame.empty
 else:
 return data_frame[coluna].empty


def teste_condicional(data_frame, condicao):
 """Verifica se o DataFrame atende a uma condição.

 Returns:
 True se atender a condicão, False caso contrário.
 """
 result = data_frame.query(condicao)
 return not(result.empty)


def teste_valores(data_frame, coluna, valores):
 """Verifica se os valores de uma coluna estão contidos em uma lista.

 Returns:
 True se estiver na lista, False caso contrário.
 """
 result = data_frame[coluna].apply(lambda x: x in valores).any()
 return result

A próxima etapa é criar uma função que irá chamar todas essas funções utilizando o comando assert

O comando assert é utilizado para verificar se uma expressão é verdadeira. Se a expressão for verdadeira, o programa continua a execução normalmente. Se a expressão for falsa, o programa lança uma exceção do tipo AssertionError.

def run_unit_test(data_frame):
 try:
 assert teste_nulos(data_frame) == False, 'Existem valores nulos'
 assert teste_nulos(data_frame, 'Name') == False, 'Existem valores nulos na coluna Name'
 assert teste_eh_numero(data_frame, 'Age'), 'A coluna Age não é do tipo numérico'
 assert teste_vazio(data_frame) == False, 'O data_frame está vazio'
 assert teste_condicional(data_frame, 'Age < 40') == True, 'Não tem menores de quarenta'
 assert teste_valores(data_frame, 'Category', ['Category A', 'Category B', 'Category C']) == True, 'Categoria Invalida'
 print('Testes finalizados com sucesso.')
 except AssertionError as e:
 print(e)

Tendo criado a função, agora só resta executa-la, observar as falhas, aplicar as correções e rodar os testes unitários novamente, até que todos passem.

1a Execução

run_unit_test(df)
Existem valores nulos

Para determinar isso, podemos apenas chamar a função info() do dataframe, que nos dá informações sobre o dataset, como o número de linhas, colunas, tipos de dados, etc.

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 7 columns):
 # Column Non-Null Count Dtype 
--- ------ -------------- ----- 
 0 Unnamed: 0 50 non-null int64 
 1 ID 50 non-null int64 
 2 Name 40 non-null object
 3 Age 50 non-null int64 
 4 Salary 50 non-null int64 
 5 Join_Date 50 non-null object
 6 Category 50 non-null object
dtypes: int64(4), object(3)
memory usage: 2.9+ KB

O campo Name está com problema. Vamos corrigir isso.

df['Name'] = df['Name'].fillna('Desconhecido')

Agora, vamos para a segunda execução.

2a Execução

run_unit_test(df)
Testes finalizados com sucesso.

Agora, é a coluna Age que está com problema. Vamos corrigir isso.

df['Age'] = pd.to_numeric(df['Age'])

Vamos para a 3a execução.

3a Execução

run_unit_test(df)
Testes finalizados com sucesso.

Vamos ver os valores possíveis do campo Category.

df['Category'].unique().tolist()
['Category B', 'Category C', 'Category A', 'No Category']

Temos um No Category ali que está fazendo o teste falhar. Vamos corrigir isso. Mas qual seria a melhor correção? Trocar o valor por um dos válidos ou corrigir o teste? A resposta dependerá do contexto do negócio. Aqui, vamos assumir que corrigir o teste é a melhor alternativa.

def run_unit_test(data_frame):
 try:
 assert teste_nulos(data_frame) == False, "Existem valores nulos"
 assert (
 teste_nulos(data_frame, "Name") == False
 ), "Existem valores nulos na coluna Name"
 assert teste_eh_numero(data_frame, "Age"), "A coluna Age não é do tipo numérico"
 assert teste_vazio(data_frame) == False, "O data_frame está vazio"
 assert (
 teste_condicional(data_frame, "Age < 40") == True
 ), "Não tem menores de quarenta"
 assert (
 teste_valores(
 data_frame, "Category", ["Category A", "Category B", "Category C", "No Category"]
 )
 == True
 ), "Categoria Invalida"
 print("Testes finalizados com sucesso.")
 except AssertionError as e:
 print(e)

4a Execução

run_unit_test(df)
Testes finalizados com sucesso.

Agora sim, finalizamos o nosso processo de testar os dados. Agora, temos um dataset que está de acordo com as regras de negócio e podemos utilizá-lo para fazer análises.

Conclusão

Neste artigo, aprendemos como fazer testes unitários em dados utilizando a biblioteca Pandas. Vimos que é possível fazer testes simples, como verificar se o dataset está vazio, se uma coluna está vazia, se existem valores nulos, se os tipos de dados estão corretos, etc. Também vimos que é possível fazer testes mais complexos, como verificar se os valores de uma coluna estão dentro de um intervalo esperado, se obedecem a uma lógica dependente de outras colunas, etc.

Aprendemos também como organizar esses testes em funções e como criar uma função que chama todas essas funções e verifica se os testes passaram ou não. Com isso, podemos garantir que os dados estão de acordo com as regras de negócio e que podemos utilizá-los para fazer análises.

Mas é importante lembrar que os testes unitários não são a única forma de garantir a qualidade dos dados. É importante também fazer uma análise exploratória dos dados, verificar se os dados estão de acordo com o esperado, se não há outliers, se a distribuição dos dados está correta, etc. E, é claro, é importante também fazer validações manuais, para garantir que os dados estão corretos.

Um abraço e até a próxima,

Walter.

Top comments (0)