अध्याय 07

पंक्तियों को क्रमबद्ध करना — और टाई होने पर क्या करें

sort_values से पंक्तियों को क्रम में लगाना: एक या कई कॉलम, प्रत्येक की अपनी दिशा, मिसिंग वैल्यूज़ और संख्याओं जैसा दिखने वाला टेक्स्ट — साथ ही "टॉप N" के लिए nlargest, और कट-ऑफ पर टाई होना क्यों कोई संयोग नहीं बल्कि एक सुविचारित निर्णय होना चाहिए।

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

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

दुकान के मालिक एक संक्षिप्त संदेश भेजते हैं: "इस महीने हमारे सबसे बड़े ऑर्डर्स कौन से थे? मुझे शीर्ष तीन भेजें।"

डेटा वही जाना-पहचाना orders.csv है — दुकान की तीन शाखाओं (north, south और east) से आठ ऑर्डर्स। आठ पंक्तियों के साथ आप केवल देखकर ही उत्तर दे सकते हैं। quantity कॉलम में नीचे देखें, पहले 30 खोजें, फिर 20, फिर 12। हो गया।

अब वास्तविक फ़ाइल की कल्पना करें: चार हज़ार ऑर्डर्स। तीन सबसे बड़ी संख्याएँ खोजने के लिए किसी कॉलम में नीचे की ओर देखना अब कोई समाधान नहीं है, यह महज़ एक उम्मीद है। और मालिक का अगला संदेश पहले ही रास्ते में है: "क्या आप मुझे शाखा के अनुसार समूहीकृत (grouped) सूची भेज सकते हैं, जिसमें प्रत्येक शाखा के भीतर सबसे बड़ा ऑर्डर पहले हो? और कृपया सबसे नए ऑर्डर्स सबसे ऊपर रखें।"

इनमें से प्रत्येक अनुरोध वास्तव में एक ही संक्रिया (operation) है: पंक्तियों को एक उपयोगी क्रम में लगाना, और फिर ऊपर से पढ़ना। यही सॉर्टिंग (sorting) है, और पांडास में यह एक अकेला मेथड है, sort_values()।

लेकिन मेथड तो सबसे आसान हिस्सा है। असली सवाल इसके इर्द-गिर्द होते हैं, और यहीं पर रिपोर्ट्स गलत हो जाती हैं:

  • किसके आधार पर "सबसे बड़ा"? मात्रा (quantity), कीमत (price), या ऑर्डर से प्राप्त कुल आय? प्रत्येक आधार एक अलग शीर्ष तीन देता है।
  • जब दो ऑर्डर्स के मान समान (टाई) हों, तो कौन सा पहले आता है — और क्या हर बार कोड चलाने पर यह उत्तर बदल जाता है?
  • उस ऑर्डर का क्या होता है जिसकी मात्रा रिक्त (blank) है?
  • यदि संख्याओं का एक कॉलम गलती से टेक्स्ट के रूप में पढ़ा गया था, तो बिना किसी चेतावनी के "9" का क्रम "30" के बाद आता है।

यह अध्याय उस मेथड को सिखाता है और, इससे भी महत्वपूर्ण बात यह है कि उन निर्णयों को लेना सिखाता है।

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

  • sort_values() के साथ एक या कई कॉलमों के आधार पर टेबल को सॉर्ट करना, प्रत्येक के लिए उसकी अपनी दिशा तय करना, और परिणाम को खोने के बजाय सहेजना
  • यह समझाना कि इंडेक्स लेबल्स अपनी पंक्तियों के साथ क्यों चलते हैं, और लेबल्स को बनाए रखने, sort_index() और reset_index(drop=True) के बीच सही चुनाव करना
  • जानबूझकर टाई को हल करना, और na_position से यह तय करना कि मिसिंग वैल्यूज़ कहाँ जाएँ
  • head(), nlargest() और nsmallest() के साथ "शीर्ष N" प्रश्नों के उत्तर देना — जिसमें किसी सबसेट का शीर्ष N और कट-ऑफ पर टाई शामिल हैं
  • key= के साथ अक्षरों के केस (छोटे-बड़े अक्षर) की परवाह किए बिना टेक्स्ट सॉर्ट करना, और टेक्स्ट के रूप में संग्रहीत संख्याओं को उनके सॉर्ट होने के तरीके से पहचानना

ज़रूरी शर्तें: मिसिंग डेटा — रिक्त मानों को खोजना, हटाना और भरना।


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

इस अध्याय का प्रत्येक उदाहरण 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

आगे का एक अनुभाग दूसरी फ़ाइल, orders_gaps.csv का उपयोग करता है। यह वही टेबल है जिसमें दो ऑर्डर्स (1002 और 1007) की मात्रा रिक्त छोड़ दी गई है — उसी प्रकार की रिक्तता जिसे अध्याय छह में खोजना सिखाया गया था:

text
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0

कोड लिखने से पहले क्या सोचें

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

1. सवाल को एक वाक्य में लिखें, जिसमें कॉलम का नाम शामिल हो। "सबसे बड़े ऑर्डर्स" ऐसा सवाल नहीं है जिसका उत्तर पांडास सीधे दे सके। लेकिन "सबसे बड़ी quantity वाले तीन ऑर्डर्स" ऐसा सवाल है। इसी तरह "सबसे अधिक price वाले तीन ऑर्डर्स" भी। वे दोनों अलग-अलग उत्तर देते हैं — 850 वाला बैग सबसे महंगा आइटम है, लेकिन केवल दो बैग बेचे गए थे। तय करें कि मालिक का क्या तात्पर्य है। यदि आप नहीं जानते, तो पूछें; इसमें केवल एक संदेश लगता है, जबकि गलत शीर्ष तीन विश्वास खो देता है।

