अध्याय 09

नए कॉलम जोड़ना — फ़ाइल में जो संख्याएँ नहीं हैं उन्हें तैयार करना

मौजूदा कॉलमों से नए कॉलम बनाना: पूरे कॉलम पर अंकगणित और इंडेक्स का संरेखण, assign, np.where और np.select, map, pd.cut, .str व .dt, ग्रुप से तुलना के लिए transform, और अंतिम विकल्प के रूप में apply।

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

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

दुकान की मालकिन का एक संदेश आता है: "प्रत्येक ऑर्डर के लिए, मैं देखना चाहती हूँ कि उससे कितने पैसे मिले, क्या वह एक बड़ा ऑर्डर था, कौन सा मैनेजर उसके लिए जिम्मेदार है, और वह सप्ताह का कौन सा दिन था। और प्रत्येक ऑर्डर के लिए, वह उसकी संबंधित शाखा की कुल कमाई का कितना हिस्सा है?"

आप orders.csv खोलते हैं। इसमें quantity (मात्रा) और price (कीमत), branch (तीन दुकानों में से कौन सी) और date (तारीख) मौजूद हैं। मालकिन ने जो कुछ माँगा, उनमें से एक भी चीज़ फ़ाइल में सीधे तौर पर मौजूद नहीं है। उनमें से प्रत्येक जानकारी को पहले से मौजूद कॉलमों से तैयार करना होगा।

यदि आप सामान्य पायथन से आए हैं, तो आपके हाथ पहले से ही जानते हैं कि क्या लिखना है — पंक्तियों पर एक लूप चलाना:

python
import pandas as pd

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

revenues = []
for i in range(len(orders)):
    revenues.append(orders.loc[i, "quantity"] * orders.loc[i, "price"])
orders["revenue"] = revenues

print(orders[["product", "quantity", "price", "revenue"]])
text
product  quantity  price  revenue
0       pen        12   15.0    180.0
1  notebook         5   60.0    300.0
2       bag         2  850.0   1700.0
3    bottle         7  120.0    840.0
4    eraser        30    8.0    240.0
5       bag         1  850.0    850.0
6       pen        20   15.0    300.0
7    bottle         4  120.0    480.0

यह काम करता है। लेकिन यह अध्याय इसी आदत को बदलने के लिए है, जिसके तीन मुख्य कारण हैं जो बाद में सामने आते हैं।

पहला, यह धीमा है: पायथन लूप के कोड को प्रति पंक्ति एक बार चलाता है, इसलिए दस लाख पंक्तियों की फ़ाइल का अर्थ है इंटरप्रेटर के दस लाख चक्कर। दूसरा, यह लंबा है: केवल एक गुणा के लिए चार लाइनें, और मालकिन की सूची में पाँच और कॉलम हैं। और तीसरा, यह एक ऐसे तरीके से नाजुक (fragile) है जिसे पकड़ना आसान नहीं होता। लूप स्थितियों को 0, 1, 2, … के रूप में गिनता है और फिर .loc से उन्हें लेबल के रूप में खोजता है। यह केवल तभी काम करता है जब स्थिति और लेबल संयोग से एक जैसे हों। लेकिन यदि आप टेबल को पहले फ़िल्टर कर लें — जैसे अध्याय पाँच में केवल उत्तरी शाखा (north) के ऑर्डर्स को अलग किया गया था — तो वे अब एक जैसे नहीं रहते:

python
north = orders[orders["branch"] == "north"]
print(north.index.tolist())

revenues = []
for i in range(len(north)):
    revenues.append(north.loc[i, "quantity"] * north.loc[i, "price"])
text
[0, 2, 4, 7]
KeyError: 1

north की पंक्तियाँ अपने मूल लेबल 0, 2, 4, 7 बनाए रखती हैं। लूप लेबल 1 माँगता है, जो कि दक्षिण (south) शाखा का ऑर्डर है और north में नहीं है, इसलिए प्रोग्राम क्रैश हो जाता है। इस गड़बड़ी का बदतर रूप वह होता है जब प्रोग्राम क्रैश भी नहीं होता — बल्कि चुपचाप ग़लत पंक्ति का डेटा पढ़कर आगे बढ़ जाता है।

पांडास में इस बारे में सोचने का तरीका बिल्कुल अलग है: आप एक बार में एक पंक्ति की गणना नहीं करते; आप पूरे कॉलम के लिए एक ही बार में गणना करते हैं। यह अध्याय इसी एक विचार पर आधारित है, और एक बार जब यह बात समझ आ जाती है, तो दुकान मालकिन द्वारा माँगा गया हर कॉलम सिर्फ़ एक लाइन का कोड बन जाता है।

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

  • बिना किसी लूप के पूरे कॉलमों पर अंकगणित (arithmetic) चलाकर नया कॉलम बनाना, और यह समझाना कि लूप की आवश्यकता क्यों नहीं है
  • जब आप किसी कॉलम में Series असाइन करते हैं तो पांडास इंडेक्स के आधार पर वैल्यूज़ को कैसे संरेखित (align) करता है यह समझना, और मिसअलाइनमेंट से होने वाले NaN से बचना
  • assign() का उपयोग करके मूल टेबल को बदले बिना नए कॉलम जोड़ना
  • np.where और np.select के साथ प्रति पंक्ति दो या दो से अधिक विकल्पों में से सही वैल्यू चुनना
  • .map() से डिक्शनरी देखकर वैल्यूज़ खोजना, pd.cut से संख्याओं को श्रेणियों में बाँटना, और .str व .dt से टेक्स्ट व तारीखों के टुकड़े अलग करना
  • groupby(...).transform(...) का उपयोग करके प्रत्येक पंक्ति की उसके ग्रुप से तुलना करना
  • यह पहचानना कि apply(axis=1) का उपयोग कब उचित है, और यह समझना कि इसे अंतिम विकल्प क्यों माना जाता है
  • यह समझाना कि फ़िल्टर की गई टेबल में नए कॉलम जोड़ने के संदर्भ में पांडास 3 का Copy-on-Write क्या भूमिका निभाता है

ज़रूरी शर्तें: समूहीकरण और एकत्रीकरण (grouping and aggregation)।


पहले फ़ाइल बनाएँ

इस अध्याय का प्रत्येक उदाहरण orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0

