अध्याय 06

अनुपस्थित डेटा — रिक्तियों को खोजना और उनका अर्थ तय करना

टेबल में हर रिक्ति को पहचानना (यहाँ तक कि - या ? जैसे टेक्स्ट में छिपे मान भी), यह समझना कि NaN कैसे योग, औसत और डेटा टाइप बदलता है, और सोच-समझकर हर कॉलम के लिए निर्णय लेना — रिक्ति को हटाएं, भरें या वैसे ही रखें।

45 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

मार्च के पहले सप्ताह के लिए दुकान के ऑर्डर्स को तीन शाखाओं में हाथ से टाइप किया गया और orders_raw.csv के रूप में एक्सपोर्ट किया गया। दुकान का मालिक शाम तक दो आंकड़े चाहता है: कुल कितनी यूनिट्स बिकीं, और कुल कितना पैसा आया।

आप फ़ाइल पढ़ते हैं और पिछले अध्यायों की आदत के अनुसार केवल पैमाना (scale) देखने के लिए पहले price कॉलम को जोड़ते हैं:

python
import pandas as pd

orders = pd.read_csv("orders_raw.csv")

print(orders["price"].sum())
text
15.060.0850.08.0-15.0120.0

यह कोई संख्या नहीं है। यह टेक्स्ट के रूप में एक साथ चिपकी हुई हर कीमत है — और पांडास ने आपको सचेत करने के लिए कोई एरर भी नहीं दिया। अब quantity कॉलम देखें:

python
print(orders["quantity"].sum())
print(orders["quantity"].mean())
text
76.0
10.857142857142858

यह ठीक लग रहा है, और यही बात इसे और भी ख़तरनाक बनाती है। फ़ाइल में कुल आठ ऑर्डर्स हैं, फिर भी 76 / 8 का मान 9.5 होता है, 10.86 नहीं। एक quantity खाली थी, और पांडास ने चुपचाप उसे कुल योग और औसत दोनों से बाहर कर दिया। किसी ने आपको इसके बारे में भी नहीं बताया।

तो फ़ाइल में रिक्तियाँ (gaps) हैं, और ये रिक्तियाँ दो प्रकार की होती हैं। कुछ दृश्यमान (visible) हैं: एक खाली सेल, जिसे पांडास एक विशेष "मिसिंग" मार्कर में बदल देता है और फिर अंकगणित करते समय चुपचाप नज़रअंदाज़ कर देता है। अन्य छिपी हुई (hidden) हैं: किसी ने सेल को खाली छोड़ने के बजाय - टाइप कर दिया, पांडास ने इसे सच मान लिया, और पूरा price कॉलम टेक्स्ट बन गया।

दोनों में से कोई भी प्रकार एरर नहीं देता। दोनों ही अंतिम उत्तर बदल देते हैं। यह अध्याय हर रिक्ति को खोजने, पांडास इसके साथ क्या करता है उसे समझने, और फिर डिफ़ॉल्ट व्यवहार पर छोड़ने के बजाय सोच-समझकर निर्णय लेने के बारे में है — रिक्ति को हटाएं (drop), भरें (fill), या वैसे ही रखें (keep)।

इस अध्याय के अंत में आप कर पाएंगे

  • यह समझाना कि NaN क्या है, NaN == NaN का मान False क्यों होता है, और एक सेल खाली होने पर पूर्ण संख्या वाला कॉलम दशमलव में क्यों बदल जाता है
  • isna().sum() से प्रति कॉलम अनुपस्थित मानों की गिनती करना और अपूर्ण पंक्तियों को बाहर निकालना
  • - या ? जैसे छिपे हुए मार्करों को na_values= या pd.to_numeric(errors="coerce") की मदद से वास्तविक अनुपस्थित मानों में बदलना
  • यह सटीक अनुमान लगाना कि sum, mean, count और कॉलम अंकगणित अनुपस्थित मानों के साथ कैसा व्यवहार करते हैं
  • dropna() से अनुपस्थित मानों को हटाना, और केवल इच्छित पंक्तियों को हटाने के लिए subset= व how= का उपयोग करना
  • fillna() से अनुपस्थित मानों को बदलना — एक मान से, प्रति कॉलम अलग मान से, या किसी सांख्यिकीय मान (statistic) से
  • प्रत्येक कॉलम के लिए सोच-समझकर निर्णय लेना कि उसे हटाएं, भरें या रखें — और उसका कारण बताना
  • पांडास 3 के तहत काम न करने वाले inplace=True के जाल से बचना

ज़रूरी शर्तें: पंक्तियों को फ़िल्टर करना।


पहले फ़ाइल बना लें

अपने प्रोजेक्ट फ़ोल्डर में orders_raw.csv नाम से एक फ़ाइल बनाएं जिसमें ठीक यही पंक्तियाँ हों। रिक्तियाँ जानबूझकर छोड़ी गई हैं — तीसरी, पाँचवीं और सातवीं पंक्ति को ध्यान से देखें:

text
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0

यह पिछले अध्यायों की orders.csv के वही आठ ऑर्डर्स हैं, जिनमें चार चीज़ें बिगड़ी हुई हैं: ऑर्डर 1002 में कोई quantity नहीं है, ऑर्डर 1004 में कोई branch नहीं है और इसके price में N/A लिखा है, और ऑर्डर 1006 के price में - है।