2. सॉर्ट करने से पहले इनपुट की जाँच करें। सॉर्ट के लिए तीन चीज़ें महत्वपूर्ण हैं: पंक्तियों की संख्या, वे डेटा प्रकार (dtypes) जिनके आधार पर आप सॉर्ट करेंगे, और क्या उन कॉलमों में कोई रिक्त मान हैं।

python
import pandas as pd

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

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

आठ पंक्तियाँ, सात कॉलम। quantity का प्रकार int64 है और price का float64, इसलिए वे संख्याओं के रूप में सॉर्ट होंगे। branch और product का प्रकार str है — यानी टेक्स्ट, जो वर्णानुक्रम (alphabetically) में सॉर्ट होगा। date भी str है। यहाँ यह कोई समस्या नहीं है, और इसका कारण जानना उपयोगी है: YYYY-MM-DD के रूप में लिखी गई तिथियाँ टेक्स्ट के रूप में भी सही ढंग से सॉर्ट होती हैं, क्योंकि सबसे महत्वपूर्ण भाग (वर्ष) पहले आता है। 4/3/2024 के रूप में लिखी गई तिथियाँ सही सॉर्ट नहीं होतीं। और quantity में कोई रिक्त मान नहीं है, इसलिए हमें यह तय करने की आवश्यकता नहीं है कि मिसिंग वैल्यूज़ कहाँ जाएँगी — अभी के लिए।

3. अपेक्षित आउटपुट का एक स्केच बनाएं। "मात्रा के आधार पर शीर्ष तीन" के लिए उत्तर तीन पंक्तियाँ होना चाहिए, सबसे बड़ा पहले: 30 (इरेज़र, ऑर्डर 1005), फिर 20 (पेन, 1007), फिर 12 (पेन, 1001)। कुछ भी चलाने से पहले इसे लिख लेने से आपको कोड के परिणाम की जाँच करने के लिए एक आधार मिल जाता है।

4. सॉर्ट के लिए आवश्यक विवरण तय करें।

  • Key (की) — कौन सा कॉलम, या कौन से कॉलम, क्रम तय करेंगे? यह by= बन जाता है।
  • Direction (दिशा) — प्रत्येक की के लिए सबसे छोटा पहले या सबसे बड़ा पहले? यह ascending= बन जाता है।
  • Ties (टाई) — यदि दो पंक्तियों का मान समान है, तो कौन पहले आएगा? यह दूसरा की बन जाता है, या kind="stable"।
  • Blanks (रिक्त मान) — क्या मिसिंग वैल्यूज़ पहले आनी चाहिए या अंत में? यह na_position= बन जाता है।
  • How many (कितनी पंक्तियाँ) — सभी पंक्तियाँ, या केवल शीर्ष N? यह .head(n) या nlargest() बन जाता है।
  • Index (इंडेक्स) — क्या पुराने पंक्ति लेबल्स का बाद में कोई अर्थ है? उन्हें बनाए रखें, या reset_index(drop=True) से हटा दें।

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

  • सभी पंक्तियाँ, एक या अधिक कॉलमों के क्रम में: sort_values()
  • किसी संख्यात्मक कॉलम के आधार पर केवल शीर्ष या न्यूनतम N: nlargest() / nsmallest()
  • पंक्तियों को वापस मूल इंडेक्स क्रम में लाना: sort_index()
  • किसी सबसेट के भीतर शीर्ष N ("north शाखा में"): पहले फ़िल्टर करें, फिर सॉर्ट करें, फिर head()

6. जानें कि आप इसकी जाँच कैसे करेंगे। सॉर्ट के बाद, पहली पंक्तियों को आपके स्केच से मेल खाना चाहिए, shape अपरिवर्तित होना चाहिए (सॉर्ट कभी भी पंक्तियाँ जोड़ता या हटाता नहीं है), और एक संख्यात्मक की के लिए कॉलम का मान पृष्ठ पर ऊपर से नीचे तक एक ही दिशा में बढ़ना या घटना चाहिए।


sort_values() — एक कॉलम

प्रिंटआउट को संक्षिप्त रखने के लिए, उदाहरण सात में से पाँच कॉलम दिखाते हैं। cols केवल कॉलम नामों की एक सूची है, जिसका उपयोग अध्याय चार के डबल-ब्रैकेट चयन के साथ किया गया है।

python
cols = ["order_id", "branch", "product", "quantity", "price"]

by_qty = orders.sort_values("quantity")

print(by_qty[cols])
text
order_id branch   product  quantity  price
5      1006  south       bag         1  850.0
2      1003  north       bag         2  850.0
7      1008  north    bottle         4  120.0
1      1002  south  notebook         5   60.0
3      1004   east    bottle         7  120.0
0      1001  north       pen        12   15.0
6      1007   east       pen        20   15.0
4      1005  north    eraser        30    8.0

डिफ़ॉल्ट दिशा ascending (आरोही) है — सबसे छोटा पहले। quantity कॉलम को ऊपर से नीचे पढ़ें: 1, 2, 4, 5, 7, 12, 20, 30। यही योजना से की गई जाँच है: एक संख्यात्मक की केवल ऊपर की ओर बढ़नी चाहिए।

अब बाएँ किनारे को देखें। इंडेक्स अब 0, 1, 2, … नहीं है। यह 5, 2, 7, 1, 3, 0, 6, 4 दिखा रहा है।

इंडेक्स पंक्तियों के साथ क्यों चलता है

पांडास ने पंक्तियों को पुनः क्रमांकित (renumber) नहीं किया, और यह जानबूझकर किया गया है। एक इंडेक्स लेबल पंक्ति का नाम होता है, उसकी स्थिति नहीं। लेबल 5 सॉर्ट से पहले ऑर्डर 1006 था और सॉर्ट के बाद भी यह ऑर्डर 1006 ही है। यदि सॉर्ट करने से पंक्तियाँ पुनः क्रमांकित हो जातीं, तो प्रत्येक लेबल अचानक एक अलग ऑर्डर की ओर इशारा करने लगता, और "पंक्ति 5" के बारे में आपने जो कुछ भी नोट किया था वह चुपचाप गलत हो जाता।

इसलिए सॉर्ट के बाद "पहली पंक्ति" कहने के दो अलग-अलग तरीके होते हैं, और वे अब एक-दूसरे से सहमत नहीं होते:

python
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])
text
1006
1001

iloc[0] का अर्थ है स्थिति (position) शून्य — पृष्ठ की सबसे पहली पंक्ति, यानी ऑर्डर 1006। और loc[0] का अर्थ है लेबल 0 वाली पंक्ति — यानी ऑर्डर 1001, जो अब पृष्ठ पर छठे स्थान पर है। अध्याय चार में यह अंतर समझाया गया था; सॉर्टिंग वह संक्रिया है जो इसे महत्वपूर्ण बनाती है। जब आप सॉर्ट करने के बाद "शीर्ष पंक्ति" चाहते हैं, तो iloc या head() का उपयोग करें, क्योंकि दोनों ही स्थितियों की गिनती करते हैं।

सबसे बड़ा पहले: ascending=False

python
print(orders.sort_values("quantity", ascending=False)[cols])
text
order_id branch   product  quantity  price
4      1005  north    eraser        30    8.0
6      1007   east       pen        20   15.0
0      1001  north       pen        12   15.0
3      1004   east    bottle         7  120.0
1      1002  south  notebook         5   60.0
7      1008  north    bottle         4  120.0
2      1003  north       bag         2  850.0
5      1006  south       bag         1  850.0

शीर्ष तीन 1005, 1007 और 1001 हैं — बिल्कुल हमारी योजना के स्केच के अनुसार। केवल उन तीनों को भेजने के लिए, head(3) जोड़ें:

python
top3 = orders.sort_values("quantity", ascending=False).head(3)

print(top3[cols])
text
order_id branch product  quantity  price
4      1005  north  eraser        30    8.0
6      1007   east     pen        20   15.0
0      1001  north     pen        12   15.0

श्रृंखला को बाएँ से दाएँ पढ़ने पर योजना स्पष्ट दिखती है: orders लें, सबसे बड़ी मात्रा पहले रखें, पहले तीन रखें। यह "सॉर्ट करें, फिर head" पैटर्न अधिकांश "शीर्ष N" सवालों का जवाब देता है।

टेक्स्ट वर्णानुक्रम में सॉर्ट होता है

एक str कॉलम डिक्शनरी क्रम में सॉर्ट होता है:

python
print(orders.sort_values("product")[cols])
text
order_id branch   product  quantity  price
2      1003  north       bag         2  850.0
5      1006  south       bag         1  850.0
3      1004   east    bottle         7  120.0
7      1008  north    bottle         4  120.0
4      1005  north    eraser        30    8.0
1      1002  south  notebook         5   60.0
0      1001  north       pen        12   15.0
6      1007   east       pen        20   15.0

bag, bottle से पहले आता है क्योंकि a, o से पहले आता है। दो बैग, दो बोतलें और दो पेन — ये टाई (ties) हैं, और हम इस सवाल पर जल्द ही वापस आएँगे कि टाई होने पर कौन सा पहले आता है।


सॉर्ट एक नई टेबल लौटाता है

यह वह गलती है जो लगभग हर कोई एक बार ज़रूर करता है, और इससे कोई एरर नहीं आता:

python
orders.sort_values("quantity", ascending=False)

print(orders[cols].head(3))
text
order_id branch   product  quantity  price
0      1001  north       pen        12   15.0
1      1002  south  notebook         5   60.0
2      1003  north       bag         2  850.0

कुछ भी सॉर्ट नहीं हुआ। sort_values() मूल orders को नहीं बदलता; यह नए क्रम में एक नया DataFrame बनाता है और उसे लौटाता है। पहली लाइन ने वह सॉर्ट की गई टेबल बनाई और, क्योंकि किसी ने उसे किसी वेरिएबल में नहीं सहेजा, इसलिए उसे छोड़ दिया। orders बिल्कुल वैसा ही है जैसा read_csv ने बनाया था।

पांडास इस तरह क्यों काम करता है? क्योंकि मूल क्रम भी एक जानकारी है। orders.csv में यह वह क्रम है जिस क्रम में ऑर्डर्स प्राप्त हुए थे। यदि प्रत्येक सॉर्ट आपकी एकमात्र प्रति को पुनर्व्यवस्थित कर देता, तो आप कभी भी "फ़ाइल जैसी थी" उस स्थिति में वापस नहीं आ पाते, और एक रिपोर्ट के लिए लिखा गया सॉर्ट अगली रिपोर्ट के इनपुट को चुपचाप बदल देता। एक नया ऑब्जेक्ट लौटाने का अर्थ है कि सॉर्टिंग को आज़माना हमेशा सुरक्षित रहता है।

