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
unittestoderpytestbasieren.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