2025, Oct 18 13:31
Polars में cum_sum_horizontal बग और 1.32.0 में unnest फिक्स
Polars 1.31.0 में cum_sum_horizontal + unnest से ghost literal कॉलम और ColumnNotFoundError क्यों आते हैं, जानें. 1.32.0 में फिक्स, *args और cum_fold का सही उपयोग.
Polars में क्षैतिज cumulative sums बहुत उपयोगी होते हैं — जब तक वे चुपचाप आपके schema में एक भुतहा कॉलम नहीं डाल देते। अगर आप cum_sum_horizontal को unnest के साथ चेन करते हैं, तो एक अड़ा हुआ literal कॉलम रह सकता है जो हटने का नाम नहीं लेता, और स्पष्ट रूप से drop करने के बाद भी ColumnNotFoundError फेंक देता है। यह गाइड पुनरुत्पादित होने वाला केस, Polars 1.32.0 में क्या बदला, और अपने कोड को कैसे समायोजित करें — सब बताती है।
समस्या को दोहराने का तरीका
नीचे दिया गया स्निपेट Polars 1.31.0 में दिखी समस्या दर्शाता है: horizontal cumulative sum और unnest के बाद schema में एक literal कॉलम दिखता है, और उसे drop करने पर भी वह बना रहता है।
import polars as pl
def run_schema_anomaly():
    print("Polars version:", pl.__version__)
    table = pl.DataFrame({
        "A": [1, 2, 3],
        "T0": [0.1, 0.2, 0.3],
        "T1": [0.4, 0.5, 0.6],
        "T2": [0.7, 0.8, 0.9],
    })
    steps = ["T0", "T1", "T2"]
    print("Original columns:", table.columns)
    print("Time columns:", steps)
    lf = table.lazy()
    print("Schema before cumsum:", lf.collect_schema().names())
    stage = (
        lf.select(pl.cum_sum_horizontal(steps))
          .unnest("cum_sum")
          .rename({name: f"C{name}" for name in steps})
    )
    print("Schema after cumsum:", stage.collect_schema().names())
    try:
        _ = stage.collect()
        print("v1: No bug reproduced")
    except pl.exceptions.ColumnNotFoundError as err:
        print(f"v1: BUG REPRODUCED: {err}")
    stage2 = stage.drop("literal")
    stage2 = pl.concat([pl.LazyFrame({"B": [1, 2, 3]})], how="horizontal").hstack(stage2)
    print("Schema after drop and concat:", stage2.collect_schema().names())
    try:
        _ = stage2.collect()
        print("v2: No bug reproduced")
    except pl.exceptions.ColumnNotFoundError as err:
        print(f"v2: BUG REPRODUCED: {err}")
if __name__ == "__main__":
    run_schema_anomaly()
नतीजा दिखाता है कि cumulative ऑपरेशन और unnest स्टेप के बाद schema में एक अप्रत्याशित literal प्रविष्टि आ जाती है। उसे drop करने और दूसरी फ्रेम को क्षैतिज रूप से जोड़ने के बाद भी, collect करने पर ColumnNotFoundError आता है।
असल में हो क्या रहा है
यह एक बग है। Polars 1.31.0 में cum_sum_horizontal और unnest का संयोजन एक भूतिया literal कॉलम दे सकता था, जो inferred schema में बना रहता था और योजना के निष्पादन पर डाउनस्ट्रीम विफलताएँ पैदा करता था। देखने में schema सही लगता था, पर collect के समय उसके अनुरूप हल नहीं हो पाता था — इसी वजह से ColumnNotFoundError आता था।
Polars 1.32.0 में सुधार और व्यवहार में बदलाव
Polars 1.32.0 में literal कॉलम वाले मुद्दे का समाधान शामिल है। अपग्रेड के बाद यह ghost कॉलम समस्या खत्म हो जाती है। हालांकि, cum_sum_horizontal अब तर्कों को स्वीकार करने के तरीके में एक जुड़ा बदलाव लाता है। नामों की सूची सीधे पास करने पर अब InvalidOperationError मिलता है; आपको सूची को अनपैक करना होगा।
# अब 1.32.0 पर यह त्रुटि देता है
lf.select(pl.cum_sum_horizontal(steps)).collect()
# InvalidOperationError: cannot add columns: dtype was not list on all nesting levels: 
# (left: list[str], right: f64)
कॉलम्स को अनपैक करना अपेक्षित रूप से काम करता है:
lf.select(pl.cum_sum_horizontal(*steps)).collect()
प्रति-कॉलम cumulative मान पाने के लिए, पहले की तरह unnest और rename करें:
fixed = (
    lf.select(pl.cum_sum_horizontal(*steps))
      .unnest("cum_sum")
      .rename({name: f"C{name}" for name in steps})
)
# fixed.collect()  # 1.32.0 पर सफल होता है
स्रोत कोड दिखाता है कि cum_sum_horizontal, cum_fold का एक wrapper है। 1.32.0 में cum_fold अब भी सूची स्वीकार करता है। अगर आप list-आधारित API रखना चाहते हैं, तो सीधे cum_fold का उपयोग करें और फिर परिणाम को unnest कर लें।
(
    lf
      .select(pl.cum_fold(0, lambda x, y: x + y, steps))
      .unnest(pl.all())
      .collect()
)
यह क्यों मायने रखता है
Lazy pipelines में हल्के से schema mismatch डीबग करना महंगा पड़ता है। जब कोई कॉलम logical plan में तो दिखता है, पर निष्पादन के समय materialize नहीं हो पाता, तब त्रुटियाँ सिर्फ collect के समय सामने आती हैं और अक्सर मूल रूपांतरण से कटी हुई लगती हैं। यह जानना कि literal कॉलम वाला मुद्दा 1.31.0 में एक बग था और 1.32.0 में cum_sum_horizontal के इनपुट लेने का तरीका बदला है, आपको भूतिया कॉलम और arguments के आकार से जुड़ी उलझनों पर समय बर्बाद होने से बचाता है।
व्यावहारिक निष्कर्ष
Horizontal cumulative sums के बाद literal कॉलम वाले schema आर्टिफैक्ट से बचने के लिए Polars 1.32.0 या नए संस्करण पर अपग्रेड करें। अगर आप cum_sum_horizontal पर निर्भर हैं, तो कॉलम्स को सूची के बजाय अनपैक्ड तर्कों के रूप में पास करें। और यदि आपका कोडबेस list-आधारित expressions पसंद करता है, तो 1.32.0 में cum_fold सूची के साथ अब भी काम करता है — उसके बाद unnest कर के विस्तारित कॉलम प्राप्त करें।
निष्कर्ष
1.31.0 में cum_sum_horizontal और unnest के साथ literal कॉलम का व्यवहार एक वास्तविक बग था, जो उलझाने वाले schema और execution errors का कारण बना। 1.32.0 में इसे ठीक कर दिया गया है, और यह cum_sum_horizontal के लिए अनपैक्ड arguments की ओर उपयोग को प्रेरित करता है। यदि नया InvalidOperationError दिखे, तो pl.cum_sum_horizontal(*cols) अपनाएँ या सूची के साथ pl.cum_fold और उसके बाद unnest का उपयोग करें। इन बातों का ध्यान रखकर horizontal cumulative aggregations पूर्वानुमेय बनते हैं और आपकी lazy योजनाएँ अधिक मजबूत रहती हैं।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Nicolò Cavalleri) और jqurious के उत्तर पर आधारित है।