الفصل 03

قراءة ملف CSV وفحص البيانات بعد قراءتها

قراءة الملفات باستخدام read_csv وفحصها مباشرة عبر shape و head و info و describe — لأن الملف المقروء بشكل غير سليم يعطي نتائج خاطئة دون إظهار أي أخطاء.

30 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

حتى الآن، كنا ندخل كل قيمة في الجدول يدوياً داخل الكود. قد يكون هذا مقبولاً لثلاثة صفوف فقط.

لكن البيانات الحقيقية لا تعيش داخل الكود البرمجي؛ بل توجد داخل ملفات — يرسلها إليك شخص عبر البريد الإلكتروني، أو تُصدّر من نظام داخلي، أو تُحفظ من جدول بيانات إلكتروني. وغالباً ما يحتوي هذا الملف على اثني عشر ألف صف، وليس اثني عشر فقط.

يبدو هذا الملف في العادة كنص بسيط: يضم السطر الأول أسماء الأعمدة، يليه صف واحد في كل سطر مفصولاً بفواصل:

text
product,quantity,price
pen,12,15.0
notebook,5,60.0

هذا هو ملف CSV — أي القيم المفصولة بفواصل (Comma-Separated Values). وتحويله إلى جدول يتطلب سطراً واحداً فقط في بانداس، وهو السطر الأكثر استخداماً على الإطلاق في هذه المكتبة.

لكن هناك عادة بالغة الأهمية يجب بناؤها فور كتابة هذا السطر، وهي الموضوع الحقيقي لهذا الفصل: بمجرد قراءة الملف، افحصه فوراً. كم عدد الصفوف التي وصلت؟ هل أسماء الأعمدة مطابقة لما توقعته؟ هل دخلت الأعمدة الرقمية كأرقام فعلية؟ إهمال هذه الثواني الثلاثين يعني ظهور الأخطاء لاحقاً في النتائج دون أن يُصدر البرنامج أي رسالة تحذير أو خطأ.

بنهاية هذا الفصل ستكون قادراً على

  • قراءة ملف باستخدام pd.read_csv() والتعامل مع مشكلات مسار الملف
  • فحص الملف سريعاً باستخدام head() و tail() و shape و columns
  • قراءة مخرجات info() لمعرفة عدد الصفوف والخلايا الفارغة ونوع كل عمود في آن واحد
  • تلخيص الأعمدة الرقمية إحصائياً باستخدام describe()
  • تمييز وإصلاح خطأ FileNotFoundError ومشاكل اختلاف الفواصل (separators)

المتطلبات المسبقة: Series و DataFrame — المفهومان الأساسيان في بانداس.


أنشئ الملف أولاً

داخل مجلد مشروعك، أنشئ ملفاً باسم sales.csv يحتوي بدقة على هذه الأسطر الستة:

text
product,quantity,price
pen,12,15.0
notebook,5,60.0
bag,2,850.0
bottle,7,120.0
eraser,30,8.0

السطر الأول ليس بيانات فعلية، بل هو أسماء الأعمدة — الترويسة (header). والأسطر الخمسة التالية هي خمسة صفوف من البيانات.

قراءة الملف في سطر واحد

python
import pandas as pd

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

print(sales)
text
product  quantity  price
0       pen        12   15.0
1  notebook         5   60.0
2       bag         2  850.0
3    bottle         7  120.0
4    eraser        30    8.0

أنجز هذا السطر الواحد ثلاثة أشياء في آن واحد، وبهدوء تام:

أولاً، اعتبر السطر الأول كترويسة للجدول — فأصبحت product و quantity و price أسماء أعمدة وليست صفاً من البيانات.

ثانياً، أنشأ الفهرس تلقائياً، وهو الأرقام من 0 إلى 4 على الجانب الأيسر، والتي لم تكن موجودة في الملف الأصلي.

ثالثاً، وهو الأهم، خمّن نوع البيانات لكل عمود. كل شيء في الملف هو في الأصل نص عادي — فالعدد 12 هو في حقيقته حرفان. نظر بانداس إلى كل عمود وقرر ما إذا كان يمثل أرقاماً أم نصوصاً.

غالباً ما يكون هذا التخمين صحيحاً. ولكن عندما يخطئ، لا يُطلق البرنامج أي خطأ برمجي — وهذا هو السبب الدقيق الذي يوجب عليك فحصه بنفسك دائماً.

أربعة أشياء يجب فحصها فور قراءة الملف

shape — كم عدد البيانات التي وصلت

python
print(sales.shape)
text
(5, 3)

خمسة صفوف وثلاثة أعمدة. يحتوي الملف على ستة أسطر والجدول على خمسة صفوف، لأن الترويسة لا تُحتسب كصف بيانات. إذا كان هذا الرقم أقل بكثير مما توقعته، أو زائداً بمقدار واحد بالضبط، فهذه الإشارة الأولى إلى أن الملف قُرئ بطريقة غير سليمة.

head() و tail() — البداية والنهاية

python
print(sales.head(3))
text
product  quantity  price
0       pen        12   15.0
1  notebook         5   60.0
2       bag         2  850.0

طباعة جميع الصفوف الاثني عشر ألفاً لملف حقيقي ستملأ شاشة الطرفية ولن تفيدك بشيء. تعرض head() الصفوف الأولى — خمسة صفوف إذا لم تحدد عدداً. بينما تعرض tail() الشيء نفسه لنهاية الجدول، وفحص النهاية مفيد بشكل خاص لأن الملفات المصدرة غالباً ما تحتوي على أسطر فارغة شاردة أو صفوف إجماليات في أسفلها.

info() — كل شيء في لمحة واحدة

python
sales.info()
text
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   product   5 non-null      object
 1   quantity  5 non-null      int64
 2   price     5 non-null      float64
dtypes: float64(1), int64(1), object(1)
memory usage: 252.0+ bytes

لاحظ أننا كتبنا sales.info() وليس print(sales.info()) — فهذه الدالة تتولى الطباعة بنفسها ولا تُعيد قيمة، ووضعها داخل print يضيف كلمة None زائدة في النهاية.

هذا هو الأمر الأكثر فائدة بعد قراءة أي ملف، لأنه يجمع بين معلومتين جوهريتين:

Non-Null Count — عدد الخلايا الممتلئة فعلياً في كل عمود. تظهر هنا 5 non-null في الأعمدة الثلاثة، مما يعني عدم وجود أي خلية فارغة. أما إذا ظهرت 4 non-null في أي عمود، فهذا يعني وجود خلية فارغة واحدة، مما سيؤثر على حساباتك اللاحقة.

Dtype — نوع البيانات الذي استقر عليه تخمين بانداس. حيث تم تمييز quantity كـ int64، و price كـ float64، و product كـ object. وهو ما نريده تماماً.

(قد يختلف رقم memory usage في السطر الأخير على جهازك، وهو أمر غير مهم حالياً.)

describe() — ملخص إحصائي للأعمدة الرقمية

python
print(sales.describe())
text
quantity       price
count   5.00000    5.000000
mean   11.20000  210.600000
std    11.12205  360.217712
min     2.00000    8.000000
25%     5.00000   15.000000
50%     7.00000   60.000000
75%    12.00000  120.000000
max    30.00000  850.000000

تقوم هذه الدالة بانتقاء الأعمدة الرقمية وإجراء ثماني عمليات حسابية إحصائية على كل منها. عمود product غائب هنا لأنه نصي.

هناك أمر يلفت الانتباه هنا: المتوسط الحسابي mean لعمود price هو 210.6، في حين أن الوسيط 50% هو 60 فقط. وجود فجوة بهذا الحجم يعني وجود قيمة واحدة أعلى بكثير من بقية القيم، وتكشف خانة max عن هذه القيمة: 850. هكذا تُقرأ مخرجات describe() — عندما يتباعد المتوسط عن الوسيط، ابحث عن القيم الشاذة والمتطرفة هناك.


مثال تطبيقي متكامل

report.py:

python
import pandas as pd

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

print("Rows, columns:", sales.shape)
print()
print(sales.head(3))
print()
print("Columns   :", list(sales.columns))
print("Total qty :", sales["quantity"].sum())
print("Avg price :", round(sales["price"].mean(), 2))
text
Rows, columns: (5, 3)

    product  quantity  price
0       pen        12   15.0
1  notebook         5   60.0
2       bag         2  850.0

Columns   : ['product', 'quantity', 'price']
Total qty : 56
Avg price : 210.6

برنامج صغير، ولكن هيكله جدير بالتأمل — اقرأ الملف، ثم افحص وتأكد، ثم احسب. الدالتان shape و head() ليستا مجرد عمليات حسابية، بل هما أسئلة استكشافية: هل قرأت الملف الصحيح؟ هل عدد الصفوف مطابق لما ينبغي؟

وهناك سبب وجيه لطباعة list(sales.columns)؛ فعند طباعتها كقائمة، يظهر كل اسم محاطاً بعلامات تنصيص، مما يكشف فوراً أي مسافة زائدة مثل 'quantity '. لو طُبعت بأي شكل آخر لبقيت تلك المسافة خفية وعادت لاحقاً لتسبب خطأ KeyError.


الأخطاء الشائعة وحلولها

خطأ FileNotFoundError: [Errno 2] No such file or directory: 'sales.csv' يبحث بايثون في المجلد الحالي ولم يعثر على الملف هناك. هذه هي نفس المشكلة التي واجهناها في دورة بايثون: الطرفية مفتوحة في مكان والملف موجود في مكان آخر. شغّل pwd لمعرفة مسارك الحالي، و dir للتأكد من وجود الملف، ثم استخدم cd للانتقال إليه. في ويندوز، قد يكون الاسم الفعلي للملف هو sales.csv.txt — فعّل إظهار امتدادات أسماء الملفات في File Explorer.

وصول الملف بأكمله في عمود واحد فقط الفاصل المستخدم ليس الفاصلة العادية. الملفات المحفوظة في العديد من الدول الأوروبية تستخدم الفاصلة المنقوطة (;)، لأن الفاصلة تُستخدم هناك كعلامة عشرية. اكتب pd.read_csv("sales.csv", sep=";"). وبالنسبة للملفات المفصولة بعلامة التبويب (Tab)، استخدم sep="\t".

تحول عمود رقمي إلى نوع object تسربت نصوص إلى العمود في مكان ما — مثل N/A، أو شرطة -، أو خلية فارغة، أو فاصلة الآلاف (1,200). شغّل df["col"].unique() لمعرفة القيم الفعلية بداخل العمود؛ فالقيمة الغريبة تبرز عادة بوضوح. الحل هو pd.to_numeric(df["col"], errors="coerce")، الذي يحول أي قيمة غير رقمية إلى قيمة فارغة — وسنتناول هذا بالتفصيل في فصل البيانات المفقودة.

خطأ UnicodeDecodeError الملف غير محفوظ بترميز UTF-8، وهو ما يحدث بانتظام مع الملفات المصدرة من برامج الجداول الإلكترونية. جرّب تمرير pd.read_csv("sales.csv", encoding="utf-8-sig")، وإذا لم ينجح فاستخدم encoding="latin-1".

تحول أول صف من البيانات إلى أسماء أعمدة على الرغم من أن الملف لا يحتوي على ترويسة اكتب pd.read_csv("sales.csv", header=None) وستصبح الأعمدة مرقمة تلقائياً 0, 1, 2. ولتسميتها بأسمائك الخاصة، أضف المعامل names=["product", "quantity", "price"].

طباعة كلمة None زائدة بعد مخرجات sales.info() السبب هو كتابتها داخل print(sales.info()). دالة info() تطبع المخرجات بنفسها ولا تعيد شيئاً، لذلك تقوم print بطباعة القيمة None التي أعادتها الدالة. اكتب ببساطة sales.info().