2025, Sep 28 07:31

लघुगणकीय स्केल वाले Seaborn हीटमैप के लिए सही normalization

Seaborn हीटमैप में LogNorm के vmin/vmax बाँधकर लॉगरिद्मिक स्केल 10^0–10^9 तक निश्चित करें, ताकि हर order of magnitude के लिए रंग स्पष्ट और सुसंगत रहे।

लघुगणकीय स्केल पर हीटमैप बनाते समय, अक्सर आप चाहते हैं कि हर रंग एक-एक order of magnitude को दर्शाए: 10^0, 10^1, 10^2, और आगे भी। अगर रंगों की मैपिंग खिसकी हुई या सिकुड़ी हुई लगे—भले ही आपकी colormap में डिस्क्रीट रंगों की सही संख्या हो—तो आमतौर पर वजह यह होती है कि normalization डेटा-रेंज को कैसे निकाल रहा है।

समस्या का सेटअप

यह डेटा मोटे तौर पर 10^0 से शुरू होकर 10^9 से काफी नीचे तक फैला है, लेकिन लक्ष्य एक डिस्क्रीट लॉगरिद्मिक कलर स्केल का है, जहाँ हर रंग एक order of magnitude का प्रतिनिधित्व करे। नीचे वह न्यूनतम उदाहरण है जो अपेक्षा के विपरीत मैपिंग देता है:

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
df_src = pd.read_excel('example_data.xlsx')
palette_rocket = sns.color_palette("rocket", as_cmap=False, n_colors=9)
canvas, axes = plt.subplots(figsize=(20, 10))
axes = sns.heatmap(
    df_src,
    norm=LogNorm(),
    annot=True,
    cmap=palette_rocket,
    linewidths=0.05,
    linecolor='grey'
)
plt.tight_layout()
plt.show()

कलरबार की निचली सीमा 10^0 से शुरू होती है जैसा चाहा गया था, लेकिन आगे के रंग 10^1, 10^2 आदि से नहीं मिलते। एनोटेशन में ~6.3×10^5 और ~9.2×10^5 जैसे मान 1×10^6 के साथ वही रंग साझा कर सकते हैं, जबकि 5×10^6 अचानक दूसरे रंग में चला जाता है—हालाँकि ये सभी एक ही order of magnitude में आते हैं।

मैपिंग अजीब क्यों दिखती है

डिफॉल्ट रूप से normalization डेटा-चालित होता है। जब आप सीमा (bounds) स्पष्ट रूप से दिए बिना लॉगरिद्मिक normalizer लगाते हैं, तो वह अपनी रेंज डेटा के न्यूनतम और अधिकतम से निकालता है। यानी यह ऐसा व्यवहार करता है मानो उसे vmin=data.min() और vmax=data.max() के साथ बुलाया गया हो। अगर आपका डेटासेट वास्तव में 10^9 तक पहुँचता ही नहीं, तो लॉग स्केल उसी छोटी रेंज पर फिट हो जाता है और उपलब्ध रंग उसी अनुमानित दायरे में बाँट दिए जाते हैं। इसलिए रंगों के बीच की सीमाएँ आपकी अपेक्षित दस की शक्तियों (10^1, 10^2, ...) के साथ नहीं बैठेंगी।

प्लॉटिंग कोड में कहीं यह नहीं बताया गया कि कलर स्केल बिल्कुल 10^0 से 10^9 तक ही चलना चाहिए। बस इतना निर्दिष्ट है: ‘लॉग स्केल पर 9 रंग इस्तेमाल करो।’ अगर डेटा उस दायरे को कवर नहीं करता, तो normalizer इच्छित domain का अंदाज़ा नहीं लगा सकता।

समाधान: normalization का डोमेन तय करें

अगर आप चाहते हैं कि हर रंग किसी खास order of magnitude से जुड़ा हो, तो normalizer को यह “मानने” के लिए कहें कि डेटा वांछित डोमेन में फैला है। इसके लिए लॉगरिद्मिक normalizer पर स्पष्ट bounds सेट करें। चूँकि normalizer का काम डेटा को [0, 1] इंटरवल में मैप करना है, इसलिए vmin और vmax को LogNorm में पास करें, heatmap कॉल में नहीं।

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
df_src = pd.read_excel('example_data.xlsx')
palette_rocket = sns.color_palette("rocket", as_cmap=False, n_colors=9)
canvas, axes = plt.subplots(figsize=(20, 10))
axes = sns.heatmap(
    df_src,
    norm=LogNorm(vmin=1, vmax=1e9),
    annot=True,
    cmap=palette_rocket,
    linewidths=0.05,
    linecolor='grey'
)
plt.tight_layout()
plt.show()

इससे 10^0 से 10^9 तक का एक डिस्क्रीट, order-of-magnitude आधारित मैपिंग लागू हो जाती है—चाहे डेटा वास्तव में ऊपरी सीमा तक पहुँचे या नहीं। उलटे, अगर आप vmin=data.min() और vmax=data.max() ही सेट करेंगे, तो आप वही डिफॉल्ट, डेटा-निर्भर बर्ताव दोहराएँगे।

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

रंग अर्थ को एन्कोड करता है। अगर आप चाहते हैं कि किसी रंग का मतलब हो “यह मान लॉग स्केल पर किसी खास दशक में आता है”, तो आपको ऐसा स्थिर डोमेन चाहिए जो डेटासेट के मौजूदा न्यूनतम और अधिकतम के साथ चुपचाप न बदलता रहे। नहीं तो, एक ही colormap के साथ बने दो प्लॉट अलग-अलग दहलीज़ का संकेत दे सकते हैं, और तुलना भरोसेमंद नहीं रहेगी।

मुख्य बातें

यदि मकसद है “हर order of magnitude के लिए एक रंग”, तो अप्रकट normalization पर निर्भर न रहें। अपना इरादा स्पष्ट करें: LogNorm(vmin=10^a, vmax=10^b) के साथ लॉगरिद्मिक डोमेन सेट करें। इससे अनपेक्षित रंग-सीमाएँ नहीं बनतीं और मैपिंग दस की शक्तियों के साथ सुसंगत रहती है। और याद रखें, bounds normalizer को देना ही अहम कदम है; heatmap कॉल पर देने से कोई कस्टम norm, जो पहले से मैपिंग को नियंत्रित कर रहा है, ओवरराइड नहीं होगा।

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