عند التعامل مع مجموعات من البيانات الذرية المتجانسة (مثلًا جميعها أعداد أو جميعها محارف)، تتوفر في R البنى التالية:
vector للبيانات أحادية البُعد، ويمكن الوصول إليها بفهرس واحد.matrix للبيانات ثنائية البُعد، المستطيلة (وليست متفاوتة الأطوال).array للحالة العامة ذات الأبعاد N. وmatrix ليست سوى array ذات N=2.لمبرمجي Python: قد تبدو لك هذه الفقرة مألوفة.
أي تشابه مع NumPy ليس مصادفة (لكن R أقدم قليلًا، وهي تملك كل هذا في اللغة الأساسية).
المصفوفة في جوهرها ليست سوى متجه، مضافًا إليه عدد الصفوف والأعمدة الذي يحدد الشكل.
وهناك دالة matrix() لإنشائها:
# data already available
matrix(1:6, nrow=2, ncol=3)
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
# empty matrix to be filled in later
matrix(nrow=2, ncol=3)
#> [,1] [,2] [,3]
#> [1,] NA NA NA
#> [2,] NA NA NA
لاحظ أن البيانات تُخزَّن بترتيب الأعمدة أولًا (أي نزولًا في العمود ثم انتقالًا إلى العمود التالي). وهذا هو نفسه ما في Fortran وMatlab وJulia، لكنه يختلف عن C/C++ وNumPy.
يمكنك إيجاد أبعاد المصفوفة باستخدام dim() للحصول على عدد الصفوف والأعمدة معًا، أو nrow() أو ncol() للحصول على بُعد واحد فقط. أما استخدام length() فسيُرجع طول المتجه الأساسي فقط، أي العدد الإجمالي للعناصر (6 في المثال أعلاه).
وهناك دوال أخرى تُنتج مخرجات على هيئة مصفوفة، مثل str_split_fixed().
library(stringr)
str_split_fixed(c("split some words", "and some more"), " ", 3)
#> [,1] [,2] [,3]
#> [1,] "split" "some" "words"
#> [2,] "and" "some" "more"
تعمل هذه الطريقة بالضبط كما تعمل فهرسة المتجهات، مع فارق واحد هو الحاجة إلى تحديد الصف (أو الصفوف) والعمود (أو الأعمدة)، بهذا الترتيب. واترك البُعد فارغًا للحصول على الصف أو العمود كاملًا.
وباستخدام المصفوفة نفسها كما في الأعلى:
m <- matrix(1:6, nrow=2, ncol=3)
m
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
m[2, 3] # row 2, col 3
#> [1] 6
m[1:2, 3] # rows 1:2, col 3
#> [1] 5 6
m[2, ] # all of row 2
#> [1] 2 4 6
لاحظ أن المجموعة الجزئية التي تتضمن صفًا واحدًا و/أو عمودًا واحدًا ستأتي في صورة متجه، لا مصفوفة.
class(m)
#> [1] "matrix" "array"
class(m[2,])
#> [1] "integer"
وهذا النوع من dimension reduction ليس مرغوبًا دائمًا، خصوصًا داخل تسلسل من الدوال يتوقع مدخلات على شكل مصفوفة.
وتحديد drop=FALSE يضمن أن تكون القيمة المُعادة مصفوفة عندما يكون المدخل مصفوفة. لاحظ أن المصفوفة التي تحتوي على صف واحد فقط أو عمود واحد فقط ليست من النوع نفسه الذي ينتمي إليه المتجه.
m[2, , drop = FALSE]
#> [,1] [,2] [,3]
#> [1,] 2 4 6
وتحوّل دالة as.matrix() المتجه إلى مصفوفة بعمود واحد.
as.matrix(1:3)
#> [,1]
#> [1,] 1
#> [2,] 2
#> [3,] 3
وكما هو الحال مع المتجهات، يمكن تعديل أجزاء من المصفوفة بمجرد إسناد قيم جديدة إليها.
m[, 2] <- c(10, 20)
m
#> [,1] [,2] [,3]
#> [1,] 1 10 5
#> [2,] 2 20 6
مع المتجهات، كانت دالة c() كافية لإدراج البيانات وإلحاقها. أما المصفوفات فبنيتها أكثر تعقيدًا، ولذلك تحتاج إلى مزيد من العناية عند تحديد المطلوب.
تربط دالتا rbind() وcbind() صفًا أو صفوفًا جديدة أو عمودًا أو أعمدة جديدة بالمصفوفة.
m
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
m2 <- matrix(7:9, nrow = 1, ncol = 3)
rbind(m, m2)
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
#> [3,] 7 8 9
ويستخدم حذف صف أو عمود الفهرسة السالبة، تمامًا كما في المتجهات.
m
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
m[, -2]
#> [,1] [,2]
#> [1,] 1 5
#> [2,] 2 6
تعمل جميع دوال المتجهات أيضًا على المصفوفات، وتُطبَّق على كل العناصر. غير أنها قد تفقد بنية المصفوفة وتُرجع متجهًا.
sqrt(m)
#> [,1] [,2] [,3]
#> [1,] 1.000000 1.732051 2.236068
#> [2,] 1.414214 2.000000 2.449490
sum(m)
#> [1] 21
m[m > 3]
#> [1] 4 5 6
ومع المصفوفات، قد تفيدك أيضًا دالتا colSums() وrowSums(). وتقليص الأبعاد مقصود في هذه الحالة، لذا تكون القيم المُعادة متجهات.
# m as defined above
rowSums(m)
#> [1] 9 12
colSums(m)
#> [1] 3 7 11
وبالمثل، هناك rowMeans() وcolMeans() لحساب المتوسطات.
رأينا في مفاهيم أخرى أن مكتبات Tidyverse يمكنها تبسيط العمليات على المتجهات والقوائم (purrr) أو على إطارات البيانات (dplyr).
لكن المصفوفات، ثنائية الأبعاد كانت أم متعددة الأبعاد، تقع في الغالب خارج نطاق مدخلات Tidyverse (على الأقل في الوقت الحالي).
ودالة apply() جزء من R الأساسية، ولا تزال أفضل أداة للتحكم الأدق في البرمجة بالمصفوفات.
والمعاملات، بالترتيب، هي:
دالة rowMeans() موجودة في R الأساسية، لكن يمكننا أيضًا محاكاتها باستخدام apply() وmean().
randm <- sample(1:10, 16, replace = TRUE) |> matrix(4, 4)
randm
#> [,1] [,2] [,3] [,4]
#> [1,] 4 9 8 3
#> [2,] 6 8 5 5
#> [3,] 4 2 1 2
#> [4,] 4 2 5 4
apply(randm, 1, mean)
#> [1] 6.00 6.00 2.25 3.75
rowMeans(randm)
#> [1] 6.00 6.00 2.25 3.75
تفترض هذه الفقرة مستوى من المعرفة الرياضية يقع خارج نطاق Exercism.
يمكنك تخطيها إن لم تكن مفهومة بالنسبة إليك.
غير أن R لغة لعلوم البيانات، وكثير من مستخدميها لديهم خلفية في الرياضيات التطبيقية والإحصاء. وقد يجد بعض الطلاب في هذا الملخص المقتضب ما يهمهم.
تعدّ R الحديثة، «كما هو واضح»، لغةً لعلوم البيانات: فهي مصممة للعمل أساسًا مع البيانات الجدولية، ويستخدمها كثيرًا علماء الإحصاء وعلماء الأوبئة وعلماء الاجتماع. ومنافسها الرئيسي في هذا المجال هو المنظومة العلمية للغة Python، بما فيها حزم مثل Pandas أو Polars.
غير أن R كانت تُرى في العقود السابقة أيضًا بوصفها لغة هندسية محتملة، تنافس Matlab مباشرة.
وربما بشكل مفاجئ، لا تزال R الأساسية تدعم مجموعة كاملة من عمليات الجبر الخطي على المتجهات والمصفوفات. وليس واضحًا إلى أي مدى ما زالت هذه العمليات حاضرة في كود المستخدم النهائي، لكنها تبقى مهمة جدًا داخليًا لتنفيذ الدوال الإحصائية.
ولا يزال الجبر الخطي في R يُدرَّس في بعض المقررات الجامعية، مثل محاضرة الاقتصاد القياسي هذه. وهناك دليل جيد للرياضيات (لا للبرمجة) على 3Blue1Brown.
أكثر مناقشة مكتملة للجبر الخطي داخل Exercism توجد في منهج Julia (والمؤلفون أنفسهم الذين أعدّوا منهج R).
ونرى أن Julia (أو Python في السياقات التي تتوفر فيها وحدة numpy.linalg) أصبحت اليوم لغة أنسب من R للعمل الجاد في الجبر الخطي.
يمتد القطر الرئيسي لمصفوفة مربعة (N x N)) اسمها A من أعلى اليسار (A[1,1]) إلى أسفل اليمين (A[N,N]).
وتتصرف دالة diag() بشكل مختلف تبعًا للوسيط المُمرَّر إليها.
مع متجه v، تُنشئ diag(v) مصفوفة قطرية.
diag(1:3)
#> [,1] [,2] [,3]
#> [1,] 1 0 0
#> [2,] 0 2 0
#> [3,] 0 0 3
ومع مصفوفة مربعة A، تُرجع diag(A) العناصر الواقعة على القطر الرئيسي في صورة متجه.
A
#> [,1] [,2] [,3]
#> [1,] 10 5 2
#> [2,] 9 3 7
#> [3,] 8 10 4
diag(A)
#> [1] 10 3 4
ومع عدد صحيح واحد k، تُنشئ diag(k) مصفوفة وحدة من الحجم k × k: وهذا ربما ليس ما كنت تتوقعه.
حتى كتب R الجادة يكثر فيها أن تضيف تعليقات مثل "فمن يفهم هذا؟" عند هذه النقطة.
diag(3)
#> [,1] [,2] [,3]
#> [1,] 1 0 0
#> [2,] 0 1 0
#> [3,] 0 0 1
لتبديل الصفوف والأعمدة، تُنتج دالة t() منقول أي مصفوفة.
m
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
t(m)
#> [,1] [,2]
#> [1,] 1 2
#> [2,] 3 4
#> [3,] 5 6
إذا كانت المصفوفة ذات قيم عقدية، فإن النتيجة ليست المرافق العقدي. استخدم Conj(t(A)) للحصول على المنقول المرافق (المرافق) للمصفوفة A.
وبالنسبة إلى المصفوفات ذات الأبعاد الأعلى، تُبدّل دالة aperm() ترتيب الأبعاد.
ولإيجاد معكوس مصفوفة مربعة A يمكننا استخدام solve(A). ويجد بعضنا هذه الصيغة ... غير متوقعة.
وضرب مصفوفة في معكوسها يعطي مصفوفة الوحدة (مع أخطاء تقريب صغيرة جدًا).
A <- matrix(sample(1:10, 9, replace = TRUE), 3, 3)
A
#> [,1] [,2] [,3]
#> [1,] 10 5 2
#> [2,] 9 3 7
#> [3,] 8 10 4
solve(A)
#> [,1] [,2] [,3]
#> [1,] 0.16666667 0.00000000 -0.08333333
#> [2,] -0.05747126 -0.06896552 0.14942529
#> [3,] -0.18965517 0.17241379 0.04310345
A %*% solve(A) |> round(digits = 10)
[,1] [,2] [,3]
#> [1,] 1 0 0
#> [2,] 0 1 0
#> [3,] 0 0 1
تعمل العوامل الحسابية المعتادة مثل + و- و* و^ على المصفوفات عنصرًا بعنصر. وإذا لم تتطابق الأبعاد، تنطبق قواعد إعادة التدوير المعتادة.
m
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
m * m
#> [,1] [,2] [,3]
#> [1,] 1 9 25
#> [2,] 4 16 36
# same calculation, different syntax
m ^ 2
#> [,1] [,2] [,3]
#> [1,] 1 9 25
#> [2,] 4 16 36
شرح الأنواع الأخرى المتنوعة من ضرب المتجهات والمصفوفات خارج نطاقنا، لكن إليك ملخصًا موجزًا جدًا:
%*% هو الصيغة الوسيطة للعامل matmult()، ويعطي جداء المصفوفات.%o% هو الصيغة الوسيطة للعامل outer()، ويعطي الجداء الخارجي.crossprod() يعطي الجداء الاتجاهي.