RMarkdown, R и ggplot


Данная статья не является ни документацией, ни рассказывает что-то принципиально новое, её стоит рассматривать как обзорную или как шпаргалку.


Преамбула


Конференция это прежде всего доклады, и далеко не последнее место занимает то, как оформлены слайды доклада.


Безусловно, есть докладчики, которые могут не смотря ни на что, провести доклад даже без единого слайда, но всё же они как правило хорошо дополняют повествование. Одним достаточно накидать мемасиков в доклад и дело готово, другим обязательно надо вставить код, причём на ассемблере (кто не в курсе ещё — JPoint — это конференция по java), и есть ещё те, кому надо показать графики. Впрочем, встречается и их комбинация.


Пожалуй известные средства для создания слайдов это:


  • PowerPoint, и вариации в лице LibreOffice Impress, Apple KeyNote
  • облачные вариации с тем же подходом — Google Slides
  • LaTeX
  • и относительно новый (для меня) RMarkdown


И если первые два по своей сути представляют бинарные форматы, а облачные Google Slides и ко ещё требуют наличие интернета, что является некоторым неприятным ограничением (во время поездок и перелётов), то последние два — и оффлайновые, и исключительно текстовые, а значит можно хранить историю всех изменений в git/hg/на ваш вкус. Кроме того, область применения далеко не ограничена только слайдами.


LaTeX формат с историей — много написано и сказано, а вот RMarkdown молодой, даже немного хипстерский, но без подворотов.


Markdown


Markdown это облегченный язык разметки, созданный с целью написания максимально читаемого и удобного для правки текста. Markdown является и лёгким для понимания, и легким для чтения даже без каких-либо трансформаций.


Сравните сами: _курсив_ это курсив, **сильное выделение** это сильное выделение, и многое другое — более подробно описано в Markdown cheatsheet.


Markdown поддерживают github, habrahabr, sublime, jira (имеет схожий синтаксис), и многие другие.


R


R — язык программирования для статистической обработки данных и работы с графикой.


Как правило это останавливает — это очень сложно, это математика, и это не надо — но никто же заставляет использовать всю доступную функциональность, пожалуй самый простой и наглядный — это графики и визуализация.


Хотя часто для того, чтобы построить какой-либо график используют Excel, он с большим трудом справляется, когда количество данных приближается уже к миллиону. Тогда как для R это не является какой-то сложной задачей.

Данные и графики


Оставим за кадром баталию, что же лучше — таблицы или графики. Вопрос вкуса.

Для построения графиков используем расширение ggplot2.


Дополнено: Нам необходим сам R и RStudio, например для MacOS / Homebrew:


$ brew tap homebrew/science
$ brew install r
$ brew сask install rstudio

устанавливаем модуль в RStudio для R :


install.packages("ggplot2")

Но чтобы строить графики нужны данные, и разумно хранить их отдельно от представления, например, в csv формате — опять же — простой текстовый формат.


Мои данные — это результаты полученные при помощи jmh для моего доклада Внутрь VM сквозь замочную скважину hashCode. Мне нравится стиль, используемый Алексеем Шипилёвым: записывать результаты benchmark'а в виде комментария в начале файла — grep-n-sed и мы имеем csv-файл.

данные csv/allocations.csv:


pos,alloc,value,error
10,single-threaded,2.836,0.285
20,java,9.878,2.676
28,epsilon,75.289,23.667
30,sync,186.672,21.195
40,cas,74.721,0.192
50,tlab,8.506,1.849
55,javaHashCode,60.270,12.318
57,readHashCode,7.296,0.316

Формируем таблицу данных (data frame) из csv-файла — по-умолчанию, считается, что в файле есть заголовок — это нам поможет при обращении к отдельным колонкам


```{r}
df = read.csv(file = "csv/allocations.csv")
```

если хотим отфильтровать, н-р, по конкретным значениям в колонке alloc


df <- subset(df, alloc == "cas" | alloc == "java" | alloc == "sync" | alloc == "tlab" )

Бары / столбики


Для начала необходимо задать схему соответствия переменных (aes) из таблицы данных — отображать будем значение от типа, в нашем случае типа аллокации, цвет бара будет выбран также на основе типа аллокации.


ggplot(data=df, aes(x=alloc, y=value, fill=alloc))

Отображать будем в виде столбиков ( bar chart ) + geom_bar()


ggplot(data=df, aes(x=alloc, y=value, fill=alloc))
+ geom_bar(stat="identity")

результат:


повернём систему координат (из вертикальных в горизонтальные бары) опцией + coord_flip()


ggplot(data=df, aes(x=alloc, y=value, fill=alloc)) +
geom_bar(stat="identity") +
coord_flip()


добавим ошибку изменений + geom_errorbar() (помните, у нас в csv-файле есть колонка error):


ggplot(data=df, aes(x=alloc, y=value, fill=alloc)) +
geom_bar(stat="identity") +
coord_flip() +
geom_errorbar(aes(ymin = value - error, ymax = value + error), width=0.5, alpha=0.5)


для наглядности стоит добавить значения рядом с баром + geom_text() (логично, что текстом будет value)


ggplot(data=df, aes(x=alloc, y=value, fill=alloc)) +
geom_bar(stat="identity") +
coord_flip() +
geom_errorbar(aes(ymin = value - error, ymax = value + error),  width=0.5, alpha=0.5) +
geom_text(aes(label=value))


добавим лоска подписи + geom_text():


  • используем функцию для изменения подписи значение ± ошибкаlabel=base::sprintf("%0.2f ± %0.2f", value, error) (привет старый добрый sprintf и шаблоный форматирования %f!)
  • поиграемся с горизонтальным hjust и вертикальным vjust расположением подписи
  • изменим размер шрифта size и начертание fontface подписи


    ggplot(data=df, aes(x=alloc, y=value, fill=alloc)) +
    geom_bar(stat="identity") +
    coord_flip() +
    geom_errorbar(aes(ymin = value — error, ymax = value + error), width=0.5, alpha=0.5) +
    geom_text(aes(label=base::sprintf("%0.2f ± %0.2f", value, error)), hjust=-0.1, vjust=-0.4, size=5, fontface = "bold")




  • подкрутим тему + theme_classic()
  • уберём легенду + theme(legend.position="none")
  • добавим подписи + labs(title =…, x =…, y = ..) и шрифты для осей + theme(axis.text.y = ..)


    ggplot(data=df, aes(x=alloc, y=value, fill=alloc)) +
    geom_bar(stat="identity") +
    coord_flip() +
    geom_errorbar(aes(ymin = value — error, ymax = value + error), width=0.5, alpha=0.5) +
    geom_text(aes(label=base::sprintf("%0.2f ± %0.2f", value, error)), hjust=-0.1, vjust=-0.4, size=5, fontface = "bold") +
    labs(title = "@Threads( 4 )", x = "", y = "ns/op") +
    theme_classic() +
    theme(axis.text.y = element_text(size = 16, face = "bold")) +
    theme(axis.title = element_text(size = 16, face = "bold")) +
    theme(legend.position="none")




и для придания финального лоска


  • специфицируем цвета + scale_fill_manual()
  • удалим зазор между вертикальной осью и баром + scale_y_continuous() и немного расширим диапазон значений, чтобы и ошибка помещалась, и подпись
  • и ещё зафиксируем порядок баров, согласно колонке pos: x=reorder(alloc, -pos)


