2025, Oct 04 11:32

pandas MultiIndex में 08:30 वाली पंक्ति पर day_high सेट करने का सही तरीका

जानें कि pandas MultiIndex में day_high सिर्फ 08:30 वाली पंक्ति में कैसे भरें: .loc मास्क, groupby.max और transform से बिना लूप के तेज़, भरोसेमंद तरीका.

प्रतिदिन के एग्रीगेट को एक ही पंक्ति में भरना pandas का एक क्लासिक भ्रम है: आप मान सही निकालते हैं, लेकिन असाइनमेंट चुपचाप उस कुंजी की सभी पंक्तियों में फैल जाता है। नीचे (Symbol, Date) प्रति ठीक एक पंक्ति — यानी 08:30 वाला रिकॉर्ड — को लक्ष्य करने का संक्षिप्त तरीका है, और इसे एक कॉन्ट्रैक्ट या पूरे डेटासेट पर लगातार लागू करने का तरीका।

पुनरुत्पादन: क्यों मान दिन की हर पंक्ति में भर जाता है

Symbol और Date पर आधारित MultiIndex वाले इंट्राडे ऑप्शन बार्स पर विचार करें। लक्ष्य है कि दिन का अधिकतम high मान day_high नामक कॉलम में सहेजा जाए, लेकिन केवल उस पंक्ति पर जहाँ hour 08:30:00 हो।

import pandas as pd
import csv

rows = [['SPXW 250715C06310000', '7/14/2025', 2.74, 2.87, 2.60, 2.65, 14, '8:30:00'],
        ['SPXW 250715C06310000', '7/14/2025', 2.80, 2.80, 2.50, 2.53, 61, '8:31:00'],
        ['SPXW 250715C06310000', '7/14/2025', 2.45, 2.45, 2.45, 2.45, 2, '8:32:00'],
        ['SPXW 250715C06310000', '7/14/2025', 2.58, 2.80, 2.58, 2.60, 32, '8:33:00'],
        ['SPXW 250715C06310000', '7/14/2025', 2.50, 2.50, 2.25, 2.30, 5, '8:34:00'],
        ['SPXW 250709C06345000', '7/9/2025', 0.05, 0.05, 0.03, 0.03, 246, '8:30:00'],
        ['SPXW 250709C06345000', '7/9/2025', 0.05, 0.10, 0.03, 0.07, 452, '8:31:00'],
        ['SPXW 250709C06345000', '7/9/2025', 0.07, 0.10, 0.05, 0.07, 137, '8:32:00'],
        ['SPXW 250709C06345000', '7/9/2025', 0.07, 0.07, 0.07, 0.07, 5, '8:33:00'],
        ['SPXW 250709C06345000', '7/9/2025', 0.07, 0.07, 0.05, 0.05, 225, '8:34:00'],
        ['SPXW 250715C06310000', '7/11/2025', 7.30, 7.30, 7.30, 7.30, 2, '8:30:00'],
        ['SPXW 250715C06310000', '7/11/2025', 7.20, 7.20, 7.20, 7.20, 2, '8:31:00'],
        ['SPXW 250715C06310000', '7/11/2025', 6.92, 6.92, 6.92, 6.92, 20, '8:32:00'],
        ['SPXW 250715C06310000', '7/11/2025', 6.58, 6.58, 6.58, 6.58, 1, '8:34:00'],
        ['SPXW 250715C06310000', '7/11/2025', 6.41, 6.41, 6.41, 6.41, 2, '8:35:00']]

frame = pd.DataFrame(rows, columns=['Symbol', 'Date', 'open', 'high', 'low', 'close', 'volume', 'hour'])

frame['Date'] = pd.to_datetime(frame['Date'])
frame['hour'] = pd.to_datetime(frame['hour'], format='%H:%M:%S')
frame = frame.set_index(['Symbol', 'Date'])

# Attempt: fills every row of that (Symbol, Date)
frame.loc[('SPXW 250715C06310000', '2025-07-14'), 'day_high'] = (
    frame.loc[('SPXW 250715C06310000', '2025-07-14'), 'high'].max()
)

असल में होता क्या है