कुछ उदाहरण NumPy का भी उपयोग करते हैं, जो पांडास के साथ ही इंस्टॉल होता है — इसके लिए बस import numpy as np लिखना पर्याप्त है।


कोड लिखने से पहले की योजना

एक नया कॉलम बनाना एक छोटे प्रोग्राम जैसा होता है। पाँच मिनट की योजना आपको बाद में यह ढूँढने में लगने वाले घंटों से बचाती है कि NaN कहाँ से आ गया।

1. प्रत्येक कॉलम के सवाल को एक वाक्य में कहें। दुकान की मालकिन द्वारा माँगे गए प्रत्येक कॉलम के लिए अपनी योजना में एक पंक्ति लिखें:

  • revenue — ऑर्डर से कुल कितने पैसे मिले। quantity × price से बनेगा।
  • size — यदि रेवेन्यू कम से कम 500 है तो "big", अन्यथा "small"। revenue से बनेगा।
  • manager — जिस शाखा से ऑर्डर आया उसका मैनेजर कौन है। लुकअप टेबल की मदद से branch से बनेगा।
  • weekday — ऑर्डर किस वार (दिन) को दिया गया था। date से बनेगा।
  • branch_share — शाखा की कुल कमाई में इस ऑर्डर का हिस्सा कितना है। revenue और branch से बनेगा।

ध्यान दें कि इन सब में एक बात समान है: प्रति पंक्ति एक वैल्यू। एक नया कॉलम हमेशा टेबल जितना ही लंबा होना चाहिए। अध्याय आठ से यही मुख्य अंतर है, जहाँ groupby(...).sum() प्रति ग्रुप एक मान देता था। यदि आप जिसकी गणना कर रहे हैं उसमें टेबल की पंक्तियों से कम मान हैं, तो वह अभी कॉलम बनने के योग्य नहीं है।

2. इनपुट को छूने से पहले उसे देखें। आप किन टूल्स का उपयोग कर सकते हैं यह उन शुरुआती कॉलमों के प्रकार (dtypes) पर निर्भर करता है:

python
import pandas as pd

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

print(orders.shape)
print(orders.dtypes)
text
(8, 7)
order_id      int64
date            str
branch          str
product         str
category        str
quantity      int64
price       float64
dtype: object

आठ पंक्तियाँ; अंत में भी यह संख्या आठ ही रहनी चाहिए। quantity का प्रकार int64 है और price का float64, इसलिए उनका गुणा वास्तविक अंकगणित करेगा। date का प्रकार str है — यानी ऐसा टेक्स्ट जो तारीख जैसा दिखता है। टेक्स्ट में कोई वार (weekday) नहीं होता, इसलिए .dt का उपयोग करने से पहले इसे बदलना होगा। इसे अभी ध्यान में रखना आपको बाद में AttributeError से बचाता है।

3. आउटपुट का एक स्केच बनाएँ और एक पंक्ति की हाथ से गणना करें। कुछ भी चलाने से पहले तय करें कि दो पंक्तियाँ क्या बननी चाहिए — एक सामान्य और एक चरम (extreme):

text
product  quantity  price  revenue  size
pen            12   15.0    180.0  small
bag             2  850.0   1700.0  big

12 × 15.0 = 180.0, और 180 का मान 500 से कम है, इसलिए small। हाथ से जाँची गई एक पंक्ति एक कसौटी की तरह होती है: कोड चलने के बाद यदि पंक्ति 0 में 180.0 और small नहीं दिखता, तो कोड ग़लत है, चाहे वह दिखने में कितना भी सही क्यों न लगे।

4. नियम के अनुसार सही टूल चुनें। लगभग हर नया कॉलम इनमें से किसी एक श्रेणी में आता है:

  1. कॉलमों या किसी संख्या पर अंकगणित → पूरे कॉलम पर + - * /। उदाहरण: quantity * price।
  2. शर्त के आधार पर दो में से एक वैल्यू → np.where। उदाहरण: "big" या "small"।
  3. क्रमवार शर्तों के आधार पर कई वैल्यूज़ में से एक → np.select। उदाहरण: "bulk", "normal", "few"।
  4. किसी निश्चित टेबल से ढूँढी गई वैल्यू → .map(dict)। उदाहरण: branch से manager।
  5. कोई संख्या किस रेंज में आती है → pd.cut। उदाहरण: quantity से band।
  6. टेक्स्ट का कोई टुकड़ा → .str मेथड्स। उदाहरण: श्रेणी के पहले कुछ अक्षर।
  7. तारीख का कोई टुकड़ा → पहले pd.to_datetime, फिर .dt। उदाहरण: सप्ताह का दिन।
  8. पंक्ति की उसके अपने ग्रुप से तुलना → groupby(...).transform(...)। उदाहरण: शाखा के कुल योग में ऑर्डर का हिस्सा।
  9. इनमें से कोई नहीं → apply(axis=1)। उदाहरण: कोई ऐसा नियम जो बाकियों के लिए बहुत अनियमित हो।

सूची को ऊपर से पढ़ना शुरू करें और जो पहला टूल फ़िट बैठे वहीं रुक जाएँ। यह क्रम जानबूझकर बनाया गया है: पहले आठ टूल पूरे कॉलम पर एक साथ काम करते हैं; अंतिम टूल वापस प्रति पंक्ति एक पायथन कॉल पर चला जाता है।

5. तय करें कि आप बाद में क्या जाँचेंगे। हर बार तीन जाँचें: पंक्तियों की संख्या नहीं बदली है; नए कॉलम का प्रकार वही है जिसकी आपने उम्मीद की थी; और उस पर isna().sum() शून्य है (या ठीक उतनी मिसिंग वैल्यूज़ हैं जिन्हें आप समझा सकते हैं)। साथ ही हाथ से जाँची गई पंक्ति का मिलान।


पूरे कॉलम पर अंकगणित (Arithmetic)

यहाँ पांडास के तरीके से रेवेन्यू कॉलम तैयार किया गया है:

python
import pandas as pd

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

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

print(orders[["product", "quantity", "price", "revenue"]])
text
product  quantity  price  revenue
0       pen        12   15.0    180.0
1  notebook         5   60.0    300.0
2       bag         2  850.0   1700.0
3    bottle         7  120.0    840.0
4    eraser        30    8.0    240.0
5       bag         1  850.0    850.0
6       pen        20   15.0    300.0
7    bottle         4  120.0    480.0