कोड लिखने से पहले

मिसिंग डेटा एकमात्र ऐसा विषय है जहाँ पहले कोड लिखना सक्रिय रूप से ख़तरनाक होता है: dropna() और fillna() दोनों बिना किसी शिकायत के चलते हैं और दोनों आपके उत्तर को बदल देते हैं। इसलिए काम पहले देखने और निर्णय लेने से शुरू होता है।

1. सवाल को एक वाक्य में कहें। "सप्ताह के लिए बेची गई कुल मात्रा और कुल राजस्व (quantity × price) क्या था — और कितने ऑर्डर्स को गिना नहीं जा सका?" यह अंतिम हिस्सा बहुत महत्वपूर्ण है। एक कुल योग जो चुपचाप दो ऑर्डर्स को छोड़ देता है, वह उस कुल योग के समान नहीं है जो कहता है कि "8 में से 6 ऑर्डर्स गिने गए"।

2. किसी भी अन्य चीज़ से पहले देखें कि क्या आया है। shape और info() मिलकर आपको एक साथ हर कॉलम के बारे में बताते हैं:

python
print(orders.shape)
orders.info()
text
(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   order_id  8 non-null      int64
 1   date      8 non-null      str
 2   branch    7 non-null      str
 3   product   8 non-null      str
 4   category  8 non-null      str
 5   quantity  7 non-null      float64
 6   price     7 non-null      str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytes

कॉलम-दर-कॉलम इसकी तुलना अपनी अपेक्षा से करें:

  • branch — 8 non-null str की अपेक्षा थी, 7 non-null मिले। एक शाखा का नाम गायब है।
  • quantity — 8 non-null int64 की अपेक्षा थी, 7 non-null float64 मिले। एक मात्रा गायब है, और यही कारण है कि यह float64 बन गया (नीचे समझाया गया है)।
  • price — 8 non-null float64 की अपेक्षा थी, 7 non-null str मिले। एक कीमत गायब है और कॉलम में कुछ टेक्स्ट आ गया है।

उस सूची की दो चीज़ें तथ्यों के बजाय सुराग हैं। किसी पूर्ण संख्या (integer) वाले कॉलम का float64 दिखना लगभग हमेशा एक रिक्ति का संकेत होता है। किसी संख्या वाले कॉलम का str दिखना हमेशा यह बताता है कि उसमें टेक्स्ट मिल गया है — और संख्या कॉलम में टेक्स्ट आमतौर पर एक छिपा हुआ "मिसिंग" मार्कर होता है।

3. अपनी मनचाही आउटपुट का एक प्रारूप (sketch) बनाएं। कुछ इस तरह:

text
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known):     76
Total revenue (counted):    ...

ध्यान दें कि यह कुल योग के साथ-साथ रिक्तियों की भी रिपोर्ट करता है। रिपोर्ट पढ़ने वाले को पता होना चाहिए कि राजस्व का यह आंकड़ा एक निचली सीमा (lower bound) है।

4. प्रत्येक रिक्ति के लिए उपकरण तय करें — कोड लिखने से पहले। प्रत्येक कॉलम के लिए पूछें: यहाँ खाली सेल का क्या अर्थ है, और क्या इसके बिना सवाल का जवाब दिया जा सकता है?

  • branch — रिक्ति का अर्थ है ऑर्डर हुआ था लेकिन शाखा दर्ज नहीं की गई। निर्णय: "Unknown" से भरें (fill), क्योंकि ऑर्डर वास्तविक है और इसका पैसा अभी भी गिना जाना चाहिए।
  • quantity — रिक्ति का अर्थ है हम नहीं जानते कि कितने बेचे गए। निर्णय: इसे अनुपस्थित ही रखें (keep) और पंक्ति को राजस्व से बाहर रखें, क्योंकि अपनी ओर से कोई संख्या (0? या औसत?) गढ़ने से बिक्री की झूठी कल्पना बन जाएगी।
  • price — रिक्ति का अर्थ है हम नहीं जानते कि यह कितने में बिका। निर्णय: उसी कारण से इसे अनुपस्थित ही रखें (keep) और राजस्व से बाहर रखें — और रिपोर्ट करें कि कितनी पंक्तियाँ बाहर रखी गईं।

5. तय करें कि आप बाद में क्या जाँचेंगे। मान हटाने या बदलने वाले प्रत्येक चरण के बाद, मिसिंग काउंट और shape को दोबारा प्रिंट करें। यदि कोई dropna() आपको 8 पंक्तियों से घटाकर 2 पर ले आता है, तो आप उसे रिपोर्ट बाहर जाने के बाद नहीं, बल्कि तुरंत देखना चाहेंगे।

इस अध्याय का शेष भाग आपको बारी-बारी से प्रत्येक उपकरण देता है, और अंत में हम इस योजना को एक प्रोग्राम में पिरोते हैं।


"अनुपस्थित" कैसा दिखता है: NaN

जब पांडास को कोई खाली सेल मिलता है, तो वह खाली स्ट्रिंग या शून्य संग्रहीत नहीं करता। वह NaN ("not a number") नामक एक विशेष फ्लोटिंग-पॉइंट मान संग्रहीत करता है। आप किसी Series में None डालकर खुद भी इसे बना सकते हैं:

python
import pandas as pd

quantity = pd.Series([12, None, 2])
print(quantity)
text
0    12.0
1     NaN
2     2.0
dtype: float64

इसके dtype को देखें। आपने इसे दो पूर्णांक और एक रिक्ति दी, और बदले में 12.0 व 2.0 के साथ float64 मिला। info() के सुराग के पीछे यही कारण है: NaN एक float मान है, और एक क्लासिक पूर्णांक कॉलम के पास इसे रखने का कोई तरीका नहीं होता। इसलिए जिस क्षण एक सेल खाली होता है, पांडास NaN के लिए जगह बनाने के लिए पूरे कॉलम को float64 में बदल देता है। यही कारण है कि हमारी फ़ाइल में quantity 12.0, 2.0 आदि के रूप में प्रिंट हुआ।

टेक्स्ट कॉलम अलग तरह से व्यवहार करते हैं। एक str कॉलम डेटा टाइप बदले बिना अनुपस्थित मान को रख सकता है:

python
branch = pd.Series(["north", None, "east"])
print(branch)
text
0    north
1      NaN
2     east
dtype: str

यह अभी भी NaN प्रिंट करता है, और dtype str ही रहता है। इसलिए पांडास 3 में आप संख्या और टेक्स्ट दोनों कॉलमों में NaN पाएंगे, और वही उपकरण दोनों में इसे खोजते हैं।

NaN किसी के बराबर नहीं है — यहाँ तक कि खुद के भी नहीं

अनुपस्थित मानों के बारे में यह सबसे आश्चर्यजनक तथ्य है, और यह उन्हें खोजने के सबसे स्वाभाविक तरीके को तोड़ देता है:

python
import numpy as np
import pandas as pd

orders = pd.read_csv("orders_raw.csv")

print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())
text
False
0

NaN का अर्थ है "कोई ऐसा मान जिसे हम नहीं जानते"। दो अज्ञात मान समान हैं या नहीं यह ज्ञात नहीं है, इसलिए तुलना हर बार False देती है। इससे == np.nan एक ऐसा परीक्षण बन जाता है जो कभी किसी चीज़ से मेल नहीं खाता: branch कॉलम में एक रिक्ति है, और गिनती 0 कहती है। इसके बजाय सीधे पांडास से पूछें।

कभी भी == से अनुपस्थित मान न खोजें। == np.nan बिना किसी एरर के चलता है और हमेशा कुछ नहीं पाता, जिससे रिक्तियों से भरा कॉलम भी पूर्ण दिखाई देता है। isna() का उपयोग करें।

अनुपस्थित मान खोजना: isna()

isna() वह सवाल पूछता है जो == नहीं पूछ सकता: "क्या यह सेल अनुपस्थित है?" किसी कॉलम पर यह True/False का एक कॉलम देता है — एक मास्क, ठीक फ़िल्टरिंग वाले अध्याय के मास्क की तरह:

python
print(orders["branch"].isna())
text
0    False
1    False
2    False
3     True
4    False
5    False
6    False
7    False
Name: branch, dtype: bool

पूरे DataFrame पर यह प्रत्येक सेल के लिए यही करता है। हालाँकि, आप शायद ही कभी आठ पंक्तियों के True/False को पढ़ना चाहेंगे। आपको एक गिनती चाहिए, और चूँकि True को 1 गिना जाता है, .sum() वह गिनती दे देता है:

python
print(orders.isna().sum())
text
order_id    0
date        0
branch      1
product     0
category    0
quantity    1
price       1
dtype: int64

किसी भी नई फ़ाइल पर info() के बाद यही लाइन चलानी चाहिए। यह Non-Null Count वाली ही जानकारी है, जिसे सीधे रिक्तियों की गिनती करने के लिए उलट दिया गया है।

लेकिन price को देखें: यह 1 कहता है। हम जानते हैं कि दो कीमतें ख़राब हैं — N/A और -। इस बात को ध्यान में रखें; हम अगले भाग में इस पर वापस आएंगे।

कौन सी पंक्तियाँ अपूर्ण हैं?

गिनती आपको बताती है कि कितने हैं; यह देखने के लिए कि कौन से हैं, मास्क का उपयोग फ़िल्टर के रूप में करें। isna().any(axis=1) प्रत्येक पंक्ति के लिए पूछता है, "क्या इस पंक्ति में कुछ भी अनुपस्थित है?":

python
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)
text
order_id        date branch   product     category  quantity price
1      1002  2024-03-01  south  notebook   stationery       NaN  60.0
3      1004  2024-03-02    NaN    bottle  accessories       7.0   NaN

axis=1 का अर्थ है प्रत्येक कॉलम में नीचे जाने के बजाय "प्रत्येक पंक्ति के आर-पार देखना"। और ध्यान दें कि इस सूची में कौन नहीं है: ऑर्डर 1006, जिसकी कीमत - है। जहाँ तक पांडास को पता है, वह पंक्ति पूर्ण है। इस विचार को थामे रखें। वास्तविक पंक्तियों को देखना उस अतिरिक्त लाइन के लायक है: इसी तरह आप नोटिस करते हैं कि, मान लीजिए, प्रत्येक अनुपस्थित मात्रा एक ही शाखा से आती है, जो पांडास के बजाय उस शाखा के लोगों से पूछने का सवाल है।