जब आप MultiIndex पर .loc को दो-स्तरीय कुंजी देते हैं, तो आप उस (Symbol, Date) जोड़ी के पूरे सब-फ़्रेम को संबोधित करते हैं। उस स्लाइस को कोई स्केलर असाइन करने पर वही मान सभी मेल खाती पंक्तियों में लिख दिया जाता है। क्योंकि कोड में समय पर पंक्ति-स्तर का फ़िल्टर नहीं था, चयनित कॉन्ट्रैक्ट के लिए उस दिन के हर 1-मिनट बार को अपडेट कर दिया गया।

एक कॉन्ट्रैक्ट/दिन के लिए सिर्फ एक पंक्ति को लक्ष्य करें

समाधान है कि ऐसा बूलियन मास्क इस्तेमाल करें जो केवल इच्छित पंक्ति के लिए true हो। दो शर्तें चाहिए: समय 08:30 हो, और MultiIndex विशेष (Symbol, Date) जोड़ी के बराबर हो।

# Choose the one exact row using a boolean mask
flag = (
    frame['hour'].dt.strftime('%H:%M').eq('08:30') &
    (frame.index == ('SPXW 250715C06310000', pd.Timestamp('2025-07-14')))
)

frame.loc[flag, 'day_high'] = (
    frame.loc[('SPXW 250715C06310000', '2025-07-14'), 'high'].max()
)

यदि Date स्तर पहले से datetime प्रकार है, तो साधारण तारीख मान से तुलना भी काम करती है; ऐसी स्थिति में समानता जाँच के लिए pd.Timestamp हटाना पर्याप्त हो सकता है।

हर कॉन्ट्रैक्ट/दिन के लिए बिना लूप के करें

आपको for लूप की ज़रूरत नहीं। (Symbol, Date) प्रति अधिकतम एक बार निकालें और pandas को उसे सही स्थानों पर संरेखित करने दें। इसके दो प्रचलित तरीके हैं।

पहला तरीका दैनिक अधिकतम निकालता है और उसे केवल वहीं असाइन करता है जहाँ hour 08:30 है। असाइनमेंट के दौरान GroupBy का परिणाम (Symbol, Date) इंडेक्स पर संरेखित हो जाता है।

flag = frame['hour'].dt.strftime('%H:%M').eq('08:30')
frame.loc[flag, 'day_high'] = frame.groupby(['Symbol', 'Date'])['high'].max()

दूसरा तरीका transform का प्रयोग करता है ताकि एग्रीगेटेड मान मूल इंडेक्स पर ब्रॉडकास्ट हो जाए, और फिर मास्क लगाकर केवल 08:30 वाली पंक्ति रखी जाए। यह रूप अक्सर सबसे सहज होता है क्योंकि यह पहले से ही पंक्ति-दर-पंक्ति संरेखित रहता है।

flag = frame['hour'].dt.strftime('%H:%M').eq('08:30')
frame['day_high'] = frame.groupby(['Symbol', 'Date'])['high'].transform('max').where(flag)

यह क्यों मायने रखता है

बड़े इंट्राडे डेटासेट पर, स्पष्ट बूलियन मास्क और वेक्टोराइज़्ड groupby ऑपरेशन्स कोड को पूर्वानुमेय और कुशल रखते हैं। इससे उद्देश्य भी साफ़ रहता है: दैनिक एग्रीगेट निकालें, और उसे प्रतिदिन ठीक एक बार 08:30 की “ओपनिंग” पंक्ति में लिखें।

मुख्य बातें

MultiIndex स्लाइस में असाइन करते समय याद रखें कि दो-स्तरीय कुंजी पूरे समूह का चयन करती है। जिस एक रिकॉर्ड की आपको परवाह है, उसे चिन्हित करने के लिए पंक्ति-स्तर का मास्क जोड़ें। सभी symbols और दिनों पर थोक अपडेट के लिए, मैनुअल लूप से बचने को index alignment के साथ GroupBy.max या where के साथ GroupBy.transform को प्राथमिकता दें। और यदि आपका Date स्तर पहले से datetime है, तो सीधे उसी मान से तुलना करना समानता जाँच के लिए पर्याप्त है।

यह लेख StackOverflow पर प्रश्न (लेखक — Dan) और jezrael के उत्तर पर आधारित है।