Pandas DataFrame-Validierung mit Bulwark#

Bulwark ist ein Paket zum eigenschaftsbasierten Testen von pandas-Dataframes. Das Projekt wurde stark von der nicht mehr unterstützten Engarde-Bibliothek beeinflusst.

1. Installation#

$ pipenv install bulwark
Installing bulwark…
Adding bulwark to Pipfile's [packages]…
✔ Installation Succeeded
Locking [dev-packages] dependencies…
✔ Success!
Updated Pipfile.lock (0d075a)!

2. Verwendung#

2.1 Überprüfungen#

Mit dem bulwark.checks-Modul könnt ihr viele gängige Annahmen überprüfen, z.B.

  • has_columns überprüft, ob bestimmte Spalten so oder so ähnlich vorhanden und in der richtigen Reihenfolge sind

  • has_dtypes überprüft die Datentypen von Spalten

  • has_no_infs überprüft, ob keine numpy.inf im DataFrame vorhanden sind

  • has_no_nans überprüft, ob es keine numpy.nan im DataFrame vorhanden sind

  • has_set_within_vals überprüft, ob die in einem dict angegebenen Werte eine Teilmenge der zugehörigen Spalte sind

  • has_unique_index überprüft, ob der Index eindeutig ist

  • is_monotonic überprüft, ob Werte einer Spalte aufsteigend oder absteigend sind

  • one_to_many überprüft, ob zwischen zwei Spalten eine n:1-Beziehung besteht

Die Überprüfungen sind dann sehr simpel, z.B. der Check, ob in der Spalte pipe keine numpy.nan vorhanden sind mit

import bulwark.checks as ck


df.pipe(ck.has_no_nans())

2.2 Decorators#

Für jeden Check erstellt bulwark.decorators, z.B. @dc.IsShape((-1, 10)) oder @dc.IsMonotonic(strict=True).

CustomCheck#

Ihr könnt auch eure eigenen benutzerdefinierten Funktionen erstellen, z.B.:

[1]:
import bulwark.checks as ck
import bulwark.decorators as dc
import numpy as np
import pandas as pd


def len_longer_than(df, l):
    if len(df) <= l:
        raise AssertionError("df is not as long as expected.")
    return df


@dc.CustomCheck(len_longer_than, 10)
def append_a_df(df, df2):
    return pd.concat([df, df2], ignore_index=True)


df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df2 = pd.DataFrame({"a": [1, np.nan, 3, 4], "b": [4, 5, 6, 7]})

append_a_df(df, df2)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[1], line 21
     18 df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
     19 df2 = pd.DataFrame({"a": [1, np.nan, 3, 4], "b": [4, 5, 6, 7]})
---> 21 append_a_df(df, df2)

File ~/.local/share/virtualenvs/python-311-6zxVKbDJ/lib/python3.11/site-packages/bulwark/decorators.py:81, in CustomCheck.__call__.<locals>.decorated(*args, **kwargs)
     78 df = f(*args, **kwargs)
     79 if self.enabled:
     80     # differs from BaseDecorator
---> 81     ck.custom_check(df, self.check_func, **self.check_func_params)
     82 return df

File ~/.local/share/virtualenvs/python-311-6zxVKbDJ/lib/python3.11/site-packages/bulwark/checks.py:588, in custom_check(df, check_func, *args, **kwargs)
    576 """Assert that `check(df, *args, **kwargs)` is true.
    577
    578 Args:
   (...)
    585
    586 """
    587 try:
--> 588     check_func(df, *args, **kwargs)
    589 except AssertionError as e:
    590     msg = "{} is not true.".format(check_func.__name__)

Cell In[1], line 9, in len_longer_than(df, l)
      7 def len_longer_than(df, l):
      8     if len(df) <= l:
----> 9         raise AssertionError("df is not as long as expected.")
     10     return df

AssertionError: len_longer_than is not true.

MultiCheck#

Mit MultiCheck könnt ihr mehrere Tests gleichzeitig ausführen und alle Fehler auf einmal sehen, z.B.:

[2]:
@dc.MultiCheck(
    checks={ck.has_no_nans: {"columns": None}, len_longer_than: {"l": 6}},
    warn=False,
)
def append_a_df(df, df2):
    return df.append(df2, ignore_index=True)


df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df2 = pd.DataFrame({"a": [1, np.nan, 3, 4], "b": [4, 5, 6, 7]})

append_a_df(df, df2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/var/folders/hk/s8m0bblj0g10hw885gld52mc0000gn/T/ipykernel_55151/1437634915.py in ?()
      8
      9 df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
     10 df2 = pd.DataFrame({"a": [1, np.nan, 3, 4], "b": [4, 5, 6, 7]})
     11
---> 12 append_a_df(df, df2)

~/.local/share/virtualenvs/python-311-6zxVKbDJ/lib/python3.11/site-packages/bulwark/decorators.py in ?(*args, **kwargs)
     20         @functools.wraps(f)
     21         def decorated(*args, **kwargs):
---> 22             df = f(*args, **kwargs)
     23             if self.enabled:
     24                 self.check_func(df, **self.check_func_params)
     25             return df

/var/folders/hk/s8m0bblj0g10hw885gld52mc0000gn/T/ipykernel_55151/1437634915.py in ?(df, df2)
      2     checks={ck.has_no_nans: {"columns": None}, len_longer_than: {"l": 6}},
      3     warn=False,
      4 )
      5 def append_a_df(df, df2):
----> 6     return df.append(df2, ignore_index=True)

~/.local/share/virtualenvs/python-311-6zxVKbDJ/lib/python3.11/site-packages/pandas/core/generic.py in ?(self, name)
   5985             and name not in self._accessors
   5986             and self._info_axis._can_hold_identifiers_and_holds_name(name)
   5987         ):
   5988             return self[name]
-> 5989         return object.__getattribute__(self, name)

AttributeError: 'DataFrame' object has no attribute 'append'