Dokumentation der Datenauswertung

Daten und Helpers importieren

Die Daten aus mehreren Exports von Prakti wurden in data/index.js zusammengeführt und vorbereitet.

In [1]:
const _ = require("lodash");
const { lessons, events } = require("./data");
const {
  getNumberOfOccurences,
  getAverageAndMean,
  filterInvalidQuestions,
  sortAlphabetically,
  sortNumerically,
  sortByValueAsc,
  sortByValueDesc,
  sortByWeekday,
} = require("./src/helpers");

const { weekdays, eventTypes, blocks } = require("./src/constants");

const {
  makeChart,
  makeBarChart,
  makeMultiBarChart,
  makeFrequencyDiagram,
  makeAvgAndMeanDiagram,
} = require("./src/charts");

Datenverarbeitung

Die Daten wurden mit Hilfe von Prakti aufgezeichnet. Der Export besteht aus einer Liste von Events mit dazugehörigem Timestamp (einer absoluten Zeitangabe in Millisekunden):

> NEW_SUBJECT - 1567503251097
> NEW_QUESTION_ASKED - 1567505950365
> FIRST_STUDENT_RAISED_HAND - 1567505951876
> FIRST_STUDENT_ANSWERED_QUESTION - 1567505960413
> NEW_QUESTION_ASKED - 1567505995484
> ...

In data/index.js wird diese Liste nun in logische Einheiten geordnet: in Unterrichtsstunden und Fragen. Da Prakti seinen Zustand als endlichen deterministischen Automaten) modelliert und wir die Zustandsübergänge (Events, dargestellt an den Pfeilen) protokolliert haben, ist es möglich, bestimmten Mustern von Events verschiedene Bedeutungen zuzuordnen.

Screenshot%202020-02-22%20at%2014.14.59.png

So wissen wir, dass immer wenn ein NEW_SUBJECT-Event protokolliert wurde, alle nachfolgenden Events bis zum nächsten NEW_SUBJECT-Event zu diesem Fach und zu dieser Lehrkraft gehören.

Gleichzeitig wissen wir, dass eine neue Frage immer mit einem Event beginnt, das in den Zustand newQuestionAsked führt. Also werten wir sowohl das Stellen einer neuen Frage (NEW_QUESTION_ASKED) als auch das Vereinfachen (TEACHER_SIMPLIFIED_QUESTION) und das Stellen einer Alternativfrage (TEACHER_POSED_NEW_QUESTION) als eine neue Frage.

Es ist auch möglich, dass eine Frage nicht durch eine SchülerIn beantwortet wird (bspw. wenn die Lehrkraft die Frage selbst beantwortet (TEACHER_ANSWERED_QUESTION). Somit ist es möglich zwischen beantworteten und nicht beantworteten (abgebrochenen) Fragen zu unterscheiden.

Insgesamt haben wir nun also Liste an Unterrichtsstunden welche wiederum eine Liste der darin gestellten Fragen enthält:

> Stunde
    > Frage
    > Frage
    > Frage
> Stunde
    > Frage
    > Frage
    > ...
> ...

Innerhalb der Datenverarbeitung wurden bereits bestimmte Werte aggregiert. Beispielsweise wurde für jede Frage bestimmt,

  • ob sie beantwortet wurde (wasAnswered) (und falls nicht warum sie abgebrochen wurde (reasonCanceled))
  • wie viel Zeit vergangen ist, bis die Frage beantwortet wurde oder abgebrochen wurde (timeUntilFinished)
  • wie viel Zeit vergangen ist, bis die Lehrkraft das erste mal versucht, die SchülerInnen zu motivieren (timeUntilTeacherMotivatesForFirstTime)
  • wie viel Zeit vergangen ist, bis sich die erste SchülerIn meldete (timeUntilFirstStudentRaisesHand)
  • wie viel Zeit vergangen ist zwischen "die erste SchülerIn meldet sich" und "die Frage wird beantwortet / abgebrochen" (timeBetweenFirstStudentRaisesHandAndQuestionFinished)
  • ob die erste SchülerIn auch antworten durfte (firstStudentHasAnswered)

Für jede Stunde wurde bestimmt,

  • zu welchem Fachbereich die Stunde gehört (subjectArea) (Wir verarbeiten also nicht Fächer, sondern Fachbereiche. Damit erhoffe ich mir, einfacher Korrelationen herstellen zu können)
  • welche Lehrkraft die Stunde gegeben hat (teacherPseudonym) (Für die Datenanalyse habe ich Klarnamen mit Pseudonymen ersetzt)
  • in welchem Block (block), zu welcher Zeit (time.hour und time.minute), in welcher Woche (time.weekNumber) und an welchem Wochentag (weekDay) die Stunde gegeben wurde

Der Datensatz hat also grob folgende Struktur:

[{
 subjectArea,
 teacherPseudonym,
 time: { minute, hour, weekDay }
 block,
 weekDay,
 questions: [{
     wasAnswered,
     timeUntilFinished,
     timeUntilFirstStudentRaisesHand,
     firstStudentHasAnswered,
     reasonCanceled,
     timeUntilTeacherMotivatesForFirstTime,
     timeBetweenFirstStudentRaisesHandAndQuestionFinished,
     events: [{
         id,
         action,
         time,
         diff,
         totalDiff
     }, ...]
 }, ...]
}, ...]

Auf dieser Grundlage sind wir nun in der Lage, Abfragen auszuführen. Alle Zeiten sind in Sekunden, soweit nicht anders angegeben.

Anzahl von Fragen und Unterrichtsblöcken

In [2]:
const questions = lessons
  .map(l => l.questions)
  .flat()
  .filter(filterInvalidQuestions); // one question has inconsistent data and is removed here
const questionCount = questions.length;
const lessonCount = lessons.length;
const avgQuestionCountPerLesson = _(questionCount / lessonCount).round(2);

({ lessonCount, questionCount, avgQuestionCountPerLesson });
{
  lessonCount: 23,
  questionCount: 396,
  avgQuestionCountPerLesson: 17.22
}

Anzahl beantworteter / abgebrochener Fragen

Als abgebrochene Frage zählen alle Fragen die nicht von einer SchülerIn beantwortet wurden. Das passiert, wenn die Lehrkraft reagiert (TEACHER_POSED_NEW_QUESTION, TEACHER_SIMPLIFIED_QUESTION, TEACHER_CANCELED_QUESTION, TEACHER_ANSWERED_QUESTION, TEACHER_REACTED_UNEXPECTEDLY)

In [3]:
const answeredQuestions = questions.filter(q => q.wasAnswered);
const answeredQuestionCount = answeredQuestions.length;
const percentageOfAnsweredQuestions = answeredQuestionCount / questionCount;

({
  answeredQuestionCount,
  canceledQuestionCount: questionCount - answeredQuestionCount,
  percentageOfAnsweredQuestions,
});
{
  answeredQuestionCount: 325,
  canceledQuestionCount: 71,
  percentageOfAnsweredQuestions: 0.8207070707070707
}

Wie viel Zeit lassen Lehrkräfte SchülerInnen zum Beantworten einer Frage? / Wie viel Zeit vergeht, bis die Lehrkraft eine SchülerIn nach Stellen einer Frage drannimmt?

In [4]:
const questionTimes = answeredQuestions.map(question => question.timeUntilFinished);
const { avg: avgQuestionTime, mean: meanQuestionTime } = getAverageAndMean(
  questionTimes,
);

({ avgQuestionTime, meanQuestionTime });
{ avgQuestionTime: 6.76, meanQuestionTime: 3.98 }

Die Verteilung der Fragezeiten lässt sich auch gut in einem Diagramm darstellen:

In [5]:
const roundedQuestionTimes = questionTimes.map(q => Math.round(q / 1000));
makeBarChart(getNumberOfOccurences(roundedQuestionTimes), {
  title: "Anzahl beobachteter Fragen pro Wartezeit",
  x: "Zeit in Sekunden",
  y: "Anzahl Fragen",
});

Um die Häufigkeiten noch etwas besser abschätzen, fassen wir mehrere Zeiten zu Kategorien zusammen und stellen das Ergebnis graphisch dar. Dargestellt wird immer das Interval untereGrenze ≤ x < obereGrenze, für ≤2 also 0 ≤ x < 2.

In [6]:
const bucketedValues = roundedQuestionTimes.map(time => {
  if (time < 2) return "≤2";
  if (time < 4) return "≤4";
  if (time < 6) return "≤6";
  if (time < 8) return "≤8";
  if (time < 10) return "≤10";
  if (time < 15) return "≤15";
  if (time >= 15) return "≥16";
});

makeBarChart(getNumberOfOccurences(bucketedValues), {
  title: "Anzahl beobachteter Fragen pro Wartezeit",
  x: "Zeit in Sekunden",
  y: "Anzahl Fragen",
});

Datenüberblick (Anzahlen, Häufigkeiten, Diagramme)

Hier möchte ich einen Überblick über die Verteilung der Daten bzgl. folgender Paramter bekommen:

  • Fächer
  • Lehrkräfte
  • Blöcke
  • Wochentage
  • Häufigste Reaktionen (nur an Events)
  • Häufigkeiten von Events (Actions)
In [7]:
makeFrequencyDiagram(
  lessons.map(l => l.subjectArea),
  { title: "Anzahl beobachteter Blöcke pro Fachbereich", x: "Fachbereich", y: "Anzahl beobachteter Blöcke" },
  sortAlphabetically,
);
{ Gesellschaftswissenschaften: 2, MINT: 13, Musisch: 1, Sprachen: 7 }
In [33]:
const questionsPerSubjectArea = _(lessons)
  .groupBy(l => l.subjectArea)
  .values()
  .map(arr => [arr[0].subjectArea, _(arr).map(arr2 => arr2.questions.length).sum()])
  .sort(sortNumerically)
  .fromPairs()
  .value();

console.log(questionsPerSubjectArea);

makeBarChart(questionsPerSubjectArea, {
  title: "Anzahl beobachteter Fragen pro Fachbereich",
  x: "Fachbereich",
  y: "Anzahl Fragen",
});
{
  Sprachen: 150,
  MINT: 229,
  Gesellschaftswissenschaften: 17,
  Musisch: 1
}
In [9]:
makeFrequencyDiagram(
  lessons.map(l => l.teacherPseudonym),
  {
    title: "Anzahl beobachteter Blöcke pro Lehrkraft",
    x: "Lehrkraft (Pseudonym)",
    y: "Anzahl beobachteter Blöcke",
  },
  sortAlphabetically,
);
{
  A: 2,
  B: 8,
  C: 1,
  D: 2,
  E: 1,
  F: 1,
  G: 2,
  H: 1,
  I: 1,
  J: 1,
  K: 1,
  L: 1,
  M: 1
}
In [10]:
const questionsPerTeacher = _(lessons)
  .groupBy(l => l.teacherPseudonym)
  .values()
  .map(arr => [arr[0].teacherPseudonym, _(arr).map(arr2 => arr2.questions.length).sum()])
  .sort(sortNumerically)
  .fromPairs()
  .value();

console.log(questionsPerTeacher);

makeBarChart(questionsPerTeacher, {
  title: "Anzahl beobachteter Fragen pro Lehrkraft",
  x: "Lehrkraft (Pseudonym)",
  y: "Anzahl Fragen",
});
{
  A: 17,
  B: 129,
  C: 30,
  D: 22,
  E: 13,
  F: 47,
  G: 30,
  H: 43,
  I: 32,
  J: 16,
  K: 12,
  L: 1,
  M: 5
}

Die Blockstunden haben folgende Anfangszeiten:

In [11]:
blocks
    .filter(b => b.name !== 'INVALID')
    .map(b => `${b.name} startet um ${b.hour}:${b.minute.toString().padEnd(2, "0")}`)
    .join('\n');
BLOCK-1 startet um 7:30
BLOCK-2 startet um 9:20
BLOCK-3 startet um 11:10
BLOCK-4 startet um 13:10
BLOCK-5 startet um 15:00
In [12]:
makeFrequencyDiagram(
  lessons.map(l => l.block),
  {
      title: "Anzahl beobachteter Blöcke",
      x: "Block",
      y: "Anzahl",
  },
  sortAlphabetically,
);
{
  'BLOCK-1': 4,
  'BLOCK-2': 6,
  'BLOCK-3': 5,
  'BLOCK-4': 7,
  'BLOCK-5': 1
}
In [13]:
const questionsPerBlock = _(lessons)
  .groupBy(l => l.block)
  .values()
  .map(arr => [arr[0].block, _(arr).map(arr2 => arr2.questions.length).sum()])
  .sort(sortAlphabetically)
  .fromPairs()
  .value();

console.log(questionsPerBlock);

makeBarChart(questionsPerBlock, {
  title: "Anzahl beobachteter Fragen pro Block",
  x: "Block",
  y: "Anzahl Fragen",
});
{
  'BLOCK-1': 65,
  'BLOCK-2': 82,
  'BLOCK-3': 85,
  'BLOCK-4': 154,
  'BLOCK-5': 11
}
In [14]:
makeFrequencyDiagram(
  lessons.map(l => l.weekDay),
  {
      title: "Anzahl beobachteter Blöcke pro Wochentag",
      x: "Tag",
      y: "Anzahl beobachteter Blöcke",
  },
  sortByWeekday,
);
{ Montag: 4, Dienstag: 8, Mittwoch: 7, Donnerstag: 4 }
In [15]:
const questionsPerWeekday = _(lessons)
  .groupBy(l => l.weekDay)
  .values()
  .map(arr => [arr[0].weekDay, _(arr).map(arr2 => arr2.questions.length).sum()])
  .sort(sortByWeekday)
  .fromPairs()
  .value();

console.log(questionsPerWeekday);

makeBarChart(questionsPerWeekday, {
  title: "Anzahl beobachteter Fragen pro Wochentag",
  x: "Wochentag",
  y: "Anzahl Fragen",
});
{ Montag: 98, Dienstag: 173, Mittwoch: 74, Donnerstag: 52 }
In [16]:
makeFrequencyDiagram(
  lessons.map(l => l.time.weekNumber),
  {
      title: "Anzahl beobachteter Blöcke pro KW",
      x: "KW",
      y: "Anzahl beobachteter Blöcke",
  },
  sortNumerically,
);
{ '36': 4, '37': 8, '38': 11 }
In [17]:
const questionsPerWeek = _(lessons)
  .groupBy(l => l.time.weekNumber)
  .values()
  .map(arr => [arr[0].time.weekNumber, _(arr).map(arr2 => arr2.questions.length).sum()])
  .sort(sortNumerically)
  .fromPairs()
  .value();

console.log(questionsPerWeek);

makeBarChart(questionsPerWeek, {
  title: "Anzahl beobachteter Fragen pro KW",
  x: "KW",
  y: "Anzahl Fragen",
});
{ '36': 73, '37': 125, '38': 199 }

Zeit zwischen Frage und SchülerIn meldet sich

In [18]:
// XXX: for all questions
const {
  avg: avgTimeUntilFirstStudentRaisedHand,
  mean: meanTimeUntilFirstStudentRaisedHand,
} = getAverageAndMean(questions.map(q => q.timeUntilFirstStudentRaisesHand));

({
  avgTimeUntilFirstStudentRaisedHand,
  meanTimeUntilFirstStudentRaisedHand,
});
{
  avgTimeUntilFirstStudentRaisedHand: 2.84,
  meanTimeUntilFirstStudentRaisedHand: 1.6
}

Zeit zwischen Frage und SchülerIn antwortet

In [19]:
// XXX: only for answeredQuestions
const {
  avg: avgTimeUntilStudentAnswer,
  mean: meanTimeUntilStudentAnswer,
} = getAverageAndMean(answeredQuestions.map(q => q.timeUntilFinished));

({
  avgTimeUntilStudentAnswer,
  meanTimeUntilStudentAnswer,
});
{ avgTimeUntilStudentAnswer: 6.76, meanTimeUntilStudentAnswer: 3.98 }

Zeit zwischen "Erste SchülerIn meldet sich" und SchülerIn antwortet

In [20]:
// XXX: for answeredQuestions
const {
  avg: avgTimeBetweenFirstStudentRaisesHandAndQuestionFinished,
  mean: meanTimeBetweenFirstStudentRaisesHandAndQuestionFinished,
} = getAverageAndMean(
  answeredQuestions.map(
    q => q.timeBetweenFirstStudentRaisesHandAndQuestionFinished,
  ),
);

({
  avgTimeBetweenFirstStudentRaisesHandAndQuestionFinished,
  meanTimeBetweenFirstStudentRaisesHandAndQuestionFinished,
});
{
  avgTimeBetweenFirstStudentRaisesHandAndQuestionFinished: 3.53,
  meanTimeBetweenFirstStudentRaisesHandAndQuestionFinished: 1.62
}

Absolute Häufigkeiten von Events

In [21]:
makeFrequencyDiagram(
  events.map(e => e.action),
  { title: "Häufigkeit von Events", x: "Event", y: "Anzahl" },
  sortByValueDesc,
);
{
  FIRST_STUDENT_RAISED_HAND: 353,
  NEW_QUESTION_ASKED: 351,
  FIRST_STUDENT_ANSWERED_QUESTION: 190,
  SOME_STUDENT_ANSWERED_QUESTION: 136,
  TEACHER_STARTED_REACTING: 93,
  TEACHER_SIMPLIFIED_QUESTION: 42,
  NEW_SUBJECT: 23,
  TEACHER_MOTIVATED: 23,
  TEACHER_ANSWERED_QUESTION: 13,
  TEACHER_CANCELED_QUESTION: 8,
  TEACHER_POSED_NEW_QUESTION: 4,
  TEACHER_REACTED_UNEXPECTEDLY: 3,
  NONE: 2
}

Häufigkeiten von Reaktionen der Lehrkraft

In [22]:
makeFrequencyDiagram(
  events.map(e => e.action).filter(action => action.includes("TEACHER")),
  { title: "Häufigkeit von Events", x: "Event", y: "Anzahl" },
  sortByValueDesc,
);
{
  TEACHER_STARTED_REACTING: 93,
  TEACHER_SIMPLIFIED_QUESTION: 42,
  TEACHER_MOTIVATED: 23,
  TEACHER_ANSWERED_QUESTION: 13,
  TEACHER_CANCELED_QUESTION: 8,
  TEACHER_POSED_NEW_QUESTION: 4,
  TEACHER_REACTED_UNEXPECTEDLY: 3
}

Zu TEACHER_STARTED_REACTING sei gesagt, dass jede Reaktion der Lehrkraft eingeleitet wird mit TEACHER_STARTED_REACTING. Das bedeutet, dass im ganzen Datensatz immer folgendes Muster zu finden ist (siehe Automaten-Diagramm oben):

> ...
> TEACHER_STARTED_REACTING
> TEACHER_*
> ...

Wie darf die erste Person antworten, die sich meldet?

In [23]:
// XXX: for answeredQuestions
const firstStudentAnsweredCount = answeredQuestions
  .map(q => q.firstStudentHasAnswered)
  .filter(b => b).length;
const nthStudentAnsweredCount = answeredQuestionCount - firstStudentAnsweredCount;

({
  answeredQuestionCount,
  firstStudentAnsweredCount,
  nthStudentAnsweredCount,
  firstStudentAnsweredPercent: firstStudentAnsweredCount / answeredQuestionCount,
  nthStudentAnsweredPercent: nthStudentAnsweredCount / answeredQuestionCount,
});
{
  answeredQuestionCount: 325,
  firstStudentAnsweredCount: 189,
  nthStudentAnsweredCount: 136,
  firstStudentAnsweredPercent: 0.5815384615384616,
  nthStudentAnsweredPercent: 0.41846153846153844
}

Zusammenhang zwischen Variablen und Wartezeit

In [32]:
makeAvgAndMeanDiagram(lessons, "subjectArea", "Fachbereich", sortAlphabetically);
{
  avg: {
    Gesellschaftswissenschaften: 5.22,
    MINT: 8.03,
    Musisch: 5.89,
    Sprachen: 8.03
  },
  mean: {
    Gesellschaftswissenschaften: 3.71,
    MINT: 5.06,
    Musisch: 5.89,
    Sprachen: 4.76
  }
}
In [25]:
makeAvgAndMeanDiagram(lessons, "block", "Block", sortAlphabetically);
{
  avg: {
    'BLOCK-1': 8.42,
    'BLOCK-2': 6.82,
    'BLOCK-3': 10.97,
    'BLOCK-4': 6.86,
    'BLOCK-5': 3.94
  },
  mean: {
    'BLOCK-1': 6.91,
    'BLOCK-2': 3.6,
    'BLOCK-3': 7.56,
    'BLOCK-4': 4.31,
    'BLOCK-5': 2.81
  }
}
In [26]:
makeAvgAndMeanDiagram(lessons, "teacherPseudonym", "Lehrkraft", sortAlphabetically);
{
  avg: {
    A: 19.06,
    B: 8.92,
    C: 8.7,
    D: 8.18,
    E: 3.8,
    F: 5.59,
    G: 5.73,
    H: 9.23,
    I: 4.11,
    J: 7.39,
    K: 6.08,
    L: 5.89,
    M: 3.15
  },
  mean: {
    A: 14.18,
    B: 4.71,
    C: 6.5,
    D: 6.88,
    E: 3.91,
    F: 3.81,
    G: 3.75,
    H: 7.77,
    I: 3.57,
    J: 7.26,
    K: 3.74,
    L: 5.89,
    M: 3.71
  }
}

Wie viel Zeit lassen Lehrkräfte, bis sie eine Frage abbrechen?

In [27]:
const timesUntilCanceled = questions
  .filter(q => !q.wasAnswered)
  .map(q => q.timeUntilFinished);
const {
  avg: avgTimeUntilCanceled,
  mean: meanTimeUntilCanceled,
} = getAverageAndMean(timesUntilCanceled);

({
  avgTimeUntilCanceled,
  meanTimeUntilCanceled,
});
{ avgTimeUntilCanceled: 13.13, meanTimeUntilCanceled: 10.27 }

Wie viel Zeit lassen Lehrkräfte, bis sie die SchülerInnen motiviert?

In [28]:
// XXX: all questions
const motivationTimes = questions.map(
  q => q.timeUntilTeacherMotivatesForFirstTime,
);
const timesMotivated = motivationTimes.filter(t => t !== null).length;
const timesNotMotivated = questionCount - timesMotivated;
const percentageMotivated = timesMotivated / questionCount;
const percentageNotMotivated = timesNotMotivated / questionCount;
const { avg: avgTimeUntilMotivated, mean: meanTimeUntilMotivated } = getAverageAndMean(
  motivationTimes.filter(t => t !== null),
);

({
  timesMotivated,
  timesNotMotivated,
  percentageMotivated,
  percentageNotMotivated,
  avgTimeUntilMotivated,
  meanTimeUntilMotivated,
});
{
  timesMotivated: 21,
  timesNotMotivated: 375,
  percentageMotivated: 0.05303030303030303,
  percentageNotMotivated: 0.946969696969697,
  avgTimeUntilMotivated: 11.52,
  meanTimeUntilMotivated: 10.79
}

Wie oft wird eine Frage beantwortet in der die Lehrkraft die SuS motiviert hat?

In [30]:
const answeredQuestionsWithMotivation = questions.filter(
  q => q.timeUntilTeacherMotivatesForFirstTime && q.wasAnswered,
);
const timesMotivatedAndAnswered = answeredQuestionsWithMotivation.length;
const timesMotivatedAndNotAnswered = timesMotivated - timesMotivatedAndAnswered;
const timesMotivatedAndAnsweredPercent =
  timesMotivatedAndAnswered / timesMotivated;

({
  timesMotivatedAndAnswered,
  timesMotivatedAndNotAnswered,
  timesMotivatedAndAnsweredPercent,
});
{
  timesMotivatedAndAnswered: 17,
  timesMotivatedAndNotAnswered: 4,
  timesMotivatedAndAnsweredPercent: 0.8095238095238095
}