Коли ми працюємо з колекціями однорідних атомарних даних (наприклад, усі числові або всі символьні), у 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() входить до 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
Для матриці з комплексними значеннями результат не є комплексно спряженим.
Щоб отримати спряжене транспонування (приєднану матрицю, adjoint) матриці A, використовують Conj(t(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() дає векторний добуток.