ggplot: как «исправить» непредставительный всплеск на графике

#r #time-series #data-modeling #smoothing

#r #временные ряды #моделирование данных #сглаживание

Вопрос:

У меня есть данные о процентном показателе по дате и времени (дата и часы: минуты: секунды). Я хочу графически «исправить» / выделить точку данных, которая не является репрезентативной.

Предыстория

У меня есть данные о том, как люди ежедневно оценивают свой уровень счастья по непрерывной шкале от 0 до> 1, где 0 означает «крайне несчастный», а 1 означает «чрезвычайно счастливый». Я спрашиваю многих людей и хочу со временем почувствовать «счастье в группе».

Данные

 library(tidyverse)
library(lubridate)

set.seed(1234)

original_df <- 
  seq(as.POSIXct('2020-09-01', tz = "UTC"), as.POSIXct('2020-09-15', tz = "UTC"), by="1 mins") %>%
  sample(15000, replace = T) %>%
  as_tibble %>%
  rename(date_time = value) %>%
  mutate(date = date(date_time)) %>%
  add_column(score = runif(15000))

original_df

## # A tibble: 15,000 x 3
##  date_time           date       score
##    <dttm>              <date>     <dbl>
##  1 2020-09-06 04:11:00 2020-09-06 0.683
##  2 2020-09-06 13:35:00 2020-09-06 0.931
##  3 2020-09-05 23:21:00 2020-09-05 0.121
##  4 2020-09-06 14:45:00 2020-09-06 0.144
##  5 2020-09-07 09:15:00 2020-09-07 0.412
##  6 2020-09-01 10:22:00 2020-09-01 0.564
##  7 2020-09-11 14:00:00 2020-09-11 0.960
##  8 2020-09-08 13:24:00 2020-09-08 0.845
##  9 2020-09-01 15:33:00 2020-09-01 0.225
## 10 2020-09-09 19:27:00 2020-09-09 0.815
## # ... with 14,990 more rows
  

Однако оказывается, что в один из дней оказывается значительно меньше точек данных. Таким образом, фактический набор данных выглядит следующим образом:

 actual_df <- 
  original_df %>%
  filter(date %in% as_date("2020-09-10")) %>%
  group_by(date) %>%
  slice_sample(n = 15) %>%
  ungroup %>%
  bind_rows(original_df %>% filter(!date %in% as_date("2020-09-10")))

> actual_df %>% count(date)

## # A tibble: 14 x 2
##    date           n
##    <date>     <int>
##  1 2020-09-01  1073
##  2 2020-09-02  1079
##  3 2020-09-03  1118
##  4 2020-09-04  1036
##  5 2020-09-05  1025
##  6 2020-09-06  1089
##  7 2020-09-07  1040
##  8 2020-09-08  1186
##  9 2020-09-09  1098
## 10 2020-09-10    15 ## <- this day has less data 
## 11 2020-09-11  1095
## 12 2020-09-12  1051
## 13 2020-09-13  1037
## 14 2020-09-14  1034
  

Построение этих данных с течением времени

То, что я делал, основывалось на работе со средствами

Я рассматриваю каждый день как фактор и получаю среднее значение за день. По статистике, это решение может быть далеко от идеального, как прокомментировал @BrianLang ниже. Однако прямо сейчас я выбрал именно этот метод.

 library(emmeans)

model_fit <- 
  actual_df %>%
  mutate(across(date, factor)) %>%
  lm(score ~ date, data = .)

emmeans_fit_data <- emmeans(model_fit, ~ date, CIs = TRUE)

emmeans_fit_data %>%
  as_tibble %>%
  ggplot(data = ., aes(x = date, y = emmean))  
  geom_line(color = "#1a476f", group = 1, lwd = 1)  
  geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), alpha = 0.5, color = "#90353b", width = 0.2)  
  geom_text(aes(label = paste0(round(100*emmean, 1), "%") , color = "90353b"), vjust = -4, hjust = 0.5, size = 3.5)  
  geom_point(color = "1a476f")  
  scale_y_continuous(labels = function(x) paste0(100*x, "%"))  
  ylab("Level of Happiness")  
  xlab("Date")  
  ggtitle("Mood Over Time")  
  theme(plot.title = element_text(hjust = 0.5, size = 14),
        axis.text.x=element_text(angle = -60, hjust = 0),
        axis.title.x = element_blank(),
        legend.title = element_blank(),
        plot.caption = element_text(hjust = 0, size = 8),
        legend.position = "none")
  

