Polars Primer 04: Creating and Altering Columns

Polars
Python
Rust
Data Analytics
A look at Polars using Python and Rust
Author

Dennis Chua

Published

August 15, 2026

Data Transformation: Motivations

All code snippets use Rent_Contracts.csv as input data, sourced from lemoninabag/Rentals · Datasets at Hugging Face.

Before raw data becomes valuable information, we often need to take the added step of converting it into a form that’s suitable for analysis. Initially this could involve removing noise or irrelevant data. Along the way we may need to reformat the data to ensure compatibility and to improve quality and usability. Later on, we may want to derive information, applying formulas or data aggregation based on our end needs.

Applying Expressions to a Column

In Polars an expression is a function that operates on a series (a single column) to yield a new series. Expressions are logical blocks that we chain together to build complex data transformations.

In its basic form, a Python Polars expression employs a select() operation anchored to a column. To that we pass a pl.col() expression object, supplying the name of the column that we wish to operate on.

For example, in Python we may wish to perform an arithmetic operation on a DataFrame column.

import polars as pl

eager_df = pl.read_csv("Rent_Contracts.csv")
adj_series = eager_df.select(
                pl.col("contract_id"),
                pl.col("actual_area"),
                (pl.col("actual_area") * 0.85).alias("adj_actual_area")
              ).head(5)

print(adj_series)

Unlike Python with its flexible typing, the Rust equivalent requires us to be mindful of function parameter types, particularly the literals we use. The Rust select() statement expects an iterable of Expr structs. To supply this, we need to explicitly convert column names from &str using the col() function. For the same reasons, we call the lit() function to handle the 0.8 literal value.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .select([
            col("contract_id"),
            col("actual_area"),
            (col("actual_area") * lit(0.85))
                .alias("adjusted_actual_area")
        ])
        .limit(5)
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (5, 3)
┌───────────────┬─────────────┬──────────────────────┐
 contract_id   ┆ actual_area ┆ adjusted_actual_area │
 ---------
 str           ┆ f64         ┆ f64                  │
╞═══════════════╪═════════════╪══════════════════════╡
 CRT2128114436 ┆ 30.0        ┆ 25.5                 │
 CNT1786418853 ┆ 89.0        ┆ 75.65                │
 CNT2126820013 ┆ 123.0       ┆ 104.55               │
 CNT2128949309 ┆ 68.0        ┆ 57.8                 │
 CNT2126929158 ┆ 88.0        ┆ 74.8                 │
└───────────────┴─────────────┴──────────────────────┘

As we’ve seen in earlier discussions, Polars supports chaining operations. Let’s say we’re interested in rounding the adjusted_actual_area column.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df
    .select(
        "contract_id",
        "actual_area",

        # The rounding signature is IDENTICAL in the eager context
        # and the lazy context
        (pl.col("actual_area") * 0.85)
            .round(1, mode="half_to_even")
            .alias("adjusted_actual_area")
    )
    .head(5) # Note: typically we use .head() for eager DataFrames instead of .limit()
)

print(subset_df)

The value 1 passed as a parameter to round() indicates the decimal place for rounding. Also, note the round mode parameter. There’s more to say about rounding numbers, and we’ll cover this in a separate section below.

To arrive at the Rust equivalent, we first need to update our Cargo.toml, setting the Polars version to 0.55.1 or later, and then enable the round_series feature.

[dependencies]
polars = { version = "0.55.1", features = ["lazy",
    "csv",
    "fmt",
    "strings",
    "round_series",
    "temporal"
] }

Then, just as with the Python example, we add the round() function and set the rounding mode.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .select([
            col("contract_id"),
            col("actual_area"),
            (col("actual_area") * lit(0.85))
                .round(1, RoundMode::HalfToEven)
                .alias("adjusted_actual_area")
        ])
        .limit(5)
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (5, 3)
┌───────────────┬─────────────┬──────────────────────┐
 contract_id   ┆ actual_area ┆ adjusted_actual_area │
 ---------
 str           ┆ f64         ┆ f64                  │
╞═══════════════╪═════════════╪══════════════════════╡
 CRT2128114436 ┆ 30.0        ┆ 25.5                 │
 CNT1786418853 ┆ 89.0        ┆ 75.6                 │
 CNT2126820013 ┆ 123.0       ┆ 104.6                │
 CNT2128949309 ┆ 68.0        ┆ 57.8                 │
 CNT2126929158 ┆ 88.0        ┆ 74.8                 │
└───────────────┴─────────────┴──────────────────────┘

Polars Rounding Modes

How does Polars round a value of tie value, for example 0.5? There are a handful of rounding modes that affect the outcome. In the Python API, Banker’s Rounding (half_to_even) is the default rounding mode. We could have omitted the mode parameter and still got the same outcome.

When working with Rust, we must pick a mode specified in the polars::prelude::RoundMode (v0.55.1 see here)

pub enum RoundMode {
    HalfToEven,              // Banker's Rounding
    HalfAwayFromZero,        // Standard School Math
    ToZero,                  // Truncation
}
  1. RoundMode::HalfToEven (a.k.a. Banker’s Rounding): If a number ends exactly at 0.5, it rounds to the nearest even integer. For example, 2.5 rounds down to 2.0. 3.5 rounds up to 4.0.

  2. RoundMode::AwayFromZero (a.k.a. Standard School Math): If a number is a tie (0.5), it expands away from zero to the next absolute whole number. For example, 2.5 rounds to 3.0 and -2.5 rounds to -3.0.

  3. RoundMode::TowardsZero (Truncate): We simply drop the decimal. For example, 2.9 rounds to 2.0. -2.9 rounds to -2.0.

Defaulting to HalfToEven rather than HalfAwayFromZero makes sense when dealing with data at scale. On the order of millions of rows, a dataset rounded to the next absolute whole number (HalfAwayFromZero) risks inflating the overall average of the values. With HalfToEven rounding, on the average one-half of the ties (0.5 values) round down, and the other half round up. The two trends balance out and the mean of the dataset stays unbiased.

The with_columns() Operator

Our examples so far use the select() together with col() to apply expressions to a Polars series. The with_columns() is another way to do so.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df
    .with_columns(
        "contract_id",
        (pl.col("actual_area") * 0.85).alias("adj_actual_area")
    )
)

print(subset_df)
shape: (1_215_770, 31)
┌─────────┬─────────┬─────────┬────────┬───┬────────┬────────┬────────┬────────┐
 contrac ┆ contrac ┆ contrac ┆ contra ┆ … ┆ tenant ┆ projec ┆ rooms_ ┆ adj_ac │
 t_id    ┆ t_reg_t ┆ t_reg_t ┆ ct_sta ┆   ┆ _type_ ┆ t_name ┆ en     ┆ tual_a │
 ---     ┆ ype_id  ┆ ype_en  ┆ rt_dat ┆   ┆ en     ┆ ------    ┆ rea    │
 str     ┆ ------     ┆ e      ┆   ┆ ---    ┆ str    ┆ str    ┆ ---
         ┆ i64     ┆ str     ┆ ---    ┆   ┆ str    ┆        ┆        ┆ f64    │
         ┆         ┆         ┆ str    ┆   ┆        ┆        ┆        ┆        │
╞═════════╪═════════╪═════════╪════════╪═══╪════════╪════════╪════════╪════════╡
 CRT2128 ┆ 1       ┆ New     ┆ 2025-1 ┆ … ┆ Person ┆ Azizi  ┆ Studio ┆ 25.5   │
 114436  ┆         ┆         ┆ 2-31   ┆   ┆        ┆ Rivier ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ a 35   ┆        ┆        │
 CNT1786 ┆ 2       ┆ Renew   ┆ 2025-1 ┆ … ┆ Person ┆ Discov ┆ 1 B/R  ┆ 75.65  │
 418853  ┆         ┆         ┆ 2-31   ┆   ┆        ┆ ery    ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ Garden ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ s      ┆        ┆        │
 CNT2126 ┆ 2       ┆ Renew   ┆ 2025-1 ┆ … ┆ Person ┆ Meydan ┆ 2 B/R  ┆ 104.55 │
 820013  ┆         ┆         ┆ 2-31   ┆   ┆        ┆        ┆        ┆        │
 CNT2128 ┆ 2       ┆ Renew   ┆ 2025-1 ┆ … ┆ Person ┆ null   ┆ 2 B/R  ┆ 57.8   │
 949309  ┆         ┆         ┆ 2-28   ┆   ┆        ┆        ┆        ┆        │
 CNT2126 ┆ 2       ┆ Renew   ┆ 2025-1 ┆ … ┆ Person ┆ DAMAC  ┆ 1 B/R  ┆ 74.8   │
 929158  ┆         ┆         ┆ 2-26   ┆   ┆        ┆ HEIGHT ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ S      ┆        ┆        │
 …       ┆ …       ┆ …       ┆ …      ┆ … ┆ …      ┆ …      ┆ …      ┆ …      │
 CNT2090 ┆ 1       ┆ New     ┆ 2014-0 ┆ … ┆ Author ┆ null   ┆ 9 B/R  ┆ 425.0  │
 80015   ┆         ┆         ┆ 2-01   ┆   ┆ ity    ┆        ┆        ┆        │
 CNT3452 ┆ 1       ┆ New     ┆ 2014-0 ┆ … ┆ Author ┆ null   ┆ None   ┆ 425.0  │
 28436   ┆         ┆         ┆ 2-01   ┆   ┆ ity    ┆        ┆ B/R    ┆        │
 CNT2081 ┆ 1       ┆ New     ┆ 2014-0 ┆ … ┆ Author ┆ null   ┆ None   ┆ 425.0  │
 47537   ┆         ┆         ┆ 2-01   ┆   ┆ ity    ┆        ┆ B/R    ┆        │
 CRT1182 ┆ 1       ┆ New     ┆ 2013-0 ┆ … ┆ Person ┆ GRANDE ┆ 4 B/R  ┆ 406.3  │
 225816  ┆         ┆         ┆ 6-20   ┆   ┆        ┆ UR RES ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ IDENCE ┆        ┆        │
         ┆         ┆         ┆        ┆   ┆        ┆ S      ┆        ┆        │
 CNT1522 ┆ 2       ┆ Renew   ┆ 2013-0 ┆ … ┆ Person ┆ null   ┆ 1 B/R  ┆ 33.15  │
 25829   ┆         ┆         ┆ 2-25   ┆   ┆        ┆        ┆        ┆        │
