Anteriormente, falei sobre o conceito dos testes unitários e sua importância no desenvolvimento de projetos de Machine Learning. Agora, vamos aprofundar ainda mais nesse tópico fundamental, desde a sua criação até a execução de testes unitários especificos para tarefas comuns de pré-processamento e engenharia de recursos (feature engineering). Para isso, será utilizado a biblioteca de testes do Python chamada pytest.
Como dito anteriormente, os testes unitários focam na verificação dos componentes individuais do código, visando garantir que cada parte do sistema funcione conforme o esperado quando isolada das outras partes do sistema. Alguns dos motivos de serem essenciais em projeto de MLOps:
Vamos começar com exemplos de testes unitários para tarefas de pré-processamento de dados. Supondo que tenha sido criado anteriormente a classe ‘DataPreprocessing’ que realiza operações como substituir valores ausentes e normalizar dados. Para testar essa classe, podemos criar um caso de teste com o pytest.
import pytest
import pandas as pd
from data_preprocessing import DataPreprocessing
def test_replace_question_marks():
"""
Testa a substituição do '?' por NaN.
"""
# Caso de teste: Substituicao de '?' por NaN
data = pd.DataFrame({'col1': ['?', 'x', 'y']})
dp = DataPreprocessing()
# Executa a função de pré-processamento
result = dp.replace_question_marks(data)
# Resultado esperado após o pré-processamento
expected = pd.DataFrame({'col1': [np.nan, 'x', 'y']})
# Verifica se o resultado é igual ao esperado
assert result.equals(expected), "Falha no teste test_replace_question_marks"
def test_normalize_data():
"""
Testa a normalização de dados.
"""
# Caso de teste: Normalização de dados
data = pd.DataFrame({'col1': [1,2,3,4,5]})
dp = DataPreprocessing()
# Executa a função de normalização
result = dp.normalize_data(data)
# Resultado esperado após a normalização
expected = pd.DataFrame({'col1': [0.0, 0.25, 0.5, 0.75, 1.0]})
# Verifica se o resultado é igual ao esperado
assert result.equals(expected), "Falha no teste test_normalize_data"
Agora, vamos explorar testes para engenharia de recursos (feature engineering). Supondo a classe ‘FeatureEngineering’ que realiza a codificação de recursos categóricos e escalonamento de recursos numéricos. Os testes ficariam dessa maneira:
import pytest
import pandas as pd
from feature_engineering import FeatureEngineering
def test_encode_categorical_features():
"""
Testa a codificação de recursos categóricos
"""
# Caso de teste: Codificação de recursos categóricos
data = pd.DataFrame({'col1': ['A', 'B', 'A', 'C']})
fe = FeatureEngineering()
# Executa a função de codificação categorica
result = fe.encode_categorical_features(data)
# Resultado esperado após a codificação
expected = pd.DataFrame({'col1': [0, 1, 0, 2]})
# Verifica se o resultado é igual ao esperado
assert result.equals(expected), "Falha no teste test_encode_categorical_features"
def test_scale_numeric_features():
"""
Testa o escalonamento
"""
# Caso de teste: Escalonamento de recursos numéricos
data = pd.DataFrame({'col1': [1, 2, 3, 4, 5]})
fe = FeatureEngineering()
# Executa o escalonamento
result = fe.scale_numeric_features(data)
# Resultado esperado após o escalonamento
expected = pd.DataFrame({'col1': [0.0, 0.25, 0.5, 0.75, 1.0]})
# Verifica se o resultado é igual ao esperado
assert result.equals(expected), "Falha no teste test_scale_numeric_features"
Como é possível visualizar nos códigos e comentários, basicamente é fornecida uma “prova” para a função responder e em seguida nós comparamos o resultado dela com o “gabarito”.
Além dos testes que verificam se a função retornou os valores corretos, existem outros tipos de testes unitários que pode ser úteis, como os testes que verificam a estrutura ou características especificas dos dados, como a quantidade de colunas, tipos de dados, valores padrão, entre outros.
Por exemplo, se você tem uma função que retorna um DataFrame após realizar um pré-processamento de dados, você pode querer criar testes unitários para verificar se:
import pandas as pd
# Função para verificar o número de colunas
def test_check_column_count():
# Criar um DataFrame de exemplo com 3 colunas
data = pd.DataFrame({'col1': [1, 2, 3],
'col2': ['A', 'B', 'C'],
'col3': [0.1, 0.2, 0.3]})
# Verificar se o DataFrame tem o número correto de colunas
assert len(data.columns) == 3
# Função para verificar os tipos de dados das colunas
def test_check_data_types():
# Criar um DataFrame de exemplo com tipos de dados especificos
data = pd.DataFrame({'int_col': [1, 2, 3],
'str_col': ['A', 'B', 'C'],
'float_col': [0.1, 0.2, 0.3]})
# Verificar se os tipos de dados das colunas correspondem ao esperado
assert data['int_col'].dtype == 'int64'
assert data['str_col'].dtype == 'object'
assert data['float_col'].dtype == 'float64'
Para executar esses testes, podemos utilizar o pytest. Basta navegar até o diretório raiz do projeto e executar o comando ‘pytest’. O pytest é uma ferramenta de teste que utiliza convenções para descobrir e executar testes em projetos Python. Para que o pytest identifique automaticamente seus casos de teste, você precisa seguir algumas convenções simples:
Ao seguir essas convenções, o pytest consegue identificar e executar seus testes sem a necessidade de configurações adicionais. Isso torna a criação e execução de testes em Python bastante conveniente e fácil de integrar ao fluxo de desenvolvimento.
Apronfundamos um pouco mais nos testes unitários, destacando a importância de testar operações e as convenções necessárias. Usamos a biblioteca pytest para criar casos de teste e garantir que o código funcione conforme o esperado. Testes unitários são uma parte fundamental do desenvolvimento de código robusto e confiável em projeto de Machine Learning.
A medida que o projeto evolui, a manutenção desses testes é tão importante quanto escrevê-los inicialmente. Eles ajudam o código a se manter confiável, facilitando a implementação de novos recursos e alterações. Além disso, é importante observar que a execução dos testes unitários é tão simples quanto executar um único comando na linha de comando usando o pytest, o que também pode ser incorporado ao fluxo de CI/CD. Dessa forma, toda vez que você enviar uma alteração ao repositório de código, os testes serão executados automaticamente, fornecendo uma camada adicional de garantia de qualidade no seu projeto.