अध्याय 08

ग्रुपिंग और एग्रीगेशन — हर ग्रुप के लिए एक उत्तर

"प्रति शाखा", "प्रति श्रेणी" और "प्रति दिन" जैसे सवालों को groupby की एक लाइन में बदलना: कुंजी (key), वैल्यू और एग्रीगेशन, named agg के साथ एक साथ कई गणनाएं, size बनाम count, दो कुंजियों से ग्रुपिंग, और औसतों के औसत का जाल।

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

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

दुकान के मालिक ने आपको एक लाइन का संदेश भेजा: "इस सप्ताह हमारी प्रत्येक शाखा में कितने उत्पाद बिके?"

आपके पास orders.csv है — आठ ऑर्डर, प्रत्येक पंक्ति में एक। पिछले चार अध्यायों में आपने जो सीखा है, उससे आप पहले ही इसका उत्तर दे सकते हैं। एक शाखा को फ़िल्टर करें, उसके कॉलम का योग निकालें, और इसे दोहराएँ:

python
import pandas as pd

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

for branch in ["north", "south", "east"]:
    units = orders[orders["branch"] == branch]["quantity"].sum()
    print(branch, units)
text
north 48
south 6
east 27

संख्याएँ सही हैं। लेकिन इस तरीके में तीन समस्याएँ हैं, और जैसे-जैसे डेटा बढ़ता है, प्रत्येक समस्या और गंभीर होती जाती है।

पहला, आपको शाखाओं के नाम पहले से जानने पड़े। आपने सूची खुद हाथ से टाइप की। जब अगले सप्ताह की फ़ाइल में किसी नई west शाखा से कोई ऑर्डर आएगा, तो यह लूप उसे देख ही नहीं पाएगा — कोई एरर नहीं आएगा, यह बस west को छोड़ देगा, और कुल योग अलग-अलग शाखाओं के योग से मेल नहीं खाएगा।

दूसरा, हर नए सवाल के लिए एक नया लूप लिखना होगा। "प्रति श्रेणी राजस्व (revenue)" के लिए हाथ से टाइप की गई दूसरी सूची पर दूसरा लूप चलाना होगा। "प्रति शाखा और प्रति श्रेणी बिक्री" के लिए लूप के अंदर लूप की आवश्यकता होगी।

तीसरा, उत्तर कोई टेबल नहीं है। यह केवल प्रिंट की गई टेक्स्ट की पंक्तियाँ हैं। आप इसे दोबारा सॉर्ट नहीं कर सकते, फ़िल्टर नहीं कर सकते, या इसे फिर से बनाए बिना किसी अन्य उत्तर के पास नहीं रख सकते।

आप वास्तव में जो कहना चाहते हैं वह एक ही लाइन में कहा जा सकता है: पंक्तियों को शाखा के अनुसार विभाजित करें, प्रत्येक टुकड़े के अंदर quantity का योग निकालें, और उन टुकड़ों को एक टेबल के रूप में वापस लौटाएँ। यही groupby है, और पांडास में लगभग हर सारांश इसी तरह लिखा जाता है — एक रिपोर्ट, डैशबोर्ड की संख्या, या "पैसा वास्तव में कहाँ से आ रहा है?" का पहला उत्तर।

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

  • "प्रति शाखा इकाइयाँ" जैसे प्रश्न को उसके तीन भागों में बदलना — कुंजी (key), वैल्यू कॉलम, एग्रीगेशन — और ग्रुपिंग से पहले कुंजी की जाँच करना
  • df.groupby(key)[column].sum() लिखना और वापस मिलने वाली Series को पढ़ना, जहाँ कुंजी उसका इंडेक्स होती है
  • .agg() और named aggregation के साथ एक साथ कई सारांश निकालना
  • size() और count() में अंतर समझना, और यह तय करना कि मिसिंग कुंजियों और मिसिंग वैल्यूज़ के साथ क्या होना चाहिए
  • दो कॉलमों के आधार पर ग्रुपिंग करना, परिणाम को समतल (flatten) या पुनर्गठित (reshape) करना, और "औसतों के औसत" की भूल से बचना

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


वह फ़ाइल जिसका हम उपयोग कर रहे हैं

पिछले अध्यायों वाली वही 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

प्रत्येक पंक्ति एक ऑर्डर है: यह किस शाखा से आया, कौन सा उत्पाद, उसकी श्रेणी, कितनी इकाइयाँ और एक इकाई की कीमत।


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

प्रत्येक ग्रुपिंग प्रश्न का ढांचा एक जैसा ही होता है। groupby टाइप करने से पहले सवाल को टुकड़ों में तोड़ें।

1. सवाल को तीन भागों में बाँटें

