Coffee Dashboard: Processing and Optimizing the Dataset

In the past blogpost, we have prepared and upload an XML export from our Franke A400 coffee machine. As we’re working with machine data, the data prep journey isn’t over yet. Let’s talk about how to find out what’s in the data, what’s left to be done, and how to resolve these issues.

One nasty thing about working with machine data is, while typically very clean and structured (because written out by a machine following predefined rules), it’s not designed to be read and understood by humans in the first place. In order to work with this data, we have to take a deeper look at the data loaded to analytics and try to get to the meaning of most of the relevant fields.

Column view in the recipe builder.

Let’s start by building a recipe. There are quite a number of helpful tools for our task in the Dataset Recipe. Switch to the “Column” view to see a list of column names and a status bar that indicates each column’s population with values. An important note of caution: Only a sample of the rows will be displayed, and the sample size is by default set to 5000. So before you decide on dropping some columns, make sure you reviewed the changed with a larger sample size or by exploring the dataset itself.

For the machine data we’re using, I know that there were relevant changes introduced with a newer software version, so I start by switching to the larger sample which is 10000 rows.

Column profile widget: You can see that only about half of the rows are populated with a value, and there are only two different values in the data – a typical profile for a dimension such as a software version.

Let’s start by analyzing the first column. Its name is “Ver”, giving us a little clue about the purpose, but we need further investigation. Using the column profile, we can see that more than half of the rows doesn’t have data, and for the rest of the sample, there are only two values, which follow typical version naming conventions. It’s a software version, and “Ver” is obviously a lazy column name (or just an”efficient memory footprint”). Let’s switch to the “attributes” widget and change the label to “System Version”.

A few more columns have obvious content, “Ti” and “TiUtc” have both been identified as date fields, and the name hints already at the content. While “Ti” is populated everywhere, “TiUtc” seems to be filled only recently, and it contains redundant information as we’re only checking the data from a single machine in a single timezone. Ket’s change the label of “Ti” to “Datetime” and drop the representation of the date and time in UTC (“TiUtc”).

Histogram for one of the low (no) variance columns

Next, let’s tackle two archenemies of any analytic approach. Columns with low variance in data, and columns that are there but empty. While low variance can happen and should be examined, no variance and no values are pointing at columns that are not relevant for analysis because they either have the same value for each row (so you don’t need an analysis to get to the value), or they contain nothing, so they are both predictable and without any value .

Going through the list of fields and looking at their value histograms, we can determine fo most of the fields if they are potentially useful for an analysis or not.

And there’s another we can find – let’s look at this histogram and the raw data and go into the details of what we can see here:

Did you find the potential issues:

  • there’s a line “2 Espressi”. The issues is – quite obviously – that the quantity and the product are combined in one field. If we applied the Count(Rows) function, we’d end up with 2 (rows), not 3 (cups served). So one task in data prep is to split quantity and product name.
  • Next: “Teewasser undosiert”. You won’t get to the core without translating the product name. It’s not a real issue, but a hint: The machine produces hot water (“Teewasser”) and the user can choose whatever quantity (“undosiert”). This is also available for hot milk, cold milk and steamed milk and will come very handy to understand the cryptically labeled telemetry fields later.
  • “rinsing”: These are lines we typically want to filter, as the machine runs a lot of automated rinsing programs over the day and these are definitely not drinks consumed by the team.
  • Did you see the last important issue by now? It’s “Latte Macchiato” vs. “Latte macchiato”

Handling the Data Prep in a Dataflow

I decided to handle these adjustments in a dataflow because I want to do a few things that are not currently available in Recipes (soon: Data Prep):

  • I want to normalize the capitalization of the product names that’s different between firmware versions of the machine (that’s a replace function in Recipes and a ComputeExpression in Dataflow).
  • I want to extract the quantities from single and double shots – this can be combined into a case formula which is available as a ComputeExpression in Dataflow
  • I want to create canonical product names so that “2 Espressi” resolves to a user interaction (pushed “2 Espressi” button), a quantity (2) and a product (“Espresso”). Again, I’m going to use a ComputeExpression.
  • … and some buckets for Tea, Coffee, Chocolate and Milk.
  • Then, some transformations for the date – day, week number, month name, day name, hour as a dimension.
  • And finally, we’re dropping all unused columns and auxilliary columns that were created in the process.

We end up with a data flow like this:

The complete JSON definition of this looks like this:

{
  "filter rinsing": {
    "action": "filter",
    "parameters": {
      "source": "Load Machine Data",
      "saqlFilter": "'Na' != \"rinsing\""
    }
  },
  "Store Coffee Data": {
    "action": "sfdcRegister",
    "parameters": {
      "name": "Coffee Data",
      "alias": "CoffeeData",
      "source": "drop Fields"
    }
  },
  "create Fields": {
    "action": "computeExpression",
    "parameters": {
      "source": "filter rinsing",
      "mergeWithSource": true,
      "computedFields": [
        {
          "defaultValue": "0",
          "precision": 2,
          "name": "WeekNumber",
          "saqlExpression": "Ti_Week",
          "label": "Week Number",
          "type": "Text"
        },
        {
          "defaultValue": "0",
          "precision": 1,
          "saqlExpression": "day_in_week(toDate('Ti_sec_epoch'))",
          "name": "DayInWeek",
          "scale": 0,
          "label": "Day In Week",
          "type": "Numeric"
        },
        {
          "defaultValue": "0",
          "precision": 3,
          "saqlExpression": "day_in_year(toDate('Ti_sec_epoch'))",
          "name": "DayInYear",
          "scale": 0,
          "label": "Day In Year",
          "type": "Numeric"
        },
        {
          "defaultValue": "0",
          "precision": 2,
          "saqlExpression": "Ti_Hour",
          "name": "Hour",
          "label": "Hour",
          "type": "Text"
        },
        {
          "saqlExpression": "case \n  Ti_Month\n  when \"01\" then \"January\"\n  when \"02\" then \"February\"\n  when \"03\" then \"March\"\n  when \"04\" then \"April\"\n  when \"05\" then \"May\"\n  when \"06\" then \"June\"\n  when \"07\" then \"June\"\n  when \"08\" then \"August\"\n  when \"09\" then \"September\"\n  when \"10\" then \"October\"\n  when \"11\" then \"November\"\n  when \"12\" then \"December\"\nelse \"\"\nend",
          "name": "MonthName",
          "label": "Month",
          "type": "Text"
        },
        {
          "name": "DayName",
          "saqlExpression": "case \n  DayInWeek\n  when 1 then \"Sunday\"\n  when 2 then \"Monday\"\n  when 3 then \"Tuesday\"\n  when 4 then \"Wednesday\"\n  when 5 then \"Thursday\"\n  when 6 then \"Friday\"\n  when 7 then \"Saturday\"\nelse \"\"\nend",
          "label": "Day",
          "type": "Text"
        },
        {
          "type": "Text",
          "saqlExpression": "replace('Na',\"macchiato\",\"Macchiato\")",
          "name": "Name",
          "label": "Name"
        },
        {
          "type": "Numeric",
          "saqlExpression": "case when substr('Na',1,1) == \"2\" then 2\nelse 1\nend",
          "name": "Quantity",
          "label": "Quantity",
          "precision": 1,
          "defaultValue": "1",
          "scale": 0
        },
        {
          "type": "Text",
          "saqlExpression": "case 'Na'\nwhen \"Teewasser undosiert\" then \"Tea\"\nwhen \"Milchschaum undosiert\" then \"Milk\"\nwhen \"Warme Milch undosiert\" then \"Milk\"\nwhen \"Kalte Milch undosiert\" then \"Milk\"\nwhen \"Schokolade\" then \"Chocolate\"\nelse \"Coffee\"\nend\n",
          "name": "ProductType",
          "label": "Product Type"
        },
        {
          "type": "Text",
          "name": "ProductName",
          "label": "Product Name",
          "saqlExpression": "case \n  'Name'\n  when \"2 Cafés Crème\" then \"Café Crème\"\n  when \"2 Cappuccini\" then \"Cappuccino\"\n  when \"2 Espressi\" then \"Espresso\"\n  when \"2 Latte Macchiato\" then \"Latte Macchiato\"\n  when \"2 Milchkaffees\" then \"Milchkaffee\"\nelse  'Name'\nend"
        }
      ]
    }
  },
  "Load Machine Data": {
    "action": "edgemart",
    "parameters": {alias": "e_xportNew2"}
  },
  "drop Fields": {
    "action": "sliceDataset",
    "parameters": {
      "mode": "select",
      "fields": [
        {"name": "WeekNumber"},
        {"name": "Tl5"},
        {"name": "Tl4"},
        {"name": "Tl3"},
        {"name": "Ti"},
        {"name": "Tf"},
        {"name": "Tb3"},
        {"name": "Tb2"},
        {"name": "Tb1"},
        {"name": "Re"},
        {"name": "Quantity},
        {"name": "ProductType"},
        {"name": "ProductName"},
        {"name": "Pp"},
        {"name": "Pl"},
        {"name": "Ms"},
        {"name": "MonthName"},
        {"name": "Miv1"},
        {"name": "Max"},
        {"name": "Io"},
        {"name": "Hour"},
        {"name": "Fl"},
        {"name": "Dd1"},
        {"name": "DayName"},
        {"name": "DayInYear"},
        {"name": "DayInWeek"},
        {"name": "Bt"},
        {"name": "Bpt"},
        {"name": "Bi2"},
        {"name": "Bi1"},
        {"name": "Bam"},
        { "name": "Name"}
      ],
      "source": "create Fields"
    }
  }
}

As a last step, let’s open the newly created dataset by double clicking on the dataset (or using the “Explore” action). Ignore the chart for now and just click on “Field” to bring up the Dataset XMD editor. There’s at least one column that could do with a human readable name, and that’s “Ti”. Let’s call it “DateTime” by clicking on the name, entering a more readable label and then hit return and click the save button. Now we’re set for the first analysis – stay tuned!

Coffee Dashboard Series

CzechDreamin’ 19: Zwei Präsentation in Prag!

Ein weiterer 2019er-Debütant unter den Community-Konferenzen ist CzechDreamin’ in Prag. Martin Humpolec hat mit seinem Team ein fantastisches Line-Up für ein Event mit vier parallelen Tracks zusammenstellt, so dass für jeden etwas dabei sein sollte. Highlights sind sicherlich: Rikke Hovgaard mit einem großen Analytics-Workshop, und einem großen Block LWCs mit René Winkelmeyer und Philippe Özil. Wenn es Eure Kalender hergeben, klickt Euch eines der verbleibenden Tickets und seid am 16. August in Prag dabei.

Ich selbst werde gleich zweimal auf der Bühne stehen:

  • Zunächst um 14:45 im Klub A, um den Platform Cache einem Realitätscheck zu unterziehen. Es ist ohne Zweifel ein Feature, das Eure Apps performanter machen kann. Aber seit Platform Cache 2016 präsentiert wurde, ist ja viel passiert. Ich werde ein wenig überlegen, wo heutzutage der Platz für den Cache ist und wie man gute Use Cases findet.
  • Um 16:45 erneut auf der Bühne im Klub A geben dann Christian Szandor Knapp und ich ein weiteres Duett, diesmal aber mit einer kleinen Seitenstory aus dem echten Leben: Wir hatten das Thema eigentlich ganz anders geplant und ihr ein Kafka-Thema gegeben. Doch alles, was so klar aussah, verwandelte sich fortlaufend weiter – ganz kafkaesk – und was wir glaubten zu kennen und zu wissen, zeigte sich uns in seinen Metamorphosen.

Insofern: Es lohnt sich – nicht nur wegen unserer Kafka-Reise!. Greift zu und kommt am 16. August nach Prag!

“Your admin toolbelt is not complete without Salesforce DX” #FTD18

Christian Szandor Knapp und ich waren wieder gemeinsam unterwegs – diesmal in der Mission, Salesforce Admins das Leben etwas leichter zu machen. Salesforce DX und vor allem die neue CLI sind das Power Tool für Admins – das haben wir bei French Touch Dreamin’ in Paris am 24. November 2018 präsentiert.

Video folgt später

Resource: https://github.com/open-force/sfcli-cookbook

#DF18 – Route Your Triggers Like a Pro!

Es ist tatsächlich passiert – ich hatte einen Dreamforce-Vortrag \o/ (und Dreamforce ist mit 170.000 Teilnehmern nun mal die größte Tech-Konferenz der Welt)! Auf diese Reise hat mich der wunderbare  Christian Szandor Knapp, begleitet, und gemeinsam standen wir dann gleich am Morgen des ersten Dreamforce-Tages auf der Bühne im Developer Theater – hier ist die Aufzeichnung auf YouTube:

„#DF18 – Route Your Triggers Like a Pro!“ weiterlesen

Continuous Integration Webinar mit Circle CI

Meinen Vortrag von ForceAcademy habe ich in Zusammenarbeit mit CircleCI noch etwas aufgefrischt und als Webinar präsentiert. Inhaltlich greift es etwas technischer und tiefer, verwendet nun auch ApexPMD und speichert Artefakte, und Testresultate und baut nun vollständig auf die Circle 2.0-Konfiguration auf. Details unten – oder eben einfach konsumieren:

„Continuous Integration Webinar mit Circle CI“ weiterlesen

Gastspiel #histocamp 2016

Ich habe ja mal wieder ein Gastspiel in meinem alten Metier gegeben und war auf dem Histocamp – und das ist definitiv ein Erlednis. Damit meine ich nicht hauptsächlich, dass sich das bereits im zweiten Jahr als das Klassentreffen einer neuen Art von Geschichtsprofis und -interessierten. Ich meine vielmehr den Geist, miteinander zu netzwerken und über Gemeinsamkeiten und gemeinsame Interessen zusammenzufinden, statt sich gegenseitig mit dem antrainierten Habitus die sorgfältig abgesteckten Claims der wissenschaftlichen Einzigartigkeit vorzuführen. „Gastspiel #histocamp 2016“ weiterlesen

Samstags-Geisteswissenschaftler-Blues

Der Samstag ist wie gemacht, meine Stimmung in Bezug auf die Geisteswissenschaften und die Wissenschaft im Allgemeinen ordentlich zu trüben (deswegen zum Beispiel). Nicht, dass es mich wirklich hart betrifft. Aber ich halte Wissenschaft an sich für etwas gesellschaftlich enorm wertvolles,  wissenschaftliche Arbeitserfahrung bringt starke und vielseitig qualifizierte Persönlichkeiten hervor. Und ganz nebenbei kenne ich viele wunderbare Menschen in der Wissenschaft – es schmerzt mich ernsthaft angesichts deren Träume und Lebensentwürfe, die auf die Wissenschaft als Beruf bauen.

Statistics, anyone?

Was mich schon länger umtreibt, ist die Frage, wie die Beschäftigungssituation von wissenschaftlich qualifizierten Geisteswissenschaftlern überhaupt aussieht. Ich kenne nun eine Menge von Historikerinnen und Historikern, plus Nachbarwissenschaften und je tiefer ich nachschaue und -frage, desto weniger finde ich Menschen, die in einem normalen, idealerweise unbefristeten Arbeitsverhältnis mit einer Institution stehen, deren primärer Zwecke Wissenschaft, Forschung oder akademische Ausbildung ist. Was ich sehe, sind vorwiegend solche Formen von Beschäftigung:

  • Stipendium als “Vergütung” einer Qualifikationsstelle, im schlimmsten Fall sogar mit einer impliziten Verpflichtung, an grundständigen Aufgaben der Einrichtung mitzuwirken.
  • Lehraufträge mit ebensolchem Zweck
  • Verkettung von Sachgrundbefristung, gerne und oft verbunden mit – siehe oben – impliziten Verplfichtung, grundständige und nominell grundfinanzierte Aufgaben der Einrichtung wahrzunehmen.
  • Halbe Stellen für volle Arbeit (der ewige Doktorandenschmerz), gerne und oft verbunden mit … siehe oben.
  • Lehrkraft für besondere Aufgaben (“Hochdeputatsstelle”), gerne und … ihr wisst schon.
  • Projektbeschäftigung mit … you know what.
  • und dann gibt es ja auch noch die Phänomene SHK, WHK und Werkverträge.

Kurzum: Ich befürchte zu wissen, dass die allermeisten, die eine Laufbahn an akademischen Einrichtungen eingeschlagen haben, mit dem Traum und der Hoffnung arbeiten, ein Leben in der Wissenschaft verbringen. Faktisch verbringen sie zwar ihr Leben in und für die Wissenschaft, aber eben nicht so, wie man sich ein Arbeitsverhältnis, vorstellt, und das finde ich katastrophal.

Der Punkt ist grundlegend: Wer seinen Beruf nicht sozialversicherungspflichtig ausübt, fällt bei Arbeitslosigkeit in der Regel direkt in die Grundsicherung. Heißt: ALGII, vulgo Hartz IV. Das sollte sich potenzielle Stipendiums-Dokrorand_innen bitte vergegenwärtigen, bevor es losgeht. Krankenkasse? Zahlt ihr selbst und ganz allein? Rentenbeiträge? Freiwillig bestenfalls, ganz alleine. Wenn eine Perspektive zur Anstellung besteht, wird die Stipendiatszeit natürlich nicht bei der Berechnung der Stufen anerkannt.

Nicht ganz so schlimm sieht es für Hilfskräfte aus – aber auch hier: Die Zeiten werden in der Regel nicht für spätere Bezügestufen anerkannt (es sei denn, ihr könnt es belegen, aber da ist ja der Haken: Wenn aus der Stellenbeschreibung eindeutig hervorgeht, dass ihr selbständig wissenschaftlich gearbeitet habt, dann seid ihr ja gar keine Hilfskräfte gewesen. Deswegen ist die Stellenbeschreibung in aller Regel die einer Hilfskraft und das wiederum ist keine einschlägige Vorerfahrung, zählt also bei der Vergütung nicht, FALLS es überhaupt je zu einer Anstellung als Mitarbeiter_in kommt.

Bei den restlichen Szenarien kann man schon milde werden und sagen, dass das zumindest Ausformungen regulärer Beschäftigung sind – auch wenn die allermeisten in diesen Beschäftigungen so viel machen (müssen / dürfen), das ihr Profil vermeintlich schärft und ihre Chancen vermeintlich verbessert, dass oft der wirkliche Eigennutz – Projekterfolg, Abschluss einer Qualifikation, dokumentierte Zielerreichung – auf der Strecke bleibt.

Der Weg führt dann oft in die faktische Abhängigkeit von einer Einrichtung, einer Szene, Clique oder bestimmten Personen, die zwar in aller Regel wohlwollende, akademisch anregende und in jeder Hinsicht nette, liebenswerte Zeitgenossen, Weg-, Lebens- und Leidensgefährten sind. Aber was ist, wenn sich Wege trennen, Finanzierungen wegfallen, ein Antrag scheitert? Was nützt die schönste Nische, wenn am Ende das Geld beim allerallereinzigen Konkurrenten in diesem speziellen Thema landet? Mentor / Mentorin versterben, emeritiert werden, die schützende und fördernde Hand an Einfluss verliert?

Das sind keine neuen Themen, beileibe nicht. Was nur zunehmend an Gewicht gewinnt, ist, dass grundfinanzierte Wissenschaft zur Ausnahme wird, Einrichtungen mit Lehrauftrag in aller Regel eine geringe Zahl grundfinanzierter “Theoria cum Praxi”-Stellen, vulgo Professuren, haben, und der ganze andere Gemischtwarenladen von Hochdeputatsstellen abgedeckt wird. Menschen auf Qualifikationsstellen “dürfen” Lehrerfahrung sammeln und hören in der Regel, dass sie das nicht müssen und es vielleicht auch besser wäre, wenn sie nicht lehren würden. Aber nachgewiesene Lehre sei am Ende auch ein Vorteil im Lebenslauf… manch einer wird das Spiel kennen: Wägen Sie Für und Wider ab…

Wissenschaft ist etwas, was man mit Spaß und Idealismus macht, und die Leidenschaft für die Sache setzt dem Druckbehälter einen natürlichen Deckel auf: Wer drin ist, dabei ist, Anerkennung erfährt, hört selten aus eigenem Antrieb auf. Und obwohl der “Funnel” nach oben massiv eng wird (und das ist wahrscheinlich noch untertrieben – die Zahl der echten Dauerstellen “meiner” Wissenschaft dürfte vergleichen mit Absolventenzahlen, Doktoranden- und Postdoc-Zahlen ziemlich überschaubar sein). Und mal ehrlich – wer promoviert wirklich nur für den Titel? Und macht dann was damit? Anfang 30, Doktortitel, zwei Jahre Volo für einen Tausender auf die Kralle?

Das kommt nicht von ungefähr, dass sich in diesem “Funnel” zuerst der Kinderwunsch verabschiedet, dann je Karrierestufe der Frauenanteil schwindet und irgendwann die Leidenschaft für die Wissenschaft.

Bemerkenswert – und irgendwer mag bitte mal den Link zur entsprechenden Studie in die Kommentare werfe, wenn sie/er ihn kennt und findet: Im Vergleich zu Wissenschaftlerstellen haben Hochschulen wie außeruniversitäre Institute sehr wohl Geld für Dauerstellen, und zwar in der Verwaltung. Eigentlich kurios: Immer mehr fest Angestellte verwalten die Belange der immer seltener fest Angestellten.

Und nun? 

Um die Umstände zu wissen, ist ja die eine Sache. Aber was damit anfangen? Ein paar Gedanken will ich in den Raum werfen:

  • Loyalität braucht Gegenleistung – Vermarktet das Angebot, das Ihr Arbeitgebern macht, besser
    Die meisten Menschen in der Wissenschaft können eine ganze Batterie von Dingen, für die man sonst Spezialisten teuer bezahlt. Warum verschenken? Warum außerhalb Eurer Aufgaben Dinge zusätzlich machen, die sonst teuer sind? Stellt zumindest Euch selbst öfter mal die Frage, warum, was dadurch eingespart wird? Kann ich etwas, für das Arbeitgeber außerhalb der Academia Geld bezahlen würden? Dann verfolgt diese Stränge so, dass sie Euch jederzeit zumindest die Sicherheit geben, woanders einen Job zu finden. Mehrarbeit muss sich auszahlen – oder dem Eigennutz dienen.
  • Wisse, wo Deine Zeit bleibt
    Ein Tracker wie das wunderfeine Tyme, Grindstone oder ähnliches darf auch bei Akademikern gerne laufen. Insbesondere, um Vor- und Nachbereitungszeiten zu erfassen und vieles mehr. Und wenn es nur dazu dient, zu dokumentieren, dass der Stundenlohn für Lehrauftrag X am Ende vielleicht real 1,50 Euro ist.
  • Zum Time Tracking gehört das Pendant der Arbeitsorganisation: Ein Kanban-Board wie etwa Trello. Für sich selbst zu dokumentieren, wo man steht, dass man keine Fortschritte nirgends macht, wenn man sich gleichzeitig zig Dinge “in Bearbeitung” hat und vieles mehr, ist ganz wichtig. Was nützt es, von der Postdoc-Stelle im kommenden Jahr zu träumen, wenn man die Halde für die Diss noch nicht annähernd abgeräumt hat und das vielleicht nicht sehen, vielleicht nicht eingestehen kann. Es befreit von Abhängigkeiten, weil man klarer sieht, wann man wirklich wovon abhängig ist. Und auch hier: Jede Aufgabe ist dokumentiert, und man sieht sofort, wenn man zu viel macht, was vom eigentlichen Ziel wegführt.
  • Und schlussendlich: So schlimm, wie die Lage auf dem Job-Markt auch sein mag. Nicht jeder Strohhalm ist einer, den man greifen muss. Wenn die Wissenschaft keinen Job für Euch hat, dann ist der Weg zu Ende. Vielleicht vorübergehend, vielleicht für immer. Es ist besser, das zu realisieren und selbst steuern zu können, ehe das Debakel der Arbeitsvermittlung droht.

Some catches…

Ja, ich bin nicht mehr in der Wissenschaft tätig und kenne mich entsprechend nur noch mit dem Stand von Ende 2014 aus. Und es ist ganz normal, Argumente und Diskussion, die das Selbst berühren, abzuweisen, zu kontern und Gegenargumente, Beschwichtigungen etc. zu finden. Die Geisteswissenschaften drillen darauf, Dinge zu hinterfragen, den Fehler zu finden, die eigene Meinung und Position argumentativ zu behaupten. Dass jemand meine Punkte hier Unsinn findet, “Ja, aber…” denkt und sich herausgefordert fühlt, ist einkalkuliert. Sucht beim Lesen nicht den Fehler im Detail, sondern gesteht mir sinnvollerweise die Punkte zu, die ich treffe, auch wenn ich hier und da nicht mehr 100% präzise weiß, wovon ich rede. Ich glaube, es gibt eine sachliche Grundlage für meine Gedanken im Leben der meisten Leserinnen und Leser.

 

Developer-Kickstart für richtige (und nicht richtige) Anfänger

Mag sein, dass das ein bisschen übermütig ist und ich mich um die letzte Freizeit bringe, aber ich will’s mal versuchen, ob das auch in Deutschland klappt. Ich teile Skills. Live bzw. per Webinar, für alle, die mitmachen wollen. Wir starten bei Null und lernen gemeinsam, was man braucht, um Dinge zu programmieren.  „Developer-Kickstart für richtige (und nicht richtige) Anfänger“ weiterlesen