2025, Nov 01 11:01

Matplotlib में secondary axis के टिक को primary से सटीक मिलाएं

Matplotlib में secondary axis का व्यावहारिक गाइड: सही forward/inverse, primary के साथ टिक संरेखण, twinx/twiny कब न अपनाएं, और जटिल ट्रांसफॉर्म के टिप्स.

Matplotlib में secondary axes दिखने में सरल लगते हैं: आप functions की एक जोड़ी पास करते हैं, और बदली हुई मानों वाला एक नया axis दिख जाता है। लेकिन जैसे ही आप चाहते हैं कि secondary axis के टिक प्राथमिक (primary) axis के टिक्स के साथ एक-एक कर के बिल्कुल लाइन अप हों, यह समझना मुश्किल हो सकता है कि असल में किस चीज़ का रूपांतरण हो रहा है और उसे कैसे नियंत्रित किया जाए। यह गाइड Axes और axis के फर्क को साफ करता है, बताता है कि इस संदर्भ में twinx/twiny क्यों उपयुक्त नहीं हैं, और दिखाता है कि स्पष्ट नियंत्रण के जरिए secondary axis के टिक्स को primary के साथ कैसे मिलाया जाए।

सेटअप को दोहराना

नीचे दिए उदाहरण में एक filled contour plot है और secondary axes जोड़े गए हैं, जिनका अभिप्राय Y = 1/y और X = 3x दिखाना है। लक्ष्य है कि secondary टिक्स primary टिक्स के साथ संरेखित रहें, और उनके लेबल primary मानों से किसी फ़ंक्शन के आधार पर निकाले जाएं।

import matplotlib.pyplot as plt
import numpy as np
# दृश्य बनाने के लिए फ़ंक्शन
def plane_fn(u, v):
    return (1 - (u ** 2 + v ** 3)) * np.exp(-(u ** 2 + v ** 2) / 2)
v_hi = 2.5
v_lo = 1
u_hi = v_hi
u_lo = v_lo
u_vals = np.arange(v_lo, v_hi, 0.01)
v_vals = np.arange(u_lo, u_hi, 0.01)
UU, VV = np.meshgrid(u_vals, v_vals)
WW = plane_fn(UU, VV)
fig1, ax1 = plt.subplots()
cf = ax1.contourf(UU, VV, WW)
x_ticks = np.linspace(u_lo, u_hi, 9)
x_tick_lbls = [fr'{i:.1f}' for i in x_ticks]
ax1.set_xticks(ticks=x_ticks, labels=x_tick_lbls)
ax1.set_xlabel('x')
y_ticks = np.linspace(v_lo, v_hi, 9)
y_tick_lbls = [fr'{i:.1f}' for i in y_ticks]
ax1.set_yticks(ticks=y_ticks, labels=y_tick_lbls)
ax1.set_ylabel('y')
cbar = fig1.colorbar(cf, ax=ax1, location='right')
cbar.ax.set_ylabel(r'$z=f(x,y)$')
_ = y_ticks ** 2  # सेटअप से लिया गया placeholder कैलकुलेशन जस का तस रखा गया
fig1.subplots_adjust(left=0.20)
def y_map(y):
    return 1 / y
# secondary y-axis; यहां वही फ़ंक्शन उसके inverse के रूप में भी पास किया गया है
sec_y = ax1.secondary_yaxis('left', functions=(y_map, y_map))
sec_y.spines['left'].set_position(('outward', 40))
sec_y.set_ylabel(r'Y=1/y')
sec_y.yaxis.set_inverted(True)
fig1.subplots_adjust(bottom=0.27)
def x_map(x):
    return 3 * x
# secondary x-axis; समस्या दिखाने के लिए जानबूझकर गलत inverse दिया गया है
sec_x = ax1.secondary_xaxis('bottom', functions=(x_map, x_map))
sec_x.spines['bottom'].set_position(('outward', 30))
sec_x.set_xlabel(r'X=3$\times$ x')
ax1.grid(visible=False)
ax1.set_title(r'$z=(1-x^2+y^3) e^{-(x^2+y^2)/2}$')
fig1.tight_layout()
plt.show()

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

