数据科学家的Pytest

做者|Khuyen Tran
编译|VK
来源|Towards Datas Sciencepython

动机

应用不一样的python代码来处理notebook中的数据是颇有趣的,可是为了使代码具备可复制性,你须要将它们放入函数和类中。将代码放入脚本时,代码可能会因某些函数而中断。那么,如何检查你的功能是否如你所指望的那样工做呢?linux

例如,咱们使用TextBlob建立一个函数来提取文本的情感,TextBlob是一个用于处理文本数据的Python库。咱们但愿确保它像咱们预期的那样工做:若是测试为积极,函数返回一个大于0的值;若是文本为消极,则返回一个小于0的值。git

from textblob import TextBlob

def extract_sentiment(text: str):
        '''使用textblob提取情绪。
        	在范围[- 1,1]内'''

        text = TextBlob(text)

        return text.sentiment.polarity

要知道函数是否每次都会返回正确的值,最好的方法是将这些函数应用于不一样的示例,看看它是否会产生咱们想要的结果。这就是测试的重要性。github

通常来讲,你应该在数据科学项目中使用测试,由于它容许你:算法

  • 确保代码按预期工做bash

  • 检测边缘状况session

  • 有信心用改进的代码交换现有代码,而没必要担忧破坏整个管道app

有许多Python工具可用于测试,但最简单的工具是Pytest。框架

Pytest入门

Pytest是一个框架,它使得用Python编写小测试变得容易。我喜欢pytest,由于它能够帮助我用最少的代码编写测试。若是你不熟悉测试,那么pytest是一个很好的入门工具。机器学习

要安装pytest,请运行

pip install -U pytest

要测试上面所示的函数,咱们能够简单地建立一个函数,该函数以test_开头,后面跟着咱们要测试的函数的名称,即extract_sentiment

#sentiment.py
def extract_sentiment(text: str):
        '''使用textblob提取情绪。
        	在范围[- 1,1]内'''

        text = TextBlob(text)

        return text.sentiment.polarity

def test_extract_sentiment():

    text = "I think today will be a great day"

    sentiment = extract_sentiment(text)

    assert sentiment > 0

在测试函数中,咱们将函数extract_sentiment应用于示例文本:“I think today will be a great day”。咱们使用assert sentiment > 0来确保情绪是积极的。

就这样!如今咱们准备好运行测试了。

若是咱们的脚本名是sentiment.py,咱们能够运行

pytest sentiment.py

Pytest将遍历咱们的脚本并运行以test开头的函数。上面的测试输出以下所示

========================================= test session starts ==========================================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1

collected 1 item
process.py .                                                                                     [100%]

========================================== 1 passed in 0.68s ===========================================

很酷!咱们不须要指定要测试哪一个函数。只要函数名以test开头,pytest就会检测并执行该函数!咱们甚至不须要导入pytest就能够运行pytest

若是测试失败,pytest会产生什么输出?

#sentiment.py

def test_extract_sentiment():

    text = "I think today will be a great day"

    sentiment = extract_sentiment(text)

    assert sentiment < 0
>>> pytest sentiment.py

========================================= test session starts ==========================================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
collected 1 item

process.py F                                                                                     [100%]
=============================================== FAILURES ===============================================
________________________________________ test_extract_sentiment ________________________________________

def test_extract_sentiment():
    
        text = "I think today will be a great day"
    
        sentiment = extract_sentiment(text)
    
>       assert sentiment < 0
E       assert 0.8 < 0

process.py:17: AssertionError
======================================= short test summary info ========================================
FAILED process.py::test_extract_sentiment - assert 0.8 < 0
========================================== 1 failed in 0.84s ===========================================

从输出能够看出,测试失败是由于函数的情感值为0.8,而且不小于0!咱们不只能够知道咱们的函数是否如预期的那样工做,并且还能够知道为何它不起做用。从这个角度来看,咱们知道在哪里修复咱们的函数,以实现咱们想要的功能。

同一函数的屡次测试

咱们能够用其余例子来测试咱们的函数。新测试函数的名称是什么?

第二个函数的名称能够是test_extract_sentiment_2,若是咱们想在带有负面情绪的文本上测试函数,那么它的名称能够是test_extract_sentiment_negative。任何函数名只要以test开头就能够工做

#sentiment.py

def test_extract_sentiment_positive():

    text = "I think today will be a great day"

    sentiment = extract_sentiment(text)

    assert sentiment > 0

def test_extract_sentiment_negative():

    text = "I do not think this will turn out well"

    sentiment = extract_sentiment(text)

    assert sentiment < 0
>>> pytest sentiment.py

========================================= test session starts ==========================================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
collected 2 items

process.py .F                                                                                    [100%]
=============================================== FAILURES ===============================================
___________________________________ test_extract_sentiment_negative ____________________________________

def test_extract_sentiment_negative():
    
        text = "I do not think this will turn out well"
    
        sentiment = extract_sentiment(text)
    
>       assert sentiment < 0
E       assert 0.0 < 0

process.py:25: AssertionError
======================================= short test summary info ========================================
FAILED process.py::test_extract_sentiment_negative - assert 0.0 < 0
===================================== 1 failed, 1 passed in 0.80s ======================================

从输出中,咱们知道一个测试经过,一个测试失败,以及测试失败的缘由。咱们但愿“I do not think this will turn out well”这句话是消极的,但结果倒是0。

这有助于咱们理解,函数可能不会100%准确;所以,在使用此函数提取文本情感时,咱们应该谨慎。

参数化:组合测试

以上2个测试功能用于测试同一功能。有没有办法把两个例子合并成一个测试函数?这时参数化就派上用场了

用样本列表参数化

使用pytest.mark.parametrize(),经过在参数中提供示例列表,咱们可使用不一样的示例执行测试。

# sentiment.py

from textblob import TextBlob
import pytest

def extract_sentiment(text: str):
        '''使用textblob提取情绪。
        	在范围[- 1,1]内'''

        text = TextBlob(text)

        return text.sentiment.polarity

testdata = ["I think today will be a great day","I do not think this will turn out well"]

@pytest.mark.parametrize('sample', testdata)
def test_extract_sentiment(sample):

    sentiment = extract_sentiment(sample)

    assert sentiment > 0

在上面的代码中,咱们将变量sample分配给一个示例列表,而后将该变量添加到测试函数的参数中。如今每一个例子将一次测试一次。

========================== test session starts ===========================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
collected 2 items

sentiment.py .F                                                    [100%]

================================ FAILURES ================================
_____ test_extract_sentiment[I do not think this will turn out well] _____

sample = 'I do not think this will turn out well'

@pytest.mark.parametrize('sample', testdata)
    def test_extract_sentiment(sample):
    
        sentiment = extract_sentiment(sample)
    
>       assert sentiment > 0
E       assert 0.0 > 0

sentiment.py:19: AssertionError
======================== short test summary info =========================
FAILED sentiment.py::test_extract_sentiment[I do not think this will turn out well]
====================== 1 failed, 1 passed in 0.80s ===================

使用parametrize(),咱们能够在once函数中测试两个不一样的示例!

使用示例列表和预期输出进行参数化

若是咱们指望不一样的例子有不一样的输出呢?Pytest还容许咱们向测试函数的参数添加示例和预期输出!

例如,下面的函数检查文本是否包含特定的单词。

def text_contain_word(word: str, text: str):
    '''检查文本是否包含特定的单词'''
    
    return word in text

若是文本包含单词,则返回True。

若是单词是“duck”,而文本是“There is a duck in this text”,咱们指望返回True。

若是单词是‘duck’,而文本是‘There is nothing here’,咱们指望返回False。

咱们将使用parametrize()而不使用元组列表。

# process.py
import pytest
def text_contain_word(word: str, text: str):
    '''查找文本是否包含特定的单词'''
    
    return word in text

testdata = [
    ('There is a duck in this text',True),
    ('There is nothing here', False)
    ]

@pytest.mark.parametrize('sample, expected_output', testdata)
def test_text_contain_word(sample, expected_output):

    word = 'duck'

    assert text_contain_word(word, sample) == expected_output

函数的参数结构为parametrize('sample,expected_out','testdata),testdata=[( ),( )

>>> pytest process.py

========================================= test session starts ==========================================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
plugins: hydra-core-1.0.0, Faker-4.1.1
collected 2 items

process.py ..                                                                                    [100%]

========================================== 2 passed in 0.04s ===========================================

咱们的两个测试都经过了!

一次测试一个函数

当脚本中测试函数的数量愈来愈大时,你可能但愿一次测试一个函数而不是多个函数。用pytest很容易,pytest file.py::function_name

testdata = ["I think today will be a great day","I do not think this will turn out well"]

@pytest.mark.parametrize('sample', testdata)
def test_extract_sentiment(sample):

    sentiment = extract_sentiment(sample)

    assert sentiment > 0


testdata = [
    ('There is a duck in this text',True),
    ('There is nothing here', False)
    ]

@pytest.mark.parametrize('sample, expected_output', testdata)
def test_text_contain_word(sample, expected_output):

    word = 'duck'

    assert text_contain_word(word, sample) == expected_output

例如,若是你只想运行test_text_contain_word,请运行

pytest process.py::test_text_contain_word

而pytest只执行咱们指定的一个测试!

fixture:使用相同的数据来测试不一样的函数

若是咱们想用相同的数据来测试不一样的函数呢?例如,咱们想测试“今Today I found a duck and I am happy”这句话是否包含“duck ”这个词,它的情绪是不是积极的。这是fixture派上用场的时候。

pytest fixture是一种向不一样的测试函数提供数据的方法

@pytest.fixture
def example_data():
    return 'Today I found a duck and I am happy'


def test_extract_sentiment(example_data):

    sentiment = extract_sentiment(example_data)

    assert sentiment > 0

def test_text_contain_word(example_data):

    word = 'duck'

    assert text_contain_word(word, example_data) == True

在上面的示例中,咱们使用decorator建立了一个示例数据@pytest.fixture在函数example_data的上方。这将把example_data转换成一个值为“Today I found a duck and I am happy”的变量

如今,咱们可使用示例数据做为任何测试的参数!

组织你的项目

最后但并不是最不重要的是,当代码变大时,咱们可能须要将数据科学函数和测试函数放在两个不一样的文件夹中。这将使咱们更容易找到每一个函数的位置。

test_<name>.py<name>_test.py命名咱们的测试函数. Pytest将搜索名称以“test”结尾或以“test”开头的文件,并在该文件中执行名称以“test”开头的函数。这很方便!

有不一样的方法来组织你的文件。你能够将咱们的数据科学文件和测试文件组织在同一个目录中,也能够在两个不一样的目录中组织,一个用于源代码,一个用于测试

方法1:

test_structure_example/
├── process.py
└── test_process.py

方法2:

test_structure_example/
├── src
│   └── process.py
└── tests
    └── test_process.py

因为数据科学函数极可能有多个文件,测试函数有多个文件,因此你可能须要将它们放在不一样的目录中,如方法2。

这是2个文件的样子

from textblob import TextBlob

def extract_sentiment(text: str):
        '''使用textblob提取情绪。
        	在范围[- 1,1]内'''

        text = TextBlob(text)

        return text.sentiment.polarity
import sys
import os.path
sys.path.append(
    os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
from src.process import extract_sentiment
import pytest


def test_extract_sentiment():

    text = 'Today I found a duck and I am happy'

    sentiment = extract_sentiment(text)

    assert sentiment > 0

简单地说添加sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))能够从父目录导入函数。

在根目录(test_structure_example/)下,运行pytest tests/test_process.py或者运行在test_structure_example/tests目录下的pytest test_process.py

========================== test session starts ===========================
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
collected 1 item

tests/test_process.py .                                            [100%]

=========================== 1 passed in 0.69s ============================

很酷!

结论

你刚刚了解了pytest。我但愿本文能很好地概述为何测试很重要,以及如何将测试与pytest结合到数据科学项目中。经过测试,你不只能够知道你的函数是否按预期工做,并且还能够自信地使用不一样的工具或不一样的代码结构来切换现有代码。

本文的源代码能够在这里找到:

https://github.com/khuyentran1401/Data-science/tree/master/data_science_tools/pytest

我喜欢写一些基本的数据科学概念,玩不一样的算法和数据科学工具。

原文连接:https://towardsdatascience.com/pytest-for-data-scientists-2990319e55e6

欢迎关注磐创AI博客站:
http://panchuang.net/

sklearn机器学习中文官方文档:
http://sklearn123.com/

欢迎关注磐创博客资源汇总站:
http://docs.panchuang.net/

相关文章
相关标签/搜索