Nach der Veröffentlichung unseres Artikels über Ibis wies uns Dr. André Schemaitat auf ein ähnliches Tool mit wachsender Beliebtheit hin – Narwhals. Narwhals beschreibt sich selbst als eine „extrem leichtgewichtige und erweiterbare Kompatibilitätsschicht zwischen Dataframe-Bibliotheken“. Im Kern verwendet es eine Untermenge der Polars-API und hat keinerlei Abhängigkeiten; es nutzt nur das, was der Benutzer übergibt, damit die Bibliothek so schlank wie möglich bleibt.
Obwohl sowohl Narwhals als auch Ibis Dataframe-agnostischen Code ermöglichen, bedienen sie grundlegend unterschiedliche Zielgruppen. Ibis ist als vollständiges Analyse-Framework für Endanwender, Data Scientists und Analysten konzipiert, die damit ihre tägliche Arbeit erledigen. Narwhals hingegen wurde für Library-Maintainer und Anwendungsentwickler entwickelt, die mehrere Dataframe-Typen als Input akzeptieren möchten, ohne alle diese als Abhängigkeiten vorauszusetzen. Diese Unterscheidung prägt jeden Aspekt des Designs von Narwhals, von der minimalen API-Oberfläche bis hin zu den strikten Garantien für die Abwärtskompatibilität.
Was macht Narwhals?
Dataframe-agnostischen Code zu schreiben ist schwierig, da derselbe Ausdruck je nach Bibliothek unterschiedliche Ergebnisse liefern kann. Eine einheitliche, einfache und vorhersagbare API kann Entwicklern helfen, sich auf das Verhalten statt auf subtile Implementierungsunterschiede zu konzentrieren. Die Installation der Bibliothek selbst ist so einfach wie erwartet, und es werden keine Abhängigkeiten benötigt, abgesehen von der oder den Dataframe-Bibliotheken, die wir tatsächlich verwenden möchten. Ein weiterer interessanter Aspekt ist der Umgang von Narwhals mit dem häufigen Deprecation-Verhalten von pandas und Polars. Narwhals testet gegen Nightly-Builds beider Bibliotheken und handhabt die Abwärtskompatibilität intern, sodass sich der Benutzer nicht darum kümmern muss.
Narwhals richtet sich in erster Linie an Library-Maintainer und weniger an Endanwender. Daher nimmt es Stabilität und Abwärtskompatibilität sehr ernst. Öffentliche Funktionen in den stabilen Releases v1 und v2 werden niemals entfernt oder geändert. Wenn abwärtsinkompatible Änderungen vorgenommen werden müssen, werden diese nur in den Haupt-Namespace von narwhals und schließlich in narwhals.stable.v3 verschoben, aber v1 und v2 bleiben immer unberührt und werden unbefristet gewartet. Das bedeutet, dass verschiedene Pakete von unterschiedlichen stabilen Narwhals-APIs abhängen können und Endanwender alle davon im selben Projekt ohne Konflikte nutzen können.
Da Narwhals eine Untermenge der Polars-API implementiert und die Syntax von Polars Änderungen unterliegt, ist diese Stabilitätsgarantie besonders wertvoll. Benutzer könnten auf veraltete Funktionen stoßen (wie wir in unserem ersten Benchmark) oder sogar auf bahnbrechende Änderungen in Upstream-Bibliotheken. Narwhals schützt die Benutzer davor: Code, der mit dem stable-Namespace geschrieben wurde, funktioniert weiterhin, selbst in neueren Versionen von Polars, in denen Funktionen umgeschrieben wurden. Dieses ehrgeizige Versprechen hat seine Grenzen, die Narwhals auch einräumt. Eindeutige Bugs werden behoben, ohne dass dies als Breaking Change zählt, Type Hints können verfeinert werden, und alles, was als „unstable“ markiert ist, kann sich ändern. Die Entwickler geben außerdem an, dass Narwhals selbst überdacht werden müsste, falls Polars Ausdrücke gänzlich entfernen oder pandas die Unterstützung für kategoriale Daten einstellen würde – sie halten solche radikalen Änderungen jedoch für unwahrscheinlich. Alte Python-Versionen werden etwa zum Zeitpunkt ihres End of Life entfernt.
An diesem Punkt mag es etwas überwältigend erscheinen, da es viele verschiedene Versionen zu geben scheint. Die Dokumentation erkennt dies an und bietet Hilfe. Im Allgemeinen sollte narwhals für das Prototyping verwendet werden, damit Benutzer schnell iterieren können. Wenn das Produkt produktionsreif und stabil werden soll, sollte auf narwhals.stable.v2 umgestellt werden. Für ein völlig neues Projekt sollten Benutzer entweder narwhals oder narwhals.stable.v2 verwenden. Falls ein Projekt bereits narwhals.stable.v1 verwendet und keine neueren Features benötigt, gibt es keinen Grund, auf v2 zu wechseln. Wenn Benutzer v2 nutzen möchten, sollten sie mindestens narwhals>=2.0 voraussetzen. Die Version v1 ist älter und weist merkliche Unterschiede auf, die hier eingesehen werden können. In v2 fehlen keine Features, und alles, was im Haupt-Namespace stabil ist, ist auch in dieser Version verfügbar.
Narwhals kann über das Modul narwhals.sql SQL generieren. Derzeit erfordert dieses Modul die Installation von DuckDB. Wenn man den Parameter pretty der Funktion to_sql auf True setzt, wird das SQL in ein lesbareres Format gebracht, was jedoch wiederum die Installation von sqlparse erfordert. Da das SQL-Modul auf DuckDB basiert, folgt der generierte SQL-Code dem Dialekt von DuckDB. Um ihn in andere Dialekte zu übersetzen, können Benutzer direkt SQLGlot verwenden oder alternativ Ibis oder SQLFrame nutzen, die beide intern SQLGlot verwenden, wie wir in unserem vorherigen Artikel über Ibis besprochen haben. Diese Abhängigkeitsanforderung wird sich wahrscheinlich noch ändern, um dem Zero-Dependency-Ansatz von Narwhals gerecht zu werden, hatte aber aufgrund der unterschiedlichen Zielgruppen keine so hohe Priorität wie bei Ibis.
Narwhals vs. Ibis
Unser erster Instinkt war es, Ibis mit Narwhals zu vergleichen, da beide scheinbar dasselbe tun. Die Narwhals-Dokumentation greift dies auf und stellt interessanterweise fest, dass sie die beiden Tools als sehr unterschiedlich und nicht in Konkurrenz zueinander stehend betrachten. Narwhals unterstützt sogar Ibis-Tabellen, was bedeutet, dass Dataframe-agnostischer Code, der mit der Lazy-API von Narwhals geschrieben wurde, auch Ibis unterstützt.
Die Dokumentation skizziert mehrere wesentliche Unterschiede zwischen den beiden Tools. Grundlegend ist Narwhals für Library-Maintainer konzipiert, die Tools bauen, welche mehrere Dataframe-Typen akzeptieren müssen, während Ibis sich an Endanwender, Data Scientists und Analysten richtet, die ihre analytische Arbeit verrichten. Dieser Unterschied in der Zielgruppe zieht sich durch jede Designentscheidung.
Narwhals ermöglicht es Benutzern, Funktionen zu schreiben, die einen Dataframe entgegennehmen und einen im exakt gleichen Format zurückgeben, wobei der Eingangstyp erhalten bleibt. Ibis kann in pandas, Polars und PyArrow materialisieren, hat aber keine integrierte Möglichkeit, exakt den Eingangstyp zurückzugeben. Auf der Seite der Datentypen unterstützt Narwhals kategoriale Typen und Enums über seine Backends hinweg, während Ibis dies nicht tut. Auch die Ausführungsmodelle unterscheiden sich: Ibis konzentriert sich auf die verzögerte Ausführung (lazy execution) mit SQL-Generierung, während Narwhals zwischen Lazy- und Eager-APIs trennt, wobei die Eager-API eine sehr feine Kontrolle über Dataframe-Operationen ermöglicht.
Aus der Perspektive der Abhängigkeiten benötigt Ibis standardmäßig pandas und PyArrow für alle Backends, während Narwhals keine erforderlichen Abhängigkeiten hat – es nutzt nur das, was der Benutzer übergibt (außer bei der Konvertierung in SQL, die DuckDB erfordert). Ibis unterstützt derzeit mehr Backends (über 20 Execution Engines), aber Narwhals unterstützt weiterhin pandas und Dask, für die Ibis den Support eingestellt hat. Der vielleicht relevanteste Unterschied im täglichen Gebrauch ist die API selbst: Narwhals verwendet eine Untermenge der Polars-API, während Ibis eine eigene, von pandas/dplyr inspirierte API nutzt.
In der Praxis können diese Tools eher komplementär als konkurrierend sein. Eine mit Narwhals erstellte Bibliothek kann Ibis-Tabellen als Input akzeptieren, und Benutzer, die mit Ibis arbeiten, können Narwhals-basierte Bibliotheken nahtlos nutzen.
Es gibt bereits einige Bibliotheken und Tools, die Narwhals für ihre Anforderungen an die Dataframe-Interoperabilität nutzen, wie Bokeh für interaktive Datenvisualisierung im Browser, Marimo für reaktive Notebooks oder die interaktive Graphing-Bibliothek Plotly, alle mit rund 20.000 Sternen auf GitHub.
Praxis
Um eine Dataframe-agnostische Funktion zu schreiben, müssen wir zunächst einen Narwhals DataFrame oder LazyFrame initialisieren, indem wir unseren Dataframe an nw.from_native übergeben. Alle Berechnungen bleiben verzögert, wenn wir mit einem LazyFrame beginnen, und Narwhals wird niemals automatisch eine Berechnung auslösen, ohne gefragt zu werden. Danach können wir unsere Logik mit der von Narwhals unterstützten Untermenge der Polars-API ausdrücken. Schließlich können wir mit nw.to_native einen Dataframe in seiner ursprünglichen Bibliothek zurückgeben. Da die Schritte 1 und 3 so häufig vorkommen, bietet Narwhals einen praktischen @nw.narwhalify-Decorator an, sodass wir nur noch Schritt 2 explizit schreiben müssen. Betrachten wir das folgende Beispiel mit einer einfachen Group-by- und Mean-Operation.
1import narwhals as nw
2from narwhals.typing import FrameT
3
4
5@nw.narwhalify
6def func(df: FrameT) -> FrameT:
7 return df.group_by("a").agg(nw.col("b").mean()).sort("a")
Wir können sie dann einfach mit jeder beliebigen Engine verwenden:
1import pandas as pd 2 3df = pd.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]}) 4print(func(df))
Wenn wir von pandas zu Polars wechseln wollen, müssen wir nicht die Logik selbst umschreiben, sondern nur den Import (und die Referenz).
1import polars as pl 2 3df = pl.LazyFrame({"a": [1, 1, 2], "b": [4, 5, 6]}) 4print(func(df).collect())
Beim Umgang mit verschiedenen Bibliotheken, insbesondere bei komplexeren Operationen, ist es möglich, auf Funktionen zu stoßen, die in Narwhals noch nicht implementiert sind. In solchen Fällen kann Narwhals dennoch als dünne Dataframe-Ingestion-Schicht nützlich sein. Wenn eine mit Narwhals entwickelte Bibliothek Dataframes in jedem Format akzeptieren möchte, aber intern mit pandas arbeitet, kann Narwhals die Konvertierung übernehmen. Dies ist wesentlich leichtgewichtiger, als alle Dataframe-Bibliotheken als Abhängigkeiten einzubinden, und die Implementierung ist unkompliziert:
1def df_to_pandas(df: IntoDataFrame) -> pd.DataFrame:
2 return nw.from_native(df).to_pandas()
Diese Übersicht deckt bei weitem nicht alle Funktionen von Narwhals ab, und die Dokumentation bietet hervorragende Ressourcen für den Einstieg.
Performance-Overhead
Die Dokumentation behauptet, dass der Overhead bei der Ausführung von pandas über Narwhals im Vergleich zu nativem pandas vernachlässigbar und manchmal „sogar negativ“ sei. Die Entwickler haben darauf geachtet, unnötige Kopien und Index-Resets zu vermeiden. Bei Lazy-Backends respektiert Narwhals die Verzögerung des Backends und wertet eine Abfrage niemals vollständig aus, es sei denn, dies wird explizit über .collect() angefordert. An einigen Stellen, wie bei Joins und Selects, muss Narwhals das Schema eines Dataframes inspizieren, um das Verhalten von Polars nachzuahmen. Dies ist jedoch in der Regel kostengünstig, da es allein anhand der Metadaten erfolgen kann, ohne den gesamten Datensatz in den Speicher zu lesen. Um diesen Overhead zu minimieren, cachead Narwhals Schema- und Spaltennamenauswertungen.
Wie Narwhals unter der Haube funktioniert
Im Kern von Narwhals steht eine Regel: „Ein Ausdruck ist eine Funktion von einem DataFrame zu einer Sequenz von Series.“ Eine Series ist ein eindimensionales benanntes Array, das jeden Datentyp aufnehmen kann. In seiner einfachsten Form gibt nw.col('a') die Series a aus dem DataFrame zurück. Ausdrücke können auch mehrere Series zurückgeben (z. B. nw.col('a', 'b')), aber alle Spalten müssen aus demselben Dataframe abgeleitet worden sein. Ein Ausdruck allein erzeugt keinen Wert; er erzeugt erst dann einen, wenn er in einen DataFrame-Kontext gegeben wird. Was passiert, hängt davon ab, welcher Kontext verwendet wird: .select erstellt einen DataFrame, der nur das Ergebnis des angegebenen Ausdrucks enthält, .with_columns erzeugt einen DataFrame wie den aktuellen plus das Ergebnis des Ausdrucks, und .filter behält nur Zeilen, bei denen der Ausdruck zu True ausgewertet wird.
Jede Implementierung in Narwhals definiert ihre eigenen Narwhals-konformen Objekte in Unterordnern wie narwhals._pandas_like, narwhals._arrow oder narwhals._polars. Währenddessen koordinieren die Top-Level-Module wie narwhals.dataframe und narwhals.series, wie die Narwhals-API an das jeweilige Backend delegiert wird. Letztendlich gibt es also mehrere Schichten: Der nw.DataFrame wird von einem Narwhals-konformen DataFrame gestützt, wie z. B. narwhals._pandas_like.dataframe.PandasLikeDataFrame oder narwhals._arrow.dataframe.ArrowDataFrame. Diese Narwhals-konformen DataFrames werden dann wiederum von einem nativen Dataframe gestützt, in unserem Fall einem pandas DataFrame oder einer PyArrow Table.
Wenn ein Benutzer Code ausführt, wird eine Top-Level-Narwhals-API aufgerufen. Die API leitet den Aufruf dann an einen Narwhals-konformen Dataframe-Wrapper weiter, wie PandasLikeDataFrame oder PolarsDataFrame. Der Dataframe-Wrapper leitet den Aufruf schließlich an die zugrunde liegende Bibliothek weiter, etwa den pandas- oder Polars-Dataframe.
Jede Operation in Narwhals ist ein Knoten, auf den über ._nodes zugegriffen werden kann. Zusätzlich bietet Narwhals Metadaten zu seinen Ausdrücken. Hier können wir sehen, wie und ob der Ausdruck zu mehreren Ausgaben expandiert, wie viele reihenfolgeabhängige Operationen er enthält, ob die Ausgabe des Ausdrucks immer die Länge 1 hat und vieles mehr.
Ein bemerkenswerter Trick (von vielen) von Narwhals ist der Elementwise-Push-Down. SQL ist pingelig bei over-Operationen, Polars hingegen nicht. In SQL ist abs(sum(a)) over (partition by b) nicht gültig. In Polars ist pl.col('a').sum().abs().over('b') gültig. Narwhals schreibt Ausdrücke um, um die Flexibilität von Polars beizubehalten, wenn in SQL-Engines übersetzt wird. Konkret verschiebt es over-Knoten hinter elementweise Operationen. In unserem Polars-Beispiel fügt Narwhals die over-Operation automatisch vor der abs-Operation ein. Die Idee dahinter ist, dass elementweise Operationen zeilenweise arbeiten und nicht von den umliegenden Zeilen abhängen. Ein over-Knoten partitioniert oder ordnet eine Berechnung. Daher ist eine elementweise Operation gefolgt von einer over-Operation dasselbe wie die over-Operation gefolgt von derselben elementweisen Operation. Es ist jedoch wichtig zu bedenken, dass Query-Optimierung nicht zum Aufgabenbereich von Narwhals gehört. Dieses Umschreiben von Ausdrücken wird von den Entwicklern als akzeptabel angesehen, da es einfach ist und es Benutzern ermöglicht, Operationen auszuwerten, die bei bestimmten Backends sonst nicht erlaubt wären.
Fazit
Sowohl Narwhals als auch Ibis widmen sich der Dataframe-Portabilität, jedoch für unterschiedliche Zielgruppen. Ibis ist ein vollständiges Analyse-Framework für Endanwender, die Logik einmal schreiben und auf über 20 Backends ausführen möchten. Narwhals ist für Library-Maintainer gedacht, die mehrere Dataframe-Typen akzeptieren wollen, ohne sie alle als Abhängigkeiten vorauszusetzen.
Die wichtigste Erkenntnis: Diese Tools ergänzen sich gegenseitig und konkurrieren nicht. Narwhals-basierte Bibliotheken wie Plotly, Bokeh und Marimo können Ibis-Tabellen als Input akzeptieren, sodass Endanwender mit ihrem bevorzugten Framework arbeiten können, während Library-Maintainer die Komplexität vermeiden, jedes Dataframe-Format direkt unterstützen zu müssen.
Narwhals ist die richtige Wahl, wenn man eine Bibliothek oder ein Tool baut, das Dataframes als Input akzeptieren muss, wenn man mehrere Dataframe-Bibliotheken ohne separate Codebasen unterstützen möchte oder wenn Garantien zur Abwärtskompatibilität für produktive Deployments wichtig sind. Ibis eignet sich besser für Endanwender, die analytische Arbeit über mehrere Datenbank-Backends hinweg leisten, für Workflows, die eine umfassende SQL-Backend-Unterstützung für über 20 Engines benötigen, oder für Teams, die lokal auf DuckDB entwickeln und auf BigQuery oder Snowflake deployen möchten, ohne den Code zu ändern.
Zusammen reduzieren diese Tools Reibungsverluste und verhindern Vendor Lock-in. Ibis ermöglicht die Portabilität analytischer Absichten über Execution Engines hinweg; Narwhals ermöglicht die Portabilität von Dataframe-Inputs über Tools und Bibliotheken hinweg. Da DuckDB, Polars und andere Hochleistungs-Engines immer mehr Verbreitung finden, stellen Tools wie Narwhals sicher, dass Wettbewerb und Innovation bei Dataframe-Bibliotheken das breitere Ökosystem nicht fragmentieren.
Wenn Sie daran interessiert sind, wie diese Tools in moderne Data-Analytics-Workflows passen, schauen Sie sich unsere verwandten Artikel über DuckDB vs. Polars vs. Pandas Benchmarking, Ibis für Backend-agnostische Analysen und unseren DuckDB- und Polars-Stresstest für extrem große Arbeitslasten an.
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Blog-Autor*in
Niklas Niggemann
Werkstudent Data & AI
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.