Установщик свойства для подкласса Pandas DataFrame - PullRequest
9 голосов
/ 09 января 2020

Я пытаюсь настроить подкласс 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']

1 Ответ

3 голосов
/ 14 января 2020

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

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 строк в валидаторе.

...