>>>markdown
# Making the Names Data Sing: Answer Key

Every model question from the instructor answer key, answered with Malloy against the SSA baby-names data (1910–2021). The sources live in `model.malloy`; run the cell below first.
>>>malloy
import "model.malloy"
>>>markdown
## 1 Type the columns

What type is each column: entity, category, time, place or measure? What other roles can it play?
>>>markdown
### How big is the dataset?
>>>malloy
run: names -> {
  aggregate:
    row_count is count()
    first_year is min(year_born)
    last_year is max(year_born)
    name_count
    state_count is count(state)
    total_births
}
>>>markdown
### One row of the data
>>>malloy
run: names -> {
  where: state = 'WA' and year_born = 1987 and name = 'Olivia'
  select: name, sex, year_born, state, births
}
>>>markdown
**Answer.** The data has **6,311,504** rows covering **1910–2021**: **32,403** different names, **51** states (the 50 states plus DC) and **319,731,698** births in total. In the model, `year` is renamed `year_born`, `number` is `births` and `gender` is `sex`.

      The worksheet's example row (Olivia, F, 1987, WA, 212) is made up. In the real data, **42** girls in Washington were named Olivia in 1987.
>>>markdown
## 3.1 Names on their own

**Seed:** "the long tail of names"

**Model question:** What share of all girls born in each year from 1910 to 2020 received one of the top 10 girls' names that year?

**How:** Rank names within year × sex; sum births for rank ≤ 10; divide by the year × sex total.
>>>markdown
### Share of babies with a top-10 name, and distinct names, by year
>>>malloy
run: names -> {
  group_by: year_born, sex, name
  aggregate: total_births
  calculate:
    name_rank is rank() { partition_by: year_born, sex  order_by: total_births desc }
    year_total is sum_window(total_births) { partition_by: year_born, sex }
} -> {
  group_by: year_born, sex
  aggregate:
    top10_share is total_births.sum() { where: name_rank <= 10 } / year_total.max()
    distinct_names is count()
  order_by: year_born, sex
}
>>>markdown
### Worksheet example: names needed to cover half of boys born in 2020
>>>malloy
run: names -> {
  where: year_born = 2020 and sex = 'M'
  group_by: name
  aggregate: total_births
  calculate:
    running_total is sum_cumulative(total_births) { order_by: total_births desc }
    all_boys is sum_window(total_births)
} -> {
  where: running_total - total_births < all_boys / 2
  aggregate: names_to_cover_half is count()
}
>>>markdown
**Answer.** **Girls with a top-10 name fell from 24% in 1910 to 9.7% in 2020.** Boys fell even further, from 37% to 9.1%, so today the two are about the same. Most of the drop came after 1990.

      Over the same period the number of distinct girls' names given each year went from 1,083 to 5,175, and boys' from 692 to 4,160. (These are lower bounds, because names with fewer than 5 babies in a state are left out.)

      **Worksheet example:** in 2020 it took just **118** names to cover half of all boys born.
>>>markdown
## 3.2 Names over time

**Seed:** "fastest risers"

**Model question:** Which girls' names had the largest increase in rank between 1990 and 2010, among names that were in the top 100 in 2010?

**How:** Rank within year × sex; put the 1990 and 2010 ranks side by side for each name; subtract.
>>>markdown
### Biggest climbers into the 2010 top 100 (girls)
>>>malloy
run: names -> {
  where: sex = 'F' and (year_born = 1990 | 2010)
  group_by: year_born, name
  aggregate: total_births
  calculate: name_rank is rank() { partition_by: year_born  order_by: total_births desc }
} -> {
  group_by: name
  aggregate:
    rank_1990 is name_rank.max() { where: year_born = 1990 }
    rank_2010 is name_rank.max() { where: year_born = 2010 }
} -> {
  where: rank_2010 <= 100
  select:
    name, rank_1990, rank_2010
    places_climbed is rank_1990 - rank_2010
  order_by: places_climbed desc
  limit: 15
}
>>>markdown
### Fads, classics and comebacks: rank by decade
>>>malloy
run: names -> {
  where: sex = 'F'
  group_by: decade, name
  aggregate: total_births
  calculate: decade_rank is rank() { partition_by: decade  order_by: total_births desc }
} -> {
  where: name = 'Jennifer' | 'Elizabeth' | 'Emma' | 'Hazel' | 'Ava'
  select: name, decade, decade_rank
  order_by: name, decade
}
>>>markdown
**Answer.** **Zoey** climbed the most: from #3,380 in 1990 to #47 in 2010. Payton, Serenity and Bella follow. Addison rose from #2,294 to #11.

      Ranking by decade shows all three patterns the key mentions:

      - **Fad:** Jennifer went from #1,388 in the 1930s to #1 in the 1970s and back down to #460 by the 2020s.

      - **Classic:** Elizabeth ranked between #8 and #25 in every decade from the 1910s to the 2020s.

      - **Comebacks:** Emma fell from #41 to #448 and came back to #1 in the 2010s. Hazel went from #27 to #1,213 and back to #29. Ava climbed from #1,090 in the 1980s to #4.
>>>markdown
## 3.3 Names and sex

**Seed:** "unisex names"

**Model question:** Among names with at least 1,000 total babies in every decade from 1950 to 2010, which ones changed the most in percent female, and in which decade did each cross 50%?

**How:** For each name and decade, compute F ÷ (F + M); compare the 1950s with the 2010s; find the decade where it crosses 0.5.
>>>markdown
### Biggest change in percent female, 1950s → 2010s
>>>malloy
run: names -> {
  where: year_born >= 1950 and year_born < 2020
  group_by: name, decade
  aggregate:
    total_births
    pct_female is coalesce(total_births { where: sex = 'F' }, 0) / total_births
} -> {
  group_by: name
  aggregate:
    decades_with_1000 is count() { where: total_births >= 1000 }
    pct_female_1950s is pct_female.max() { where: decade = 1950 }
    pct_female_2010s is pct_female.max() { where: decade = 2010 }
    first_majority_female is decade.min() { where: pct_female > 0.5 }
    first_majority_male is decade.min() { where: pct_female < 0.5 }
} -> {
  where: decades_with_1000 = 7
  select:
    name, pct_female_1950s, pct_female_2010s
    change is pct_female_2010s - pct_female_1950s
    size_of_change is abs(pct_female_2010s - pct_female_1950s)
    crossed_50_in is
      pick first_majority_female when pct_female_1950s < 0.5 and pct_female_2010s > 0.5
      pick first_majority_male when pct_female_1950s > 0.5 and pct_female_2010s < 0.5
      else null
  order_by: size_of_change desc
  limit: 15
}
>>>markdown
### Worksheet example: names that went from >80% one sex to >80% the other
>>>malloy
run: names -> {
  group_by: name, decade
  aggregate:
    total_births
    pct_female is coalesce(total_births { where: sex = 'F' }, 0) / total_births
} -> {
  where: total_births >= 500
  group_by: name
  aggregate:
    first_mostly_boys is decade.min() { where: pct_female < 0.2 }
    last_mostly_boys is decade.max() { where: pct_female < 0.2 }
    first_mostly_girls is decade.min() { where: pct_female > 0.8 }
    last_mostly_girls is decade.max() { where: pct_female > 0.8 }
    first_majority_girls is decade.min() { where: pct_female > 0.5 }
    first_majority_boys is decade.min() { where: pct_female < 0.5 }
} -> {
  where: (first_mostly_boys < last_mostly_girls and first_mostly_boys < first_mostly_girls)
      or (first_mostly_girls < last_mostly_boys and first_mostly_girls < first_mostly_boys)
  select:
    name
    direction is pick 'Boys → girls' when first_mostly_boys < first_mostly_girls else 'Girls → boys'
    crossed_50_in is pick first_majority_girls when first_mostly_boys < first_mostly_girls else first_majority_boys
  order_by: direction, crossed_50_in
}
>>>markdown
**Answer.** **Aubrey** changed the most: 0% female in the 1950s, 98% in the 2010s. Of the top 15, 8 crossed 50%: Aubrey (1970s), Stevie (1980s), Blair (1980s), Morgan (1980s), Kendall (1990s), Jean (1990s), Joan (2000s), Sidney (1990s). Jean and Joan are the two that crossed toward boys.

      **Worksheet example:** counting decades with at least 500 babies, **30** names went from more than 80% boys to more than 80% girls, but only **9** went the other way. The boys → girls list includes Shelby, Dee, Rosario, Gale, Sydney, Lynn, Kelly, Leslie, Aubrey, Lacy, Lindsey, Morgan and more.

      Some of the key's examples don't appear. Ashley, Madison and Shannon were never given to 500 boys in a single decade, so they never count as “mostly boys” under this threshold, even though they switched. Changing the threshold changes the list.
