Polars - 05 İfadeler ve Bağlamlar

İfadeler (Expressions) ve Bağlamlar (Contexts)

Polars, veri dönüşümü için kendi Spesifik Etki Alanı Dilini (Domain Specific Language - DSL) geliştirmiştir. Bu dilin kullanımı çok kolaydır ve insan tarafından okunabilir kalan karmaşık sorgulara izin verir. Expression'lar (ifadeler) ve context'ler (bağlamlar), bu okunabilirliği sağlarken aynı zamanda Polars sorgu motorunun sorgularınızı olabildiğince hızlı çalıştırmak için optimize etmesine izin vermede çok önemlidir.

İfadeler

Polars'da bir ifade (expression), bir veri dönüşümünün lazy (tembel) bir temsilidir. ifadeler (expressions) modüler ve esnektir, yani bunları daha karmaşık ifadeler oluşturmak için yapı taşları olarak kullanabilirsiniz. İşte bir Polars ifade (expression) örneği:

import polars as pl

pl.col("weight") / (pl.col("height") ** 2)

Tahmin edebileceğiniz gibi, bu ifade "weight" adlı bir sütunu alır ve değerlerini "height" sütunundaki değerlerin karesine bölerek bir kişinin BMI'ını (vücut kütle endeksini) hesaplar.

Yukarıdaki kod, bir değişkende kaydedebileceğimiz, daha fazla manipüle edebileceğimiz veya yazdırabileceğimiz soyut bir hesaplamayı ifade eder:

bmi_expr = pl.col("weight") / (pl.col("height") ** 2)
print(bmi_expr)
[(col("weight")) / (col("height").pow([dyn int: 2]))]

İfadeler lazy olduğu için henüz hiçbir hesaplama gerçekleşmemiştir. İşte bu noktada bağlamlara (contexts) ihtiyacımız var.

Bağlamlar (Contexts)

Polars ifadelerinin bir sonuç üretmesi için yürütülecekleri bir bağlam'a ihtiyaçları vardır. Kullanıldığı bağlam'a bağlı olarak, aynı Polars ifade’si farklı sonuçlar üretebilir. Bu bölümde, Polars'ın sağladığı dört yaygın bağlam’ı (context'i) öğreneceğiz:

  1. select
  2. with_columns
  3. filter
  4. group_by

Her bir bağlam'ın nasıl çalıştığını göstermek için aşağıdaki veri çerçevesi’ni (dataframe'i) kullanıyoruz.

Python

from datetime import date

df = pl.DataFrame(
    {
        "name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
        "birthdate": [
            date(1997, 1, 10),
            date(1985, 2, 15),
            date(1983, 3, 22),
            date(1981, 4, 30),
        ],
        "weight": [57.9, 72.5, 53.6, 83.1],  # (kg)
        "height": [1.56, 1.77, 1.65, 1.75],  # (m)
    }
)

print(df)

Rust

use chrono::prelude::*;
use polars::prelude::*;

let df: DataFrame = df!(
    "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
    "birthdate" => [
        NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
        NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
        NaiveDate::from_ymd_opt(1983, 3, 22).unwrap(),
        NaiveDate::from_ymd_opt(1981, 4, 30).unwrap(),
    ],
    "weight" => [57.9, 72.5, 53.6, 83.1],  // (kg)
    "height" => [1.56, 1.77, 1.65, 1.75],  // (m)
)
.unwrap();
println!("{df}");
shape: (4, 4)
┌────────────────┬────────────┬────────┬────────┐
│ name            birthdate   weight  height │
│ ---             ---         ---     ---    │
│ str             date        f64     f64    │
╞════════════════╪════════════╪════════╪════════╡
│ Alice Archer    1997-01-10  57.9    1.56   │
│ Ben Brown       1985-02-15  72.5    1.77   │
│ Chloe Cooper    1983-03-22  53.6    1.65   │
│ Daniel Donovan  1981-04-30  83.1    1.75   │
└────────────────┴────────────┴────────┴────────┘

select Bağlamı

select bağlamı, ifadeleri sütunlar üzerinde uygular. select bağlamı, aggregasyonlar, diğer sütunların kombinasyonları veya literal'ler olan yeni sütunlar üretebilir:

Python

select

result = df.select(
    bmi=bmi_expr,
    avg_bmi=bmi_expr.mean(),
    ideal_max_bmi=25,
)
print(result)

Rust

let bmi = col("weight") / col("height").pow(2);
let result = df
    .clone()
    .lazy()
    .select([
        bmi.clone().alias("bmi"),
        bmi.clone().mean().alias("avg_bmi"),
        lit(25).alias("ideal_max_bmi"),
    ])
    .collect()?;
