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]:
import pandas as pd
import numpy as np

from tdda.constraints import 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
123221 2017-02-19T17:27:43 wilsonpatricia 20 60 2452cba2-623a-9aaf-4620-b1a00a707088 0 NaN
80684 2017-02-02T18:32:06 vmiller 27 79 877d2246-f54d-2d6b-8991-5698199def39 1 user
108810 2017-02-13T23:50:44 sandraobrien 9 73 2c185e6b-ad8a-bbc3-5b8a-a7f0cff3b3b5 1 user
47330 2017-01-20T10:00:25 rcline 18 63 c8b03e53-db4a-bbb7-1d06-efb20eca17ec 0 NaN
25842 2017-01-11T19:32:20 colton99 20 72 1d99cb10-0189-3bc2-d7e3-4fee382387ef 0 sleep
31413 2017-01-14T01:06:46 zjimenez 11 89 eb2bfbd3-6a61-cfae-afcc-6879319aebad 1 user
115347 2017-02-16T14:11:28 alvin94 23 87 9924687d-959e-99cf-6510-712904df2583 0 wake
138131 2017-02-25T16:45:28 chelsea05 7 66 f995acb5-fff8-3ff7-0581-152e81988b81 1 user
97693 2017-02-09T13:31:43 thomasknight 18 66 a9180bc3-90a3-88bf-36e5-a67549147b28 1 user
33904 2017-01-15T01:02:00 paulwall 8 66 56b09285-495a-23ef-31f7-489fad16096f 1 sleep

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 0x144fd7250>
[7]:
constraints.fields
[7]:
Fields([('timestamp', <tdda.constraints.base.FieldConstraints at 0x15fec8790>),
        ('username', <tdda.constraints.base.FieldConstraints at 0x15fec8bd0>),
        ('temperature',
         <tdda.constraints.base.FieldConstraints at 0x15fec9050>),
        ('heartrate', <tdda.constraints.base.FieldConstraints at 0x15fec9750>),
        ('build', <tdda.constraints.base.FieldConstraints at 0x15fec9b10>),
        ('latest', <tdda.constraints.base.FieldConstraints at 0x15feca390>),
        ('note', <tdda.constraints.base.FieldConstraints at 0x15feca710>)])

4. Schreiben der Constraints in eine Datei#

[8]:
with open("ignore-iot_constraints.tdda", "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 ignore-iot_constraints.tdda
{
    "creation_metadata": {
        "local_time": "2023-08-19 13:13:48",
        "utc_time": "2023-08-19 11:11:48",
        "creator": "TDDA 2.0.09",
        "host": "fay.fritz.box",
        "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
126044 2017-02-20T20:33:42 karenrichards NaN 79 284fab65-9fcc-18e4-8838-6e89ac938f77 NaN NaN
54844 2017-01-23T10:00:53 karen37 NaN 76 6ea2310d-b136-dfae-3e4a-730cb01a6881 1.0 wake
15484 2017-01-07T16:36:43 carterjill 24.0 61 3b524f97-4a6a-156e-e182-760818cc5c6b 0.0 interval
3709 2017-01-02T23:50:12 ebenton 18.0 82 f23b7b48-0ad1-18b5-c5a8-033d66d47007 1.0 NaN
131978 2017-02-23T05:37:46 cameron67 NaN 87 2806ee39-a668-d0f7-44b9-9255188d51df 1.0 interval
61302 2017-01-25T23:57:04 rebecca88 19.0 71 NaN NaN interval
130493 2017-02-22T15:15:56 cgriffin 29.0 81 NaN 1.0 NaN
121685 2017-02-19T02:53:16 johnberg 16.0 68 e3b81408-7c4f-78b5-c373-2ee086e6dbbf NaN NaN
87150 2017-02-05T08:17:46 kelly71 28.0 68 99842995-cc89-638c-9067-a7d92e450097 0.0 interval
138723 2017-02-25T22:22:49 ayoung 22.0 75 dca3e5f6-c05a-c490-8384-4f7cfda4e19a 0.0 NaN

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, "ignore-iot_constraints.tdda")
[12]:
v
[12]:
<tdda.constraints.pd.constraints.PandasVerification at 0x174786fd0>
[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