>>>markdown
## 3.4 Names and places

**Seed:** "regional names"

**Model question:** For each state in 2010, which girls' name had the highest ratio of state share to national share, among names given to at least 100 girls in that state?

**How:** Compute each name's share in the state and nationally; divide; keep the top name per state. The minimum-count filter is essential.
>>>markdown
### Each state's signature girls' name, 2010
>>>malloy
run: names -> {
  where: year_born = 2010 and sex = 'F'
  group_by: state, name
  aggregate:
    total_births
    state_share is total_births / all(total_births, state)
    national_share is all(total_births, name) / all(total_births)
} -> {
  where: total_births >= 100
  group_by: state, name, total_births
  aggregate: over_index is max(state_share / national_share)
  calculate: state_rank is rank() { partition_by: state  order_by: over_index desc }
} -> {
  where: state_rank = 1
  select: state, name, total_births, over_index
  order_by: over_index desc
}
>>>markdown
**Answer.** The most extreme is **Gitty in NY**, given 14.4× as often as nationally. It's a Yiddish name used in Orthodox Jewish communities. Next come Ansley (GA, 9.7×) and Renata (TX, 4.9×).

      The signature names reflect language and culture, as the key predicts. Spanish-language names appear in the Southwest and Florida: Renata (TX), Valentina (FL), Camila (AZ), Isabella (NM). Traditional names appear in the South: Mary (AL), Mary (TN), Caroline (SC), Caroline (NC).

      **The threshold matters:** only **39** states have an answer. In the other 12 (mostly small states), no girls' name reached 100 births in 2010. Lower the threshold and tiny names take over the list.
>>>markdown
## 3.5 Boys vs girls over time

**Seed:** "do boys' names change slower"

**Model question:** For each decade, how many distinct girls' names versus boys' names were needed to cover 50% of babies?

**How:** Rank names within decade × sex; take a running total; count the names until it crosses 50%.
>>>markdown
### Names needed to cover half of babies, by decade
>>>malloy
run: names -> {
  group_by: decade, sex, name
  aggregate: total_births
  calculate:
    running_total is sum_cumulative(total_births) { partition_by: decade, sex  order_by: total_births desc }
    decade_total is sum_window(total_births) { partition_by: decade, sex }
} -> {
  where: running_total - total_births < decade_total / 2
  group_by: decade, sex
  aggregate: names_to_cover_half is count()
  order_by: decade, sex
}
>>>markdown
**Answer.** **Girls' names have been more diverse in every decade.** In the 1950s, 46 girls' names covered half of all girls, against 24 boys' names. By the 2010s it took 155 and 101.

      **Has the gap narrowed?** It depends how you measure it. As a ratio, it has: girls needed 1.9× as many names in the 1950s and 1.5× in the 2010s. As a difference, it grew from 22 to 54 names. Saying which you used is part of a full question. (The 2020s cover only 2020–2021.)

      The worksheet's version (share of babies with a top-10 name) is answered by the first chart in 3.1.
>>>markdown
## 3.6 States over time

**Seed:** "national monoculture"

**Model question:** For each year, how many distinct names were #1 for girls across all states? Has that number gone up or down since 1930?