└─────────┴─────────┴─────────┴────────┴───┴────────┴────────┴────────┴────────┘

When we substitute the select() for the with_columns() operation we get the same result but with an important twist.

subset_df = (
    rental_df
    .select(
        "contract_id",
        (pl.col("actual_area") * 0.85).alias("adj_actual_area")
    )
)
shape: (1_215_770, 2)
┌───────────────┬─────────────────┐
 contract_id   ┆ adj_actual_area │
 ------
 str           ┆ f64             │
╞═══════════════╪═════════════════╡
 CRT2128114436 ┆ 25.5            │
 CNT1786418853 ┆ 75.65           │
 CNT2126820013 ┆ 104.55          │
 CNT2128949309 ┆ 57.8            │
 CNT2126929158 ┆ 74.8            │
 …             ┆ …               │
 CNT209080015  ┆ 425.0           │
 CNT345228436  ┆ 425.0           │
 CNT208147537  ┆ 425.0           │
 CRT1182225816 ┆ 406.3           │
 CNT152225829  ┆ 33.15           │
└───────────────┴─────────────────┘

Visually the select() only returns the columns named as parameters to the method: two columns. In contrast, with_columns() appears to take the original dataset and append the new column (adj_actual_area) on to it, all thirty-one.

Behind the scenes, Polars leaves the original dataset untouched. In the case of a with_columns() operation, it duplicates the table columns (all 31) and appends the new column specified with the alias() method. With select() Polars takes a narrowing approach. While leaving the source dataset untouched, it duplicates only the specified columns and appends any newly computed column data. The behavior holds for both the Rust and Python APIs, and regardless of the mode of execution, eager or lazy.

For the sake of completion, here is the Rust equivalent that uses with_columns().

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .with_columns([
            col("contract_id"),
            (col("actual_area") * lit(0.85)).alias("adj_actual_area")
        ])
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}

The use of select() or with_columns() goes beyond expressions involving a single column. In the following example, contract_amount and no_of_prop are involved in the calculation.

import polars as pl

rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df
    .select(
        "contract_id",
        "contract_amount",
        "no_of_prop",
        (pl.col("contract_amount") / pl.col("no_of_prop")).alias("contract_cost_per_prop")
    )
    # Sort the ENTIRE DataFrame by the new column, keeping rows aligned
    .sort("contrast_cost_per_prop", descending=False)
)

print(subset_df)
shape: (1_215_770, 4)
┌───────────────┬─────────────────┬────────────┬────────────────────────┐
 contract_id   ┆ contract_amount ┆ no_of_prop ┆ contract_cost_per_prop │
 ------------
 str           ┆ i64             ┆ i64        ┆ f64                    │
╞═══════════════╪═════════════════╪════════════╪════════════════════════╡
 CRT2079944156 ┆ 3300037950      ┆ 1          ┆ 3.3000e9               │
 CNT2121530555 ┆ 441080000       ┆ 1          ┆ 4.4108e8               │
 CNT1078663856 ┆ 441080000       ┆ 1          ┆ 4.4108e8               │
 CRT2101278846 ┆ 276281563       ┆ 1          ┆ 2.76281563e8           │
 CNT2128571585 ┆ 261360000       ┆ 1          ┆ 2.6136e8               │
 …             ┆ …               ┆ …          ┆ …                      │
 CNT2121503290 ┆ 0               ┆ 1          ┆ 0.0                    │
 CNT2121498848 ┆ 0               ┆ 1          ┆ 0.0                    │
 CNT2121495732 ┆ 0               ┆ 1          ┆ 0.0                    │
 CNT2121502991 ┆ 0               ┆ 1          ┆ 0.0                    │
 CNT2043525650 ┆ 0               ┆ 1          ┆ 0.0                    │