पंक्ति 0 पर 180.0 दिखाई देता है — वही मान जो हमने हाथ से निकाला था। कोई लूप नहीं, कोई इंडेक्स की परेशानी नहीं।

यह कैसे काम करता है। orders["quantity"] आठ संख्याओं की एक Series है; वैसे ही orders["price"] भी है। जब आप दो Series के बीच * लगाते हैं, तो पांडास उनकी वैल्यूज़ को इंडेक्स लेबल के अनुसार जोड़ता है — लेबल 0 को लेबल 0 से, लेबल 1 को लेबल 1 से — और प्रत्येक जोड़े को गुणा करता है। लूप अभी भी चलता है, लेकिन पांडास के संकलित (compiled) कोड के अंदर, पायथन में नहीं। इसीलिए यह तेज़ है, और इसीलिए यह कभी ग़लत पंक्ति नहीं पढ़ सकता: क्योंकि यहाँ कोई मैन्युअल काउंटर नहीं होता जो तालमेल खो दे।

फिर orders["revenue"] = ... कॉलम नाम के आधार पर दो में से एक काम करता है:

  • यदि revenue मौजूद नहीं है, तो यह दाईं ओर एक नया कॉलम जोड़ता है;
  • यदि यह पहले से मौजूद है, तो यह उसे चुपचाप बदल देता है — बिना किसी "क्या आप सुनिश्चित हैं?" चेतावनी के।

परिणाम का प्रकार अंकगणित का अनुसरण करता है: int64 × float64 से float64 मिलता है, इसीलिए revenue में 180 के बजाय 180.0 दिखता है।

कॉलम को किसी एक संख्या के साथ भी जोड़ा जा सकता है। वह संख्या प्रत्येक पंक्ति के लिए उपयोग की जाती है — पांडास इसे ब्रॉडकास्टिंग (broadcasting) कहता है:

python
orders["price_with_vat"] = (orders["price"] * 1.15).round(2)

print(orders[["product", "price", "price_with_vat"]].head(4))
text
product  price  price_with_vat
0       pen   15.0           17.25
1  notebook   60.0           69.00
2       bag  850.0          977.50
3    bottle  120.0          138.00

कोष्ठक महत्वपूर्ण हैं: .round(2) गुणा के पूरे परिणाम पर लागू होता है, जो कि फिर से एक Series है। यहाँ प्रत्येक चरण एक कॉलम लेता है और एक कॉलम लौटाता है, इसलिए आप आसानी से चेनिंग जारी रख सकते हैं।


इंडेक्स तय करता है कि कौन सी वैल्यू कहाँ जाएगी

लेबल के अनुसार वैल्यूज़ का मिलान केवल अंकगणित के लिए नहीं होता। किसी कॉलम में Series को असाइन करते समय भी मान लेबल के आधार पर ही बैठते हैं। अधिकांश समय आप इस पर ध्यान नहीं देते, क्योंकि नए मान उसी टेबल से आते हैं और लेबल बिल्कुल मेल खाते हैं। लेकिन जिस दिन वे मेल नहीं खाते, उस दिन यह साफ़ दिखाई देता है।

मान लीजिए आप एक ऐसा कॉलम चाहते हैं जिसमें केवल उत्तरी शाखा (north) के ऑर्डर्स की मात्रा हो:

python
north = orders[orders["branch"] == "north"]

orders["north_qty"] = north["quantity"]

print(orders[["branch", "quantity", "north_qty"]])
text
branch  quantity  north_qty
0  north        12       12.0
1  south         5        NaN
2  north         2        2.0
3   east         7        NaN
4  north        30       30.0
5  south         1        NaN
6   east        20        NaN
7  north         4        4.0

north में लेबल 0, 2, 4, 7 मौजूद हैं। पांडास ने प्रत्येक मान को उसी लेबल वाली पंक्ति के सामने रखा, और बाकी सभी पंक्तियों को NaN मिला — जिसका अर्थ है "इस लेबल के लिए कोई मान नहीं"। और चूँकि NaN फ़्लोट होता है, इसलिए कॉलम का प्रकार float64 बन गया, भले ही मात्राएँ पूर्ण संख्याएँ थीं। यह ठीक वही हो सकता है जो आप चाहते थे, या फिर अपने ही बनाए मिसिंग डेटा की एक लंबी तलाश की शुरुआत। इसका नियम हमेशा याद रखें: असाइनमेंट पोज़ीशन से नहीं, लेबल्स के मिलान से होता है।

यही नियम आपकी सुरक्षा भी करता है। सॉर्टिंग (अध्याय सात) किसी Series के क्रम को बदल देती है लेकिन प्रत्येक मान को उसके लेबल के साथ बनाए रखती है, इसलिए सॉर्ट की गई Series को वापस असाइन करने पर प्रत्येक मान अपनी मूल पंक्ति में ही जाता है:

python
by_revenue = orders["revenue"].sort_values()
print(by_revenue.head(3))

orders["revenue_copy"] = by_revenue
print(orders[["product", "revenue", "revenue_copy"]].head(3))
print((orders["revenue_copy"] == orders["revenue"]).all())
text
0    180.0
4    240.0
6    300.0
Name: revenue, dtype: float64
    product  revenue  revenue_copy
0       pen    180.0         180.0
1  notebook    300.0         300.0
2       bag   1700.0        1700.0
True

सॉर्ट की गई Series 0, 4, 6 लेबल्स के क्रम में चलती है — दूसरा मान, 240.0, पंक्ति 4 का है, पंक्ति 1 का नहीं। फिर भी revenue_copy पंक्ति-दर-पंक्ति revenue के बिल्कुल समान है, जिसकी पुष्टि अंतिम पंक्ति सभी आठ पंक्तियों के लिए करती है। पांडास ने लेबल्स का उपयोग किया और सॉर्टिंग के क्रम को नज़रअंदाज़ कर दिया। यदि यह पोज़ीशन से चलता, तो पंक्ति 1 (नोटबुक, 300.0) को 240.0 मिल जाता।

साधारण पायथन लिस्ट में कोई लेबल नहीं होता, इसलिए उसका उपयोग पोज़ीशन के अनुसार किया जाता है — और तब उसकी लंबाई का पूरी तरह मेल खाना अनिवार्य होता है:

python
try:
    orders["rating"] = [5, 4, 3]
