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
oderpytest
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 |