2025, Sep 24 17:31

NumPy में बिना लूप या पैडिंग के प्रति‑बिन न्यूनतम

असमान लंबाई वाले बिन के लिए NumPy में तेज़ प्रति‑बिन न्यूनतम निकालें। np.minimum.reduceat के साथ वेक्टराइज़्ड segmented reduction करें—बिना Python लूप और पैडिंग के, कुशलता से।

NumPy में बिना लूप या पैडिंग के प्रति‑बिन न्यूनतम

असमान लंबाई वाले m बिन में विभाजित 1‑डी ऐरे के साथ काम करना आम है। हर तत्व 0 से m−1 तक बढ़ते बिन इंडेक्स से जुड़ा होता है, और लक्ष्य हर बिन का न्यूनतम मान निकालना है। सीधे तरीकों में या तो Python में लूप लगाना पड़ता है, या फिर पैडिंग कर के मध्यवर्ती संरचनाएँ बनानी पड़ती हैं—दोनों ही विकल्प ऐसा ओवरहेड जोड़ते हैं जो स्केलिंग के साथ खराब होता है।

समस्या की रूपरेखा और एक सामान्य प्रयास

मान लीजिए आपको हर बिन का संलग्न स्लाइस पहले से मालूम है। तब सीधा तरीका है प्रत्येक स्लाइस पर इटरेट कर के रिड्यूस करना:

import numpy as np
# डेटा: 1-डी वेक्टर
data_vec = np.array([...])  # लंबाई n
# group_count = m, और group_slices (lo, hi) जोड़ों की सूची है
# ताकि हर बिन का डेटा data_vec[lo:hi] हो
group_count = m
group_slices = [(lo0, hi0), (lo1, hi1), ...]  # असमान लंबाइयाँ
min_per_group = np.zeros(group_count)
for pos, (lo, hi) in enumerate(group_slices):
    min_per_group[pos] = np.min(data_vec[lo:hi])

यह काम करता है, पर लूप Python स्पेस में चलता है और हर बिन पर लागत चुकानी पड़ती है। एक और तरीका है पैडिंग करके आयत बनाना, फिर reshape कर के किसी अक्ष पर reduce करना, लेकिन इसमें नकली डेटा गढ़ना और अनावश्यक मेमरी शफलिंग शामिल हो जाती है।

import numpy as np
# data_vec जैसा ऊपर
# valid_mask पैड किए गए स्लॉट्स में वास्तविक तत्वों को चिह्नित करता है
# scratch_1d पहले से एक बड़े sentinel मान से भरा है; आकार पैडेड आयत के कुल आकार जितना ही
scratch_1d[valid_mask] = data_vec
scratch_2d = scratch_1d.reshape((group_count, slot_size))
min_per_group = np.min(scratch_2d, axis=1)

इन दोनों तरीकों में या तो लूप चलता है या कृत्रिम मान इधर‑उधर किए जाते हैं—नतीजा यह कि अतिरिक्त ट्रैवर्सल होते हैं जिनका परिणाम से सीधा लेना‑देना नहीं होता।

ऐसा क्यों होता है

बिन मूल 1‑डी ऐरे के संलग्न खंड होते हैं, लेकिन उनकी लंबाई समान नहीं होती। बदलती लंबाई वाले खंडों के लिए लूप स्वाभाविक है, जबकि वेक्टराइज़्ड reductions नियमित आकृतियों को तरजीह देते हैं। पैडिंग अनियमित डेटा को जबरन एक समान ग्रिड में ढालने की कोशिश करती है, जिसकी कीमत अतिरिक्त मेमरी और compute होती है। लक्ष्य है ऐसा वेक्टराइज़्ड reduction जो मौजूदा खंड सीमाओं का सम्मान करे, बिना कोई पैडेड संरचना बनाए।

np.minimum.reduceat के साथ एक वेक्टराइज़्ड समाधान

NumPy np.minimum.reduceat का उपयोग कर सीधे segmented reduction कर सकता है। बस एक चीज़ चाहिए—हर बिन के लिए शुरुआती ऑफसेट का ऐरे, लंबाई m, जिसमें पहला प्रारंभ 0 हो। इन ऑफसेट्स के साथ, reduction मूल ऐरे को एक ही पास में चलते हुए हर खंड का न्यूनतम निकाल देता है—न लूप, न पैडिंग।

import numpy as np
# data_vec: मूल 1-डी ऐरे (लंबाई n)
# start_idx: लंबाई m का 1-डी ऐरे, जिसमें हर बिन का begin इंडेक्स है; start_idx[0] अनिवार्य रूप से 0 होना चाहिए
min_per_group = np.minimum.reduceat(data_vec, start_idx)

यह इस तथ्य का लाभ उठाता है कि संबद्ध बिन इंडेक्स 0 से m−1 तक बढ़ते हैं, इसलिए हर बिन के तत्व संलग्न स्लाइस बनाते हैं। reduction start_idx में दिए हर स्थान से शुरू होकर खंड‑वार न्यूनतम को वेक्टराइज़्ड तरीके से गिनती है।

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

यह तरीका Python लूप्स को हटाता है और नकली तत्व गढ़ने से बचाता है। मेमरी ट्रैफिक सिर्फ ज़रूरत तक सीमित रहता है, जटिलता घटती है, और आप NumPy के अनुकूलित execution पथ में बने रहते हैं। बड़े n और बहुत से असमान बिन के लिए यह फर्क अक्सर तेज़ एग्रीगेशन और सुस्त वाली के बीच की रेखा बन जाता है।

मुख्य बातें

यदि आपका 1‑डी डेटा 0 से m−1 तक बढ़ते इंडेक्स वाले संलग्न बिन में बँटा है, तो बिन‑स्टार्ट्स का ऐरे बनाए रखें और प्रति‑बिन न्यूनतम निकालने के लिए np.minimum.reduceat का उपयोग करें। यह उसी डेटा लेआउट के अनुकूल है जो आपके पास पहले से है, ऑपरेशन को वेक्टराइज़्ड रखता है, और पैडिंग या अतिरिक्त ट्रैवर्सल से बचाता है।

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