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('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
51603 2017-01-22T02:55:23 sherry06 28 83 5c1d8967-fcfc-1a8d-2f62-5036da241848 1 sleep
119933 2017-02-18T10:09:06 psnyder 9 83 d4e5846a-0b8e-ecd1-5346-a680c8271524 1 test
67013 2017-01-28T06:57:26 hayesthomas 22 78 a5cca8fd-e6aa-ddd9-9980-8d32077ca099 0 update
5554 2017-01-03T17:25:28 dianajohnson 29 80 7e30f6b8-4e2f-025b-515d-4f2593e7ce08 1 NaN
118950 2017-02-18T00:42:42 katherinefaulkner 17 79 71613d5f-72fd-ee43-a27c-5f93cc693be1 1 interval
50388 2017-01-21T15:19:28 diazgregory 20 68 6ef03856-0470-1664-f749-4fd59572efda 0 wake
88116 2017-02-05T17:38:11 thomas62 10 74 7c19890c-ef1b-75a0-acfa-efdf21ac90b6 0 NaN
64332 2017-01-27T05:17:04 kanderson 28 81 0b94e0ba-ecee-0b76-8b53-191f93f12404 1 sleep
48896 2017-01-21T00:55:48 heidi76 28 74 c3fd9b2a-2900-ced7-e721-ff7940419a13 0 update
143209 2017-02-27T17:28:19 johnsonmiguel 9 74 785fc5b8-7be8-1a01-ddbe-c0581d8c5d5f 0 test

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 0x7fe58e48dcd0>
[7]:
constraints.fields
[7]:
Fields([('timestamp',
         <tdda.constraints.base.FieldConstraints at 0x7fe58e48dfd0>),
        ('username',
         <tdda.constraints.base.FieldConstraints at 0x7fe58e4ab280>),
        ('temperature',
         <tdda.constraints.base.FieldConstraints at 0x7fe58e4ab5e0>),
        ('heartrate',
         <tdda.constraints.base.FieldConstraints at 0x7fe58e4ab940>),
        ('build', <tdda.constraints.base.FieldConstraints at 0x7fe58e4abca0>),
        ('latest', <tdda.constraints.base.FieldConstraints at 0x7fe58e4b0040>),
        ('note', <tdda.constraints.base.FieldConstraints at 0x7fe58e4b0370>)])

4. Schreiben der Constraints in eine Datei#

[8]:
with open('../../data/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 ../../data/ignore-iot_constraints.tdda
{
    "creation_metadata": {
        "local_time": "2021-11-20 16:16:01",
        "utc_time": "2021-11-20 15:15:01",
        "creator": "TDDA 1.0.32",
        "host": "eve.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('iot_example_with_nulls.csv')
new_df.sample(10)
[10]:
timestamp username temperature heartrate build latest note
34897 2017-01-15T10:33:45 waltersann 19.0 76 9a55a840-e586-4cc4-375f-00db11ad6157 NaN interval
46490 2017-01-20T01:59:35 dunlaprobert NaN 63 NaN 0.0 NaN
48329 2017-01-20T19:33:15 heidi31 16.0 64 e14014b4-b96b-82dd-5e9b-a4fea08839b4 NaN interval
23625 2017-01-10T22:15:30 kurtcain 28.0 73 66e31ec0-2e6c-9882-cbf5-8d572cd18bf1 1.0 NaN
114909 2017-02-16T10:01:53 frankbates 22.0 75 9afa2b75-0f44-b530-4ab1-fb29beac6443 NaN interval
40464 2017-01-17T16:01:21 rbaker NaN 71 c6a27614-1632-885b-1e3c-b1e0441b231d 1.0 test
110461 2017-02-14T15:30:22 carpenterashlee 23.0 85 c45944a9-1c69-8692-d6a2-c3462dd6b4d3 0.0 NaN
79579 2017-02-02T07:49:53 alexistucker 8.0 61 f787577b-1080-ac9d-e871-40db40c7225f 0.0 NaN
68692 2017-01-28T23:09:11 hallmaria 12.0 62 f6b642b7-6fdf-d772-34de-f8e8da949ff1 0.0 NaN
4142 2017-01-03T03:56:31 veronicalamb 18.0 76 NaN 0.0 update

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/ignore-iot_constraints.tdda')
[12]:
v
[12]:
<tdda.constraints.pd.constraints.PandasVerification at 0x7fe57a173f70>
[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