ggplot(data=df, aes(x=reorder(alloc, -pos), y=value, fill=alloc)) +
geom_bar(stat="identity") +
coord_flip() +
geom_errorbar(aes(ymin = value - error, ymax = value + error),  width=0.5, alpha=0.5) +
geom_text(aes(label=base::sprintf("%0.2f ± %0.2f", value, error)), hjust=-0.1, vjust=-0.4, size=5, fontface = "bold") +
scale_fill_manual(values=c(java'='#a9a518','sync'='#fa8074', 'cas'='#00b3f6', 'tlab'='#e67bf3')) +
labs(title = "@Threads( 4 )", x = "", y = "ns/op") +
theme_classic() +
scale_y_continuous(limits=c(0, max(df$value) + 40), expand = c(0, 0)) +
theme(axis.text.y = element_text(size = 16, face = "bold")) +
theme(axis.title = element_text(size = 16, face = "bold")) +
theme(legend.position="none")


tip: можно сохранить график в файл + ggsave("allocations.svg"), но не стоит злоупотреблять векторным форматом, если на графике много точек — сохраняйте в растр, н-р png.


install.packages("svglite")

Отдельно стоит отметить цветовую палитру, используемую по-умолчанию: выбираемые цвета хорошо различимы даже для людей с ослабленым цветовосприятием.

Не используйте сочетание красный/зелёный даже если вы используете всего два цвета, чтобы выделить, что лучше/хуже — эти цвета слабо различимы для людей с ослабленым цветовосприятием.

tip: colorbrewer2 поможет выбрать в т.ч. безопасные цвета.


Точки


Есть некоторое распределение адресов по нитям


step,thread,address
1,thread-0,807437816
1,thread-1,807437784
1,thread-2,807437800
..........

  • отобразим отдельно по точкам + geom_point


    • чтобы можно было различить одну нить от другой, укажем не только цветовую aes(..., colour = thread,...) дифференциацию, но и на основе формы маркера aes(..., shape = thread,...)

    ggplot(data=df, aes(x = address, y = index, group=thread, colour=thread, shape=thread)) +
    geom_point(size=2)




Первое, что бросается в глаза (кроме того, что много данных) — это значения адресов в научной нотации. Как-то привычней иметь дело с адресами в 16-тиричной системе: добавим форматирование значений по оси X +scale_x_continuous():


ggplot(data=df, aes(x = address, y = index, group=thread, colour=thread, shape=thread)) +
geom_point(size=2) +
scale_x_continuous(
    labels = function(n){base::sprintf("0x%X", as.integer(n))}
)


Лучше, но всё равно как-то сложно и мало понятно.


Поскольку мы можем задавать любую функцию, то почему бы не отображать смещение относительно некоторого базового, н-р минимального, адреса:


min_address = min(df$address)

ggplot(data=df, aes(x = address, y = index, group=thread, colour=thread, shape=thread)) +
geom_point(size=2) +
scale_x_continuous(
    labels = function(n){base::sprintf("+ %d MiB", as.integer((n - min_address)/1024/1024))}
)


Опять же в силу привычки — поставим метки (breaks =) по целым числам — а именно 16, 32, 48, 64Мб:


ggplot(data=df, aes(x = address, y = index, group=thread, colour=thread, shape=thread)) +
geom_point(size=2) +
scale_x_continuous(
    labels = function(n){ifelse(n == min_address,  base::sprintf("base"), base::sprintf("+ %d MiB", as.integer((n - min_address)/1024/1024)))},
    breaks=c(min_address, min_address + 16*1024*1024, min_address +  32*1024*1024,  min_address + 48*1024*1024,  min_address + 64*1024*1024)
)


Данных много — хотим взглянуть на небольшую её часть


  • ограничим набор данных по количеству nrows при загрузке read.csv(...)
  • добавим метки в нужных местах на оси Y: + scale_y_continuous(breaks = c(...))


    df = read.csv(file = "csv/hashCodesNoTLAB.csv", nrows = 36, header = TRUE)


    min_address = min(df$address)


    ggplot(data=df, aes(x = address, y = index, group=thread, colour=thread, shape=thread)) +
    geom_point(size=4) +
    scale_x_continuous(
    labels = function(n){ifelse(n == min_address, base::sprintf("base"), base::sprintf("+ %d", as.integer((n — min_address))))},
    breaks=c(min_address, min_address + 1610, min_address + 21610, min_address + 31610, min_address + 41610, min_address + 516*10)
    ) +
    scale_y_continuous( breaks = c(1,2,3,4,5,6,7,8,9,10) ) +




Гистограмма


И конечно же — гистограмма, частотное распределение. Очень грубо это можно описать как количество элементов попадающих в диапазон значений. Н-р, для ряда [1, 2, 3, 1, 1] — гистограмма будет выглядеть как [3, 1, 1] — т.к элемент 1 встретился 3 раза, а элементы 2 и 3 по одному разу.


addressHashCode = read.csv(file = "csv/addressHashCode.csv")
defaultHashCode = read.csv(file = "csv/defaultHashCode.csv")

ggplot() +
geom_histogram(data=addressHashCode, aes(x=hashCode, fill="address"), alpha=0.7, bins = 500) +
geom_histogram(data=defaultHashCode, aes(x=hashCode, fill="MXSRng"), alpha=0.7, bins = 500) +


Добавляем уже известные опции до придания нужного вида:


ggplot() +
geom_histogram(data=addressHashCode, aes(x=hashCode, fill="address"), alpha=0.7, bins = 500) +
geom_histogram(data=defaultHashCode, aes(x=hashCode, fill="MXSRng"), alpha=0.7, bins = 500) +
scale_fill_manual(name="тип hashCode:", labels=c("address"="адрес", "MXSRng"="MXS-гпсч"), values=c("address" ="#003dae", "MXSRng" = "#ae003d")) +
labs(title =
     sprintf("количество коллизий по адресу: %s k, по MXS-гпсч: %s k",
             round( sum(duplicated(addressHashCode)) / 1000, 1),
             round( sum(duplicated(defaultHashCode)) / 1000, 1)),
   x = "hashCode") +
theme_classic() +
theme(axis.title.y=element_blank()) +
scale_y_continuous(labels = function(n){format(n, big.mark = "_", scientific = FALSE)}, expand = c(0, 0)) +
scale_x_continuous(labels = function(n){format(n, big.mark = "_", scientific = FALSE)}, expand = c(0, 0)) +
theme(axis.title = element_text(size = 16, face = "bold")) +
theme(axis.text.y = element_text(size = 14, face = "bold")) +
theme(axis.text.x = element_text(size = 14, face = "bold")) +
theme(axis.title = element_text(size = 16, face = "bold")) +
theme(axis.title.x=element_text(margin=margin(t=20))) +
theme(legend.text = element_text(size = 14, face = "bold")) +
theme(title = element_text(size = 16, face = "bold")) +


А пони? Запросто!


  • нужен пакет png: install.packages('png')
  • img <- readPNG("images/pony.png")
  • отрендерим во внутренний буфер g <- rasterGrob(img)
  • добавим пони как аннотацию + annotation_custom():

# install.packages('png')

img <- readPNG("images/pony.png")
g <- rasterGrob(img, interpolate=TRUE, x = 0.1, y = 0.9, width = 0.2, height = 0.2)

  ggplot() +
  annotation_custom(g, xmin=-Inf, ymin = -Inf, xmax=Inf, ymax=Inf) +
  geom_histogram(data=addressHashCode, aes(x=hashCode, fill="address"), alpha=0.7, bins = 500) +
  geom_histogram(data=defaultHashCode, aes(x=hashCode, fill="MXSRng"), alpha=0.7, bins = 500)


Немного полезных ссылок: ggplot2 tutorial и ещё один полезный блог Data wrangling, exploration, and analysis with R


RMarkdown


Не надо быть большим капитаном, чтобы сообразить, что RMarkdown это R + Markdown.


Устанавливаем модуль для R :


install.packages("rmarkdown")

И со словами HTML знаю, LaTeX люблю рендерим RMarkdown:


rmarkdown::render("path_to_file.Rmd", encoding = "UTF-8")

  • возможен вывод в следующие форматы:

В заголовке досаточно указать, например:


output: pdf_document

  • слайды/презентация:
    • HTML5 и упарываться javascript / css / html5, хотя далеко не все одобряют html-презентации
    • PDF и использовать в т.ч. макросы LaTeX

для этого в заголовке достаточно поменять output:


output: ioslides_presentation
Поделиться с друзьями
-->

Комментарии (26)


  1. Meklon
    03.05.2017 16:22

    image
    С эстетической так себе, имхо. Я на seaborn в python рисую такое.


    1. vladimir_dolzhenko
      03.05.2017 16:29

      На выходе jmh даёт две цифры — значение и ошибку, указывая, что подразумевается нормальное распределение — поэтому просто нет данных, чтобы строить ещё и outlier.

      Но, если мне не изменяет память, что-то подобное можно и в R/gglot нарисовать


      1. Meklon
        03.05.2017 16:50

        Вопрос именно по эстетической компоненте, что критично для презентаций. Очень уж брутально и цвета не слишком приятны из коробки.


        1. kxx
          04.05.2017 12:59
          +2

          В R есть пакет RColorBrewer. Там присутствуют более приятные палитры.

          Пример


    1. Zenker
      04.05.2017 02:17
      +3

      Просто тут с эстетикой, мягко говоря, не заморачивались. Возможностей у ggplot2 в этом плане бесчисленное множество. Вот такое у меня получалось после смены темы с палитрой и добавления мелочей вроде наклона текста:

      Картинка
      image


      1. Meklon
        04.05.2017 08:44

        Намного симпатичнее, да. Штатные палитры почему-то обычно очень вырвиглазны.


        1. Ananiev_Genrih
          04.05.2017 15:13
          +2

          простите, не смог удержаться


          library(ggplot2)
          ggplot(mpg,aes(x = as.factor(class),y=cty))+geom_point(position = position_jitter(height = .1),shape=1,size=3,aes(col=as.factor(class)))+
            geom_boxplot( width = 0.2,alpha=.6)+  geom_violin(aes(fill=as.factor(class)),alpha=.3)+theme_light()+theme(legend.position = "NA")+
            labs(x="кузов",y="миль на галлон",title="Распределение расхода топлива  в городском режиме")

          image


          1. Meklon
            04.05.2017 15:23

            Мне все равно Seaborn больше нравится. и его Violinplot тоже)
            image


            1. rkosolapov
              05.05.2017 11:56
              +1

              Такой вид ggplot2 наверняка может, насколько я его знаю, с настройками там всё в порядке. Почему дефолты не такие красивые — ну потому что в первую очередь они для анализа, а не для красоты, поэтому там упор на читаемость и отсутствие ошибок восприятия. В целом, наверное, было бы неплохо создать пакетик, который такие дефолты выставляет, чтобы было красиво. Но, как обычно, те, кто это могут — тем это не надо, а тем, кому надо — не могут :)


      1. vladimir_dolzhenko
        04.05.2017 10:51

        Отличный пример — Вангую sniplet графика!

        У меня была ещё идея показать не просто, что R/ggplot2 крут, но и ввести в тематику постепенно увеличивая сложность графиков, добавляя больше опций.


  1. SirEdvin
    03.05.2017 17:23

    Не совсем понятно, почему не Jupyter, который аж несколько раз умеет в презентации.
    Или, например, Mathematica, в которой их можно делать даже интерактивными.


    1. vladimir_dolzhenko
      03.05.2017 17:39
      +3

      На мой взгляд серебрянной пули нет — для каких-то целей хороша и Mathematica — мне нужен был выход pdf / html, интерактивность не требовалась. Про Jupyter не слышал, как впрочем, полагаю про многие другие решения — как-то R был больше на слуху — вот и воспользовался им.


      1. atikhonov
        03.05.2017 17:43
        +1

        в R также можно делать интерактивность, как элементов вывода, так и ввода,
        а с минимумом изменений в разметке, разместить отчет в интернете, создать сайт или книгу, и также их опубликовать «одним кликом».


      1. WizardryIB
        04.05.2017 10:46

        Использование Shiny позволяет интегрировать интерактивную графику на R даже в PowerPoint.


  1. Realetive
    04.05.2017 10:47
    +1

    Для тех, кто всё же «одобряют html» для создания презентаций, есть Shower. Никаких компиляций и сборок, правите в %editor% исходные файлы и в браузере наслаждаетесь результатом. PDF получаем, сохранив печатную версию.


    1. vladimir_dolzhenko
      04.05.2017 10:48

      При этом остаётся открытым вопрос — как и чем вы будете рисовать графики?


      1. shoorick
        04.05.2017 16:35
        -1

        А это уже другой вопрос, ответ на который зависит, в том числе и от того, что за графики. В некоторых случаях достаточно нарисовать в чём-то знакомом и сделать растровый файл, который и вставить в презентацию.


        1. vladimir_dolzhenko
          04.05.2017 16:56

          Вот я и рисую графики в R.


    1. shoorick
      04.05.2017 16:34
      -1

      А есть ещё Bochar — пишете исходник в Маркдауне, получаете HTML-презентацию в Shower.


  1. PieceOfMeat
    04.05.2017 10:48

    (оффтоп) Как нечто среднее между обычными офисными пакетами для презентаций и презентациями в виде программного кода могу посоветовать reveal.js


    Разметка презентаций в HTML, соответственно если умеете верстать, то можно добиться отличных результатов гораздо быстрее и удобнее чем в PowerPoint. И плюс плагины, highlight-js подсветка кода, markdown-разметка, можно дополнительно написать свой js-код.


    В свое время пришлось готовить много лекций, и reveal просто спас.


    1. vladimir_dolzhenko
      04.05.2017 10:49

      Тот же самый вопрос, что к комментарию выше про Shower — как и чем вы будете рисовать графики со всей эстетикой?


      1. PieceOfMeat
        04.05.2017 14:03
        -1

        Через javascript, c3.js например


        1. vladimir_dolzhenko
          04.05.2017 17:00

          Можно PoC как с его помощью построить гистограму на 10млн объектов — как оно будешь шевелится при этом?


    1. ikashnitsky
      04.05.2017 17:00
      +2

      reveal.js прекрасно создается из-под R.
      http://rmarkdown.rstudio.com/revealjs_presentation_format.html
      Очень рекомендую.
      Мой пример


  1. relgames
    11.05.2017 12:46
    +1

    Не хватает инструкции, как и где это все делать.

    MacOS / Homebrew:

    brew install homebrew/science/r
    brew cask install rstudio
    

    Затем RStudio.


    1. relgames
      11.05.2017 12:51

      Последняя строка лишняя — «brew cask install rstudio» сделает это за вас. Не успел убрать.