2025, Oct 16 12:32

yfinance के बाद pandas में MultiIndex कॉलम तुलना ValueError को ठीक करें

yfinance के MultiIndex कॉलम वाले pandas DataFrame में तुलना असंगति (ValueError) कैसे ठीक करें: 200 SMA, BME resample, सही इंडेक्सिंग या कॉलम फ्लैटनिंग के तरीके।

yfinance के बाद pandas में असंगत तुलना को ठीक करना: MultiIndex कॉलम फिर मुश्किलें खड़ी करते हैं

yfinance से OHLCV डेटा लेना और उसके बाद अपने तकनीकी कॉलम जोड़ना तब तक सरल है, जब तक आप ऐसे कॉलमों की तुलना नहीं करते जिनके लेबल मेल नहीं खाते। अगर आप DataFrame को “in place” अपडेट करते समय ValueError संदेश देख रहे हैं, तो अक्सर वजह कॉलम इंडेक्स में छिपी होती है। यहाँ व्यावहारिक तरीके से समझते हैं कि गड़बड़ कहाँ होती है और उसे साफ़-सुथरे ढंग से कैसे ठीक करें।

समस्या दोहराकर समझें

लक्ष्य सीधा है: 200-दिन का सरल मूविंग एवरेज (SMA) निकालना, हर महीने के आखिरी कारोबारी दिन को चुनना, और यह देखते हुए Indicator सेट करना कि Close SMA के ऊपर है या नीचे।

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta
sym = 'AAPL'  # एप्पल इंक.
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)
try:
    quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
except Exception as err:
    print(f"Error downloading data: {err}")
    raise SystemExit
if quotes.empty:
    print("No data downloaded. Please check the ticker and dates.")
    raise SystemExit
quotes['200_SMA'] = quotes['Close'].rolling(window=200).mean()
month_end = quotes.resample('BME').last().dropna()
month_end['Indicator'] = 'Hold'
# यह पंक्ति ValueError उठाती है
month_end['Indicator'] = [
    'Above' if row['Close'] > row['200_SMA'] else 'Below'
    for _, row in month_end.iterrows()
]

आम तौर पर त्रुटियाँ कुछ यूँ दिखती हैं:

ValueError: Can Only Compare Identically-labeled Series Objects
ValueError: Operands are not aligned. Do left, right = left.align(right, axis=1, copy=False) before operating.

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

yfinance ऐसा DataFrame लौटाता है जिसकी columns एक MultiIndex होती हैं। एक स्तर में Close, High, Low, Open, Volume जैसे प्राइस फील्ड होते हैं। दूसरे स्तर में टिकर होता है। एक ही सिंबल डाउनलोड करने पर आपको ऐसे ट्यूपल दिखते हैं: ("Close", "AAPL")। जब आप 200_SMA जैसा कस्टम कॉलम जोड़ते हैं, तो उसके दूसरे स्तर का लेबल अलग हो जाता है, यानी ("200_SMA", "").

अब देखें कि आपका कोड क्या करता है: यह row['Close'] > row['200_SMA'] जाँचता है। पर्दे के पीछे यह ("Close", "AAPL") लेबल वाली Series की तुलना ("200_SMA", "") लेबल वाली Series से करने की कोशिश है। क्योंकि लेबल्स एक जैसे नहीं हैं, pandas इस ऑपरेशन को मना कर देता है और एलाइनमेंट की त्रुटि उठाता है। अगर बाद में आप और टिकर लोड करेंगे, तो आपको ("Close", "AMZN") और ("Close", "GOOG") भी दिखेंगे—यानी दूसरा स्तर मायने रखता है।

इसे ठीक करने के दो तरीके

या तो आप सटीक MultiIndex keys से इंडेक्स करें, या फिर कॉलम्स को फ्लैट कर एक स्तर पर ले आएँ ताकि तुलना सही से संरेखित हो सके।

विकल्प 1: पूरे MultiIndex को स्पष्ट रूप से संबोधित करें

यह MultiIndex को उसी रूप में रखता है और तुलना करते समय दोनों स्तरों से चयन करता है।

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta
sym = 'AAPL'
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)
quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
if quotes.empty:
    raise SystemExit("No data downloaded. Please check the ticker and dates.")
quotes['200_SMA'] = quotes['Close'].rolling(window=200).mean()
month_end = quotes.resample('BME').last().dropna()
month_end['Indicator'] = [
    'Above' if row[('Close', 'AAPL')] > row[('200_SMA', '')] else 'Below'
    for _, row in month_end.iterrows()
]

यहाँ ('Close', 'AAPL') और ('200_SMA', '') वही असली कॉलम लेबल हैं, इसलिए pandas Series को सही से संरेखित कर लेता है और तुलना काम करती है।

विकल्प 2: टिकर स्तर हटाएँ (एक-स्तरीय कॉलम बनाएँ)

"Ticker" स्तर हटा दें ताकि Close और 200_SMA एक ही-स्तर वाले कॉलम इंडेक्स में रहें।

import pandas as pd
import yfinance as yf
from datetime import date
from dateutil.relativedelta import relativedelta
sym = 'AAPL'
today_dt = date.today()
until_dt = today_dt
span_years = 3
since_dt = today_dt - relativedelta(days=span_years * 365.25 + 200)
quotes = yf.download(sym, start=since_dt, end=until_dt, auto_adjust=True)
if quotes.empty:
    raise SystemExit("No data downloaded. Please check the ticker and dates.")
# दूसरे स्तर को हटाकर कॉलम फ्लैट करें
flat = quotes.droplevel('Ticker', axis=1).copy()
flat['200_SMA'] = flat['Close'].rolling(window=200).mean()
month_end = flat.resample('BME').last().dropna()
month_end['Indicator'] = [
    'Above' if row['Close'] > row['200_SMA'] else 'Below'
    for _, row in month_end.iterrows()
]

फ्लैट करने का एक और तरीका है कि हर कॉलम ट्यूपल के पहले तत्व को मैप कर लें: flat.columns = [c[0] for c in flat.columns]. मकसद यह है कि तुलना करते समय दोनों Series के लेबल एक जैसे हों।

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

Series की तुलना करते समय या DataFrames पर ब्रॉडकास्टिंग करते समय pandas लेबल के आधार पर एलाइन करता है, पोजीशन के आधार पर नहीं। MultiIndex कॉलम होने पर वे ऑब्जेक्ट आसानी से एक जैसे लग सकते हैं, लेकिन इंडेक्स के किसी स्तर में भिन्नता के कारण तुलना असंगत हो जाती है। जो त्रुटि संदेश दिखते हैं, वे इस ओर इशारा करते हैं कि एलाइनमेंट विफल हुआ है—मूल्य खुद समस्या नहीं हैं। कॉलम लेबल को स्पष्ट तौर पर संबोधित करना या सही चरण पर कॉलम फ्लैट करना सूक्ष्म बग्स से बचाता है, खासकर तब जब आपका कोड कई टिकर्स सँभालने लगे।

मुख्य बातें

अगर तुलना या असाइनमेंट के साथ “Can only compare identically-labeled Series objects” या “Operands are not aligned” जैसी त्रुटियाँ मिलें, तो कॉलम इंडेक्स जाँचें कि कहीं आप MultiIndex से तो नहीं जूझ रहे। yfinance के आउटपुट में ("Close", "AAPL") जैसे ट्यूपल सामान्य हैं। या तो चयन करते समय दोनों स्तरों को संबोधित करें, या व्युत्पन्न कॉलम गिनने से पहले उपयोग में न आने वाला स्तर हटा दें। एक बार लेबल मेल खा जाएँ, तो in-place अपडेट अपेक्षित रूप से काम करेगा।

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