البحث في الموقع
المحتوى عن 'think stats'.
-
ركَّزت هذه السلسلة على الأساليب الحسابية مثل المحاكاة وإعادة أخذ العينات، لكن قد يكون من الأسرع حل بعض المسائل بالاستعانة بالأساليب التحليلية، حيث سنتناول في هذا المقال بعضًا من هذه الطرق وسنشرح كيفية عملها، كما سنقدِّم اقتراحات في نهاية المقال لدمج الأساليب الحسابية والتحليلية لتحليل البيانات الاستكشافية. توجد الشيفرة الخاصة بهذا المقال في الملف 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;}
-
تُعَدّ السلسلة الزمنية 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 ودوره في ملاءمة النماذج المختلفة مع أنواع البيانات المتاحة
-
يُعَدّ تحليل البقاء 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;}
-
تُعَدّ ملاءمة المربعات الصغرى الخطية التي ذكرناها في المقال السابق مثالًا عن الانحدار 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 العلاقات بين المتغيرات الإحصائية وكيفية تنفيذها في بايثون
-
توجد الشيفرة الخاصة بهذا المقال في الملف 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;}
-
توجد الشيفرة الخاصة بهذا المقال في الملف 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;}
-
توجد الشيفرة الخاصة بهذا المقال في ملف 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;}
-
تناولنا في المقالات السابقة كل متغير على حدة وسنناقش العلاقات بين المتغيرات في هذا الفصل، حيث نقول عن متغيرين أنهما مرتبطان إذا استطعت استنباط معلومات عن أحدهما لمجرّد علمك بالمتغير الآخر، إذ يُعَدّ الطول والوزن مثلًا متغيرَين مرتبطَين، فعادةً ما يكون الأشخاص الأطول هم الأكثر وزنًا من غيرهم، لكنها بالطبع ليست علاقةً مثاليةً، إذ يوجد بعض الأشخاص الذين لا يتمتعون بطول كبير لكنّ وزنهم مرتفع، كما يوجد بعض الأشخاص النحيلين والطوال، لكن إذا حاولت تخمين وزن شخص ما، فستكون إجابتك أكثر دقةً إذا علمت طوله. يمكنك الحصول على الشيفرة الخاصة بهذا المقال في 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;}
-
قبل البدء في ذكر تفاصيل المقال، تجدر الإشارة إلى أنه توجد الشيفرة الخاصة بهذا المقال في ملف probability.py في مستودع ThinkStats2 على GitHub. دوال الكتلة الاحتمالية تُعَد دالة الكتلة الاحتمالية probability mass function -أو PMF اختصارًا- طريقةً أخرى لتمثيل التوزيع، وهي دالة تحوّل القيمة إلى احتمالها. الاحتمال هو تردد يُعبَّر عنه على أساس كسر من حجم العينة n، حيث نقسِّم التردد على n لتحويله إلى احتمال، وهذا ما يُسمى بالتوحيد normalization. إذا كان لدينا مدرَّج تكراري Hist، فيمكننا إنشاء قاموس dictionary يربط كل قيمة باحتمالها، كما يلي: n = hist.Total() d = {} for x, freq in hist.Items(): d[x] = freq / n أو يمكننا استخدام الصنف Pmf الموجود في thinkstats2. يأخذ باني الصنف Pmf مثل Hist قائمةً أو pandas Series -أي سلسلة بانداز- أو قاموسًا dictionary، أو مدرَّجًا تكراريًا Hist، أو كائن Pmf آخر، وإليك مثالًا عن استخدام قائمة بسيطة: >>> import thinkstats2 >>> pmf = thinkstats2.Pmf([1, 2, 2, 3, 5]) >>> pmf Pmf({1: 0.2, 2: 0.4, 3: 0.2, 5: 0.2}) سنلاحظ هنا أنّ صنف Pmf موحّد بحيث يكون الاحتمال الكلي هو 1. يتشابه الكائنان Pmf وHist في الكثير من الأمور، إذ يرثان الكثير من التوابع صنف أب مشترك، حيث يؤدي التابعان Values وitems مثلًا العمل نفسه في كل من Hist وPmf، ويكمن الفرق الأكبر في أنّ Hist يحوّل القيم إلى عدّاد صحيح integer counters؛ أما Pmf فيحوِّل القيم إلى احتمال عشري floating-point probabilities. ويمكن استخدام Prob للبحث عن الاحتمال المرتبط بقيمة معينة كما يلي: >>> pmf.Prob(2) 0.4 كما يمكننا في هذه الحالة استخدام عامِل القوس المربع [] للحصول على النتيجة نفسها كما يلي: >>> pmf[2] 0.4 يمكن تعديل Pmf حالي بزيادة الاحتمال المرتبط بقيمة معينة كما يلي: >>> pmf.Incr(2, 0.2) >>> pmf.Prob(2) 0.6 أو ضرب الاحتمال بمعامل factor كما يلي: >>> pmf.Mult(2, 0.5) >>> pmf.Prob(2) 0.3 إذا عدَّلت Pmf ما، فقد تكون النتيجة غير موحَّدة، أي قد لا يكون مجموع الاحتمالات 1، ويمكننا هنا استدعاء دالة Total التي تُعيد مجموع الاحتمالات للتحقق من هذا: >>> pmf.Total() 0.9 كما يمكننا استدعاء Normalize لإعادة التوحيد: >>> pmf.Normalize() >>> pmf.Total() 1.0 يوفّر كائن Pmf التابع Copy الذي يعمل على إنشاء وتعديل نسخة دون التأثير على النسخة الأصلية. وقد تبدو الصيغة المذكورة في هذه الفقرة غير متّسقة، إلا أنّ كل التسميات السابقة تجري وفق نظام معيَّن كما يلي: يدلّ Pmf على الصنف class، ويدل pmf على نسخة instance من الصنف؛ أما PMF فهو مصطلح الدالة الكتلة الاحتمالية الرياضي. رسم دوال الكتلة الاحتمالية يوفِّر thinkplot طريقتَين لرسم دوال الكتلة الاحتمالية: يمكن استخدام thinkplot.Hist لرسم دالة الكتلة الاحتمالية على أساس رسم بياني شريطي، حيث يكون الرسم البياني الشريطي مفيدًا جدًا إذا كان عدد القيم في Pmf صغيرًا. يمكن استخدام thinkplot.Pmf لرسم دالة الكتلة الاحتمالية على أساس دالة خطوة step function، كما يُعدّ هذا الخيار مفيدًا جدًا في حال وجود عدد كبير من القيم وفي حال كانت دالة الكتلة الاحتمالية منتظمة، في حين تعمل هذه الدالة مع كائنات Hist أيضًا. يزودنا pyplot أيضًا بدالة تُدعى hist؛ حيث تأخذ متسلسلةً من القيم ثم تحسب المدرَّج التكراري وترسمه، كما لا نستخدِم pyplot.hist عادةً لأننا نستخدِم كائنات Hist. يُظهِر الشكل السابق دالة الكتلة الاحتمالية لمدة الحمل بالأطفال الأوائل وبقية الأطفال، وذلك باستخدام الرسم البياني الشريطي الموجود على يسار الشكل ودالة الخطوة الموجودة على يمين الشكل. يمكننا موازنة التوزيعين من دون أن يضللنا الفرق في حجم العيّنة بين التوزيعين، وذلك برسم دالة الكتلة الاحتمالية PMF بدلًا من رسم المدرَّج التكراري histogram. بناءً على هذا الشكل يكون احتمال ولادة الطفل الأول في الوقت المحدد -أي في الأسبوع التاسع والثلاثين 39- أقل من البقية، ومن المحتمل أكثر أن يتأخر في الولادة أي حتى الأسبوع 41 والأسبوع 42. تكون الشيفرة التي تولِّد الشكل السابق كما يلي: thinkplot.PrePlot(2, cols=2) thinkplot.Hist(first_pmf, align='right', width=width) thinkplot.Hist(other_pmf, align='left', width=width) thinkplot.Config(xlabel='weeks', ylabel='probability', axis=[27, 46, 0, 0.6]) thinkplot.PrePlot(2) thinkplot.SubPlot(2) thinkplot.Pmfs([first_pmf, other_pmf]) thinkplot.Show(xlabel='weeks', axis=[27, 46, 0, 0.6]) تأخذ PrePlot معاملَين اختياريّين هما rows وcols لتصنع شبكةً من الأشكال، وفي حالتنا هذه سيتوضَّع شكلان في سطر واحد، حيث يُظهر الشكل الأول الموجود في الجهة اليسرى الـ Pmfs باستخدام thinkplot.Hist كما رأينا سابقًا؛ أمّا الاستدعاء الثاني لـ PrePlot فهو يضبط مولّد اللون، ومن ثمّ ينتقل SubPlot إلى الشكل الثاني الموجود في الجهة اليمنى ويعرض الـ Pmfs باستخدام thinkplot.Pmfs، كما استخدِم خيار axis لضمان توضُّع الشكلين على المحاور ذاتها، وهذه فكرة جيّدة إذا كان الهدف هو الموازنة بين شكلين اثنين. رسوم بيانية أخرى تفيد كل من دوال الكتلة الاحتمالية والمدرَّجات التكرارية في اسكتشاف البيانات ومحاولة تحديد الأنماط patterns والعلاقات relationships، لكن حالما نعلم بما يحصل، فستصبح الخطوة التالية الجيدة هي تصميم رسم بياني يجعل الأنماط التي حدَّدتها واضحةً قدر الإمكان. تكون الفوارق الكبرى في التوزيعات في بيانات المسح الوطني لنمو الأسرة NSFG قريبةً من المنوال mode، لذا فمن المنطقي تكبير الصورة على هذا الجزء من الرسم البياني وتحويل البيانات للتأكد من الاختلافات: weeks = range(35, 46) diffs = [] for week in weeks: p1 = first_pmf.Prob(week) p2 = other_pmf.Prob(week) diff = 100 * (p1 - p2) diffs.append(diff) thinkplot.Bar(weeks, diffs) يدل المتغير weeks في الشيفرة السابقة على مجال الأسابيع؛ أما diffs فهو الفارق بين دالتي كتلة احتمالية بالنقاط المئوية. يُظهِر الشكل التالي النتيجة على أساس مخطط شريطي bar chart، كما يُظهِر هذا الشكل النمط بوضوح أكبر، إذ يشير إلى قلة احتمال ولادة الأطفال الأوائل في الأسبوع 39، ويزداد احتمال الولادة في الأسبوعين 41 و42. كما يُظهِر الشكل السابق الفرق بالنقاط المئوية أسبوعيًا. لنضع هذه النتيجة نصب أعيننا مبدئيًا ومؤقتًا فحسب، حيث استخدمنا مجموعة البيانات نفسها لتحديد الفرق الواضح ومن ثم اخترنا تصورًا يجعل هذا الفرق واضحًا، إلا أنه لا يمكننا التأكد من أنّ هذا التأثير حقيقي، إذ يُمكن أن يكون بسبب التباين العشوائي، لكننا سنعالِج هذا لاحقًا. مفارقة حجم الصفوف الدراسية سنوضِّح قبل المتابعة أحد أنواع الحسابات التي يمكننا إجراؤها باستخدام كائنات Pmf، وسنسمّي هذا المثال: "مفارقة حجم الصف الدراسي" تكون نسبة الطلاب إلى أعضاء الهيئة التدريسية في الكثير من الجامعات والكليّات الأمريكية هي حوالي 10:1، ولكن غالبًا ما يفاجَأ الطلاب أن المتوسط الحسابي لحجم الصف الدراسي الذي يدرسون فيه أكبر من 10. ولهذا التناقض سببين اثنين هما: يدرس الطلاب عادةً حوالي 4 - 5 صفوف دراسية في الفصل الدراسي الواحد، لكن غالبًا ما يدرِّس الأستاذ حوالي صفًا أو صفَين دراسيَين. يفضل عدد قليل من الطلاب التواجد في صف دراسي صغير، إلا أنّ عدد الطلاب كبير في صف دراسي ضخم. بالطبع فإنّ التاثير الأول واضح، كما يصبح واضحًا حالما يُشار إليه على الأقل، كما أنّ التأثير الثاني أقل وضوحًا، وسنأخذ مثالًا على هذا كما يلي: لنفترض أنّ الكليّة توفِّر 65 صفًا دراسيًا في الفصل الدراسيّ الواحد مع التوزيع التالي للأحجام: size count 5- 9 8 10-14 8 15-19 14 20-24 4 25-29 6 30-34 12 35-39 8 40-44 3 45-49 2 إذا سألت عميد الكليّة عن المتوسط الحسابي لحجم الصف الدراسي، فسيبني دالة كتلة احتمالية ويحسب المتوسط mean، ويبلغك بأنّ المتوسط الحسابي لحجم الصف الدراسي هو 23.7، وستكون الشيفرة الموافقة لهذا كما يلي: d = { 7: 8, 12: 8, 17: 14, 22: 4, 27: 6, 32: 12, 37: 8, 42: 3, 47: 2 } pmf = thinkstats2.Pmf(d, label='actual') print('mean', pmf.Mean()) لكن إذا أجريت مسحًا لمجموعة من الطلاب وسألتهم عن عدد الطلاب في صفهم وحسبت المتوسط، فسنعتقِد أنّ المتوسط الحسابي للصف الدراسي أكبر، لكن دعنا نرى كم سيكون أكبر. سنحسب في البداية التوزيع حسب ملاحظة الطلاب، حيث يكون الاحتمال المرتبط بحجم الصف الدراسي "منحازًا biased" إلى عدد الطلاب في الصف الدراسي. def BiasPmf(pmf, label): new_pmf = pmf.Copy(label=label) for x, p in pmf.Items(): new_pmf.Mult(x, x) new_pmf.Normalize() return new_pmf لكل صف دراسي حجم هو x، وسنضرب الاحتمال بـ x وهو عدد الطلاب الذين يلاحظون حجم الصف الدراسي، حيث ستكون النتيجة هي Pmf جديد يمثِّل التوزيع المتحيِّز، ويمكننا رسم التوزيع الحقيقي والمُلاحظ كما يلي: biased_pmf = BiasPmf(pmf, label='observed') thinkplot.PrePlot(2) thinkplot.Pmfs([pmf, biased_pmf]) thinkplot.Show(xlabel='class size', ylabel='PMF') يُظهر الشكل السابق توزيع أحجام الصفوف الدراسية الفعلية والملاحظة من قِبَل الطلاب، كما يُظهِر النتيجة، حيث نرى أنه في التوزيع المتحيِّز هناك عدد أقل من الصفوف الدراسية ذات العدد الطلابي القليل، وعدد أكبر من الصفوف الدراسية ذات العدد الطلابي الكبير، كما يكون متوسط التوزيع المتحِيّز هو 29.1، أي أعلى بحوالي 25% من المتوسط الفعلي. من الممكن عكس هذه العملية أيضًا، حيث ستفترض أنك تريد إيجاد توزيع أحجام الصفوف الدراسية في الكليّة، ولكن لم يتسنّ لك الحصول على بيانات موثوقة من العميد، لذا سيكون البديل هو اختيار عيّنة عشوائية من الطلّاب وسؤالهم عن عدد الطلاب في صفوفهم الدراسية. ستكون النتيجة متحيِّزةً للأسباب التي ناقشناها للتو، ولكن يمكنك استخدامها لتقدير التوزيع الفعلي. وستجعل الدالة التالية الـ Pmf غير متحيِّزة: def UnbiasPmf(pmf, label): new_pmf = pmf.Copy(label=label) for x, p in pmf.Items(): new_pmf.Mult(x, 1.0/x) new_pmf.Normalize() return new_pmf تشبه الدالة السابقة دالة BiasPmf، إلا أنّ الفارق الوحيد هو تقسيم BiasPmf كل احتمال على x بدلًا من إجراء عملية الجداء. فهرسة إطار البيانات سنلقي الآن نظرةً على تحديد الأسطر، حيث سننشئ بدايةً مصفوفة نمباي NumPy تحوي أعدادًا عشوائيةً سنستخدمها لتهيئة إطار بيانات كما يلي: >>> import numpy as np >>> import pandas >>> array = np.random.randn(4, 2) >>> df = pandas.DataFrame(array) >>> df 0 1 0 -0.143510 0.616050 1 -1.489647 0.300774 2 -0.074350 0.039621 3 -1.369968 0.545897 تُرَقَّم الأسطر والأعمدة بدءًا من الصفر في الحالة الافتراضية، لكن يمكنك تزويد الأعمدة بأسماء كما يلي: >>> columns = ['A', 'B'] >>> df = pandas.DataFrame(array, columns=columns) >>> df A B 0 -0.143510 0.616050 1 -1.489647 0.300774 2 -0.074350 0.039621 3 -1.369968 0.545897 كما يمكنك إعطاء أسماء للأسطر، حيث تُدعى مجموعة أسماء الأسطر بالفهرس index، وتدعى أسماء الأسطر نفسها بالعناوين labels. >>> index = ['a', 'b', 'c', 'd'] >>> df = pandas.DataFrame(array, columns=columns, index=index) >>> df A B a -0.143510 0.616050 b -1.489647 0.300774 c -0.074350 0.039621 d -1.369968 0.545897 وكما رأينا في المقال السابق، تُحدِّد الفهرسة البسيطة عمودًا وتُعيد سلسلةً Series كما يلي: >>> df['A'] a -0.143510 b -1.489647 c -0.074350 d -1.369968 Name: A, dtype: float64 يمكننا استخدام سمة loc لتحديد سطر عن طريق عنوانه، والتي تُعيد سلسلةً Series كما يلي: >>> df.loc['a'] A -0.14351 B 0.61605 Name: a, dtype: float64 إذا علِمت موقع السطر الذي من نمط integer، فيمكنك استخدام سمة iloc بدلًا من عنوانه، والتي تُعيد سلسلةً Series أيضًا، أي كما يلي: >>> df.iloc[0] A -0.14351 B 0.61605 Name: a, dtype: float64 يمكن أن تأخذ السمة loc قائمةً من العناوين وتكون النتيجة في هذه الحالة إطار بيانات DataFrame، أي كما يلي: >>> indices = ['a', 'c'] >>> df.loc[indices] A B a -0.14351 0.616050 c -0.07435 0.039621 كما يمكنك في النهاية استخدام شريحة slice لتحديد مجالًا من الأسطر عن طريق عنوان العنوان بالصورة التالي: >>> df['a':'c'] A B a -0.143510 0.616050 b -1.489647 0.300774 c -0.074350 0.039621 أو عن طريق الموقع الذي من نمط integer: >>> df[0:2] A B a -0.143510 0.616050 b -1.489647 0.300774 ستكون النتيجة في كلتا الحالتين إطار بيانات، لكن نلاحظ أنّ النتيجة الأولى تتضمّن نهاية الشريحة على عكس الثانية التي لا تحوي النهاية. تمارين حلُّ هذه التمارين موجود في: chap03soln.ipynb و chap03soln.py في مستودع ThinkStats2 على GitHub حيث ستجد كل الملفات والشيفرات. التمرين الأول إذا سألنا الأطفال عن عدد الأطفال الموجودين في عائلتهم كما في مفارقة حجم الصف الدراسي، حيث من المحتمل أكثر أن يظهر في عيّنتك عدد كبير من العائلات التي لديها العديد من الأطفال، ولا يوجد احتمال ظهور عائلة لا تحتوي على أيّ أولاد في العيّنة؛ فاستخدم متغيِّر المستجيب NUMKDHH الموجود في المسح الوطني لنموّ الأسرة NSFG لبناء التوزيع الفعلي لعدد الأطفال تحت عمر 18 سنة في المنزل. احسب الآن التوزيع المتحيِّز الذي نراه إن سألنا الأطفال عن عدد الأطفال تحت عمر الثمانية عشر، مع حساب الأطفال أنفسهم الموجودين في منازلهم. ارسم التوزيع الحقيقي والمتحيِّز واحسب متوسطهم، مع العلم أنه يمكنك استخدام chap03ex.ipynb على أساس نقطة انطلاق من المستودع السابق. التمرين الثاني حسبنا في المقال السابق متوسط عيّنة عن طريق جمع العناصر وتقسيمها على n. إذا كانت لديك دالة كتلة احتمالية، فيمكنك حساب المتوسط لكن مع اختلاف طفيف في العملية: x = ∑ i pi xi حيث تكون xi هي القيم الفريدة في دالة الكتلة الاحتمالية وفي pi =PMF(xi)، وبالمثل يمكننا حساب التباين بالصورة التالية: S2 = ∑ i pi (xi − x)2 اكتب دالتَين بحيث تكون الأولى باسم PmfVar والثانية باسم PmfMean تأخذان كائن Pmf وتحسبان المتوسط mean والتباين variance، ولاختبار هذه التوابع، تأكّد من أنها متوافقة مع التّوابع Mean وVar التي يزودنا بها Pmf. التمرين الثالث لقد كانت بدايتنا هي طرح السؤال "هل من المرجَّح أن تتأخر ولادة الأطفال الأوائل؟"، ولتناول هذا السؤال، حسبنا الفرق في المتوسطات بين مجموعات الأطفال babies، لكننا تجاهلنا احتمال أن يكون هناك فرق في موعد الولادة بين الأطفال الأوائل وبقية الأطفال للأم ذاتها. ولتناول هذه النسخة من السؤال، حدِّد المستجيبات اللواتي لديهن طفلين على الأقل واحسب الاختلافات الزوجية. فهل تؤدي هذه الصيغة من السؤال إلى ظهور نتيجة مختلفة؟ التمرين الرابع يبدأ جميع العدائين في الوقت ذاته في معظم سباقات الجري، فإذا كنت عدّاءً سريعًا، فستتخطى غالبًا الكثير من الأشخاص في بداية السباق، ولكن بعد عدّة أميال سيصبح جميع من حولك يركض بالسرعة نفسها. لاحظنا ظاهرةً غريبةً عندما ركضنا في سباق التتابع لمسافات طويلة -209 ميل- للمرة الأولى، فعندما تجاوزنا عدّاءً آخرًا، كنا أسرع منه بكثير في أغلب الأحيان، وعندما كان تجاوزنا عدّاء آخر، كان أسرع منا بكثير في أغلب الأحيان. اعتقدنا في البداية أنّ توزيع السرعات هو ثنائي النسق bimodal، وهذا يعني أنه كان هناك العديد من العدائين البطيئين والعديد من العدائين السريعين، وقلّة قليلة منهم يركضون سرعتنا ذاتها، ومن ثم أدركنا أننا كنا ضحية تحيُّز مشابه لأثر حجم الصف الدراسي. كان السباق غريبًا من ناحيتين هما: أنه قد كانت بداية السباق متداخلة، حيث بدأت الفِرق بأوقات مختلفة، كما تضمنت العديد من الفِرق متسابقِين بقدرات متنوعة. انتشر العدّاؤون نتيجةً لذلك على طول المضمار مع وجود علاقة طفيفة بين السرعة والموقع، وعندما انضممنا إلى السباق كان العدّاؤون بقربنا يشكِّلون -إلى حد كبير- عيّنةً عشوائيّةً من العدّائين في السباق. إذًا من أين أتى التحيُّز؟ كان احتمال تجاوزنا لعدّاءً أو تجاوز عدّاء لنا خلال الوقت الذي أمضيناه في المضمار؛ يتناسب مع الاختلاف في سرعاتنا، حيث أنّه من المرجَّح تجاوز عدّاء بطيء، ومن المرجَّح أن يتجاوزنا عدّاء سريع، ولكن من غير المرجَّح أن يرى العدّاؤون الذين يركضون بالسرعة ذاتها بعضهم البعض. اكتب دالّة باسم ObservedPmf تأخذ Pmf الذي يمثِّل التوزيع الفعلي لسرعات العدّائِين وسرعة العداء المراقِب، وتُعيد صنف Pmf جديد يمثل توزيع سرعات العدّائين تبعًا للمراقِب. يمكنك استخدام relay.py لاختبار دالتك، حيث يقرأ النتائج من سباق جيمس جويس رامبل James Joyce Ramble بطول مسار 10 كيلو متر في ديدهام في ماساتشوستس، ويحوِّل سرعة العدّاء إلى ميل في الساعة mph. احسب توزيع السرعات التي ستلاحظها أنت إذا ركضت بسباق تتابعي relay race بسرعة 7.5 ميل في الساعة مع مجموعة العدّائِين هذه. المصطلحات الأساسية دالة الكتلة الاحتمالية Probability mass function- أو PMF اختصارًا-: تمثِّل التوزيع على أساس دالة تحوِّل القيم إلى احتمالاتها. الاحتمال probability: هو تردد يُعبَّر عنه على أساس كسر fraction من حجم العيّنة. التوحيد normalization: هو عملية تقسيم التردد على حجم العيّنة بهدف الحصول على احتمال. الفهرس index: هو عمود خاص في إطار بيانات البانداز pandas، بحيث يحتوي على تسميات الأسطر. ترجمة -وبتصرف- للفصل Chapter 3 Probability mass functions analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا المقال السابق: التوزيعات الإحصائية في بايثون تحليل البيانات الاستكشافية لإثبات النظريات الإحصائية
-
يمكنك الحصول على الشيفرة الخاصة توجد بهذا المقال في 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. اقرأ أيضًا المقال السابق: دوال الكتلة الاحتمالية في جافاسكريبت دوال الكتلة الاحتمالية في جافاسكريبت التوزيعات الإحصائية في بايثون
-
تندرج جميع التوزيعات التي استخدمناها حتى الآن تحت اسم التوزيعات التجريبية 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 اقرأ أيضًا التوزيعات الإحصائية في بايثون دوال الكتلة الاحتمالية في جافاسكريبت
-
توجد الشيفرة الخاصة بهذا الفصل في ملف 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 الافتراض الذي يقول أن الدخل الأعلى ضمن المستجيبين هو 106 أي مليون دولار. يولّد InterpolateSample عيّنةً وهميةً pseudo-sample، أي عيّنةً من مداخيل (جمع دخل) الأسر المعيشية تحتوي على عدد المستجيبين نفسه الموجود في كل مجال البيانات الفعلية، كما تفترض العيّنة الوهمية أن جميع المداخيل في كل مجال تبعد عن بعضها مسافات متساوية على مقياس log10. احسب الوسيط والمتوسط والتجانف وتجانف بيرسون للعيّنة الناتجة، وما هي نسبة الأسر المعيشية التي يكون دخلها الخاضع للضريبة أقل من المتوسط؟ وكيف تعتمد النتائج على الحد الأعلى upper bound المفترَض؟ ترجمة -وبتصرف- للفصل Chapter 6 Probability density functions analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا دوال الكتلة الاحتمالية في جافاسكريبت التوزيعات الإحصائية في بايثون النسخة الكاملة من كتب مدخل إلى الذكاء الاصطناعي وتعلم الآلة
-
سنتعرف في هذا المقال على التوزيعات الإحصائية في بايثون. المدرجات التكرارية Histograms يُعَدّ توزيع المتغير من أفضل الطرق المستخدمة لوصف متغير Variable، وهو إعطاء تقرير عن القيم Values الموجودة في مجموعة البيانات Dataset وعدد مرات ظهور هذه القيمة، كما يُعَدّ المدرَّج التكراري histogram التمثيل الأكثر شيوعًا، وهو عبارة عن مدرَّج بياني يُظهر تردد frequency كل قيمة، حيث يشير التردد في هذا السياق إلى عدد مرات ظهور كل قيمة. يُعَدّ القاموس Dictionary طريقةً فعالةً لحساب الترددات في لغة بايثون، فإذا كان لدينا تسلسل t من القيم كما يلي: hist = {} for x in t: hist[x] = hist.get(x, 0) + 1 تكون النتيجة قاموسًا يربط بين القيم وتردداتها، كما يمكنك استخدام الصنف Counter المعرَّف في وحدة collections بدلًا من ذلك، أي كما يلي: from collections import Counter counter = Counter(t) تكون النتيجة هنا كائن Counter وهو صنف فرعي من القاموس dictionary. يوجد خيار آخر وهو استخدام تابع pandas المعروف باسم value_counts والذي رأيناه في المقال السابق، ولكن أنشأنا هنا صنفًا باسم Hist يمثِّل المدرَّجات التكرارية ويزودنا بالتوابع التي ستعمل على تلك المدرَّجات. تمثيل المدرجات التكرارية يقبل الباني Hist تسلسلًا sequence أو قاموسًا dictionary أو سلسلة pandas أو كائن Hist آخر، كما يمكنك تهيئة وتمثيل كائن Hist كما يلي: >>> import thinkstats2 >>> hist = thinkstats2.Hist([1, 2, 2, 3, 5]) >>> hist Hist({1: 1, 2: 2, 3: 1, 5: 1}) توفِّر كائنات Hist التابع Freq الذي يقبل قيمةً على أساس وسيط ويُعيد ترددها، كما يلي: >>> hist.Freq(2) 2 يفعل عامِل القوس المربع bracket operator الشيء ذاته: >>> hist[2] 2 يكون التردد 0 عند البحث عن قيمة غير موجودة، أي: >>> hist.Freq(4) 0 يُعيد التابع Values قائمةً غير مرتَّبة بالقيم الموجودة في Hist. >>> hist.Values() [1, 5, 3, 2] ويمكن استخدام الدالة المبنية مسبقًا sorted للمرور على القيم بالترتيب: for val in sorted(hist.Values()): print(val, hist.Freq(val)) أو استخدم items للمرور على أزواج القيم وتردداتها، كما يلي: for val, freq in hist.Items(): print(val, freq) الرسم البياني للمدرجات التكرارية يُظهِر الشكل السابق المدرَّج التكراري لوزن الولادات بالرطل. أُنشئت لهذه السلسلة تحديدًا وحدة باسم thinkplot.py توفِّر دوالًا لرسم المدرَّجات التكرارية، وكائنات أخرى مُعرَّفة في الوحدة thinkstats2.py، وهي مبنية على pyplot الذي هو جزء من حزمة matplotlib. جرّب الشيفرة التالية لرسم hist باستخدام thinkplot: >>> import thinkplot >>> thinkplot.Hist(hist) >>> thinkplot.Show(xlabel='value', ylabel='frequency') يمكنك قراءة توثيق thinkplot من الموقع . يُظهِر الشكل السابق المدرَّج التكراري لوزن الولادات بالأوقية. متغيرات المسح الوطني لنمو الأسرة NSFG توجد شيفرة هذا المقال في ملف first.py، ويُفضَّل اكتشاف المتغيرات المخطَّط لاستخدامها واحدًا تلو الآخر عند البدء بالعمل مع مجموعة بيانات جديدة، وأفضل طريقة للبدء هي إلقاء نظرة على المدرَّجات االتكرارية. حوّلنا agepreg في القسم 1.6 من سنتي-سنوات centiyears إلى سنوات years، كما دمجنا birthwgt_lb وbirthwgt_oz في كمية واحدة وهي totalwgt_lb. سنستخدِم في هذا القسم تلك المتغيرات لإظهار بعض ميزات المدرَّجات التكرارية. يُظهر الشكل السابق المدرَّج التكراري لعمر الأم في نهاية الحمل. سنبدأ بقراءة البيانات ونحدِّد سجلات الولادات الحية كما يلي: preg = nsfg.ReadFemPreg() live = preg[preg.outcome == 1] يُعَدّ التعبير الموجود بين القوسين الهلاليَين سلسلةً بوليانيةً تُحدِّد الأسطر من إطار البيانات وتُعيد إطار بيانات جديد. سنرسم مدرَّجًا تكراريًا لـ birthwgt_lb للولادات الحية كما يلي: hist = thinkstats2.Hist(live.birthwgt_lb, label='birthwgt_lb') thinkplot.Hist(hist) thinkplot.Show(xlabel='pounds', ylabel='frequency') ستُزال قيم nan -أي ليس عددًا- عندما يكون الوسيط الممرَّر إلى المدرَّج التكراري Hist سلسلة بانداز pandas؛ أما label فهي سلسلة نصية تظهر في العنوان التفسيري عندما يُرسم Hist. يُظهِر الشكل السابق المدرَّج التكراري لمدة الحمل مقاسةً بالأسابيع. يوضِّح الشكل الأول النتيجة، حيث تكون القيمة الأكثر شيوعًا والتي تدعى المنوال mode هي 7 أرطال، إذ يشبه التوزيع الجرس تقريبًا، وهو شكل التوزيع الطبيعي normal distribution الذي يسمى أيضًا بالتوزيع الغاوسي Gaussian distribution، ولكن على عكس التوزيع الطبيعي الحقيقي؛ يكون هذا التوزيع غير متناظر، حيث يملك ذيلًا tail يمتد من إلى اليسار أكثر من اليمين، ويوضِّح الشكل الثاني المدرَّج التكراري الخاص بالمتغير birthwgt_oz، وهو وزن الولادة بالأوقية، كما نتوقَّع أن يكون هذا التوزيع موحّدًا uniform، أي ستمتلك جميع القيم التردد نفسه نظريًا، إلا أنه في الواقع ستكون القيمة 0 أكثر شيوعًا من القيم الأخرى، في حين تكون القيمتان 1 و15 أقل شيوعًا، وذلك ربما لأن المستجيبين قرّبوا أوزان المواليد إلى أقرب قيمة صحيحة؛ أما الشكل الثالث فيوضّح المدرَّج التكراري الخاص بالمتغير agepreg، وهو عمر الأم في نهاية الحمل، ويكون المنوال هنا هو 21 سنة، كما يكون التوزيع على صورة جرس تقريبًا، ولكن هنا سيمتد الذيل إلى اليمين أكثر من اليسار، حيث أن معظم الأمهات في العشرينات من عمرهن وقليل منهن في الثلاثينات. يوضِّح الشكل الرابع المدرَّج التكراري الخاص بالمتغير prglngth، وهو مدة الحمل مقاسةً بالأسابيع، إذ تكون القيمة الأكثر شيوعًا هي 39 أسبوعًا، كما يكون الذيل الأيسر أطول من الأيمن، أي أن ولادة الأطفال باكرًا هي أمر شائع، لكن من النادر استمرار الحمل أكثر من 43 أسبوعًا، وإلا فسيتدخل الأطباء غالبًا. القيم الشاذة Outliers يمكننا تحديد القيم الأكثر تكرارًا وتحديد شكل التوزيع بسهولة عن طريق النظر إلى المدرَّجات التكرارية، إلا أن القيم النادرة الوجود لا تظهر دائمًا، كما يُستحسَن التحقق من وجود قيم شاذة، أي القيم المتطرفة التي يمكن أن تكون أخطاءً في القياس والتسجيل، أو تكون تقاريرًا دقيقةً ناتجةً عن أحداث نادرة. توفِّر Hist تابعَين باسم Largest وSmallest، حيث يأخذان عددًا صحيحًا n، ويُعيدان أكبر أو أصغر n قيمة من المدرَّج التكراري كما يلي: for weeks, freq in hist.Smallest(10): print(weeks, freq) تكون القيم العشرة القليلة في قائمة مدة حمل الولادات الحية هي [22, 21, 20, 19, 18, 17, 13, 9, 4, 0]، حيث أن القيم التي تقل عن 10 أسابيع بالتأكيد خاطئة، والتفسير الأكثر احتمالًا هو عدم ترميز الخرج بصورة صحيحة. القيم التي تزيد عن 30 أسبوعًا صحيحة غالبًا؛ أمّا القيم التي تتراوح بين 10 و30 أسبوع، فمن الصعب التأكّد من صحتها، فقد تكون بعض القيم خاطئة، ولكن تمثِّل بعضها أطفالًا خُدّج. وتكون القيم العليا بالنسبة للطرف الآخر من المجال هي: weeks count 43 148 44 46 45 10 46 1 47 1 48 7 50 2 يقترح معظم الأطباء الولادة المحفَّزة إذا تجاوزت مدة الحمل 42 أسبوعًا، لذا ستكون بعض القيم التي تمثل مدةً طويلةً مدهشة، إذ تبدو مدة 50 أسبوع مستبعَدة طبيًا. تعتمد أفضل طريقة لمعالجة القيم الشاذة على "معرفة النطاق"، أي معلومات عن مصدر البيانات ومعنى هذه البيانات، كما تعتمد على التحليل الذي نخطط لإجرائه. السؤال الذي دعانا للبحث في هذا المثال هو ما إن كان الأطفال الأوائل يولدون باكرًا -أم متأخرًا-، وعندما يطرح أحدهم هذا السؤال، فهو مهتم بحالات الحمل المكتملة غالبًا، لذا سنركِّز في هذا التحليل على حالات الحمل التي استمرت لأكثر من 27 أسبوعًا. الأطفال الأوائل First babies أصبح بإمكاننا الآن موازنة توزيعات مدة حمل الأطفال الأوائل وغيرهم. قُسِّم إطار البيانات للولادات الحية باستخدام birthord، ومن ثم حساب مدرَّجهم التكراري، أي كما يلي: firsts = live[live.birthord == 1] others = live[live.birthord != 1] first_hist = thinkstats2.Hist(firsts.prglngth, label='first') other_hist = thinkstats2.Hist(others.prglngth, label='other') ثم رسمنا مدرَّجهم التكراري على المحاور نفسها: width = 0.45 thinkplot.PrePlot(2) thinkplot.Hist(first_hist, align='right', width=width) thinkplot.Hist(other_hist, align='left', width=width) thinkplot.Show(xlabel='weeks', ylabel='frequency', xlim=[27, 46]) تأخذ الدالة thinkplot.PrePlot عدد المدرَّجات التكرارية التي ستُرسم، حيث تُستخدم هذه المعلومة من أجل اختيار تجميعة مناسبة من الألوان. يُظهِر الشكل السابق مدرَّجًا تكراريًا لمدة الحمل. تحاذي الدالة thinkplot.Hist القيم عادةًّ إلى الوسط أي باعتماد 'align='center، لذا يتوضّع كل شريط فوق قيمته، كما تُستخدَم 'align='right و'align='left لوضع الأشرطة المقابِلة على جانبي القيمة. وقد وضعنا width=0.45 ليكون عرض الشريط 0.45، أي سيكون العرض الكلي للشريطَين هو 0.9 مع ترك بعض الفراغ بين كل زوج، وأخيرًا ضبطنا المحاور لإظهار البيانات الموجودة بين 27 و46 أسبوعًا فقط، حيث سيعرض الشكل السابق النتيجة. كان عدد "الأطفال الأوائل" في هذا المثال أقل من عدد "الأطفال الآخرين"، لذا تعود بعض الفروق الواضحة بين المدرَّجين التكراريَين إلى أحجام العينة، حيث سنعالِج هذه المشكلة في المقال القادم باستخدام دوال الكتلة الاحتمالية. تلخيص التوزيعات يُعَدّ المدرَّج التكراري وصفًا كاملًا لتوزيع عينة ما، حيث أنه يمكننا إعادة بناء قيم العينة من خلال المدرَّج التكراري وحده، إلا أنه لا يمكننا معرفة ترتيب القيم. إذا كانت تفاصيل التوزيع هامة، فقد يكون من الضروري تمثيل مدرَّج تكراري، ولكن يمكننا تلخيص التوزيع غالبًا من خلال بعض الإحصائيات الوصفية، وتكون بعض الخصائص التي قد نرغب في تقديم تقرير عنها: النزعة المركزية central tendency: هل تميل القيم للتجمُّع حول نقطة معيّنة؟ المنوالات modes: هل يوجد أكثر من تجمُّع؟ الانتشارspread: ما مقدار التباين variability الموجود في القيم؟ الذيول tails: ما مدى سرعة انخفاض الاحتمالات عندما نبتعد عن المنوال؟ القيم الشاذة outliers: هل توجد قيم متطرِّفة بعيدة عن المنوالات؟ تُدعى الإحصائيات المصمَّمة للإجابة عن مثل هذه الأسئلة إحصائيات موجزة summary statistics، وأكثر الإحصائيات الموجزة شيوعًا هي المتوسط mean والتي تهدف إلى وصف النزعة المركزية للتوزيع. إن كانت لديك عيّنة تحوي n قيمة xi فإنّ المتوسط x̄ هو مجموع القيم مقسومًا على عددها، وبمعنى آخر: x = 1 n ∑ i xi تُستخدم عادةً الكلمتان المتوسط mean والمتوسط الحسابي average على سبيل الترادف، وقد يعتقد البعض أنهما تعنيان الشيء ذاته على الرغم من وجود فرق بينهما وهو: متوسط العينة هو الإحصائية الموجزة التي تُحسَب بالمعادلة السابقة. المتوسط الحسابي هو أحد الإحصائيات الموجزة التي قد تختارها لوصف النزعة المركزية. يُمثّل المتوسط وصفًا جيدًا لمجموعة من القيم في بعض الأحيان. فمثلًا، تملك حبات التفاح جميعها الوزن نفسه تقريبًا -والتي تُباع في المتاجر الكبرى-، فإذا اشترينا ست تفاحات وكان الوزن الكلي هو 3 أرطال، فسيكون قولنا "تَزِن كل تفاحة نصف رطل" موجزًا منطقيًا. لكن القرع أكثر تنوّعًا، فبفرض أننا زرعنا عدة أنواع في الحديقة وحصدنا في يوم من الأيام 3 حبات قرع للزينة بحيث تَزِن كل حبة رطلًا واحدًا، وحبتي قرع للفطيرة بحيث تَزِن كل منها 3 أرطال، وحبة واحدة من نوع آتلانتيك جيانت Atlantic Giant تَزن 591 رطلًا. سيكون متوسط mean هذه العينة هو 100 رطل، لكن من الخطأ القول أنّ المتوسط الحسابي average هو 100 رطل، وبالتالي لا يوجد في هذا المثال متوسط حسابي ذو معنى لأنه لا يوجد قرع مثالي. التباين variance إذا لم يكن هناك عددًا واحدًا يُلخِّص أوزان القرع، فسيمكننا فعل ذلك باستخدام عددين هما المتوسط mean والتباين variance. التباين هو إحصائية موجزة تهدف إلى وصف التباين أو انتشار التوزيع.، وتكون الصيغة الرياضية لتباين مجموعة من القيم كما يلي: S2 = 1 n ∑ i (xi − x)2 يُسمّى(x<sub>i</sub>-x̄) "الانحراف عن المتوسط"، لذا سيكون التباين هو متوسط مربع الانحراف، ويكون الجذر التربيعي للتباين هو الانحراف المعياري S. إذا كانت لديك خبرة سابقة في المجال، فربما صادفت صيغةً للتباين تحوي n − 1 في المقام بدلًا من n، إذ تُستخدَم هذه الإحصائية لتقدير التباين في إحصاء السكان باستخدام عينة. تزوِّدنا بنى بيانات البانداز pandas بتوابع لحساب المتوسط والتباين والانحراف المعياري كما يلي: mean = live.prglngth.mean() var = live.prglngth.var() std = live.prglngth.std() يكون متوسط مدة الحمل بالنسبة للولادات الحية 38.6 أسبوعًا، وانحرافه المعياري 2.7 أسبوعًا، أي علينا أن نتوقع الانحرافات بمقدار بين أسبوعين وثلاثة أسابيع وهي أمر شائع، وبذلك يكون تباين مدة الحمل هو 7.3، وهو أمر يَصعُب تفسيره، خاصةً أنّ الوحدات هي مربع الأسابيع، ويُعَدّ التباين مفيدًا في بعض العمليات الحسابية لكنه ليس إحصائية موجزة جيدة. حجم الأثر حجم الأثر هو إحصائية موجزة تهدف إلى وصف حجم الأثر، إذ لن تتوقع ما هو قادم، فإذا أردنا وصف الفرق بين مجموعتين مثلًا، فسيكون أحد الخيارات الشائعة هو حساب الفرق بين المتوسطات. متوسط الحمل للأطفال الأوائل هو 38.601 ويكون لبقية الأطفال 38.523، وبالتالي يكون الفرق بينهما هو 0.078 أسبوع أي حوالي 13 ساعة، كما يكون الفارق هو 0.2% على أساس جزء من مدة الحمل الطبيعي. إذا افترضنا أنّ هذا التقدير دقيقًا، فلن يكون لهذا الفارق أيّ عواقب عملية، ومن غير المحتمل واقعيًا ملاحظة أحد أيّ اختلاف على الإطلاق بدون مراقبة عدد كبير من حالات الحمل. هناك طريقة أخرى لتوضيح حجم الأثر، وهي موازنة الفرق بين المجموعات مع التباين داخل المجموعات، حيث يهدف معامل كوهن د Cohen's d إلى فعل ذلك، ويُعرَّف بالصورة التالية: S2 = 1 n ∑ i (xi − x)2 يكون x̄-1 وx̄-2 متوسطي المجموعتين وs هي "الانحراف المعياري المجمَّع"، كما تكون شيفرة البايثون لحساب Cohen's d بالصورة التالية: def CohenEffectSize(group1, group2): diff = group1.mean() - group2.mean() var1 = group1.var() var2 = group2.var() n1, n2 = len(group1), len(group2) pooled_var = (n1 * var1 + n2 * var2) / (n1 + n2) d = diff / math.sqrt(pooled_var) return d يكون الفرق بين المتوسطات في هذا المثال هو 0.029 انحرافًا معياريًا الذي هو صغير جدًا، ولتوضيح ذلك بمثال آخر، سيكون الفرق في الطول بين النساء والرجال هو 1.7 انحرافًا معياريًّا، كما يمكنك الإطلاع هنا. التقارير الخاصة بالنتائج رأينا عدة طرق لوصف الفرق في مدة الحمل بين الأطفال الأوائل وبقية الأطفال، فكيف يمكننا إعطاء تقارير بهذه النتائج؟ تعتمد الإجابة على مَن يطرح السؤال، فقد يكون أحد العلماء مثلًا مهتمًا بأيّ تأثير حقيقي مهما كان صغيرًا؛ أما الطبيب فقد يكون مهتمًا بالتأثيرات ذات الأهمية السريرية فقط، أي أن الفروق التي تؤثِّر على القرارات العلاجية للمريض، هي حين قد تهتم المرأة الحامل بالنتائج التي تخصُّها مثل احتمالية الولادة الباكرة أو المتأخرة. كما تعتمد طريقة تقديم تقارير بالنتائج على أهدافك، فإذا كنت تحاول إثبات أهمية تأثير معيَّن، فقد تختار إحصائية موجزة تشدِّد على الفروقات؛ أما إذا كنت تحاول طمأنة مريض، فقد تختار إحصائيةً موجزة تضع الفروقات في السياق. يجب أن تأخذ قراراتك أخلاقيات المهنة بالحسبان، ولا بأس بأن تكون مُقنعًا، إذ عليك تصميم تقارير إحصائية وتقارير رسومية تروي القصة بوضوح، كما عليك بذل جهدك لجعل تقاريرك صادقة، وتعترف بالقيود والأمور التي ليست مؤكَّدة. تمارين التمرين الأول افترض أنه قد طُلب منك تلخيص ما تعلَّمته حول ما إذا كان الأطفال الأوائل يولدون متأخرين أم لا بناءً على النتائج الواردة في هذا المقال. ما هي الإحصائية الموجزة التي ستستخدِمها إن أردت عرض نتائجك في أخبار المساء؟ وما هي الإحصائية الموجزة التي ستستخدِمها إذا أردت طمأنة أم حامل قلقة حيال هذا الأمر؟ تخيَّل أنك سيسل آدامز Cecil Adams وهو مؤلف كتاب The Straight Dope الموجود هنا، وكانت مهمتك هي الإجابة عن هذا السؤال: "هل يولد الأطفال الأوائل متأخرين؟". اكتب فقرةً تستخدِم النتائج الموجودة في هذا المقال للإجابة عن السؤال بكل وضوح ودقة وشفافية. التمرين الثاني يجب أن تجد في المستودع repository الذي حمَّلته، ملفًا باسم chap02ex.ipynb افتحه أولًا. لقد مُلئت بعض الخلايا وعليك تنفيذها؛ أما الخلايا الأخرى فستزوِّدك بتعليمات حول التمارين، لذلك اتبع التعليمات واملأ الإجابات. ستجد في المستودع repository الذي حمَّلته ملفًا باسم chap02ex.py، حيث يمكنك استخدام هذا الملف على أساس نقطة انطلاق للتمارين اللاحقة. التمرين الثالث يكون منوال التوزيع هو القيمة الأكثر تكرارًا، كما يمكنك الاطلاع على Mode_(statistics). اكتب دالةً اسمها Mode بحيث تأخذ مدرَّجًا تكراريًا Hist وتُعيد القيمة الأكثر تكرارًا، واكتب دالةً اسمها AllModes تُعيد قائمةً من أزواج القيم وتردداتها مرتَّبةً تنازليًا حسب التردد. التمرين الرابع ابحث فيما إن كان وزن الأطفال الأوائل عادةً أخف وزنًا أو أثقل من البقية بالاعتماد على المتغيرtotalwgt_lb، واحسب Cohen’s d لقياس مقدار الفرق بين المجموعات، وكيف يمكن موازنتها مع الاختلاف في مدة الحمل؟ المفاهيم الأساسية التوزيع distribution: القيم التي تظهر في العينة وتردد كل منها. المدرَّج التكراري histogram: تحويل القيم إلى ترددات، أو رسم بياني يُظهِر هذا التحويل. التردد frequency: عدد المرات ظهور قيمة معيَّنة في العينة. المنوال mode: القيمة الأكثر تكرارًا في العينة أو إحدى القيم التي تملك أكبر تكرار. التوزيع الطبيعي normal distribution: معالجة مثالية للتوزيع الذي يشبه الجرس، أو ما يُعرَف بالتوزيع الغاوسي. التوزيع الموحَّد uniform distribution: هو التوزيع الذي يكون فيه التردد نفسه لكل القيم. الذيل tail: جزء من التوزيع الموجود في نهاية الطرف المرتفع ونهاية الطرف المنخفض. النزعة المركزية central tendency: صفة لعينة أو السكان، هي قيمة متوسطة أو نموذجية بديهيًا. القيمة الشاذة outlier: قيمة بعيدة عن النزعة المركزية. الانتشار spread: مقياس لكيفية انتشار القيم في التوزيع. إحصائية موجزة summary statistic: إحصائية تُحدِّد بعض جوانب التوزيع مثل النزعة المركزية أو الانتشار. التباين variance: إحصائية موجزة تُستخدَم غالبًا لقياس الانتشار. الانحراف المعياري standard deviation: هو الجذر التربيعي للتباين، ويستخدم أيضًا على أساس مقياس للانتشار. حجم الأثر effect size: إحصائية موجزة تهدف إلى تحديد حجم التأثير، مثل الفرق بين المجموعات. أهمية سريرية clinically significant: نتيجة مهمة من الناحية العملية مثل الفرق بين المجموعات. ترجمة -وبتصرف- للفصل Chapter 2 Distributions analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا المقال السابق: تحليل البيانات الاستكشافية لإثبات النظريات الإحصائية تحليل نظام الملفات لإدارة البيانات وتخزينها واختلافه عن نظام قاعدة البيانات مدخل إلى الذكاء الاصطناعي وتعلم الآلة
-
تقول الفرضية الأساسية لهذه السلسلة أنّ دمج البيانات مع الطرق العملية يمكنه الإجابة عن الأسئلة المطروحة وتوجيه القرارات في حالات الشك uncertainty. يطرح آلان بي داوني Allen B. Downey مثالًا استلهمه من سؤال سمعه عندما كانت زوجته حاملًا بطفلهما الأول، وهو: هل تتأخر ولادة الأطفال الأوائل؟ إذا توجهنا بسؤالنا هذا إلى غوغل، فسنجد الكثير من النّقاشات حوله؛ إذ يَعتقد البعض أنها حقيقة، ويَعتقد آخرون أنها خرافة، كما يقول البعض الآخر عكس الحالة أي يولد الأطفال الأوائل باكرًا. يلجأ المشاركون غالبًا في هذه النقاشات إلى تقديم بيانات تدعم اعتقادهم مثل: تُدعى التقارير هذه بالأدلة القولية anecdotal evidence، لأنها تستند إلى بيانات غير منشورة وغالبًا ما تكون تجارب شخصية، ولا حرج في استخدامها في الأحاديث العادية أي لا ننتقد أصحاب الأقوال السابقة، لكن قد نحتاج إلى أدلة أكثر إقناعًا وإلى إجابة أكثر وثوقية، وعندها لا يمكننا استخدام الأدلة غير الموثَّقة للأسباب التالية: وجود عدد صغير من الحالات: إذا كان الحمل بالأطفال الأوائل أطول من بقية الأطفال، فسيكون الفارق صغيرًا غالبًا إذا ما وازنّاه بالاختلافات الطبيعية، وفي هذه الحالة علينا دراسة عدد كبير من حالات الحمل حتى نتأكد من وجود الفارق. الانحياز الاختياري Selection bias: قد يكون المشاركون في النقاشات التي تدور حول هذا السؤال مهتمين بالأمر بسبب تأخر ولادة أطفالهم الأوائل، وفي هذه الحالة ستجعل عملية اختيار البيانات النتائجَ متحيزة. الانحياز التأكيدي Confirmation bias: من المرجَّح أن يطرح الأشخاص الذين يعتقدون بصحّة هذه المقولة أمثلةً تؤكِّد أفكارهم، ومن المرجَّح أن يطرح الأشخاص الذين يعتقدون أنّ هذه المقولة خاطئة أمثلةً معاكسة. عدم الدقة: غالبًا ما تكون الأدلة القولية قصصًا شخصيةً، وغالبًا ما تكون خاطئةً ومحرَّفةً ومكرَّرةً بصورة غير دقيقة. إذًا، كيف يمكننا الوصول إلى نتيجة أفضل؟ نهج إحصائي سنستخدِم أدوات الإحصاء لتحديد قيود الأدلة القولية، حيث تتضمّن هذه الأدوات ما يلي: جمع البيانات: سنستخدِم بيانات مأخوذة من مسح كبير على مستوى الدولة، حيث صُمّم خصّيصًا لتوليد استنتاجات صحيحة إحصائيًا حول سكان الولايات المتحِدة الأمريكية. الإحصائيات الوصفية: سنولِّد إحصائيات مهمتها تلخيص البيانات بإيجاز، وتقييم الطرق المختلفة التي يمكن فيها عرض البيانات. تحليل البيانات الاستكشافية: سنبحث عن الأنماط المتكرّرة والفروق وميزات أخرى تعالج المسألة التي نناقشها، كما سنتحقق في الوقت ذاته من التناقضات ونحدِّد القيود. التخمين Estimation: سنستخدم بيانات مأخوذة من عيّنة لتخمين صفات عامة الشعب. اختبار الفرضيات: سنقيِّم التأثيرات الواضحة -مثل فرق بين مجموعتين من الأشخاص- فيما إذا كان قد حدث التأثير بمحض الصدْفة أم لا. يمكننا الوصول إلى استنتاجات أكثر تبريرًا واحتمالًا لأن تكون صحيحةً عن طريق اتباع هذه الخطوات بعناية لتجنب العثرات والمطبّات. المسح الوطني لنمو الأسرة بدأت المراكز الأمريكية منذ عام 1973 بإجراء المسح الوطني لنمو الأسرة NSFG للسيطرة على الأمراض والوقاية منها CDC، إذ يهدف هذا المسح إلى جمع معلومات عن الحياة الأسريّة والزواج والطلاق والحمل والعقم واستخدام وسائل منع الحمل وصحّة النساء والرجال. تُستخدَم نتائج المسح من أجل تخطيط الخدمات الصحية وبرامج التثقيف الصحي، ومن أجل إجراء دراسات إحصائية حول الأُسر والخصوبة والصحة، كما يمكنك الاطّلاع على صفحة المسح على الموقع الرسمي للمراكز سنستخدم البيانات التي جمِّعت أثناء هذا المسح للتحقق من تأخر ولادة الأطفال الأوائل وللإجابة على أسئلة أخرى أيضًا، كما علينا فهم تصميم الدراسة من أجل استخدام هذه البيانات بفعالية. يُعَدّ المسح الوطني لنمو الأسرة دراسةً مقطعيةً cross-sectional study، أي يخزِّن لقطةً سريعةً عن مجموعة محدَّدة في فترة محدَّدة من الزمن، كما تُعَدّ الدراسة الطولية longtitudal study البديل الأكثر شيوعًا، حيث تراقب مجموعةً معيَّنةً بصورة متكررة على مدار فترة زمنية. أُجري المسح الوطني لنمو الأسرة سبع مرات، حيث تسمى كل عملية نشر بدورة cycle، كما سنستخدِم بيانات الدورة السادسة التي أجريت من شهر 1 من عام 2002 حتى شهر 3 من عام 2003. يتلخص هدف المسح في الوصول إلى استنتاجات حول السكان، إذ تتراوح أعمار السكان المستهدَفِين من المسح الوطني لنمو الأسرة بين 15 و44 عامًا وهم أشخاص من الولايات المتحدة الأمريكية. يجمع المسح المثالي بيانات من كل فرد، ولكن نادرًا ما يكون هذا متاحًا أو ممكنًا، وبدلًا من ذلك تُجمَع البيانات من مجموعة معيَّنة من السكان تدعى عيّنة sample، كما يُطلق على المشاركين في المسح اسم المستجيبون respondents. يكون الهدف من إجراء الدراسات مقطعيًا هو جعلها تمثيلية representative عمومًا، بمعنى أن يكون لكل فرد من أفراد السكان فرصةً متساويةً من المشاركة، وعلى الرغم من كون هذه الحالة المثالية صعبة التحقيق عمليًا، إلا أنّ الأشخاص الذين يجرون الاستطلاعات يحاولون الاقتراب منها قدر الإمكان، كما لا يُعَدّ NSFG مسحًا تمثيليًا، وبدلًا من ذلك يُفرَط في أخذ العينات oversampled عمدًا. يتعامل مصممو الدراسة مع ثلاث مجموعات وهي: الهسبانيون (حسب مكتب التعداد فإنّ الهسباني أو اللاتيني هو شخص من كوبا أو المكسيك أو بورتوريكو أو أمريكا الجنوبية أو الوسطى أو غيرها من الأصول الإسبانية بغض النظر عن العرق) والأفارقة الأمريكيون (هم مجموعة عرقية من أصول أفريقية تعيش في القارتين الأمريكيتين) والمراهقون بمعدَّلات أعلى من تمثيلهم الحقيقي في الولايات المتحدة الأمريكية، وذلك للتأكد من أنّ عدد المستجيبين في هذه المجموعات كبير بما يكفي لاستخلاص استدلالات إحصائية صحيحة. يتمثّل الجانب السلبي من الإفراط في أخذ العينات بطبيعة الحال في صعوبة استخلاص استنتاجات حول عامة السكان بناءً على إحصاءات من المسح، وسنعود إلى هذه النقطة في وقت لاحق. من المهم أن نكون على دراية بما يعرف بدليل الأسس والمعايير codebook عند التعامل مع هذا النوع من البيانات، إذ توثِّق هذه السلسلة تصميم الدراسة وأسئلة المسح وترميز إجابات المستجيبين، كما يتوفر دليل الأسس والمعايير ومرشِد المستخدِم لبيانات NSFG هنا. استيراد البيانات تتوفر الشيفرة والبيانات المستخدَمة في هذه السلسلة github، حيثسيكون لديك ملف باسم ThinkStats2/code/nsfg.py فور تنزيل الشيفرة، وإذا شغّلت هذا الملف فسيقرأ ملف البيانات ويشغّل بعض الاختبارات ويطبع رسالة "All tests passed" تشير إلى نجاح جميع الاختبارات، وسنرى فيما يلي مهمة هذا الملف. توجد بيانات الحمل الخاصة بالدورة السادسة من NSFG في ملف يدعى 2002FemPreg.dat.gz، وهو ملف بيانات مضغوط بتنسيق نصي بسيط ASCII وأعمدة ذات عرض ثابت، كما يحتوي كل سطر من الملف على سجل record يحوي بيانات حمل واحد. تنسيق الملف موثَّق في ملف قاموس لبرنامج ستاتا Stata وهو 2002FemPreg.dct، كما يُعَدّ ستاتا نظامًا برمجيًا إحصائيًا؛ أما "dictionary" أي القاموس في هذا السياق، فهو قائمة بأسماء وأنواع وفهارس المتغيرات التي تحدِّد في أيّ سطر يوجد كل متغير من المتغيرات. فيما يلي بعض الأسطر من ملف 2002FemPreg.dctعلى سبيل المثال: infile dictionary { _column(1) str12 caseid %12s "RESPONDENT ID NUMBER" _column(13) byte pregordr %2f "PREGNANCY ORDER (NUMBER)" } يصف هذا القاموس متغيرين: المتغير الأول caseid وهو سلسلة مكونة من 12 محرفًا تمثل معرِّف المستجيب؛ أما المتغير الثانيpregordr وهو عدد صحيح مؤلَّف من بايت واحد one-byte integer بحيث يشير إلى الحمل الذي يصفه هذا السجل لهذا المستجيب. تحتوي الشيفرة التي حمَّلتها على thinkstats2.py، وهو وحدة بايثون تحتوي على العديد من الأصناف والدوال المستخدَمة في هذه السلسلة بما في ذلك الدوال القارئة لقاموس ستاتا وملف بيانات NSFG، وإليك كيفية استخدامها فيnsfg.py: def ReadFemPreg(dct_file='2002FemPreg.dct', dat_file='2002FemPreg.dat.gz'): dct = thinkstats2.ReadStataDct(dct_file) df = dct.ReadFixedWidth(dat_file, compression='gzip') CleanFemPreg(df) return df يأخذ ReadStataDct اسم ملف القاموس ويُعيد dct، الذي هو كائن من النوع FixedWidthVariables يحتوي على معلومات من ملف القاموس، كما يوفر dct التابع ReadFixedWidth الذي يقرأ ملف البيانات. إطارات البيانات النتيجة التي يُعيدها التابع ReadFixedWidth هي إطار بيانات DataFrame، وهو بنية البيانات الأساسيّة التي توفرها بانداز pandas التي هي حزمة بيانات وإحصائيات خاصة بلغة بايثون، كما ستُستَخدم في هذه السلسلة. يتألف إطار البيانات من سطر لكل سجل، وفي هذه الحالة يكون سطر لكل حمل وعمود لكل متغير، كما يتألف إطار البيانات من أسماء المتغيرات وأنواعها، ويوفر توابع مهمتها الوصول إلى البيانات وتعديلها. إذا طبعنا df فسنحصل على عرض مقطوع truncated view للأسطر والأعمدة، وعلى شكل إطار البيانات الذي يتألف من 13593 سطر/سجل، و244 عمود/متغير. >>> import nsfg >>> df = nsfg.ReadFemPreg() >>> df ... [13593 rows x 244 columns] عُرض جزء من الخرج وأخُفي الباقي لأنّ حجم إطار البيانات أكبر من أن يُعرض كاملًا، ويُظهر السطر الأخير عدد الأسطر والأعمدة، كما تُعيد سمة الأعمدة columns سلسلةً من أسماء الأعمدة بتنسيق سلاسل الترميز الموحَّد Unicode: >>> df.columns Index([u'caseid', u'pregordr', u'howpreg_n', u'howpreg_p', ... ]) تكون نتيجة ما سبق Index أي فهرسًا، وهو بنية بيانات من حزمة البانداز pandas، كما سنشرح بنية الفهرس لاحقًا، لكننا سنتعامل معه حاليًا على أساس قائمة list: >>> df.columns[1] 'pregordr' للوصول إلى أحد أعمدة إطار البيانات، يمكننا استخدام اسم العمود على أساس مفتاح: >>> pregordr = df['pregordr'] >>> type(pregordr) <class 'pandas.core.series.Series'> تكون نتيجة ما سبق هي Series أي سلسلةً، وهي بنية بيانات في حزمة البانداز pandas أيضًا. تشبه السلسلة نوع القائمة list الموجودة في لغة البايثون مع بعض الميزات الإضافية، وسنحصل على الفهارس والقيم المقابلة لها عند طباعة السلسلة كما يلي: >>> pregordr 0 1 1 2 2 1 3 2 ... 13590 3 13591 4 13592 5 Name: pregordr, Length: 13593, dtype: int64 نلاحظ في هذا المثال أنّ الفهارس هي أعداد صحيحة موجودة بين العددين 0 و13592، ولكن يمكنها أن تكون أيًا من الأنواع القابلة للفرز عمومًا، كما تُعَدّ العناصر أعدادًا صحيحةً ويمكنها أن تكون أيّ نوع آخر أيضًا. يتضمن السطر الأخير اسم المتغير وطول السلسلة ونوع البيانات، حيث يُعَدّ int64 أحد الأنواع التي توفرها مكتبة نمباي NumPy، وإذا جرّبنا هذا المثال على آلة تعمل بنظام 32-بِتّ، فقد نحصل علىint32. يمكن الوصول إلى عناصر السلسلة باستخدام فهارس وشرائح من الأعداد الصحيحة: >>> pregordr[0] 1 >>> pregordr[2:5] 2 1 3 2 4 3 Name: pregordr, dtype: int64 تكون نتيجة عامِل الفهرس هي int64، ونتيجة الشريحة سلسلةً أخرى، كما يمكننا الوصول إلى أعمدة إطار البيانات عن طريق استخدام صيغة الاستدعاء النقطية، أي كما يلي: >>> pregordr = df.pregordr لا يمكن استخدام هذه الصيغة إلا إذا كان اسم العمود مُعرِّفًا صالحًا في بايثون، لذا يجب أن يبدأ بحرف ولا يمكن أن يحتوي على فراغات، …إلخ. المتغيرات وجدنا في مجموعة بيانات NSFG متغيرَين اثنين وهما caseid وpregordr، كما لاحظنا وجود 244 متغير في كامل مجموعة بيانات NSFG. سنستخدِم المتغيرات التالية للوصول إلى الاستنتاجات في هذه السلسلة: caseid عدد صحيح يمثل معرِّف المستجيب. prglngth عدد صحيح يمثل مدة الحمل مقاسة بالأسابيع. outcome: رمز صحيح يمثِّل نتيجة الحمل، حيث يشير الرقم 1 إلى تمام الولادة والطفل حي. pregordr: عدد تسلسلي للحمل، إذ يكون رمز الحمل الأوّل للمستجيب هو 1، ورمز الحمل الثاني هو 2 وهكذا على سبيل المثال. birthord: عدد تسلسلي للولادات الحية، حيث يكون رمز الطفل الأول للمستجيب هو 1؛ أما بالنسبة للولادات غير الحية فيبقى الحقل فارغًَا. birthwgt_lb وbirthwgt_oz: يمثِّلان وزن الطفل عند ولادته، مقدرًا بالرطل وبالأوقية. agepreg: عمر الأم في نهاية الحمل. finalwgt: الوزن الإحصائي المرتبط بالمستجيب، وهو قيمة عشرية تمثِّل عدد سكان الولايات المتحِدة الأمريكية الذين يمثلهم المستجيب. إذا قرأت دليل الأسس والمعايير بعناية، فسترى أنّ العديد من المتغيرات هي متغيرات محسوبة القيمة recodes، أي أنها ليست جزءًا من البيانات الأولية raw data التي جُمِعت أثناء المسح، بل حُسِبت باستخدام البيانات الأولية، فمثلًا، يساوي المتغير prglngth للولادات الحية المتغير الأوليwksgest -أي عدد أسابيع الحمل- إذا كان متوفرًا، وإلا فسيُقدَّر باستخدام mosgest * 4.33 -أي عدد أشهر الحمل مضروب بمتوسط عدد الأسابيع في الشهر الواحد-. تستند عمليات إعادة الترميز غالبًا إلى المنطق الذي يتحقق من اتساق البيانات ودقتها، كما من الجيد عمومًا اللجوء إلى استخدام إعادة الترميز إذا كانت متاحةً وما لم يكن هناك سبب مقنع يجبرنا على معالجة البيانات الأولية بأنفسنا. عمليات التحويل يتعين علينا غالبًا عند استيراد هذا النوع من البيانات التحقق من الأخطاء والتعامل مع قيم خاصة وتحويل البيانات إلى صيغ مختلفة وإجراء العمليات الحسابية، حيث تُسمى هذه العمليات بتنقية تنظيف البيانات data cleaning. يحتويnsfg.py على CleanFemPreg، وهو دالة تُنقي المتغيرات التي نُخطِّط لاستخدامها. def CleanFemPreg(df): df.agepreg /= 100.0 na_vals = [97, 98, 99] df.birthwgt_lb.replace(na_vals, np.nan, inplace=True) df.birthwgt_oz.replace(na_vals, np.nan, inplace=True) df['totalwgt_lb'] = df.birthwgt_lb + df.birthwgt_oz / 16.0 يحتوي متغير agepreg على عمر الأم في نهاية الحمل، كما يُرمَّز agepreg في ملف البيانات ليكون رقمًا صحيحًا يعبر عن السنوات، وبالتالي يقسِّم السطر الأول كل عنصر من عناصر agepreg على 100، مما ينتج عنه قيمة عشرية تمثِّل السنوات، كما يحتوي المتغيران birthwgt_lb وbirthwgt_oz على وزن الطفل بالرطل والأوقية على الترتيب، وذلك لحالات الحمل التي تنتهي بولادة حيّة، كما يَستخدِمان عدة رموز خاصة: 97 NOT ASCERTAINED 98 REFUSED 99 DON'T KNOW تكمن خطورة القيم الخاصة المرمَّزة على أساس أعداد في عدم معالجتها بصورة صحيحة، إذ يمكنها توليد نتائج وهمية، مثل طفل بوزن 99 رطل. يستبدل التابع replace هذه القيم لتكون np.nan، وهي قيمة عشرية خاصة تمثِّل "ليس عددًا" أي Not a Number، كما تطلب راية inplace من التابع replace تعديل السلسلة الموجودة عوضًا عن إنشاء سلسلة جديدة. تُعيد جميع العمليات الرياضية nan إذا كان أيًا من الوسطاء nan أي "ليس عددًا"، وذلك على أساس جزء من معيار معهد مهندسي الكهرباء والإلكترونيات IEEE للعدد العشري، كما يلي: >>> import numpy as np >>> np.nan / 100.0 nan لذا غالبًا ما تفي الحسابات باستخدام nan بالغرض وتعطي النتيجة المطلوبة، كما تعالِج معظم دوال البانداز pandas ظهور nan بصورة مناسبة، لكن سنواجه مشكلةً متكرِّرةً وهي التعامل مع بيانات ناقصة. يُنشِئ السطر الأخير من CleanFemPreg عمودًا جديدًا totalwgt_lb، ويدمج الوزن بالرطل والأوقية في كمية واحدة تقاس بالرطل. # CORRECT df['totalwgt_lb'] = df.birthwgt_lb + df.birthwgt_oz / 16.0 وليس الصيغة النقطية التالية: # WRONG! df.totalwgt_lb = df.birthwgt_lb + df.birthwgt_oz / 16.0 يضيف الإصدار الذي يحتوي على الصيغة النقطية سمةً إلى كائن إطار البيانات، ولكن لايُتعامَل مع هذه السمة على أنها عمود جديد. التحقق قد تظهر أخطاء عند تصدير البيانات من بيئة برمجية معينة واستيرادها إلى بيئة أخرى، وعندما نتعرَّف على مجموعة بيانات جديدة، فقد تفسر البيانات بصورة غير صحيحة، أو قد تفهم أمرًا ما بصورة خاطئة، فإذا خصصت وقتًا للتحقق من صحة البيانات، فستستطيع توفير الوقت لاحقًا وستتجنب الوقوع في أخطاء. تتمثَّل إحدى طرق التحقق من البيانات في حساب الإحصائيات الأساسية وموازنتها مع النتائج المنشورة، إذ يلخِّص بدليل الأسس والمعايير الخاص بالـ NSFG كل متغير مثلًا. إليك جدولًا بالخرج، حيث رُمِّز كل خرج حمل: value label Total 1 LIVE BIRTH 9148 2 INDUCED ABORTION 1862 3 STILLBIRTH 120 4 MISCARRIAGE 1921 5 ECTOPIC PREGNANCY 190 6 CURRENT PREGNANCY 352 يوفر صنف السلسلة Series تابعًا باسم value_counts مهمته حساب عدد المرات التي تظهر فيها كل قيمة، فإذا حددنا سلسلة outcome من إطار البيانات،فيمكننا استخدام value_counts لموازنتها مع البيانات المنشورة كما يلي: >>> df.outcome.value_counts().sort_index() 1 9148 2 1862 3 120 4 1921 5 190 6 352 تكون نتيجة value_counts سلسلةً؛ أما ()sort_index فسيفرز السلسلة حسب الفهرس بحيث تظهر القيم بالترتيب. وسنجد أن القيم في outcome صحيحة عند موازنة النتائج بالجدول المنشور، وبالمثل يكون هذا هو الجدول المنشور لـ birthwgt_lb: value label Total . INAPPLICABLE 4449 0-5 UNDER 6 POUNDS 1125 6 6 POUNDS 2223 7 7 POUNDS 3049 8 8 POUNDS 1889 9-95 9 POUNDS OR MORE 799 وهذه هي عدد مرات ظهور القيم: >>> df.birthwgt_lb.value_counts(sort=False) 0 8 1 40 2 53 3 98 4 229 5 697 6 2223 7 3049 8 1889 9 623 10 132 11 26 12 10 13 3 14 3 15 1 51 1 تُعَدّ الإحصاءات counts الخاصة بـ 6 و7 و8 أرطالًا صحيحةً، وإذا جمعنا الإحصاءات للأرقام بين 0-5، و9-95 فسنجد أنها صحيحة أيضًا. ولكن إذا نظرنا عن كثب، فسنلاحظ وجود خطأ في قيمة واحدة وهي الطفل بوزن 51 رطلًا، لذا سنضيف سطرًا إلى CleanFemPreg للتعامل مع هذا الخطأ: df.loc[df.birthwgt_lb > 20, 'birthwgt_lb'] = np.nan تستبدِل هذه التعليمة القيم غير الصالحة لتكون np.nan، حيث تُوفِّر السمة loc عدة طرق لتحديد الصفوف والأعمدة من إطار البيانات، إذ يكون أول تعبير محاط بقوسين في هذا المثال مُفهرس الصف؛ أما التعبير الثاني فسيحدد العمود. ينتج عن التعبير df.birthwgt_lb > 20 سلسلةً من النوع البولياني bool، حيث تشير القيمة الصحيحة True إلى صحة الشرط، كما ستحدِّد السلسلة البوليانية المستخدَمة على أساس فهرس العناصر التي تحقق الشرط فقط. التفسير يجب التفكير بمستويَين في آن معًا لنستطيع العمل مع البيانات بفعالية، وهما مستوى الإحصائيات ومستوى السياق. سنلقي نظرةً على تسلسل sequence نتائج بعض المستجيبين مثلًا، كما يتعين علينا إجراء بعض المعالجة من أجل جمع بيانات الحمل لكل مستجيب، وذلك لأن ملفات البيانات منظَّمة، وهذا مثال على دالة تفعل ذلك: def MakePregMap(df): d = defaultdict(list) for index, caseid in df.caseid.iteritems(): d[caseid].append(index) return d يُعَدّ df إطار بيانات يحتوي على بيانات الحمل، كما يحصي تابع iteritems الفهرس - أي عدد الصنف- ومعرِّف الحالة caseid لكل حمل؛ أما d فهو قاموس يعيّن لكل معرِّف حالة قائمةً من الفهارس، وفي حال عدم معرفتك السابقة بـ defaultdict فهو وحدة collections في بايثون. كما يمكننا البحث عن مستجيب والحصول على فهارس حمل خاصة به باستخدام d. يبحث المثال التالي عن مستجيب ويطبع قائمةً بنتائج الحمل الخاص به: >>> caseid = 10229 >>> preg_map = nsfg.MakePregMap(df) >>> indices = preg_map[caseid] >>> df.outcome[indices].values [4 4 4 4 4 4 1] تُعَدّ indices قائمةً بفهارس حالات الحمل الموافقة للمستجيب صاحب المعرِّف 10229. يؤدي استخدام هذه القائمة على أساس فهرس لـ df.outcome إلى تحديد الأصناف المشار إليها وإنتاج سلسلة، ولكننا حددنا سمة values بدلًا من طباعة السلسلة كاملة، والتي هي مصفوفة نمباي NumPy array، حيث يدل رمز الخرج 1 على أنّ الولادة حية، ويدل الرمز 4 على حدوث إجهاض -أي انتهى الحمل انتهاءً ذاتيًا وغالبًا بدون سبب طبي معروف-. يُعَدّ احتمال مصادفة مستجيب حصل معه هذا ممكنًا إحصائيًا، فالإجهاض أمر شائع، إذ أبلغ مستجيبون آخرون عن عدة حالات إجهاض أو أكثر، ولكن إذا نظرنا إلى مستوى السياق، فسنجد هذه البيانات تحكي قصة امرأة حملت ست مرات وانتهى كل حمل بإجهاض؛ أما الحالة السابعة والأخيرة فقد انتهت بولادة حية، وإذا فكرنا بهذه البيانات بتعاطف، فمن الطبيعي أن نتأثر بالقصة التي تحكيها. يمثِّل كل سجل في مجموعة بيانات NSFG فردًا قدَّم إجابات صادقة عن العديد من الأسئلة الشخصية والصعبة، ويمكننا بالطبع استخدام هذه البيانات للإجابة عن أسئلة إحصائية حول الحياة الأسرية والإنجاب والصحة، كما علينا في الوقت ذاته النظر في الأشخاص الذين تمثلهم هذه البيانات ونقدِّم لهم الاحترام والامتنان. تمارين التمرين الأول ستجد في المستودع repository الذي حمَّلته ملفًا باسم chap01ex.ipynb وهو IPython notebook أي مفكرة من نوع IPython، كما يمكنك تشغيل IPython notebook من سطر الأوامر command line بالصورة التالية: $ ipython notebook & يجب عليك إذا ثُبِّت IPython تشغيل خادم يعمل في الخلفية وفتح متصفح ويب من أجل عرض المفكرة، وإذا لم تكن معتادًا على IPython، فمن الأفضل البدء من IPython notebook، والذي يجب لفتحه تشغيل ما يلي: $ ipython notebook & يجب فتح نافذة متصفح ويب جديدة، وإن لم يحصل هذا، فستوفِّر رسالة بدء التشغيل محدِّد موارد مُوحَّد هو رابط URL، والذي يمكنك تحميله في المتصفح، وغالبًا ما يكون http://localhost:8888، كما يجب أن تُظهِر النافذة الجديدة قائمةً بكل الـ notebooks الموجودة في المستودع. افتح chap01ex.ipynb، حيث مُلِئت بعض الخلايا وعليك تنفيذها؛ أما الخلايا الأخرى فستزودك بتعليمات حول التمارين التي يجب عليك تجريبها. التمرين الثاني ستجد في المستودع repository الذي حمَّلته ملفًا باسم chap01ex.py، اكتب دالةً تقرأ ملف المستجيب 2002FemResp.dat.gz باستخدام هذا الملف على أساس نقطة انطلاق. يكون المتغير pregnum هو ترميز يشير إلى عدد المرات التي حدث فيها حمل مع المستجيب، وبالتالي اطبع عدد القيم value counts الخاصة بهذا المتغير ووازنها مع النتائج المنشورة في دليل الأسس والمعايير لـ NSFG. يمكنك التحقق من ملفات المستجيب والحمل عن طريق موازنة متغير pregnum مع عدد السجلات في ملف الحمل أيضًا، كما يمكنك استخدام nsfg.MalePregMap لإنشاء قاموس يصل بين كل caseid إلى قائمة من الفهارس في إطار بيانات الحمل. التمرين الثالث يُعَدّ العمل على مشروع يهمّك هو الطريقة الأفضل لتعلُّم الإحصاء، فهل ترغب بالتحقيق في سؤال مثل "هل تتأخر ولادة الأطفال الأوائل؟" فكِّر في أسئلة تجدها مثيرةً للاهتمام أو أفكار حول الحكمة التقليدية، أو مواضيع مثيرة للجدل، وانظر فيما إذا كنت تستطيع صياغة سؤال يفسح لك المجال أمام استقصاء إحصائي، وبعدها ابحث عن البيانات لكي تساعدك على تناول السؤال، وتُعَدّ الحكومات مصادر جيدة لأن البيانات من البحوث العامة غالبًا ما تكون متاحة مجانًا دون مقابل. إليك بعض المواقع الجيدة التي يمكنك البدء منها data.gov وفي المملكة المتحدة الموقع التالي gov.uk. توجد اثنتان من مجموعات البيانات المفضلة لدينا، وهما المسح الاجتماعي العام والمسح الاجتماعي الأوروبي. إذا أجاب أحد ما عن سؤالك، فألق نظرةً عن كثب لترى ما إذا كان الجواب مبررًا، فقد تكون هناك أخطاء في البيانات أو التحليل تجعل النتيجة غير موثوقة، ويمكنك في هذه الحالة إجراء تحليل آخر على البيانات ذاتها أو البحث عن مصدر أفضل للبيانات. إذا وجدت ورقة بحث منشورة تتناول سؤالك، فيجب أن تكون قادرًا على الحصول على البيانات الأولية، حيث يتيح الكثير من المؤلفِين بياناتهم على الإنترنت، ولكن قد تضطر إلى التواصل مع المؤلفِين من أجل البيانات الحساسة، وتخبرهم بما تخطط له لاستخدام البيانات، أو ربما عليك الموافقة على شروط محدَّدة، لذلك كن مثابرًا. المفاهيم الأساسية الأدلة القولية anecdotal evidence: أدلة جُمِّعت بصورة عرضية بدلًا من دراسة مصمَّمة بصورة جيدة، وغالبًا ما تكون شخصية. السكان population: مجموعة من أشخاص هم محط اهتمام دراستنا، وغالبًا ما تشير كلمة "سكّان" إلى مجموعة من الأشخاص؛ إلا أنه يمكن استخدام هذا المصطلح لمواضيع أخرى أيضًا. الدراسة المقطعية cross-sectional study: هي الدراسة التي تجمع البيانات عن السكان في نقطة محدَّدة من الزمن. دورة cycle: في الدراسة المقطعية التي تتكرر عدة مرات، يُدعى كل تكرار للدراسة بدورة. الدراسة الطولية longitudinal study: هي الدراسة التي تتبع السكان على طول فترة من الزمن، وتجمع البيانات من المجموعة ذاتها بصورة متكرِّرة. سجل record: في مجموعة البيانات، فإنّ السجل هو تجميعة من المعلومات حول شخص واحد أو موضوع معين. مستجيب respondent: هو الشخص الذي يستجيب للمسح الإحصائي. عيّنة sample: هي مجموعة فرعية من السكان الذين تُجمع البيانات منهم. تمثيلي representative: تكون العينة تمثيليةً إذا كان لكل فرد من السكّان فرصةً متساويةً ليكون في العينة. الإفراط في أخذ العينات oversampling: تعتمد هذه التقنية على زيادة تمثيل مجموعة فرعية من السكّان لتجنب الأخطاء الناجمة عن صغر أحجام العينات. البيانات الأولية raw data: هي القيم التي تُجمَع وتُسجَّل مع القليل من عمليات التحقّق أو الحساب أو التفسير إن وجدت. متغير محسوب القيمة recode: قيمة مولَّدة عن طريق حسابات على البيانات الأولية أو عن طريق تطبيق منطق آخر عليها. تنقية البيانات data cleaning: هي العمليات التي تشمل التحقق من البيانات وتحديد الأخطاء والتحويل بين أنواع البيانات والتمثيلات، …إلخ. ترجمة -بتصرف- للفصل الأول Chapter 1 Exploratory data analysis من كتاب Think Stats: Exploratory Data Analysis in Python. اقرأ أيضًا تطويع البيانات في جافاسكربت تحليل نظام الملفات لإدارة البيانات وتخزينها واختلافه عن نظام قاعدة البيانات النسخة الكاملة لكتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة