Установщик свойств для подкласса Pandas DataFrame

9

Я пытаюсь настроить подкласс, pd.DataFrameкоторый имеет два обязательных аргумента при инициализации ( groupи timestamp_col). Я хочу запустить проверку этих аргументов groupи timestamp_col, таким образом, у меня есть метод установки для каждого из свойств. Это все работает, пока я не попытаюсь set_index()и получить TypeError: 'NoneType' object is not iterable. Похоже, что никакие аргументы не передаются моей функции установки в test_set_indexи test_assignment_with_indexed_obj. Если я добавлю if g == None: returnк своей функции установки, я могу пройти тестовые случаи, но не думаю, что это правильное решение.

Как мне реализовать проверку свойства для этих обязательных аргументов?

Ниже мой класс:

import pandas as pd
import numpy as np


class HistDollarGains(pd.DataFrame):
    @property
    def _constructor(self):
        return HistDollarGains._internal_ctor

    _metadata = ["group", "timestamp_col", "_group", "_timestamp_col"]

    @classmethod
    def _internal_ctor(cls, *args, **kwargs):
        kwargs["group"] = None
        kwargs["timestamp_col"] = None
        return cls(*args, **kwargs)

    def __init__(
        self,
        data,
        group,
        timestamp_col,
        index=None,
        columns=None,
        dtype=None,
        copy=True,
    ):
        super(HistDollarGains, self).__init__(
            data=data, index=index, columns=columns, dtype=dtype, copy=copy
        )

        self.group = group
        self.timestamp_col = timestamp_col

    @property
    def group(self):
        return self._group

    @group.setter
    def group(self, g):
        if g == None:
            return

        if isinstance(g, str):
            group_list = [g]
        else:
            group_list = g

        if not set(group_list).issubset(self.columns):
            raise ValueError("Data does not contain " + '[' + ', '.join(group_list) + ']')
        self._group = group_list

    @property
    def timestamp_col(self):
        return self._timestamp_col

    @timestamp_col.setter
    def timestamp_col(self, t):
        if t == None:
            return
        if not t in self.columns:
            raise ValueError("Data does not contain " + '[' + t + ']')
        self._timestamp_col = t

Вот мои тесты:

import pytest

import pandas as pd
import numpy as np

from myclass import *


@pytest.fixture(scope="module")
def sample():
    samp = pd.DataFrame(
        [
            {"timestamp": "2020-01-01", "group": "a", "dollar_gains": 100},
            {"timestamp": "2020-01-01", "group": "b", "dollar_gains": 100},
            {"timestamp": "2020-01-01", "group": "c", "dollar_gains": 110},
            {"timestamp": "2020-01-01", "group": "a", "dollar_gains": 110},
            {"timestamp": "2020-01-01", "group": "b", "dollar_gains": 90},
            {"timestamp": "2020-01-01", "group": "d", "dollar_gains": 100},
        ]
    )

    return samp

@pytest.fixture(scope="module")
def sample_obj(sample):
    return HistDollarGains(sample, "group", "timestamp")

def test_constructor_without_args(sample):
    with pytest.raises(TypeError):
        HistDollarGains(sample)


def test_constructor_with_string_group(sample):
    hist_dg = HistDollarGains(sample, "group", "timestamp")
    assert hist_dg.group == ["group"]
    assert hist_dg.timestamp_col == "timestamp"


def test_constructor_with_list_group(sample):
    hist_dg = HistDollarGains(sample, ["group", "timestamp"], "timestamp")

def test_constructor_with_invalid_group(sample):
    with pytest.raises(ValueError):
        HistDollarGains(sample, "invalid_group", np.random.choice(sample.columns))

def test_constructor_with_invalid_timestamp(sample):
    with pytest.raises(ValueError):
        HistDollarGains(sample, np.random.choice(sample.columns), "invalid_timestamp")

def test_assignment_with_indexed_obj(sample_obj):
    b = sample_obj.set_index(sample_obj.group + [sample_obj.timestamp_col])

def test_set_index(sample_obj):
    # print(isinstance(a, pd.DataFrame))
    assert sample_obj.set_index(sample_obj.group + [sample_obj.timestamp_col]).index.names == ['group', 'timestamp']
cpage
источник
1
Если Noneэто недопустимое значение для groupсвойства, вы не должны поднять ValueError?
Чепнер
1
Вы правы, Noneэто неверное значение, поэтому мне не нравится выражение if. Но добавив, что None заставляет его пройти тесты. Я ищу, как правильно это исправить без заявления None if.
cpage
2
Сеттер должен поднять ValueError. Проблема состоит в том, чтобы выяснить, что пытается установить groupатрибут Noneв первую очередь.
Чепнер
@chepner да, именно так.
cpage
Может быть, пакет Pandas Flavor может помочь.
Николай Зотко

Ответы:

3

set_index()Метод будет вызывать self.copy()внутри , чтобы создать копию объекта DataFrame (просмотреть исходный код здесь ), внутри которого он использует свой собственный алгоритм конструктора _internal_ctor(), чтобы создать новый объект ( источник ). Обратите внимание, что self._constructor()это идентично self._internal_ctor(), что является общим внутренним методом почти для всех классов панд для создания новых экземпляров во время таких операций, как глубокое копирование или нарезка. Ваша проблема на самом деле происходит из этой функции:

class HistDollarGains(pd.DataFrame):
    ...
    @classmethod
    def _internal_ctor(cls, *args, **kwargs):
        kwargs["group"]         = None
        kwargs["timestamp_col"] = None
        return cls(*args, **kwargs) # this is equivalent to calling
                                    # HistDollarGains(data, group=None, timestamp_col=None)

Я полагаю, вы скопировали этот код из выпуска GitHub . Строки kwargs["**"] = Noneявно указывают конструктору установить Noneоба параметра groupи timestamp_col. Наконец установщик / валидатор получает Noneновое значение и выдает ошибку.

Поэтому вы должны установить приемлемое значение groupи timestamp_col.

    @classmethod
    def _internal_ctor(cls, *args, **kwargs):
        kwargs["group"]         = []
        kwargs["timestamp_col"] = 'timestamp' # or whatever name that makes your validator happy
        return cls(*args, **kwargs)

Затем вы можете удалить if g == None: returnстроки в валидаторе.

gdlmx
источник