sort_values() कभी भी उस टेबल को नहीं बदलता जिस पर इसे कॉल किया जाता है। यदि आप परिणाम को किसी वेरिएबल में असाइन नहीं करते हैं, तो सॉर्ट हुआ और छोड़ दिया गया — और कोई चेतावनी आपको सतर्क नहीं करती।

इसलिए परिणाम को सहेजें, या तो किसी नए नाम में या उसी नाम में:

python
by_qty_desc = orders.sort_values("quantity", ascending=False)   # keep both

print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())
text
[1005, 1007, 1001]
[1001, 1002, 1003]

सॉर्ट की गई कॉपी में बड़े ऑर्डर्स सबसे ऊपर हैं; मूल टेबल अछूती है। orders = orders.sort_values(...) लिखना भी पूरी तरह से ठीक है जब आप सुनिश्चित हों कि आपको पुराने क्रम की आवश्यकता नहीं है।

आप पुराने कोड में inplace=True देख सकते हैं। यह टेबल को उसी जगह बदलता है और None लौटाता है, जिससे अपना एक अलग बग पैदा होता है (देखें जब कोड काम न करे)। पांडास 3 में Copy-on-Write के साथ, inplace=True कोई मेमोरी भी नहीं बचाता है। परिणाम को असाइन करना अधिक स्पष्ट है: आप लाइन पर ही देख सकते हैं कि सॉर्ट की गई टेबल कहाँ जा रही है।


एकाधिक कॉलम: प्राथमिकता और दिशा

मालिक का दूसरा अनुरोध था: "शाखा के अनुसार समूहीकृत, प्रत्येक शाखा के भीतर सबसे बड़ा पहले।" ये दो की हैं, और सूची का क्रम प्राथमिकता (priority) का क्रम है:

python
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])

print(by_branch[cols])
text
order_id branch   product  quantity  price
6      1007   east       pen        20   15.0
3      1004   east    bottle         7  120.0
4      1005  north    eraser        30    8.0
0      1001  north       pen        12   15.0
7      1008  north    bottle         4  120.0
2      1003  north       bag         2  850.0
1      1002  south  notebook         5   60.0
5      1006  south       bag         1  850.0

इसे वैसे पढ़ें जैसे पांडास ने इसे बनाया है। सबसे पहले, प्रत्येक पंक्ति को branch द्वारा रखा जाता है, A से Z: east, north, south। दूसरा की, quantity, केवल तभी देखा जाता है जब पहला की टाई होता है — east के भीतर, north के भीतर, south के भीतर। north के भीतर यह 30, 12, 4, 2 चलता है: सबसे बड़ा पहले, क्योंकि ascending की दूसरी प्रविष्टि False है।

ascending प्रत्येक की के लिए एक True/False लेता है, उसी क्रम में जिस क्रम में by है। एक अकेला False दोनों कीज़ पर लागू होगा और south को पहले रख देगा।

सूची में कॉलमों की अदला-बदली करने से सवाल पूरी तरह बदल जाता है:

python
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))
text
order_id branch product  quantity  price
4      1005  north  eraser        30    8.0
6      1007   east     pen        20   15.0
0      1001  north     pen        12   15.0
3      1004   east  bottle         7  120.0

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


टाई: क्रम को एक निर्णय बनाएं, कोई संयोग नहीं

price के अनुसार सॉर्ट करें, सबसे महंगी वस्तु पहले:

python
print(orders.sort_values("price", ascending=False)[cols])
text
order_id branch   product  quantity  price
2      1003  north       bag         2  850.0
5      1006  south       bag         1  850.0
7      1008  north    bottle         4  120.0
3      1004   east    bottle         7  120.0
1      1002  south  notebook         5   60.0
0      1001  north       pen        12   15.0
6      1007   east       pen        20   15.0
4      1005  north    eraser        30    8.0

120 वाली दो बोतलों को देखें। फ़ाइल में, ऑर्डर 1004, 1008 से पहले आता है। सॉर्ट की गई टेबल में, 1008 पहले आता है। कुछ भी टूटा नहीं है: एक अकेले की के लिए, पांडास का डिफ़ॉल्ट सॉर्टिंग एल्गोरिदम (quicksort) टाई पंक्तियों को उनके मूल क्रम में रखने का वादा नहीं करता है, और यहाँ उसने ऐसा नहीं किया। 850 वाले दो बैग फ़ाइल क्रम में रहे, दो बोतलों ने स्थान बदल लिया — ऐसा कोई नियम नहीं है जिसका आप पहले से अनुमान लगा सकते थे।

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

उस निर्णय को वापस अपने हाथ में लेने के दो तरीके हैं।

विकल्प 1: एक स्थिर सॉर्ट (stable sort)। kind="stable" वादा करता है कि टाई पंक्तियाँ सॉर्ट से पहले का अपना क्रम बनाए रखती हैं — यहाँ, फ़ाइल का क्रम:

python
stable = orders.sort_values("price", ascending=False, kind="stable")

print(stable["order_id"].tolist())
text
[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]

विकल्प 2 (आमतौर पर बेहतर): एक स्पष्ट टाई-ब्रेकर। तय करें कि टाई होने पर किसे जीतना चाहिए। बिक्री रिपोर्ट के लिए, "यदि कीमत समान है, तो बड़ा ऑर्डर पहले" एक समझदारी भरा नियम है:

python
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])

print(tie_broken[cols])
text
order_id branch   product  quantity  price
2      1003  north       bag         2  850.0
5      1006  south       bag         1  850.0
3      1004   east    bottle         7  120.0
7      1008  north    bottle         4  120.0
1      1002  south  notebook         5   60.0
6      1007   east       pen        20   15.0
0      1001  north       pen        12   15.0
4      1005  north    eraser        30    8.0

बोतलों को फिर से देखें: 1004 अब 1008 से ऊपर है, क्योंकि 7, 4 से बड़ा है। और 15 वाले दो पेन: पहले, 1001 पहले आया था; अब 1007 जीतता है, क्योंकि 20, 12 से बड़ा है — और यह हर मशीन पर, किसी भी क्रम में फ़ाइल आने पर, हर बार जीतेगा। एक टाई-ब्रेकर एक संयोग को ऐसे नियम में बदल देता है जिसे आप मालिक को समझा सकते हैं। (जब आप एक से अधिक कॉलम द्वारा सॉर्ट करते हैं, तो पांडास का सॉर्ट वैसे भी स्थिर होता है, इसलिए कीज़ की एक सूची वास्तविक टाई को कभी नहीं बिखेरती।)

यदि आपके पास कोई व्यावसायिक नियम नहीं है, तो order_id एक अच्छा अंतिम की बनता है: यह अद्वितीय (unique) है, इसलिए इसके बाद कोई टाई नहीं बचता।


मिसिंग वैल्यूज़: na_position

दो रिक्त मात्राओं वाली फ़ाइल का संस्करण पढ़ें:

python
gaps = pd.read_csv("orders_gaps.csv")

print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])
text
2
   order_id branch   product  quantity  price
4      1005  north    eraser      30.0    8.0
0      1001  north       pen      12.0   15.0
3      1004   east    bottle       7.0  120.0
7      1008  north    bottle       4.0  120.0
2      1003  north       bag       2.0  850.0
5      1006  south       bag       1.0  850.0
1      1002  south  notebook       NaN   60.0
6      1007   east       pen       NaN   15.0

ध्यान देने योग्य दो बातें। पहला, quantity अब 30.0, 12.0 के रूप में प्रिंट होता है — रिक्त मानों ने कॉलम को float64 में बदल दिया, जैसा कि अध्याय छह में बताया गया था, क्योंकि NaN एक फ़्लोट है। दूसरा, सबसे बड़े मान पहले मांगने के बावजूद NaN पंक्तियाँ सबसे नीचे चली गईं। यह डिफ़ॉल्ट व्यवहार है, na_position="last", और यह दोनों दिशाओं में लागू होता है: एक अनुपस्थित मान "छोटा" या "बड़ा" नहीं होता, यह अज्ञात होता है, इसलिए पांडास अनुमान लगाने के बजाय इसे एक तरफ़ रख देता है।

ध्यान दें कि इसने शीर्ष तीन के साथ क्या किया। ऑर्डर 1007 ने वास्तव में 20 पेन बेचे थे, लेकिन इस फ़ाइल में इसकी मात्रा रिक्त है, इसलिए यह शीर्ष तीन से बाहर हो गया और 1004 ने इसकी जगह ले ली। एक सॉर्ट मिसिंग डेटा को ठीक नहीं कर सकता; यह केवल आपको दिखा सकता है कि रिक्तियाँ कहाँ गईं। यही कारण है कि योजना का चरण 2 सॉर्ट करने से पहले रिक्तियों की गणना करता है।

जब आप डेटा की जाँच (audit) कर रहे होते हैं, तो आप अक्सर इसका उल्टा चाहते हैं — समस्याएँ सबसे ऊपर हों, जहाँ आप उन्हें देख सकें:

python
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))
text
order_id branch   product  quantity  price
1      1002  south  notebook       NaN   60.0
6      1007   east       pen       NaN   15.0
5      1006  south       bag       1.0  850.0

इसलिए na_position कोई फ़ॉर्मेटिंग विकल्प नहीं है। "last" रिपोर्ट के लिए उपयुक्त है, जहाँ रिक्तियाँ वास्तविक मानों को बाहर न करें। "first" ऑडिट के लिए उपयुक्त है, जहाँ रिक्तियाँ वही हैं जिन्हें आप खोज रहे हैं।


शुरुआत में वापस: sort_index() और reset_index()

क्योंकि प्रत्येक पंक्ति ने अपना लेबल बनाए रखा, इसलिए मूल क्रम कभी खोता नहीं है। इंडेक्स के अनुसार सॉर्ट करने पर यह वापस आ जाता है:

python
restored = by_qty.sort_index()

print(restored["order_id"].tolist())
text
[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]

sort_index() भी ascending=False स्वीकार करता है, जिसके द्वारा आप टेबल को "नवीनतम लेबल पहले" सूचीबद्ध कर सकते हैं।

कभी-कभी पुराने लेबल्स का कोई अर्थ नहीं रह जाता। यदि आप एक रैंकिंग सूची बनाने और उसे भेजने के लिए सॉर्ट करते हैं, तो साइड में 4, 6, 0 लेबल्स भ्रमित करने वाले होते हैं। reset_index(drop=True) उन्हें हटा देता है और पंक्तियों को उनके नए क्रम में 0, 1, 2, … से क्रमांकित करता है:

python
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)

print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])
text
order_id branch product  quantity  price
0      1005  north  eraser        30    8.0
1      1007   east     pen        20   15.0
2      1001  north     pen        12   15.0
1005

