नए कॉलम जोड़ना — फ़ाइल में जो संख्याएँ नहीं हैं उन्हें तैयार करना
मौजूदा कॉलमों से नए कॉलम बनाना: पूरे कॉलम पर अंकगणित और इंडेक्स का संरेखण, assign, np.where और np.select, map, pd.cut, .str व .dt, ग्रुप से तुलना के लिए transform, और अंतिम विकल्प के रूप में apply।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
दुकान की मालकिन का एक संदेश आता है: "प्रत्येक ऑर्डर के लिए, मैं देखना चाहती हूँ कि उससे कितने पैसे मिले, क्या वह एक बड़ा ऑर्डर था, कौन सा मैनेजर उसके लिए जिम्मेदार है, और वह सप्ताह का कौन सा दिन था। और प्रत्येक ऑर्डर के लिए, वह उसकी संबंधित शाखा की कुल कमाई का कितना हिस्सा है?"
आप orders.csv खोलते हैं। इसमें quantity (मात्रा) और price (कीमत), branch (तीन दुकानों में से कौन सी) और date (तारीख) मौजूद हैं। मालकिन ने जो कुछ माँगा, उनमें से एक भी चीज़ फ़ाइल में सीधे तौर पर मौजूद नहीं है। उनमें से प्रत्येक जानकारी को पहले से मौजूद कॉलमों से तैयार करना होगा।
यदि आप सामान्य पायथन से आए हैं, तो आपके हाथ पहले से ही जानते हैं कि क्या लिखना है — पंक्तियों पर एक लूप चलाना:
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"]])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) के ऑर्डर्स को अलग किया गया था — तो वे अब एक जैसे नहीं रहते:
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"])[0, 2, 4, 7]
KeyError: 1north की पंक्तियाँ अपने मूल लेबल 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 फ़ाइल को पढ़ता है, वही फ़ाइल जो अध्याय चार से इस्तेमाल हो रही है। यदि आपके पास यह नहीं है, तो अपने प्रोजेक्ट फ़ोल्डर में ठीक इन पंक्तियों के साथ यह फ़ाइल बनाएँ:
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) पर निर्भर करता है:
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(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):
product quantity price revenue size
pen 12 15.0 180.0 small
bag 2 850.0 1700.0 big12 × 15.0 = 180.0, और 180 का मान 500 से कम है, इसलिए small। हाथ से जाँची गई एक पंक्ति एक कसौटी की तरह होती है: कोड चलने के बाद यदि पंक्ति 0 में 180.0 और small नहीं दिखता, तो कोड ग़लत है, चाहे वह दिखने में कितना भी सही क्यों न लगे।
4. नियम के अनुसार सही टूल चुनें। लगभग हर नया कॉलम इनमें से किसी एक श्रेणी में आता है:
- कॉलमों या किसी संख्या पर अंकगणित → पूरे कॉलम पर
+ - * /। उदाहरण:quantity * price। - शर्त के आधार पर दो में से एक वैल्यू →
np.where। उदाहरण:"big"या"small"। - क्रमवार शर्तों के आधार पर कई वैल्यूज़ में से एक →
np.select। उदाहरण:"bulk","normal","few"। - किसी निश्चित टेबल से ढूँढी गई वैल्यू →
.map(dict)। उदाहरण: branch से manager। - कोई संख्या किस रेंज में आती है →
pd.cut। उदाहरण: quantity से band। - टेक्स्ट का कोई टुकड़ा →
.strमेथड्स। उदाहरण: श्रेणी के पहले कुछ अक्षर। - तारीख का कोई टुकड़ा → पहले
pd.to_datetime, फिर.dt। उदाहरण: सप्ताह का दिन। - पंक्ति की उसके अपने ग्रुप से तुलना →
groupby(...).transform(...)। उदाहरण: शाखा के कुल योग में ऑर्डर का हिस्सा। - इनमें से कोई नहीं →
apply(axis=1)। उदाहरण: कोई ऐसा नियम जो बाकियों के लिए बहुत अनियमित हो।
सूची को ऊपर से पढ़ना शुरू करें और जो पहला टूल फ़िट बैठे वहीं रुक जाएँ। यह क्रम जानबूझकर बनाया गया है: पहले आठ टूल पूरे कॉलम पर एक साथ काम करते हैं; अंतिम टूल वापस प्रति पंक्ति एक पायथन कॉल पर चला जाता है।
5. तय करें कि आप बाद में क्या जाँचेंगे। हर बार तीन जाँचें: पंक्तियों की संख्या नहीं बदली है; नए कॉलम का प्रकार वही है जिसकी आपने उम्मीद की थी; और उस पर isna().sum() शून्य है (या ठीक उतनी मिसिंग वैल्यूज़ हैं जिन्हें आप समझा सकते हैं)। साथ ही हाथ से जाँची गई पंक्ति का मिलान।
पूरे कॉलम पर अंकगणित (Arithmetic)
यहाँ पांडास के तरीके से रेवेन्यू कॉलम तैयार किया गया है:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders["revenue"] = orders["quantity"] * orders["price"]
print(orders[["product", "quantity", "price", "revenue"]])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) कहता है:
orders["price_with_vat"] = (orders["price"] * 1.15).round(2)
print(orders[["product", "price", "price_with_vat"]].head(4))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) के ऑर्डर्स की मात्रा हो:
north = orders[orders["branch"] == "north"]
orders["north_qty"] = north["quantity"]
print(orders[["branch", "quantity", "north_qty"]])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.0north में लेबल 0, 2, 4, 7 मौजूद हैं। पांडास ने प्रत्येक मान को उसी लेबल वाली पंक्ति के सामने रखा, और बाकी सभी पंक्तियों को NaN मिला — जिसका अर्थ है "इस लेबल के लिए कोई मान नहीं"। और चूँकि NaN फ़्लोट होता है, इसलिए कॉलम का प्रकार float64 बन गया, भले ही मात्राएँ पूर्ण संख्याएँ थीं। यह ठीक वही हो सकता है जो आप चाहते थे, या फिर अपने ही बनाए मिसिंग डेटा की एक लंबी तलाश की शुरुआत। इसका नियम हमेशा याद रखें: असाइनमेंट पोज़ीशन से नहीं, लेबल्स के मिलान से होता है।
यही नियम आपकी सुरक्षा भी करता है। सॉर्टिंग (अध्याय सात) किसी Series के क्रम को बदल देती है लेकिन प्रत्येक मान को उसके लेबल के साथ बनाए रखती है, इसलिए सॉर्ट की गई Series को वापस असाइन करने पर प्रत्येक मान अपनी मूल पंक्ति में ही जाता है:
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())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 मिल जाता।
साधारण पायथन लिस्ट में कोई लेबल नहीं होता, इसलिए उसका उपयोग पोज़ीशन के अनुसार किया जाता है — और तब उसकी लंबाई का पूरी तरह मेल खाना अनिवार्य होता है:
try:
orders["rating"] = [5, 4, 3]
except ValueError as error:
print("ValueError:", error)ValueError: Length of values (3) does not match length of index (8)आठ पंक्तियों के लिए तीन वैल्यूज़ को अंदाज़े से बैठाने के बजाय सीधे अस्वीकार कर दिया जाता है। यह सबसे सुरक्षित व्यवहार है।
अब हमें इन प्रायोगिक कॉलमों की आवश्यकता नहीं है। drop(columns=...) इनके बिना टेबल लौटाता है:
orders = orders.drop(columns=["north_qty", "revenue_copy", "price_with_vat"])
print(list(orders.columns))['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'revenue']assign(): मूल टेबल को बदले बिना नए कॉलम जोड़ना
orders["revenue"] = ... स्वयं orders को बदल देता है। अक्सर आप यही चाहते हैं। लेकिन कभी-कभी आप ऐसा नहीं चाहते — आप कुछ कॉलम आज़माना चाहते हैं, या कोई रिपोर्ट तैयार करना चाहते हैं, और मूल टेबल को बिल्कुल वैसा ही रखना चाहते हैं जैसा वह पढ़ा गया था। assign() अतिरिक्त कॉलमों के साथ एक नया DataFrame लौटाता है और मूल टेबल को अछूता छोड़ देता है:
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)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 है, पहला मान लें; अन्य जगहों पर दूसरा।
report["size"] = np.where(report["revenue"] >= 500, "big", "small")
print(report[["product", "revenue", "size"]])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 मान लेता है:
conditions = [
report["quantity"] >= 20,
report["quantity"] >= 5,
]
labels = ["bulk", "normal"]
report["tier"] = np.select(conditions, labels, default="few")
print(report[["product", "quantity", "tier"]])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 मात्रा वाले ऑर्डर के साथ क्या होता है:
wrong = np.select(
[report["quantity"] >= 5, report["quantity"] >= 20],
["normal", "bulk"],
default="few",
)
print(wrong)['normal' 'normal' 'few' 'normal' 'normal' 'few' 'normal' 'few']प्रत्येक ऑर्डर normal या few बन गया; कुछ भी bulk नहीं बना, क्योंकि 30 और 20 भी >= 5 हैं और उस शर्त की जाँच पहले की गई थी। कोई एरर नहीं आया, बस एक ग़लत कॉलम बन गया — इसीलिए योजना में हाथ से जाँची जाने वाली पंक्ति में एक चरम मान शामिल होना चाहिए।
.map(): डिक्शनरी से वैल्यूज़ खोजना (Lookup)
मैनेजर का नाम किसी गणना से नहीं निकलता। यह एक पूर्व-निर्धारित तथ्य है जो आप जानते हैं: आशा उत्तरी शाखा चलाती हैं, उमर दक्षिणी शाखा, लीना पूर्वी शाखा। जब नियम "खोजने" का हो, तो लुकअप टेबल को एक डिक्शनरी के रूप में लिखें और कॉलम को .map() से जोड़ें:
manager_of = {
"north": "Asha",
"south": "Omar",
"east": "Lina",
}
report["manager"] = report["branch"].map(manager_of)
print(report[["branch", "manager"]])
print(report["manager"].isna().sum())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) शाखा खुलने से पहले लिखी गई थी:
old_managers = {"north": "Asha", "south": "Omar"}
print(report["branch"].map(old_managers).isna().sum())2कोई एरर नहीं आया। दो पूर्वी ऑर्डर्स को बस NaN मिल गया। आठ पंक्तियों पर आप इसे पकड़ लेंगे; लेकिन अस्सी हज़ार पंक्तियों पर यह नज़र नहीं आएगा, और बाद का groupby("manager") उन ऑर्डर्स को चुपचाप छोड़ देगा (अध्याय आठ: NaN कुंजियाँ छोड़ दी जाती हैं)। प्रत्येक .map() के तुरंत बाद isna().sum() की जाँच अवश्य करें।
pd.cut: कोई संख्या किस रेंज में आती है
दुकान चाहती है कि प्रत्येक ऑर्डर मात्रा के अनुसार बैंड (श्रेणी) में बँट जाए: 5 आइटम तक small, 6 से 15 तक medium, और 15 से अधिक large। आप इसे np.select से भी लिख सकते हैं, लेकिन "कौन सी रेंज" के लिए अपना एक विशिष्ट टूल है। pd.cut सीमाओं के किनारे (edges) और प्रत्येक श्रेणी के लिए एक लेबल लेता है:
report["band"] = pd.cut(
report["quantity"],
bins=[0, 5, 15, np.inf],
labels=["small", "medium", "large"],
)
print(report[["product", "quantity", "band"]])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) है, एक ऐसा ऊपरी किनारा जिससे कोई मात्रा अधिक नहीं हो सकती।
परिणाम का प्रकार कुछ ऐसा है जो आपने अभी तक नहीं देखा है:
print(report["band"].dtype)
print(report["band"].cat.categories.tolist())category
['small', 'medium', 'large']एक category कॉलम याद रखता है कि small < medium < large। इसलिए band के अनुसार सॉर्ट करने पर वर्णमाला क्रम के बजाय यह क्रम मिलता है — जो आमतौर पर रिपोर्ट के लिए आवश्यक होता है।
टेक्स्ट और तारीखें: .str और .dt
टेक्स्ट के टुकड़े
अध्याय पाँच में फ़िल्टर करने के लिए .str.contains का उपयोग किया गया था। वही .str एक्सेसर्स कॉलम भी बनाते हैं: पायथन से आप जो भी स्ट्रिंग मेथड जानते हैं, वह एक साथ सभी मानों पर लागू हो जाता है। मान लीजिए प्रत्येक ऑर्डर को STA-1001 जैसे छोटे कोड की आवश्यकता है — श्रेणी के पहले तीन अक्षर बड़े अक्षरों में, एक डैश, और फिर ऑर्डर नंबर:
report["code"] = (
report["category"].str[:3].str.upper()
+ "-"
+ report["order_id"].astype(str)
)
print(report[["category", "order_id", "code"]].head(3))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 परिदृश्यों के द्वार खुल जाते हैं:
report["date"] = pd.to_datetime(report["date"])
print(report["date"].dtype)
report["weekday"] = report["date"].dt.day_name()
print(report[["date", "weekday"]].head(4))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: प्रत्येक पंक्ति की उसके ग्रुप से तुलना करना
अंतिम अनुरोध — अपनी शाखा के रेवेन्यू में प्रत्येक ऑर्डर का हिस्सा — एक ही पंक्ति में दो चीज़ें चाहता है: उस ऑर्डर का रेवेन्यू, और उसकी शाखा का कुल योग। अध्याय आठ ने हमें कुल योग निकालना सिखाया था:
print(report.groupby("branch")["revenue"].sum())branch
east 1140.0
north 2600.0
south 1150.0
Name: revenue, dtype: float64लेकिन वे प्रति शाखा केवल तीन मान हैं, और हमारी टेबल में आठ पंक्तियाँ हैं। आप आठ संख्याओं को तीन से विभाजित नहीं कर सकते। आपको जो चाहिए वह है शाखा का कुल योग उस शाखा की प्रत्येक पंक्ति पर दोहराया जाए। ठीक यही काम transform करता है:
branch_total = report.groupby("branch")["revenue"].transform("sum")
print(branch_total)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। अब यह साधारण कॉलम अंकगणित बन गया:
report["branch_share"] = (report["revenue"] / branch_total).round(3)
print(report[["branch", "product", "revenue", "branch_share"]])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 होना चाहिए:
print(report.groupby("branch")["branch_share"].sum())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 कर सकते हैं:
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"]])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 60axis=1 का अर्थ है "फ़ंक्शन को एक बार में एक पंक्ति दें"। अंदर, row एक Series है जिसके लेबल कॉलम के नाम होते हैं, इसलिए row["branch"] ठीक काम करता है।
इसे पढ़ना बहुत आसान है, और यही इसका मुख्य आकर्षण है। लेकिन इसकी कीमत यह है कि यह अध्याय की शुरुआत वाले लूप जैसा ही धीमा है: पांडास प्रत्येक पंक्ति के लिए एक Series बनाता है और प्रति पंक्ति एक बार आपके पायथन फ़ंक्शन को कॉल करता है। आठ पंक्तियों पर यह कुछ भी नहीं है; लेकिन दस लाख पंक्तियों पर यह पलक झपकने और कॉफ़ी ब्रेक के बीच का अंतर बन जाता है।
इसलिए apply का उपयोग करने से पहले, नियम को पूरे कॉलम वाले टूल्स से व्यक्त करने का प्रयास करें। यह नियम वास्तव में "क्रमवार जाँची जाने वाली तीन स्थितियाँ" है — यानी np.select:
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())Trueबिल्कुल वही परिणाम, बिना प्रति पंक्ति पायथन कॉल के। apply(axis=1) को केवल उन्हीं नियमों के लिए बचाकर रखें जिन्हें वास्तव में इस तरह नहीं लिखा जा सकता — जैसे किसी बाहरी फ़ंक्शन को कॉल करना, या कई चरणों के साथ किसी जटिल स्ट्रिंग को पार्स करना — और इसका उपयोग करने से पहले एक बार फिर सोचने की आदत डालें।
फ़िल्टर की गई टेबल में कॉलम जोड़ना: Copy-on-Write
अक्सर आप पहले डेटा को फ़िल्टर करेंगे और फिर परिणाम में एक नया कॉलम जोड़ेंगे। पांडास 3 में यह बहुत सीधा है, और यह सीधा क्यों है यह समझना ज़रूरी है क्योंकि पुराने ट्यूटोरियल कुछ और कहते हैं।
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))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 को बदलना। यदि आप कुछ पंक्तियों के लिए मूल टेबल में मान बदलना चाहते हैं, तो यह तरीका कुछ नहीं करता:
orders[orders["branch"] == "north"]["revenue"] = 0
print(orders["revenue"].tolist())[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] के साथ एक ही चरण में करें:
orders.loc[orders["branch"] == "north", "note"] = "main branch"
print(orders[["branch", "note"]])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 दुकान मालकिन के पूरे संदेश का उत्तर देता है: पढ़ना, जाँचना, कॉलम जोड़ना, और फिर से जाँचना।
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)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" — और पांडास इसे बिना किसी शिकायत के पंक्ति-दर-पंक्ति लागू कर देता है:
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"])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 से बदलें:
good = pd.read_csv(StringIO(RAW), thousands=",")
print(good.dtypes["price"])
print(good["quantity"] * good["price"])float64
0 180.0
1 2400.0
dtype: float64AttributeError: 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() से ढूँढें, फिर डिक्शनरी या डेटा को ठीक करें।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
नॉर्थ ब्रांच के ऑर्डर्स से बनाया गया एक कॉलम पूरी टेबल में असाइन किया जाता है। क्या प्रिंट होगा?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """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
"""
orders = pd.read_csv(StringIO(RAW))
north = orders[orders["branch"] == "north"]
orders["north_qty"] = north["quantity"]
print(orders["north_qty"].isna().sum(), orders["north_qty"].dtype)- A0 int64
- B4 int64
- C4 float64
- D0 float64
उद्देश्य orders में एक revenue कॉलम जोड़ना है। क्या प्रिंट होगा?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """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
"""
orders = pd.read_csv(StringIO(RAW))
orders.assign(revenue=orders["quantity"] * orders["price"])
print("revenue" in orders.columns)- A`True`
- B`False` — `assign` एक नई टेबल लौटाता है और `orders` को अपरिवर्तित छोड़ देता है, और परिणाम को कहीं सहेजा नहीं गया
- C`KeyError: 'revenue'`
- D`ChainedAssignmentError`
आपको एक ऐसा कॉलम चाहिए जो प्रत्येक पंक्ति में उस ऑर्डर का उसकी शाखा (branch) की कुल मात्रा में हिस्सा (share) रखे। कौन सी लाइन प्रति पंक्ति एक मान देगी?
- Aorders["quantity"] / orders.groupby("branch")["quantity"].sum()
- Borders["quantity"] / orders["quantity"].sum()
- Corders.groupby("branch")["quantity"].sum() / orders["quantity"]
- Dorders["quantity"] / orders.groupby("branch")["quantity"].transform("sum")
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
उसी orders.csv का उपयोग करके enrich_task.py लिखें जो दुकान की मालकिन को डिस्काउंट रिपोर्ट प्रदान करे। पहले योजना बनाएँ — नए कॉलमों की सूची लिखें और पंक्ति 0 की हाथ से गणना करें — फिर इस क्रम में ये कॉलम जोड़ें:
revenue=quantity×pricediscount= जबquantity10 या अधिक हो तोrevenueका 10%, अन्यथा 0net=revenue−discountmanager— इस अध्याय की डिक्शनरी का उपयोग करके शाखा सेband—netसे: 300 तक"low", 300 से अधिक और 800 तक"mid", 800 से अधिक"high"weekday—dateसेcategory_share— अपनी श्रेणी के कुलnetमें प्रत्येक ऑर्डर केnetका हिस्सा, 2 दशमलव स्थानों तक राउंड किया गया
पहले और बाद का shape प्रिंट करें, फिर order_id, product, quantity, net, band, weekday और category_share को net के अनुसार उच्चतम से निम्नतम क्रम में सॉर्ट करके प्रिंट करें।
आपका प्रोग्राम तब सही होगा जब:
- यह नीचे दिया गया सटीक आउटपुट प्रिंट करे।
- कहीं भी कोई
forलूप याapplyन हो — प्रत्येक कॉलम पूरे कॉलम पर एक साथ बनाया गया हो। - यदि डिक्शनरी से कोई शाखा छूट जाती है, तो प्रोग्राम
NaNके साथ आगे बढ़ने के बजायAssertionErrorके साथ रुक जाए। - पहले और बाद में पंक्तियों की संख्या 8 बनी रहे।
अपेक्षित आउटपुट:
before: (8, 7)
after: (8, 14)
order_id product quantity net band weekday category_share
2 1003 bag 2 1700.0 high Saturday 0.44
5 1006 bag 1 850.0 high Sunday 0.22
3 1004 bottle 7 840.0 high Saturday 0.22
7 1008 bottle 4 480.0 mid Monday 0.12
1 1002 notebook 5 300.0 low Friday 0.32
6 1007 pen 20 270.0 low Monday 0.28
4 1005 eraser 30 216.0 low Sunday 0.23
0 1001 pen 12 162.0 low Friday 0.17इसे चलाने से पहले अनुमान लगाएँ: किस ऑर्डर का net सबसे अधिक होगा, और 30 मात्रा वाला इरेज़र ऑर्डर किस बैंड में आएगा? फिर आउटपुट से मिलान करें।
समाधान
योजना। पंक्ति 0 की हाथ से गणना: पेन का ऑर्डर, 12 × 15.0 = 180.0; मात्रा 12 है, इसलिए डिस्काउंट 18.0 और net हुआ 162.0; 162 का मान अधिकतम 300 है, इसलिए low। इरेज़र का ऑर्डर: 30 × 8.0 = 240.0, डिस्काउंट 24.0, net हुआ 216.0 — यह भी low, भले ही यह मात्रा के हिसाब से सबसे बड़ा ऑर्डर है। सबसे अधिक net 2 बैग वाले ऑर्डर का होना चाहिए: 1700.0, बिना किसी डिस्काउंट के।
revenueऔरnet— कॉलम अंकगणित: सीधा गुणा और घटाव।discount—np.where: प्रति पंक्ति दो में से एक मान।manager—.map(dict), जिसके बादisna().sum(): एक लुकअप, जाँचा हुआ।band—pd.cutसीमाओं[0, 300, 800, inf]के साथ: श्रेणियाँ। प्रत्येक श्रेणी अपने दाहिने किनारे को शामिल करती है, इसलिए ठीक 300lowहै और ठीक 800midहै।weekday—pd.to_datetime, फिर.dt.day_name(): तारीख के टुकड़ों के लिए वास्तविक तारीख आवश्यक है।category_share—groupby("category")["net"].transform("sum"): प्रति पंक्ति एक मान, अपने समूह से तुलना।
enrich_task.py:
import numpy as np
import pandas as pd
MANAGER_OF = {
"north": "Asha",
"south": "Omar",
"east": "Lina",
}
orders = pd.read_csv("orders.csv")
print("before:", orders.shape)
orders["revenue"] = orders["quantity"] * orders["price"]
orders["discount"] = np.where(orders["quantity"] >= 10, orders["revenue"] * 0.10, 0.0)
orders["net"] = orders["revenue"] - orders["discount"]
orders["manager"] = orders["branch"].map(MANAGER_OF)
assert orders["manager"].isna().sum() == 0
orders["band"] = pd.cut(
orders["net"],
bins=[0, 300, 800, np.inf],
labels=["low", "mid", "high"],
)
orders["date"] = pd.to_datetime(orders["date"])
orders["weekday"] = orders["date"].dt.day_name()
category_total = orders.groupby("category")["net"].transform("sum")
orders["category_share"] = (orders["net"] / category_total).round(2)
print("after: ", orders.shape)
print()
columns = ["order_id", "product", "quantity", "net", "band", "weekday", "category_share"]
print(orders[columns].sort_values("net", ascending=False))before: (8, 7)
after: (8, 14)
order_id product quantity net band weekday category_share
2 1003 bag 2 1700.0 high Saturday 0.44
5 1006 bag 1 850.0 high Sunday 0.22
3 1004 bottle 7 840.0 high Saturday 0.22
7 1008 bottle 4 480.0 mid Monday 0.12
1 1002 notebook 5 300.0 low Friday 0.32
6 1007 pen 20 270.0 low Monday 0.28
4 1005 eraser 30 216.0 low Sunday 0.23
0 1001 pen 12 162.0 low Friday 0.17योजना के अनुसार पढ़ना:
- Shape 7 कॉलम से 14 हो गया और 8 पंक्तियों पर ही बना रहा: सात नए कॉलम, न कोई पंक्ति बढ़ी न घटी।
- पंक्ति 0 (ऑर्डर 1001) का
net162.0और बैंडlowहै — जो हाथ से जाँचे गए मानों से मेल खाता है। - उच्चतम
netऑर्डर 1003 (बैग) का है,1700.0, जैसा कि अनुमान लगाया गया था। इरेज़र ऑर्डर 1005216.0के साथlowहै: सबसे अधिक आइटम, लेकिन सस्ते। np.whereने अपने "true" मान के रूप में एक कॉलम लिया —orders["revenue"] * 0.10— जिससे प्रत्येक पंक्ति को अपना 10% मिला, कोई एक संख्या नहीं।np.whereकी दोनों शाखाएँ कॉलम या एकल मान हो सकती हैं।- बैंड का किनारा स्पष्ट है। नोटबुक ऑर्डर का
netठीक300.0है और वहlowहै, क्योंकि(0, 300]में 300 शामिल है। यदि दुकान मालकिन का मतलब था "300 और उससे ऊपर mid है", तो किनारों को बदलना होगा — यह एक निर्णय है, कोई तकनीकी छोटी बात नहीं। - प्रत्येक श्रेणी में हिस्सों का योग 1 होता है। Accessories:
0.44 + 0.22 + 0.22 + 0.12 = 1.00। Stationery:0.32 + 0.28 + 0.23 + 0.17 = 1.00। यदि किसी श्रेणी के हिस्सों का योग 1 न होता, तोtransformको ग़लत कॉलम से ग्रुप किया गया होता।
फिर इसे जानबूझकर तोड़ें, एक-एक बार: डिक्शनरी से "east" हटाएँ और देखें कि assert प्रोग्राम को कैसे रोकता है; bins को 0 के बजाय 200 से शुरू करें और देखें कि किन पंक्तियों को band में NaN मिलता है, और क्यों।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz