Polars Tutorial: Ultra-Fast DataFrame Library for Data Science

# Polars - Tutorial Lengkap Library DataFrame Ultra-Cepat ## Daftar Isi 1. [Pendahuluan](#pendahuluan) 2. [Prasyarat](#prasyarat) 3. [Dasar-Dasar Polars](#dasar-dasar-polars) 4. [Evaluasi Lazy vs Ea...

By Ruby Abdullah · · tutorial
PolarsDataFrameData SciencePythonPandas AlternativePerformance

Polars - Ultra-Fast DataFrame Library Complete Tutorial

Table of Contents

  • Introduction
  • Prerequisites
  • Polars Basics
  • Lazy vs Eager Evaluation
  • Expressions and Transformations
  • Joins and Combining DataFrames
  • Group By and Aggregations
  • Window Functions
  • Polars vs Pandas Benchmark
  • ML Pipeline Integration
  • Handling Large Datasets with Streaming
  • Best Practices
  • Conclusion

  • Introduction

    Polars is a blazingly fast DataFrame library written in Rust with Python bindings. It leverages Apache Arrow's columnar memory format and a multi-threaded query engine to deliver performance that is often 10-100x faster than Pandas. Polars supports both eager and lazy evaluation, making it ideal for both exploratory data analysis and production data pipelines.

    Key advantages of Polars over Pandas:

    • Written in Rust with zero-copy interoperability via Apache Arrow
    • Multi-threaded execution by default
    • Lazy evaluation with query optimization
    • Consistent API with no index-based confusion
    • Streaming support for out-of-core processing

    Prerequisites

    • Python 3.8 or higher
    • Basic understanding of DataFrames and tabular data

    pip install polars
    

    pip install polars[all] # Includes optional dependencies (Excel, database connectors, etc.)

    pip install pandas # For comparison benchmarks

    pip install scikit-learn # For ML pipeline examples

    Polars Basics

    Creating DataFrames

    import polars as pl
    
    

    From a dictionary

    df = pl.DataFrame({

    "name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],

    "age": [30, 25, 35, 28, 32],

    "department": ["Engineering", "Marketing", "Engineering", "Sales", "Marketing"],

    "salary": [95000, 65000, 105000, 72000, 78000],

    "joindate": ["2020-01-15", "2021-06-01", "2019-03-20", "2022-01-10", "2020-09-05"]

    })

    Cast date column

    df = df.withcolumns(pl.col("joindate").str.todate())

    print(df)

    print(f"Shape: {df.shape}")

    print(f"Schema: {df.schema}")

    print(f"Dtypes: {df.dtypes}")

    Reading Data

    # CSV
    

    df = pl.readcsv("data.csv")

    Parquet (highly recommended for performance)

    df = pl.readparquet("data.parquet")

    JSON

    df = pl.readjson("data.json")

    From Pandas

    import pandas as pd

    pandasdf = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

    polarsdf = pl.frompandas(pandasdf)

    Back to Pandas when needed

    pandasback = polarsdf.topandas()

    Basic Operations

    # Select columns
    

    df.select("name", "salary")

    df.select(pl.col("name"), pl.col("salary"))

    Filter rows

    df.filter(pl.col("salary") > 80000)

    df.filter((pl.col("department") == "Engineering") & (pl.col("age") > 30))

    Sort

    df.sort("salary", descending=True)

    df.sort(["department", "salary"], descending=[False, True])

    Add/modify columns

    df.withcolumns(

    (pl.col("salary") 1.1).alias("salaryafterraise"),

    pl.col("name").str.touppercase().alias("nameupper"),

    pl.lit("Active").alias("status")

    )

    Drop columns

    df.drop("joindate")

    Rename columns

    df.rename({"name": "employeename", "salary": "annualsalary"})

    Descriptive statistics

    df.describe()

    Lazy vs Eager Evaluation

    Polars' lazy evaluation is one of its most powerful features. It builds a query plan that is optimized before execution:

    import polars as pl
    
    

    EAGER: operations execute immediately

    df = pl.readcsv("largefile.csv")

    result = df.filter(pl.col("amount") > 100).select("id", "amount").sort("amount")

    LAZY: builds a query plan, optimizes, then executes

    result = (

    pl.scancsv("largefile.csv") # Returns LazyFrame

    .filter(pl.col("amount") > 100)

    .select("id", "amount")

    .sort("amount")

    .collect() # Triggers execution

    )

    Inspect the query plan

    lazyquery = (

    pl.scancsv("largefile.csv")

    .filter(pl.col("amount") > 100)

    .select("id", "amount")

    .sort("amount")

    )

    See the optimized plan

    print(lazyquery.explain()) # Optimized plan

    print(lazyquery.explain(optimized=False)) # Unoptimized plan

    The optimizer performs several transformations:

    • Predicate pushdown: Filters are pushed as early as possible
    • Projection pushdown: Only required columns are read from disk
    • Type coercion: Automatic type optimization
    • Common subexpression elimination: Reuses computed results

    # Example: The optimizer will only read "id" and "amount" columns from the CSV
    

    and apply the filter during reading, not after loading the entire file

    result = (

    pl.scancsv("largefile.csv")

    .filter(pl.col("amount") > 100)

    .select("id", "amount")

    .collect()

    )

    Expressions and Transformations

    Expressions are the core building blocks of Polars operations:

    import polars as pl
    
    

    df = pl.DataFrame({

    "product": ["A", "B", "A", "C", "B", "A", "C", "B"],

    "quantity": [10, 20, 15, 5, 25, 30, 10, 15],

    "price": [9.99, 14.99, 9.99, 24.99, 14.99, 9.99, 24.99, 14.99],

    "region": ["North", "South", "North", "East", "South", "East", "North", "East"]

    })

    Column expressions

    result = df.select(

    pl.col("product"),

    pl.col("quantity"),

    (pl.col("quantity") pl.col("price")).alias("revenue"),

    pl.col("quantity").sum().alias("totalquantity"),

    pl.col("quantity").mean().alias("avgquantity"),

    pl.col("quantity").std().alias("stdquantity"),

    )

    String expressions

    dfstrings = pl.DataFrame({

    "text": ["Hello World", "foo bar baz", "POLARS IS FAST", "data science"]

    })

    result = dfstrings.select(

    pl.col("text"),

    pl.col("text").str.tolowercase().alias("lower"),

    pl.col("text").str.touppercase().alias("upper"),

    pl.col("text").str.split(" ").alias("words"),

    pl.col("text").str.lenchars().alias("charcount"),

    pl.col("text").str.contains("(?i)world|fast").alias("matchespattern"),

    pl.col("text").str.replaceall(" ", "").alias("underscored"),

    )

    Conditional expressions (when/then/otherwise)

    result = df.withcolumns(

    pl.when(pl.col("quantity") > 20)

    .then(pl.lit("High"))

    .when(pl.col("quantity") > 10)

    .then(pl.lit("Medium"))

    .otherwise(pl.lit("Low"))

    .alias("demandlevel")

    )

    List/Array expressions

    dflists = pl.DataFrame({

    "tags": [["python", "rust"], ["java", "python"], ["rust", "go", "python"]],

    "scores": [[90, 85, 92], [78, 88], [95, 91, 87, 93]]

    })

    result = dflists.select(

    pl.col("tags").list.len().alias("numtags"),

    pl.col("tags").list.contains("python").alias("haspython"),

    pl.col("scores").list.mean().alias("avgscore"),

    pl.col("scores").list.max().alias("maxscore"),

    pl.col("scores").list.sort().alias("sortedscores"),

    )

    Date/Time expressions

    dfdates = pl.DataFrame({

    "timestamp": pl.daterange(pl.date(2024, 1, 1), pl.date(2024, 12, 31), eager=True),

    }).withcolumns(

    pl.col("timestamp").dt.year().alias("year"),

    pl.col("timestamp").dt.month().alias("month"),

    pl.col("timestamp").dt.weekday().alias("weekday"),

    pl.col("timestamp").dt.quarter().alias("quarter"),

    pl.col("timestamp").dt.isleapyear().alias("isleap"),

    )

    Joins and Combining DataFrames

    # Sample DataFrames
    

    orders = pl.DataFrame({

    "orderid": [1, 2, 3, 4, 5],

    "customerid": [101, 102, 101, 103, 104],

    "productid": ["P1", "P2", "P3", "P1", "P2"],

    "amount": [250.0, 150.0, 300.0, 250.0, 150.0]

    })

    customers = pl.DataFrame({

    "customerid": [101, 102, 103, 105],

    "name": ["Alice", "Bob", "Charlie", "Eve"],

    "tier": ["Gold", "Silver", "Gold", "Bronze"]

    })

    products = pl.DataFrame({

    "productid": ["P1", "P2", "P3"],

    "productname": ["Widget", "Gadget", "Doohickey"],

    "category": ["Electronics", "Electronics", "Tools"]

    })

    Inner join

    result = orders.join(customers, on="customerid", how="inner")

    Left join

    result = orders.join(customers, on="customerid", how="left")

    Multiple joins chained

    result = (

    orders

    .join(customers, on="customerid", how="left")

    .join(products, on="productid", how="left")

    )

    Join with different column names

    dfa = pl.DataFrame({"ida": [1, 2, 3], "valuea": [10, 20, 30]})

    dfb = pl.DataFrame({"idb": [1, 2, 4], "valueb": [100, 200, 400]})

    result = dfa.join(dfb, lefton="ida", righton="idb", how="outer")

    Cross join

    sizes = pl.DataFrame({"size": ["S", "M", "L"]})

    colors = pl.DataFrame({"color": ["Red", "Blue"]})

    combinations = sizes.join(colors, how="cross")

    Semi join (keep rows from left that have a match in right)

    activecustomers = orders.join(customers, on="customerid", how="semi")

    Anti join (keep rows from left that do NOT have a match in right)

    ordersmissingcustomer = orders.join(customers, on="customerid", how="anti")

    Concatenation

    df1 = pl.DataFrame({"a": [1, 2], "b": [3, 4]})

    df2 = pl.DataFrame({"a": [5, 6], "b": [7, 8]})

    combined = pl.concat([df1, df2]) # Vertical (row-wise)

    Horizontal concatenation

    combinedh = pl.concat([df1, df2], how="horizontal")

    Group By and Aggregations

    sales = pl.DataFrame({
    

    "date": ["2024-01-15", "2024-01-15", "2024-02-10", "2024-02-10",

    "2024-03-05", "2024-03-05", "2024-01-20", "2024-02-25"],

    "product": ["A", "B", "A", "B", "A", "B", "C", "C"],

    "region": ["North", "South", "North", "South", "North", "South", "East", "East"],

    "quantity": [100, 150, 200, 120, 180, 90, 50, 75],

    "revenue": [1000, 2250, 2000, 1800, 1800, 1350, 1250, 1875]

    })

    Simple group by

    result = sales.groupby("product").agg(

    pl.col("quantity").sum().alias("totalquantity"),

    pl.col("revenue").sum().alias("totalrevenue"),

    pl.col("revenue").mean().alias("avgrevenue"),

    pl.len().alias("numtransactions"),

    )

    Multiple group by columns

    result = sales.groupby("product", "region").agg(

    pl.col("quantity").sum().alias("totalquantity"),

    pl.col("revenue").sum().alias("totalrevenue"),

    )

    Advanced aggregations

    result = sales.groupby("product").agg(

    pl.col("quantity").sum().alias("totalqty"),

    pl.col("revenue").sum().alias("totalrev"),

    pl.col("revenue").min().alias("minrev"),

    pl.col("revenue").max().alias("maxrev"),

    pl.col("revenue").std().alias("stdrev"),

    pl.col("revenue").quantile(0.75).alias("q75rev"),

    pl.col("region").nunique().alias("regionsserved"),

    pl.col("date").first().alias("firstsale"),

    pl.col("date").last().alias("lastsale"),

    )

    Group by with sorting

    result = (

    sales

    .groupby("product")

    .agg(pl.col("revenue").sum().alias("totalrevenue"))

    .sort("totalrevenue", descending=True)

    )

    Dynamic group by (time-based)

    salests = sales.withcolumns(pl.col("date").str.todate())

    monthly = salests.groupbydynamic("date", every="1mo").agg(

    pl.col("revenue").sum().alias("monthlyrevenue"),

    pl.col("quantity").sum().alias("monthlyquantity"),

    )

    Window Functions

    Window functions compute values across rows related to the current row without collapsing the DataFrame:

    df = pl.DataFrame({
    

    "employee": ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"],

    "department": ["Eng", "Eng", "Eng", "Sales", "Sales", "Sales"],

    "salary": [95000, 85000, 105000, 72000, 78000, 68000],

    "hireyear": [2020, 2021, 2019, 2022, 2020, 2023]

    })

    over() - Polars' window function

    result = df.withcolumns(

    # Average salary within each department

    pl.col("salary").mean().over("department").alias("deptavgsalary"),

    # Rank within department by salary

    pl.col("salary").rank(descending=True).over("department").alias("salaryrank"),

    # Min and max salary in department

    pl.col("salary").min().over("department").alias("deptminsalary"),

    pl.col("salary").max().over("department").alias("deptmaxsalary"),

    # Count of employees in department

    pl.len().over("department").alias("deptsize"),

    # Difference from department average

    (pl.col("salary") - pl.col("salary").mean().over("department")).alias("salaryvsavg"),

    )

    Rolling window functions

    tsdata = pl.DataFrame({

    "date": pl.daterange(pl.date(2024, 1, 1), pl.date(2024, 3, 31), eager=True),

    "value": [float(i) + (i % 7) 3.0 for i in range(91)]

    })

    result = tsdata.withcolumns(

    pl.col("value").rollingmean(windowsize=7).alias("rolling7davg"),

    pl.col("value").rollingsum(windowsize=7).alias("rolling7dsum"),

    pl.col("value").rollingstd(windowsize=7).alias("rolling7dstd"),

    pl.col("value").rollingmin(windowsize=14).alias("rolling14dmin"),

    pl.col("value").rollingmax(windowsize=14).alias("rolling14dmax"),

    pl.col("value").shift(1).alias("prevvalue"),

    (pl.col("value") - pl.col("value").shift(1)).alias("dailychange"),

    ((pl.col("value") / pl.col("value").shift(1)) - 1).alias("dailypctchange"),

    )

    Cumulative operations

    result = df.sort("salary").withcolumns(

    pl.col("salary").cumsum().alias("cumulativesalary"),

    pl.col("salary").cummax().alias("runningmax"),

    pl.col("salary").cummin().alias("runningmin"),

    pl.col("salary").cumcount().alias("runningcount"),

    )

    Polars vs Pandas Benchmark

    import polars as pl
    

    import pandas as pd

    import numpy as np

    import time

    def benchmarkcomparison(nrows: int = 10000000):

    # Generate test data

    np.random.seed(42)

    data = {

    "id": np.arange(nrows),

    "category": np.random.choice(["A", "B", "C", "D", "E"], nrows),

    "value1": np.random.randn(nrows),

    "value2": np.random.randn(nrows),

    "value3": np.random.uniform(0, 1000, nrows),

    }

    # Pandas

    pdf = pd.DataFrame(data)

    start = time.time()

    pdfresult = (

    pdf.groupby("category")

    .agg({"value1": "mean", "value2": "sum", "value3": ["min", "max", "std"]})

    )

    pandastime = time.time() - start

    # Polars

    pldf = pl.DataFrame(data)

    start = time.time()

    plresult = pldf.groupby("category").agg(

    pl.col("value1").mean(),

    pl.col("value2").sum(),

    pl.col("value3").min().alias("value3min"),

    pl.col("value3").max().alias("value3max"),

    pl.col("value3").std().alias("value3std"),

    )

    polarstime = time.time() - start

    print(f"Data size: {nrows:,} rows")

    print(f"Pandas: {pandastime:.3f}s")

    print(f"Polars: {polarstime:.3f}s")

    print(f"Speedup: {pandastime / polarstime:.1f}x")

    # Filter benchmark

    start = time.time()

    = pdf[(pdf["value1"] > 0) & (pdf["value3"] < 500)]

    pandasfilter = time.time() - start

    start = time.time()

    = pldf.filter((pl.col("value1") > 0) & (pl.col("value3") < 500))

    polarsfilter = time.time() - start

    print(f"\nFilter benchmark:")

    print(f"Pandas: {pandasfilter:.3f}s")

    print(f"Polars: {polarsfilter:.3f}s")

    print(f"Speedup: {pandasfilter / polarsfilter:.1f}x")

    # Sort benchmark

    start = time.time()

    = pdf.sortvalues(["category", "value1"])

    pandassort = time.time() - start

    start = time.time()

    = pldf.sort(["category", "value1"])

    polarssort = time.time() - start

    print(f"\nSort benchmark:")

    print(f"Pandas: {pandassort:.3f}s")

    print(f"Polars: {polarssort:.3f}s")

    print(f"Speedup: {pandassort / polarssort:.1f}x")

    benchmarkcomparison(10000000)

    ML Pipeline Integration

    import polars as pl
    

    import numpy as np

    from sklearn.modelselection import traintestsplit

    from sklearn.preprocessing import StandardScaler, LabelEncoder

    from sklearn.ensemble import RandomForestClassifier

    from sklearn.metrics import classificationreport

    Load and prepare data with Polars

    df = pl.readcsv("customerdata.csv")

    Feature engineering with Polars

    dffeatures = df.withcolumns(

    # Numerical transformations

    pl.col("purchaseamount").log().alias("logpurchase"),

    (pl.col("purchaseamount") / pl.col("visits")).alias("avgpurchasepervisit"),

    pl.col("dayssincelastpurchase").clip(0, 365).alias("recencycapped"),

    # Categorical encoding

    pl.col("region").cast(pl.Categorical).alias("regioncat"),

    # Time-based features

    pl.col("signupdate").str.todate().dt.year().alias("signupyear"),

    pl.col("signupdate").str.todate().dt.month().alias("signupmonth"),

    # Group-based features

    pl.col("purchaseamount").mean().over("region").alias("regionavgpurchase"),

    pl.col("purchaseamount").rank().over("region").alias("purchaserankinregion"),

    )

    Define features and target

    featurecols = [

    "logpurchase", "avgpurchasepervisit", "recencycapped",

    "signupyear", "signupmonth", "regionavgpurchase",

    "purchaserankinregion", "visits", "age"

    ]

    Convert to numpy for sklearn

    X = dffeatures.select(featurecols).tonumpy()

    y = dffeatures.select("churn").tonumpy().flatten()

    Split data

    Xtrain, Xtest, ytrain, ytest = traintestsplit(X, y, testsize=0.2, randomstate=42)

    Scale features

    scaler = StandardScaler()

    Xtrainscaled = scaler.fittransform(Xtrain)

    Xtestscaled = scaler.transform(Xtest)

    Train model

    clf = RandomForestClassifier(nestimators=100, randomstate=42, njobs=-1)

    clf.fit(Xtrainscaled, ytrain)

    Evaluate

    ypred = clf.predict(Xtestscaled)

    print(classificationreport(ytest, ypred))

    Feature importance back in Polars

    importancedf = pl.DataFrame({

    "feature": featurecols,

    "importance": clf.featureimportances

    }).sort("importance", descending=True)

    print(importancedf)

    Handling Large Datasets with Streaming

    Polars can handle datasets larger than available RAM through streaming:

    import polars as pl
    
    

    Streaming: process data in chunks without loading everything into memory

    result = (

    pl.scancsv("hugedataset.csv") # Does not load into memory

    .filter(pl.col("status") == "active")

    .groupby("category")

    .agg(

    pl.col("amount").sum().alias("totalamount"),

    pl.col("amount").mean().alias("avgamount"),

    pl.len().alias("count")

    )

    .sort("totalamount", descending=True)

    .collect(streaming=True) # Process in streaming mode

    )

    Scan Parquet files (even more efficient)

    result = (

    pl.scanparquet("data/.parquet") # Glob pattern for multiple files

    .filter(pl.col("year") >= 2023)

    .select("id", "category", "amount", "year")

    .collect(streaming=True)

    )

    Sink results directly to file (never materializes full result in memory)

    (

    pl.scancsv("inputlarge.csv")

    .filter(pl.col("value") > 0)

    .withcolumns(

    (pl.col("value") 2).alias("doubledvalue")

    )

    .sinkparquet("outputprocessed.parquet")

    )

    Process multiple large files efficiently

    import glob

    lazyframes = [pl.scanparquet(f) for f in glob.glob("data/part.parquet")]

    combined = pl.concat(lazyframes)

    result = (

    combined

    .groupby("customerid")

    .agg(

    pl.col("purchaseamount").sum().alias("lifetimevalue"),

    pl.col("purchasedate").max().alias("lastpurchase"),

    pl.len().alias("totalorders")

    )

    .filter(pl.col("lifetimevalue") > 1000)

    .sort("lifetimevalue", descending=True)

    .head(1000)

    .collect(streaming=True)

    )

    Convert large CSV to Parquet for better performance

    (

    pl.scancsv("legacydata.csv")

    .sinkparquet(

    "legacydata.parquet",

    compression="zstd",

    rowgroupsize=100000

    )

    )

    Best Practices

  • Use Lazy Evaluation: Always prefer scan over read for large datasets. Let the optimizer do its work.
  • Prefer Parquet: Parquet files are columnar, compressed, and support predicate/projection pushdown. Always convert CSVs to Parquet for repeated analysis.
  • Avoid Row-Wise Operations: Think in columns, not rows. Use expressions instead of apply() or maprows() whenever possible.
  • Chain Operations: Build long expression chains rather than creating intermediate DataFrames. This allows better optimization.
  • Use Appropriate Data Types: Cast columns to the smallest appropriate type. Use pl.Categorical for string columns with few unique values.
  • Leverage over() for Window Functions: Instead of self-joins or merge operations, use over() for group-level computations.
  • Profile Your Queries: Use explain() to inspect query plans and identify bottlenecks.
  • Memory Management: Use streaming mode for datasets larger than RAM. Sink results directly to disk when possible.
  • Use struct and list Types: Polars natively supports nested data types, avoiding the need for complex workarounds.
  • Parallelize I/O: When reading multiple files, use scanparquet with glob patterns to let Polars parallelize reading.
  • Conclusion

    Polars represents a significant advancement in the Python data processing ecosystem. Its Rust-based engine, lazy evaluation with query optimization, and seamless streaming support make it the ideal choice for both analytical workloads and production data pipelines. By adopting Polars best practices -- lazy evaluation, Parquet file formats, columnar thinking, and streaming -- you can process datasets orders of magnitude faster than with traditional tools, while using significantly less memory. For new projects, Polars should be your default DataFrame library.

    Related Articles

    Ibis Tutorial: The Portable Python DataFrame API Across Backends

    Ibis: API Dataframe Python yang Portabel di Banyak Backend Ibis adalah library dataframe Python yang memungkinkan Anda m...

    Marimo Tutorial: Reactive and Reproducible Python Notebooks

    Marimo: Notebook Python yang Reaktif dan Reproducible Marimo adalah notebook Python yang menyimpan isinya sebagai berkas...

    Kedro Tutorial: Reproducible and Maintainable Data Science Pipelines

    Kedro: Pipeline Data Science yang Reproducible dan Mudah Dirawat Sebagian besar proyek data science dimulai dari satu no...

    DuckDB: In-Process Analytical Database for Data Science

    DuckDB: Database Analitik In-Process untuk Data Science DuckDB adalah database analitik in-process yang dirancang khusus...