**How:** Rank within state × year × sex; keep rank 1; count distinct names per year.
>>>markdown
### Different #1 names across the states, each year
>>>malloy
run: names -> {
  group_by: year_born, sex, state, name
  aggregate: total_births
  calculate: state_rank is rank() { partition_by: year_born, sex, state  order_by: total_births desc }
} -> {
  where: state_rank = 1
  group_by: year_born, sex
  aggregate: different_no1_names is count(name)
  nest: most_common_no1 is {
    group_by: name
    aggregate: states is count(state)
    limit: 1
  }
  order_by: year_born, sex
}
>>>markdown
**Answer.** **Since 1930, the number has gone up.** In 1930, just 3 girls' names were #1 anywhere, and **Mary** was #1 in 40 states. In 2020, 6 names were #1 somewhere, and the most common, **Olivia**, led in 32 states. The peak for girls was 9 names in 1997.

      So by this measure, states are **less** alike at the top than in 1930. They're still very similar overall, though: see 4.6, where every state's profile is at least 0.78 similar to Washington's. As the key says, a similarity measure is a stronger test than #1 names alone.
>>>markdown
## 3.7 The name itself as data

**Seed:** "vowel endings for girls"

**Model question:** What share of boys born each decade had a name ending in 'n', and how has it changed since 1910?

**How:** Derive the last letter with substr(); sum the births with that ending; divide by all boys.
>>>markdown
### Boys' names ending in “n” and girls' names ending in “a”
>>>malloy
run: names -> {
  group_by: decade
  aggregate:
    boys_ending_n is total_births { where: sex = 'M' and last_letter = 'n' } / total_births { where: sex = 'M' }
    girls_ending_a is total_births { where: sex = 'F' and last_letter = 'a' } / total_births { where: sex = 'F' }
  calculate: n_change_from_last_decade is boys_ending_n - lag(boys_ending_n)
  order_by: decade
}
>>>markdown
**Answer.** **Boys' names ending in “n” went from 13% of boys in the 1940s to 35% in the 2010s**, more than one boy in three. It grew fastest in the **2000s** (+6.2 points) and the **1970s** (+6.2 points). The 2020s (31%, only two years of data) hint that it may have peaked.

      Girls' names ending in “a” rose steadily too, from 25% in the 1910s to 39% in the 2020s.
>>>markdown
## 4.1 Cohorts

**Seed:** "do newer names burn out faster"

**Model question:** For names that first entered the top 100 in the 1950s versus the 1980s, how long did each stay in the top 100?

**How:** Rank within year × sex; find each name's first year in the top 100 (its cohort); count its years in the top 100. Watch for censoring: count only the first 20 years after entry so both cohorts get the same amount of time.
>>>markdown
### Years in the top 100, by entry cohort
>>>malloy
run: names -> {
  group_by: sex, year_born, name
  aggregate: total_births
  calculate: name_rank is rank() { partition_by: sex, year_born  order_by: total_births desc }
} -> {
  where: name_rank <= 100
  group_by: sex, name, year_born
  calculate: entry_year is min_window(year_born) { partition_by: sex, name }
} -> {
  group_by: sex, name, entry_year
  aggregate:
    years_in_top100_first_20 is count() { where: year_born < entry_year + 20 }
    years_in_top100_ever is count()
} -> {
  where: entry_year >= 1950 and entry_year < 1960 or entry_year >= 1980 and entry_year < 1990
  group_by:
    cohort is pick '1950s' when entry_year < 1960 else '1980s'
    sex
  aggregate:
    names_entering is count()
    avg_years_first_20 is years_in_top100_first_20.avg()
    avg_years_ever is years_in_top100_ever.avg()
    pct_10_plus_years is count() { where: years_in_top100_first_20 >= 10 } / count()
  order_by: sex, cohort
}
>>>markdown
**Answer.** **For girls, yes: the 1980s cohort faded much faster.** 31 girls' names entered the top 100 in the 1950s and spent an average of 18.2 of their first 20 years there. The 47 that entered in the 1980s averaged only 11.1. 97% of the 1950s names lasted at least 10 years, against 55% of the 1980s names.

      **For boys, it's the opposite:** the 1980s names lasted longer (17.4 years vs. 14.0). Fewer boys' names enter the top 100 at all (17 and 20), which fits the slower turnover in 3.5 and 4.3.

      **Censoring:** the “ever” column gives the 1950s names 30 more years to rack up time, which inflates the gap. Counting only the first 20 years makes the comparison fair.
>>>markdown
## 4.2 Survival

**Seed:** "half-life of a popular name"

