2025, Oct 30 23:02

Polars 1.31.0 में rolling_sum का NaN/null बग और आगामी फिक्स

Polars 1.31.0 में rolling_sum के NaN/null बग की व्याख्या, Python 3.12 व NumPy 2.3.1 पर मिनिमल उदाहरण, प्रभाव, rolling_map वैलिडेशन और अगले रिलीज़ में फिक्स.

रोलिंग विंडो एनालिटिक्स डेटा पाइपलाइनों का आधार होते हैं, लेकिन जब मिसिंग मान शामिल हो जाते हैं, तो उनका व्यवहार चौंका सकता है। यदि आप Python 3.12.11 और NumPy 2.3.1 के साथ Polars 1.31.0 का उपयोग कर रहे हैं, तो ऐसे कॉलम में जहां NaN (np.nan) और null (None) साथ हों, rolling_sum से अप्रत्याशित परिणाम मिल सकते हैं। यह गाइड न्यूनतम पुनरुत्पादन दिखाती है, घटित व्यवहार की व्याख्या करती है और उस सुधार की ओर इशारा करती है जो अगले रिलीज़ में आएगा।

केवल NaN के साथ अपेक्षित व्यवहार का पुनरुत्पादन

एक ऐसे कॉलम से शुरू करें जिसमें एक मात्र NaN हो और बाकी सभी मान वैध फ्लोट हों। विंडो आकार 2 पर, पहली स्थिति null (पर्याप्त डेटा नहीं) होती है, जिन दो विंडो में NaN शामिल है वे NaN देती हैं, और अन्य सभी विंडो का योग अपेक्षित रूप से 2.0 होता है।

import polars as pl
import numpy as np

src_one = {"x": [1., 1., 1., np.nan, 1., 1., 1., 1., 1.]}
df_one = pl.DataFrame(src_one)

with pl.Config(tbl_rows=20):
    print(
        df_one.with_columns(
            pl.col("x").rolling_sum(2).alias("roll_val")
        )
    )
shape: (9, 2)
┌─────┬──────────┐
│ x   ┆ roll_val │
│ --- ┆ ---      │
│ f64 ┆ f64      │
╞═════╪══════════╡
│ 1.0 ┆ null     │
│ 1.0 ┆ 2.0      │
│ 1.0 ┆ 2.0      │
│ NaN ┆ NaN      │
│ 1.0 ┆ NaN      │
│ 1.0 ┆ 2.0      │
│ 1.0 ┆ 2.0      │
│ 1.0 ┆ 2.0      │
│ 1.0 ┆ 2.0      │
└─────┴──────────┘

जब NaN और null साथ हों

अब NaN के साथ एक null जोड़ें। सहज रूप से, आप NaN के आसपास वही पैटर्न और जिन विंडो में null आता है, वहां null के कारण खालीपन की उम्मीद करेंगे। लेकिन इसके बजाय, पहली बार NaN आने के बाद rolling_sum अपेक्षा से अधिक दूर तक NaN का प्रसार करने लगता है।

src_two = {"x": [1., 1., 1., np.nan, 1., 1., 1., 1., 1., None, 1., 1., 1.]}
df_two = pl.DataFrame(src_two)

with pl.Config(tbl_rows=20):
    print(
        df_two.with_columns(
            pl.col("x").rolling_sum(2).alias("roll_val")
        )
    )
shape: (13, 2)
┌──────┬──────────┐
│ x    ┆ roll_val │
│ ---  ┆ ---      │
│ f64  ┆ f64      │
╞══════╪══════════╡
│ 1.0  ┆ null     │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ NaN  ┆ NaN      │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ NaN      │
│ null ┆ null     │
│ 1.0  ┆ null     │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ NaN      │
└──────┴──────────┘

NaN के बाद केवल एक सामान्य 2.0 दिखाई देता है; इसके बाद की विंडो, सीमित मान होने पर भी, अप्रत्याशित रूप से NaN लौटाती हैं। यह केवल NaN वाले इनपुट के सामान्य अर्थ से अलग है।

क्या हो रहा है

ऊपर वर्णित व्यवहार rolling_sum में एक बग है, जो तब सामने आता है जब एक ही कॉलम में NaN और null साथ मौजूद हों। इसे मुख्य शाखा में ठीक कर दिया गया है और यह सुधार अगले Polars रिलीज़ में शामिल होगा। परिवर्तन यहां ट्रैक हो रहा है: https://github.com/pola-rs/polars/pull/23482.

