Tag Archives: dwh

python

Table syncing with Python

Recently, I had to move around a lot of data in an annoyingly error-prone process: I would receive awfully colorful spreadsheets in ever changing formats. Frightened by seemingly infinite creativity of Excel users I decided to move the data to a staging environment first. After some quality checking – aka: Excel exorcism – I subsequently would merge the source data with my production database, the target.

In the lingo of database people, I needed to implement INSERT, UPDATE and DELETE. Regarding my Python programme, this meant covering three cases:

  • new records in the source (INSERT),
  • changes between source and target (UPDATE) as well as
  • lost records, which only appear in target (DELETE).

In the context of a production system the latter case is the nasty one: You cannot simply delete records when you already have child records (i.e. foreign key relationships) in your system. So this meant shovelling data around manually. Thus my programme only handles the first two cases automatically and simply warns about the third case.

hxht
Moving around data and breaking some rules for the sake of backwards compatibility.

Object-relational mapping and a set()

The central piece of my Python code is an object-relational mapping between every database record and my custom class:

class Room(object):
    
    def __init__(self, pk, name, size):

        self.pk = pk
        self.name = name
        self.size = size

The “Syncer” does the work of fetching from target and source databases, comparing and taking proper action. I store all the records in a Python set() – a very powerful data structure for the task.
class RoomSyncer(object):
    
    def __init__(self):
        
        self.target_rooms = set()
    
    def get_target(self):
        """Fetch rooms from production db"""
        
        ...
        
        for room in target:
            ...
            self.target_rooms.add(
                Room(pk, name, size )
                )    

If you run into the problem that your target and source do not resemble each other (i.e. that their attributes are different) you cannot instantiate with the __init__() method in both cases. Use a factory method instead:
class Room(object):

    ...

    @classmethod
    def from_source(cls, pk, other_name, size):
        
        # do some magic to make objects simiar
        name = other_name
        
        return cls(pk, name, size)

class RoomSyncer(object):
    
    def __init__(self):
        
        ...
        
        self.source_rooms = set()
    
    def get_source(self):
        """Fetch rooms from staging db"""
        
        ...
        
        for room in source:
            ...
            self.source_rooms.add(
                Room.from_source(pk, other_name, size )
                )          

set() operations

So far we have two sets filled with our objects: target and source. If you subtract the target from the source you’ll get the newly arrived objects:

# rooms only in source
rooms_to_add = self.source_rooms - self.target_rooms

Oh no! The code won’t work since Python does not know whether your objects are equal or not. Hence the set operations won’t work out of the box. So you have to override the __hash__ and __eq__ methods – Python will then compare as you  would expect (more info on stackoverflow):
class Room(object):
    
    ...

    def __eq__(self, other):
        
        return self.pk == other.pk
    
    def __hash__(self):
        
        return hash(self.pk)

With this simple update you can now easily determine objects that only appear on one side. The last thing we need to do is to cover the UPDATE case. By overriding the methods above we implicitly confine the powers of Python’s set data structure. For a more sophisticated search for updates we’ll have to look at every attribute. Luckily for us, a Python object is just a simple dictionary under the hood. As a consequence we simply have to test the candidate dictionaries for equality:
class Room(object):
    
    ...

    def cmp(self, other):
        # is entire object equal?
        
        return self.__dict__ == other.__dict__

class RoomSyncer(object):
    ....

    def cmp_rooms(self, rooms):
        """objects appear on both sides"""

        rooms = self.target_rooms.intersection(self.source_rooms)

        for r in rooms:

            # simply get one object by key
            t = self._get_target(r.pk)
            s = self._get_source(r.pk)

            if t.cmp(s):
                # objects are identical
                ...
            
            else:
                # update
                ...

    def _get_target(self, pk):
        for r in self.target_rooms:
            if r.pk == pk:
                return r
    
    def _get_source(self, pk):  
        for r in self.source_rooms:
            if r.pk == pk:
                return r

hacks

Redesign SAP Finanzbericht

Zur Thematik, dass erfolgreiche ERP-Systeme nicht zwangsweise durch einfach bedienbare User Interfaces glänzen, habe ich bereits gebloggt. Spätestens, seitdem ich meine eigene Kostenstelle verantworte, war’s nun aber höchste Zeit, den Bericht, der mir Überblick über meine Finanzen geben sollte, zu überarbeiten.

Zu allererst ein Blick auf den Status Quo

Nach einigem Herumgeklicke und der Erkenntnis, dass SAP-Jahre aus 16 Monaten bestehen, erreiche ich Woche für Woche den Überblick über meine Kostenstelle im Look & Feel von Teletext, aka SAP GUI.

Der Bericht listet alle von mir unterschriebenen Rechnungen mit Betrag, Datum, Belegtext sowie Kostenart. Um den Leser nicht mit Details zu verwirren (so interessant sind meine Zahlen nun auch wieder nicht), habe ich die Berichtsstruktur hervorgehoben:

Der Bericht zeigt auf Kontierungselemente (Kostenstellen) gebuchte Kosten aufgeschlüsselt nach Kostenarten (Konten). Die Logik des Berichts orientiert sich mMn zu stark am System.

Im Großen und Ganzen befinden sich auf der Seite zwei interessante Blöcke: eine Übersicht oben links, sowie eine Tabelle mit den Buchungen darunter.

Wenn ich mir nun überlege, welche Aufgaben ich mit dem Bericht abwickeln muss, dann sind das nach absteigender Wichtigkeit:

  1. Überblick über Planungs- sowie Ist-Stand meiner Finanzen
  2. Details zu einzelnen Rechnungen (z.B.: im Falle von Reklamationen)
  3. Analyse meiner Kostenstruktur nach Zeit und Kostenart

Traurig, aber wahr: bereits der erste Task ist mit dem Bericht nicht ohne Weiteres möglich. Die Transaktion, also der SAP-Name für eine Bildschirm-Maske, listet entweder Plan- oder Ist-Daten. Die jeweils andere Ansicht erreicht man durch Beenden und Neustart des Berichts nach anderen Auswahlkriterien. (…)

Finanzbericht Marke Eigenbau

Eine kleiner Entwurf auf einer McDonalds-Rechnung (ein so genannter Low-Fidelity Mockup) und ein paar Stunden Datenexport, HTML, SQL, Javascript und Python später ist die Alternative fertig: Eine Webseite, die die drei oben genannten Tasks auf einen Blick ermöglicht.

Der Kasten oben links bietet mir die Gesamtsummen von Ist und Plan als Zahlen, zusätzlich verdeutlicht ein Prozentwert das Verhältnis (Wieviel ist schon weg?).  Darüber ein einziges Auswahlfeld für das Berichtsjahr, kein Formular-Moloch. Rechts daneben befindet sich Säulendiagramm, das mir dieses Plan/Ist-Verhältnis nochmal grafisch verdeutlicht. Bei mehreren Jahren – bei mir nicht der Fall – sieht man so automatisch den Budgetverlauf.

Die Details zu einzelnen Rechnungen finden sich darunter. Die Tabelle verwendet das jQuery-Plugin DataTables, welches Such-, Sortierfunktion und Pagination bietet. So sind ein Sortieren nach Datum, oder das Suchen einer bestimmten Rechnung keine große Hexerei. Die rein Client-seitige Umsetzung sorgt dafür, dass das Ding responsive bleibt.

Alternativvorschlag: Ein Überblick über Plan und Ist (gleichzeitig!) oben links, eine Tabelle aller Buchungen mit Sortier- und Suchfunktion darunter. Oben rechts ein paar grafische Darstellungen eben jener Daten.

Den Zuckerguss stellen die drei Diagramme im TabbedView oben rechts dar. Neben dem bereits besprochenen Jahresüberblick, gibt es noch ein Liniendiagramm, welches den Planungsstand mit dem Iststand unterjährig vergleicht. Dadurch dass auf Jahresebene geplant wird, ist eine kumulative Auswertung sinnvoll.

Die wichtigste Frage auf einen Blick beantwortet: Bin ich drüber oder drunter?

Abschließend noch eine Auswertung nach Kostenarten, die eine hierarchische Datenstruktur mit variabler Tiefe darstellen. In diesem Fall habe ich mich für eine Treemap entschieden. Diese codiert zwei Variable (Größe des Rechtecks entspricht der Summe; Farbe ist die Abweichung) in eine klickbare Fläche. Das “Entdecken” der Kostenzusammensetzung macht so richtig Spaß.

Sehr schöne Visualisierung einer hierarchischen Datenstruktur: die Größe entspricht der Summe, die Farbe der Plan/Ist-Abweichung

Die hier dargestellte Kritik an aktuellen Berichten ist positiv gemeint und soll keineswegs als SAP-Bashing missverstanden werden. Dass die Software derart erfolgreich war, ist und vermutlich bleiben wird, hat schon seine guten Gründe. Ich bin selbst überrascht, wie sich mein Blick auf eben jenen Bericht verändert hat, seitdem ich auch wirklich ein Anwender bin. (…)