введите описание изображения здесь

Но затем я получаю этот всплеск в 2020-09-10, что связано только с малым количеством точек данных.
Одним из графических решений было бы сделать что-то вроде удаления проблемной линии и «завершения» того, как это выглядело бы с достаточным количеством точек данных. Возможно, на основе усреднения за день до и на следующий день? Я не хочу избавляться от реальных данных, но хочу графически подчеркнуть, что это нерепрезентативно, и что реальное значение должно было быть намного ближе к дню до и после. Я думал, что использование пунктирных линий является разумным графическим решением.
пунктирная

В противном случае я надеялся, что может быть другой метод моделирования / построения таких «временных» данных с использованием ggplot сглаживания, что даст мне более плавную линию тренда и доверительную ленту, которая будет учитывать проблемный день. Но я понимаю, что это может выходить за рамки этого вопроса, поэтому я просто добавляю его в качестве примечания; на случай, если кто-то захочет предложить решение, основанное на другом моделировании, вместо простой графической коррекции. Но я буду благодарен и за то, и за другое.

Комментарии:

1. Похоже, что вы подходите для этой модели с данными как категорией. Я думаю, вам, вероятно, следует рассматривать date как непрерывную переменную. Ваша проблема в том, что вы не понимаете имеющиеся у вас данные или модели, которые были бы уместны для данных. Для этого вам необходимо узнать о моделях, потенциально моделях arima.

2. Спасибо. Да, мой вопрос, безусловно, связан с недостатком знаний, которые я надеюсь получить с помощью вопроса, который я опубликовал. Не могли бы вы уточнить или направить меня дальше? Модели ARIMA — это один из типов, о котором я не знал. Что-нибудь еще, что вы могли бы придумать? Я могу свободно использовать Google, но это то, что я делал до публикации здесь, поэтому мне нужно больше внимания. Спасибо!

Ответ №1:

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

Мне нужно было немного изменить ваш код, чтобы я мог избежать установки новейших версий некоторых пакетов ;-).

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

 library(chron)

## added a numeric version of your date variable.
actual_df <- original_df %>%
 filter(datez %in% lubridate::date("2020-09-10")) %>%
 sample_n(size = 15) %>%
 group_by(datez) %>%
 ungroup %>%
 bind_rows(original_df %>% filter(!datez %in% lubridate::date("2020-09-10"))) %>%
 mutate(num_date = as.numeric(datez))
  
 ## How many knots across the dates do you want?
number_of_knots = 15

## This is to make sure that visreg is passed the actual knot locations! RMS::RCS does not store them in the model fits. 
knots <- paste0("c(", paste0(attr(rms::rcs(actual_df$num_date, number_of_knots), "parms"), collapse = ", "), ")") 

## We can construct the formula early.
formula <- as.formula(paste("score ~ rms::rcs(num_date,", knots,")"))

## fit the model as a gaussian glm and pass it to visreg for it's prediction function. This will give you predicted means and 95% CI for that mean. Then I convert the numeric dates back to real dates. 
glm_rcs <- glm(data = actual_df, formula = formula, family = "gaussian") %>% visreg::visreg(plot = F) %>% .$fit %>%
 mutate(date_date = chron::as.chron(num_date) %>% as.POSIXct())

## plot it!
ggplot(data = glm_rcs, aes(date_date,
                           y = visregFit))   
 geom_ribbon(aes(ymin = visregLwr, ymax = visregUpr), alpha = .5)  
 geom_line()
  

График после сглаживания с помощью RCS


РЕДАКТИРОВАТЬ: вы собираете данные по дням, но вы можете добавить дрожание к дате таким образом, чтобы они распределялись в течение дня.

 actual_df <- original_df %>%
 filter(datez %in% lubridate::date("2020-09-10")) %>%
 sample_n(size = 15) %>%
 group_by(datez) %>%
 ungroup %>%
 bind_rows(original_df %>% filter(!datez %in% lubridate::date("2020-09-10"))) %>%
 mutate(num_date = as.numeric(datez))  %>%
## Here we add random noise (uniform -.5 to .5) to each numeric date.
 mutate(jittered_date = num_date   runif(n(), -.5, .5))