except ValueError as error:
    print("ValueError:", error)
text
ValueError: Length of values (3) does not match length of index (8)

आठ पंक्तियों के लिए तीन वैल्यूज़ को अंदाज़े से बैठाने के बजाय सीधे अस्वीकार कर दिया जाता है। यह सबसे सुरक्षित व्यवहार है।

अब हमें इन प्रायोगिक कॉलमों की आवश्यकता नहीं है। drop(columns=...) इनके बिना टेबल लौटाता है:

python
orders = orders.drop(columns=["north_qty", "revenue_copy", "price_with_vat"])
print(list(orders.columns))
text
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'revenue']

assign(): मूल टेबल को बदले बिना नए कॉलम जोड़ना

orders["revenue"] = ... स्वयं orders को बदल देता है। अक्सर आप यही चाहते हैं। लेकिन कभी-कभी आप ऐसा नहीं चाहते — आप कुछ कॉलम आज़माना चाहते हैं, या कोई रिपोर्ट तैयार करना चाहते हैं, और मूल टेबल को बिल्कुल वैसा ही रखना चाहते हैं जैसा वह पढ़ा गया था। assign() अतिरिक्त कॉलमों के साथ एक नया DataFrame लौटाता है और मूल टेबल को अछूता छोड़ देता है:

python
import numpy as np
import pandas as pd

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

report = orders.assign(
    revenue=orders["quantity"] * orders["price"],
    vat=lambda d: (d["revenue"] * 0.15).round(2),
)

print(report[["product", "revenue", "vat"]].head(3))
print("revenue" in orders.columns)
text
product  revenue    vat
0       pen    180.0   27.0
1  notebook    300.0   45.0
2       bag   1700.0  255.0
False

तीन बातों पर ध्यान दें। (शीर्ष पर import numpy as np अगले भाग के लिए है।)

तर्कों (arguments) के नाम कॉलम के नाम बन जाते हैं — revenue=... से revenue नामक कॉलम बनता है। इसीलिए assign() का उपयोग करते समय कॉलम के नाम मान्य पायथन पहचानकर्ता होने चाहिए।

vat को एक lambda के रूप में लिखा गया है। ऐसा होना ज़रूरी है: vat को revenue की आवश्यकता है, और revenue मूल orders में मौजूद नहीं है — यह केवल बनाई जा रही नई टेबल पर मौजूद है। lambda d: ... उस नई टेबल को d के रूप में प्राप्त करता है, इसलिए d["revenue"] वहाँ पहले से मौजूद रहता है। एक ही assign() के अंदर कॉलम ऊपर से नीचे जोड़े जाते हैं, और प्रत्येक lambda अपने ऊपर के कॉलमों को देख सकता है।

और अंतिम पंक्ति False प्रिंट करती है: orders में कोई revenue कॉलम नहीं जुड़ा है। आपकी जानकारी के बिना कुछ भी नहीं बदला गया।


np.where: दो में से एक वैल्यू चुनना

"यदि रेवेन्यू कम से कम 500 है तो big, अन्यथा small" — यह प्रत्येक पंक्ति के लिए एक if/else शर्त है। यहाँ साधारण if लिखने का अर्थ वापस लूप पर जाना होगा, क्योंकि एक if को केवल एक True या False की आवश्यकता होती है, जबकि पूरे कॉलम की तुलना आठ मान देती है (अध्याय पाँच की "truth value of a Series is ambiguous" त्रुटि)।

NumPy का where एक ऐसा if/else है जो पूरे कॉलम पर काम करता है: जहाँ (where) शर्त True है, पहला मान लें; अन्य जगहों पर दूसरा।

python
report["size"] = np.where(report["revenue"] >= 500, "big", "small")

print(report[["product", "revenue", "size"]])
text
product  revenue   size
0       pen    180.0  small
1  notebook    300.0  small
2       bag   1700.0    big
3    bottle    840.0    big
4    eraser    240.0  small
5       bag    850.0    big
6       pen    300.0  small
7    bottle    480.0  small

पंक्ति 0 पर small है, पंक्ति 2 पर big — दोनों स्केच से मेल खाते हैं। शर्त उसी प्रकार का बूलियन मास्क है जो आपने अध्याय पाँच में बनाया था; np.where बस प्रत्येक True/False को एक मान में बदल देता है। सीमा की जाँच करना हमेशा उपयोगी होता है: >= 500 का अर्थ है कि ठीक 500 का ऑर्डर big होगा। इसे सोच-समझकर तय करें।

दो से अधिक वैल्यूज़: np.select

मात्रा के आधार पर तीन श्रेणियाँ — 20 या अधिक "bulk" है, 5 या अधिक "normal" है, और बाकी सब "few" है। np.select शर्तों की एक सूची और संबंधित मानों की एक सूची लेता है, तथा किसी भी शर्त से मेल न खाने वाली पंक्तियों के लिए एक default मान लेता है:

python
conditions = [
    report["quantity"] >= 20,
    report["quantity"] >= 5,
]
labels = ["bulk", "normal"]

report["tier"] = np.select(conditions, labels, default="few")

print(report[["product", "quantity", "tier"]])
text
product  quantity    tier
0       pen        12  normal
1  notebook         5  normal
2       bag         2     few
3    bottle         7  normal
4    eraser        30    bulk
5       bag         1     few
6       pen        20    bulk
7    bottle         4     few

जो पहली शर्त True होती है, वही चुनी जाती है। इसीलिए सबसे सख्त शर्त पहले आती है। क्रम बदलें और देखें कि इरेज़र के 30 मात्रा वाले ऑर्डर के साथ क्या होता है:

python
wrong = np.select(
    [report["quantity"] >= 5, report["quantity"] >= 20],
    ["normal", "bulk"],
    default="few",
)
print(wrong)
text
['normal' 'normal' 'few' 'normal' 'normal' 'few' 'normal' 'few']

प्रत्येक ऑर्डर normal या few बन गया; कुछ भी bulk नहीं बना, क्योंकि 30 और 20 भी >= 5 हैं और उस शर्त की जाँच पहले की गई थी। कोई एरर नहीं आया, बस एक ग़लत कॉलम बन गया — इसीलिए योजना में हाथ से जाँची जाने वाली पंक्ति में एक चरम मान शामिल होना चाहिए।


.map(): डिक्शनरी से वैल्यूज़ खोजना (Lookup)