└───────────────┴─────────────────┴────────────┴────────────────────────┘

As far as data analysis is concerned, are there any reasons for favoring select() over with_columns()? When we use select() early on, we eliminate columns that are not relevant to analysis. This narrowing of data optimizes the memory Polars uses, resulting in faster operations in subsequent expressions involving with_columns().

Altering Existing Columns

To round out this section, let’s explore ways we can alter the nature of a Polars column.

1. Updating a Column Name

We can rename an existing column using a combination of the rename() and the select() operations. First, we supply rename() with a mapping between existing and new column names. Then we use select() to project the new columns onto the resulting DataFrame.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

column_rename = { "tenant_type_en": "occupancy_type", "project_name": "customer_name" }

subset_df = (
    rental_df.rename(column_rename)
).select(pl.col("contract_id"), pl.col("occupancy_type"), pl.col("customer_name")).head(3)

print(subset_df)

In the example above, we use a Python dictionary to remap the column names. For the Rust version, we need two separate arrays for this sort of transformation.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .rename(
            ["tenant_type_en", "project_name"],
            ["occupancy_type", "customer_name"],
            true
        )
        .select([
            col("contract_id"),
            col("occupancy_type"),
            col("customer_name")
        ])
        .limit(3)
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (3, 3)
┌───────────────┬────────────────┬───────────────────┐
 contract_id   ┆ occupancy_type ┆ customer_name     │
 ---------
 str           ┆ str            ┆ str               │
╞═══════════════╪════════════════╪═══════════════════╡
 CRT2128114436 ┆ Person         ┆ Azizi Riviera 35  │
 CNT1786418853 ┆ Person         ┆ Discovery Gardens │
 CNT2126820013 ┆ Person         ┆ Meydan            │
└───────────────┴────────────────┴───────────────────┘

Note the third parameter of the Rust rename() operation, a boolean flag. When this strict flag is set to true Rust first confirms that a column to be renamed exists. In case it doesn’t exist, Rust throws an exception. A false setting is simply for compatibility with older versions of Polars. When the strict flag is set to false, Rust ignores any missing column.

Similar to rename() the name.suffix() operator let’s us append a string to a column name. In the following example, we rename columns in the context of aggregate expressions.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df
    .select(
        pl.col("actual_area").min().name.suffix("_min"),
        pl.col("actual_area").max().name.suffix("_max")
    )
)

print(subset_df)
use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .select([
            col("actual_area").min().name().suffix("_min"),
            col("actual_area").max().name().suffix("_max")
        ])
        .collect()?; // Materialize the DataFrame

    println!("{:?}", subset_df);

    Ok(())
}
shape: (1, 2)
┌─────────────────┬─────────────────┐
 actual_area_min ┆ actual_area_max │
 ------
 f64             ┆ f64             │
╞═════════════════╪═════════════════╡
 0.0             ┆ 4.211183e8      │
└─────────────────┴─────────────────┘

Both the name and name() refer to the name namespace. The name() function belong to a set of methods that operate on column metadata.

2. Update a Column Data Type

Aside from renaming a column, we can also change an existing column’s dtype, or data type, the specific format in which a piece of data is stored inside a column. For this we use the cast() operator.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df.with_columns(
        pl.col("no_of_prop").cast(pl.Int16)
    )
).select(pl.col("contract_id"), pl.col("no_of_prop")).head(3)

print(subset_df)
use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .with_columns([
            col("no_of_prop").cast(DataType::Int16)
        ])
        .select([
            col("contract_id"),
            col("no_of_prop"),
        ])
        .limit(3)
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (3, 2)
┌───────────────┬────────────┐
 contract_id   ┆ no_of_prop │
 ------
 str           ┆ i16        │
╞═══════════════╪════════════╡
 CRT2128114436 ┆ 1          │
 CNT1786418853 ┆ 1          │
 CNT2126820013 ┆ 1          │
└───────────────┴────────────┘

Because Polars underneath the hood is written with Rust, the strongly-typed language enforces safety and predictability rules when casting data types. The rules we explore here are explained in detail in the Expressions/Casting section of the Polars User Guide.

  1. Numerical Types

    A float cast to an integer type is truncated towards zero. For example 2.9 becomes 2 and -2.9 becomes -2. To achieve Banker’s Rounding or Standard Rounding, we need to explicitly call the round() operation followed by a call to .cast().

    When casting within floating types or within integer types, care must be taken in the case of narrowing cast that could result in overflow. In these situations, the resulting integer has fewer bits than the original and using cast() may alter the sign of the result or its value. If instead we use strict_cast(), Rust will raise an error, potentially saving us from corrupting the data crucial to our analysis.

  2. String Types

    The standard cast() will convert string representations of integers to their numerical value. When it comes to strings such as "banana", cast() will convert them to NULL. To avoid the propagation of NULLs in our data, we can use strict_cast() in which case Rust will throw an error with details about the conversion error.

  3. Boolean Types

    We can convert between boolean type and numerical type, with True mapped to 1 and False converting to 0. In case we start with a numerical type, cast() will change a 0 to False and convert any other integer value to True, acting in accordance with Python’s concept of Truthy and Falsy values.

  4. Date/Time Types

    The Rust API requires the "temporal" feature enabled in Cargo.toml. With this set, we need to explicitly define the formatting schema.

    col("date_string").str().to_date(StrptimeOptions::default())

The bottom line: Rust offers the strict_cast() as an alternative to standard cast() to catch conversion situations that corrupt data. Python also allows us to switch between the two conversion modes.

# 1. STRICT MODE (The Default)
# This will CRASH because "apple" cannot be an integer.
# Equivalent to Rust's .strict_cast()
strict_df = df.select(
    pl.col("problematic_data").cast(pl.Int32)
)


# 2. LENIENT MODE
# This succeeds. "apple" silently becomes a Null (None) value.
# Equivalent to Rust's standard .cast()
lenient_df = df.select(
    pl.col("problematic_data").cast(pl.Int32, strict=False)
)

By default the Python cast() maps to Rust API that uses strict_cast() mode.

3. Delete a Column

The last Polars operation we’re discussing is how to remove an existing column. With both the Python and Rust APIs, we simply use the drop() operation, passing a series of column names to delete.

import polars as pl

# Eagerly load the entire CSV into memory as a DataFrame
rental_df = pl.read_csv("Rent_Contracts.csv")

subset_df = (
    rental_df.drop("contract_reg_type_id")
).head(3)

print(subset_df)
use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .drop(cols(["contract_reg_type_id"]))
        .limit(3)
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (3, 29)
┌──────────┬──────────┬──────────┬──────────┬───┬──────────┬──────────┬──────────┬──────────┐
 contract ┆ contract ┆ contract ┆ contract ┆ … ┆ tenant_t ┆ tenant_t ┆ project_ ┆ rooms_en │
 _id      ┆ _reg_typ ┆ _start_d ┆ _end_dat ┆   ┆ ype_id   ┆ ype_en   ┆ name     ┆ ---
 ---      ┆ e_en     ┆ ate      ┆ e        ┆   ┆ ---------      ┆ str      │
 str      ┆ ---------      ┆   ┆ f64      ┆ str      ┆ str      ┆          │
          ┆ str      ┆ str      ┆ str      ┆   ┆          ┆          ┆          ┆          │
╞══════════╪══════════╪══════════╪══════════╪═══╪══════════╪══════════╪══════════╪══════════╡
 CRT21281 ┆ New      ┆ 2025-12- ┆ 2026-12- ┆ … ┆ 1.0      ┆ Person   ┆ Azizi    ┆ Studio   │
 14436    ┆          ┆ 31       ┆ 30       ┆   ┆          ┆          ┆ Riviera  ┆          │
          ┆          ┆          ┆          ┆   ┆          ┆          ┆ 35       ┆          │
 CNT17864 ┆ Renew    ┆ 2025-12- ┆ 2030-12- ┆ … ┆ 1.0      ┆ Person   ┆ Discover ┆ 1 B/R    │
 18853    ┆          ┆ 31       ┆ 30       ┆   ┆          ┆          ┆ y        ┆          │
          ┆          ┆          ┆          ┆   ┆          ┆          ┆ Gardens  ┆          │
 CNT21268 ┆ Renew    ┆ 2025-12- ┆ 2026-12- ┆ … ┆ 1.0      ┆ Person   ┆ Meydan   ┆ 2 B/R    │
 20013    ┆          ┆ 31       ┆ 30       ┆   ┆          ┆          ┆          ┆          │
└──────────┴──────────┴──────────┴──────────┴───┴──────────┴──────────┴──────────┴──────────┘