Smooth transitions between different layouts of the same network in ggraph - r

I would like to animate the same network using different layouts and having a smooth transition between layouts. I'd like to do this inside the gganimate framework.
library(igraph)
library(ggraph)
library(gganimate)
set.seed(1)
g <- erdos.renyi.game(10, .5, "gnp")
V(g)$name <- letters[1:vcount(g)]
l1 <- create_layout(g, "kk")
l2 <- create_layout(g, "circle")
l3 <- create_layout(g, "nicely")
long <- rbind(l1,l2,l3)
long$frame <- rep(1:3, each =10)
Following the ggplot approach, I store the node positions in the long format (long) and add a frame variable to each layout.
I tried to make it work with the following code, which is working fine and almost what I want. However, I cannot seem to find a way to include the edges:
ggplot(long, aes(x, y, label = name, color = as.factor(name), frame = frame)) +
geom_point(size = 3, show.legend = F) +
geom_text(show.legend = F) +
transition_components(frame)
I also tried to add the edges as geom_segment but ended up with them being static while the nodes kept moving. This is why I use the ggraph package and fail:
ggraph(g, layout = "manual", node.position = long) +
geom_node_point() +
geom_edge_link() +
transition_components(frame)
I'd like to have an animation of one network with changing node positions that both displays nodes and edges.
Any help is much appreciated!
Edit: I learned that one can include the layout directly into ggraph (and even manipulate the attributes). This is what I've done in the following gif. Additionally geom_edge_link0' instead of geom_edge_link is being used.
ggraph(long) +
geom_edge_link0() +
geom_node_point() +
transition_states(frame)
Note that the edges are not moving.

I'm not sure this is currently ready in gganimate as is. As of May 2019, here's what looks to be a related issue: https://github.com/thomasp85/gganimate/issues/139
EDIT I've replaced with a working solution. Fair warning, I'm a newbie with network manipulations, and I expect someone with more experience could refactor the code to be much shorter.
My general approach was to create the layouts, put the nodes into a table long2, and then create another table with all the edges. gganimate then calls the respective data source each layer needs.
1. Create the nodes table for the three layouts:
set.seed(1)
g <- erdos.renyi.game(10, .5, "gnp")
V(g)$name <- letters[1:vcount(g)]
layouts <- c("kk", "circle", "nicely")
long2 <- lapply(layouts, create_layout, graph = g) %>%
enframe(name = "frame") %>%
unnest()
> head(long2)
# A tibble: 6 x 7
frame x y name ggraph.orig_index circular ggraph.index
<int> <dbl> <dbl> <fct> <int> <lgl> <int>
1 1 -1.07 0.363 a 1 FALSE 1
2 1 1.06 0.160 b 2 FALSE 2
3 1 -1.69 -0.310 c 3 FALSE 3
4 1 -0.481 0.135 d 4 FALSE 4
5 1 -0.0603 -0.496 e 5 FALSE 5
6 1 0.0373 1.02 f 6 FALSE 6
2. Convert the edges from the original layout into a table.
Here, I extract the edges from g and reshape into format that geom_segment can use, with columns for x, y, xend, and yend. This is ripe for refactoring, but it works.
edges_df <- igraph::as_data_frame(g, "edges") %>%
tibble::rowid_to_column() %>%
gather(end, name, -rowid) %>%
# Here we get the coordinates for each node from `long2`.
left_join(long2 %>% select(frame, name, x, y)) %>%
gather(coord, val, x:y) %>%
# create xend and yend when at the "to" end, for geom_segment use later
mutate(col = paste0(coord, if_else(end == "to", "end", ""))) %>%
select(frame, rowid, col, val) %>%
arrange(frame, rowid) %>%
spread(col, val) %>%
# Get the node names for the coordinates we're using, so that we
# can name the edge from a to b as "a_b" and gganimate can tween
# correctly between frames.
left_join(long2 %>% select(frame, x, y, start_name = name)) %>%
left_join(long2 %>% select(frame, xend = x, yend = y, end_name = name)) %>%
unite(edge_name, c("start_name", "end_name"))
> head(edges_df)
frame rowid x xend y yend edge_name
1 1 1 -1.0709480 -1.69252646 0.3630563 -0.3095612 a_c
2 1 2 -1.0709480 -0.48086213 0.3630563 0.1353664 a_d
3 1 3 -1.6925265 -0.48086213 -0.3095612 0.1353664 c_d
4 1 4 -1.0709480 -0.06032354 0.3630563 -0.4957609 a_e
5 1 5 1.0571895 -0.06032354 0.1596417 -0.4957609 b_e
6 1 6 -0.4808621 -0.06032354 0.1353664 -0.4957609 d_e
3. Plot!
ggplot() +
geom_segment(data = edges_df,
aes(x = x, xend = xend, y = y, yend = yend, color = edge_name)) +
geom_point(data = long2, aes(x, y, color = name), size = 4) +
geom_text(data = long2, aes(x, y, label = name)) +
guides(color = F) +
ease_aes("quadratic-in-out") +
transition_states(frame, state_length = 0.5) -> a
animate(a, nframes = 400, fps = 30, width = 700, height = 300)

Related

ggplot2 heatmap with tile height and width as aes()

I'm trying to create a heat map for an OD matrix, but I wanted to scale the rows and columns by certain weights. Since these weights are constant across each category I would expect the plot would keep the rows and columns structure.
# Tidy OD matrix
df <- data.frame (origin = c(rep("A", 3), rep("B", 3),rep("C", 3)),
destination = rep(c("A","B","C"),3),
value = c(0, 1, 10, 5, 0, 11, 15, 6, 0))
# Weights
wdf <- data.frame(region = c("A","B","C"),
w = c(1,2,3))
# Add weights to the data.
plot_df <- df %>%
merge(wdf %>% rename(w_origin = w), by.x = 'origin', by.y = 'region') %>%
merge(wdf %>% rename(w_destination = w), by.x = 'destination', by.y = 'region')
Here's how the data looks like:
> plot_df
destination origin value w_origin w_destination
1 A A 0 1 1
2 A C 15 3 1
3 A B 5 2 1
4 B A 1 1 2
5 B B 0 2 2
6 B C 6 3 2
7 C B 11 2 3
8 C A 10 1 3
9 C C 0 3 3
However, when passing the weights as width and height in the aes() I get this:
ggplot(plot_df,
aes(x = destination,
y = origin)) +
geom_tile(
aes(
width = w_destination,
height = w_origin,
fill = value),
color = 'black')
It seems to be working for the size of the columns (width), but not quite because the proportions are not the right. And the rows are all over the place and not aligned.
I'm only using geom_tile because I could pass height and width as aesthetics, but I accept other suggestions.
The issue is that your tiles are overlapping. The reason is that while you could pass the width and the heights as aesthetics, geom_tile will not adjust the x and y positions of the tiles for you. As your are mapping a discrete variable on x and y your tiles are positioned on a equidistant grid. In your case the tiles are positioned at .5, 1.5 and 2.5. The tiles are then drawn on these positions with the specified width and height.
This could be easily seen by adding some transparency to your plot:
library(ggplot2)
library(dplyr)
ggplot(plot_df,
aes(x = destination,
y = origin)) +
geom_tile(
aes(
width = w_destination,
height = w_origin,
fill = value), color = "black", alpha = .2)
To achieve your desired result you have to manually compute the x and y positions according to the desired widths and heights to prevent the overlapping of the boxes. To this end you could switch to a continuous scale and set the desired breaks and labels via scale_x/y_ continuous:
breaks <- wdf %>%
mutate(cumw = cumsum(w),
pos = .5 * (cumw + lag(cumw, default = 0))) %>%
select(region, pos)
plot_df <- plot_df %>%
left_join(breaks, by = c("origin" = "region")) %>%
rename(y = pos) %>%
left_join(breaks, by = c("destination" = "region")) %>%
rename(x = pos)
ggplot(plot_df,
aes(x = x,
y = y)) +
geom_tile(
aes(
width = w_destination,
height = w_origin,
fill = value), color = "black") +
scale_x_continuous(breaks = breaks$pos, labels = breaks$region, expand = c(0, 0.1)) +
scale_y_continuous(breaks = breaks$pos, labels = breaks$region, expand = c(0, 0.1))
So I think I have a partial solution for you. After playing arround with geom_tile, it appears that the order of your dataframe matters when you are using height and width.
Here is some example code I came up with off of yours (run your code first). I converted your data_frame to a tibble (part of dplyr) to make it easier to sort by a column.
# Converted your dataframe to a tibble dataframe
plot_df_tibble = tibble(plot_df)
# Sorted your dataframe by your w_origin column:
plot_df_tibble2 = plot_df_tibble[order(plot_df_tibble$w_origin),]
# Plotted the sorted data frame:
ggplot(plot_df_tibble2,
aes(x = destination,
y = origin)) +
geom_tile(
aes(
width = w_destination,
height = w_origin,
fill = value),
color = 'black')
And got this plot:
Link to image I made
I should note that if you run the converted tibble before you sort that you get the same plot you posted.
It seems like the height and width arguements may not be fully developed for this portion of geom_tile, as I feel that the order of the df should not matter.
Cheers

