TDDA: Test-Driven Data Analysis

TDDA verwendet Dateieingaben (wie NumPy-Arrays oder Pandas DataFrames) und eine Reihe von Einschränkungen (engl.: constraints), die als JSON-Datei gespeichert werden.

  • Reference Test unterstützt die Erstellung von Referenztests, die entweder auf unittest oder pytest basieren.

  • Constraints wird verwendet, um Constraints aus einem (Pandas)-DataFrame zu ermitteln, sie als JSON auszuschreiben und zu überprüfen, ob Datensätze die Constraints in der Constraints-Datei erfüllen. Es unterstützt auch Tabellen in einer Vielzahl von relationalen Datenbanken.

  • Rexpy ist ein Werkzeug zur automatischen Ableitung von regulären Ausdrücken aus einer Spalte in einem Pandas DataFrame oder aus einer (Python)-Liste von Beispielen.

1. Importe

[1]:
from pathlib import Path

import pandas as pd

from tdda.constraints import detect_df, discover_df, verify_df
[2]:
df = pd.read_csv(
    "https://raw.githubusercontent.com/kjam/data-cleaning-101/master/data/iot_example.csv",
)

2. Daten überprüfen

Mit pandas.DataFrame.sample lassen wir uns die ersten zehn Datensätze anzeigen:

[3]:
df.sample(10)
[3]:
timestamp username temperature heartrate build latest note
77827 2017-02-01T14:54:10 lindsay77 23 62 26317041-1f28-d128-a1af-a44c5dc1eda0 0 update
65826 2017-01-27T19:31:16 manderson 10 70 5700c2c7-e5e7-5bda-9a17-c3d340344bb4 0 user
123647 2017-02-19T21:31:01 robertneal 27 86 bc9da618-bcfe-6b4c-b7ff-30f6b595c450 0 NaN
82447 2017-02-03T11:19:17 davidanderson 28 60 81298dd3-1a0f-6ad4-26d2-81907f047b5d 1 sleep
123673 2017-02-19T21:46:39 ayang 13 60 45224df1-bd40-ab37-4a47-afd19c6949f2 1 update
131661 2017-02-23T02:32:03 fmedina 16 81 0f90ef61-e3ef-4433-2d57-14227bde3aa6 0 wake
73189 2017-01-30T18:23:46 jasminfleming 22 75 16205322-995b-510f-895d-ca2ecde39901 0 sleep
92468 2017-02-07T11:20:06 hudsonangela 9 61 f7b8f4e2-dfbe-7f8e-fc68-ee37cf6ba4e2 0 test
78680 2017-02-01T23:11:35 richardnewton 23 78 1ce40a86-f5a8-767c-5afd-396a1dde224f 0 interval
20694 2017-01-09T18:21:41 zachary74 22 83 d1b5c037-8499-893f-6b14-53584b03b050 0 wake

Und mit pandas.DataFrame.dtypes lassen wir uns die Datentypen für die einzelnen Spalten anzeigen:

[4]:
df.dtypes
[4]:
timestamp      object
username       object
temperature     int64
heartrate       int64
build          object
latest          int64
note           object
dtype: object

3. Erstellen eines constraints-Objekt

Mit discover_constraints kann ein Vonstraints-Objekt erzeugt werden.

[5]:
constraints = discover_df(df)
[6]:
constraints
[6]:
<tdda.constraints.base.DatasetConstraints at 0x133d9cc20>
[7]:
constraints.fields
[7]:
Fields([('timestamp', <tdda.constraints.base.FieldConstraints at 0x133d9c590>),
        ('username', <tdda.constraints.base.FieldConstraints at 0x1233ae990>),
        ('temperature',
         <tdda.constraints.base.FieldConstraints at 0x1233aee90>),
        ('heartrate', <tdda.constraints.base.FieldConstraints at 0x1233835c0>),
        ('build', <tdda.constraints.base.FieldConstraints at 0x123383950>),
        ('latest', <tdda.constraints.base.FieldConstraints at 0x1233f4710>),
        ('note', <tdda.constraints.base.FieldConstraints at 0x114f478a0>)])

4. Schreiben der Constraints in eine Datei

[8]:
with Path.open("../../data/iot_example.json", "w") as f:
    f.write(constraints.to_json())

Wenn wir uns die Datei genauer betrachten können wir erkennen, dass z.B. für die timestamp-Spalte eine Zeichenkette mit 19 Zeichen erwartet wird und temperature Integer mit Werten von 5–29 erwartet.

[9]:
!cat ../../data/iot_example.json
{
    "creation_metadata": {
        "local_time": "2026-05-22T15:21:06",
        "utc_time": "2026-05-22T13:21:06+00:00",
        "creator": "TDDA 2.2.17",
        "host": "fay.local",
        "user": "veit",
        "n_records": 146397,
        "n_selected": 146397
    },
    "fields": {
        "timestamp": {
            "type": "string",
            "min_length": 19,
            "max_length": 19,
            "max_nulls": 0,
            "no_duplicates": true
        },
        "username": {
            "type": "string",
            "min_length": 3,
            "max_length": 21,
            "max_nulls": 0
        },
        "temperature": {
            "type": "int",
            "min": 5,
            "max": 29,
            "sign": "positive",
            "max_nulls": 0
        },
        "heartrate": {
            "type": "int",
            "min": 60,
            "max": 89,
            "sign": "positive",
            "max_nulls": 0
        },
        "build": {
            "type": "string",
            "min_length": 36,
            "max_length": 36,
            "max_nulls": 0,
            "no_duplicates": true
        },
        "latest": {
            "type": "int",
            "min": 0,
            "max": 1,
            "sign": "non-negative",
            "max_nulls": 0
        },
        "note": {
            "type": "string",
            "min_length": 4,
            "max_length": 8,
            "allowed_values": [
                "interval",
                "sleep",
                "test",
                "update",
                "user",
                "wake"
            ]
        }
    }
}

5. Überprüfen von Dataframes

Hierfür lesen wir zunächst eine neue csv-Datei mit Pandas ein und lassen uns dann zehn Datensätze exemplarisch ausgeben:

[10]:
new_df = pd.read_csv(
    "https://raw.githubusercontent.com/kjam/data-cleaning-101/master/data/iot_example_with_nulls.csv"
)

new_df.sample(10)
[10]:
timestamp username temperature heartrate build latest note
50968 2017-01-21T20:52:57 cguerra 16.0 60 d745f0ec-cc32-d105-da12-2b78760c031c 1.0 wake
102289 2017-02-11T09:20:55 james22 NaN 67 62650884-e7a8-c655-ec93-68481ba40254 0.0 NaN
3516 2017-01-02T21:59:41 sandersmatthew NaN 69 3a1202f8-d362-e7a0-0590-c440dddb7251 NaN test
73137 2017-01-30T17:50:59 dixonalison 12.0 81 44ab03bb-a015-04f5-1547-75984ab022b3 1.0 test
18967 2017-01-09T01:43:42 dillon91 NaN 63 944d5821-2a9a-56f9-60f6-4d329f76da46 0.0 NaN
122212 2017-02-19T07:54:50 philip23 8.0 68 e0f57946-2df2-0e9b-1af7-04b174d6fd13 1.0 sleep
108919 2017-02-14T00:50:51 wadebrian 18.0 63 1cfe6513-21ef-b87b-f21a-e6b1e63dbe29 0.0 NaN
132834 2017-02-23T13:46:52 walkerkimberly 5.0 67 NaN 0.0 NaN
76472 2017-02-01T01:50:30 raymond34 24.0 85 NaN 0.0 wake
44192 2017-01-19T03:49:03 brenda45 13.0 60 c72d9894-95ca-4cb5-7517-637d26dad91f 1.0 test

Wir sehen mehrere Felder, die als NaN ausgegeben werden. Um dies nun systematisch zu analysieren, wenden wir verify_df auf unseren neuen DataFrame an. Dabei gibt passes gibt die Anzahl der bestandenen, failures die Anzahl der fehlgeschlagenen Constraints zurück.

[11]:
v = verify_df(new_df, "../../data/iot_example.json")
[12]:
v
[12]:
<tdda.constraints.pd.constraints.PandasVerification at 0x133d9dbe0>
[13]:
v.passes
[13]:
30
[14]:
v.failures
[14]:
3

Wir können uns auch anzeigen lassen, in welchen Spalten welche Constraints bestanden und fehlgeschlagen sind:

[15]:
print(str(v))
FIELDS:

timestamp: 0 failures  5 passes  type ✓  min_length ✓  max_length ✓  max_nulls ✓  no_duplicates ✓

username: 0 failures  4 passes  type ✓  min_length ✓  max_length ✓  max_nulls ✓

temperature: 1 failure  4 passes  type ✓  min ✓  max ✓  sign ✓  max_nulls ✗

heartrate: 0 failures  5 passes  type ✓  min ✓  max ✓  sign ✓  max_nulls ✓

build: 1 failure  4 passes  type ✓  min_length ✓  max_length ✓  max_nulls ✗  no_duplicates ✓

latest: 1 failure  4 passes  type ✓  min ✓  max ✓  sign ✓  max_nulls ✗

note: 0 failures  4 passes  type ✓  min_length ✓  max_length ✓  allowed_values ✓

SUMMARY:

Constraints passing: 30
Constraints failing: 3

Alternativ können wir uns diese Ergebnisse auch tabellarisch anzeigen lassen:

[16]:
v.to_frame()
[16]:
field failures passes type min min_length max max_length sign max_nulls no_duplicates allowed_values
0 timestamp 0 5 True NaN True NaN True NaN True True NaN
1 username 0 4 True NaN True NaN True NaN True NaN NaN
2 temperature 1 4 True True NaN True NaN True False NaN NaN
3 heartrate 0 5 True True NaN True NaN True True NaN NaN
4 build 1 4 True NaN True NaN True NaN False True NaN
5 latest 1 4 True True NaN True NaN True False NaN NaN
6 note 0 4 True NaN True NaN True NaN NaN NaN True

6. Finden der fehlerhaften Zeilen

tdda.constraints.pd.constraints.detect_df() erkennt Datensätze des pandas DataFrame, die gegen eine der Einschränkungen in der bereitgestellten JSON-Datei verstoßen. Anschließend können wir über dem erstellten PandasDetection-Objekt die Funktion detected() aufrufen um uns die Zeilen ausgeben zu lassen, die fehlerhaft sind:

[17]:
d = detect_df(new_df, "iot_example.json")

d.detected()
[17]:
n_failures
Index
3 1
4 1
7 1
10 2
12 1
... ...
146385 1
146387 2
146391 2
146393 2
146394 1

77260 rows × 1 columns

Wir können uns alle fehlerhaften Datensätze anzeigen lassen, indem wir nur den Teil des Index von new_df verwenden, der auch in d.detected() vorkommt:

[18]:
d_index = d.detected().index
[19]:
new_df[new_df.index.isin(d_index)]
[19]:
timestamp username temperature heartrate build latest note
3 2017-01-01T12:02:09 eddierodriguez 28.0 76 NaN 0.0 update
4 2017-01-01T12:02:36 kenneth94 29.0 62 122f1c6a-403c-2221-6ed1-b5caa08f11e0 NaN NaN
7 2017-01-01T12:04:35 scott28 16.0 76 7a60219f-6621-e548-180e-ca69624f9824 NaN interval
10 2017-01-01T12:06:21 njohnson NaN 63 e09b6001-125d-51cf-9c3f-9cb686c19d02 NaN NaN
12 2017-01-01T12:07:41 jessica48 22.0 83 03e1a07b-3e14-412c-3a69-6b45bc79f81c NaN update
... ... ... ... ... ... ... ...
146385 2017-02-28T23:53:59 powelleric 20.0 86 152eda10-676a-069c-b664-19443f2c8081 NaN test
146387 2017-02-28T23:54:50 jthompson NaN 66 8da10303-fe49-e313-8fda-0d5e79ded054 NaN update
146391 2017-02-28T23:57:21 aaronbecker NaN 87 7e52f4a8-345c-5ee0-e515-b8c392213062 NaN sleep
146393 2017-02-28T23:58:43 joelrusso NaN 89 NaN 0.0 NaN
146394 2017-02-28T23:59:23 lellis NaN 84 dac87426-e147-9c39-6e4c-790bb11f8fc9 0.0 update

77260 rows × 7 columns

Alternativ können wir uns auch alle fehlerfreien Datensätze anzeigen lassen.

[20]:
new_df[~new_df.index.isin(d_index)]
[20]:
timestamp username temperature heartrate build latest note
0 2017-01-01T12:00:23 michaelsmith 12.0 67 4e6a7805-8faa-2768-6ef6-eb3198b483ac 0.0 interval
1 2017-01-01T12:01:09 kharrison 6.0 78 7256b7b0-e502-f576-62ec-ed73533c9c84 0.0 wake
2 2017-01-01T12:01:34 smithadam 5.0 89 9226c94b-bb4b-a6c8-8e02-cb42b53e9c90 0.0 NaN
5 2017-01-01T12:03:04 bryanttodd 13.0 86 0897dbe5-9c5b-71ca-73a1-7586959ca198 0.0 interval
6 2017-01-01T12:03:51 andrea98 17.0 81 1c07ab9b-5f66-137d-a74f-921a41001f4e 1.0 NaN
... ... ... ... ... ... ... ...
146389 2017-02-28T23:56:05 kathy63 5.0 88 c2f76050-abd4-aee4-7bc0-3498325d0573 0.0 NaN
146390 2017-02-28T23:56:34 cookallison 16.0 84 f0b0c1f9-900b-276c-bca9-ac4d4ec4e88e 0.0 user
146392 2017-02-28T23:58:06 mcontreras 15.0 63 69e61a15-d2d0-47a7-1a27-e07b3eeeba10 0.0 NaN
146395 2017-02-28T23:59:48 grayjasmin 17.0 64 4911a589-3a15-4bbf-1de1-e5a69ab739da 1.0 update
146396 2017-03-01T00:00:30 jgreene 23.0 70 4f95bbca-26a7-29e7-1f19-aaedf1a51741 0.0 interval

69137 rows × 7 columns