ग्रुपिंग के सवाल में हमेशा तीन चीज़ें होती हैं, और सवाल के शब्द ही बताते हैं कि कौन क्या है:

  • कुंजी (Key) — वह कॉलम जिसके मान समूहों को परिभाषित करते हैं। यह per, by या for each (प्रति / के अनुसार) के बाद आने वाला शब्द होता है। "प्रति शाखा इकाइयाँ" में यह branch है।
  • वैल्यू (Value) — वह कॉलम जिसका सारांश निकाला जा रहा है: जिस चीज़ को मापा जा रहा है। यहाँ, quantity।
  • एग्रीगेशन (Aggregation) — कई मान मिलकर एक कैसे बनते हैं: कुल योग, औसत, कितने, अधिकतम। यहाँ, sum।

पैटर्न को पक्का करने के लिए कुछ और उदाहरण:

  • "प्रति श्रेणी राजस्व" — कुंजी category, वैल्यू revenue, एग्रीगेशन sum
  • "प्रत्येक शाखा में औसत ऑर्डर आकार" — कुंजी branch, वैल्यू quantity, एग्रीगेशन mean
  • "प्रत्येक दिन कितने ऑर्डर" — कुंजी date, कोई वैल्यू कॉलम नहीं (पंक्तियाँ गिननी हैं), एग्रीगेशन size
  • "प्रति उत्पाद सबसे बड़ा एकल ऑर्डर" — कुंजी product, वैल्यू quantity, एग्रीगेशन max

यदि आप इन तीनों भागों के नाम नहीं बता सकते, तो सवाल अभी कोड के लिए तैयार नहीं है। "कौन सी शाखा सबसे अच्छी है?" में कोई वैल्यू और कोई एग्रीगेशन नहीं है — इकाइयों के हिसाब से सबसे अच्छी? राजस्व के हिसाब से? या ऑर्डरों की संख्या के हिसाब से? गणना करने से पहले पूछें। तीनों सवालों के जवाब तीन अलग-अलग शाखाएँ हो सकते हैं।

2. ग्रुपिंग करने से पहले इनपुट की जाँच करें

ग्रुपिंग पंक्तियों को छिपा देती है। एक बार जब आठ ऑर्डर तीन योगों में बदल जाते हैं, तो किसी टूटी हुई या अमान्य पंक्ति को योग के अंदर देख पाना असंभव हो जाता है। इसलिए जाँच हमेशा पहले की जाती है:

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 है — संख्यात्मक, इसलिए sum टेक्स्ट को जोड़ने के बजाय गणितीय योग करेगा। कुंजी कॉलम, branch, str है, जो कुंजी के रूप में बिल्कुल सही है।

इसके बाद कुंजी के वास्तविक मानों को देखें। यह वह जाँच है जिसे अक्सर लोग छोड़ देते हैं, जबकि यह सबसे महत्वपूर्ण है:

python
print(orders["branch"].value_counts())
print(orders[["branch", "quantity"]].isna().sum())
text
branch
north    4
south    2
east     2
Name: count, dtype: int64
branch      0
quantity    0
dtype: int64

तीन अलग-अलग शाखाएँ, और किसी भी कॉलम में कुछ भी मिसिंग नहीं है। groupby प्रत्येक अद्वितीय मान के लिए अक्षर-दर-अक्षर एक ग्रुप बनाता है — इसलिए "north" और "north " दो अलग ग्रुप बन जाएंगे। value_counts() वह जगह है जहाँ आप गलत रिपोर्ट बनने से पहले ही इसे पकड़ सकते हैं।

यहाँ देखें कि जब यह जाँच छूट जाती है तो क्या होता है। तीन पंक्तियाँ जो सभी north शाखा के लिए हैं — एक सही टाइप की गई, एक के अंत में स्पेस है, और एक बड़े अक्षर से शुरू होती है:

python
messy = pd.DataFrame({"branch": ["north", "north ", "North"], "quantity": [12, 2, 30]})
print(messy.groupby("branch")["quantity"].sum())

messy["branch"] = messy["branch"].str.strip().str.lower()
print(messy.groupby("branch")["quantity"].sum())
text
branch
North     30
north     12
north      2
Name: quantity, dtype: int64
branch
north    44
Name: quantity, dtype: int64

कोई एरर नहीं आया, और जहाँ एक शाखा होनी चाहिए थी वहाँ तीन "शाखाएँ" बन गईं। पांडास को जो दिया गया था उसके अनुसार प्रत्येक ग्रुप की संख्या गणितीय रूप से सही है, फिर भी रिपोर्ट गलत है। ग्रुपिंग से पहले कुंजी को साफ़ करना — स्पेस हटाना, लोअरकेस करना — उन्हें 44 के वास्तविक योग के साथ एक ही ग्रुप बना देता है।

3. आउटपुट कैसा दिखेगा, इसका पहले ही खाका तैयार करें

तय करें कि उत्तर कैसा दिखना चाहिए, ताकि जब वह वैसा न दिखे तो आप तुरंत पहचान सकें:

text
branch    units
east          ?
north         ?
south         ?

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

4. सही टूल चुनें

  • एक कॉलम से प्रति ग्रुप एक संख्या: df.groupby(key)[col].sum() (या mean, max, ...)
  • एक कॉलम से कई संख्याएँ: df.groupby(key)[col].agg(["sum", "mean"])
  • कई कॉलमों से अपने नामों के साथ कई संख्याएँ: df.groupby(key).agg(name=(col, "sum"), ...)
  • प्रत्येक ग्रुप में कितनी पंक्तियाँ हैं: df.groupby(key).size()
  • एक ऐसा कॉलम जो अभी मौजूद नहीं है, जैसे कि राजस्व: पहले उसे बनाएँ, फिर ग्रुप करें

अब कोड पर आते हैं।


स्प्लिट, अप्लाई, कंबाइन (Split, apply, combine)

groupby क्रम से तीन काम करता है, और इन नामों को जानना ज़रूरी है क्योंकि प्रत्येक ग्रुपिंग समस्या इन्हीं तीन चरणों का संयोजन है:

  1. स्प्लिट (Split) — प्रत्येक अद्वितीय कुंजी मान के आधार पर पंक्तियों को टुकड़ों में बाँटना
  2. अप्लाई (Apply) — प्रत्येक टुकड़े पर अलग से गणना लागू करना
  3. कंबाइन (Combine) — परिणामों को कुंजी द्वारा अनुक्रमित (indexed) करके एक नए ऑब्जेक्ट में जोड़ना

आप स्प्लिट को स्वतंत्र रूप से देख सकते हैं। एक groupby ऑब्जेक्ट पर लूप चलाया जा सकता है, जो आपको प्रत्येक कुंजी और टेबल का उसका संबंधित टुकड़ा देता है:

python
for branch, part in orders.groupby("branch"):
    print(branch, part.shape, list(part["order_id"]))
text
east (2, 7) [1004, 1007]
north (4, 7) [1001, 1003, 1005, 1008]
south (2, 7) [1002, 1006]

प्रत्येक टुकड़ा सभी सात कॉलमों वाला एक सामान्य DataFrame है — north टुकड़े में चार पंक्तियाँ हैं, अन्य दो में दो-दो। किसी को शाखाओं के नाम टाइप नहीं करने पड़े; पांडास ने उन्हें डेटा में ढूँढ लिया। अगले सप्ताह किसी नई west शाखा से आने वाला ऑर्डर बस चौथा टुकड़ा बन जाएगा।

वास्तविक कोड में आपको शायद ही कभी इस तरह लूप चलाने की आवश्यकता होगी। यह यहाँ इसलिए है ताकि मानसिक मॉडल स्पष्ट हो सके: नीचे जो कुछ भी है वह "प्रत्येक टुकड़े पर कुछ करना, फिर उत्तरों को एक साथ जोड़ना" ही है।

प्रति ग्रुप एक संख्या

सवाल "प्रति शाखा इकाइयाँ" अपने तीन भागों में — कुंजी branch, वैल्यू quantity, एग्रीगेशन sum — ठीक इसी क्रम में कोड में लिखा जाता है:

python
units = orders.groupby("branch")["quantity"].sum()
print(units)
text
branch
east     27
north    48
south     6
Name: quantity, dtype: int64

इसे बाएँ से दाएँ पढ़ें: branch के आधार पर ग्रुप करें, quantity लें, उसका योग करें। लूप वाली वही संख्याएँ, एक ही लाइन में, बिना किसी शाखा का नाम हाथ से टाइप किए।

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

python
print(units.sum(), orders["quantity"].sum())
text
81 81

समूहों का योग पूरी तालिका के योग के बराबर है, इसलिए कोई पंक्ति नहीं छूटी और न ही किसी को दो बार गिना गया। यह एक लाइन की जाँच आदत बनाने लायक है: जब भी आप ग्रुप करें, परिणाम के कुल योग की तुलना इनपुट के कुल योग से करें।

परिणाम एक Series है, और कुंजी उसका इंडेक्स है

python
print(type(units))
print(units.index)
print(units["north"])
text
<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48

कंबाइन चरण ने शाखाओं को किसी कॉलम में नहीं, बल्कि इंडेक्स में डाल दिया। यही कारण है कि आउटपुट में मानों के ऊपर एक अलग लाइन पर branch दिखता है — यह इंडेक्स का नाम है। इसका यह भी अर्थ है कि आप शाखा के नाम से मान देख सकते हैं, जैसे units["north"], ठीक वैसे ही जैसे आपने चौथे अध्याय में .loc के साथ किया था।