gganimate: include additional variable other than states level variable or frame in title expression

I'd like to insert another column value of my data into a gganimate animation title.
Example, here the states level variable is x and I'd like to add to title variable y:
df <- tibble(x = 1:10, y = c('a', 'a', 'b', 'd', 'c', letters[1:5]))
df
A tibble: 10 x 2
x y
<int> <chr>
1 1 a
2 2 a
3 3 b
4 4 d
5 5 c
6 6 a
7 7 b
8 8 c
9 9 d
10 10 e
This works as expected:
ggplot(df, aes(x, x)) +
geom_point() +
labs(title = '{closest_state}') +
transition_states(x,
transition_length = 0.1,
state_length = 0.1)
This fails:
ggplot(df, aes(x, x)) +
geom_point() +
labs(title = '{closest_state}, another_var: {y}') +
transition_states(x,
transition_length = 0.1,
state_length = 0.1)
Error in eval(parse(text = text, keep.source = FALSE), envir) :
object 'y' not found
Also tried this, but y will not change:
ggplot(df, aes(x, x)) +
geom_point() +
labs(title = str_c('{closest_state}, another_var: ', df$y)) +
transition_states(x,
transition_length = 0.1,
state_length = 0.1)
Another option is to map y as the states level variable and use the frame variable instead of x, but in my application y is either a not-necessarily-unique character variable like above, or it is a numeric variable but again not-necessarily-unique and not-necessarily-ordered. In which case gganimate (or ggplot?) will order it as it sees fit, making the final result weird not ordered by x:
ggplot(df, aes(x, x)) +
geom_point() +
labs(title = '{frame}, another_var: {closest_state}') +
transition_states(y,
transition_length = 0.1,
state_length = 0.1)
So how to simply add the changing value of the un-ordered, not numeric, y variable?
Finally: This question was asked here but without a reproducible example so it was not answered, hoping this one is better.
One dirty solution would be to paste together the variables and make a new one to use in the transition_states:
df <- mutate(df, title_var = factor(paste(x, y, sep="-"), levels = paste(x, y, sep="-")))
# # A tibble: 6 x 3
# x y title_var
# <int> <chr> <fct>
# 1 1 a 1-a
# 2 2 a 2-a
# 3 3 b 3-b
# 4 4 d 4-d
# 5 5 c 5-c
# 6 6 a 6-a
Then we could use gsub() in ordet to strip closest_state from the unwanted part, like this:
gsub(pattern = "\\d+-", replacement = "", "1-a")
"a"
So:
ggplot(df, aes(x, x)) +
geom_point() +
labs(title = '{gsub(pattern = "\\d+-", replacement = "", closest_state)}') +
transition_states(title_var, transition_length = 0.1, state_length = 0.1)
Another possibility, slightly more compact, from the author of gganimate himself, following the issue I opened:
https://github.com/thomasp85/gganimate/issues/252#issuecomment-450846868
According to Thomas:
There are multiple reasons why random columns from the input data
cannot be accessed so it is unlikely to get any better than this...
Here's a solution using dplyr, based on the gganimate developer Thomas's solution, provided by Giora.
library(tidyverse)
library(gganimate)
df <- tibble::tibble(x = 1:10, y = c('a', 'a', 'b', 'd', 'c', letters[1:5]))
a <- ggplot(df, aes(x, x)) +
geom_point() +
labs(title = "{closest_state}, another_var: {df %>% filter(x == closest_state) %>% pull(y)}") +
transition_states(x,
transition_length = 0.1,
state_length = 0.1)
animate(a)
The gganimate titles use glue syntax for the animated title elements, and you can include entire dplyr data manipulation pipelines within them.
You can refer to the closest_state variable provided by gganimate::transition_states() within your dplyr calls. Here, since the animation's frames are indexed by successive levels of x, I use filter() to subset df for a given frame based on the value of x and then refer to corresponding rows of column y, which contain additional information I'd like to display in the title. Using pull, you can grab the individual value of y corresponding to x and display it within the animation's title.
This is a clean and straightforward way to do it with the advantage that you can, e.g., compute summary values to display on-the-fly by adding summarize() and other calls in your magrittr pipeline.

