Глава 6 Продвинутая графика

6.1 Предварительные требования

Для работы по теме текущей лекции вам понадобятся пакеты ggplot2, dplyr и tidyr из tidyverse. Помимо этого, мы будем работать напрямую с данными Евростата, NASA POWER и USDA NRCS Soil Data Access к которым можно обращаться напрямую с использованием пакетов eurostat, nasapower и soildb:

Пакет soilDB лучше устанавливать из консоли командой install.packages('soilDB', dependencies = TRUE). Указание параметра dependencies = TRUE обеспечит установку других пакетов, от которых он зависит.

В настоящей главе мы кратко познакомимся с системой ggplot2. gg расшифровывается как grammar of graphics. Под этим понимается определенная (какая — мы узнаем далее) система правил, позволяющих описывать и строить графики. ggplot довольно сильно отличается от стандартной графической подсистемы R. Прежде всего — модульным подходом к построению изображений. В ggplot вы собираете графики «по кирпичикам», отдельно определяя источник данных, способы изображения, параметры системы координат и т.д. – путем вызова и сложения результатов соответствующих функций.

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

6.2 Загрузка данных Евростата

Таблицы данных Евростата имеют уникальные коды, по которым их можно загружать, используя API (Application programming interface). В этой лекции мы будем работать с данными о крупнейших международных партнерах Евросоюза по импорту и экспорту основных видов товаров. Например, таблица данных по продуктам питания, напиткам и табаку имеет код tet00034:

Для чтения таблиц по кодам в пакете eurostat имеется функция get_eurostat(). Чтобы год измерения получить в виде числа, а не объекта типа Date, используем второй параметр time_format = num. Для перехода от кодов продукции и стран к их полным наименованиям, дополнительно вызовем функцию label_eurostat() из того же пакета:

tables = c('tet00034', 'tet00033', 'tet00032', 'tet00031','tet00030', 'tet00029')

trades = lapply(tables, function(X) { # прочтем несколько таблиц в список
  get_eurostat(X) |> label_eurostat()
}) |> 
  bind_rows() |> # объединим прочитанные таблицы в одну
  select(-geo) |> # убираем столбец с территорией торговли, т.к. там только Евросоюз
  dplyr::filter(stringr::str_detect(indic_et, 'Exports in|Imports in')) |> # оставим только экспорт и импорт
  pivot_wider(names_from = indic_et, values_from = values) |>  # вынесем данные по экспорту и импорту в отдельные переменные
  rename(export = `Exports in million of ECU/EURO`, # дадим им краткие названия
         import = `Imports in million of ECU/EURO`) |> 
  mutate(partner = as.factor(partner))

trades # посмотрим, что получилось
## # A tibble: 720 × 5
##    sitc06                   partner                time        export import
##    <chr>                    <fct>                  <date>       <dbl>  <dbl>
##  1 Food, drinks and tobacco Argentina              2008-01-01    81.3  7334 
##  2 Food, drinks and tobacco Brazil                 2008-01-01   600.   9639.
##  3 Food, drinks and tobacco Canada                 2008-01-01  1950.   1458.
##  4 Food, drinks and tobacco Switzerland            2008-01-01  5000.   2727.
##  5 Food, drinks and tobacco China except Hong Kong 2008-01-01  1322.   3567.
##  6 Food, drinks and tobacco Japan                  2008-01-01  3964.    119.
##  7 Food, drinks and tobacco Norway                 2008-01-01  2416.   3012.
##  8 Food, drinks and tobacco Russia                 2008-01-01  7567.    855.
##  9 Food, drinks and tobacco Turkey                 2008-01-01  1175    3160.
## 10 Food, drinks and tobacco United States          2008-01-01 10021.   6030.
## # … with 710 more rows

6.3 Загрузка данных NASA POWER

NASA POWER — это проект NASA, предоставляющий метеорологические, климатические и энергетические данные для целей исследования возобновляемых источников энергии, энергетической эффективности зданий и сельскохозяйственных приложений. Доступ к этим данным, как и Евростату, можно получить через программный интерфейса (API), используя пакет nasapower.

В основе выгрузки данных лежат реанализы с разрешением \(0.5^\circ\) Выгрузим данные по температуре, относительной влажности и осадкам в Екатеринбурге (\(60.59~в.д.\), \(56.84~с.ш.\)) за период с 1 по 30 апреля 1995 года:

daily_single_ag <- get_power(
  community = "ag",
  lonlat = c(60.59, 56.84),
  pars = c("RH2M", "T2M"),
  dates = c("1995-04-01", "1995-04-30"),
  temporal_api = "daily"
)

daily_single_ag # посмотрим, что получилось

Аналогичным путем можно выгрузить данные, осредненные по годам. Например, можно получить данные по суммарной и прямой солнечной радиации (\(кВт/ч/м^2/день\)) для той же точки с 1995 по 2015 год:

interannual_sse <- get_power(
  community = "sse",
  lonlat = c(60.59, 56.84),
  dates = 1995:2015,
  temporal_api = "interannual",
  pars = c("CLRSKY_SFC_SW_DWN",
           "ALLSKY_SFC_SW_DWN")
)
interannual_sse # посмотрим, что получилось

6.4 Загрузка данных Soil Data Access

Soil Data Access — это онлайн-сервис департамента сельского хозяйства США, который позволяет получать подробные данные о почвенных ресурсах этой страны. Наиболее часто запрашиваются данные по так называемым почвенным сериям — группам почвенных профилей, обладающих схожими характеристиками и, таким образом, идентичными с точки зрения сельскохозяйственного использования. Как правило, серии именуются по названию населенного пункта, рядом с которым впервые были найдены подобные почвы.

Например, серия Cecil имеет следующее покрытие и обеспеченность разрезами в базе данных SDA при запросе на сайте Series Extent Explorer:

Для запросов данных по почвенным сериям достаточно вызвать функцию fetchOSD и передать ей имя одной или более серий:

soils = c('wilkes',  'chewacla', 'congaree')
series = fetchOSD(soils, extended = TRUE)

Результирющий объект представляет собой список со множеством таблиц, которые характеризуют как почвенную серию в целом, так и отдельные ее разрезы:

str(series)
## List of 14
##  $ SPC             :Formal class 'SoilProfileCollection' [package "aqp"] with 9 slots
##   .. ..@ idcol       : chr "id"
##   .. ..@ hzidcol     : chr "hzID"
##   .. ..@ depthcols   : chr [1:2] "top" "bottom"
##   .. ..@ metadata    :List of 8
##   .. .. ..$ aqp_df_class    : chr "data.frame"
##   .. .. ..$ aqp_group_by    : chr ""
##   .. .. ..$ aqp_hzdesgn     : chr "hzname"
##   .. .. ..$ aqp_hztexcl     : chr "texture_class"
##   .. .. ..$ depth_units     : chr "cm"
##   .. .. ..$ stringsAsFactors: logi FALSE
##   .. .. ..$ original.order  : int [1:22] 1 2 3 4 5 6 7 8 9 10 ...
##   .. .. ..$ origin          : chr "OSD via Soilweb / fetchOSD"
##   .. ..@ horizons    :'data.frame':  22 obs. of  21 variables:
##   .. .. ..$ id                   : chr [1:22] "CHEWACLA" "CHEWACLA" "CHEWACLA" "CHEWACLA" ...
##   .. .. ..$ top                  : int [1:22] 0 10 36 66 97 119 152 0 20 46 ...
##   .. .. ..$ bottom               : int [1:22] 10 36 66 97 119 152 203 20 46 56 ...
##   .. .. ..$ hzname               : chr [1:22] "Ap" "Bw1" "Bw2" "Bw3" ...
##   .. .. ..$ soil_color           : chr [1:22] "#7E5A3BFF" "#7A5C37FF" "#7A5C37FF" "#7E5A3BFF" ...
##   .. .. ..$ hue                  : chr [1:22] "7.5YR" "10YR" "10YR" "7.5YR" ...
##   .. .. ..$ value                : int [1:22] 4 4 4 4 5 5 4 4 4 3 ...
##   .. .. ..$ chroma               : int [1:22] 4 4 4 4 8 1 4 4 3 3 ...
##   .. .. ..$ dry_hue              : chr [1:22] "7.5YR" "10YR" "10YR" "7.5YR" ...
##   .. .. ..$ dry_value            : int [1:22] 6 6 6 6 6 6 6 6 6 5 ...
##   .. .. ..$ dry_chroma           : int [1:22] 4 4 4 4 7 1 4 4 3 3 ...
##   .. .. ..$ texture_class        : Ord.factor w/ 21 levels "coarse sand"<..: 13 18 17 13 17 17 13 13 13 NA ...
##   .. .. ..$ cf_class             : logi [1:22] NA NA NA NA NA NA ...
##   .. .. ..$ pH                   : logi [1:22] NA NA NA NA NA NA ...
##   .. .. ..$ pH_class             : Ord.factor w/ 12 levels "ultra acid"<"extremely acid"<..: 3 3 3 3 3 3 3 4 NA NA ...
##   .. .. ..$ distinctness         : chr [1:22] "clear" "gradual" "gradual" "gradual" ...
##   .. .. ..$ topography           : chr [1:22] "smooth" "wavy" "wavy" "wavy" ...
##   .. .. ..$ dry_color_estimated  : logi [1:22] TRUE TRUE TRUE TRUE TRUE TRUE ...
##   .. .. ..$ moist_color_estimated: logi [1:22] FALSE FALSE FALSE FALSE FALSE FALSE ...
##   .. .. ..$ narrative            : chr [1:22] "Ap--0 to 4 inches; brown (7.5YR 4/4) loam; weak medium granular structure; friable; common very fine, fine, and"| __truncated__ "Bw1--4 to 14 inches; dark yellowish brown (10YR 4/4) silty clay loam; weak medium subangular blocky structure; "| __truncated__ "Bw2--14 to 26 inches; dark yellowish brown (10YR 4/4) clay loam; weak medium subangular blocky structure; friab"| __truncated__ "Bw3--26 to 38 inches; brown (7.5YR 4/4) loam; weak medium subangular blocky structure; friable; common fine roo"| __truncated__ ...
##   .. .. ..$ hzID                 : chr [1:22] "1" "2" "3" "4" ...
##   .. ..@ site        :'data.frame':  3 obs. of  33 variables:
##   .. .. ..$ id                     : chr [1:3] "CHEWACLA" "CONGAREE" "WILKES"
##   .. .. ..$ soiltaxclasslastupdated: chr [1:3] "2010-02-11 00:00:00+00" "2002-07-18 00:00:00+00" "1997-06-06 00:00:00+00"
##   .. .. ..$ mlraoffice             : chr [1:3] "raleigh, nc" "raleigh, nc" "raleigh, nc"
##   .. .. ..$ series_status          : chr [1:3] "established" "established" "established"
##   .. .. ..$ family                 : chr [1:3] "fine-loamy, mixed, active, thermic fluvaquentic dystrudepts" "fine-loamy, mixed, active, nonacid, thermic oxyaquic udifluvents" "loamy, mixed, active, thermic, shallow typic hapludalfs"
##   .. .. ..$ soilorder              : chr [1:3] "inceptisols" "entisols" "alfisols"
##   .. .. ..$ suborder               : chr [1:3] "udepts" "fluvents" "udalfs"
##   .. .. ..$ greatgroup             : chr [1:3] "dystrudepts" "udifluvents" "hapludalfs"
##   .. .. ..$ subgroup               : chr [1:3] "fluvaquentic dystrudepts" "oxyaquic udifluvents" "typic hapludalfs"
##   .. .. ..$ tax_partsize           : chr [1:3] "fine-loamy" "fine-loamy" "loamy"
##   .. .. ..$ tax_partsizemod        : logi [1:3] NA NA NA
##   .. .. ..$ tax_ceactcl            : chr [1:3] "active" "active" "active"
##   .. .. ..$ tax_reaction           : chr [1:3] NA "nonacid" NA
##   .. .. ..$ tax_tempcl             : chr [1:3] "thermic" "thermic" "thermic"
##   .. .. ..$ originyear             : logi [1:3] NA NA NA
##   .. .. ..$ establishedyear        : int [1:3] 1937 1904 1916
##   .. .. ..$ descriptiondateinitial : chr [1:3] "2010-02-11 00:00:00+00" "2002-07-18 00:00:00+00" "2007-09-06 00:00:00+00"
##   .. .. ..$ descriptiondateupdated : chr [1:3] "2010-02-11 00:00:00+00" "2002-07-18 00:00:00+00" "2021-01-27 16:02:50+00"
##   .. .. ..$ benchmarksoilflag      : int [1:3] 1 0 0
##   .. .. ..$ statsgoflag            : int [1:3] 1 1 1
##   .. .. ..$ objwlupdated           : chr [1:3] "2012-08-01 14:06:37+00" "2012-08-01 14:06:37+00" "2021-01-27 16:02:50+00"
##   .. .. ..$ recwlupdated           : chr [1:3] "2012-08-01 14:06:37+00" "2012-08-01 14:06:37+00" "2021-01-27 16:02:50+00"
##   .. .. ..$ typelocstareaiidref    : int [1:3] 6691 6706 6691
##   .. .. ..$ typelocstareatypeiidref: int [1:3] 3 3 3
##   .. .. ..$ soilseriesiid          : int [1:3] 24628 1057 23800
##   .. .. ..$ soilseriesdbiidref     : int [1:3] 122 122 122
##   .. .. ..$ grpiidref              : int [1:3] 15801 15801 15801
##   .. .. ..$ tax_minclass           : chr [1:3] "mixed" "mixed" "mixed"
##   .. .. ..$ subgroup_mod           : chr [1:3] "fluvaquentic" "oxyaquic" "typic"
##   .. .. ..$ greatgroup_mod         : chr [1:3] "dystr" "udi" "hapl"
##   .. .. ..$ drainagecl             : chr [1:3] "somewhat poorly" "well to moderately well" "well"
##   .. .. ..$ ac                     : int [1:3] 1256820 216072 614614
##   .. .. ..$ n_polygons             : int [1:3] 39164 10384 34815
##   .. ..@ sp          :Formal class 'SpatialPoints' [package "sp"] with 3 slots
##   .. .. .. ..@ coords     : num [1, 1] 0
##   .. .. .. ..@ bbox       : logi [1, 1] NA
##   .. .. .. ..@ proj4string:Formal class 'CRS' [package "sp"] with 1 slot
##   .. .. .. .. .. ..@ projargs: chr NA
##   .. ..@ diagnostic  :'data.frame':  0 obs. of  0 variables
##   .. ..@ restrictions:'data.frame':  0 obs. of  0 variables
##  $ competing       :'data.frame':    1 obs. of  3 variables:
##   ..$ series   : chr "CHEWACLA"
##   ..$ competing: chr "OAKBORO"
##   ..$ family   : chr "fine-loamy, mixed, active, thermic fluvaquentic dystrudepts"
##  $ geog_assoc_soils:'data.frame':    22 obs. of  2 variables:
##   ..$ series: chr [1:22] "CONGAREE" "CONGAREE" "CONGAREE" "CONGAREE" ...
##   ..$ gas   : chr [1:22] "ALTAVISTA" "AUGUSTA" "BUNCOMBE" "CARTECAY" ...
##  $ geomcomp        :'data.frame':    3 obs. of  9 variables:
##   ..$ series         : chr [1:3] "CHEWACLA" "CONGAREE" "WILKES"
##   ..$ Interfluve     : num [1:3] 1 0 0.178
##   ..$ Crest          : num [1:3] 0 0 0.027
##   ..$ Head Slope     : int [1:3] 0 0 0
##   ..$ Nose Slope     : int [1:3] 0 0 0
##   ..$ Side Slope     : num [1:3] 0 0 0.795
##   ..$ Base Slope     : int [1:3] 0 1 0
##   ..$ n              : int [1:3] 3 1 185
##   ..$ shannon_entropy: num [1:3] 0 0 0.365
##  $ hillpos         :'data.frame':    3 obs. of  8 variables:
##   ..$ series         : chr [1:3] "CHEWACLA" "CONGAREE" "WILKES"
##   ..$ Toeslope       : num [1:3] 0.963 0.786 0
##   ..$ Footslope      : num [1:3] 0.0366 0.2143 0
##   ..$ Backslope      : num [1:3] 0 0 0.637
##   ..$ Shoulder       : num [1:3] 0 0 0.225
##   ..$ Summit         : num [1:3] 0 0 0.139
##   ..$ n              : int [1:3] 82 14 245
##   ..$ shannon_entropy: num [1:3] 0.0975 0.3228 0.5573
##  $ mtnpos          : logi FALSE
##  $ terrace         :'data.frame':    2 obs. of  5 variables:
##   ..$ series         : chr [1:2] "CHEWACLA" "CONGAREE"
##   ..$ Tread          : num [1:2] 0.977 1
##   ..$ Riser          : num [1:2] 0.023 0
##   ..$ n              : int [1:2] 87 36
##   ..$ shannon_entropy: num [1:2] 0.068 0
##  $ flats           :'data.frame':    2 obs. of  7 variables:
##   ..$ series         : chr [1:2] "CHEWACLA" "CONGAREE"
##   ..$ Dip            : num [1:2] 0.1455 0.0667
##   ..$ Talf           : num [1:2] 0.855 0.867
##   ..$ Flat           : int [1:2] 0 0
##   ..$ Rise           : num [1:2] 0 0.0667
##   ..$ n              : int [1:2] 55 15
##   ..$ shannon_entropy: num [1:2] 0.258 0.301
##  $ pmkind          :'data.frame':    6 obs. of  5 variables:
##   ..$ series: chr [1:6] "CHEWACLA" "CHEWACLA" "CONGAREE" "CONGAREE" ...
##   ..$ pmkind: chr [1:6] "Alluvium" "Residuum" "Alluvium" "Fluviomarine deposits" ...
##   ..$ n     : int [1:6] 206 1 72 13 1 262
##   ..$ total : int [1:6] 207 207 86 86 86 262
##   ..$ P     : num [1:6] 0.9952 0.0048 0.8372 0.1512 0.0116 ...
##  $ pmorigin        :'data.frame':    24 obs. of  5 variables:
##   ..$ series  : chr [1:24] "CHEWACLA" "CHEWACLA" "CHEWACLA" "CHEWACLA" ...
##   ..$ pmorigin: chr [1:24] "Igneous and metamorphic rock" "Sedimentary rock" "Mixed" "Granite and gneiss" ...
##   ..$ n       : int [1:24] 29 11 2 2 1 1 1 1 1 1 ...
##   ..$ total   : int [1:24] 51 51 51 51 51 51 51 51 51 51 ...
##   ..$ P       : num [1:24] 0.5686 0.2157 0.0392 0.0392 0.0196 ...
##  $ mlra            :'data.frame':    19 obs. of  4 variables:
##   ..$ series    : chr [1:19] "CHEWACLA" "CHEWACLA" "CHEWACLA" "CHEWACLA" ...
##   ..$ mlra      : chr [1:19] "129" "135A" "136" "133A" ...
##   ..$ area_ac   : int [1:19] 6166 3878 927251 128381 78428 60214 29004 13536 9495 2126 ...
##   ..$ membership: num [1:19] 0.005 0.003 0.738 0.102 0.062 0.048 0.023 0.011 0.008 0.01 ...
##  $ climate.annual  :'data.frame':    24 obs. of  12 variables:
##   ..$ series     : chr [1:24] "CHEWACLA" "CHEWACLA" "CHEWACLA" "CHEWACLA" ...
##   ..$ climate_var: chr [1:24] "Elevation (m)" "Effective Precipitation (mm)" "Frost-Free Days" "Mean Annual Air Temperature (degrees C)" ...
##   ..$ minimum    : num [1:24] 0 128.8 177 11.3 986 ...
##   ..$ q01        : num [1:24] 7 216.4 188 12.9 1069 ...
##   ..$ q05        : num [1:24] 34 247.4 196 13.5 1093 ...
##   ..$ q25        : num [1:24] 125 312.2 208 14.9 1136 ...
##   ..$ q50        : num [1:24] 192 349.8 218 15.7 1178 ...
##   ..$ q75        : num [1:24] 248 441.2 227 16.4 1276 ...
##   ..$ q95        : num [1:24] 348 568.8 235 17.3 1392 ...
##   ..$ q99        : num [1:24] 480 740.8 243 17.8 1575 ...
##   ..$ maximum    : num [1:24] 919 1382.8 300 19.7 2202 ...
##   ..$ n          : int [1:24] 32689 32689 32689 32689 32689 32689 32689 32689 8392 8392 ...
##  $ climate.monthly :'data.frame':    72 obs. of  14 variables:
##   ..$ series     : chr [1:72] "CHEWACLA" "CHEWACLA" "CHEWACLA" "CHEWACLA" ...
##   ..$ climate_var: chr [1:72] "ppt1" "ppt2" "ppt3" "ppt4" ...
##   ..$ minimum    : num [1:72] 61 64 81 63 60 78 83 74 68 53 ...
##   ..$ q01        : num [1:72] 74 71 92 71 69 83 92 85 81 72 ...
##   ..$ q05        : num [1:72] 82 73 98 75 72 89 100 89 86 76 ...
##   ..$ q25        : num [1:72] 93 83 105 83 81 96 108 96 91 82 ...
##   ..$ q50        : num [1:72] 105 107 120 88 92 101 116 102 98 86 ...
##   ..$ q75        : num [1:72] 115 121 128 95 101 106 123 110 105 91 ...
##   ..$ q95        : num [1:72] 131 135 137 111 111 117 133 129 118 100 ...
##   ..$ q99        : num [1:72] 149 144 147 122 125 133 143 142 137 109 ...
##   ..$ maximum    : num [1:72] 248 227 239 150 167 198 220 207 192 158 ...
##   ..$ n          : int [1:72] 32689 32689 32689 32689 32689 32689 32689 32689 32689 32689 ...
##   ..$ month      : Factor w/ 12 levels "1","2","3","4",..: 1 2 3 4 5 6 7 8 9 10 ...
##   ..$ variable   : Factor w/ 2 levels "Potential ET (mm)",..: 2 2 2 2 2 2 2 2 2 2 ...
##  $ soilweb.metadata:'data.frame':    18 obs. of  2 variables:
##   ..$ product    : chr [1:18] "block diagram archive" "cached sketches" "component pedons" "KSSL snapshot" ...
##   ..$ last_update: chr [1:18] "2019-12-17" "2021-10-07" "2020-12-08" "2020-03-18" ...

6.5 Базовый шаблон ggplot

Для начала посмотрим, как можно показать суммарный экспорт по годам:

trades_total = trades |> 
  group_by(time) |> 
  summarise(export = sum(export),
            import = sum(import))
  
ggplot(data = trades_total) +
  geom_point(mapping = aes(x = time, y = export))

Базовый (минимально необходимый) шаблон построения графика через ggplot выглядит следующим образом:

ggplot(data = <DATA>) + 
  <GEOM_FUNCTION>(mapping = aes(<MAPPINGS>))

где:

  • DATA — источник данных (фрейм, тиббл)
  • GEOM_FUNCTION — функция, отвечающая за геометрический тип графика (точки, линии, гистограммы и т.д.)
  • MAPPINGS — перечень соответствий между переменными данных (содержащихся в DATA) и графическими переменными (координатами, размерами, цветами и т.д.)

6.6 Геометрические типы и преобразования

ggplot предлагает несколько десятков различных видов геометрий для отображения данных. С их полным перечнем можно познакомиться тут. Мы рассмотрим несколько наиболее употребительных, а геометрии, связанные со статистическими преобразованиями, оставим для следующей темы.

В первом примере мы отображали данные по экспорту за разные года, однако точечный тип не очень подходит для данного типа графика, поскольку он показывает динамику изменения. А это означает, что желательно соединить точки линиями. Для этого используем геометрию geom_line():