अब loc[0] और iloc[0] फिर से सहमत हैं: दोनों ऑर्डर 1005 हैं, जो सबसे बड़ा है। drop=True के बिना, पांडास पुराने लेबल्स को index नामक एक नए कॉलम के रूप में रखता — यदि आप पंक्तियों को मूल स्रोत से मिलाना चाहते हैं तो उपयोगी है, अन्यथा यह अव्यवस्था है।

किसे चुनें? जब आप अभी भी डेटा के साथ काम कर रहे हों और पंक्तियों को मूल से मिलाने की आवश्यकता हो, तो लेबल्स रखें। जब क्रम ही अंतिम परिणाम हो — जैसे रैंकिंग या शीर्ष दस की सूची — तो उन्हें रीसेट करें।


"शीर्ष N" का संक्षिप्त तरीका: nlargest() और nsmallest()

"मात्रा के अनुसार शीर्ष तीन" इतना सामान्य है कि पांडास के पास एक ऐसा मेथड है जो ठीक यही कहता है:

python
print(orders.nlargest(3, "quantity")[cols])
text
order_id branch product  quantity  price
4      1005  north  eraser        30    8.0
6      1007   east     pen        20   15.0
0      1001  north     pen        12   15.0

यह बिल्कुल sort_values("quantity", ascending=False).head(3) जैसी ही पंक्तियाँ देता है। इसे प्राथमिकता क्यों दें? यह लाइन तंत्र (mechanism) के बजाय उद्देश्य बताती है — "तीन सबसे बड़े"। और एक बड़ी टेबल पर यह तेज़ होता है, क्योंकि इसे केवल तीन रखने के लिए सभी पंक्तियों को क्रम में लगाने की आवश्यकता नहीं होती।

nsmallest() इसका ठीक उल्टा है: दुकान में सबसे सस्ते तीन आइटम:

python
print(orders.nsmallest(3, "price")[cols])
text
order_id branch product  quantity  price
4      1005  north  eraser        30    8.0
0      1001  north     pen        12   15.0
6      1007   east     pen        20   15.0

कट-ऑफ पर टाई: keep=

तीन सबसे महंगे आइटम मांगें और एक असहज स्थिति उत्पन्न होती है:

python
print(orders.nlargest(3, "price")[cols])
text
order_id branch product  quantity  price
2      1003  north     bag         2  850.0
5      1006  south     bag         1  850.0
3      1004   east  bottle         7  120.0

तीसरा स्थान 120 है — लेकिन दो ऑर्डर्स की कीमत 120 है, 1004 और 1008। पांडास ने उनमें से एक लौटाया — डिफ़ॉल्ट keep="first" के साथ, जो टेबल में पहले दिखाई देता है — और चुपचाप दूसरे को छोड़ दिया। यदि आप "शीर्ष तीन" को पुरस्कार दे रहे हैं, तो यह 1008 के साथ अनुचित है। keep="all" कट-ऑफ पर टाई होने वाली प्रत्येक पंक्ति को रखता है, भले ही इसका मतलब N से अधिक पंक्तियाँ हों:

python
print(orders.nlargest(3, "price", keep="all")[cols])
text
order_id branch product  quantity  price
2      1003  north     bag         2  850.0
5      1006  south     bag         1  850.0
3      1004   east  bottle         7  120.0
7      1008  north  bottle         4  120.0

"शीर्ष तीन" के लिए चार पंक्तियाँ। यह कोई बग नहीं है: जब तीसरा स्थान साझा किया जाता है तो यह सच्चा उत्तर है। क्या आप इसे चाहते हैं, यह फिर से एक योजना का निर्णय है।

nlargest और nsmallest केवल संख्यात्मक कॉलमों पर काम करते हैं। टेक्स्ट के लिए, sort_values और head का उपयोग करें।


किसी सबसेट का शीर्ष N: पहले फ़िल्टर करें, फिर सॉर्ट करें, फिर काटें

"north शाखा में दो सबसे बड़े ऑर्डर्स कौन से थे?" यह अध्याय पाँच को इस अध्याय के साथ जोड़ता है, और चरणों का क्रम महत्वपूर्ण है:

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

top_north = north.sort_values("quantity", ascending=False).head(2)

print(top_north[cols])
text
order_id branch product  quantity  price
4      1005  north  eraser        30    8.0
0      1001  north     pen        12   15.0

पहले फ़िल्टर करें, क्योंकि सवाल केवल north शाखा के बारे में है। यदि आपने पूरी टेबल को सॉर्ट किया और पहले head(2) लिया, तो आपको 1005 और 1007 मिलते — और 1007 east शाखा का है। बाद में फ़िल्टर करने पर केवल एक पंक्ति बचती, और आप एक प्रविष्टि के साथ "शीर्ष दो" की रिपोर्ट करते। नियम: उन पंक्तियों तक सीमित करें जिनके बारे में सवाल है, फिर उन्हें रैंक करें, फिर काटें।

यही काम nlargest की श्रृंखला के साथ भी होता है: orders[orders["branch"] == "north"].nlargest(2, "quantity")।


केस की परवाह किए बिना टेक्स्ट सॉर्ट करना: key=

टेक्स्ट सॉर्टिंग वर्णों की तुलना उनके कोड के अनुसार करती है, और प्रत्येक बड़ा अक्षर प्रत्येक छोटे अक्षर से पहले आता है। मनुष्यों द्वारा टाइप किए गए डेटा में दोनों का मिश्रण होता है:

python
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})

print(names.sort_values("product"))
text
product
3    Bottle
1  Notebook
2       bag
4    eraser
0       pen

