اذهب إلى المحتوى

زينة معلا

الأعضاء
  • المساهمات

    19
  • تاريخ الانضمام

  • تاريخ آخر زيارة

3 متابعين

آخر الزوار

لوحة آخر الزوار معطلة ولن تظهر للأعضاء

إنجازات زينة معلا

عضو مساهم

عضو مساهم (2/3)

2

السمعة بالموقع

  1. تُعَدّ السلسلة الزمنية time series تسلسلًا sequence من القياسات المأخوذة من نظام والمتغيرة بمرور الزمن، ومن الأمثلة الشهيرة "مخطط عصا الهوكي" الذي يُظهر متوسط درجة الحرارة في العالم مع مرور الوقت، كما يمكنك الاطلاع على صفحة ويكيبيديا للمزيد من المعلومات، حيث أنّ مصدر المثال الذي نناقشه في هذا المقال هو البيانات المتاحة من موقع kaggle والتي تُعطي بيانات مبيعات الأفوكادو للأعوام 2015-2020 في الولايات المتحدة الأمريكية وذلك لكل من نوعي الأفوكادو العادي Conventional والأفوكادو العضوي Organic. نأمل أن تجدوا هذا المقال مثيرًا للاهتمام. توجد الشيفرة الخاصة بهذا المقال في الرابط على Google Colab أو يمكنك تنزيلها من المرفقات. استيراد وتنظيف البيانات توجد البيانات المستخدمة في موقع Kaggle كما أرفقناها في نهاية المقال، وتُعَدّ الشيفرة التالية مسؤولةً عن قراءتها على هيئة إطار بيانات بانداز pandas DataFrame: transactions = pandas.read_csv('avocado.csv', parse_dates=[0]) حيث أنّ مهمة الوسيط parse_dates هي الإشارة إلى الدالة read_csv لتفسير القيم الموجودة في العمود رقم 0 (العمود الأول) على أساس تواريخ ومن ثم تحويلها إلى كائنات datetime64 من نوع نمباي NumPy، كما يحتوي إطار البيانات DataFrame على سطر لكل عملية مبيع مسجلة بالإضافة إلى الأعمدة التالية: average_price: السعر الوسطي مقدرًا بالدولار. total_volume: الحجم الكلي المُباع. type: نوع الأفوكادو: عادي أم عضوي. date: تاريخ عملية المبيع. year: عام المبيع. geography: الولاية والمدينة. علمًا أنّ كل عملية مبيع هي حدث في الزمن، لذا يمكننا معاملة مجموعة البيانات هذه على أساس سلسلة زمنية time series، لكن المسافة الزمنية بين الأحداث غير متساوية، إذ يتراوح عدد عمليات المبيع المسجلة بين 0 إلى عدة عشرات يوميًا، حيث تتطلب الكثير من طرق تحليل سلاسل البيانات أن تكون الأزمنة بين الأحداث متساويةً أو على الأقل فإن الأمور أكثر بساطةً إن كانت الأزمنة متساويةً، ومن أجل توضيح هذه الطرق قسَّمنا مجموعة البيانات هذه إلى مجموعات حسب الكمية ومن ثم حوَّلنا كل مجموعة إلى سلسلة تتباعد فيها الأحداث تباعدًا متساويًا عن بعضها وذلك عن طريق حساب متوسط السعر اليومي. def GroupByTypeAndDay(transactions): groups = transactions.groupby('type') dailies = {} for name, group in groups: dailies[name] = GroupByDay(group) return dailies علمًا أن groupby هو تابع خاص بأُطر البيانات وهو يُعيد كائن GroupBy باسم groups، حيث يُستخدم الكائن groups في حلقة for ليمر مرورًا تكراريًا على أسماء المجموعات وأُطر البيانات التي تمثِّلها، وبما أنّ للنوع احتمالين هما عادي أو عضوي، فسينتج مجموعتين تحمل هذه الأسماء -أي عادي وعضوي-، حيث تمر الحلقة مرورًا تكراريًا على المجموعات وتستدعي التابع GroupByDay الذي يحسب متوسط السعر اليومي ويُعيد إطار بيانات جديد: def GroupByDay(transactions, func=np.mean): grouped = transactions[['date', 'average_price']].groupby('date') daily = grouped.aggregate(func) daily['date'] = daily.index start = daily.date[0] one_year = np.timedelta64(1, 'Y') daily['years'] = (daily.date - start) / one_year return daily يُعَدّ المعامِل transactions إطار بيانات ويحتوي على العمودين date وaverage_price، لذا سنحدِّد هذين العمودين ومن ثم نجمِّعهما على أساس التاريخ، وتكون النتيجة هي grouped التي تُعَدّ خريطةً map تحوِّل كل تاريخ إلى إطار بيانات يحتوي على الأسعار التي أُبلِغ عنها في ذلك التاريخ المحدَّد، ويُعَدّ aggregate تابع تجميع GroupB يمر مرورًا تكراريًا على المجموعات ويطبق دالةً على كل عمود من المجموعة، وفي حالتنا هذه لا يوجد سوى عمود واحد هو average_price الذي يمثِّل السعر الوسطي. تُخزَّن البيانات في هذه الأُطر على أساس كائنات نمباي NumPy من نوع datetime64 والتي تمثَّل على أساس أعداد صحيحة حجمها 46 بتًا -أي 64-bit integers- بالنانو ثانية، لكن سيكون من المناسب التعامل في بعض التحليلات القادمة مع وحدات قياس زمنية مألوفة أكثر بالنسبة للبشر مثل السنوات، أي يُضيف التابع GroupByDay عمودًا اسمه date عن طريق نسخ الفهرس ومن ثم يُضيف العمود years الذي يحتوي على عدد السنوات التي مرت منذ أول عملية مبيع وهو عدد عشري، كما يحتوي إطار البيانات الناتج على average_price الذي يمثِّل السعر الوسطي بالدولار والعمود date الذي يمثِّل التاريخ وyears. رسم المخططات تكون نتيجة GroupByTypeAndDay خريطةً map تحوِّل كل نوع إلى إطار بيانات يحتوي على الأسعار اليومية، وإليك الشيفرة التي استخدمناها لرسم السلسلتين الزمنيتين: thinkplot.PrePlot(rows=2) for i, (name, daily) in enumerate(dailies.items()): thinkplot.SubPlot(i+1) title = 'Average Price' if i == 0 else '' thinkplot.Config(ylim=[0, 3], title=title) thinkplot.Scatter(daily.average_price, s=10, label=name) if i == 1: pyplot.xticks(rotation=30) else: thinkplot.Config(xticks=[]) تشير الدالة PrePlot في حال وجود الوسيط rows=2 إلى أننا سنرسم مخططين فرعيين في السطرين، حيث تمر الحلقة مرورًا تكراريًا على أطر البيانات وتنشئ مخطط انتشار لكل إطار، ومن الشائع رسم السلاسل الزمنية مع قطع مستقيمة تصل بين النقطة والأخرى، لكن توجد في هذه الحالة العديد من نقاط البيانات والأسعار متغيرةً تغيرًا كبيرًا، لذا لن يكون من المفيد إضافة القطع المستقيمة، وبما أن التواريخ موجودة على محور x -أي المحور الأفقي-، فسنستخدِم pyplot.xticks للتدوير بمقدار 30 درجة، وبذلك نجعلها مقروءةً أكثر. يوضِّح الشكل السابق سلسلةً زمنيةً تمثِّل متوسط السعر اليومي، وذلك بالنسبة للنوع العادي والنوع العضوي، كما يُظهر الشكل السابق النتيجة، حيث نرى صفةً واضحةً في هذه المخططات وهي وجود بعض الفجوات الزمنية ، فمن المحتمل أنّ جمع البيانات في ذلك الوقت كان متوقفًا أو أنَّ البيانات غير متوافرة، لكننا سنفكر لاحقًا في طرق للتعامل مع هذه البيانات المفقودة على أية حال. يبدو لنا من النظر إلى المخطط أنّ أسعار الأفوكادو وصلت لذروتها في عام 2017 ثم عاودت بالتذبذب إلا أن أسعار الأفوكادو العضوي حافظت دائماً على ارتفاعها مقارنة بالأفوكادو العادي. الانحدار الخطي على الرغم من وجود توابع خاصة بتحليل السلاسل الزمنية، إلا أنّ الطريقة الأبسط التي تحل الكثير من المسائل تتمثل في تطبيق أدوات ذات غرض عام مثل الانحدار الخطي، إذ تأخذ الدالة التالية إطار بيانات للأسعار اليومية وتحسب ملائمة مربعات صغرى، ومن ثم تُعيد النموذج والكائنات الناتجة من StatsModels: def RunLinearModel(daily): model = smf.ols('average_price ~ years', data=daily) results = model.fit() return model, results يمكننا المرور مرورًا تكراريًا على النوعين المختلفين (العادي والعضوي) وملاءمة نموذج لكل منها: dailies = GroupByTypeAndDay(transactions) for name, daily in dailies.items(): model, results = RunLinearModel(daily) print(results.summary() إليك النتائج: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } النوع نقطة التقاطع الميل R2 عادي 1.1348 0.0034 0.001 عضوي 1.6690 0.0183- 0.044 تشير قيم الميل المُقدَّرة إلى انخفاض سعر الأفوكادو العضوي قليلًا (2 سنتًا) في كل سنة ضمن الفترة الزمنية التي رُصدَت الأسعار فيها؛ أما الأفوكادو العادي فقد ارتفع سعره قليلًا (أقل من 1 سنتًا في كل سنة)، علمًا أن التقديرات هذه ذات دلالة إحصائية وقيمها الاحتمالية صغيرة جدًا. إنّ قيمة R2 هي 0.044 للأفوكادو العضوي أي أن الزمن بصفته متغير توضيحي يمثِّل 4%‎ من التباين المرصود في السعر، لكن يكون التغير في السعر أصغر والتباين في الأسعار أقل بالنسبة للأفوكادو العادي، لذا فإن قيم R2 أصغر لكنها ما زالت ذات دلالة إحصائية، وترسم الشيفرة التالية الأسعار المرصودة والقيم الملاءمة: def PlotFittedValues(model, results, label=''): years = model.exog[:,1] values = model.endog thinkplot.Scatter(years, values, s=15, label=label) thinkplot.Plot(years, results.fittedvalues, label='model' ) يحتوي model على exog وendog كما رأينا في قسم سابق في مقال الانحدار الإحصائي regression، حيث أنهما مصفوفتا نمباي NumPy تحتويان على المتغيرات الخارجية -أي التوضيحية- والمتغيرات الداخلية -أي التابعة-. يوضِّح الشكل السابق سلسلةً زمنيةً للأسعار اليومية، بالإضافة إلى ملاءمة مربعات صغرى خطية linear least squares fit، كما تُنشئ الدالة PlotFiitedValues مخططًا انتشاريًا لنقاط البيانات ورسمًا خطيًا للقيم الملائمة، ويُظهر الشكل السابق نتائج الأفوكادو العضوي، حيث يبدو أنّ النموذج يلائم البيانات ملاءمةً جيدةً ولكن الانحدار الخطي ليس الخيار الأفضل لهذه البيانات: أولًا: ما من سبب يدفعنا للتوقُّع أنّ الاتجاه الذي استمر فترةً طويلةً هو خط أو دالة بسيطة، والذي يحدِّد الأسعار عمومًا هو العرض والطلب وكلاهما يختلف بمرور الزمن بطرق لا يمكن التنبؤ بها. ثانيًا: يعطي نموذج الانحدار الخطي وزنًا متساويًا لكل البيانات سواءً البيانات الحديثة أو السابقة، لكن يجب علينا إعطاء البيانات الحديثة وزنًا أكبر. أخيرًا: تقول إحدى الفرضيات حول الانحدار الخطي أنّ الرواسب residuals هي ضجيج غير مترابط، لكن غالبًا ما تكون هذه الفرضية غير صحيحة في حال تعاملنا مع سلاسل زمنية لأن القيم المتتالية مترابطة. يقدِّم القسم التالي بديلًا أفضل للتعامل مع بيانات السلاسل الزمنية. المتوسطات المتحركة تعتمد معظم السلاسل الزمنية على افتراض النمذجة عادةً والذي يقول أنّ السلسلة المرصودة هي ناتج جمع المكوِّنات الثلاثة التالية: الاتجاه: هو دالة ملساء -أي منتظمة- تخزِّن التغييرات المستمرة. الموسمية: هو تباين دوري، وقد يتضمن دورات يومية أو أسبوعية أو شهرية أو سنوية. الضجيج: التباين العشوائي حول الاتجاه طويل الأمد. يُعَدّ الانحدار أحد طرق استخراج الاتجاه من سلسلة معينة تمامًا كما رأينا في القسم السابق، لكن يوجد بديل آخر في حال لم يكن الاتجاه دالةً بسيطةً وهو المتوسط المتحرك moving average، حيث يقسِّم المتوسط المتحرك السلسلة إلى مناطق متداخلة تُدعى نوافذ windows ومن ثم يحسب متوسط القيم في كل نافذة window. يُعَدّ المتوسط المتدحرج rolling mean الذي يحسب متوسط القيم في كل نافذة من أبسط أنواع المتوسطات المتحركة، فإذا كان حجم النافذة 3 مثلًا، فسيحسب المتوسط المتدحرج متوسط القيم من 0 إلى 2 ومن 1 إلى 3 ومن 2 إلى 4 وهكذا دواليك، كما يُبين المثال التالي: df=pandas.DataFrame(np.arange(10)) roll_mean = df.rolling(3).mean() print(roll_mean) nan, nan, 1, 2, 3, 4, 5, 6, 7, 8 نلاحظ أنّ أول قيمتين هما nan -أي ليس عددًا-؛ أما القيمة التالية فهي متوسط العناصر الثلاث الأولى أي 0 و1 و2، والقيمة التالية هي متوسط 1 و2 و3، وهكذا، حيث يتعين علينا التعامل مع القيم المفقودة في البداية وقبل تطبيق rolling على بيانات الأفوكادو، ونلاحظ في الواقع في الفترة المرصودة وجود عدة أيام لم يُبلَّغ فيها عن أيّ عمليات مبيع لنوع معين أو أكثر من نوع -أي النوع العادي أو العضوي- من الأفوكادو. لم تكن هذه التواريخ موجودةً في أيّ من أُطر البيانات التي استخدمناها سابقًا، حيث كان الفهرس يتخطى الأيام التي لا تحتوي على أية بيانات، لكن بالنسبة للتحليل التالي، فنحتاج إلى تمثيل البيانات المفقودة تمثيلًا صريحًا، حيث يمكننا إنجاز ذلك عن طريق إعادة فهرسة reindexing إطار البيانات: dates = pandas.date_range(daily.index.min(), daily.index.max()) reindexed = daily.reindex(dates) يحسب السطر الأول من الشيفرة السابقة مجال التواريخ الذي يتضمن تاريخ كل الأيام من بداية فترة رصد عمليات المبيع حتى اليوم الأخير؛ أما السطر الثاني فينشئ إطار بيانات جديد يحتوي على كل البيانات الموجودة في daily بالإضافة إلى الأسطر التي تحتوي على جميع التواريخ ذات القيمة nan، حيث يمكننا الآن رسم المتوسط المتجدد كما يلي: roll_mean = reindexed.average_price.rolling(30).mean() thinkplot.Plot(roll_mean.index, roll_mean,label="rolling mean") thinkplot.config(xlabel='date', ylabel='average-price') thinkplot.show() حجم النافذة هنا هو 30، لذا فإن كل قيمة في roll_mean هي متوسط 30 قيمة من reindexed.average-price. يوضِّح الشكل السابق الأسعار اليومية والمتوسط المتدحرج rolling mean في الجهة اليسرى والمتوسط المتحرك الموزون أسيًا exponentially-weighted moving average في الجهة اليمنى، كما يُظهر الشكل السابق الموجود في الجهة اليسرى النتيجة، حيث يبدو أن المتوسط المتدحرج قد أتقن تنظيم smoothing الضجيج واستخرج الاتجاه. البديل هو **المتوسط المتحرك الموزون أسيًا exponentially-weighted moving average -أو EWMA اختصارًا-، والذي يتمتع بميزتين اثنتين، حيث يمكننا استنتاج الميزة الأولى من الاسم، أي يحسب متوسطًا موزونًا يكون فيه لأحدث قيمة أعلى وزن؛ أما القيم السابقة فتنخفض قيمتها انخفاضًا أسيًا، والميزة الثانية هي أنّ تنفيذ بانداز pandas للمتوسط المتحرك الموزون أسيًا يعالِج القيم المفقودة بإتقان أكبر. ewm=reindexed['average_price'].ewm(span=30).mean() thinkplot.Plot(ewm) يتوافق المعامِل span تقريبًا مع حجم نافذة متوسط متحرك، كما أنه يتحكم في مدى سرعة انخفاض الأوزان، لذا فهو يحدِّد عدد النقاط التي تقدِّم مساهمةً كبيرةً لكل متوسط من المتوسطات، ويُظهر الشكل السابق الموجود في الجهة اليمنى المتوسط المتحرك الموزون أسيًا EWMA الخاص بالبيانات نفسها، وهو يشبه المتوسط المتدحرج rolling mean في الحالات التي يكون كلاهما معرَّفًا، إلا أنه لا يحتوي على أي قيم مفقودة، لذا فمن السهل التعامل معه، ونلاحظ أنّ القيم تحتوي على ضجيج في بداية السلسلة الزمنية لأنها مبنية على عدد أقل من نقاط البيانات. ewm=reindexed['average_price'].ewm(span=30).mean() thinkplot.Plot(ewm, label="ewma") thinkplot.config(xlabel='date', ylabel='average-price') thinkplot.show() القيم المفقودة ستكون الخطوة التالية بعد أن حدَّدنا توجه السلسلة الزمنية هي البحث في كل موسم على حدة، فالموسم هو فترة أسبوع أو يوم أو سنة أو …إلخ، أي ليس بالضرورة موسم كما يشير الاسم، فهو سلوك دوري، وغالبًا ما تكون السلاسل الزمنية التي تستند إلى السلوك البشري دورات يومية أو أسبوعية أو شهرية أو سنوية، لذا سنقدِّم في القسم التالي طُرقًا لاختبار المواسم لكنها لا تعمل جيدًا في حال وجود قيم مفقودة، لذا علينا حل هذه المشكلة أولًا، حيث توجد طريقة سهلة وشائعة يمكننا من خلالها ملء البيانات المفقودة وهي استخدام متوسط متحرك، حيث يزودنا تابع السلسلة fillna بتنفيذ ملائم جدًا لمتطلباتنا: reindexed.average_price.fillna(ewm, inplace=True) عندما تكون قيمة reindexed.average_price هي nan، فسيستبدلها التابع fillna بالقيم الموافقة من ewm، حيث تشير الراية inplace إلى التابع fillna لكي يعدل السلسلة الحالية بدلًا من إنشاء سلسلة جديدة، وإحدى مساوئ هذه الطريقة هي أنها تقلل من قيمة الضجيج في السلسلة، لكن يمكننا حل هذه المشكلة عن طريق إضافة الرواسب التي طُبِّق عليها أخذ عينات: resid = (reindexed.average_price - ewm).dropna() fake_data = ewm + thinkstats2.Resample(resid, len(reindexed)) reindexed.average_price.fillna(fake_data, inplace=True) يحتوي المتغير resid على قيم الرواسب، لكن لا يحتوي على الأيام التي يكون فيها average_price أي السعر الوسطي هو nan، كما يحتوي fake_data على مجموع المتوسط المتحرك وعينة عشوائية من الرواسب، وأخيرًا، يستبدل التابع fillna قيم fake_data بقيم من nan. يوضِّح الشكل السابق الأسعار اليومية بعد ملء القيم ويُظهر النتيجة، حيث تشبه البيانات المملوءة القيم الفعلية من الناحية البصرية، وبما أنّ الرواسب التي تطُبِّق عليها إعادة أخذ عينات هي قيم عشوائية، فستكون النتائج مختلفةً في كل مرة، لذا سنرى لاحقًا طريقةً لوصف الخطأ الذي نتج عن القيم المفقودة. الارتباط التسلسلي قد تتوقع أن تجد أنماطًا متكررةً لأن الأسعار تتغيَّر يوميًا، فإذا كان السعر مرتفعًا يوم الاثنين، فقد تتوقع أن يكون مرتفعًا للأيام القليلة التالية، وإذا كان منخفضًا، فقد تتوقع أن يبقى منخفضًا، حيث يُدعى هذا النمط الارتباط التسلسلي serial correlation لأن كل قيمة مترابطة مع القيمة التالية في السلسلة، ويمكنك إزاحة السلسلة الزمنية بمقدار قدره تأخير lag من أجل حساب الارتباط التسلسلي ومن ثم حساب الارتباط بين السلسلة المزاحة والسلسلة الأصلية: def SerialCorr(series, lag=1): xs = series[lag:] ys = series.shift(lag)[lag:] corr = thinkstats2.Corr(xs, ys) return corr تكون قيم التأخير الأول nan بعد أول إزاحة، لذا فقد استخدمنا شريحةً لإزالتها قبل حساب Corr، فإذا طبقنا SerialCorr على بيانات الأسعار الأولية بقيمة تأخير 1، فسنجد أنَّ الارتباط التسلسلي 0.26 للأفوكادو العضوي و 0.39 للأفوكادو العادي، كما نتوقَّع رؤية ارتباط تسلسلي قوي في حال كانت السلسلة الزمنية ذا اتجاه طويل الأمد، فإذا كانت الأسعار تنخفض على سبيل المثال، فسنتوقع رؤية قيم النصف الأول من السلسلة أعلى من المتوسط، وقيم النصف الثاني من السلسلة أقل من المتوسط، ومن المثير للاهتمام رؤية فيما إذا كان الارتباط مستمرًا إذا لم نأخذ الاتجاه بالحسبان، إذ يمكننا مثلًا حساب راسب المتوسط المتحرك الموزون أسيًا ومن ثم حساب ارتباطه التسلسلي كما يلي: ewm=reindexed['average_price'].ewm(span=30).mean() resid = reindexed.average_price - ewm corr = thinkstats2.SerialCorr(resid, 1) تكون الارتباطات التسلسلية للبيانات التي أهملنا فيها الاتجاه في حال كانت قيمة التأخير 1 أي lag=1 هي 0.89 بالنسبة للأفوكادو العضوي، والقيمة 0.86 بالنسبة للأفوكادو العادي، كما تُعَدّ قيمًا كبيرة مما يشير إلى وجود ترابط تسلسلي يومي جيد، حيث سننفِّذ التحليل مرةً أخرى مع قيم تأخير مختلفة وذلك للتحقق من وجود موسمية أسبوعية أو شهرية أو سنوية، وإليك نتائج التحليل: التأخير العادي العضوي 1 0.86 0.89 7 0.29 0.37 30 -0.18 -0.33 300 0.33 -0.5 سنُجري في القسم التالي اختبارات لنعلم ما إذا كانت هذه الارتباطات ذات دلالة إحصائية (ليست ذات دلالة إحصائية)، ولكن يمكننا مبدئيًا استنتاج أنه لا توجد أنماط موسمية كبيرة في السلسلة، فعلى الأقل لا توجد أنماط بوجود هذه التأخيرات. الارتباط الذاتي ستضطر إلى اختبار جميع القيم إذا اعتقدت بوجود ارتباط تسلسلي في سلسلة زمنية ما لكنك لست متأكدًا من قيم التأخير التي يجب عليك اختبارها، حيث تُعَدّ دالة الارتباط الذاتي autocorrelation function دالةً تحوِّل التأخير lag إلى ارتباط تسلسلي بتأخير مُعطى، كما يُعَدّ الارتباط الذاتي والارتباط التسلسلي وجهَين لعملة واحدة، أي أنهما يشيران إلى المفهوم ذاته لكن غالبًا يُستخدَم الارتباط الذاتي عندما تكون قيمة التأخير مختلفةً عن 1، كما تزودنا StatsModels التي استخدمناها في الانحدار الخطي في المقال السابق بدوال لتحليل السلاسل الزمنية مثل acf التي تحسب دالة الارتباط الذاتي كما يلي: import statsmodels.tsa.stattools as smtsa acf = smtsa.acf(filled.resid, nlags=365) تحسب الدالة acf الارتباطات التسلسلية مع قيم تأخير بين 0 وnlags، وتكون النتيجة مصفوفةً من الارتباطات، فإذا حددنا الأسعار اليومية للأفوكادو العضوي واستخرجنا قيم الارتباطات في حال كانت قيم التأخير هي 1 و 7 و 30 و 365، فسيمكننا عندها التأكُّد من إنتاج الدالتين acf وSerialCorr النتائج نفسها تقريبًا: >>> acf[0], acf[1], acf[7], acf[30], acf[365] 1.000, 0.859, 0.286, -0.176, 0.000 تحسب الدالة acf ارتباط السلسلة مع نفسها إذا كانت قيمة التأخير 0 أي lag=0، علمًا أن الارتباط في هذه الحالة هو 1 دائمًا. يوضِّح الشكل السابق دالة الارتباط الذاتي للأسعار اليومية في الجهة اليسرى؛ أما في الجهة اليمنى فيوضِّح الأسعار اليومية إذا أجرينا محاكاةً لموسمية أسبوعية، حيث يُظهر الشكل السابق في الجهة اليسرى دوال الارتباط الذاتي للأفوكادو العادي والعضوي وذلك من أجل nlags=40، حيث تُظهر المنطقة الرمادية التباين الطبيعي الذي نتوقعه إذا لم يكن هناك أي ارتباط ذاتي فعلي، علمًا أن القيم التي تقع خارج هذا المجال هي ذات دلالة إحصائية وقيمتها الاحتمالية p-value هي أقل من 5‎%‎، وبما أن معدل الإيجابية الكاذبة هو ‎5%‎ ونحن في طور حساب 120 ارتباطًا (بمعدل 40 تأخير لكل سلسلة من السلسلتين الزمنيتين)، فسنتوقع رؤية 6 نقاط خارج هذه المنطقة وفي الواقع يوجد أكثر من 6 نقاط، لذا نستنتج أنه لا يمكننا تفسير أيّ ارتباط ذاتي في السلسلة على أنه حدث بمحض الصدفة فحسب. حسبنا المناطق الرمادية عن طريق إعادة أخذ عينات الرواسب (ويمكنك الاطلاع على شيفرتنا في في الرابط وتُدعى الدالة SimulateAutocorrelation)، ولرؤية كيف تبدو دالة الارتباط الذاتي في حال وجود موسمية من نوع ما، ولَّدنا بيانات محاكاة وذلك عن طريق إضافة دورة أسبوعية، ففي حال كان الطلب على الأفوكادو أعلى في العطل الأسبوعية، فقد نتوقع أن يكون السعر أعلى، حيث حدَّدنا التواريخ التي تصادف يومي الجمعة والسبت ومن ثم أضفنا قيمةً عشوائيةً مُختارةً من توزيع منتظم بين 0$ و 2$ على السعر وذلك من أجل محاكاة هذا التأخير. def AddWeeklySeasonality(daily): frisat = (daily.index.dayofweek==4) | (daily.index.dayofweek==5) fake = daily.copy() fake.average_price[frisat] += np.random.uniform(0, 2, frisat.sum()) return fake يُعَدّ frisat سلسلةً بوليانيةً، بحيث تكون القيمة True ليومي الجمعة والسبت، كما يُعَدّ fake إطار بيانات جديد وهو نسخة من إطار البيانات daily بعد أن أجرينا عليه تعديل إضافة قيم عشوائية إلى average_price.frisat.sum وهو العدد الكلي لأيام الجمعة والسبت التي ظهرت، أي عدد القيم التي سيتوجب علينا توليدها. يُظهر الشكل السابق في الجهة اليمنى دوال الارتباط الذاتي للأسعار مع موسمية محاكاة، حيث نرى كما هو متوقَّع أن الارتباطات أعلى في حال كان التأخير من مضاعفات العدد 7، كما تكون الارتباطات لنوعي الأفوكادو ليست ذات دلالة إحصائية لأن الرواسب هنا كبيرة ومن المفترض أن يكون التأخير أكبر ليكون مرئيًا في وسط كل هذا الضجيج. التنبؤ يمكن استخدام تحليل السلاسل الزمنية لاكتشاف أو شرح سلوك الأنظمة التي تتغير بمرور الزمن، كما يمكن استخدامها لتوليد تنبؤات، كما يمكن استخدام الانحدار الخطي الذي تناولناه في قسم الانحدار الخطي في هذا المقال لتوليد تنبؤات أيضًا، حيث يزودنا الصنف RegressionResults بالدالة predict التي تأخذ إطار بيانات يحتوي على المتغيرات التوضيحية ويُعيد تسلسلًا من التنبؤات، وإليك الشيفرة الموافقة كما يلي: def GenerateSimplePrediction(results, years): n = len(years) inter = np.ones(n) d = dict(Intercept=inter, years=years) predict_df = pandas.DataFrame(d) predict = results.predict(predict_df) return predict يُعَدّ results كائنًا من الصنف RegressionResults، ويُعَدّ years تسلسلًا من قيم الزمن التي نريد استنباط تنبؤاتها، كما تبني الدالة إطار بيانات وتمرره إلى الدالة predict وتُعيد النتيجة، فإذا كان ما نريد هو تنبؤ وحيد وأفضل ما في الإمكان فقط، فستكون مهمتنا قد انتهت هنا، لكن من المهم حساب الخطأ في معظم الأحيان، أي نريد بكلمات أخرى معرفة دقة التنبؤ المحتملة، كما يجب علينا أخذ مصادر الخطأ الثلاثة هذه بالحسبان: خطأ أخذ العينات: يكون التنبؤ هنا مبنيًا على المعامِلات المُقدَّرة التي تعتمد على التبيان العشوائي في العينة، حيث نتوقَّع تغيُّر التقديرات إذا أجرينا التجربة مرةً ثانيةً. التباين العشوائي: ستتغير البيانات المرصودة قرب الاتجاه طويل الأمد حتى لو كانت المعاملات المُقدَّرة دقيقةً تمامًا، ونتوقع استمرار هذا التباين في المستقبل أيضًا. خطأ النمذجة: لدينا أدلةً تثبت أنّ الاتجاه طويل الأمد ليس خطيًا، لذا فإن التنبؤات مبنية على نموذج خطي سيفشل عاجلًا أم آجلًا. من مصادر الخطأ الأخرى التي يجب علينا أخذها بالحسبان هي الحوادث المستقبلية غير المتوقعة مثل تأثر أسعار المنتجات الزراعية بالطقس وتأثر جميع الأسعار بالقوانين والسياسات، ومن الصعب حساب أخطاء النمذجة والأخطاء المستقبلية غير المتوقعة، ومن السهل التعامل مع خطأ أخذ العينات والتباين العشوائي لذا سنبدأ بهذين النوعين. استخدمنا إعادة أخذ العينات كما فعلنا في قسم الرواسب في مقال المربعات الصغرى الخطية في بايثون بهدف حساب خطأ أخذ العينات، وكما هي العادة فهدفنا هو استخدام عمليات الرصد الفعلية لإجراء محاكاة لما يمكن أن يحدث إذا أجرينا التجربة مرةً أخرى، حيث أنّ عمليات المحاكاة مبنية على افتراض أن المعاملات المقدَّرة صحيحة، لكن قد تكون الرواسب العشوائية مختلفةً، وإليك الدالة التي تجري عمليات المحاكاة: def SimulateResults(daily, iters=101): model, results = RunLinearModel(daily) fake = daily.copy() result_seq = [] for i in range(iters): fake.average_price = results.fittedvalues + thinkstats2.Resample(results.resid) _, fake_results = RunLinearModel(fake) result_seq.append(fake_results) return result_seq يُعَدّ daily إطار بيانات يحتوي على الأسعار المرصودة، وiters هو عدد عمليات المحاكاة التي يجب تشغيلها، كما تستخدِم الدالة SimulateResults الدالة RunLinearModel من قسم الانحدار الخطي الموجود في هذا المقال، وذلك من أجل تقدير ميل القيم المرصودة ونقطة تقاطعها، كما تولِّد الدالة مجموعة بيانات مزيفة في كل تكرار من الحلقة عن طريق إعادة أخذ عينات الرواسب وإضافتها إلى القيم الملائمة، ومن ثم تشغِّل نموذجًا خطيًا على البيانات المزيفة وتخزِّن الكائن RegresssionResults، في حين تكون الخطوة القادمة هنا هي استخدام النتائج التي أجرينا عليها محاكاةً من أجل توليد تنبؤات: def GeneratePredictions(result_seq, years, add_resid=False): n = len(years) d = dict(Intercept=np.ones(n), years=years, years2=years**2) predict_df = pandas.DataFrame(d) predict_seq = [] for fake_results in result_seq: predict = fake_results.predict(predict_df) if add_resid: predict += thinkstats2.Resample(fake_results.resid, n) predict_seq.append(predict) return predict_seq تأخذ الدالة GeneratePredictions تسلسل النتائج من الخطوة السابقة، بالإضافة إلى القيم years والتي هي تسلسل من القيم العشرية التي تحدِّد المجال الذي يجب علينا توليد تنبؤات له، كما تأخذ هذه الدالة الوسيط add_resid الذي يخبرنا فيما إذا كان يجب إضافة رواسب مُعاد أخذ عيناتها إلى التنبؤ المباشر، حيث تمر GeneratePredictions مرورًا تكراريًا على التسلسل RegressionResults وتولِّد تسلسلًا من التنبؤات. وأخيرًا إليك الشيفرة التي ترسم مجال الثقة 90‎%‎ للتنبؤات: def PlotPredictions(daily, years, iters=101, percent=90): result_seq = SimulateResults(daily, iters=iters) p = (100 - percent) / 2 percents = p, 100-p predict_seq = GeneratePredictions(result_seq, years, True) low, high = thinkstats2.PercentileRows(predict_seq, percents) thinkplot.FillBetween(years, low, high, alpha=0.3, color='gray') predict_seq = GeneratePredictions(result_seq, years, False) low, high = thinkstats2.PercentileRows(predict_seq, percents) thinkplot.FillBetween(years, low, high, alpha=0.5, color='gray') تستدعي الدالة PlotPredictions دالة GeneratePredictions مرتين، مرةً إذا كان add_resid=True ومرةً أخرى إذا كان add_resid=False، كما تستخدِم PercentileRows لتحديد المئين 95 والمئين 5 لكل سنة، وترسم أخيرًا منطقةً رماديةً بين الحدَّين. يُظهر الشكل السابق النتيجة، حيث تمثِّل المنطقة الرمادية الداكنة 90‎%‎ من مجال الثقة لخطأ أخذ العينات، أي عدم اليقين بشأن الميل المقدَّر ونقطة التقاطع بسبب أخذ العينات، تُظهر المنطقة فاتحة اللون مجال ثقة 90‎%‎ لخطأ التنبؤ وهو نتيجة جمع التباين العشوائي مع خطأ أخذ العينات. تحسب هاتان المنطقتان خطأ أخذ العينات والتباين العشوائي وليس خطأ النمذجة، فمن الصعب عمومًا حساب خطأ النمذجة، لكن يمكننا في هذه الحالة معالجة مصدر خطأ واحد على الأقل وهو الأحداث الخارجية غير المتوقعة، فنموذج الانحدار مبني على افتراض أنّ النظام ثابت stationary، أي أن معامِلات النموذج لا تتغير بمرور الزمن، خاصةً الميل ونقطة التقاطع بالإضافة إلى توزيع الرواسب. لكن سيبدو لنا بالنظر إلى المتوسطات المتحركة في الشكل 12.5 أنّ الميل يتغير مرةً واحدةً على الأقل خلال فترة الرصد، وسيبدو تباين الرواسب في النصف الأول أكبر من تباينه في النصف الثاني، ونتيجةً لذلك نقول أنّ المعاملات تعتمد على الفترة التي نرصد فيها عمليات البيع، ولرؤية مدى تأثير هذا على التنبؤات يمكننا توسيع SimulateResults لاستخدام فترات الرصد لكن مع تغيير موعد بدء الرصد وانتهائه. يوضِّح الشكل السابق التنبؤات المبنية على الملاءمة الخطية ويُظهِر التباين الناتج عن فترة الرصد، كما يُظهر النتيجة من أجل الأفوكادو العادي، في حين تُظهر المنطقة الرمادية الفاتحة اللون مجال الثقة الذي يحتوي على عدم اليقين الناتج عن خطأ أخذ العينات، وكذلك يحتوي على التباين العشوائي والتباين في فترة الرصد، علمًا أنّ ميل النموذج المبني على الفترة الكلية موجب، مما يدل على أنّ الأسعار آخذة في الارتفاع، لكن تُظهر الفترة الأحدث أدلةً على انخفاض الأسعار، لذا فإن ميل النماذج المبنية على البيانات الأحدث سالب، وبالتالي تتضمن أوسع فترة تنبؤية إمكانية خفض الأسعار خلال العام المقبل. لقراءة أكثر تفصيلا يُعَدّ تحليل السلاسل الزمنية موضوعًا كبيرًا، ولا يتناول هذا المقال سوى جزء يسير منه، إذ يُعَدّ الانحدار الذاتي autoregression من الأدوات الهامة للتعامل مع السلاسل الزمنية، لكنا لم نذكره في هذا المقال، ويعود ذلك إلى أنه قد تبين أنه ليس مفيدًا للبيانات التي عملنا معها، لكن سيؤهلك فهمك للمواد الموجودة في هذا المقال لتعلُّم الانحدار الذاتي، كما يمكننا اقتراح مصدر يتناول موضوع تحليل السلاسل الزمنية وهو تحليل البيانات باستخدام أدوات مفتوحة المصدر، أروايلي ميديا، 2011 (Data Analysis with Open Source Tools, O’Reilly Media, 2011) للكاتب فيليب جارنيت Philipp Janert، حيث يُعَدّ المقال الذي يتحدث فيه عن تحليل السلاسل الزمنية استمرارًا لهذا المقال. تمارين إليك التمارين التالية لحلها والتدرب عليها، وانتبه إلى أننا تصرفنا في النص أثناء ترجمته لذا لن تكون الحلول في الملف chap12soln.py في مستودع الشيفرات ThinkStats2 على GitHub صالحة. تمرين 1 يملك النموذج الخطي الذي استخدمناه في هذا المقال عيبًا واضحًا وهو أنه خطي، وما من سبب يدفعنا للتوقُع أن تغير الأسعار سيبقى خطيًا بمرور الوقت، حيث يمكننا إضافة مرونة إلى النموذج عن طريق إضافة مصطلح تربيعي كما فعلنا في قسم العلاقات اللاخطية في المقال الماضي. استخدم نموذجًا تربيعيًا لملاءمة السلسلة الزمنية للأسعار اليومية، واستخدم هذا النموذج لتوليد التنبؤات، لكن سيكون عليك في البداية كتابة نسخة من RunLinearModel لتستطيع تشغيل هذا النموذج التربيعي، وبعد ذلك يمكنك توليد التنبؤات عن طريق إعادة استخدام الشيفرة التي استخدمناها في هذا المقال. تمرين 2 اكتب تعريفًا للصنف SerialCorrelationTest يرث الصنف HypothesisTest من القسم HypothesisTest الموجود في مقال اختبار الفرضيات الإحصائية، بحيث يأخذ سلسلةً series وتأخيرًا lag على أساس وسيطين، ومن ثم يحسب الارتباط التسلسلي للسلسلة مع التأخير المُعطى، ثم يحسب الصنف القيمة الاحتمالية p-value للارتباط المرصود. استخدم هذا الصنف لتعلم ما إذا كان الارتباط التسلسلي ذا دلالة إحصائية أم لا، واختبرأيضًا الرواسب للنموذج التربيعي والنموذج الخطي (إذا حللت التمرين السابق). تمرين 3 يمكننا توسيع نموذج المتوسط المتحرك الموزون أسيًا EWMA لتوليد التنبؤات باستخدام عدة طرق، ومن أبسطها اتباع الخطوات التالية: احسب المتوسط المتحرك الموزون أسيًا EWMA للسلسلة الزمنية، ومن ثم استخدِم النقطة الأخيرة على أساس نقطة تقاطع inter. احسب المتوسط المتحرك الموزون أسيًا EWMA للفروق بين العناصر المتتالية في السلسلة الزمنية، ومن ثم استخدِم النقطة الأخيرة على أساس ميل slope. احسب inter+slope * dt للتنبؤ بالقيم المستقبلية، حيث أنّ dt هو الفرق بين زمن التنبؤ وزمن آخر عملية رصد. استخدِم هذه الطريقة لتوليد تنبؤات لمدة سنة بعد آخر عملية رصد، وإليك بعض التلميحات: استخدِم timeseries.FillMissing لملء القيم المفقودة قبل إجراء هذا التحليل لكي يكون الزمن بين العناصر المتتالية متسقًا. استخدِم Series.diff لحساب الفرق بين العناصر المتتالية. استخدِم reindex لتوسيع فهرس إطار البيانات في المستقبل. استخدِم fillna لوضع القيم التي تنبأت بها في إطار البيانات. ترجمة وبتصرف للفصل Chapter 12 Time series Analysis من كتاب Think Stats: Exploratory Data Analysis in Python. الملف المرفق dataset.zip. اقرأ أيضًا المقال التالي: كيفية إجراء تحليل البقاء لمعرفة المدة الافتراضية للأشياء المقال السابق: الانحدار الإحصائي regression ودوره في ملاءمة النماذج المختلفة مع أنواع البيانات المتاحة
  2. ركَّزت هذه السلسلة على الأساليب الحسابية مثل المحاكاة وإعادة أخذ العينات، لكن قد يكون من الأسرع حل بعض المسائل بالاستعانة بالأساليب التحليلية، حيث سنتناول في هذا المقال بعضًا من هذه الطرق وسنشرح كيفية عملها، كما سنقدِّم اقتراحات في نهاية المقال لدمج الأساليب الحسابية والتحليلية لتحليل البيانات الاستكشافية. توجد الشيفرة الخاصة بهذا المقال في الملف normal.py، في مستودع الشيفرات ThinkStats2 على GitHub. التوزيع الطبيعي دعنا نتحدث عن المسألة الموجودة في مقال التقدير Estimation الإحصائي في بايثون: يجب أن يكون توزيع أخذ عينات x̄ معروفًا إذا أردنا الإجابة على هذا السؤال، وكما رأينا في قسم توزيع أخذ العينات في مقال التقدير Estimation الإحصائي في بايثون المشار إليه بالأعلى، فإننا قرّبنا التوزيع عن طريق إجراء محاكاة للتجربة -أي تجربة وزن 9 إناث غوريلا- ثم حساب x̄ لكل تجربة محاكاة وتجميع توزيع التقديرات، وتكون النتيجة هنا هي تقريب لتوزيع أخذ العينات، ثم نستخدِم توزيع أخذ العينات لحساب الأخطاء المعيارية وفواصل الثقة: يُعَدّ الانحراف المعياري لتوزيع أخذ العينات هو الخطأ المعياري للتقدير، ويكون في هذا المثال حوالي 2.5 كيلوغرامًا. الفاصل بين المئين 5 والمئين 95 لتوزيع أخذ العينات هو فاصل ثقة ‎90%‎ تقريبًا، وإذا أجرينا التجربة عدة مرات، فسنتوقع أن يكون التقدير في هذا الفاصل ‎90% من المرات، حيث تكون قيمة فاصل الثقة 90‎%‎ في هذا المثال هي (94, 86) كيلوغرامًا. سنجري الآن الحسابات ذاتها بأسلوب تحليلي، وسنستفيد من حقيقة أنّ أوزان إناث الغوريلا البالغات هي توزيع طبيعي تقريبًا، إذ تملك التوزيعات الطبيعية خاصتين اثنتين تجعلها قابلةً للتحليل، فهي مغلقة في التحويل الخطي والإضافة، لكنا نحتاج إلى بعض الرموز لشرح معنى هذا الكلام، فإذا كان توزيع كمية X طبيعيًا وكان يحوي وسيطَين هما µ وσ، فيمكننا القول: X ∼ N (µ, σ2) حيث يشير الرمز ∼ إلى أن الكمية موزعة ويشير الرمز N إلى طبيعي normal؛ أما التحويل الخطي لـ X فهو ‎X′ = a X + b، حيث أنّ a وb هما عددان حقيقيان، وتكون عائلة من التوزيعات مغلقةً في التحويل الخطي إذا كانت X′‎ في عائلة X نفسها، ويكون للتوزيع الطبيعي هذه الخاصية إذا كان X ∼ N (µ, σ2). X′ ∼ N (a µ + b, a2 σ2) تُعَدّ التوزيعات الطبيعية مغلقةً في الإضافة، فإذا كانت Z = X + Y و X ∼ N (µX, σX2) وY ∼ N (µY, σY2) فيكون: Z ∼ N (µX + µY, σX2 + σY2) تكون المعادلة التالية محققة في الحالة الخاصة عندما Z = X + X Z ∼ N (n µX, n σX2) إذا سحبنا n قيمة من X وجمعناها يكون عمومًا: X ∼ N (µ, σ2) توزيعات أخذ العينات لدينا كل ما نحتاجه لحساب توزيع أخذ عينات x̄، وتذكَّر أنه سنحسب x̄ عن طريق وزن n إناث غوريلا ونجمع القيم لنحسب الوزن الكلي ثم نقسم المجموع على n، فبفرض أنّ X توزيع أوزان الغوريلا هو توزيع طبيعي تقريبًا: X ∼ N (µ, σ2) يكون الوزن الكلي Y موزعًا إذا وزَنّا n غوريلا. Y ∼ N (n µ, n σ2) يكون Z متوسط العينة موزعًا إذا قسمنا على n وبالاستعانة بالمعادلة الثالثة. Z ∼ N(, 2ln) بالاستعانة بالمعادلة الأولى بافتراض a = 1/n. يكون توزيع Z هو توزيع أخذ عينات x̄، ومتوسط Z هو µ الذي يظهر أن x̄ هو تقدير غير متحيز للمقدار µ، في حين يكون تباين توزيع أخذ العينات هو σ2/n، لذا فإن الانحراف المعياري لتوزيع أخذ العينات الذي يمثل الخطأ المعياري للتقدير هو σ / √n، ويكون σ في هذا المثال هو 7.5 كيلوغرامًا وn هو 9، لذا يكون الخطأ المعياري هو 2.5 كيلوغرامًا، ونلاحظ أنّ النتيجة متسقة مع التقدير الذي نتج عن المحاكاة لكن أسرع في الحساب. يمكننا أيضًا استخدام توزيع أخذ العينات لحساب فواصل الثقة، حيث أنّ فاصل الثقة ‎90% لـ x̄ هو الفاصل بين المئين 9 والمئين 95 لـ Z، وبما أنّ توزيع Z توزيع طبيعي، فيمكننا حساب قيم المئين عن طريق تقييم دالة التوزيع التراكمي العكسية، كما لا يوجد شكل مغلق من دالة التوزيع التراكمي للتوزيع الطبيعي أو دالة التوزيع التراكمي العكسية، لكن توجد أساليب عددية سريعة وهي موجود على أساس تنفيذ برمجي في حزمة ساي باي SciPy كما رأينا في قسم التوزيع الطبيعي في مقال نمذجة التوزيعات Modelling distributions في بايثون، كما تزودنا مكتبة thinkstats2 بدالة مغلفة تجعل دالة ساي باي SciPy سهلة الاستخدام: def EvalNormalCdfInverse(p, mu=0, sigma=1): return scipy.stats.norm.ppf(p, loc=mu, scale=sigma) يعيد المئين الموافق من توزيع طبيعي له الوسيطين mu وsigma إذا كان لدينا احتمال p، كما حسبنا من أجل فاصل الثقة ‎90% للمقدار x̄ المئين 5 والمئين 95 كما يلي: >>> thinkstats2.EvalNormalCdfInverse(0.05, mu=90, sigma=2.5) 85.888 >>> thinkstats2.EvalNormalCdfInverse(0.95, mu=90, sigma=2.5) 94.112 لذا إذا أجرينا التجربة عدة مرات، فسنتوقع أن يكون التقدير في المدى (94.1, 85.9) حوالي ‎90% من المرات، وهذا متسق مع النتائج التي حصلنا عليها عندما أجرينا محاكاة. تمثيل التوزيعات الطبيعية عرّفنا صنفًا يدعى Normal يمثِّل التوزيع الطبيعي ويرمز المعادلات الموجودة في الأقسام السابقة بهدف توضيح هذه الحسابات، أي كما يلي: class Normal(object): def __init__(self, mu, sigma2): self.mu = mu self.sigma2 = sigma2 def __str__(self): return 'N(%g, %g)' % (self.mu, self.sigma2) يمكننا استنساخ الصنف Normal لتمثيل توزيع أوزان الغوريلا: >>> dist = Normal(90, 7.5**2) >>> dist N(90, 56.25) يزودنا الصنف Normal بالدالة Sum التي تأخذ حجم العينة n وتعيد توزيع مجموع n قيمة باستخدام المعادلة الثالثة: def Sum(self, n): return Normal(n * self.mu, n * self.sigma2) يمكن تطبيق عمليات القسمة والضرب باستخدام المعادلة الأولى: def __mul__(self, factor): return Normal(factor * self.mu, factor**2 * self.sigma2) def __div__(self, divisor): return 1 / divisor * self يمكننا الآن حساب توزيع أخذ عينات المتوسط مع حجم عينة قدره 9: >>> dist_xbar = dist.Sum(9) / 9 >>> dist_xbar.sigma 2.5 يكون الانحراف المعياري لتوزيع أخذ العينات هو 2.5 كيلوغرامًا كما رأينا في القسم السابق، وأخيرًا يزودنا الصنف Normal بالدالة Percentile التي تحسب فاصل الثقة كما يلي: >>> dist_xbar.Percentile(5), dist_xbar.Percentile(95) 85.888 94.113 هذه هي الإجابة ذاتها التي حصلنا عليها سابقًا، حيث سنستخدم الصنف Normal مرةً أخرى لاحقًا، لكن علينا استكشاف بعض أساليب التحليل الأخرى أولًا قبل ذلك. مبرهنة النهاية المركزية رأينا في الأقسام السابقة أنه إذا جمعنا القيم المأخوذة من توزيع طبيعي، فسيكون توزيع المجموع طبيعيًا، ولكن لا تتميز معظم التوزيعات الأخرى بهذه الخاصية، أي إذا جمعنا القيم المأخوذة من توزيعات أخرى، فلن يكون المجموع توزيعًا تحليليًا عادةً، لكن إذا جمعنا n قيمة من معظم التوزيعات، فسيتقارب توزيع المجموع إلى التوزيع الطبيعي مع زيادة n. وبتحديد أكبر، إذا كان لتوزيع القيم متوسطًا µ وانحرافًا معياريًا σ، فسيكون توزيع المجموع N(n µ, nσ 2) تقريبًا، وتكون هذه النتيجة هي مبرهنة النهاية المركزية -أو CLT اختصارًا-، إذ تُعَدّ من أفضل الأدوات للتحليل الإحصائي، لكن مع بعض التحذيرات وهي: يجب أخذ القيم بصورة مستقلة، إذ لا يمكن تطبيق مبرهنة النهاية المركزية إذا كانت القيم مترابطة على الرغم من أنه نادرًا ما يمثِّل مشكلةً أثناء التطبيق العملي. يجب انتماء القيم إلى التوزيع نفسه على الرغم أنه يمكن التغاضي عن هذا الشرط إلى حد ما. يجب أخذ القيم من توزيع له متوسط وتباين محدودَين، لذا لا تنطبق معظم توزيعات باريتو Pareto على هذا الشرط. يعتمد معدل التقارب على تجانف التوزيع، إذ تتلاقى المجاميع من التوزيع الأسي إذا كانت n صغيرةً، في حين تتطلب مجاميع القيم المأخوذة من التوزيع اللوغاريتمي الطبيعي أحجامًا أكبر. تشرح مبرهنة النهاية المركزية انتشار التوزيعات الطبيعية في العالم الطبيعي، وتتأثر العديد من خصائص الكائنات الحية بالعوامل الوراثية والبيئية التي يكون تأثيرها مضافًا، كما تكون الخصائص التي نقيسها هي مجموع عدد كبير من التأثيرات الصغيرة، لذا يميل توزيعها إلى أن يكون طبيعيًا. اختبار مبرهنة النهاية المركزية سنجري بعض التجارب لنرى متى وكيف تنطبق مبرهنة النهاية المركزية، وسنجرب في البداية توزيعًا أسيًا: def MakeExpoSamples(beta=2.0, iters=1000): samples = [] for n in [1, 10, 100]: sample = [np.sum(np.random.exponential(beta, n)) for _ in range(iters)] samples.append((n, sample)) return samples تولِّد الدالة MakeExpoExamples عينات من مجاميع القيم الأسية، حيث استخدمنا مصطلح القيم الأسية على أساس اختصار لجملة القيم المأخوذة من توزيع أسي، ويكون beta هو وسيط التوزيع؛ أما iters هو عدد المجاميع التي يجب توليدها، ولتفسير هذه الدالة سنبدأ من الداخل أولًا، حيث نحصل على تسلسل من n قيمة أسية في كل استدعاء للدالة np.normal.exponential ونحسب مجموعها. يُعَدّ sample قائمةً لهذه المجاميع وبطول iters، ومن الصعب التمييز بين n وiters، لكن n هو عدد التعبيرات في كل مجموع، وiters هو عدد المجاميع التي نحسبها لوصف توزيع المجاميع، حيث أنّ القيمة المعادة هي قائمة من أزواج (n, sample)، ثم ننشئ رسمًا احتماليًا طبيعيًا لكل زوج: def NormalPlotSamples(samples, plot=1, ylabel=''): for n, sample in samples: thinkplot.SubPlot(plot) thinkstats2.NormalProbabilityPlot(sample) thinkplot.Config(title='n=%d' % n, ylabel=ylabel) plot += 1 تأخذ NormalPlotSamples قائمة الأزواج من MakeExpoSamples وتولِّد سطرًا من رسوم الاحتمالات الطبيعية. يوضِّح الشكل السابق توزيع مجاميع القيم الأسية في السطر العلوي والقيم اللوغاريتمية الطبيعية في السطر السفلي، كما يُظهر الشكل السابق الموجود في الأعلى النتائج، إذ يكون توزيع المجموع أسيًا من أجل n=1، لذا فإن رسم الاحتمال الطبيعي ليس مستقيمًا، لكن إذا كان n=10، فيكون توزيع المجموع طبيعيًا تقريبًا، وإذا كان n=100، فلا يمكن تمييز التوزيع عندها عن الطبيعي. يُظهر الشكل السابق في السطر السفلي نتائجًا مشابهةً للتوزيع اللوغاريتمي الطبيعي، إذ عادةً ما تكون التوزيعات اللوغاريتمية الطبيعية أكثر تجانفًا من التوزيعات الأسية، لذا يأخذ توزيع المجاميع وقتًا أطول لكي يتقارب، وإذا كان n=10، يكون الرسم الاحتمالي الطبيعي أبعد ما يكون عن المستقيم، لكن إذا كان n=100 فيكون التوزيع طبيعيًا تقربيًا. يُظهر الشكل السابق توزيعات مجاميع قيم باريتو Pareto في السطر العلوي والقيم الأسية المترابطة في السطر السفلي، حيث تُعَدّ توزيعات باريتو Pareto أكثر تجانفًا من التوزيعات اللوغاريتمية الطبيعية، وغالبًا لا يكون للعديد من توزيعات باريتو Pareto متوسطًا وتباينًا محدودَين اعتمادًا على المعامِلات، وبالتالي لا تنطبق مبرهنة النهاية المركزية على توزيع باريتو Pareto، كما يُظهر الشكل السابق في السطر العلوي توزيعات مجاميع قيم باريتو Pareto، فحتى إذا كان n=100، فسيكون الرسم الاحتمالي الطبيعي أبعد ما يكون عن المستقيم. ذكرنا أيضًا أنه لا يمكن تطبيق مبرهنة النهاية المركزية إذا كانت القيم مترابطةً، ولاختبار ذلك سنولِّد قيمًا مترابطةً من التوزيع الأسي، علمًا أنّ خطوات الخوارزمية لتوليد القيم المترابطة هي: توليد القيم العادية المترابطة. استخدام دالة التوزيع التراكمي الطبيعي لجعل القيم موحدةً. 3.استخدام دالة التوزيع التراكمي العكسية الأسية لتحويل القيم الموحَّدة إلى أسية. تعيد الدالة GenerateCorrelated مكررًا لـ n قيمة طبيعية من الارتباط التسلسلي rho : def GenerateCorrelated(rho, n): x = random.gauss(0, 1) yield x sigma = math.sqrt(1 - rho**2) for _ in range(n-1): x = random.gauss(x*rho, sigma) yield x تكون القيمة الأولى قيمةً طبيعيةً معياريةً، وتعتمد كل قيمة لاحقة على سابقتها، أي إذا كانت القيمة السابقة هي x، فيكون متوسط القيمة التالية هوx * rho ويكون التباين هو 1‎-rho**2، علمًا أنّ random.gauss تأخذ الانحراف المعياري على أساس وسيط ثان وليس التباين، كما تأخذ الدالة GenerateExpoCorrelated التسلسل الناتج وتجعله أسيًا: def GenerateExpoCorrelated(rho, n): normal = list(GenerateCorrelated(rho, n)) uniform = scipy.stats.norm.cdf(normal) expo = scipy.stats.expon.ppf(uniform) return expo حيث يكون normal قائمةً من القيم الطبيعية المترابطة، وuniform تسلسلًا من القيم الموحَّدة التي تقع بين 0 و1، وexpo تسلسلًا مترابطًا من القيم الأسية، في حين ترمز ppf إلى دالة نقطة النسبة المئوية percent point function التي هي اسم آخر لدالة التوزيع التراكمي المعكوسة. يُظهر الشكل السابق في السطر السفلي توزيعات مجاميع القيم الأسية المترابطة إذا كان rho=0.9، ويبطئ الترابط من معدل التقارب، لكن إذا كان n=100، فيكون الرسم الاحتمالي الطبيعي مستقيمًا تقريبًا، لذا على الرغم من أنّ مبرهنة النهاية المركزية لا تطبق تمامًا عندما تكون القيم مترابطة، إلا أنه نادرًا ما تشكِّل الترابطات المتوسطة مشكلةً أثناء التطبيق العملي، كما تهدف هذه التجارب إلى إظهار الطريقة التي تعمل بها مبرهنة النهاية المركزية بالإضافة إلى إظهار ماذا يحدث عندما لا تعمل، ودعونا الآن نرى كيف يمكننا استخدامها. تطبيق مبرهنة النهاية المركزية علينا العودة إلى المثال الموجود في قسم اختبار الفرق في المتوسطات في مقال اختبار الفرضيات الإحصائية، وهو اختيار الفرق الواضح في متوسط مدة الحمل للأطفال الأوائل والأطفال الآخرين، وكما رأينا فإن الفرق الواضح هو حوالي 0.078 أسبوع: >>> live, firsts, others = first.MakeFrames() >>> delta = firsts.prglngth.mean() - others.prglngth.mean() 0.078 تذكَّر منطق اختبار الفرضيات: نحسب القيمة الاحتمالية p-value وهي احتمال الفرق المرصود في ظل فرضية العدم، فإذا كان الاحتمال صغيرًا، فنستنتج أنه من غير المرجح أن يكون الفرق المرصود ناجمًا عن الصدفة فحسب، وتكون فرضية العدم في هذا المثال هي أنّ توزيع مدة الحمل هي نفسها للأطفال الأوائل ولبقية الأطفال، لذا يمكننا حساب توزيع أخذ عينات المتوسط كما يلي: dist1 = SamplingDistMean(live.prglngth, len(firsts)) dist2 = SamplingDistMean(live.prglngth, len(others)) علمًا أنّ توزيعي أخذ العينات مبنيان على البيانات نفسها وهي مجموعة الولادات الحية كلها، حيث تأخذ SamplingDistMeans تسلسلًا من القيم وحجم العينة، وتعيد كائنًا طبيعيًا يمثِّل توزيع أخذ العينات: def SamplingDistMean(data, n): mean, var = data.mean(), data.var() dist = Normal(mean, var) return dist.Sum(n) / n يمثِّل mean متوسط البيانات؛ أما var فهو التباين، وسننشئ تقريبًا لتوزيع البيانات بالاستعانة بتوزيع طبيعي dist، إذ يُعَدّ توزيع البيانات في هذا المثال لاطبيعيًا، لذا فإنّ هذا التقريب غير جيد، لكن علينا الآن حساب dis.Sum(n)/n وهو توزيع أخذ عينات متوسط n قيمة، ويكون حسب مبرهنة النهاية المركزية أنّ توزيع أخذ عينات المتوسط هو توزيع طبيعي حتى لو لم يكن توزيع البيانات طبيعيًا، ثم نحسب توزيع أخذ عينات الفرق في المتوسطات، حيث يعلم الصنف Normal كيفية تطبيق الطرح باستخدام المعادلة الثانية: def __sub__(self, other): return Normal(self.mu - other.mu, self.sigma2 + other.sigma2) لذا يمكننا حساب توزيع أخذ عينات الفرق كما يلي: >>> dist = dist1 - dist2 N(0, 0.0032) يكون المتوسط هو 0، وهذا منطقي لأننا نتوقع أن يكون للعينتين من التوزيع نفسه المتوسط نفسه وسطيًا، ويكون تباين توزيع أخذ العينات هو 0.0032، كما يزودنا الصنف Normal بالدالة Prob التي تقيّم دالة التوزيع التراكمي الطبيعية، ويمكننا استخدام Prob لحساب احتمالية وجود فرق بحجم delta في ظل فرضية العدم: >>> 1 - dist.Prob(delta) 0.084 يعني هذا أنّ القيمة الاحتمالية للاختبار أحادي الجانب هو 0.84؛ أما بالنسبة للاختبار ثنائي الجانب فسنحسب كما يلي: >>> dist.Prob(-delta) 0.084 ظهر لدينا النتيجة نفسها لأن التوزيع الطبيعي متناظر، ويكون مجموع الذيول هو 0.168، وهو متسق مع التقدير في قسم اختبار الفرق في المتوسطات في مقال اختبار الفرضيات الإحصائية الذي كانت قيمته 0.17. اختبار الارتباط استخدمنا في قسم اختبار الارتباط في مقال اختبار الفرضيات الإحصائية من هذه السلسلة والمشار إليه بالأعلى، اختبار التبديل permutation test لاختبار الارتباط بين وزن الطفل عند الولادة وعمر الأم، ووجدنا أنه ذو دلالة إحصائية والقيمة الاحتمالية هي أقل من 0.001، حيث يمكننا فعل الشيء ذاته لكن بأسلوب تحليلي مبني على نتيجة رياضية: إذا كان لدينا متغيرين موزعَين طبيعيًا وغير مترابطَين، فإذا ولدنا عينةً حجمها n وحسبنا ارتباط بيرسون r ثم حسبنا الارتباط بعد التحويل، يكون: t = r √ n−2 1−r2 يُعَدّ توزيع t هو توزيع ستيودنت الاحتمالي Student’s t-distribution مع معامِل n-2، حيث يُعَدّ التوزيع t توزيعًا تحليليًا، ويمكن حساب دالة التوزيع التراكمي بفعالية باستخدام دوال غاما gamma، حيث يمكننا استخدام النتيجة لحساب توزيع أخذ عينات الارتباط في ظل فرضية العدم، أي إذا ولَّدنا التسلسلات غير المترابطة للقيم الطبيعية، فما هو توزيع الارتباط؟ تأخذ الدالة StudentCdf حجم العينة n ويُعيد توزيع أخذ عينات الارتباط: def StudentCdf(n): ts = np.linspace(-3, 3, 101) ps = scipy.stats.t.cdf(ts, df=n-2) rs = ts / np.sqrt(n - 2 + ts**2) return thinkstats2.Cdf(rs, ps حيث أنّ ts هي مصفوفة نمباي NumPy لتوزيع t وهو الارتباط بعد التحويل، كما تحتوي ps على الاحتمالات الموافقة المحسوبة باستخدام دالة التوزيع التراكمي لتوزيع ستيودنت الاحتمالي وهي منفَّذة برمجيًا في حزمة ساي باي SciPy، ويمثِّل معامِل توزيع t (أي t-distribution) الذي يدعى df درجات الحرية degrees of freedom، ولن نشرح هذا المصطلح لكن يمكنك القراءة عنه في صفحة الويكيبيديا. يوضِّح الشكل السابق توزيع أخذ عينات ارتباط القيم الطبيعية غير المرتبطة، ويتوجب علينا تطبيق التحويل العكسي إذا أردنا تحويل ts إلى معاملات الترابط rs: r = t / √ n − 2 + t2 تكون النتيجة هي توزيع أخذ عينات r في ظل فرضية العدم، كما يُظهر الشكل السابق هذا التوزيع إلى جانب التوزيع الذي ولَّدناه في قسم اختبار الارتباط في مقال اختبار الفرضيات الإحصائية، وذلك عن طريق تطبيق إعادة أخذ العينات، فالتوزيعان متطابقان تقريبًا، فعلى الرغم من أنّ التوزيعين الفعليين ليسا طبيعيين، إلا أنّ معامِل ارتباط بيرسون مبني على متوسطي وتبايني العينة، وبحسب مبرهنة النهاية المركزية فإنّ الإحصائيات المبنية على العزوم موزعة توزيعًا طبيعيًا حتى لو لم تكن البيانات كذلك. نستنتج من الشكل السابق أنّ قيمة الارتباط المرصود هو 0.07، ومن غير المرجح أن تظهر لنا هذه القيمة إذا لم تكن المتغيرات مرتبطةً، كما يمكننا حساب مدى احتمال حدوث ذلك باستخدام التوزيع التحليلي: t = r * math.sqrt((n-2) / (1-r**2)) p_value = 1 - scipy.stats.t.cdf(t, df=n-2) نحسب قيمة t الموافقة لـ r=0.07 ثم نقيِّم توزيع t عند t، ونلاحظ أنّ النتيجة هي 2.9e-11، إذ يُظهر هذا المثال إحدى ميزات الأسلوب التحليلي، حيث يمكننا حساب قيم احتمالية صغيرة جدًا لكن لا يهمنا هذا الأمر في الحالات الواقعية عادةً. اختبار مربع كاي استخدمنا في قسم اختبارات مربع كاي الموجود في مقال اختبار الفرضيات الإحصائية إحصائيات مربع كاي لنختبر فيما إن كان حجر النرد ملتويًا، حيث تقيس إحصائية مربع كاي الانحراف الكلي الموحَّد عن القيم المتوقعة في جدول: χ2 = ∑ i (Oi − Ei)2 Ei يُعَدّ توزيع أخذ العينات فيها تحليليًا في ظل فرضية العدم، وهو من أسباب شيوع استخدام إحصائية مربع كاي، وبصدفة رائعة فإنه يدعى توزيع مربع كاي، ويمكن حساب دالة التوزيع التراكمي لمربع كاي بكفاءة باستخدام دوال غاما تمامًا مثل توزيع t. يوضِّح الشكل السابق توزيع أخذ عينات إحصائية مربع كاي للنرد العادل ذي الوجوه الستة، حيث تزودنا مكتبة ساي باي SciPy بتنفيذ برمجي لتوزيع مربع كاي الذي يمكننا استخدامه لحساب توزيع أخذ عينات إحصائية مربع كاي كما يلي: def ChiSquaredCdf(n): xs = np.linspace(0, 25, 101) ps = scipy.stats.chi2.cdf(xs, df=n-1) return thinkstats2.Cdf(xs, ps) يُظهر الشكل السابق النتيجة التحليلية إلى جانب التوزيع الذي حصلنا عليه عن طريق تطبيق إعادة أخذ العينات، وهما متماثلان جدًا خاصةً من حيث شكل الذيل وهو الجزء الذي يهمنا، كما يمكننا استخدام هذا التوزيع لحساب القيمة الاحتمالية لإحصائية الاختبار chi2: p_value = 1 - scipy.stats.chi2.cdf(chi2, df=n-1) نرى أن النتيجة هي 0.041 وهي متسقة مع النتيجة التي رأيناها في قسم اختبارات مربع كاي الموجود في مقال اختبار الفرضيات الإحصائية، حيث أن معامِل توزيع مربع كاي هو درجة الحرية أيضًا، ويكون المعامِل الصحيح في هذه الحالة هو n-1 حيث أنّ n هو حجم الجدول 6، وقد يكون اختيار هذا المعامِل أمرًا صعبًا، وفي الواقع لا نستطيع التأكد من أننا أصبنا حتى نولِّد شكلًا مثل الشكل السابق لنقارن النتائج التحليلية مع نتائج إعادة أخذ العينات. نقاش تركِّز هذه السلسلة على الأساليب الحسابية مثل إعادة أخذ العينات والتبديل، وتملك هذه الأساليب ميزات لا تمتلكها الأساليب التحليلية مثل: سهلة الفهم والشرح، إذ يُعَدّ اختبار الفرضيات على سبيل المثال من أصعب المواضيع في مجال لإحصاء، ولا يستطيع العديد من الطلاب فهم ماهية القيم الاحتمالية، لكن الطريقة التي شرحناها في مقال اختبار الفرضيات الإحصائية جعلت المفهوم أوضح والتي هي حساب إحصائية الاختبار ومحاكاة فرضية العدم. متينة ومتعددة الاستعمالات، إذ غالبًا ما تكون الأساليب التحليلية مبنيةً على افتراضات لا تنطبق على الواقع؛ أما الأساليب الحسابية فهي تتطلب افتراضات أقل ويمكن تعديلها وتوسيعها بصورة أسهل. يمكن تصحيحها، لكن غالبًا ما تكون الأساليب التحليلية أشبه بالصندوق الأسود، حيث تدخل الأعداد وتخرج الأساليب النتائج، لذا من السهل ارتكاب أخطاء خفية ومن الصعب التأكد من صحة النتائج ومن الصعب إيجاد المشكلة إذا كانت خاطئة؛ أما الأساليب الحسابية فهي قابلة للتطوير والاختبار التدريجي مما يعزِّز الثقة في النتائج. لكن هناك سلبية واحدة وهي أنّ الأساليب الحسابية بطيئة، لكن إذا أخذنا السلبيات والإيجابيات بالحسبان، فنعتقد أنّ العملية التالية هي الأفضل: استخدم الأساليب الحسابية أثناء الاستكشاف، وإذا وجدت إجابةً مرضيةً وزمنًا مقبولًا للتنفيذ، يمكنك التوقف. إذا لم يكن زمن التنفيذ مقبولًا، فابحث عن حلول للتحسين. إذا كان استخدام الأسلوب التحليلي أنسب من الأسلوب الحسابي، فاستخدم الأسلوب الحسابي ليكون أساسًا للمقارنة، إذ سيوفِّر لك هذا الأمر إمكانية التحقق المتبادل في النتائج الحسابية والتحليلية. لم تتطلب معظم المسائل التي عملت عليها تجاوز الخطوة الأولى من العملية السابقة. تمارين يوجد حل هذه التمارين في الملف chap14soln.py في مستودع الشيفرات ThinkStats2 على GitHub.. تمرين 1 رأينا في قسم التوزيع اللوغاريتمي الطبيعي الموجود في مقال نمذجة التوزيعات Modelling distributions في بايثون، أنّ توزيع أوزان البالغين لوغاريتمي طبيعي تقريبًا، وتتمثَّل إحدى التفسيرات في أنّ الوزن الذي يكتسبه الشخص في كل عام يتناسب مع وزنه الحالي، ويكون وزن البالغين في هذه الحالة ناتجًا عن عدد كبير من العوامل التي نطبق بينها عملية جداء: w = w0 f1 f2 … fn حيث أنّ w هو وزن البالغ، وw<sub>0</sub>‎ هو وزن الطفل عند الولادة، وf<sub>i</sub>‎ هو عامل الوزن المكتسب في العام i، علمًا أنّ لوغاريتم الجداء هو جمع لوغاريتمات العوامل: logw = logw0 + logf1 + logf2 + ⋯ + logfn يكون توزيع logw حسب مبرهنة النهاية المركزية طبيعيًا تقريبًا إذا كانت n كبيرةً، مما يعني أنّ توزيع w لوغاريتمي طبيعي، ويمكنك من أجل نمذجة هذه الظاهرة اختيار توزيع منطقي لـ f ثم توليد عينة من أوزان البالغين عن طريق اختيار قيمة عشوائية من توزيع أوزان المواليد ثم اختيار تسلسل عوامل من توزيع f وحساب الجداء؛ ما هي قيمة n التي نحتاجها للتقارب من توزيع لوغاريتمي طبيعي؟ تمرين 2 استخدمنا في هذا المقال مبرهنة النهاية المركزية لإيجاد توزيع أخذ عينات الفرق في المتوسطات δ في ظل فرضية العدم التي تقول أنّ العينتين مأخوذتان من البيانات نفسها، يمكننا أيضًا استخدام هذا التوزيع لإيجاد الخطأ المعياري للتقدير ولفواصل الثقة لكن لن تكون النتيجة صحيحة تمامًا، وبصورة أدق، يجب حساب توزيع أخذ العينات الخاص بـ δ بموجب الفرضية البديلة التي مفادها أنّ العينات مأخوذة من مجموعات مختلفة. أوجد هذا التوزيع واستخدمه لحساب الخطأ المعياري وفاصل الثقة ‎90% للفرق في المتوسطات. تمرين 3 بحث القائمون على ورقة بحثية حديثة في تأثيرات التدخل الهادف إلى تخفيف النمطية بين الجنسَين فيما يخص توزيع المهام داخل المجموعات في الكليات الهندسية، حيث أجاب الطلاب والطالبات على استطلاع قبل وبعد التدخل، وكان مفاد الاستطلاع الطلب من المشاركين تقييم مساهمتهم في كل جانب من جوانب المشاريع الصفيّة على مقياس مكوَّن من 7 نقاط. سجّل الطلاب الذكور قبل التدخل درجات أعلى فيما يخص البرمجة في المشروع مقارنةً بالطالبات، وسجّل الرجال في المتوسط درجة 3.57 مع خطأ معياري قدره 0.28، بينما سجّلت النساء في المتوسط 1.91 مع خطأ معياري قدره 0.32. احسب توزيع أخذ العينات للفجوة بين الجنسين -أي الفرق في المتوسطات-، واختبر ما إذا كان التوزيع ذا دلالة إحصائية، ولا تحتاج إلى معرفة حجم العينة لتحسب توزيعات أخذ العينات لأنك تعلم الأخطاء المعيارية للمتوسطات المقدَّرة. أصبحت الفجوة بعد التدخل أصغر، حيث أصبح المتوسط الحسابي للرجال هو 3.44 وبخطأ معياري قدره 0.16؛ أما المتوسط الحسابي للنساء فهو 3.18 وبخطأ معياري قدره 0.16؛ احسب توزيع العينات للفجوة بين الجنسين مرةً أخرى واختبرها. اختبر أخيرًا التغيير في الفجوة بين الجنسين، وما هو توزيع أخذ عينات هذا التغيير؟ وهل له دلالة إحصائية؟ ترجمة -وبتصرف- للفصل Chapter 14 Analytics methods analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا المقال السابق: كيفية إجراء تحليل البقاء لمعرفة المدة الافتراضية للأشياء العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون التوزيعات الإحصائية في بايثون div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  3. يُعَدّ تحليل البقاء survival analysis أحد طرق وصف مدة بقاء شيء ما، حيث يستخدَم لدراسة عمر الإنسان غالبًا، ولكنه ينطبق أيضًا على بقاء الأجهزة الميكانيكية والإلكترونية، أو قد يدل على الفترات الزمنية التي تسبق حدثًا ما. فلربما قد رأيت سابقًا مصطلح "معدل البقاء على قيد الحياة لمدة 5 سنوات" إذا شُخِّص أحد معارفك بمرض خطير، وهو احتمال بقاء المريض على قيد الحياة لمدة 5 سنوات بعد التشخيص، علمًا أنّ هذا التقدير والإحصاءات ذات الصلة هي نتيجة لتحليل البقاء. توجد الشيفرة الخاصة بهذا المقال في الملف survival.py، في مستودع الشيفرات ThinkStats2 على GitHub. منحنيات البقاء يُعَدّ منحني البقاء survival curve الذي يرمز له بـ S(t) المفهوم الأساسي في تحليل البقاء، كما يُعَدّ دالةً تحوِّل المدة t إلى احتمال البقاء أطول من t، ويُعَدّ حساب منحني البقاء سهلًا إذا علمت توزيع المدة أو مدة الحياة، حيث يمكن حساب المنحني عندها عن طريق حساب مكمل دالة التوزيع التراكمي كما يلي: S(t)=1-CDF(t) حيث يكون CDF(t) هو احتمال أن تكون مدة البقاء على قيد الحياة أقل أو تساوي t، ونعلم مثلًا في مجموعة بيانات المسح الوطني لنمو الأسرة مدة حالات الحمل التامة التي بلغ عددها 1189 حالة، حيث يمكننا قراءة هذه البيانات وحساب دالة التوزيع التراكمي كما يلي: preg = nsfg.ReadFemPreg() complete = preg.query('outcome in [1, 3, 4]').prglngth cdf = thinkstats2.Cdf(complete, label='cdf') يدل رمز الخرج 1 على ولادة حية، ويدل رمز الخرج 3 على ولادة جنين ميت، في حين يدل رمز الخرج 4 على حالة إجهاض لا إرادية -أي غير متعمدة من قبل الأم-، كما استبعدنا حالات الإجهاض المتعمدة وحالات الحمل خارج الرحم وحالات الحمل التي كانت مستمرة أثناء مقابلة المستجيبة وذلك لأغراض هذا التحليل، كما يأخذ تابع إطار البيانات query تعبيرًا بوليانيًا ويقيّمه لكل سطر، ومن ثم يحدِّد الأسطر التي ينتج عنها قيمة True. يوضِّح الشكل السابق دالة التوزيع التراكمي ومنحني البقاء لمدة الحمل في الأعلى؛ أما في الأسفل فيوضِّح منحني الخطر hazard curve، حيث عرّفنا كائنًا يغلِّف صنف Cdf وينفذ الواجهة: class SurvivalFunction(object): def __init__(self, cdf, label=''): self.cdf = cdf self.label = label or cdf.label @property def ts(self): return self.cdf.xs @property def ss(self): return 1 - self.cdf.ps يزودنا الصنف SurvivalFunction بخاصيتين اثنتين هما ts وهي تسلسل مدد الحياة وss التي هي منحني البقاء، إذ تُعَدّ الخاصية في لغة بايثون تابعًا يمكن استدعاؤه كما لو أنه متغير، كما يمكننا استنتاج الصنف SurvivalFunction عن طريق تمرير دالة التوزيع التراكمي لمدة الحياة كما يلي: sf = SurvivalFunction(cdf) كما يزودنا الصنف SurvivalFunction بالدالتين __getitem__ وProb اللتين تقيّمان منحني البقاء. # class SurvivalFunction def __getitem__(self, t): return self.Prob(t) def Prob(self, t): return 1 - self.cdf.Prob(t) يُعَدّ sf[13] على سبيل المثال نسبة حالات الحمل التي تجاوزت الثلث الأول من الحمل: >>> sf[13] 0.86022 >>> cdf[13] 0.13978 نرى أنّ 86‎%‎ من حالات الحمل تتجاوز الثلث الأول من الحمل؛ أما النسبة المتبقية 14‎‎%‎ فهي لا تتجاوز هذه المدة، كما يزودنا الصنف SurvivalFunction بالدالة Render التي ترسم sf باستخدام الدوال الموجودة في المكتبة thinkplot: thinkplot.Plot(sf) يُظهر الشكل السابق الموجود في الأعلى النتيجة، حيث يكون المنحني مسطحًا تقريبًا بين الأسبوعين 13 و26، مما يدل على أن عدد قليل من حالات الحمل تنتهي في الثلث الثاني من الحمل، ويكون المنحني أكثر حدةً عند حوالي 39 أسبوعًا وهي أكثر فترات الحمل شيوعًا. دالة الخطر يمكننا اشتقاق دالة الخطر hazard function من منحني البقاء، حيث تُعَدّ دالة الخطر لمدة الحمل دالةً تحوِّل الزمن t إلى نسبة حالات الحمل التي تستمر حتى المدة t ومن ثم تنتهي عند t، ونقول بصورة أدق: λ(t) = S(t) − S(t+1) S(t) يُعَدّ البسط نسبة مدة الحياة التي تنتهي عند t وهي تمثِّل أيضًا دالة الكثافة الاحتمالية عند t أي PMF(t)، كما يزودنا الصنف SurvivalFunction بالدالة MakeHazard التي تحسب دالة الخطر: # class SurvivalFunction def MakeHazard(self, label=''): ss = self.ss lams = {} for i, t in enumerate(self.ts[:-1]): hazard = (ss[i] - ss[i+1]) / ss[i] lams[t] = hazard return HazardFunction(lams, label=label) حيث يُعَدّ الكائن HazardFuntion مغلِفًا لسلسلة بانداز: class HazardFunction(object): def __init__(self, d, label=''): self.series = pandas.Series(d) self.label = label قد يكون d قاموسًا أو أيّ نوع آخر قادر على استنساخ سلسلة تتضمن سلسلةً أخرى، في حين يكون label سلسلةً نصيةً مستخدَمةً لتحديد HazardFunction عند رسمه، كما يزودنا HazardFunction بالدالة __getitem__، وبالتالي يمكننا تقييمه كما يلي: >>> hf = sf.MakeHazard() >>> hf[39] 0.49689 لذا تنتهي حوالي ‎50% من بين جميع حالات الحمل التي تستمر حتى الأسبوع 39 في الأسبوع 39. يُظهر الشكل السابق الموجود في الأسفل دالة الخطر hazard function لمدة الحمل، كما نرى أنّ دالة الخطر بعد الأسبوع 42 تصبح غير منتظمة لأنها مبنية على عدد صغير من الحالات، لكن بخلاف ذلك يكون شكل المنحني كما هو متوقع، بحيث يبلغ ذروته عند الأسبوع 30 تقريبًا ويصبح في الثلث الأول أعلى من الثلث الثاني، كما تُعَدّ دالة الخطر مفيدةً لوحدها، لكنها أيضًا أداةً مهمةً لتقدير منحنيات البقاء كما سنرى في القسم التالي. استنتاج منحنيات البقاء إذا علمت دالة التوزيع التراكمي CDF، فمن السهل حساب دالة البقاء ودالة الخطر، لكن من الصعب في كثير من المواقف الواقعية قياس توزيع مدة الحياة مباشرةً ويجب علينا استنتاجها، فلنفترض مثلًا أنك تراقب مجموعةً من المرضى لترى المدة التي بقوا فيها على قيد الحياة بعد التشخيص، وبما أنّ التشخيص لا يكون في اليوم نفسه لكل المرضى، فسيعيش بعض المرضى فترةً أطول من غيرهم في أي فترة من الزمن، وبالطبع نعلم مدة بقاء المرضى الذين تُوفوا، إلا أننا لا نعلم مدة بقاء المرضى الذين لا زالوا على قيد الحياة وإنما لدينا حدًا أدنى لمدة البقاء. يمكننا حساب منحني البقاء إذا انتظرنا وفاة جميع المرضى، لكننا لن نستطيع الانتظار مدةً طويلةً إذا كنا بصدد تقييم فعالية دواء جديد، لذا نحتاج إلى تقدير منحنيات البقاء باستخدام معلومات غير مكتملة، وبالانتقال إلى مثال مُبهج، استخدمنا بيانات المسح الوطني لنمو الأسرة لحساب مدة بقاء المستجيبين بدون أول حالة زواج، أي المدة التي تسبق أول حالة زواج، بالطبع فإن المستجيبين هم من النساء كون الأسئلة تخص حالات الحمل، كما يتراوح مدى عمر المستجيبات بين 14 و 44 سنة، وبالتالي تزودنا مجموعة البيانات بلمحة عن النساء في مراحل مختلفة من حياتهن. تتضمن مجموعة البيانات بالنسبة للنساء المتزوجات تاريخ أول زواج بالإضافة إلى عمر المرأة عندها؛ أما بالنسبة لغير المتزوجات فنحن نعلم عمر المستجيبة أثناء المسح لكننا لا نعلم متى ستتزوج أو أنها ستتزوج حتى، ونظرًا لأننا نعلم عمر أول حالة زواج لبعض النساء، فسيبدو لنا مغريًا استبعاد بقية النساء وحساب دالة التوزيع التراكمي للبيانات المعلومة، ولكنها فكرة سيئة لأن النتيجة ستكون في هذه الحالة مضللةً جدًا لسببين اثنين هما: سينتج عن هذا مبالغة في تمثيل النساء الأكبر عمرًا، لأنه من المرجح أن تكون هذه الفئة متزوجة أثناء إجراء المسح. سينتج مبالغة في تمثيل النساء المتزوجات. سيؤدي هذا التحليل في الواقع إلى استنتاج مفاده أنّ جميع النساء يتزوجن، وهذا الأمر غير صحيح وضوحًا. تقدير كابلان ماير ليس من المفضل في هذا المثال تضمين حالات النساء غير المتزوجات وإنما هو أمر ضروري، وهو ما يقودنا إلى إحدى الخوارزميات الأساسية في تحليل البقاء والتي هي تقدير كابلان ماير Kaplan-Meier estimation. تستند الفكرة العامة على استخدام البيانات لتقدير دالة الخطر ومن ثم تحويل دالة الخطر إلى منحني البقاء، وإذا أردنا تقدير تابع الخطر، فيمكننا من أجل كل عمر الأخذ في الحسبان: (1) عدد النساء اللواتي تزوجن في هذا العمر و(2) عدد النساء "المعرضات لخطر" الزواج، وهذا يتضمن النساء اللواتي لم يتزوجن من قبل، وإليك الشيفرة الموافقة كما يلي: def EstimateHazardFunction(complete, ongoing, label=''): hist_complete = Counter(complete) hist_ongoing = Counter(ongoing) ts = list(hist_complete | hist_ongoing) ts.sort() at_risk = len(complete) + len(ongoing) lams = pandas.Series(index=ts) for t in ts: ended = hist_complete[t] censored = hist_ongoing[t] lams[t] = ended / at_risk at_risk -= ended + censored return HazardFunction(lams, label=label) تُعَدّ complete أنها الحالات الكاملة التي رُصِدَت، وتكون في مثالنا هذا أعمار المستجيبات عندما تزوجن، في حين تُعَدّ ongoing أنها الحالات غير الكاملة وهي أعمار النساء غير المتزوجات في المسح. نحسب بدايةً hist_complete، وهو دالة عدادة Counter تحوِّل العمر إلى عدد النساء المتزوجات في هذا العمر، كما نحسب hist_ongoing هو دالة عدادة Counter تحوِّل العمر إلى عدد النساء غير المتزوجات اللواتي قوبِلن في ذلك العمر؛ أما ts فهو اجتماع الأعمار التي تزوجت فيها المستجيبات والأعمار التي قوبلت فيها النساء غير المتزوجات مرتبًا ترتيبًا تصاعديًا، كما تتتبّع at_risk عدد المستجيبات المعرضات للخطر في كل عمر وهو العدد الكلي للمستجيبات، وتُخزن النتيجة في سلسلة Series بانداز Pandas والتي تحول كل عمر إلى دالة الخطر المقدَّرة في ذلك العمر. نتعامل في كل مرور على الحلقة مع عمر واحد t ونحسب عدد الأحداث التي تنتهي عند t -أي عدد المستجيبات المتزوجات عند هذا العمر- وعدد الأحداث التي أوقِفت عند t -أي عدد النساء اللواتي قوبِلن عند t ولكن تواريخ زواجهن المستقبلية موقفة censored- ويشير مصطلح "أوقف" إلى أنّ البيانات غير متاحة بسبب عملية جمع البيانات، كما تُعَدّ دالة الخطر المُقدَّرة بأنها نسبة الحالات المعرَّضة للخطر والتي تنتهي عند t، ونطرح في نهاية الحلقة من at_risk عدد الحالات التي انتهت أو أوقفت عند t، ثم نمرِّر في النهاية lams إلى الباني HazardFunction ونُعيد النتيجة. منحني الزواج علينا تنظيف البيانات وتحويلها إذا أردنا اختبار هذه الدالة، علمًا أنّ المتغيرات التي نحتاجها من المسح الوطني لنمو الأسرة هي: cmbirth: يوم ميلاد كل مستجيبة وهو معلوم في كل الحالات. cmintvw: تاريخ مقابلة كل مستجيبة وهو معلوم في كل الحالات. cmmarrhx: تاريخ أول حالة زواج للمستجيبة إذا كانت متزوجةً وكان التاريخ معلومًا. evrmarry: قيمة هذا المتغير 1 إذا كانت المستجيبة قد تزوجت قبل تاريخ المقابلة و0 بخلاف ذلك. حيث أن المتغيرات الثلاثة الأولى مرمَّزة بنظام أشهر القرن وهو العدد الصحيح للأشهر منذ شهر 12 من عام 1899، أي يكون شهر القرن 1 هو شهر 1 من عام 1900، وسنقرأ في البداية ملف المستجيبات ونستبدل قيم cmmarrhx غير الصالحة كما يلي: resp = chap01soln.ReadFemResp() resp.cmmarrhx.replace([9997, 9998, 9999], np.nan, inplace=True ثم نحسب عمر كل مستجيبة عند الزواج وعند مقابلتها: resp['agemarry'] = (resp.cmmarrhx - resp.cmbirth) / 12.0 resp['age'] = (resp.cmintvw - resp.cmbirth) / 12.0 ثم نستخرِج complete وهو عمر النساء المتزوجات عند زواجهن واللاتي لم تزلن متزوجات، وongoing وهو عمر النساء اللاتي لا يحققن ما سبق أثناء المقابلة: complete = resp[resp.evrmarry==1].agemarry ongoing = resp[resp.evrmarry==0].age سنحسب أخيرًا دالة الخطر: hf = EstimateHazardFunction(complete, ongoing) يُظهر الشكل 13.2 (الموجود في الجهة العليا) دالة الخطر المُقدَّرة، وهي منخفضة في فترة المراهقة ومرتفعة في العشرينات من العمر وتنخفض في الثلاثينات وتعود لترتفع في الأربعينات، لكن هذا ناتج عملية التقدير، حيث سينتج عن زواج عدد صغير من النساء خطرًا مقدَّرًا كبيرًا مع نقصان المستجيبات لمعرضات للخطر، لكن سيخفف منحني البقاء من هذا الضجيج. تقدير منحني البقاء يمكننا تقدير منحني البقاء عند حصولنا على دالة الخطر، حيث أنّ فرصة البقاء بعد الوقت t هو فرصة البقاء لكل الأوقات بدءًا من بداية الرصد حتى t، وهو ناتج الجداء التراكمي لدالة الخطر المكملة: [1−λ(0)] [1−λ(1)] … [1−λ(t)] يزودنا الصنف HazardFunction بالدالة MakeSurvival التي تحسب هذا الجداء: # class HazardFunction: def MakeSurvival(self): ts = self.series.index ss = (1 - self.series).cumprod() cdf = thinkstats2.Cdf(ts, 1-ss) sf = SurvivalFunction(cdf) return sf حيث أنّ ts هو تسلسل الأوقات التي قُدِّرت فيها دالة الخطر، وss هو ناتج الجداء التراكمي لدالة الخطر المكملة، وبالتالي فهو منحني البقاء، كما يتوجب علينا حساب مكمل ss ومن ثم إنشاء Cdf واستنساخ كائن SurvivalFunction وذلك بسبب الطريقة التي يُنفَّذ بها SurvivalFunction. يوضَّح الشكل السابق الموجود في الأعلى دالة الخطر لعمر أول زواج؛ أما الذي في الأسفل فيوضَّح منحني البقاء، كما يُظهر الشكل السابق الموجود في الأسفل النتيجة، حيث يكون منحني البقاء أكثر حدةً بين العمرين 25 و35، وهو المدى الذي تتزوج فيه معظم النساء؛ أما بين العمرين 35 و45 فيكون المنحني مسطحًا تقريبًا، مما يشير إلى أنه من غير المرجح زواج النساء اللواتي لم يتزوجن قبل سن الخامس والثلاثين. كان منحني ما مثل المنحني السابق أساس مقال شهير ظهر في مجلة عام 1986، فقد نُشر في مجلة نيوزويك Newsweek أن احتمال موت امرأة غير متزوجة عمرها 40 على يد قاتل أكبر من احتمال زواجها، وانتشرت هذه الإحصائيات انتشارًا واسعًا وأصبحت جزءًا من الثقافة الشعبية، لكنها كانت خاطئةً لأنها بُنيَت على تحليل خاطئ، واتضح أنهم على خطأ بسبب التغيرات الثقافية التي كانت جاريةً حينها واستمرت بعدها، لذا فقد نشرت مجلة نيوزويك Newsweek مقالًا آخرًا اعترفوا فيه بأنهم كانوا مخطئين، ونرى أنه من الأفضل قراءة المزيد عن هذا المقال والإحصائية المبنية عليه وردود الفعل لأنه سيذكِّرك بالالتزام الأخلاقي الذي يحتم عليك إجراء التحليل بعناية وتفسير النتائج بحيادية وعرضها على الجمهور بدقة وصدق. فواصل الثقة ينتج عن تحليل كابلان-ماير تقديرًا واحدًا لمنحني البقاء ولكنه مهم لحساب عدم اليقين الناتج عن التقدير، كما توجد ثلاثة مصادر محتملة للخطأ على أساس العادة وهي خطأ القياس measurement error وخطأ أخذ العينات sampling error وخطأ النمذجة modeling error، حيث يُعَدّ خطأ القياس في هذا المثال صغيرًا غالبًا، أي يعلم الأشخاص التاريخ الصحيح لولادتهم وإن كانوا قد تزوجوا بالإضافة إلى تاريخ الزواج، ويفترض أنهم قدَّموا هذه المعلومات بدقة، وإليك الشيفرة التي تحسب خطأ أخذ العينات عن طريق تطبيق إعادة أخذ العينات: def ResampleSurvival(resp, iters=101): low, high = resp.agemarry.min(), resp.agemarry.max() ts = np.arange(low, high, 1/12.0) ss_seq = [] for i in range(iters): sample = thinkstats2.ResampleRowsWeighted(resp) hf, sf = EstimateSurvival(sample) ss_seq.append(sf.Probs(ts)) low, high = thinkstats2.PercentileRows(ss_seq, [5, 95]) thinkplot.FillBetween(ts, low, high) تأخذ الدالة ResampleSurvival الوسيطَين resp وهو إطار بيانات المستجيبين وiters وهو عدد المرات التي يجب فيها إعادة أخذ العينات، ومن ثم تحسب ts وهو تسلسل الأعمار وهنا سنقيِّم منحني البقاء، كما تقوم الدالة ResampleSurvival بالخطوات التالية ضمن الحلقة: تعيد أخذ عينات المستجيبين باستخدام ResampleRowsWeighted، ورأينا هذا في قسم إعادة أخذ العينات مع الأوزان في مقال المربعات الصغرى الخطية في بايثون. تستدعي EstimateSurvival التي تستخدِم العملية الموجودة في الأقسام السابقة بهدف تقدير منحني البقاء ومنحني الخطر. ثم تقيِّم منحني البقاء في كل عمر في ts. يُعَدّ ss_seq تسلسل منحنيات البقاء المقدَّرة، كما تأخذ الدالة PercentileRows هذا التسلسل وتحسب المئين الخامس والمئين الخامس والتسعين وتعيد فاصل الثقة 90‎%‎ الخاص بمنحني البقاء. يوضِّح الشكل السابق منحني البقاء للعمر عند أول زواج الممثَّل بالخط الداكن وفاصل الثقة ‎90% المبني على إعادة أخذ العينات مع الأوزان والممثَّل بالخط الرمادي، كما يظهر الشكل السابق النتيجة ومنحني البقاء الذي قدَّرناه في القسم السابق، ويأخذ فاصل الثقة أوزان أخذ العينات بالحسبان على عكس المنحني المقدَّر الذي لا يضع الأوزان في حسبانه، علمًا أنَّ التناقض بينهما يشير إلى التأثير الكبير لأوزان أخذ العينات على التقدير وعلينا أخذ ذلك في الحسبان. تأثيرات الفوج يُعَدّ اعتماد الأجزاء المختلفة للمنحني المقدَّر على عدة مجموعات بأنه أحد تحديات تحليل البقاء، حيث أنّ جزء المنحني عند الوقت t مبني على المستجيبات اللاتي كان عمرهن t على الأقل أثناء المقابلة، لذا يحتوي الجزء الموجود في اليسار على بيانات جميع من شارك في المسح، في حين يحتوي الجزء الموجود في اليمين على المستجيبات الأكبر سنًا. إذا لم تكن صفات المستجيبات ذات الصلة متغيرةً بمرور الوقت، فلا توجد مشكلة بالطبع، لكن يبدو في هذه الحالة أنّ أنماط الزواج متغيرة بالنسبة للنساء المولودات في أجيال مختلفة، كما يمكننا البحث في هذا التأثير عن طريق تصنيف المستجيبات إلى مجموعات بحسب عقد الميلاد، علمًا أنّ المجموعات المشابهة لهذه أي المعرفة بتاريخ ميلاد أو حدث مشابه تدعى الأفواج cohorts، في حين تدعى الفروق بين المجموعات تأثيرات الفوج cohort effects. جمعنا بيانات الدورة السادسة من 2002 المستخدَمة في هذه السلسلة وبيانات الدورة السابعة من 2006-2010 المستخدَمة في قسم التكرار في مقال اختبار الفرضيات الإحصائية وبيانات الدورة الخامسة من 1995، حيث تحوي مجموعة البيانات كلها 30769 مستجيبةً، وذلك من أجل البحث في تأثيرات الفوج في بيانات الزواج في المسح الوطني لنمو الأسرة. resp5 = ReadFemResp1995() resp6 = ReadFemResp2002() resp7 = ReadFemResp2010() resps = [resp5, resp6, resp7] استخدمنا cmbirth من أجل كل إطار بيانات resp لحساب عقد ولادة كل مستجيبة: month0 = pandas.to_datetime('1899-12-15') dates = [month0 + pandas.DateOffset(months=cm) for cm in resp.cmbirth] resp['decade'] = (pandas.DatetimeIndex(dates).year - 1900) // 10 حيث أن المتغير cmbirth مرمَّز ليدل على العدد الصحيح للأشهر التي مضت منذ شهر 12 من عام 1899، فتمثِّل month0 هذا التاريخ على أساس كائن ختم زمني Timestamp، كما نستنسخ DateOffset من أجل كل تاريخ ميلاد والذي يحتوي على أشهر القرن ونضيفه إلى month0 لتكون النتيجة تسلسلًا من الأختام الزمنية TimeStamps التي يجري تحوَّل إلى النوع DateTimeIndex، ونستخرج أخيرًا year الذي يمثِّل السنة ونحسب decades الذي يمثِّل العقد. أعدنا أخذ العينات وصّنفنا المستجيبات حسب عقد الميلاد ورسمنا منحني البقاء وذلك لكي نحرص على أخذ أوزان أخذ العينات بالحسبان وإظهار التباين الناتج عن خطأ أخذ العينات أيضًا: for i in range(iters): samples = [thinkstats2.ResampleRowsWeighted(resp) for resp in resps] sample = pandas.concat(samples, ignore_index=True) groups = sample.groupby('decade') EstimateSurvivalByDecade(groups, alpha=0.2) تستخدِم بيانات الدورات الثلاثة للمسح الوطني لنمو الأسرة أوزانًا مختلفةً، لذا أعدنا أخذ عينات كل منها على حدة ثم استخدمنا concat لدمجها لتصبح إطار بيانات واحد، علمًا أنّ المعامِل ignore_index يشير إلى concat لكي لا يطابق المستجيبين حسب الفهرس وإنما ينشئ فهرسًا جديدًا من 0 إلى 30768، كما ترسم الدالة EstimateSurvivalByDecade منحنيات بقاء كل فوج. def EstimateSurvivalByDecade(resp): for name, group in groups: hf, sf = EstimateSurvival(group) thinkplot.Plot(sf) يوضِّح الشكل السابق منحنيات بقاء المستجيبات اللواتي ولدن ضمن عقود مختلفة، كما يظهر الشكل السابق النتائج ونرى عدة أنماط فيه وهي: النساء اللواتي ولدن في الخمسينيات هم أكثر من تزوج في عمر صغير، وكذلك فإن أفراد الفوج التالي تزوجن في عمر متأخر أكثر، والفوج التالي بعد الفوج السابق وهكذا، وبقوا على الأقل حتى عمر الثلاثين تقريبًا. تملك النساء اللواتي ولدن في ستينيات القرن الماضي نمطًا غريبًا، إذ تزوجت النساء هنا في عمر 25 بمعدل أبطأ من الفوج السابق، ولكن بعد عمر 25 أصبح معدل الزواج أسرع، لكن النساء في هذا الفوج تجاوزت فوج الخمسينيات عند عمر 32، واتضح أنَّ احتمال زواج النساء في عمر 44 هو المرجَّح، لكن النساء اللواتي ولدن في الستينيات بلغن عمر 25 بين عامَي 1985 و1995، ومن المغري اعتقاد أنّ المقال الذي ذكرناه منذ قليل قد تسبب في ازدياد حالات الزواج، لكنه تفسير رديء للغاية، ومع ذلك فإنه من المحتمل أن يكون المقال وردّ الفعل عليه مؤشرَين على حالة مزاجية أثرت على سلوك هذا الفوج. يملك فوج السبعينات نمطًا مشابهًا، حيث أن النساء هنا أقل احتمالًا لأن يتزوجن قبل عمر 25 إذا وازناه مع الأفواج السابقة، لكن هذا لهذا الفوج احتمالات مشابهة للأفواج السابقة فيما يخص الزواج عند عمر 35. احتمال أن زواج فوج الثمانينيات قبل 25 هو أقل من الفوج السابق، ولكن ما يحدث بعد ذلك هو غير واضح، وإذا أردنا بيانات أكثر، فعلينا الانتظار حتى الدورة التالية من المسح الوطني لنمو الأسرة. يمكننا توليد بعض التنبؤات ريثما تصلنا البيانات. الاستقراء الخارجي Extrapolation ينتهي منحني البقاء لفوج السبعينات عند عمر 38 تقريبًا؛ أما فوج الثمانينات فينتهي منحني البقاء الخاص به عند سن 28، وبالطبع فإن بيانات فوج التسعينات نادرة جدًا، كما يمكننا استقراء هذه المنحنيات خارجيًا عن طريق استعارة بيانات من الفوج السابق، حيث يزودنا الصنف HazardFunction بالتابع Extend الذي ينسخ الذيل من HazardFunction أطول كما يلي: # class HazardFunction def Extend(self, other): last = self.series.index[-1] more = other.series[other.series.index > last] self.series = pandas.concat([self.series, more]) يحتوي HazardFunction على سلسلة تحوِّل الوقت t إلى λ(t)، ويجد التابع Extend المتغير last وهو الفهرس الأخير في self.series،ثم يختار قيم من other التي تأتي بعد last ويضيفها إلى نهاية self.series، ويمكننا الآن توسيع HazardFunction الخاص بكل فوج وذلك بالاستعانة بقيم من الفوج السابق: def PlotPredictionsByDecade(groups): hfs = [] for name, group in groups: hf, sf = EstimateSurvival(group) hfs.append(hf) thinkplot.PrePlot(len(hfs)) for i, hf in enumerate(hfs): if i > 0: hf.Extend(hfs[i-1]) sf = hf.MakeSurvival() thinkplot.Plot(sf) حيث أن groups هو كائن GroupBy فيه معلومات المستجيبات مصنفة إلى مجموعات حسب عقد الولادة، كما تحسب الحلقة الأولى HazardFunction كل مجموعة، في حين توسِّع الحلقة الثانية كل HazardFunction بقيم من الفوج السابق له والذي قد يحتوي على قيم من المجموعة التي تسبقه أيضًا (أي قد يحتوي فوج الخمسينات على قيم من فوج الأربعينات وقد يحتوي فوج الأربعينات على قيم من فوج الثلاثينات وهكذا)، ثم تحوِّل كل HazardFunction إلى SurvivalFunction وترسمه. يوضِّح الشكل السابق منحنيات البقاء الخاصة بالمستجيبات اللواتي ولدن خلال عقود مختلفة، مع تنبؤات للأفواج اللاحقة، كما يُظهر الشكل السابق النتائج، وقد حذفنا فوج الخمسينات لجعل التنبؤات أكثر وضوحًا، وتقترح هذه النتائج أنه بحلول السن الأربعين ستتقارب الأفواج الأحدث مع فوج الستينيات وستمثل المستجيبات المتزوجات نسبةً تقل عن %20 من المجموع الكلي. العمر المتبقي المتوقع إذا كان لدينا منحني بقاء، فيمكننا حساب العمر المتبقي المتوقع على أساس دالة للعمر الحالي، أي إذا كان لدينا مثلًا منحني البقاء لطول الحمل من القسم الأول من هذا المقال، فيمكننا حساب الوقت المتوقع حتى حدوث المخاض والولادة، حيث تتمثل الخطوة الأولى في استخراج دالة الكثافة الاحتمالية PMF للأعمار، كما يزودنا الصنف SurvivalFunction بالتابع الذي يقوم بالمطلوب: # class SurvivalFunction def MakePmf(self, filler=None): pmf = thinkstats2.Pmf() for val, prob in self.cdf.Items(): pmf.Set(val, prob) cutoff = self.cdf.ps[-1] if filler is not None: pmf[filler] = 1-cutoff return pmf تذكر أنّ SurvivalFunction تحتوي على Cdf للعمر، وتنسخ الحلقة القيم والاحتمالات من Cdf إلى Pmf، كما تُعَدّ cutoff هي أعلى احتمال في Cdf، وهي 1 إذا كان Cdf كاملًا وأقل من 1 بخلاف ذلك، وإذا كان Cdf غير كامل، فسنُدخل القيمة المزوَّدة إلى filter لنكملها، لكن يُعَدّ Cdf لمدة الحمل كاملًا، لذا لا داع للقلق حول هذا الأمر؛ أما الخطوة التالية هنا فهي حساب العمر المتبقي المتوقع، حيث يعني المتوقع هنا المتوسط الحسابي، كما يزودنا SurvivalFunction بدالة تقوم بهذا أيضًا: # class SurvivalFunction def RemainingLifetime(self, filler=None, func=thinkstats2.Pmf.Mean): pmf = self.MakePmf(filler=filler) d = {} for t in sorted(pmf.Values())[:-1]: pmf[t] = 0 pmf.Normalize() d[t] = func(pmf) - t return pandas.Series(d) تأخذ RemainingLifetime الوسيط filterالذي يمرَّر إلى MakePmf وfunc وهو الدالة المستخدَمة لتلخيص توزيع العمر المتبقي؛ أما pmf فهي Pmf الأعمار المتبقية المستخرَجة من SurvivalFunction، وd هو قاموس يحتوي على النتائج وهو تحويل من العمر الحالي t إلى العمر المتبقي المتوقع. تمر الحلقة مرورًا تكراريًا على القيم في Pmf، وهي تحسب التوزيع الشرطي للأعمار المتبقية لكل قيمة من t، باعتبار أنّ العمر المتبقي يتجاوز t، فإنها تنجز هذه المهمة عن طريق إزالة القيم من Pmf الواحدة تلو الأخرى ومن ثم إعادة توحيد renoramlizing القيم المتبقية، كما تستخدِم بعدها func لتلخيص التوزيع الشرطي وفي هذا المثال النتيجة هي مدة الحمل المتوسطة باعتبار أنّ المدة تتجاوز t، كما نحصل على متوسط مدة الحمل المتبقية عن طريق طرح t. يوضِّح الشكل السابق مدة الحمل المتوقعة المتبقية في الجهة اليسرى؛ أما في الجهة اليمنى فيوضِّح السنوات حتى أول زواج، كما يُظهر الشكل السابق في الجهة اليسرى طول الحمل المتبقي المتوقع على أساس دالة للمدة الحالية، أي المدة المتبقية المتوقعة في الأسبوع 0 مثلًا هي حوالي 34 أسبوع، وهي أقل من طول الحمل الكامل -أي 39 أسبوع- لأن حالات الإجهاض التي حصلت في الثلث الأول قد خفضت من المتوسط. ينخفض المنحني ببطء في الثلث الأول، وتكون المدة المتبقية المتوقعة بعد 13 أسبوع قد انخفضت 9 أسابيع لتصبح 25 أسبوع، بعد ذلك ينخفض بسرعة أكبر، وذلك بمعدل انخفاض أسبوع كامل في كل أسبوع جديد، في حين ينخفض المنحني حوالي أسبوع أو أسبوعين في الفترة ما بين الأسبوع 37 والأسبوع 42، وتكون المدة المتبقية المتوقعة في هذه الفترة ثابتةً، أي لا تصبح الوجهة أقرب مع مرور الأسابيع، وتدعى العمليات التي تحمل هذه الخاصية بعمليات بلا ذاكرة memoryless لأن ليس للماضي تأثير على التنبؤات، علمًا أنّ هذا السلوك هو الأساس الرياضي لجملة الممرضات الشهيرة التي تثير الغضب: اقترب موعد الولادة ومتوقع أن تلدي في أيّ يوم الآن. يُظهر الشكل السابق في الجهة اليمنى الوقت الوسيط المتبقي حتى أول زواج على أساس دالة للعمر، حيث يكون الوسيط هو 14 عامًا بالنسبة لفتاة عمرها 11 عامًا، ويقل المنحني حتى عمر 22 حينما يصبح الوقت المتبقي الوسيط هو حوالي 7 سنوات، ويزداد مرةً أخرى بعدها وبحلول العمر 30 يعود إلى ما كان عليه أي 14 عامًا، ويمكننا استنادًا إلى هذه البيانات استنتاج أنّ للنساء صغيرات السن أعمارًا متبقيةً متناقصةً، وتدعى المكونات الميكانيكية المرتبطة بهذه الخاصية NBUE وهي اختصار لمِن المتوقع أن يكون الجديد أفضل من المستخدَم new better than used in expectation، أي من المتوقع بقاء الجزء الجديد فترةً أطول. تملك النساء اللواتي تجاوزت أعمارهن 22 سنة وقتًا متبقيًا متزايدًا حتى أول زواج، وتدعى المكونات الميكانيكية المرتبطة بهذه الخاصية UBNE وهي اختصار لمِن المتوقع أن يكون المستخدَم أفضل من الجديد used better than new in expectation، أي من المتوقع أن يبقى الجزء المستخدَم فترةً أطول، إذ يُعَدّ الأطفال حديثو الولادة مثلًا ومرضى السرطان هم UBNE أيضًا لأن العمر المتوقع لديهم يزيد كلما طالت مدة حياتهم، فقد حسبنا الوسيط median في هذا المثال بدلًا من المتوسط mean لأن Cdf غير كامل، ويتوقع منحني البقاء أن نسبة ‎20% من المستجيبات لن يتزوجن قبل سن 44، وبما أن سن الزواج الأول لهؤلاء النساء غير معلوم وقد يكون غير موجود، فلن نتمكن من حساب المتوسط. استبدلنا القيم غير المعلومة هنا بالقيمة np.inf وهي قيمة خاصة تمثِّل اللانهاية، أي أنها تجعل متوسط اللانهاية لكل الاعمار، لكن يبقى الوسيط محدَّدًا تمامًا طالما أنّ أكثر من 50‎%‎ من الأعمار المتبقية نهائية، وهذا صحيح حتى عمر الثلاثين؛ أما بعدها فمن الصعب تحديد عمر متبقي متوقع له، وإليك الشيفرة التي تحسب وترسم هذه الدوال: rem_life1 = sf1.RemainingLifetime() thinkplot.Plot(rem_life1) func = lambda pmf: pmf.Percentile(50) rem_life2 = sf2.RemainingLifetime(filler=np.inf, func=func) thinkplot.Plot(rem_life2) حيث أن sf1 هو منحني البقاء لطول الحمل، ويمكن في هذه الحالة استخدام القيم الافتراضية للدالة RemainingLifetime؛ أما sf2 فهو منحني البقاء للعمر عند أول زواج، وfunc هو دالة تأخذ Pmf وتحسب وسيطها -أي المئين رقم 50-. تمارين يوجد الحل الخاص بهذا التمرين في chap13soln.py في مستودع الشيفرات ThinkStats2 على GitHub (وسائر ملفات التمارين). تمرين 1 يحتوي المتغير cmdivorcx في الدورتين السادسة والسابعة من المسح الوطني لنمو الأسرة على تاريخ طلاق المستجيبين من أول حالة زواج، وهي مرمَّزة بطريقة أشهر القرن. احسب مدة حالات الزواج التي انتهت بطلاق ومدة حالات الزواج المستمرة حتى الآن، وقدِّر منحني الخطر ومنحني البقاء لمدة الزواج، ثم استخدِم طريقة إعادة أخذ العينات resampling لمراعاة أوزان أخذ العينات، ثم وضِّح خطأ أخذ العينات بصريًا عن طريق رسم البيانات الناتجة عن عدة مرات أخذ عينات، علمًا أنه من الأفضل أن تفكِّر في تقسيم المستجيبين إلى مجموعات حسب عقد الولادة وربما حسب العمر عند أول حالة زواج. ترجمة وبتصرف للمقال Chapter 13 Survival analysis analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا الانحدار الإحصائي regression ودوره في ملاءمة النماذج المختلفة مع أنواع البيانات المتاحة نمذجة التوزيعات Modelling distributions في بايثون تحليل البيانات الاستكشافية لإثبات النظريات الإحصائية div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  4. تُعَدّ ملاءمة المربعات الصغرى الخطية التي ذكرناها في المقال السابق مثالًا عن الانحدار regression، وهو المشكلة الأكثر عمومية لمسألة ملاءمة النماذج المختلفة مع أي نوع من البيانات، علمًا أنّ استخدام مصطلح الانحدار regression هو مصادفة تاريخية فهو مرتبط بالمعنى الأصلي للكلمة الأجنبية ارتباطًا غير مباشر ليس إلا، ويتمثَّل هدف تحليل الانحدار في وصف العلاقة بين مجموعة واحدة من المتغيرات التي تُدعى بالمتغيرات التابعة dependent variables ومجموعة أخرى من البيانات والتي تُدعى بالمتغيرات التوضيحية explanatory variables أو المتغيرات المستقلة independent. استخدمنا في المقال السابق عمر الأم على أساس متغير توضيحي للتنبؤ بوزن الطفل على أساس متغير تابع، علمًا أنّ الانحدار البسيط simple regression هو الحالة التي توجد فيها متغير تابع واحد فقط ومتغير توضيحي واحد فقط، وسنتناول في هذا المقال الانحدار المتعدد multiple regression الذي يحوي أكثر من متغير توضيحي، لكن إذا كان هناك أكثر من متغير تابع واحد، فسيكون الانحدار من نوع الانحدار متعدد المتغيرات multivariate regression، وإذا كانت العلاقة بين المتغير التوضيحي والمتغير التابع خطيةً، فسيكون الانحدار من نوع الانحدار الخطي linear regression، فإذا كان مثلًا المتغير التابع y والمتغيران التوضيحيان هما x1‎ وx2‎، فيمكننا صياغة نموذج الانحدار الخطي كما يلي: y = β0 + β1 x1 + β2 x2 + ε حيث يكون β0‎ هو نقطة التقاطع وβ1‎ هو المُعامِل المرتبط بالمتغير x1‎ ويكون β2‎ هو الوسيط المرتبط بالمتغير x2‎، في حين يكون ε هو الراسب residual (أو الباقي) الناتج عن إما تباين عشوائي أو عوامل أخرى غير معروفة، فإذا كان لدينا متسلسلةً من قيم متسلسلةً من قيم y ومتسلسلتَين من `x1 و x2‎، فسيكننا إيجاد المُعامِلات β0‎ وβ1‎ وβ2‎ التي تقلل من مجموع ε2‎، وتُدعى هذه العملية بالمربعات الصغرى العادية ordinary least squares. يُعَدّ هذا الحساب مشابهًا لـ thinkstats2.LeastSquare لكنه معمَّم للتعامل مع أكثر من متغير توضيحي واحد، وللمزيد من التفاصيل يمكنك زيارة صفحة ويكيبيديا، كما توجد الشيفرة الخاصة بهذا المقال في regression.py. الحزمة StatsModels تناولنا في المقال السابق التابع thinkstats2.LeastSquares، الذي يُعَدّ تنفيذًا للانحدار الخطي البسيط وهو مصمم ليكون سهل القراءة؛ أما بالنسبة للانحدار المتعدد multiple regression، فسننتقل للتعامل مع الحزمة StatsModels وهي حزمة بايثون تزودنا بعدة أشكال من الانحدار بالإضافة إلى عدة تحليلات أخرى، علمًا أنه ستكون هذه الحزمة مثبتةً لديك في حال كنت تستخدم أناكوندا Anaconda، وإلا فقد تضطر إلى تثبيتها، حيث سننفِّذ النموذج الذي تناولناه في الفصل السابق على أساس مثال على ذلك لكن باستخدام الحزمة StatsModels: import statsmodels.formula.api as smf live, firsts, others = first.MakeFrames() formula = 'totalwgt_lb ~ agepreg' model = smf.ols(formula, data=live) results = model.fit() توفِّر الحزمة statsmodels واجهتَين من نوع واجهات برمجة التطبيقات APIs، حيث تستخدِم المعادلة formula الخاصة بواجهة برمجة التطبيقات السلاسل strings لتحديد المتغيرات التابعة والمتغيرات التوضيحية، كما تستخدِم صيغة قواعدية syntax تُدعى patsy، تكون مهمة العامِل ~ في هذا المثال فصل المتغير التابع عن المتغيرات التوضيحية بحيث يضع المتغير التابع في الجهة اليسرى والمتغيرات التوضيحية في الجهة اليمنى. يأخذ التابع smf.ols السلسلة formula وإطار البيانات live ويُعيد كائن OLS الذي يمثِّل النموذج علمًا أنّ تسمية ols اختصار لمصطلح المربعات الصغرى العادية ordinary least squares؛ أما التابع fit فهو يلائم النموذج مع البيانات ويُعيد الكائن RegressionResults الذي يحتوي على النتائج، علمًا أنّ النتائج متوافرة على صورة سمات attributes، وتكون params هي سلسلة Series تحوِّل أسماء المتغيرات إلى معامِلاتها لكي نحصل على الميل ونقطة التقاطع كما في الشيفرة التالية: inter = results.params['Intercept'] slope = results.params['agepreg'] المعامِلان المقدَّران هما 6.83 و0.0175 أي تمامًا مثل LeastSquares. تُعَدّ pvalues سلسلةً Series تحول أسماء المتغيرات إلى القيمة الاحتمالية المرتبطة بها لكي نتحقق فيما إن كان الميل المقدَّر ذا دلالة إحصائية: slope_pvalue = results.pvalues['agepreg'] القيمة الاحتمالية p-value المرتبطة بالمتغير agepreg هي 5.7e-11 أي أقل من 0.001 كما هو متوقع تمامًا. يحتوي results.rsquared على R2‎ التي تبلغ قيمتها 0.0047، ويزودنا results بـ f_pvalue وهي القيمة الاحتمالية المرتبطة بالنموذج بأكمله بصورة مشابهة لاختبار فيما إن كان R2 ذي دلالة إحصائية، كما يزودنا results بـ resid وهو متسلسلة من الرواسب، وبـ fittedvalues وهو متسلسلة من القيم الملاءمة المقابلة لـ agepreg، كما يزودنا الكائن results بالدالة summary() التي تمثُِل النتائج بصيغة مقروءة. print(results.summary()) لكن تطبع هذه الدالة الكثير من المعلومات التي لا تهمنا حاليًا، لذا سنستخدِم دالةً أبسط تُدعى SummarizeResults، إليك نتائج هذا النموذج كما يلي: Intercept 6.83 (0) agepreg 0.0175 (5.72e-11) R^2 0.004738 Std(ys) 1.408 Std(res) 1.405 يُعَدّ Std(ys) الانحراف المعياري للمتغير التابع، وهو جذر متوسط مربع الخطأ RMSE نفسه إذا خمّنت أوزان الولادات بدون متغيرات توضيحية؛ أما Std(res) فهو الانحراف المعياري للرواسب وهو خطأ الجذر التربيعي المتوسط RMSE إذا كانت تخميناتك مبينةً على عمر الأم، حيث أنّ عمر الأم -كما رأينا سابقًا- لا يقدِّم أيّ تحسين جوهري إلى التنبؤات. الانحدار المتعدد رأينا في مقال دوال التوزيع التراكمي Cumulative distribution functions ميل الأطفال الأوائل ليكونوا أقل وزنًا من بقية الأطفال، ويُعَدّ هذا التأثير ذا دلالة إحصائية لكنه نتيجةً غريبةً بسبب عدم وجود آلية واضحة تتسبب في جعل الأطفال الأوائل أقل وزنًا من غيرهم، لذا قد نتساءل فيما إذا كانت هذه العلاقة زائفةً spurious. يوجد تفسير محتمل لهذا التأثير في الواقع، فقد رأينا اعتماد وزن الطفل عند الولادة على عمر الأم، لذا قد نتوقع أنّ أمهات الأطفال الأوائل أصغر عمرًا من غيرهن، ويمكننا التحقق مما إن كان هذا التفسير معقولًا من خلال بعض العمليات الحسابية ثم سنستخدِم الانحدار المتعدد لإجراء تحقيق أكثر دقة، لذا سنرى في البداية حجم الفرق في الوزن ما يلي: diff_weight = firsts.totalwgt_lb.mean() - others.totalwgt_lb.mean() عادةً ما يكون الأطفال الأوائل أخف وزنًا من غيرهم بمقدار 0.125 رطلًا أو 2 أوقية أو ما يعادل 0.1 كيلوغرامًا؛ أما الفرق في الأعمار فهو: diff_weight = firsts.totalwgt_lb.mean() - others.totalwgt_lb.mean() أي أن أمهات الأطفال الأوائل أصغر من أمهات بقية الأطفال بـ 3.59 عامًا، حيث يمكننا الحصول على الفرق في وزن الطفل عند الولادة على أساس دالة العمر عن طريق تشغيل النموذج الخطي مرةً أخرى: results = smf.ols('totalwgt_lb ~ agepreg', data=live).fit() slope = results.params['agepreg'] يقدَّر الميل بـ 0.0175 رطلًا في العام الواحد، فإذا أجرينا عملية جداء بين الميل والفرق في الأعمار، فسنحصل على الفرق المتوقَّع في وزن الطفل عند الولادة للأطفال الأوائل وبقية الأطفال والناتج عن عمر الأم: slope * diff_age النتيجة هي 0.063 وتساوي نصف الفرق المرصود، لذا نستنتج مبدئيًا أنه يمكن تفسير الفرق الملحوظ في وزن الطفل عند الولادة بالاختلاف في عمر الأم، كما يمكننا استكشاف هذه العلاقات بطريقة منهجية باستخدام الانحدار المتعدد كما يلي: live['isfirst'] = live.birthord == 1 formula = 'totalwgt_lb ~ isfirst' results = smf.ols(formula, data=live).fit() ينشئ السطر الأول من الشيفرة السابقة عمودًا جديدًا باسم isfirst وقيمته البوليانية صحيحية True للأطفال الأوائل وخاطئة false ما عدا ذلك، ثم نستخدِم العمود isfirst على أساس متغير توضيحي لكي نلائم نموذجًا، وإليك النتائج كما يلي: Intercept 7.33 (0) isfirst[T.True] -0.125 (2.55e-05) R^2 0.00196 تعامِل ols العمود isfirst على أنه متغير فئوي categorical variable نظرًا لأنه من النوع البولياني boolean، أي أن القيم تندرج في فئتين هما True وFalse ولا ينبغي معاملتها على أساس أعداد، علمًا أن المعامِلات المقدَّرة estimated parameter هي التأثير على وزن الطفل عند الولادة في حال كانت قيمة isfirst هي true، لذا تكون النتيجة المقدَّرة بـ ‎-0.125‎‎ رطل هي الفرق في وزن الطفل عند الولادة بين الأطفال الأوائل وبقية الأطفال. يملك الميل slope ونقطة التقاطع intercept دلالةً إحصائيةً، أي أنه من غير المحتمل حدوث التأثير صدفةً، لكن قيمة R2‎ الخاصة بالنموذج صغيرةً، مما يعني أنّ isfirst لا يمثِّل جزءًا كبيرًا من التباين في وزن الطفل عند الولادة، كما تتشابه النتائج مع النتائج التي ظهرت مع agepreg كما يلي: Intercept 6.83 (0) agepreg 0.0175 (5.72e-11) R^2 0.004738 تملك المعامِلات -كما ذكرنا سابقًا- دلالةً إحصائيةً لكن قيمة R2‎ منخفضةً، كما تؤكِّد هذه النماذج النتائج التي رأيناها بالفعل، لكن يمكننا الآن ملاءمة نموذج واحد يتضمن كلا المتغيرين بحيث نحصل على ما يلي باستخدام المعادلة totalwgt_lb ~ isfirst + agepreg: Intercept 6.91 (0) isfirst[T.True] -0.0698 (0.0253) agepreg 0.0154 (3.93e-08) R^2 0.005289 إنّ قيمة المعامِل isfirst أصغر بحوالي النصف في النموذج المشترك، مما يعني أنه يتم حساب جزء من التأثير الظاهر للعمود isfirst بواسطة agepreg، كما أن القيمة الاحتمالية للعمود isfirst هي حوالي 2.5‎%‎ وهي على حدود الدلالة الإحصائية، لكن قيمة R2 في هذا النموذج أعلى بقليل، أي أنّ المتغيرين معًا يمثلان تباينًا أكبر من التباين الذي يمثله كل منهما بمفرده في وزن المواليد ولكن الفرق ليس بكبير. العلاقات اللاخطية قد تكون المساهمة التي قدمها agepreg لاخطيةً، لذا قد نفكر في إضافة متغير يصف العلاقة وصفًا أفضل، ويتمثل أحد الخيارات في إنشاء عمود آخر باسم agepreg2 يحتوي على مربعات الأعمار كما يلي: live['agepreg2'] = live.agepreg**2 formula = 'totalwgt_lb ~ isfirst + agepreg + agepreg2' يمكننا ملاءمة قطع مكافئ parabola ملائمةً فعالةً عن طريق تقدير المعامِلَين agepreg وagepreg2 كما يلي: Intercept 5.69 (1.38e-86) isfirst[T.True] -0.0504 (0.109) agepreg 0.112 (3.23e-07) agepreg2 -0.00185 (8.8e-06) R^2 0.007462 لدينا المعامِل agepreg2 سالبًا لذا ينحني القطع المكافئ إلى الأسفل، وهذا متوافق تمامًا مع شكل الخطوط في الشكل 10.2 في هذا الفصل، ويمثِّل النموذج التربيعي quadratic model للمعامِل agepreg2 تباينًا أكبر في وزن الطفل عند الولادة، كما أنّ isfirst أصغر في هذا النموذج ولم يعُد ذا دلالة إحصائية. يُعَدّ استخدام متغيرات محسوبة مثل agepreg2 طريقةً شائعةً لملاءمة كثيرات الحدود ودوال أخرى مع البيانات، ولا تزال هذه العملية مندرجةً تحت نوع الانحدار الخطي لأن المتغير التابع هو دالة خطية للمتغيرات التوضيحية بغض النظر عما إذا كانت بعض المتغيرات تمثِّل دوالًا لاخطيةً لغيرها، كما يلخِّص الجدول التالي نتائج تحليلات الانحدار هذه: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;} isfirst agepreg agepreg2 R2 النموذج الأول -0.125 * - - 0.002 النموذج الثاني - 0.0175 * - 0.0047 النموذج الثالث -0.0698 (0.025) 0.0154 * - 0.0053 النموذج الرابع -0.0504 (0.11) 0.112* -0.00185 * 0.0075 تمثِّل أعمدة هذا الجدول المتغيرات التوضيحية بالإضافة إلى مُعامِل التحديد R2، حيث تُعَدّ كل خانة من الجدول معامِلًا مقدَّرًا وإما قيمةً احتماليةً بين قوسين أو قيمةً احتماليةً بجانبها علامة نجمية * تشير إلى أن القيمة الاحتمالية أقل من 0.001. نستنتج مما سبق أنّ الاختلاف الواضح في وزن الطفل عند الولادة هو بسبب الفرق في عمر الأم بصورة جزئية على الأقل، إذ يصغر تأثير isfirst عندما نضيف عمر الأم إلى النموذج، وقد يكون التأثير المتبقي هو بسبب الصدفة فقط، ويكون عمر الأم في هذا المثال متغير تحكم control variable حيث أن إضافة المتغير agepreg إلى النموذج "يتحكم" بالفرق في العمر بين أمهات الأطفال الأوائل وأمهات بقية الأطفال مما يجعل عزل تأثير isfirst ممكنًا إذا وجد. التنقيب في البيانات لم نستخدِم حتى الآن سوى نماذج الانحدار لتفسير التأثيرات، فقد اكتشفنا في القسم السابق أن الفرق الواضح في وزن الطفل عند الولادة ناتج عن فرق في عمر الأم، لكن قيم R2 لهذه النماذج هي قيم منخفضة جدًا، مما يعني أن قوتها التنبؤية ضئيلة، لذا سنحاول إيجاد طريقة أفضل في هذا المقال. لنفترض أنّ أحد زملائك يتوقع ولادة طفله في الوقت القريب القادم وهناك لعبة تخمين في المكتب لتوقُّع وزن الطفل عند الولادة، فإذا افترضنا الآن أنه لديك رغبةً كبيرةً في ربح هذه اللعبة، ما الذي يمكنك أن تفعله لتحسين احتمال فوزك؟ تحتوي مجموعة بيانات المسح الوطني لنمو الأسرة على 244 متغير لكل حالة حمل وحوالي 3087 متغير لكل مستجيب، فقد يكون لبعض هذه المتغيرات قوةً تنبؤيةً، لذا لِمَ لا نجربها جميعًا لنعلم أي منها هو الأكثر فائدةً؟ تُعَدّ عملية اختبار المتغيرات في جدول الحَمل عمليةً سهلةً، لكن سيتوجب علينا مطابقة كل حالة حمل مع مستجيب إذا أردنا استخدام متغيرات جدول المستجيبين، ويمكننا نظريًا المرور على صفوف جدول الحمل ومن ثم استخدام المتغير caseid لإيجاد المستجيب الموافق لحالة الحمل ثم نسخ القيم من جدول المستجيبين إلى جدول حالات الحمل لكن ستتطلب هذه العملية وقتًا كبيرًا. يوجد خيار أفضل وهو عملية الضم join المُعرَّفة في لغة الاستعلامات المهيكلة SQL، كما يمكنك الاطلاع على مقال الدمج بين الجداول في SQL لمزيد من المعلومات حول لمزيد من المعلومات حول العملية، علمًا أنّ التنفيذ البرمجي لهذه العملية هنا هو على صورة إطار بيانات، لذا يمكننا إجراءها كما يلي: live = live[live.prglngth>30] resp = chap01soln.ReadFemResp() resp.index = resp.caseid join = live.join(resp, on='caseid', rsuffix='_r') يحدِّد السطر الأول سجلات حالات الحمل التي تزيد مدتها عن 30 أسبوعًا بافتراض أنّ لعبة التخمين في المكتب قد بدأت قبل عدة أسابيع من موعد الولادة، في حين يقرأ السطر التالي ملف المستجيبين، وتكون النتيجة هي إطار بيانات يحوي فهارس صحيحة integer، لكننا استبدلنا resp.caseid مكان resp.index من أجل البحث عن المستجيبين بكفاءة عالية. استُدعِي التابع join في live وهو يمثِّل الجدول الأيسر، ومرِّر respالذي يمثِّل الجدول الأيمن؛ أما الوسيط المحجوز on فيشير إلى المتغير المستخدَم لمطابقة الصفوف من الجدولين، وتظهر في هذا المثال بعض أسماء الجداول في الجدولين، لذا يجب توفير المتغير rsuffix الذي يُعَدّ سلسلةً نصيةً string والتي ستُضاف إلى نهاية أسماء الأعمدة المتكررة في الجدول الأيمن، فقد يحتوي مثلًا كلا الجدولين على عمود باسم race يرمز لعِرق المستجيب، وبالتالي ستحتوي نتيجة الضم على عمود باسم race وعمود باسم race_r. يُعَدّ التنفيذ البرمجي لبانداز pandas سريعًا، حيث لا يستغرق ضم جداول المسح الوطني لنمو الأسرة أكثر من ثانية واحدة وباستخدام حاسوب عادي، ويمكننا الآن البدء باختبار المتغيرات. t = [] for name in join.columns: try: if join[name].var() < 1e-7: continue formula = 'totalwgt_lb ~ agepreg + ' + name model = smf.ols(formula, data=join) if model.nobs < len(join)/2: continue results = model.fit() except (ValueError, TypeError): continue t.append((results.rsquared, name)) يمكننا إنشاء نموذج لكل متغير ثم حساب R2 وإضافة النتيجة إلى قائمة، كما تحتوي النماذج كلها على agepreg نظرًا لأننا نعلم مسبقًا أنها تمتلك بعض القوة التنبؤية، كما تحققنا أن لكل لمتغير توضيحي بعض التباين، وإلا لن يكون من الممكن الاعتماد على نتائج الانحدار، وتحققنا أيضًا من عدد المرات التي رُصد فيها كل نموذج، فلا يمكن أن تكون المتغيرات التي تحتوي على عدد كبير من قيم nans جيدةً للتنبؤ. لم يُطبَّق على معظم هذه المتغيرات أيّ عملية تنظيف للبيانات، حيث أن بعضها مرمَّز بطريقة غير مناسبة كثيرًا للانحدار الخطي، ونتيجة لهذا فقد نتجاهل بعض المتغيرات التي قد تكون مفيدةً إذا نُقّيَت تنقيةً صحيحةً، لكن ربما سنجد بعض المتغيرات التي يحتمل أن تكون مناسبة للتنبؤ. التنبؤ تتمثل الخطوة التالية في فرز النتائج وتحديد المتغيرات التي تنتج أعلى قيم R2: t.sort(reverse=True) for mse, name in t[:30]: print(name, mse) يُعَدّ totalwgt_lb أول متغير على القائمة ثم birthwgt_lb، لكن من الواضح أنه لا يمكننا استخدام وزن الطفل عند الولادة لتوقع الوزن نفسه، كما يملك المتغير prglngth قوةً تنبؤيةً مفيدةً، لكننا سنفترض أثناء تعاملنا مع لعبة التخمين في المكتب أنّ مدة الحمل وبقية المتغيرات ذات الصلة غير معروفة بعد. يُعَدّ babysex أول متغير تنبؤي مفيد والذي يشير إلى جنس الطفل فيما إذا هو ذكر أو أنثى، ويكون الصبيان في مجموعة بيانات المسح الوطني لنمو الأسرة أكبر وزنًا بحوالي 0.3 رطلًا -أي 0.13 كيلوغرامًا تقريبًا-، لذا سنتمكن من استخدام جنس المولود في التنبؤ إذا افترضنا أنه معروف. يشير المتغير race الذي إلى عِرق المستجيب فيما إذا هو أسود البشرة أو أبيض البشرة أو غير ذلك، وقد يكون العِرق إشكاليًا إن تعاملنا معه على أساس متغير توضيحي، لكن يرتبط المتغير race في بيانات المسح الوطني لنمو الأسرة بالعديد من المتغيرات الأخرى بما في ذلك الدخل والعوامل الاجتماعية والاقتصادية الأخرى، علمًا أنه في نموذج الانحدار كان العِرق متغيرًا وكيلًا proxy variable، لذلك غالبًا ما تكون الارتباطات الظاهرة مع العِرق ناتجةً عن عوامل أخرى بصورة جزئية على الأقل. يكون المتغير التالي في القائمة هو nbrnaliv والذي يشير إلى ما إذا كان الحمل قد أدى إلى ولادة متعددة -أي ولادة توأم من طفلين أو أكثر-، وعادةً ما يكون التوأم من طفلين أو ثلاث أطفال أقل وزنًا من غيرهم، لذا فقد يساعدنا معرفة فيما إذا كان زميلنا الافتراضي يتوقع ولادة توأم؛ أما المتغير paydu فيشير إلى ما إذا كان المستجيب يمتلك منزله أم لا، وهو أحد المتغيرات التي تتعلق بالدخل بالإضافة إلى عدة متغيرات أخرى والتي توضح أنها تنبؤية، ويكون الدخل والثروة في مجموعة بيانات مثل المسح الوطني لنمو الأسرة مرتبطَين بكل شي تقريبًا، حيث يرتبط الدخل في هذا المثال بالحمية الغذائية والصحة والعناية الصحية وعوامل أخرى من المرجح أن تؤثِّر على وزن الطفل عند الولادة. توجد متغيرات أخرى في القائمة لكنها أمور لن نعرفها إلا في وقت لاحق مثل bfeedwks والذي يمثِّل عدد الأسابيع التي رضع فيها الطفل رضاعةً طبيعيةً، وعلى الرغم أنه لا يمكننا استخدام هذه المتغيرات للتنبؤ إلا أنك قد ترغب في التكهن بالأسباب التي قد يرتبط فيها المتغير bfeedwks مع الوزن عند الولادة، وقد تلجأ في بعض الأحيان إلى البدء بنظرية ومن ثم اختبارها باستخدام بيانات معينة، وفي بعض الأحيان قد تبدأ ببيانات ومن ثم تبحث عن نظريات ممكنة. سنستعرِض في هذا القسم الطريقة الثانية والتي تدعى التنقيب في البيانات data mining، ومن مزايا التنقيب في البيانات هو اكتشاف أنماط غير متوقعة، لكن الخطر هو أنه قد تكون العديد من الأنماط المُكتشَفة عشوائيةً أو زائفةً، وقد اختبرنا بعد تحديد المتغيرات التوضيحية المحتمَلة بعض النماذج واستقرَّينا على هذا النموذج: formula = ('totalwgt_lb ~ agepreg + C(race) + babysex==1 + ' 'nbrnaliv>1 + paydu==1 + totincr') results = smf.ols(formula, data=join).fit() تستخدِم هذه المعادلة formula صياغة syntax لم نرها بعد مثل C(race) التي تطلب من محلل الصيغة باتسي Patsy معاملة العِرق على أساس متغير فئوي على الرغم أنه مرمَّز بصورة عددية، كما يكون ترميز المتغير babysex هو 1 للذكر و2 للأنثى، لكن إذا كتبنا babysex==1، فسيتحول المتغير إلى النوع البولياني ليصبح True للذكر وfalse للأنثى، وبالمثل يكون nbrnaliv> 1 هو True للولادات المتعددة ويكون paydu == 1 هو True للمستجيبين الذين يمتلكون منازلهم، علمًا أن المتغير totincr مرمز عدديًا من 1‎-14، حيث تمثِّل كل زيادة حوالي 5000 دولارًا أمريكيًا في الدخل السنوي، لذا يمكننا التعامل مع هذه القيم على أنها عددية ونعبِّر عنها بوحدات من 5000 دولارًا أمريكيًا، وإليك نتائج النموذج كما يلي: Intercept 6.63 (0) C(race)[T.2] 0.357 (5.43e-29) C(race)[T.3] 0.266 (2.33e-07) babysex == 1[T.True] 0.295 (5.39e-29) nbrnaliv > 1[T.True] -1.38 (5.1e-37) paydu == 1[T.True] 0.12 (0.000114) agepreg 0.00741 (0.0035) totincr 0.0122 (0.00188) من المفاجئ أن المعامِلات المقدَّرة للعِرق أكبر مما توقعنا خاصةً وأن المتغير التحكم هو الدخل، علمًا أن الترميز هو 1 لذوي البشرة السوداء و2 لذوي البشرة البيضاء و3 غير ذلك، إذًا يكون أطفال الأمهات ذوات البشرة السوداء أقل وزنًا من الأطفال الذي ينتمون إلى عروق أخرى بحوالي 0.27‎ - 0.36 رطلًا -أي بين 0.12 - 0.16 كيلوغرامًا-، وكما رأينا سابقًا فقد يميل الصبيان لأن يكونوا أكثر وزنًا بحوالي 0.3 رطلًا -أي 0.13 كيلوغرامًا تقريبًا-؛ أما التوائم سواءً اثنين أو أكثر فتكون أقل وزنًا بحوالي 1.4 رطلًا -أي 0.6 كيلوغرامًا تقريبًا-. الأشخاص الذين يمتلكون منازلهم يلدون أطفالًا أكثر وزنًا بحوالي 0.12 رطلًا -أي 0.05 كيلوغرامًا تقريبًا- حتى عندما يكون متغير التحكم هو الدخل، ويكون معامِل عمر الأم هنا أقل من الذي رأيناه في جزئية الانحدار المتعدد في هذا المقال، مما يشير إلى أن بعض المتغيرات الأخرى مرتبطةً بالعمر، بما في ذلك على الأرجح paydu وtotincr. نستنتج أنّ هذه المتغيرات ذو دلالة إحصائية ولبعضها قيمة احتمالية منخفضة جدًا، لكن قيمة R2 هي 0.06 فقط وهو مقدار صغير جدًا، كما أن قيمة جذر متوسط مربع الخطأ RMSE هي 1.27 رطل -أي حوالي 0.57 كيلوغرام- بدون استخدام النموذج؛ أما في حال استخدمنا النموذج فستنخفض القيمة إلى 1.23 رطل -أي حوالي 0.55 كيلوغرام-، أي لم تتحسن فرصتك في الفوز بلعبة التخمين تسحنًا كبيرًا. الانحدار اللوجستي وردت في أمثلتنا السابقة عدة متغيرات توضيحية، كان بعضها عدديًا وبعضها الآخر فئويًا بما فيها البولياني، لكن المتغير التابع كان دائمًا عدديًا، لذا يمكن تعميم الانحدار الخطي ليكون قادرًا على التعامل مع أنواع أخرى من المتغيرات التابعة، فإذا كان المتغير التابع بوليانيًا، فسيُدعى النموذج المعمم بالانحدار اللوجستي logistic regression؛ أما إذا كان المتغير التابع عددًا صحيحًا ويمثِّل تعدادًا، فسيُدعى بانحدار بواسون Poisson regression. دعنا نضع تباينًا على سيناريو لعبة التخمين في المكتب على أساس مثال عن الانحدار اللوجستي، حيث سنفترض أنّ صديقتك حامل وتريد توقُّع فيما إذا كان المولود صبيًا أو بنتًا، إذ يمكنك عندها استخدام البيانات الخاصة بالمسح الوطني لنمو الأسرة لإيجاد العوامل المؤثرة على نسبة الجنس التي عُرِّفت عادةً على أنها احتمال كون المولود صبيًا، فإذا كان المتغير التابع من النوع العددي، بحيث يكون 0 إذا كان المولود بنتًا و1 إذا كان صبيًا على سبيل المثال، ويمكنك عندها تطبيق طريقة المربعات الصغرى العادية لكن سنواجه عندها بعض المشاكل، وقد يكون النموذج الخطي مثل هذا: y = β0 + β1 x1 + β2 x2 + ε حيث يكون y هو المتغير التابع وتكون x1 وx2 هي متغيرات توضيحية، أي يمكننا الآن إيجاد المعامِلات التي تقلل من الرواسب residuals، لكن مشكلة هذه الطريقة أنها تنتج تنبؤات يصعب تفسيرها. نظرًا للمعامِلات المقدَّرة وقيم كل من x1 وx2، فقد يتنبأ النموذج أنّ y=0.5، لكن القيم الوحيدة ذات المعنى للمتغير y هي 0 و1، وقد يكون من المغري تفسير نتيجة مثل هذه على أنها احتمال، فقد نقول مثلًا أنّ المستجيب صاحب القيم المعينة للمتغيرَين x1 وx2 لديه فرصة 50‎%‎ في إنجاب صبي، لكن من الممكن أن يتنبأ هذا النموذج بـy=1.1 أو y= -0.1، إلا أنّ هذه الاحتمالات غير صالحة. يتجنب الانحدار اللوجستي هذه المشكلة عن طريق التعبير عن التنبؤات على صورة أرجحية odds بدلًا من الاحتمالات، وإذا لم يكن لديك فكرةً مسبقةً عن الأرجحية، فسنقول لك أنّ "الأرجحية لصالح" odds in favor حدث ما هي نسبة احتمال حدوثه إلى احتمال عدم حدوثه، فإذا اعتقدنا أنّ للفريق فرصة فوز بنسبة 75‎%‎، فسنقول أنّ الأرجحية لصالحهم هي 3 على 1 لأن فرصة الفوز هي ثلاثة أضعاف فرصة الخسارة، ويمثِّل كل من الأرجحية والاحتمال المعلومات نفسها لكن بطرق مختلفة، كما يمكننا حساب يمكننا حساب الأرجحية إذا كان لدينا احتمال ما باستخدام هذه المعادلة: o = p / (1-p) أما إذا كان لدينا قيمة الأرجحية لصالح، فيمكننا تحويلها إلى احتمال باستخدام هذه المعادلة: p = o / (o+1) يعتمد الانحدار اللوجستي على النموذج التالي: logo = β0 + β1 x1 + β2 x2 + ε يمثِّل o الأرجحية لصالح نتيجة ما، حيث يكون o هو الأرجحية لصالح ولادة صبي مثلًا، ولنفترض أنه لدينا المعامِلات المقدَّرةβ0 وβ1وβ2 والتي سنشرحها بعد قليل، ولنفترض أنه لدينا قيم x1 وx2، حيث يمكننا عندها حساب القيمة المتوقعة لـ logo ثم تحويلها إلى احتمال كما يلي: o = np.exp(log_o) p = o / (o+1) لذا يمكننا في سيناريو لعبة التخمين في المكتب حساب الاحتمال التنبؤي لولادة صبي، لكن كيف يمكننا تقدير المعامِلات؟ تقدير المعاملات لا يحتوي الانحدار الخطي على حل منغلق الشكل على عكس الانحدار الخطي، لذا يمكن حله عن طريق تخمين حل أولي ومن ثم تحسينه في كل تكرار، حيث أنّ الهدف المعتاد هو إيجاد تقدير الاحتمال الأعظم maximum-likelihood estimate -أو MLE اختصارًا- وهو مجموعة من المعامِلات التي تزيد من احتمالية البيانات، ولنفترض مثلًا أنه لدينا البيانات التالية: >>> y = np.array([0, 1, 0, 1]) >>> x1 = np.array([0, 0, 0, 1]) >>> x2 = np.array([0, 1, 1, 1]) نبدأ بالتخمينات الأولية وهي β0= -1.5 و β1=2.8و β2=1.1: >>> beta = [-1.5, 2.8, 1.1] ثم نحسب log_o لكل صف بمفرده: >>> log_o = beta[0] + beta[1] * x1 + beta[2] * x2 [-1.5 -0.4 -0.4 2.4] بعد ذلك نحول الأرجحية اللوغاريتمية إلى احتمالات: >>> o = np.exp(log_o) [ 0.223 0.670 0.670 11.02 ] >>> p = o / (o+1) [ 0.182 0.401 0.401 0.916 ] لاحظ أنه عندما يكونlog_o أكبر من الصفر، تكون قيمة o أكبر من الواحد، وتكون قيمة p أكبر من 0.5. تكون احتمالية likelihood خرج ما هي p عندما يكون 1==y وتكون 1-p عندما يكون 0==y، ولنفترض مثلًا أنه اعتقدنا أن احتمال probability ولادة صبي هي 0.8 وكان الخرج هو ولادة صبي، فستكون الاحتمالية likelihood هي 0.8، وإذا كان الخرج فتاةً، فستتكون الاحتمالية 0.2، كما يمكننا إجراء الحسابات كما يلي: >>> likes = y * p + (1-y) * (1-p) [ 0.817 0.401 0.598 0.916 ] تكون الاحتمالية الإجمالية للبيانات هي ناتج ضرب قيم likes: >>> like = np.prod(likes) 0.18 تكون احتمالية البيانات 0.18 في حال اعتمدنا قيم بيتا beta هذه، علمًا أن هدف الانحدار اللوجستي هو إيجاد المعامِلات التي تزيد من هذه الاحتمالية، ولإجراء هذا تستخدِم معظم الحِزم حلًا تكراريًا مثل تابع نيوتن Newton’s method، كما يمكنك زيارة صفحة ويكيبيديا للمزيد من التفاصيل. التنفيذ implementation تزودنا StatsModels بتنفيذ برمجي للانحدار اللوجستي ويُدعى logit وسُمي باسم الدالة التي تحول الاحتمال إلى الأرجحية اللوغاريتمية log odds، كما سنبحث عن متغيرات تؤثِّر على نسبة الجنس لتوضيح استخدامه.، وسنُعيد تحميل بيانات المسح الوطني لنمو الأسرة وسنختار حالات الحمل التي تجاوزت مدتها 30 أسبوع: live, firsts, others = first.MakeFrames() df = live[live.prglngth>30] لكن أحد شروط logit هو أن يكون المتغير التابع ثنائيًا binary عوضًا بدلًا من النوع البولياني boolean، لذا فقد أنشأنا عمودًا جديدًا باسم boy باستخدام astype(int) لتحويل القيم إلى قيم ثنائية صحيحة binary integers: df['boy'] = (df.babysex==1).astype(int) يُعَدّ عمر الوالدَين وترتيب الولادة والعِرق والحالة الاجتماعية عواملًا مؤثرةً في نسبة الجنس، كما يمكننا استخدام الانحدار اللوجستي من أجل التحقق فيما إذا كانت تظهر هذه التأثيرات في بيانات المسح الوطني لنمو الأسرة، حيث سنبدأ أولًا من عمر الأم: import statsmodels.formula.api as smf model = smf.logit('boy ~ agepreg', data=df) results = model.fit() SummarizeResults(results) تأخذ الدالتان logit وols الوسائط نفسها وهي عبارة عن معادلة بصيغة Patsy بالإضافة إلى إطار بيانات، وتكون نتيجة الدالة logit هي كائن Logit يمثِّل النموذج، حيث يتضمن سمتين بحيث تحتوي السمة الأولى endog على المتغير الداخلي endogenous variable وهو اسم آخر للمتغير التابع؛ أما السمة الثانية exog فتحتوي على المتغيرات الخارجية exogenous variables وهو اسم آخر للمتغيرات التوضيحية، وبما أنهما مصفوفتَي نمباي NumPy arrays، فقد يكون من الأفضل في بعض الأحيان تحويلها إلى أُطر بيانات DataFrames: endog = pandas.DataFrame(model.endog, columns=[model.endog_names]) exog = pandas.DataFrame(model.exog, columns=model.exog_names) تكون نتيجة model.fit كائن BinaryResults يشبه كائن RegressionResults الذي ينتج من ols، وإليك تلخيص للنتائج كما يلي: Intercept 0.00579 (0.953) agepreg 0.00105 (0.783) R^2 6.144e-06 وجدنا أن المعامِل الخاص بـ ageprep موجب الذي يشير إلى أنه من المرجح ولادة الأمهات الأكبر سنًا صبيانًا، لكن القيمة الاحتمالية هي 0.783 التي تعني أنه يمكن أن يكون التأثير الواضح ناتجًا عن الصدفة، ولا ينطبق مُعامِل التحديد R2 على الانحدار اللوجستي لكن يوجد عدة بدائل وتُدعى قيم R2 الوهمية أي pseudo-R2‎، إذ يمكن أن تكون هذه القيم مفيدةً لموازنة النماذج، وإليك على سبيل المثال نموذج يتضمن عدة عوامل يُقال أنها مرتبطة بنسبة الجنس: formula = 'boy ~ agepreg + hpagelb + birthord + C(race)' model = smf.logit(formula, data=df) results = model.fit() يتضمن هذا النموذج عمر الأب عند الولادة hpagelb إلى جانب عمر الأم أيضًا، ويمثِّل المتغير birthord ترتيب ولادة الطفل؛ أما العِرق فهو متغير فئوي، وإليك ما نتج كما يلي: Intercept -0.0301 (0.772) C(race)[T.2] -0.0224 (0.66) C(race)[T.3] -0.000457 (0.996) agepreg -0.00267 (0.629) hpagelb 0.0047 (0.266) birthord 0.00501 (0.821) R^2 0.000144 ليس لأي من هذه المعامِلات المُقدَّرة دلالة إحصائية، وعلى الرغم أن قيمة R2 الوهمية أعلى بقليل إلا أنه من المحتمل أن يكون هذا صدفةً. الدقة Accuracy أكثر ما يهمنا في سيناريو لعبة التخمين في المكتب هو دقة النموذج، وهي عبارة عن عدد التنبؤات الصحيحة موازنةً مع ما نتوقعه صدفةً، ونظرًا لأن عدد الصبيان يفوق عدد الإناث في بيانات المسح الوطني لنمو الأسرة، فيمكننا تحديد الاستراتيجية الأساسية على أنها تخمين ولادة صبي في كل مرة، وبذلك تكون دقة النموذج هي نسبة الصبيان: actual = endog['boy'] baseline = actual.mean() يكون المتوسط mean هو نسبة الصبيان وقيمته 0.507 بما أنه رُمِّز المتغير actual بترميز ثنائي صحيح binary integers، وإليك طريقة حساب دقة النموذج كما يلي: predict = (results.predict() >= 0.5) true_pos = predict * actual true_neg = (1 - predict) * (1 - actual) يُعيد results.predict مصفوفة نمباي NumPy array تحتوي على الاحتمالات والتي نقربها إلى 0 أو 1، علمًا أنه ينتج 1 عن عملية جداء مع المتغير actual إذا توقعنا ولادة صبي وأصبنا التوقُّع، في حين ينتج 0 في غير ذلك، لذا يشير المتغير true_pos إلى true positives أي الإيجابيات الصحيحة، كما يشير المتغير true_neg إلى الحالات التي نتوقع فيها ولادة بنت ويصيب توقعنا، وبلتالي تكون الدقة هي نسبة التخمينات الصحيحة: acc = (sum(true_pos) + sum(true_neg)) / len(actual) ظهرت النتيجة 0.512 وهي أفضل بقليل من القيمة الأساسية 0.507، لكن عليك ألا تأخذ هذه النتيجة على محمل الجد لأننا استخدَمنا البيانات نفسها في عمليتَي بناء واختبار النموذج، لذا قد لا يمتلك النموذج قوةً تنبؤيةً على البيانات الجديدة، لكن دعنا على أيّ حال نستخدِم النموذج للتبنؤ في لعبة التخمين في المكتب ولنفترض أنّ عمر صديقتك 35 عامًا وذات بشرة بيضاء وعمر زوجها 39 عامًا وأنها حامل بطفلهما الثالث: columns = ['agepreg', 'hpagelb', 'birthord', 'race'] new = pandas.DataFrame([[35, 39, 3, 2]], columns=columns) y = results.predict(new) إذا أردنا استدعاء results.predict لحالة حمل جديدة، فسيتوجب علينا إنشاء إطار بيانات DataFrame يحوي عمودًا لكل متغير في النموذج وتكون النتيجة في هذه الحالة 0.52، لذا يجب عليك تخمين ولادة صبي، لكن إذا حسَّن النموذج من فرصك في الفوز، فسيكون الفارق ضئيلًا جدًا. التمارين يوجد الحل الخاص بنا في chap11soln.ipynb. تمرين 1 لنفترض أن ولادة طفل أحد زملائك في العمل قد اقتربت وقررت المشاركة في لعبة التخمين في المكتب لتوقع موعد الولادة، فإذا افترضنا أنّ التخمينات قد حصلت في الأسبوع الثلاثين من الحمل، فما هي المتغيرات التي ستستخدِمها لتحقيق أفضل تنبؤ؟ يجب أن يقتصر استخدامك للمتغيرات على المتغيرات المعلومة قبل الولادة وتلك التي من المرجح أن تكون معروفةً عند المشاركين في لعبة المكتب. تمرين 2 تقترح فرضية تريفرز-ويلارد Trivers-Willard hypothesis اعتمادية نسبة الجنس بالنسبة للعديد من الثدييات على حالة الأم، أي تعتمد على عوامل مثل عمر الأم وحجمها وصحتها وحالتها الاجتماعية، كما يمكنك الاطلاع على صفحة ويكيبيديا للمزيد من التفاصيل.، حيث أظهرت بعض الدراسات وجود هذا التأثير ضمن البشر لكن النتائج مختلطة، إذ أجرينا اختبارات على بعض المتغيرات المتعلِّقة بهذه العوامل لكننا لم نجد أي تأثير ذا دلالة إحصائية على نسبة الجنس. اختبر المتغيرات الأخرى في ملفات الحمل والمستجيبِين باستخدام طريقة التنقيب عن البيانات، وذلك على أساس تمرين على ما سبق، هل وجدت بعد ذلك أيّ عوامل لها تأثير كبير؟ تمرين 3 يمكنك استخدام انحدار بواسون Poisson regression إذا كانت القيمة التي تريد التنبؤ بها تعدادًا، علمًا أنّ تنفيذ انحدار بواسون البرمجي موجود في StatsModels مع دالة اسمها poisson، وهي تعمل بالطريقة نفسها التي تعمل فيها الدالتين ols وlogit، لذا دعنا نستخدِم هذه الدالة على أساس تمرين لما سبق للتنبؤ بعدد أطفال امرأة معيّنة في بيانات المسح الوطني لنمو الأسرة، حيث أن اسم المتغير الذي يمثل عدد الأطفال هو numbabes. لنفترض أنك قابلت امرأةً تبلغ من العمر 35 عامًا سوداء البشرة وخريجة جامعية يتجاوز دخل أسرتها السنوي 75 ألف دولار، فكم تتوقع أن يكون عدد أطفالها؟ تمرين 4 يمكنك استخدام الانحدار اللوجستي متعدد الحدود multinomial logistic regression إذا أردت تنبؤ قيمة فئوية، علمًا أنّ تنفيذه البرمجي في StatsModels مع دالة اسمها mnlogit، لذا دعنا نستخدِم هذه الدالة على أساس تمرين لما سبق لتخمين فيما إذا كانت المرأة متزوجة أو أرملة أو منفصلة عن زوجها أو أنها لم تتزوج على الإطلاق، علمًا أنّ اسم المتغير الذي يمثل الحالة الزوجية في المسح الوطني لنمو الأسرة هو rmarital. لنفترض أنك قابلت امرأةً تبلغ من العمر 25 عامًا بيضاء البشرة وأنهت دراسة المرحلة الثانوية لكنها لم تدرس في المرحلة الجامعية ودخل أسرتها السنوي حوالي 45 ألف دولار، ما هو احتمال أن تكون متزوجة أو أرملة، …إلخ؟ ترجمة -وبتصرف- للفصل Chapter 11 Regression analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا اختبار الفرضيات الإحصائية التقدير Estimation الإحصائي في بايثون دوال التوزيع التراكمي Cumulative distribution functions العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون
  5. توجد الشيفرة الخاصة بهذا المقال في الملف linear.py. ملاءمة المربعات الصغرى تقيس معاملات الترابط قوة وإشارة العلاقة لكنها لا تقيس الميل slope، إذ توجد عدة طرق لتقدير الميل وأكثر هذه الطرق شيوعًا هي ملاءمة المربعات الصغرى الخطية linear least squares fit، حيث تُعَدّ الملاءمة الخطية linear fit خطًا ينمذج العلاقة بين المتغيرات؛ أما ملاءمة المربعات الصغرى least squares فتقلل الخطأ التربيعي المتوسط mean squared error -أو MSE اختصارًا- بين الخط والبيانات. لنفترض أنه لدينا متسلسلةً من النقاط ys، ونريد التعبير عنها على أساس دالة من متسلسلة أخرى xs، فإذا كانت هناك علاقةً خطيةً بين xs وys مع نقطة تقاطع inter وميل slope، فسنتوقع عندها y ليكون inter + slope * x، علمًا أنّ هذا التوقع تقريبي فحسب أي ليس دقيقًا إلا إذا كان الترابط مثاليًا، وتكون الصيغة الرياضية للانحراف العمودي عن الخط أو ما يُعرف بالراسب residual هي: res = ys - (inter + slope * xs) قد تكون الرواسب ناتجةً عن عوامل عشوائية مثل الخطأ في القياس أو عوامل غير عشوائية وغير معروفة، فإذا حاولنا مثلًا توقّع الوزن من دالة الطول، فستشمل عوامل غير معروفة الحمية الغذائية والتمارين الرياضية ونوع الجسم، وإذا لم يكن حسابنا للمعامِلَين inter وslope صحيحًا، فستكون الرواسب أكبر، لذا من المنطقي أن تكون المعاملات التي نريدها هي تلك التي تقلل من الرواسب. قد نحاول تقليل القيمة المطلقة للرواسب أو مربعها أو مكعبها، لكن الخيار الأكثر شيوعًا واستخدامًا هو تقليل مجموع مربعات الرواسب sum(res**2)، لكن لمَ تُعَدّ هذه الطريقة هي الأشيع؟ هناك ثلاث أسباب وجيهة وسبب أقل أهمية لذلك كما يلي: يعامِل التربيع الرواسب الموجبة والسالبة بالطريقة نفسها، وهي ميزة مفيدة لنا في أغلب الأحيان. يُعطي التربيع وزنًا -أي ترجيحًا- أكبر للرواسب الكبيرة، لكن ليس للدرجة التي تتسبب فيها هذه الميزة بهيمنة الرواسب الأكبر دائمًا. *إذا لم تكن الرواسب مترابطةً وإذا كان توزيعها طبيعيًا مع متوسط قدره 0 وتباين ثابت لكن غير معروف، فستكون ملاءمة المربعات الصغرى هي نفسها مُقدِّر الاحتمال الأعظم maximum likelihood estimator -أو MLE اختصارًا- لكل من inter وslope، ويمكنك زيارة صفحة ويكيبيديا لمزيد من المعلومات. يمكن حساب قيم inter وslope التي تقلل من الرواسب المربعة بكفاءة. يُعَدّ السبب الأخير منطقيًا حينما كانت كفاءة الحسابات أهم من اختيار الطريقة الأنسب للمشكلة التي نحاول حلها لكن الأمر لم يعُد كذلك، لذلك يجدر التفكير فيما إذا كانت الرواسب المربّعة هي ما نريد تقليله أم لا، ولنفترض مثلًا أنك تحاول التنبؤ بقيم ys باستخدام xs، فقد يكون تخمين القيم المرتفعة جدًا أفضل -أو أسوأ- من تخمين القيم المنخفضة جدًا، وقد نرغب في هذه الحالة في حساب بعض من دوال الكلفة لكل راسب من الرواسب ثم تقليل الكلفة الإجمالية sum(cost(res))، ومع ذلك يُعَدّ حساب ملاءمة المربعات الصغرى سريعًا وسهلًا وغالبًا ما يكون جيدًا بدرجة كافية. التنفيذ يزود المستودع thinkstats2 بدوال بسيطة توضِّح المربعات الصغرى الخطية، إليك الشيفرة الموافقة لذلك كما يلي: def LeastSquares(xs, ys): meanx, varx = MeanVar(xs) meany = Mean(ys) slope = Cov(xs, ys, meanx, meany) / varx inter = meany - slope * meanx return inter, slope يأخذ التابع LeastSquares متسلسلتين هما xs وys ويُعيد المعامِلَين المُقدِّرَين inter وslop، كما يمكنك الاطلاع على مزيد من التفاصيل حول آلية عمله عن طريق زيارة صفحة ويكيبيديا، كما يزود المستودع thinkstats2 بالتابع FitLine أيضًا، حيث يأخذ المعامِلَين inter وslope ويُعيد الخط الملائم للمتسلسلة xs، وإليك الشيفرة الموافقة لذلك كما يلي: def FitLine(xs, inter, slope): fit_xs = np.sort(xs) fit_ys = inter + slope * fit_xs return fit_xs, fit_ys يمكننا استخدام هذه الدوال لحساب ملاءمة المربعات الصغرى لوزن الولادات على أساس دالة لعمر الأم: live, firsts, others = first.MakeFrames() live = live.dropna(subset=['agepreg', 'totalwgt_lb']) ages = live.agepreg weights = live.totalwgt_lb inter, slope = thinkstats2.LeastSquares(ages, weights) fit_xs, fit_ys = thinkstats2.FitLine(ages, inter, slope) تبلغ قيمة نقطة التقاطع المقدَّرة والميل المقدَّر 6.8 رطلًا -أي حوالي 3.08 كيلوغرامًا- و0.017 رطلًا في العام الواحد -أي حوالي 0.007 كيلوغرامًا في العام الواحد-، ومن الصعب تفسير هذه القيم بهذه الصورة، حيث تُعَدّ نقطة التقاطع الوزن المتوقع للطفل إذا كان عمره أمه 0 عام، وهذا ليس منطقيًا في سياقنا، والميل صغير جدًا بحيث لا يمكن فهمه بسهولة. غالبًا ما يكون من المفيد البدء بنقطة التقاطع عند متوسط x بدلًا من البدء بنقطة التقاطع من القيمة 0 أي x=0، لذا يكون متوسط العمر في هذه الحالة هو 25 تقريبًا ومتوسط وزن الطفل للأم التي يبلغ عمرها 25 عامًا هو 7.3 رطلًا -أي حوالي 3.31 كيلوغرامًا-، بذلك يكون الميل 0.27 رطلًا في العام الواحد أي حوالي 0.12 كيلوغرامًأ- أو 0.17 رطلًا كل 10 أعوام -أي حوالي 0.07 كيلوغرامًا كل 10 أعوام-. يوضِّح الشكل السابق مخطط انتشار أوزان الولادات وعمر الأم مع ملاءمة خطية، ومن الجيد النظر إلى شكل مثل هذا بهدف تقييم ما إذا كانت العلاقة خطيةً أم لا وما إذا كان الخط الملائم يبدو نموذجًا جيدًا للعلاقة أم لا. الرواسب يُعَدّ رسم الرواسب اختبارًا مفيدًا أيضًا، كما يزودنا المستودع thinkstats2 بدالة تحسب الرواسب، إليك الشيفرة الموافقة لذلك كما يلي: def Residuals(xs, ys, inter, slope): xs = np.asarray(xs) ys = np.asarray(ys) res = ys - (inter + slope * xs) return res تأخذ الدالة Residuals متسلسلتين هما xs وys، ومعامِلَين مُقدِّرَين هما inter وslope وتُعيد الفروق بين القيم الفعلية والخط الملائم. يوضِّح الشكل السابق رواسب الملاءمة الخطية. جمعنا المستجيبين حسب العمر وحسبنا قيم المئين في كل مجموعة تمامًا من أجل رسم مخططات للرواسب كما رأينا في قسم توصيف العلاقات في المقال السابق، حيث يوضِّح الشكل السابق المئين 25 -أي 25th percentile- والمئين 50 -أي 50th percentile- والمئين 75 -أي 75th percentile- للرواسب الموجودة في كل مجموعة، وكما هو متوقع فإن الوسيط median يقارب الصفر والانحراف الربيعي interquartile range هو حوالي رطلين -أي حوالي 0.9 كيلوغرامًا-، لذا يمكننا تخمين وزن الطفل بخطأ قدره رطلًا واحدًا بحوالي ‎50% من المرات إذا علمنا عمر الأم مسبقًا. تكون هذه الخطوط في الوضع المثالي مسطحةً ويشير هذا إلى أن الرواسب عشوائية ومتوازية وإلى أن المجموعات متساوية فيما بينهما من حيث تباين الرواسب -أي أن قيم الرواسب متساوية بالنسبة لكل المجموعات-، حيث أن الخطوط قريبة من التوازي وهذا أمر جيد، لكن يوجد بعض الانحناءات في هذه الخطوط مما يدل على أن العلاقة لاخطية، ومع ذلك تُعَدّ الملاءمة الخطية نموذجًا بسيطًا ربما يكون جيدًا بما يكفي لبعض الأغراض. التقدير يُعَدّ المعامِلان slope وinter تقديرَين بناءً على عينة ما، وهما عُرضة للتحيز في أخذ العينات sampling bias تمامًا مثل التقديرات الأخرى، وكذلك فهما عُرضة للخطأ في القياس measurement error والخطأ في أخذ العينات sampling error، حيث أن التحيُز في أخذ العينات -كما ناقشنا في مقال التقدير Estimation الإحصائي في بايثون- ناتج عن أخذ عينات غير تمثيلية non-representative sampling، والخطأ في القياس ناتج عن أخطاء في جمع وتسجيل البيانات؛ أما الخطأ في أخذ العينات فهو نتيجة لقياس عينة ما بدلًا من قياس السكان بأكملهم. سنطرح السؤال التالي من أجل تقييم الخطأ في أخذ العينات: ما مقدار التباين الذي نتوقعه في التقديرات إذا أجرينا هذه التجربة مرةً أخرى؟ حيث سنجيب عن هذا السؤال عن طريق إجراء عدة تجارب محاكاة ومن ثم حساب توزيعات أخذ العينات للتقديرات، وقد طبقنا محاكاةً للتجارب عن طريق إعادة أخذ عينات البيانات، أي نعامل حالات الحمل المرصودة كما لو أنها تمثل السكان جميعهم ومن ثم سحبنا عينات مع الاستبدال من العينة المرصودة، وإليك الشيفرة الموافقة لذلك كما يلي: def SamplingDistributions(live, iters=101): t = [] for _ in range(iters): sample = thinkstats2.ResampleRows(live) ages = sample.agepreg weights = sample.totalwgt_lb estimates = thinkstats2.LeastSquares(ages, weights) t.append(estimates) inters, slopes = zip(*t) return inters, slopes تأخذ الدالة SamplingDistributions إطار بيانات DataFrame بسطر واحد لكل ولادة حية، ويمثِّل المتغير iter عدد التجارب التي سنحاكيها، كما تستخدِم ResampleRows لإعادة أخذ العينات الخاصة بحالات الحمل المرصودة، حيث أننا تطرقنا سابقًا إلى SampleRows التي تختار أسطرًا عشوائية من إطار بيانات، كما يزودنا المستودع thinkstats2 بالدالة ResampleRows التي تُعيد عينةً حجمها بحجم العينة الأصلية كما يلي: def ResampleRows(df): return SampleRows(df, len(df), replace=True) استخدمنا العينات المُحاكاة لتقدير المِعاملات بعد عملية إعادة أخذ العينات، إذ تكون النتيجة متسلسلتين هما نقط التقاطع المُقّدَّرة والميول المقدَّرة، كما لخصنا توزيعات أخذ العينات عن طريق طباعة فاصل الثقة والخطأ المعياري، وإليك الشيفرة الموافقة لذلك كما يلي: def Summarize(estimates, actual=None): mean = thinkstats2.Mean(estimates) stderr = thinkstats2.Std(estimates, mu=actual) cdf = thinkstats2.Cdf(estimates) ci = cdf.ConfidenceInterval(90) print('mean, SE, CI', mean, stderr, ci) يأخذ التابع Summarize متسلسلةً من التقديرات والقيمة الفعلية، حيث تطبع تقديرات المتوسط والخطأ المعياري وفاصل الثقة ‎90%؛ أما بالنسبة لنقطة التقاطع فيكون تقدير المتوسط هو 6.83 مع خطأ معياري قدره 0.07 وفاصل ثقة ‎90% هو (6.71‎- 6.94)؛ أما الميل المُقدَّر فشكله أكثر تراصًا وإحكامًا، حيث أن قيمته هي 0.0174 وقيمة الخطأ المعياري 0.0028، وقيمة فاصل الثقة هي (‎0.0126 - 0.0220)، وفي الواقع الفرق بين النهاية المنخفضة والمرتفعة من فاصل الثقة هو الضعف، لذا يجب اعتباره تقديرًا تقريبيًا. يمكننا حساب كل الخطوط الملائمة إذا أردنا رسم أخطاء أخذ العينات للتقديرات، لكن إذا أردنا أن يكون تمثيل البيانات أقل تفاوتًا، فيمكننا رسم فاصل الثقة ‎90% لكل عمر، إليك الشيفرة الموافقة لذلك كما يلي: def PlotConfidenceIntervals(xs, inters, slopes, percent=90, **options): fys_seq = [] for inter, slope in zip(inters, slopes): fxs, fys = thinkstats2.FitLine(xs, inter, slope) fys_seq.append(fys) p = (100 - percent) / 2 percents = p, 100 - p low, high = thinkstats2.PercentileRows(fys_seq, percents) thinkplot.FillBetween(fxs, low, high, **options) يمثِّل المتغير xs متسلسلةً لعمر الأم؛ أما inters وslopes فهما معامِلَين مقدَّرَين ولَّدهما التابع SamplingDistributions، في حين يشير المتغير percent إلى فاصل الثقة الذي نريد رسمه، كما يولِّد التابع PlotConfidenceIntervals الخط الملائم لكل زوج من inter وslope ويخزن النتائج في متسلسلة fys_seq، ومن ثم يستخدِم PercentileRows لتحديد مئين العلوي والسفلي من y لكل قيمة x؛ أما بالنسبة لفاصل الثقة ‎90% فيحدد التابع المئين 5 -أي 5th percentile- والمئين 95 -أي 95th percentile-، كما يرسم التابع FillBetween مضلعًا يملأ الفراغ بين خطين اثنين. يُظهِر الشكل السابق فاصلي الثقة ‎50% و‎90% التباين في الخط الملائم الناتج عن الخطأ في أخذ عينات inter وslope، كما يوضِّح فاصلي الثقة ‎50% و‎90% للمنحنيات الملائمة لأوزان الولادات على أساس دالة عمر الأم؛ أما العرض الرأسي للمنطقة فيمثِّل تأثير الخطأ في أخذ العينات، حيث أن التأثير أصغر على القيم التي تقارب المتوسط mean وأكبر على القيم المتطرفة extremes. حسن الملاءمة يمكننا قياس جودة النموذج الخطي أو ما يُعرَف بحُسن الملائمة goodness of fit بعدة طرق، وإحدى أبسط هذه الطرق هي الانحراف المعياري للرواسب، فإذا استخدمت نموذجًا خطيًا للتنبؤ، فسيكونStd(res) هو خطأ الجذر التربيعي المتوسط -أو RMSE اختصارًا- لتنبؤاتك. يكون خطأ الجذر التربيعي المتوسط مثلًا لتخمينك هو 1.14 رطلًا -أي حوالي 0.5 كيلوغرامًا- إذا استخدمت عمر الأم لتخمين وزن الطفل، وإذا خمنت وزن الطفل دون معرفة عمر الأم، فسيكون خطأ الجذر التربيعي المتوسط لتخمينك هو Std(ys) أي 1.14 رطل، لذا لا تؤدي معرفة عمر الأم في مثالنا هذا إلى تحسين التنبؤ إلى حد كبير، كما يمكننا قياس حُسن الملاءمة عن طريق مُعامِل التحديد coefficient of determination أيضًا، علمًا أنه يرمز له R2 ويدعى مربع R: def CoefDetermination(ys, res): return 1 - Var(res) / Var(ys) يُعَدّ Var(res) الخطأ التربيعي المتوسط -أو MSE اختصارًا- لتنبؤاتك باستخدام النموذج؛ أما الخطأ التربيعي المتوسط للتنبؤات بدون استخدام النموذج فهو Var(ys)، لذا فإن نسبتها هي الجزء المتبقي من الخطأ التربيعي المتوسط إذا استخدمتَ النموذج، وR2 هو جزء الخطأ التربيع المتوسط الذي يحذفه النموذج، حيث أنّ قيمة R2 بالنسبة لوزن الولادة وعمر الأم هو 0.0047 أي أن عمر الأم يتنبأ بنصف ‎1% من التباين في وزن الولادة. توجد علاقة بسيطة بين مُعامِل ترابط بيرسون وبين مُعامِل التحديد وهي تحدَّد بالعلاقة R2=ρ2، فإذا كان ρ مثلًا 0.8 أو 0.8-، فسيكون R2=0.64، وعلى الرغم أنه غالبًا ما يُستخدَم كل من ρ وR2 لتحديد قوة علاقة، إلا أنه ليس من السهل تفسير هذين المقدارين من حيث القوة التنبؤية، كما يكون أفضل تمثيل لجودة التنبؤ برأينا هو Std(res) خاصةً إذا مُثِّل بالعلاقة مع Std(ys)، فعندما يتحدث الأشخاص مثلًا عن صلاحية اختبار سات SAT -وهو اختبار موحد للالتحاق بالجامعات في الولايات المتحدة الأمريكية-، فإنهم غالبًا ما يتحدثون عن الارتباطات بين درجات سات ومقاييس الذكاء الأخرى. يوجد - وقفًا لإحدى الدراسات- ارتباط بيرسون ρ=0.72 بين درجات اختبار سات ومعدل اختبار الذكاء IQ، وقد يبدو هذا الارتباط قويًا لكن R2=ρ=0.52، لذا فإن درجات سات لا تمثِّل سوى 52‎%‎ من التباين في اختبار الذكاء IQ، كما وُحِّد معدل اختبار الذكاء IQ بالمعادلة Std(ys)=15، لذا يكون: >>> var_ys = 15**2 >>> rho = 0.72 >>> r2 = rho**2 >>> var_res = (1 - r2) * var_ys >>> std_res = math.sqrt(var_res) 10.4096 لذا يقلل استخدام نتيجة اختبار سات لتوقع معدل الذكاء IQ من معدل خطأ الجذر التربيعي المتوسط RMSE بحيث يصبح 10.4 نقطة بعد أن كان 15 نقطة، أي ينتج عن ارتباط 0.72 انخفاضًا في خطأ الجذر التربيعي المتوسط بنسبة ‎31% فقط، فإذا رأيتَ ارتباطًا مذهلًا، فتذكَّر أن R2 هو مؤشر أفضل للانخفاض في الخطأ التربيعي المتوسط MSE، وكذلك يكون الانخفاض في خطأ الجذر التربيعي المتوسط RMSE مؤشرًا أفضل للقوة التنبؤية. اختبار نموذج خطي يُعَدّ تأثير عمر الأم على وزن الولادة صغيرًا وتُعَدّ قوته التنبؤية صغيرةً، لذا هل من الممكن أن تكون العلاقة الظاهرة ناتجةً عن الصدفة؟ هناك عدة طرق يمكننا من خلالها اختبار نتيجة الملاءمة الخطية linear fit. يتمثَّل أحد الخيارات في اختبار كون الانخفاض الظاهر في الخطأ التربيعي المتوسط ناتجًا عن الصدفة، وفي هذه الحالة تكون إحصائية الاختبار هي R2 وفرضية العدم هنا هي أنه لا توجد علاقة بين المتغيرات، كما يمكننا محاكاة فرضية العدم عن طريق التبديل permutation كما فعلنا في قسم اختبار الارتباط في المقال السابق عندما اختبرنا الارتباط بين عمر الأم ووزن الولادة. يكافئ الاختبار أحادي الجانب للمقدار R2 للاختبار ثنائي الجانب لارتباط بيرسون ρ في الواقع نظرًا لأن R2=ρ2، وقد أجرينا هذا الاختبار سابقًا ووجدنا أنّ القيمة الاحتمالية أصغر من 0.001 أي p<0.001، لذا نستنتج أن للعلاقة الظاهرة بين عمر الأم ووزن الولادة دلالة إحصائية. توجد طريقة أخرى لاختبار فيما إن كان الميل slope الظاهر ناجمًا عن الصدف فحسب، إذ تقول فرضية العدم في هذا الحالة أنّ الميل صفر، ويمكننا في هذه الحالة نمذجة أوزان الولادات على أساس تباينات عشوائية حول قيمة المتوسط الخاصة بهم، وإليك HypothesisTest خاص بهذا النموذج كما يلي: class SlopeTest(thinkstats2.HypothesisTest): def TestStatistic(self, data): ages, weights = data _, slope = thinkstats2.LeastSquares(ages, weights) return slope def MakeModel(self): _, weights = self.data self.ybar = weights.mean() self.res = weights - self.ybar def RunModel(self): ages, _ = self.data weights = self.ybar + np.random.permutation(self.res) return ages, weights البيانات هنا مُمثَّلة بمتسلسلتين هما ages يمثِّل الأعمار وweights يمثِّل الأوزان، وتكون إحصائية الاختبار test statistic هنا الميل المُقدَّر بواسطة LeastSquares، في حين يكون نموذج فرضية العدم ممثَّلًا بمتوسط أوزان جميع الأطفال وبالانحرافات عن المتوسط، كما يمكننا توليد بيانات مُحاكاة عن طريق تبديل الانحرافات وإضافة هذه الانحرافات إلى المتوسط، وإليك الشيفرة التي تنفِّذ اختبار الفرضية كما يلي: live, firsts, others = first.MakeFrames() live = live.dropna(subset=['agepreg', 'totalwgt_lb']) ht = SlopeTest((live.agepreg, live.totalwgt_lb)) pvalue = ht.PValue() تكون القيمة الاحتمالية هنا أقل من 0.001، أي على الرغم من أن الميل المُقدَّر صغير إلا أنه من غير المرجح أن يكون ناجمًا عن الصدفة، ويُعَدّ تقدير القيم الاحتمالية عن طريق إجراء محاكاة لفرضية العدم تقديرًا صحيحًا تمامًا لكن يوجد بديل أبسط من هذه الطريقة، علمًا أننا حسبنا توزيع أخذ العينات الخاص بالميل في قسم التقدير في هذا المقال، لذا افترضنا أنّ الميل المرصود صحيح ثم حاكينا تجاربًا عن طريق إعادة أخذ العينات resampling. يوضِّح الشكل التالي توزيع أخذ العينات للميل من قسم التقدير في هذا المقال، ويوضح أيضًا توزيع الميول المولَّدة في ظل فرضية العدم، كما يتركَّز توزيع أخذ العينات حول الميل المقدَّر وهو 0.017 رطلًا في العام الواحد، في حين تتركز الميول تحت فرضية العدم حول القيمة 0، لكن بخلاف ذلك فإن التوزيعات متطابقة وهي متماثلة أيضًا لأسباب سنراها في قسم لاحق في مقال لاحق. يوضِّح الشكل السابق توزيع أخذ العينات للميل المُقدَّر وتوزيع الميول المولَّدة في ظل فرضية العدم، حيث أن الخطوط العمودية هي عند القيمة 0 والميل المرصود هو 0.017 رطل في العام الواحد، لذا يمكننا تقدير القيمة الاحتمالية بطريقتين هما: حساب احتمال تخطي قيمة الميل في ظل فرضية العدم لقيمة الميل المرصود. حساب احتمال انخفاض قيمة الميل في توزيع أخذ العينات عن الصفر، فإذا كان الميل المُقدَّر سالبًا، فسنحسب احتمال زيادة قيمة الميل في توزيع أخذ العينات عن الصفر. يُعَدّ الخيار الثاني خيارًا أسهل لأننا عادةً نريد حساب توزيع أخذ العينات للمعامِلات على أي حال، كما يُعَدّ تقديرًا تقريبيًا جيدًا إلا في حال كان حجم العينة صغيرًا وكان توزيع الرواسب متجانفًا skewed، وحتى حينها يكون هذا الخيار جيدًا بما فيه الكفاية لأنه ليس من الضروري أن تكون القيم الاحتمالية دقيقةً، وإليك الشيفرة التي تقدِّر القيمة الاحتمالية للميل باستخدام توزيع أخذ العينات: inters, slopes = SamplingDistributions(live, iters=1001) slope_cdf = thinkstats2.Cdf(slopes) pvalue = slope_cdf[0] وجدنا مجدَّدًا أنّ القيمة الاحتمالية أصغر من 0.001 أي p<0.001. إعادة أخذ العينات مع الأوزان عامَلنا في هذا الكتاب بيانات المسح الوطني لنمو الأسرة NSFG على أنها عينة تمثيلية، لكنها ليس تمثيلية كما ذكرنا في مقال تحليل البيانات الاستكشافية لإثبات النظريات الإحصائية، حيث تعمَّد هذا المسح الإفراط في أخذ عينات عدة مجموعات من أجل زيادة احتمال ظهور نتائج ذات دلالة إحصائية، أي بهدف تحسين قوة الاختبارات التي تخص هذه المجموعات. يُعَدّ تصميم الإحصائية هذا مفيدًا لعدة أغراض لكنه يعني أنه لا يمكننا استخدام العينة لتقدير قيم عامة السكان من دون حساب عملية أخذ العينات، كما تحوي بيانات المسح الوطني لنمو الأسرة متغيرًا يُدعى finalwgt لكل مستجيب بحيث يشير إلى عدد الأشخاص الذي يمثلهم هذا المستجيب، وتُدعى هذه القيمة بوزن أخذ العينات sampling weight أو الوزن فقط، فإذا أجريت مسحًا لمائة ألف مستجيب في بلد يحوي 300 مليون نسمة، فسيمثل كل مستجيب 3000 شخص، وإذا بالغت في أخذ عينات مجموعة ما بعامِل 2 -أي الضعف-، فسيكون لكل شخص في المجموعة ذات العينات المبالغة بها وزنًا أقل، أي حوالي 1500. يمكننا استخدام إعادة أخذ العينات إذا أردنا تصحيح المبالغة في أخذ العينات oversampling، أي يمكننا سحب عينات من المسح باستخدام الاحتمالات المتناسبة مع أوزان أخذ العينات، ثم يمكننا توليد توزيعات أخذ العينات sampling distributions والأخطاء المعيارية standard errors ومجالات الثقة confidence intervals، حيث سنقدِّر مثلًا قيمة متوسط وزن الولادة مع وبدون أوزان أخذ العينات. رأينا في قسم التقدير في هذا المقال التابع ResampleRows الذي يختار أسطرًا من إطار بيانات معطيًا كل الأسطر الاحتمال نفسه؛ أما الآن فسيتوجب علينا إجراء ذات العمليات لكن مع استخدام احتمالات متناسبة مع أوزان أخذ العينات، في حين يأخذ التابع ResampleRowsWeighted إطار بيانات ويَعيد أخذ عينات resamples الأسطر حسب الوزن في المتغير finalwgt، ثم يُعيد إطار البيانات الذي يحتوي على الأسطر التي أُجري عليها عملية إعادة أخذ عينات. def ResampleRowsWeighted(df, column='finalwgt'): weights = df[column] cdf = Cdf(dict(weights)) indices = cdf.Sample(len(weights)) sample = df.loc[indices] return sample يُعَدّ المتغير weights سلسلةً Series، ويؤدي تحويلها إلى قاموس dictionary إلى إنشاء خريطة map من الفهارس إلى الأوزان، علمًا أنّ القيم في cdf هي فهارس والاحتمالات متناسبة مع الأوزان؛ أما indicies فهي متسلسلة sequence من أسطر تحوي فهارس، ويمثِّل sample إطار بيانات يحتوي على الأسطر المُحدَّدة، وقد يظهر السطر نفسه أكثر من مرة لأننا نستخدِم عينات مع الاستبدال، كما يمكننا الآن موازنة تأثير إعادة أخذ العينات مع الأوزان وبدونها، حيث نولِّد توزيعات أخذ العينات بدون أوزان (أي دون إعطاء ترجيح) كما يلي: estimates = [ResampleRows(live).totalwgt_lb.mean() for _ in range(iters)] أما مع أوزان (أي مع ترجيح) فيصبح كما يلي: estimates = [ResampleRowsWeighted(live).totalwgt_lb.mean() for _ in range(iters)] يلخص الجدول التالي النتائج: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } متوسط أوزان الولادات (مقدرةً بالرطل) الخطأ المعياري فاصل الثقة 90% بدون أوزان 7.27 0.014 (7.24, 7.29) مع أوزان 7.35 0.014 (7.32, 7.37) يُعَدّ أثر الترجيح في هذا المثال صغيرًا لكنه غير مهمل، ويكون الفرق في المتوسطين المُقدَّرَين مع ترجيح وبدون ترجيح هو 0.08 رطلًا تقريبًا -أي حوالي 0.03 كيلوغرامًا- أو 1.3 أوقيةً، وهذا الفرق أكبر بكثير من الخطأ المعياري للتقدير الذي تبلغ قيمته 0.014 رطلًا -أي حوالي 0.006 كيلوغرامًا-، مما يعني أنّ الفرق لم يحدث صدفةً. تمارين يوجد حل هذا التمرين في chap10soln.ipynb. تمرين 1 استخدِم البيانات الخاصة بنظام مراقبة عوامل المخاطر السلوكية BRFSS واحسب ملاءمة المربعات الصغرى الخطية للوغاريتم الوزن مقابل الطول. ما هي أفضل طريقة لتمثيل المعامِلات المُقدَّرة لنموذج مثل هذا، أي نموذج طُبق التحويل اللوغاريتم على أحد متغيراته؟ وإلى أي مدى سيساعدك على معرفة وزن شخص ما في حال كنت تحاول تخمينه؟ يُفرِط نظام مراقبة العوامل السلوكية في أخذ العينات لبعض المجموعات تمامًا مثل المسح الوطني لنمو الأسرة ويزودنا بوزن أخذ العينات لكل مستجيب، حيث أنّ اسم متغير هذه الأوزان في بيانات نظام مراقبة العوامل السلوكية هو finalwt. استخدم إعادة أخذ العينات resampling مع الأوزان وبدونها من أجل تقدير متوسط أطوال المستجيبين في نظام مراقبة العوامل السلوكية، بالإضافة إلى الخطأ المعياري للمتوسط وفاصل الثقة 90‎%‎، وإلى أي مدى يؤثر الترجيح الصحيح على التقديرات؟ ترجمة -وبتصرف- للفصل Chapter 10 Linear least squares analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون نمذجة التوزيعات Modelling distributions في بايثون دوال الكثافة الاحتمالية في بايثون div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  6. توجد الشيفرة الخاصة بهذا المقال في الملف hypothesis.py. الطريقة الكلاسيكية في اختبار الفرضيات لاحظنا عند استكشاف بيانات المسح الوطني لنمو الأسرة NSFG تأثيرات واضحةً منها الفرق بين الأطفال الأوائل وبقية الأطفال، ولم نشكك في صحتها حتى الآن، لذا سنخصص هذا المقال لاختبار صحة الآثار التي ظهرت من عدمها، حيث سنتطرق إلى سؤال أساسي وهو: في حال رأينا تأثيرات في عينة ما، فهل من المرجح أن تظهر هذه التأثيرات في عينة أكبر؟ فقد نرى فرقًا على سبيل المثال في متوسط مدة الحمل للأطفال الأوائل وبقية الأطفال في المسح الوطني لنمو الأسرة، ونود معرفة ما إذا كان هذا التأثير يعكس اختلافًا حقيقيًا في مدة حمل النساء في الولايات المتحدة الأمريكية أم أنه قد يظهر في العينة عن طريق الصدفة. يمكننا صياغة هذا السؤال بعدة طرق، منها اختبار فرضيات العدم عند فيشر Fisher null hypothesis testing ونظرية قرار نيمان بيرسون Neyman-Pearson decision theory، والاستدلال البايزي Bayesian inference، كما يُعَدّ ما سنقدِّمه في هذا المقال مجموعةً فرعيةً من الطرق الثلاث المذكورة آنفًا وهي ما يستخدِمه معظم الأشخاص في الممارسات العملية وسنسميها الطريقة الكلاسيكية في اختبار الفرضيات classical hypothesis testing. الهدف من الطريقة الكلاسيكية في اختبار الفرضيات هو الإجابة على السؤال التالي: إذ كان لدينا عينةً ما وهناك تأثير ظاهر فيها، فما هو احتمال رؤية مثل هذا التأثير عن طريق الصدفة؟ إليك كيف نجيب على هذا السؤال: تتمثل الخطوة الأولى في تحديد حجم التأثير الظاهر وذلك عن طريق اختيار إحصائية اختبار test statistic، حيث أنّ التأثير الظاهر في مثال المسح الوطني لنمو الأسرة هو فرق في مدة الحمل بين الأطفال الأوائل وبقية الأطفال، لذا فمن المنطقي أن تكون إحصائية الاختبار هي فرق المتوسطات means بين المجموعتين. تتمثل الخطوة الثانية في تعريف فرضية عدم null hypothesis، وهي نموذج للنظام مبنية على افتراض أن التأثير الظاهر ليس حقيقيًا، وتكون فرضية العدم في مثال المسح الوطني لنمو الأسرة هي أنه لا يوجد فرق بين الأطفال الأوائل وبقية الأطفال، أي أنّ توزيع مدة الحمل للأطفال الأوائل هو توزيع مدة الحمل لبقية الأطفال نفسه. تتمثل الخطوة الثالثة في حساب القيمة الاحتمالية p-value، وهي احتمال وجود التأثير الظاهر في حال كانت فرضية العدم صحيحة، حيث نحسب الفرق الفعلي في المتوسطين في مثال المسح الوطني لنمو الأسرة، ومن ثم نحسب احتمال وجود الفارق بالحجم نفسه أو وجود فارق أكبر وذلك في ظل وجود فرضية العدم. تتمثّل الخطوة الأخيرة في أنها تفسير النتيجة، حيث إذا كانت القيمة الاحتمالية منخفضةً فيُقال عن التأثير أنه ذات دلالة إحصائية statistically significant، أي أنه من غير المحتمل أن يكون قد ظهر بمحض الصدفة، حيث يمكننا في هذه الحالة استنتاج أنه من المرجح ظهور هذا التأثير في عينة أكبر. يتشابه منطق هذه العملية مع البرهان بالتناقض، أي لإثبات تعبير رياضي A، سنفترض مؤقتًا أنّ A خاطئ، فإذا أدى هذا الافتراض إلى تناقض، فسنستنتج أنّ A صحيحة، وبالمثل لاختبار فرضية مثل "هذا التأثير حقيقي"، سنفترض مؤقتًا أنها خاطئة وهذه هي فرضية العدم، حيث نحسب احتمال التأثير الظاهر بناءً على هذا الافتراض وهذه هي القيمة الاحتمالية p-value، فإذا كانت هذه القيمة الاحتمالية منخفضةً، فسنستنتج أنه من المرجح أن تكون فرضية العدم صحيحة. الصنف HypothesisTest يزوِّدنا المستودع thinkstats2 بالصنف Hypothesis والذي يمثِّل بنية الطريقة الكلاسيكية في اختبار الفرضيات، إليك تعريفه كما يلي: class HypothesisTest(object): def __init__(self, data): self.data = data self.MakeModel() self.actual = self.TestStatistic(data) def PValue(self, iters=1000): self.test_stats = [self.TestStatistic(self.RunModel()) for _ in range(iters)] count = sum(1 for x in self.test_stats if x >= self.actual) return count / iters def TestStatistic(self, data): raise UnimplementedMethodException() def MakeModel(self): pass def RunModel(self): raise UnimplementedMethodException() يُعَدّ الصنف HypothesisTest صنفًا مجردًا وهو صنف أب أيضًا، حيث يزوِّدنا بتعريفات كاملة لبعض التوابع وتوابع نائبة عن أخرى place-keepers، كما ترث الأصناف الأبناء المبنية على الصنف HypothesisTest التابعين _init_ وPValue، كما تزوِّدنا بالتوابع TestStatistic وRunModel وقد تزودنا اختياريًا بالتابع MakeModel. يأخذ التابع _init_ البيانات بأي صيغة مناسبة، ويستدعي التابع MakeModel الذي يبني تمثيلًا لفرضية العدم، ومن ثم يمرر البيانات إلى التابع TestStatistics الذي يحسب حجم التأثير في العينة، كما يحسب التابع PValue احتمال التأثير الظاهر في ظل فرضية العدم، ويأخذ المتغير iters على أساس معامِل له، حيث يمثل هذا المتغير عدد مرات تكرار المحاكاة. يولد السطر الأول البيانات التي تمت محاكاتها ويحسب إحصائيات الاختبار test statistics ويخزنها في test_stats، حيث تكون النتيجة جزءًا من العناصر في test_stats مساويًا لإحصائية الاختبار المرصودة self.actual أو أكبر منها، ولنفترض مثلًا أننا رمينا قطعة نقود 250 مرة وظهر لنا الشعار 140 مرة وظهرت لنا الكتابة 110 مرات، فقد نشك أنّ العملة منحازة بناءً على هذه النتيجة أي منحازة لظهور الشعار، ولاختبار هذه الفرضية يمكننا حساب احتمال وجود مثل هذا الفرق إذا كانت قطعة النقود عادلة: class CoinTest(thinkstats2.HypothesisTest): def TestStatistic(self, data): heads, tails = data test_stat = abs(heads - tails) return test_stat def RunModel(self): heads, tails = self.data n = heads + tails sample = [random.choice('HT') for _ in range(n)] hist = thinkstats2.Hist(sample) data = hist['H'], hist['T'] return data يُعَدّ المعامِل data زوجًا من الأرقام الصحيحة يحوي عدد مرات ظهور الشعار والكتابة، كما تكون إحصائية الاختبار هي الفرق المطلق بينهما لذا فإن قيمة self.actual هي 30، في حين ينفِّذ التابع RunModel محاكاةً لرمي قطع النقود بافتراض أن القطعة عادلة، حيث يولِّد عينةً من 250 رمية ويستخدِم Hist لحساب عدد مرات ظهور الشعار والكتابة ويُعيد زوجًا من الأعداد الصحيحة، ولا يتعيّن علينا الآن سوى إنشاء نسخة من CoinTest واستدعاء التابع PValue كما يلي: ct = CoinTest((140, 110)) pvalue = ct.PValue() النتيحة هي 0.07 تقريبًا أي إذا كانت قطعة النقود عادلةً، فسنتوقَّع وجود فارق بحجم 30 حوالي 7‎‎%‎ من المرات، لذا كيف يمكننا تفسير هذه النتيجة؟ تكون 5‎‎%‎ هي عتبة الأهمية الإحصائية اصطلاحيًا، وإذا كانت القيمة الاحتمالية p-value هي أقل من 5‎‎%‎ يكون التأثير ذا دلالة إحصائية statistically significant وإلا لا يكون ذا دلالة إحصائية، لكن يُعَدّ اختيار ‎5‎‎%‎ اختيارًا اعتباطيًا، وكما سنرى لاحقًا فإن القيمة الاحتمالية تعتمد على اختيار الاختبار الإحصائي ونموذج فرضية العدم، لذلك لا يجب افتراض أنّ القيم الاحتمالية هي مقاييس دقيقة. نوصي بتفسير القيم الاحتمالية وفقًا لحجمها، أي إذا كانت القيمة الاحتمالية أقل من 1‎%‎ فمن غير المرجح أن يكون التأثير بمحض الصدفة؛ أما إذا كانت أكبر من ‎10%‎، فيمكننا القول أنّ التأثير ربما قد ظهر بمحض الصدفة؛ أما بالنسبة للقيم بين 1‎%‎ و10‎%‎ فلا يمكن عدّها حديةً، لذا فقد استنتجنا في هذا المثال أنّ البيانات لا تقدِّم دليلًا قويًا على انحياز العملة أو عدم انحيازها. اختبار الفرق في المتوسطين يُعَدّ الفرق بين متوسطي مجموعتين اثنتين من أكثر التأثيرات التي يجري اختبارها. حيث وجدنا في بيانات المسح الوطني لنمو الأسرة أنّ متوسط مدة حمل الأطفال الأوائل أكبر بقليل، ومتوسط الوزن عند ولادة الأطفال الأوائل أقل بقليل، وسنرى الآن فيما إذا كانت هذه التأثيرات ذات دلالة إحصائية، كما تتمثَّل فرضية العدم في هذه الأمثلة في تطابق توزيعي المجموعتين، وتتمثَّل إحدى طرق نمذجة فرضية العدم في التبديل permutation، وهو عبارة عن خلط قيم الأطفال الأوائل وبقية الأطفال ومعاملة المجموعتين على أنهما مجموعة واحدة كبيرة، وإليك الشيفرة الموافقة لذلك كما يلي: class DiffMeansPermute(thinkstats2.HypothesisTest): def TestStatistic(self, data): group1, group2 = data test_stat = abs(group1.mean() - group2.mean()) return test_stat def MakeModel(self): group1, group2 = self.data self.n, self.m = len(group1), len(group2) self.pool = np.hstack((group1, group2)) def RunModel(self): np.random.shuffle(self.pool) data = self.pool[:self.n], self.pool[self.n:] return data تُعَدّ data زوجًا من المتسلسلات، أي متسلسلة sequence لكل مجموعة، وتكون إحصائية الاختبار هي الفرق المطلق في المتوسطين؛ أما MakeModel فيسجل حجمي المجموعتين n وm ويجمعهما في مصفوفة نمباي NumPy واحدة باسم self.pool، كما يحاكي RunModel فرضية العدم عن طريق خلط القيم المجمَّعة وتقسيمها إلى مجموعتين بحيث يكون حجم الأولى n والثانية m، وكما هو الحال دائمًا تكون القيمة التي يعيدها التابع RunModel بصيغة البيانات المرصودة نفسها، وإليك شيفرة اختبار الفرق في مدة الحمل كما يلي: live, firsts, others = first.MakeFrames() data = firsts.prglngth.values, others.prglngth.values ht = DiffMeansPermute(data) pvalue = ht.PValue() يقرأ التابع MakeFrames بيانات المسح الوطني لنمو الأسرة ويعيد أُطر بيانات DataFrames تمثِّل جميع الولادات الحية للأطفال الأوائل وبقية الأطفال، حيث نستخرِج مدة حالات الحمل على شكل مصفوفات نمباي NumPy ونمررها على أساس بيانات إلى DiffMeansPermute ثم نحسب القيمة الاحتمالية p-value، وتكون النتيجة 0.17 تقريبًا أي أننا نتوقع رؤية فارقًا حجمه بحجم التأثير المرصود حاولي 17‎%‎ من المرات، لذا نستنتج أنه ليس للتأثير دلالةً إحصائيةً. يوضِّح الشكل السابق دالة التوزيع التراكمي CDF للفرق في متوسط الحمل في ظل فرضية العدم. يزودنا الصنف HypothesisTest بالتابع PlotCdf الذي يرسم توزيع إحصائية الاختبار وكذلك فهو يرسم خطًا رماديًا يدل على حجم التأثير المرصود. ht.PlotCdf() thinkplot.Show(xlabel='test statistic', ylabel='CDF') يُظهِر الشكل السابق النتيجة، حيث تتقاطع دالة التوزيع التراكمي مع الفرق المرصود عند 0.83 وهو مكمِّل complement القيمة الاحتمالية وهي 0.17. إذا نفَّذنا التحليل نفسه لأوزان الولادات حيث أن القيمة الاحتمالية المحسوبة هي 0، فلن تسفر المحاكاة بعد 100 محاولة عن تأثير بحجم الفرق المرصود أبدًا، فالفرق المرصود هو 0.12 رطلًا أي ما يعادل حوالي 0.05 كيلوغرامًا، ونقول عندها أنّ القيمة الاحتمالية أصغر من 0.001 أي p<0.001 ونستنتج أنّ الفرق في أوزان الولادات ذو دلالة إحصائية statistically significant. إحصائيات اختبار أخرى يعتمد اختيار أفضل إحصائية اختبار على السؤال الذي نحاول تناوله، فإذا كان السؤال المطروح مثلًا هو عن كَون مدة حمل الأطفال الأوائل مختلفةً عن بقية الأطفال فمن المنطقي حساب الفرق المطلق في المتوسطين كما فعلنا في القسم السابق، وإذا كنا نعتقد لسبب ما أنه من المرجح أن تتأخر ولادة الأطفال الأوائل، نستخدم إحصائية الاختبار بدلًا من القيمة المطلقة للفرق، وإليك إحصائية الاختبار كما يلي: class DiffMeansOneSided(DiffMeansPermute): def TestStatistic(self, data): group1, group2 = data test_stat = group1.mean() - group2.mean() return test_stat يرث الصنف DiffMeansOneSided التابعين MakeModel وRunModel من DiffMeansPermute والفرق الوحيد هو أن TestStatistic لا يأخذ القيمة المطلقة للفرق، كما يندرج هذا الاختبار تحت نوع الاختبارات أحادية الجانب one-sided لأن هذا الاختبار يحسب جانبًا واحدًا فقط من توزيع الفروق، في حين يستخدِم الاختبار السابق الجانبين، لذا فهو ثنائي الجانب two-sided. تكون القيمة الاحتمالية في هذه النسخة من الاختبار هي 0.09، في حين تكون القيمة الاحتمالية للاختبار أحادي الجانب عمومًا هي حوالي نصف القيمة الاحتمالية للاختبار ثنائي الجانب وذلك اعتمادًا على شكل التوزيع، وتُعَدّ الفرضية أحادية الجانب التي تقول أنّ الأطفال الأوائل يولدون متأخرين هي أكثر دقة من الفرضية ثنائية الجانب، لذا تكون القيمة الاحتمالية أصغر، لكن ليس للفرق أيّ دلالة إحصائية حتى بالنسبة للفرضية الأقوى. يمكننا استخدام إطار العمل ذاته لاختبار وجود فرق في الانحراف المعياري، وقد رأينا في قسم سابق في مقال دوال الكتلة الاحتمالية في بايثون دليلًا على أنه من المرجح ولادة الأطفال الأوائل مبكرين أو متأخرين وأقل احتمالًا أن يولدوا في الوقت المحدد، لذا قد نفترض أنّ الانحراف المعياري أعلى، وإليك الشيفرة التي تنفِّذ الاختبار كما يلي: class DiffStdPermute(DiffMeansPermute): def TestStatistic(self, data): group1, group2 = data test_stat = group1.std() - group2.std() return test_stat يُعَدّ هذا اختبارًا أحادي الجانب، إذ لا تقول الفرضية إن قيمة الانحراف المعياري للأطفال الأوائل مختلفة عن القيمة الاحتمالية فحسب بل تحدد أن قيمتها أعلى، حيث أن القيمة الاحتمالية هي 0.09 أي ليس لها دلالة إحصائية. اختبار الارتباط يمكن لإطار العمل هذا أن يختبر الارتباطات أيضًا، ففي مجموعة بيانات المسح الوطني لنمو الأسرة مثلًا يكون الارتباط بين عمر الأم وبين أوزان الولادات هو حوالي 0.07، أي يبدو أن الأمهات الأكبر عمرًا يلدن أطفالًا أكثر وزنًا، لكن هل يمكن لهذا التأثير أن يكون بسبب الصدفة؟ استخدمنا ارتباط بيرسون لإحصائية الاختبار هذه، علمًا أن ارتباط سبيرمان مناسب أيضًا. حيث يمكن إجراء اختبار أحادي الجانب إذا توقَّعنا لسبب ما أن الارتباط موجب، لكن بما أنه لا يوجب سبب لنعتقد هذا سنُجري اختبارًا ثنائي الجانب باستخدام قيمة الارتباط المطلقة، وتقول فرضية العدم أنه لا يوجد ارتباط بين عمر الأم ووزن الولادة، وإذا خلطنا القيم المرصودة فيمكننا محاكاة عالَم يكون فيه توزيع عمر الأمهات مساويًا لوزن الولادات لكن لا تكون المتغيرات متعلقة ببعضها: class CorrelationPermute(thinkstats2.HypothesisTest): def TestStatistic(self, data): xs, ys = data test_stat = abs(thinkstats2.Corr(xs, ys)) return test_stat def RunModel(self): xs, ys = self.data xs = np.random.permutation(xs) return xs, ys يُعَدّ المتغير data زوجًا من المتسلسلات sequences، حيث يحسب TestStatistic القيمة المطلقة لارتباط بيرسون، أما RunModel فهو يخلط قيم المصفوفة xs ويعيد البيانات المُحاكاة، وإليك الشيفرة التي تقرأ البيانات وتنفِّذ الاختبار كما يلي: live, firsts, others = first.MakeFrames() live = live.dropna(subset=['agepreg', 'totalwgt_lb']) data = live.agepreg.values, live.totalwgt_lb.values ht = CorrelationPermute(data) pvalue = ht.PValue() استخدمنا dropna في هذه الشيفرة مع الوسيط subset لحذف الأسطر التي لا تحوي أحد المتغيرات التي نحتاجها، وتكون قيمة الارتباط الفعلي هي 0.07 والقيمة الاحتمالية المحسوبة هي 0 وبعد حوالي 1000 تكرار يكون أكبر ارتباط مُحاكى هو 0.04، لذا على الرغم من صغر الارتباط المرصود إلا أنه ذو دلالة إحصائية، كما يُعَدّ هذا المثال بمثابة تذكير بأنّ كَون التأثير ذا دلالة إحصائية فلا يعني دائمًا أنه مهم في الناحية العملية، وإنما يعني فقط أنه من غير المرجح أن يكون قد ظهر بمحض الصدفة فقط. اختبار النسب لنفترض أنك تدير منتدى ترفيهي وشككت في يوم من الأيام أنك أحد الزبائن يستخدِم قطعة نرد ملتوية، أي أنه يُتلاعب بها لكي يكون احتمال ظهور أحد الوجوه أكبر من احتمال ظهور الوجوه الأخرى، لذا تقبض على المتهم وتصادر قطعة النرد لكن يتعيّن عليك عندها إثبات غشه، حيث ترمي قطعة النرد 60 مرة وتحصل على النتائج الموضحة بالجدول التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } القيمة 1 2 3 4 5 6 التردد (عدد مرات التكرار) 8 9 19 5 8 11 نتوقع ظهور كل قيمة 10 مرات وسطيًا، وفي مجموعة البيانات هذه تظهر القيمة 3 أكثر من المتوقع وتظهر القيمة 4 أقل من المتوقع، لكن هل لهذه الفروق دلالة إحصائية؟ يمكننا اختبار هذه الفرضية عن طريق حساب التردد المتوقع لكل قيمة، والفرق بين الترددات المتوقعة والترددات المرصودة والفرق المطلق الكلي، بحيث نتوقع في هذا المثال ظهور كل وجه 10 مرات من أصل 60 مرة، وتكون الانحرافات عن هذا التوقع هي: 2- و1- و9 و5- و2- و1، لذا فإن الفرق المطلق الكلي هو 20، إذًا كم مرة سنرى مثل هذا الاختلاف صدفةً؟ إليك نسخة من الصنف HypothesisTest تجيبنا عن هذا السؤال كما يلي: class DiceTest(thinkstats2.HypothesisTest): def TestStatistic(self, data): observed = data n = sum(observed) expected = np.ones(6) * n / 6 test_stat = sum(abs(observed - expected)) return test_stat def RunModel(self): n = sum(self.data) values = [1, 2, 3, 4, 5, 6] rolls = np.random.choice(values, n, replace=True) hist = thinkstats2.Hist(rolls) freqs = hist.Freqs(values) return freqs تُمثَّل هذه البيانات على صورة قائمة من الترددات حيث أن القيم المرصودة هي: [8‎, 9, 19, 5, 8, 11] والترددات المتوقعة هي 10 لكل القيم، كما أن إحصائية الاختبار هي مجموع الفروق المطلقة؛ أما فرضية العدم فتقول أن قطعة النرد عادلة، لذا سنحاكي هذه الفرضية عن طريق أخذ عينات عشوائية من القيم values، ويستخدِم التابع RunModel الصنف Hist لحساب وإعادة قائمة الترددات، كما تكون القيمة الاحتمالية للبيانات هي 0.13 أي أنه إذا كانت قطعة النرد عادلةً، فسنتوقع ظهور الانحراف الكلي المرصود أو قيمة انحراف أكبر من المتوقعة لتكون حوالي ‎13% من المرات، لذا ليس للتأثير الظاهر دلالة إحصائية. اختبارات مربع كاي استخدمنا في القسم السابق الانحراف الكلي على أساس اختبار إحصائي، لكن يشيع استخدام إحصائية مربع كاي لاختبار النسب، وتكون الصيغة الرياضية لمربع كاي كما يلي: χ2 = ∑ i (Oi − Ei)2 Ei حيث Oi هي الترددات المرصودة وEi هي الترددات المتوقعة، وفيما يلي شيفرة بايثون الموافقة: class DiceChiTest(DiceTest): def TestStatistic(self, data): observed = data n = sum(observed) expected = np.ones(6) * n / 6 test_stat = sum((observed - expected)**2 / expected) return test_stat يعطي تربيع الانحرافات -بدلًا من أخذ القيم المطلقة- وزنًا أكبر للانحرافات الكبيرة، إذ يوحِّد standardizes التقسيم على expected الانحرافات على الرغم أنه ليس لها في هذه الحالة أثر لأن الترددات المتوقَّعة متساوية فيما بينها، وتكون القيمة الاحتمالية في حال استخدام إحصائية مربع كاي مساويةً لـ 0.04 وهي أصغر بكثير من 0.13 والتي حصلنا عليها عندما استخدمنا الانحراف الكلي، فإذا أخذنا العتبة 5‎‎%‎ على محمل الجد، فسننظر للتأثير على أنه ذو دلالة إحصائية، لكن يمكننا القول إذا أخذنا الاختبارَين بالحسبان فستكون النتائج حديةً borderline، وعلى الرغم أنه لن نستبعد احتمال أن يكون النرد ملتويًا، إلا أننا لن نُدين المتهم بالغش. يوضِّح هذا المثال نقطة مهمة وهي اعتمادية القيمة الاحتمالية على اختيار إحصائية الاختبار وعلى اختيار نموذج فرضية العدم، وفي بعض الأحيان تحدِّد هذه الاختيارات ما إذا كان التأثير ذا دلالة إحصائية أم لا، وللمزيد من المعلومات حول اختبار مربع كاي يمكنك زيارة صفحة ويكيبيديا. عودة إلى الأطفال الأوائل ألقينا نظرة في بداية المقال على مدة حالات الحمل للأطفال الأوائل وبقية الأطفال واستنتجنا أن الفروق الظاهرة في المتوسط mean والانحراف المعياري standard deviation ليست ذا دلالة إحصائية، لكن رأينا في قسم سابق في مقال دوال الكتلة الاحتمالية في بايثون فروقًا واضحةً في توزيع مدة الحمل خاصةً في المجال بين 35 إلى 43 أسبوع، كما يمكننا استخدام اختبار مبني على إحصائية مربع كاي لنتحقق فيما إذا كانت هذه الفروق ذات دلالة إحصائية أم لا، وتجمع الشيفرة هذه عدة عناصر من الأمثلة السابقة: class PregLengthTest(thinkstats2.HypothesisTest): def MakeModel(self): firsts, others = self.data self.n = len(firsts) self.pool = np.hstack((firsts, others)) pmf = thinkstats2.Pmf(self.pool) self.values = range(35, 44) self.expected_probs = np.array(pmf.Probs(self.values)) def RunModel(self): np.random.shuffle(self.pool) data = self.pool[:self.n], self.pool[self.n:] return data تُمثَّل هذه البيانات على أساس قائمتي مدة حمل، وفرضية العدم هي أنّ العينتين مأخوذتان من التوزيع ذاته، كما ينمذج التابع MakeModel التوزيع عن طريق تجميع العينتَين باستخدام hstack، ومن ثم يولِّد التابع RunModel البيانات المُحاكاة عن طريق خلط العينة المجمَّعة وقسمها إلى جزئين اثنين، كما يُعرِّف MakeModel المتغير values وهو مجال الأسابيع الذي سنستخدمه، والمتغير expected_probs وهو احتمال كل قيمة في التوزيع المجمَّع، وفيما يلي الشيفرة التي تحسب إحصائية الاختبار: # class PregLengthTest: def TestStatistic(self, data): firsts, others = data stat = self.ChiSquared(firsts) + self.ChiSquared(others) return stat def ChiSquared(self, lengths): hist = thinkstats2.Hist(lengths) observed = np.array(hist.Freqs(self.values)) expected = self.expected_probs * len(lengths) stat = sum((observed - expected)**2 / expected) return stat يحسب TestStatistics إحصائية مربع كاي للأطفال الأوائل وبقية الأطفال وتحسب مجموعها، في حين يأخذ التابع ChiSquared متسلسلةً من مدة الحمل ويحسب مدرَّجه التكراري histogram ويحسب observed التي هي قائمة من الترددات الموافقة للقيم self.values، كما يضرب ChiSquared الاحتمالات المحسوبة مسبقًا expected_probs بحجم العينة لحساب قائمة الترددات المتوقعة ويُعيد إحصائية مربع كايstat. تكون إحصائية مربع كاي الكليّة لبيانات المسح الوطني لنمو الأسرة هي 102 وليس لهذه القيمة معنى لوحدها، لكن بعد 1000 تكرار تكون قيمة أكبر إحصائية اختبار مولدة في ظل فرضية العدم 32، وبالتالي نستنتج أنه من غير المرجح أن تكون إحصائية مربع كاي مرصودةً في ظل فرضية العدم -أي من غير المرجح أن تكون مرصودة بافتراض أن فرضية العدم صحيحة-، لذا فإن التأثير الظاهر ذو دلالة إحصائية، كما يوضح هذا المثال وجود قيود على اختبارات مربع كاي، فهي تشير إلى وجود اختلاف بين المجموعتين، لكنها لا تذكر أي معلومة محددة حول ماهية الاختلاف. الأخطاء يكون التأثير ذا دلالة إحصائية في الطريقة الكلاسيكية في اختبار الفرضيات إذا كانت القيمة الاحتمالية أقل من عتبة معينة وغالبًا ما تكون ‎5%، كما يثير هذا النهج سؤالين هما: إذا كان التأثير قد ظهر صدفةً، فما هو احتمال أن نعده ذو دلالة إحصائية؟ يدعى هذا الاحتمال بمعدل السلبية الكاذبة false negative rate. إذا كان التأثير حقيقيًا، فما احتمال فشل اختبار الفرضية؟ يدعى هذا الاحتمال بمعدل الإيجابية الكاذبة false positive rate. من السهل نسبيًا حساب معدل الإيجابية الكاذبة، فهو 5‎‎%‎ إذا كانت العتبة 5‎%‎، وإليك السبب كما يلي: إذا لم يكن هناك تأثير حقيقي، فستكون فرضية العدم صحيحةً، لذا يمكننا حساب توزيع إحصائية الاختبار عن طريق محاكاة فرضية العدم، وندعو هذا التوزيع CDFT. نحصل على إحصائية اختبار t مأخوذة من CDFT في كل مرة ننفِّذ فيها تجربة، ومن ثم نحسب القيمة الاحتمالية التي هي احتمال تجاوز قيمة عشوائية مأخوذة من CDFT الإحصائية t، أي 1-CDFT(t). إذا كانت CDFT(t) أكبر من 95‎%‎، فستكون القيمة الاحتمالية أصغر من ‎5% وذلك إذا تجاوزت t المئين 95 -أي 95th percentile، وكم مرة تتجاوز قيمة مختارة ما من CDFT المئين 95؟ حوالي 5‎%‎ من المرات الكلية. لذا إن أدّيت اختبار فرضية واحدًا مع عتبة 5%، فستتوقع حصول إيجابية كاذبة مرة واحدة من كل 20 مرة. القوة يعتمد معدل السلبية الكاذبة على حجم التأثير الفعلي الذي لا يكون معلومًا عادةً، لذا فمن الصعب حسابه، لكن يمكننا حساب المعدل عن طريق حساب معدل مشروط بحجم تأثير افتراضي، فإذا افترضنا مثلًا أن الفارق المرصود بين المجموعتين دقيقًا، فيمكننا استخدام العينات المرصودة على أساس نموذج للسكان ومن ثم استخدام البيانات المُحاكاة من أجل تنفيذ اختبارات الفرضيات كما يلي: def FalseNegRate(data, num_runs=100): group1, group2 = data count = 0 for i in range(num_runs): sample1 = thinkstats2.Resample(group1) sample2 = thinkstats2.Resample(group2) ht = DiffMeansPermute((sample1, sample2)) pvalue = ht.PValue(iters=101) if pvalue > 0.05: count += 1 return count / num_runs يأخذ التابع FalseNegRate بيانات على صورة متسلسلتين بحيث يكون لكل مجموعة متسلسلة، إذ يحاكي التابع تجربةً في كل تكرار من الحلقة عن طريق سحب عينة عشوائية من كل مجموعة وتنفيذ اختبار فرضية، ومن ثم يتحقق التابع من النتيجة ويحسب عدد مرات السلبية الكاذبة، في حين يأخذ التابع Resample متسلسلةً ويسحب عينةً من الطول نفسه مع استبدال كما يلي: def Resample(xs): return np.random.choice(xs, len(xs), replace=True) إليك الشيفرة التي تختبر مدة حالات الحمل: live, firsts, others = first.MakeFrames() data = firsts.prglngth.values, others.prglngth.values neg_rate = FalseNegRate(data) تكون النتيجة حوالي 70‎%‎، أي نتوقع أن تؤدي تجربة بحجم العينة هذا إلى اختبار سلبي 70‎%‎ من المرات إذا كان الفرق الفعلي في متوسط مدة الحمل 0.078 أسبوعًا، لكن غالبًا ما تُقدَّم هذه النتيجة بالطريقة المعاكسة أي نتوقع أن تؤدي تجربة بحجم العينة هذا إلى اختبار إيجابي ‎30% من المرات إذا كان الفرق الفعلي في متوسط مدة الحمل 0.078 أسبوعًا، حيث يدعى معدل الإيجابية الصحيحة هذا بقوة power الاختبار، أو يدعى أحيانًا بحساسية sensitivity الاختبار، إذ تعكس هذه التسمية قدرة الاختبار على تحديد تأثير بحجم معين مُعطى سابقًا. كان احتمال أن يعطي الاختبار نتيجةً إيجابيةً في هذا المثال هو 30‎%‎ -في حال كان الفرق هو 0.078 أسبوعًا كما ذكرنا سابقًا-، وتقول القاعدة العامة أنه تُعَدّ قوة ‎80% قيمةً مقبولةً، لذلك يمكننا القول أنّ هذا الاختبار كان ضعيفًا أو يفتقر للقوة underpowered، وبصورة عامة لا يعني اختبار الفرضية السلبي negative hypothesis test أنه لا يوجد فرق بين المجموعات، وإنما يقترح أنه إذا كان هناك فارقًا، فهو صغير جدًا لتحديده باستخدام حجم العينة هذا. التكرار تُعَدّ عملية اختبار الفرضيات الموضحة في هذا المقال ليست ممارسة جيدة بالمعنى الدقيق للكلمة. أولًا، أدينا عدة اختبارات، حيث أنك إذا نفَّذت اختبار فرضية واحد، فسيكون احتمال ظهور إيجابية كاذبة هو 1 من 20 وقد يكون هذا مقبولًا، لكن إن أجريت 20 اختبارًا تتوقع ظهور إيجابية كاذبة مرة واحدة على الأقل في معظم الأوقات. ثانيًا، استخدمنا مجموعة البيانات ذاتها لعمليتي الاستكشاف والاختبار، وإذا استكشفتَ مجموعة بيانات ضخمة ووجدت تأثيرًا مفاجئًا ثم اختبرته لتعرف فيما إذا كان ذا دلالة إحصائية أم لا، فسيكون من المرجح توليد إيجابية كاذبة. يمكنك ضبط عتبة القيمة الاحتمالية للتعويض عن الاختبارات المتعددة، كما هو وارد في صفحة ويكيبيديا، أو يمكنك معالجة كلا المشكلتين عن طريق تقسيم البيانات باستخدام مجموعة للاستكشاف ومجموعة أخرى للاختبار، كما تكون بعض هذه الممارسات إجباريةً في بعض المجالات أو مستحسنَةً على الأقل، لكن من الشائع أيضًا معالجة هذه المشاكل ضمنيًا عن طريق تكرار النتائج المنشورة، وعادةً ما تُعَدّ الورقة البحثية الأولى التي تقدم نتيجة جديدة أنها استكشافية exploratory، كما تُعَدّ الأوراق اللاحقة التي تكرر النتيجة باستخدام بيانات جديدة أنها مؤكِدة confirmatory. صدف وأن أتُيحت لنا الفرصة لتكرار النتائج في هذا المقال، حيث أن النسخة الأولى من الكتاب مبنية على الدورة السادسة من المسح الوطني لنمو الأسرة التي صدرت عام 2002، لكن أصدرت مراكز السيطرة على الأمراض والوقاية منها CDC في الشهر 10 من عام 2010 بيانات إضافية مبنية على المقابلات التي أجريت بين عامي 2006-2010، كما يحتوي nsfg2.py على تعليمات برمجية لقراءة هذه البيانات وتنظيفها، حيث يكون في مجموعة البيانات الجديدة ما يلي: الفرق في متوسط مدة الحمل هو 0.16 أسبوع وله دلالة إحصائية وقيمته الاحتمالية أصغر من 0.001 أيp<0.001 موازنةً بفارق 0.078 أسبوع في مجموعة البيانات الأصلية. الفرق في وزن الولادة هو 0.17 رطل مع قيمة احتمالية أصغر من 0.001 أي p<0.001 موازنةً بفارق 0.12 رطل في مجموعة البيانات الأصلية. الارتباط بين وزن الولادة وعمر الأم هو 0.08 مع قيمة احتمالية أصغر من 0.001 أي p<0.001 موازنةً بفارق 0.07. اختبار مربع كاي فهو ذو دلالة إحصائية مع قيمة احتمالية أصغر من 0.001 أي p<0.001 كما كان في مجموعة البيانات الأصلية. باختصار، تكررت التأثيرات في مجموعة البيانات الجديدة والتي تمتعت بدلالة إحصائية في مجموعة البيانات الأصلية، وكذلك فإن الفرق في طول الحمل أكبر في مجموعة البيانات الجديدة وهو ذو دلالة إحصائية أيضًا مع أنه لم يكن ذا دلالة إحصائية في مجموعة البيانات الأصلية. تمارين يوجد حل التمارين في chap09soln.py. تمرين 1 تزداد قوة اختبار الفرضية كلما ازداد حجم العينة، أي أنه من المرجح أن يكون موجبًا إن كان التأثير حقيقيًا، والعكس صحيح فعندما ينقص حجم العينة يصبح من غير المرجح أن يكون الاختبار موجبًا حتى ولو كان التأثير حقيقيًا، وللتحقق من هذا الأمر نفِّذ الاختبارات التي ذكرناها في هذا المقال على مجموعات فرعية من بيانات المسح الوطني لنمو الأسرة، علمًا أنه يمكنك استخدام thinkstats2.SampleRows لاختيار مجموعة فرعية عشوائية من الأسطر في إطار بيانات. ماذا سيكون مصير القيم الاحتمالية لهذه الاختبارات عندما ينقص حجم العينة؟ وما هو أصغر حجم عينة ينتج عنه اختبار إيجابي؟ تمرين 2 أجرينا في قسم اختبار الفرق في المتوسطين محاكاةً لفرضية العدم عن طريق التبديل permutation، أي أننا عاملنا القيم المرصودة على أساس جمع السكان ومن ثم قسمنا الأشخاص إلى مجموعتين عشوائيًا، كما يوجد بديل آخر وهو استخدام العينة لتقدير توزيع السكان ومن ثم سحب عينة عشوائية من هذا التوزيع، وتدعى هذه العملية بإعادة أخذ عينات resampling، حيث أنه هناك عدة طرق لإعادة أخذ العينات، وإحدى أبسط هذه الطريق هي سحب عينة مع استبدال with replacement من القيم المرصودة كما فعلنا في قسم "القوة". اكتب صنفًا class باسم DiffMeansResample يرث من الصنف DiffMeansPermute وأعِد تعريف التابع RunModel لتنفيذ إعادة أخذ عينات resampling بدلًا من التبديل permutation، ثم استخدم هذا النموذج لاختبار الفروق في مدة الحمل ووزن الولادة، وما مدى تأثير النموذج على النتائج؟ ترجمة -وبتصرف- للفصل Chapter 9 Hypothesis testing analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون دوال الكثافة الاحتمالية في بايثون div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  7. توجد الشيفرة الخاصة بهذا المقال في ملف estimation.py. لعبة التقدير ستكون بداية هذا المقال مع لعبة، حيث سنعطيك توزيعًا ومهمتك هي تخمين ماهية هذا التوزيع، كما سنزوِّدك بتلميحَين هما أنّ التوزيع طبيعي، وأن هناك عينةً عشوائيةً منه: [-0.441, 1.774, -0.101, -1.138, 2.975, -2.138] برأيك ما هو معامل المتوسط μ لهذا التوزيع؟ يمكننا استخدام متوسط العينة x̄ على أساس تقدير لـ μ، حيث يكون متوسط العينة في هذا المثال هو 0.155 أي x̄=0.155، لذا من المنطقي قولنا أنّ μ=0.155، حيث تدعى هذه العملية بالتقدير estimation، وتُدعى الإحصائية التي استخدمناها -أي متوسط العينة- بالمقدِّر estimator. في الواقع يُعَدّ استخدام متوسط العينة لتقدير واضحًا لدرجة أنه من الصعب علينا تخيل وجود بديل منطقي، لكننا سنعدِّل اللعبة الآن عن طريق إضافة قيم شاذة outliers، ويكون التوزيع الجديد توزيعًا طبيعيًا أيضًا، وإليك عينةً جمَعها مسّاح غير موثوق به يضع الفاصلة العشرية في المكان الخطأ أحيانًا: [-0.441, 1.774, -0.101, -1.138, 2.975, -213.8] ما هو تقديرك لقيمة μ؟ إذا استخدمت متوسط العينة في عملية التقدير، فسيكون تخمينك هو 35.12-، لكن هل هذا هو الخيار الأفضل؟ وما هي البدائل؟ تُعَدّ عملية تحديد القيم الشاذة والتخلص منها ومن ثم حساب متوسط العينة لما تبقى هي إحدى هذه البدائل، كما يمكننا استخدام الوسيط median على أساس مُقدِّر، لكن يعتمد تحديد المُقدِّر الأفضل على الظروف مثل ما إذا كانت هناك قيم شاذة، وعلى الهدف مثل هل تحاول تقليل الأخطاء أو زيادة فرصتك في الحصول على الإجابة الصحيحة؟ يقلل متوسط العينة من متوسط الخطأ التربيعي mean squared error - أو MSE اختصارًا- إذا لم تكن هناك أيّ قيم شاذة، أي إذا لعبنا اللعبة ذاتها عدة مرات وحسبنا الخطأ x̄-μ، فسيقِل متوسط العينة. MSE = 1 m ∑(x − µ)2 حيث أنّ m هو عدد مرات لعبك للعبة التقدير، ومن المهم التفريق بينها وبين n حجم العينة المستخدَم في حساب x̄، وإليك الدالة التي تحاكي لعبة التقدير ومن ثم تحسب خطأ الجذر التربيعي المتوسط RSME وهو الجذر التربيعي لمتوسط الخطأ التربيعي: def Estimate1(n=7, m=1000): mu = 0 sigma = 1 means = [] medians = [] for _ in range(m): xs = [random.gauss(mu, sigma) for i in range(n)] xbar = np.mean(xs) median = np.median(xs) means.append(xbar) medians.append(median) print('rmse xbar', RMSE(means, mu)) print('rmse median', RMSE(medians, mu)) نؤكد على أنّ n هو حجم العينة وm هو عدد مرات لعب اللعبة، في حين يكون means قائمة التقديرات المبنية على x̄ ويكون medians قائمة وسطاء medians، وفيما يلي الدالة التي تحسب خطأ الجذر التربيعي المتوسط RSME: def RMSE(estimates, actual): e2 = [(estimate-actual)**2 for estimate in estimates] mse = np.mean(e2) return math.sqrt(mse) تُعَدّ estimates قائمة التقديرات وactual القيمة الفعلية التي يتم تقديرها، لكن في التطبيق العملي تكون قيمة actual غير معروفة، إذ لسنا بحاجة إلى تقديرها في حال كنا نعرفها مسبقًا، حيث أن الغرض من هذه التجربة هو موازنة أداء المقدِّرات. ظهرت نتيجة خطأ الجذر التربيعي المتوسط لمتوسط العينة 0.41 عندما نفّذنا هذه الشيفرة، مما يعني أنه إذا استخدمنا x̄ لتقدير متوسط هذا التوزيع بناءً على عينة تحوي 7 قيم أي n=7، فيجب علينا التوقع أن يكون الخطأ وسطيًا 0.41، وفي حال استخدام الوسيط median من أجل تقدير المتوسط فستنتج لدينا قيمة للخطأ الجذر التربيعي المتوسط، وهي 0.53، مما يعني أنه وعلى الأقل بالنسبة لهذا المثال ينتج عن x̄ خطأ الجذر التربيعي المتوسط الأدنى. يُعَدّ تقليل الخطأ التربيعي المتوسط MSE خاصيةً جيدةً لكنها ليست الاستراتيجية الأفضل في كل الأوقات، فلنفترض مثلًا أننا نقدِّر توزيع سرعات الرياح في موقع بناء، فإذا كان التقدير مرتفعًا جدًا، فقد نبالغ في بناء الهيكل مما يزيد من تكلفته، لكن إذا كان التقدير منخفضًا للغاية، فقد ينهار البناء بسبب عدم الحذر أثناء البناء، كما أنّ تقليل الخطأ التربيعي المتوسط MSE ليس أفضل استراتيجية ممكنة، وذلك لأن التكلفة في حال كانت دالة خطأ ليست متناظرةً. افترض أيضًا أننا ألقينا ثلاثة أحجار نرد سداسية الجوانب وطلبنا أن تتوقع مجموع الناتج الكلي، فإذا كان تقديرك صحيحًا تمامًا، فستحصل على جائزة؛ وإلا فلن تحصل على أيّ شيء، لذا تكون القيمة التي تقلل الخطأ التربيعي المتوسط MSE في هذه الحالة هي 10.5، لكن سيكون هذا تخمينًا سيئًا لأنه لا يمكن أن يكون مجموع الأرقام التي ظهرت على أحجار النرد الثلاثة 10.5 في أي حال من الأحوال، وبالنسبة لهذه اللعبة أنت تريد مُقدِّرًا لديه أعلى فرصة ليكون صحيحًا وهو مُقدِّر الاحتمال الأعظم maximum likelihood estimator -أو MLE اختصارًا-، فإذا اخترت 10 أو 11، فستكون فرصتك في الفوز هي 1 من 8 وهذا أفضل ما يمكنك الوصول إليه. خمن التباين إليك هذا التوزيع الطبيعي المألوف: [-0.441, 1.774, -0.101, -1.138, 2.975, -2.138] ما هي برأيك قيمة التباين σ2 الخاصة بالتوزيع السابق؟ بالطبع يُعَد الخيار الواضح هو استخدام تباين العينة S2 على أساس مُقدِّر estimator. S2 = 1 n ∑(xi − x)2 لكن يكون S2 مقدِّرًا مناسبًا بالنسبة للعينات الضخمة إلا أنه يميل إلى أن يكون منخفضًا جدًا بالنسبة للعينات الصغيرة، حيث تطلق عليه تسمية المقدِّر المتحيز biased بسبب هذه الخاصية المؤسفة، لكن يكون المقدِّر غير متحيز unbiased إذا كان الخطأ المتوقع الكلي -أو المتوسط- هو 0 وذلك بعد عدة تكرارات للعبة التقدير، وتوجد لحسن الحظ إحصائية بسيطة أخرى غير متحيزة للتباين σ2 كما يلي: Sn−12 = 1 n−1 ∑(xi − x)2 إذا كنت تريد شرحًا عن سبب تحيز S2 وبرهانًا على عدم تحيز Sn-12 ، فيمكنك الاطلاع على الانحياز المقدر. تتمثل أكبر مشكلة لهذا المُقدِّر في كون الاسم والرمز غير متناسقَين، حيث يمكن أن يشير الاسم "تباين العينة" إلى S2 أو Sn-12، كما أن فإن الرمز S2 يستخدَم للمصطلحين، وفيما يلي دالة تُحاكي لعبة التقدير وتختبر أداء كل من S2 وSn-12: def Estimate2(n=7, m=1000): mu = 0 sigma = 1 estimates1 = [] estimates2 = [] for _ in range(m): xs = [random.gauss(mu, sigma) for i in range(n)] biased = np.var(xs) unbiased = np.var(xs, ddof=1) estimates1.append(biased) estimates2.append(unbiased) print('mean error biased', MeanError(estimates1, sigma**2)) print('mean error unbiased', MeanError(estimates2, sigma**2)) يشير n إلى حجم العينة وm إلى عدد مرات لعب اللعبة، كما يحسب التابع np.var المقدار S2 اقتراضيًا، إلى جانب أنه يمكن أن يحسب Sn-12 إذا زُوِّد بالوسيط ddof=1 الذي يشير إلى "درجة حرية دلتا"، وعلى الرغم من أنه لن نشرح هذا المصطلح إلا أنه يمكنك معرفة تفاصيله عن طريق الاطلاع على صفحة درجة الحرية على ويكيبيديا. يحسب التابع MeanError متوسط الفرق بين التقديرات والقيمة الفعلية: def MeanError(estimates, actual): errors = [estimate-actual for estimate in estimates] return np.mean(errors) كان متوسط خطأ S2 عندما نفّذنا هذه الشيفرة هو -0.13، وكما هو متوقع، يميل المُقدِّر المتحيز إلى أن يكون منخفضًا للغاية، كما كانت قيمة Sn-12 هي 0.014 أي أقل بعشر مرات، وكلما ازدادت قيمة m توقعنا مقاربة متوسط خطأ Sn-12 من الصفر. تُعَدّ خاصتَي الخطأ التربيعي المتوسط MSE والتحيز bias والخواص المشابهة توقعات للمدى الطويل وهي مبنية على عدة تكرارات للعبة التقدير، بحيث يمكننا موازنة المُقدِّرات والتحقق فيما إذا كان لها خصائصًا مرغوبًا بها أم لا عن طريق تشغيل عمليات محاكاة مثل الموجودة في هذا المقال، لكنك لا تحصل إلا على تقدير واحد حينما تطبق المُقدِّر على البيانات الحقيقية، إذ لن يكون قولنا بأنّ التقدير غير متحيز ذا أهمية أو معنى، وذلك لأن خاصية عدم التحيز هي خاصية المُقدِّر لا التقدير، وتكون الخطوة التالية بعد اختيارك المُقدِّر الذي يمتلك الخصائص المناسبة لك واستخدامه لتوليد التقدير هي توصيف عدم استقرار التقدير الذي سنتناوله في القسم التالي. توزيعات أخذ العينات sampling distributions لنفترض أنك عالم تهتم بدراسة الغوريلا في محمية للحياة البرية مثلًا، وتريد معرفة متوسط وزن أنثى حيوان الغوريلا في هذه المحمية، لكن سيتوجب عليك تهدئتها أولًا، وهذا أمر خطير ومُكلف وقد يضر بصحة الحيوان نفسه، لكن إذا كان الحصول على المعلومات أمرًا هامًا، فقد يكون من المقبول وزن تسعة منها إذا افترضنا أنّ العدد الكامل للمحمية معلوم مسبقًا ويمكننا عندها اختيار عينة تمثيلية للإناث البالغات، كما يمكننا استخدام متوسط العينة x̄ لتقدير متوسط عدد الحيوانات المجهولين μ. قد تجد عند وزن 9 إناث أن x̄=90 kg وأن الانحراف المعياري للعينة هو S = 7.5 kg، حيث أن متوسط العينة هو مُقدِّر غير متحيز للمقدار μ ويقلل متوسط الخطأ التربيعي MSE على المدى الطويل، لذا إن كنت تريد الخروج بتقدير واحد يلخص النتيجة، فيمكنك اختيار القيمة 90 kg، لكن ما مدى ثقتك بهذا التقدير؟ إذا وزنت 9 إناث فقط n=9 من أصل عدد كبير، فقد لا يكون الحظ حليفك، فربما تكون قد اخترت أكثر الإناث وزنًا -أو أقلها وزنًا- عن طريق الصدفة وحسب. يُعرَف تباين التقدير الناتج عن الاختيار العشوائي بخطأ أخذ العينات sampling error، حيث يمكننا حساب خطأ أخذ العينات عن طريق محاكاة عملية أخذ العينات بقيم افتراضية للمقدارين σ وμ، ومن ثم مراقبة مدى تباين x̄، كما سنستخدِم تقديرات كلًا من x̄ وS لعدم معرفتنا القيم الفعلية للمقدارين σ وμ، لذا يكون السؤال المطروح هو إذا كانت القيمة الفعلية هي σ=90 kg وμ = 7.5 kg ونفّذنا التجربة عدة مرات، فكيف سيتغير المتوسط المُقدَّر x̄؟ إليك الدالة التي تجيب عن هذا السؤال: def SimulateSample(mu=90, sigma=7.5, n=9, m=1000): means = [] for j in range(m): xs = np.random.normal(mu, sigma, n) xbar = np.mean(xs) means.append(xbar) cdf = thinkstats2.Cdf(means) ci = cdf.Percentile(5), cdf.Percentile(95) stderr = RMSE(means, mu) تُعَدّ كلًا من mu وsigma قيمًا افتراضيةً للمعامِلات، وn هي حجم العينة أي عدد إناث الغوريلا التي وزناها، وm هي عدد المرات التي ننفِّذ فيها المحاكاة. يوضِّح الشكل السابق توزيع أخذ عينات x̄ مع مجال الثقة. نختار في كل تكرار n قيمةً من التوزيع الطبيعي مع المعامِلات المعطاة ونحسب متوسط العينة xbar وننفِّذ 1000 محاكاةً للتجربة، ثم نحسب توزيع التقديرات cdf، ويُظهر الشكل السابق النتيجة ويُدعى هذا التوزيع بتوزيع أخذ العينات sampling distributions للمُقدِّر، وهو يُظهر مدى تنوّع التقديرات إذا نفّذنا التجربة عدة مرات. تقترب قيمة متوسط توزيع أخذ العينات من القيمة الافتراضية للمقدار μ، مما يعني أن التجربة تعطي الإجابة الصحيحة وسطيًا، حيث أنّ أقل نتيجة بعد 1000 محاولة هي 82 كيلوغرامًا وأعلى قيمة هي 98 كيلوغرامًا، ويشير هذا المجال إلى احتمالية ابتعاد التقدير عن القيمة الحقيقية بمقدار 8 كيلوغرام. توجد طريقتان شائعتان لتلخيص توزيع أخذ العينات وهما: الخطأ المعياري standard error -أو SE اختصارًا-: هو مقياس لمدى توقعنا أن يكون التقدير بعيدًا عن القيمة الحقيقية وسطيًا، حيث نحسب الخطأ x̄-μ لكل تجربة محاكاة ومن ثم نحسب خطأ الجذر التربيعي المتوسط RSME، إذ تكون قيمة الخطأ في هذا المثال 2.5 كيلو غرامًا. lمجال الثقة confidence interval -أو IC اختصارًا-: هو مجال يتضمن جزءًا من توزيع أخذ العينات، أي مجال الثقة 90% مثلًا هو المجال من المئين رقم 5 5th percentile إلى المئين رقم 95 95th percentile؛ أما في هذا المثال فيكون مجال الثقة 90% هو (94 ,86) كيلوغرامًا. غالبًا ما تكون الأخطاء المعيارية ومجالات الثقة مصدرًا للالتباس كما يلي: غالبًا ما يحصل خلط بين الخطأ المعياري والانحراف المعياري، لذا تذكَّر أنّ الانحراف المعياري يصف التباين في عينة مُقاسة، حيث يكون الانحراف المعياري لوزن إناث الغوريلا في هذا المثال هو 7.5 كيلو غرامًا؛ أما الخطأ المعياري فيصف التباين في التقدير، حيث يكون الخطأ المعياري للمتوسط بناءً على عينة من 9 قياسات هي 2.5 كيلو غرامًا، كما يمكنك تذكُّر الفرق بين المفهومين عن طريق حفظ القاعدة التي تقول أنه كلما ازداد حجم العينة، صغر الخطأ المعياري، على عكس الانحراف المعياري الذي لا يقل. غالبًا ما يعتقد الأشخاص أنه هناك احتمال بنسبة 90% لوقوع المعامِل الفعلي في مجال الثقة، لكن هذا ليس صحيحًا لأنه سيتوجب عليك استخدام التوابع البايزية -ويمكنك الاطلاع على كتابنا Think Bayes- في حال كنت تريد تقديم ادّعاءً من هذا القبيل، كما يجيب توزيع أخذ العينات عن سؤال آخر، فهو يمنحك معلومات عن مدى تغير التقدير إذا كررت التجربة، لذا تستطيع من خلاله معرفة ما إذا كان التقدير هذا موثوقًا أم لا. من المهم أن تتذكر أن مجالات الثقة confidence intervals والأخطاء المعيارية standard errors تحسب خطأ أخذ العينات sampling error فقط، أي أنها لا تحسب سوى الأخطاء الناتجة عن معاينة جزء من العدد الكلي وحسب، كما لا يأخذ توزيع العينات في الحسبان مصادر الخطأ الأخرى خاصةً تحيز أخذ العينات sampling bias وخطأ القياس measurement error وهما موضوعا القسم التالي. تحيز أخذ العينات sampling bias إذا افترضنا أنك تريد معرفة متوسط أوزان النساء في المدينة التي تعيش فيها بدلًا عن وزن الغوريلات في محمية طبيعية، فسيكون من غير المرجح السماح لك باختيار عينة تمثيلية من النساء وتسجيل أوزانهن، لذا ستحتاج إلى بديل وسيكون البديل البسيط هو أخذ العينات عن طريق المكالمة الهاتفية telephone sampling، أي يمكنك اختيار أرقام عشوائية من دليل الهاتف ومن ثم الاتصال وسؤال امرأة بالغة عن وزنها. تملك طريقة أخذ العينات عن طريق المكالمة الهاتفية بالطبع قيودًا وحدودًا واضحةً، أي تُعَدّ هذه الطريقة مثلًا محدودةً بالأشخاص الذين يملكون أرقامًا هاتفية ومسجلةً في دليل الهاتف، لذا فهي تستبعد الأشخاص الذين لا يملكون هواتفًا -أي مَن قد يكونوا أفقر من المتوسط- ومَن رقمه ليس مسجلًا -أي مَن قد يكونوا أثرى من المتوسط-، كما أنه إذا اتصلت على المنازل في النهار، فسيكون احتمال إيجاد الأشخاص الذين يمتلكون عملًا احتمالًا ضعيفًا إلى حد ما، وإذا لم تسأل سوى أول مَن يجيب على الهاتف، فسيكون احتمال أخذ عينة من الأشخاص الذين يشتركون معه في الهاتف ضعيفًا أيضًا. ستتأثر نتيجة المسح هذا بطريقة أو بأخرى إذا وُجدت بعض العوامل، مثل الدخل والحالة الوظيفية وعدد أفراد الأسرة التي تُعَدّ أمورًا متعلقةً بالوزن -ومن المنطقي أن تكون كذلك-، حيث تُدعى هذه المشكلة بتحيز أخذ العينات sampling bias لأنها خاصية من عملية أخذ العينات، كذلك فإنّ عملية أخذ العينات مُعرّضة لما يُعرَف بالاختيار الذاتي self-selection الذي يُعَدّ نوعًا من أنواع تحيز أخذ العينات. أخيرًا، قد تكون النتائج غير دقيقة في حال سألت الأشخاص عن وزنهم بدلًا من قيامك أنت بعملية الوزن، حيث سترى في أفضل الحالات -أي إذا كان المستجيبون متعاونِين- أنه قد يلجأ المشاركون إلى تقريب وزنهم من أقرب قيمة عليا صحيحة أو أقرب قيمة دنيا صحيحة إن كانت لديهم بعض المشاكل المتعلقة بالثقة أو عدم الراحة تجاه وزنهم الفعلي، فضلًا عن أنّ بعض المستجيبين لا يكونون متعاونِين كثيرًا، وتُعَدّ هذه الأخطاء في الدقة أمثلةً عن الخطأ في القياس measurement error، وفي حال كان تقريرك يحوي قيمةً مُقدَّرةً، فقد يكون من المفيد حساب الخطأ المعياري أو مجال الثقة أو كلاهما معًا، وذلك من أجل تحديد خطأ أخذ العينات sampling error، ولكن من المهم أيضًا التذكُّر أنّ خطأ أخذ العينات هو أحد مصادر الخطأ -أي ليس المصدر الوحيد-، وغالبًا لا يكون المصدر الأكبر. التوزيعات الأسية دعنا نلعب لعبة التقدير مرةً أخرى، بحيث يكون التوزيع هنا توزيعًا أسيًا، وإليك عينةً منه: [5.384, 4.493, 19.198, 2.790, 6.122, 12.844] برأيك ما هو معامِل λ الخاص بهذا التوزيع؟ تقول القاعدة إنه بصورة عامة فإن متوسط التوزيع الأسي هو 1/λ، لذا إذا عكسنا العملية فقد نختار: L=1/ x̄ يُعَدّ L مُقدِّرًا للـ λ كما أنه ليس مقدرًا عاديًا بل هو مُقدِّر الاحتمال الأعظم maximum likelihood estimator -أو MLE اختصارًا-، ويمكنك قراءة المزيد من المعلومات عنه في صفحة الويكيبيديا، فإذا كنت ترغب بزيادة فرصتك إلى أعلى حد ممكن في تخمين λ تخمينًا دقيقًا، فعليك اللجوء إلى L، لكننا نعلم أنه في حال وجود قيم شاذة، فلن يكون x̄ متينًا؛ لذا من المتوقع أن يكون للمقدر L المشكلة ذاتها، كما يمكننا اختيار بديل بناءً على وسيط العينة sample median، حيث أن الصيغة الرياضية لوسيط التوزيع الأسي هو ln(2)/λ، لذا إذا عكسنا العملية، فيمكننا تعريف مُقدِّر كما يلي: Lm=ln(2)/m حيث m وسيط العينة sample median. يمكننا إجراء محاكاة لعملية أخذ العينات إذا أردنا اختبار أداء هذه المُقدِّرات، وإليك الشيفرة الموافقة كما يلي: def Estimate3(n=7, m=1000): lam = 2 means = [] medians = [] for _ in range(m): xs = np.random.exponential(1.0/lam, n) L = 1 / np.mean(xs) Lm = math.log(2) / thinkstats2.Median(xs) means.append(L) medians.append(Lm) print('rmse L', RMSE(means, lam)) print('rmse Lm', RMSE(medians, lam)) print('mean error L', MeanError(means, lam)) print('mean error Lm', MeanError(medians, lam)) كان خطأ الجذر التربيعي المتوسط RMSE للمقدر L هو 1.1 عندما نفَّذنا هذه التجربة من أجل λ=2 ؛ أما بالنسبة للمُقدِّر Lm المبني على الوسيط median-based، فإن خطأ الجذر التربيعي المتوسط RMSE هو 1.8، وفي الواقع لا يمكننا الاستنتاج من هذه التجربة ما إذا كان L يقلل من الخطأ التربيعي المتوسط MSE أم لا، لكن يبدو لنا أن L هو على الأقل أفضل من Lm، لكن لسوء الحظ يبدو أنّ المُقدِّران متحيزان، حيث أن الخطأ المتوسط للـ L هو 0.33 والخطأ المتوسط للمقدر Lm هو 0.45، ولا يتقارب أيّ منهما إلى الصفر مع ازدياد قيمة m، وبالتالي يتضح أنّ x̄ هي مُقدِّر غير متحيز لمتوسط التوزيع 1/λ، في حين L ليس مُقدِّرًا غير متحيز لـ λ. التمارين قد يكون من المفيد لك أخذ نسخة من الملف estimation.py على أساس نقطة انطلاق لهذه التمارين، مع العلم أنّ الحلول موجودة في chap08soln.py. تمرين 1 استخدمنا في هذا المقال كلًا من x̄ والوسيط من أجل تقدير μ، ووجدنا أنّ الخطأ التربيعي المتوسط الأدنى ينتج عن x̄، كما استخدمنا S2 وSn-12 لتقدير الانحراف المعياري σ ووجدنا أنّ S2 متحيز وأنّ Sn-12 غير متحيز، لذا وانطلاقًا من هذا نفِّذ بعض التجارب المماثلة لترى فيما إذا كان x̄ والوسيط هي تقديرات متحيزة للمتوسط μ، وتحقق فيما إذا كان ينتج عن S2 أو Sn-12 خطأً تربيعيًا متوسطًا أدنى أم لا. تمرين 2 لنفترض أنك رسمت عينةً تحوي 10 قيم أي n=10 وبتوزيع أسي، بحيث يكون λ=2. نفِّذ محاكاةً لهذه التجربة 1000 مرة وارسم توزيع أخذ العينات للتقدير L، واحسب الخطأ المعياري للتقدير ومجال الثقة 90%، ثم كرر التجربة مع تغيير قيمة n بضع مرات، وارسم مخطط الخطأ المعياري مقابل n. تمرين 3 عادةً ما يكون الوقت بين الأهداف في رياضات مثل الهوكي وكرة القدم أسيًا، لذا يمكنك تقدير معدل تسجيل الفريق للأهداف عن طريق رصد عدد الأهداف التي يسجلونها في مباراة ما، حيث تختلف عملية التقدير هذه اختلافًا صغيرًا عن عملية أخذ عينات من الوقت بين الأهداف، لذا سنرى التطبيق العملي لهذا. اكتب دالةً تأخذ معدل تسجيل الأهداف lam والذي وحدة قياسه أهداف لكل مباراة، ثم حاكي مباراةً بتوليد الوقت بين الأهداف إلى أن يتخطى الوقت زمن مباراة واحدة، ومن ثم تُعيد هذه الدالة عدد الأهداف المسجلة، واكتب دالةً أخرى تُحاكي عدة مباريات وتسجِّل تقديرات لـ lam، ثم تحسب خطأ المتوسط وخطأ الجذر التربيعي المتوسط RMSE. برأيك هل تُعَدّ هذه الطريقة في إنشاء تقدير متحيزة؟ ارسم توزيع أخذ العينات sampling distribution الذي يحوي التقديرات ومجال الثقة 90%؛ ما هو الخطأ المعياري في هذه الحالة؟ وكيف تؤثر زيادة قيم lam على خطأ أخذ العينات sampling error؟ ترجمة -وبتصرف- للفصل Chapter 8 Estimate analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا المقال السابق: العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون المقال التالي: اختبار الفرضيات الإحصائية البرمجة بلغة بايثون div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  8. تناولنا في المقالات السابقة كل متغير على حدة وسنناقش العلاقات بين المتغيرات في هذا الفصل، حيث نقول عن متغيرين أنهما مرتبطان إذا استطعت استنباط معلومات عن أحدهما لمجرّد علمك بالمتغير الآخر، إذ يُعَدّ الطول والوزن مثلًا متغيرَين مرتبطَين، فعادةً ما يكون الأشخاص الأطول هم الأكثر وزنًا من غيرهم، لكنها بالطبع ليست علاقةً مثاليةً، إذ يوجد بعض الأشخاص الذين لا يتمتعون بطول كبير لكنّ وزنهم مرتفع، كما يوجد بعض الأشخاص النحيلين والطوال، لكن إذا حاولت تخمين وزن شخص ما، فستكون إجابتك أكثر دقةً إذا علمت طوله. يمكنك الحصول على الشيفرة الخاصة بهذا المقال في scatter.py في مستودع ThinkStats2 على GitHub. مخططات الانتشار Scatter plots تتمثل أسهل طريقة للتحقق مما إذا كان هناك علاقةً بين متغيرَين في إنشاء مخطط انتشار scatter plot، لكن رسم مخطط انتشار جيّد ليس بالمهمة السهلة. سنرسم مخطط الأوزان مقابل الأطوال للمستجيبين في نظام مراقبة عوامل المخاطر السلوكية BRFSS على أساس مثال على ذلك، كما يمكنك الاطلاع على قسم التوزيع اللوغاريتمي الطبيعي في مقال نمذجة التوزيعات Modelling distributions في بايثون. إليك الشيفرة التي تقرأ ملف البيانات وتستخرج الطول والوزن: df = brfss.ReadBrfss(nrows=None) sample = thinkstats2.SampleRows(df, 5000) heights, weights = sample.htm3, sample.wtkg2 يختار التابع SampleRows مجموعةً جزئيةً عشوائيةً من البيانات كما يلي: def SampleRows(df, nrows, replace=False): indices = np.random.choice(df.index, nrows, replace=replace) sample = df.loc[indices] return sample يشير df إلى إطار البيانات DataFrame، في حين يشير nrows إلى عدد الأسطر المختارة، كما يُعَدّ replace متغيرًا بوليانيًا يخبرنا عما إذا كانت عملية أخذ العيّنات sampling ستكون مع الاستبدال أم لا، أي إذا كان بالإمكان اختيار الأسطر نفسها أكثر من مرة. تزوِّدنا thinkplot بالتابع Scatter الذي ينشئ مخططات انتشار، وفيما يلي الشيفرة الموافقة: thinkplot.Scatter(heights, weights) thinkplot.Show(xlabel='Height (cm)', ylabel='Weight (kg)', axis=[140, 210, 20, 200]) تظهر النتيجة الموجودة في الشكل التالي من الجهة اليسرى شكل العلاقة، وكما هو متوقع فإنّ الأشخاص الذين يتمتعون بطول عالٍ يميلون لأن يكونوا أكثر وزنًا. يوضِّح الشكل السابق مخططات الانتشار للأوزان مقابل الأطوال للمستجيبين BRFSS، مع العلم أنه غير عشوائي في الجهة اليسرى وعشوائي في الجهة اليمنى. لا يُعَدّ هذا أفضل تمثيل للبيانات لأنها محزَّمة ضمن أعمدة، وتكمن المشكلة في كون الأطوال مُقرّبة إلى أقرب بوصة inch ثم حُوِّلت إلى سنتيمترات، وبعدها قُرِّبت مرةً أخرى، وبالتالي ققد ضاعت بعض المعلومات بسبب العمليات السابقة. لا يمكننا استرجاع المعلومات المفقودة في الواقع، إلّا أنه يمكننا تقليل الأثر على مخططات الانتشار عن طريق عشوائية (أو قلقلة jittering) البيانات، أي إضافة ضجيج عشوائي عليها لعكس أثر التقريب. بما أنه طُبِّقَت عملية تقريب لأقرب بوصة على هذه القيم، فمن المحتمل أن تكون بعيدةً عن القيم الأصلية بمقدار 0.5 بوصة أو 1.3 سنتيمتر، وكذلك الأمر بالنسبة للأوزان التي يمكن أن تكون بعيدةً عن القيم الأصلية بمقدار 0.5 كيلوغرامًا. heights = thinkstats2.Jitter(heights, 1.3) weights = thinkstats2.Jitter(weights, 0.5) إليك تنفيذ التابع Jitter: def Jitter(values, jitter=0.5): n = len(values) return np.random.uniform(-jitter, +jitter, n) + values يمكن أن تنتمي القيم إلى أي تسلسل لكن ستكون النتيجة مصفوفة NumPy حتمًا. يُظهر الشكل السابق في الجهة اليمنى النتيجة، حيث تقلل العشوائية jittering الأثر المرئي للتقريب ويوضِّح شكل العلاقة، لكن من المهم الانتباه إلى وجوب اللجوء إلى قلقلة (عشوائية) البيانات في حال كنت تريد عرضها فقط، كما يجب الابتعاد عن استخدام البيانات العشواء (المُقلقلة) من أجل التحليل، ولكن مع ذلك فلا يُعَدّ التذبذب أفضل طريقة لتمثيل البيانات، حيث يوجد العديد من النقاط المتداخلة التي تخفي البيانات في الأجزاء الكثيفة من الشكل وتعطي تركيزًا غير متناسب على القيم الشاذة، ويُدعى هذا التأثير بالإشباع saturation. يوضِّح الشكل السابق مخطط الانتشار مع عشوائية وشفافية في الجهة اليسرى، ومخطط هيكسبين في الجهة اليمنى. يمكننا حل هذه المشكلة باستخدام المعامِل alpha الذي يجعل النقاط شفافةً إلى حد ما، وتكون التعليمة الموافقة كما يلي: thinkplot.Scatter(heights, weights, alpha=0.2) يُظهِر الشكل السابق في الجهة اليسرى النتيجة، حيث تبدو نقاط البيانات المتداخلة أقتم من غيرها، أي يتناسب القتامة darkness مع الكثافة. نرى في هذا المخطط تفصيلِين اثنين لم يظهرا سابقًا، حيث يكون التفصيل الأول هو عناقيد عمودية عند أطوال مختلفة، والتفصيل الثاني هو خط أفقي قريب من الوزن 90 كيلوغرام أو 200 رطل. بما أن هذه البيانات تستند إلى تقارير ذاتية بالأرطال، فمن المرجَّح أن بعض المستجيبين قد طبقوا عملية تقريب على القيم. عادةً ما تكون الشفافية مناسبةً لمجموعات البيانات متوسطة الحجم، إلا أنّ هذا الشكل لا يُظهر سوى أول 5000 سجل في BRFSS من أصل 414509 سجل. يُعَدّ مخطط هيكسبين hexbin plot أحد الخيارات المطروحة للتعامل مع مجموعات البيانات الأكبر حجمًا، فهو يقسم المخطط graph إلى صناديق سداسية، ويلوِّن كل صندوق بلون مختلف حسب عدد نقاط البيانات الموجودة فيه، كما توفِّر thinkplot التابع HexBin: thinkplot.HexBin(heights, weights) يُظهر الشكل السابق في الجهة اليمنى النتيجة، وتتمثل إحدى ميّزات مخطط هيكسبين في توضيح شكل العلاقة جيدًا، كما أنه فعّال في حالة مجموعات البيانات الكبيرة بالنسبة للزمن ولحجم الملف الذي يولده، إلّا أنه لا يُظهر القيم الشاذة. استعرضنا هذا المثال لهدف أساسي ألا وهو توضيح عدم سهولة إنشاء مخطط انتشار يوضِّح العلاقات دون ظهور عناصر مضلّلة. توصيف العلاقات تُعطينا مخططات الانتشار انطباعًا عامًا عن العلاقة بين المتغيرات، إلّا أنه يوجد أنواع أخرى من المخططات التي تزودنا بمعلومات أكثر تفصيلًا عن طبيعة العلاقة، إذ يمكننا مثلًا فرز أو تصنيف متغير واحد ورسم مئين percentile المتغير الآخر. تزوّدنا مكتبتي نمباي NumPy وبانداز pandas بدوال لتصنيف البيانات binning data كما يلي: df = df.dropna(subset=['htm3', 'wtkg2']) bins = np.arange(135, 210, 5) indices = np.digitize(df.htm3, bins) groups = df.groupby(indices) تحذف dropna الأسطر التي تحوي قيمة nan -أي ليس عددًا- في أيّ عمود مُدرَج، وتنشئ الدالة arange مصفوفة نمباي NumPy تحوي صناديق bins من 135 إلى 210 دون احتساب القيمة 210 بفارق 5 بين الصندوق والآخر، كما تحسب digitize فهرس الصندوق الذي يحوي كل قيمة في df.htm3، وتكون النتيجة مصفوفة نمباي NumPy من فهارس الأعداد الصحيحة، بحيث تكون فهارس القيم التي تقل عن أصغر صندوق هي 0، في حين تكون فهارس القيم التي تزيد عن أعلى صندوق هي len(bins). يوضِّح الشكل السابق قيم المئين للوزن بمجال صناديق الأطوال. يُعيد تابع من إطار البيانات groupby كائن GroupBy وهو يُستخدَم في حلقة for؛ أما التابع groups فهو يمر بالترتيب على أسماء المجموعات في أُطُر البيانات التي تمثّلها، أي يمكننا مثلًا طباعة عدد الأسطر في كل مجموعة بالصورة التالية: for i, group in groups: print(i, len(group)) يمكننا الآن حساب متوسط الطول لكل مجموعة بالإضافة إلى دالة التوزيع التراكمي للوزن: heights = [group.htm3.mean() for i, group in groups] cdfs = [thinkstats2.Cdf(group.wtkg2) for i, group in groups] يمكننا أخيرًا رسم قيم المئين للوزن مقابل الطول كما يلي: for percent in [75, 50, 25]: weights = [cdf.Percentile(percent) for cdf in cdfs] label = '%dth' % percent thinkplot.Plot(heights, weights, label=label) يُظهر الشكل السابق النتيجة، حيث تكون العلاقة بين المتغيرات من 140 إلى 200 سنتيمتر خطيّةً تقريبًا، حيث يحوي هذا المجال أكثر من نسبة 99% من البيانات، لذا ليس علينا القلق بشأن القيم المتطرفة. الارتباط Correlation الارتباط هو إحصائيّة هدفها تحديد قوة العلاقة بين متغيرَين، لكن في أغلب الأحيان تختلف واحدات قياس الارتباط بين المتغيرات التي نرغب بموازنتها، مما يخلق لنا تحديًا يواجهنا عند قياس الارتباط، وفي الواقع يكون مصدر هذه المتغيرات غالبًا توزيعات مختلفة حتى عندما تمتلك واحدت القياس نفسها. فيما يلي بعض الحلول الشائعة لهذه المشاكل: حوِّل كل قيمة إلى درجة معيارية standard score التي هي عدد الانحرافات المعيارية عن المتوسط، حيث ينتج عن هذا التحويل "معامل ارتباط بيرسون الناتج عن العزوم". حوِّل كل قيمة إلى رتبتها rank التي هي الفهرس الخاص بها في القائمة المرتبة من القيم، حيث ينتج عن هذا التحويل مُعامل ارتباط سبيرمان Spearman rank correlation coefficient. إذا كانت X سلسلةً series من n قيمة وكل قيمة فيها هي xi، فسيمكننا تحويلها إلى درجاتها المعيارية عن طريق طرح المتوسط منها والتقسيم على الانحراف المعياري، بحيث تكون المعادلة بالصورة: zi=(xi-μ)/σ ، مع العلم أنّ البسط هو انحراف المسافة عن المتوسط، وتؤدي القسمة على σ إلى تقييس الانحراف standardizes the deviation، وبالتالي تكون قيم Z بلا أبعاد -أي ليس لها واحدات قياس- ويكون متوسط توزيعها مساوويًا للصفر وتباينه مساويًا للواحد. إذا كان توزيع قيم X طبيعيًا، فسيكون توزيع قيم Z طبيعيًا أيضًا، لكن إذا كان X متجانفًا skewed أو يحوي قيمًا شاذةً، فسيكون Z مثل X، وفي هذه الحالات يكون استخدام رتب المئين percentile ranks أكثر متانةً، لكن إذا حسبنا متغيرًا جديدًا هو R بحيث يكون ri رتبة xi، فسيكون توزيع R موحَّدًا uniform من 1 إلى n بغض النظر عن ماهية توزيع X. التغاير Covariance يُعَدّ التغاير مقياسًا لمَيل المتغيرين إلى الاختلاف معًا بحيث إذا كان لدينا سلسلتين X وY، فسيكون انحرافهما عن المتوسط كما يلي: dxi=xi-x̄ dyi=yi-ȳ بحيث تكون x̄ متوسط عيّنة X وȳ هي متوسط عيّنة Y، وإذا كانت العيّنتان X وY متغايرتان معًا، فسيملك انحرافهما الإشارة ذاتها. إن ضربنا انحرافي العيّنتين ببعضهما، فسيكون الناتج موجبًا في حال كان لهما الإشارة ذاتها، في حين سيكون سالبًا إذا كان لهما إشارة متعاكسة، لذا يمكننا القول أنّ جمع النواتج يعطي قياسًا لميل العيّنتين للتغاير معًا. يكون التغاير هو متوسط هذه النواتج: Cov(X,Y) = 1 n ∑dxi dyi حيث يكون n طول السلسلتين ويجب أن يكون لهما الطول نفسه. إذا درستَ الجبر الخطي، لا بد أنّك تدرك أن Cov هي حاصل الضرب القياسي dot product للانحرافات مقسومًا على طولها، لذا يكون التغاير في حده الأقصى إذا كان المتجهان متطابقَين تمامًا، و0 إذا كانا متعامدين، وسالبًا إذا أشارا إلى اتجاهين متعاكسين. تزوِّدنا thinkstats2 بالتابع np.dot لتنفيذ Cov تنفيذًا فعالًا، وإليك الشيفرة الموافقة لذلك: def Cov(xs, ys, meanx=None, meany=None): xs = np.asarray(xs) ys = np.asarray(ys) if meanx is None: meanx = np.mean(xs) if meany is None: meany = np.mean(ys) cov = np.dot(xs-meanx, ys-meany) / len(xs) return cov يحسب Cov في الحالة الافتراضية الانحرافات من متوسطات العيّنة، أو بإمكانك تزويده بالمتوسطات المعلومة. إذا كانتا xs وys تسلسلي بايثون، فسيحوِّلهما التابع np.asarray إلى مصفوفتي نمباي NumPy، وبطبيعة الحال لا يغيّر np.asarray شيئًا إذا كانتا xs وys مصفوفتي نمباي NumPy. تقصّدنا أن يكون تنفيذ التغاير هذا بسيطًا لأنّ هدفنا هو الشرح فحسب، كما توفر كل من نمباي NumPy وبانداز pandas أيضًا تنفيذات للتغاير لكن كلاهما يطبِّق تصحيحًا لأحجام العينات الصغيرة التي لم نحوِّلها بعد، كما تُعيد np.cov مصفوفة التغاير covariance matrix التي تكفينا الآن. ارتباط بيرسون Pearson’s correlation يُعَدّ التغاير مفيدًا في بعض الحسابات، ولكن نادرًا ما يتم وضعه في الإحصائيات الموجزة لأنه من الصعب تفسيره، فهو يعاني من بعض المشاكل منها أنّ واحدة قياسه هي ناتج واحدات X و Y. فمثلًا، يكون تغاير الطول والوزن في BRFSS هو 113 كيلوغرام-سنتيمترات على الرغم أنها ليست منطقية تمامًا. يمكن حل هذه المشكلة بعدة طرق منها تقسيم الانحرافات على الانحراف المعياري التي تحقق درجات معيارية وتحسب ناتج درجات معيارية كما يلي: pi = (xi − x) SX (yi − ȳ) SY يكون SX وSY الانحرافَين المعياريَين لكل من X وY، كما يكون متوسط هذه النواتج كما يلي: ρ = 1 n ∑pi يمكننا إعادة كتابة ρ عن طريق أخذSX وSY في الحسبان لينتج لدينا المعادلة التالية: ρ = Cov(X,Y) SX SY سميت هذه القيمة بارتباط بيرسون Pearson’s correlation تيمّنًا بكارل بيرسون عالم الإحصاء الرائد، وفي الواقع فمن السهل حسابه وتفسيره أيضًا لأنّ الدرجات المعيارية وρ بلا أبعاد. إليك التنفيذ في thinkstats2: def Corr(xs, ys): xs = np.asarray(xs) ys = np.asarray(ys) meanx, varx = MeanVar(xs) meany, vary = MeanVar(ys) corr = Cov(xs, ys, meanx, meany) / math.sqrt(varx * vary) return corr يحسب MeanVar المتوسط والتباين بصورة فعّالة أكثر من الاستدعاء المنفصل للتابعين np.mean وnp.var. دائمًا ما يكون ارتباط بيرسون بين القيمتين 1- و 1+ متضمنًا هاتين القيمتين، وإذا كان ρ موجبًا فنقول أنّ الارتباط موجب ويعني هذا أنه إذا كانت قيمة أحد المتغيرَين عالية، فستكون قيمة الآخر عاليةً أيضًا؛ أما إذا كان ρ سالبًا فنقول أنّ الارتباط سالب ويعني هذا أنه إذا كانت قيمة أحد المتغيرَين عالية، فستكون قيمة الآخر منخفضةً. يشير حجم ρ إلى قوة الارتباط، حيث إذا كانت تساوي 1 أو 1- فسيكون المتغيرين مرتبطَين تمامًا، مما يعني أنه إذا كنت تعرف أحد المتغيرين فسيصبح بإمكانك تنبؤ الآخر بصورة صحيحة. على الرغم من أن معظم الارتباطات في العالم الحقيقي غير مثالية إلا أنها مفيدة، كما أنّ الارتباط بين الطول والوزن هو 0.51 والذي يُعَدّ ارتباطًا قويًا موازنةً بالمتغيرات المماثلة المتعلقة بالإنسان. العلاقات اللاخطية Nonlinear relationships قد نعتقد أنه لا يوجد علاقة بين المتغيرات إذا كان مُعامِل ارتباط بيرسون يقارب الصفر، إلا أنّ هذا ليس صحيحًا، حيث يقيس ارتباط بيرسون العلاقات الخطية فقط، وفي حال وجود علاقة لاخطية فلا يقيس ρ قوتها قياسًا صحيحًا. يوضِّح الشكل السابق أمثلةً عن مجموعات البيانات مع مجال متنوع من الارتباطات فيما بينها، ومصدر هذا الشكل من صفحة Correlation، حيث يُظهِر مخططات الانتشار ومعامِلات الارتباطات لعدة مجموعات بيانات مبنيّة بعناية. يُظهِر الصف العلوي العلاقات الخطية مع مجال من الارتباطات، علمًا أنه يمكنك استخدام هذا الصف لمعرفة كيف تبدو القيم المختلفة لـ ρ؛ أمّا الصف الثاني فيُظهر ارتباطات مثاليّة مع مجال متنوع من قيم الميل، مما يشير إلى أنّ الارتباط لا علاقة له بالميل -وسنتحدث عن تقدير الميل قريبًا-، كما يُظهِر الصف الثالث متغيرات مرتبطة ارتباطًا واضحًا، ولكن معامِل الارتباط هو 0 نظرًا لكَون العلاقة لاخطيةً. يمكننا القول أنّ المغزى من هذه القصة هو أنه عليك الاطلاع على مخطط انتشار بياناتك قبل حساب معامِل الارتباط دون الانتباه لأي شيء. معامِل ارتباط سبيرمان حسب الرتب يؤدي مُعامِل بيرسون عمله جيّدًا إن كانت العلاقات بين المتغيرات خطية وإذا كانت المتغيرات طبيعيةً إلى حد ما، إلا أنه ليس متينًا في حال وجود قيم شاذة. يعَدّ معامِل ارتباط سبيرمان حسب الرتب Spearman’s rank correlation بديلًا يخفف من تأثير القيم الشاذة والتوزيعات المتجانفة skewed distributions، إذ يمكننا حساب ارتباط سبيرمان عن طريق حساب رتبة كل قيمة والتي هي فهرس القيمة في العيّنة المرتّبة. فمثلًا، لدينا فتكون رتبة القيمة 5 في العيّنة [7, 5, 2, 1] هي 3 لأن ترتيبها في القائمة المرتبة هو الثالث، ومن ثم نحسب ارتباط بيرسون لهذه الرُتب. تزودنا thinkstats2 بدالة تحسب معامِل ارتباط سبيرمان للرتب كما يلي: def SpearmanCorr(xs, ys): xranks = pandas.Series(xs).rank() yranks = pandas.Series(ys).rank() return Corr(xranks, yranks) حوَّلنا الوسائط arguments إلى كائنات سلسلة بانداز pandas Series لكي نستطيع استخدَام rank وهي تحسب رتبة كل قيمة وتُعيد سلسلةً، ومن ثم استخدمنا Corr لحساب ارتباط الرتب. كما يمكننا أيضًا استخدام Series.corr مباشرةً ومن ثم تحديد تابع سبيرمان كما يلي: def SpearmanCorr(xs, ys): xs = pandas.Series(xs) ys = pandas.Series(ys) return xs.corr(ys, method='spearman') مع العلم أن معامِل ارتباط سبيرمان للرتب لبيانات BRFSS هي 0.54، وهي أعلى بقليل من ارتباط بيرسون المساوية لـ 0.51، إذ يوجد هناك عدة أسباب وراء هذا الفرق منها: إذا كانت العلاقة غير خطية فعادةً ما يقلل ارتباط بيرسون من قوة العلاقة. يمكن أن يتأثر ارتباط بيرسون -في أيّ اتجاه- إذا كان أحد التوزيعين متجانفًا أو يحتوي على قيم متطرفة، حيث تُعَدّ معامِل ارتباط سبيرمان للرتب أكثر متانةً من ارتباط بيرسون. نعلم أنه في مثال BRFSS يكون توزيع الأوزان لوغاريتميًا طبيعيًا تقريبًا، أي أنه في حال كان التحويل لوغاريتميًّا فهو يقارب توزيعًا طبيعيًا لا تجانف فيه، كما يمكن أيضًا إلغاء أثر التجانف عن طريق حساب ارتباط بيرسون بتطبيق لوغاريتم على الوزن والطول، أي كما يلي: thinkstats2.Corr(df.htm3, np.log(df.wtkg2))) تكون النتيجة هي 0.53 وهي قريبة من معامِل ارتباط سبيرمان التي تقدر قيمتها بـ 0.54، أي أنها تفترض أن التجانف في توزيع الأوزان يفسّر أغلب الفروق بين ارتباط بيرسون وارتباط سبيرمان. الارتباط والسببية إذا كان المتغيران A وB مرتبطين فسيكون لدينا ثلاثة تفسيرات وهي أنّ A تسبب B أو B تسبب A أو مجموعة أخرى من العوامل تسبب كلاً من A وB، وتدعى هذه التفسيرات بالعلاقات السببية causal relationships. لا يُميِّز الارتباط لوحده بين هذه التفسيرات، أي أنها لا تعطينا فكرة عن أيّ منها هو الصحيح، وغالبًا ما تُلخَّص هذه القاعدة بما يلي: "الارتباط لا يقتضي السببية" وهو قول بليغ لدرجة أنّ له صفحة ويكيبيديا خاصة به. إذًا ماذا يمكنك أن تفعل لكي تقدِّم دليلًا على السببيّة؟ استخدِم زمنًا: إذا أتى المتغير A قبل B فيعني هذا أنّ A سبّب B وليس العكس -وهذا على الأقل حسب فهمنا الشائع للسببية-، حيث يساعدنا ترتيب الأحداث في استنتاج اتجاه السببية، لكنه لا يستبعد احتمال أن يتسبب شيء آخر في حدوث كل من A وB. استخدِم عشوائيةً: إذا قسمّتَ عينةً كبيرةً إلى مجموعتين عشوائيًا وحسبت متوسط أيّ متغير تقريبًا فستتوقع أن الفرق سيكون صغيرًا، وإذا كانت المجموعات متطابقةً تقريبًا في جميع المتغيرات ما عدا متغير واحد، فسيمكنك عندئذ استبعاد العلاقات الزائفة، وهذه الطريقة مناسبة حتى لو لم تعلم ما هي المتغيرات ذات الصلة، لكن من الأفضل أن تكون على علم بهذا لأنك تستطيع عندها التحقق فيما إذا كانت المجموعات متطابقةً أم لا. كانت هذه الأفكار هي الدافع وراء ما يُعرف بالتجربة العشوائية المنتظمة randomized controlled trial، التي يتم فيها إسناد المشاركِين إلى مجموعتين -أو أكثر-: مجموعة العلاج treatment group التي تتلقى علاجًا أو تدخّلًا من نوع ما مثل دواء جديد، ومجموعة الموازنة أو المجموعة المرجعية control group التي لا تتلقى أيّ علاج أو تتلقى علاجًا أثره معروف مسبقًا. تُعَدّ التجربة المنتظمة التي تستخدم عينات عشوائية الطريقة الأكثر موثوقية لإثبات العلاقة السببية، وهي أساس الطب القائم على العلم انظر إلى صفحة الويكيبيديا. لكن لسوء الحظ، فإن التجارب العشوائية المنتظمة ليست ممكنةً إلا في العلوم المختبرية والطب وعدد قليل من التخصصات الأخرى، حيث نادرًا ما تحدث في العلوم الاجتماعية لأنها مستحيلة أو غير أخلاقية. يتمثَّل أحد البدائل في البحث عن تجربة طبيعية natural experiment، حيث تتلقى مجموعات متشابهة علاجات مختلفة، وأحد مخاطر التجارب الطبيعية هو أنّ المجموعات قد تكون مختلفةً بطرق غير واضحة لنا، ويمكنك قراءة المزيد عن هذا الموضوع هنا. يمكننا في بعض الأحيان استنتاج العلاقات السببية باستخدام تحليل الانحدار regression analysis، وهو موضوع الفصل الحادي عشر. تمارين يوجد حل هذا التمرين في chap07soln.py في مستودع ThinkStats2 على GitHub. التمرين الأول استخدِم بيانات المسح الوطني لنمو الأسرة من أجل إنشاء مخطط انتشار لأوزان الولادات مقابل عمر الأم، ومن ثم ارسم قيم مئين أوزان الولادات مقابل عمر الأم، واحسب مُعامِل ارتباط بيرسون ومُعامِل ارتباط سبيرمان، وكيف تصف العلاقة بين هذه المتغيرات؟ ترجمة -وبتصرف- للفصل Chapter 7 Relationships between variables analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا تحليل البيانات الاستكشافية لإثبات النظريات الإحصائية دوال الكتلة الاحتمالية في بايثون التوزيعات الإحصائية في بايثون div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;}
  9. توجد الشيفرة الخاصة بهذا الفصل في ملف density.py في مستودع ThinkStats2 على GitHub. دوال الكثافة الاحتمالية probability density functions يمكن تعريف دالة الكثافة الاحتمالية probability density function -أو PDF اختصارًا- على أنها مشتق دالة التوزيع التراكمي cumulative distribution function -أو CDF اختصارًا-، وتكون الصيغة الرياضية على سبيل المثال لدالة الكثافة الاحتمالية الخاصة بالتوزيع الأسي exponential distribution هي: PDFexpo(x) = λ e−λ x كما تكون الصيغة الرياضية لدالة الكثافة الاحتمالية الخاصة بالتوزيع الطبيعي normal distribution هي: div table{margin-left:inherit;margin-right:inherit;margin-bottom:2px;margin-top:2px} td p{margin:0px;} .vbar{border:none;width:2px;background-color:black;} .hbar{display: block;border:none;height:2px;width:100%;background-color:black;} .display{border-collapse:separate;border-spacing:2px;width:auto;border:none;} .dcell{white-space:nowrap;padding:0px; border:none;} .dcenter{margin:0ex auto;} .theorem{text-align:left;margin:1ex auto 1ex 0ex;} table{border-collapse:collapse;} td{padding:0;} .cellpadding0 tr td{padding:0;} .cellpadding1 tr td{padding:1px;} .center{text-align:center;margin-left:auto;margin-right:auto;} PDFnormal(x) = 1 σ √ 2 π exp ⎡ ⎢ ⎢ ⎢ ⎢ ⎢ ⎣ − 1 2 ⎛ ⎜ ⎜ ⎝ x − µ σ ⎞ ⎟ ⎟ ⎠ 2 ⎤ ⎥ ⎥ ⎥ ⎥ ⎥ ⎦ وبطبيعة الحال، لا يفيدنا في أغلب الأحيان تقييم دالة الكثافة الاحتمالية لقيمة معيّنة مثل x، كما لا تكون النتيجة احتمالًا بل كثافة احتمالية. تُعرّف الكثافة في الفيزياء على أنها كتلة المادة لكل وحدة حجم، ومن أجل الحصول على الكتلة يجب أن نضرب بالحجم أو علينا إجراء تكامل مع الحجم في حال لم تكن الكثافة ثابتة، وكذلك فإنّ الكثافة الاحتمالية تقيس الاحتمال لكل وحدة x، لذا فمن أجل الحصول على كتلة احتمالية يجب إجراء تكامل مع x. يزوّدنا مستودع thinkstats2 بصنف class يسمى Pdf يمثِّل دالة الكثافة الاحتمالية، كما يزوِّدنا كل كائن من Pdf بالتوابع التالية: Density يأخذ هذا التابع قيمة x ويُعيد كثافة التوزيع عند x. Render يقيِّم هذا التابع الكثافة عند مجموعة متقطِّعة من القيم ويعيد زوجًا من التسلسلات sequences والتي هي xs أي القيم التي رُتّبت وdf أي الكثافة الاحتمالية الخاصة بهذه القيم. MakePmf يقيِّم هذا التابع الكثافة عند مجموعة متقطّعة من القيم ويعيد صنف Pmf بعد توحيده بحيث يكون مقاربًا للصنف Pdf. GetLinspace يُعيد هذا التابع المجموعة الافتراضية من النقاط التي يستخدِمها كل من التابعَين Render وMakePmf يُعَدّ Pdf صنف أب مجرَّد abstract parent class أو صنفًا مورِّثًا -أي أن صنف ترثه عدة أصناف أخرى-، وتدلّ كلمة مجرَّد على عدم إمكانية استنساخه أي لا يمكن إنشاء كائن Pdf، وبالتالي علينا بدلًا عن ذلك تعريف صنف ابن يرث من الصنف Pdf ويعرِّف التابعَين Density وGetLinspace؛ أما التابعين Render وMakePmf فسيتكفّل الصنف Pdf بتعريفهما. يزوّدنا مستودع thinkstats2 على سبيل المثال بصنف يُدعى NormalPdf يقيِّم دالة الكثافة الطبيعية، وتكون الشيفرة الموافقة له كما يلي: class NormalPdf(Pdf): def __init__(self, mu=0, sigma=1, label=''): self.mu = mu self.sigma = sigma self.label = label def Density(self, xs): return scipy.stats.norm.pdf(xs, self.mu, self.sigma) def GetLinspace(self): low, high = self.mu-3*self.sigma, self.mu+3*self.sigma return np.linspace(low, high, 101) يحتوي كائن الصنف NormalPdf على المعامِلَين mu وsigma، كما يستخدِم التابع Density الكائن scipy.stats.norm الذي يمثِّل توزيعًا طبيعيًا ويزوِّدنا بالتابعَين cdf وpdf، بالإضافة إلى العديد من التوابع الأخرى -ويمكنك الاطلاع على نمذجة التوزيعات Modelling distributions-. تُنشِئ الشيفرة الموجودة في المثال التالي الصنف NormalPdf مع المتوسط mean والتباين variance الخاصَّين بأطوال الإناث البالغات مقدَّرةً بالسنتيمتر ومأخوذةً من نظام مراقبة عوامل المخاطر السلوكية BRFSS، ومن ثم تحسب هي كثافة التوزيع في موقع يبعد انحرافًا معياريًا واحدًا عن المتوسط كما يلي: >>> mean, var = 163, 52.8 >>> std = math.sqrt(var) >>> pdf = thinkstats2.NormalPdf(mean, std) >>> pdf.Density(mean + std) 0.0333001 تكون النتيجة حوالي 0.03 مقدَّرةً بواحدة كتلة احتمالية لكل سنتيمتر، كما نشدِّد على فكرة أنّ الكثافة الاحتمالية لا تعنينا لوحدها لكن إذا رسمنا الصنف Pdf كما يلي، فيمكننا رؤية شكل التوزيع: >>> thinkplot.Pdf(pdf, label='normal') >>> thinkplot.Show() يرسم التابع thinkplot.Pdf الصنف Pdf على أساس دالة منتظمة smooth على عكس التابع thinkplot.Pmf الذي يرسم الصنف Pmf على أساس دالة خطوة، ويُظهِر الشكل التالي النتيجة بالإضافة إلى دالة الكثافة الاحتمالية المقدَّرة من عيّنة، والتي سنحسبها في القسم التالي. يمكننا استخدام التابع MakePmf من أجل إنشاء نسخة تقريبية من الصنف Pdf كما يلي: >>> pmf = pdf.MakePmf() يحتوي الصنف Pmf الناتج على 101 نقطة افتراضيًا تبتعد عن بعضها بمسافات متساوية من mu-3sigma إلى mu+3sigma، ويمكن للتابعَين MakePmf وRender أخذ الوسطاء المفتاحية التالية: low وhigh وn بصورة اختيارية. يوضِّح الشكل السابق دالة كثافة احتمالية طبيعية لنمذجة أطوال الإناث البالغات في الولايات المتحدة الأمريكية، وتقدير كثافة نواة العيّنة في حال كانت n=500. تقدير كثافة النواة يُعَد تقدير كثافة النواة Kernel density estimation -أو KDE اختصارًا- خوارزميةً تأخذ عيّنة، وتوجِد دالة كثافة احتمالية منتظمة تناسب البيانات، ويمكنك قراءة تفاصيل على ويكيبيديا. تزوّدنا scipy بتنفيذ لتقدير كثافة النواة، كما يزوّدنا مستودع thinkstats2 بصنف يُدعى EstimatedPdf يستخدِم ذلك التنفيذ كما يلي: class EstimatedPdf(Pdf): def __init__(self, sample): self.kde = scipy.stats.gaussian_kde(sample) def Density(self, xs): return self.kde.evaluate(xs) يأخذ التابع __init__ عيّنةً ويحسب تقدير كثافة النواة، حيث يكون الناتج كائن gaussian_kde الذي يزوِّدنا بتابع evaluate؛ أما التابع Density فيأخذ قيمةً أو تسلسلًا يُدعى gaussian_kde.evaluate ويُعيد الكثافة الناتجة، كما تَظهر الكلمة Gaussian في الاسم لأنها تستخدم مرشِّحًا يعتمد على التوزيع الغاوسي لجعل تقدير كثافة النواة منتظمًا smooth. إليك شيفرةً تولِّد عيّنةً من توزيع طبيعي ومن ثم تنشِئ صنف EstimatedPdf يناسبها: >>> sample = [random.gauss(mean, std) for i in range(500)] >>> sample_pdf = thinkstats2.EstimatedPdf(sample) >>> thinkplot.Pdf(sample_pdf, label='sample KDE') تمثِّل sample قائمةً تحوي أطوال عشوائية عددها 500؛ أما sample_pdf فهو كائن Pdf يحتوي على تقدير كثافة النواة المقدَّر من العيّنة. يُظهر الشكل السابق دالة الكثافة الطبيعية وتقدير كثافة النواة بناءً على العيّنة التي تحتوي على أطوال عشوائية عددها 500، بحيث يكون التقدير مناسبًا للتوزيع الأصلي. يُعَدّ تقدير دالة الكثافة عن طريق تقدير كثافة النواة مفيدًا لعدة أغراض منها: التصوّر/التوضيح المرئي Visualization: فغالبًا ما تُعَدّ دوال الكثافة التراكمية أفضل توضيح مرئي للتوزيع خلال مرحلة استكشاف المشروع، حيث أنه بإمكانك بعد النظر إلى دالة التوزيع التراكمي تحديد فيما إذا كانت دالة الكثافة الاحتمالية المقدَّرة تُعَدّ نموذجًا مناسبًا للتوزيع أم لا، فإذا كانت كذلك فستكون خيارًا أفضل لتمثيل التوزيع في حال لم يكن الجمهور المستهدف على اطلاع على دوال التوزيع التراكمي. الاستيفاء Interpolation: تفيد دالة الكثافة الاحتمالية المقدَّرة في الانتقال من عيّنة إلى نموذج للسكان، حيث يمكنك استخدَام تقدير كثافة النواة لاستيفاء كثافة القيم التي لا تظهر في العيّنة إذا كنت تعتقد أنّ توزيع السكان منتظمًا smooth. المحاكاة Simulation: غالبًا ما تكون المحاكاة مبنيّة على توزيع العيّنة، فإذا كان حجم العيّنة صغيرًا قد يكون من المناسب استخدام تقدير كثافة النواة KDE من أجل جعل توزيع العيّنة منتظمًا، مما يسمح للمحاكاة باستكشاف المزيد من النتائج المحتملة بدلًا من تكرار البيانات الملحوظة. إطار التوزيع The distribution framework يوضِّح الشكل السابق إطارًا يربط بين تمثيلات دوال التوزيع. الآن بعد مرورنا على دوال الكتلة الاحتمالية PMFs ودوال التوزيع التراكمي CDFs ودوال الكثافة الاحتمالية PDFs، سنتوقف قليلًا لنراجع هذه المفاهيم، حيث يُظهر الشكل السابق كيفية ارتباط هذه الدوال ببعضها البعض. بدأنا بدراسة دوال الكتلة الاحتمالية التي تمثِّل احتمالات مجموعة متقطِّعة من القيم، حيث يمكننا الانتقال من دالة كتلة احتمالية PMF إلى دالة توزيع تراكمي CDF عن طريق جمع الكتل الاحتمالية للحصول على الاحتمالات التراكمية، وللانتقال من دالة توزيع تراكمي إلى دالة كتلة احتمالية يمكننا حساب الفروق في الاحتمالات التراكمية، كما سنرى تنفيذ هذه العمليات في الأقسام القليلة القادمة. يمكن تعريف دالة الكثافة الاحتمالية على أنها مشتق من دالة التوزيع التراكمي المستمرة، أو على نحو مكافئ بأنها تكامل لدالة الكثافة الاحتمالية -أي نحصل على دالة توزيع تراكمي عن طريق تطبيق تكامل على دالة الكثافة الاحتمالية-، حيث تحوِّل دالة الكثافة الاحتمالية القيم إلى كثافات احتمالية، ويجب تطبيق التكامل للحصول على احتمال. يمكننا جعل التوزيع منتظمًا بعدة طرق من أجل الانتقال من توزيع متقطِّع إلى توزيع مستمر، وإحدى أشكال التنظيم أو التنعيم smoothing هي افتراض أنّ مصدر البيانات هو توزيع تحليلي مستمر -مثل التوزيع الأسي أو الطبيعي-، ومن ثم تقدير معامِلات التوزيع، كما يمكننا جعل التوزيع منتظمًا عن طريق تقدير كثافة النواة KDE على أساس خيار آخر. يُعَدّ التكميم quantizing -أو التقطيع discretizing- عمليةً معاكسةً لعملية التنظيم smoothing، كما يمكننا توليد دالة الكتلة الاحتمالية التي تُعدّ تقريبًا لدالة الكثافة الاحتمالية وذلك عن طريق تقييم دالة الكثافة الاحتمالية عند نقاط متقطِّعة، كما يمكننا الحصول على تقريب أفضل باستخدام التكامل العددي. سنستخدم مصطلح دالة الكثافة التراكمية للدلالة على دالة التوزيع التراكمي المتقطعة، وذلك بهدف التمييز بين دوال التوزيع التراكمي المستمرة ودوال التوزيع التراكمي المتقطِّعة، لكننا نعتقد أن هذا المصطلح غير مستخدَم من قِبَل أيّ شخص آخر. تنفيذ Hist لا بدّ أنك الآن تجيد استخدام الأنواع الأساسية التي يزوّدنا بها مستودع thinkstats2 وهي: Hist وPmf وCdf وPdf، كما ستزوِّدنا الأقسام القليلة القادمة بتفاصيل حول تنفيذها، وقد تساعدك هذه المعلومات على استخدام هذه الأصناف استخدامًا فعّالًا لكنها ليست ضروريةً تمامًا. يرث الصنفان Hist وPmf توابعهما من صنف أب يُدعى ‎_DictWrapper‎ حيث تشير الشَرطة السفلية underscore إلى أن الصنف "داخلي"، أي لا يمكن استخدامه من قِبَل شيفرات موجودة في وحدات modules أخرى؛ أمّا اسم الصنف فيشير إلى قاموس مغلّف Dictionary Wrapper، والسمة الأساسية فيه هي d أي القاموس dictionary الذي يحوِّل القيم إلى تردداتها. يمكن أن تنتمي القيم إلى أيّ نوع قابل للتجزئة hashable، وعلى الرغم أنه يجب أن تكون الترددات قيمًا صحيحةً إلا أنها يمكن أن تنتمي إلى أي نوع عددي. يحتوي ‎_DictWrapper على توابع ملائمة لكلًا من الصنف Hist والصنف Pmf بما فيها __init__ و Values و Items و Render بالإضافة إلى توفير توابع محوِّلة هي Set و Incr و Mult و Remove فكل هذه التوابع مُنفَّذة مع عمليات القاموس، انظر مثلًا: # class _DictWrapper def Incr(self, x, term=1): self.d[x] = self.d.get(x, 0) + term def Mult(self, x, factor): self.d[x] = self.d.get(x, 0) * factor def Remove(self, x): del self.d[x] يزوّدنا الصنف Hist بالتابع Freq الذي يوجِد تردد قيمة معطاة. هذه التوابع تعمل عوامِل وتوابع Hist بزمن ثابت لأنها مبنية على قواميس، أي أنّ زمن تنفيذها لا يزداد مع ازدياد حجم الصنف Hist. تنفيذ Pmf يتشابه الصنفان Pmf وHist إلى حد التطابق تقريبًا إلا أنّ Pmf يحوِّل القيم إلى احتمالات عشرية؛ أما Hist فيحول القيم إلى ترددات نوعها صحيح integer، بحيث إذا كان مجموع الاحتمالات مساويًا للواحد فسيكون Pmf موحَّدًا. يزوِّدنا الصنف Pmf بالتابع Normalize الذي يحسب مجموع الاحتمالات ويقسمها على معامِل factor كما يلي: # class Pmf def Normalize(self, fraction=1.0): total = self.Total() if total == 0.0: raise ValueError('Total probability is zero.') factor = float(fraction) / total for x in self.d: self.d[x] *= factor return total يحدِّد المتغير fraction مجموع الاحتمالات بعد توحيدها للواحد، حيث أنّ القيمة الاقتراضية هي الواحد، وإذا كان مجموع الاحتمالات 0 فلا يمكن توحيد الصنف Pmf، لذا سترمي دالة Normalize خطأً من النوع ValueError. يملك الصنفان Hist وPmf الباني نفسه، حيث يأخذ هذا الباني وسيطًا ليكون dict أو Hist أو Pmf أو Cdf، أو سلسلة بانداز pandas Series أو قائمةً من أزواج (قيمة وتردد) أو تسلسلًا من القيم. إذا أنشأت نسخةً من الصنف Pmf، فستكون النتيجة موحَّدة إلى الواحد (normalized)؛ أما إذا أنشأت نسخةً من الصنف Hist، فلن تكون النتيجة موحَّدة إلى الواحد، حيث يمكنك إنشاء صنف Pmf فارغ وتعديله لبناء صنف Pmf غير موحَّد، إذ أنّ معدلات Pmf لا لا تعيد توحيد الصنف Pmf. تنفيذ Cdf تحوّل دالة التوزيع التراكمي القيم إلى احتمالاتها التراكمية، لذا كان من الممكن تنفيذ Hist على أساس ‎_‎DictWrapper إلا أنّ القيم في الصنف Hist مرتبّة على عكس ‎_DictWrapper. يُعَد حساب دالة التوزيع التراكمي العكسية inverse CDF مفيدًا في البعض الأحيان، بحيث تكون دالة التوزيع التراكمي العكسية هي تحويل الاحتمال التراكمي إلى قيمته، لذا اخترنا التنفيذ الذي يحوي قائمتَين مرتبتَين وذلك لكي نستطيع إجراء بحث lookup أمامي أو عكسي في زمن تنفيذ لوغاريتمي عن طريق استخدام البحث الثنائي binary search. يمكن لباني Cdf أن يأخذ تسلسلًا من القيم على أساس معامِل له أو قد يأخذ سلسلة بانداز pandas Series أو قاموسًا dictionary يحوِّل القيم إلى احتمالاتها، أو تسلسلًا من أزواج (القيمة والاحتمال) أو Hist، أو Pmf، أو Cdf، أو إذا أُعطي الباني معامِلان فسيعاملهما على أساس تسلسل مرتّب من القيم، وتسلسل من الاحتمالات التراكمية الموافقة. يمكن للباني إنشاء Hist بإعطاء تسلسل أو سلسلة بانداز pandas Series، أو قاموس، ومن ثم يستخدِم Hist من أجل تهيئة السمات: self.xs, freqs = zip(*sorted(dw.Items())) self.ps = np.cumsum(freqs, dtype=np.float) self.ps /= self.ps[-1] تمثِّل xs قائمةً مرتّبةً من القيم وتمثِّل freqs قائمة الترددات الموافقة للقيم الموجودة في xs. يحسب التابع np.cumsum المجموع التراكمي للترددات علمًا أنّ التقسيم على التردد الكلي يُنتِج الاحتمالات التراكمية، كما يتناسب وقت بناء الصنف Cdf مع n logn في حال كان عدد القيم يساوي n. إليك تنفيذ التابع Prob الذي يأخذ قيمةً ويُعيد الاحتمال التراكمي: # class Cdf def Prob(self, x): if x < self.xs[0]: return 0.0 index = bisect.bisect(self.xs, x) p = self.ps[index - 1] return p تزوّدنا الوحدة bisect بتنفيذ البحث الثنائي، وإليك تنفيذ التابع Value الذي يأخذ الاحتمال التراكمي ويُعيد القيمة الموافقة: # class Cdf def Value(self, p): if p < 0 or p > 1: raise ValueError('p must be in range [0, 1]') index = bisect.bisect_left(self.ps, p) return self.xs[index] يمكننا حساب Pmf في حال كان لدينا Cdf عن طريق حساب الفروقات بين احتمالين تراكميَّين متتاليين، وإذا استدعينا باني Cdf ومررنا له Pmf، فسيحسب الفروقات عن طريق استدعاء Cdf.Items كما يلي: # class Cdf def Items(self): a = self.ps b = np.roll(a, 1) b[0] = 0 return zip(self.xs, a-b) يُزيح التابع np.roll قيمًا من a إلى اليمين ويُدحرج القيمة الأخيرة إلى البداية، كما نستبدل القيمة 0 بالعنصر الأول من b ثم نحسب الفرق a-b، وتكون النتيجة هي مصفوفة نمباي NumPy من الاحتمالات. يزوِّدنا Cdf بالتابعَين Shift وScale الذين يعدِّلان القيم الموجودة في Cdf إلا أنه يجب التعامل مع الاحتمالات على أنها قيم ثابتة غير قابلة للتبديل أو التعديل. العزوم moments عندما نأخذ عيّنةً ونحولّها إلى عدد منفرد أي نقلّصها، سينتج لدينا ما يُعرف بالإحصائية، وقد رأينا عدة إحصائيات حتى الآن، منها المتوسط mean والتباين variance والوسيط median والانحراف الربيعي interquartile range. يُعِدّ العزم الخام raw moment نوعًا من أنواع الإحصائيات، فإذا كانت لديك إحصائية تحوي قيمًا عددها xi‎ فستكون الصيغة الرياضية للعزم الخام رقم k أي kth raw moment كما يلي: m′k = 1 n ∑ i xik أو إذا كنت تفضِّل صيغة بايثون، فهذه هي الشيفرة الموافقة: def RawMoment(xs, k): return sum(x**k for x in xs) / len(xs) وفي حال كنا نريد إيجاد العزم الأول أي k=1 ستكون النتيجة هي متوسط العيّنة x̄، وفي الواقع لا تفيدنا العزوم الخام لوحدها إلّا أنها تُستخدَم في بعض أنواع الحسابات. تُعَدّ العزوم المركزية أكثر فائدةً من العزوم الخام، وتكون الصيغة الرياضية للعزم المركزي ذو الرقم k كما يلي: mk = 1 n ∑ i (xi − x)k أمّا الشيفرة الموافقة في بايثون فتكون كما يلي: def CentralMoment(xs, k): mean = RawMoment(xs, 1) return sum((x - mean)**k for x in xs) / len(xs) إذا كانت k=2 فستكون النتيجة هي العزم المركزي الثاني، أي ما يُعرَف بالتباين variance، وقد يفيدنا التعريف الخاص بالتباين في معرفة السبب وراء تسمية هذه الإحصائيات بالعزوم، حيث إذا ثبّتنا ثقلًا على طول مسطرة في كل موقع xi‎ ومن ثم دوّرنا المسطرة حول المتوسط mean، فسيكون عزم العطالة -أو عزم القصور الذاتي- مساويًا لتباين القيم، وإذا لم تكن لديك فكرةً مسبقةً عن عزم العطالة، فيمكنك الاطلاع على معنى عزم القصور الذاتي. من المهم وضع واحدات القياس في الحسبان عند التعامل مع الإحصائيات المبنية على العزوم، فإذا كانت القيم xi‎ مقدَّرةً بالسنتيمتر، فسيكون العزم الخام الأول مقدَّرًا بالسنتيمتر أيضًا، لكن يكون العزم الثاني مقدَّرًا بالسنتيمتر مربّع أي cm2‎، ويكون العزم الثالث مقدَّرًا بالسنتيمتر مكعَّب أي cm3‎ وهكذا. وبسبب هذه الواحدات فإنه من الصعب تفسير وفهم العزوم لوحدها، لذا عادةً ما يُحسب الانحراف المعياري عند ذكر العزم الثاني، حيث يمكن حساب الانحراف المعياري عن طريق تطبيق الجذر التربيعي على التباين، لذا فهو يُقدَّر واحدات قياس xi‎ نفسها. 8 معامل التجانف Skewness التجانف skewness هو خاصية تصف شكل التوزيع، فإذا كان التوزيع متناظرًا حول النزعة المركزية central tendency سنقول أنّه غير متجانف unskewed، وإذا كانت القيم ممتدة إلى أقصى اليمين فسيكون متجانفًا إلى اليمين؛ أما إن كانت ممتدة إلى أقصى اليسار فسيكون متجانفًا إلى اليسار. لا يدل في الواقع استخدام كلمة متجانف skewed على المعنى المعتاد منحازة biased، حيث يصف التجانف شكل التوزيع فقط ولا يذكر أيّ معلومات حول ما إن كانت عملية أخذ العيّنات منحازةً أم لا. عادةً ما يتم حساب كمية تجانف -أو انحراف- توزيع معيّن عن طريق استخدام عدة أنواع من الإحصائيات، فإذا كان لدينا تسلسل من القيم xi‎، فيمكننا حساب تجانف العيّنة sample skewness التي يرمز لها g1 بالصورة التالية: def StandardizedMoment(xs, k): var = CentralMoment(xs, 2) std = math.sqrt(var) return CentralMoment(xs, k) / std**k def Skewness(xs): return StandardizedMoment(xs, 3) يكون g1 هو العزم القياسي standardized moment الثالث، أي أنه وُحِّد normalized ولذلك ليس له واحدات قياس. يدل التجانف السالب على أنّ التوزيع متجانف إلى اليسار -أي منحرف نحو اليسار-؛ أمّا التجانف الموجب فيدل على أنّ التوزيع متجانف إلى اليمين -أي منحرف نحو اليمين-، كما يدل مقدار g1‎ على قوة التجانف إلا أنه ليس من السهل تفسيره لوحده. بالنظر إلى الجانب العملي يمكننا القول إن حساب عيّنة التجانف sample skewness ليست فكرةً سديدةً في أغلب الأحيان، إذ يخلق وجود القيم الشاذة outliers تأثيرًا غير متناسب على g1‎. توجد طريقة أخرى لتقييم لاتناظر توزيع معيّن من خلال دراسة العلاقة بين المتوسط والوسيط، حيث تؤثِّر القيم المتطرِّفة على المتوسط أكثر مما تؤثره على الوسيط، لذا يكون المتوسط أقل من الوسيط في التوزيعات التي تتجانف إلى اليسار، ويكون المتوسط أكبر من الوسيط في التوزيعات التي تتجانف إلى اليمين. يُعدّ معامل التجانف المتوسط لبيرسون Pearson’s median skewness coefficient مقياسًا للتجانف يعتمد على الفرق بين متوسط العيّنة والوسيط، وتكون الصيغة الرياضية له كما يلي: gp = 3 (x − m) / S حيث يكون x̄ متوسط العيّنة وm هي الوسيط وS هي الانحراف المعياري، وتكون شيفرة بايثون كما يلي: def Median(xs): cdf = thinkstats2.Cdf(xs) return cdf.Value(0.5) def PearsonMedianSkewness(xs): median = Median(xs) mean = RawMoment(xs, 1) var = CentralMoment(xs, 2) std = math.sqrt(var) gp = 3 * (mean - median) / std return gp تُعَدّ الإحصائية متينةً robust أي أنها أقل عرضة لتأثير القيم الشاذة outliers. يوضِّح الشكل السابق دالة الكثافة الاحتمالية PDF المقدَّرة لبيانات أوزان المواليد من المسح الوطني لنمو الأسرة NSFG. سنرى مثالًا عن هذا وهو تجانف أوزان المواليد في بيانات حالات الحمل الموجودة في المسح الوطني لنموّ الأسرة NSFG، وفيما يلي الشيفرة التي ترسم دالة الكتلة الاحتمالية وتقدِّرها: live, firsts, others = first.MakeFrames() data = live.totalwgt_lb.dropna() pdf = thinkstats2.EstimatedPdf(data) thinkplot.Pdf(pdf, label='birth weight') يُظهِر الشكل السابق النتيجة، حيث يبدو الذيل الأيسر أطول من الذيل الأيمن لذا قد نعتقد أنّ التوزيع متجانفًا إلى اليسار، كما نجد أنّ المتوسط الذي يقدَّر بحوالي 3.29 كيلوغرامًا أي 7.27 رطلًا هو أقل من الوسيط الذي يقدَّر بحوالي 3.34 كيلوغرامًا أي 7.38 رطلًا، لذلك يتناسب هذا مع التجانف إلى اليسار، كما نجد أنّ معاملَي التجانف سالبان، حيث تكون قيمة عيّنة التجانف-0.59، وقيمة معامل التجانف المتوسط لبيرسون هو -0.23. يوضِّح الشكل السابق دالة الكثافة الاحتمالية المقدَّرة لبيانات أوزان البالغين من نظام مراقبة عوامل المخاطر السلوكية BRFSS؛ أما الآن فسنوازن بين هذا التوزيع وبين توزيع أوزان البالغين في نظام مراقبة عوامل المخاطر السلوكية BRFSS، وفيما يلي الشيفرة الموافقة لذلك: df = brfss.ReadBrfss(nrows=None) data = df.wtkg2.dropna() pdf = thinkstats2.EstimatedPdf(data) thinkplot.Pdf(pdf, label='adult weight') يُظهر الشكل السابق النتيجة، حيث يبدو التوزيع متجانفًا إلى اليمين، وبالطبع يكون المتوسط الذي يقدَّر بحوالي 35.8338 كيلوغرامًا أي 79.0 رطلًا أكبر من الوسيط الذي يقدَّر بحوالي 35.06269 كيلوغرامًا أي 77.3 رطلًا، كما تكون عيّنة التجانف هي 1.1، ومعامل التجانف المتوسط لبيرسون هو 0.26. يمكننا الاستنتاج من إشارة مُعامِل التجانف ما إذا كان التوزيع متجانفًا إلى اليمين أو إلى اليسار، لكن من الصعب تفسيرها بخلاف ذلك. تُعَد عيّنة التجانف أقل متانةً أي أنها أكثر عرضةً لتأثير القيم الشاذة، ونتيجةً لذلك فإنّ العيّنة أقل موثوقيةً عند تطبيقها على التوزيعات المتجانفة، أي هي غير موثوقة كثيرًا في أكثر وقت نحتاجها فيه بأن تكون موثوقة. يعتمد معامِل التجانف المتوسط لبيرسون على المتوسط والتباين المحسوبَين، لذا فهو أكثر عرضةً لتأثير القيم الشاذة، لكن بما أنه لا يعتمد على عزم ثالث فهو أكثر متانةً إلى حد ما. تمارين يوجد حل هذا التمرين في chap06soln.py في مستودع ThinkStats2 على GitHub. تمرين 1 من المعروف أن توزيع الدخل يتجانف إلى اليمين، لذا سنقيس في هذا التمرين مدى قوة هذا التجانف. يُعَدّ المسح السكاني الحالي Current Population Survey -أو CPS اختصارًا- جهدًا مشتركًا بين مكتب إحصاءات العمل ومكتب التعداد لدراسة الدخل والمتغيرات ذات الصلة، كما أنّ البيانات التي جُمعَت في عام 2013 متاحة في census.gov. حمّلنا ملف hinc06.xls وهو جدول بيانات إكسل يحتوي على معلومات حول دخل الأسر المعيشية، ومن ثم حولناه إلى hinc06.csv وهو ملف من النوع CSV يمكنك إيجاده في مستودع هذا الكتاب، كما ستجد hinc2.py الذي يقرأ الملف السابق ويحوِّل بياناته. تتخذ مجموعة البيانات شكل سلسلة مجالات الدخل وعدد المستجيبين الموجودين في كل مجال، إذ يشمل المجال الأدنى المستجيبين الذين أفادوا بأن دخل الأسر المعيشية في السنة أقل من 5000 دولارًا أمريكيًا، في حين يشمل المجال الأعلى المستجيبين الذين يكسبون 250000 دولارًا أمريكيًا أو أكثر. لتقدير المتوسط والإحصائيات الأخرى الخاصة من هذه البيانات علينا وضع بعض الافتراضات بما يخص الحدود الدنيا والعليا وبما يخص توزيع القيم في كل مجال. يزوِّدنا hinc2.py بالتابع InterpolateSample الذي ينمذج البيانات بإحدى الطرق المتاحة، حيث يأخذ إطار بيانات DataFrame مع عمود، وincome الذي يحتوي القيمة العليا لكل مجال، وfreq الذي يحوي عدد المستجيبين في كل إطار، وlog_upper وهي القيمة العليا المفترضة على المجال الأعلى ويُعبَّر عنها كما يلي: log10 دولارًا -أي نحسب اللوغاريتم العشري للقيمة بالدولار-. تمثِّل القيمة الافتراضية log_upper=6.0‎ الافتراض الذي يقول أن الدخل الأعلى ضمن المستجيبين هو 10‎6‎ أي مليون دولار. يولّد InterpolateSample عيّنةً وهميةً pseudo-sample، أي عيّنةً من مداخيل (جمع دخل) الأسر المعيشية تحتوي على عدد المستجيبين نفسه الموجود في كل مجال البيانات الفعلية، كما تفترض العيّنة الوهمية أن جميع المداخيل في كل مجال تبعد عن بعضها مسافات متساوية على مقياس log10. احسب الوسيط والمتوسط والتجانف وتجانف بيرسون للعيّنة الناتجة، وما هي نسبة الأسر المعيشية التي يكون دخلها الخاضع للضريبة أقل من المتوسط؟ وكيف تعتمد النتائج على الحد الأعلى upper bound المفترَض؟ ترجمة -وبتصرف- للفصل Chapter 6 Probability density functions analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا دوال الكتلة الاحتمالية في جافاسكريبت التوزيعات الإحصائية في بايثون النسخة الكاملة من كتب مدخل إلى الذكاء الاصطناعي وتعلم الآلة
  10. تندرج جميع التوزيعات التي استخدمناها حتى الآن تحت اسم التوزيعات التجريبية empirical distributions لأنها مبنية على ملاحظات تجريبية وهي بالضرورة عيّنات محدودة. يأتي التوزيع التحليلي analytic distribution بديلًا عنها، وهو يتميز بدالة توزيع تراكمي -cumulative distribution function أو CDF اختصارًا-، والتي تتصف بأنها دالة رياضية، حيث يمكننا استخدام التوزيعات التحليلية لنمذجة التوزيعات التجريبية. ويكون النموذج model في هذا السياق مبسَّطًا ولا يتعمق في التفاصيل الغير ضرورية. يناقش هذا المقال التوزيعات التحليلية الشائعة وطريقة استخدامها لنمذجة البيانات من مصادر متنوعة، وتوجد الشيفرة الخاصة بهذا المقال في analytic.py في مستودع ThinkStats2 على GitHub. التوزيع الأسي يوضِّح الشكل السابق دوال التوزيع التراكمي للتوزيعات الأسية مع معامِلات متنوعة. ستكون البداية مع التوزيع الأسي exponential distribution لأنه سهل نسبيًا، وتكون الصيغة الرياضية لدالة التوزيع التراكمي للتوزيع الأسي هي: CDF(x) = 1- e-λx يحدِّد المعامِل λ شكل التوزيع، ويُظهر الشكل السابق شكل دالة التوزيع التراكمي عندما تكون λ=0.5 وλ=1 وλ=2. تظهر التوزيعات الأسيّة في العالم الحقيقي عندما ندرس سلسلة من الأحداث ونقيس الأزمنة الفاصلة بينها، وتُدعى أوقات الوصول البينية interarrival times، حيث إذا كان احتمال حصول الأحداث متساو في أيّ وقت كان فسيبدو توزيع أوقات الوصول البينية أنه التوزيع الأسي، كما يمكنك الاطلاع على هذه الورقة للاستئناس. سنلقي نظرةً على أوقات الوصول البينية الخاصة بالولادات على أساس مثال عن الفكرة السابقة، ففي يوم 18 من شهر 12 عام 1997، ولِد 44 طفلًا في مستشفى في مدينة بريسبان في أستراليا1، حيث وُثِّق وقت ولادة جميع هؤلاء الأطفال في صحيفة محلية، كما توجد مجموعة البيانات كاملةً في ملف babyboom.dat في مستودع ThinkStats2. df = ReadBabyBoom() diffs = df.minutes.diff() cdf = thinkstats2.Cdf(diffs, label='actual') thinkplot.Cdf(cdf) thinkplot.Show(xlabel='minutes', ylabel='CDF') تقرأ الدالة ReadBabyBoom ملف البيانات وتُعيد إطار بيانات DataFrame مع الأعمدة التالية: time يدل على وقت الولادة وsex يدل على جنس المولود، وweight_g يدل على الوزن عند الولادة، وminutes يدل على وقت الولادة بعد تحويله إلى الدقائق منذ منتصف الليل. يوضِّح الشكل السابق دالة التوزيع التراكمي لأوقات الوصول البينية في الجهة اليمنى، ودالة التوزيع التراكمي المتمِّمة complementary cumulative distribution function -أو CCDF اختصارًا- على مقياس لوغاريتمي على محور y في الجهة اليسرى، كما أنّ diffs هو الفارق بين أزمنة الولادة المتتالية، وcdf هو توزيع أوقات الوصول البينية هذه، كما يُظهر الشكل السابق دالة التوزيع التراكمي، إذ يبدو أن للدالة الشكل العام للتوزيع الأسي، لكن كيف يمكننا التيقن من هذا؟ تتمثل إحدى الطرق في رسم دالة التوزيع التراكمي المتمِّمة complementary CDF ومعادلتها ‎‎1‎-CDF(x‎)‎ على مقياس لوغاريتمي على محور y -أي المحور العمودي-، كما تكون النتيجة هي عبارة عن خط مستقيم في حال كانت البيانات من توزيع أسّي، وسنرى تطبيقًا عمليًا لهذا. إذا رسمت دالة التوزيع التراكمي المتمِّمة لمجموعة بيانات معيّنة تعتقد أنها أسيّة، فستتوقع رؤية دالة مثل التي يُعبّر عنها بالصيغة الرياضية التالية: y ≈ e-λx بتطبيق اللوغاريتم على الطرفين: logy ≈ -λx أي أنّ دالة التوزيع التراكمي المتمِّمة على مقياس لوغاريتمي على المحور العمودي هي عبارة عن خط مستقيم بميل قدره ‏‏‏‏‏‏‏‏‏λ-، وفيما يلي الشيفرة التي تولِّد الرسم: thinkplot.Cdf(cdf, complement=True) thinkplot.Show(xlabel='minutes', ylabel='CCDF', yscale='log') يمكن لدالة thinkplot.Cdf حساب دالة التوزيع التراكمي المتمِّمة قبل الرسم وذلك بفضل الوسيط complement=True، كما يمكن للدالة thinkplot.Show ضبط مقياس محور x -أي المحور الأفقي- ليصبح لوغاريتميًا بفضل الوسيط yscale='log'‎. يُظهِر الشكل اليميني السابق النتيجة، وفي الواقع فإنّ الخرج ليس خطًا مستقيمًا مما يعني أنّ التوزيع الأسي ليس نموذجًا مثاليًا لهذه البيانات، ومن المرجَّح أنّ الافتراض الأساسي -الذي يقول أنّ احتمال الولادة نفسه لأيّ وقت من اليوم- ليس صحيحًا تمامًا، ومع ذلك قد يكون من المنطقي نمذجة مجموعة البيانات هذه بتوزيع أسي، حيث يمكننا تلخيص التوزيع بمعامِل واحد إذا استخدمنا هذا التبسيط في عملية النمذجة. يمكن تفسير المعامِل λ على أنّه معدّل rate أي عدد الأحداث التي تحدث وسطيًا في وحدة الزمن، أي في هذا المثال وُلِد 44 طفلًا في 24 ساعة، لذا فإن المعدل هو λ=0.306 ولادة في الدقيقة الواحدة، كما يكون متوسط mean التوزيع الأسي هو ‎1/‎λ، لذا فإنّ متوسط الزمن بين الولادات هو 32.7 دقيقة. التوزيع الطبيعي normal distribution يُستخدَم التوزيع الطبيعي normal distribution والذي يُدعى أيضًا "التوزيع الغاوسي" استخدامًا كبيرًا لأنه يصف العديد من الظواهر -يصفها بصورة تقريبية على الأقل-، ولم يأتِ هذا الانتشار الواسع للتوزيع الطبيعي من عبث، بل يوجد سبب مقنع سنناقشه في مقال لاحق، في القسم الرابع تحت عنوان "نظرية الحد المركزيّ". يوضِّح الشكل السابق دالة التوزيع التراكمي لتوزيعات طبيعية مع مجال وسائط parameters ما. ويتميّز التوزيع الطبيعي بمعامِلَين اثنين هما μ المتوسط mean، وσ الانحراف المعياري standard deviation. يُعَدّ التوزيع الطبيعي القياسي standard normal distribution حالةً خاصةً من التوزيع الطبيعي يكون فيها المتوسط مساويًا للصفرμ = 0 وقيمة الانحراف المعياري مساويةً للواحد σ = 1، حيث يمكن تعريف دالة التوزيع التراكمي الخاصة بهذا التوزيع على أنّه تكامل لا يحوي على حلّ منغلق الشكل إلا أنه هناك خوارزميات تستطيع تقييمه بكفاءة. تزوِّدنا مكتبة ساي باي SciPy بإحدى هذه الخوارزميات، حيث أنّ scipy.stats.norm هو كائن يمثِّل توزيعًا طبيعيًا، ويزوِّدنا بتابع cdf يقيّم دالة التوزيع التراكمي القياسية الطبيعية: >>> import scipy.stats >>> scipy.stats.norm.cdf(0) 0.5 هذه النتيجة صحيحة، حيث يكون وسيط median التوزيع الطبيعي هو 0 -كما هو الحال في المتوسط mean-، ونصف القيم أقلّ من الوسيط، وبالتالي CDF(0)=0.5. تأخذ الدالة norm.cdf معامِلَين اختياريين هما loc الذي يحدِّد المتوسط mean، وscale الذي يحدِّد الانحراف المعياري، ويسّهِل مستودع thinkstats2 علينا هذه الدالة عن طريق تزويدنا بالدالة EvalNormalCdf التي تأخذ معامِلَين اختياريين هما mu وsigma وتقيِّم دالة التوزيع التراكمي عند x: def EvalNormalCdf(x, mu=0, sigma=1): return scipy.stats.norm.cdf(x, loc=mu, scale=sigma) يُظهِر الشكل السابق دوال التوزيع التراكمي للتوزيعات الطبيعية مع مجال من المعامِلات، كما يُعَدّ هذا الشكل السّيني sigmoid للمنحنيات صفة مميّزة للتوزيع الطبيعي. ألقينا في المقال السابق نظرةً على توزيع أوزان الولادات في المسح الوطني لنمو الأسرة، ويُظهِر الشكل التالي دوال التوزيع التراكمي التجريبي لأوزان جميع الولادات الحية، كما يُظهِر التوزيع الطبيعي مع مراعاة أن قيمة التباين variance وقيمة المتوسط mean هي ذاتها في الحالتين. يوضِّح الشكل السابق دالة التوزيع التراكمي لأوزان الولادات مع نموذج طبيعي، حيث يُعَدّ التوزيع الطبيعي نموذجًا جيّدًا لمجموعة البيانات هذا، لذا فإن لخصّنا هذا التوزيع بالمعامِلات التالية: μ=7.28 وσ=1.24، فسيكون الخطأ الناتج صغير، مع العلم أنّ الخطأ هنا يشير إلى الفرق بين النموذج والبيانات. هناك تناقض discrepancy بين البيانات وبين النموذج تحت المئين العاشر، حيث أنه يوجد في التوزيع عدد أطفال خفيفي الوزن أكثر من المتوقع في التوزيع الطبيعي، لكن إذا كنا مهتمين بدراسة الأطفال الخدّج، فسيكون الحصول على نتيجة دقيقة لهذا الجزء من التوزيع أمرًا هامًّا، لذلك قد لا يكون النموذج الطبيعي مناسبًا لهذا الغرض. رسم الاحتمال الطبيعي normal probability plot هناك تحويلات بسيطة يمكن استخدامها لاختبار فيما إذا كان التوزيع التحليلي يُعَدّ نموذجًا جيدًا لمجموعة بيانات معينة أم لا، ويمكن تطبيق ذلك على التوزيعات الأسية بالإضافة إلى بعض الأنواع الأخرى. وعلى الرغم من استحالة تطبيق هذه التحويلات على التوزيع الطبيعي، إلا أنه يوجد حل بديل يدعى رسم الاحتمال الطبيعي normal probability plot، كما يمكن توليده بطريقتين إحداهما صعبة والأخرى سهلة، ويمكنك الاطلاع على الطريقة الصعبة من هنا. أما بالنسبة للطريقة السهلة، فإليك الخطوات التالية: رتّب القيم في العيّنة. بدءًا من توزيع طبيعي قياسي -أي μ=0 وσ=1-، وولِّد عيّنة عشوائية لها حجم العيّنة نفسه ثم رتّبها. ارسم القيم المرتّبة من العيّنة والقيم العشوائية. تكون النتيجة خطًا مستقيمًا مع نقطة تقاطع ميو mu أي μ وميل قدره سيمغا sigma أيσ إذا كان توزيع العيّنة طبيعيًا تقريبًا. يزوّدنا مستودع thinkstats2 بالدالة NormalProbability التي تأخذ عيّنةً وتُعيد مصفوفتَي نمباي NumPy، أي كما يلي: xs, ys = thinkstats2.NormalProbability(sample) يوضِّح الشكل السابق رسم الاحتمال الطبيعي لعيّنات عشوائية من توزيع طبيعي، حيث تحتوي المصفوفة ys على القيم المرتّبة من العيّنة؛ أما المصفوفة xs فتحتوي على القيم العشوائية من التوزيع الطبيعي القياسي. وقد وَلّدنا بعض العيّنات المزيفة التي استقيناها من توزيعات طبيعية ذات معامِلات متنوعة وذلك بهدف اختبار الدالة NormalProbability، حيث يُظهِر الشكل السابق النتائج. نلاحظ أنّ الخطوط مستقيمة تقريبًا مع وجود انحراف في القيم الموجودة في الذيول أكثر من القيم القريبة من المتوسط. دعنا نجري التجربة على البيانات الحقيقية الآن. تولِّد هذه الشيفرة رسمًا احتماليًا طبيعيًا لبيانات أوزان الولادات الموجودة في القسم السابق، حيث ترسم هذه الشيفرة خطًا رماديًا يمثِّل النموذج، وخطًا أزرقًا يمثِّل البيانات. def MakeNormalPlot(weights): mean = weights.mean() std = weights.std() xs = [-4, 4] fxs, fys = thinkstats2.FitLine(xs, inter=mean, slope=std) thinkplot.Plot(fxs, fys, color='gray', label='model') xs, ys = thinkstats2.NormalProbability(weights) thinkplot.Plot(xs, ys, label='birth weights') تمثِّل weights سلسلة بانداز pandas Series لأوزان الولادات، كما يمثِّل mean المتوسط، في حين يمثِّل std الانحراف المعياري. وتأخذ الدالة FitLine تسلسلًا sequence من xs ونقطة تقاطع ومَيل، كما تُعيد xs وys اللتين تمثِّلان خطًا مع المعامِلات المُعطاة مقيّمًا عند القيم في xs. تُعيد الدالة NormalProbability مصفوفتَين xs وys تحتويان على قيم من التوزيع الطبيعي القياسي وقيمًا من weights، حيث يجب أن تكون البيانات مطابقةً للنموذج في حال كان توزيع الأوزان طبيعيًا. يوضِّح الشكل السابق رسم الاحتمال الطبيعي لأوزان الولادات. يُظهر الشكل السابق نتائج جميع الولادات الحيّة وجميع الولادات التامة -التي كانت مدة الحمل فيها أكثر من 36 أسبوعًا-، حيث يُطابق المنحينان النموذج قرب المتوسط وينحرفان عند الذيول. كما أنّ وزن الأطفال الأكثر وزنًا أكثر مما يتوقّعه النموذج، ووزن الأطفال الأخف وزنًا أقل مما يتوقّعه النموذج. عندما نحدِّد الولادات التامة فقط سنستبعد بعض الأطفال خفيفي الوزن مما يقلّل من التناقض في الذيل المنخفض من التوزيع. وبحسب هذا الرسم فإنّ النموذج الطبيعي يصف التوزيع وصفًا جيدًا مع بعض الانحرافات المعيارية ضمن المتوسط، لكن لا توجد في الذيول، لكن كَون النموذج مناسبًا بما فيه الكفاية للتطبيقات العملية أم لا، فهو أمر يعود إلى الغرض من النمذجة. التوزيع اللوغاريتمي الطبيعي The lognormal distribution إذا كان للوغاريتمات مجموعة من القيم توزيعًا طبيعيًا، فستمتلك القيم توزيعًا لوغاريتميًا طبيعيًا lognormal distribution، كما أنّ دالة التوزيع التراكمي للتوزيع اللوغاريتمي الطبيعي هي ذاتها بالنسبة للتوزيع الطبيعي لكن مع استبدال logx بـ x. CDFlognormal(x) = CDFnormal(logx)‎ عادةً ما تتم الإشارة إلى معامِلات التوزيع اللوغاريتمي الطبيعي بـ μ وσ، لكنها لا تشير هنا إلى المتوسط والانحراف المعياري كما هو الحال سابقًا، إذ يكون المتوسط للتوزيع اللوغاريتمي الطبيعي هو exp(μ + σ2/2)‎؛ أما الانحراف المعياري فهو معقد، ويمكنك الاطلاع عليه من هنا. يوضِّح الشكل السابق دالة التوزيع التراكمي لأوزان البالغين على مقياس خطي في الجهة اليسرى، ومقياس لوغاريتمي في الجهة اليمنى. إذا كانت العيّنة تمثِّل توزيعًا لوغاريتميًا طبيعيًا تقريبًا ورسمتَ دالة التوزيع التراكمي الخاصة بها على مقياس لوغاريتمي على المحور الأفقي -أي محور x-، فستكون له خصائص شكل التوزيع الطبيعي. يمكنك إنشاء رسم احتمالي طبيعي باستخدام لوغاريتم القيم في العيّنة لاختبار ما إذا كان النموذج اللوغاريتمي الطبيعي يناسب العيّنة، كما سنلقي نظرةً على توزيع أوزان البالغين على سبيل المثال والذي يُعَدّ توزيعًا لوغاريتميًا طبيعيًا تقريبًا.2 يُجري المركز الوطني للوقاية من الأمراض المزمنة وتعزيز الصحة مسحًا سنويًا على أساس جزء من نظام مراقبة عوامل المخاطر السلوكية Behavioral Risk Factor Surveillance System -أو BRFSS3 اختصارًا-، حيث قابل المسؤولون عن المسح 414509 مستجيبًا عام 2008 وسألوهم عن معلوماتهم الديموغرافية وصحتهم والمخاطر الصحية التي تحيط بهم، ومن بين المعلومات التي جمعوها هي أوزان 398484 مستجيب مقدرةً بالكيلوغرام. يحوي المستودع repository الخاص بهذا الكتاب ملفًا باسم CDBRFS08.ASC.gz وهو ملف أسكي ASCII -أي الشيفرة القياسية الأمريكية لتبادل المعلومات- ذو عرض ثابت يحتوي على بيانات نظام مراقبة عوامل المخاطر السلوكية، وكذلك يحوي ملفًا باسم brfss.py الذي يقرأ الملف ويحلِّل البيانات. يُظهر الشكل السابق اليساري توزيع أوزان البالغين على مقياس خطي وباستخدام نموذج طبيعي؛ أما الشكل اليميني فيُظهر التوزيع نفسه على مقياس لوغاريتمي وباستخدام نموذج لوغاريتمي طبيعي، كما نلاحظ أنّ النموذج اللوغاريتمي الطبيعي أكثر ملاءمةً للعيّنة، إلا أن هذا التمثيل للبيانات لا يجعل الفرق واضحًا بصورة كبيرة. رسم الاحتمال الطبيعي لأوزان البالغين على مقياس خطي في الجهة اليسرى ومقياسًا لوغاريتميًا في الجهة اليمنى. يُظهر الشكل السابق الرسوم الاحتمالية الطبيعية لأوزان البالغين w ويُظهر الرسوم الاحتمالية الطبيعية للوغاريتماتها log10w‎. وبهذا فقد أصبح من الواضح الآن أنّ البيانات تنحرف انحرافًا كبيرًا عن النموذج الطبيعي، لكن من ناحية أخرى نرى أنّ النموذج اللوغاريتمي الطبيعي يلائم البيانات جيدًا. توزيع باريتو The Pareto distribution سُمي توزيع باريتو Pareto distribution بهذا الاسم نسبةً إلى الاقتصادي فيلفريدو باريتو Vilfredo Pareto الذي استخدَم هذا التوزيع في وصف توزيع الثروات، ولمزيد من التفاصيل يمكنك الاطلاع على توزيع باريتو بويكيبيديا. لقد اعتُمِد هذا التوزيع من ذلك الوقت لوصف الظواهر الطبيعية والاجتماعية بما في ذلك أحجام المدن والبلدات وذرات الرمل والنيازك وحرائق الغابات والزلازل. الصيغة الرياضيّة لدالة التوزيع التراكمي لتوزيع باريتو Pareto distribution هي: CDF(x) =1- xxm- يحدِّد المعامِلَين xm‎ وα الموقع وشكل التوزيع، حيث xm هي أصغر قيمة ممكنة، ويُظهر الشكل التالي دوال التوزيع التراكمي لتوزيعات باريتو Pareto distributions مع افتراض أنّ xm = 0.5 وتجريب عدة قيم مختلفة لـ α. يوضِّح الشكل السابق دوال التوزيع التراكمي لتوزيعات باريتو Pareto distributions بمعامِلات مختلفة. هناك اختبار مرئي بسيط يخبرنا عما إن كان التوزيع التجريبي يلائم توزيع باريتو Pareto distribution أم لا، ويبدو لنا على مقياس لوغاريتمي-لوغاريتمي أنّ دوال التوزيع التراكمي المتمِّمة هي خط مستقيم، وسنرى التطبيق العملي لهذا. y = xxm - بتطبيق اللوغاريتم على الطرفين يكون لدينا: logy ≈ -α(logx - logxm) لذا فإن رسمت logy مقابل logx، فيجب أن يبدو الشكل مستقيمًا مع ميل قدره ‎-‎α ونقطة تقاطع هي α logxm ‎‎‎. سنأخذ أحجام المدن والبلدات على أساس مثال عن هذا، حيث ينشر مكتب الإحصاء الأمريكي The U.S. Census Bureau عدد السكان في كل مدينة وبلدة في الولايات المتحدة. يوضِّح الشكل السابق دوال التوزيع التراكمي المتمِّمة لسكان المدينة والبلدة على مقياس لوغاريتمي-لوغاريتمي أي أنّ المقياس المستخدَم للمحورين الأفقي والعمودي هو لوغاريتمي. يمكن تنزيل بيانات هذا المركز من هنا، كما توجد هذه البيانات في مستودع الكتاب تحت اسم PEP_2012_PEPANNRES_with_ann.csv، ويحتوي هذا المستودع على ملف populations.py الذي يقرأ الملف ويرسم توزيع السكان. يُظهر الشكل السابق وقوع أكبر 1% من البلدات والمدن التي تقل عن ‎10-2‎ على طول خط مستقيم، لذا يمكننا استنتاج أنّ ذيل هذا التوزيع يناسب نموذج باريتو Pareto model وهذا يطابق قول بعض الباحثِين. ينمذِج التوزيع اللوغاريتمي الطبيعي من ناحية أخرى البيانات جيدًا، ويوضِّح الشكل التالي دالة التوزيع التراكمي للسكان ونموذجًا لوغاريتميًا طبيعيًا في الجهة اليسرى، وفي الجهة اليمنى رسمًا احتماليًا طبيعيًا، كما نستنتج أنّ الرسمَين يُظهران ملاءمةً جيدةً بين البيانات والنموذج. لا يُعَدّ كل نموذج لوحده هنا مثاليًا في الواقع، حيث ينطبق نموذج باريتو Pareto model على أكبر 1% من المدن فقط، لكنه أكثر ملاءمةً لهذا الجزء من التوزيع، كما يناسب النموذج اللوغاريتمي الطبيعي بقية البيانات أي ما يقارب 99% من البيانات، أي يعتمد مدى ملاءمة نموذج معيّن للبيانات على جزء التوزيع الذي نهتم بدراسته. يُظهر الشكل السابق دالة التوزيع التراكمي لسكان المدينة والبلدة على مقياس لوغاريتمي على المحور الأفقي x في الجهة اليسرى، ورسم الاحتمال الطبيعي للسكان بعد تطبيق تحويل لوغاريتمي log-transformation على السكان في الجهة اليمنى. توليد أعداد عشوائية يمكن استخدام دوال التوزيع التراكمي التحليلية لتوليد أعداد عشوائية إذا كان لدينا دالة توزيع مُعطاة مسبقًا، حيث أنّ p=CDF(x)، وإذا كانت هناك طريقة فعالة لحساب دالة التوزيع التراكمي العكسية، فسيمكننا توليد قيم عشوائية مع التوزيع المناسب وذلك عن طريق اختيار p من توزيع موحَّد بين 0 و1، ومن ثم اختيار x=ICDF(p). الصيغة الرياضية على سبيل المثال لدالة التوزيع التراكمي للتوزيع الأسي هي: p = 1- e-λx بحل المعادلة بالنسبة لـ x ينتج: x = -log(1-p) / λ كما تكون الشيفرة الموافقة بلغة بايثون: def expovariate(lam): p = random.random() x = -math.log(1-p) / lam return x تأخذ الدالة expovariate معامِلًا هو lam وتُعيد قيمةً عشوائيةً مختارة من التوزيع الأسي مع المعامِل lam. إليك ملاحظتين اثنتين حول هذا التنفيذ: استدعينا المعامِل lam لأن lambda هي كلمة مفتاحية في لغة بايثون، وبما أن log0 هي قيمة غير معرَّفة undefined، فيجب أن نكون حذرين قليلًا. يمكن أن تُعيد الدالة random.random القيمة 0 لكن لا يمكنها إعادة 1، لذا يمكن أن تكون قيمة 1-pهي 1، لكن لا يمكن أن تكون 0، وبالتالي تكون قيمة log(1-p) هي دائمًا معرَّفة. بم تفيدنا النمذجة؟ ذكرنا في بداية المقال أنه يمكن نمذجة العديد من الظواهر التي تحصل في الحياة الواقعية باستخدام النماذج التحليلية، لذا قد تقول: "ماذا إذًا؟" تُعَدّ التوزيعات التحليلية تجريدات مثل حال النماذج الأخرى، أي أنّها تُهمل التفاصيل التي لا علاقة لها بموضوع الدراسة. فمثلًا، قد يكون في التوزيع الملحوظ أخطاء في القياس أو عيوب متعلقة بالعيّنة، فتأتي النماذج التحليلية لتخفيف هذه المشاكل. كما تُعَدّ النماذج التحليلية شكلًا form من ضغط البيانات، حيث أنه عندما يناسب نموذج معيّن لمجموعة بيانات، فسيمكن عندها تلخيص كمية كبيرة من البيانات باستخدام مجموعة صغيرة من المعامِلات. قد نتفاجأ في حال وجدنا أنّ بيانات إحدى الظواهر الطبيعية قد لاءمت توزيعًا تحليليًا، لكن يمكن أن توفِّر لنا هذه الملاحظات معلومات أكثر عن النظم الفيزيائية. يمكننا في بعض الأحيان وصف السبب الذي يجعل توزيعًا ملحوظًا يظهر بشكل معيّن، فغالبًا ما تكون توزيعات باريتو Pareto distributions على سبيل المثال، نتيجةً لعمليات توليدية ذات رد فعل إيجابي أو ما يُعرَف باسم عمليات الإلحاق التفضيلية preferential attachment processes. تصلح التوزيعات التحليلية للتحليل الرياضي كما سنرى في مقال لاحق، لكن من المهم أن نتذكّر أن جميع النماذج غير مثالية، ولا يمكن للبيانات الصادرة من العالم الحقيقي أن تناسب التوزيع التحليلي بصورة مثالية. يتحدث الأفراد في بعض الأحيان كما لو كانت البيانات تُولَّد من نماذج، فقد يسأل البعض مثلًا فيما إذا كان توزيع أطوال البشر طبيعيًا، أو قد يسألون فيما إن كان توزيع الدخل لوغاريتميًا طبيعيًا، وإذا أخذنا الأمور بحَرفيّة فستكون هذه الأقوال غير صحيحة بسبب وجود فروق بين النماذج الرياضية والعالم الحقيقي. تفيد هذه النماذج إذا استطاعت التقاط الجوانب التي تهمنا من العالم الحقيقي وإهمال التفاصيل غير المهمة في آن واحد، لكن "الجوانب التي تهمنا" و"التفاصيل غير المهمة" هي أمور معتمدة على الغرض من استخدام النموذج. تمارين يمكنك الانطلاق من chap05ex.ipynb لحل هذه التمارين، مع العلم أن الحل الخاص بنا موجود في ملف chap05soln.ipynb في مستودع ThinkStats2 على GitHub حيث ستجد كل الشيفرات والملفات المطلوبة. التمرين الأول يُعَدّ توزيع الأطوال في نظام مراقبة عوامل المخاطر السلوكية BRFSS طبيعيًا تقربيًا مع العلم أنّ المعامِلات بالنسبة للرجال هي μ=178 cm وσ = 7.7، وبالنسبة للنساء هي μ=163 cm وσ = 7.3. هناك شرط للانتساب لمجموعة بلو مان وهو أن يكون الفرد ذكرًا طوله بين ''10'5 قدمًا -أي حوالي 177.8 سنتي مترًا- و''1'6 قدمًا -أي حوالي 185.42 سنتي مترًا-. فما هي نسبة الرجال الأمريكيين الذي ينطبق عليهم هذا الشرط؟ التمرين الثاني للتعرف على توزيع باريتو Pareto distribution، سنرى كيف سيكون العالم مختلفًا فيما لو كان توزيع أطوال البشر هو عن باريتو Pareto. يمكننا الحصول على توزيع ذي قيمة صغرى منطقية 1m أي مترًا واحدًا، ووسيط median هو 1.5 m أي متر ونصف، أي في حال كانت المعامِلات هي xm = 1 وα=1.7. ارسم هذا التوزيع وأجب عن الأسئلة التالية: ما هو متوسط mean أطوال البشر في عالم باريتو؟ ما هي نسبة fraction الأشخاص الذين يقل طولهم عن المتوسط؟ إذا كان هناك 7 مليار فرد في عالم باريتو، فكم هو عدد الأفراد الذي من المتوقع أن يزيد طولهم عن كيلومتر واحد؟ كم طول أطول شخص ممكن توقّعه؟ التمرين الثالث يُعَدّ توزيع وايبول Weibull distribution تعميمًا للتوزيع الأسي الذي يظهر في تحليل الفشل، حيث يمكنك الاطلاع على ذلك في ويكيبيديا للمزيد من المعلومات، وتكون الصيغة الرياضية لدالة التوزيع التراكمي لهذا التوزيع بالصورة التالية: CDF(x) = 1 - e-(x/λ)k لكن هل يمكننا إيجاد تحويل ليبدو توزيع وايبول خطًا مستقيمًا؟ علامَ يدل كل من الميل ونقطة التقاطع؟ استخدِم هنا random.weibullvariate لتوليد عيّنة من توزيع وايبول واستخدِمه في اختبار تحويلك. التمرين الرابع لا نتوقع أن يكون التوزيع التجريبي مناسبًا للتوزيع التحليلي بدقة من أجل القيم الصغيرة من n، حيث تتمثل إحدى طرق تقييم جودة الملاءمة في توليد عيّنة من توزيع تحليلي ومن ثم التحقق من مدى ملاءمتها للبيانات. رسمنا مثلًا في القسم الأول -التوزيع الأسي- توزيع الوقت بين الولادات، ورأينا أنه أسي تقريبًا، لكن التوزيع مبني على 44 نقطة بيانات فقط. لنرى ما إذا كانت البيانات قد أتت من توزيع أسي، يجب عليك توليد 44 قيمة من توزيع أسي له متوسط البيانات هذه نفسها، أي بوجود 33 دقيقة بين الولادة والأخرى. وارسم توزيع القيم العشوائية ووازنه مع التوزيع الفعلي، كما يمكنك استخدام random.expovariate لتوليد القيم. التمرين الخامس ستجد في مستودع هذا الكتاب مجموعةً من ملفات البيانات تُدعى mystery0.dat و mystery1.dat وهكذا، حيث يحتوي كل ملف على تسلسل من الأعداد العشوائية المولَّدة من توزيع تحليلي. كما ستجد ملفًا يدعى test_models.py وهو عبارة عن سكربت يقرأ البيانات من ملف ويرسم دالة التوزيع التراكمي مع مجموعة متنوعة من التحويلات، إذ يمكنك تشغيل الملف عن طريق استخدام مثل التعليمة التالية: $ python test_models.py mystery0.dat يمكنك استنتاج نوع التوزيع المولَّد في كل ملف بناءً على الرسوم هذه، لكن إذا كنت في حيرة من أمرك، فيمكنك البحث في ملف mystery.py الذي يحتوي على الشيفرة التي ولَّدت الملفات. التمرين السادس تكون توزيعات الدخل والثروة في بعض الأحيان منمذجةً باستخدام توزيعات باريتو Pareto distributions وتوزيعات لوغاريتمية طبيعية lognormal، ولكي نرى أيّ منها أفضل سنلقي نظرةً على مجموعة من البيانات. يُعَدّ المسح السكاني الحالي Current Population Survey -أو CPS اختصارًا- جهدًا مشتركًا بين مكتب إحصاءات العمل ومكتب التعداد لدراسة الدخل والمتغيرات ذات الصلة. حمّلنا الملف hinc06.xls وهو عبارة عن جدول بيانات spreadsheet إكسل يحوي معلومات عن دخل الأسرة المعيشية household income، وحوَّلناه إلى hinc06.csv وهو ملف ستجده في المستودع الخاص بهذا الكتاب، كما ستجد hinc.py الذي يقرأ الملف السابق. استخرج توزيع مجموعة المداخيل من مجموعة البيانات هذه، وهل يُعَد أيّ توزيع من التوزيعات التحليلية الواردة في هذا المقال نموذجًا جيدًا للبيانات؟ يوجد حل هذا التمرين في الملف hinc_soln.py فستجد كل الشيفرات والملفات في مستودع ThinkStats2. مفاهيم أساسية التوزيع التجريبي exponential distribution: هو توزيع القيم في عيّنة ما. التوزيع الأسي analytic distribution: هو التوزيع الذي تتصف دالة التوزيع التراكمي الخاصة به بأنّها دالة تحليلية. النموذج model: هو تبسيط يقدِّم معلومات مفيدة، حيث غالبًا ما تُعَدّ التوزيعات التحليلية أنها نماذجَ جيدة لتوزيعات تجريبية empirical distributions أكثر تعقيدًا. فاصل الوصول interarrival time: هو الزمن الفاصل بين حدثين اثنين. دالة التوزيع التراكمي المتمِّمة complementary CDF: هي دالة تحوِّل القيمة x إلى كسر من القيم التي تتجاوز x، وتكون صيغتها الرياضية 1-CDF(x). التوزيع الطبيعي القياسي standard normal distribution: هو حالة خاصة من التوزيع الطبيعي تكون قيمة الانحراف المعياري standard deviation فيه هي 1، وقيمة المتوسط mean هي 0. رسم الاحتمال الطبيعي normal probability plot: هو رسم للقيم الموجودة في عيّنة ما مقابل قيم عشوائية من توزيع طبيعي قياسي. ترجمة -وبتصرف- للفصل Chapter 5 Modelling distributions analysis من كتاب Think Stats: Exploratory Data Analysis in Python اقرأ أيضًا التوزيعات الإحصائية في بايثون دوال الكتلة الاحتمالية في جافاسكريبت
  11. قرأ أحد زوّار موقع CSS Tricks مقالًا يشرح للمبتدئين كيفية رفع المواقع على الإنترنت، فطَرَح بعض الأسئلة، خاصةً فيما يتعلّق بالشركات التي تُوفّر خدمات الاستضافة، حيث كانت أسئلته على النحو التالي: وكانت إجاباته على النحو التالي. اختيار الباقة المناسبة لقد ذكرت غودادي GoDaddy أولاً، ولذلك سنبدأ بالحديث عنه؛ سنحاول تقديم معلوماتٍ مفيدةٍ بعد قليلٍ، وسنبدأ بالسؤال الآتي، لماذا نلاحظ أنّ استضافة ووردبريس WordPress أغلى من الاستضافة العادية والتي قد تَبلُغ دولارًا واحدًا في بعض الأحيان؟ وهل تستطيع تثبيت ووردبريس WordPress لو اشتريتَ استضافة ويبٍ بقيمة 5.99 دولارٍ أمريكيٍ؟ أم أن الأنسب اختيار استضافة ووردبريس WordPress مُهيّأةٍ ومُعدّةٍ مُسبقًا؟ وإذا كان ووكوميرس WooCommerce إضافةً plugin تعمل على ووردبريس WordPress، فهل ستستطيع تثبيتها في حال لم تكن الحزمة التي اخترتها خاصةً باستضافة التجارة الالكترونيّة؟ ولماذا لا تستطيع استخدام ووكوميرس WooCommerce في ووردبريس WordPress إلا للحزمة الأغلى ثمنًا؟ ولماذا تُعَد باقة استضافة الخادم الافتراضي الخاص VPS هي الأرخص ثمنًا رغم أنها إحدى خدمات الاستضافة التي تُقدّمها غودادي Godaddy؟ لا داعي للقلق، فهذا محير فعلًا. عمومًا، تربح شركة غودادي GoDaddy مليارات الدولارات سنويًا، ولذا فمن المؤكّد أنهم يفهمون كلّ هذا، ولكنّني شخصيًّا -الكاتب المقال بنسخته الأصلية- أعتمد على التخمين في اختيار باقة الاستضافة المناسبة، ولذلك من الممكن أن تكون هذه الحيرة مقصودةٌ كونها أحد أساليب البيع. التقنية ما ندركه هو أن الباقات التي تحتوي على جميع أنظمة إدارة المحتوى الكبيرة مثل باقة لامب LAMP مخصّصةٌ للمواقع التي تعمل بلغة PHP ومواقع MySQL مثل ووردبريس WordPress وكرافت Craft وبيرش Perch وغوست Ghost ودروبال Drupal وجوملا Joomla، وما إلى ذلك، وكذلك هو الحال فيما يتعلّق بالاستضافات مثل ميديا تيمبل Media Temple وهوسغيتور Hostgator وبلوهوست Bluehost وغيرها، ولذلك يُعتَقد أن توصيف شركات الاستضافة هذه بالتقليديّة، صفةٌ مناسبة. هل تريد تشغيل نظام إدارة البيانات PostgreSQL أو MariaDB بدلاً من MySQL؟ أو تريد العمل مع ASP بدلاً من لغة البرمجة PHP؟ يُزعم أنّ (أغلب) شركات الاستضافة ستجيبك بشيء من هذا القبيل عن كلّ هذه المتطلبّات، إذ ستَقترح عليك استخدام الخادم الافتراضي الخاص VPS والذي يتمتع بوصولٍ مباشرٍ إلى الجذر root، بحيث يمكنك تثبيته بنفسك بدلًا من استخدام الاستضافة المشتركة، وفي الحقيقة، لا يُعتَقَد أن هذا جيدًا بما يكفي، ومن المهم أن تنتبه إلى أنها ليست أفضل الخيارات لأنه من الصعب الحصول على دعمٍ فنيٍ جيدٍ في حال واجهتك مشكلةٌ ما. وهو ما يقودني إلى وجهة النظر القائلة بوجوب تَتبُّع عروض الطريق السعيد التي تُقدّمها شركات الاستضافة. ولنفترض رغبتنا في تطوير تطبيقٍ بلغة بايثون Python، عندها لن نشتري خادم هوستغيتور Hostgator، فعلى الرغم أن التطبيق سيعمل بصورةٍ جيدةٍ، إلا أنها قليلًا ما تُروّج لاستضافة تطبيقات بايثون، ولذلك لا يبدو أنه الطريق السعيد؛ بينما تُروّج له هيروكو Heroku كثيرًا. لا يمكننا ترجيح هيروكو Heroku لأنني لم أستخدمها من قبل، ولكننا نسمع الكثير من الآراء الإيجابية مِمّن يستخدمونها منذ 15 عامًا. تُذكّرنا هيروكو بوجود فجوةٍ أخرى مهمّةٍ بين شركات الاستضافة، وهي أنّ شركات استضافة الويب التقليديّة لا تساعدك في نَشْر موقعك، وإنما تعطيك بيانات اعتماد بروتوكول نَقْل الملفات FTP فحسب، ثُم تقول لك حظًّا سعيدً؛ أما بالنسبة لشركات الاستضافةٍ مثل هيروكو Heroku، فهي تقدّم لك واجهة سطر الأوامر مع أوامرٍ مساعدةٍ مثل heroku container:push، والتي تمكّنك من نشر الكود المحلي الخاص بك في الموقع مباشرةً، بل وتقدّم لك ما هو أفضل من ذلك، إذ تساعدك هيروكو Heroku على نشر الموقع مباشرةً من مستودع غيت هاب GitHub، فلماذا لا تَفعَل شركات الاستضافة كلّها هذا؟ يا له من لغزٍ بالنسبة لي، وذلك لأنّ المُضيف الذي يساعدك في النّشْر هو مضيفٌ لا يُقدّر بثمنٍ. حتى كُنا نتحدث عن الطرق السعيدة، وتُطلِق هيروكو على نفسها اسم منصة التطبيقات السحابية، أي أنّ الطريق السعيد في هيروكو هي لغات المُخدّم مثل نود Node وروبي Ruby وبايثون Python وجو Go، ولكن ماذا لو لم تكن بحاجةٍ لأي من ذلك؟ فلنفترض أنك ستُنشِئ موقعًا ثابتًا باستخدام أداةٍ خاصّةٍ بإنشاء المواقع الثابتة مثل إيليفينتي Eleventy، أي مواقع جامستاك Jamstack، فهل يُفضّل اختيار هيروكو Heroku عندها؟ طبعًا لا، فهيروكو هنا ليست الحل الأمثل، فعلى الرغم من عمل الموقع عليها بصورةٍ جيدةٍ، إلا أن استضافة المواقع الثابتة ليست جَوهَر هيروكو، وبالتالي فهي ليست طريقًا سعيدًا. إذًا أين يُفضّل أن تستضيف موقعًا ثابتًا؟ الإجابة هنا هي أن نيتليفاي Netlify أفضل بكلّ تأكيد، فهذا هو اختصاصها، وهي الطريق الأسعد للمواقع الثابتة. وقد أجادت نيتليفاي Netlify باستضافة جامستاك Jamstack لدرجة أن الكثير من الشركات حاولت تقديم عروضًا مماثلةً، مثل آزور Azure لتطبيقات الويب الثابتة. فلماذا إذًا تستخدم آزور وليس نيتليفاي Netlify؟ ربما يبدو طريقًا سعيدًا وربما كنت تستخدم منتجات Azure الأخرى واعتقدتَ لهذا السبب أن جميع منتجاتها جيّدةً، ففي الواقع، آزور هي منصّةٌ سحابيةٌ ضخمةٌ تُوفّر الكثير من العروض والخدمات الأخرى، وقد تكون معجبًا بمنتجات مايكروسوفت مثل كثيرٍ من المطورين وسنتحدث عن هذا لاحقًا. أما جامستاك Jamstack، فهي للاستضافة الثابتة بالإضافة إلى الخدمات الأخرى، وتُتاح في الكثير من الأماكن الآن. تحتوي كلاودفلير Cloudflare على كلاودفلير بيجز Cloudflare Pages، وهي خدمةٌ يمكنك الاستفادة منها عن طريق استخدام العروض غير المحدودة مثل استضافة عددٍ غير محدودٍ من المواقع غير المحدودة، والطلبات غير المحدودة، والسّعات غير المحدودة، وخدمة عددٍ غير محدودٍ من أفراد الفِرق البرمجيّة. قد تختار كلاودفلير بيجز Cloudflare Pages لأن منتجات كلاودفير مثل العمّال أو الوصول السهل هي أمورٌ مهمةٌ بالنسبة لك، ولذلك قد تشعر بأنها الطريق السعيد المناسب لك. كما تمتلك فيرسيل Vercel استضافة جامستاك Jamstack كذلك، وستقدّم لك مُخدّماتٍ إذا كنت تحتاجها، حيث إن إطار عملها نيكست جافاسكربت Next.js وصفحاتها مُجّهزةٌ مُسبقًا، كما أنها تُقدّم صفحاتٍ من جانب المُخدمّ بلغة البرمجة نود Node، فتعطيك فيرسيل Vercel لغة الواجهة الخلفيّة back end كذلك. أي أن إطار العمل نيكست جافا سكربت Next.js هو الطريق السعيد بالنسبة لاستضافة فيرسيل Vercel، فهو يدعم النّشْر على النظام الأساسي المُصمّم للإطار Next.js، ومن الصعب التَغلّب على ذلك. تمتلك استضافة AWS Amplify كذلك استضافة جامستاك Jamstack، والطريق السعيد هنا هو استخدام أمبليفاي Amplify لتجميع خدماتٍ مثل AWS خدمات ويب أمازون، فهذا هو الهدف من AWS Amplify. هل تحتاج إلى مصادقةٍ؟ أمازون كوغنيتو Amazon Cognito هي من تنجز العمل من وراء الكواليس، بينما يساعد Amplify على ربط المصادقة بعملك الحاليّ. هل تحتاج إلى تخزينٍ؟ تُعدّ S3 معيارًا صناعيًّا وسيساعدك Amplify على دمجِه. هل تحتاج إلى قاعدة بياناتٍ؟ سيساعدك Amplify على تصميم قاعدة البيانات وبناء واجهات برمجة التطبيقات API. وتمتلك منصة فايربيس Firebase استضافة على غرار Jamstack، والطريق السعيد هنا هو في الغالب إطار العمل Firebase. وتمتلك منصة فايربيس Firebase الكثير من الميزات المفيدة مثل تخزين البيانات في الزمن الحقيقي، بالإضافة إلى المصادقة وتحليلات المراقبة الحقيقية للمستخدم RUM، لكن إذا لم نكن قد استخدمنا أيًا من هذه الأشياء فمن غير المرجح غالبًا اختيارنا لاستضافة فايربيس Firebase، فعلى سبيل المثال، هل من المناسب استخدام فايريبس Firebase لمدونةٍ بسيطةٍ مثل مدونة Jekyll؟ بالطبع لا، لأن فايربيس Firebase لا تُروّج لذلك فهي ليست بالطريق السعيد. ويجدر الحديث عن ذاكرة المطور العضليّة، فأنت بصفتك مطوّرًا تبني ذاكرةً عضليةً للأشياء التي تفعلها بتكرارٍ، فإن استضفتَ خمسة مواقعٍ على Netlify وأنجزت خطوات الاستضافة مِرارًا وتكرارًا، فمن المنطقي أن يكون موقعك السادس موجودًا على نيتليفاي Netlify أيضًا، حتى لو كان هناك مضيفٌ آخرٌ يناسب موقعك الجديد أكثر من نيتليفاي Netlify. وذلك لأن معرفة أدواتك معرفةٌ جيّدةٌ وشعورك بالارتياح أمران مهمّان، حيث يمكنك موازنة الأسعار والميزات وكل التفاصيل الصغيرة، ولكنّ ذاكرتك العضليّة هي إحدى أقوى تأثيرات الاختيار ولا يُعتَقد بأن هذا أمرٌ سيّءٌ. يجب أن يعتني مضيفك باحتياجاتك الأساسية هل تتذكر عندما قلنا من قبل أن المُضيف الذي يساعدك في النشر مُضيفٌ قيّمٌ جدًا؟ إليك الأخبار الجيّدة، سيساعدك بالنشر كلٌ من نيتليفاي Netlify وفيرسيل Verce و AWS Amplify وغوغل فايربيس Google Firebase وكلاودفير بيجز Cloudflare Pages وآزور Azure للصفحات الثابتة، فلقد أصبحت هذه أقلّ ما يمكن أن يقدّمه المُضيفون، كما توجد هناك المزيد من الخدمات الأخرى. وفيما يلي الخدمات الأساسيّة لكل المُضيفات الحديثة بالإضافة إلى استضافة المواقع بالطبع. HTTPS: إذ يجب على المُضيف منح موقعي شهادة SSL تلقائيًا أو حتى مجانًا، وذلك نظرًا لأن خدمة Let’s Encrypt مجانيّة، فهي تُوفّر شهادات أمانٍ مجّانًا. CDN: وهي شبكة توصيل المحتوى، حيث يجب أن يساعد المُضيف في تخديم موقعي بالإضافة إلى خدمات توصيل المحتوى، حتى لو كانت ميزةً مدفوعةً أو تتطلب التهيئة المُسبقة. Deployment: وهو النشر، حيث يجب على المُضيف الاتصال بمستودعات غيت Git، ونقل الملفات من المستودع الرئيسي إلى الموقع المباشر. Staging: فعلى المُضيف توفير بيئاتٍ لتهيئة الملفات للنقل من مرحلة التطوير إلى مرحلة الإنتاج. وبالعودة إلى ووردبريس WordPress وغيره من أنظمة إدارة المحتوى العاملة بلغات PHP وMySQL، فغالبًا ما تُوفّر المُضيفات التقليديّة استضافة هذه المواقع، حيث يُمثّل ووردبريس 35.2٪ من مواقع الويب، وهذا رقمٌ كبيرٌ جدًا، مما يعني أنه هناك الكثير من الأموال الناتجة عن استضافة ووردبريس WordPress، لكن من واقع الخبرة لكريس كوير Chris Coyier يمكن القول بأن المُضيفين التقليديين لا يُقدّمون أيًّا من الخدمات التي ذُكِرت أعلاه رغم أنّها الخدمات الأساسيّة لكل المُضيفات الحديثة، ففي كثيرٍ من الأحيان قد تضطر إلى استخدام HTTPS ودمج شبكة توصيل المحتوى ونشر الموقع بنفسك، كما سيتوجّب عليك شراء مخدّمٍ آخرٍ لإجراء عملية staging، ومع وجود فجوةٍ واسعةٍ في مُضيفي الويب الحديثة التي تُقدّم ما لا تقدّمه المُضيفات التقليديّة، أصبح مجال الاستضافة غريبٌ جدًا. ولكنّ هذا لا ينطبق على كلّ مُضيفات ووردبريس، حيث يُعدّ استخدام مُضيفٍ خاصٍ بووردبريس لاستضافة موقع ووردبريس بمثابة طريقٍ سعيدٍ، لكن يمكن استخدام فلاي وييل FlyWheel مثلًا، إذ يقدّمون الميّزات الأساسيّة بالإضافة إلى أمورٍ أخرى، كما أنّهم يساعدون في التطوير المحليّ. ديجيتال أوشن Digital Ocean يقول Chris Coyier: لدى هذه المنصّة مفهوم دروبلت Droplet والترجمة الحرفيّة لهذه الكلمة هي قطرات المياه، ولكنّها في هذا السياق تعني مُخدّم، وهي مكوّنةٌ من حاوياتٍ، أو لنَقُل بأنّها مُخدّماتٌ مُعدةٌ مسبقًا وقادرةٌ على تشغيل جميع أنواع التقنيّات، فإن كنت تريد تشغيل لامب LAMP في Droplet، فسيكون طريقًا سعيدًا، ولكن هناك الكثير من التقنيات الأخرى كذلك. ولنأخذ المثال التالي، فإذا كنت تريد استخدام سترابي Strapi وهو نظام إدارة محتوىً يتكون من نود Node وNginx وPostgreSQL، فتستطيع منصة ديجيتال أوشن Digital Ocean تقديم droplet جاهزةٍ لذلك. تبدأ أسعار droplets من 5 دولاراتٍ شهريًا، لذا فهي اقتصاديةٌ مثل الاستضافات الأخرى تمامًا إذا لم تكن أكثر منهم، وقد تجد منتجات استضافةٍ متنوعةً في ظاهرها، بينما هي ديجيتال أوشن في حقيقتها. فعلى سبيل المثال، تتيح أداة استضافة SpinupWP إنشاء بيئات استضافة ووردبريس مهيأةً بسرعةٍ ولكنها لا تتكفّل بالاستضافة نفسها لأنّك قد تجلب مضيفك الخاص، والذي قد يكون ديجيتال أوشن أو خدمات ويب أمازون Amazon Web Services واختصارًا AWS. تعقيد الخدمات المقدمة وإذا بدا ديجيتال أوشن Digital Ocean معقدًا، فسترى أنّ خدمات ويب أمازون AWS أكثر تعقيدًا، فلقد تحدثنا عن AWS Amplify مسبقًا، فهو يشْبِه AWS المُصمّم للمطورين الأفراد لاستضافة التطبيقات بسرعةٍ، وهو رائعٌ جدًا لكنه جزءٌ صغيرٌ مما يُمثّل AWS. إذًا AWS هو مزود خدماتٍ سحابيةٍ ضخمٍ يمكنه تشغيل مخدمات ويبٍ، ولكنه يُوفّر المئات من الخدمات الأخرى مثل قواعد قواعد البيانات والتخزين والعمليات عديمة المُخدمّات serveless وواجهات برمجة التطبيقات API والسجلات، وكثيرٍ من الأمور الأخرى، حتى أنّه يمكنك استئجار حاسوب كميّ تمامًا مثل ما يُعرض في الخيال العلمي، لكن هذا لا يعني أنه لا يمكن للمطور العادي استخدام AWS لاستضافة الويب، إلا أنه غير مُصممٍ لهذا حسب تجربة المطوّرين. يمكنك الاطلاع على كيفية تثبيت ووردبريس WordPress على AWS ، حيث تُعَد AWS قويةً للغاية ولديها حلولٌ لكل شيءٍ وبأسعارٍ منخفضةٍ جدًا، ومن المفيد النظر إلى AWS على أنها بِنيةٌ أساسيةٌ بسيطةٌ للويب لكنها مصممةٌ للعمليات واسعة النطاق كذلك، فقد يُنشَأ مُضيفي الويب على AWS. مطابقة احتياجاتك مع ما هو متاح سنستعرض بعض النتائج السريعة لنُطابِق الاحتياجات مع الخيارات المتاحة، لكنّه ليس جدولًا شاملًا بل وُضِع فيه ما ما يذكر عادةً، وما يبدو بأنّه طريقٌ سعيد. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } تقليدية اقتصادية WordPress MediaTemple وGoDaddy Flywheel وWP Engine Other PHP + MySQL (مثل Craft CMS) Bluehost Cloudways وfortrabbit Ruby on Rails Linode Heroku Node.js ضعه في Lambda Digital Ocean Python Vercel Heroku Go Vercel Cloud Run Jamstack GitHub Pages Netlify وCloudflare Pages GraphQL API Hasura AWS Amplify وAppSync Image Storage S3 Cloudinary ومن المهم التذكير بوجود الكثير من القواسم المشتركة في مجال الاستضافة، فلنفترض أنك تريد استضافة ملف index.html علمًا بأن هذا هو موقعك بالكامل. في الواقع، يمكن لأيّ مضيفٍ أن يفعل ذلك، حيث أنّ مهمة شركات استضافة الويب هي استضافة الملفّات عن طريق تشغيل الشيفرة البرمجية الموجودة بداخلها، وهي غير مختلفةٍ كثيرًا عن بعضها البعض، لذلك علينا الإجابة عن الأسئلة التالية، هل سيُشغّل ما أحتاجه؟ هل التعامل معه مباشرٌ وواضحُ؟ هل سيساعدك في تسهيل العمل؟ هل سيُقدّم الدعم المناسب؟ وأهم شيء هل هذا هو الطريق السعيد؟ أسعار الاستضافات لم نتحدث كثيرًا عن السعر على الرغم من أهميته الكبيرة للكثيرٍ من الناس، ولكن من الصعب التحدث عنه دون معرفة احتياجاتك، حيث لا يُراد أن يتخذ أي شخصٍ قراراتٍ مهمّةٍ بشأن أمورٍ مثل استضافة الويب بناءً على الفَرق في بضعة دولاراتٍ في الشهر، لأنّك إن أمضيتَ نصف ساعةٍ في محاولة اكتشاف مشكلةٍ وحلّها وكان باستطاعة استضافةٍ أغلى ثمنًا حلّها، حينها ستهدر فرق السعر المُكتسَب. تُعَد استضافة الويب أشبه بسوقٍ لبيع السلع، حيث تستَقّر أسعاره إلى حدٍ ما، وإذا كان المُضيف يبدو باهظ الثمن، فربما لأنه يقدم الكثير من الخدمات والميّزات، وإذا بدا رخيصًا فقد تشعر بهذا لاحقًا بسبب قلة الميزات أو سوء الخدمة وما إلى ذلك. فإذا كان لديك موقعٌ صغيرٌ للأطفال يُحتَمل أنك ستستضيفه مجانًا؛ أما إذا بدأ الموقع بالتوسّع، فستشعر حينها أن تكاليف الاستضافة ضئيلةٌ وعادلةٌ. مهلًا، هل يبدو كل هذا ممتعًا ومسليًّا لك؟ إذا كانت إجابتك نعم، فربما عليك التفكير في مهنةٍ في مجال البرمجيات بحيث تتعامل مع المُخدمات والنشر والبِنية التحتية، فقد لا يكون المطور الداعم مهمَّةً جانبيةً لأعمال التطوير الأخرى بل قد يكون مهمةً كاملةً. ترجمة -وبتصرف- للمقال The Differences in Web Hosting (Go with the Happy Path) من موقع CSS Tricks. اقرأ أيضًا مساعدة المبتدئين في فهم كيفية رفع الموقع على الإنترنت دليل إعداد خادم ويب محلي خطوة بخطوة كيفية تثبيت برمجية Jenkins على خادوم أوبنتو 16.04 كل ما تود معرفته عن السحابة الهجينة Hybrid Cloud
  12. نختم مناقشتنا لجودة الخدمة من خلال العودة إلى التحكم في ازدحام بروتوكول TCP، ولكن هذه المرة في سياق التطبيقات في الوقت الحقيقي. تذكر أن بروتوكول TCP يضبط نافذة ازدحام المرسل وبالتالي معدل الإرسال استجابةً لأحداث الإشعار ACK والمهلة timeout. تتمثل إحدى نقاط القوة في هذا الأسلوب في أنه لا يتطلب التعاون من موجّهات الشبكة، فهي استراتيجيةٌ تعتمد على المضيف فقط. وتكمّل هذه الاستراتيجية آليات جودة الخدمة التي كنا ندرسها لسببين، أولهما امكانية التطبيقات استخدام الحلول المستندة إلى المضيف دون الاعتماد على دعم الموجّه، وثانيًا أنه حتى مع نشر DiffServ بالكامل، فلا يزال من الممكن أن يكون لرتل الموجه زيادةٌ في عدد المشتركين، ونود أن تتفاعل التطبيقات في الوقت الحقيقي بطريقة معقولة في حالة حدوث ذلك. نرغب في الاستفادة من خوارزمية التحكم في الازدحام في TCP، ولكن بروتوكول TCP نفسه ليس مناسبًا لتطبيقات الوقت الحقيقي، نظرًا لأن بروتوكول TCP هو بروتوكولٌ موثوق، ولا تستطيع التطبيقات في الوقت الحقيقي تحمُّل التأخيرات الناتجة عن إعادة الإرسال. ولكن ماذا لو فصلنا بروتوكول TCP عن آلية التحكم في الازدحام الخاصة به من أجل إضافة آلية تحكم في الازدحام مثل الموجودة في بروتوكول TCP إلى بروتوكولٍ غير موثوقٍ به مثل بروتوكول UDP؟ هل يمكن لتطبيقات الوقت الحقيقي الاستفادة من مثل هذا البروتوكول؟ هذه فكرةٌ جذابة لأنها قد تتسبب في تنافس تدفقات الوقت الحقيقي بصورةٍ عادلة مع تدفقات TCP، لكن البديل الذي يحدث اليوم هو أن تطبيقات الفيديو تستخدم بروتوكول UDP بدون أي نوعٍ من أنواع التحكم في الازدحام، ولذلك يُسلب حيز النطاق التراسلي بعيدًا عن تدفقات TCP التي تتراجع في وجود الازدحام. إن سلوك مسننة المنشار sawtooth لخوارزمية التحكم في الازدحام في بروتوكول TCP غير مناسبٍ للتطبيقات في الوقت الحقيقي؛ وهذا يعني أن معدل إرسال التطبيق يتزايد وينخفض باستمرار، ولكن تعمل تطبيقات الوقت الحقيقي بصورةٍ أفضل عندما تكون قادرةً على الحفاظ على معدل نقل سلس على مدى فترةٍ زمنيةٍ طويلة نسبيًا. هل من الممكن تحقيق الأفضل -أي التوافق مع التحكم في ازدحام بروتوكول TCP من أجل العدل- مع الحفاظ على معدل إرسالٍ سلس لصالح التطبيق؟ يشير العمل الأخير إلى أن الإجابة هي نعم. اُقترِحت العديد من خوارزميات التحكم في الازدحام الصديقة لبروتوكول TCP، وهذه الخوارزميات لها هدفان رئيسيان، أولهما هو التكيف ببطء مع نافذة الازدحام وذلك عن طريق التكيف على فتراتٍ زمنية أطول نسبيًا مثل RTT وليس على أساس كل رزمة، فيخفف ذلك من معدل الإرسال؛ أما الهدف الثاني فهو أن تكون صديقةً لبروتوكول TCP بمعنى أنها عادلة لتدفقات بروتوكول TCP المنافسة، حيث تُفرض هذه الخاصية من خلال التأكد من أن سلوك التدفق يلتزم بمساواةٍ تشكل نموذجًا لسلوك TCP. يُطلق على هذا النهج أحيانًا التحكم في الازدحام المعتمد إلى المساواة. لقد رأينا نموذجًا مبسطًا من مساواة معدل TCP سابقًا. ويكفي ملاحظة أن المساواة تأخذ هذا النموذج العام: والذي ينص على أنه لكي تكون صديقًا لبروتوكول TCP، يجب أن يكون معدل الإرسال متناسبًا عكسيًا مع الوقت ذهابًا وإيابًا RTT ومع الجذر التربيعي لمعدل الخسارة ρ. ولبناء آلية للتحكم في الازدحام خارج هذه العلاقة، يجب على المستلم الإبلاغ دوريًا عن معدل الخسارة الذي يواجهه إلى المرسل، فقد يبلّغ عن فشل في تلقي 10% من آخر 100 رزمة على سبيل المثال، ثم يعدّل المرسل معدل الإرسال لأعلى أو لأسفل، بحيث تستمر هذه العلاقة في الصمود. لا يزال الأمر متروكًا للتطبيق للتكيف مع هذه التغييرات في المعدل المتاح، ولكن العديد من تطبيقات الوقت الحقيقي قابلةٌ للتكيف تمامًا. هندسة حركة المرور المعرفة بالبرمجيات Software-Defined Traffic Engineering تتمثل المشكلة الشاملة التي يتناولها هذا مقال في كيفية تخصيص حيز النطاق التراسلي للشبكة المتاح لمجموعةٍ من التدفقات من طرفٍ إلى طرف. سواءٌ كان الأمر مُتعلق بالتحكم في ازدحام بروتوكول TCP أو الخدمات المتكاملة أو الخدمات المميزة، وهناك افتراضٌ بأن حيز النطاق التراسلي للشبكة الأساسي المخصَّص ثابت؛ فرابطٌ بسرعة 1 جيجابت في الثانية بين الموقع A والموقع B هو دائمًا رابطٌ بسرعة 1 جيجابت في الثانية، وتركّز الخوارزميات حول أفضل طريقةٍ لمشاركة 1 جيجابت في الثانية بين المستخدمين المتنافسين. ولكن ماذا لو لم يكن الأمر كذلك؟ ماذا لو كان بإمكانك الحصول على سعة إضافية "على الفور" بحيث يجري ترقية الرابط بسرعة 1 جيجابت في الثانية إلى رابطٍ بسرعة 10 جيجابت في الثانية؟ أو ربما يمكنك إضافة رابطٍ جديد بين موقعين غير متصلين من قبل؟ هذا الاحتمال حقيقي وهو موضوعٌ يُشار إليه عادةً باسم هندسة حركة المرور traffic engineering، وهو مصطلحٌ يعود إلى الأيام الأولى للشبكات عندما حلّل المُشغلون أعباء العمل المرورية على شبكتهم، وأعادوا هندسة شبكاتهم دوريًا لإضافة سعةٍ عندما أصبحت الروابط الحالية مثقلةً جدًا. لم يُتخذ قرار إضافة سعةٍ على محمل الجد في تلك الأيام الأولى، حيث كنت بحاجةٍ إلى التأكد من أن اتجاه الاستخدام الذي لاحظته لم يكن مجرد صورةً عابرة لأنه سيستغرق قدرًا كبيرًا من الوقت والمال لتغيير الشبكة، فقد يتضمن ذلك مد كابل عبر محيطٍ أو إطلاق قمرٍ صناعي في الفضاء في أسوأ الحالات. ولكن مع ظهور تقنيات مثل DWDM وMPLS الموصوفتين سابقًا، لا يتعين علينا دائمًا وضع المزيد من الألياف، ويمكننا بدلًا من ذلك تشغيل أطوالٍ موجيةٍ إضافية أو إنشاء داراتٍ جديدة بين أي زوج من المواقع. ليس من الواجب ربط هذه المواقع مباشرةً بالألياف، فقد يمر الطول الموجي بين بوسطن وسان فرانسيسكو عبر ROADM في شيكاغو ودنفر على سبيل المثال، ولكن ترتبط بوسطن وسان فرانسيسكو عبر رابطٍ مباشر من منظور مخطط شبكة L2 / L3، ويقلّل ذلك من وقت الوصول التوافرية بصورةٍ كبيرة، ولكن لا تزال إعادة ضبط العتاد تتطلب تدخلًا يدويًا، وبالتالي لا يزال تعريفنا لمصطلح "على الفور" يُقاس بالأيام إن لم يكن بالأسابيع، وهناك أيضًا استمارات طلبٍ يجب ملؤها ضمن ثلاث نسخ. ولكن كما رأينا مرارًا وتكرارًا، يمكن استخدام البرنامج للتعامل مع المشكلة بمجرد توفير واجهات برمجية مناسبة، ويمكن لمصطلح "على الفور" أن يكون فوريًا حقًا، وهذا هو ما يفعله مزودو الخدمات السحابية بفعالية مع الشبكة الرئيسية Backbone الخاصة المُنشأة لربط مراكز البيانات الخاصة بهم. فعلى سبيل المثال، وصفت Google علنًا شبكة WAN الخاصة المُسماة B4، والتي أنشِأت بالكامل باستخدام مبدلات الصندوق الأبيض وSDN، حيث لا تضيف أو تُسقِط B4 أطوالًا موجيةً من أجل ضبط حيز النطاق التراسلي بين العقد، فهي تبني أنفاقًا من طرفٍ إلى طرف ديناميكيًا باستخدام تقنية تسمى المسارات المتعددة ذات التكلفة المتساوية Equal-Cost Multipath أو اختصارًا ECMP، وهي بديلٌ عن CSPF الموصوفة سابقًا وتوفّر نفس المرونة. يزوّد برنامج التحكم في هندسة حركة المرور TE بعد ذلك الشبكة وفقًا لاحتياجات أصنافٍ التطبيقات المختلفة. يحدد B4 ثلاثة أصناف من هذا القبيل: نسخ بيانات المستخدم مثل البريد الإلكتروني والمستندات والصوت / الفيديو إلى مراكز البيانات البعيدة لتحقيق التوافرية. الوصول إلى التخزين البعيد عن طريق الحسابات التي تعمل عبر مصادر البيانات الموزعة. دفع البيانات واسعة النطاق لمزامنة الحالة عبر مراكز بيانات متعددة. تُرتَّب هذه الأصناف حسب زيادة الحجم وتقليل حساسية وقت الاستجابة وتقليل الأولوية الإجمالية، حيث تمثل بيانات المستخدم أقل حجم ضمن B4، والأكثر حساسيةٍ لوقت الاستجابة، وذات الأولوية العليا. تمكنت Google من خلال مركزية عملية صنع القرار -وهي إحدى مزايا SDN المزعومة-، من زيادة استخدامية الروابط الخاصة بها إلى ما يقرب من 100%، وهذا أفضل بمرتين إلى ثلاث مرات من متوسط الاستخدام الذي يبلغ 30-40% والتي توفرها روابط WAN لها عادةً، وهو أمرٌ ضروري للسماح لهذه الشبكات بالتعامل مع كلٍ من رشقات حركة المرور وفشل رابط أو مبدّل. إذا كان بإمكانك تحديد كيفية تخصيص الموارد مركزيًا عبر الشبكة بالكامل، فمن الممكن تشغيل الشبكة بصورةٍ أقرب إلى أقصى حدٍ من الاستخدامية. ضع في حساباتك أن توفير الروابط في الشبكة يكون لأصناف التطبيقات القاسية coarse-grain، وأنه لا يزال التحكم في ازدحام بروتوكول TCP يعمل على أساس اتصالٍ تلو الآخر، ولا تزال قرارات التوجيه تُتخذ على مخطط B4. تجدر الإشارة إلى أنه نظرًا لأن B4 عبارة عن شبكة WAN خاصة، فإن Google حرةٌ في تشغيل خوارزمية التحكم في الازدحام الخاصة بها مثل BBR دون الخوف من حدوث ضررٍ غير عادل للخوارزميات الأخرى. أحد الدروس التي يمكن استخلاصها من أنظمةٍ مثل B4 هو أن الخط الفاصل بين هندسة حركة المرور والتحكم في الازدحام وكذلك بين هندسة حركة المرور والتوجيه غير واضح، وهناك آلياتٌ مختلفة تعمل على معالجة نفس المشكلة العامة، وبالتالي لا يوجد خطٌ ثابتٌ ومتشددٌ يقول أين تتوقف إحدى الآليات وتبدأ الأخرى، حيث تصبح حدود الطبقة لطيفة ويسهل نقلها عند تطبيق الطبقات ضمن البرمجيات بدلًا من العتاد. ترجمة -وبتصرّف- للقسم Quality of Service من فصل Congestion Control من كتاب Computer Networks: A Systems Approach. اقرأ أيضًا المقال السابق: الخدمات المميزة لتطبيق جودة الخدمة ضمن الشبكات الحاسوبية التحكم المتقدم في الازدحام في الشبكات الحاسوبية من خلال الطرق القائمة على المصدر التحكم المتقدم في الازدحام في الشبكات الحاسوبية باستخدام إدارة الأرتال النشطة التحكم في الازدحام باستخدام بروتوكول TCP في الشبكات الحاسوبية أنظمة الأرتال المستخدمة في التحكم بازدحام الشبكات الحاسوبية
  13. تخصّص معمارية الخدمات المتكاملة المواردَ للتدفقات الفردية، بينما يخصّص نموذج الخدمات المميزة Differentiated Services والمُسمى اختصارًا DiffServ الموارد لعددٍ صغير من فئات حركة المرور. إذا فكرت في الصعوبة التي يواجهها مشغلو الشبكات لمجرد محاولة الحفاظ على تشغيل الإنترنت بأفضل جهدٍ بسلاسة، فمن المنطقي أن تضيف إلى نموذج الخدمة زياداتٍ صغيرة. لنفترض أننا قررنا تحسين نموذج الخدمة بأفضل جهد من خلال إضافة صنفٍ جديد واحدٍ فقط، والذي سنطلق عليه اسم "تفضيلي premium". من الواضح أننا سنحتاج إلى طريقةٍ ما لاكتشاف الرزم التفضيلية عن رزم أفضل جهد قديمة اعتيادية، وسيكون من الأسهل كثيرًا إذا تمكنت الرزم من التعريف عن نفسها للموجّه عند وصولها، بدلًا من استخدام بروتوكول مثل بروتوكول RSVP لإخبار جميع الموجهات بأن بعض التدفقات ترسل رزمًا تفضيلية، ويمكن ذلك باستخدام بتٍ في ترويسة الرزمة، فإذا كان هذا البت هو 1، فإن الرزمة هي رزمة مميزة؛ وإذا كانت قيمة البت 0، فإن الرزمة هي رزمة أفضل جهد. لكن هناك سؤالان نحتاج إلى معالجتهما هما: من الذي يحدّد البت المميز وتحت أي ظروف؟ ما الذي يفعله الموجّه بصورةٍ مختلفة عندما يرى رزمة مع ضبط هذا البت؟ هناك العديد من الإجابات المحتملة على السؤال الأول، ولكن هناك نهجٌ شائع يتمثل في ضبط البت عند الحدود الإدارية، فقد يضبط الموجهُ الموجود على حافة شبكة مزود خدمة الإنترنت البتَ للرزم التي تصل إلى الواجهة التي تتصل بشبكةِ شركةٍ معينة على سبيل المثال، وقد يفعل مزود خدمة الإنترنت ذلك لأن هذه الشركة قد دفعت مقابل مستوى خدمةٍ أعلى من أفضل جهد. من الممكن أيضًا ألا تكون جميع الرزم تفضيلية؛ فعلى سبيل المثال قد يُضيَط الموجه لوضع علامةٍ على الرزم بأنها رزمٌ تفضيلية لتلك التي تصل إلى الحد الأقصى للمعدل وترك كل الرزم الزائدة رزمًا بأفضل جهد على سبيل المثال. بافتراض أنه وُضِعت علامات لتمييز الرزم بطريقةٍ ما، فماذا تفعل الموجهات بهذه الرزم المميزة بعلامة التي تواجهها؟ هناك العديد من الإجابات. وحّدت منظمة IETF مجموعةً من سلوكيات الموجّه لتطبيقها على الرزم المميزة بعلامة يُطلق عليها سلوكيات كل قفزة per-hop behaviors أو اختصارًا PHBs، وهو مصطلحٌ يشير إلى أنها تحدّد سلوك كل موجهٍ بدلًا من خدمات طرفٍ إلى طرف. بما أن هناك أكثر من سلوكٍ جديد، فتوجد حاجةٌ أيضًا إلى أكثر من 1 بت في ترويسة الرزمة لإخبار الموجهات بالسلوك الذي يجب تطبيقه. قررت IETF أخذ بايت TOS القديم من ترويسة IP، والذي لم يُستخدَم على نطاقٍ واسع، مع إعادة تعريفه، كما جرى تخصيص ستة بتات من هذا البايت لنقاط شيفرة DiffServ أو اختصارًا DSCPs، حيث تكون كل نقطة DSCP عبارةً عن قيمة مؤلفةٍ من 6 بتات تحدد PHB معينة لتُطَّبق على الرزمة، ويستخدم ECN البتين المتبقيين. سلوك PHB للتمرير العاجل يُعرف سلوك التمرير العاجل expedited forwarding أو اختصارًا EF أنه واحدٌ من أبسط PHBs بالشرح، حيث يجب تمرير الرزم المميزة لمعالجة EF بواسطة الموجّه بأقل تأخيرٍ وخسارة، والطريقة الوحيدة التي يمكن أن يضمن بها الموجه ذلك لجميع رزم EF هي إذا كان معدل وصول رزم EF إلى الموجه محدودًا بصورةٍ صارمة ليكون أقل من المعدل الذي يمكن للموجّه من خلاله تمرير رزم EF. يحتاج الموجه بواجهة 100 ميجابت في الثانية على سبيل المثال إلى التأكد من أن معدل وصول رزم EF المخصصة لتلك الواجهة لا يتجاوز أبدًا 100 ميجابت في الثانية، وقد يرغب أيضًا في التأكد من أن المعدل سيكون إلى حدٍ ما أقل من 100 ميجابت في الثانية، بحيث يكون لديه وقت لإرسال رزمٍ أخرى مثل تحديثات التوجيه. يُحقَّق الحد من معدل رزم EF من خلال ضبط الموجهات على حافة نطاقٍ إداري للسماح بحدٍ أقصى معين لوصول رزم EF إلى النطاق domain، ويتمثل النهج البسيط وإن كان متحفظًا، في التأكد من أن مجموع معدلات جميع رزم EF التي تدخل النطاق أقل من حيز النطاق التراسلي لأبطأ رابطٍ في النطاق، وهذا من شأنه أن يضمن عدم تحميل الروابط بصورةٍ زائدة وامكانية توفير السلوك الصحيح حتى في أسوأ الحالات حيث تتلاقى جميع رزم EF على أبطأ رابط. هناك العديد من استراتيجيات التطبيق الممكنة لسلوك EF، حيث تتمثل الأولى بإعطاء رزم EF أولويةً صارمةً على جميع الرزم الأخرى، وهناك طريقةٌ أخرى تتمثل في إجراء رتلٍ عادلٍ موزون weighted fair queuing بين رزم EF والرزم الأخرى، مع إعطاء مجموعة EF وزنًا كافيًا، بحيث يمكن تسليم جميع رزم EF بسرعة؛ هذا له أفضليةٌ على الأولوية الصارمة، حيث يمكن ضمان حصول الرزم غير التابعة لمجموعة EF على بعض الوصول إلى الرابط، حتى إذا كان مقدار حركة مرور EF مفرطًا؛ وقد يعني هذا أن رزم EF تفشل في الحصول على السلوك المحدد تمامًا، ولكنه من الممكن أيضًا منع حركة مرور التوجيه الأساسية من أن تُحظر في الشبكة في حالة الحمل الزائد لحركة مرور EF. سلوك PHB للتمرير المؤكد تعود جذور سلوك PHB للتمرير المؤكد assured forwarding أو اختصارًا AF إلى نهجٍ يُعرف باسم RED with In and Out أو اختصارًا RIO، أو نهج RED الموزون، وكلاهما يُعَدّان من التحسينات لخوارزمية RED الأساسية الموضحة في قسمٍ سابق. يوضح الشكل التالي كيفية عمل RIO؛ حيث نرى زيادة احتمالية الإسقاط على المحور y مع زيادة متوسط طول الرتل على طول المحور x كما هو الحال مع RED، ولكن لدينا منحنيان منفصلان لاحتمال الإسقاط بالنسبة لصنفي حركة المرور، حيث يستدعي RIO الصنفين "in" و"out" لأسبابٍ ستتضح قريبًا، ويحتوي المنحنى "out" على أدنى عتبة MinThreshold من منحنى "in"، لذلك من الواضح أنه في ظل المستويات المنخفضة من الازدحام، إذ ستُهمَل الرزم المميزة على أنها "out" فقط بواسطة خوارزمية RED إذا أصبح الازدحام أخطر، تُسقَط نسبة أعلى من رزم "out"، وبعد ذلك إذا تجاوز متوسط طول الرتل Minin، ستبدأ RED في إسقاط رزم "in" أيضًا. ينبع سبب استدعاء صنفي الرزم "in" و"out" من طريقة تمييز الرزم. لقد لاحظنا أنه يمكن فعليًا إجراء وضع العلامات على الرزم بواسطة موجهٍ على حافة النطاق الإداري، حيث يمكننا التفكير في هذا الموجّه على أنه يقع على الحدود بين مزود خدمة الشبكة وبعض عملاء تلك الشبكة، وقد يكون العميل أي شبكةٍ أخرى، مثل شبكة شركة أو مزود خدمة شبكةٍ آخر، كما قد يوافق العميل ومزود خدمة الشبكة على ملفٍ شخصي للخدمة المؤكَّدة، وقد يدفع العميل لمزود خدمة الشبكة من أجل هذا الملف الشخصي. من الممكن أن يكون الملف الشخصي شيئًا مثل "يُسمح للعميل X بإرسال ما يصل إلى y ميجابت في الثانية من حركة المرور المؤكّدة"، أو قد يكون أعقد، لكن مهما كان ملف التعريف، يمكن لجهاز التوجيه الحدودي تحديد الرزم التي تصل من هذا العميل على أنها إما داخل أو خارج ملف التعريف. طالما أن العميل يرسل أقل من y ميجابت في الثانية في المثال المذكور للتو، ستُوضع علامة "in" على جميع الرزم الخاصة به، ولكن ستوضَع علامة "out" على الرزم الزائدة بمجرد تجاوز هذا المعدل. يجب أن يوفّر الجمع بين مقياس ملف التعريف على الحافة وRIO في جميع الموجّهات الخاصة بشبكة مزود الخدمة تأكيدًا عاليًا للعميل ولكن ليس ضمانًا بامكانية تسليم الرزم الموجودة في ملفه الشخصي. إذا كانت غالبية الرزم بما في ذلك تلك التي أرسلها العملاء الذين لم يدفعوا مبلغًا إضافيًا لإنشاء ملف تعريف هي رزم "out"، فمن المفترض أن تعمل آلية RIO على إبقاء الازدحام منخفضًا بما يكفي لتكون رزم "in" نادرة الإسقاط. من الواضح أنه يجب أن يكون هناك حيز نطاقٍ تراسلي كافٍ في الشبكة بحيث نادرًا ما تكون رزم "in" وحدها قادرةً على جعل رابطٍ مزدحمًا إلى النقطة التي تبدأ فيها RIO في إسقاط رزم "in". تعتمد فعالية آليةٍ مثل RIO إلى حدٍ ما على خيارات المعاملات الصحيحة تمامًا كما هو الحال في RED، وهناك المزيد من المعاملات التي يجب تعيينها لآلية RIO. إحدى خصائص RIO المثيرة للاهتمام هي أنها لا تغير ترتيب رزم "in" و"out"، فإذا كان اتصال TCP يرسل رزمًا عبر مقياس ملف تعريف على سبيل المثال، ووُضِعت علامةٌ على بعض رزم "in" بينما وُضِعت علامة "out" على رزمٍ أخرى؛ فستتلقى هذه الرزم احتمالات إسقاط مختلفة في أرتال الموجّه، ولكنها ستُسلَّم إلى المتستقبِل بنفس الترتيب الذي أُرسلت به. هذا مهمٌ لمعظم تطبيقات TCP التي تعمل بصورةٍ أفضل عند وصول الرزم بالترتيب، حتى لو كانت مصممةً للتعامل مع وجود أخطاءٍ في الترتيب. لاحظ أيضًا أنه يمكن تشغيل آلياتٍ مثل إعادة الإرسال السريع بصورةٍ خاطئة عند حدوث خطأٍ في الترتيب. يمكن تعميم فكرة RIO لتقديم أكثر من منحنيين لاحتمالية الإسقاط، وهذه هي الفكرة وراء النهج المعروف باسم RED الموزون weighted RED أو اختصارًا WRED. تُستخدَم قيمة حقل DSCP في هذه الحالة لاختيار منحنى من العديد من منحنيات احتمالية السقوط، بحيث يمكن توفير عدة أصنافٍ مختلفة من الخدمة. تتمثل الطريقة الثالثة لتقديم خدماتٍ مميزة في استخدام قيمة DSCP لتحديد الرتل الذي يجب وضع رزمة فيه في مجدول رتلٍ عادلٍ وموزون. قد نستخدم نقطة شيفرةٍ code point واحدة للإشارة إلى رتل رزم أفضل جهد best-effort ونقطة شيفرة ثانية لتحديد رتل الرزم التفضيلية premium، وسنحتاج بعد ذلك إلى اختيار وزنٍ لرتل الرزم التفضيلية الذي يجعلها تحصل على خدمة أفضل من الرزم الأفضل جهدًا، وهذا يعتمد على الحِمل المعروض على الرزم التفضيلية، فإذا منحنا رتل الرزم التفضيلية وزنًا قدره 1 على سبيل المثال ورتل الأفضل جهدًا وزنًا قدره 4، فهذا يضمن أن حيز النطاق التراسلي المتاح للرزم المميزة هو: Bpremium = Wpremium / (Wpremium + Wbest-effort) = 1/(1 + 4) = 0.2 وهذا يعني أننا حجزنا فعليًا 20% من الرابط للرزم التفضيلية، لذلك إذا كان الحِمل المعروض على حركة المرور التفضيلية هو 10% فقط من الرابط وسطيًا، فستتصرف حركة المرور التفضيلية كما لو كانت تعمل على شبكةٍ ذات حِملٍ منخفض جدًا وستكون الخدمة جيدةً جدًا. يمكن الإبقاء على التأخير الذي يعاني منه الصنف التفضيلي premium class منخفضًا، حيث سيحاول WFQ إرسال الرزم التفضيلية بمجرد وصولها في هذا السيناريو. إذا كان الحِمل لحركة المرور التفضيلية 30%، فسيتصرف مثل شبكةٍ محمَّلةٍ بصورةٍ كبيرة، وقد يكون التأخير مرتفعًا للغاية بالنسبة للرزم التفضيلية، بل أسوأ من رزم الأفضل جهدًا، وبالتالي فإن معرفة الحِمل المعروض والإعداد الدقيق للأوزان أمرٌ مهم لهذا النوع من الخدمة. نلاحظ أن النهج الآمن هو أن تكون متحفظًا للغاية في تحديد وزن رتل الرزم التفضيلية، فإذا كان هذا الوزن مرتفعًا جدًا بالنسبة للحِمل المتوقع، فإنه يوفر هامشًا للخطأ ولكنه لا يمنع حركة المرور الأفضل جهدًا من استخدام أي حيز نطاقٍ تراسلي محجوزٍ للرزم التفضيلية ولكنها لم تستخدمه. يمكننا تعميم هذا النهج المستند إلى WFQ كما في WRED للسماح بأكثر من صنفين مُمثلَين بنقاط شيفرةٍ مختلفة، ويمكننا دمج فكرة محدد الرتل queue selector مع نظام تفضيل الإسقاط؛ فيمكن أن يكون لدينا على سبيل المثال أربعة أرتال بأوزانٍ مختلفة من خلال 12 نقطة شيفرة، لكلٍ منها ثلاثة تفضيلات إسقاط. هذا هو بالضبط ما فعلته منظمة IETF في تعريف "الخدمة المؤكدة assured service". ترجمة -وبتصرّف- للقسم Quality of Service من فصل Congestion Control من كتاب Computer Networks: A Systems Approach. اقرأ أيضًا المقال السابق: الخدمات المتكاملة باستخدام بروتوكول RSVP متطلبات تطبيق جودة الخدمة ضمن الشبكات الحاسوبية كشف الأخطاء على مستوى البت في الشبكات الحاسوبية
  14. تلقّى كريس كويير منذ فترةٍ قريبةٍ بريدًا إلكترونيًا رائعًا من شابّ يُدعى جوش لونغ، أخبَره فيه أنه جديدٌ نسبيًا في عالم الويب ولا يفهم طريقة رفع الموقع على الإنترنت ليُتاح للجميع، ودائمًا ما يَسعَد كريس بتلقي مثل هذه الرسائل، ورغم عدم استطاعته تقديم الدعم الفني عبر البريد الإلكتروني في أغلب الأحيان، فغالبًا ما يرشدهم إلى مواقع أخرى قد تُقدّم الدعم لهم. يقول كريس: وهذه إحدى الفقرات الأصيلة التي أرسلها لي جوش وبدون أي تعديل: لقد صيغت بعض الأسئلة في هذا المقال، مع الحفاظ على جوهر ومضمون ما كتبه جوش. ما هو مُسجّل النطاق domain registrar؟ نعرف أنه مخصّصٌ لتسجيل أسماء النطاقات، ولكن ما الفَرق بينهم؟ وكيف تعلم ما المناسب منهم؟ بحثتُ سريعًا عن أفضل مستضيفي النطاقات على غوغل ورأيت 5 إعلاناتٍ لشركات تسجيل واستضافة النطاقات، كما وجدت 9 صفحاتٍ من نمط أفضل 10 وتبدو جميعها كما لو تنتمي إلى إحدى الشركات التي تقترحها أو إحداها على الأقل، فهل أبحث عن أقلّ سعرٍ ممكنٍ؟ أحسنتَ الإجابة، مُسجّلات النطاق مخصصةٌ لتسجيل أسماء النطاقات، فإذا أردت نطاقًا مثل joshlongisverycool.com فسيتوجب عليك شراؤه عن طريق شركات تسجيل النطاقات، فمساعدتك على إنجاز هذه المَهمة إحدى الخدمات التي تقدّمها. البحث عن اسم للنطاق. تمتلئ نتائج بحْث الويب بالإعلانات وبصفحات تحسين محركات البحث المشبّعة بالروابط، وهذا الأمر مزعجٌ جدًا، ولذلك لن يكون المحتوى الذي تراه صادقًا بصورةٍ كبيرةٍ لأن أغلب هذه الصفحات تمتلئ بالروابط التي تُروّج لمن يَدفع لهم أكثر عند إرسال عملاء جُدُد، لدرجة أن محرك البحث الشهير غوغل قد يبيع لك اسم نطاق! في الحقيقة، لا يجب أن ترتكب الأخطاء هنا، إذ تُعَد أسماء النطاقات سلعةً إلى حدٍ ما، ولذلك تتنافس مئات شركات تسجيل واستضافة النطاقات على التسويق فيما بينها. وإليك بعض الأشياء التي يجب أن تنتبه لها: تتعامل بعض الشركات مع اسم النطاق مثل المنتج، حيث يبيعونه بسعرٍ منخفضٍ جدًا ليكسبوا عميلًا جديدًا، تمامًا مثل محل البقالة الذي يبيع لك الحليب بسعرٍ رخيصٍ على أمل أن تشتري منه المزيد من الأشياء بعد ذلك، ففي معظم الأحيان تحاول مُسجلات النطاقات أن تبيع لك سلعةً أخرىً في الخطوة الأخيرة في عملية شرائك لاسم النطاق، وقد يحاولون بيع أسماء نطاقاتٍ إضافيةٍ أو استضافة بريدٍ إلكترونيٍ لا تحتاجها، ولذلك عليك أن تكون حذرًا. غالبًا ما تبيع لك شركات الاستضافة اسم النطاق جنبًا إلى جنبٍ مع الاستضافة، وفي الواقع، يُرجَّح أن هذا جيدٌ ولكنه يُعَد نوعًا من تضارب المصالح. فلنفترض أنك اخترت نَقل الموقع إلى استضافةٍ أخرى يومًا ما، هنا ستضطر شركة الاستضافة حينها إلى تسهيل عملية انتقال موقعك رغم أنك أصبحت عميل شركةٍ منافسةٍ لها، وإذا أصبح نطاقك في شركة استضافةٍ أخرى، فستحثّك الشركة الجديدة على تغييره. ليس لإزعاجك بالكثير من المعلومات والروابط، ولكن سنوفر لك بعض أسماء مُسجلات النطاق التي استُخدِمت مسبقًا والتي لا تَدفَع أي مبالغٍ للرعاية، كما أنها ليست روابط تابعةً affiliate links: Hover GoDaddy Google Domains Network Solution Amazon Route 53 تُوصي سارة درانسر بالاطّلاع على نطاقات شركة زيت ZEIT، فهي جديرةٌ بالاهتمام، حيث يمكنك شراء النطاقات وإدارتها عبْر سطر الأوامر. كما يُقترحُ بقاء جميع نطاقاتك (في حال كنت تُخطط لامتلاك أكثر من نطاقٍ) في مسجّلٍ registrar واحدٍ، وذلك لأنك ستدير النطاقات مراتٍ قليلةً في حياتك، ومن السهل أن تنسى أسماء النطاقات التي تمتلكها وأماكنها، بالإضافة إلى صعوبة إدارة وتغيير الإعدادات في المُسجلات المُختلفة. وبما أن التعلّم وتجربة الأمورٍ أمرٌ جيدٌ لأنها الطريقة التي تَعلّمنا بها جميعًا، فكلّ ما عليك فِعله هو اختيار واجهةٍ مناسبةٍ لك وشركة استضافةٍ موثوقةٍ (من الممكن أن تأخذ بنصيحة صديقٍ يستخدمها)، وبعدها ستسير الأمور بسلاسة، كما يمكنك استبدال الشركة إذا لم تعجبك. ما هي شركات استضافة مواقع الويب ولماذا تحتاج إليها؟ يُظهِر بحث غوغل الكثير والكثير من الإعلانات والمقالات حول أفضل استضافة ويبٍ، وتمتلئ هذه المواقع بمصطلحاتٍ مثل الاستضافة المشتركة والاستضافة المُدارة. وإذا ظَهَرت بعض شركات الاستضافة المُقتَرحة في بعض المواقع، فكيف تجد شركة الاستضافة المناسبة؟ فأنت لست متأكدًا من احتياجاتك بعد، وهل يجب أن البحث عن أقلّ سعرٍ وحسْب؟ امتلاكك لنطاقٍ معيّنٍ لا يعني أنّه سيقدم لك أيّ خدمةٍ، في الواقع يُحتمَل أن يَعرِض لك مُسجل النطاق بعد شرائه مباشرةً صفحة سيتم افتتاحه قريبًا، كما في الصورة التالية: ولذلك ستحتاج عند استضافة موقع ويبٍ في نطاقك الجديد إلى تهيئة نظام أسماء النطاقات Domain Name System واختصارًا بـ DNS لنطاقك الجديد لتشير إلى خادمٍ متصلٍ بالإنترنت، والمعلومة الشيقة هي أنك تقدر على فعل هذا مباشرةً من منزلك إذا أردت، فقد يمنحك مُزوّد خدمة الإنترنت ISP في منزلك عنوان IP خاصًا بك، وإذا بدا كلّ هذا خاصًّا بمهووسي التقنية فقط، فهذا ليس صحيحًا تمامًا لأنك تستطيع ربط اسم النطاق الخاص بك مع عنوان IP مُخصصٍ، وسيمكنك إعداد حاسوبك ليكون مُخدّم الويب الذي يستجيب للطلبات الواردة ويُخدّم على موقع الويب الخاص بك، إلا أن قلّةً قليلةً من الأشخاص يفعلون ذلك، فلا أحد يريد للمُخدّم التوقّف عن العمل بسبب إغلاق الحاسوب، أو لأنّ مُزوّد خدمة الإنترنت ISP غيّر عنوان IP الخاص بك. تمنحك خدمات استضافة الويب مُخدّمًا كهذا، كما تُعِد الشركات استضافة الويب سلعةً مثل مُسجلات النطاق تقريبًا، ولذلك يوجد الكثير منها، حيث تُقدّم معظمها خدمةً مماثلةً ولذلك يَنخفض السعر إلى حدٍ ما، كما تجد هذه الشركات أشياءً أخرى لتنافس عليها. يَصعُب شراء استضافة الويب قليلاً عن شراء النطاق، إذ ستحتاج إلى القليل من المعرفة حول موقع الويب الذي تنوي استضافته عند اتخاذ هذا الاختيار، مثلًا سيتوجب عليك أن تجيب على أسئلةٍ مثل هل سيعمل موقعك بتقنية WordPress؟ أم أنه سيرتكز على لغتي PHP وMySQL مثل مواقع نظام إدارة المحتوى CMS (سنشرح هذه المفاهيم لاحقًا)؟ وهذا يعني أن مُضيفك سيحتاج إلى دعم هذه التقنيات التي تدعمها معظم شركات الاستضافة هذه التقنيات، إلا أن بعضها لا تدعهما، ولذلك من الأفضل أن تَبحث في توثيق هذه المواقع أو تسألهم قبل الشراء، فإذا لم تكن متأكدًا فهناك الكثير من التقنيات لتشغيل مواقع الويب، ولنفترض أن الموقع سيستخدم روبي أون ريلز Ruby On Rails وهي منصةٌ برمجيةٌ لتطبيقات الإنترنت، وهذه مجموعةٌ مختلفةٌ من المتطلبات لا تُقدّمها جميع شركات الاستضافة، وكذلك الأمر بالنسبة للغة بايثون Python ونود Node. إذا كان مُضيف الويب مُتخصصًا في مجموعةٍ معينةٍ من التقنيات التي تحتاجها، فهذا هذا هو الخيار الأنسب، خاصةً في الفترة الأولى، ولكن لننظر مرةً أخرى، فهذه ليست روابطًا تابعةً affiliate links أو مدفوعة الأجر، بل هي شركات استضافة ويب تتبادر إلى الذهن -الكاتب الأصلي للمقال- وكان اختيارها عشوائيًا إلى حدٍ ما. يُركّز محرك دبليو بي WP Engine على مواقع ووردبريس WordPress. تحتوي Media Temple على استضافةٍ خاصةٍ بووردبريس WordPress، حيث تمتلك مجموعةً واسعةً من الخدمات المُصغّرة ذات السعر المناسب والاهتمام الجيّد. تمتلك شركة بلو هوست Blue Host استضافاتٍ رخيصة الثمن مع قدراتٍ مماثلةٍ لغيرها. كما توجد بعض شركات الاستضافة الأخرى والتي يمكننا عدُّها غير تقليديّةٍ، حيث: تُقدّم نيتليفاي Netlify استضافة مواقعٍ ثابتةٍ، ويناسب هذا مُولّدات المواقع الثابتة ومواقع جامستاك JAMstack، وللمزيد من المعلومات حول مواقع جامستاك، يمكنك زيارة صفحة جامستاك على ويكيبيديا. أمّا زيت Zeit فهو مضيفٌ تتفاعل معه من خلال سطر الأوامر فقط. وبالنسبة لشركة ديجيتال أوشن Digital Ocean فلها طريقتها الخاصة في الاستضافة، وتطلق عليها اسم دروبليتس droplets وتعني ترجمتها الحرفية قطرات المياه، فهي تُشبِه الأجهزة الافتراضية إلى حدٍ ما ولكن مع ميزاتٍ إضافيةٍ. تُعَد هيروكو heroku رائعةً لاستضافة التطبيقات ذات الواجهة الخلفية backend الجاهزة لاستخدام لغاتٍ أو أُطُر عمل مثل نود Node وروبي Ruby وجافا Java وبايثون Python. أخيرًا، تُعَد خدمات ويب أمازون Amazon Web Services واختصارًا بـ AWS مجموعةً كاملةً من المنتجات التي تُركّز على الاستضافة بصورةٍ خاصةٍ، ولذلك فهي مخصصةٌ للمستخدمين المحترفين، كما تتشابه سحابة غوغل Google Cloud ومايكروسوفت أزور مع AWS كثيرًا. من الضروري التأكيد على أنه من الجيد أن تَرتكب بعض الأخطاء هنا من بعض النواحي، فإذا كنت تستضيف شيئًا غير مهمٍ مثل موقعك الشخصي، فاختر مضيفًا يناسبك من حيث التكلفة وجربّه، بالطبع نأمل أن تكون تجربةً رائعةً، ولكن إذا لم تكن كذلك، فيمكنك تغيير الاستضافة، وعلى الرغم من ملل وصعوبة التنقل بين الاستضافات، إلا إنه أمرٌ ضروريٌ للتعلّم، حيث يضطر الجميع لفعله في هذه الرحلة. فعندما تشتري استضافة الويب سيخبرك المضيف بكيفية استخدامها، حيث سيمنحك المُضيف بيانات اعتماد بروتوكول نقل الملفات الآمن SFTP، وبعدها ستستخدم برنامجًا مُصممًا للاتصال بالمُخدّمات عبر بروتوكول نَقْل الملفات الآمن SFTP، والذي يُتيح رَفع الملفات إلى مُخدّمات الويب. سنفترض أنك تعمل على ملف index.html بحيث يتضمّن ملف style.css، ارفع هذين الملفّين بواسطة بروتوكول نَقل الملفّات الآمن SFTP في المجلد الذي يخبرك مضيفك بأنه دليل الجذر العام public root directory لهذا الموقع. هذه هي عملية رفع الموقع على الانترنت، حيث لا توجد بها أيّة مشكلة، وأيضًا يُشار إليها بالنّشر deployment، وهذا أمرٌ أساسيٌ وبسيطٌ جدًا، وحتى الطُرق التي تبدو مُعقّدة تَتّبع هذه الخطوات في الخلفية، وسنتحدث لاحقًا عن النشر بالتفصيل. هل يجب شراء مُسجل النطاق ومُضيف الويب من نفس الشركة إذا كانت تُقدّم الخدمتين؟ لقد ذكرنا هذا آنفًا، إذ لا يُفضَّل هذا بصورةٍ عامةٍ، على الرغم من سهولة الاستخدام في أمورٍ مِثل الفاتورة المشتركة وعملية الشراء الواحدة، حيث سيُنجِز المُضيف بعض الأمور مثل تكوين نظام أسماء النطاق DNS لتصير إعداداتك جاهزةٌ للتعامل مع استضافته، وربما دون أن تُفكّر في الأمر. لكن لنفترض أنه في يومٍ من الأيام رغبت في تغيير المُضيف لأنك وجدت خيارًا أفضل أو لم يعجبك دعمهم أو خدمتهم أو لسببٍ آخرٍ، وهنا ستظهر مشكلةٌ تتمثل في أن هذه الشركة ليست المُضيف وحسب، ولكنها المُضيف ومُسجّل النطاق ولن تستطيع إبقاء النطاق وتبديل الاستضافة، ولذلك سيتوجّب عليك تغيير الأمرين معًا، مما يحُفّ هذه الخطوة بالمخاطر، ولكن الأسوأ من ذلك هو أن الشركة لن تستجيب لطلباتك بصورةٍ سريعةٍ ومفيدةٍ لأنها ستفقدك كعميلٍ. ما هو نظام إدارة المحتوى Content Management System واختصارًا بـ CMS؟ وما الغرض منه؟ نعلَم أن ووردبريس WordPress وجوملا Joomla ودروبلا Drupal هي أنظمة إدارة المحتوى الأكثر شيوعًا، وتبدو حسب توصيفها متشابهةً فيما بينها، ولكن ما هي الأمور التي تُميّز أحدها عن الآخر؟ وهل كلّ ما تحتاجه لنقل موقع الويب الخاص بك من حاسوبك المحلي إلى شبكة الإنترنت العامة هو نظام إدارة المحتوى؟ نظام إدارة المحتوى أو CMS هو مصطلحٌ عامٌ جدًا فهو يشير إلى أي شيءٍ سيساعدك في إدارة المحتوى، وبالطبع هناك أنظمةٌ كبيرةٌ مسيطرةٌ، مثل ووردبريس WordPress وكرافت سي إم إس CraftCMS، وفي الواقع لا توجد علاقةٌ مباشرةٌ بالربط بينها وبين تشغيل الموقع محليًا أو نشر ذلك الموقع على شبكة الإنترنت العامة، فهذه عمليةٌ معقدّةٌ. لكننا نستخدم نظام إدارة المحتوى لتسهيل العمل على الموقع، حيث توجد في المواقع الضخمة عشرات آلاف الصفحات، ولذلك لا يُقبَل أن يكون لكلّ منها ملف file.html مكتوب يدويًا. ولذا يتيح لنا نظام إدارة المحتوى إنشاء هذه الصفحات من خلال دَمْج البيانات والقوالب templates. لنُفكّر في التقنية الكامنة وراء ووردبريس WordPress، والذي يُعَد نظام إدارة محتوىً يناسب موقع CSS-Tricks كثيرًا. في البداية، سيحتاج ووردبريس شيئين ليعمل وهما: PHP، وهي لغة الواجهة الخلفية backend. نظام MySQL، لإدارة قواعد البيانات. Apache، وهو مُخدّم الويب. أستخدِم برمجية Local by Flywheel التي تعمل لنظامي ويندوز وماكينتوش، وهناك عددٌ من الطرق لتشغيل هذه التقنيات مثل MAMP وDocker وVagrant وما إلى ذلك. سيؤدي ذلك إلى تشغيله محليًا باستخدام نظام إدارة المحتوى الخاص بك وهو أمرٌ رائعٌ حيث إن وجود بيئة تطوير محليةٍ جيدةٍ لموقعك هو أمرٌ بالغ الأهمية، ولكنه لن يساعدك في نَشْر الموقع على شبكة الإنترنت العامة. سننقل الموقع ليُتَاح على الإنترنت بعد قليلٍ ولكن يجب أن تعرف أن مُخدّم الويب الذي تشغله سيحتاج إلى هذه التقنيات نفسها، فإذا أخذنا المثال السابق مثل ووردبريس WordPress، فسنقول إن مُخدّم الويب الخاص بك سيحتاج إلى تشغيل PHP وMySQL وApache أيضًا، وبعدها سيتوجب عليك إعداد نظامٍ لتنقل جميع ملفاتك من حاسوبك المحلي إلى خادم الويب، تمامًا مثل أي موقعٍ آخرٍ، ولكن من المحتمل أن يكون لديك نظامٌ معيّنٌ يتعامل مع قاعدة البيانات، فكما نعلَم، قاعدة البيانات صعبةٌ بعض الشيء لأنها ليست ملفًا ثابتًا مثل باقي أجزاء موقعك. ويمكن بناء نظام إدارة المحتوى باستخدام أي خليطٍ من البرمجيات والتقنيات، وليس فقط تلك المذكورة آنفًا، فعلى سبيل المثال يستخدم موقع KeystoneJS لغة Node.js بدلاً من PHP. ويدير قواعد البيانات بمونغو MongoDB بدلًا من MySQL، كما يستخدم المُخدّم Express بدلًا من Apache، ففي الواقع هي مجرد مجموعةٍ مختلفةٍ من البرمجيّات، ويمكننا تشغيل كلتا المجموعتين محليًا وعلى خادم ويبٍ مباشرٍ كذلك. وفي بعض الأحيان لا يحتوي نظام إدارة المحتوى على قاعدة بياناتٍ أبدًا، وتُعَد مولدات الموقع الثابتة مثالًا على هذا، حيث يمكنك تشغيل الموقع محليًا، بحيث ينتج مجموعةً من الملفات الثابتة التي تنقلها مباشرةً إلى المُخدم الخاص بك، فهي مجرد طريقةٌ مختلفةٌ لفعل بنفس الأشياء، ولكنها لا تَزال تُصنّف ضمن أنظمة إدارة المحتوى. المراد توضيحه هنا ببساطة هو أن أفضل نظام إدارة محتوى هو ذاك الذي يُخصَّص لاحتياجاتك. ما هي استضافة الأصول Asset Hosting؟ ألا يُعَد المحتوى أصولًا؟ وما الفرق بين نظام إدارة المحتوى CMS وخدمة استضافة الأصول؟ وماذا يفعل مضيف الأصول؟ يُعرّف الأصل بأنه أي ملفٍ ثابتٍ flat أي لا يُنشَأ بصورةٍ ديناميكيةٍ بذات الطريقة التي يُنشِئ بها نظام إدارة المحتوى ملف HTML، وربما أفضل مثالٍ على الأصل هي الصور، وملفات CSS وجافاسكريبت كذلك، بالإضافة إلى مقاطع الفيديو وملفات PDF. قبل أن نستمر، لا يجب عليك أن تقلق بشأن هذا الآن، حيث يمكن لمضيف الويب استضافة الأصول وهذا يناسبك تمامًا ويكفيك للفترة الأولى أثناء عملك على مواقع صغيرةٍ. أحد الأسباب الرئيسية التي تجعل الأشخاص يستخدمون مضيف الأصول أو شبكة توصيل المحتوى واختصارًا بـ CDN، هي زيادة السرعة، حيث تعمل مضيفات الأصول مثل المخدّمات كذلك، تمامًا مثل مخدم الويب الخاص بمضيف الويب، إلا أن مضيفات الأصول مصممةٌ لاستضافة أصول الملفات الثابتة بميّزة سرعةٍ فائقةٍ، ولذلك ليست الميّزة الوحيدة هي إيصال المحتوى بسرعةٍ فائقةٍ إلى زوار الموقع وحسب، بل بهذه العملية يتخلّص مخدم الويب من عبء هذه العملية. يمكنك أن تَعُد يوتيوب YouTube مضيف أصولٍ، حيث يُعَد مقطع الفيديو الذي تبلغ سعته 100 ميغابايت والذي يَعرِض فراشةً في حديقتك حِملًا ثقيلًا على مخدم الويب الصغير الخاص بك، ومن المحتمل أن يسبّب مشكلةً إذا قُيّد عَرْض النطاق الترددي الصادر كما يحدث في كثيرٍ من الأحيان، إذ يؤدي رفع هذا الفيديو على يوتيوب لإتاحته على الانترنت، حيث يستضيفه يوتيوب بالنيابة عنك، طبعًا بالإضافة إلى الأسباب الاجتماعيّة لرفع الفيديو. من المؤسف أنهم قالوا لك "افعل هذا وحسب"، فهناك وباءٌ في العالم التقني يقول فيه الأشخاص "افعل هذا وحسب"، ويجعلون الأمر يبدو كما لو أنّ ما سيقولونه سهلٌ وواضحٌ، رغم أنه قد يكون صعبٌ وغير واضحٍ بالنسبة للبعض. والآن لنتحدث عن مستودعات غيت Git. يُعدّ Git شكلًا محددًّا من أشكال أنظمة أنظمة التحكم بالنسخ version control، وبالطبع هناك أنظمةٌ مختلفةٌ عن Git، لكن Git يهيمن على مجال تطوير الويب لدرجة أنه من النادر أن يُذكَر أي نظامٍ آخرِ. سنفترض أننا نطوّر سويًا موقع ويبٍ، واشترينا نطاقًا domain واستضافةً وبدأنا بتشغيل الموقع، كما تشاركنا في بيانات اعتماد بروتوكول نقل الملفات الآمن SFTP حتى يتسنى لكلينا الوصول لتغيير الملفات على الموقع المباشر الموجود على شبكة الانترنت العامّة. يُعَد كودا Coda محرّرًا للنصوص البرمجيّة، حيث يتيح تعديل الملفات مباشرةً على المخدّم في حال كنت متّصلًا به عبر بروتوكول نقل الملفّات الآمن SFTP، وهذا يبدو رائعًا، لكنّه خطير. سنفترض مثلًا أننا نحن الاثنان نحرر ملفًا في نفس الوقت ومن ثَم نرفعه عبر بروتوكول نقل الملفّات الآمن SFTP فأي تغييرٍ هو الذي سيُعتمد؟ بالطبع التغيير الذي رُفع أخيرًا، لكن ليس لدينا فكرة عمّن أجرى التغيير الفلانيّ، إذ أننا نستبدل تغييراتنا مرارًا وتكرارًا ولا نستطيع أن نبقى في تزامنٍ مع بعضنا البعض، وتؤثر التغييرات على الموقع مباشرةً، مما قد يؤدي إلى تعطّل ميّزات أو خدمات معيّنة ولا نستطيع التراجع عن التغييرات في حالة تعطّل شيءٍ ما. فهذا موقفٌ غير مقبولٍ لدرجة أنه لا يمكن أن يحدث. إذًا سنستخدم نظم التحكُّم في النسخ version control system مثل غيت مثل Git. ففي حال كنا نستخدم غيت، نثبّت commit التغييرات في مستودعٍ، حيث يمكن استضافة المستودع في مكانٍ ما على الإنترنت ليتسنى للجميع الوصول إليه، ولقد رأيت بالتأكيد غيت هاب GitHub، الذي يستضيف هذه المستودعات ويضيف إليها مجموعةً من الميزات الأخرى مثل تتبع المشاكل، وكذلك غيت لاب GitLab وبيت باكيت Bitbucket. لنفترض الآن أننا نعمل على نفس الموقع سويةً وأنشأنا مستودع غيت Git له، لذا عندما نجري تغييرًا، فعلينا أن نثبّته في المستودع، وإن كنت ترغب في إجراء تغيير أيضًا، فيجب عليك سحب pull التغييرات التي أجريتها ومن ثَم دمجها merge في نسخة الكود الخاصة بك، ثم ترسل تغييراتك إلى المستودع، وبالطبع هناك المزيد من التفاصيل وسيصبح الأمر أكثر تعقيدًا لكن هذا هو المبدأ العام. لكن عليك أن تعرف أن مستودع غيت Git هو ليس موقع الويب المباشر، بل أنت تنقل الملفات من مستودع غيت Git إلى الموقع المباشر لوحدك وبصورةٍ يدويةٍ، لكن لحسن الحظ يواجه الجميع هذا الموقف لذلك توجد الكثير من الخيارات. بعد أن انتهيت من كلّ هذا، كيف ستنتقل من استضافة الموقع محليًا إلى استضافته على شبكة الانترنت العامّة؟ أين سترفع ملفات HTML وCSS وJavaScript الخاصة بك؟ كيف ستربط اسم نطاقك الجديد مع هذه الملفّات؟ وما هي الخدمة المسؤولة عن إضافة محتوىً جديدٍ إلى موقعك أو تحديث المحتوى الموجود مسبقًا؟ هل سيكون الأمر محيرًا حقًا إذا قُدّمت الخدمات من عَدّة شركات؟ سنبدأ بموقع ويبٍ بسيطٍ للغاية حول استضافة الويب النموذجية ولنفترض أن لديك الملفات التالية على حاسوبك index.html وstyle.css وscript.js حيث تمثّل موقعك كاملًا، واشتريت اسم نطاقٍ وربطت تكوين نظام أسماء النطاق DNS مع مضيف الويب، ومنحك هذا المضيف بيانات اعتماد بروتوكول نقل الملفات الآمن SFTP، علمًا بأنّك ستستخدم بيانات الاعتماد هذه في تطبيقٍ يسمح لاتصالات بروتوكول نقل الملفات الآمن SFTP بتسجيل الدخول، كما في الصورة التالية: سيدلّك مضيفك أيضًا على المجلد الذي يمثّل الجذر العام public root لموقعك، حيث ستوجد هذه الملفات على شبكة الإنترنت ليراها العالم أجمع. قد تسمع أشخاصًا يشيرون إلى موقع الويب الذي رفعته على الإنترنت على أنه موقع إنتاج production، فعندما يسأل أحدهم هل وصل هذا الخطأ إلى الإنتاج؟ فهو يسأل ما إذا كان الخطأ موجودًا على موقع الويب المباشر أم لا؛ أما نسخة التطوير development فهي تلك الموجودة على حاسوبك، وقد يكون لديك موقعٌ في مرحلة الانطلاق staging وهي نسخةٌ طبق الأصل من موقع الويب المباشر على نفس أجهزة أو برمجيات الموقع المباشر، ولكنها نسخة لم تُطلق بعد حيث ستخضع للاختبار. هل تتذكر عندما تحدثنا عن مستودعات غيت Git؟ على الرغم أن المستودعات repositories لا تساعدك مباشرةً في عمليّة نقل الملفات الموجودة فيها إلى مخدّمك، إلّا أن معظم الأنظمة المسؤولة عن نقل الموقع ليُتَاح مباشرةً على الانترنت العام قادرةٌ على التعامل مع المستودعات هذه. إذ تشير عبارة أصبح مباشرًا حيًّا live على الانترنت إلى عمليّة النشر deployment، وعندما ترغب في نشر أيّ تغييرات على الموقع المباشر، فأنت تنشرها، وهذه هي عملية نقل عملك من التطوير إلى الإنتاج. وإحدى الخدمات التي تساعدك على النشر هي بينزتوك Beanstalk، حيث تستضيف بينزتوك مستودع غيت الخاص بك، ومن ثَم تمنحها بيانات اعتماد بروتوكول نقل الملفات الآمن SFTP الخاصّة بمخدّمك، وبهذه الطريقة تنقل لبينزتوك الملفات إلى مخدّم الويب الخاص بك عند إجراء التغييرات. لنفترض أنك تريد استضافة Git مستودع غيت في مكانٍ آخرٍ، مثل غيت هاب GitHub أو بيت باكيت Bitbucket أو غيت لاب Gitlab، ويمكنك أيضًا الاطّلاع على ديبلوي بوت DeployBot، والذي ينفّذ هذه المهام لكنه يتصل بمواقع المستودعات أيضًا، ومن الطبيعي أن تعلم أنه يوجد الكثير من الخيارات، ولكلّ منها سعره وميزاته. دعنا نعود الآن إلى مثال ووردبريس الذي تحدثنا عنه من قبل: لقد شغّلته على حاسوبك، أي محليًّا وتريد الآن رفعه على الانترنت ليراه العالم. ثم اشتريت نطاقًا من المسجّل registrar. اشتريت استضافةً تستطيع تشغيل ووردبريس WordPress. ربطت بين نظام أسماء النطاقات DNS وبين المضيف. تحققت من أن كلّ الميزات تعمل (إليك طريقة سهلة، ارفع الملف index.html على مجلّد الجذر العام عبر بروتوكول نقل الملفات الآمن وتحقّق من أن محتوياته تظهر حينما تنتقل إلى اسم النطاق). لكن لا يزال عليك فعل بعض الأمور: عليك إعداد مستودع غيت Git للموقع. ومن ثم يجب التجهيز للنشر أي نقل الملفات من المستودع إلى الموقع المباشر. أمّا الآن فحان وقت تغيير إعدادات الموقع المباشر حسب حاجتك، فقد تحتاج إلى قاعدة بياناتٍ، وسيتعين عليك عندها إنشاءها (بالطبع ستوفر لك شركة الاستضافة إرشادات لتعلم كيفيّة إعداد قاعدة البيانات وغيرها) بالإضافة إلى تشغيل مُثبِّت ووردبريس وتحديث ملفات التهيئة. إن كنت تريد نقل بيانات من قاعدة البيانات المحليّة إلى الموقع المباشر، فقد تضطر إلى تصدير/استيراد البيانات، ويحدث ذلك على مستوى MySQL الأولي باستخدام ميزات الاستيراد/التصدير الموجودة في ووردبريس، أو باستخدام إضافةٍ رائعةٍ مثل WP DB Migrate Pro. وهذا ليس بالعمل السهل طبعًا، لكنها عمليّةٌ ستقدم عليها في أي موقعٍ تقريبًا، فهي أمورٍ مثل تكوين وإعداد مخدم الويب ثم نشر الملفات، وقد يختلف كلّ موقعٍ عن الآخر في جوانب معيّنة لكن المبدأ العام نفسه. إنها قصةٌ كبيرةٌ، وما رُسم هنا هي صورةٌ واحدةٌ فقط كان المقصد منها أن تكون عامةً بما يكفي لتُظهِر المعلومات الكافية، ولكن لكلّ مرحلةٍ من هذه القصة الطرق كثيرة لتنفيذها، مثل الخدمات والشركات المختلفة التي يمكنك اختيارها، تتمتع نيتفلاي Netlify في الوقت الحالي بشعبيةٍ كبيرةٍ لأنها إحدى شركات الاستضافة القلائل التي تساعدك على النشر، فهي تراقب مستودعات غيت Git الخاصة بك وتنشرها بالنيابة عنك، وعلى الرغم من أنّ نيتفلاي Netlify مخصصة للمواقع الثابتة فقط، إلّا أن الموقع الثابتة قد تكون لوحدها عالمًا كاملًا، وكذلك فإن زيت ZEIT رائعةٌ للغاية فيما يتعلق بمساعدة الزبائن في النشر واستضافة المواقع والاتصال المباشر بغيت هاب. نتتمنى أن يكون المقال مفيدًا لك، وتذكر أنك لست وحيدًا فقد أقدم ملايين المطورين الآخرين على هذا العمل من قبلك، ويمكنك طلب المساعدة من الانترنت. ترجمة -وبتصرف- للمقال Helping a Beginner Understand Getting a Website Live من موقع CSS Tricks. اقرأ أيضًا دليل إعداد خادم ويب محلي خطوة بخطوة كيفية تثبيت برمجية Jenkins على خادوم أوبنتو 16.04 كل ما تود معرفته عن السحابة الهجينة Hybrid Cloud كيفية تثبيت ووردبريس على خادوم LAMP في أوبنتو 16.04
  15. يمكنك الحصول على الشيفرة الخاصة توجد بهذا المقال في cumulative.py في مستودع ThinkStats2 على GitHub. حدود دوال الكتلة الاحتمالية تؤدي دوال الكتلة الاحتمالية probability mass functions -أو PMFs اختصارًا- وظيفتها بصورة جيدة عندما يكون عدد القيم صغيرًا، لكن كلما ازداد عدد القيم، أصبح الاحتمال المرتبط بكل قيمة أصغر ويزداد أثر الضجيج العشوائيّ. قد نرغب بدراسة توزيع أوزان الأطفال الخدّج في بيانات المسح الوطني لنمو الأسرة NSFG على سبيل المثال، حيث يسجِّل المتغير totalwgt_lb وزن الطفل عند ولادته مقدرًا بالرطل، ويُظهر الشكل التالي دالة الكتلة الاحتمالية لأوزان الأطفال الأوائل والأطفال الآخرين -أي جميع الأطفال الذين يولدون بعد الطفل الأول للأم ذاتها- عند الولادة. يوضِّح الشكل السابق دالة الكتلة الاحتمالية لأوزان المواليد، كما يظهر هذا الشكل وجود حدود على دوال الكتلة الاحتمالية، إلا أنه لا يمكن موازنتها بصريًا. تشبه هذه التوزيعات عمومًا شكل الجرس كما هو الحال في التوزيع الطبيعي normal distribution مع توضُّع الكثير من القيم بالقرب من المتوسط mean، وتوضُّع قلة قليلة من القيم في كلا الطرفين -أي أعلى وأخفض من المتوسط بكثير-، ولكن يصعب تفسير بعض هذه أجزاء هذا الشكل، إذ توجد العديد من القمم -الارتفاعات- والانخفاضات بالإضافة إلى بعض الفروقات الواضحة بين التوزيعات، إلا أنه من الصعب معرفة إن كانت هذه الميزات ذات معنى، ومن الصعب أيضًا رؤية الأنماط العامة، مثل ما هو برأيك التوزيع الذي يحتوي على المتوسط الأعلى؟ يمكن تخفيف هذه المشاكل عن طريق تصنيف binning البيانات وهي تقسيم مجالات القيم إلى مجالات غير متداخلة بالإضافة إلى حساب عدد القيم في كل مجال مصنَّف bin. قد تكون عملية تصنيف مفيدة، إلا أنّ الحصول على الحجم الصحيح للمجالات المصنَّفة ليس بالأمر السهل، فإذا كان حجمها كبيرًا بما يكفي لتخفيف الضجيج، فستتسبب في تنعيم معلومات مفيدة. تُعَدّ دوال التوزيع التراكمي cumulative distribution functions -أو CDF اختصارًا- حلًا بديلًا يجنبنا هذه المشاكل، وسيكون هذا المفهوم موضوع دراسة المقال الحالي. سنستهل حديثنا بشرح مفهوم المئين percentiles قبل البدء بشرح دوال التوزيع التراكمي CDFs. المئين percentiles إذا خضعت لامتحان موحد سابقًا، فمن المرجح أن تكون تلقيت نتيجتك على صورة مجموع صافي ورتبة مئين percentile rank، وتكون رتبة المئين في سياقنا هذا هي نسبة الأشخاص الذين حصلوا على مجموع أقل من المجموع الذي حصلت عليه أو مجموع يساويه، فإذا كنت في رتبة المئين التسعين، فهذا يعني أنّ أداءك في الامتحان كان بمستوى أو أفضل من تسعين بالمئة ممن خضعوا لهذا الامتحان. إليك طريقة حساب رتبة المئين لقيمة معيّنة (ويمثلها المتغيّر your_score) بالنسبة للقيم في متسلسلة sequence التي تحوي الدرجات scores: def PercentileRank(scores, your_score): count = 0 for score in scores: if score <= your_score: count += 1 percentile_rank = 100.0 * count / len(scores) return percentile_rank إذا كانت الدرجات في متسلسلة على سبيل المثال هي 55 و66 و77 و88 و99، وكانت نتيجتك هي 88، فستكون رتبة المئين هي 80 والتي تحصل عليها عن طريق المعادلة التالية: 100‎ * 4 / 5. فإذا كانت لديك قيمة معيّنة ،فسيكون حساب رتبة المئين سهل للغاية، إلا أنّ العملية العكسية أصعب بقليل، فإذا أُعطيت رتية مئين وتريد إيجاد القيمة الموافقة لها فسيكون أحد الخيارات المتاحة لديك هي ترتيب القيم، ومن ثم البحث عن القيمة التي تريدها كما في هذه الشيفرة التالية: def Percentile(scores, percentile_rank): scores.sort() for score in scores: if PercentileRank(scores, score) >= percentile_rank: return score نتيجة هذا الحساب هي المئين percentile، فالمئين الخمسين مثلًا هو القيمة ذات رتبة المئين percentile rank الخمسين، ويكون المئين الخمسين في توزيع درجات الامتحان هو 77. لا يُعَدّ تنفيذ دالة Percentile في الشيفرة السابقة فعالًا، إلا أنه توجد طريقة أفضل، وهي استخدام رتبة المئين percentile rank من أجل حساب فهرس المئين الموافق كما يلي: def Percentile2(scores, percentile_rank): scores.sort() index = percentile_rank * (len(scores)-1) // 100 return scores[index] قد يكون التمييز بين "المئين" percentile و"رتبة المئين" percentile rank أمرًا محيّرًا خاصةً أن الناس لا يستخدِمون المصطلحات بدقة دائمًا، ويمكننا القول باختصار تأخذ الدالة PercentileRank قيمة على أساس وسيط لها وتحسب رتبة المئين percentile rank لها في مجموعة من القيم؛ أما الدالة Percentile فهي تأخذ ترتيبًا مئيني وتحسب القيمة الموافقة له. دوال التوزيع التراكمي والآن بعد شرح مفهومي المئين ورتبة المئين أصبحنا جاهزين لتناول موضوع دالة التوزيع التراكمي Cumulative distribution function -أو CDF اختصارًا-، وهي دالة تحوِّل القيمة إلى ترتيب مئين percentile rank. تُعَدّ دالة التوزيع التراكمي دالةً لـ x، بحيث تكون x هي أيّ قيمة موجودة في التوزيع، ولتقييم دالة التوزيع التراكمي CDF(‎x‎)‎ لقيمة محدَّدة x علينا حساب نسبة القيم الموجودة في التوزيع والتي هي أقل أو تساوي القيمة x. لدينا فيما يلي الدالة التي تأخذ وسيطين هما sample وهو متسلسلة sequence وx القيمة التي لدينا: def EvalCdf(sample, x): count = 0.0 for value in sample: if value <= x: count += 1 prob = count / len(sample) return prob تشبه هذه الدالة دالة PercentileRank إلى حد التطابق تقريبًا، إلا أنّ الفرق الوحيد هو أنّ نتيجة دالة EvalCdf احتمالية في المجال 0-1، في حين تكون نتيجة PercentileRank هي رتبة مئين في المجال 0-100. لنستعرض مثالًا عن الفكرة السابقة، حيث لدينا عيّنة تحوي القيم [5, 3, 2, 2, 1]، وإليك قيم من دالة التوزيع التراكمي الخاصة بهذه العيّنة: CDF(0)=0 CDF(1)=0.2 CDF(2)=0.6 CDF(3)=0.8 CDF(4)=0.8 CDF(5)=1 يمكننا تخمين دالة التوزيع التراكمي لأيّ قيمة للمتغير x، أي ليس فقط للقيم الموجودة في العيّنة، فإذا كانت قيمة x أقل من أصغر قيمة موجودة في العيّنة، فإن دالة التوزيع التراكميّ هي 0 أي CDF(x)=0؛ أما إذا كانت قيمتها أعلى من أكبر قيمة، فستساوي دالة التوزيع التراكمي للمتغيّر x الواحد أي CDF(x)=1. يوضِّح الشكل التالي مثالًا عن دالة توزيع تراكمي CDF. يُعَدّ الشكل السابق تمثيلًا رسوميًا لدالة التوزيع التراكمي هذه، كما تُعَدّ دالة التوزيع التراكمي الخاصة بعيّنة دالة خطوة step function. تمثيل دوال التوزيع التراكمي تزوّدنا مكتبة thinkstats2 بصنف class يدعى Cdf ويمثِّل دوال التوزيع التراكمي، وفيما يلي التوابع الأساسية التي يزودنا بها صنف Cdf: (x)Prob: بفرض لدينا قيمة x، فإن هذا التابع يحسب الاحتمال p=CDF(x) (p)Value: بفرض أنه لدينا قيمة احتمال p، فسيحسب هذا التابع القيمة الموافقة x، أي أنّها دالة التوزيع التراكمي العكسية inverse CDF للاحتمال p. يوضِّح الشكل التالي دالة التوزيع التراكمي لمدة الحمل. يمكن أن يأخذ باني Cdf قائمةً list من القيم على أساس وسيط أو سلسلة بانداز pandas Series، أو Hist، أو Pmf، أو Cdf آخر. تمثِّل الشيفرة التالية Cdf لتوزيع احتمالي لمدة الحمل في المسح الوطني لنمو الأسرة: live, firsts, others = first.MakeFrames() cdf = thinkstats2.Cdf(live.prglngth, label='prglngth') يزوّدنا thinkplot بدالة تدعى Cdf ترسم دوال التوزيع التراكمي على صورة أسطر: thinkplot.Cdf(cdf) thinkplot.Show(xlabel='weeks', ylabel='CDF') يُظهر الشكل السابق النتيجة، وإحدى طرق قراءة دالة توزيع تراكمي هي المئينات. فمثلًا، يبدو أنّ 10% من حالات الحمل استمرت فترةً تقل عن 36 أسبوع، بينما لم تتعدى 90% من الحالات مدة 41 أسبوع. تزوّدنا دالة التوزيع التراكمي بتمثيل رسومي لشكل التوزيع، وتكون القيم الشائعة -التي تظهر عادةً- على شكل أجزاء حادة أو رأسية من دالة التوزيع التراكمي، حيث يكون المنوال mode في هذا المثال وضوحًا 39 أسبوع، كما يوجد عدد قليل من القيم أقل من 30 أسبوع، لذا تكون دالة التوزيع التراكمي في هذا المجال منبسطة flat إنّ التعوّد على دوال التوزيع التراكميّ ليس بالمهمة السهلة، فهي تحتاج إلى وقت طويل، لكن حالما تصبح مألوفةً بالنسبة لك ستجد أنها تزودنا بمعلومات أكثر من دوال الكتلة الاحتمالية PMFs وبوضوح أكبر منها أيضًا. موازنة دوال التوزيع التراكمي تتجلى فائدة دوال التوزيع التراكمي بصورة خاصة في موازنة التوزيعات. فمثلًا، إليك الشيفرة التي ترسم دالة التوزيع التراكمي لأوزان الأطفال الأوائل والأطفال الآخرين عند الولادة: first_cdf = thinkstats2.Cdf(firsts.totalwgt_lb, label='first') other_cdf = thinkstats2.Cdf(others.totalwgt_lb, label='other') thinkplot.PrePlot(2) thinkplot.Cdfs([first_cdf, other_cdf]) thinkplot.Show(xlabel='weight (pounds)', ylabel='CDF') يوضِّح الشكل السابق دالة التوزيع التراكمي لأوزان الأطفال الأوائل وبقية الأطفال، كما يُظهر النتيجة، ويمكن أن نلاحظ بالموازنة مع الشكل الأول في هذا المقال أن شكل التوزيعات والفرق بينهما تظهر بوضوح أكبر في الشكل السابق، كما نلاحظ أيضًا أن التوزيع يُظهر أنّ وزن الأطفال الأوائل أخف، بالإضافة إلى وجود تباين أكبر فوق المتوسط mean. الإحصائيات المبنية على أساس المئين يصبح حساب المئينات ورتبة المئين عمليةً سهلةً عند حساب دالة التوزيع التراكمي، ويزوّدنا الصنف Cdf بالتابعين التاليين: PercentileRank(x)‎: يحسب هذا التابع رتبة المئين لقيمة معيّنة x بالشكل: 100 CDF(x) Percentile(p)‎: يحسب هذا التابع القيمة الموافقة x لرتبة مئين p وهي تساوي Value(p/100)‎. يمكن استخدام التابع Percentile لحساب إحصائية موجزة مبينة على أساس المئين. فالمئين الخمسين مثلًا هو القيمة التي تقسم التوزيع إلى النصف، ويُعرف أيضًا باسم الوسيط median الذي يُعَدّ مقياسًا للنزعة المركزية لتوزيع معيّن مثل المتوسط mean. توجد في الواقع عدة تعريفات لمفهوم "الوسيط" ولكل منها خصائص مختلفة عن الآخر، إلا أنه من السهل حسابه عن طريق Percentile(50) وهي طريقة فعالة وبسيطة. يُعدّ الانحراف الربيعي interquartile range -أو IQR اختصارًا- إحصائيةً مبنيةً على أساس المئين، وهو مقياس للانتشار في توزيع معيّن، أي أنّ الانحراف الربيعي هو الفرق بين المئين الخامس والسبعين والمئين الخامس والعشرين. يُستخدَم المئين عمومًا لتلخيص شكل التوزيع، فغالبًا ما يكون توزيع الدخل مثلًا على صورة نقاط التجزيء الخمسية quintiles أي مقسومة عند المئينات العشرين والأربعين والستين والثمانين؛ أما التوزيعات الأخرى فتُقسم إلى 10 نقاط أعشارية deciles. يُدعى هذا النوع من الإحصائيات التي تمثل النقاط التي تبعد عن بعضها مسافات متساوية في دالة توزيع تراكمي نقاط التجزيء quantiles، ويمكنك زيارة الرابط لمزيد من المعلومات. الأعداد العشوائية لنفترض أننا اخترنا عيّنةً عشوائيةً من مجموعة الولادات الحية وننظر إلى رتبة المئين لأوزان الأطفال عند الولادة، ومن ثم لنفترض أننا حسبنا دالة التوزيع التراكمي لرتب المئين، برأيك كيف سيبدو التوزيع؟ إليك كيفية حسابه، لكن في البداية يجب إنشاء دالة التوزيع التراكمي Cdf لأوزان الولادات: weights = live.totalwgt_lb cdf = thinkstats2.Cdf(weights, label='totalwgt_lb') ومن ثم نولِّد عيّنةً ونحسب رتبة المئين لكل قيمة في العيّنة: sample = np.random.choice(weights, 100, replace=True) ranks = [cdf.PercentileRank(x) for x in sample] بحيث تكون sample عيّنةً عشوائيةً لأوزان 100 ولادة مُختارة مع الاستبدال replacement، أي أنّه يمكن اختيار القيمة نفسها أكثر من مرة، وكذلك تكون ranks قائمةً list من رتب المئين. يمكننا الآن إنشاء ورسم دالة التوزيع التراكمي لرتب المئين: rank_cdf = thinkstats2.Cdf(ranks) thinkplot.Cdf(rank_cdf) thinkplot.Show(xlabel='percentile rank', ylabel='CDF') يوضَّح الشكل السابق دالة التوزيع التراكمي لرتب المئين لعينة عشوائية من أوزان الولادات، كما يُظهر النتيجة، بحيث تكون دالة التوزيع التراكمي خطًا مستقيمًا أي أنّ التوزيع موحَّد. قد لا يكون هذا الخرج واضحًا إلا أنّه نتج بسبب طريقة تعريف دالة التوزيع التراكمي، حيث يُظهر هذا الشكل أن 10% من العيّنة هي تحت المئين العاشر، و20% من العيّنة تحت رتبة المئين العشرين، وهكذا، كما كنا متوقعين. لذا يكون توزيع رتب المئين موحَّدًا بعض النظر عن شكل دالة التوزيع التراكمي، وتُعَدّ هذه الخاصية مفيدةً لأنها أساس لخوارزمية بسيطة وفعالة تولِّد أعداد عشوائية في حال وجود دالة توزيع تراكمي، وذلك عن طريق اتباع الطريقة التالية: اختر رتبة مئين بصورة موحَّدة من المجال 0-100. استخدِم Cdf.Percentile من أجل إيجاد القيمة الموجودة في التوزيع والتي توافق رتبة المئين التي اخترتها. يزوّدنا Cdf بتنفيذ لهذه الخوارزمية ويدعى Random: # class Cdf: def Random(self): return self.Percentile(random.uniform(0, 100)) كما يزودنا Cdf بـ Sample التي تأخذ عددًا صحيحًا n، وتُعيد قائمةً عدد عناصرها n وقيمها مُختارة عشوائيًا من Cdf. موازنة رتب المئين تفيد رُتب المئين percentile ranks في موازنة المقاييس في مجموعات مختلفة، إذ يُجمع الأشخاص الذين يشاركون في سباقات الركض مثلًا حسب العمر والجنس عادةً، كما يمكنك تحويل وقت السباق إلى رُتب مئين لموازنة الأشخاص في المجموعات العمرية المختلفة. شارك آلان بي داوني Allen B. Downey في سباق جيمس جويس رامبل بطول مسار 10 كيلومتر في ديدهام في ماساتشوستس منذ عدة سنوات، وكان في ترتيب 97 من أصل 1633، أي تغلّب على أو تعادل مع 1633 عدّاء، وهذا يعني أنّ رتبة المئين الخاصة به في الملعب كانت 94%. يمكننا عمومًا حساب رتبة المئين باستخدام الموقع وحجم الملعب: def PositionToPercentile(position, field_size): beat = field_size - position + 1 percentile = 100.0 * beat / field_size return percentile كانت مرتبته 26 من أصل 256 في فئته العمرية التي كان يُشار إليها بـ M4049، والتي تعني ذّكر male بين عمري 40 و49، وبالتالي كانت رتبة المئين الخاصة بي في فئته العمرية هي 90%، وإذا لم يتوقف عن الركض بعد 10 سنوات -وهو يأمل ذلك-، فسيصبح في فئة M5059. لنفترض الآن أنّ رتبة المئين الخاصة به في الفئة العمرية هذه لم تتغير، كم سأكون أبطأ حينها؟ يمكننا الإجابة عن هذا السؤال عن طريق تحويل رتبة المئين في الفئة M4049 إلى موقع في الفئة M5059، وإليك الشيفرة التي تعبر عن ذلك: def PercentileToPosition(percentile, field_size): beat = percentile * field_size / 100.0 position = field_size - beat + 1 return position كان هناك 171 شخصًا في الفئة M5059، لذا يجب أن يكون في المكان السابع عشر أو الثامن عشر ليكون لديه رتبة المئين نفسها، كما أنّ زمن انتهاء العدّاء رقم 17 في الفئة M5059 كان 46:05، أي يجب أن يصل بهذا الوقت لكي يحافظ على رتبة المئين. تمارين يمكنك البدء بـ chap04ex.ipynb من أجل التمارين التالية، علمًا أنّ الحل الخاص بنا موجود في ملف chap04soln.ipynb في مستودع ThinkStats2 على GitHub حيث ستجد كل الشيفرات والملفات المطلوبة. تمرين 1 كم كان وزنك عندما ولدت؟ إذا لم يكن لديك الجواب فبإمكانك الاتصال بوالدتك أو أحد آخر يملك الجواب، وباستخدام بيانات المسح الوطني لنمو الأسرة لكل الولادات الحية؛ احسب توزيع أوزان الولادات، ومن ثم استخدمه لحساب رتبة المئين الخاصة بك. إذا كنت الطفل الأول في عائلتك، فاحسب رتبة المئين الخاصة بك في توزيع الأطفال الأوائل، وإلا فاستخدم توزيع الأطفال الآخرين. وإذا كنت في المئين التسعين أو أكثر، فاتصل بوالدتك واعتذر إليها. تمرين 2 من المفترض أن تكون الأعداد التي تولِّدها random.random موحَّدةً بين 0 و1، أي يجب أن يكون لكل قيمة في المجالالاحتمال نفسه. ولِّد 1000 عدد باستخدام الدالة random.random وارسم دالة الكتلة الاحتمالية ودالة التوزيع التراكمي لها. هل وجدت أنّ التوزيع موحَّد؟ المفاهيم الأساسية رتبة المئين percentile rank: تشير إلى النسبة المئوية للقيم الموجودة في توزيع معيّن والتي تساوي قيمة محدَّدة أو أصغر منها. المئين percentile: القيمة المرتبطة برتبة مئين معطاة. دالة التوزيع التراكمي cumulative distribution function -أو CDF اختصارًا-: دالة تحوِّل القيم إلى احتمالاتها التراكمية، حيث أنّ CDF(x) هو نسبة القيم من العيّنة التي إما تساوي x أو أصغر منها. دالة التوزيع التراكمي العكسية inverse CDF: دالة تحوِّل الاحتمال التراكمي p إلى القيمة الموافقة له. الوسيط median: وهو المئين الخمسون -أي 50th percentile-، وغالبًا ما يُستخدَم على أساس مقياس للنزعة المركزيّة central tendency. الانحراف الربيعي interquartile range: الفرق بين المئين الخامس والسبعين 75th percentile والمئين الخامس والعشرين 25th percentile، ويُستخدَم على أساس مقياس للانتشار. نقطة التجزيء quantile: متسلسلة من القيم التي توافق الترتيبات المئينة التي تبعد عن بعضها مسافات متساوية، فنقاط التجزيء لتوزيع ما هي المئين رقم 25 والمئين رقم 50 والمئين رقم 75. الاستبدال replacement: خاصية من خواص عملية اختيار العيّنات sampling، حيث يشير المصطلح "مع استبدال" إلى أنه يمكن استخدام القيمة نفسها أكثر من مرة، كما يشير مصطلح "بدون استبدال" إلى أنه حالما تُختار القيمة فإنها تُحذَف من المجموعة السكانية. ترجمة -وبتصرف- للفصل Chapter 4 Cumulative distribution functions analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا المقال السابق: دوال الكتلة الاحتمالية في جافاسكريبت دوال الكتلة الاحتمالية في جافاسكريبت التوزيعات الإحصائية في بايثون
×
×
  • أضف...