ggplot(data = trades_total) +
  geom_line(mapping = aes(x = time, y = export))

Поскольку в данном случае величина является агрегированной за год, более правильным может быть показ ее изменений в виде ступенчатого линейного графика, который получается через геометрию geom_step():

ggplot(data = trades_total) +
  geom_step(mapping = aes(x = time, y = export))

Можно совместить несколько геометрий, добавив их последовательно на график:

ggplot(data = trades_total) +
  geom_line(mapping = aes(x = time, y = export)) +
  geom_point(mapping = aes(x = time, y = export))

Если у нескольких геометрий одинаковые отображения, их можно вынести в вызов функции ggplot() (чтобы не дублировать):

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_line() +
  geom_point()

Наглядность линейного графика можно усилить, добавив “заливку” области с использованием geom_area():

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_area(alpha = 0.5) + # полигон с прозрачностью 0,5
  geom_line() +
  geom_point()

Для построения столбчатой диаграммы следует использовать геометрию geom_col(). Например, вот так выглядит структура экспорта продукции машиностроения из Евросоюза по ведущим партнерам:

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export)) +
  geom_col()

Развернуть диаграмму можно, используя функцию coord_flip():

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export)) +
  geom_col() +
  coord_flip()

6.7 Графические переменные и группировки

Графические переменные — это параметры, определяющие внешний вид символов. К ним относятся цвет (тон, насыщенность и светлота), размер, форма, ориентировка, внутренняя структура символа. В ggplot значения графических переменных могут быть едиными для всех измерений, а могут зависеть от величины измерений. С точки зрения управления здесь все просто: если вы хотите, чтобы какой-то графический параметр зависел от значения показателя, он должен быть указан внутри конструкции mapping = aes(...). Если необходимо, чтобы этот параметр был одинаковым для всех измерений, вы должны его указать внутри <GEOM_FUNCTION>(...), то есть не передавать в mapping.

Для управления цветом, формой и размером (толщиной) графического примитива следует использовать параметры color, shape и size соответственно. Посмотрим, как они работают внутри и за пределами функции aes():

# один цвет для графика (параметр за пределами aes)
ggplot(trades_total) + 
    geom_line(mapping = aes(x = time, y = export), color = 'blue')

trade_russia = trades |> dplyr::filter(partner == 'Russia')

ggplot(trade_russia) + # у каждой группы данных свой цвет (параметр внутри aes)
  geom_line(mapping = aes(x = time, y = export, color = sitc06))

ggplot(trade_russia, mapping = aes(x = time, y = export, color = sitc06)) + # а теперь и с точками
  geom_line() +
  geom_point()

Аналогичным образом работает параметр формы значка:

# один значок для графика
ggplot(trades_total) + 
    geom_point(mapping = aes(x = time, y = export), shape = 15)
    

ggplot(trade_russia) + # у каждой группы данных свой значок
    geom_point(mapping = aes(x = time, y = export, shape = sitc06))

Для изменения размера значка или линии используйте параметр size:

# изменение размера значка и линии
ggplot(trades_total, mapping = aes(x = time, y = export)) + 
    geom_point(size = 5) +
    geom_line(size = 2)

Если вы используете зависимые от значений графические переменные и при этом хотите добавить на график еще одну геометрию (c постоянными параметрами), то вам необходимо сгруппировать объекты второй геометрии по той же переменной, по которой вы осуществляете разбиение в первой геометрии. Для этого используйте параметр group:

ggplot(trade_russia, aes(x = time, y = export)) + 
    geom_point(aes(shape = sitc06)) +
    geom_line(aes(group = sitc06))

Для изменения цвета столбчатых диаграмм следует использовать параметр fill, а цвет и толщина обводки определяются параметрами color и size:

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export)) +
  geom_col(fill = 'plum4', color = 'black', size = 0.2) +
  coord_flip()

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

trades |> 
  dplyr::filter(time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export, fill = sitc06)) +
  geom_col(color = 'black', size = 0.2) +
  coord_flip()

Если вам важно не абсолютное количество, а процентное соотношение величин, вы можете применить вид группировки position == 'fill:

trades |> 
  dplyr::filter(time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export, fill = sitc06)) +
    geom_col(color = 'black', size = 0.2, position = 'fill') +
    coord_flip()

Еще один вид группировки — это группировка по соседству. Чтобы использовать ее, применить метод position == 'dodge:

trade_russia |> 
  dplyr::filter(time >= as.Date('2013-01-01')) |> 
  ggplot(mapping = aes(x = time, y = export, fill = sitc06)) +
    geom_col(color = 'black', size = 0.2, position = 'dodge')

6.8 Системы координат

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

Смена переменных происходит благодаря уже знакомой нам функции coord_flip(). Рассмотрим, например, как изменилась структура экспорта/импорта по годам:

trades_type = trades |> 
  group_by(sitc06, time) |> 
  summarise(export = sum(export),
            import = sum(import))

ggplot(trades_type) + 
    geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5)

ggplot(trades_type) + 
    geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
    coord_flip()

Поскольку объемы продукции различаются на порядки, для различимости малых объемов целесообразно перейти к логарифмической шкале. Для этого используем scale_log_x() и scale_log_y():

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  scale_x_log10() +
  scale_y_log10()

Преобразование в полярную систему координат используется для того чтобы получить круговую секторную диаграмму Найтингейл (coxcomb chart):

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export, fill = partner)) +
  geom_col() +
  coord_polar()

Разумеется, здесь тоже можно использовать преобразование шкалы по оси Y (которая теперь отвечает за радиус). Применим правило квадратного корня, добавив вызов функции scale_y_sqrt():

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export, fill = partner)) +
  geom_col() +
  coord_polar() +
  scale_y_sqrt()

Чтобы построить классическую секторную диаграмму, необходимо, чтобы угол поворота соответствовал величине показателя (оси Y), а не названию категории (оси X). Для этого при вызове функции coord_polar() следует указать параметр theta = 'y', а при вызове geom_col() оставить параметр x пустым:

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = '', y = export, fill = partner), color = 'black', size = 0.2) +
  geom_col() +
  coord_polar(theta = 'y')

6.9 Названия осей и легенды

ggplot предоставляет ряд функций для аннотирования осей и легенды. Для этого можно использовать одну из следующих функций:

  • labs(...) модифицирует заголовок легенды для соответствующей графической переменной, либо заголовок/подзаголовок графика
  • xlab(label) модифицирует подпись оси X
  • ylab(label) модифицирует подпись оси Y
  • ggtitle(label, subtitle = NULL) модифицирует заголовок и подзаголовок графика

Создадим подписи легенд, отвечающих за цвет и размер значка на графике соотношения импорта и экспорта разных видов продукции:

ggplot(trades_type) + 
  geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
  labs(color = "Вид продукции", size = 'Год')

Добавим заголовок и подзаголовок графика:

ggplot(trades_type) + 
  geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
  labs(color = "Вид продукции", size = 'Год') +
  ggtitle('Соотношение импорта и экспорта в странах Евросоюза (млн долл. США)',
          subtitle = 'Данные по ключевым партнерам')

Изменим подписи осей:

ggplot(trades_type) + 
  geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
  labs(color = "Вид продукции", size = 'Год') +
  ggtitle('Соотношение импорта и экспорта в странах Евросоюза (млн долл. США)',
          subtitle = 'Данные по ключевым партнерам') +
  xlab('Экспорт') +
  ylab('Импорт')

6.10 Разметка осей

Первое, что вам скорее всего захочется убрать — это экспоненциальная запись чисел. На самом деле, эта запись не является параметром ggplot или стандартной системы graphics. Количество значащих цифр, после которых число автоматически представляется в экспоненциальном виде, управляется параметром scipen. Мы можем задать его достаточно большим, чтобы запретить переводить любые разумные числа в экспоненциальный вид:

options(scipen = 999)
ggplot(trades_type) + 
  geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
  labs(color = "Вид продукции", size = 'Год') +
  ggtitle('Соотношение импорта и экспорта в странах Евросоюза (млн долл. США)',
          subtitle = 'Данные по ключевым партнерам') +
  xlab('Экспорт') +
  ylab('Импорт')

Для управления разметкой осей необходимо использовать функции scale_x_continuous(), scale_y_continuous(), scale_x_log10(...), scale_y_log10(...), scale_x_reverse(...), scale_y_reverse(...), scale_x_sqrt(...), scale_y_sqrt(...), которые, с одной стороны, указывают тип оси, а с другой стороны — позволяют управлять параметрами сетки координат и подписями.

