2025, Oct 23 07:31

pandas DataFrame में स्ट्रीक आधारित TrendUp, TrendDown, NoTrend लेबलिंग का वेक्टराइज्ड तरीका

pandas DataFrame में बढ़त/गिरावट स्ट्रीक को ब्लॉकों में पहचानें और groupby.transform से TrendUp, TrendDown या NoTrend लेबल करें—तेज़, वेक्टराइज़्ड समाधान.

pandas DataFrame में लगातार बढ़त और गिरावट के हिस्सों को चिह्नित करना सुनने में आसान लगता है, लेकिन जैसे ही स्केलेबल, साफ-सुथरी, वेक्टराइज़्ड लॉजिक चाहिए, बात बदल जाती है। काम यह है: जैसे ही बढ़ते या घटते मानों की कोई श्रृंखला तय सीमा (पाँच या अधिक) तक पहुँचती है, उस रन की शुरुआत से लेकर उस बिंदु तक पूरे दायरे को TrendUp या TrendDown का लेबल दें; और अगर कोई शर्त पूरी नहीं होती, तो NoTrend रखें। नीचे बिना पंक्तियों पर मैन्युअल लूप के, इसे करने का संक्षिप्त तरीका दिया है।

डेटासेट और प्रारंभिक सेटअप

हम एक एकल संख्यात्मक कॉलम से शुरू करते हैं और दो हेल्पर कॉलम निकालते हैं जो लगातार बढ़त और गिरावट की गिनती करते हैं। यही अंतिम Trend लेबल निकालने का आधार बनाता है।

import pandas as pd
import numpy as np

payload = {'col1': [234,321,284,286,287,300,301,303,305,299,288,300,299,287,286,280,279,270,269,301]}

table = pd.DataFrame(data=payload)

ups = []
downs = []
arr = np.array(table['col1'])

for j in range(len(table)):
    ups.append(0)
    downs.append(0)
    if arr[j] > arr[j-1]:
        ups[j] = ups[j-1] + 1
    else:
        ups[j] = 0

    if arr[j] < arr[j-1]:
        downs[j] = downs[j-1] + 1
    else:
        downs[j] = 0

table['cnsIncr'] = ups
table['cnsDecr'] = downs

असल समस्या क्या है

मकसद सिर्फ रन गिनना नहीं है; बल्कि उस पूरे हिस्से को टैग करना है जो उस पहले बिंदु तक जाता है और उसे शामिल करता है जहाँ रन की लंबाई सीमा पार करती है। दूसरे शब्दों में, जैसे ही बढ़त का रन पाँच या उससे अधिक हो जाए, रीसेट (जहाँ काउंटर 0 था) से उस बिंदु तक पूरा ब्लॉक TrendUp होना चाहिए। यही तर्क घटतियों और TrendDown पर भी लागू होता है। अगर कहीं भी कोई सीमा नहीं छूती, तो सब कुछ NoTrend रहेगा।

एक अहम बात यह है कि ये रन काउंटर शून्य पर रीसेट होकर अलग-अलग ब्लॉक बनाते हैं। हर ब्लॉक के भीतर अंतिम मान बताता है कि वह ब्लॉक सीमा तक पहुँचा या नहीं। अगर पहुँचा, तो वही लेबल पूरे ब्लॉक को दिया जाता है।

groupby.transform के साथ समाधान

इसी दृष्टि से इम्प्लिमेंटेशन सीधी हो जाती है: शून्यों के बीच ब्लॉक पहचानें, प्रत्येक ब्लॉक की अंतिम रन-लंबाई निकालें, उसे सीमा से मिलाएँ और वही निर्णय ब्लॉक की सभी पंक्तियों तक पहुँचा दें। लेबलिंग के लिए किसी स्पष्ट Python लूप की जरूरत नहीं।

MIN_RUN = 5

# समूह आईडी: हर स्ट्रेच वहीं शुरू होता है जहाँ काउंटर 0 होता है
grp_up = table['cnsIncr'].eq(0).cumsum()
grp_dn = table['cnsDecr'].eq(0).cumsum()

# क्या ब्लॉक का अंतिम मान सीमा तक पहुँचता है?
mask_up = table['cnsIncr'].groupby(grp_up).transform('last').ge(MIN_RUN)
mask_dn = table['cnsDecr'].groupby(grp_dn).transform('last').ge(MIN_RUN)

# अंतिम लेबलिंग: पहले Up, फिर Down, वरना NoTrend
table['Trend'] = np.select([mask_up, mask_dn], ['TrendUp', 'TrendDown'], 'NoTrend')

यह बताई गई नियमों से मेल खाता है: किसी ब्लॉक में cnsIncr का मान 5 या उससे अधिक हो तो TrendUp, cnsDecr के लिए वही शर्त होने पर TrendDown, और अन्य सभी मामलों में NoTrend।

यह क्यों काम करता है

हर रन काउंटर (cnsIncr और cnsDecr) एक स्ट्रीक के भीतर एकरस रूप से बढ़ता है और अगली स्ट्रीक की शुरुआत में 0 पर रीसेट हो जाता है। eq(0).cumsum() लगाने से ये रीसेट स्थिर समूह पहचानकर्ताओं में बदल जाते हैं। हर समूह के भीतर transform('last') उस ब्लॉक की अंतिम रन-लंबाई देता है। इसे सीमा से मिलाने पर पता चलता है कि पूरे ब्लॉक को लेबल करना है या नहीं, और np.select उसी फैसले को एक ही पास में पंक्ति-दर-पंक्ति फैला देता है।

यह जानना क्यों उपयोगी है

यह पैटर्न साफ-सुथरे ढंग से स्केल करता है और आपकी लॉजिक को डिक्लेरेटिव रखता है। यह लेबलिंग के लिए रो-वाइज लूप से बचता है, groupby और transform जैसी भरोसेमंद pandas प्रिमिटिव्स पर निर्भर रहता है, और ठीक उसी ढंग को दर्शाता है जिसमें कई टाइम-सीरीज़ और इवेंट सेगमेंटेशन कार्य रचे जा सकते हैं। वही सोच—ब्लॉक पहचानें, प्रति ब्लॉक सार निकालें, और उसे वापस प्रसारित करें—रन-लंबाई वर्गीकरण, स्ट्रीक टैगिंग और थ्रेशहोल्ड-आधारित रेजीम डिटेक्शन में व्यापक रूप से लागू होती है।

मुख्य बातें

जब लगातार व्यवहार के आधार पर दायरों को टैग करना हो, तो रीसेट बिंदुओं से ब्लॉक तय करें, प्रति ब्लॉक एक बार निर्णय निकालें और उसे ब्लॉक की सभी पंक्तियों तक लौटा दें। यहाँ यही तरीका एक संक्षिप्त, पठनीय और वेक्टराइज़्ड Trend कॉलम देता है—TrendUp, TrendDown या NoTrend—मूल नियमों के अनुरूप: सीमा के बराबर या उससे अधिक और उस रीसेट से शुरू जो स्ट्रीक को आरंभ करता है।

यह लेख StackOverflow पर प्रश्न और Giampaolo Levorato तथा mozway के उत्तर पर आधारित है।