مبادئ وتقنيات علم البيانات
الفصل الأول: دورة حياة علم البيانات
فهرس الفصل:
مقدمة
في علم البيانات، نستخدم بيانات عديدة ومتنوعة لاتخاذ قراراتنا. في هذا الكتاب سنشرح مبادئ وتقنيات علم البيانات من الجانب الحسابي والتفكير الاستدلالي. وتشمل الخطوات التالية:
- تشكيل السؤال أو المشكلة.
- إيجاد وتنظيف البيانات.
- التحليل الاستكشافي للبيانات.
- استخدام التوقع والاستدلال لإيجاد النتائج.
ومن المتوقع أن تظهر مزيد من الأسئلة والمشاكل بعد آخر خطوة، في ذلك الوقت يمكننا إعادة الخطوات مرة أخرى لاكتشاف أي خصائص جديدة في مشكلتنا. هذا التكرار الإيجابي في عملنا يسمى دورة حياة علم البيانات.
إذا كانت دورة حياة علم البيانات سهلة، لما احتجنا كتباً لشرحها. لحسن حظنا، كل خطوة لديها عدد مختلف من التحديات التي تكشف لنا أفكار جديدة تكون هي أساساً لاتخاذ قرارات مدروسة باستخدام البيانات.
طلاب داتا 100
دورة حياة علم البيانات تتكون من الخطوات التالية:
1- تشكيل السؤال أو المشكلة:
- ما الذي نريد معرفته، أو ما هي المشكلة التي نريد حلها؟
- ما هي الفرضيات؟
- ما هي مقاييس نجاحنا؟
2- إيجاد وتنظيف البيانات:
- ما هي البيانات المتوفرة لدينا وما هي التي نبحث عنها؟
- كيف سنتمكن من جمع المزيد من البيانات؟
- كيف نرتب البيانات لنبدأ التحليل؟
3- التحليل الاستكشافي للبيانات:
- هل لدينا بيانات ذات علاقة بمشكلتنا؟
- هل تحتوي البيانات على تحيزات، بيانات شاذة، أو مشاكل أخرى؟
- كيف نحول البيانات لتساعدنا على القيام بتحليل فعال؟
4- التوقع والاستدلال:
- ماذا تخبرنا البيانات؟
- هل أجابت على السؤال أو حلت المشكلة؟
- ما مدى قوة نتائجنا؟
سنقوم الآن بتجربة هذه الخطوات على قاعدة بيانات الأسماء الأولى لطلاب داتا 100 من الفصول السابقة. في هذا الفصل، قمنا بالمرور بشكل سريع على الخطوات لإعطاء القارئ معلومات عن الدورة الكاملة. في فصول لاحقة، سنتحدث بشكل مفصل ونشرح كل خطوة.
تشكيل السؤال أو المشكلة
نريد أن نعرف ما إذا كانت الأسماء الأولى للطلاب تقدم لنا معلومات إضافية عنهم. رغم أن السؤال يبدو غامضاً نوعاً ما، لكنه كافياً لجعلنا نعمل على البيانات المتوفرة لدينا ويمكننا التعديل في السؤال أثناء عملنا لنجعله أكثر دقة.
إيجاد وتنظيف البيانات
لنبدأ بأخذ نظرة سريعة عن البيانات المتوفرة لدينا، البيانات هي قائمة لأسماء الطلاب الأولى للذين سبق أن درسوا مادة داتا 100. لا تقلق إن لم تفهم الكود البرمجي؛ سنشرحه ونشرح المكتبات المستخدمة لاحقاً. حالياً، ركز على الخطوات والرسوم البيانية:
لتحميل بيانات اسماء الطلاب اضغط هنا.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
students = pd.read_csv('roster.csv')
students
Name | Role | |
---|---|---|
Keeley | Student | 0 |
John | Student | 1 |
BRYAN | Student | 2 |
… | … | … |
Ernesto | Waitlist Student | 276 |
Athan | Waitlist Student | 277 |
Michael | Waitlist Student | 278 |
279 rows × 2 columns
يمكن أن نلاحظ بشكل سريع وجود بعض المشاكل في بياناتنا. مثلاً، أحد الطلاب كُتب اسمه بالأحرف الكبيرة بشكل كامل BRYAN، بالإضافة إلى أن معنى العمود Role لا يبدو واضحاً. نلاحظ أيضاً أن الجدول يحتوي على عامودين و 279 سطراً.
في هذه المادة، سنتعلم كيفية اكتشاف الأخطاء في بياناتنا وتصحيحها. الاختلاف في الحروف الكبيرة في الاسم Bryan سيجعل البرنامج يتوقع أن BRYAN يختلف عن Bryan ولكن في الحقيقة هما نفس الشخص. لذا سنحول جميع الأسماء إلى حروف صغيرة:
students['Name'] = students['Name'].str.lower()
students
Name | Role | |
---|---|---|
keeley | Student | 0 |
john | Student | 1 |
bryan | Student | 2 |
… | … | … |
ernesto | Waitlist Student | 276 |
athan | Waitlist Student | 277 |
michael | Waitlist Student | 278 |
279 rows × 2 columns
الآن، وبما أن البيانات لدينا بدأت تظهر بشكل مقبول، يمكننا الانتقال للخطوة التالية.
التحليل الاستكشافي للبيانات
التحليل الاستكشافي للبيانات أو Exploratory Data Analysis واختصارها EDA يطلق على الخطوات التي نتبعها لمعرفة صفات البيانات لعمل تحليلات لها لاحقاً. لنستعرض بيانات الطلاب:
students
Name | Role | |
---|---|---|
keeley | Student | 0 |
john | Student | 1 |
bryan | Student | 2 |
… | … | … |
ernesto | Waitlist Student | 276 |
athan | Waitlist Student | 277 |
michael | Waitlist Student | 278 |
279 rows × 2 columns
الآن لدينا بعض الأسئلة، كم عدد الطلاب؟ ماذا يعني عمود Role؟ نقوم بخطوة التحليل الاستكشافي للبيانات للإجابة على مثل هذه الأسئلة.
كم عدد الطلاب؟
print("There are", len(students), "students on the roster.")
There are 279 students on the roster.
عدد الطلاب لدينا هو 279 طالباً. السؤال التالي دائماً يكون: هل تحتوي البيانات على كامل الطلاب؟ في حالتنا، الجدول يحتوي على جميع الطلاب الذين درسوا مادة داتا 100 في فصلٍ دراسيٍّ واحد.
ماذا يعني عمود Role؟
لنستكشف البيانات التي في هذا العمود لنعرف معناه:
students['Role'].value_counts().to_frame()
Role | |
---|---|
237 | Student |
42 | Waitlist Student |
يمكن أن نرى في الجدول السابق أن البيانات لا تحتوي فقط على الطلاب الذين درسوا المادة Student، بل أيضاً على الطلاب الموجودين في قائمة الانتظار Waitlist Student. إذاً، العمود Role يخبرنا إذا كان الطالب التحق بالمادة أم لا.
ماذا عن عمود Name؟ كيف يمكننا استكشافه؟
في هذه المادة سنتعامل مع عدد كبير من أنواع البيانات. الرقمية، النوعية والنصية. كل نوع له أساليبه وأدواته الخاصة للاستكشاف.
طريقة سريعة لفهم عمود الأسماء Name هي بمعرفة عدد الأحرف في كل اسم:
sns.distplot(students['Name'].str.len(),
rug=True,
bins=np.arange(12),
axlabel="Number of Characters")
plt.xlim(0, 12)
plt.xticks(np.arange(12))
plt.ylabel('Proportion per character');
الرسم البياني السابق يخبرنا أن أكثر الأسماء يبلغ طولها بين 4 إلى 8 أحرف. هذا يساعدنا على معرفة ما إذا كانت بياناتنا معقولة أم لا. إذا كان هناك الكثير من الأسماء ذات حرف واحد، فيكون ذلك سبب مناسب لإعادة استكشاف البيانات.
رغم أن البيانات تبدو واضحة وبسيطة، سنعرف لاحقاً كيف أن الاسم الأول فقط قد يخبرنا الكثير عن مجموعة الطلاب لدينا.
ماذا بداخل عمود الاسم؟
حتى الآن، وجهنا سؤال عام: “هل يخبرنا الاسم الأول للطالب أي شيء عن المادة؟”
قمنا بتنظيف البيانات بتحويلها جميعها لأحرف صغيرة. أثناء التحليل الاستكشافي للبيانات لاحظنا أن لدينا حوالي 270 طالباً منهم من درس المادة ومنهم من على قائمة الانتظار. وأكثر الأسماء بين 4 إلى 8 أحرف.
ماذا يمكننا معرفته عن طلاب المادة من أسمائهم؟ لنأخذ اسماً واحداً منها:
students['Name'][5]
'jerry'
من هذا الاسم، يمكننا القول أن صاحب الاسم ذكر. ويمكننا أيضاً توقع عمر الطالب. على سبيل المثال، إذا عرفنا أن اسم Jerry مشهور من بين أسماء الأطفال الذين ولدو في عام 1998، يمكننا التوقع إن عمر الطالب في العشرينيات.
التفكير بهذه الطريقة أوصلنا إلى سؤالين:
- هل تخبرنا أسماء الطلاب عن توزيع الذكور والإناث؟
- هل تخبرنا أسماء الطلاب عن توزيع الأعمار؟
للإجابة على هذه الأسئلة، سنحتاج بيانات تربط بين الأسماء مع الجنس والسنوات. مؤسسة الضمان الاجتماعي الأمريكية لديها مثل هذه البيانات ومتوفرة على الإنترنت على الرابط.
سنبدأ أولاً بتحميل البيانات من الموقع ثم نقلها إلى بايثون. مرة أخرى، لا تقلق إذا لم تفهم الكود البرمجي في هذا الفصل، فقط ركز على فهم الخطوات بشكل عام:
import urllib.request
import os.path
data_url = "https://www.ssa.gov/oact/babynames/names.zip"
local_filename = "babynames.zip"
if not os.path.exists(local_filename): # اذا توفرت البيانات، لا تحملها مرة أخرى
with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
f.write(resp.read())
import zipfile
babynames = []
with zipfile.ZipFile(local_filename, "r") as zf:
data_files = [f for f in zf.filelist if f.filename[-3:] == "txt"]
def extract_year_from_filename(fn):
return int(fn[3:7])
for f in data_files:
year = extract_year_from_filename(f.filename)
with zf.open(f) as fp:
df = pd.read_csv(fp, names=["Name", "Sex", "Count"])
df["Year"] = year
babynames.append(df)
babynames = pd.concat(babynames)
babynames
Year | Count | Sex | Name | |
---|---|---|---|---|
1884 | 9217 | F | Mary | 0 |
1884 | 3860 | F | Anna | 1 |
1884 | 2587 | F | Emma | 2 |
… | … | … | ||
1883 | 5 | M | Verna | 2081 |
1883 | 5 | M | Winnie | 2082 |
1883 | 5 | M | Winthrop | 2083 |
1891894 rows × 4 columns
البيانات تحتوي على الأسماء، جنس الطفل، عدد الأطفال بهذا الاسم وسنة ميلاد كل طفل. للتأكيد، لنقرأ ما كتبه مكتب الضمان الاجتماعي في شرحهم للبيانات على الرابط.
جميع الأسماء أتت من بطاقات التقديم للضمان الاجتماعي لجميع الولادات التي كانت في الولايات المتحدة بعد عام 1879. ملاحظة: الكثير ممن ولدوا قبل 1937 لم يقوموا بالتقديم للحصول على البطاقة، لذلك أسماؤهم ليست ضمن البيانات. الآخرين الذين قاموا بتقديم طلباتهم، سجلاتنا لا تُظهر أماكن ولاداتهم، لذلك أسماؤهم أيضاً لم تضاف إلى البيانات. هذه عينة كاملة من البيانات لدينا حتى تاريخ مارس 2017.
نبدأ أولاً بعرض عدد المواليد الذكور والإناث كل سنة:
pivot_year_name_count = pd.pivot_table(
babynames, index='Year', columns='Sex',
values='Count', aggfunc=np.sum)
pink_blue = ["#E188DB", "#334FFF"]
with sns.color_palette(sns.color_palette(pink_blue)):
pivot_year_name_count.plot(marker=".")
plt.title("Registered Names vs Year Stratified by Sex")
plt.ylabel('Names Registered that Year')
الزيادة المفاجئة لعدد المواليد في عام 1920 تبدو مشبوهة، ولكن في الاقتباس السابق تم توضيح السبب:
ملاحظة: الكثير ممن ولدوا قبل 1937 لم يقوموا بالتقديم للحصول على البطاقة، لذلك أسماؤهم ليست ضمن البيانات. الآخرين الذين قاموا بتقديم طلباتهم، سجلاتنا لا تُظهر أماكن ولاداتهم، لذلك أسماؤهم أيضاً لم تضاف إلى البيانات.
يمكن ملاحظة فترة الإنجاب المتزايدة أو ما تسمى ب Baby boomers والتي كانت في الفترة بين 1946 حتى 1964، لقراءة المزيد عن هذا الموضوع قم بزيارة الرابط.
معرفة الجنس من الاسم
لنستخدم بيانات الأطفال السابقة لمعرفة عدد الذكور والإناث. كما فعلنا سابقاً، نبدأ أولاً بتصغير جميع أحرف الأسماء في بيانات الأطفال:
babynames['Name'] = babynames['Name'].str.lower()
babynames
Year | Count | Sex | Name | |
---|---|---|---|---|
1884 | 9217 | F | mary | 0 |
1884 | 3860 | F | anna | 1 |
1884 | 2587 | F | emma | 2 |
… | … | … | ||
1883 | 5 | M | verna | 2081 |
1883 | 5 | M | winnie | 2082 |
1883 | 5 | M | winthrop | 2083 |
2084 rows × 4 columns
ثم نجمع عدد المواليد لكل اسم ونوع المولود:
sex_counts = pd.pivot_table(babynames, index='Name', columns='Sex',
values='Count', aggfunc='sum',
fill_value=0., margins=True)
sex_counts
All | M | F | Sex |
---|---|---|---|
Name | |||
96 | 96 | 0 | aaban |
35 | 0 | 35 | aabha |
10 | 10 | 0 | aabid |
… | … | … | |
6 | 6 | 0 | zyyon |
5 | 5 | 0 | zzyzx |
344533897 | 173894326 | 170639571 | All |
96175 rows × 3 columns
لتحديد ما إذا كان الاسم أكثر شيوعاً للأطفال الذكور أم الإناث، يمكننا حساب نسبة تكرار الاسم لدى أحد الجنسين:
prop_female = sex_counts['F'] / sex_counts['All']
sex_counts['prop_female'] = prop_female
sex_counts
prop_female | All | M | F | Sex |
---|---|---|---|---|
Name | ||||
0.0 | 96 | 96 | 0 | aaban |
1.0 | 35 | 0 | 35 | aabha |
0.0 | 10 | 10 | 0 | aabid |
… | … | … | ||
0.0 | 6 | 6 | 0 | zyyon |
0.0 | 5 | 5 | 0 | zzyzx |
0.5 | 344533897 | 173894326 | 170639571 | All |
96175 rows × 4 columns
يمكننا تعريف دالة لتبحث لنا عمّا إذا كان الاسم ذكراً أو أنثى باستخدام النسبة السابقة:
def sex_from_name(name):
if name in sex_counts.index:
prop = sex_counts.loc[name, 'prop_female']
return 'F' if prop > 0.5 else 'M'
else:
return 'Name not in dataset'
students['sex'] = students['Name'].apply(sex_from_name)
sex_from_name('sam')
'M'
باستخدام الكود البرمجي السابق يمكنك تجربة أي اسم ومعرفة ما إذا كانت نسبة تسميته كذكر أعلى من نسبة تسميته كأنثى.
الآن لنعود إلى بيانات الطلاب ونضيف عليها ما إذا كان الطالب ذكراً أو أنثى:
Sex | Name | Role | |
---|---|---|---|
F | keeley | Student | 0 |
M | john | Student | 1 |
M | bryan | Student | 2 |
… | … | … | |
M | ernesto | Waitlist Student | 276 |
M | athan | Waitlist Student | 277 |
M | michael | Waitlist Student | 278 |
279 rows × 4 columns
الآن يمكننا بسهولة معرفة عدد الذكور والإناث بين طلابنا:
students['sex'].value_counts()
M 144
F 92
Name not in dataset 43
Name: Sex, dtype: int64
إيجاد العمر من الاسم
باستخدام نفس الطريقة السابقة يمكننا إيجاد توزيع العمر في فصلنا، بربط كل اسم مع متوسط السنوات الذي تكرر فيها:
def avg_year(group):
return np.average(group['Year'], weights=group['Count'])
avg_years = (
babynames
.groupby('Name')
.apply(avg_year)
.rename('avg_year')
.to_frame()
)
avg_years
avg_year | |
---|---|
Name | |
2012.57 | aaban |
2013.71 | aabha |
2009.50 | aabid |
… | … |
2010.00 | zyyanna |
2014.00 | zyyon |
2010.00 | zzyzx |
96174 rows × 1 columns
بنفس الطريقة السابقة، يمكن أن نبحث عن أي اسم ومعرفة متوسط سنة الميلاد:
def year_from_name(name):
return (avg_years.loc[name, 'avg_year']
if name in avg_years.index
else None)
students['year'] = students['Name'].apply(year_from_name)
students
Year | Sex | Name | Role | |
---|---|---|---|---|
1998.15 | F | keeley | Student | 0 |
1951.08 | M | john | Student | 1 |
1983.57 | M | bryan | Student | 2 |
… | … | … | ||
1981.44 | M | ernesto | Waitlist Student | 276 |
2004.40 | M | athan | Waitlist Student | 277 |
1971.18 | M | michael | Waitlist Student | 278 |
279 rows × 4 columns
الآن، يمكننا بسهولة عرض توزيع السنوات بين الطلاب:
sns.distplot(students['year'].dropna());
ولعرض متوسط عمود السنة نقوم بالآتي:
students['year'].mean()
1983.846741800525
متوسط الأعمار لدينا هو 35 سنه (2018-1983=35)، تقريباً أكثر بمرتين من العمر المتوقع للطلاب الجامعيين. لماذا تظهر لنا الأعمار مرتفعة بهذا الشكل؟
كعالم بيانات، قد نصل لنتائج لا نتفق معها أو عكس توقعاتنا. التحدي الدائم الذي يواجهنا هو معرفة ما إذا كانت النتائج التي فاجأتنا سببها خطأ في إحدى خطواتنا أو خطأ حقيقي في البيانات. بما أنه لا يوجد هناك طريقة سهلة لضمان نتائج دقيقة، يجب أن يكون لدى عالم البيانات مبادئ وقواعد للتقليل من إيجاد نتائج خاطئة.
في حالتنا، التفسير الوحيد للنتيجة غير المتوقعة التي ظهرت لنا هو أن الأسماء الأكثر شيوعاً تستخدم منذ سنوات عديدة. مثلاً الاسم John يعتبر من الأسماء الأكثر شيوعاً عبر التاريخ بناءً على البيانات التي حصلنا عليها. يمكننا تأكيد ذلك بعرض رسم بياني لعدد الأطفال الذين تم تسميتهم John كل سنة:
names = babynames.set_index('Name').sort_values('Year')
john = names.loc['john']
john[john['Sex'] == 'M'].plot('Year', 'Count')
plt.title('Frequency of "John"');
يبدو لنا أن متوسط السنة لا يعطي توقع دقيق لعمر الشخص. ولكن في بعض الحالات، الاسم الأول للشخص يساعدنا على ذلك، على سبيل المثال عند التجربة على اسم Kanye تظهر لنا النتيجة التالية:
names = babynames.set_index('Name').sort_values('Year')
kanye = names.loc['kanye']
kanye[kanye['Sex'] == 'M'].plot('Year', 'Count')
plt.title('Frequency of "Kanye"');
الملخص
في هذا الفصل، قمنا بالمرور بشكل سريع على دورة حياة علم البيانات: تشكيل السؤال أو المشكلة، إيجاد وتنظيف البيانات، التحليل الاستكشافي للبيانات، التوقع والاستدلال. سنشرح بشكل مفصل كل خطوة في الفصول القادمة.
النصف الأول من الكتاب (الفصل 1 حتى 9) سيغطي الثلاث خطوات الأولى من دورة حياة علم البيانات ويركز بشكل كبير على طريقة الحساب وإيجاد النتائج. النصف الثاني (الفصل 10 حتى 18) يستخدم التفكير الحسابي والإحصائي لتغطية مواضيع بناء النماذج، الاستدلال والتوقع.
بشكل عام، يسعى هذا الكتاب لتعريف القارئ على مبادئ وتقنيات علم البيانات.