Bottle और Notebook केवल इसलिए bag से आगे निकल गए क्योंकि वे बड़े अक्षर से शुरू होते हैं। कोई भी पाठक इसे वर्णानुक्रम नहीं कहेगा।

आप डेटा को ठीक कर सकते थे, लेकिन अक्सर आपको ऐसा नहीं करना चाहिए — वर्तनी वही हो सकती है जो ग्राहक ने लिखी थी। key= तर्क आपको मूल मानों को बनाए रखते हुए कॉलम के रूपांतरित (transformed) संस्करण के आधार पर सॉर्ट करने की अनुमति देता है। यह एक ऐसा फ़ंक्शन लेता है जो पूरे कॉलम को प्राप्त करता है और सॉर्ट करने के लिए रूपांतरित संस्करण लौटाता है:

python
print(names.sort_values("product", key=lambda col: col.str.lower()))
text
product
2       bag
3    Bottle
4    eraser
1  Notebook
0       pen

लोअरकेस कॉपी का उपयोग केवल तुलना के लिए किया जाता है और फिर उसे छोड़ दिया जाता है। टेबल अभी भी Bottle और Notebook को ठीक वैसे ही दिखाती है जैसे वे टाइप किए गए थे। (lambda col: col.str.lower() एक पंक्ति का फ़ंक्शन है: कॉलम col दिए जाने पर, इसे लोअरकेस में लौटाएं।)


जब संख्याएँ टेक्स्ट के रूप में संग्रहीत हों

यहाँ वह जाल है जिसे पकड़ने के लिए dtypes वाला योजना चरण मौजूद है। मान लीजिए कि quantity टेक्स्ट के रूप में आया — फ़ाइल में कोई भटका हुआ "N/A", या कोई ऐसा सिस्टम जो सब कुछ स्ट्रिंग्स के रूप में निर्यात करता है। यह देखने के लिए कि यह क्या करता है, हम astype के साथ जानबूझकर ऐसा कॉलम बनाते हैं:

python
as_text = orders.astype({"quantity": "str"})

print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])
text
str
   order_id quantity
5      1006        1
0      1001       12
2      1003        2
6      1007       20
4      1005       30
7      1008        4
1      1002        5
3      1004        7

1, 12, 2, 20, 30, 4, 5, 7। कोई एरर नहीं, और एक ऐसा क्रम जो लगभग सही दिखता है। टेक्स्ट की तुलना अक्षर-दर-अक्षर की जाती है: "12" शुरू होता है 1 से, जो 2 से छोटा है, इसलिए "12", "2" से पहले आता है — जैसे डिक्शनरी में "ab", "b" से पहले आता है। सॉर्ट ने बिल्कुल वही किया जो उसे बताया गया था। लेकिन उसे गलत चीज़ बताई गई थी।

दो सुराग इसे उजागर करते हैं: प्रिंटआउट में quantity बाईं ओर संरेखित (left-aligned) है, जिस तरह पांडास टेक्स्ट प्रिंट करता है, और dtype, str कहता है। असली समाधान कॉलम को संख्याओं में बदलना है (अध्याय छह का pd.to_numeric)। यदि आपको कॉलम को वैसा ही छोड़ना है, तो key= इसके बजाय संख्यात्मक मान द्वारा सॉर्ट कर सकता है:

python
fixed = as_text.sort_values("quantity", key=pd.to_numeric)

print(fixed["quantity"].tolist())
text
['1', '2', '4', '5', '7', '12', '20', '30']

अब सही क्रम है — लेकिन मान अभी भी स्ट्रिंग्स हैं, इसलिए आप उन्हें जोड़ नहीं सकते। फ़ाइल पढ़ने के तुरंत बाद कॉलम को एक बार बदल लेना सबसे अच्छा तरीका है। तब उसके बाद आने वाला हर सॉर्ट, फ़िल्टर और जोड़ बिना याद रखे सही काम करता है।


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

मालिक का पूरा अनुरोध, एक प्रोग्राम में उत्तर दिया गया: (1) अर्जित राजस्व द्वारा शीर्ष तीन ऑर्डर्स, (2) सबसे नए ऑर्डर्स पहले, (3) शाखा द्वारा एक सूची जिसमें प्रत्येक शाखा में सबसे बड़ा ऑर्डर पहले हो।

"अर्जित राजस्व" मात्रा गुणा कीमत है। कॉलम जोड़ना पाठ्यक्रम में बाद में अच्छी तरह से कवर किया गया है; अभी के लिए एक पंक्ति पर्याप्त है — एक नए कॉलम नाम को असाइन करने से वह बन जाता है।

sorted_report.py:

python
import pandas as pd

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

# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()

orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]

# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
    orders.sort_values(["revenue", "quantity"], ascending=[False, False])
    .head(3)
    .reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()

# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()

# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()

print("Still eight rows:", by_branch.shape[0] == orders.shape[0])
text
Rows, columns: (8, 7)
Blank quantities: 0

Top three orders by revenue
   order_id        date branch product  revenue
0      1003  2024-03-02  north     bag   1700.0
1      1006  2024-03-03  south     bag    850.0
2      1004  2024-03-02   east  bottle    840.0

Newest first
   order_id        date branch product  revenue
7      1008  2024-03-04  north  bottle    480.0
6      1007  2024-03-04   east     pen    300.0
5      1006  2024-03-03  south     bag    850.0
4      1005  2024-03-03  north  eraser    240.0

By branch
   order_id        date branch   product  revenue