गिनती के बजाय अनुपात (share) के रूप में

आठ पंक्तियों के साथ गिनती ठीक है। अस्सी हज़ार के साथ, "312 गायब" का बहुत कम अर्थ है जब तक कि आप यह न जान लें कि यह 0.4% है। एक True/False कॉलम का .mean() मान True का अनुपात होता है:

python
print(orders.isna().mean())
text
order_id    0.000
date        0.000
branch      0.125
product     0.000
category    0.000
quantity    0.125
price       0.125
dtype: float64

branch, quantity और price के आठ सेल्स में से एक सेल गायब है — प्रत्येक में 12.5%। 90% खाली कॉलम और 0.1% खाली कॉलम पूरी तरह से अलग निर्णयों की मांग करते हैं, और इसी तरह आप उन्हें अलग पहचानते हैं।


छिपे हुए अनुपस्थित मान: जब रिक्ति टेक्स्ट के रूप में लिखी हो

isna() ने एक अनुपस्थित कीमत गिनी, लेकिन दो कीमतें अनुपयोगी हैं। दूसरा ऑर्डर 1006 में - है। पांडास के लिए, - अनुपस्थित नहीं है — यह टेक्स्ट का एक पूर्ण रूप से मान्य टुकड़ा है। और संख्याओं के कॉलम में टेक्स्ट का एक टुकड़ा पूरे कॉलम को str बनाने के लिए पर्याप्त है, जो कि ठीक वही है जो info() ने दिखाया था।

यहाँ बताया गया है कि आप कैसे पता लगाते हैं कि वास्तव में कॉलम में क्या है:

python
print(orders["price"].unique())
text
<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: str

प्रत्येक मान कोट्स में है — वे सभी टेक्स्ट हैं, जिसमें '15.0' भी शामिल है। और साथ-साथ दो प्रकार की रिक्तियाँ हैं: nan, जिसे पांडास ने पहचाना, और '-', जिसे उसने नहीं पहचाना।

N/A nan क्यों बना लेकिन - क्यों नहीं? क्योंकि read_csv में स्ट्रिंग्स की एक अंतर्निहित सूची होती है जिसे वह मिसिंग मानता है: एक खाली सेल, NA, N/A, n/a, NaN, null, None और कुछ अन्य। -, ?, missing, 0 और unknown उस सूची में नहीं हैं। डेटा टाइप करने वाले व्यक्ति ने "अज्ञात" के लिए जो कुछ भी इस्तेमाल किया, वही तय करता है कि आपको किस तरह की रिक्ति मिलती है।

छिपे हुए मानों को वास्तविक NaN में बदलने के दो तरीके हैं।

उपाय 1: read_csv को मार्कर के बारे में बताएं — na_values=

यदि आप मार्कर जानते हैं, तो सबसे साफ़ समाधान फ़ाइल पढ़ने के क्षण में ही होता है। na_values= आपकी स्ट्रिंग्स को अंतर्निहित सूची में जोड़ देता है:

python
orders = pd.read_csv("orders_raw.csv", na_values=["-"])

print(orders["price"].dtype)
print(orders.isna().sum())
text
float64
order_id    0
date        0
branch      1
product     0
category    0
quantity    1
price       2
dtype: int64

पहले की गिनती से इसकी तुलना करें। branch और quantity अपरिवर्तित हैं — na_values= सूची में जोड़ता है, उसे बदलता नहीं है, इसलिए खाली सेल्स और N/A अभी भी पहचाने जाते हैं। जो बदला है वह है price: यह अब float64 है, और यह 2 मिसिंग मान रिपोर्ट करता है — N/A और - दोनों अब वास्तविक NaN हैं। कॉलम फिर से संख्यात्मक है, इसलिए इस पर अंकगणित का मतलब वास्तव में अंकगणित होता है।

ध्यान दें कि na_values=["-"] प्रत्येक कॉलम पर लागू होता है। यहाँ हम यही चाहते हैं। यदि कोई - कहीं और एक वास्तविक मान हो सकता है — मान लीजिए कोई उत्पाद कोड — तो इसके बजाय एक dict पास करें, na_values={"price": ["-"]}, और केवल price कॉलम प्रभावित होगा।

उपाय 2: बाद में रूपांतरित करें — pd.to_numeric(errors="coerce")

कभी-कभी आप पहले से नहीं जानते कि कॉलम में कौन सी अजीब स्ट्रिंग्स हैं, या फ़ाइल किसी और के कोड द्वारा पढ़ी गई थी। तब आप पढ़ने के बाद कॉलम को रूपांतरित करते हैं। पहले देखें कि एक सादा रूपांतरण क्या करता है:

python
raw = pd.read_csv("orders_raw.csv")

price = pd.to_numeric(raw["price"])
text
ValueError: Unable to parse string "-" at position 5

वह एरर उपयोगी है: यह आपत्तिजनक मान और उसकी स्थिति का नाम बताता है। errors="coerce" जोड़ने का अर्थ है "वह सब कुछ जिसे आप संख्या में नहीं बदल सकते, उसे NaN बना दें":

python
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)
text
0     15.0
1     60.0
2    850.0
3      NaN
4      8.0
5      NaN
6     15.0
7    120.0
Name: price, dtype: float64

टेबल को वास्तव में बदलने के लिए, परिणाम को वापस कॉलम में असाइन करें — to_numeric एक नई Series लौटाता है और raw को अपरिवर्तित छोड़ देता है:

python
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())
text
float64 2

कौन सा उपाय चुनें

errors="coerce" शक्तिशाली और सीधा है। यह गैर-संख्यात्मक किसी भी चीज़ को NaN में बदल देता है — जिसमें 1O0 (अक्षर O) या हज़ारों के कॉमा के साथ 1,200 के रूप में गलत टाइप की गई वास्तविक कीमत भी शामिल है। वे मिसिंग मान नहीं हैं; वे टाइपो वाले मान हैं, और उन्हें जबरन coerce करने से जानकारी चुपचाप नष्ट हो जाती है।

इसलिए आदत यह होनी चाहिए: पहले गिनें, बाद में गिनें, और अंतर की व्याख्या करें।

  • आप मार्कर जानते हैं (-, ?, missing): read_csv में na_values= का उपयोग करें। यह सटीक है — केवल वही स्ट्रिंग मिसिंग बनती है।
  • अज्ञात कचरा है, और कॉलम का संख्यात्मक होना अनिवार्य है: pd.to_numeric(errors="coerce") का उपयोग करें। यह सब कुछ पकड़ लेता है, इसलिए जाँचें कि इसने क्या पकड़ा।
  • कुछ मान टाइपो हैं जिन्हें आप सुधार सकते हैं: पहले unique() को देखें, उन्हें ठीक करें, फिर रूपांतरित करें। Coerce करने से वास्तविक डेटा नष्ट हो जाएगा।

Coerce करने से पहले, raw["price"].unique() ने ठीक एक गैर-संख्यात्मक स्ट्रिंग ('-') और एक nan दिखाया था। Coerce करने के बाद, ठीक 2 NaN हैं। संख्याएं सहमत हैं, इसलिए कुछ और नहीं खोया। वह जाँच डेटा को साफ़ करने और उसे नुकसान पहुँचाने के बीच का पूरा अंतर है।

यहाँ से आगे हम na_values=["-"] के साथ पढ़ी गई फ़ाइल का उपयोग करते हैं।


गणना में पांडास NaN के साथ कैसा व्यवहार करता है

अब जब रिक्तियाँ वास्तविक NaN बन गई हैं, तो आपको यह जानना होगा कि पांडास उनके साथ क्या करता है — क्योंकि वह रुककर आपसे नहीं पूछता।

कॉलम सारांश NaN को छोड़ देते हैं

python
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]

print("len  :", len(q))
print("count:", q.count())
print("sum  :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())
text
len  : 8
count: 7
sum  : 76.0
mean : 10.857142857142858
check: 10.857142857142858

याद रखने योग्य तीन व्यवहार:

  • len पंक्तियों को गिनता है; count() मानों को गिनता है। उनके बीच का अंतर रिक्तियों की संख्या है। यह info() में Non-Null Count के समान विचार है।
  • sum() NaN को छोड़ देता है। यह ज्ञात सात मात्राओं को जोड़ता है। यह उचित है — लेकिन इसका मतलब है कि "76" सभी ऑर्डर्स का नहीं, बल्कि केवल ज्ञात मात्राओं का कुल योग है।
  • mean() count() से विभाजित करता है, len से नहीं। औसत ज्ञात सात मानों पर निकाला गया है। आप आमतौर पर यही चाहते हैं; रिक्ति को 0 मानने से अकारण ही औसत नीचे गिर जाता।

यदि आप चाहते हैं कि कुछ भी गायब होने पर आपको सूचित किया जाए, तो skipna=False किसी भी मान के NaN होते ही परिणाम को NaN बना देता है:

python
print(q.sum(skipna=False))
text
nan

कॉलमों के बीच अंकगणित NaN को फैलाता है

सारांश रिक्तियों को छोड़ देते हैं; अंकगणित उन्हें फैलाता है। NaN के साथ संयुक्त कुछ भी NaN देता है, क्योंकि "अज्ञात कीमत का 7 गुना" खुद अज्ञात है:

python
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))
text
0     180.0
1       NaN
2    1700.0
3       NaN
4     240.0
5       NaN
6     300.0
7     480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8

दोनों व्यवहारों को एक साथ रखें और आप अध्याय की शुरुआत का जाल देख सकते हैं। revenue.sum() ने बिना किसी चेतावनी के 2900.0 प्रिंट किया — लेकिन आठ में से तीन ऑर्डर्स ने इसमें कुछ भी योगदान नहीं दिया। ऑर्डर 1002 में quantity गायब है, 1004 और 1006 में price गायब है। कुल योग केवल तभी ईमानदार होता है जब आप कहें "8 में से 5 ऑर्डर्स"। इसीलिए योजना में प्रत्येक कुल योग के बगल में एक गिनती मांगी गई थी।


अनुपस्थित मान हटाना: dropna()

dropna() उन पंक्तियों को हटा देता है जिनमें NaN होता है। बिना किसी तर्क के यह किसी भी कॉलम में किसी भी रिक्ति वाली प्रत्येक पंक्ति को हटा देता है:

python
print(orders.shape)
print(orders.dropna().shape)
text
(8, 7)
(5, 7)

आठ में से तीन पंक्तियाँ चली गईं — और ऑर्डर 1004 इसलिए चला गया क्योंकि इसकी branch गायब थी, भले ही मात्रा के सवाल के लिए यह पूरी तरह से अच्छी पंक्ति थी। एक सादे dropna() की यही समस्या है: यह "अपूर्ण कुछ भी हटा दें" का उत्तर देता है, जो शायद ही कभी मुख्य सवाल होता है।

subset= — केवल वही कॉलम जिनकी प्रश्न को आवश्यकता है

बताएं कि किन कॉलमों का मौजूद होना अनिवार्य है:

python
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())
text
(7, 7)
76.0

केवल ऑर्डर 1002 हटाया जाता है — एकमात्र पंक्ति जहाँ मात्रा वास्तव में अज्ञात है। राजस्व के लिए दोनों कॉलमों की आवश्यकता होती है:

python
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])
text
order_id  quantity  price
0      1001      12.0   15.0
2      1003       2.0  850.0
4      1005      30.0    8.0
6      1007      20.0   15.0
7      1008       4.0  120.0

इंडेक्स पर ध्यान दें: 0, 2, 4, 6, 7। dropna() मूल लेबलों को बनाए रखता है, ठीक वैसे ही जैसे फ़िल्टरिंग करती है, ताकि आप हमेशा किसी पंक्ति को फ़ाइल में वापस ट्रैक कर सकें। यदि आपको फिर से 0, 1, 2… चाहिए, तो .reset_index(drop=True) जोड़ें।

how="all" — केवल वे पंक्तियाँ जो पूरी तरह खाली हैं

निर्यात की गई स्प्रेडशीट अक्सर कुछ ऐसी पंक्तियों के साथ समाप्त होती हैं जो हर कॉलम में खाली होती हैं। how="all" केवल उन्हीं को हटाता है और किसी भी ऐसी पंक्ति को छोड़ देता है जिसमें एक भी मान हो:

python
print(orders.dropna(how="all").shape)
text
(8, 7)

यहाँ कुछ भी नहीं हटाया गया — हमारी कोई भी पंक्ति पूरी तरह से खाली नहीं है — जिसकी आप ठीक-ठीक पुष्टि करना चाहते हैं।

dropna() एक नई टेबल लौटाता है

अधिकांश पांडास मेथड्स की तरह, dropna() orders को नहीं बदलता; यह आपको एक नया DataFrame सौंपता है। यदि आप एक पंक्ति पर अकेले orders.dropna() लिखते हैं, तो orders को कुछ नहीं होता। परिणाम को असाइन करें — एक नए नाम पर यदि आपको अभी भी पूरी टेबल की आवश्यकता होगी, जो हमारी जैसी रिपोर्ट में होगी (यह बताने के लिए कि कितने हटाए गए थे)।


अनुपस्थित मान बदलना: fillna()

कभी-कभी किसी रिक्ति का एक समझदारी भरा प्रतिस्थापन होता है। जहाँ NaN था, fillna() वहाँ एक मान रख देता है।

एक कॉलम के लिए एक मान

अनुपस्थित शाखा सबसे स्पष्ट मामला है। ऑर्डर हुआ और उसका पैसा वास्तविक है; हम बस यह नहीं जानते कि किस शाखा ने इसे लिया। इसे एक लेबल से भरने से पंक्ति सुरक्षित रहती है और बाद में शाखा द्वारा की जाने वाली किसी भी गिनती में रिक्ति स्पष्ट दिखाई देती है:

python
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())
text
['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']

उस लाइन का आकार महत्वपूर्ण है: कॉलम लें, fillna करें, और उसे उसी कॉलम में वापस असाइन करें। fillna एक नई Series लौटाता है; सामने orders["branch"] = के बिना, कुछ भी संग्रहीत नहीं होता है।

अलग-अलग कॉलम के लिए अलग मान: एक dict

एक साथ कई कॉलमों को उनके अपने मानों से भरने के लिए {column: value} की एक डिक्शनरी पास करें। जिन कॉलमों का नाम नहीं दिया गया है उन्हें अछूता छोड़ दिया जाता है:

python
orders = pd.read_csv("orders_raw.csv", na_values=["-"])

filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())
text
order_id    0
date        0
branch      0
product     0
category    0
quantity    0
price       2
dtype: int64

price में अभी भी इसकी दो रिक्तियाँ हैं, क्योंकि हमने इसका उल्लेख नहीं किया। orders.fillna(0) की तुलना में dict स्वरूप का यही लाभ है, जो कीमतों सहित हर कॉलम को 0 से भर देता — और 0 की कीमत का मतलब एक मुफ्त बैग है।

भरने से उत्तर बदल जाता है

मात्रा को 0 से भरना हानिरहित लग रहा था। औसतों की तुलना करें:

python
print("skip the gap :", orders["quantity"].mean())
print("fill with 0  :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())
text
skip the gap : 10.857142857142858
fill with 0  : 9.5
fill with median: 10.375

एक कॉलम से तीन अलग-अलग औसत, और एकमात्र अंतर यह है कि आपने अज्ञात मात्रा का क्या अर्थ माना:

  • 0 कहता है "इस ऑर्डर में कुछ नहीं बिका"। ऑर्डर 1002 के लिए, एक नोटबुक ऑर्डर जिसमें निश्चित रूप से कुछ बिका था, यह गलत है, और यह औसत को नीचे खींचता है।
  • माध्यिका (median) (यहाँ 7) कहती है "एक सामान्य ऑर्डर मान लें"। यह औसत को लगभग स्थिर रखती है, लेकिन कुल योग में अब 7 नोटबुक शामिल हैं जिन्हें किसी ने नहीं गिना।
  • छोड़ना (skipping) कहता है "हम नहीं जानते, इसलिए इसे बाहर छोड़ दें"। औसत उन सात ऑर्डर्स पर है जिन्हें हम जानते हैं।

सामान्य तौर पर इनमें से कोई भी "सही" नहीं है। प्रत्येक थोड़ा अलग सवाल का जवाब देता है। यही कारण है कि यह निर्णय किसी भी डिफ़ॉल्ट व्यवहार के बजाय आपकी योजना में लिखित रूप में होना चाहिए।

एक नियम जो अधिकांश समय काम करता है:

  • एक लेबल (branch, category, name): "Unknown" या इसी तरह के मान से भरें। पंक्ति रखी जाती है, और रिक्ति अपने समूह के रूप में दिखाई देती है।
  • एक गिनती जहाँ खाली होने का वास्तव में अर्थ कुछ नहीं है (रिटर्न, शिकायतें): 0 से भरें — लेकिन केवल तभी जब स्रोत का वास्तव में खाली होने से अर्थ शून्य हो।
  • एक माप या पैसा (बेची गई मात्रा, कीमत, अंक): NaN रखें, इसे गणना से बाहर रखें, और रिपोर्ट करें कि कितने बाहर रखे गए थे। कल्पित संख्याएँ कल्पित कुल योग बन जाती हैं।
  • सवाल के लिए आवश्यक कॉलम, लेकिन अधिकतर मौजूद: केवल उस सवाल के लिए dropna(subset=[...]) का उपयोग करें। एक छोटा सा नुकसान, और एक ईमानदार उत्तर।

पूर्णांक (whole numbers) वापस पाना

भरने के बाद, quantity में कोई रिक्ति नहीं है, लेकिन यह अभी भी float64 है — पांडास इसे अपने आप वापस नहीं बदलता। एक बार जब कोई NaN नहीं बचता है, तो astype(int) काम करता है:

python
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)
text
[12, 0, 2, 7, 30, 1, 20, 4]
int64

भरने से पहले इसे आज़माएं और यह विफल हो जाता है, क्योंकि ऐसा कोई पूर्णांक नहीं है जिसका अर्थ "अज्ञात" हो — नीचे "जब चीज़ें काम नहीं करतीं" देखें।


एक संपूर्ण उदाहरण

clean_orders.py "कोड लिखने से पहले" की योजना को एक प्रोग्राम में रखता है: ज्ञात मार्कर के साथ पढ़ें, प्रत्येक रिक्ति की रिपोर्ट करें, शाखा भरें, जिन पंक्तियों को नहीं गिना जा सकता उन्हें बाहर करें, और कुल योग के साथ यह रिपोर्ट करें कि वे कितने ऑर्डर्स को कवर करते हैं।

python
import pandas as pd

orders = pd.read_csv("orders_raw.csv", na_values=["-"])

# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()

# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")

# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]

revenue = countable["quantity"] * countable["price"]

print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known):    ", int(orders["quantity"].sum()))
print("Total revenue (counted):   ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])
text
Rows, columns: (8, 7)
Missing per column:
branch      1
quantity    1
price       2
dtype: int64

Orders counted for revenue: 5 of 8
Total quantity (known):     76
Total revenue (counted):    2900.0

Not counted - check these with the branch:
   order_id   branch  quantity  price
1      1002    south       NaN   60.0
3      1004  Unknown       7.0    NaN
5      1006    south       1.0    NaN

इसे इस तरह क्यों लिखा गया है:

  • na_values=["-"] पहली ही पंक्ति में है। unique() को देखकर मार्कर एक बार पाया गया था। इसे read_csv में रखने का मतलब है कि कॉलम शुरू से ही संख्यात्मक है, और प्रोग्राम पढ़ने वाला कोई भी व्यक्ति देखता है कि फ़ाइल किस मार्कर का उपयोग करती है।
  • missing[missing > 0] केवल उन्हीं कॉलमों को प्रिंट करता है जिनमें रिक्तियाँ हैं। चालीस कॉलम वाली फ़ाइल पर, यह एक पठनीय रिपोर्ट और शून्यों की दीवार के बीच का अंतर है।
  • शाखा भरी गई है, संख्याएँ नहीं। यह योजना की निर्णय सूची है, जिसे कोड के रूप में लिखा गया है। कोई पाठक इससे असहमत हो सकता है, लेकिन वह इसे देख सकता है।
  • dropna(subset=...) उन दो कॉलमों का नाम देता है जिनकी राजस्व को आवश्यकता होती है, इसलिए ऑर्डर 1004 को इसकी गायब कीमत के लिए बाहर रखा गया है, इसकी गायब शाखा के लिए नहीं।
  • छूटी हुई पंक्तियों को प्रिंट किया जाता है, केवल गिना नहीं जाता। "3 ऑर्डर्स नहीं गिने गए" यह सवाल आमंत्रित करता है "कौन से?" — रिपोर्ट में इसका उत्तर दें। वे तीन ऑर्डर्स एक ऐसी सूची हैं जिसे कोई व्यक्ति वास्तव में जाकर ठीक कर सकता है।
  • "Total quantity (known)" और "(counted)" बताते हैं कि संख्याएँ क्या हैं। दोनों निचली सीमाएं (lower bounds) हैं; ये लेबल किसी को भी उन्हें पूर्ण मानने से रोकते हैं।

जब चीज़ें काम नहीं करतीं

TypeError: Cannot perform reduction 'mean' with string dtype आपने ऐसे कॉलम के औसत की मांग की जिसे पांडास ने टेक्स्ट के रूप में पढ़ा — आमतौर पर इसलिए क्योंकि एक सेल में -, ? या missing जैसा मार्कर होता है। अपराधी को खोजने के लिए df["col"].unique() चलाएं, फिर या तो na_values=["-"] के साथ दोबारा पढ़ें या pd.to_numeric(df["col"], errors="coerce") से रूपांतरित करें।

python
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())
text
TypeError: Cannot perform reduction 'mean' with string dtype

