Umsetzungsvorschläge zur API-Spezifikation¶
Konkrete Erweiterungsvorschläge zur TBA3 API-Spec v1.1, basierend auf den offenen Fragen (
api_fragen.md) und dem Audit der Frontend-Komponenten.Stand: 2026-03-26
1. Schulamt-/Bezirks-Hierarchie¶
Empfehlung: ?type=state,district nutzen + groupType-Feld¶
Die Spec kennt bereits den Example-Wert state,district für den type-Parameter auf
/states/{id}/*. Value-Groups im Response repräsentieren dann sowohl Landes- als auch
Bezirksdaten. Problem: Es fehlt ein Diskriminator-Feld, um die Value-Groups zu unterscheiden.
Vorgeschlagene Spec-Änderung¶
Neues optionales Feld groupType im value-group-Schema:
value-group:
type: object
required:
- name
properties:
id:
type: string
name:
type: string
groupType:
type: string
description: >
Typ der Value-Group zur Unterscheidung bei kombinierten type-Abfragen.
Ermöglicht dem Client, Landes- von Bezirksdaten zu trennen.
enum:
- state
- district
- school
- group
- student
covariates:
# ... (bestehend)
properties:
# ... (bestehend)
Beispiel-Response: /states/nrw/competence-levels?type=state,district¶
[
{
"id": "nrw",
"name": "Nordrhein-Westfalen",
"groupType": "state",
"competenceLevels": [
{ "nameShort": "Ib", "descriptiveStatistics": { "frequency": 312, "mean": 0.05 } }
]
},
{
"id": "sa-1",
"name": "Stadt Bottrop",
"groupType": "district",
"competenceLevels": [
{ "nameShort": "Ib", "descriptiveStatistics": { "frequency": 42, "mean": 0.06 } }
]
},
{
"id": "sa-2",
"name": "Stadt Gelsenkirchen",
"groupType": "district",
"competenceLevels": [
{ "nameShort": "Ib", "descriptiveStatistics": { "frequency": 38, "mean": 0.04 } }
]
}
]
Frontend-Mapping¶
Das Frontend filtert nach groupType:
const allGroups = await statesService.getCompetenceLevels(stateId, 'state,district');
const stateData = allGroups.filter((g) => g.groupType === 'state');
const districtData = allGroups.filter((g) => g.groupType === 'district');
Langfristiger Ausblick¶
Wenn die Schulamt-Ebene komplexer wird (eigene Aggregationen, Detailansichten), wäre ein
dediziertes Endpoint-Level /authorities/{id}/* die sauberere Lösung — analog zu den bestehenden
/groups, /schools, /states-Levels.
Stand im Prototyp (Mai 2026)¶
Die Detail-Sichten verlangen mittlerweile zwei zusätzliche Aufsichts-Ebenen mit eigener
Identität (Bezirk + Schulamt), beide mit Aggregationen analog zu /states/{id}/*. Daher wurde im
Prototyp das langfristig empfohlene Pattern bereits umgesetzt:
| Ebene | Pfad | Status in der Spec |
|---|---|---|
| Bezirk | /districts/{id}/* |
fehlt — lokal über DistrictsApiService gemockt |
| Schulamt | /authorities/{id}/* |
vorhanden in der Spec (generierter AuthoritiesService) |
Empfehlung an die Spec: /districts/{id}/* analog zu /authorities/{id}/* ergänzen
(competence-levels / aggregations / items). Der lokale DistrictsApiService ist signaturgleich
zum generierten StatesService/AuthoritiesService aufgebaut, sodass ein reiner Import-Swap
ausreicht, sobald die Spec nachzieht.
2. Schulliste / Schulübersicht¶
Empfehlung: ?type=schools auf States-Endpoints + Organisations-Metadaten via properties¶
Bestehendes Muster: Klassen einer Schule¶
Die Spec löst die Beziehung Schule→Klassen bereits implizit: Beim Abruf von
/schools/{id}/competence-levels kommen Klassen als Einträge im comparison-Array zurück. Es gibt
keinen dedizierten /schools/{id}/groups-Endpoint.
Analog sollten Schulen eines Bezirks/Landes funktionieren: Über ?type=schools kommen Schulen
als eigene Value-Groups im Response zurück. Organisatorische Metadaten (Schulamt-Zuordnung,
Klassenanzahl, Teilnahmequote) werden über das properties-Feld transportiert.
Vorgeschlagene Spec-Änderung¶
Neuer type-Example-Wert schools auf /states/{id}/*-Endpoints:
# Ergänzung der type-Examples auf /states/{id}/*
parameters:
- name: type
in: query
schema:
type: string
examples:
Lerngruppe:
value: ''
Schulen:
value: 'schools'
description: 'Eine Value-Group pro Schule mit Leistungsdaten'
Land und Schulamtsbezirke:
value: 'state,district'
Beispiel-Response: /states/nrw/aggregations?type=schools¶
[
{
"id": "s-1",
"name": "Grundschule Am Stadtpark",
"groupType": "school",
"properties": [
{ "key": "authorityId", "value": "sa-1" },
{ "key": "authorityName", "value": "Stadt Bottrop" },
{ "key": "klassen", "value": "4" },
{ "key": "teilnehmende", "value": "87" },
{ "key": "teilnahmequote", "value": "95.6" }
],
"aggregations": [
{
"type": "competence",
"value": "Deutsch",
"descriptiveStatistics": { "mean": 0.78 }
},
{
"type": "competence",
"value": "Mathematik",
"descriptiveStatistics": { "mean": 0.72 }
}
]
}
]
Frontend-Mapping¶
interface SchoolOverviewRow {
id: string;
name: string;
authorityName: string; // aus properties["authorityName"]
klassen: number; // aus properties["klassen"]
teilnehmende: number; // aus properties["teilnehmende"]
teilnahmequote: number; // aus properties["teilnahmequote"]
minStDeutsch: number; // aus aggregations[type=competences, value=Deutsch].mean * 100
minStMathematik: number; // aus aggregations[type=competences, value=Mathematik].mean * 100
}
Hinweis¶
Schulmetadaten (Adresse, Schultyp, Schulamt-Zuordnung) kommen über properties —
analog zu Klassen, die implizit über comparison der Schuldaten gelöst sind.
3. Übersichts-Metadaten (Schulaufsicht)¶
Empfehlung: properties-Feld auf der Landes-Value-Group¶
Das properties-Feld ist laut Spec für "zusätzliche Informationen, z.B. Testdauer, andere
systemspezifische Attribute" vorgesehen — Teilnahme-Metadaten passen hier gut.
Vorgeschlagene Konvention für properties-Keys¶
| Key | Typ | Beschreibung |
|---|---|---|
schulenSoll |
number | Erwartete Anzahl Schulen (angemeldet) |
schulenIst |
number | Tatsächlich teilgenommene Schulen |
angemeldeteTeilnehmende |
number | Registrierte Schüler:innen |
teilgenommenTeilnehmende |
number | Tatsächlich teilgenommene Schüler:innen |
Hinweis: properties.value ist laut Spec ein string. Numerische Werte werden als String
transportiert und im Frontend geparst.
Beispiel-Response: /states/nrw/aggregations?type=state¶
[
{
"id": "nrw",
"name": "Nordrhein-Westfalen",
"groupType": "state",
"properties": [
{ "key": "schulenSoll", "value": "125" },
{ "key": "schulenIst", "value": "118" },
{ "key": "angemeldeteTeilnehmende", "value": "4250" },
{ "key": "teilgenommenTeilnehmende", "value": "3990" }
],
"covariates": [
{ "type": "gender", "value": "female", "descriptiveStatistics": { "frequency": 1950 } },
{ "type": "gender", "value": "male", "descriptiveStatistics": { "frequency": 1980 } },
{ "type": "gender", "value": "diverse", "descriptiveStatistics": { "frequency": 60 } },
{
"type": "languageAtHome",
"value": "german",
"descriptiveStatistics": { "frequency": 2790 }
},
{ "type": "languageAtHome", "value": "other", "descriptiveStatistics": { "frequency": 1200 } }
]
}
]
Frontend-Mapping¶
function parseOverviewMetadata(group: ValueGroup): OverviewMetadata {
const props = new Map(group.properties?.map((p) => [p.key, p.value]));
return {
schulenSoll: Number(props.get('schulenSoll')),
schulenIst: Number(props.get('schulenIst')),
angemeldeteTeilnehmende: Number(props.get('angemeldeteTeilnehmende')),
teilgenommenTeilnehmende: Number(props.get('teilgenommenTeilnehmende')),
};
}
4. type-Parameter: Definierte Werte und Response-Varianten¶
Empfehlung: type als Enum definieren, pro Endpoint-Level dokumentieren¶
Vorgeschlagene Spec-Änderung¶
parameters:
type-groups:
name: type
in: query
schema:
type: string
enum:
- ''
- students
- group,students
description: 'Bestimmt die Granularität der Value-Groups im Response.'
type-schools:
name: type
in: query
schema:
type: string
enum:
- ''
- students
description: 'Bestimmt die Granularität der Value-Groups im Response.'
type-states:
name: type
in: query
schema:
type: string
enum:
- ''
- district
- schools
- state,district
description: 'Bestimmt die Granularität der Value-Groups im Response.'
Response-Varianten Übersicht¶
| Endpoint-Level | type-Wert |
Value-Groups repräsentieren | groupType-Wert |
|---|---|---|---|
/groups |
"" (default) |
Die Lerngruppe selbst | group |
/groups |
students |
Einzelne Schüler:innen | student |
/groups |
group,students |
Gruppe + Schüler:innen kombiniert | group/student |
/schools |
"" (default) |
Die Schule selbst | school |
/schools |
students |
Aggregiert pro Schüler:in | student |
/states |
"" (default) |
Das Land selbst | state |
/states |
district |
Einzelne Schulamtsbezirke | district |
/states |
schools |
Einzelne Schulen | school |
/states |
state,district |
Land + Bezirke kombiniert | state/district |
5. aggregation-Parameter: Erlaubte Werte dokumentieren¶
Problem¶
Die Spec dokumentiert nur competence und gender als Beispielwerte für den aggregation-Parameter
auf den /aggregations-Endpoints. Das Frontend nutzt aber weitere Werte, die vom Backend unterstützt
werden müssen. Ohne explizite Dokumentation ist unklar, welche Werte das Backend akzeptiert.
Ist-Zustand¶
aggregation-Wert |
In Spec dokumentiert? | Genutzt von |
|---|---|---|
competence |
Ja (Beispiel) | Class-Teacher, School-Manager, Student-Detail |
gender |
Ja (Beispiel) | (nicht aktiv genutzt) |
exercise |
Nein | Class-Teacher, School-Manager, Student-Detail |
competenceLevel |
Nein | Class-Teacher (Lösungsquoten) |
domain |
Nein | Class-Teacher (Schüler-Aggregation) |
Vorgeschlagene Spec-Änderung¶
Den aggregation-Parameter als Enum definieren mit allen unterstützten Werten:
parameters:
aggregation:
name: aggregation
in: query
description: >
Filtert, welche Aggregationen berechnet und zurückgegeben werden.
Bestimmt die Gruppierung der Lösungshäufigkeiten.
schema:
type: string
enum:
- competence
- exercise
- competenceLevel
- domain
Bedeutung der Werte¶
| Wert | Beschreibung | Beispiel-Response |
|---|---|---|
competence |
Lösungshäufigkeiten gruppiert nach Kompetenzbereich (Domäne) | Leseverstehen: mean 0.72, Sprechen: mean 0.65 |
exercise |
Lösungshäufigkeiten gruppiert nach Aufgabe/Testlet | "Im Ferienlager": mean 0.68, "Am Strand": mean 0.55 |
competenceLevel |
Lösungshäufigkeiten gruppiert nach Kompetenzstufe (I–V) | Stufe I: mean 0.25, Stufe III: mean 0.65, Stufe V: mean 0.92 |
domain |
Lösungshäufigkeiten pro Domäne (ähnlich competence, für studentenbezogene Sicht) |
Wird bei type=students genutzt für individuelle Domänen-Werte |
Beispiel-Response: /groups/1/aggregations?aggregation=exercise¶
[
{
"type": "exercise",
"value": "Im Ferienlager",
"description": "Aufgabe 1",
"descriptiveStatistics": { "mean": 0.68, "total": 5 },
"comparison": [{ "name": "Landesmittelwert", "descriptiveStatistics": { "mean": 0.62 } }]
},
{
"type": "exercise",
"value": "Am Strand",
"description": "Aufgabe 2",
"descriptiveStatistics": { "mean": 0.55, "total": 4 },
"comparison": [{ "name": "Landesmittelwert", "descriptiveStatistics": { "mean": 0.58 } }]
}
]
Beispiel-Response: /groups/1/aggregations?aggregation=competenceLevel¶
[
{
"type": "competenceLevel",
"value": "Ia",
"descriptiveStatistics": { "mean": 0.15, "frequency": 2 }
},
{
"type": "competenceLevel",
"value": "II",
"descriptiveStatistics": { "mean": 0.45, "frequency": 5 }
},
{
"type": "competenceLevel",
"value": "III",
"descriptiveStatistics": { "mean": 0.65, "frequency": 8 }
},
{
"type": "competenceLevel",
"value": "V",
"descriptiveStatistics": { "mean": 0.92, "frequency": 3 }
}
]
Hinweis: aggregation vs. Covariaten¶
Aggregationswerte beschreiben die Gruppierung der Lösungshäufigkeiten — sie definieren, wonach
die Ergebnisse aufgeschlüsselt werden. Covariaten (gender, ses, languageAtHome) sind dagegen
Merkmale der Value-Group und werden über das covariates-Array transportiert, nicht über den
aggregation-Parameter.
6. SES-Stufen A–E: Wertebereich und Bedeutung dokumentieren¶
Problem¶
Im Schema characteristic (Spec-Zeilen 434–457) ist value: string ohne Enum definiert.
Das einzige SES-Beispiel verwendet den Wert niedrig (Zeile 456). Im Projekt nutzen wir aber
Buchstaben-Codes A–E und mappen diese intern auf:
| Code | Bedeutung |
|---|---|
| A | sehr niedrig |
| B | niedrig |
| C | mittel |
| D | hoch |
| E | sehr hoch |
Diese Konvention ist nirgends spec-seitig festgehalten. Verschiedene Backends könnten
abweichende Werte (niedrig/mittel/hoch als Volltext oder andere Codes) liefern und damit
die Komponentenbibliothek brechen.
Vorgeschlagene Spec-Änderung¶
Das characteristic-Schema um eine optionale, wohldefinierte SES-Codierung erweitern, oder ein
ergänzendes Schema ses-characteristic mit Enum:
characteristic:
# ... bestehend ...
example:
type: SES
label: Sozioökonomischer Status
value: D
description: >
Für SES gelten die Werte A (sehr niedrig), B (niedrig), C (mittel), D (hoch),
E (sehr hoch). Andere Kovariate-Typen können beliebige Werte führen.
Frontend-Mapping¶
SES_LABELS und SES_ORDER in group-summary.ts und school-summary.ts setzen die obige
Konvention um. Sobald die Spec den Wertebereich verbindlich dokumentiert, kann das Mapping
zentralisiert (z.B. in shared/config/ses.config.ts) werden.
7. descriptiveStatistics.frequency für demografische Aggregationen klären¶
Problem¶
Spec definiert frequency (Zeilen 583–584) im Kontext von Lösungs-/Items-Statistiken — also als
„Anzahl richtig gelöster Items". Für demografische Aggregationen wie type=ses (oder gender,
languageAtHome) wird das Feld jedoch zweckentfremdet als „Anzahl Personen pro Kategorie"
genutzt.
Beispiel aus /Users/yannick/workspaces/tba3-evaluation/src/assets/mock-api/schools/aggregations.json:73–116:
Hier ist frequency = 18 die Anzahl Schüler:innen mit SES=A, nicht eine Lösungshäufigkeit.
Ein generisches Backend könnte frequency für demografische Aggregationen anders interpretieren.
Vorgeschlagene Spec-Änderung¶
Entweder:
Option A: descriptive-statistics-Schema um eine konsumentenneutrale Beschreibung erweitern:
descriptive-statistics:
properties:
frequency:
type: integer
description: >
Häufigkeit des betreffenden Werts. Im Kontext von Lösungsstatistiken: Anzahl
richtig gelöster Items. Im Kontext demografischer Aggregationen: Anzahl Personen
in der jeweiligen Kategorie.
Option B: Eigenes Schema count-statistics für demografische Aggregationen einführen, das
explizit count (statt frequency) trägt.
Option A ist abwärtskompatibel, Option B sauberer. Empfehlung: Option A zuerst, Option B als spätere Migration.
8. competence-level: Natürliche Sortierung mitliefern¶
Problem¶
Stufen werden im Frontend regelmäßig schwach→stark sortiert dargestellt
(z.B. Bucket-Reihen, Heatmap-Farben, Stufen-Verteilungen). Die Spec liefert
heute nur nameShort ("I", "Ia", "A2.1", …) und name — keine
Sortierinformation. nameShort-Strings lassen sich nicht zuverlässig in eine
Reihenfolge bringen (römische Zahlen, gemischte Schemata pro Fach, GER-Stufen
für Sprachen).
Folge: Jede konsumierende Anwendung muss die Reihenfolge je Fach manuell
kuratieren (siehe src/assets/competence-level-order.json im Frontend).
Vorgeschlagene Spec-Änderung¶
competence-level-Schema um ein optionales Feld level ergänzen, das die
natürliche Sortierung trägt (1 = schwächste Stufe, aufsteigend):
competence-level:
type: object
required:
- nameShort
properties:
id: { type: string }
name: { type: string }
nameShort: { type: string }
description: { type: string }
level:
type: integer
minimum: 1
description: >
Natürliche Reihenfolge der Stufe innerhalb des Fachs (1 = schwächste).
Erlaubt es Konsumenten, Stufen ohne fachspezifische Sortierregel
aufsteigend zu ordnen.
Optional, abwärtskompatibel: Konsumenten ohne Sortier-Anforderung ignorieren das Feld; Konsumenten mit Sortier-Anforderung fallen bei fehlendem Feld auf eine lokale Konfig zurück.
Frontend-Mapping¶
Sobald S8 umgesetzt ist, kann competence-level-order.json im Frontend
schrumpfen (nur noch Farben statt Reihenfolge) oder ganz entfallen, wenn auch
Farben über Konvention abgeleitet werden.
9. Performance-Aggregation nach Covariate (SES, Gender, …)¶
Problem¶
Die Schulleitungs-Sicht "Schulentwicklung → Bildungsgerechtigkeit" zeigt einen SES-Gradienten: Lösungsquote pro SES-Quartil A–E. Die Spec kennt aktuell zwei Use Cases für demografische Information:
- Verteilung pro Kategorie (über
?aggregation=...-Pattern via Covariate): Wie viele Kinder sind in jeder SES-Gruppe? — heute im Mock alstype=ses,value=A,descriptiveStatistics.frequency=18. Siehe Vorschlag S6/S7. - Performance pro Kategorie: Wie gut schneiden Kinder mit SES=A ab vs. SES=E? — fehlt heute komplett.
Ohne (2) lässt sich Bildungsgerechtigkeit innerhalb einer Schule nicht aus den Standard-Endpoints ableiten. Stattdessen müsste auf Schüler:innen-Ebene aggregiert werden, was a) datenschutzkritisch ist und b) auf Schul-Endpoint-Ebene heute keinen Pfad hat.
Empfehlung: zweiter Aggregations-Achsen-Parameter byCovariate¶
Den aggregation-Parameter (S5) um einen Wert ergänzen, der eine Performance-Aggregation
anhand einer Covariate auslöst. Die Covariate wird über einen zusätzlichen Parameter spezifiziert:
parameters:
aggregation:
schema:
type: string
enum:
- competence
- exercise
- competenceLevel
- domain
- byCovariate # NEU
byCovariate:
name: byCovariate
in: query
description: >
Bei `aggregation=byCovariate` aktiv: Name der Covariate, nach der die
Lösungshäufigkeiten gruppiert werden. Folgt der Covariaten-Konvention
(siehe S6).
schema:
type: string
enum:
- ses
- gender
- languageAtHome
Alternative (kürzer, weniger explizit): Die Covariaten-Werte direkt als zulässige
aggregation-Werte zulassen — aggregation=ses liefert dann Performance pro SES-Quartil.
Frontend nutzt heute diese Variante (Mock-Datei aggregations-ses.json,
Aufruf schoolsIdAggregationsGet('1', 'ses')).
Empfehlung: explizite Variante mit byCovariate, weil sie Performance-Aggregation klar von
Verteilungs-Aggregation trennt und die zulässigen Covariaten-Werte zentral pflegbar sind.
Beispiel-Response: /schools/{id}/aggregations?aggregation=byCovariate&byCovariate=ses¶
[
{
"type": "ses",
"value": "A",
"description": "sehr niedrig",
"descriptiveStatistics": {
"total": 18,
"frequency": 18,
"mean": 0.412
},
"comparison": [
{
"name": "Vergleichsschule",
"descriptiveStatistics": { "mean": 0.548, "frequency": 7 }
},
{
"name": "Landesmittelwert",
"descriptiveStatistics": { "mean": 0.451, "frequency": 1342 }
}
]
},
{
"type": "ses",
"value": "E",
"description": "sehr hoch",
"descriptiveStatistics": { "total": 33, "frequency": 33, "mean": 0.724 },
"comparison": [
{
"name": "Vergleichsschule",
"descriptiveStatistics": { "mean": 0.694, "frequency": 14 }
}
]
}
]
mean = Lösungsquote der Schüler:innen in dieser Covariaten-Gruppe.
frequency = Anzahl Schüler:innen in dieser Gruppe (vgl. S7).
Frontend-Mapping¶
Aktuell genutzt im Schulentwicklungs-Dashboard
(src/app/school-manager/school-development/) im Block "Bildungsgerechtigkeit". Datenquelle:
Mock-Datei src/assets/mock-api/schools/aggregations-ses.json. Sobald S9 ausgeliefert ist,
muss der Mock-Aufruf umgestellt werden auf ?aggregation=byCovariate&byCovariate=ses
(heute: ?aggregation=ses).
Auswirkung auf andere Sichten¶
Dieselbe Mechanik kann für Gender-Gaps (byCovariate=gender) und Sprache-zu-Hause-Gaps
(byCovariate=languageAtHome) genutzt werden — relevant für die Schul- und Schulaufsichts-Sichten.
Die Authority-/District-Endpoints (S1) erben das Pattern automatisch, sobald sie Aggregations-Endpoints
bereitstellen.
Zusammenfassung: Benötigte Spec-Änderungen¶
| # | Änderung | Aufwand | Priorität |
|---|---|---|---|
| S1 | groupType-Feld im value-group-Schema |
Klein | Hoch |
| S2 | type-Parameter als Enum (pro Endpoint-Level) |
Klein | Hoch |
| S3 | type=schools Example für /states-Endpoints |
Klein | Mittel |
| S4 | properties-Keys für Teilnahme-Metadaten |
Doku | Mittel |
| S5 | aggregation-Werte als Enum dokumentieren |
Doku | Mittel |
| S6 | SES-Stufen A–E als Konvention spezifizieren |
Doku | Hoch |
| S7 | frequency-Semantik für demografische Aggregationen klären |
Doku | Mittel |
| S8 | competence-level.level für natürliche Sortierung |
Klein | Mittel |
| S9 | Performance-Aggregation nach Covariate (byCovariate=ses/gender/…) |
Klein | Hoch |