Руководство по тестированию кода Pandas для новых разработчиков Python

Руководство по тестированию кода Pandas для новых разработчиков Python

9 марта 2023 г.

Тестирование — важный этап разработки программного обеспечения. В каждом языке программирования есть множество инструментов, облегчающих нам жизнь при тестировании кода, и Python не исключение.

Однако при использовании библиотеки Pandas я заметил, что тестирование не так распространено.

Это может быть связано с тем, что это сложнее, чем тестирование обычного кода Python, или с тем, что Pandas обычно используется профессионалами, которые не обязательно являются программистами.

Независимо от обстоятельств, факт остается фактом: тестирование играет решающую роль, и его нельзя упускать из виду.

В этом посте я поделюсь некоторыми своими знаниями по этой теме, в том числе о том, какие инструменты использовать, общие стратегии тестирования и как тестировать код, использующий Pandas.

Использование Pytest

Существуют и другие библиотеки, но я считаю Pytest самой простой и надежной из них.

Другие инструменты тестирования обычно требуют определения классов с методами для каждого теста, что делает это немного более громоздким. С другой стороны, Pytest позволяет нам определять простые и компактные функции.

Чтобы использовать его, мы должны сначала установить его:

pip install pytest

В качестве примера предположим, что у нас есть следующая функция, которую мы хотим протестировать в файле с именем «operations.py»:

# operations.py
def sum_a_and_b(a, b):
  return a + b

Чтобы протестировать его с помощью Pytest, мы создаем файл, который либо начинается, либо заканчивается словом «test» (то есть «test_operations.py» или «operations_test.py»). Внутри этого файла мы определяем функцию для проверки нашей функции sum_a_and_b с помощью утверждение:

# test_operations.py
from operations import sum_a_and_b

def test_sum():
  result = sum_a_and_b(1, 2)

  assert result == 3

Мы запускаем тесты, набрав pytest в терминале:

(venv) pandas-testing % pytest
========================== test session starts ==========================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /Users/eduardo/development/tutorials/pandas-testing
collected 1 item                                                        

test_operations.py .                                              [100%]

=========================== 1 passed in 0.01s ===========================

Написание хороших тестов

За пределами счастливого пути

Конечно, приведенный выше простой тест не гарантирует, что наша функция работает правильно в 100 % случаев. Например, он не проверяет поведение функции, когда мы передаем строки в качестве аргументов.

Чтобы написать хорошие тесты, мы должны начать с тестирования «счастливых путей» (т. е. нормального ожидаемого поведения), но мы всегда должны помнить также о тестировании пограничных случаев.

Принципы тестирования

Для модульных тестов полезно помнить, что тесты всегда должны следовать общей структуре «организовать/действовать/утвердить». То есть мы должны:

* организовать все, что необходимо для теста, например, создать необходимые данные или специальные настройки, подготовить базу данных в памяти или имитировать вызовы API;

* воздействовать на тестируемую функцию или метод, вызывая их;

* подтвердить ожидаемый результат.

Если мы тестируем что-то, что зависит от взаимодействия с пользователем, мы также можем думать об этом в терминах «дано/когда/тогда», т. е.:

* с учетом того, что необходимо для запуска теста;

* когда происходит какое-то взаимодействие;

* тогда следует ожидать определенного результата.

Не торопитесь называть все правильно

Как и при написании любого другого кода, при тестировании настоятельно рекомендуется правильно называть вещи.

Наши тестовые функции должны описывать то, что тестируется. То же самое касается любых переменных, используемых внутри него.

Например, наш пример test_sum выше можно было бы написать так:

from operations import s

def test1():
  x = s(1, 2)

  assert x == 3

Это тот же тест, и он дает тот же результат.

Но даже на этом простом примере сложнее понять, что на самом деле тестируется. Вы можете себе представить, что для более сложных тестовых случаев это быстро станет непонятным.

Тестирование панд

Итак, как мы применим это к Pandas?

Если у нас есть функции или методы, которые выводят кадры данных, мы хотим убедиться, что результаты соответствуют ожиданиям. Например, у нас есть функция double_dataframe, которая умножает каждое значение DataFrame на два:

# pandas_example.py
import pandas as pd

def double_dataframe(df):
    return df * 2

Чтобы проверить это, мы можем захотеть использовать утверждение, как мы делали в нашем предыдущем примере. Мы определяем ожидаемый результат и сравниваем его с фактическим с помощью оператора равенства (==):

import pandas as pd
from pandas_example import double_dataframe


def test_double_dataframe():
    # arrange
    input_df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
    # act
    result_df = double_dataframe(input_df)
    # assert
    expected_df = pd.DataFrame({'a': [2, 4, 6], 'b': [8, 10, 12]})
    assert result_df == expected_df

Но это не имеет смысла в Pandas, поскольку DataFrame представляет собой набор векторизованных значений. Pandas не поймет, что мы подразумеваем под этим, и вернет ошибку: ValueError: истинное значение DataFrame неоднозначно. Используйте a.empty, a.bool(), a.item(), a.any() или a.all().

Использование тестовых функций Pandas

Вместо этого мы можем использовать методы тестирования Pandas: assert_frame_equal, assert_series_equal и assert_index_equal. (Во многих случаях утверждения Pytest по-прежнему будут полезны и необходимы, поэтому мы все равно должны использовать их, когда это уместно.)

assert_frame_equal

Мы можем переписать приведенный выше пример, чтобы правильно сравнить результат и ожидаемые кадры данных с assert_frame_equal:

import pandas as pd
from pandas.testing import assert_frame_equal
from pandas_example import double_dataframe


def test_double_dataframe():
    input_df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})

    result_df = double_dataframe(input_df)

    expected_df = pd.DataFrame({'a': [2, 4, 6], 'b': [8, 10, 12]})
    assert_frame_equal(result_df, expected_df)

И на этот раз испытание пройдет успешно. assert_frame_equal сравнит два кадра данных и выведет все различия.

Мы можем разрешить варьировать строгость проверок на равенство, используя дополнительные параметры, такие как check_dtype, check_index_type, check_exact… и многие другие, которые вы можете найти в документацию Панды.

Например, если мы хотим сравнить значения, но нам все равно, являются ли они float или int, мы можем установить check_dtype = false :

import pandas as pd
from pandas.testing import assert_frame_equal
from pandas_example import double_dataframe


def test_double_dataframe():
    input_df = pd.DataFrame({'a': [1.0, 2.0, 3.0], 'b': [4, 5, 6]})

    result_df = double_dataframe(input_df)

    expected_df = pd.DataFrame({'a': [2, 4, 6], 'b': [8, 10, 12]})
    assert_frame_equal(result_df, expected_df, check_dtype = False)

assert_series_equal

assert_series_equal работает аналогичным образом, но для серий.

Скажем, у нас есть следующая функция, которая удваивает значения одного столбца в DataFrame и возвращает его в виде серии:

import pandas as pd

def double_column(df, col_name):
    return df.loc[:, col_name] * 2

Мы можем проверить это с помощью assert_series_equal:

def test_double_column():
    input_df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})

    result_series = double_column(input_df, 'a')

    expected_series = pd.Series([2, 4, 6], name='a')
    assert_series_equal(result_series, expected_series)

assert_index_equal

assert_index_equal можно использовать, когда нам нужно убедиться, что индексы двух фреймов данных совпадают.

Например, если у нас есть функция get_top_n_countries, которая принимает кадр данных, содержащий информацию о странах, и возвращает первые n страны на основе указанного столбца.

Результирующий DataFrame использует названия стран в качестве индекса.

import pandas as pd

def get_top_n_countries(df, column_name, n):
    sorted_df = df.sort_values(column_name, ascending=False)
    top_n_countries = sorted_df.head(n)
    top_n_countries.set_index('country', inplace=True)
    return top_n_countries

Мы можем проверить это с помощью assert_index_equal:

import pandas as pd
from pandas.testing import assert_index_equal
from pandas_example import get_top_n_countries

def test_get_top_n_countries():
    data = {'country': ['USA', 'China', 'Japan', 'Germany', 'UK'],
            'population': [328, 1393, 126, 83, 66]}
    df = pd.DataFrame(data)

    top_3_countries = get_top_n_countries(df, 'population', 3)

    expected_index = pd.Index(['China', 'USA', 'Japan'], name='country')
    assert_index_equal(top_3_countries.index, expected_index)

Не забывайте о крайних случаях

В приведенных выше примерах мы тестируем только счастливый путь, ожидаемое поведение, когда все идет по плану. Однако важно учитывать ситуации, когда что-то может пойти не так, как ожидалось.

Это называется пограничными случаями, и нам нужно определить для них желаемое поведение. Например, мы можем захотеть разрешить отрицательные значения или нет, в зависимости от типа данных, с которыми мы работаем.

Чтобы проиллюстрировать это, давайте расширим последний пример, в котором мы тестировали функцию get_top_n_countries. Мы можем придумать несколько ситуаций, в которых у нас могут быть результаты, отклоняющиеся от положительного пути.

Кадр данных пуст

Если DataFrame пуст, мы можем захотеть убедиться, что функция работает и не возникает никаких ошибок:

def test_get_top_n_countries_empty_dataframe():
    data = {'country': [],
            'population': []}
    df = pd.DataFrame(data)

    top_3_countries = get_top_n_countries(df, 'population', 3)

    expected_index = pd.Index([], name='country')
    assert_index_equal(top_3_countries.index, expected_index, exact=False)

Кадр данных содержит пропущенные значения (NaN)

Если DataFrame содержит отсутствующие значения, мы можем убедиться, что они обрабатываются правильно и строки не удаляются:

def test_get_top_n_countries_missing_values():
    data = {'country': ['USA', 'China', 'Japan'],
            'population': [328, 1393, float('nan')]}
    df = pd.DataFrame(data)

    top_3_countries = get_top_n_countries(df, 'population', 3)

    expected_index = pd.Index(['China', 'USA', 'Japan'], name='country')
    assert_index_equal(top_3_countries.index, expected_index)

Кадр данных содержит нечисловые значения

Если DataFrame содержит значения, не являющиеся числами, мы можем создать исключение, если они находятся в сортируемом столбце:

def test_get_top_n_countries_non_numeric_values():
    # arrange
    data = {'country': ['USA', 'China', 'Japan'],
            'population': [328, 1393, 'One hundred twenty six']}
    df = pd.DataFrame(data)
    # act and assert
    with pytest.raises(TypeError):
        get_top_n_countries(df, 'population', 3)

Кадр данных содержит отрицательные значения

Если DataFrame содержит отрицательные значения, мы можем создать исключение при работе с данными о населении, поскольку отрицательное значение населенности не имеет смысла.

Для этого нам сначала нужно изменить тестируемую функцию:

def get_top_n_countries(df, column_name, n):
    if column_name == 'population' and (df[column_name] < 0).any():
        raise ValueError('population values must be greater than zero')

    sorted_df = df.sort_values(column_name, ascending=False)
    top_n_countries = sorted_df.head(n)
    top_n_countries.set_index('country', inplace=True)
    return top_n_countries

Затем мы можем реализовать тест:

def test_get_top_n_countries_non_numeric_values():
    data = {'country': ['USA', 'China', 'Japan'],
            'population': [328, 1393, -100]}
    df = pd.DataFrame(data)

    with pytest.raises(ValueError):
        get_top_n_countries(df, 'population', 3)

Тестируя эти пограничные случаи, мы можем убедиться, что наша функция работает правильно и надлежащим образом обрабатывает непредвиденные ситуации.

Заключительные слова

Тестирование — важный этап разработки программного обеспечения, и Pandas — не исключение. В заключение:

* Хотя тестировать код Pandas может быть сложнее, и это не так часто, как тестирование обычного кода Python, это все же важная часть процесса разработки, которую нельзя упускать из виду.

* Когда дело доходит до тестирования, Pytest — это простой и надежный инструмент, который позволяет нам определять компактные функции для тестирования.

* Чтобы писать хорошие тесты, мы всегда должны помнить о тестировании как «счастливых путей», так и пограничных случаев, и следовать структуре «упорядочить/действовать/утвердить» для модульных тестов.

* Настоятельно рекомендуется правильно называть все при тестировании, чтобы избежать путаницы.

* При тестировании Pandas мы должны использовать методы тестирования Pandas, такие как assert_frame_equal, assert_series_equal и assert_index_equal.

Следуя этим советам и рекомендациям, мы можем гарантировать, что наш код Pandas надежен, надежен и работает должным образом.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE