Aufgabe 7*: Operatoren der Relationalen Algebra implementieren#

Im folgenden sollen die Operatoren der relationalen Algebra für Pandas Dataframes implementiert werden.

Gehe hierbei davon aus, dass die Spalten der Dataframes benannt sind und ignoriert den Index der DataFrames.

In diesem ersten Snippet wird eine Funktion definiert, mit der du deine Lösungen später testen kannst. Du musst es dir nicht im Detail ansehen, sondern nur einmal ausführen.

import pandas as pd
from pandas import DataFrame
from typing import *
from io import StringIO
import numpy as np

df = DataFrame(
    [
        (11, 22, 34, 23),
        (11, 22, 31, 11),
        (13, 42, 16, 21),
        (55, 32, 71, 22),
        (66, 33, 19, 27),
        (13, 42, 31, 11),
    ],
    columns=list("abyz"),
)

df2 = DataFrame([(11, 22), (13, 42)], columns=list("ab"))

df3 = DataFrame(
    [
        (11, 22, 34, 23),
        (15, 22, 41, 11),
        (13, 42, 16, 21),
        (55, 32, 71, 22),
        (66, 34, 19, 27),
    ],
    columns=list("abyz"),
)

test = [
    ('select("y > 30 and z <= 20", df)', "a,b,y,z\r\n11,22,31,11\r\n13,42,31,11\r\n"),
    (
        'project(["z", "y"], df)',
        "z,y\r\n23,34\r\n11,31\r\n21,16\r\n22,71\r\n27,19\r\n11,31\r\n",
    ),
    (
        'rename(["a", "b", "c", "d"], df)',
        "a,b,c,d\r\n"
        "11,22,34,23\r\n"
        "11,22,31,11\r\n"
        "13,42,16,21\r\n"
        "55,32,71,22\r\n"
        "66,33,19,27\r\n"
        "13,42,31,11\r\n",
    ),
    (
        "cross(df, df2)",
        "L_a,L_b,y,z,R_a,R_b\r\n"
        "11,22,34,23,11,22\r\n"
        "11,22,34,23,13,42\r\n"
        "11,22,31,11,11,22\r\n"
        "11,22,31,11,13,42\r\n"
        "13,42,16,21,11,22\r\n"
        "13,42,16,21,13,42\r\n"
        "55,32,71,22,11,22\r\n"
        "55,32,71,22,13,42\r\n"
        "66,33,19,27,11,22\r\n"
        "66,33,19,27,13,42\r\n"
        "13,42,31,11,11,22\r\n"
        "13,42,31,11,13,42\r\n",
    ),
    (
        "union(df, df3)",
        "a,b,y,z\r\n"
        "11,22,34,23\r\n"
        "11,22,31,11\r\n"
        "13,42,16,21\r\n"
        "55,32,71,22\r\n"
        "66,33,19,27\r\n"
        "13,42,31,11\r\n"
        "11,22,34,23\r\n"
        "15,22,41,11\r\n"
        "13,42,16,21\r\n"
        "55,32,71,22\r\n"
        "66,34,19,27\r\n",
    ),
    ("minus(df, df3)", "a,b,y,z\r\n11,22,31,11\r\n66,33,19,27\r\n13,42,31,11\r\n"),
    (
        "intersect(df, df3)",
        "a,b,y,z\r\n"
        "11,22,34,23\r\n"
        "11,22,31,11\r\n"
        "13,42,16,21\r\n"
        "55,32,71,22\r\n"
        "66,33,19,27\r\n"
        "13,42,31,11\r\n",
    ),
    (
        "join(df, df2)",
        "a,b,y,z\r\n11,22,34,23\r\n11,22,31,11\r\n13,42,16,21\r\n13,42,31,11\r\n",
    ),
    ('join(df, df2, theta="z == R_b")', "L_a,L_b,y,z,R_a,R_b\r\n55,32,71,22,11,22\r\n"),
    ("divide(df, df2)", "y,z\r\n31,11\r\n"),
]


def validate(i):
    query, result = test[i]
    print(f"test {i}: {query}")
    expected = pd.read_csv(StringIO(result)).reset_index(drop=True)
    actual = eval(query).reset_index(drop=True)
    try:
        pd.testing.assert_frame_equal(expected, actual)
        print("OK")
    except:
        print("expected")
        print(expected)
        print("actual")
        print(actual)
        raise

Aufgabe 7.1*: Basisoperatoren#

In den folgenden Teilaufgaben implementieren Sie die Basisoperatoren der relationalen Algebra.

Aufgabe 7.1.1*: Selektion (\(\sigma\))#

Hinweis: Der Prädikatstring ist ein beliebiger boolescher Ausdruck über die Attribute einer Entity. Du kannst die Funktion eval benutzen um diesen String auszuwerten. Benutze das optionale Argument locals um die Werte der Variablen des Ausdrucks zu übergeben. Zum Beispiel:

eval("a < 10", {"a": 3})
True
def select(predicate: str, relation: DataFrame) -> DataFrame:
    # TODO: Hier code hinzufügen.
    return relation
validate(0)
test 0: select("y > 30 and z <= 20", df)
expected
    a   b   y   z
0  11  22  31  11
1  13  42  31  11
actual
    a   b   y   z
0  11  22  34  23
1  11  22  31  11
2  13  42  16  21
3  55  32  71  22
4  66  33  19  27
5  13  42  31  11
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 validate(0)

Cell In[1], line 105, in validate(i)
    103 actual = eval(query).reset_index(drop=True)
    104 try:
--> 105     pd.testing.assert_frame_equal(expected, actual)
    106     print("OK")
    107 except:

    [... skipping hidden 1 frame]

File /usr/local/lib/python3.13/site-packages/pandas/_testing/asserters.py:614, in raise_assert_detail(obj, message, left, right, diff, first_diff, index_values)
    611 if first_diff is not None:
    612     msg += f"\n{first_diff}"
--> 614 raise AssertionError(msg)

AssertionError: DataFrame are different

DataFrame shape mismatch
[left]:  (2, 4)
[right]: (6, 4)

Aufgabe 7.1.2*: Projektion (\(\pi\))#

def project(attributes: List[str], relation: DataFrame) -> DataFrame:
    # TODO: Hier code hinzufügen.
    return relation
validate(1)

Aufgabe 7.1.3*: rename (umbenennen) (\(\rho\))#

def rename(new_attribute_names: List[str], relation: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return relation
validate(2)

Aufgabe 7.1.4*: Kreuzprodukt (\(\times\))#

def cross(left: DataFrame, right: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return relation
validate(3)

Aufgabe 7.1.5*: Vereinigung ( \(\cup\))#

def union(left: DataFrame, right: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return relation
validate(4)

Aufgabe 7.1.6*: Differenz (\(-\))#

def minus(left: DataFrame, right: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return relation
validate(5)

Aufgabe 7.2*: Abgeleitete Operatoren#

Hinweis: Die abgeleiteten Operatoren heißen so, weil man sie von den Basisoperatoren ableiten kann.

Aufgabe 7.2.1*: Schnitt (\(\cap\))#

def intersect(left: DataFrame, right: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return left
validate(6)

Aufgabe 7.2.2*: Natural Join (\(\bowtie\)), Theta Join ( \(\bowtie_{\theta}\))#

Wenn das optionale Argument theta leer ist, soll ein natural join durchgeführt werden.

def join(left: DataFrame, right: DataFrame, theta: str = "", outer: bool = False) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return left
validate(7)
validate(8)

Aufgabe 7.2.3*: Division (\(/\))#

def divide(left: DataFrame, right: DataFrame) -> DataFrame:
    # TODO: Hier Code hinzufügen.
    return left
validate(9)

Bei Bedarf: Alle Tests ausführen#

# Alle Lösungen mittels implementierter Funktionen generieren:
x = []
divide(df, df2)
for q, _ in test:
    print(q)
    x.append((q, eval(q).to_csv(index=False)))

from pprint import pprint

pprint(x)

# Alle Tests ausführen:
for i in range(10):
    validate(i)