पहली बारीकी शब्दावली की है। Matplotlib का Axes (वह ऑब्जेक्ट जिस पर आप प्लॉट करते हैं, जैसे ax1) एक axis (x/y स्केल, उसके टिक और लेबल) से अलग चीज़ है। SecondaryAxis मौजूदा axis का एक परिवर्तित दृश्य जोड़ता है: नई टिक स्थितियां और लेबल, जो फ़ंक्शनों की जोड़ी से निकाले जाते हैं। यह कोई नया Axes नहीं बनाता और डेटा प्लॉट करने के लिए दूसरी जगह उपलब्ध नहीं कराता। इसके विपरीत, twinx/twiny मूल Axes के ऊपर एक बिल्कुल नया Axes बनाते हैं, जो एक दिशा (twinx में x, twiny में y) साझा करता है और दूसरी दिशा में स्वतंत्र autoscaling बनाए रखता है। यह असंबंधित यूनिट्स के लिए उपयोगी है, न कि एक ही मात्रा की दो मापों के बीच नियत (deterministic) रूपांतरण दिखाने के लिए।

संक्षेप में, twinx और twiny यहां वाली समस्या से अलग हैं। आपको दूसरा Axes नहीं चाहिए। आपको चाहिए कि secondary axis के टिक प्राथमिक axis के टिक्स के रूपांतरण को दर्शाएं और स्क्रीन पर स्पष्ट रूप से उसी के साथ लाइन अप रहें।

दूसरी बारीकी नियंत्रण से जुड़ी है। डिफॉल्ट रूप से SecondaryAxis दिए गए transform फ़ंक्शनों और मौजूदा दृश्य सीमाओं (view limits) के आधार पर अपने टिक खुद तय करता है। यदि आपको एक-एक कर के सटीक संरेखण चाहिए, तो आपको primary टिक स्थितियों को फॉरवर्ड फ़ंक्शन से बदलकर secondary टिक्स को स्पष्ट रूप से सेट करना चाहिए। अंत में, SecondaryAxis बनाते समय Matplotlib अग्र-फ़ंक्शन (forward) और उसके सच्चे inverse की जोड़ी अपेक्षित करता है। गलत inverse देने से कुछ मामलों में परिणाम ठीक-ठाक दिख सकता है, लेकिन इसकी गारंटी नहीं है और व्यवहार बिगड़ सकता है।

समाधान: secondary ticks को खुद नियंत्रित करें

टिक्स को पूरी तरह संरेखित करने का सबसे आसान तरीका है: मौजूदा primary टिक लें, उन्हें अपने forward फ़ंक्शन से मैप करें, और वही secondary टिक्स के रूप में असाइन कर दें। इससे यह सुनिश्चित होता है कि स्क्रीन पर उनकी स्थितियां और अंतर (spacing) ठीक उसी तरह मेल खाएं। साथ ही जहां आवश्यक हो, फ़ंक्शनों की जोड़ी वास्तव में परस्पर inverse होनी चाहिए।

import matplotlib.pyplot as plt
import numpy as np
def plane_fn(u, v):
    return (1 - (u ** 2 + v ** 3)) * np.exp(-(u ** 2 + v ** 2) / 2)
v_hi = 2.5
v_lo = 1
u_hi = v_hi
u_lo = v_lo
u_vals = np.arange(v_lo, v_hi, 0.01)
v_vals = np.arange(u_lo, u_hi, 0.01)
UU, VV = np.meshgrid(u_vals, v_vals)
WW = plane_fn(UU, VV)
fig1, ax1 = plt.subplots()
cf = ax1.contourf(UU, VV, WW)
x_ticks = np.linspace(u_lo, u_hi, 9)
x_tick_lbls = [fr'{i:.1f}' for i in x_ticks]
ax1.set_xticks(ticks=x_ticks, labels=x_tick_lbls)
ax1.set_xlabel('x')
y_ticks = np.linspace(v_lo, v_hi, 9)
y_tick_lbls = [fr'{i:.1f}' for i in y_ticks]
ax1.set_yticks(ticks=y_ticks, labels=y_tick_lbls)
ax1.set_ylabel('y')
cbar = fig1.colorbar(cf, ax=ax1, location='right')
cbar.ax.set_ylabel(r'$z=f(x,y)$')
_ = y_ticks ** 2
fig1.subplots_adjust(left=0.20)
def y_map(y):
    return 1 / y
# सकारात्मक y के लिए y_map स्वयं का inverse है
sec_y = ax1.secondary_yaxis('left', functions=(y_map, y_map))
sec_y.spines['left'].set_position(('outward', 40))
sec_y.set_ylabel(r'Y=1/y')
sec_y.yaxis.set_inverted(True)
# forward transform के जरिए secondary y ticks को primary ticks के साथ संरेखित करें
sec_y.set_yticks(y_map(ax1.get_yticks()))
fig1.subplots_adjust(bottom=0.27)
def x_map(x):
    return 3 * x
