~/blog/how-i-built-bewertiqPOSTED
von Shady Nathan Tawfik · 11. Oktober 2026 · 17 min

Wie ich Bewertiq gebaut habe: 10 Millionen Google-Bewertungen als Marktsignal

Ein Build-Log: fünf Scraper-Generationen, eine Pipeline von SQLite zum statischen Frontend und die Statistik, die aus Sternen und Löschhinweisen ein Entscheidungssystem macht.

Interaktiv[blog-2.0]

TL;DR: Bewertiq ist ein Build. Fünf Generationen von Playwright-Scrapern haben 24.981 Betriebsprofile und knapp zehn Millionen Bewertungen in eine statische Anwendung ohne Backend verwandelt. Das hier ist die Pipeline, die Datenbank und die Statistik — inklusive der Schätzung für Bewertungen, die Google still entfernt hat.

Die Ausgangslage

Jeder lokale Betrieb läuft über Google Maps, und jedes Maps-Profil ist kostenlose Marktforschung, die kaum jemand liest. Der Sternedurchschnitt und die Anzahl der Bewertungen sind öffentlich. Die Struktur darunter — wie schnell Bewertungen entstehen, wie viele entfernt wurden, wie sich ein Stadtteil vom nächsten unterscheidet — ist es nicht.

Ich wollte den Markt lesen, nicht die Sterne. Dafür brauchte ich einen großen, sauberen, verorteten Korpus deutscher Betriebe und Bewertungen und darauf ein Modell. Keine Accounts, keine API-Keys: nur das, was Google einem ausgeloggten Browser öffentlich zeigt.

Das Ergebnis steht auf bewertiq.org — 24.981 Betriebsprofile aus 30 Städten und 29 Kategorien, gestützt auf 9.999.625 ausgewertete Bewertungen, mit jedem Score im Browser des Besuchers berechnet.

Bewertiq-Banner
Bewertiq — den Markt lesen, nicht die Sterne.

Warum der Stern nicht reicht

Das Erste, was die Daten sagen: Die Leitmetrik ist gestaucht. Im vollen Korpus von 47.032 gescrapten Profilen liegen 91,9 Prozent der Betriebe zwischen 4,0 und 5,0 Sternen. Eine 4,4 von einer 4,8 zu trennen heißt, ein Lineal mit abgeriebenen Markierungen zu lesen.

Das Zweite: Der Sternedurchschnitt und die Löschhinweise sind fast unabhängig. Die Pearson-Korrelation zwischen Rating und Löschquote beträgt r = 0,058 — kein Zusammenhang. Wenn überhaupt, tragen die besser bewerteten Betriebe mehr Löschungen, einfach weil mehr Bewertungen mehr Löschpotenzial bedeuten. 89,7 Prozent der Profile haben nie eine Bewertung verloren, und rund 95 Prozent der Profile mit Löschmitteilung liegen weiterhin im Band von 4 bis 5 Sternen.

Der Stern ist also kein Transparenz-Indikator. Um die zweite Dimension zu sehen, braucht man eine Zahl, die Google einem nicht gibt: wie viele Bewertungen fehlen.

Der Scraper, fünf Generationen

Der Crawler wurde nicht einmal entworfen. Er hat sich entwickelt, jede Generation behebt den Fehler der vorigen.

  1. Sequenziell (Gen 1). Ein Browser, eine Suche nach der anderen, Ergebnis-Cards klicken, Seitenpanel öffnen. 30 Einträge pro Lauf und hohes CAPTCHA-Risiko.
  2. Card-basiert (Gen 2). Inhalte direkt aus den Ergebnis-Cards, kein Klick; das Panel öffnete sich nur für den Löschhinweis. CAPTCHA trat so häufig auf, dass es 120-Sekunden-Back-offs erzwang.
  3. Parallele Kontexte (Gen 3). Drei Playwright-Browser-Kontexte über asyncio.gather(), Stadt-Konfiguration in einem Dictionary, Checkpoint und Resume, damit ein abgebrochener Lauf dort weitermachte, wo er aufhörte.
  4. Maps-Route (Gen 4). Die Discovery wanderte von der Google-Suche zu Google Maps, was CAPTCHA fast vollständig eliminiert, danach paralleles Detail-Scraping. SQLite wechselte in den WAL-Modus, damit Schreiber und Leser sich nicht mehr blockieren.
  5. Prozess-parallel (Gen 5). Drei gleichzeitige OS-Prozesse: ein Scraper für Ratings und Löschzahlen, ein Address-Scraper in einem sichtbaren Browser und ein Cleaner, der alle fünf Minuten dedupliziert.