**Model question:** Of girls' names that entered the top 10, what fraction were still in the top 10 after 5, 10 and 20 years, and does this differ for names entering before vs. after 1980?

**How:** Find each name's first top-10 year; count how many are in the top 10 k years later; divide by the number that entered. Censoring: only names that entered by 2001 have 20 years of data, so the later group stops at 2001.
>>>markdown
### Survival curve: share of entering names still in the top 10
>>>malloy
run: names -> {
  where: sex = 'F'
  group_by: year_born, name
  aggregate: total_births
  calculate: name_rank is rank() { partition_by: year_born  order_by: total_births desc }
} -> {
  where: name_rank <= 10
  group_by: name, year_born
  calculate: entry_year is min_window(year_born) { partition_by: name }
} -> {
  where: entry_year > 1910 and entry_year <= 2001 and year_born <= entry_year + 20
  group_by:
    era is pick 'Entered 1911–1979' when entry_year < 1980 else 'Entered 1980–2001'
    years_after_entry is year_born - entry_year
  aggregate: names_in_top10 is count()
  calculate: names_entering is first_value(names_in_top10) { partition_by: era  order_by: years_after_entry }
  order_by: era, years_after_entry
} -> {
  select: era, years_after_entry, names_in_top10, names_entering
    still_in_top10 is names_in_top10 / names_entering
  order_by: era, years_after_entry
}
>>>markdown
**Answer.** 44 girls' names first entered the top 10 between 1911 and 1979, and 14 between 1980 and 2001.

      after 5 yearsafter 10 yearsafter 20 years

      Entered 1911–197970%61%23%

      Entered 1980–200171%50%21%

      **The two curves are close.** About 7 in 10 names are still there after 5 years in both eras. The later names drop off faster in years 8–12, but both groups end up with about 1 in 5 still in the top 10 after 20 years. With only 14 names in the later group, a difference of one or two names moves the curve a lot, so this is weak evidence either way.

      “Still in the top 10” here means in the top 10 that year, not continuously; a few names drop out and come back, which is why the curve wiggles up in places.
>>>markdown
## 4.3 Rank trajectories

**Seed:** "leaderboard churn"

**Model question:** For each year since 1920, how many boys' top-10 names were new compared with the previous year?

**How:** Rank within year × sex; keep the top 10; use lag() to find each name's previous top-10 year; a name is new if that wasn't last year.
>>>markdown
### New names in the top 10, each year
>>>malloy
run: names -> {
  group_by: sex, year_born, name
  aggregate: total_births
  calculate: name_rank is rank() { partition_by: sex, year_born  order_by: total_births desc }
} -> {
  where: name_rank <= 10
  group_by: sex, name, year_born
  calculate: previous_top10_year is lag(year_born) { partition_by: sex, name  order_by: year_born }
} -> {
  where: year_born >= 1920
  group_by: sex, year_born
  aggregate: new_to_top10 is count() { where: previous_top10_year is null or previous_top10_year != year_born - 1 }
  order_by: sex, year_born
}
>>>markdown
### Average new top-10 names per year, by decade
>>>malloy
run: names -> {
  group_by: sex, year_born, name
  aggregate: total_births
  calculate: name_rank is rank() { partition_by: sex, year_born  order_by: total_births desc }
} -> {
  where: name_rank <= 10
  group_by: sex, name, year_born
  calculate: previous_top10_year is lag(year_born) { partition_by: sex, name  order_by: year_born }
} -> {
  where: year_born >= 1920
  group_by: sex, year_born
  aggregate: new_to_top10 is count() { where: previous_top10_year is null or previous_top10_year != year_born - 1 }
} -> {
  group_by: decade is floor(year_born / 10) * 10
  aggregate:
    boys_new_per_year is new_to_top10.avg() { where: sex = 'M' }
    girls_new_per_year is new_to_top10.avg() { where: sex = 'F' }
  order_by: decade
}
>>>markdown
**Answer.** **Yes, the boys' top 10 is more volatile now.** In the 1950s, an average of 0.3 new names entered the boys' top 10 each year, and 8 of those 10 years had no newcomers at all. In the 2010s the average was 1.2 new names per year.

      Girls' top 10 turned over faster than boys' for most of the century, peaking at 1.2 per year in the 1970s. Since the 1990s the boys have caught up, and in the 2010s they turned over faster than the girls (0.6). The 2020s cover only two years.