How to make stacked circle plot without coord_polar

I've got a dataset similar to this:
x <- 100 - abs(rnorm(1e6, 0, 5))
y <- 50 + rnorm(1e6, 0, 3)
dist <- sqrt((x - 100)^2 + (y - 50)^2)
z <- exp(-(dist / 8)^2)
which can be visualised as follows:
data.frame(x, y, z) %>%
ggplot() + geom_point(aes(x, y, color = z))
What I would like to do is a stacked half-circle plot with averaged value of z in subsequent layers. I think it can be done with the combination of geom_col and coord_polar(), although the farthest I can get is
data.frame(x, y, z, dist) %>%
mutate(dist_fct = cut(dist, seq(0, max(dist), by = 5))) %>%
ggplot() + geom_bar(aes(x = 1, y = 1, fill = dist_fct), stat = 'identity', position = 'fill') +
coord_polar()
which is obviously far from the expectation (layers should be of equal size, plot should be clipped on the right half).
The problem is that I can't really use coord_polar() due to further use of annotate_custom(). So my question are:
can plot like this can be done without coord_polar()?
If not, how can it be done with coord_polar()?
The result should be similar to a graphic below, except from plotting layers constructed from points I would like to plot only layers as a whole with color defined as an average value of z inside a layer.
If you want simple radius bands, perhaps something like this would work as you pictured it in your question:
# your original sample data
x <- 100 - abs(rnorm(1e6, 0, 5))
y <- 50 + rnorm(1e6, 0, 3)
dist <- sqrt((x - 100)^2 + (y - 50)^2)
nbr_bands <- 6 # set nbr of bands to plot
# calculate width of bands
band_width <- max(dist)/(nbr_bands-1)
# dist div band_width yields an integer 0 to nbr bands
# as.factor makes it categorical, which is what you want for the plot
band = as.factor(dist %/% (band_width))
library(dplyr)
library(ggplot2)
data.frame(x, y, band) %>%
ggplot() + geom_point(aes(x, y, color = band)) + coord_fixed() +
theme_dark() # dark theme
Edit to elaborate:
As you first attempted, it would be nice to use the very handy cut() function to calculate the radius color categories.
One way to get categorical (discrete) colors, rather than continuous shading, for your plot color groups is to set your aes color= to a factor column.
To directly get a factor from cut() you may use option ordered_result=TRUE:
band <- cut(dist, nbr_bands, ordered_result=TRUE, labels=1:nbr_bands) # also use `labels=` to specify your own labels
data.frame(x, y, band) %>%
ggplot() + geom_point(aes(x, y, color = band)) + coord_fixed()
Or more simply you may use cut() without options and convert to a factor using as.factor():
band <- as.factor( cut(dist, nbr_bands, labels=FALSE) )
data.frame(x, y, band) %>%
ggplot() + geom_point(aes(x, y, color = band)) + coord_fixed()
Sounds like you may find the circle & arc plotting functions from the ggforce package useful:
# data
set.seed(1234)
df <- data.frame(x = 100 - abs(rnorm(1e6, 0, 5)),
y = 50 + rnorm(1e6, 0, 3)) %>%
mutate(dist = sqrt((x - 100)^2 + (y - 50)^2)) %>%
mutate(z = exp(-(dist / 8)^2))
# define cut-off values
cutoff.values <- seq(0, ceiling(max(df$dist)), by = 5)
df %>%
# calculate the mean z for each distance band
mutate(dist_fct = cut(dist, cutoff.values)) %>%
group_by(dist_fct) %>%
summarise(z = mean(z)) %>%
ungroup() %>%
# add the cutoff values to the dataframe for inner & outer radius
arrange(dist_fct) %>%
mutate(r0 = cutoff.values[-length(cutoff.values)],
r = cutoff.values[-1]) %>%
# add coordinates for circle centre
mutate(x = 100, y = 50) %>%
# plot
ggplot(aes(x0 = x, y0 = y,
r0 = r0, r = r,
fill = z)) +
geom_arc_bar(aes(start = 0, end = 2 * pi),
color = NA) + # hide outline
# force equal aspect ratio in order to get true circle
coord_equal(xlim = c(70, 100), expand = FALSE)
Plot generation took <1s on my machine. Yours may differ.
I'm not sure this satisfies everything, but it should be a start. To cut down on the time for plotting, I'm summarizing the data into a grid, which lets you use geom_raster. I don't entirely understand the breaks and everything you're using, so you might want to tweak some of how I divided the data for making the distinct bands. I tried out a couple ways with cut_interval and cut_width--this would be a good place to plug in different options, such as the number or width of bands.
Since you mentioned getting the average z for each band, I'm grouping by the gridded x and y and the cut dist, then using mean of z for setting bands. I threw in a step to make labels like in the example--you probably want to reverse them or adjust their positioning--but that comes from getting the number of each band's factor level.
library(tidyverse)
set.seed(555)
n <- 1e6
df <- data_frame(
x = 100 - abs(rnorm(n, 0, 5)),
y = 50 + rnorm(n, 0, 3),
dist = sqrt((x - 100)^2 + (y - 50)^2),
z = exp(-(dist / 8)^2)
) %>%
mutate(brk = cut(dist, seq(0, max(dist), by = 5), include.lowest = T))
summarized <- df %>%
filter(!is.na(brk)) %>%
mutate(x_grid = floor(x), y_grid = floor(y)) %>%
group_by(x_grid, y_grid, brk) %>%
summarise(avg_z = mean(z)) %>%
ungroup() %>%
# mutate(z_brk = cut_width(avg_z, width = 0.15)) %>%
mutate(z_brk = cut_interval(avg_z, n = 9)) %>%
mutate(brk_num = as.numeric(z_brk))
head(summarized)
#> # A tibble: 6 x 6
#> x_grid y_grid brk avg_z z_brk brk_num
#> <dbl> <dbl> <fct> <dbl> <fct> <dbl>
#> 1 75 46 (20,25] 0.0000697 [6.97e-05,0.11] 1
#> 2 75 47 (20,25] 0.000101 [6.97e-05,0.11] 1
#> 3 75 49 (20,25] 0.0000926 [6.97e-05,0.11] 1
#> 4 75 50 (20,25] 0.0000858 [6.97e-05,0.11] 1
#> 5 75 52 (20,25] 0.0000800 [6.97e-05,0.11] 1
#> 6 76 51 (20,25] 0.000209 [6.97e-05,0.11] 1
To make the labels, summarize that data to have a single row per band--I did this by taking the minimum of the gridded x, then using the average of y so they'll show up in the middle of the plot.
labels <- summarized %>%
group_by(brk_num) %>%
summarise(min_x = min(x_grid)) %>%
ungroup() %>%
mutate(y_grid = mean(summarized$y_grid))
head(labels)
#> # A tibble: 6 x 3
#> brk_num min_x y_grid
#> <dbl> <dbl> <dbl>
#> 1 1 75 49.7
#> 2 2 88 49.7
#> 3 3 90 49.7
#> 4 4 92 49.7
#> 5 5 93 49.7
#> 6 6 94 49.7
geom_raster is great for these situations where you have data in an evenly spaced grid that just needs uniform tiles at each position. At this point, the summarized data has 595 rows, instead of the original 1 million, so the time to plot shouldn't be an issue.
ggplot(summarized) +
geom_raster(aes(x = x_grid, y = y_grid, fill = z_brk)) +
geom_label(aes(x = min_x, y = y_grid, label = brk_num), data = labels, size = 3, hjust = 0.5) +
theme_void() +
theme(legend.position = "none", panel.background = element_rect(fill = "gray40")) +
coord_fixed() +
scale_fill_brewer(palette = "PuBu")
Created on 2018-11-04 by the reprex package (v0.2.1)

