Bamboo Weekly #194: GitHub (solutions)
Last chance to join PythonDAB!
The 10th cohort of my Python, Data, and AI bootcamp (PythonDAB) starts on November 5th. If you want to get fluent with these technologies in a small, collaborative cohort that emphasizes discussion, interaction, and hands-on practice, then this might be the program for you. Get more information at https://lernerpython.com/bootcamp/, or contact me at [email protected].
How much do people use GitHub? I had these thoughts in the last few weeks, when Anthropic (the soon-to-be-public AI company behind Claude) told the world that they were no longer using Git for version control, because it couldn't handle the number of commits they were now creating with agentic programming.
The thing is, Git isn't just a version-control system any more. It's also a way for people to collaborate. For most Git users, this means using GitHub (or one of its smaller competitors, such as GitLab or Bitbucket), which allows you to share your Git repository, and all its commits with others — sometimes in your organization, and sometimes with the entire world. It's up to you.
This week, we'll look at data about GitHub: How many people use it? How many pushes do we see in a given year? How many repositories are there? What countries produce the most pushes? And have we seen any serious changes in the last year, since agentic coding took over the industry?
Data and five questions
This week's data comes from GitHub itself – and more specifically, from what it calls the "GitHub Innovation Graph," with data (of course) hosted on GitHub at https://github.com/github/innovationgraph/ . This data reflects public (i.e., largely open-source) activity on GitHub, aggregated and published every quarter since 2020.
The data comes as a set of eight CSV files, each describing a different facet -- pushes, repositories, and licenses, among others.
Paid subscribers, both to Bamboo Weekly and to my LernerPython+data membership program (https://LernerPython.com) get all of the questions and answers, as well as downloadable data files, downloadable versions of my notebooks, one-click access to my notebooks, and invitations to monthly office hours.
Learning goals for this week include: Reading multiple files, joins, grouping, pivot tables, window functions, and plotting with Plotly.
Here are my five solutions and explanations for this week's questions:
Clone the repository from GitHub. Read all eight files into a dictionary, in which the filenames are the keys and the data frames (from reading the CSV files) are the values. Create a line plot showing, in total, the number of pushes that GitHub has seen per year. How do you explain the numbers for 2026?
I started off by importing Pandas, Plotly, and also pathlib, a module in the standard library that is perfect for these sorts of situations:
import pandas as pd
from plotly import express as px
from pathlib import PathNext, in order to create a dictionary of data frames, I used a dict comprehension. I started off by creating a Path object on the directory, and invoking glob on it. This returned a list of files in the data directory matching the '*.csv' pattern.
I then iterated over these filenames, one at a time, creating a key-value pair:
- To get the key, I first retrieved
one_filename.name, giving me the file's name, without the leading path. I then invokedstr.removesuffixon that string to remove the'.csv'suffix. - To get the value, I invoked
pd.read_csvonone_filename. That returned a data frame – and while that might seem like a weird way to store data frames, it's actually quite convenient when you have a number of them and don't want to use multiple variables.
The code is thus:
dirname = 'data/bw-194/'
all_dfs = {one_filename.name.removesuffix('.csv') : pd.read_csv(one_filename)
for one_filename in Path(dirname).glob('*.csv')}
That resulted in a dict with eight key-value pairs.
To get the total number of pushes in GitHub from each year, I queried the all_dfs['git_pushes'] data frame. I then used groupby and the sum aggregation method to find the total number of pushes per year. I then used pipe and px.line to get a line plot:
(
all_dfs['git_pushes']
.groupby('year')['git_pushes'].sum()
.pipe(px.line)
)The resulting plot:
We can see a dramatic increase in the number of pushes to GitHub over the years. It hit about 1 billion total pushes in 2025, when the number in 2024 was about 0.75 billion. So we see lots of growth, right?
Well... we did, but we don't see it in 2026. What's going on?
The main problem here, in my opinion, is that we're looking at the wrong level of granularity. The data includes not just a year column, but also a quarter column. I decided to do a second query, creating a new year_quarter column (with assign) that contains both, and then plotting with that:
(
all_dfs['git_pushes']
.assign(year_quarter = pd.col('year').astype(str) + '-' + pd.col('quarter').astype(str))
.groupby('year_quarter')['git_pushes'].sum()
.pipe(px.line)
)The result:
That makes a lot more sense, no? moreover, we can see that the number of pushes has absolutely skyrocketed since the start of 2025, roughly when we saw people start to use AI for coding in a serious way.
What 10 economies (i.e., countries plus territories that aren't quite countries) have the greatest number of repositories in 2026? Using data from https://github.com/datasets/population, which 10 countries have the greatest number of repositories per capita in 2026?
I retrieved the repositories data frame from my dictionary, and then used loc andpd.col to keep only the rows in which year was 2026.
Then I used groupby on iso2_code (the two-letter country abbreviation) to get the total number of repositories for each country in each year. Finally, I invoked nlargest(10) to get the 10 countries with the greatest number of repos:
(
all_dfs['repositories']
.loc[pd.col('year') == 2026]
.groupby('iso2_code')['repositories'].sum()
.nlargest(10)
)The result:
iso2_code repositories
US 77284974
IN 69251720
EU 56672501
BR 19918250
CN 18424702
JP 11846724
GB 11018799
HK 10849015
ID 10517820
DE 10506693
This isn't a huge surprise: The US is in first place, followed by India, followed by the EU (which fits the definition of an "economy," even if it isn't a country). Then we have Brazil, China, Japan, the UK, and Hong Kong, with Indonesia and Germany taking the last two slots.
Population obviously plays a role here; a country with more people will likely have more repositories. That's why I thought it would be interesting to get the per-capita number of repositories. A country in which every person had one GitHub repo would have a per-capita number of one (and a very nerdy population, at that!).
I suggested using a population data set from GitHub. I read it into Pandas in this way:
population_url = 'https://raw.githubusercontent.com/datasets/population/refs/heads/main/data/population.csv'
population_df = (
pd
.read_csv(population_url)
.pivot_table(index='Country Code',
columns='Year',
values='Value')
[2025]
.reset_index()
)In other words, I retrieved the data, and immediately invoked pivot_table to put countries in the rows and years in the columns. I then grabbed the column for the 2025 year, and also invoked reset_index, giving me a two-column data frame.
It would be great to join the earlier data with this, but that won't be possible – because this population data set uses three-letter country abbreviations, and the GitHub data uses two-letter country abbreviations.
I thus downloaded another data set from GitHub, one which has both abbreviations. I invoked p.read_csv to retrieve it from GitHub, and passed the usecols keyword argument to keep only the country name and the two abbreviations:
country_url = 'https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'
country_codes = pd.read_csv(country_url,
usecols=['name', 'alpha-2', 'alpha-3'])I then went about combining these data frames. I did so using merge, because it allows us to perform the combination using any columns. This is in contrast with join, which only works on the index.
I started the query by retrieving all_dfs['repositories'], and then keeping only those lines from 2026. I then (again) performed the groupby on iso2_code, getting the total number of repos for each economy. Next, I used reset_index to move the two-letter code back into the data frame:
(
all_dfs['repositories']
.loc[pd.col('year') == 2026]
.groupby('iso2_code')['repositories'].sum()
.reset_index()
)Next, I invoked merge twice, once between the repositories data frame and the country_codes data frame, and a second time between the result of the first merge and population_df. country_codes acted as a translation table, since it contained both of the types of codes.
(
all_dfs['repositories']
.loc[pd.col('year') == 2026]
.groupby('iso2_code')['repositories'].sum()
.reset_index()
.merge(country_codes, left_on='iso2_code', right_on='alpha-2')
.merge(population_df, left_on='alpha-3', right_on='Country Code')
)I then went about cleaning things up: I moved the name (i.e., country name) column to the index with set_index. I then used assign to create a repos_per_person column, the result of dividing repositories by pd.col(2025), the population we have from last year. I then used [] to get just the result column, invoked round to get reasonable (if approximate) numbers, and asked for the 10 largest per-capita GitHub countries:
(
all_dfs['repositories']
.loc[pd.col('year') == 2026]
.groupby('iso2_code')['repositories'].sum()
.reset_index()
.merge(country_codes, left_on='iso2_code', right_on='alpha-2')
.merge(population_df, left_on='alpha-3', right_on='Country Code')
.set_index('name')
.assign(repos_per_person = pd.col('repositories') / pd.col(2025))
['repos_per_person']
.round(2)
.nlargest(10)
)The result:
name repos_per_person
Hong Kong 1.45
Singapore 1.19
Seychelles 0.55
Estonia 0.32
Netherlands, Kingdom of the 0.27
Iceland 0.25
Andorra 0.24
Canada 0.24
Finland 0.24
Ireland 0.24
Hong Kong and Singapore each has more than one GitHub repo per person. Wow! We then get to some smaller countries, where a small pouplation helps boost their rating – although the Netherlands, Canada, Finland, and Ireland aren't in the same league as, for example, Estonia or Iceland.
Upgrade to continue reading.
Become a paid member of Bamboo Weekly to get access to all premium content.
Keep reading
Subscription details
You are receiving this because you are a free subscriber to Bamboo Weekly.
Email: [subscriber]
Member since: 18 October 2026
Want Python itself, not just Pandas? I also write Better Developers, a free Monday newsletter about how the language works.