This figure allows you to make two types of comparisons: (1) differences in rent changes across different building classes within the same city and (2) differences in rent changes across different cities. Which perceptual visualization tasks might you use when making these comparisons using Figure 2?
Length/area: We are looking at the length of each bar in comparison to other bars.
Hue (color): Classes A-C and the mean are all represented by a different color.
Position along a common scale: All the bars in this chart share a common baseline and all are measured on a common scale (percentage). If there were different bar charts for each city or class, that would be positioned along non-aligned scales.
Direction (sort of): Bars extend below and above the X-axis, so direction is important in understanding whether rental prices have gone up or down.
The data is available to you in the file pew_fig2_rent_change_class.csv, with pew_fig2_dict.qmd describing the data source and variables. Visualize this data in two different ways: (1) using a Cleveland dot plot and (2) splitting the bar-plots onto multiple axes (see UCRs dot-plot page or Wilke chapter 6 ). Compare these two images to the original Figure 2, what are the pros and cons of each?
Pivoting the data so it can be plotted in a bar chart:
rentals_pivoted <- rental_data |>pivot_longer( -metro,names_to ="class",values_to ="percent_change" )#facet-wrap plot broken up by metro areaggplot(rentals_pivoted, aes(x = class, y = percent_change, fill = class)) +geom_col() +labs (y ="Percent Change", title ="Average Rent Change, 2023-24, by Metro Area") +theme_minimal() +theme(axis.text.x =element_blank()) +facet_wrap(~ metro)
#facet-wrap plotbroken up by class#I originally used color to represent city here, but it was overwhelmingrentals_pivoted |>mutate(metro =factor(metro, levels = metro[class =="class_c"][order(percent_change[class =="class_c"], decreasing =FALSE)])) |>ggplot(aes(x = metro, y = percent_change, fill = class)) +geom_col() +labs (y ="Percent Change", x ="Metro Area",title ="Average Rent Change, 2023-24, by Class of Housing") +theme_minimal() +theme(axis.text.x =element_text(angle =45, hjust =1)) +theme(legend.position ="none") +facet_wrap(~ class)
The first facet-wrapped graph (broken up by city) is the same as the original, just with a line break. We will compare the original graph to the second facet-wrapped graph, broken up by class. I know that the best practice is to flip this rather than rotate the names on the x-axis, but this gets more visually confusing when some percentages are in the negative.
This graph is helpful in understanding rent change differences across classes of housing. Class A represents newer housing with lots of amenities, class B represents buildings with some amenities, class C are buildings with outdated features located in less desirable areas. This graph makes it easier to understand that across these cities, generally, class C housing proves the point of the article, that new housing slows growth of (or actually makes cheaper) older, more affordable units.
The original shows that prices generally decreased across the board. It’s better for comparing cities—it looks like it’s ordered by Class C decrease, but, generally, the graph’s message is that rent prices have decreased or stabilized across cities., with growth slowing the least in Houston.
Pros: Better understand rental price changes by class of housing based on the type of housing and the average overall change.
Cons: It’s harder to understand data by city. It’s also harder to under how different classes of housing compare within a city. My initial choice to use color to represent city would probably be more useful if there were fewer cities. It was overwhelming, so I changed it to represent class.
Cleveland dot plot
class_c_order <- rentals_pivoted |>filter(class =="class_c") |>arrange(percent_change) |>pull(metro)rentals_pivoted |>mutate(metro =factor(metro, levels = class_c_order)) |>ggplot(aes(x = percent_change, y = metro, color = class, shape = class)) +geom_point(size =4) +scale_shape_manual(values =c(15, 16, 17, 18)) +theme_bw() +labs(title ="Average Rent Change, 2023-24, by City and Class of Housing",x ="Percent Change",y =NULL,color ="Class",shape ="Class" )
#with lines, which may not make as much sense with four variablesrentals_pivoted |>mutate(metro =factor(metro, levels = class_c_order)) |>ggplot(aes(x = percent_change, y = metro, shape = class)) +geom_line(aes(group = metro)) +geom_point(size =4, aes(color = class)) +scale_shape_manual(values =c(15, 16, 17, 18)) +theme_bw() +labs(title ="Average Rent Change, 2023-24, by City and Class of Housing",x ="Percent Change",y =NULL,color ="Class",shape ="Class" )
Like with the original bar graph, it’s easy to understand data by city, though even with the gridlines, it’s somewhat easier to get lost between the city and price data. With a bar chart, it’s super clear when quantities are below zero, but that’s not as immediately clear with a dot plot. The dot plot makes it easier to see which classes of housing prices decreased the most, because they are generally clustered together, and that’s a little tougher to put together with the original.
Pros: This is a simple, relatively easy to understand plot. Every city falls on a straight line and it’s about as easy to understand how cities differ as it is to see how classes differ. Easy to see trends in housing classes.
Cons: There’s overlap between points, but if I change it to position dodge, the dots are moved down/up, which would be very confusing. Because there are so many cities, this is just kind of busy. It’s easy to get lost/hard to connect the city with its representative dots. If there were only 3 cities, this would be a clearer, cleaner visual. It’s hard to track. I tried a few different types of themes/gridlines, and I decided on the theme_bw because otherwise the legend looks like it’s part of the graph.
Part 2
Analyze the problems with a bad figure. Your predecessor on the data team had been working on a presentation about permiting reform and left you the following figure. There are a number of problems with the figure, and it has a surprising conclusion given the recent research. Take a close look at the figure and code and list all of the ways that the figure is bad/ugly/wrong.
library(extrafont)
Warning: package 'extrafont' was built under R version 4.5.3
Registering fonts with R
library(systemfonts)library(showtext)
Warning: package 'showtext' was built under R version 4.5.3
Loading required package: sysfonts
Warning: package 'sysfonts' was built under R version 4.5.3
Loading required package: showtextdb
Warning: package 'showtextdb' was built under R version 4.5.3
Attaching package: 'showtextdb'
The following object is masked from 'package:extrafont':
font_install
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): Region, State, region
dbl (9): cbsa, year, population, units_total, units_1, units_5plus, n_struct...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Rows: 51924 Columns: 7
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): Region, City, State
dbl (3): cbsa, SizeRank, zori
date (1): date
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
I’m going to first discuss issues with the visuals in general, then what data was chosen and how it’s represented.
Visuals (why these charts are bad and ugly)
First, and this must be addressed, we did not need one Brooklyn Tower, and these charts altogether contain about twenty. As columns, visually, they’re also problematic: the width of the bar is actually showing the beginning of a second tower (at least when it renders on my machine), which is visually confusing. There’s shading for no reason (because this is a 3-D building with sides), which makes the eye do more work than it has to. The top of the tower has some white space, so, in some places, there isn’t a clean top of the bar to indicate where it stops. The “Low Permit” chart only shows little stumps of the tower, so it’s not even clear what we’re looking it–it just looks like a bunch of pixels. Overall, a regular bar would work much better in this case, and, just like in real life, Brooklyn Tower is just a confusing eyesore.
Labeling issues: There are no labels for the region name, so the user has no idea what cities are represented in either graph, so there’s a lack of context. Not knowing what cities are represented isn’t a good start. There are no numbers on the y-axis, so we have no idea what the scale is or what numbers we’re looking at. The labels used are confusing and kind of jargon-ey. The analyst decided to take the perfectly servicable “region” and rename it “RegionName,” which is less clear and doesn’t have a space even though axis labels support them High-Permit and Low-Permit are missing hyphens. It’s unclear what Zori is (something like “Mean Rent” or “Mean Rent According to Zillow” might have been a clearer label). Otherwise, it’s unreasonable to assume that everyone knows what it means. We’re also making a jump from most units built = high-permit metro, and that’s not really explained.
The scale: Both graphs show y values 750-3500 (average rents $750 - $3500), which, combined with the lack of axis labels, is incredibly misleading. While it doesn’t do much to the higher rents, it makes the lower rents look much lower. Bars are not ordered from lowest to highest or highest to lowest, and there’s no reason to have them in this order (they are not sequential).
The gridlines, which are red and green, are distracting. Color is used without a purpose (I don’t automatically register green = low, red = high). The red seems to match a red in Brooklyn Tower (that could just be a trick of the eye). The title is also really large and takes up more space than it should. Fonts are different between charts for no reason, which makes them feel less like part of the same story. Script, used in the low-permit graph, is harder to read and less ADA-friendly than sans serif fonts.
In case I have not sufficiently emphasized why the chart is ugly: the colors are garish and have been changed from a neutral for no reason, Brooklyn Tower is pixelated, and the fonts are distracting.
What data is represented? (Why these charts are wrong)
“The metros that permitted the highest number of units had the highest rents, suggesting that permitting fewer units is the best policy.” This logic is incredibly faulty and misleading. Let’s look at the cities represented in the graphs:
#lowest number of unitsnew_sheet <- msa |>filter(year ==2025, population >100000) |>arrange(units_total) |>rename(RegionName = Region) |>left_join(raw,by=c('cbsa','year'))head(new_sheet, 10)
# A tibble: 10 × 14
cbsa RegionName State region year population units_total units_1
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 48540 Wheeling, WV-OH WV South… 2025 134089 5 5
2 48260 Weirton-Steubenville… WV South… 2025 112468 17 17
3 34060 Morgantown, WV WV South… 2025 141995 30 30
4 41140 St. Joseph, MO-KS MO Midwe… 2025 119170 59 57
5 27780 Johnstown, PA PA North… 2025 128968 61 61
6 13020 Bay City, MI MI Midwe… 2025 102123 67 38
7 48700 Williamsport, PA PA North… 2025 112587 69 67
8 13220 Beckley, WV WV South… 2025 109940 77 77
9 22140 Farmington, NM NM Mount… 2025 120340 80 80
10 11020 Altoona, PA PA North… 2025 119541 86 66
# ℹ 6 more variables: units_5plus <dbl>, n_structs <dbl>, units_2_4 <dbl>,
# migration <dbl>, Region <chr>, zori <dbl>
#highest number of unitsnew_sheet_2 <- msa |>filter(year ==2025, population >100000) |>arrange(desc(units_total)) |>rename(RegionName = Region) |>left_join(raw,by=c('cbsa','year'))head(new_sheet_2, 10)
These graphs represent incredibly different lists. The lower rents/number of units are in small towns/cities that are generally inexpensive (the floor is 100k residents, while the highest population metro area is about 20 million residents). The higher rents are in expensive metro areas. Also, the number of new units isn’t adjusted for population (i.e., per capita): it’s an overall number, so this isn’t a meaningful comparison. In the most extreme case, we are comparing a city with 5 new units to a city with about 40k new units (i.e., comparing a city with high rental demand to one with low rental demand). The lists are sorted by number of units, not rent. Overall, this is intentionally misleading.
These two charts together suggest that the number of permits is related to high rents. Again, these numbers are raw, not per capita, so a large number of permits just means high population (e.g., NYC, Houston) or fast-growing areas (e.g. Phoenix). These two things are related—there is a high demand for units, these are large metro areas surrounding economic hubs like New York City and Houston, of course the rent is going to be higher. However, there’s no compelling reason shown why higher absolute permit numbers would equal higher rents–these are just bigger cities that need more housing. In fact, fewer new units/permits would probably mean higher rents because of a higher demand.
Average rent is not a meaningful comparison because these areas have different costs of living. A more meaningful comparison might be year-over-year change.
Finally, the charts assume that new units = number of permits. I will assume it means new units on the market that year. However, the relationship to permitting is not made 100% clear. I did not have an issues with the “raw” data frame, which calculates average Zori by year. I thought that methodology was fine, and I use it below. However, I thought it should have been explained. The choice to exclude areas with fewer than 100k residents is also not explained.
Create a data story about this data. You have been given datasets with information about rent, building permits, and demographics (population and net-migration) in metropolitan statistical areas, covering a period from 2015 to 2026 (rents) and 2021 to 2025 (permits and demographics). You should create several figures (2-3) to explain whether or not building permits policy can help reduce rents. Assume that your audience is the city government at a large city in the United States (e.g. New York City). Your story must be entirely told by your figures, though you may use text sparingly in the title/subtitle and annotations to enhance your story. Provide a one to two paragraph explanation of the choices you made for your figure. Do not include exploratory figures/discussion.
#calculating new units per capitamsa_joined <- msa |>left_join(raw,by=c('cbsa','year'))units_per_capita <- msa_joined |>mutate(units_per_capita =round(units_total / population, 5))
#There are some NA values in here where there was no zori.only_25 <- units_per_capita |>filter(year ==2025)percent_change <- zori_change |>full_join(only_25, by=c('cbsa')) |>select(-Region.x.y) |>rename(metro_area = Region.x.x)
#join with yearly rent increase tableinflation_comparison <- rent_change_2 |>left_join(yearly_inflation, by=c('year'))
#filter out 2015 and 2026 because we don't have complete rent increase datainflation_comparison |>filter(year >2015 , year <2026) |>pivot_longer(cols =c(percent_increase, inflation), names_to ="metric", values_to ="rate" ) |>mutate(metric =if_else(metric =="percent_increase", "Rent Increase", "Inflation")) |>ggplot(aes(x =factor(year), y = rate, fill = metric)) +geom_col(position ="dodge") +scale_y_continuous(labels = scales::label_percent(scale =1)) +theme_minimal() +labs(x ="Year", y ="Percent Increase", fill ="Metric",title ="Rents Continue to Rise, and Frequently Outpace Inflation",subtitle ="Comparing Inflation to Rent Incresaes by Year")
*Rent increase = mean increase across cities in the U.S.
Explanation: This is the “problem.” I wasn’t sure if it was okay to bring in outside data, but I ended up doing it anyway–inflation data from the BLS to compare to average year-on-year rent increases. I tried a few things before I landed on this, including comparing migration, which seems loosely related to higher rents, but people leaving cities didn’t seem like a compelling enough problem. Also, that correlation wasn’t particularly strong, and I didn’t want to use three dot plots.
Rationale for using December numbers: “A 12-month percent change from, say, December-to-December, is arguably a more recent estimate of price change than an annual average percent change. Said another way, the December-to-December percent change is the most recent 12-month percent change in a year, while the annual average percent change reflects the change in the average index for all 12 months of one year to the average index for all 12 months the next year.”
#Dot plot of Zori pct change and permits per capitamost_per_cap |>ggplot(aes(x = units_per_capita,y = zori_pct_change)) +geom_point(color ="steelblue", alpha = .6) +geom_hline(yintercept =0, linetype ="dashed", color ="gray50") +scale_y_continuous(labels = scales::label_percent(scale =1)) +geom_smooth(method ="lm", color ="red", linetype ="dashed", se =FALSE) +theme_minimal() +labs(x ="Permits Per Capita", y ="% Change in Average Rent", title ="More Permits to Build May Keep Rents Down",subtitle ="2024-25 Average Rent Change vs. Permits Per Capita by City")
`geom_smooth()` using formula = 'y ~ x'
Explanation: This was the simplest way I could think of to show that more permits per capita tends to lead to lower/more stable rents. I used 2025 data because it was the most recent. I assume new units = units currently available, though the permits are not tied to a particular year. The two dashed lines make it a little busy, but I wanted it to be clear that 1) the trend was downward and 2) some values were below zero. The alpha allows for some emphasis on clusters of values. I had a punchier title along of the lines of “Does more permits mean lower rents?” but that implied causation.
#looking at slightly larger metro areas with 1 million +high_pop <- most_per_cap |>arrange(desc(zori_pct_change)) |>filter(population >1000000)top_twenty <-bind_rows( high_pop |>slice_max(zori_pct_change, n =10) |>mutate(group ="Highest Increase"), high_pop |>slice_min(zori_pct_change, n =10) |>mutate(group ="Lowest Increase"))
top_twenty |>ggplot(aes(x = units_per_capita,y = zori_pct_change,color = group)) +geom_point(size =4, shape =17) +geom_hline(yintercept =0, linetype ="dashed", color ="gray50") +theme_bw() +labs(x ="Permits Per Capita", y ="% Change in Average Rent", title ="Focus: Comparing the 10 Highest and Lowest Rent Increases Nationwide",subtitle ="2024-25 Average Rent Change vs. Permits Per Capita by City",color ="Category")
*Top and bottom 10 rents taken from metro areas with > 1 million residents
Explanation: I filtered out metro areas with fewer than 1 million people, which seems like a lot, but these are greater metro areas, so the counts are much higher than within the city limits. I thought that after the second chart, which includes a lot of data, zooming in on the most extreme cases (with context!) would be helpful to understanding that more new units = lower overall rent. I initailly called this a “case study,” but that seemed offensive to social and behavioral scientists.