How to make geom_smooth less dynamic

When generating smoothed plots with faceting in ggplot, if the range of the data changes from facet to facet the smoothing may acquire too many degress of freedom for the facets with less data.
For example
library(dplyr)
library(ggplot2) # ggplot2_2.2.1
set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
mutate(y = dnorm(x) + 0.4*runif(n())) %>%
filter(z <= x) %>%
ggplot(aes(x,y)) +
geom_line() +
geom_smooth(method = 'loess', span = 0.3) +
facet_wrap(~ z)
generates the following:
The z=-5 facet is fine, but as one moves to subsequent facets the smoothing seems to 'overfit'; indeed z=-1 already suffers from that, and in the last facet, z=2, the smoothed line fits the data perfectly. Ideally, what I would like is a less dynamic smoothing that for example always smooths about 4 points (or kernel smoothing with a fixed kernel).
The following SO question is related but perhaps more ambitious (in that it wants more control over span); here I want a simpler form of smoothing.
I moved a few things around in your code to get this to work. I'm not sure if it's the best way to do it, but it's a simple way.
First we group by your z variable and then generate a number span that is small for large numbers of observations but large for small numbers. I guessed at 10/length(x). Perhaps there's some more statistically sound way of looking at it. Or perhaps it should be 2/diff(range(x)). Since this is for your own visual smoothing, you'll have to fine tune that parameter yourself.
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
filter(z <= x) %>%
group_by(z) %>%
mutate(y = dnorm(x) + 0.4*runif(length(x)),
span = 10/length(x)) %>%
distinct(z, span)
# A tibble: 8 x 2
# Groups: z [8]
z span
<int> <dbl>
1 -5 0.2000000
2 -4 0.2222222
3 -3 0.2500000
4 -2 0.2857143
5 -1 0.3333333
6 0 0.4000000
7 1 0.5000000
8 2 0.6666667
Update
The method I did have here was not working correctly. The best way to do this (and the most flexible way to do model-fitting in general) is to pre-compute it.
So we take our grouped dataframe with the computed span, fit a loess model to each group with the appropriate span, and then use broom::augment to form it back into a dataframe.
library(broom)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
filter(z <= x) %>%
group_by(z) %>%
mutate(y = dnorm(x) + 0.4*runif(length(x)),
span = 10/length(x)) %>%
do(fit = list(augment(loess(y~x, data = ., span = unique(.$span)), newdata = .))) %>%
unnest()
# A tibble: 260 x 7
z z1 x y span .fitted .se.fit
<int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
1 -5 -5 -5.000000 0.045482851 0.2 0.07700057 0.08151451
2 -5 -5 -4.795918 0.248923802 0.2 0.18835244 0.05101045
3 -5 -5 -4.591837 0.243720422 0.2 0.25458037 0.04571323
4 -5 -5 -4.387755 0.249378098 0.2 0.28132026 0.04947480
5 -5 -5 -4.183673 0.344429272 0.2 0.24619206 0.04861535
6 -5 -5 -3.979592 0.256269425 0.2 0.19213489 0.05135924
7 -5 -5 -3.775510 0.004118627 0.2 0.14574901 0.05135924
8 -5 -5 -3.571429 0.093698117 0.2 0.15185599 0.04750935
9 -5 -5 -3.367347 0.267809673 0.2 0.17593182 0.05135924
10 -5 -5 -3.163265 0.208380125 0.2 0.22919335 0.05135924
# ... with 250 more rows
This has the side effect of duplicating the grouping column z, but it intelligently renames it to avoid name-collision, so we can ignore it. You can see that there are the same number of rows as the original data, and the original x, y, and z are there, as well as our computed span.
If you want to prove to yourself that it's really fitting each group with the right span, you can do something like:
... mutate(...) %>%
do(fit = (loess(y~x, data = ., span = unique(.$span)))) %>%
pull(fit) %>% purrr::map(summary)
That will print out the model summaries with the span included.
Now it's just a matter of plotting the augmented dataframe we just made, and manually reconstructing the smoothed line and confidence interval.
... %>%
ggplot(aes(x,y)) +
geom_line() +
geom_ribbon(aes(x, ymin = .fitted - 1.96*.se.fit,
ymax = .fitted + 1.96*.se.fit),
alpha = 0.2) +
geom_line(aes(x, .fitted), color = "blue", size = 1) +
facet_wrap(~ z)
I would simply remove the span option (because 0.3 seems too granular) or use lm method to do polynomial fit.
library(dplyr)
library(ggplot2) # ggplot2_2.2.1
set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
mutate(y = dnorm(x) + 0.4*runif(n())) %>%
filter(z <= x) %>%
ggplot(aes(x,y)) +
geom_line() +
geom_smooth(method = 'lm', formula = y ~ poly(x, 4)) +
#geom_smooth(method = 'loess') +
#geom_smooth(method = 'loess', span = 0.3) +
facet_wrap(~ z)
Since I asked how to do kernel smoothing I wanted to provide an answer for that.
I'll start by just adding it as extra data to data frame and plotting that, much as the accepted answer does.
First here is the data and packages I'll be using (same as in my post):
library(dplyr)
library(ggplot2) # ggplot2_2.2.1
set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
mutate(y = dnorm(x) + 0.4*runif(n())) %>%
filter(z <= x) ->
Z
Next here is the plot:
Z %>%
group_by(z) %>%
do(data.frame(ksmooth(.$x, .$y, 'normal', bandwidth = 2))) %>%
ggplot(aes(x,y)) +
geom_line(data = Z) +
geom_line(color = 'blue', size = 1) +
facet_wrap(~ z)
which simply uses ksmooth from base R. Note that it's quite simple to avoid the dynamic smoothing (making the bandwidth constant takes care of that). In fact, one can recover the a dynamic style smoothing (i.e., more like geom_smooth) as follows:
Z %>%
group_by(z) %>%
do(data.frame(ksmooth(.$x, .$y, 'normal', bandwidth = diff(range(.$x))/5))) %>%
ggplot(aes(x,y)) +
geom_line(data = Z) +
geom_line(color = 'blue', size = 1) +
facet_wrap(~ z)
I also followed the example in https://github.com/hrbrmstr/ggalt/blob/master/R/geom_xspline.r to turn this idea into an actual stat_ and geom_ as follows:
geom_ksmooth <- function(mapping = NULL, data = NULL, stat = "ksmooth",
position = "identity", na.rm = TRUE, show.legend = NA,
inherit.aes = TRUE,
bandwidth = 0.5, ...) {
layer(
geom = GeomKsmooth,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(bandwidth = bandwidth,
...)
)
}
GeomKsmooth <- ggproto("GeomKsmooth", GeomLine,
required_aes = c("x", "y"),
default_aes = aes(colour = "blue", size = 1, linetype = 1, alpha = NA)
)
stat_ksmooth <- function(mapping = NULL, data = NULL, geom = "line",
position = "identity", na.rm = TRUE, show.legend = NA, inherit.aes = TRUE,
bandwidth = 0.5, ...) {
layer(
stat = StatKsmooth,
data = data,
mapping = mapping,
geom = geom,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(bandwidth = bandwidth,
...
)
)
}
StatKsmooth <- ggproto("StatKsmooth", Stat,
required_aes = c("x", "y"),
compute_group = function(self, data, scales, params,
bandwidth = 0.5) {
data.frame(ksmooth(data$x, data$y, kernel = 'normal', bandwidth = bandwidth))
}
)
(Note that I have a very poor understanding of the above code.) But now we can do:
Z %>%
ggplot(aes(x,y)) +
geom_line() +
geom_ksmooth(bandwidth = 2) +
facet_wrap(~ z)
And the smoothing is not dynamic, as I originally wanted.
I do wonder if there is a simpler way, though.

plot line width (size) based on counts using ggplot or any other method in R

I have a dataset in long-format, each ID 'walks' 3 steps, each step (variable name is step) can land on different locations (variable name is milestone), I want to draw all of the paths. Because there are some paths more traveled, I want to make the width (size) of the paths proportional to their counts. I am imagining it to be something like geom_line(aes(size=..count..))in ggplot, but it doesn't work.
Below is my code, in the code you can find the url for the example dataset. My silly solution to add width was to dodge the line, but it's not proportional, and it leaves cracks.
ddnew <- read.csv("https://raw.github.com/bossaround/question/master/data9.csv" )
ggplot(ddnew, aes(x=step, y=milestone, group=user_id)) +
geom_line(position = position_dodge(width=0.05)) +
scale_x_discrete(limits=c("0","1","2","3","4","5","6","7","8","9")) +
scale_y_discrete(limits=c("0","1","2","3","4","5","6","7","8","9"))
The plot from my current code looks like this, but you can see the cracks, and it's not proportional.
I was hoping this can look like a Sankey diagram with the width indicating counts.
Does this help?
library(ggplot2)
ddnew <- read.csv("https://raw.github.com/bossaround/question/master/data9.csv" )
ggplot(ddnew, aes(x=step, y=milestone, group=user_id)) +
stat_summary(geom="line", fun.y = "sum", aes(size=milestone),alpha=0.2, color="grey50")+
scale_x_discrete(limits=factor(0:2)) +
scale_y_discrete(limits=factor(0:10)) +
theme(panel.background = element_blank(),
legend.position = "none")
One option is to use the riverplot package. First you'll need to summarize your data so that you can define the edges and nodes.
> library(riverplot)
>
> paths <- spread(ddnew, step, milestone) %>%
+ count(`1`, `2`, `3`)
> paths
Source: local data frame [9 x 4]
Groups: 1, 2 [?]
`1` `2` `3` n
<int> <int> <int> <int>
1 1 2 3 7
2 1 2 10 8
3 1 3 2 1
4 1 4 8 1
5 1 10 2 118
6 1 10 3 33
7 1 10 4 2
8 1 10 5 1
9 1 10 NA 46
Next define your nodes (i.e. each combination of step and milestone).
prefix <- function(p, n) {paste(p, n, sep = '-')}
nodes <- distinct(ddnew, step, milestone) %>%
mutate(ID = prefix(step, milestone),
y = dense_rank(milestone)) %>%
select(ID, x = step, y)
Then define your edges:
e12 <- group_by(paths, N1 = `1`, N2 = `2`) %>%
summarise(Value = sum(n)) %>%
ungroup() %>%
mutate(N1 = prefix(1, N1),
N2 = prefix(2, N2))
e23 <- group_by(paths, N1 = `2`, N2 = `3`) %>%
filter(!is.na(N2)) %>%
summarise(Value = sum(n)) %>%
ungroup() %>%
mutate(N1 = prefix(2, N1),
N2 = prefix(3, N2))
edges <- bind_rows(e12, e23) %>%
mutate(Value = Value) %>%
as.data.frame()
Finally, make the plot:
style <- default.style()
style$srt <- '0' # display node labels horizontally
makeRiver(nodes, edges) %>% plot(default_style = style)
If you are looking for user-specifc counts of paths then this might help:
ddnew <- read.csv("https://raw.github.com/bossaround/question/master/data9.csv" )
ddnew <- ddnew %>%
group_by(user_id) %>%
mutate(step_id = paste(step, collapse = ","),
milestone_id = paste(milestone, collapse = ",")) %>%
group_by(step_id, milestone_id) %>%
mutate(width = n())
ggplot(ddnew, aes(x=step, y=milestone, group=user_id)) +
geom_line(aes(size = width)) +
scale_x_discrete(limits=c("0","1","2","3","4","5","6","7","8","9")) +
scale_y_discrete(limits=c("0","1","2","3","4","5","6","7","8","9"))
The idea is to count unique user-specific paths and assign these counts as width in the geom_line() aesthetic.

Resources