3      1004  2024-03-02   east    bottle    840.0
6      1007  2024-03-04   east       pen    300.0
2      1003  2024-03-02  north       bag   1700.0
7      1008  2024-03-04  north    bottle    480.0
4      1005  2024-03-03  north    eraser    240.0
0      1001  2024-03-01  north       pen    180.0
5      1006  2024-03-03  south       bag    850.0
1      1002  2024-03-01  south  notebook    300.0

Still eight rows: True

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

  • जाँच पहले आती है। किसी भी रैंकिंग से पहले shape और रिक्तियों की संख्या प्रिंट की जाती है। आधी पढ़ी गई फ़ाइल से या रिक्तियों को नीचे धकेल कर तैयार किए गए शीर्ष तीन भी उतने ही आश्वस्त दिखते।
  • "सबसे बड़े" को एक कॉलम में बदला गया। quantity द्वारा रैंकिंग करने पर 30 इरेज़र सबसे ऊपर आते — 240 का व्यवसाय। revenue द्वारा रैंकिंग करने पर 1,700 मूल्य के दो बैग सबसे ऊपर आते हैं। योजना का पहला कदम — कॉलम के साथ सवाल लिखें — तय करता है कि मालिक को कौन सा उत्तर मिले।
  • प्रत्येक सॉर्ट में एक टाई-ब्रेकर है। 2024-03-04 को दो ऑर्डर्स हैं; revenue तय करता है कि कौन सा पहले सूचीबद्ध हो, इसलिए हर रन पर रिपोर्ट समान रहती है।
  • केवल रैंकिंग को पुनः क्रमांकित किया गया है। top3 एक पूर्ण सूची है, इसलिए इसके लेबल्स 0, 1, 2 बन जाते हैं। newest और by_branch अपने मूल लेबल्स बनाए रखते हैं, ताकि किसी भी पंक्ति को वापस फ़ाइल से मिलाया जा सके।
  • orders को स्वयं कभी भी दोबारा सॉर्ट नहीं किया गया। प्रत्येक रिपोर्ट उसी अछूते इनपुट से बनी एक नई टेबल है, इसलिए तीनों उत्तर एक-दूसरे में हस्तक्षेप नहीं कर सकते।
  • अंतिम पंक्ति एक आसान जांच (assertion) है। एक सॉर्ट को कभी भी पंक्तियों की संख्या नहीं बदलनी चाहिए। True प्रिंट करने में कुछ खर्च नहीं होता और यह, उदाहरण के लिए, गलत जगह छूटे हुए आकस्मिक head() को पकड़ सकता है।

जब कोड काम न करे (सामान्य त्रुटियाँ)

KeyError: 'Quantity'

python
orders.sort_values("Quantity")
text
KeyError: 'Quantity'

by= में नाम कॉलम से बिल्कुल मेल खाना चाहिए — केस, स्पेस और सब कुछ। कॉलम quantity है, छोटे अक्षरों में। उद्धरण और किसी भी अतिरिक्त स्पेस सहित वास्तविक नाम देखने के लिए list(orders.columns) प्रिंट करें, जैसा कि अध्याय तीन में अनुशंसित किया गया था।

AttributeError: 'NoneType' object has no attribute 'head'

python
top = orders.sort_values("quantity", inplace=True)
top.head()
text
AttributeError: 'NoneType' object has no attribute 'head'

inplace=True के साथ, sort_values, orders को बदलता है और None लौटाता है। इसलिए top, None है, और None में कोई head() नहीं होता है। एक शैली चुनें: या तो अपनी पंक्ति पर orders.sort_values("quantity", inplace=True), या — बेहतर — inplace के बिना top = orders.sort_values("quantity")।

ValueError: Length of ascending (1) != length of by (2)

python
orders.sort_values(["branch", "quantity"], ascending=[False])
text
ValueError: Length of ascending (1) != length of by (2)

ascending की सूची में प्रति की एक प्रविष्टि की आवश्यकता होती है। दो की, दो बूलियन: ascending=[True, False]। (एक अकेला सादा False, जो सूची में नहीं है, मान्य है — यह प्रत्येक की पर लागू होता है।)

ValueError: For argument "ascending" expected type bool, received type str.

python
orders.sort_values("branch", ascending="False")
text
ValueError: For argument "ascending" expected type bool, received type str.

उद्धरण चिह्नों में "False" एक स्ट्रिंग है, बूलियन False नहीं। उद्धरण चिह्न हटा दें।

TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype

python
orders.nlargest(3, "branch")
text
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype

nlargest और nsmallest केवल संख्याओं को रैंक करते हैं। "वर्णानुक्रम में अंतिम तीन शाखाएँ" के लिए orders.sort_values("branch", ascending=False).head(3) का उपयोग करें। यदि आपको किसी ऐसे कॉलम पर यह त्रुटि मिली जिसे आपने संख्यात्मक माना था, तो वास्तविक समस्या यह है कि इसे टेक्स्ट के रूप में पढ़ा गया था — dtypes की जाँच करें।

TypeError: '<' not supported between instances of 'str' and 'int'

python
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()
text
TypeError: '<' not supported between instances of 'str' and 'int'

यह कॉलम वास्तविक संख्याओं और टेक्स्ट के एक टुकड़े को रखता है, इसलिए इसका dtype, object है, और पायथन यह तय नहीं कर सकता कि "unknown" 12 से बड़ा है या छोटा। पहले कॉलम को साफ़ करें: pd.to_numeric(mixed, errors="coerce"), "unknown" को NaN में बदल देता है, और फिर सॉर्ट काम करता है और na_position तय करता है कि रिक्त स्थान कहाँ जाएगा।