অধ্যায় 06

অনুপস্থিত মান — ফাঁকগুলো খুঁজে বের করা এবং সেগুলোর অর্থ বোঝা

একটি টেবিলের প্রতিটি ফাঁকা ঘর খুঁজে বের করা (এমনকি - বা ?-এর মতো লেখার আড়ালে লুকানো ফাঁকও), NaN কীভাবে যোগফল, গড় ও ডেটার ধরন বদলে দেয় তা বোঝা, এবং প্রতি কলামের জন্য বুঝে-শুনে সিদ্ধান্ত নেওয়া — ফাঁকটি বাদ দেবেন, পূরণ করবেন নাকি রেখে দেবেন।

45 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

মার্চের প্রথম সপ্তাহের দোকানের অর্ডারের হিসাব তিনটি শাখায় হাতে লিখে রাখা হয়েছিল এবং orders_raw.csv হিসেবে এক্সপোর্ট করা হয়েছিল। মালিক সন্ধ্যার মধ্যেই দুটি হিসাব জানতে চেয়েছেন: মোট কতগুলো জিনিস বিক্রি হয়েছে, আর কত টাকা আয় হয়েছে।

আপনি ফাইলটি পড়লেন এবং আগের অধ্যায়গুলোর অভ্যাসবশত স্কেল বা পরিসর বোঝার জন্য প্রথমেই দামের কলামটি যোগ করলেন:

python
import pandas as pd

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

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

এটি কোনো সংখ্যা নয়। এটি প্রতিটি দাম টেক্সট হিসেবে পাশাপাশি জোড়া লেগে যাওয়া এক অদ্ভুত লেখা — অথচ পান্ডাস আপনাকে সতর্ক করার জন্য কোনো ত্রুটিও দেয়নি। এবার পরিমাণের কলামটি দেখুন:

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

এটি দেখতে ঠিকঠাক মনে হচ্ছে, আর সেটাই সবচেয়ে বিপজ্জনক। ফাইলে মোট আটটি অর্ডার আছে, কিন্তু 76 / 8 তো 9.5, 10.86 নয়। একটি অর্ডারের quantity খালি ছিল, আর পান্ডাস নিঃশব্দে মোট যোগফল ও গড় উভয় হিসাব থেকেই সেটি বাদ দিয়ে দিয়েছে। সেটাও আপনাকে কেউ বলেনি।

কাজেই ফাইলটিতে ফাঁক বা শূন্যস্থান রয়েছে, আর এই ফাঁকগুলো মূলত দুই রকমের। কিছু ফাঁক দৃশ্যমান: একটি খালি সেল, যাকে পান্ডাস একটি বিশেষ "অনুপস্থিত" (missing) মার্কারে রূপান্তর করে এবং পাটিগণিত করার সময় কোনো বার্তা না দিয়েই এড়িয়ে যায়। অন্য ফাঁকগুলো লুকানো: কেউ ঘরটি খালি রাখার বদলে - লিখে দিয়েছে, পান্ডাস সেটিকেই সত্যি ধরে নিয়েছে, আর পুরো price কলামটি টেক্সট বা লেখায় পরিণত হয়েছে।

কোনো ধরনের ফাঁকই সরাসরি কোনো এরর দেয় না। কিন্তু উভয় ফাঁকই আপনার উত্তরের হিসাব বদলে দেয়। এই অধ্যায়টি প্রতিটি ফাঁকা ঘর খুঁজে বের করা, সেগুলোর সাথে পান্ডাস কী আচরণ করে তা বোঝা, এবং ডিফল্টের ওপর সিদ্ধান্ত ছেড়ে না দিয়ে সচেতনভাবে সিদ্ধান্ত নেওয়ার বিষয়ে — ফাঁকটি বাদ দেবেন, পূরণ করবেন নাকি রেখে দেবেন।

এই অধ্যায় শেষে আপনি পারবেন

  • NaN কী, কেন NaN == NaN আসলে False হয়, এবং কোনো একটি সেল খালি থাকলে পূর্ণসংখ্যার কলাম কেন দশমিকে পরিণত হয় তা ব্যাখ্যা করতে
  • isna().sum() দিয়ে প্রতি কলামের অনুপস্থিত মান গণনা করতে এবং অসম্পূর্ণ সারিগুলো আলাদা করে দেখতে
  • na_values= বা pd.to_numeric(errors="coerce") দিয়ে - বা ?-এর মতো লুকানো মার্কারগুলোকে আসল অনুপস্থিত মানে রূপান্তর করতে
  • sum, mean, count এবং কলামের পাটিগণিত অনুপস্থিত মানকে কীভাবে দেখে তা অনুমান করতে
  • dropna() ব্যবহার করে অনুপস্থিত মান মুছে ফেলতে, যেখানে subset= ও how= দিয়ে কেবল প্রয়োজনীয় সারিগুলোই সতর্কতার সাথে বাদ দেওয়া যায়
  • fillna() দিয়ে অনুপস্থিত মান প্রতিস্থাপন করতে — একটি নির্দিষ্ট মান দিয়ে, কলামভেদে ভিন্ন মান দিয়ে, কিংবা কোনো পরিসংখ্যানগত মান দিয়ে
  • প্রতি কলামের জন্য যুক্তি সহকারে সিদ্ধান্ত নিতে — বাদ দেবেন, পূরণ করবেন নাকি রেখে দেবেন
  • পান্ডাস ৩-এ কোনো কাজ না করা inplace=True-এর ফাঁদ এড়িয়ে চলতে

আগে যা জানা লাগবে: সারি ছাঁকা বা ফিল্টার করা।


আগে ফাইলটি তৈরি করে নিন

আপনার প্রজেক্ট ফোল্ডারে orders_raw.csv নামে একটি ফাইল বানান এবং ঠিক এই লাইনগুলো লিখুন। ফাঁকগুলো ইচ্ছে করেই রাখা হয়েছে — তৃতীয়, পঞ্চম এবং সপ্তম লাইনের দিকে মনোযোগ দিয়ে তাকান:

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

এটি আগের অধ্যায়গুলোর orders.csv-এর আটটি অর্ডারই, তবে চারটি জায়গায় গড়মিল আছে: ১০০২ নম্বর অর্ডারে কোনো quantity নেই, ১০০৪ নম্বরে কোনো branch নেই এবং তার price-এ লেখা আছে N/A, আর ১০০৬ নম্বর অর্ডারের price-এ আছে -।


কোড লেখার আগে

অনুপস্থিত ডেটা বা মিসিং ডেটা এমন একটি বিষয় যেখানে আগেই কোড লিখতে শুরু করা বিপজ্জনক: dropna() এবং fillna() দুটিই কোনো অভিযোগ বা ত্রুটি ছাড়াই চলে এবং দুটিই আপনার হিসাব ও ফলাফল বদলে দেয়। তাই কাজ শুরু করতে হয় চোখ বুলিয়ে দেখে এবং সিদ্ধান্ত নিয়ে।

১. প্রশ্নটিকে এক বাক্যে বলুন। "সপ্তাহে মোট কতটি পণ্য বিক্রি হয়েছে এবং মোট আয় (quantity × price) কত — আর কতগুলো অর্ডার হিসাবে অন্তর্ভুক্ত করা সম্ভব হয়নি?" এই শেষের অংশটি অত্যন্ত গুরুত্বপূর্ণ। একটি মোট হিসাব যা নিঃশব্দে দুটি অর্ডার বাদ দিয়ে দেয়, আর একটি মোট হিসাব যা পরিষ্কারভাবে বলে "৮টি অর্ডারের মধ্যে ৬টি হিসাবে ধরা হয়েছে" — দুটি কখনোই এক জিনিস নয়।

২. অন্য কিছুর আগে কী ডেটা এসেছে তা দেখুন। shape এবং info() একসাথে প্রতিটি কলাম সম্পর্কে একবারে জানিয়ে দেয়:

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

আপনি যা প্রত্যাশা করেছিলেন তার সাথে কলাম ধরে ধরে মিলিয়ে দেখুন:

  • branch — ৮টি non-null str প্রত্যাশিত ছিল, এসেছে ৭টি non-null। একটি শাখার নাম অনুপস্থিত।
  • quantity — ৮টি non-null int64 প্রত্যাশিত ছিল, এসেছে ৭টি non-null float64। একটি quantity অনুপস্থিত, আর ঠিক সেই কারণেই এটি float64 হয়ে গেছে (নিচে ব্যাখ্যা করা হয়েছে)।
  • price — ৮টি non-null float64 প্রত্যাশিত ছিল, এসেছে ৭টি non-null str। একটি দাম অনুপস্থিত এবং কলামের ভেতর কিছু টেক্সট ঢুকে পড়েছে।

এই তালিকার দুটি বিষয় তথ্যের চেয়ে ক্লু বা সংকেত হিসেবে বেশি মূল্যবান। একটি পূর্ণসংখ্যার কলামে float64 দেখা যাওয়ার মানে প্রায় সবসময়ই একটি ফাঁক। আর একটি সংখ্যার কলাম str দেখাচ্ছে মানেই তাতে টেক্সট মিশে গেছে — আর সংখ্যার কলামে টেক্সট থাকার মানে সাধারণত এটি একটি লুকানো "অনুপস্থিত" মার্কার।

৩. আপনার কাঙ্ক্ষিত আউটপুট কেমন হবে তার একটি খসড়া তৈরি করুন। অনেকটা এরকম:

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

লক্ষ্য করুন, এটি মোট হিসাবের পাশাপাশি ফাঁকগুলোর কথাও রিপোর্ট করে। যিনি এই রিপোর্টটি পড়বেন তাঁর জানা উচিত যে মোট আয়ের এই সংখ্যাটি ন্যূনতম হিসাব (lower bound)।

৪. প্রতিটি ফাঁকের জন্য হাতিয়ার বেছে নিন — কোড লেখার আগেই। প্রতিটি কলামের জন্য নিজেকে প্রশ্ন করুন: এখানে একটি খালি সেলের অর্থ কী, আর এই মানটি ছাড়াই কি প্রশ্নের উত্তর দেওয়া সম্ভব?

  • branch — একটি ফাঁকের অর্থ অর্ডারটি সম্পন্ন হয়েছিল কিন্তু শাখা রেকর্ড করা হয়নি। সিদ্ধান্ত: "Unknown" দিয়ে পূরণ করুন (fill), কারণ অর্ডারটি বাস্তব এবং এর টাকা এখনও গণনায় ধরা উচিত।
  • quantity — একটি ফাঁকের অর্থ কতগুলো পণ্য বিক্রি হয়েছে তা আমরা জানি না। সিদ্ধান্ত: এটিকে অনুপস্থিত হিসেবেই রেখে দিন (keep) এবং মোট আয়ের হিসাব থেকে এই সারিটি বাদ দিন, কারণ মনগড়া একটি সংখ্যা (০? নাকি গড়?) বসালে কাল্পনিক বিক্রি তৈরি হয়ে যাবে।
  • price — একটি ফাঁকের অর্থ এটি কত দামে বিক্রি হয়েছে তা আমরা জানি না। সিদ্ধান্ত: একই কারণে এটিকে অনুপস্থিত হিসেবেই রেখে দিন (keep) এবং আয়ের হিসাব থেকে সারিটি বাদ দিন — এবং কতগুলো সারি বাদ পড়ল তা রিপোর্টে উল্লেখ করুন।

৫. কাজ শেষ করার পর কী পরীক্ষা করবেন তা আগেই ঠিক করুন। যে পদক্ষেপটি কোনো মান মুছে ফেলে বা প্রতিস্থাপন করে, তার প্রতিটির পরে আবার মিসিং কাউন্ট ও আকার (shape) প্রিন্ট করে দেখুন। একটি dropna() যদি আপনার ৮টি সারিকে কমিয়ে ২টিতে নামিয়ে আনে, তবে রিপোর্ট বাইরে পাঠানোর পর নয়, তখনই আপনি তা দেখতে চাইবেন।

বাকি অধ্যায়ে প্রতিটি হাতিয়ার পর্যায়ক্রমে দেখানো হয়েছে, এবং শেষে আমরা এই পুরো পরিকল্পনাটিকে একটি একক প্রোগ্রামে রূপ দেব।


"অনুপস্থিত" দেখতে কেমন: NaN

পান্ডাস যখন একটি খালি সেল খুঁজে পায়, তখন সে কোনো ফাঁকা স্ট্রিং বা শূন্য জমা রাখে না। সে NaN ("not a number") নামের একটি বিশেষ ফ্লোটিং-পয়েন্ট মান সংরক্ষণ করে। একটি Series-এ None বসিয়ে আপনি নিজেও একটি বানাতে পারেন:

python
import pandas as pd

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

কলামটির dtype-এর দিকে তাকান। আপনি দুটি পূর্ণসংখ্যা এবং একটি ফাঁকা ঘর দিয়েছিলেন, আর ফেরত পেলেন float64, সাথে 12.0 এবং 2.0। info()-তে পাওয়া সেই ক্লুর পেছনের কারণ এটিই: NaN হলো একটি float মান, আর সনাতন পূর্ণসংখ্যার কলামের এটি ধারণ করার কোনো উপায় নেই। তাই যেই মুহূর্তে একটি সেল খালি হয়, পান্ডাস NaN-এর জায়গা করে দিতে পুরো কলামের ধরনকে float64-এ উন্নীত করে। এই কারণেই আমাদের ফাইলের quantity ছাপা হয়েছিল 12.0, 2.0 ইত্যাদি হিসেবে।

টেক্সট বা লেখার কলামগুলো ভিন্নভাবে আচরণ করে। একটি str কলাম ডেটার ধরন না বদলেই অনুপস্থিত মান ধরে রাখতে পারে:

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

এটি এখনও NaN হিসেবেই ছাপা হয় এবং dtype থাকে str। তাই পান্ডাস ৩-এ আপনি সংখ্যা এবং টেক্সট উভয় ধরনের কলামেই NaN-এর দেখা পাবেন, এবং একই হাতিয়ার উভয় জায়গায় এটি খুঁজে বের করে।

NaN কোনো কিছুর সমান নয় — এমনকি নিজেরও না

অনুপস্থিত মান সম্পর্কে এটিই সবচেয়ে আশ্চর্যজনক সত্য, এবং এটি তাদের খুঁজে বের করার সবচেয়ে স্বাভাবিক উপায়টিকে ভেঙে দেয়:

python
import numpy as np
import pandas as pd

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

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

NaN-এর অর্থ হলো "অজানা কোনো একটি মান"। দুটি অজানা মান সমান কি না তা নিশ্চিত জানা নেই, তাই তুলনাটি প্রতিবারই False বলে। ফলে == np.nan এমন একটি পরীক্ষা যা কখনোই কোনো কিছুর সাথে মেলে না: branch কলামে ফাঁক আছে, অথচ গণনায় দেখাচ্ছে 0। এর বদলে সরাসরি পান্ডাসকে জিজ্ঞেস করুন।

কখনোই == দিয়ে অনুপস্থিত মান খুঁজবেন না। == np.nan কোনো ত্রুটি ছাড়াই চলে এবং কখনোই কিছু খুঁজে পায় না, ফলে ফাঁকে ভরা কলামকেও সম্পূর্ণ মনে হয়। ব্যবহার করুন isna()।

অনুপস্থিত মান খুঁজে বের করা: isna()

== যা পারে না, isna() ঠিক সেই প্রশ্নটিই করে: "এই সেলটি কি অনুপস্থিত?" একটি কলামের ওপর এটি True/False-এর একটি কলাম ফেরত দেয় — অর্থাৎ একটি মাস্ক, ঠিক সারি ছাঁকার অধ্যায়ের মাস্কগুলোর মতো:

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

পুরো DataFrame-এ এটি প্রতিটি সেলের জন্য একই কাজ করে। তবে আট সারির True/False পড়ে দেখার প্রয়োজন খুব কমই হয়। আপনি সাধারণত একটি গণনা চান, আর যেহেতু True-এর মান ১ হিসেবে গণ্য হয়, তাই .sum() সেই গণনাটি এনে দেয়:

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

যেকোনো নতুন ফাইলে info() চালানোর পর এই লাইনটিই চালানো উচিত। এটি info()-র সেই Non-Null Count-এরই বিপরীত রূপ, যা সরাসরি ফাঁকগুলোর সংখ্যা গুনে দেয়।

কিন্তু price-এর দিকে তাকান: এতে দেখাচ্ছে 1। আমরা জানি দুটি দাম নষ্ট ছিল — N/A এবং -। এই বিষয়টি মাথায় রাখুন; পরের সেকশনে আমরা এতে ফিরে আসছি।

কোন সারিগুলো অসম্পূর্ণ?

গণনা আপনাকে বলে সংখ্যায় কয়টি; কিন্তু কোনগুলো তা দেখতে সেই মাস্কটিকে ফিল্টার হিসেবে ব্যবহার করুন। isna().any(axis=1) প্রতিটি সারির কাছে জানতে চায়, "এই সারিতে কি কোনো কিছু অনুপস্থিত?":

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

axis=1-এর অর্থ হলো প্রতিটি কলাম ধরে নিচে নামার বদলে "প্রতিটি সারির আড়াআড়ি দেখা"। আর লক্ষ্য করুন কে এই তালিকায় নেই: ১০০৬ নম্বর অর্ডার, যার দাম ছিল -। পান্ডাস যতদূর জানে, সেই সারিটি সম্পূর্ণ। এই বিষয়টি মনে রাখুন। বাস্তব সারিগুলো দেখার জন্য বাড়তি এই লাইনটি লেখা খুবই সার্থক: এটি দেখলেই আপনি ধরতে পারবেন যে, ধরা যাক, প্রতিটি অনুপস্থিত quantity একই শাখা থেকে আসছে — যা পান্ডাসের জন্য নয়, বরং সেই শাখার দায়িত্বপ্রাপ্ত ব্যক্তিদের জিজ্ঞাসা করার মতো একটি বিষয়।

সংখ্যার বদলে অনুপাত বা ভাগ হিসেবে

আটটি সারির ক্ষেত্রে সাধারণ সংখ্যাই যথেষ্ট। কিন্তু আশি হাজার সারির ক্ষেত্রে "৩১২টি অনুপস্থিত" তথ্যের বিশেষ কোনো অর্থ থাকে না যতক্ষণ না আপনি জানেন যে এটি মাত্র ০.৪%। একটি True/False কলামের .mean() হলো True-এর অনুপাত:

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

branch, quantity এবং price-এর আটটি সেলের মধ্যে একটি সেল অনুপস্থিত — প্রতিটিতে ১২.৫% করে। ৯০% খালি একটি কলাম আর ০.১% খালি একটি কলাম সম্পূর্ণ ভিন্ন ধরনের সিদ্ধান্তের দাবি রাখে, আর এই পদ্ধতিতেই আপনি তাদের আলাদা করবেন।


লুকানো অনুপস্থিত মান: যখন ফাঁকটি টেক্সট হিসেবে লেখা থাকে

isna() একটি অনুপযোগী দাম গুনেছিল, কিন্তু আসলে দুটি দামই ব্যবহারের অযোগ্য। দ্বিতীয়টি হলো ১০০৬ নম্বর অর্ডারের -। পান্ডাসের কাছে - কোনো অনুপস্থিত মান নয় — এটি একটি নিখুঁত টেক্সট। আর সংখ্যার একটি কলামে একটিমাত্র টেক্সট থাকলেই পুরো কলামের ধরন str বানিয়ে ফেলার জন্য তা যথেষ্ট, যা ঠিক info()-তে দেখা গিয়েছিল।

একটি কলামে আসলে কী আছে তা যেভাবে খুঁজে বের করবেন:

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

প্রতিটি মান কোটেশনের ভেতর আছে — '15.0' সহ সবই টেক্সট। আর পাশাপাশি দুই ধরনের ফাঁক দেখা যাচ্ছে: nan, যা পান্ডাস চিনতে পেরেছে, এবং '-', যা সে চিনতে পারেনি।

কেন N/A পরিণত হলো nan-এ কিন্তু - হলো না? কারণ read_csv-এর ভেতর আগে থেকেই কিছু স্ট্রিংয়ের তালিকা তৈরি করা থাকে যেগুলোকে সে অনুপস্থিত হিসেবে গণ্য করে: খালি সেল, NA, N/A, n/a, NaN, null, None এবং আরও কয়েকটি। -, ?, missing, 0 এবং unknown সেই তালিকায় নেই। যিনি ডেটা এন্ট্রি করেছেন তিনি "অজানা" বোঝাতে কী ব্যবহার করেছিলেন, তার ওপরই নির্ভর করে আপনি কোন ধরনের ফাঁক পাবেন।

লুকানো মার্কারগুলোকে আসল NaN-এ রূপান্তর করার দুটি উপায় আছে।

সমাধান ১: read_csv-কে মার্কারের কথা জানিয়ে দিন — na_values=

আপনি যদি মার্কারটি আগেই জানেন, তবে সবচেয়ে পরিচ্ছন্ন সমাধান হলো ফাইল পড়ার মুহূর্তেই তা ঠিক করা। na_values= অন্তর্নির্মিত তালিকার সাথে আপনার দেওয়া স্ট্রিংগুলো যোগ করে দেয়:

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

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

আগের গণনার সাথে তুলনা করে দেখুন। branch এবং quantity অপরিবর্তিত রয়েছে — na_values= তালিকার সাথে যোগ করে, আগের তালিকা মুছে ফেলে না, তাই খালি সেল এবং N/A এখনও স্বীকৃত। যা বদলেছে তা হলো price: এটি এখন float64, এবং এটি 2 টি অনুপস্থিত মান রিপোর্ট করছে — N/A এবং - উভয়ই এখন আসল NaN। কলামটি আবার সংখ্যায় ফিরে এসেছে, ফলে এতে পাটিগণিত করার অর্থ সত্যিই পাটিগণিত হওয়া।

মনে রাখবেন, na_values=["-"] প্রতিটি কলামের ওপর প্রযোজ্য হয়। এখানে আমরা ঠিক এটাই চাই। কিন্তু - যদি অন্য কোনো কলামে বৈধ মান হতে পারত — ধরা যাক কোনো প্রোডাক্ট কোড — তবে একটি ডিকশনারি পাস করুন, যেমন na_values={"price": ["-"]}, তাহলে কেবল price কলামটিই প্রভাবিত হবে।

সমাধান ২: ফাইল পড়ার পর রূপান্তর করুন — pd.to_numeric(errors="coerce")

কখনও কখনও আপনি আগে থেকে জানেন না যে কোনো কলামে কী অদ্ভুত স্ট্রিং রয়েছে, কিংবা ফাইলটি অন্য কারো কোড দিয়ে পড়া হয়েছে। তখন পড়ার পরে কলামটি রূপান্তর করতে হয়। প্রথমে দেখুন সাধারণ রূপান্তর কী করে:

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

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

সেই ত্রুটিটি কিন্তু কার্যকর: এটি অপরাধী মানটি এবং তার অবস্থান উল্লেখ করে দেয়। errors="coerce" যোগ করার অর্থ হলো "যেকোনো কিছু যাকে তুমি সংখ্যায় রূপান্তর করতে পারবে না, তাকে NaN বানিয়ে দাও":

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

টেবিলে বাস্তবে পরিবর্তন আনতে ফলাফলটি পুনরায় কলামে অ্যাসাইন করুন — to_numeric একটি নতুন Series ফেরত দেয় এবং raw-কে অপরিবর্তিত রাখে:

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

কোন সমাধানটি বেছে নেবেন

errors="coerce" যেমন শক্তিশালী, তেমনই কিছুটা অনমনীয় বা ভোঁতা। এটি সংখ্যা নয় এমন যেকোনো কিছুকে NaN-এ পরিণত করে — এমনকি কোনো আসল দাম যদি টাইপিং ভুলে 1O0 (ইংরেজি O অক্ষর) বা হাজার বোঝাতে কমা দিয়ে 1,200 লেখা থাকে, তাকেও। এগুলো কিন্তু অনুপস্থিত মান নয়; এগুলো ভুল টাইপ করা মান, আর জোর করে কোয়ার্স (coerce) করলে তথ্যগুলো নিঃশব্দে হারিয়ে যায়।

তাই অভ্যাসটি হবে: রূপান্তরের আগে গণনা করুন, রূপান্তরের পরে গণনা করুন, এবং পার্থক্যের কারণ ব্যাখ্যা করুন।

  • মার্কারটি জানা থাকলে (-, ?, missing): read_csv-তে na_values= ব্যবহার করুন। এটি নিখুঁত — কেবল সেই নির্দিষ্ট স্ট্রিংটিই অনুপস্থিত মানে রূপান্তর হয়।
  • অজানা আবর্জনা থাকলে, এবং কলামটি অবশ্যই সংখ্যা হতে হলে: pd.to_numeric(errors="coerce") ব্যবহার করুন। এটি সবকিছুকে ধরে ফেলে, তাই এটি কী কী ধরেছে তা পরে যাচাই করে নিন।
  • কিছু মান টাইপো হলে যা আপনি মেরামত করতে পারেন: প্রথমে unique() দিয়ে দেখে নিন, সেগুলো সংশোধন করুন, তারপর রূপান্তর করুন। সরাসরি কোয়ার্স করলে আসল ডেটা নষ্ট হয়ে যাবে।

কোয়ার্স করার আগে raw["price"].unique() ঠিক একটি নন-নিউমেরিক স্ট্রিং ('-') এবং একটি nan দেখিয়েছিল। কোয়ার্স করার পরে সেখানে ঠিক ২টি NaN তৈরি হয়েছে। দুটি সংখ্যা মিলে গেছে, কাজেই অন্য কোনো তথ্য নষ্ট হয়নি। ডেটা পরিষ্কার করা এবং ডেটা নষ্ট করার মধ্যে আসল পার্থক্য গড়ে দেয় এই সামান্য যাচাইটিই।

এরপর থেকে আমরা na_values=["-"] দিয়ে পড়া সংস্করণটি ব্যবহার করব।


গণনার সময় পান্ডাস NaN-কে কীভাবে দেখে

এখন যেহেতু ফাঁকগুলো আসল NaN-এ রূপ নিয়েছে, পান্ডাস সেগুলোর সাথে কী আচরণ করে তা আপনার জানা দরকার — কারণ সে নিজে থেকে থেমে গিয়ে আপনাকে কিছু জিজ্ঞেস করবে না।

কলামের সারসংক্ষেপ NaN-কে বাদ দেয়

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

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

মনে রাখার মতো তিনটি আচরণ:

  • len সারি গণনা করে; count() মান গণনা করে। এ দুটির ব্যবধানই হলো ফাঁকের সংখ্যা। এটি info()-র Non-Null Count-এর ধারণার মতোই।
  • sum() NaN-কে বাদ দিয়ে যোগ করে। এটি জানা সাতটি quantity যোগ করেছে। এটি যুক্তিযুক্ত — তবে এর অর্থ হলো "76" হলো কেবল জানা পরিমাণের যোগফল, সব অর্ডারের নয়।
  • mean() ভাগ করে count() দিয়ে, len দিয়ে নয়। গড় হিসাব করা হয় জানা সাতটি মানের ওপর। সাধারণত আপনি এটাই চান; ফাঁকা ঘরকে 0 ধরে নিলে অকারণে গড় অনেক নিচে নেমে যেত।

আপনি যদি চান যে কোনো কিছু অনুপস্থিত থাকলে আপনাকে সতর্ক করা হোক, তবে skipna=False যোগ করুন; তাহলে যেকোনো একটি মান NaN হলেই পুরো ফলাফল NaN হয়ে যাবে:

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

কলামের মধ্যকার পাটিগণিত NaN-কে ছড়িয়ে দেয়

সারসংক্ষেপ ফাঁকগুলোকে এড়িয়ে যায়; কিন্তু পাটিগণিত ফাঁকগুলোকে ছড়িয়ে দেয়। NaN-এর সাথে যা কিছুই যুক্ত হোক না কেন, ফলাফল NaN হয়, কারণ "অজানা দামের সাথে ৭ গুণ করলে" ফলাফলটিও অজানা:

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

এই দুটি আচরণকে একসাথে মেলালেই অধ্যায়ের শুরুর ফাঁদটি পরিষ্কার দেখা যায়। revenue.sum() কোনো সতর্কতা ছাড়াই 2900.0 প্রিন্ট করেছিল — কিন্তু আটটি অর্ডারের মধ্যে তিনটি অর্ডার এতে কোনো অবদানই রাখেনি। ১০০২ নম্বর অর্ডারে quantity নেই, ১০০৪ ও ১০০৬ নম্বরে price নেই। এই মোট হিসাবটি তখনই সৎ ও সঠিক হবে যখন আপনি বলবেন "৮টি অর্ডারের মধ্যে ৫টি"। এই কারণেই আমাদের পরিকল্পনায় প্রতিটি মোট হিসাবের পাশে গণনার সংখ্যা রাখার কথা বলা হয়েছিল।


অনুপস্থিত মান মুছে ফেলা: dropna()