Stadt und Kategorie ConfigPlaywright ScraperSQLite WAL StoreClean und DedupCSV ExportStatischer React BuildBrowser rechnet Scores

Die Lehre über alle fünf Generationen: Der Engpass ist nie das Parsen, sondern das Beschaffen der Seiten. Der Wechsel von der Suche zu Maps und von einem Kontext zu mehreren Prozessen brachte mehr Durchsatz als jede Selektor-Optimierung.

Diese fünf Generationen sind auch eine Zeitleiste — und jeder Schritt war ein Kompromiss, kein reines Upgrade.

Loading diagram...
Loading diagram...
Loading diagram...

Die Pipeline selbst, Phase für Phase:

Von SQLite zum statischen Frontend

Die Datenschicht ist bewusst langweilig. Jeder Lauf normalisiert in eine einzige SQLite-Tabelle mit einer Zeile pro Betrieb: Name, Kategorie, Rating, Gesamtbewertungen, der rohe Löschhinweis, die geparste Löschspanne, Adresse und Koordinaten. Ein Cleaner entfernt Nicht-Betriebe, faltet Duplikate und korrigiert Adressen (erst über den Photon-Geocoder, dann für die Nachzügler ein kleiner Maps-Lauf, der fast 100 Prozent der fehlenden Adressen zurückholte).

Der Export schreibt dieselbe CSV an drei Orte — Projektwurzel, public/data/ und src/data/ des Frontends. Diese eine Datei ist der Vertrag zwischen Scraper und Anwendung.

Die Anwendung selbst ist ein statisches React-Bundle. Zur Laufzeit gibt es kein Backend und keine Datenbank: Der Browser lädt die CSV, reichert sie im Speicher an und rendert eine Leaflet-Karte und mehrere Chart.js-Ansichten. Einzelne Routen werden für Suchmaschinen vorgerendert, aber jede Statistik wird live aus den Rohzeilen berechnet. Genau diese Entscheidung macht den Modellteil unten ohne Server möglich.

Bewertiq — Korpus und Pipeline im Überblick
Korpus und Pipeline auf einen Blick.

Die Architektur in Diagrammen

Das Projekt hält seine Architektur als Code fest. Die folgenden Diagramme sind dieselben, die im Repository liegen — für das Web aufgeräumt, inklusive der Teile, die ein Build-Log sonst weglässt.

Die Scraper-Pipeline

Loading diagram...

Das Gesamtsystem

Loading diagram...

Eine Anfrage von Anfang bis Ende

Loading diagram...

Das Datenmodell

Loading diagram...

Nutzlast und Traffic

Loading diagram...
Loading diagram...

Das Agent-System

Loading diagram...

Schätzen, was Google verbirgt

Google veröffentlicht keine exakte Löschzahl. Es veröffentlicht einen Hinweis in einer Spanne — „21 bis 50 Bewertungen entfernt", „über 250 entfernt" — oder gar nichts. Alles Weitere hängt davon ab, diesen Text in eine Zahl zu verwandeln.

Die einfachste Schicht ist der Review Stability Score, den ich als RSI auf einer Skala von 0 bis 100 ausgebe:

RSI=max⁡(0,  100(1−DR))\text{RSI} = \max\left(0,\; 100\left(1 - \frac{D}{R}\right)\right)

Dabei ist R die Gesamtzahl der Bewertungen und D die geschätzte Zahl der entfernten Bewertungen, das Verhältnis D / R also die Löschquote und der Score schlicht der Anteil, der überlebt. Ein Profil ohne Hinweis erhält 100, ein Profil, das ein Zehntel seiner Bewertungen verloren hat, 90. RSI ist ein heuristischer Orientierungswert dafür, wie viel Löschaktivität ein Profil angezogen hat — kein Urteil über Absicht, denn der öffentliche Hinweis sagt nie, wer die Entfernung veranlasst hat.

Für die Spannen nehme ich den Mittelwert, wenn beide Grenzen existieren, und die Untergrenze, wenn die Spanne offen ist. Damit bleibt der Worst Case — die „über 250"-Hinweise — eine zensierte Variable. Statt zu raten, fitte ich eine Regression auf den Profilen, deren exakte Zahl bekannt ist, und schätze die zensierten daraus:

ln⁡(E)=β0+β1 RvR‾v (c)+β2 (S−S‾ (c))\ln(E) = \beta_0 + \beta_1\,\frac{R_v}{\overline{R}^{\,(c)}_v} + \beta_2\,(S - \overline{S}^{\,(c)})

Dabei ist R_v das Bewertungsvolumen, S das Rating und die Querstriche sind die Kategorie-Mittelwerte. Die Koeffizienten kommen aus einer gewöhnlichen Drei-Spalten-Kleinstquadrat-Schätzung, gelöst per Gauß-Elimination — klein genug für den Browser und nur auf unzensierten Zeilen trainiert. Die Vorhersage wird auf die angegebene Untergrenze angehoben, sodass das Modell nie weniger Entfernungen schätzt, als Google zugibt.

Um ein Unsicherheitsband um ein einzelnes Profil zu legen, nutze ich ein Beta-Binomial-Modell. Jede Bewertung ist ein Bernoulli-Versuch (entfernt oder nicht), also hat die Entfernungswahrscheinlichkeit eine konjugierte Posterior-Verteilung:

D∣R,θ∼Binomial(R,θ),θ∼Beta(1,1)D \mid R, \theta \sim \text{Binomial}(R, \theta), \qquad \theta \sim \text{Beta}(1,1) ⇒  θ∣D∼Beta(1+D,  1+R−D)\Rightarrow\; \theta \mid D \sim \text{Beta}(1 + D,\; 1 + R - D)

Der Posterior-Mittelwert liefert die erwartete Zahl und das Glaubwürdigkeitsintervall die Spanne. Eine hierarchische Variante zieht jedes Profil zu seinem Kategorie-Mittel, begrenzt auf zehn Pseudo-Beobachtungen, damit eine kleine Kategorie nicht dominiert. Auf Populationsebene werden dieselben 0-bis-100-Scores mit einer Beta-Regression zusammengefasst, in der die Dispersion aus Mittel und Varianz zurückgewonnen und die Lage auf der Logit-Skala modelliert wird:

ϕ=μ(100−μ)σ2−1,logit⁡(s)=ln⁡s100−s\phi = \frac{\mu(100-\mu)}{\sigma^2} - 1, \qquad \operatorname{logit}(s) = \ln\frac{s}{100 - s}

Das Logit ist wichtig, weil RSI beschränkt ist: Ein lineares Modell auf einer 0-bis-100-Proportion sagt fröhlich Werte außerhalb des Bereichs voraus, ein logit-normales kann das nicht.

Von Q zum Real-Score

RSI beantwortet „wie viel überlebt hat". Drei abgeleitete Kennzahlen beantworten „und dann".

Q normalisiert die Löschquote auf eine Fünf-Sterne-Skala, damit sie neben dem Rating stehen kann:

Q=5−4×min⁡(1,  deletedRate0.10)Q = 5 - 4 \times \min\left(1,\; \frac{\text{deletedRate}}{0.10}\right)

Zehn Prozent Löschungen sind der Cap: Kein Hinweis ergibt 5,0, eine Fünf-Prozent-Quote 3,0 und alles bei oder über zehn Prozent 1,0. Der Cap ist eine bekannte Grenze — Q ist eine Ampel für „aufgefallen oder nicht", kein Maß für die Effektstärke. Oberhalb von zehn Prozent muss man die Rohquote vergleichen.

Der Real-Score fragt, welchen Durchschnitt ein Betrieb hätte, wenn die entfernten Bewertungen noch da wären:

Real(a)=Sv Nv+a DeNv+De,a∈[1,2]\text{Real}(a) = \frac{S_v\,N_v + a\,D_e}{N_v + D_e}, \qquad a \in [1, 2]