# x transform के लिए वास्तविक inverse दें
sec_x = ax1.secondary_xaxis('bottom', functions=(x_map, lambda t: t / 3))
sec_x.spines['bottom'].set_position(('outward', 30))
sec_x.set_xlabel(r'X=3$\times$ x')
# forward transform के जरिए secondary x ticks को primary ticks के साथ संरेखित करें
sec_x.set_xticks(x_map(ax1.get_xticks()))
ax1.grid(visible=False)
ax1.set_title(r'$z=(1-x^2+y^3) e^{-(x^2+y^2)/2}$')
fig1.tight_layout()
plt.show()

यह तरीका परिवर्तित स्पेस में secondary टिक्स की रैखिक दूरी बनाए रखता है और कैनवास पर उनकी स्थिति को primary टिक्स के साथ बिल्कुल मैच कर देता है।

ध्यान देने लायक बातें

फ़ंक्शन-युग्म सचमुच inverse होने चाहिए। y मैपिंग के लिए y → 1/y (सकारात्मक डोमेन पर) अपना ही inverse है। x मैपिंग में x → 3x के साथ inverse t → t/3 होना चाहिए। कभी-कभी गलत inverse के साथ परिणाम सही-सा दिख सकता है, पर यह भरोसेमंद नहीं है। दस्तावेज़ में स्पष्ट रूप से reciprocal जोड़ी अपेक्षित है; इससे हटना अनिर्धारित व्यवहार को न्योता देता है।

यदि आप टिक्स linspace से बनाते हैं और लेबल को एक दशमलव तक गोल कर देते हैं, तो लेबल वास्तविक टिक-स्थिति से पूरी तरह मेल नहीं भी खा सकते—और जब आप इन्हीं मानों को secondary axis पर ट्रांसफॉर्म करते हैं, तो यह असंगति दिखाई दे सकती है। इसे स्वीकार करना, टिक रणनीति बदलना, या अधिक सटीकता से फ़ॉर्मेट करना—यह आपकी पसंद है; प्रभाव प्रस्तुति से जुड़ा है, पर दिखता ज़रूर है।

जटिल ट्रांसफॉर्म के बारे में क्या?

यदि secondary axis के मान किसी जटिल कस्टम फ़ंक्शन से निकलते हैं और उसका बंद-रूप (closed-form) inverse देना मुश्किल है, तब भी SecondaryAxis के अनुबंध के अनुसार inverse देना आवश्यक है। व्यवहार में एक व्यावहारिक रास्ता यह है कि गैलरी वाले उदाहरण की तर्ज पर numpy इंटरपॉलेशन का सहारा लिया जाए; एक संदर्भ समाधान ने Matplotlib secondary axis गैलरी के चौथे उदाहरण से विचार लेकर np.interp का उपयोग किया था। अलग से यह स्वाभाविक सवाल है कि बिना सटीक inverse के क्या होता है; दस्तावेज़ित इंटरफ़ेस कहता है कि दूसरी फ़ंक्शन पहली का inverse होना चाहिए।

यह समझना क्यों उपयोगी है

सही तरीका अपनाने से भूमिकाएं साफ़ रहती हैं। SecondaryAxis उसी मूल निर्देशांक का वैकल्पिक दृश्य देने के लिए है, जहाँ निर्धार्य रूपांतरण हो और बस टिक्स व लेबल चाहिए हों। twinx/twiny अलग-अलग इकाइयों या रेंज वाले कई डेटासेट्स को एक-दूसरे पर ओवरले किए Axes पर प्लॉट करने के लिए हैं, जहाँ एक दिशा साझा होती है। सही औज़ार चुनने से लेआउट सरल होता है, नाज़ुक autoscaling टकरावों से बचाव होता है, और ट्रांसफॉर्मेशन स्पष्ट व अनुमानित रहते हैं।

सार और व्यावहारिक सलाह

यदि आपका लक्ष्य primary axis का कोई फ़ंक्शन लेकर secondary स्केल दिखाना है, तो SecondaryAxis पर टिके रहें। हमेशा सही forward/inverse जोड़ी दें। संरेखण पक्का करने के लिए primary टिक-स्थितियों को रूपांतरित करके secondary टिक्स को स्पष्ट रूप से सेट करें। लेबल राउंडिंग पर ध्यान दें—फ़ॉर्मेटेड स्ट्रिंग्स कभी-कभी असली टिक-निर्देशांकों से अलग पड़ जाती हैं और ट्रांसफॉर्म किए गए लेबल गलत दिख सकते हैं। जटिल ट्रांसफॉर्म में, जहाँ विश्लेषणात्मक inverse असहज हो, एक कामचलाऊ तरीका यह है कि Matplotlib की secondary axis गैलरी में दिखाए गए इंटरपोलेशन पैटर्न को अनुकूलित किया जाए।

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