मैनेजर का नाम किसी गणना से नहीं निकलता। यह एक पूर्व-निर्धारित तथ्य है जो आप जानते हैं: आशा उत्तरी शाखा चलाती हैं, उमर दक्षिणी शाखा, लीना पूर्वी शाखा। जब नियम "खोजने" का हो, तो लुकअप टेबल को एक डिक्शनरी के रूप में लिखें और कॉलम को .map() से जोड़ें:

python
manager_of = {
    "north": "Asha",
    "south": "Omar",
    "east": "Lina",
}

report["manager"] = report["branch"].map(manager_of)

print(report[["branch", "manager"]])
print(report["manager"].isna().sum())
text
branch manager
0  north    Asha
1  south    Omar
2  north    Asha
3   east    Lina
4  north    Asha
5  south    Omar
6   east    Lina
7  north    Asha
0

प्रत्येक शाखा को डिक्शनरी में उसके मान — यानी मैनेजर के नाम — से बदल दिया गया। दूसरा प्रिंट योजना से की गई जाँच है, और यह देखने से कहीं अधिक महत्वपूर्ण है, क्योंकि .map() उस कुंजी के साथ क्या करता है जो उसे नहीं मिलती। कल्पना कीजिए कि डिक्शनरी पूर्वी (east) शाखा खुलने से पहले लिखी गई थी:

python
old_managers = {"north": "Asha", "south": "Omar"}

print(report["branch"].map(old_managers).isna().sum())
text
2

कोई एरर नहीं आया। दो पूर्वी ऑर्डर्स को बस NaN मिल गया। आठ पंक्तियों पर आप इसे पकड़ लेंगे; लेकिन अस्सी हज़ार पंक्तियों पर यह नज़र नहीं आएगा, और बाद का groupby("manager") उन ऑर्डर्स को चुपचाप छोड़ देगा (अध्याय आठ: NaN कुंजियाँ छोड़ दी जाती हैं)। प्रत्येक .map() के तुरंत बाद isna().sum() की जाँच अवश्य करें।


pd.cut: कोई संख्या किस रेंज में आती है

दुकान चाहती है कि प्रत्येक ऑर्डर मात्रा के अनुसार बैंड (श्रेणी) में बँट जाए: 5 आइटम तक small, 6 से 15 तक medium, और 15 से अधिक large। आप इसे np.select से भी लिख सकते हैं, लेकिन "कौन सी रेंज" के लिए अपना एक विशिष्ट टूल है। pd.cut सीमाओं के किनारे (edges) और प्रत्येक श्रेणी के लिए एक लेबल लेता है:

python
report["band"] = pd.cut(
    report["quantity"],
    bins=[0, 5, 15, np.inf],
    labels=["small", "medium", "large"],
)

print(report[["product", "quantity", "band"]])
text
product  quantity    band
0       pen        12  medium
1  notebook         5   small
2       bag         2   small
3    bottle         7  medium
4    eraser        30   large
5       bag         1   small
6       pen        20   large
7    bottle         4   small

चार किनारे तीन श्रेणियाँ बनाते हैं: (0, 5], (5, 15] और (15, ∞]। गोल कोष्ठक का अर्थ है "शामिल नहीं", और चौकोर कोष्ठक का अर्थ है "शामिल" — यानी प्रत्येक श्रेणी अपने दाहिने किनारे को शामिल करती है। इसीलिए नोटबुक का ठीक 5 का ऑर्डर small है, medium नहीं। np.inf अनंत (infinity) है, एक ऐसा ऊपरी किनारा जिससे कोई मात्रा अधिक नहीं हो सकती।

परिणाम का प्रकार कुछ ऐसा है जो आपने अभी तक नहीं देखा है:

python
print(report["band"].dtype)
print(report["band"].cat.categories.tolist())
text
category
['small', 'medium', 'large']

एक category कॉलम याद रखता है कि small < medium < large। इसलिए band के अनुसार सॉर्ट करने पर वर्णमाला क्रम के बजाय यह क्रम मिलता है — जो आमतौर पर रिपोर्ट के लिए आवश्यक होता है।


टेक्स्ट और तारीखें: .str और .dt

टेक्स्ट के टुकड़े

अध्याय पाँच में फ़िल्टर करने के लिए .str.contains का उपयोग किया गया था। वही .str एक्सेसर्स कॉलम भी बनाते हैं: पायथन से आप जो भी स्ट्रिंग मेथड जानते हैं, वह एक साथ सभी मानों पर लागू हो जाता है। मान लीजिए प्रत्येक ऑर्डर को STA-1001 जैसे छोटे कोड की आवश्यकता है — श्रेणी के पहले तीन अक्षर बड़े अक्षरों में, एक डैश, और फिर ऑर्डर नंबर:

python
report["code"] = (
    report["category"].str[:3].str.upper()
    + "-"
    + report["order_id"].astype(str)
)

print(report[["category", "order_id", "code"]].head(3))
text
category  order_id      code
0   stationery      1001  STA-1001
1   stationery      1002  STA-1002
2  accessories      1003  ACC-1003

.str[:3] प्रत्येक मान को स्लाइस करता है, .str.upper() प्रत्येक मान को बड़े अक्षरों में बदलता है, और टेक्स्ट कॉलमों के बीच + उन्हें पंक्ति-दर-पंक्ति जोड़ता है। order_id एक संख्या है, और संख्या को टेक्स्ट में नहीं जोड़ा जा सकता, इसलिए .astype(str) इसे पहले टेक्स्ट में बदलता है।

तारीख के टुकड़े

योजना में पहले ही संकेत दिया गया था कि date का प्रकार str है। इसे pd.to_datetime से बदलें, और .dt परिदृश्यों के द्वार खुल जाते हैं:

python
report["date"] = pd.to_datetime(report["date"])
print(report["date"].dtype)

report["weekday"] = report["date"].dt.day_name()

print(report[["date", "weekday"]].head(4))
text
datetime64[us]
        date   weekday
0 2024-03-01    Friday
1 2024-03-01    Friday
2 2024-03-02  Saturday
3 2024-03-02  Saturday