mit S_v als sichtbarem Rating, N_v als sichtbarer Anzahl und D_e als geschätzten Entfernungen. Die Sterne der entfernten Bewertungen sind nicht rekonstruierbar, also gebe ich statt eines festen Werts einen Korridor aus: a = 1 nimmt an, jede Entfernung war eine Ein-Stern-Bewertung (größtmöglicher Effekt), a = 2 den Mittelwert des Ein-bis-Zwei-Stern-Segments (kleinerer Effekt). Das Ergebnis ist ein Intervall, kein Punkt — und weil die Zahl selbst unsicher ist, wird das Intervall meist von der unbekannten Anzahl dominiert, nicht vom unbekannten Sternwert. Für 93 Prozent der Profile bewegt Googles Spanne das Ergebnis stärker als die gesamte a-Spanne.

Der konservative Sonderfall ist der Bereinigt-Score im Worst Case, in dem jede entfernte Bewertung als ein Stern angenommen wird:

Sclean=Sv Nv+1⋅DeNv+DeS_{\text{clean}} = \frac{S_v\,N_v + 1 \cdot D_e}{N_v + D_e}

Das Composite C ist der Mittelwert aus sichtbarem Rating und Q und gibt offen zu, dass es Qs Cap erbt:

C=Sv+Q2C = \frac{S_v + Q}{2}

Die Transparenz-Skala

Q kollabiert alles oberhalb von zehn Prozent, was es für ein Ranking der stärksten Profile unbrauchbar macht. Die Transparenz-Skala behebt das, indem sie beide Dimensionen über ihre realistischen Bereiche statt über ihre vollen Spannen dehnt.

Das Rating wird aus seinem gestauchten Band von 4,0 bis 5,0 auf eine volle Sterneskala abgebildet:

R∗=1+4 (S−4),S∈[4.0, 5.0]R^{*} = 1 + 4\,(S - 4), \qquad S \in [4.0,\, 5.0]

Eine 4,0 wird also zu 1,0, eine 4,5 zu 3,0 und eine 5,0 bleibt 5,0. Ratings unter 4,0 unterscheiden bereits, also laufen sie unverändert durch.

Aus der Löschquote werden Transparenz-Sterne, mit derselben Dehnung von 0 bis 10 Prozent:

T⋆=5−4×min⁡(1,  deletedRate0.10)T_{\star} = 5 - 4 \times \min\left(1,\; \frac{\text{deletedRate}}{0.10}\right)

Gegeneinander aufgetragen ergeben die beiden Achsen eine 2x2-Matrix mit einem klaren Elite-Quadranten — hohes R* und perfektes T★ —, der 22,5 Prozent der Stichprobe hält (2.934 Betriebe), einer „transparent und solide"-Mehrheit von 67,2 Prozent und einer kleinen „Lösch-Verdacht"-Ecke von 2,3 Prozent: gut bewertet, aber stark bereinigt. Der einzelne Composite lautet:

C=R∗+T⋆2C = \frac{R^{*} + T_{\star}}{2}

Keine dieser Transformationen berührt die gespeicherten Daten. Die CSV behält Googles Originale; die Umrechnung passiert zur Anzeigezeit. So bleiben die strukturierten Daten (das aggregateRating, das Google liest) wahr und die Darstellung lesbar.

Die Enhanced Formula und die 19 Variablen

Das Hauptmodell zur Schätzung einer zensierten Zahl ist eine Regression mit 19 Features. Achtzehn Prädiktoren beschreiben den Betrieb und seinen Markt; der neunzehnte Term ist das Alter, und er geht logarithmisch ein, weil jedes zusätzliche Jahr weniger Information trägt als das vorige — eine Halbwertszeit von etwa fünf Jahren:

E^=exp⁡(β0+∑i=118βixi)+log⁡2(max⁡(t,1))\hat{E} = \exp\left(\beta_0 + \sum_{i=1}^{18} \beta_i x_i\right) + \log_2(\max(t,1))

Die Prädiktoren fallen in fünf Gruppen:

GruppeVariablen
KernRating, Logarithmus des Bewertungsvolumens, Kategorie-Faktor
LageEinwohnerdichte, geodätischer Standort-Cluster
WirtschaftKaufkraft, Gewerbesteuer-Hebesatz, Arbeitslosenquote
ÖffnungszeitenÖffnungsdauer, Sonntagsöffnung, Spätöffnung
Profil und MarktWettbewerbsdichte, Parkplätze, Fußgängerzone, ÖPNV, Tourismus, Gründungssaison, Social Media

Der Kategorie-Faktor und der Standort-Cluster sind die beiden, die praktisch am meisten leisten: Ein Kinderarzt und ein Club mit derselben Postleitzahl sind nicht vergleichbar, und das muss das Modell wissen, bevor es sie vergleicht.

Illustration der Kategorie Auto
Auto
Illustration der Kategorie Recht
Recht
Illustration der Kategorie Gesundheit
Gesundheit
Illustration der Kategorie Tierarzt
Tierarzt
Illustration der Kategorie Mode
Mode
Illustration der Kategorie Fahrschulen
Fahrschulen
Illustration der Kategorie Agenturen
Agenturen
Illustration der Kategorie Vintage
Vintage
Illustration der Kategorie Städte
Städte

Eine Auswahl der Kategorie-Illustrationen aus der App.

Was ein statisches Frontend rechnen kann

Der Grund, warum all das im Browser läuft, ist eine einzelne Architektur-Entscheidung: den rohen Korpus ausliefern, nicht die Schlussfolgerungen. Das Bundle lädt eine CSV, und jede Regression, jedes Glaubwürdigkeitsintervall und jede Stern-Transformation ist eine reine Funktion über Arrays. Der einzige Server ist der statische Dateihost.

Das ist nicht nur eine Kostenfrage. Es bedeutet, dass es keinen Scoring-Service zum Drosseln gibt, keinen API-Key zum Rotieren und keine Pipeline, die zwischen zwei Seitenaufrufen still eine Zahl ändern kann. Der Preis ist die Nutzlast: Der Korpus ist groß, also cacht die App die angereicherten Zeilen in IndexedDB und rechnet nur das neu, was eine bestimmte Ansicht braucht.

Listenansicht — jeder Betrieb nach der Transparenz-Skala sortiert.
Listenansicht mit offengelegter Löschungsquote.

Die Anwendung ist ein statisches Bundle: Der Browser lädt den Korpus und berechnet jeden Score selbst.

Kartenansicht — der Korpus über Bielefeld georeferenziert.
Analyse-Ansicht — Kennzahlen und Verteilungen, im Browser berechnet.

Grenzen

Der Datensatz ist ein Querschnitt, kein Panel. Er hält eine Momentaufnahme jedes Profils fest, mit der Bewertungshistorie nur als Proxy für die Zeit. Löschzahlen sind Spannen, keine ganzen Zahlen, und der „über 250"-Ausläufer wird modelliert statt beobachtet. Der Löschhinweis nennt nie, wer eine Entfernung veranlasst hat, also ist eine hohe Löschzahl ein Signal über Sichtbarkeit, nicht über Fehlverhalten — die Profile mit den meisten Entfernungen sind oft das Ziel von Kampagnen statt ihr Urheber. Und jeder Score ist eine Wahrscheinlichkeitsaussage, keine Garantie: Eine Datenplattform kann sagen, wie ein Markt aussieht, nicht, was ein bestimmter Betrieb morgen tun wird.

Fazit

  • Der Stern ist gestaucht und unkorreliert mit Löschungen; die fehlenden Bewertungen sind die zweite Achse.
  • Zu schätzen, was eine Plattform verbirgt, heißt, die bekannte Population zu fitten und die zensierte vorherzusagen, statt zu raten.
  • Beta-Binomial-Posteriors machen aus einer Spanne ein Glaubwürdigkeitsintervall, und das Logit hält einen beschränkten Score beschränkt.
  • Den Effekt als Korridor berichten und offen sagen, was die Zahl nicht beweisen kann.
  • Ein Korpus plus reine Funktionen reichen, um einen vollen Modell-Stack ganz ohne Backend zu betreiben.
Shady Nathan Tawfik

Gründer & Cyber IT Security Engineer in Bielefeld. Offensive Security, Audits und Fullstack-Entwicklung. Mehr auf

Nur zu Bildungszwecken. Alle reproduzierbaren Befehle zielen auf meine eigene Lab-Umgebung (localhost), nie auf ein laufendes System. © 2026