rolling_map के साथ शुद्धता का बेसलाइन

यदि आप rolling_map(sum, 2) के साथ वही रोलिंग सम निकालते हैं, तो परिणाम NaN और null वाली विंडो के लिए भी सहज अपेक्षा से मेल खाते हैं। इसलिए यह वैधता जांच के लिए एक उपयोगी शुद्धता-मानक बनता है।

with pl.Config(tbl_rows=20):
    print(
        df_two.with_columns(
            pl.col("x").rolling_map(sum, 2).alias("roll_val")
        )
    )
shape: (13, 2)
┌──────┬──────────┐
│ x    ┆ roll_val │
│ ---  ┆ ---      │
│ f64  ┆ f64      │
╞══════╪══════════╡
│ 1.0  ┆ null     │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ NaN  ┆ NaN      │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ null ┆ null     │
│ 1.0  ┆ null     │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
└──────┴──────────┘

हालांकि, rolling_map एक Python UDF चलाता है और Series ऑब्जेक्ट्स को मैटेरियलाइज़ करता है, जिससे पर्याप्त ओवरहेड जुड़ता है। दस्तावेज़ीकरण चेतावनी देता है कि अत्यावश्यक न हो तो इसे प्रोडक्शन वर्कलोड में न अपनाएं।

कस्टम फ़ंक्शन की गणना अत्यंत धीमी होती है। संभव हो तो Expr.rolling_sum() जैसी विशेषीकृत रोलिंग फ़ंक्शंस का उपयोग करें।

सुधार और शुद्ध आउटपुट

मुख्य शाखा में सुधार के विलय के बाद (ऊपर दिए गए PR देखें), मिश्रित NaN और null के लिए rolling_sum फिर से अपेक्षित परिणाम देता है। वही अभिव्यक्ति चलाने पर सही योग और NaN/null का स्थानीयकृत प्रभाव मिलता है।

with pl.Config(tbl_rows=20):
    print(
        df_two.with_columns(
            pl.col("x").rolling_sum(2).alias("roll_val")
        )
    )
┌──────┬──────────┐
│ x    ┆ roll_val │
│ ---  ┆ ---      │
│ f64  ┆ f64      │
╞══════╪══════════╡
│ 1.0  ┆ null     │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ NaN  ┆ NaN      │
│ 1.0  ┆ NaN      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
│ null ┆ null     │
│ 1.0  ┆ null     │
│ 1.0  ┆ 2.0      │
│ 1.0  ┆ 2.0      │
└──────┴──────────┘

यह क्यों महत्वपूर्ण है

रोलिंग एग्रीगेशन असामान्यता पहचान, KPI और टाइम-सीरीज़ फ़ीचर्स की नींव हैं। जब NaN और null साथ दिखें और चुपचाप विचलन हो, तो मेट्रिक्स बिगड़ सकते हैं या मॉडल इनपुट अमान्य हो सकते हैं। इस किनारी मामले और आने वाले सुधार के बारे में जानना आपको सही साधन चुनने में मदद करता है—या तो सहीपन की जांच के लिए rolling_map से वैलिडेट करें, या सुधार उपलब्ध होते ही अनुकूलित rolling_sum पर भरोसा करें।

व्यावहारिक निष्कर्ष

यदि आप Polars 1.31.0 पर हैं और रोलिंग विंडो में NaN को null के साथ मिलाते हैं, तो ऊपर दिखाए गए अप्रत्याशित NaN-प्रसार से सावधान रहें। सहीपन की वैलिडेशन के लिए rolling_map(sum, 2) इच्छित व्यवहार को दोहराता है, लेकिन दस्तावेज़ में बताया गया है कि Python UDF निष्पादन और Series मैटेरियलाइज़ेशन के कारण भारी ओवरहेड लगता है। मूल समस्या का समाधान कर दिया गया है और यह अगले रिलीज़ का हिस्सा होगा, जिसे यहां ट्रैक किया जा रहा है: https://github.com/pola-rs/polars/pull/23482. इस बात पर निर्भर करते हुए कि आप सुधरा हुआ रिलीज़ अपना सकते हैं या नहीं, प्रदर्शन और तत्काल शुद्धता के बीच अपनी पसंद को संरेखित करें।

यह लेख StackOverflow पर प्रश्न (लेखक: Arran) और jqurious द्वारा दिए गए उत्तर पर आधारित है।