{"id":2747,"date":"2026-02-05T07:00:45","date_gmt":"2026-02-05T13:00:45","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=2747"},"modified":"2026-02-06T06:59:47","modified_gmt":"2026-02-06T12:59:47","slug":"the-aging-us-population","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2026\/02\/the-aging-us-population\/","title":{"rendered":"The aging US population"},"content":{"rendered":"<p>It&#8217;s difficult to overstate how the United States has aged in recent decades. We have fewer kids, longer lifespans, and better medical care than ever.<\/p>\n<p>Those things aren&#8217;t bad per se, but they do present challenges for a program like Social Security, which relies on younger people paying into the system while older people draw benefits. It becomes increasingly difficult to stay afloat as the balance tips from young to old.<\/p>\n<p>Our society&#8217;s composition is dramatically different than when FDR signed the program into law in 1935. We should think about how to account for these changes when planning for the future.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/our-world-in-data-1.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2761 size-medium\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/our-world-in-data-1-300x268.jpg\" alt=\"\" width=\"300\" height=\"268\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/our-world-in-data-1-300x268.jpg 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/our-world-in-data-1-768x686.jpg 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/our-world-in-data-1.jpg 800w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<hr \/>\n<h4>1. The code.<\/h4>\n<p>First, let&#8217;s take a look at census data and get a feel for the American population.<\/p>\n<p>Pulling 125-year-old data from the US Census Bureau is its own challenge. I&#8217;ve gone ahead and combined all the annual datasets into one standardized CSV file. If you&#8217;d like to play around with the data yourself, check out the Census Bureau&#8217;s data archive <a href=\"https:\/\/www2.census.gov\/programs-surveys\/popest\/datasets\/\" target=\"_blank\" rel=\"noopener\">here<\/a>.<\/p>\n<p>The plan is to create an <em>age pyramid<\/em> of the US population. This type of plot presents male and female age distributions as a double-sided horizontal bar chart. Because there are generally more young people than old, bars roughly form a pyramid shape. Hence the name.<\/p>\n<figure id=\"attachment_2752\" aria-describedby=\"caption-attachment-2752\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2752 size-medium\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census-300x155.jpg\" alt=\"\" width=\"300\" height=\"155\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census-300x155.jpg 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census-1024x530.jpg 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census-768x398.jpg 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/population_pyramid_1900-census.jpg 1280w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption id=\"caption-attachment-2752\" class=\"wp-caption-text\">An age pyramid visualization that appeared in the 1900 US Census report.<\/figcaption><\/figure>\n<p>For this post we&#8217;ll add a time dimension by turning the pyramid into an animated GIF. Every year from 1900 to 2019 will be its own frame (it&#8217;s better to stop short of 2020 because COVID-19 threw a wrench into data collection). The output will visualize age distribution <strong>and<\/strong> how it&#8217;s changed over time.<\/p>\n<p>Begin by reading the CSV into a pandas DataFrame. Name it <code>df_all<\/code> because we&#8217;ll copy and filter it many times.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\n\r\ndf_all = pd.read_csv(\"census_age_1900-2019.csv\")<\/pre>\n<p><code>df.head()<\/code> looks like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">   year    age     male   female     both     total\r\n0  1900    0-4  4639000  4542000  9181000  76094000\r\n1  1900    5-9  4483000  4397000  8880000  76094000\r\n2  1900  10-14  4086000  4000000  8086000  76094000\r\n3  1900  15-19  3757000  3811000  7568000  76094000\r\n4  1900  20-24  3653000  3730000  7383000  76094000<\/pre>\n<p>You can see that I&#8217;ve grouped ages into five-year bins. We have coverage for ages up to 70-74 for all census years. Beginning in 1980, we have 75-79 and 80-84 bins as well. It&#8217;s not a problem. Those bars will pop up about halfway through the animation.<\/p>\n<p>I&#8217;ll use a custom Matplotlib style that will be linked at the bottom of this post.<\/p>\n<p>The bulk of the code will be nested inside a large <code>for<\/code> loop. Each iteration of the loop will process one year, or one frame of the GIF.<\/p>\n<p>We&#8217;ll use the DataFrame&#8217;s index as a vertical axis. Reset the index so that every frame is identical.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\n\r\nplt.style.use(\"wollen_age_pyramid.mplstyle\")\r\n\r\nfor year in range(1900, 2020):\r\n    df = df_all.copy()\r\n\r\n    df = df[df['year'] == year].reset_index(drop=True)\r\n\r\n    [...]<\/pre>\n<p>Our age pyramid will be slightly different than the figure above. It will convert age group counts to percentages of the total population. Since the overall US population has quadrupled since 1900, it&#8217;s a good idea to normalize the age distribution, i.e. make the bars add up to 100%.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n    \r\n    [...]\r\n\r\n    df['male_pct'] = df['male'] \/ df['total'] * 100\r\n\r\n    df['female_pct'] = df['female'] \/ df['total'] * 100\r\n\r\n    [...]<\/pre>\n<p>I&#8217;d like the animation to have a &#8220;progress bar&#8221; near the top of the figure. Rather than printing a year on every frame, which would be difficult to read, we can have a bar that goes from 1900 to 2019.<\/p>\n<p>Create two Axes in a vertical (2, 1) layout. <code>height_ratios<\/code> makes for easy scaling. The upper progress bar Axes will be 1\/30<sup>th<\/sup> the size of the lower pyramid Axes.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    fig, (ax0, ax1) = plt.subplots(2, 1, height_ratios=(1, 30))\r\n\r\n    [...]<\/pre>\n<p>We can use Matplotlib&#8217;s horizontal bar chart method, <code>barh<\/code>.<\/p>\n<p>Male bars extend leftward so we have to multiply them by -1. The vertical axis is categorical with age ranges. Pass <code>df.index<\/code> and later we&#8217;ll replace vertical ticks with custom strings.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    ax1.barh(df.index, df['male_pct'] * -1)\r\n\r\n    ax1.barh(df.index, df['female_pct'])\r\n\r\n    [...]<\/pre>\n<p>Let&#8217;s also <code>plot<\/code> a dark gray vertical line at 0 to serve as a boundary between male and female bars.<\/p>\n<p>Set ticks and window limits. The vertical axis, <code>df.index<\/code>, ranges from 0 to 16. Set integer ticks and then replace their labels with custom strings, e.g. &#8220;25-29&#8221;.<\/p>\n<p><code>barh<\/code> is a little odd because it accepts the vertical axis first, but Matplotlib still follows the normal convention of horizontal x-ticks and vertical y-ticks.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    ax1.plot([0, 0], [-100, 100], color=\"#555\")\r\n\r\n    ax1.set(xticks=range(-7, 8),\r\n            xticklabels=[f\"{abs(n)}%\" if n != 0 else \"\u2014\" for n in range(-7, 8)],\r\n            xlim=(-7.5, 7.5),\r\n            yticks=range(17),\r\n            yticklabels=[f\"{n}-{n + 4}\" for n in range(0, 85, 5)],\r\n            ylim=(-0.8, 17))\r\n\r\n    [...]<\/pre>\n<p>Use <code>text<\/code> to label &#8220;Male&#8221; and &#8220;Female&#8221; near the top of the window. A <code>bbox<\/code> dictionary creates background for text. These background colors match the bars themselves.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    ax1.text(x=-0.1,\r\n             y=16.8,\r\n             s=\"Male\",\r\n             ha=\"right\",\r\n             va=\"center\",\r\n             bbox={\"boxstyle\": \"Round\", \"facecolor\": \"#B5D63D\", \"edgecolor\": \"None\", \"alpha\": 0.35, \"pad\": 0.1})\r\n\r\n    ax1.text(x=0.1,\r\n             y=16.8,\r\n             s=\"Female\",\r\n             ha=\"left\",\r\n             va=\"center\",\r\n             bbox={\"boxstyle\": \"Round\", \"facecolor\": \"#7A30AB\", \"edgecolor\": \"None\", \"alpha\": 0.35, \"pad\": 0.1})\r\n\r\n    [...]<\/pre>\n<p>The progress bar on <code>ax0<\/code> is simpler than it sounds. Just <code>plot<\/code> a red line spanning 1900 to the current year. It will appear to travel from left to right as the animation plays.<\/p>\n<p>Specify ticks and window limits to scale the line appropriately.<\/p>\n<p>We can&#8217;t use <code>ax0.set_axis_off()<\/code> because it would hide x-ticks, but we can make grid lines invisible by setting <code>grid<\/code> to False.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    ax0.plot([1900, year], [0, 0], color=\"#D00\", marker=\"o\", markersize=4)\r\n\r\n    ax0.set(xticks=range(1900, 2030, 10),\r\n            xlim=(1899, 2023),\r\n            yticks=[],\r\n            ylim=(-10, 10),\r\n            title=\"US Age Pyramid  \u2022  1900\u20132019\")\r\n\r\n    ax0.grid(False)\r\n\r\n    [...]<\/pre>\n<p>Finally, save the figure and <code>close<\/code> it, which prevents Matplotlib from holding every frame in memory.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for year in range(1900, 2020):\r\n\r\n    [...]\r\n\r\n    plt.savefig(f\"frames\/{year}.png\")\r\n\r\n    plt.close()<\/pre>\n<p>The script will generate 120 frames in a &#8220;frames&#8221; folder. You can then use a program like <a href=\"https:\/\/www.gimp.org\/\" target=\"_blank\" rel=\"noopener\">GIMP<\/a> to create an animated GIF.<\/p>\n<hr \/>\n<h4>2. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/age_composition_1900-2019.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2754\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/03\/age_composition_1900-2019.gif\" alt=\"\" width=\"1000\" height=\"1000\" \/><\/a><\/p>\n<p>We&#8217;ve gone from an age pyramid to an age cylinder! In 1900 you were 6x more likely to run into a 5-year-old than a 65-year-old. Today, the chances are roughly equal.<\/p>\n<p>It&#8217;s important to remember that these are age <em>distributions<\/em>. We have percentages on the x-axis, not absolute numbers of people. The country today has a greater number of kids but they&#8217;re a smaller share of the overall population.<\/p>\n<p>You can see the post-WWII baby boom ripple up the pyramid. But when that generation grew up, they had fewer kids of their own.<\/p>\n<p>Shifting demographics have lurked behind countless 20th century social changes\u2014many of which we hardly notice\u2014from positions of leadership to family dynamics to pop culture. But I especially want to highlight the trouble for social insurance programs. Working-age people who pay into the system are a smaller slice of the population, while the share of retired people is proportionally larger. Even without expanding benefits you would expect Social Security and Medicare obligations to grow. And these programs account for more than one-third of the overall federal budget (!).<\/p>\n<p>Low birth rates are common issue <a href=\"https:\/\/www.unfpa.org\/swp2023\/too-few\" target=\"_blank\" rel=\"noopener\">across the developed world<\/a>. In fact many European and East Asian countries are struggling even more than the United States. That&#8217;s not surprising as <a href=\"https:\/\/budgetmodel.wharton.upenn.edu\/issues\/2022\/7\/8\/decline-in-fertility-the-role-of-marriage-and-education\" target=\"_blank\" rel=\"noopener\">it&#8217;s natural<\/a> for birth rates to fall as education and income levels rise. Higher incomes raise the opportunity cost of having children.<\/p>\n<p>It&#8217;s unlikely that we&#8217;ll return to having as many kids as our great-grandparents did, but we <em>can<\/em> craft forward-looking policy to address modern problems. Of course I don&#8217;t want to get rid of old people, so to improve the demographic balance we&#8217;ll need <em>more<\/em> young people.<\/p>\n<p>How can we work toward that goal? We can start with two obvious measures: (1) using incentives to nudge people toward children, and (2) sharply increasing high-skill immigration.<\/p>\n<p>Raising kids is expensive! Subsidies like the Child Tax Credit (CTC) make life a little easier for parents who are considering another child. And in this case there are obvious\u2014arguably more important\u2014social benefits. When we expanded the CTC in 2021 as part of the American Rescue Plan we saw a <a href=\"https:\/\/southdakotasearchlight.com\/2022\/11\/15\/studies-show-gains-against-childhood-hunger-were-lost-after-child-tax-credit-ended\/\" target=\"_blank\" rel=\"noopener\">significant reduction in childhood hunger<\/a>.<\/p>\n<p>Expanding immigration is another way to grow the base of the pyramid. We forego <a href=\"https:\/\/eig.org\/exceptional-by-design\/\" target=\"_blank\" rel=\"noopener\">enormous economic potential<\/a> by overly restricting high-skilled individuals from working here. Immigrants don&#8217;t only grow the economy. They pay taxes as well.<\/p>\n<p>Addressing demographic challenges will require long-term strategic thinking from the federal government, which unfortunately is in short supply these days. You might feel that pro-natal incentives are expensive, or that they don&#8217;t apply to you, or that immigrants undermine some ahistorical idea of American culture. But a stagnating American economy with a shrinking tax base hurts everyone, including non-Americans.<\/p>\n<p>The first step is to understand. I hope this post can demonstrate the scale of the challenge.<!-- HFCM by 99 Robots - Snippet # 15: endmark-python -->\n<span class=\"endmark-python\"><\/span>\n<!-- \/end HFCM by 99 Robots -->\n<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/age_composition_2026.zip\"><strong>Download the data.<\/strong><\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\ndf_all = pd.read_csv(\"census_age_1900-2019.csv\")\r\n\r\nplt.style.use(\"wollen_age_pyramid.mplstyle\")\r\n\r\nfor year in range(1900, 2020):\r\n    df = df_all.copy()\r\n\r\n    df = df[df['year'] == year].reset_index(drop=True)\r\n\r\n    df['male_pct'] = df['male'] \/ df['total'] * 100\r\n\r\n    df['female_pct'] = df['female'] \/ df['total'] * 100\r\n\r\n    fig, (ax0, ax1) = plt.subplots(2, 1, height_ratios=(1, 30))\r\n\r\n    ax1.barh(df.index, df['male_pct'] * -1)\r\n\r\n    ax1.barh(df.index, df['female_pct'])\r\n\r\n    ax1.plot([0, 0], [-100, 100], color=\"#555\")\r\n\r\n    ax1.set(xticks=range(-7, 8),\r\n            xticklabels=[f\"{abs(n)}%\" if n != 0 else \"\u2014\" for n in range(-7, 8)],\r\n            xlim=(-7.5, 7.5),\r\n            yticks=range(17),\r\n            yticklabels=[f\"{n}-{n + 4}\" for n in range(0, 85, 5)],\r\n            ylim=(-0.8, 17))\r\n\r\n    ax1.text(x=-0.1,\r\n             y=16.8,\r\n             s=\"Male\",\r\n             ha=\"right\",\r\n             va=\"center\",\r\n             bbox={\"boxstyle\": \"Round\", \"facecolor\": \"#B5D63D\", \"edgecolor\": \"None\", \"alpha\": 0.35, \"pad\": 0.1})\r\n\r\n    ax1.text(x=0.1,\r\n             y=16.8,\r\n             s=\"Female\",\r\n             ha=\"left\",\r\n             va=\"center\",\r\n             bbox={\"boxstyle\": \"Round\", \"facecolor\": \"#7A30AB\", \"edgecolor\": \"None\", \"alpha\": 0.35, \"pad\": 0.1})\r\n\r\n    ax0.plot([1900, year], [0, 0], color=\"#D00\", marker=\"o\", markersize=4)\r\n\r\n    ax0.set(xticks=range(1900, 2030, 10),\r\n            xlim=(1899, 2023),\r\n            yticks=[],\r\n            ylim=(-10, 10),\r\n            title=\"US Age Pyramid  \u2022  1900\u20132019\")\r\n\r\n    ax0.grid(False)\r\n\r\n    plt.savefig(f\"frames\/{year}.png\")\r\n\r\n    plt.close()<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It&#8217;s difficult to overstate how the United States has aged in recent decades. We have fewer kids, longer lifespans, and better medical care than ever. Those things aren&#8217;t bad per se, but they do present challenges for a program like<\/p>\n","protected":false},"author":1,"featured_media":3354,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[238,561,68],"tags":[599,389,586,601,587,437,160,590,598,264,23,482,135,22,334,122,32,589,593,248,594,438,242,576,24,592,30,591,31,588,25,595,378,600,596,597,252,575],"class_list":["post-2747","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-government","category-animated","category-history","tag-599","tag-age","tag-age-pyramid","tag-american","tag-animated-gif","tag-animation","tag-barh","tag-bbox","tag-billion","tag-budget","tag-census","tag-census-bureau","tag-csv","tag-data","tag-dataframe","tag-dataset","tag-demographics","tag-distribution","tag-entitlement","tag-federal","tag-federal-budget","tag-gif","tag-government","tag-height_ratios","tag-matplotlib","tag-medicare","tag-pandas","tag-policy","tag-population","tag-progress-bar","tag-python","tag-retired","tag-social-security","tag-society","tag-spending","tag-trillion","tag-united-states","tag-width_ratios"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/comments?post=2747"}],"version-history":[{"count":27,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2747\/revisions"}],"predecessor-version":[{"id":3371,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2747\/revisions\/3371"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/3354"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=2747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=2747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=2747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}