dropna() এমন সারিগুলো মুছে ফেলে যাতে NaN রয়েছে। কোনো আর্গুমেন্ট না দিলে এটি যেকোনো কলামে যেকোনো ফাঁক থাকা প্রতিটি সারি মুছে ফেলে:

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

আটটির মধ্যে তিনটি সারি উধাও হয়ে গেল — আর ১০০৪ নম্বর অর্ডারটি বাদ গেল কারণ তার branch অনুপস্থিত ছিল, অথচ পরিমাণ বা quantity সম্পর্কিত প্রশ্নের জন্য এটি একটি নিখুঁত সারি ছিল। সরাসরি খালি dropna() ব্যবহারের সমস্যা এটিই: এটি "অনিখুঁত যেকোনো কিছু মুছে ফেলো" প্রশ্নের উত্তর দেয়, যা খুব কম ক্ষেত্রেই আমাদের মূল প্রশ্ন হয়।

subset= — কেবল সেই কলামগুলো যা প্রশ্নের জন্য দরকার

কোন কলামগুলো অবশ্যই উপস্থিত থাকতে হবে তা নির্দিষ্ট করে দিন:

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

কেবল ১০০২ নম্বর অর্ডারটি বাদ গেছে — একমাত্র সেই সারি যেখানে quantity সত্যিই অজানা। মোট আয়ের (revenue) জন্য দুটি কলামই প্রয়োজন:

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

ইনডেক্সটির দিকে লক্ষ্য করুন: 0, 2, 4, 6, 7। ফিল্টারিংয়ের মতো dropna()-ও মূল লেবেলগুলোকে অক্ষত রাখে, যাতে আপনি সবসময় কোনো সারিকে মূল ফাইলে মিলিয়ে দেখতে পারেন। আপনার যদি আবার 0, 1, 2… প্রয়োজন হয়, তবে শেষে .reset_index(drop=True) যোগ করুন।

how="all" — কেবল যে সারিগুলো পুরোপুরি ফাঁকা

এক্সপোর্ট করা স্প্রেডশিটগুলোর শেষে প্রায়ই এমন কিছু সারি থাকে যার প্রতিটি কলামই ফাঁকা। how="all" কেবল সেই সারিগুলোকে মুছে ফেলে এবং অন্তত একটি মান আছে এমন যেকোনো সারি রেখে দেয়:

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

এখানে কিছুই মোছা হয়নি — আমাদের কোনো সারিই পুরোপুরি ফাঁকা নয় — যা আপনি নিশ্চিত করতে চেয়েছিলেন।

dropna() একটি নতুন টেবিল ফেরত দেয়

বেশিরভাগ পান্ডাস মেথডের মতো dropna() মূল orders-কে পরিবর্তন করে না; এটি আপনাকে একটি নতুন DataFrame উপহার দেয়। আপনি যদি একটি পৃথক লাইনে কেবল orders.dropna() লিখে রেখে দেন, তবে orders-এর কিছুই হবে না। ফলাফলটি একটি নতুন নামে সংরক্ষণ করুন — বিশেষ করে যদি আপনার পুরো টেবিলটি এখনও প্রয়োজন হয়, যা আমাদের মতো রিপোর্টে দরকার হবেই (কতগুলো সারি বাদ পড়ল তা বলার জন্য)।


অনুপস্থিত মান প্রতিস্থাপন করা: fillna()

কখনও কখনও কোনো ফাঁকের একটি যুক্তিযুক্ত বিকল্প মান থাকে। যেখানে NaN ছিল, fillna() সেখানে একটি নতুন মান বসিয়ে দেয়।

একটি কলামের জন্য একটি মান

অনুপস্থিত শাখাটি সবচেয়ে স্পষ্ট উদাহরণ। অর্ডারটি ঘটেছে এবং এর টাকা বাস্তব; আমরা কেবল জানি না কোন শাখা এটি সম্পন্ন করেছে। একটি লেবেল দিয়ে এটি পূরণ করলে সারিটি অক্ষত থাকে এবং পরবর্তীতে শাখাভিত্তিক যেকোনো গণনায় ফাঁকটি স্পষ্টভাবে দৃশ্যমান থাকে:

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

সেই লাইনটির গঠন গুরুত্বপূর্ণ: কলামটি নিন, fillna করুন, এবং একই কলামে পুনরায় সংরক্ষণ করুন। fillna একটি নতুন Series ফেরত দেয়; সামনে orders["branch"] = না লিখলে কোনো কিছুই জমা থাকবে না।

ভিন্ন ভিন্ন কলামের জন্য ভিন্ন মান: একটি dict

একসাথে একাধিক কলামকে তাদের নিজস্ব মান দিয়ে পূরণ করতে {column: value} আকারের একটি ডিকশনারি পাস করুন। যেসব কলামের নাম উল্লেখ করা হবে না সেগুলো অপরিবর্তিত থাকবে:

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

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

price-এ এখনও তার দুটি ফাঁক রয়ে গেছে, কারণ আমরা এর নাম উল্লেখ করিনি। orders.fillna(0)-এর চেয়ে ডিকশনারি ব্যবহারের সুবিধা এখানেই; কারণ orders.fillna(0) লিখলে দাম সহ প্রতিটি কলাম 0 দিয়ে পূরণ হয়ে যেত — আর ০ টাকা দাম মানে তো একটি ব্যাগ ফ্রিতে দিয়ে দেওয়া।

মান পূরণ করলে উত্তর বদলে যায়

পরিমাণকে 0 দিয়ে পূরণ করা নিরীহ মনে হতে পারে। গড়গুলো তুলনা করে দেখুন:

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

একটি কলাম থেকেই তিনটি ভিন্ন ভিন্ন গড় তৈরি হলো, আর একমাত্র পার্থক্য হলো অজানা একটি পরিমাণ বলতে আপনি কী ধরে নিয়েছেন:

  • 0 বলে "এই অর্ডারে কিছুই বিক্রি হয়নি"। ১০০২ নম্বর নোটবুকের অর্ডারের জন্য, যা নিশ্চিতভাবেই কিছু না কিছু বিক্রি করেছিল, এটি অসত্য, এবং এটি গড়কে টেনে নিচে নামিয়ে দেয়।
  • মধ্যমা বা Median (এখানে ৭) বলে "একটি সাধারণ বা গড়পড়তা অর্ডারের অনুমান ধরো"। এটি গড়কে মোটামুটি কাছাকাছি রাখে, কিন্তু মোট হিসাবে এখন ৭টি নোটবুক যুক্ত হয়ে যায় যা কেউ আসলেই গোনেনি।
  • ফাঁক এড়িয়ে যাওয়া বলে "আমরা জানি না, তাই হিসাবে ধরব না"। গড়টি হিসাব করা হয় জানা সাতটি অর্ডারের ওপর।

সাধারণভাবে এগুলোর কোনটিই একচেটিয়াভাবে "সঠিক" নয়। প্রতিটি বিকল্প সামান্য ভিন্ন প্রশ্নের উত্তর দেয়। এই কারণেই সিদ্ধান্তটি আগে থেকেই আপনার পরিকল্পনায় লিখে রাখা উচিত, হাতের কাছে পাওয়া যেকোনো ডিফল্ট অন্ধভাবে বেছে নেওয়ার বদলে।

একটি সাধারণ নিয়ম যা বেশিরভাগ সময় কাজে আসে:

  • একটি লেবেল বা নাম (branch, category, name): "Unknown" বা অনুরূপ কিছু দিয়ে পূরণ করুন। সারিটি টিকে থাকে, এবং ফাঁকটি নিজস্ব একটি গ্রুপ হিসেবে দৃশ্যমান থাকে।
  • এমন গণনা যেখানে খালি থাকা মানে সত্যিই কিছু নেই (ফেরত পণ্য, অভিযোগ): 0 দিয়ে পূরণ করুন — তবে কেবল তখনই যখন উৎস ডেটাতে খালি ঘর রাখার প্রকৃত অর্থ সত্যিই শূন্য হয়।
  • পরিমাপ বা টাকা (বিক্রির পরিমাণ, দাম, নম্বর): NaN রেখে দিন, গণনায় একে অন্তর্ভুক্ত করবেন না, এবং কতগুলো বাদ দেওয়া হলো তা উল্লেখ করুন। কাল্পনিক সংখ্যা কাল্পনিক মোটে রূপ নেয়।
  • প্রশ্নের জন্য প্রয়োজনীয় কলাম, কিন্তু বেশিরভাগই উপস্থিত: কেবল সেই প্রশ্নের জন্যই dropna(subset=[...]) করুন। সামান্য কিছু ক্ষতি, কিন্তু উত্তরটি হবে সৎ ও নির্ভুল।

পূর্ণসংখ্যা ফিরিয়ে আনা

মান পূরণ করার পর quantity-তে কোনো ফাঁক নেই, কিন্তু এটি এখনও float64 — পান্ডাস নিজে থেকে এটিকে আবার পূর্ণসংখ্যায় ফিরিয়ে নেয় না। একবার কোনো NaN না থাকলে, astype(int) সফলভাবে কাজ করে:

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

পূরণ করার আগে এটি চালালে ব্যর্থ হবে, কারণ "অজানা" অর্থ প্রকাশ করে এমন কোনো পূর্ণসংখ্যা পাইথনে নেই — নিচে "কিছু ভাঙা অবস্থা ও তার সমাধান" দেখুন।


একটি সম্পূর্ণ উদাহরণ

"কোড লেখার আগে" অংশের পরিকল্পনাটিকে clean_orders.py একটি একক প্রোগ্রামে সাজিয়ে নিয়েছে: পরিচিত মার্কারসহ ফাইলটি পড়ুন, প্রতিটি ফাঁক রিপোর্ট করুন, শাখা পূরণ করুন, যেসব সারি হিসাব করা সম্ভব নয় সেগুলো বাদ দিন, এবং মোট হিসাবের সাথে তা কয়টি অর্ডার কাভার করে তা পরিষ্কারভাবে দেখান।

python
import pandas as pd

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

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

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

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

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

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

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

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

কেন কোডটি এভাবে লেখা হয়েছে:

  • na_values=["-"] একদম প্রথম লাইনেই রয়েছে। unique() দেখে মার্কারটি আগেই একবার চিহ্নিত করা হয়েছিল। read_csv-তে এটি বসানোর অর্থ হলো কলামটি শুরু থেকেই সংখ্যা হিসেবে থাকে, এবং যে কেউ কোডটি পড়লেই বুঝতে পারেন ফাইলটিতে কোন মার্কার ব্যবহার করা হয়েছিল।
  • missing[missing > 0] কেবল সেই কলামগুলোই প্রিন্ট করে যেগুলোতে ফাঁক রয়েছে। চল্লিশটি কলামের ফাইলে এটি একটি পাঠযোগ্য রিপোর্ট এবং শূন্যের পাহাড়ের মধ্যে পার্থক্য গড়ে দেয়।
  • শাখা পূরণ করা হয়েছে, কিন্তু সংখ্যাগুলো পূরণ করা হয়নি। এটি পরিকল্পনার সেই সিদ্ধান্তের তালিকারই কোড রূপ। কোনো পাঠক এর সাথে দ্বিমত পোষণ করতে পারেন, কিন্তু তিনি স্পষ্ট দেখতে পাচ্ছেন কী করা হয়েছে।
  • dropna(subset=...) আয়ের জন্য প্রয়োজনীয় দুটি কলামের নাম উল্লেখ করে, যাতে ১০০৪ নম্বর অর্ডারটি তার অনুপস্থিত দামের জন্য বাদ পড়ে, অনুপস্থিত শাখার জন্য নয়।
  • বাদ পড়া সারিগুলো কেবল সংখ্যায় না দেখিয়ে বিস্তারিত প্রিন্ট করা হয়েছে। "৩টি অর্ডার হিসাবে ধরা হয়নি" কথাটি স্বভাবতই প্রশ্ন তোলে "কোনগুলো?" — রিপোর্টে আগেই তার উত্তর দিয়ে দিন। এই তিনটি অর্ডারের তালিকা নিয়ে কেউ একজন সরাসরি শাখায় গিয়ে ভুল সংশোধন করতে পারবেন।
  • "Total quantity (known)" এবং "(counted)" লেবেলগুলো স্পষ্ট করে দেয় সংখ্যাগুলো আসলে কী। দুটিই ন্যূনতম সীমা; এই লেবেলগুলো কাউকে এগুলোকে সম্পূর্ণ হিসাব ভেবে ভুল করা থেকে বিরত রাখে।

