অনুপস্থিত মান — ফাঁকগুলো খুঁজে বের করা এবং সেগুলোর অর্থ বোঝা
একটি টেবিলের প্রতিটি ফাঁকা ঘর খুঁজে বের করা (এমনকি - বা ?-এর মতো লেখার আড়ালে লুকানো ফাঁকও), NaN কীভাবে যোগফল, গড় ও ডেটার ধরন বদলে দেয় তা বোঝা, এবং প্রতি কলামের জন্য বুঝে-শুনে সিদ্ধান্ত নেওয়া — ফাঁকটি বাদ দেবেন, পূরণ করবেন নাকি রেখে দেবেন।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
মার্চের প্রথম সপ্তাহের দোকানের অর্ডারের হিসাব তিনটি শাখায় হাতে লিখে রাখা হয়েছিল এবং orders_raw.csv হিসেবে এক্সপোর্ট করা হয়েছিল। মালিক সন্ধ্যার মধ্যেই দুটি হিসাব জানতে চেয়েছেন: মোট কতগুলো জিনিস বিক্রি হয়েছে, আর কত টাকা আয় হয়েছে।
আপনি ফাইলটি পড়লেন এবং আগের অধ্যায়গুলোর অভ্যাসবশত স্কেল বা পরিসর বোঝার জন্য প্রথমেই দামের কলামটি যোগ করলেন:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0এটি কোনো সংখ্যা নয়। এটি প্রতিটি দাম টেক্সট হিসেবে পাশাপাশি জোড়া লেগে যাওয়া এক অদ্ভুত লেখা — অথচ পান্ডাস আপনাকে সতর্ক করার জন্য কোনো ত্রুটিও দেয়নি। এবার পরিমাণের কলামটি দেখুন:
print(orders["quantity"].sum())
print(orders["quantity"].mean())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 নামে একটি ফাইল বানান এবং ঠিক এই লাইনগুলো লিখুন। ফাঁকগুলো ইচ্ছে করেই রাখা হয়েছে — তৃতীয়, পঞ্চম এবং সপ্তম লাইনের দিকে মনোযোগ দিয়ে তাকান:
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() একসাথে প্রতিটি কলাম সম্পর্কে একবারে জানিয়ে দেয়:
print(orders.shape)
orders.info()(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-nullstrপ্রত্যাশিত ছিল, এসেছে ৭টি non-null। একটি শাখার নাম অনুপস্থিত।quantity— ৮টি non-nullint64প্রত্যাশিত ছিল, এসেছে ৭টি non-nullfloat64। একটি quantity অনুপস্থিত, আর ঠিক সেই কারণেই এটিfloat64হয়ে গেছে (নিচে ব্যাখ্যা করা হয়েছে)।price— ৮টি non-nullfloat64প্রত্যাশিত ছিল, এসেছে ৭টি non-nullstr। একটি দাম অনুপস্থিত এবং কলামের ভেতর কিছু টেক্সট ঢুকে পড়েছে।
এই তালিকার দুটি বিষয় তথ্যের চেয়ে ক্লু বা সংকেত হিসেবে বেশি মূল্যবান। একটি পূর্ণসংখ্যার কলামে float64 দেখা যাওয়ার মানে প্রায় সবসময়ই একটি ফাঁক। আর একটি সংখ্যার কলাম str দেখাচ্ছে মানেই তাতে টেক্সট মিশে গেছে — আর সংখ্যার কলামে টেক্সট থাকার মানে সাধারণত এটি একটি লুকানো "অনুপস্থিত" মার্কার।
৩. আপনার কাঙ্ক্ষিত আউটপুট কেমন হবে তার একটি খসড়া তৈরি করুন। অনেকটা এরকম:
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 বসিয়ে আপনি নিজেও একটি বানাতে পারেন:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)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 কলাম ডেটার ধরন না বদলেই অনুপস্থিত মান ধরে রাখতে পারে:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strএটি এখনও NaN হিসেবেই ছাপা হয় এবং dtype থাকে str। তাই পান্ডাস ৩-এ আপনি সংখ্যা এবং টেক্সট উভয় ধরনের কলামেই NaN-এর দেখা পাবেন, এবং একই হাতিয়ার উভয় জায়গায় এটি খুঁজে বের করে।
NaN কোনো কিছুর সমান নয় — এমনকি নিজেরও না
অনুপস্থিত মান সম্পর্কে এটিই সবচেয়ে আশ্চর্যজনক সত্য, এবং এটি তাদের খুঁজে বের করার সবচেয়ে স্বাভাবিক উপায়টিকে ভেঙে দেয়:
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())False
0NaN-এর অর্থ হলো "অজানা কোনো একটি মান"। দুটি অজানা মান সমান কি না তা নিশ্চিত জানা নেই, তাই তুলনাটি প্রতিবারই False বলে। ফলে == np.nan এমন একটি পরীক্ষা যা কখনোই কোনো কিছুর সাথে মেলে না: branch কলামে ফাঁক আছে, অথচ গণনায় দেখাচ্ছে 0। এর বদলে সরাসরি পান্ডাসকে জিজ্ঞেস করুন।
কখনোই==দিয়ে অনুপস্থিত মান খুঁজবেন না।== np.nanকোনো ত্রুটি ছাড়াই চলে এবং কখনোই কিছু খুঁজে পায় না, ফলে ফাঁকে ভরা কলামকেও সম্পূর্ণ মনে হয়। ব্যবহার করুনisna()।
অনুপস্থিত মান খুঁজে বের করা: isna()
== যা পারে না, isna() ঠিক সেই প্রশ্নটিই করে: "এই সেলটি কি অনুপস্থিত?" একটি কলামের ওপর এটি True/False-এর একটি কলাম ফেরত দেয় — অর্থাৎ একটি মাস্ক, ঠিক সারি ছাঁকার অধ্যায়ের মাস্কগুলোর মতো:
print(orders["branch"].isna())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() সেই গণনাটি এনে দেয়:
print(orders.isna().sum())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) প্রতিটি সারির কাছে জানতে চায়, "এই সারিতে কি কোনো কিছু অনুপস্থিত?":
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)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 NaNaxis=1-এর অর্থ হলো প্রতিটি কলাম ধরে নিচে নামার বদলে "প্রতিটি সারির আড়াআড়ি দেখা"। আর লক্ষ্য করুন কে এই তালিকায় নেই: ১০০৬ নম্বর অর্ডার, যার দাম ছিল -। পান্ডাস যতদূর জানে, সেই সারিটি সম্পূর্ণ। এই বিষয়টি মনে রাখুন। বাস্তব সারিগুলো দেখার জন্য বাড়তি এই লাইনটি লেখা খুবই সার্থক: এটি দেখলেই আপনি ধরতে পারবেন যে, ধরা যাক, প্রতিটি অনুপস্থিত quantity একই শাখা থেকে আসছে — যা পান্ডাসের জন্য নয়, বরং সেই শাখার দায়িত্বপ্রাপ্ত ব্যক্তিদের জিজ্ঞাসা করার মতো একটি বিষয়।
সংখ্যার বদলে অনুপাত বা ভাগ হিসেবে
আটটি সারির ক্ষেত্রে সাধারণ সংখ্যাই যথেষ্ট। কিন্তু আশি হাজার সারির ক্ষেত্রে "৩১২টি অনুপস্থিত" তথ্যের বিশেষ কোনো অর্থ থাকে না যতক্ষণ না আপনি জানেন যে এটি মাত্র ০.৪%। একটি True/False কলামের .mean() হলো True-এর অনুপাত:
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64branch, quantity এবং price-এর আটটি সেলের মধ্যে একটি সেল অনুপস্থিত — প্রতিটিতে ১২.৫% করে। ৯০% খালি একটি কলাম আর ০.১% খালি একটি কলাম সম্পূর্ণ ভিন্ন ধরনের সিদ্ধান্তের দাবি রাখে, আর এই পদ্ধতিতেই আপনি তাদের আলাদা করবেন।
লুকানো অনুপস্থিত মান: যখন ফাঁকটি টেক্সট হিসেবে লেখা থাকে
isna() একটি অনুপযোগী দাম গুনেছিল, কিন্তু আসলে দুটি দামই ব্যবহারের অযোগ্য। দ্বিতীয়টি হলো ১০০৬ নম্বর অর্ডারের -। পান্ডাসের কাছে - কোনো অনুপস্থিত মান নয় — এটি একটি নিখুঁত টেক্সট। আর সংখ্যার একটি কলামে একটিমাত্র টেক্সট থাকলেই পুরো কলামের ধরন str বানিয়ে ফেলার জন্য তা যথেষ্ট, যা ঠিক info()-তে দেখা গিয়েছিল।
একটি কলামে আসলে কী আছে তা যেভাবে খুঁজে বের করবেন:
print(orders["price"].unique())<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= অন্তর্নির্মিত তালিকার সাথে আপনার দেওয়া স্ট্রিংগুলো যোগ করে দেয়:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())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")
কখনও কখনও আপনি আগে থেকে জানেন না যে কোনো কলামে কী অদ্ভুত স্ট্রিং রয়েছে, কিংবা ফাইলটি অন্য কারো কোড দিয়ে পড়া হয়েছে। তখন পড়ার পরে কলামটি রূপান্তর করতে হয়। প্রথমে দেখুন সাধারণ রূপান্তর কী করে:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5সেই ত্রুটিটি কিন্তু কার্যকর: এটি অপরাধী মানটি এবং তার অবস্থান উল্লেখ করে দেয়। errors="coerce" যোগ করার অর্থ হলো "যেকোনো কিছু যাকে তুমি সংখ্যায় রূপান্তর করতে পারবে না, তাকে NaN বানিয়ে দাও":
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)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-কে অপরিবর্তিত রাখে:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())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-কে বাদ দেয়
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())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 হয়ে যাবে:
print(q.sum(skipna=False))nanকলামের মধ্যকার পাটিগণিত NaN-কে ছড়িয়ে দেয়
সারসংক্ষেপ ফাঁকগুলোকে এড়িয়ে যায়; কিন্তু পাটিগণিত ফাঁকগুলোকে ছড়িয়ে দেয়। NaN-এর সাথে যা কিছুই যুক্ত হোক না কেন, ফলাফল NaN হয়, কারণ "অজানা দামের সাথে ৭ গুণ করলে" ফলাফলটিও অজানা:
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))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 রয়েছে। কোনো আর্গুমেন্ট না দিলে এটি যেকোনো কলামে যেকোনো ফাঁক থাকা প্রতিটি সারি মুছে ফেলে:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)আটটির মধ্যে তিনটি সারি উধাও হয়ে গেল — আর ১০০৪ নম্বর অর্ডারটি বাদ গেল কারণ তার branch অনুপস্থিত ছিল, অথচ পরিমাণ বা quantity সম্পর্কিত প্রশ্নের জন্য এটি একটি নিখুঁত সারি ছিল। সরাসরি খালি dropna() ব্যবহারের সমস্যা এটিই: এটি "অনিখুঁত যেকোনো কিছু মুছে ফেলো" প্রশ্নের উত্তর দেয়, যা খুব কম ক্ষেত্রেই আমাদের মূল প্রশ্ন হয়।
subset= — কেবল সেই কলামগুলো যা প্রশ্নের জন্য দরকার
কোন কলামগুলো অবশ্যই উপস্থিত থাকতে হবে তা নির্দিষ্ট করে দিন:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0কেবল ১০০২ নম্বর অর্ডারটি বাদ গেছে — একমাত্র সেই সারি যেখানে quantity সত্যিই অজানা। মোট আয়ের (revenue) জন্য দুটি কলামই প্রয়োজন:
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])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" কেবল সেই সারিগুলোকে মুছে ফেলে এবং অন্তত একটি মান আছে এমন যেকোনো সারি রেখে দেয়:
print(orders.dropna(how="all").shape)(8, 7)এখানে কিছুই মোছা হয়নি — আমাদের কোনো সারিই পুরোপুরি ফাঁকা নয় — যা আপনি নিশ্চিত করতে চেয়েছিলেন।
dropna() একটি নতুন টেবিল ফেরত দেয়
বেশিরভাগ পান্ডাস মেথডের মতো dropna() মূল orders-কে পরিবর্তন করে না; এটি আপনাকে একটি নতুন DataFrame উপহার দেয়। আপনি যদি একটি পৃথক লাইনে কেবল orders.dropna() লিখে রেখে দেন, তবে orders-এর কিছুই হবে না। ফলাফলটি একটি নতুন নামে সংরক্ষণ করুন — বিশেষ করে যদি আপনার পুরো টেবিলটি এখনও প্রয়োজন হয়, যা আমাদের মতো রিপোর্টে দরকার হবেই (কতগুলো সারি বাদ পড়ল তা বলার জন্য)।
অনুপস্থিত মান প্রতিস্থাপন করা: fillna()
কখনও কখনও কোনো ফাঁকের একটি যুক্তিযুক্ত বিকল্প মান থাকে। যেখানে NaN ছিল, fillna() সেখানে একটি নতুন মান বসিয়ে দেয়।
একটি কলামের জন্য একটি মান
অনুপস্থিত শাখাটি সবচেয়ে স্পষ্ট উদাহরণ। অর্ডারটি ঘটেছে এবং এর টাকা বাস্তব; আমরা কেবল জানি না কোন শাখা এটি সম্পন্ন করেছে। একটি লেবেল দিয়ে এটি পূরণ করলে সারিটি অক্ষত থাকে এবং পরবর্তীতে শাখাভিত্তিক যেকোনো গণনায় ফাঁকটি স্পষ্টভাবে দৃশ্যমান থাকে:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']সেই লাইনটির গঠন গুরুত্বপূর্ণ: কলামটি নিন, fillna করুন, এবং একই কলামে পুনরায় সংরক্ষণ করুন। fillna একটি নতুন Series ফেরত দেয়; সামনে orders["branch"] = না লিখলে কোনো কিছুই জমা থাকবে না।
ভিন্ন ভিন্ন কলামের জন্য ভিন্ন মান: একটি dict
একসাথে একাধিক কলামকে তাদের নিজস্ব মান দিয়ে পূরণ করতে {column: value} আকারের একটি ডিকশনারি পাস করুন। যেসব কলামের নাম উল্লেখ করা হবে না সেগুলো অপরিবর্তিত থাকবে:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64price-এ এখনও তার দুটি ফাঁক রয়ে গেছে, কারণ আমরা এর নাম উল্লেখ করিনি। orders.fillna(0)-এর চেয়ে ডিকশনারি ব্যবহারের সুবিধা এখানেই; কারণ orders.fillna(0) লিখলে দাম সহ প্রতিটি কলাম 0 দিয়ে পূরণ হয়ে যেত — আর ০ টাকা দাম মানে তো একটি ব্যাগ ফ্রিতে দিয়ে দেওয়া।
মান পূরণ করলে উত্তর বদলে যায়
পরিমাণকে 0 দিয়ে পূরণ করা নিরীহ মনে হতে পারে। গড়গুলো তুলনা করে দেখুন:
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())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) সফলভাবে কাজ করে:
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64পূরণ করার আগে এটি চালালে ব্যর্থ হবে, কারণ "অজানা" অর্থ প্রকাশ করে এমন কোনো পূর্ণসংখ্যা পাইথনে নেই — নিচে "কিছু ভাঙা অবস্থা ও তার সমাধান" দেখুন।
একটি সম্পূর্ণ উদাহরণ
"কোড লেখার আগে" অংশের পরিকল্পনাটিকে clean_orders.py একটি একক প্রোগ্রামে সাজিয়ে নিয়েছে: পরিচিত মার্কারসহ ফাইলটি পড়ুন, প্রতিটি ফাঁক রিপোর্ট করুন, শাখা পূরণ করুন, যেসব সারি হিসাব করা সম্ভব নয় সেগুলো বাদ দিন, এবং মোট হিসাবের সাথে তা কয়টি অর্ডার কাভার করে তা পরিষ্কারভাবে দেখান।
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"]])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") দিয়ে রূপান্তর করুন।
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypesum() সংখ্যার দীর্ঘ এক স্ট্রিং ফেরত দিয়েছে, যেমন 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 রয়েছে। "অজানা" অর্থ প্রকাশ করে এমন কোনো পূর্ণসংখ্যা নেই। প্রথমে ফাঁকগুলো পূরণ করুন বা বাদ দিন, তারপর রূপান্তর করুন:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)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 পদ্ধতিটি লিখেছেন:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())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" পূরণ করার অর্থ হলো সেই কপিটিকে পূরণ করা, যা পরে ছুড়ে ফেলে দেওয়া হয়। পুরোনো পান্ডাসে কখনও কখনও এটি কাজ করত, যার কারণে ইন্টারনেটের সর্বত্র এই প্যাটার্নটি দেখা যায়। এর বদলে পরিষ্কারভাবে অ্যাসাইনমেন্ট লিখুন — এটি প্রতিটি সংস্করণে কাজ করে এবং স্পষ্টভাবে বলে দেয় কী পরিবর্তিত হচ্ছে:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() আপনার প্রত্যাশার চেয়ে অনেক বেশি সারি মুছে ফেলেছে খালি dropna() যেকোনো কলামে ফাঁক থাকার কারণে পুরো সারি মুছে ফেলে — এমনকি বেশিরভাগ অংশ খালি থাকা notes বা discount কলামের জন্যও যা আপনি হয়তো ব্যবহারই করছেন না। কোন কলামে অনেক ফাঁক আছে তা দেখতে df.isna().sum() প্রিন্ট করুন, তারপর কেবল আপনার প্রশ্নের জন্য প্রয়োজনীয় কলামগুলো দিয়ে dropna(subset=[...]) ব্যবহার করুন।
TypeError: can't multiply sequence by non-int of type 'float' আপনি একটি সংখ্যার কলামকে একটি টেক্সট কলাম দিয়ে গুণ করেছেন (price যখন str, তখন quantity * price করেছেন)। সমাধান প্রথম ত্রুটির মতোই: আগে price-কে সংখ্যায় রূপান্তর করুন।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
একটি quantity ঘর ফাঁকা রয়েছে। প্রিন্ট করলে কী দেখা যাবে?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,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 = pd.read_csv(StringIO(RAW))
print(orders["quantity"].sum(), orders["quantity"].count(), len(orders))- A76 8 8
- B76.0 7 8
- Cnan 7 8
- D76.0 8 8
উদ্দেশ্য হলো যে অর্ডারগুলোর দাম (price) নেই সেগুলো খুঁজে বের করা। দুটি দাম অনুপস্থিত। প্রিন্ট করলে কী দেখা যাবে?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,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 = pd.read_csv(StringIO(RAW), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A2
- B1
- C0 — `NaN` কোনো কিছুর সমান নয়, এমনকি নিজেরও না, তাই `==` এটি কখনোই খুঁজে পায় না
- Dএকটি `TypeError`: `nan`-এর সাথে তুলনা করা যায় না
দাম (price) কলামটি str হিসেবে পড়া হয়েছে কারণ একটি ঘরে - এবং অন্যটিতে N/A রয়েছে। কোন লাইনটি একে সংখ্যায় রূপান্তর করবে এবং দুটি ফাঁককেই NaN বানাবে?
- Aorders["price"] = orders["price"].astype(float)
- Borders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Corders["price"] = orders["price"].fillna(0)
- Dorders["price"] = orders["price"].dropna()
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
দোকানটি হোম ডেলিভারি শুরু করেছে, এবং প্রথম সপ্তাহের রাইডারদের লগ এসেছে deliveries.csv হিসেবে। ঠিক এভাবে ফাইলটি তৈরি করুন:
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3মালিক জানতে চেয়েছেন: ডেলিভারি ফি বাবদ মোট কত টাকা উঠেছে, কতটি ডেলিভারির ফি অনুপস্থিত, এবং কিলোমিটার প্রতি গড় ডেলিভারি ফি কত?
কোনো কোড লেখার আগে unique() দিয়ে ফাইলটি দেখুন এবং লুকানো মার্কারটি কী তা চিহ্নিত করুন। তারপর deliveries_report.py লিখুন যা:
- ফাইলটি এমনভাবে পড়বে যাতে লুকানো মার্কারটি প্রতিটি কলামে আসল অনুপস্থিত মানে পরিণত হয়
- প্রতি কলামের অনুপস্থিত মানের সংখ্যা এবং অসম্পূর্ণ সারিগুলো প্রিন্ট করবে
- অনুপস্থিত
rider-কে"unassigned"দিয়ে পূরণ করবে - জানা মোট ফি, ফি ছাড়া ডেলিভারির সংখ্যা, এবং কিলোমিটার প্রতি ফি — মোট ফি-কে মোট দূরত্ব দিয়ে ভাগ করে — প্রদর্শন করবে, যা কেবল সেইসব ডেলিভারি থেকে হিসাব করা হবে যেখানে ফি এবং দূরত্ব উভয়ই জানা আছে
আপনার প্রোগ্রামের শেষ তিনটি লাইন ঠিক এরকম প্রিন্ট হওয়া উচিত:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesকোড লেখার আগে "কোড লেখার আগে" অংশের মতো সিদ্ধান্তের একটি তালিকাও লিখুন: distance_km, fee এবং rider-এর প্রতিটির জন্য ফাঁক থাকার অর্থ কী, এবং আপনি কি তা পূরণ করবেন, বাদ দেবেন নাকি রেখে দেবেন?
তারপর একটি পরীক্ষা চালিয়ে দেখুন: dropna(subset=...)-এর বদলে খালি dropna() দিয়ে কিলোমিটার প্রতি ফি হিসাব করুন। সংখ্যাটি কি বদলে যায়? এটি কয়টি সারি ব্যবহার করেছিল এবং কেন?
সমাধান
আগে পরিকল্পনা। প্রশ্নটির তিনটি অংশ রয়েছে: একটি মোট যোগফল, ফাঁকের সংখ্যা, এবং একটি অনুপাত। info() দেখলে fee কলামটি str দেখাবে — একটি ফি কলাম সংখ্যা হওয়া উচিত, কাজেই এর ভেতরে টেক্সট লুকিয়ে আছে। কলামভিত্তিক সিদ্ধান্ত:
rider— ডেলিভারি হয়েছে, রাইডারের নাম লেখা হয়নি।"unassigned"দিয়ে পূরণ করুন।fee— কত ফি নেওয়া হয়েছে তা আমরা জানি না।NaNরেখে দিন; জানা ফি যোগ করুন এবং অজানাগুলোর সংখ্যা গুনুন।distance_km— দূরত্ব কত ছিল তা আমরা জানি না।NaNরেখে দিন; প্রতি-কিলোমিটার অনুপাত থেকে এই সারিগুলো বাদ দিন।
অনুপাতের জন্য একই ডেলিভারির ক্ষেত্রে উভয় সংখ্যাই জানা থাকতে হবে — অন্যথায় আপনি সাতটি ডেলিভারির ফি-কে ছয়টি ডেলিভারির দূরত্ব দিয়ে ভাগ করে ফেলবেন।
ধাপ ১ — মার্কার খুঁজে পেতে সাধারণভাবে পড়ুন এবং দেখুন:
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: str?-এর কারণে fee কলামটি str হয়ে গেছে। আর রাইডার হিসেবেও ? দেখা যাচ্ছে — অর্থাৎ একই মার্কার দুটি কলামেই ব্যবহৃত হয়েছে। distance_km ইতিমধ্যে float64: এর ফাঁকগুলো খালি সেল ছিল, যা পান্ডাস নিজে থেকেই চিনে নিয়েছে। যেহেতু মার্কারটি সব জায়গায় একই, তাই ফাইল পড়ার সময় na_values=["?"] দিলে দুটি কলামই একসাথে ঠিক হয়ে যায়।
সম্পূর্ণ প্রোগ্রাম, deliveries_report.py:
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesসংখ্যাগুলো একবার হাতে হাতে যাচাই করে নিন, কারণ এভাবেই পরিচ্ছন্ন করা মোট হিসেবের ওপর আস্থা তৈরি হয়। জানা ফি: ৬০ + ৬০ + ১২০ + ৪০ + ৯০ + ১১০ + ৬০ = ৫৪০, সাতটি ডেলিভারি থেকে — ২০০৩ নম্বরে কোনো ফি নেই। অনুপাতের জন্য dropna(subset=["fee", "distance_km"]) ২০০৩ (ফি নেই) এবং ২০০২ ও ২০০৭ (দূরত্ব নেই) বাদ দেয়। মনে মনে হিসাব না করে পান্ডাসের কাছেই জানতে চান কোন সারিগুলো রয়ে গেছে এবং তাদের যোগফল কত:
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0৩৭০ / ২৮.০ = ১৩.২১, যা প্রোগ্রামটিতে প্রিন্ট হওয়া সংখ্যাটির সমান। লক্ষ্য করুন অনুপাতটি কী করেনি: এটি সাতটি ডেলিভারির ফি ৫৪০-কে ছয়টি ডেলিভারির দূরত্ব দিয়ে ভাগ করেনি। উভয় সংখ্যাই এসেছে সেই একই পাঁচটি সারি থেকে, যা "কিলোমিটার প্রতি" কোনো সংখ্যা অর্থপূর্ণ হওয়ার একমাত্র শর্ত।
আরও দুটি সিদ্ধান্ত লক্ষ্য করার মতো। গণনার আগেই rider পূরণ করা হয়েছিল, কিন্তু এটি কখনোই গণনায় প্রভাব ফেলেনি — subset= কেবল fee এবং distance_km-এর দিকে তাকিয়েছিল। আর ফি-র মোট হিসেবে "(known)" লেবেল দেওয়া হয়েছে, এবং তার ঠিক নিচেই অনুপস্থিত ফি-র সংখ্যা প্রিন্ট করা হয়েছে, যাতে ৫৪০ টাকাকে কেউ ভুল করে মোট আদায় করা সব টাকা না ভেবে বসেন।
পরীক্ষাটি। খালি dropna() চালালে ২০০৪ এবং ২০০৬ নম্বর অর্ডারও বাদ পড়ে যেত — যাদের ফি এবং দূরত্ব উভয়ই সঠিক ছিল — কারণ তাদের rider অনুপস্থিত ছিল। rider পূরণ করার আগে যদি আপনি এটি টেবিলে চালান:
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84কেবল তিনটি ডেলিভারি টিকে থাকে, এবং অনুপাতটি ১৩.২১ থেকে একলাফে ১৬.৮৪-এ উঠে যায় — কোনো ফি পরিবর্তনের জন্য নয়, বরং দুটি দীর্ঘতম ট্রিপ (১২ এবং ৬.৫ কিমি), যা কিলোমিটার প্রতি অপেক্ষাকৃত সস্তা ছিল, একটি অনুপস্থিত নামের কারণে বাদ পড়ে যাওয়ায়। রাইডার কলামের সাথে এই প্রশ্নের কোনো সম্পর্ক নেই, তাই কোন সারিগুলো গণনায় আসবে তা এর নির্ধারণ করা উচিত নয়। ঠিক এই কারণেই subset= ব্যবহার করা হয়।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন