
Bild vom Autor
# Einführung
Die Datenanalyse hat sich in den letzten Jahren verändert. Der traditionelle Ansatz, alles in eine relationale Datenbank zu laden und SQL-Abfragen auszuführen, funktioniert immer noch, ist aber für einige analytische Workloads oft übertrieben. Daten speichern in Parkett Dateien und deren direkte Abfrage mit DuckDB ist schneller, einfacher und effektiver.
In diesem Artikel zeige ich Ihnen, wie Sie einen Datenanalyse-Stack erstellen Python das DuckDB verwendet, um in Parquet-Dateien gespeicherte Daten abzufragen. Wir arbeiten mit einem Beispieldatensatz, untersuchen die Funktionsweise der einzelnen Komponenten und verstehen, warum dieser Ansatz für Ihre Information-Science-Projekte nützlich sein kann.
Den Code finden Sie auf GitHub.
# Voraussetzungen
Bevor wir beginnen, stellen Sie sicher, dass Sie Folgendes haben:
- Python 3.10 oder eine neuere Model installiert
- Ein Verständnis der SQL-Grundlagen und Pandas DataFrame-Operationen
- Vertrautheit mit Datenanalysekonzepten
Installieren Sie außerdem die erforderlichen Bibliotheken:
pip set up duckdb pandas pyarrow numpy faker
# Den empfohlenen Datenanalyse-Stack verstehen
Beginnen wir damit, zu verstehen, was die einzelnen Komponenten bewirken und warum sie intestine zusammenarbeiten.
Parkett ist ein säulenförmiges Speicherformat, das ursprünglich für den entwickelt wurde Hadoop Ökosystem. Im Gegensatz zu zeilenbasierten Formaten wie CSV, bei denen jede Zeile ein vollständiger Datensatz ist, organisiert Parquet Daten nach Spalten. Dies magazine wie ein kleiner Unterschied erscheinen, hat aber enorme Auswirkungen auf die Analyse.
Wenn Sie eine Abfrage ausführen, die nur drei Spalten aus einer Tabelle mit fünfzig Spalten benötigt, können Sie mit Parquet nur diese drei Spalten lesen. Bei CSV müssen Sie jede Zeile vollständig lesen und dann die 47 Spalten, die Sie nicht benötigen, wegwerfen. Dies macht Parquet für typische analytische Abfragen schneller. Darüber hinaus lässt sich die spaltenbasierte Speicherung intestine komprimieren, da die Werte in derselben Spalte tendenziell ähnlich sind.
DuckDB ist eine eingebettete Analysedatenbank. Während SQLite DuckDB ist für transaktionale Arbeitslasten optimiert, die viele kleine Lese- und Schreibvorgänge erfordern. Es wurde speziell für analytische Abfragen entwickelt, die das Scannen großer Datenmengen, Aggregationen und Verknüpfungen erfordern. Der eingebettete Teil bedeutet, dass er innerhalb Ihres Python-Prozesses ausgeführt wird, sodass kein separater Datenbankserver installiert oder verwaltet werden muss.
Das Besondere an DuckDB für die Analyse ist, dass es Parquet-Dateien direkt abfragen kann. Sie müssen die Daten nicht zuerst in die Datenbank importieren. Richten Sie DuckDB auf eine Parquet-Datei, schreiben Sie SQL und es liest nur das, was es benötigt. Diese „Question-in-Place“-Funktion macht den gesamten Stack nützlich.
Sie können dies in Ihrer Python-Entwicklungsumgebung verwenden. Sie speichern Daten in Parquet-Dateien, Pandas übernimmt die Datenmanipulation, DuckDB führt analytische Abfragen aus und das gesamte Python-Ökosystem steht für Visualisierung, maschinelles Lernen und Automatisierung zur Verfügung.
# Erstellen eines Beispieldatensatzes
Wir werden einen E-Commerce-Datensatz verwenden. Sie können die verwenden data_generator.py-Skript um den Beispieldatensatz zu generieren oder Folgen Sie diesem Notizbuch.
Der Datensatz umfasst Kunden, die Bestellungen aufgeben, Bestellungen, die mehrere Artikel enthalten, und Produkte mit Kategorien und Preisen.
Die Daten haben referenzielle Integrität. Jede Bestellung verweist auf einen gültigen Kunden und jede Bestellposition verweist sowohl auf eine gültige Bestellung als auch auf ein gültiges Produkt. Dadurch können wir sinnvolle Verknüpfungen und Aggregationen durchführen.
# Daten in einer Parquet-Datei speichern
Bevor wir unsere Daten speichern, wollen wir verstehen, warum Parquet für Analysen effektiv ist. Wir haben die Vorteile von säulenförmigen Speicherformaten wie Parquet bereits besprochen, aber gehen wir noch einmal darauf ein, diesmal detaillierter.
In einer CSV-Datei werden Daten Zeile für Zeile gespeichert. Wenn Sie eine Million Zeilen mit jeweils 50 Spalten haben und nur eine Spalte analysieren möchten, müssen Sie dennoch alle 50 Millionen Werte lesen, um die Spalten zu überspringen, die Sie nicht benötigen. Das ist Verschwendung.
Wie wir jetzt wissen, speichert Parquet Daten Spalte für Spalte. Alle Werte für eine Spalte werden zusammen gespeichert. Wenn Sie eine Spalte abfragen, lesen Sie genau diese Spalte und nichts anderes. Bei analytischen Abfragen, die typischerweise eine kleine Anzahl von Spalten betreffen, ist dies viel schneller.
Auch die säulenförmige Speicherung lässt sich besser komprimieren. Werte in derselben Spalte neigen dazu, ähnlich zu sein – sie sind normalerweise alle Ganzzahlen, alle Datumsangaben oder alle aus derselben kategorialen Menge. Komprimierungsalgorithmen funktionieren bei ähnlichen Daten viel besser als bei Zufallsdaten.
Speichern wir unsere Daten als Parquet und sehen wir uns die Vorteile an:
# Save tables as Parquet information
customers_df.to_parquet('clients.parquet', engine="pyarrow", compression='snappy')
products_df.to_parquet('merchandise.parquet', engine="pyarrow", compression='snappy')
orders_df.to_parquet('orders.parquet', engine="pyarrow", compression='snappy')
order_items_df.to_parquet('order_items.parquet', engine="pyarrow", compression='snappy')
# Evaluate with CSV to see the distinction
customers_df.to_csv('clients.csv', index=False)
orders_df.to_csv('orders.csv', index=False)
import os
def get_size_mb(filename):
return os.path.getsize(filename) / (1024 * 1024)
print("Storage Comparability:")
print(f"clients.csv: {get_size_mb('clients.csv'):.2f} MB")
print(f"clients.parquet: {get_size_mb('clients.parquet'):.2f} MB")
print(f"Financial savings: {(1 - get_size_mb('clients.parquet')/get_size_mb('clients.csv'))*100:.1f}%n")
print(f"orders.csv: {get_size_mb('orders.csv'):.2f} MB")
print(f"orders.parquet: {get_size_mb('orders.parquet'):.2f} MB")
print(f"Financial savings: {(1 - get_size_mb('orders.parquet')/get_size_mb('orders.csv'))*100:.1f}%")
Ausgabe:
Storage Comparability:
clients.csv: 0.73 MB
clients.parquet: 0.38 MB
Financial savings: 48.5%
orders.csv: 3.01 MB
orders.parquet: 1.25 MB
Financial savings: 58.5%
Diese Kompressionsverhältnisse sind typisch. Parquet erreicht im Allgemeinen eine bessere Komprimierung als CSV. Die Komprimierung, die wir hier verwenden, ist Bissigbei dem Geschwindigkeit Vorrang vor maximaler Komprimierung hat.
Hinweis: Parquet unterstützt andere Codecs wie Gzipdas eine bessere Komprimierung bietet, aber langsamer ist, und Zstd für eine gute Steadiness zwischen Kompression und Geschwindigkeit.
# Parquet-Dateien mit DuckDB abfragen
Jetzt kommt der interessante Teil. Wir können diese Parquet-Dateien direkt mit SQL abfragen, ohne sie zuerst in eine Datenbank zu laden.
import duckdb
# Create a DuckDB connection
con = duckdb.join(database=":reminiscence:")
# Question the Parquet file immediately
question = """
SELECT
customer_segment,
COUNT(*) as num_customers,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () as share
FROM 'clients.parquet'
GROUP BY customer_segment
ORDER BY num_customers DESC
"""
end result = con.execute(question).fetchdf()
print("Buyer Distribution:")
print(end result)
Ausgabe:
Buyer Distribution:
customer_segment num_customers share
0 Normal 5070 50.70
1 Fundamental 2887 28.87
2 Premium 2043 20.43
Schauen Sie sich die Abfragesyntax an: FROM 'clients.parquet'. DuckDB liest die Datei direkt. Es gibt keinen Importschritt, nein CREATE TABLE Anweisung und kein Warten auf das Laden der Daten. Sie schreiben SQL, DuckDB ermittelt aus der Datei, welche Daten es benötigt, und gibt Ergebnisse zurück.
In herkömmlichen Arbeitsabläufen müssten Sie eine Datenbank erstellen, Schemata definieren, Daten importieren, Indizes erstellen und schließlich eine Abfrage durchführen. Mit DuckDB und Parquet überspringen Sie das alles. Unter der Haube liest DuckDB die Metadaten der Parquet-Datei, um das Schema zu verstehen, und verwendet dann Prädikat-Pushdown, um das Lesen von Daten zu überspringen, die nicht mit Ihrem Schema übereinstimmen WHERE Klausel. Es liest nur die Spalten, die Ihre Abfrage tatsächlich verwendet. Bei großen Dateien werden Abfragen dadurch superschnell.
# Durchführen komplexer Analysen
Lassen Sie uns eine etwas komplexere analytische Abfrage ausführen. Wir analysieren die monatlichen Umsatztrends aufgeschlüsselt nach Kundensegmenten.
question = """
SELECT
strftime(o.order_date, '%Y-%m') as month,
c.customer_segment,
COUNT(DISTINCT o.order_id) as num_orders,
COUNT(DISTINCT o.customer_id) as unique_customers,
ROUND(SUM(o.order_total), 2) as total_revenue,
ROUND(AVG(o.order_total), 2) as avg_order_value
FROM 'orders.parquet' AS o
JOIN 'clients.parquet' AS c
ON o.customer_id = c.customer_id
WHERE o.payment_status="accomplished"
GROUP BY month, c.customer_segment
ORDER BY month DESC, total_revenue DESC
LIMIT 15
"""
monthly_revenue = con.execute(question).fetchdf()
print("Latest Month-to-month Income by Section:")
print(monthly_revenue.to_string(index=False))
Ausgabe:
Latest Month-to-month Income by Section:
month customer_segment num_orders unique_customers total_revenue avg_order_value
2026-01 Normal 2600 1468 1683223.68 647.39
2026-01 Fundamental 1585 857 1031126.44 650.55
2026-01 Premium 970 560 914105.61 942.38
2025-12 Normal 2254 1571 1533076.22 680.16
2025-12 Premium 885 613 921775.85 1041.55
2025-12 Fundamental 1297 876 889270.86 685.64
2025-11 Normal 1795 1359 1241006.08 691.37
2025-11 Premium 725 554 717625.75 989.83
2025-11 Fundamental 1012 767 682270.44 674.18
2025-10 Normal 1646 1296 1118400.61 679.47
2025-10 Premium 702 550 695913.24 991.33
2025-10 Fundamental 988 769 688428.86 696.79
2025-09 Normal 1446 1181 970017.17 670.83
2025-09 Premium 594 485 577486.81 972.20
2025-09 Fundamental 750 618 495726.69 660.97
Diese Abfrage gruppiert nach zwei Dimensionen (Monat und Section), aggregiert mehrere Metriken und filtert nach dem Zahlungsstatus. Es ist die Artwork von Abfrage, die Sie in der analytischen Arbeit ständig schreiben würden. Der strftime Funktion formatiert Datumsangaben direkt in SQL. Der ROUND Funktion bereinigt Dezimalstellen. Mehrere Aggregationen laufen effizient und liefern die erwarteten Ergebnisse.
# Mehrere Tabellen verbinden
Echte Analysen umfassen selten eine einzelne Tabelle. Kommen wir zu unseren Tischen, um eine geschäftliche Frage zu beantworten: Welche Produktkategorien generieren den meisten Umsatz und wie unterscheidet sich dieser je nach Kundensegment?
question = """
SELECT
p.class,
c.customer_segment,
COUNT(DISTINCT oi.order_id) as num_orders,
SUM(oi.amount) as units_sold,
ROUND(SUM(oi.item_total), 2) as total_revenue,
ROUND(AVG(oi.item_total), 2) as avg_item_value
FROM 'order_items.parquet' oi
JOIN 'orders.parquet' o ON oi.order_id = o.order_id
JOIN 'merchandise.parquet' p ON oi.product_id = p.product_id
JOIN 'clients.parquet' c ON o.customer_id = c.customer_id
WHERE o.payment_status="accomplished"
GROUP BY p.class, c.customer_segment
ORDER BY total_revenue DESC
LIMIT 20
"""
category_analysis = con.execute(question).fetchdf()
print("Income by Class and Buyer Section:")
print(category_analysis.to_string(index=False))
Gekürzte Ausgabe:
Income by Class and Buyer Section:
class customer_segment num_orders units_sold total_revenue avg_item_value
Electronics Normal 4729 6431.0 6638814.75 1299.18
Electronics Premium 2597 3723.0 3816429.62 1292.39
Electronics Fundamental 2685 3566.0 3585652.92 1240.28
Automotive Normal 4506 5926.0 3050679.12 633.18
Sports activities Normal 5049 6898.0 2745487.54 497.55
...
...
Clothes Premium 3028 4342.0 400704.25 114.55
Clothes Fundamental 3102 4285.0 400391.18 117.49
Books Normal 6196 8511.0 252357.39 36.74
Diese Abfrage verknüpft drei Tabellen. DuckDB ermittelt automatisch die optimale Be a part of-Reihenfolge und Ausführungsstrategie. Beachten Sie, wie lesbar die SQL im Vergleich zu entsprechendem Pandas-Code ist. Bei komplexer analytischer Logik drückt SQL die Absicht häufig klarer aus als DataFrame-Operationen.
# Abfrageleistung verstehen
Vergleichen wir DuckDB mit Pandas für eine häufige Analyseaufgabe.
// Methode 1: Pandas verwenden
import time
# Analytical activity: Calculate buyer buy patterns
print("Efficiency Comparability: Buyer Buy Analysisn")
start_time = time.time()
# Merge dataframes
merged = order_items_df.merge(orders_df, on='order_id')
merged = merged.merge(products_df, on='product_id')
# Filter accomplished orders
accomplished = merged(merged('payment_status') == 'accomplished')
# Group and mixture
customer_patterns = accomplished.groupby('customer_id').agg({
'order_id': 'nunique',
'product_id': 'nunique',
'item_total': ('sum', 'imply'),
'class': lambda x: x.mode()(0) if len(x) > 0 else None
})
customer_patterns.columns = ('num_orders', 'unique_products', 'total_spent', 'avg_spent', 'favorite_category')
customer_patterns = customer_patterns.sort_values('total_spent', ascending=False).head(100)
pandas_time = time.time() - start_time
// Methode 2: Verwenden von DuckDB
start_time = time.time()
question = """
SELECT
o.customer_id,
COUNT(DISTINCT oi.order_id) as num_orders,
COUNT(DISTINCT oi.product_id) as unique_products,
ROUND(SUM(oi.item_total), 2) as total_spent,
ROUND(AVG(oi.item_total), 2) as avg_spent,
MODE(p.class) as favorite_category
FROM 'order_items.parquet' oi
JOIN 'orders.parquet' o ON oi.order_id = o.order_id
JOIN 'merchandise.parquet' p ON oi.product_id = p.product_id
WHERE o.payment_status="accomplished"
GROUP BY o.customer_id
ORDER BY total_spent DESC
LIMIT 100
"""
duckdb_result = con.execute(question).fetchdf()
duckdb_time = time.time() - start_time
print(f"Pandas execution time: {pandas_time:.4f} seconds")
print(f"DuckDB execution time: {duckdb_time:.4f} seconds")
print(f"Speedup: {pandas_time/duckdb_time:.1f}x quicker with DuckDBn")
print("Prime 5 clients by whole spent:")
print(duckdb_result.head().to_string(index=False))
Ausgabe:
Efficiency Comparability: Buyer Buy Evaluation
Pandas execution time: 1.9872 seconds
DuckDB execution time: 0.1171 seconds
Speedup: 17.0x quicker with DuckDB
Prime 5 clients by whole spent:
customer_id num_orders unique_products total_spent avg_spent favorite_category
8747 8 24 21103.21 879.30 Electronics
617 9 27 19596.22 725.79 Electronics
2579 9 18 17011.30 895.33 Sports activities
6242 7 23 16781.11 729.61 Electronics
5443 8 22 16697.02 758.96 Automotive
DuckDB ist etwa 17x schneller. Dieser Leistungsunterschied ist bei größeren Datensätzen stärker ausgeprägt. Der Pandas-Ansatz lädt alle Daten in den Speicher, führt mehrere Zusammenführungsvorgänge durch (die Kopien erstellen) und aggregiert sie dann. DuckDB liest direkt aus Parquet-Dateien, verschiebt Filter nach unten, um das Lesen unnötiger Daten zu vermeiden, und verwendet optimierte Be a part of-Algorithmen.
# Erstellen wiederverwendbarer Analytics-Abfragen
In der Produktionsanalyse führen Sie ähnliche Abfragen wiederholt mit unterschiedlichen Parametern aus. Lassen Sie uns eine wiederverwendbare Funktion erstellen, die den Finest Practices für diesen Workflow folgt.
def analyze_product_performance(con, class=None, min_revenue=None, date_from=None, top_n=20):
"""
Analyze product efficiency with versatile filtering.
This demonstrates how one can construct reusable analytical queries that may be
parameterized for various use circumstances. In manufacturing, you'd construct a library
of those features for widespread analytical questions.
"""
# Construct the WHERE clause dynamically primarily based on parameters
where_clauses = ("o.payment_status="accomplished"")
if class:
where_clauses.append(f"p.class = '{class}'")
if date_from:
where_clauses.append(f"o.order_date >= '{date_from}'")
where_clause = " AND ".be part of(where_clauses)
# Principal analytical question
question = f"""
WITH product_metrics AS (
SELECT
p.product_id,
p.product_name,
p.class,
p.base_price,
COUNT(DISTINCT oi.order_id) as times_ordered,
SUM(oi.amount) as units_sold,
ROUND(SUM(oi.item_total), 2) as total_revenue,
ROUND(AVG(oi.unit_price), 2) as avg_selling_price,
ROUND(SUM(oi.item_total) - (p.value * SUM(oi.amount)), 2) as revenue
FROM 'order_items.parquet' oi
JOIN 'orders.parquet' o ON oi.order_id = o.order_id
JOIN 'merchandise.parquet' p ON oi.product_id = p.product_id
WHERE {where_clause}
GROUP BY p.product_id, p.product_name, p.class, p.base_price, p.value
)
SELECT
*,
ROUND(100.0 * revenue / total_revenue, 2) as profit_margin_pct,
ROUND(avg_selling_price / base_price, 2) as price_realization
FROM product_metrics
"""
# Add income filter if specified
if min_revenue:
question += f" WHERE total_revenue >= {min_revenue}"
question += f"""
ORDER BY total_revenue DESC
LIMIT {top_n}
"""
return con.execute(question).fetchdf()
Diese Funktion führt Folgendes aus. Erstens baut es SQL dynamisch auf der Grundlage von Parametern auf und ermöglicht so eine versatile Filterung, ohne für jeden Fall separate Abfragen schreiben zu müssen. Zweitens verwendet es a Gemeinsamer Tabellenausdruck (CTE), um komplexe Logik in lesbare Schritte zu organisieren. Drittens berechnet es abgeleitete Kennzahlen wie Gewinnspanne und Preisrealisierung, die mehrere Quellspalten erfordern.
Bei der Gewinnberechnung werden die Kosten vom Umsatz subtrahiert, wobei Daten aus den Bestellpositions- und Produkttabellen verwendet werden. Diese Artwork der tabellenübergreifenden Berechnung ist in SQL unkompliziert, wäre jedoch bei mehreren Pandas-Operationen umständlich. DuckDB verarbeitet dies effizient in einer einzigen Abfrage.
Hier ist ein Beispiel, das die obige Funktion verwendet:
# Instance 1: Prime electronics merchandise
electronics = analyze_product_performance(con, class='Electronics', top_n=10)
print("Prime 10 Electronics Merchandise:")
print(electronics(('product_name', 'units_sold', 'total_revenue', 'profit_margin_pct')).to_string(index=False))
Ausgabe:
Prime 10 Electronics Merchandise:
product_name units_sold total_revenue profit_margin_pct
Electronics Merchandise 113 262.0 510331.81 38.57
Electronics Merchandise 154 289.0 486307.74 38.28
Electronics Merchandise 122 229.0 448680.64 38.88
Electronics Merchandise 472 251.0 444680.20 38.51
Electronics Merchandise 368 222.0 424057.14 38.96
Electronics Merchandise 241 219.0 407648.10 38.75
Electronics Merchandise 410 243.0 400078.65 38.31
Electronics Merchandise 104 233.0 400036.84 38.73
Electronics Merchandise 2 213.0 382583.85 38.76
Electronics Merchandise 341 240.0 376722.94 38.94
Und hier ist ein weiteres Beispiel:
# Instance 2: Excessive-revenue merchandise throughout all classes
print("nnHigh-Income Merchandise (>$50k income):")
high_revenue = analyze_product_performance(con, min_revenue=50000, top_n=10)
print(high_revenue(('product_name', 'class', 'total_revenue', 'revenue')).to_string(index=False))
Ausgabe:
Excessive-Income Merchandise (>$50k income):
product_name class total_revenue revenue
Electronics Merchandise 113 Electronics 510331.81 196846.19
Electronics Merchandise 154 Electronics 486307.74 186140.78
Electronics Merchandise 122 Electronics 448680.64 174439.40
Electronics Merchandise 472 Electronics 444680.20 171240.80
Electronics Merchandise 368 Electronics 424057.14 165194.04
Electronics Merchandise 241 Electronics 407648.10 157955.25
Electronics Merchandise 410 Electronics 400078.65 153270.84
Electronics Merchandise 104 Electronics 400036.84 154953.46
Electronics Merchandise 2 Electronics 382583.85 148305.15
Electronics Merchandise 341 Electronics 376722.94 146682.94
# Zusammenfassung
In diesem Artikel haben wir E-Commerce-Daten analysiert. Wir haben relationale Daten generiert, sie als Parquet gespeichert und mit DuckDB abgefragt. Die Leistungsvergleiche zeigten erhebliche Beschleunigungen im Vergleich zu herkömmlichen Pandas-Ansätzen.
Verwenden Sie diesen Stack, wenn Sie analytische Workloads für strukturierte Daten durchführen. Wenn Sie Metriken aggregieren, filtern, verknüpfen und berechnen, ist dies nützlich. Es eignet sich intestine für Daten, die sich stapelweise und nicht ständig ändern. Wenn Sie die gestrigen Verkäufe analysieren, monatliche Berichte verarbeiten oder historische Traits untersuchen, funktionieren die regelmäßig aktualisierten Parquet-Dateien hervorragend. Sie benötigen keine Dwell-Datenbank, die ständig Schreibvorgänge akzeptiert.
Allerdings ist dieser Stack nicht für alles geeignet:
- Wenn Sie Echtzeitaktualisierungen mit vielen gleichzeitigen Autoren benötigen, benötigen Sie eine herkömmliche Datenbank mit ACID-Transaktionen
- Wenn Sie eine Anwendung mit benutzerorientierten Abfragen erstellen, die Antwortzeiten im Millisekundenbereich erfordern, ist eine indizierte Datenbank besser
- Wenn mehrere Benutzer gleichzeitig mit unterschiedlichen Zugriffsberechtigungen Abfragen durchführen müssen, bietet ein Datenbankserver eine bessere Kontrolle
Der Candy Spot ist analytische Arbeit an großen Datensätzen, bei denen Datenaktualisierungen in Stapeln erfolgen und Sie schnelle, versatile Abfragen und Analysen benötigen.
Viel Spaß beim Analysieren!
Bala Priya C ist ein Entwickler und technischer Redakteur aus Indien. Sie arbeitet gerne an der Schnittstelle von Mathematik, Programmierung, Datenwissenschaft und Inhaltserstellung. Zu ihren Interessen- und Fachgebieten gehören DevOps, Datenwissenschaft und Verarbeitung natürlicher Sprache. Sie liebt es zu lesen, zu schreiben, zu programmieren und Kaffee zu trinken! Derzeit arbeitet sie daran, zu lernen und ihr Wissen mit der Entwickler-Neighborhood zu teilen, indem sie Tutorials, Anleitungen, Meinungsbeiträge und mehr verfasst. Bala erstellt außerdem ansprechende Ressourcenübersichten und Programmier-Tutorials.
