I am trying to visually represent the distribution of last names in the US. Specifically, I am trying to show that the distribution is such that the most common names (say the top 50) are very common but that it drops off quickly after that. The conclusion I hope to support is that it's not that meaningful to differentiate between common and less common among names that are not in the top X because they are all a very small portion of the population.

I have the observed frequency of all last names appearing more than 100 times in the 2000 census.

## Data from the US Census, extracted and CSV re-hosted 
## http://www.census.gov/genealogy/www/data/2000surnames/names.zip
names <- read.csv("http://samswift.org/files/app_c.csv")

My intuition was to bin the ranked list by groups of 50. Most common names 1-50, 51-100, ...

sum50 <-  tapply(names$count, (seq_along(names$count)-1) %/% 50, sum)

So we now have the sum population of people with a top 50 name, a second 50 name, etc.

I was imaging a plot like this desired plot where the x-axis is the ordered factor of bins (1-50, 51-100 ..) and the y-axis is the sum population in that bin. I think it's important that bar widths scale with the y variable too so that the area of the square conveys the mass of the population.

So, two part question really (although I think that's frowned upon)

  1. How might I generate this plot in R with the provided data. I generally use ggplot2, but I am not wed to it. I tried using geom_bar and trying to set the width, but I failed to generate anything even a bit functional.

  2. Do you have a better thought on how to visualize the assertion I'm making, or disagree with the assertion entirely?


This kind of plot could be generated with geom_rect.

Your data:


First, we need additional variables:

The cumulative sum:

cum <- rev(cumsum(rev(sum50)))

Put all into a data frame. The variables start and stop indicate where the rectangles should begin and end, respectively:

data <- data.frame(sum = sum50,
                   names = paste(as.numeric(names(sum50)) * 50 + 1,
                                 as.numeric(names(sum50)) * 50 + 50, sep = "-"),
                   start = c(cum[-1], 0),
                   stop = cum, stringsAsFactors = FALSE)
data$names[nrow(data)] <- paste(as.numeric(names(sum50)[length(sum50)]) * 50 + 1,
                                                          nrow(names) %% 50, sep = "-")

The variable center is the center between start and stop position:

data$center <- (data$stop - data$start)/2 + data$start

For this example, I use the first five rows:

data <- data[1:5, ]



ggplot(data, aes(xmin = start, xmax = stop, ymin = 0, ymax = sum)) +
  geom_rect(fill = NA, colour = "black") +
  scale_x_reverse("bin", breaks = data$center, labels = data$names) +
  coord_equal() # because we want squares

enter image description here

This is the version based on the complete data set. You should consider using only a subset of x-axis labels.

enter image description here

  • 1
    $\begingroup$ +1 very nice answer! You might consider throwing some sqrts in there so that the it's the area of the rect rather than its length and width that represents the data. $\endgroup$ – Gregor Sep 20 '12 at 18:21
  • $\begingroup$ @shujaa The idea with the sqrt is nice. But the easiest way would be to apply it to the labels of the y-axis: scale_y_continuous(breaks = seq(0, max(data$sum), length.out = 5), labels = round(sqrt(seq(0, max(data$sum), length.out = 5)))) $\endgroup$ – Sven Hohenstein Sep 21 '12 at 6:21
  • $\begingroup$ @SvenHohenstein yes, that would be easiest, but still misleading in the visual comparisons because the areas will correspond to the square of the population in that bin. To get the area to correspond linearly with the population you'd either need to make the bins fixed width or calculate both width and height with the square root. $\endgroup$ – Gregor Sep 21 '12 at 17:35

Your Answer