>>>markdown
## 4.4 Name lifecycles (process mining)

**Seed:** "rise and fall paths"

**Model question:** Assigning each name a stage per decade (Absent, Rare, Top 1000, Top 100, Top 10), what are the most common stage sequences for girls' names that ever reached the top 10?

**How:** Rank within decade; map rank to a stage; keep a decade only when the stage changes (lag); join the stages in order into one string (the variant); group by it.
>>>markdown
### Most common lifecycle variants (girls' names that reached the top 10)
>>>malloy
run: names -> {
  where: sex = 'F' and year_born < 2020
  group_by: decade, name
  aggregate: total_births
  calculate: decade_rank is rank() { partition_by: decade  order_by: total_births desc }
} -> {
  group_by:
    name, decade, decade_rank
    stage is decade_rank ?
      pick 'Top 10' when <= 10
      pick 'Top 100' when <= 100
      pick 'Top 1000' when <= 1000
      else 'Rare'
  calculate:
    previous_stage is lag(stage) { partition_by: name  order_by: decade }
    best_rank_ever is min_window(decade_rank) { partition_by: name }
} -> {
  where: best_rank_ever <= 10 and (previous_stage is null or previous_stage != stage)
  group_by: name
  aggregate:
    first_decade is decade.min()
    path is string_agg(stage, ' → ') { order_by: decade }
} -> {
  group_by: variant is concat(pick 'Absent → ' when first_decade > 1910 else '', path)
  aggregate:
    names is count()
    examples is string_agg(name, ', ') { order_by: name }
  order_by: names desc
  limit: 8
}
>>>markdown
### Transitions between stages (the directly-follows graph)
>>>malloy
run: names -> {
  where: sex = 'F' and year_born < 2020
  group_by: decade, name
  aggregate: total_births
  calculate: decade_rank is rank() { partition_by: decade  order_by: total_births desc }
} -> {
  group_by:
    name, decade, decade_rank
    stage is decade_rank ?
      pick 'Top 10' when <= 10
      pick 'Top 100' when <= 100
      pick 'Top 1000' when <= 1000
      else 'Rare'
  calculate:
    previous_stage is lag(stage) { partition_by: name  order_by: decade }
    best_rank_ever is min_window(decade_rank) { partition_by: name }
} -> {
  where: best_rank_ever <= 10 and previous_stage is not null and previous_stage != stage
  group_by: from_stage is previous_stage, to_stage is stage
  aggregate: transitions is count()
  order_by: transitions desc
}
>>>markdown
### Comebacks: names that entered the top 10 twice
>>>malloy
run: names -> {
  where: sex = 'F' and year_born < 2020
  group_by: decade, name
  aggregate: total_births
  calculate: decade_rank is rank() { partition_by: decade  order_by: total_births desc }
} -> {
  group_by:
    name, decade, decade_rank
    stage is decade_rank ?
      pick 'Top 10' when <= 10
      pick 'Top 100' when <= 100
      pick 'Top 1000' when <= 1000
      else 'Rare'
  calculate:
    previous_stage is lag(stage) { partition_by: name  order_by: decade }
    best_rank_ever is min_window(decade_rank) { partition_by: name }
} -> {
  where: best_rank_ever <= 10 and (previous_stage is null or previous_stage != stage)
  group_by: name
  aggregate:
    times_entering_top10 is count() { where: stage = 'Top 10' }
    path is string_agg(stage, ' → ') { order_by: decade }
} -> {
  where: times_entering_top10 >= 2
  select: name, times_entering_top10, path
}
>>>markdown
**Answer.** The most common path, shared by 11 names (Amanda, Amy, Angela, Cynthia, Deborah, Judith, Linda, Nancy, Patricia, Sandra, Stephanie), is **Top 1000 → Top 100 → Top 10 → Top 100 → Top 1000**: a gradual climb and a gradual fall. Next is **Top 10 → Top 100 → Top 1000** (Dorothy, Frances, Helen, Mary, Ruth, Virginia), names that were already at the top in the 1910s.

      **Most names climb gradually.** Of 48 entries into the top 10, 40 came from the top 100. Only 8 jumped straight from the top 1000 in a single decade, for example Ashley and Taylor (*Absent → Rare → Top 1000 → Top 10 → Top 100*).

      **Comebacks are rare.** Only **1** girls' name entered the top 10 in two separate runs by decade: Elizabeth (Top 10 → Top 100 → Top 10 → Top 100). Nobody made the worksheet's full Rare → Top 10 → Rare → Top 10 trip. Some names did come back to the top 100, though (Anna, Margaret).

      Stages are by decade (1910s–2010s); “Rare” means in the data but ranked below 1000.