कॉलम अब datetime64 है — एक वास्तविक तारीख, न कि मात्र टेक्स्ट। (यहाँ [us] वह प्रिसिजन है जिसे पांडास 3 ने चुना है; आप इसे अनदेखा कर सकते हैं।) एक वास्तविक तारीख से, .dt आपको .dt.day_name(), .dt.month, .dt.year, .dt.day और बहुत कुछ देता है। और यह रूपांतरण एक बार किया जाता है, तथा वापस date में असाइन किया जाता है, ताकि बाद का हर चरण तारीख पाए, न कि स्ट्रिंग।


transform: प्रत्येक पंक्ति की उसके ग्रुप से तुलना करना

अंतिम अनुरोध — अपनी शाखा के रेवेन्यू में प्रत्येक ऑर्डर का हिस्सा — एक ही पंक्ति में दो चीज़ें चाहता है: उस ऑर्डर का रेवेन्यू, और उसकी शाखा का कुल योग। अध्याय आठ ने हमें कुल योग निकालना सिखाया था:

python
print(report.groupby("branch")["revenue"].sum())
text
branch
east     1140.0
north    2600.0
south    1150.0
Name: revenue, dtype: float64

लेकिन वे प्रति शाखा केवल तीन मान हैं, और हमारी टेबल में आठ पंक्तियाँ हैं। आप आठ संख्याओं को तीन से विभाजित नहीं कर सकते। आपको जो चाहिए वह है शाखा का कुल योग उस शाखा की प्रत्येक पंक्ति पर दोहराया जाए। ठीक यही काम transform करता है:

python
branch_total = report.groupby("branch")["revenue"].transform("sum")
print(branch_total)
text
0    2600.0
1    1150.0
2    2600.0
3    1140.0
4    2600.0
5    1150.0
6    1140.0
7    2600.0
Name: revenue, dtype: float64

वही ग्रुपिंग, वही "sum", लेकिन परिणाम में टेबल के अपने लेबल्स के साथ आठ मान हैं — उत्तरी शाखा की प्रत्येक पंक्ति 2600 रखती है, और दक्षिणी शाखा की प्रत्येक पंक्ति 1150। अब यह साधारण कॉलम अंकगणित बन गया:

python
report["branch_share"] = (report["revenue"] / branch_total).round(3)

print(report[["branch", "product", "revenue", "branch_share"]])
text
branch   product  revenue  branch_share
0  north       pen    180.0         0.069
1  south  notebook    300.0         0.261
2  north       bag   1700.0         0.654
3   east    bottle    840.0         0.737
4  north    eraser    240.0         0.092
5  south       bag    850.0         0.739
6   east       pen    300.0         0.263
7  north    bottle    480.0         0.185

इसकी जाँच करें: उत्तरी शाखा में बैग का ऑर्डर 1700 / 2600 = 0.654 है। और प्रत्येक शाखा के अंदर के हिस्सों का योग 1 होना चाहिए:

python
print(report.groupby("branch")["branch_share"].sum())
text
branch
east     1.0
north    1.0
south    1.0
Name: branch_share, dtype: float64

याद रखने योग्य नियम: agg/sum एक समूह को सिकोड़कर एक पंक्ति बना देते हैं; जबकि transform मूल टेबल की प्रत्येक पंक्ति के लिए एक मान लौटाता है। सारांश टेबल के लिए पहले का उपयोग करें, और नए कॉलम के लिए दूसरे का।


apply(axis=1): सबसे अंतिम विकल्प

कभी-कभी नियम वास्तव में अनियमित होते हैं। डिलीवरी शुल्क का नियम कुछ ऐसा है: उत्तरी शाखा में — जो मुख्य शाखा है और गोदाम के बगल में है — यदि रेवेन्यू कम से कम 500 है तो मुफ़्त, अन्यथा 60; अन्य शाखाओं में 120, लेकिन 1000 से ऊपर मुफ़्त। आप इसे एक सामान्य पायथन फ़ंक्शन के रूप में लिख सकते हैं और प्रत्येक पंक्ति पर apply कर सकते हैं:

python
def delivery_fee(row):
    if row["branch"] == "north":
        return 0 if row["revenue"] >= 500 else 60
    return 0 if row["revenue"] > 1000 else 120


report["fee"] = report.apply(delivery_fee, axis=1)

print(report[["branch", "revenue", "fee"]])
text
branch  revenue  fee
0  north    180.0   60
1  south    300.0  120
2  north   1700.0    0
3   east    840.0  120
4  north    240.0   60
5  south    850.0  120
6   east    300.0  120
7  north    480.0   60

axis=1 का अर्थ है "फ़ंक्शन को एक बार में एक पंक्ति दें"। अंदर, row एक Series है जिसके लेबल कॉलम के नाम होते हैं, इसलिए row["branch"] ठीक काम करता है।

इसे पढ़ना बहुत आसान है, और यही इसका मुख्य आकर्षण है। लेकिन इसकी कीमत यह है कि यह अध्याय की शुरुआत वाले लूप जैसा ही धीमा है: पांडास प्रत्येक पंक्ति के लिए एक Series बनाता है और प्रति पंक्ति एक बार आपके पायथन फ़ंक्शन को कॉल करता है। आठ पंक्तियों पर यह कुछ भी नहीं है; लेकिन दस लाख पंक्तियों पर यह पलक झपकने और कॉफ़ी ब्रेक के बीच का अंतर बन जाता है।

इसलिए apply का उपयोग करने से पहले, नियम को पूरे कॉलम वाले टूल्स से व्यक्त करने का प्रयास करें। यह नियम वास्तव में "क्रमवार जाँची जाने वाली तीन स्थितियाँ" है — यानी np.select:

python
in_north = report["branch"] == "north"

fee_fast = np.select(
    [in_north & (report["revenue"] >= 500), in_north, report["revenue"] > 1000],
    [0, 60, 0],
    default=120,
)

print((fee_fast == report["fee"]).all())
text
True

बिल्कुल वही परिणाम, बिना प्रति पंक्ति पायथन कॉल के। apply(axis=1) को केवल उन्हीं नियमों के लिए बचाकर रखें जिन्हें वास्तव में इस तरह नहीं लिखा जा सकता — जैसे किसी बाहरी फ़ंक्शन को कॉल करना, या कई चरणों के साथ किसी जटिल स्ट्रिंग को पार्स करना — और इसका उपयोग करने से पहले एक बार फिर सोचने की आदत डालें।


फ़िल्टर की गई टेबल में कॉलम जोड़ना: Copy-on-Write

