Setter de propiedades para la subclase de marco de datos de pandas

9

Estoy tratando de configurar una subclase de pd.DataFrameque tiene dos argumentos necesarios al inicializar ( groupy timestamp_col). Quiero ejecutar la validación de esos argumentos groupy timestamp_col, por lo tanto, tengo un método setter para cada una de las propiedades. Todo esto funciona hasta que lo intento set_index()y lo consigo TypeError: 'NoneType' object is not iterable. Parece que no se está pasando ningún argumento a mi función setter en test_set_indexy test_assignment_with_indexed_obj. Si agrego if g == None: returna mi función de establecimiento, puedo pasar los casos de prueba, pero no creo que esa sea la solución adecuada.

¿Cómo debo implementar la validación de propiedad para estos argumentos requeridos?

Abajo está mi clase:

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

Aquí están mis casos de prueba:

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
fuente
1
Si Nonees un valor no válido para la grouppropiedad, ¿no debería aumentar un ValueError?
Chepner
1
Tiene razón en que Nonees un valor no válido, por eso no me gusta la declaración if. Pero agregar que Ninguno lo hace pasar las pruebas. Estoy buscando cómo solucionar esto correctamente sin la instrucción None if.
cpage
2
El colocador debe levantar a ValueError. El problema es descubrir qué está tratando de establecer el groupatributo Noneen primer lugar.
Chepner
@chepner sí, exactamente.
cpage
Quizás el paquete Pandas Flavor pueda ayudar.
Mykola Zotko

Respuestas:

3

El set_index()método llamará self.copy()internamente para crear una copia de su objeto DataFrame (vea el código fuente aquí ), dentro del cual utiliza su método de constructor personalizado _internal_ctor(), para crear el nuevo objeto ( fuente ). Tenga en cuenta que self._constructor()es idéntico a self._internal_ctor(), que es un método interno común para casi todas las clases de pandas para crear nuevas instancias durante operaciones como copia profunda o segmentación. Su problema en realidad se origina en esta función:

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)

Supongo que copiaste este código del problema de github . Las líneas kwargs["**"] = Nonele dicen explícitamente al constructor que establezca Noneen ambos groupy timestamp_col. Finalmente, el configurador / validador obtiene Noneel nuevo valor y genera un error.

Por lo tanto, debe establecer un valor aceptable para groupy 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)

Luego puede eliminar las if g == None: returnlíneas en el validador.

gdlmx
fuente