>>>markdown
## 4.5 Before / after an event

**Seed:** "hurricane names"

**Model question:** For girls' names shared with major Atlantic hurricanes since 1980, how did the name's share change in the three years after the storm compared with the three years before?

**How:** Build a small events table (name, year) yourself; join it to the names; compare average share before vs. after. Compare with the trend before the storm too.
>>>markdown
### Share before and after each storm (per million girls born)
>>>malloy
run: hurricane_names -> {
  where: storm.storm_name is not null
  group_by: name, storm_year is storm.storm_year, year_born
  aggregate: per_million_girls is total_births / max(girls.girls_born) * 1000000
} -> {
  group_by: name, storm_year
  aggregate:
    three_years_before is per_million_girls.avg() { where: year_born >= storm_year - 3 and year_born <= storm_year - 1 }
    three_years_after is per_million_girls.avg() { where: year_born >= storm_year + 1 and year_born <= storm_year + 3 }
    years_4_to_6_before is per_million_girls.avg() { where: year_born >= storm_year - 6 and year_born <= storm_year - 4 }
} -> {
  where: three_years_before is not null
  select:
    name, storm_year, three_years_before, three_years_after
    change_after_storm is three_years_after / three_years_before - 1
    change_before_storm is three_years_before / years_4_to_6_before - 1
  order_by: change_after_storm
}
>>>markdown
### Katrina, year by year
>>>malloy
run: hurricane_names -> {
  where: name = 'Katrina' and year_born >= 1990
  group_by: year_born
  aggregate: per_million_girls is total_births / max(girls.girls_born) * 1000000
  order_by: year_born
}
>>>markdown
**Answer.** **Katrina is the clearest case.** After the 2005 storm, the name's share fell 52% (three years after vs. three years before). It was already declining, but only by 13% over the previous three years. The chart shows the break: Katrina fell by more than a third the very next year.

      Across all 25 storms with data, 17 names fell after the storm, but only 8 fell faster than they had been falling before. Roxanne, Sandy and Erika fell after their storms, but by about as much as before. That's the **pre-trend** the key warns about. Some names rose after their storm (Lili, Opal, Florence), and Florence was already rising fast.

      Fran and Hortense don't appear: neither name was given to 5 girls in any state in those years. The hurricane list (retired Atlantic storm names, 1983–2018) isn't in the dataset. It's in `data/hurricanes.csv`, joined in the model, and you should check it against NOAA's list before teaching from it.
>>>markdown
## 4.6 Similarity between states

**Seed:** "twin states"

**Model question:** Using each state's 2020 girls' name shares as a profile, which state is most similar to Washington by cosine similarity, and which is least similar?

**How:** Build a state × name profile (top 500 national names); scale each state's vector to length 1; cosine similarity = sum of products over shared names. In Malloy that's a self-join on name (see state_similarity in the model).
>>>markdown
### Cosine similarity of each state's 2020 girls' names to Washington's
>>>malloy
run: state_similarity -> {
  where: other_state = 'WA'
  group_by: state, region is r1.region
  aggregate: similarity_to_wa is cosine.max()
  order_by: similarity_to_wa desc
}
>>>markdown
**Answer.** **Most similar to Washington: CO (0.983) and OR (0.978)**, then IL and KS. **Least similar: WY (0.778)**, VT and MS.

      Two cautions make this a good discussion:

      - **Every state is very similar** (all above 0.78), because the same popular names dominate everywhere. The interesting part is the ranking, not the size.

      - **Small states look unusual partly because of suppression.** Wyoming, Vermont, the Dakotas and DC have few names with 5+ babies, so their profiles are missing many names. Mississippi isn't small, so its low score is more likely a real difference in how families there name their daughters.
>>>markdown
## 4.7 Networks and clusters

**Seed:** "map of naming culture"

**Model question:** If each state is connected to its three most similar states by 2020 girls' name profile, how many clusters form, and do they match the Census regions?

**How:** From the similarity matrix in 4.6, keep each state's top 3 links; draw the graph; compare the clusters with the Census regions.
>>>markdown
### Each state's three most similar states
>>>malloy
run: state_similarity -> {
  group_by: state, other_state, region is r1.region, other_region is r2.region
  aggregate: cosine is cosine.max()
  calculate: neighbor_rank is rank() { partition_by: state  order_by: cosine desc }
} -> {
  where: neighbor_rank <= 3
  select: state, region, other_state, other_region, cosine, neighbor_rank
    same_region is region = other_region
  order_by: state, neighbor_rank
}
>>>markdown
### How many of those links stay inside the state's Census region?
>>>malloy
run: state_similarity -> {
  group_by: state, other_state, region is r1.region, other_region is r2.region
  aggregate: cosine is cosine.max()
  calculate: neighbor_rank is rank() { partition_by: state  order_by: cosine desc }
} -> {
  where: neighbor_rank <= 3
  group_by: region
  aggregate:
    neighbor_links is count()
    pct_same_region is count() { where: region = other_region } / count()
  order_by: pct_same_region desc
}
>>>markdown
**Answer.** **Mostly, but not exactly.** Of 153 top-3 links, 106 (69%) connect states in the same Census region. The Midwest is the most self-contained (89% of its links stay inside it); the Northeast is the least (56%).

      **Clusters:** keeping only *mutual* links (both states list each other in their top 3) gives **6** groups of two or more states; the other 21 states have no mutual link. The groups:

      - GA, IN, MD, MI, MO, NC, OH, PA, SC, VA
- AZ, CA, CT, FL, MA, NJ, NV, NY, TX
- CO, IL, OR, WA
- IA, MN, WI
- ID, UT
- KY, TN

      The mismatches are the interesting part, as the key predicts. The second group spans three regions: large, diverse states from the West (AZ, CA, NV), the South (TX, FL) and the Northeast (NY, NJ, MA, CT) name their daughters alike. Colorado pairs with Illinois, Idaho with Iowa and Kansas, and Alaska's top three include Indiana. *(Grouping the mutual links is the one step done outside Malloy, in the build script's JavaScript.)*
>>>markdown
## 4.8 Hierarchy: states inside regions

**Seed:** "national trend driven by one region"

**Model question:** Between 2000 and 2015, the name Wyatt rose nationally. What share of the increase in Wyatt births came from each of the four Census regions?

**How:** Join states to a region lookup; compute Wyatt births per region in 2000 and 2015; each region's change ÷ the total change is its contribution.
>>>markdown
### Wyatt (boys), 2000 vs. 2015, by Census region
>>>malloy
run: names -> {
  where: name = 'Wyatt' and sex = 'M'
  group_by: region is regions.region
  aggregate:
    births_2000 is total_births { where: year_born = 2000 }
    births_2015 is total_births { where: year_born = 2015 }
} -> {
  select:
    region, births_2000, births_2015
    increase is births_2015 - births_2000
    growth is births_2015 / births_2000
  calculate: share_of_national_increase is increase / sum_window(increase)
  order_by: increase desc
}
>>>markdown
**Answer.** Wyatt births went from 3,407 in 2000 to 9,650 in 2015. **The South contributed 45% of the increase**, the Midwest 23%, the West 19% and the Northeast 13%.

      So it's national (Wyatt more than doubled in every region), but the South drove almost half of it, partly because the South has the most births. The growth column shows the rise was fastest in the South (4.0×) and, from a small base, the Northeast (3.6×).
>>>markdown
## 6 Reflection

Model answers for the three reflection questions. None needs a query, but each points back to the results above.