अक्सर आप पहले डेटा को फ़िल्टर करेंगे और फिर परिणाम में एक नया कॉलम जोड़ेंगे। पांडास 3 में यह बहुत सीधा है, और यह सीधा क्यों है यह समझना ज़रूरी है क्योंकि पुराने ट्यूटोरियल कुछ और कहते हैं।

python
import pandas as pd

orders = pd.read_csv("orders.csv")
orders["revenue"] = orders["quantity"] * orders["price"]

north = orders[orders["branch"] == "north"]
north["revenue_k"] = north["revenue"] / 1000

print(north[["product", "revenue", "revenue_k"]])
print(list(orders.columns))
text
product  revenue  revenue_k
0     pen    180.0       0.18
2     bag   1700.0       1.70
4  eraser    240.0       0.24
7  bottle    480.0       0.48
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'revenue']

कोई चेतावनी नहीं आई, और orders में revenue_k कॉलम भी नहीं जुड़ा। पांडास 3 Copy-on-Write का उपयोग करता है: फ़िल्टरिंग या चयन का प्रत्येक परिणाम अपनी स्वतंत्र टेबल की तरह व्यवहार करता है। north को बदलने से orders कभी नहीं बदल सकता।

यदि आप पुराना कोड या ऑनलाइन उत्तर पढ़ते हैं, तो आप इस पैटर्न के ठीक बाद एक SettingWithCopyWarning ("A value is trying to be set on a copy of a slice from a DataFrame") देखेंगे, और orders[...].copy() लिखने की सलाह दी जाएगी। वह चेतावनी इसलिए मौजूद थी क्योंकि पुराना पांडास कभी-कभी दोनों टेबल्स के बीच मेमोरी साझा करता था, और कोई भी आसानी से यह नहीं बता सकता था कि कब ऐसा हुआ। पांडास 3 ने उस अनिश्चितता को दूर कर दिया, और उसके साथ ही चेतावनी को भी। अब .copy() लिखना अनावश्यक है, हालाँकि इससे कोई नुकसान नहीं होता।

लेकिन Copy-on-Write जिसकी अनुमति नहीं देता, वह है दो चरणों की चेन के माध्यम से orders को बदलना। यदि आप कुछ पंक्तियों के लिए मूल टेबल में मान बदलना चाहते हैं, तो यह तरीका कुछ नहीं करता:

python
orders[orders["branch"] == "north"]["revenue"] = 0

print(orders["revenue"].tolist())
text
[180.0, 300.0, 1700.0, 840.0, 240.0, 850.0, 300.0, 480.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment 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).

Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.

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

पहला ब्रैकेट एक नई टेबल बनाता है, दूसरा ब्रैकेट उस नई टेबल में मान सेट करता है, और वह नई टेबल तुरंत नष्ट हो जाती है। पांडास आपको ChainedAssignmentError के साथ चेतावनी देता है, और सूची दिखाती है कि revenue अपरिवर्तित रहा — उत्तर के ऑर्डर्स अभी भी 180.0, 1700.0, 240.0 और 480.0 हैं। (चेतावनियाँ एरर स्ट्रीम में जाती हैं, इसलिए आपके टर्मिनल में चेतावनी सूची के नीचे के बजाय ऊपर दिखाई दे सकती है।) मूल टेबल को बदलने के लिए, इसे .loc[rows, column] के साथ एक ही चरण में करें:

python
orders.loc[orders["branch"] == "north", "note"] = "main branch"

print(orders[["branch", "note"]])
text
branch         note
0  north  main branch
1  south          NaN
2  north  main branch
3   east          NaN
4  north  main branch
5  south          NaN
6   east          NaN
7  north  main branch

जो पंक्तियाँ मेल खाती हैं उन्हें मान मिल जाता है; यदि आवश्यक हो तो कॉलम बन जाता है; और जो पंक्तियाँ मेल नहीं खातीं उन्हें NaN मिलता है। एक चरण, मूल टेबल, कोई चेतावनी नहीं।


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

योजना का पालन करते हुए enrich.py दुकान मालकिन के पूरे संदेश का उत्तर देता है: पढ़ना, जाँचना, कॉलम जोड़ना, और फिर से जाँचना।

python
import numpy as np
import pandas as pd

MANAGER_OF = {
    "north": "Asha",
    "south": "Omar",
    "east": "Lina",
}

orders = pd.read_csv("orders.csv")
rows_before = len(orders)

# 1. Conversions first, so later steps get the right types.
orders["date"] = pd.to_datetime(orders["date"])

# 2. New columns, one rule each.
orders["revenue"] = orders["quantity"] * orders["price"]
orders["size"] = np.where(orders["revenue"] >= 500, "big", "small")
orders["manager"] = orders["branch"].map(MANAGER_OF)
orders["weekday"] = orders["date"].dt.day_name()
branch_total = orders.groupby("branch")["revenue"].transform("sum")
orders["branch_share"] = (orders["revenue"] / branch_total).round(3)

# 3. Checks: same rows, nothing missing, the hand-worked row is right.
new_columns = ["revenue", "size", "manager", "weekday", "branch_share"]
assert len(orders) == rows_before
assert orders[new_columns].isna().sum().sum() == 0
assert orders.loc[0, "revenue"] == 180.0

print(orders[["order_id", "branch", "manager", "weekday", "revenue", "size", "branch_share"]])
print()
print(orders[new_columns].dtypes)
text
order_id branch manager   weekday  revenue   size  branch_share
0      1001  north    Asha    Friday    180.0  small         0.069
1      1002  south    Omar    Friday    300.0  small         0.261
2      1003  north    Asha  Saturday   1700.0    big         0.654
3      1004   east    Lina  Saturday    840.0    big         0.737
4      1005  north    Asha    Sunday    240.0  small         0.092
5      1006  south    Omar    Sunday    850.0    big         0.739
6      1007   east    Lina    Monday    300.0  small         0.263
7      1008  north    Asha    Monday    480.0  small         0.185

revenue         float64
size                str
manager             str
weekday             str
branch_share    float64
dtype: object

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

  • रूपांतरण सबसे पहले आता है। किसी भी चीज़ द्वारा उपयोग किए जाने से पहले date को एक वास्तविक तारीख में बदल दिया गया है, ताकि बाद में .dt विफल न हो सके और किसी को भी इसे बदलने की बात याद रखने की आवश्यकता न पड़े।
  • प्रति पंक्ति एक नियम। प्रत्येक पंक्ति योजना की एक पंक्ति का उत्तर देती है, इसलिए जब कोई संख्या ग़लत दिखती है तो आप जानते हैं कि किस अकेली पंक्ति ने उसे बनाया है।
  • लुकअप टेबल शीर्ष पर एक नामित स्थिरांक (constant) है। जब दुकान एक पश्चिमी शाखा खोलेगी, तो परिवर्तन एक स्पष्ट स्थान पर सिर्फ़ एक पंक्ति का होगा, और यदि कोई भूल जाता है तो isna जाँच स्पष्ट रूप से प्रोग्राम को रोक देगी।
  • जाँचें assert हैं, प्रिंट नहीं। यदि पंक्तियों की संख्या बदलती है, .map() किसी शाखा को छोड़ देता है, या पंक्ति 0 का मान 180 नहीं रहता है, तो प्रोग्राम एक ग़लत रिपोर्ट छापने के बजाय तुरंत रुक जाता है।
  • अंत में dtypes प्रिंट किया गया है क्योंकि प्रकार उत्तर का एक हिस्सा है: revenue एक फ़्लोट होना चाहिए, branch_share एक फ़्लोट, और टेक्स्ट कॉलम str।

जब यह काम न करे

KeyError: 'Price' दाईं ओर के कॉलम नाम की वर्तनी ग़लत है या केस (capitalization) ग़लत है। list(orders.columns) प्रिंट करें और वहाँ से नाम कॉपी करें। बाईं ओर भी ध्यान दें: orders["revenu"] = ... लिखने पर कोई एरर नहीं आता — यह आपके इच्छित कॉलम के बगल में एक नया, ग़लत नाम वाला कॉलम बना देता है। यदि कोई कॉलम जिसे आपने "अपडेट" किया था नहीं बदला है, तो टाइपो वाले जुड़वाँ कॉलम की तलाश करें।

ValueError: Length of values (3) does not match length of index (8) आपने एक ऐसी लिस्ट (या ऐरे) असाइन की है जिसकी लंबाई टेबल की पंक्तियों की संख्या से मेल नहीं खाती। लिस्ट में कोई लेबल नहीं होता, इसलिए पांडास उसे मिला नहीं पाता और अस्वीकार कर देता है। या तो टेबल के अपने कॉलमों से वैल्यूज़ बनाएँ, या पहले len(values) == len(df) की पुष्टि करें।

नया कॉलम कुछ पंक्तियों में NaN से भर गया है, और प्रकार float64 में बदल गया है आपने एक ऐसी Series असाइन की है जिसके लेबल्स टेबल से केवल आंशिक रूप से मेल खाते हैं — आमतौर पर फ़िल्टर की गई टेबल से बनी सीरीज़। असाइनमेंट पोज़ीशन से नहीं बल्कि लेबल्स से मेल खाता है। किसी सबसेट के बजाय पूरी टेबल से नया कॉलम बनाएँ (जैसे np.where या .loc[mask, "col"] = ... के साथ)।

revenue में 15.015.015.0… जैसी चीज़ें दिख रही हैं, या TypeError: can't multiply sequence by non-int of type 'float' कॉलमों में से एक टेक्स्ट है, संख्या नहीं। एक int को स्ट्रिंग से गुणा करने पर स्ट्रिंग बार-बार दोहराई जाती है — पायथन का नियम 3 * "ab" == "ababab" — और पांडास इसे बिना किसी शिकायत के पंक्ति-दर-पंक्ति लागू कर देता है:

python
import pandas as pd
from io import StringIO

RAW = """product,quantity,price
pen,12,15.0
bag,2,"1,200"
"""
bad = pd.read_csv(StringIO(RAW))

print(bad.dtypes)
print(bad["quantity"] * bad["price"])
text
product       str
quantity    int64
price         str
dtype: object
0    15.015.015.015.015.015.015.015.015.015.015.015.0
1                                          1,2001,200
dtype: str

"1,200" में हज़ार वाले कॉमा ने पूरे price कॉलम को str बना दिया। इसीलिए योजना कहती है कि अंकगणित से पहले dtypes को देखें। इसे पढ़ते समय ही ठीक करें, thousands="," के साथ, या pd.to_numeric से बदलें:

python
good = pd.read_csv(StringIO(RAW), thousands=",")

print(good.dtypes["price"])
print(good["quantity"] * good["price"])
text
float64
0     180.0
1    2400.0
dtype: float64

AttributeError: Can only use .dt accessor with datetimelike values कॉलम अभी भी टेक्स्ट है। इसे पहले df["date"] = pd.to_datetime(df["date"]) से बदलें और उसके बाद .dt का उपयोग करें।

apply से KeyError: 'quantity' आप axis=1 लिखना भूल गए। इसके बिना, apply आपके फ़ंक्शन को पूरे कॉलम पास करता है, इसलिए row["quantity"] कॉलम के अंदर quantity नाम का पंक्ति लेबल ढूँढता है और विफल हो जाता है। axis=1 जोड़ें — और फिर विचार करें कि क्या आपको वास्तव में apply की आवश्यकता है।

IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer आपने NaN युक्त कॉलम पर .astype(int) कॉल किया है। एक NumPy पूर्णांक मिसिंग वैल्यूज़ को संग्रहीत नहीं कर सकता। पहले खाली स्थान भरें (अध्याय छह), या पांडास के नलेबल पूर्णांक प्रकार, .astype("Int64") (बड़ा I) का उपयोग करें, जो मिसिंग मानों को संभाल सकता है।

ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. आपने लिखा df[mask]["col"] = value। Copy-on-Write के तहत पहला चरण एक अलग टेबल बनाता है, इसलिए मूल टेबल कभी नहीं बदलती। इसे एक ही चरण में लिखें: df.loc[mask, "col"] = value।

.map() वाले कॉलम में वहाँ NaN है जहाँ आप वैल्यू की उम्मीद कर रहे थे कॉलम की कोई कुंजी डिक्शनरी में मौजूद नहीं है — कोई नई शाखा, वर्तनी में अंतर, या अंत में कोई अतिरिक्त स्पेस। उन्हें df.loc[df["new"].isna(), "key_column"].unique() से ढूँढें, फिर डिक्शनरी या डेटा को ठीक करें।