Для изменения координат линий сетки и подписей необходимо использовать, соответственно, параметры breaks и labels:

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  scale_x_log10(breaks = seq(0, 500000, 100000)) +
  scale_y_log10(breaks = seq(0, 500000, 100000))

В данном случае, как раз, будет достаточно полезным параметр labels, поскольку метки можно сделать более компактными, поделив их на 1000 (и не забыть потом указать, что объемы теперь указаны не в миллионах, а в миллиардах долларов):

brks = seq(0, 500000, 100000)
ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  scale_x_log10(breaks = brks, labels = brks / 1000) +
  scale_y_log10(breaks = brks, labels = brks / 1000)

Для обычной шкалы используйте функции scale_x_continuous() и scale_y_continuous():

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  scale_x_continuous(breaks = brks, labels = brks / 1000) +
  scale_y_continuous(breaks = brks, labels = brks / 1000)

Для того чтобы принудительно указать диапазоны осей и графических переменных, следует использовать функции lims(...), xlim(...) и ylim(...). Например, мы можем приблизиться в левый нижний угол графика, задав диапазон 0-200000 по обеим осям:

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  xlim(0, 75000) +
  ylim(0, 75000)

Функция lims() работает еще хитрее: она позволяет применять графические переменные только к ограниченному набору значений исходных данных. Например, таким путем я могу выделить на графике продукцию машиностроения:

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  geom_point(alpha = 0.5) +
  lims(color = 'Machinery and transport equipment')

6.11 Подписи и аннотации

С точки зрения ggplot текст на графике, отображающий входные данные, является одной из разновидностей геометрии. Размещается он с помощью функции geom_text(). Как и в случае с другими геометриями, параметры, зависящие от исходных данных, должны быть переданы внутри mapping = aes(...):

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_area(alpha = 0.5) + # полигон с прозрачностью 0,5
  geom_line() +
  geom_point() +
  geom_text(aes(label = floor(export / 1000))) # добавляем подписи

Выравнивание подписи относительно якорной точки (снизу, сверху, справа, слева) по горизонтали и вертикали управляется параметрами hjust и vjust, а смещения по осям X (в координатах графика) — параметрами nudge_x и nudge_y:

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_area(alpha = 0.5) + # полигон с прозрачностью 0,5
  geom_line() +
  geom_point() +
  geom_text(aes(label = floor(export / 1000)), 
            vjust = 0, nudge_y = 40000) # добавляем подписи

Подписи с фоновой плашкой добавляются через функцию geom_label(), которая имеет аналогичный синтаксис:

trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment', time == as.Date('2017-01-01')) |> 
  ggplot(mapping = aes(x = partner, y = export)) +
  geom_col(fill = 'plum4', color = 'black', size = 0.2) +
  coord_flip() +
  geom_label(aes(y = export / 2, label = floor(export / 1000))) # добавляем подписи

Аннотации представляют собой объекты, размещаемые на графике вручную, и используемые, как правило, для выделения объектов и областей. Для размещения аннотаций используется функция annotate():

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_area(alpha = 0.5) + # полигон с прозрачностью 0,5
  geom_line() +
  geom_point() +
  geom_text(aes(label = floor(export / 1000)), 
            vjust = 0, nudge_y = 40000) +
  annotate("text", x = as.Date('2009-01-01'), y = 550000, label = "Это провал", color = 'red')

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

ggplot(trades_type, mapping = aes(x = export, y = import, color = sitc06, size = time)) + 
  annotate("rect", xmin = 100000, xmax = 250000, ymin = 75000, ymax = 175000,  alpha = .2, color = 'black', size = 0.1) +
  geom_point(alpha = 0.5) +
  annotate("text", x = 175000, y = 190000, label = "Chemicals", color = 'coral')

6.12 Фасеты

Фасеты представляют собой множество графиков, каждый из которых отображает свою переменную или набор значений. Для разбиения на фасеты используется функция facet_wrap(), которой необходимо передать переменную разбиения с тильдой. Например, рассмотрим изменение структуры импорта по годам:

brks = c(0, 50, 100, 150, 200)
trades |> 
  dplyr::filter(sitc06 == 'Machinery and transport equipment') |> 
  ggplot(mapping = aes(x = partner, y = import)) +
  geom_col() +
  scale_y_continuous(breaks = brks * 1e3, labels = brks) +
  ggtitle('Импорт продукции машиностроения (мдрд долл. США)',
        subtitle = 'Данные по ключевым партнерам') +
  coord_flip() +
  facet_wrap(~time)

6.13 Темы

Система ggplot интересна также тем, что для нее существует множество предопределенных “тем” или скинов для оформления графиков. Часть из них входит в состав самой библиотеки. Дополнительные темы можно установить через пакет ggthemes. Чтобы изменить тему оформления ggplot, достаточно прибавить в конце построения графика соответствующую функцию. Например, классическая черно-белая тема получается прибавлением функции theme_bw():

ggplot(data = trades_total, mapping = aes(x = time, y = export)) +
  geom_area(alpha = 0.5) + # полигон с прозрачностью 0,5
  geom_line() +
  geom_point() +
  geom_text(aes(label = floor(export / 1000)), 
            vjust = 0, nudge_y = 40000) +
  theme_bw()

ggplot(trades_type) + 
  geom_point(mapping = aes(x = export, y = import, color = sitc06, size = time), alpha = 0.5) +
  labs(color = "Вид продукции", size = 'Год') +
  ggtitle('Соотношение импорта и экспорта в странах Евросоюза (млн долл. США)',
          subtitle = 'Данные по ключевым партнерам') +
  xlab('Экспорт') +
  ylab('Импорт') +
  theme_bw()

6.14 Краткий обзор

Для просмотра презентации щелкните на ней один раз левой кнопкой мыши и листайте, используя кнопки на клавиатуре:

Презентацию можно открыть в отдельном окне или вкладке браузере. Для этого щелкните по ней правой кнопкой мыши и выберите соответствующую команду.

6.15 Контрольные вопросы и упражнения

6.15.1 Вопросы

  1. Назовите три основных компоненты шаблона построения графика в ggplot2.
  2. Как называются геометрии ggplot2, отвечающие за построение точек, линий и ступенчатых линий?
  3. Как называется геометрия ggplot2, отвечающая за построение столбчатой диаграммы?
  4. Как сделать так, чтобы графический параметр ggplot2 был постоянным для всех измерений?
  5. Как сделать так, чтобы графический параметр ggplot2 зависел от значения переменной?
  6. Перечислите названия параметров, отвечающих за цвет, размер, заливку и тип значка графического примитива.
  7. Если вы используете зависимые от значений графические переменные и при этом хотите добавить на график еще одну геометрию с постоянными параметрами, то как это можно реализовать?
  8. Перечислите названия режимов группировки столбчатых диаграмм и пути их реализации.
  9. Какая функция ggplot2 позволяет поменять местами оси координат?
  10. Перечислите типы шкал для осей координат, которые доступны в ggplot2.
  11. Назовите функцию, позволяющую перейти к полярной системе координат при построении графика в ggplot2.
  12. В чем отличие построения розы-диаграммы (coxcomb chart) и секторной диаграммы (pie chart) средствами ggplot2?
  13. Что делает функция labs()?
  14. Какие функции позволяют определить названия осей и заголовок графика?
  15. Что делает функция lims()?
  16. Как ограничить область построения графика заданным диапазоном значений координат?
  17. Как ограничить применение графических переменных только к определенным значениям измерений?
  18. Назовите геометрии, которые позволяют размещать подписи и подписи с плашками (фоном) на графиках ggplot2.
  19. Чем отличаются аннотации от геометрии подписей в ggplot? Какие виды аннотаций можно создавать?
  20. Каким образом можно построить фасетный график, на котором каждое изображение соответствует значению переменной? Каков синтаксис вызова соответствующей функции?
  21. Как поменять стиль отображения (тему) графика ggplot2?
  22. Как получить программный доступ к таблицам Евростата, не прибегая к закачке файлов? Какой пакет можно использовать для этого?
  23. Что является уникальным идентификатором таблицы в данных Евростата и как его узнать?
  24. Как преобразовать коды Евростата в загруженных таблицах в человеко-читаемые обозначения?

6.15.2 Упражнения

Упражнения данной главы частично повторяют упражнения предыдущей главы по базовой графике в целях сравнения двух графических систем R.

  1. Постройте для набора данных quakes пакета datasets гистограммы распределения глубин и магнитуд, а также диаграмму рассеяния для двух этих характеристик. Используйте сначала функцию qplot(), а затем выполните то же самое с использованием полного синтаксиса ggplot2().

  2. На портале открытых данных Тульской области есть данные о распределении площади лесов и запасов древесины по преобладающим породам и группам возраста. Скачайте эти данные в виде таблицы CSV и постройте по ним круговую и столбчатую диаграмму для категории Площадь земель, занятых лесными насаждениями (покрытых лесной растительностью), всего. Подберите цвета, попробуйте изменить ориентировку столбцов на горизонтальную, а для круговой диаграммы поменять угол поворота.

  3. Используя данные4 по балансу масс ледника Гарабаши, постройте график с тремя кривыми (аккумуляции, абляции и кумулятивного баланса) за период 1981 по 2017 г. Добавьте на график легенду. Обратите внимание на то, что таблица содержит агрегирующие строки (1982-1997, 1998-2017, 1982-2017), которые вам необходимо предварительно исключить.

    Подсказка: Чтобы построить кривую кумулятивного баланса, используйте функцию cumsum.

  4. Загрузите таблицу данных по импорту/экспорту продуктов питания, напитков и табака с портала Евростата (с использованием пакета eurostat). Постройте линейный график изменения суммарных величин импорта и экспорта по данному показателю (у вас должно получиться 2 графика на одном изображении). Используйте цвет для разделения графиков. Добавьте текстовые подписи величин импорта и экспорта. Постройте также две круговых диаграммы, показывающих соотношение ведущих импортеров и экспортеров за последний имеющийся год. Сделайте сначала это отдельными графиками, а затем одним фасетным графиком (для этого потребуется привести таблицу к длинной форме).

  5. Постройте линейный график хода температуры , а также столбчатую диаграмму хода суммарной солнечной радиации в Екатеринбурге на примере данных NASA POWER, загруженных в разделе 6.3.

    Подсказка: Для построения столбчатой диаграммы вам потребуется использовать функцию geom_col(), поскольку высота столбика отражает не встречаемость значения, а величину переменной. Также вам потребуется преобразовать таблицу среднемесячных величин к длинной форме, где название месяца будет отдельной переменной (тип — упорядоченный фактор).

Самсонов Т.Е. Визуализация и анализ географических данных на языке R. М.: Географический факультет МГУ, 2021. DOI: 10.5281/zenodo.901911