चूँकि यह एक सामान्य Series है, इसलिए पिछले अध्यायों की सभी चीज़ें इस पर काम करती हैं। इसे सॉर्ट करने से "किस शाखा ने सबसे अधिक बिक्री की" का उत्तर मिलता है:

python
print(units.sort_values(ascending=False))
print(units.idxmax())
text
branch
north    48
east     27
south     6
Name: quantity, dtype: int64
north

idxmax() सबसे बड़े मान का लेबल लौटाता है — शाखा का नाम, संख्या नहीं। "कौन शीर्ष पर है" जानने के लिए आमतौर पर यही चाहिए होता है।

पहले कॉलम चुनना क्यों ज़रूरी है

यह प्रलोभन हो सकता है कि ["quantity"] छोड़ दिया जाए और पांडास को सब कुछ जोड़ने दिया जाए:

python
print(orders.groupby("branch").sum())
text
order_id                                      date  ... quantity  price
branch                                                      ...                
east        2011                      2024-03-022024-03-04  ...       27  135.0
north       4017  2024-03-012024-03-022024-03-032024-03-04  ...       48  993.0
south       2008                      2024-03-012024-03-03  ...        6  910.0

[3 rows x 6 columns]

कोई एरर नहीं आया — और इसमें लगभग हर संख्या निरर्थक है। order_id को जोड़ दिया गया, जैसे ऑर्डर नंबर कोई वित्तीय राशि हों। date स्ट्रिंग्स एक साथ चिपक गईं, क्योंकि टेक्स्ट के लिए "जोड़ने" का मतलब उन्हें जोड़ना (concatenate) होता है। price का भी योग कर दिया गया: north के लिए 993.0 चार अलग-अलग उत्पादों की कीमतों का योग है, जो कुछ भी नहीं दर्शाता।

orders.groupby("branch").sum() कोई एरर नहीं देता। यह आईडी नंबर जोड़ता है, तारीखों को चिपकाता है और इकाई कीमतों का योग करता है, और परिणाम को ऐसे लौटाता है जैसे कि यह एक वैध रिपोर्ट हो।

केवल quantity ही सार्थक था, और आप केवल वही मांग सकते थे। यही कारण है कि नियम है एग्रीगेशन से पहले वैल्यू कॉलम चुनें: sum प्रत्येक डेटा टाइप के लिए वही करता है जो जोड़ का मतलब होता है, और अधिकांश कॉलमों के लिए वह ऐसा सवाल नहीं है जो किसी ने पूछा हो।

mean अधिक सख्त है, और निरर्थक परिणाम देने के बजाय साफ़ मना कर देता है:

python
print(orders.groupby("branch").mean())
text
TypeError: dtype 'str' does not support operation 'mean'

यह एरर उसी गलती को उजागर करता है। इसे ऐसे पढ़ें: आपने मुझसे टेक्स्ट कॉलम का औसत निकालने को कहा। इसका समाधान numeric_only=True नहीं है — वह फिर भी order_id और price का औसत निकालेगा — बल्कि यह बताना है कि आपका मतलब किस कॉलम से है।

एक साथ कई संख्याएँ: .agg()

एक सारांश शायद ही कभी पर्याप्त होता है। "प्रत्येक शाखा ने कितना बेचा, प्रति ऑर्डर औसतन कितना और कितने ऑर्डरों में?" — यह एक ही कॉलम के तीन एग्रीगेशन हैं। उनके नामों की एक लिस्ट .agg() में पास करें:

python
print(orders.groupby("branch")["quantity"].agg(["sum", "mean", "count"]))
text
sum  mean  count
branch                  
east     27  13.5      2
north    48  12.0      4
south     6   3.0      2

अब परिणाम एक DataFrame है — प्रति एग्रीगेशन एक कॉलम, प्रति शाखा एक पंक्ति। यह वह कहानी बताता है जिसे केवल कुल योग ने छिपा दिया था: east ने north की आधी से कुछ अधिक इकाइयाँ बेचीं, लेकिन उसका औसत ऑर्डर बड़ा है। north शाखा को अधिक ऑर्डर मिलते हैं; east को कम, लेकिन थोड़े बड़े ऑर्डर मिलते हैं।

लिस्ट में नाम स्ट्रिंग्स हैं: "sum", "mean", "count", "min", "max", "median", "nunique" (अलग मानों की संख्या), "first", "last"। ये वही मेथड्स हैं जिनका उपयोग आपने किसी एक कॉलम पर किया है, जो अब प्रत्येक टुकड़े पर लागू होते हैं।

Named aggregation: आपके कॉलम, आपके नाम

वास्तविक रिपोर्टें अलग-अलग कॉलमों को अलग-अलग तरीकों से सारांशित करती हैं, और पठनीय कॉलम नाम चाहती हैं। Named aggregation एक ही कॉल में दोनों काम करता है। प्रत्येक कीवर्ड तर्क एक आउटपुट कॉलम का नाम होता है, और उसका मान एक जोड़ा होता है: (इनपुट कॉलम, एग्रीगेशन)।

राजस्व (revenue) रिपोर्ट करने के लिए एक स्वाभाविक चीज़ है, और यह अभी कोई कॉलम नहीं है — यह प्रत्येक पंक्ति पर मात्रा गुणा कीमत है। नए कॉलम बनाने पर अगला अध्याय पूरी तरह केंद्रित है; अभी के लिए एक पंक्ति पर्याप्त है, और यह योजना के चरण 4 को कार्य में दिखाती है: यदि वैल्यू कॉलम मौजूद नहीं है, तो ग्रुपिंग से पहले उसे बनाएँ।

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

summary = orders.groupby("branch").agg(
    orders=("order_id", "count"),
    units=("quantity", "sum"),
    revenue=("revenue", "sum"),
)
print(summary)
text
orders  units  revenue
branch                        
east         2     27   1140.0
north        4     48   2600.0
south        2      6   1150.0

यह एक ऐसी रिपोर्ट है जिसे आप सीधे भेज सकते हैं। agg कॉल की प्रत्येक पंक्ति एक वाक्य की तरह पढ़ी जा सकती है: orders order_id का count है, units quantity का sum है, revenue revenue का sum है। आउटपुट कॉलम के नाम आपके द्वारा चुने गए हैं, न कि डिफ़ॉल्ट sum और count, इसलिए बाद में नाम बदलने की कोई ज़रूरत नहीं है।

और यह पिछले उत्तर को बदल देता है। इकाइयों के हिसाब से east स्पष्ट रूप से दूसरे स्थान पर था; लेकिन राजस्व के मामले में south आगे निकल जाता है — 850 का एक बैग बीस पेनों से भारी पड़ता है। यही कारण है कि चरण 1 ने वैल्यू का नाम तय करने पर ज़ोर दिया था: इकाइयों के हिसाब से "सर्वश्रेष्ठ शाखा" और राजस्व के हिसाब से "सर्वश्रेष्ठ शाखा" दो अलग-अलग सवाल हैं।

परिणाम एक DataFrame है, इसलिए यह उसी की तरह सॉर्ट होता है:

python
print(summary.sort_values("revenue", ascending=False))
text
orders  units  revenue
branch                        
north        4     48   2600.0
south        2      6   1150.0
east         2     27   1140.0

गिनती: size() बनाम count()

"प्रति शाखा कितने ऑर्डर" एक ही सवाल लगता है। लेकिन पांडास के पास दो उत्तर हैं, और वे ठीक उसी समय अलग होते हैं जब डेटा मिसिंग होता है।

  • size() प्रत्येक ग्रुप में पंक्तियों की गिनती करता है, चाहे उनमें कुछ भी हो।
  • count() प्रत्येक ग्रुप में किसी कॉलम के गैर-मिसिंग मानों की गिनती करता है।

साफ़ फ़ाइल पर दोनों सहमत होते हैं। दोनों में अंतर देखने के लिए, यहाँ orders के पास हाथ से बनाई गई एक छोटी टेबल है, जिसमें एक मिसिंग शाखा और एक मिसिंग मात्रा है:

python
gaps = pd.DataFrame(
    {
        "order_id": [1001, 1002, 1003, 1004, 1005],
        "branch": ["north", "south", None, "east", "north"],
        "quantity": [12, 5, 2, None, 30],
    }
)
print(gaps)
text
order_id branch  quantity
0      1001  north      12.0
1      1002  south       5.0
2      1003    NaN       2.0
3      1004   east       NaN
4      1005  north      30.0

(मिसिंग मान के कारण quantity फ्लोट में बदल गई — छठे अध्याय में समझाया गया था कि ऐसा क्यों होता है।)

python
print(gaps.groupby("branch").size())
print(gaps.groupby("branch")["quantity"].count())
text
branch
east     1
north    2
south    1
dtype: int64
branch
east     0
north    2
south    1
Name: quantity, dtype: int64

east में एक ऑर्डर है (size = 1) लेकिन शून्य ज्ञात मात्राएँ हैं (count = 0)। दोनों सही हैं; वे अलग-अलग सवालों के जवाब देते हैं। "कितने ऑर्डर हैं" के लिए size() का उपयोग करें, और "वास्तव में कितनों में मान मौजूद है" के लिए count() का उपयोग करें।

मिसिंग डेटा जो दो काम चुपचाप करता है

उन आउटपुट को फिर से देखें। ऑर्डर 1003, जिसमें कोई शाखा नहीं थी, उनमें से किसी में भी नहीं है। पाँच ऑर्डर अंदर गए थे; चार बाहर आए:

python
print(gaps.groupby("branch").size().sum(), len(gaps))
text
4 5

डिफ़ॉल्ट रूप से groupby उन पंक्तियों को छोड़ देता है जिनकी कुंजी मिसिंग होती है। "unknown" नाम का कोई ग्रुप नहीं बनता, इसलिए वह पंक्ति हर परिणाम से गायब हो जाती है। पहले बताई गई कुल योग जाँच इसे तुरंत पकड़ लेती है। यदि वे पंक्तियाँ महत्वपूर्ण हैं — और बिना शाखा का ऑर्डर भी एक ऑर्डर ही है — तो dropna=False के साथ उन्हें शामिल करें:

python
print(gaps.groupby("branch", dropna=False)["quantity"].sum())
text
branch
east      0.0
north    42.0
south     5.0
NaN       2.0
Name: quantity, dtype: float64

अब चारों समूह मौजूद हैं, और NaN समूह में वे 2 इकाइयाँ हैं जिन्हें किसी शाखा में नहीं रखा जा सका।

दूसरी शांत बात उसी आउटपुट में है: east 0.0 दिखाता है। east शाखा ने शून्य बिक्री नहीं की; उसकी एकमात्र मात्रा अज्ञात है। sum "जोड़ने के लिए कुछ नहीं" को शून्य मानता है। यदि "अज्ञात" को अज्ञात ही रहना चाहिए, तो min_count=1 के साथ ऐसा कहें — "केवल तभी योग निकालें जब कम से कम एक वास्तविक मान मौजूद हो":

python
print(gaps.groupby("branch", dropna=False)["quantity"].sum(min_count=1))
text
branch
east      NaN
north    42.0
south     5.0
NaN       2.0
Name: quantity, dtype: float64

कौन सा सही है यह सवाल पर निर्भर करता है, और यही मुख्य बात है: इसे योजना बनाने के चरण में तय करें, दुर्घटना से नहीं। दोनों समस्याओं का समाधान आमतौर पर पहले ही होता है — ग्रुप करने से पहले मिसिंग मानों को साफ़ करें (अध्याय छह)।

दो कुंजियाँ: समूहों के अंदर समूह

"प्रति शाखा और प्रति श्रेणी राजस्व" में दो कुंजियाँ हैं। एक लिस्ट पास करें:

python
by_both = orders.groupby(["branch", "category"])["revenue"].sum()
print(by_both)
text
branch  category   
east    accessories     840.0
        stationery      300.0
north   accessories    2180.0
        stationery      420.0
south   accessories     850.0
        stationery      300.0
Name: revenue, dtype: float64

डेटा में वास्तव में आने वाले प्रत्येक संयोजन के लिए एक पंक्ति — यहाँ छह। इंडेक्स में अब दो स्तर हैं, branch और category; पांडास इसे MultiIndex कहता है, और पठनीयता बनाए रखने के लिए बाहरी स्तर को प्रति ब्लॉक केवल एक बार प्रिंट करता है। east के नीचे खाली स्थान का अर्थ है "अभी भी east"।

आप इसमें से सामान्य रूप से डेटा चुन सकते हैं:

python
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])
text
category
accessories    2180.0
stationery      420.0
Name: revenue, dtype: float64
420.0

एक लेबल आपको north का पूरा हिस्सा देता है; दोनों लेबलों का एक टपल आपको एक विशिष्ट संख्या देता है।

साधारण कॉलम वापस पाना

MultiIndex संक्षिप्त होता है, लेकिन कई अगले चरणों के लिए — जैसे CSV में सहेजना, मास्क के साथ फ़िल्टर करना, या इसे किसी अन्य टेबल के साथ जोड़ना — सादे कॉलम अधिक सुविधाजनक होते हैं। इसके दो तरीके हैं, और वे एक ही टेबल देते हैं:

python
flat = orders.groupby(["branch", "category"], as_index=False)["revenue"].sum()
print(flat)
print(flat.equals(by_both.reset_index()))
text
branch     category  revenue
0   east  accessories    840.0
1   east   stationery    300.0
2  north  accessories   2180.0
3  north   stationery    420.0
4  south  accessories    850.0
5  south   stationery    300.0
True

as_index=False groupby को पहले स्थान पर कुंजियों को इंडेक्स में न ले जाने के लिए कहता है; reset_index() उन्हें बाद में वापस बाहर निकालता है। जो भी आपके संदर्भ में पढ़ने में बेहतर लगे, उसका उपयोग करें।

पढ़ने में आसान बनाना: unstack()

किसी व्यक्ति के पढ़ने के लिए, एक लंबी सूची की तुलना में ग्रिड अधिक सुविधाजनक होता है — किनारे पर शाखाएँ और शीर्ष पर श्रेणियाँ:

python
print(by_both.unstack())
text
category  accessories  stationery
branch                           
east            840.0       300.0
north          2180.0       420.0
south           850.0       300.0

unstack() आंतरिक इंडेक्स स्तर, category, को कॉलमों में बदल देता है। संख्याएं बिल्कुल वही छह हैं। यदि किसी शाखा ने कभी कोई श्रेणी नहीं बेची होती, तो उसका सेल NaN होता; unstack(fill_value=0) इसके बजाय वहाँ 0 लिखता है।

औसतों का औसत (average of averages) निकालने का जाल

एक आकर्षक सवाल: प्रत्येक शाखा में औसतन एक इकाई की कीमत क्या है? त्वरित उत्तर कीमत कॉलम का औसत निकालना लगता है:

python
print(orders.groupby("branch")["price"].mean())
text
branch
east      67.50
north    248.25
south    455.00
Name: price, dtype: float64

यह ऑर्डर लाइनों पर कीमतों का औसत है, और प्रत्येक लाइन को एक बार गिना जाता है — south शाखा का एकल बैग (850 पर 1 इकाई) उसकी नोटबुक्स (60 पर 5 इकाइयाँ) के बराबर भार रखता है। लेकिन ग्राहकों ने प्रत्येक लाइन में एक इकाई नहीं खरीदी। उन्होंने कई इकाइयाँ खरीदीं। बेची गई प्रति इकाई औसत मूल्य कुल राजस्व को कुल इकाइयों से विभाजित करके निकाला जाता है, और इसकी गणना योगों से की जानी चाहिए:

python
per_unit = orders.groupby("branch").agg(
    units=("quantity", "sum"),
    revenue=("revenue", "sum"),
)
per_unit["avg_unit_price"] = (per_unit["revenue"] / per_unit["units"]).round(2)
print(per_unit)
text
units  revenue  avg_unit_price
branch                                
east       27   1140.0           42.22
north      48   2600.0           54.17
south       6   1150.0          191.67

south का आंकड़ा 455 से गिरकर लगभग 192 पर आ जाता है। अंकगणित के रूप में कोई भी संख्या "गलत" नहीं है; केवल एक ही सवाल का सही जवाब देती है। नियम: जब आप जो चाहते हैं वह एक अनुपात (ratio) है, तो अंश और हर को अलग-अलग एग्रीगेट करें, फिर भाग दें। किसी ऐसी चीज़ का औसत निकालना जो पहले से ही एक औसत है, या ऐसी कीमत जो पहले से ही प्रति-इकाई है, प्रत्येक पंक्ति को समान भार देती है चाहे वह एक इकाई हो या तीस।


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

branch_report.py मालिक के सवाल का सही उत्तर देता है — प्रति शाखा: ऑर्डरों की संख्या, इकाइयाँ, राजस्व, कुल राजस्व में हिस्सेदारी का प्रतिशत, और इकाइयों के हिसाब से सबसे अधिक बिकने वाला उत्पाद — और अपने कुल योगों को स्वयं प्रमाणित करता है।

python
import pandas as pd

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

# 1. Check the input before grouping hides anything.
print("Rows, columns:", orders.shape)
print("Branches:", sorted(orders["branch"].unique()))
print("Missing in key/value:", int(orders[["branch", "quantity", "price"]].isna().sum().sum()))
print()

# 2. Create the value column that does not exist yet.
orders["revenue"] = orders["quantity"] * orders["price"]

# 3. One grouped call, one row per branch.
report = orders.groupby("branch").agg(
    orders=("order_id", "count"),
    units=("quantity", "sum"),
    revenue=("revenue", "sum"),
)
report["share_pct"] = (report["revenue"] / report["revenue"].sum() * 100).round(1)

# 4. Best product per branch: group by both keys, then keep each branch's top row.
by_product = orders.groupby(["branch", "product"], as_index=False)["quantity"].sum()
top = by_product.sort_values("quantity", ascending=False).drop_duplicates("branch")
report["top_product"] = top.set_index("branch")["product"]

report = report.sort_values("revenue", ascending=False)
print(report)
print()

# 5. Prove nothing was lost or double-counted.
print("Revenue check:", report["revenue"].sum(), "==", orders["revenue"].sum())
print("Orders check :", report["orders"].sum(), "==", len(orders))
text
Rows, columns: (8, 7)
Branches: ['east', 'north', 'south']
Missing in key/value: 0

        orders  units  revenue  share_pct top_product
branch                                               
north        4     48   2600.0       53.2      eraser
south        2      6   1150.0       23.5    notebook
east         2     27   1140.0       23.3         pen

Revenue check: 4890.0 == 4890.0
Orders check : 8 == 8

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

  • जाँचें सबसे पहले आती हैं और प्रिंट की जाती हैं। तीन अलग-अलग शाखाएँ और शून्य मिसिंग मान वे स्थितियाँ हैं जिनके तहत रिपोर्ट विश्वसनीय है। यदि अगले सप्ताह की फ़ाइल में चार शाखाएँ या दो मिसिंग मान हैं, तो आप किसी भी योग को पढ़ने से पहले इसे शीर्ष पर देख लेते हैं।
  • revenue ग्रुपिंग से पहले बनाई गई है। प्रति पंक्ति quantity * price का योग करना और फिर ग्रुप करना वास्तविक राजस्व देता है। बाद में कुल मात्रा को औसत कीमत से गुणा करना फिर से औसतों के औसत का जाल बन जाता।
  • एक agg कॉल, नामित कॉलम। प्रत्येक आउटपुट कॉलम को एक बार घोषित किया जाता है, उसी नाम के साथ जिसे पाठक देखेगा। चौथा माप जोड़ना बस एक और पंक्ति का काम है।
  • share_pct की गणना परिणाम पर की जाती है, इनपुट पर नहीं। ग्रुपिंग के बाद, report शाखा द्वारा अनुक्रमित एक सामान्य DataFrame है, इसलिए सामान्य कॉलम अंकगणित इस पर काम करता है। इसके प्रतिशत जुड़कर 100.0 बनते हैं।
  • शीर्ष उत्पाद दो-कुंजी ग्रुपिंग और सॉर्टिंग का उपयोग करता है। groupby(["branch", "product"]) प्रति शाखा-उत्पाद जोड़ी इकाइयाँ देता है; quantity को अवरोही क्रम में सॉर्ट करना और प्रति शाखा पहली पंक्ति रखना (drop_duplicates("branch")) प्रत्येक शाखा के सर्वश्रेष्ठ विक्रेता को छोड़ देता है। top.set_index("branch")["product"] असाइन करने से उत्पाद स्थिति के बजाय शाखा लेबल द्वारा संरेखित होते हैं — इंडेक्स मिलान का ध्यान रखता है।
  • अंतिम दो पंक्तियाँ प्रमाण हैं। समूहों का राजस्व और ऑर्डर गणना इनपुट के बराबर हैं। यदि कोई शाखा छूट गई होती, या फ़ाइल में कोई खाली शाखा होती, तो संख्याएँ भिन्न होतीं और रिपोर्ट बाहर जाने से पहले ही आपको पता चल जाता।

कुछ सामान्य समस्याएँ और समाधान

KeyError: 'Branch' कुंजी कॉलम की स्पेलिंग फ़ाइल से भिन्न है — यहाँ एक बड़ा B है। कॉलम के नाम केस-सेंसिटिव होते हैं, और groupby तुरंत उनकी जाँच करता है। list(orders.columns) प्रिंट करें और नाम को ठीक से कॉपी करें; लिस्ट के कोट्स 'branch ' जैसे अतिरिक्त स्पेस को भी उजागर करते हैं।

KeyError: 'Column not found: Quantity' एक कदम बाद वही गलती: कुंजी ठीक थी लेकिन groupby("branch")["Quantity"] में वैल्यू कॉलम मौजूद नहीं है। संदेश केवल नाम के बजाय "Column not found" कहता है, जिससे पता चलता है कि groupby के बाद का चयन विफल रहा।

KeyError: "Label(s) ['qty'] do not exist" Named aggregation में, प्रत्येक जोड़े का पहला आइटम एक वास्तविक कॉलम होना चाहिए: units=("qty", "sum") विफल हो जाता है क्योंकि कोई qty नहीं है। बाईं ओर का आउटपुट नाम (units) कुछ भी हो सकता है; दाईं ओर का इनपुट टेबल में होना आवश्यक है।

ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. आपने orders.groupby("branch")["quantity", "price"] लिखा। कोष्ठक के एक जोड़े के अंदर दो नाम एक टपल बनाते हैं। कई कॉलम चुनने के लिए आपको एक लिस्ट की आवश्यकता होती है — डबल ब्रैकेट्स, ठीक चौथे अध्याय की तरह: orders.groupby("branch")[["quantity", "price"]].sum()।

TypeError: dtype 'str' does not support operation 'mean' एग्रीगेशन किसी टेक्स्ट कॉलम पर चला — या तो इसलिए कि कोई कॉलम नहीं चुना गया था (groupby("branch").mean()), या इसलिए कि आपके द्वारा चुना गया कॉलम टेक्स्ट के रूप में आया था। दूसरे मामले में, info() वहाँ str दिखाएगा जहाँ आप संख्या की उम्मीद कर रहे थे; अध्याय छह का pd.to_numeric(..., errors="coerce") इसका समाधान है।

परिणाम पढ़ते समय KeyError: 'quantity' units = orders.groupby("branch")["quantity"].sum() के बाद, शाखाएँ units का इंडेक्स हैं और quantity केवल उसका नाम है। units["quantity"] quantity नाम की शाखा की खोज करता है। विशिष्ट शाखा के लिए units["north"] का उपयोग करें, या कुल योग के लिए units.sum() का उपयोग करें।