## You can lower this number to increase smoothing.
number_of_knots = 15

knots <- paste0("c(", paste0(attr(rms::rcs(actual_df$jittered_date, number_of_knots), "parms"), collapse = ", "), ")")

formula <- as.formula(paste("score ~ rms::rcs(jittered_date,", knots,")"))

glm_rcs <- glm(data = actual_df, formula = formula, family = "gaussian") %>% visreg::visreg(plot = F) %>% .$fit %>%
 mutate(date_date = chron::as.chron(jittered_date) %>% as.POSIXct())

ggplot(data = glm_rcs, aes(date_date,
                           y = visregFit))  
 geom_ribbon(aes(ymin = visregLwr, ymax = visregUpr), alpha = .5)  
 geom_line()
  

рисунок после добавления дрожания к датам


Правка 2:

Дрожание точек не так необходимо, если у вас есть вектор даты и времени, а не простой день. В вашем исходном коде для создания используемых вами поддельных данных lubridate::date() , которые берут ваш вектор posix datetime и сокращают до простой даты! Вы можете избежать этого с помощью чего-то вроде этого:

 original_df <- tibble(datez = seq(as.POSIXct('2020-09-01', tz = "UTC"), as.POSIXct('2020-09-15', tz = "UTC"), by="1 mins") %>%
 sample(15000, replace = T)) %>%
 mutate(datez_day = lubridate::date(datez)) %>%
 add_column(score = runif(15000))

actual_df <- original_df %>%
 filter(datez_day %in% lubridate::date("2020-09-10")) %>%
 sample_n(size = 15) %>%
 bind_rows(original_df %>% filter(!datez_day %in% lubridate::date("2020-09-10"))) %>%
 mutate(num_date = as.numeric(datez))
  

теперь у вас есть datez_day , что является значением дня, datez что является датой-временем, а num_date что является числовым представлением даты-времени.

оттуда вы можете напрямую моделировать num_date , не добавляя никакого дрожания.

 number_of_knots = 20

knots <- paste0("c(", paste0(attr(rms::rcs(actual_df$num_date, number_of_knots), "parms"), collapse = ", "), ")")

formula <- as.formula(paste("score ~ rms::rcs(num_date,", knots,")"))

glm_rcs <- glm(data = actual_df, formula = formula, family = "gaussian") %>% 
        visreg::visreg(plot = F) %>% 
        .$fit %>% 
        as_tibble() %>%
   ## Translate the num_date back into a datetime object so it is correct in the figures!
        mutate(date_date = as.POSIXct.numeric(round(num_date), origin = "1970/01/01"))

ggplot(data = glm_rcs, aes(date_date,
                           y = visregFit))  
 geom_ribbon(aes(ymin = visregLwr, ymax = visregUpr), alpha = .5)  
 geom_line()
  

Комментарии:

1. Это очень полезно. Однако в моих реальных данных (а не в тех, которые были смоделированы для вопроса) я получаю более сильное падение, чем показано в вашем решении. (смотрите здесь: i.stack.imgur.com/oNQ3B.png ) Хотя это, очевидно, связано с различиями между наборами данных, и не видя реальных данных, не могли бы вы выдвинуть гипотезу, почему это дает такой доминирующий спад?

2. На самом деле возникает вопрос, который я поднял в сообщении. Если у меня такое доминирующее падение, которое явно связано с малым количеством точек данных за заданное время, то как я могу решить эту проблему, если сглаживание не сглаживает ее? Мотивация для решения этой проблемы исходит из необходимости донести сюжет до других. Я хочу, чтобы люди, впервые увидевшие сюжет, не были отвлечены или введены в заблуждение таким падением. Итак, я либо исправляю это косметически / графически, либо с помощью методологии сглаживания.

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

4. Я подумал об этом немного больше. У вас есть весь вес данных конкретно для значений x в виде дней. Если вы добавите дрожание к датам таким образом, чтобы они охватывали весь день, мы можем получить более плавные подгонки.

5. Я понимаю. Но если мы учтем, что данные имели время как непрерывную дату и время и не были жестко запрограммированы по дням? Взвешивание по дням было выбором, но можно ли было сделать иначе? Можно ли моделировать данные с помощью X как непрерывный временной поток, а затем отмечать сегменты графика как дни?