Close Menu
AI News TodayAI News Today

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Graph Neural Networks: GCN, MPNN, and GAT, Explained Simply

    ‘Wordle in 1’ is the NYT’s new puzzle just for subscribers

    Uber is laying off 10% of staff, or 3,300 people

    Facebook X (Twitter) Instagram
    • About Us
    • Contact Us
    Facebook X (Twitter) Instagram Pinterest Vimeo
    AI News TodayAI News Today
    • Home
    • AI News
    • AI Reviews
    • AI Tools
    • AI Tutorials
    • Chatbots
    • Free AI Tools
    • Artificial Intelligence
    AI News TodayAI News Today
    Home»AI Tools»A Practical Introduction to PySpark Window Functions
    AI Tools

    A Practical Introduction to PySpark Window Functions

    By No Comments16 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Reddit Telegram Email
    A Practical Introduction to PySpark Window Functions
    Share
    Facebook Twitter LinkedIn Pinterest Email

    When you are looking to aggregate your data, the standard PySpark groupBy() function can do all that for you. It’s what it was built for, but it has a fundamental restriction. It only ever returns one row per collection of data records. You SUM a thousand rows, or a million rows, you get one row back.

    Often, that’s exactly what you want, but sometimes it would be handy to also get back some additional data from some or all of the rows that went into the aggregation.

    That’s where the PySpark Window functions come into play. They let you calculate values across related records without collapsing those records into a single result. This means you keep the transaction details while gaining aggregation information about the wider group.

    They are useful when you need rankings, running totals, comparisons with previous records, or calculations within groups.

    In this article, I’ll explain how PySpark window functions work and show how to use them for several common tasks:

    • Ranking rows within groups

    • Calculating running totals

    • Comparing current and previous values

    • Finding each row’s share of a group total

    • Selecting the top records from each group

    My examples use a small sales dataset, but the same techniques apply equally well to larger data sets like event logs, financial records, customer activity, sensor readings, and many other types of ordered or grouped data.

    Table of contents

    1. Setting up PySpark
    2. Creating our example dataset
    3. What is a window?
    4. Ranking rows within each group
    5. row_number, rank, and dense_rank
    6. Selecting the top records from each group
    7. Calculating running totals
    8. Comparing a row with the previous row
    9. Calculating a row’s share of a group total
    10. Calculating moving averages
    11. The rowsBetween and rangeBetween window frames
    12. Reusing window specifications
    13. Performance consideration
    14. Filter early
    15. Select only the required columns
    16. Watch for skewed partitions
    17. Reuse calculated results carefully
    18. Common mistakes
      1. Forgetting partitionBy
      2. Using an incomplete ordering
      3. Expecting a window to reduce rows
      4. Treating row-based windows as time-based windows
    19. Putting the techniques together
    20. Summary

    Setting up PySpark

    If PySpark is not already installed on your system, create a project folder and add it with uv:

    mkdir pyspark-windowscd pyspark-windowsuv inituv venv.venvScriptsactivateuv pip install pyspark

    Next, we can test whether the installation worked OK by creating a Spark session:

    import osimport sysos.environ["PYSPARK_PYTHON"] = sys.executableos.environ["PYSPARK_DRIVER_PYTHON"] = sys.executablefrom pyspark.sql import SparkSessionspark = (    SparkSession.builder    .master("local[*]")    .appName("sales-analysis")    .config("spark.pyspark.python", sys.executable)    .config("spark.pyspark.driver.python", sys.executable)    .getOrCreate())

    The local[*] setting runs Spark locally and allows it to use the available processor cores. You don’t need a cluster to follow my examples. Just make sure the code above runs without errors by saving it to a suitable file and running this command.

    spark-submit spark_example.py

    Creating our example dataset

    The dataset contains sales made by three stores over several days:

    from datetime import datefrom pyspark.sql import functions as Ffrom pyspark.sql import types as Tfrom pyspark.sql.window import Windowsales_data = [    (1, "London_Store", date(2026, 1, 2), "Laptop", 1200.00),    (2, "London_Store", date(2026, 1, 3), "Monitor", 350.00),    (3, "London_Store", date(2026, 1, 5), "Keyboard", 90.00),    (4, "London_Store", date(2026, 1, 8), "Laptop", 1350.00),    (5, "Manchester_Store", date(2026, 1, 2), "Monitor", 320.00),    (6, "Manchester_Store", date(2026, 1, 4), "Laptop", 1100.00),    (7, "Manchester_Store", date(2026, 1, 6), "Mouse", 45.00),    (8, "Manchester_Store", date(2026, 1, 9), "Laptop", 1250.00),    (9, "Bristol_Store", date(2026, 1, 3), "Keyboard", 85.00),    (10, "Bristol_Store", date(2026, 1, 4), "Monitor", 300.00),    (11, "Bristol_Store", date(2026, 1, 7), "Laptop", 1050.00),    (12, "Bristol_Store", date(2026, 1, 10), "Monitor", 330.00),]sales_schema = T.StructType(    [        T.StructField("transaction_id", T.IntegerType(), False),        T.StructField("store", T.StringType(), False),        T.StructField("sale_date", T.DateType(), False),        T.StructField("product", T.StringType(), False),        T.StructField("amount", T.DoubleType(), False),    ])sales = spark.createDataFrame(sales_data, schema=sales_schema)sales.orderBy("store", "sale_date").show()## Output#+--------------+----------------+----------+--------+------+|transaction_id|           store| sale_date| product|amount|+--------------+----------------+----------+--------+------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0||            10|   Bristol_Store|2026-01-04| Monitor| 300.0||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0||            12|   Bristol_Store|2026-01-10| Monitor| 330.0||             1|    London_Store|2026-01-02|  Laptop|1200.0||             2|    London_Store|2026-01-03| Monitor| 350.0||             3|    London_Store|2026-01-05|Keyboard|  90.0||             4|    London_Store|2026-01-08|  Laptop|1350.0||             5|Manchester_Store|2026-01-02| Monitor| 320.0||             6|Manchester_Store|2026-01-04|  Laptop|1100.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|+--------------+----------------+----------+--------+------+

    Each row represents one transaction. We’ll use window functions to analyse the transactions while keeping every row in the result.

    What is a window?

    A window defines the set of rows that PySpark should consider when calculating a value for the current row.

    Most window specifications contain one or more of these parts:

    • partitionBy() divides the data into groups.

    • orderBy() defines the order of rows inside each group.

    • rowsBetween() or rangeBetween() defines the window frame relative to the current row.

    Here is a basic window specification:

    store_window = Window.partitionBy("store")

    This divides the data by store. Every London transaction belongs to one window partition, every Manchester transaction belongs to another, and every Bristol transaction belongs to a third.

    You can use that window with an aggregate function:

    sales_with_store_total = sales.withColumn(    "store_total",    F.sum("amount").over(store_window),)sales_with_store_total.orderBy("store", "sale_date").show()## Output#+--------------+----------------+----------+--------+------+-----------+|transaction_id|           store| sale_date| product|amount|store_total|+--------------+----------------+----------+--------+------+-----------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|     1765.0||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|     1765.0||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|     1765.0||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|     1765.0||             1|    London_Store|2026-01-02|  Laptop|1200.0|     2990.0||             2|    London_Store|2026-01-03| Monitor| 350.0|     2990.0||             3|    London_Store|2026-01-05|Keyboard|  90.0|     2990.0||             4|    London_Store|2026-01-08|  Laptop|1350.0|     2990.0||             5|Manchester_Store|2026-01-02| Monitor| 320.0|     2715.0||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|     2715.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|     2715.0||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|     2715.0|+--------------+----------------+----------+--------+------+-----------+

    The result still contains every transaction, but each row now includes the total sales for its store.

    In contrast, a groupBy() would produce only three rows:

    sales.groupBy("store").agg(    F.sum("amount").alias("store_total")).show()## Output#+----------------+-----------+|           store|store_total|+----------------+-----------+|    London_Store|     2990.0||Manchester_Store|     2715.0||   Bristol_Store|     1765.0|+----------------+-----------+

    The difference is important:

    • Use groupBy() when you want one result row per group.

    • Use a window function when you want group-level calculations alongside the original rows.

    Ranking rows within each group

    Ranking is one of the most common uses of window functions. For example, you might want to rank transactions from largest to smallest within each store.

    First, define a window that partitions by store and orders by transactions by amount:

    sales_rank_window = (Window.partitionBy("store").orderBy(F.col("amount").desc()))

    Then apply row_number():

    ranked_sales = sales.withColumn(    "sale_rank",    F.row_number().over(sales_rank_window),)ranked_sales.orderBy("store", "sale_rank").show()## Output#+--------------+----------------+----------+--------+------+---------+|transaction_id|           store| sale_date| product|amount|sale_rank|+--------------+----------------+----------+--------+------+---------+|            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|        1||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|        2||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|        3||             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|        4||             4|    London_Store|2026-01-08|  Laptop|1350.0|        1||             1|    London_Store|2026-01-02|  Laptop|1200.0|        2||             2|    London_Store|2026-01-03| Monitor| 350.0|        3||             3|    London_Store|2026-01-05|Keyboard|  90.0|        4||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|        1||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|        2||             5|Manchester_Store|2026-01-02| Monitor| 320.0|        3||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|        4|+--------------+----------------+----------+--------+------+---------+

    The largest sale in each store receives rank 1, the second-largest receives rank 2, and so on.

    row_number, rank, and dense_rank

    PySpark provides three closely related ranking functions:

    ranking_comparison = (    sales    .withColumn(        "row_number",        F.row_number().over(sales_rank_window),    )    .withColumn(        "rank",        F.rank().over(sales_rank_window),    )    .withColumn(        "dense_rank",        F.dense_rank().over(sales_rank_window),    ))ranking_comparison.show()## Output#+--------------+----------------+----------+--------+------+----------+----+----------+|transaction_id|           store| sale_date| product|amount|row_number|rank|dense_rank|+--------------+----------------+----------+--------+------+----------+----+----------+|            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|         1|   1|         1||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|         2|   2|         2||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|         3|   3|         3||             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|         4|   4|         4||             4|    London_Store|2026-01-08|  Laptop|1350.0|         1|   1|         1||             1|    London_Store|2026-01-02|  Laptop|1200.0|         2|   2|         2||             2|    London_Store|2026-01-03| Monitor| 350.0|         3|   3|         3||             3|    London_Store|2026-01-05|Keyboard|  90.0|         4|   4|         4||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|         1|   1|         1||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|         2|   2|         2||             5|Manchester_Store|2026-01-02| Monitor| 320.0|         3|   3|         3||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|         4|   4|         4|+--------------+----------------+----------+--------+------+----------+----+----------+

    They differ when two rows have the same ordering value:

    • row_number() always assigns a unique sequential number

    • rank() gives tied rows the same rank and leaves gaps afterwards.

    • dense_rank() gives tied rows the same rank without leaving gaps.

    Imagine sales amounts of 100, 100, and 80. The results for the three rankings would be:

    +--------+------------+------+------------+| amount | row_number | rank | dense_rank |+--------+------------+------+------------+| 100    | 1          | 1    | 1          || 100    | 2          | 1    | 1          || 80     | 3          | 3    | 2          |+--------+------------+------+------------+

    Use row_number() when you need exactly one first, second, or third row. Use rank() or dense_rank() when ties should receive equal treatment.

    Selecting the top records from each group

    One of the most useful ranking patterns is to select the top one or more records from each group. For example, the following code returns the two largest transactions from each store:

    top_two_sales_per_store = (    sales    .withColumn(        "sale_rank",        F.row_number().over(sales_rank_window),    )    .filter(F.col("sale_rank") <= 2)    .orderBy("store", "sale_rank"))top_two_sales_per_store.show()## Output#+--------------+----------------+----------+-------+------+---------+|transaction_id|           store| sale_date|product|amount|sale_rank|+--------------+----------------+----------+-------+------+---------+|            11|   Bristol_Store|2026-01-07| Laptop|1050.0|        1||            12|   Bristol_Store|2026-01-10|Monitor| 330.0|        2||             4|    London_Store|2026-01-08| Laptop|1350.0|        1||             1|    London_Store|2026-01-02| Laptop|1200.0|        2||             8|Manchester_Store|2026-01-09| Laptop|1250.0|        1||             6|Manchester_Store|2026-01-04| Laptop|1100.0|        2|+--------------+----------------+----------+-------+------+---------+

    This is different from, say,

    sales.orderBy(F.col("amount").desc()).limit(2).show()## Output#+--------------+----------------+----------+-------+------+|transaction_id|           store| sale_date|product|amount|+--------------+----------------+----------+-------+------+|             4|    London_Store|2026-01-08| Laptop|1350.0||             8|Manchester_Store|2026-01-09| Laptop|1250.0|+--------------+----------------+----------+-------+------+

    The second example returns the two largest sales across the entire dataset. The window-based example returns two sales from each store.

    This pattern is useful for questions such as:

    • What are the five highest-value orders for each customer?

    • Which three products sell best in each region?

    • What are the latest two events for each device?

    Calculating running totals

    A running total adds the current row’s value to the values from earlier rows.

    To calculate cumulative sales for each store, define a window that:

    1/ Partitions transactions by store.
    2/ Orders them by date and transaction ID.
    3/ Starts at the first row in the partition and ends at the current row.

    running_total_window = (    Window    .partitionBy("store")    .orderBy("sale_date", "transaction_id")    .rowsBetween(        Window.unboundedPreceding,        Window.currentRow,    ))

    Now we can apply the sum() aggregation to the above window:

    sales_with_running_total = sales.withColumn(    "running_store_total",    F.sum("amount").over(running_total_window),)sales_with_running_total.orderBy("store", "sale_date").show()## Output#+--------------+----------------+----------+--------+------+-------------------+|transaction_id|           store| sale_date| product|amount|running_store_total|+--------------+----------------+----------+--------+------+-------------------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|               85.0||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|              385.0||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|             1435.0||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|             1765.0||             1|    London_Store|2026-01-02|  Laptop|1200.0|             1200.0||             2|    London_Store|2026-01-03| Monitor| 350.0|             1550.0||             3|    London_Store|2026-01-05|Keyboard|  90.0|             1640.0||             4|    London_Store|2026-01-08|  Laptop|1350.0|             2990.0||             5|Manchester_Store|2026-01-02| Monitor| 320.0|              320.0||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|             1420.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|             1465.0||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|             2715.0|+--------------+----------------+----------+--------+------+-------------------+

    The first transaction in each store begins the total. Each later transaction adds its amount to the previous total. Because the window is partitioned by store, the calculation starts again when the store changes.

    The second ordering column, transaction_id, makes the ordering deterministic when two transactions share the same date. Without a clear tie-breaker, rows with equal ordering values may not always appear in the order you expect.

    Comparing a row with the previous row

    The lag() function retrieves a value from an earlier row in the same window. It is useful for measuring changes over time.

    First, define a window that orders transactions chronologically within each store:

    store_date_window = (    Window    .partitionBy("store")    .orderBy("sale_date", "transaction_id"))

    Now we can use the lag() function on that window to retrieve the previous transaction amount:

    sales_with_previous_amount = (    sales    .withColumn(        "previous_amount",        F.lag("amount").over(store_date_window),    )    .withColumn(        "change_from_previous",        F.col("amount") - F.col("previous_amount"),    ))sales_with_previous_amount.orderBy("store", "sale_date").show()## Output#+--------------+-------------+----------+--------+------+---------------+--------------------+|transaction_id|        store|sale_date |product |amount|previous_amount|change_from_previous|+--------------+-------------+----------+--------+------+---------------+--------------------+|             9|Bristol_Store|2026-01-03|Keyboard|  85.0|           NULL|                NULL||            10|Bristol_Store|2026-01-04| Monitor| 300.0|           85.0|               215.0||            11|Bristol_Store|2026-01-07|  Laptop|1050.0|          300.0|               750.0||            12|Bristol_Store|2026-01-10| Monitor| 330.0|         1050.0|              -720.0||             1| London_Store|2026-01-02|  Laptop|1200.0|           NULL|                NULL||             2| London_Store|2026-01-03| Monitor| 350.0|         1200.0|              -850.0||             3| London_Store|2026-01-05|Keyboard|  90.0|          350.0|              -260.0||             4| London_Store|2026-01-08|  Laptop|1350.0|           90.0|              1260.0||             5|Manchester_Store|2026-01-02| Monitor| 320.0|        NULL|                NULL||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|       320.0|               780.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|      1100.0|             -1055.0||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|        45.0|              1205.0|+--------------+----------------+----------+--------+------+------------+--------------------+

    The first transaction for each store has no previous transaction, so the previous_amount and change_from_previous columns are NULL.

    You can also compare dates.

    sales_with_previous_date = (    sales    .withColumn(        "previous_sale_date",        F.lag("sale_date").over(store_date_window),    )    .withColumn(        "days_since_previous_sale",        F.datediff("sale_date", "previous_sale_date"),    ))sales_with_previous_date.show()## Output#+--------------+----------------+----------+--------+------+------------------+------------------------+|transaction_id|           store| sale_date| product|amount|previous_sale_date|days_since_previous_sale|+--------------+----------------+----------+--------+------+------------------+------------------------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|              NULL|                    NULL||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|        2026-01-03|                       1||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|        2026-01-04|                       3||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|        2026-01-07|                       3||             1|    London_Store|2026-01-02|  Laptop|1200.0|              NULL|                    NULL||             2|    London_Store|2026-01-03| Monitor| 350.0|        2026-01-02|                       1||             3|    London_Store|2026-01-05|Keyboard|  90.0|        2026-01-03|                       2||             4|    London_Store|2026-01-08|  Laptop|1350.0|        2026-01-05|                       3||             5|Manchester_Store|2026-01-02| Monitor| 320.0|              NULL|                    NULL||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|        2026-01-02|                       2||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|        2026-01-04|                       2||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|        2026-01-06|                       3|+--------------+----------------+----------+--------+------+------------------+------------------------+

    This can help identify gaps in customer activity, delayed events, or changes in daily measurements.

    The related lead() function looks forward instead of backwards.

    sales_with_next_date = sales.withColumn(    "next_sale_date",    F.lead("sale_date").over(store_date_window),)sales_with_next_date.show()## Output#+--------------+----------------+----------+--------+------+--------------+|transaction_id|           store| sale_date| product|amount|next_sale_date|+--------------+----------------+----------+--------+------+--------------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|    2026-01-04||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|    2026-01-07||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|    2026-01-10||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|          NULL||             1|    London_Store|2026-01-02|  Laptop|1200.0|    2026-01-03||             2|    London_Store|2026-01-03| Monitor| 350.0|    2026-01-05||             3|    London_Store|2026-01-05|Keyboard|  90.0|    2026-01-08||             4|    London_Store|2026-01-08|  Laptop|1350.0|          NULL||             5|Manchester_Store|2026-01-02| Monitor| 320.0|    2026-01-04||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|    2026-01-06||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|    2026-01-09||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|          NULL|+--------------+----------------+----------+--------+------+--------------+

    Calculating a row’s share of a group total

    Window aggregates make it easy to compare each row with its group.

    The following code calculates each transaction’s percentage of its store’s total sales:

    store_total_window = Window.partitionBy("store")sales_with_share = (    sales    .withColumn(        "store_total",        F.sum("amount").over(store_total_window),    )    .withColumn(        "share_of_store_sales",        F.col("amount") / F.col("store_total"),    ))sales_with_share.select(    "store",    "transaction_id",    "amount",    "store_total",    F.round("share_of_store_sales", 3).alias(        "share_of_store_sales"    ),).orderBy("store", F.col("amount").desc()).show()## Output#+----------------+--------------+------+-----------+--------------------+|           store|transaction_id|amount|store_total|share_of_store_sales|+----------------+--------------+------+-----------+--------------------+|   Bristol_Store|            11|1050.0|     1765.0|               0.595||   Bristol_Store|            12| 330.0|     1765.0|               0.187||   Bristol_Store|            10| 300.0|     1765.0|                0.17||   Bristol_Store|             9|  85.0|     1765.0|               0.048||    London_Store|             4|1350.0|     2990.0|               0.452||    London_Store|             1|1200.0|     2990.0|               0.401||    London_Store|             2| 350.0|     2990.0|               0.117||    London_Store|             3|  90.0|     2990.0|                0.03||Manchester_Store|             8|1250.0|     2715.0|                0.46||Manchester_Store|             6|1100.0|     2715.0|               0.405||Manchester_Store|             5| 320.0|     2715.0|               0.118||Manchester_Store|             7|  45.0|     2715.0|               0.017|+----------------+--------------+------+-----------+--------------------+

    Because the store total appears alongside every transaction, there is no need to aggregate the data and join the totals back to the original rows.

    This same pattern can calculate:

    • An employee’s salary as a share of departmental payroll

    • A product’s sales as a share of its category

    • A transaction’s value compared with a customer’s total spending

    Calculating moving averages

    A running total includes every earlier row in the partition. A moving calculation uses a limited number of nearby rows.

    For example, the following window includes the current transaction and the previous two transactions:

    moving_average_window = (    Window    .partitionBy("store")    .orderBy("sale_date", "transaction_id")    .rowsBetween(-2, Window.currentRow))

    We can now use this window to calculate a three-transaction moving average:

    sales_with_moving_average = sales.withColumn(    "three_sale_average",    F.avg("amount").over(moving_average_window),)sales_with_moving_average.orderBy("store", "sale_date").show()## Output#+--------------+----------------+----------+--------+------+------------------+|transaction_id|           store| sale_date| product|amount|three_sale_average|+--------------+----------------+----------+--------+------+------------------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|              85.0||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|             192.5||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0| 478.3333333333333||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|             560.0||             1|    London_Store|2026-01-02|  Laptop|1200.0|            1200.0||             2|    London_Store|2026-01-03| Monitor| 350.0|             775.0||             3|    London_Store|2026-01-05|Keyboard|  90.0| 546.6666666666666||             4|    London_Store|2026-01-08|  Laptop|1350.0| 596.6666666666666||             5|Manchester_Store|2026-01-02| Monitor| 320.0|             320.0||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|             710.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0| 488.3333333333333||             8|Manchester_Store|2026-01-09|  Laptop|1250.0| 798.3333333333334|+--------------+----------------+----------+--------+------+------------------+

    For the first row in each store, the average uses one transaction. For the second row, it uses two. From the third row onward, it uses the current transaction and the previous two.

    This is a row-based window, not a time-based window. If one store makes several sales in a day and another makes one sale per week, each calculation still covers three rows.

    The rowsBetween and rangeBetween window frames

    Window frames control which rows contribute to a calculation.
    rowsBetween() uses row positions. This frame example includes the current row and the previous three rows:

    .rowsBetween(-3, Window.currentRow)

    rangeBetween() uses values from the ordering column. Rows with ordering values inside the specified range are included. For example, if the ordering column contains Unix timestamps measured in seconds, this window covers the current timestamp and the previous seven days:

    seconds_in_seven_days = 7 * 24 * 60 * 60seven_day_window = (    Window    .partitionBy("store")    .orderBy(F.col("sale_timestamp").cast("long"))    .rangeBetween(-seconds_in_seven_days, 0))

    The distinction between these two functions is important:

    • Use rowsBetween() when you want a fixed number of records.

    • Use rangeBetween() when you want records within a value or time range.

    Time-based range windows require care because the ordering expression must use a suitable numeric representation and consistent units.

    Reusing window specifications

    Window specifications do not modify a DataFrame by themselves. They describe how a calculation should group, order, and frame rows. Defining windows once and reusing them makes code easier to read:

    store_total_window = Window.partitionBy("store")store_date_window = (    Window    .partitionBy("store")    .orderBy("sale_date", "transaction_id"))running_total_window = (    store_date_window    .rowsBetween(        Window.unboundedPreceding,        Window.currentRow,    ))

    You can then apply several calculations:

    analysed_sales = (    sales    .withColumn(        "store_total",        F.sum("amount").over(store_total_window),    )    .withColumn(        "previous_amount",        F.lag("amount").over(store_date_window),    )    .withColumn(        "running_total",        F.sum("amount").over(running_total_window),    ))analysed_sales.show()## Output#+--------------+----------------+----------+--------+------+-----------+---------------+-------------+|transaction_id|           store| sale_date| product|amount|store_total|previous_amount|running_total|+--------------+----------------+----------+--------+------+-----------+---------------+-------------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|     1765.0|           NULL|         85.0||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|     1765.0|           85.0|        385.0||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|     1765.0|          300.0|       1435.0||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|     1765.0|         1050.0|       1765.0||             1|    London_Store|2026-01-02|  Laptop|1200.0|     2990.0|           NULL|       1200.0||             2|    London_Store|2026-01-03| Monitor| 350.0|     2990.0|         1200.0|       1550.0||             3|    London_Store|2026-01-05|Keyboard|  90.0|     2990.0|          350.0|       1640.0||             4|    London_Store|2026-01-08|  Laptop|1350.0|     2990.0|           90.0|       2990.0||             5|Manchester_Store|2026-01-02| Monitor| 320.0|     2715.0|           NULL|        320.0||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|     2715.0|          320.0|       1420.0||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|     2715.0|         1100.0|       1465.0||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|     2715.0|           45.0|       2715.0|+--------------+----------------+----------+--------+------+-----------+---------------+-------------+

    Clear names such as store_date_window and running_total_window also make it easier to understand why each calculation behaves as it does.

    Performance consideration

    Window functions are useful, but they are not free. PySpark may need to move and sort data so that rows with the same partition key are processed together and appear in the required order. That data shuffle can come at a performance cost.

    To see if that happens, you can inspect the execution plan with:

    analysed_sales.explain("formatted")

    Look for exchange and sort operations. These are often necessary for window calculations, but they can become expensive on large datasets.

    Several habits help keep window queries manageable:

    Filter early

    Remove unnecessary rows before applying a window:

    recent_sales = sales.filter(    F.col("sale_date") >= F.lit("2026-01-05"))

    Filtering first reduces the amount of data that Spark may need to move and sort.

    Select only the required columns

    If the calculation needs only a few columns, remove the others before the window operation:

    window_input = sales.select(    "transaction_id",    "store",    "sale_date",    "amount",)

    Watch for skewed partitions

    If one partition key contains far more rows than the others, one task may have substantially more work to do. For example, partitioning by country can be uneven if most records belong to one country.

    Choose partition keys that match the calculation, but be aware of how the data is distributed.

    Reuse calculated results carefully

    If the same windowed DataFrame is used by several later actions, caching may avoid recalculating it:

    analysed_sales.cache() analysed_sales.count()analysed_sales...

    Caching only helps when the result is reused. Do not automatically cache every intermediate DataFrame.

    Common mistakes

    Forgetting partitionBy

    This window ranks every row across the complete dataset:

    Window.orderBy(F.col("amount").desc())

    That may be correct, but it is not the same as ranking sales separately within each store:

    Window.partitionBy("store").orderBy(F.col("amount").desc() )

    Using an incomplete ordering

    If several rows share the same date, ordering only by date may leave their relative order unclear. Add a suitable tie-breaker, such as a transaction ID, when the sequence matters.

    Expecting a window to reduce rows

    Window functions add calculations to rows; they do not normally reduce the number of rows. To keep only the highest-ranked records, add a rank column and filter it afterwards.

    Treating row-based windows as time-based windows

    rowsBetween(-6, 0) includes seven rows, not seven days. Use a range-based window when the calculation must cover a specific period.

    Putting the techniques together

    The following example adds several useful measures to each transaction:

    store_total_window = Window.partitionBy("store")store_date_window = (    Window    .partitionBy("store")    .orderBy("sale_date", "transaction_id"))running_total_window = (    store_date_window    .rowsBetween(        Window.unboundedPreceding,        Window.currentRow,    ))store_rank_window = (    Window    .partitionBy("store")    .orderBy(F.col("amount").desc()))sales_analysis = (    sales    .withColumn(        "store_total",        F.sum("amount").over(store_total_window),    )    .withColumn(        "share_of_store_total",        F.col("amount") / F.col("store_total"),    )    .withColumn(        "running_store_total",        F.sum("amount").over(running_total_window),    )    .withColumn(        "previous_amount",        F.lag("amount").over(store_date_window),    )    .withColumn(        "change_from_previous",        F.col("amount") - F.col("previous_amount"),    )    .withColumn(        "sale_rank",        F.row_number().over(store_rank_window),    ))sales_analysis.orderBy("store", "sale_date").show()## Output#+--------------+----------------+----------+--------+------+-----------+--------------------+-------------------+---------------+--------------------+---------+|transaction_id|           store| sale_date| product|amount|store_total|share_of_store_total|running_store_total|previous_amount|change_from_previous|sale_rank|+--------------+----------------+----------+--------+------+-----------+--------------------+-------------------+---------------+--------------------+---------+|             9|   Bristol_Store|2026-01-03|Keyboard|  85.0|     1765.0| 0.04815864022662889|               85.0|           NULL|                NULL|        4||            10|   Bristol_Store|2026-01-04| Monitor| 300.0|     1765.0| 0.16997167138810199|              385.0|           85.0|               215.0|        3||            11|   Bristol_Store|2026-01-07|  Laptop|1050.0|     1765.0|  0.5949008498583569|             1435.0|          300.0|               750.0|        1||            12|   Bristol_Store|2026-01-10| Monitor| 330.0|     1765.0| 0.18696883852691218|             1765.0|         1050.0|              -720.0|        2||             1|    London_Store|2026-01-02|  Laptop|1200.0|     2990.0|  0.4013377926421405|             1200.0|           NULL|                NULL|        2||             2|    London_Store|2026-01-03| Monitor| 350.0|     2990.0| 0.11705685618729098|             1550.0|         1200.0|              -850.0|        3||             3|    London_Store|2026-01-05|Keyboard|  90.0|     2990.0|0.030100334448160536|             1640.0|          350.0|              -260.0|        4||             4|    London_Store|2026-01-08|  Laptop|1350.0|     2990.0|   0.451505016722408|             2990.0|           90.0|              1260.0|        1||             5|Manchester_Store|2026-01-02| Monitor| 320.0|     2715.0| 0.11786372007366483|              320.0|           NULL|                NULL|        3||             6|Manchester_Store|2026-01-04|  Laptop|1100.0|     2715.0| 0.40515653775322286|             1420.0|          320.0|               780.0|        2||             7|Manchester_Store|2026-01-06|   Mouse|  45.0|     2715.0|0.016574585635359115|             1465.0|         1100.0|             -1055.0|        4||             8|Manchester_Store|2026-01-09|  Laptop|1250.0|     2715.0|  0.4604051565377532|             2715.0|           45.0|              1205.0|        1|+--------------+----------------+----------+--------+------+-----------+--------------------+-------------------+---------------+--------------------+---------+

    Each transaction now carries information about its store, its position in time, and its ranking by value. The original transaction-level detail remains intact.

    Summary

    PySpark window functions calculate values across related rows while preserving the original records. They are particularly useful when a groupBy() would remove detail that you still need.

    When using Windowing functions, the main takeaways are:

    • Use partitionBy() to define independent groups.

    • Use orderBy() when the calculation depends on sequence.

    • Use a window frame to control which nearby rows contribute.

    • Use ranking functions to compare rows within groups.

    • Use lag() and lead() to compare records across time.

    • Use aggregate functions over windows for totals, shares, and moving calculations.

    Window functions often require Spark to repartition, sort and shuffle data, so inspect execution plans if processing slows down and reduce the inputs by filtering early where possible. Once you understand how the partition, ordering, and window frames work together, window functions become a practical tool for solving many common data-engineering problems.

    Functions Introduction Practical PySpark window
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Previous ArticleWell-executed BGP hijattack uses hijacked IPs to infect real networks
    Next Article Is Russia’s rival to Starlink failing? Here’s what we know.
    • Website

    Related Posts

    AI Tools

    Graph Neural Networks: GCN, MPNN, and GAT, Explained Simply

    AI Tools

    Canva Magic Media Explained: Create Images and Video from Text Prompts

    AI Tools

    Canva Magic Design: The AI Feature That Does the Heavy Lifting

    Add A Comment
    Leave A Reply Cancel Reply

    Top Posts

    Graph Neural Networks: GCN, MPNN, and GAT, Explained Simply

    0 Views

    ‘Wordle in 1’ is the NYT’s new puzzle just for subscribers

    0 Views

    Uber is laying off 10% of staff, or 3,300 people

    0 Views
    Stay In Touch
    • Facebook
    • YouTube
    • TikTok
    • WhatsApp
    • Twitter
    • Instagram
    Latest Reviews
    AI Tutorials

    Quantization from the ground up

    AI Tools

    David Sacks is done as AI czar — here’s what he’s doing instead

    AI Reviews

    Judge sides with Anthropic to temporarily block the Pentagon’s ban

    Subscribe to Updates

    Get the latest tech news from FooBar about tech, design and biz.

    Most Popular

    Graph Neural Networks: GCN, MPNN, and GAT, Explained Simply

    0 Views

    ‘Wordle in 1’ is the NYT’s new puzzle just for subscribers

    0 Views

    Uber is laying off 10% of staff, or 3,300 people

    0 Views
    Our Picks

    Quantization from the ground up

    David Sacks is done as AI czar — here’s what he’s doing instead

    Judge sides with Anthropic to temporarily block the Pentagon’s ban

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    Facebook X (Twitter) Instagram Pinterest
    • About Us
    • Contact Us
    • Terms & Conditions
    • Privacy Policy
    • Disclaimer

    © 2026 ainewstoday.co. All rights reserved. Designed by DD.

    Type above and press Enter to search. Press Esc to cancel.