sum() ने अंकों की एक लंबी स्ट्रिंग लौटाई, जैसे 15.060.0850.0... वही कारण, बदतर लक्षण: टेक्स्ट कॉलम पर sum() संख्याओं को जोड़ने के बजाय स्ट्रिंग्स को आपस में जोड़ देता है, और कोई एरर नहीं देता। ऐसा दिखने वाला कोई भी कुल योग str कॉलम से आया है। कुछ भी जोड़ने से पहले df.dtypes की जाँच करें।

ValueError: Unable to parse string "-" at position 5 pd.to_numeric को ऐसा टेक्स्ट मिला जिसे वह रूपांतरित नहीं कर सका, और उसने आपको बिल्कुल सही बताया कि क्या और कहाँ समस्या है। या तो errors="coerce" जोड़ें (unique() से यह जाँचने के बाद कि अजीब मान वास्तव में "अज्ञात" मार्कर हैं, टाइपो नहीं), या पहले टाइपो को ठीक करें।

(df["col"] == np.nan).sum() 0 कहता है, लेकिन रिक्तियाँ हैं NaN कभी भी किसी के बराबर नहीं होता, यहाँ तक कि खुद के भी नहीं, इसलिए == np.nan किसी भी सेल से मेल नहीं खाता। df["col"].isna() का उपयोग करें।

IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer आपने ऐसे कॉलम पर astype(int) कॉल किया जिसमें अभी भी NaN है। "अज्ञात" अर्थ वाला कोई पूर्णांक नहीं है। पहले रिक्तियों को भरें या हटाएं, फिर रूपांतरित करें:

python
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)
text
pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')

(पांडास के अपने संदेश में "integer.Replace" और "typethat" में स्पेस छूटे हुए हैं।) संदेश में बड़े अक्षर I वाले 'Int64' का भी उल्लेख है: एक अलग पांडास पूर्णांक प्रकार जो अनुपस्थित मानों को रख सकता है। यह उपयोगी है, लेकिन पहले भरना या हटाना सरल समाधान है जिसे अपनाया जाना चाहिए।

ChainedAssignmentError — और रिक्ति अभी भी वहीं है आपने inplace=True वाला तरीका लिखा जो आपको कई पुराने ट्यूटोरियल्स में मिलेगा:

python
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())
text
1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.

See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html

गिनती अभी भी 1 है: कुछ भी नहीं भरा गया था। पांडास 3 में, orders["quantity"] हमेशा अपनी खुद की कॉपी (Copy-on-Write) के रूप में व्यवहार करता है, इसलिए इसे "in place" भरने से वह कॉपी भर जाती है, जिसे बाद में फेंक दिया जाता है। पुराने पांडास में कभी-कभी यह काम कर जाता था, यही वजह है कि यह पैटर्न ऑनलाइन हर जगह है। इसके बजाय सीधे असाइनमेंट लिखें — यह हर संस्करण में काम करता है और स्पष्ट रूप से बताता है कि क्या बदलता है:

python
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())
text
0

dropna() ने आपकी अपेक्षा से कहीं अधिक पंक्तियाँ हटा दीं एक सादा dropna() किसी भी कॉलम में रिक्ति होने पर पंक्ति को हटा देता है — जिसमें अधिकतर खाली रहने वाला notes या discount कॉलम भी शामिल है जिसका आप उपयोग भी नहीं कर रहे थे। कई रिक्तियों वाले कॉलम को खोजने के लिए df.isna().sum() प्रिंट करें, फिर केवल अपने सवाल के लिए आवश्यक कॉलमों के साथ dropna(subset=[...]) का उपयोग करें।

TypeError: can't multiply sequence by non-int of type 'float' आपने एक संख्या कॉलम को एक टेक्स्ट कॉलम से गुणा किया (quantity * price जबकि price str है)। समाधान पहले एरर के समान ही है: पहले price को संख्यात्मक बनाएं।