هنگام کار با مجموعههایی از دادههای اتمی همگن (مثلاً همه عددی یا همه کاراکتری)، ساختارهای زیر در R در دسترس هستند:
vector برای دادههای یکبعدی، که با یک اندیس به آن دسترسی پیدا میکنید.matrix برای دادههای دوبعدی و مستطیلی (نه دندانهدار).array برای حالت کلی Nبعدی. یک matrix صرفاً یک array با N=۲ است.برای برنامهنویسان 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() فقط طول بردار زیربنایی را برمیگرداند: تعداد کل عناصر (۶ در مثال بالا).
برخی توابع دیگر خروجی ماتریسی تولید میکنند، مثلاً 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() بخشی از Base R است و هنوز بهترین ابزار برای کنترل دقیقتر برنامهنویسی ماتریس است.
پارامترها، به ترتیب، عبارتاند از:
تابع rowMeans() در Base 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 رقابت میکرد.
شاید جای تعجب باشد که Base 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() ضرب برداری را میدهد.