println!("{result}");
shape: (4, 3)
┌───────────┬───────────┬───────────────┐
│ bmi        avg_bmi    ideal_max_bmi │
│ ---        ---        ---           │
│ f64        f64        i32           │
╞═══════════╪═══════════╪═══════════════╡
│ 23.791913  23.438973  25            │
│ 23.141498  23.438973  25            │
│ 19.687787  23.438973  25            │
│ 27.134694  23.438973  25            │
└───────────┴───────────┴───────────────┘

Bir select bağlam’ındaki ifade'ler, ya aynı uzunlukta series'ler üretmeli ya da bir skaler (scalar) üretmelidir. Skaler'ler, kalan series'lerin uzunluğuna uyacak şekilde broadcast edilir. Yukarıda kullanılan sayı gibi değişmez değerler (literal'ler) de yayınlanır.

Yayınlamanın ifade’ler içinde de gerçekleşebileceğini unutmayın. Örneğin, aşağıdaki ifade'yi ele alalım:

Python

select

result = df.select(deviation=(bmi_expr - bmi_expr.mean()) / bmi_expr.std())
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .select([((bmi.clone() - bmi.clone().mean()) / bmi.clone().std(1)).alias("deviation")])
    .collect()?;
println!("{result}");
shape: (4, 1)
┌───────────┐
│ deviation │
│ ---       │
│ f64       │
╞═══════════╡
│ 0.115645  │
│ -0.097471 │
│ -1.22912  │
│ 1.210946  │
└───────────┘

Hem çıkarma hem de bölme, ifade içinde yayınlama kullanır çünkü ortalamayı ve standart sapmayı hesaplayan alt ifade'ler tek değerlere (skaler) dönüşür.

select bağlam'ı çok esnek ve güçlüdür ve birbirinden bağımsız ve paralel olarak keyfi ifade'lari değerlendirmenize izin verir. Bu, daha sonra göreceğimiz diğer bağlam'lar için de geçerlidir.

with_columns Bağlamı

with_columns bağlam’ı select bağlam'ına çok benzer. İkisi arasındaki temel fark, with_columns bağlamı, orijinal dataframe'deki sütunları ve girdi ifade’lerine göre yeni sütunları içeren yeni bir dataframe oluşturmasıdır; oysa select bağlamı yalnızca girdi ifade'leri tarafından seçilen sütunları içerir:

Python

with_columns

result = df.with_columns(
    bmi=bmi_expr,
    avg_bmi=bmi_expr.mean(),
    ideal_max_bmi=25,
)
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .with_columns([
        bmi.clone().alias("bmi"),
        bmi.mean().alias("avg_bmi"),
        lit(25).alias("ideal_max_bmi"),
    ])
    .collect()?;
println!("{result}");
shape: (4, 7)
┌────────────────┬────────────┬────────┬────────┬───────────┬───────────┬───────────────┐
│ name           ┆ birthdate  ┆ weight ┆ height ┆ bmi       ┆ avg_bmi   ┆ ideal_max_bmi │
│ ---            ┆ ---        ┆ ---    ┆ ---    ┆ ---       ┆ ---       ┆ ---           │
│ str            ┆ date       ┆ f64    ┆ f64    ┆ f64       ┆ f64       ┆ i32           │
╞════════════════╪════════════╪════════╪════════╪═══════════╪═══════════╪═══════════════╡
│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   ┆ 23.791913 ┆ 23.438973 ┆ 25            │
│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   ┆ 23.141498 ┆ 23.438973 ┆ 25            │
│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   ┆ 19.687787 ┆ 23.438973 ┆ 25            │
│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   ┆ 27.134694 ┆ 23.438973 ┆ 25            │
└────────────────┴────────────┴────────┴────────┴───────────┴───────────┴───────────────┘

select ve with_columns arasındaki bu fark nedeniyle, with_columns bağlamında kullanılan ifadeler, dataframe'deki orijinal sütunlarla aynı uzunlukta series'ler üretmelidir; oysa select bağlam'ındaki ifade'lerin kendi aralarında aynı uzunlukta series'ler üretmesi yeterlidir.

filter Bağlamı

filter bağlam'ı, Boolean (Doğru (True) / Yanlış (False)) veri türüne göre değerlendirilen bir veya daha fazla ifadeye dayalı olarak bir veri çerçevesinin satırlarını filtreler.

Python

filter

result = df.filter(
    pl.col("birthdate").is_between(date(1982, 12, 31), date(1996, 1, 1)),
    pl.col("height") > 1.7,
)
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .filter(
        col("birthdate")
            .is_between(
                lit(NaiveDate::from_ymd_opt(1982, 12, 31).unwrap()),
                lit(NaiveDate::from_ymd_opt(1996, 1, 1).unwrap()),
                ClosedInterval::Both,
            )
            .and(col("height").gt(lit(1.7))),
    )
    .collect()?;
println!("{result}");
shape: (1, 4)
┌───────────┬────────────┬────────┬────────┐
│ name       birthdate   weight  height │
│ ---        ---         ---     ---    │
│ str        date        f64     f64    │
╞═══════════╪════════════╪════════╪════════╡
│ Ben Brown  1985-02-15  72.5    1.77   │
└───────────┴────────────┴────────┴────────┘

group_by Bağlamı ve Toplamalar/Kümelemeler (Aggregations)

group_by bağlam’ında, satırlar gruplandırma ifadelerinin benzersiz değerlerine göre gruplandırılır. Daha sonra ortaya çıkan gruplara değişken uzunluklarda olabilecek ifadeler uygulayabilirsiniz.

group_by bağlam'ını kullanırken, gruplamaları dinamik olarak hesaplamak için bir ifade kullanabilirsiniz:

Python

group_by

result = df.group_by(
    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
).agg(pl.col("name"))
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .group_by([(col("birthdate").dt().year() / lit(10) * lit(10)).alias("decade")])
    .agg([col("name")])
    .collect()?;
println!("{result}");
shape: (2, 2)
┌────────┬─────────────────────────────────┐
│ decade  name                            │
│ ---     ---                             │
│ i32     list[str]                       │
╞════════╪═════════════════════════════════╡
│ 1990    ["Alice Archer"]                │
│ 1980    ["Ben Brown", "Chloe Cooper",  │
└────────┴─────────────────────────────────┘

group_by kullandıktan sonra, gruplara toplama/kümeleme/yığınlama/birleştirme ifadelerini (aggregating expressions) uygulamak için agg kullanırız. Yukarıdaki örnekte yalnızca bir sütunun adını belirttiğimiz için, bu sütunun gruplarını listeler halinde alırız.

İstediğimiz kadar gruplama ifadesi (grouping expression) belirtebiliriz ve group_by bağlam’ı, belirtilen ifade’ler arasındaki benzersiz değerlere göre satırları gruplar. Burada, doğum onyılı ve kişinin 1.7 metreden kısa olup olmamasının bir kombinasyonuna göre gruplama yapıyoruz:

Python

group_by

result = df.group_by(
    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
    (pl.col("height") < 1.7).alias("short?"),
).agg(pl.col("name"))
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .group_by([
        (col("birthdate").dt().year() / lit(10) * lit(10)).alias("decade"),
        (col("height").lt(lit(1.7)).alias("short?")),
    ])
    .agg([col("name")])
    .collect()?;
println!("{result}");
shape: (3, 3)
┌────────┬────────┬─────────────────────────────────┐
 decade  short?  name                            
 ---    ┆ ---    ┆ ---                             │
 i32     bool    list[str]                       
╞════════╪════════╪═════════════════════════════════╡
 1980    true    ["Chloe Cooper"]                
 1980    false   ["Ben Brown", "Daniel Donovan"… │
│ 1990   ┆ true   ┆ ["Alice Archer"]                
└────────┴────────┴─────────────────────────────────┘

Toplama/kümeleme/yığınlama/birleştirme ifadeleri (aggregating expressions) uygulandıktan sonra ortaya çıkan veri çerçevesi, soldaki her gruplandırma ifadesi için bir sütun ve ardından toplama/kümeleme/yığınlama/birleştirme ifadelerinin sonuçlarını temsil etmek için gereken sayıda sütun içerir. Buna karşılık istediğimiz kadar toplama ifadesi belirtebiliriz:

Python

group_by

result = df.group_by(
    (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
    (pl.col("height") < 1.7).alias("short?"),
).agg(
    pl.len(),
    pl.col("height").max().alias("tallest"),
    pl.col("weight", "height").mean().name.prefix("avg_"),
)
print(result)

Rust

let result = df
    .clone()
    .lazy()
    .group_by([
        (col("birthdate").dt().year() / lit(10) * lit(10)).alias("decade"),
        (col("height").lt(lit(1.7)).alias("short?")),
    ])
    .agg([
        len(),
        col("height").max().alias("tallest"),
        cols(["weight", "height"])
            .as_expr()
            .mean()
            .name()
            .prefix("avg_"),
    ])
    .collect()?;
println!("{result}");
shape: (3, 6)
┌────────┬────────┬─────┬─────────┬────────────┬────────────┐
│ decade  short?  len  tallest  avg_weight  avg_height │
│ ---     ---     ---  ---      ---         ---        │
│ i32     bool    u32  f64      f64         f64        │
╞════════╪════════╪═════╪═════════╪════════════╪════════════╡
│ 1980    false   2    1.77     77.8        1.76       │
│ 1980    true    1    1.65     53.6        1.65       │
│ 1990    true    1    1.56     57.9        1.56       │
└────────┴────────┴─────┴─────────┴────────────┴────────────┘

Diğer gruplama bağlam’ları için group_by_dynamic ve rolling bölümlerine de bakın.

İfade Genişletme (Expression Expansion)

Son örnek, iki gruplandırma ifadesi ve üç toplama ifadesi içeriyordu, ancak ortaya çıkan veri çerçevesi, beş yerine altı sütun içeriyordu. Yakından bakarsak, son toplama ifadesi iki farklı sütundan bahsediyordu: "weight" ve "height".

Polars ifade’leri, ifade genişletme (expression expansion) adı verilen bir özelliği destekler. İfade Genişletme, aynı dönüşümü birden fazla sütuna uygulamak istediğinizde kullanılan bir kısa gösterimdir. Gördüğümüz gibi,

pl.col("weight", "height").mean().name.prefix("avg_")

ifadesi "weight" ve "height" sütunlarının ortalama değerini hesaplar ve bunları sırasıyla "avg_weight" ve "avg_height" olarak yeniden adlandırır. Aslında, yukarıdaki ifade aşağıdaki iki ifade'yi kullanmaya eşdeğerdir:

[
    pl.col("weight").mean().alias("avg_weight"),
    pl.col("height").mean().alias("avg_height"),
]

Bu durumda, bu ifade, Polars'ın paralel olarak yürütebileceği iki bağımsız ifade'ye genişler. Diğer durumlarda, bir ifade'nin kaç tane bağımsız ifade’ye dönüşeceğini önceden bilemeyebiliriz.

Şu basit ama açıklayıcı örneği ele alalım:

(pl.col(pl.Float64) * 1.1).name.suffix("*1.1")

Bu ifade, Float64 veri tipine sahip tüm sütunları 1.1 ile çarpar. Bunun uygulandığı sütun sayısı, her veri çerçevesinin şema (schema)'sına bağlıdır. Kullandığımız veri çerçevesi durumunda, iki sütuna uygulanır:

Python

select

expr = (pl.col(pl.Float64) * 1.1).name.suffix("*1.1")
result = df.select(expr)
print(result)

Rust

let expr = (dtype_col(&DataType::Float64).as_selector().as_expr() * lit(1.1))
    .name()
    .suffix("*1.1");
let result = df.lazy().select([expr.clone()]).collect()?;
println!("{result}");
shape: (4, 2)
┌────────────┬────────────┐
│ weight*1.1  height*1.1 │
│ ---         ---        │
│ f64         f64        │
╞════════════╪════════════╡
│ 63.69       1.716      │
│ 79.75       1.947      │
│ 58.96       1.815      │
│ 91.41       1.925      │
└────────────┴────────────┘

Aşağıdaki df2 veri çerçevesi'nde ise, aynı ifade hiçbir sütuna uygulanmaz çünkü Float64 veri tipinde hiçbir sütun yoktur:

Python

select

df2 = pl.DataFrame(
    {
        "ints": [1, 2, 3, 4],
        "letters": ["A", "B", "C", "D"],
    }
)
result = df2.select(expr)
print(result)

Rust

let df2: DataFrame = df!(
    "ints" => [1, 2, 3, 4],
    "letters" => ["A", "B", "C", "D"],
)
.unwrap();
let result = df2.lazy().select([expr]).collect()?;
println!("{result}");
shape: (0, 0)
┌┐
╞╡
└┘

Aynı ifade'nin düzinelerce sütuna genişleyeceği bir senaryo hayal etmek de aynı derecede kolaydır.

Bir sonraki bölümde, bir ifade'nin belirli bir şema (schema) karşısında nasıl genişleyeceğini önizlemek için kullanabileceğiniz lazy API ve explain fonksiyonu hakkında bilgi edineceksiniz.

Sonuç

İfade’ler (Expressions) lazy olduğu için, bir ifade'yi bir bağlam içinde kullandığınızda Polars, temsil ettiği veri dönüşümünü çalıştırmadan önce ifade’nizi basitleştirmeyi deneyebilir. Bir bağlam içindeki ayrı ifade'ler utandırıcı derecede paraleldir (embarrassingly parallel) ve Polars bundan yararlanırken, ifade genişletmeyi (expression expansion) kullanıldığında ifade yürütmeyi de paralelleştirir. Daha fazla performans kazancı, bir sonraki bölümde tanıtılan Polars lazy API kullanılarak elde edilebilir.

İfade’lerin yeteneklerinin yalnızca yüzeyine dokunduk. Çok daha fazla ifade vardır ve bunlar çeşitli şekillerde birleştirilebilir. Mevcut farklı ifade türleri hakkında daha derinlemesine bilgi için expression'lar bölümüne bakın.


  1. Ek Liste ve SQL bağlam'ları vardır, bunlar bu kılavuzun ilerleyen bölümlerinde ele alınmaktadır. Ancak basitlik adına, şimdilik bunları kapsam dışı bırakıyoruz.