2025, Nov 01 20:02
Polars में struct बनाम multi-key join: null और is_in का सही उपयोग
Polars में row‑level तुलना में null/None, is_in, struct key और multi‑key join का अंतर, कारण और pandas जैसा सुसंगत व्यवहार पाने के व्यावहारिक तरीके जानें.
DataFrames के बीच पंक्ति-स्तर पर तुलना तब तक भले ही बहुत सीधी लगे, जब तक कि null मान तस्वीर में नहीं आते। Polars में None का डिफॉल्ट व्यवहार इस बात पर बदल सकता है कि आप struct के साथ is_in इस्तेमाल कर रहे हैं या multi‑key join। अगर आप pandas‑स्टाइल सेमांटिक्स चाहते हैं, जहाँ None कभी मेल नहीं खाता, तो इसे स्पष्ट रूप से बताना होगा। यह गाइड यह अंतर दिखाती है, कारण समझाती है, और बताती है कि एकसमान व्यवहार कैसे सुनिश्चित करें।
अंतर को पुनः उत्पन्न करना
सेटअप में पूरी तालिका और उसका उपसमुच्चय शामिल है। हम दोनों के बीच पंक्तियों की तुलना करेंगे, उन स्थितियों सहित जहाँ किसी कुंजी स्तंभ में None है।
import polars as pl
rec1 = {"foo": "a", "bar": "b", "baz": "c"}
rec2 = {"foo": "x", "bar": "y", "baz": "z"}
rec3 = {"foo": "a", "bar": "b", "baz": None}
rec4 = {"foo": "m", "bar": "n", "baz": "o"}
rec5 = {"foo": "x", "bar": "y", "baz": None}
rec6 = {"foo": "a", "bar": "b", "baz": None}
all_rows = [rec1, rec2, rec3, rec4, rec5, rec6]
subset_rows = [rec1, rec2, rec3]
key_cols = ["foo", "bar", "baz"]
df_all = pl.DataFrame(all_rows)
df_sub = pl.DataFrame(subset_rows)
key_struct = (
df_sub
.select(pl.struct(pl.col(key_cols)).alias("key_struct"))
.get_column("key_struct")
)
सभी कॉलमों के struct पर is_in लगाने से उपसमुच्चय में पंक्ति‑सदस्यता का पता चलता है। गौर करें कि baz में None होने पर भी कुछ पंक्तियाँ मेल खाती दिखाई दे सकती हैं।
df_all.with_columns(
pl.struct(pl.all()).is_in(key_struct.implode()).alias("hit")
)
वहीं, साधारण multi‑key join null मानों को समान नहीं मानता, इसलिए वह केवल पूरी तरह non‑null मेल वापस करता है।
df_all.join(df_sub, on=key_cols)
एकल struct key पर join, is_in के व्यवहार से मेल खाता है और ऐसी पंक्तियाँ भी मिला सकता है जिनका संयुक्त key None समेटे हो।
df_all.join(df_sub, on=pl.struct(key_cols))
परिणाम अलग क्यों आते हैं
यह अंतर इस बात से उपजता है कि ये ऑपरेशन्स null के साथ समानता को कैसे समझते हैं। अलग‑अलग कॉलमों पर multi‑key join null को null के बराबर नहीं मानता। इसके विपरीत, is_in या join के लिए एक संयुक्त struct key लेने पर पूरा struct एक इकाई की तरह जाँचा जाता है; ऊपर के उदाहरण में इससे None वाले संयोजनों को भी मेल माना जा सकता है। इसलिए null मौजूद हों तो ये दोनों तरीके परस्पर अदला‑बदली योग्य नहीं हैं।
संस्करण संबंधी नोट: is_in और null हैंडलिंग
हाल में is_in के null propagation में बदलाव आया है। नीचे दिया गया छोटा उदाहरण दिखाता है कि पहले null वाली सूची के विरुद्ध एक स्केलर null की जाँच true देती थी, जबकि अब उसका परिणाम null आता है।
import polars as pl
tab = pl.select(a=None, b=[None])
tab = tab.cast({"a": pl.String, "b": pl.List(pl.String)})
print(tab.with_columns(c=pl.col.a.is_in("b")))
polars 1.27.1 में परिणाम था:
shape: (1, 3)
┌──────┬───────────┬──────┐
│ a ┆ b ┆ c │
│ str ┆ list[str] ┆ bool │
╞══════╪═══════════╪══════╡
│ null ┆ [null] ┆ true │
└──────┴───────────┴──────┘
polars 1.28.0 में यह बना:
shape: (1, 3)
┌──────┬───────────┬──────┐
│ a ┆ b ┆ c │
│ str ┆ list[str] ┆ bool │
╞══════╪═══════════╪══════╡
│ null ┆ [null] ┆ null │
└──────┴───────────┴──────┘
एक नेस्टेड बाएँ‑पक्ष के लिए—जैसे null समेटे एक सूची की तुलना null वाली सूचियों की सूची से—यह उदाहरण true देता है:
import polars as pl
tab2 = pl.select(a=[None], b=[[None]])
tab2 = tab2.cast({"a": pl.List(pl.String), "b": pl.List(pl.List(pl.String))})
print(tab2.with_columns(c=pl.col.a.is_in("b")))
shape: (1, 3)
┌───────────┬─────────────────┬──────┐
│ a ┆ b ┆ c │
│ list[str] ┆ list[list[str]] ┆ bool │
╞═══════════╪═════════════════╪══════╡
│ [null] ┆ [[null]] ┆ true │
└───────────┴─────────────────┴──────┘
ये उदाहरण दिखाते हैं कि is_in में null की व्याख्या सूक्ष्म है और संस्करण पर निर्भर करती है। जब शुद्धता किसी खास नियम पर टिकी हो, तो उसे स्पष्ट रूप से व्यक्त करें।
Polars में pandas जैसा व्यवहार पाना
डिफॉल्ट रूप से, pandas में सामान्य DataFrame.isin और उसके बाद all(axis=1) वाले वर्कफ़्लो में None कभी मेल नहीं खाता। Polars में वही नतीजा पाने के लिए पंक्ति‑स्तर की सदस्यता जाँचने से पहले null वाली पंक्तियों को बाहर करना होगा।
import polars as pl
rec1 = {"foo": "a", "bar": "b", "baz": "c"}
rec2 = {"foo": "x", "bar": "y", "baz": "z"}
rec3 = {"foo": "a", "bar": "b", "baz": None}
rec4 = {"foo": "m", "bar": "n", "baz": "o"}
rec5 = {"foo": "x", "bar": "y", "baz": None}
rec6 = {"foo": "a", "bar": "b", "baz": None}
all_rows = [rec1, rec2, rec3, rec4, rec5, rec6]
subset_rows = [rec1, rec2, rec3]
key_cols = ["foo", "bar", "baz"]
df_all = pl.DataFrame(all_rows)
df_sub = pl.DataFrame(subset_rows)
key_struct = (
df_sub
.select(pl.struct(pl.col(key_cols)).alias("key_struct"))
.get_column("key_struct")
)
result = df_all.with_columns(
pl.all_horizontal(
pl.all().is_not_null(),
pl.struct(pl.all()).is_in(key_struct.implode())
).alias("hit")
)
print(result)
तुलना के लिए अगर आप pandas का आधार देखना चाहें, तो वहाँ समान कोड में कोई अतिरिक्त जाँच की जरूरत नहीं पड़ती, क्योंकि इस पैटर्न में None मान डिफॉल्ट रूप से नहीं मिलते:
import pandas as pd
pd_all = pd.DataFrame(all_rows)
pd_sub = pd.DataFrame(subset_rows)
pd_all["hit_like_pandas"] = pd_all[key_cols].isin(pd_sub).all(1)
print(pd_all)
ऊपर दिए गए Polars अभिव्यक्ति में all_horizontal दो शर्तों को जोड़ता है: पहला, पंक्ति के हर कॉलम का non‑null होना; दूसरा, पूरी पंक्ति (struct के रूप में) उपसमुच्चय की struct series का सदस्य होना। दिए गए डेटा पर यह pandas वाले स्निपेट जैसा ही परिणाम देता है।
यह क्यों मायने रखता है
डुप्लीकेट हटाने, फ़िल्टरिंग और अखंडता जाँच जैसे कार्यों में क्रॉस‑फ़्रेम तुलना बुनियादी है। None की व्याख्या में छोटे फर्क multi‑key join, struct join या is_in में से जो भी आप चुनें, उसके अनुसार अनपेक्षित मिसमैच या छूटे हुए मेल बन सकते हैं। क्योंकि is_in का null से जुड़ा व्यवहार संस्करणों के बीच बदला है, केवल निहित डिफॉल्ट्स पर भरोसा अलग‑अलग नतीजे दे सकता है। इसलिए इच्छित सेमांटिक्स—null मिलना चाहिए या नहीं—को स्पष्ट रूप से कोड करना तर्क को भरोसेमंद बनाता है।
निष्कर्ष
कई कॉलमों में सचमुच पंक्ति‑दर‑पंक्ति मिलान चाहिए तो struct चुनें। multi‑key join वहाँ मेल छोड़ देगा जहाँ कोई भी key null हो, जबकि struct‑आधारित तरीका अलग तरह से पेश आ सकता है। यदि आप pandas जैसा व्यवहार चाहते हैं, जहाँ इस परिदृश्य में None कभी मेल न गिने, तो ऊपर दिखाए अनुसार non‑null जाँच को struct‑आधारित is_in के साथ जोड़ें। जब परिणाम null हैंडलिंग पर निर्भर हों, तो डिफॉल्ट पर नहीं, बल्कि नियम को साफ‑साफ परिभाषित करें।
यह लेख StackOverflow के प्रश्न (लेखक: dewser_the_board) और jqurious के उत्तर पर आधारित है।