কিছু ভাঙা অবস্থা ও তার সমাধান

TypeError: Cannot perform reduction 'mean' with string dtype আপনি এমন একটি কলামের গড় চেয়েছেন যা পান্ডাস টেক্সট হিসেবে পড়েছে — সাধারণত কারণ একটি সেলে -, ? বা missing-এর মতো মার্কার রয়েছে। অপরাধী খুঁজে পেতে df["col"].unique() চালান, তারপর হয় na_values=["-"] দিয়ে পুনরায় পড়ুন অথবা pd.to_numeric(df["col"], errors="coerce") দিয়ে রূপান্তর করুন।

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

sum() সংখ্যার দীর্ঘ এক স্ট্রিং ফেরত দিয়েছে, যেমন 15.060.0850.0... একই কারণ, কিন্তু লক্ষণটি আরও খারাপ: কোনো টেক্সট কলামে sum() চালালে সে সংখ্যা যোগ করার বদলে স্ট্রিংগুলোকে জোড়া লাগিয়ে দেয়, আর কোনো এররও দেয় না। এরকম দেখতে যেকোনো মোট হিসাব এসেছে একটি str কলাম থেকে। কোনো কিছু যোগ করার আগে df.dtypes পরীক্ষা করে নিন।

ValueError: Unable to parse string "-" at position 5 pd.to_numeric এমন টেক্সট পেয়েছে যা সে রূপান্তর করতে পারেনি, এবং সে স্পষ্টভাবে বলে দিয়েছে কী এবং কোথায়। হয় errors="coerce" যোগ করুন (unique() দিয়ে আগে দেখে নিন যে অদ্ভুত মানগুলো সত্যিই "অজানা" মার্কার, কোনো টাইপো নয়), অথবা প্রথমে টাইপো ঠিক করুন।

(df["col"] == np.nan).sum() বলছে 0, কিন্তু ফাঁক রয়েছে NaN কোনো কিছুর সমান নয়, এমনকি নিজেরও নয়, তাই == np.nan কোনো সেলের সাথেই মেলে না। ব্যবহার করুন df["col"].isna()।

IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer আপনি এমন একটি কলামে astype(int) কল করেছেন যাতে এখনও NaN রয়েছে। "অজানা" অর্থ প্রকাশ করে এমন কোনো পূর্ণসংখ্যা নেই। প্রথমে ফাঁকগুলো পূরণ করুন বা বাদ দিন, তারপর রূপান্তর করুন:

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

(পান্ডাসের নিজস্ব বার্তায় "integer.Replace" এবং "typethat"-এর মাঝের স্পেসগুলো বাদ পড়েছে।) বার্তায় বড় হাতের I দিয়ে 'Int64'-এর কথাও বলা হয়েছে: এটি পান্ডাসের নিজস্ব একটি ইন্টিজার ধরন যা অনুপস্থিত মান ধারণ করতে পারে। এটি কার্যকর, তবে প্রথমে পূরণ করা বা বাদ দেওয়াই সহজ ও প্রচলিত সমাধান।

ChainedAssignmentError — এবং ফাঁকটি এখনও সেখানে রয়ে গেছে আপনি পুরোনো অনেক টিউটোরিয়ালে দেখতে পাওয়া inplace=True পদ্ধতিটি লিখেছেন:

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

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

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

কাউন্টে এখনও দেখাচ্ছে 1: কিছুই পূরণ হয়নি। পান্ডাস ৩-এ orders["quantity"] সবসময় নিজস্ব কপি হিসেবে কাজ করে (Copy-on-Write), তাই একে "in place" পূরণ করার অর্থ হলো সেই কপিটিকে পূরণ করা, যা পরে ছুড়ে ফেলে দেওয়া হয়। পুরোনো পান্ডাসে কখনও কখনও এটি কাজ করত, যার কারণে ইন্টারনেটের সর্বত্র এই প্যাটার্নটি দেখা যায়। এর বদলে পরিষ্কারভাবে অ্যাসাইনমেন্ট লিখুন — এটি প্রতিটি সংস্করণে কাজ করে এবং স্পষ্টভাবে বলে দেয় কী পরিবর্তিত হচ্ছে:

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

dropna() আপনার প্রত্যাশার চেয়ে অনেক বেশি সারি মুছে ফেলেছে খালি dropna() যেকোনো কলামে ফাঁক থাকার কারণে পুরো সারি মুছে ফেলে — এমনকি বেশিরভাগ অংশ খালি থাকা notes বা discount কলামের জন্যও যা আপনি হয়তো ব্যবহারই করছেন না। কোন কলামে অনেক ফাঁক আছে তা দেখতে df.isna().sum() প্রিন্ট করুন, তারপর কেবল আপনার প্রশ্নের জন্য প্রয়োজনীয় কলামগুলো দিয়ে dropna(subset=[...]) ব্যবহার করুন।

TypeError: can't multiply sequence by non-int of type 'float' আপনি একটি সংখ্যার কলামকে একটি টেক্সট কলাম দিয়ে গুণ করেছেন (price যখন str, তখন quantity * price করেছেন)। সমাধান প্রথম ত্রুটির মতোই: আগে price-কে সংখ্যায় রূপান্তর করুন।