Plotting baseball team progress
July 23, 2026 at 8:36 PM by Dr. Drang
I noticed today that the Cubs and Yankees have the same record. I knew that the Cubs have had some extreme ups and downs this year, but I wasn’t sure if the Yankees had, so I decided to plot their progress over the course of the season so far.
A plot of how far above or below .500 they were seemed like the best way to compare their seasons, so I got their records from baseball-reference.com (Cubs results, Yankees results) and cleaned up the data through a combination of Numbers and BBEdit. The result was a pair of CSV files that looked like this:
Date,Home,Opponent,Win
Mar 26 2026,TRUE,WSN,-1
Mar 28 2026,TRUE,WSN,1
Mar 29 2026,TRUE,WSN,-1
Mar 30 2026,TRUE,LAA,1
Mar 31 2026,TRUE,LAA,-1
Apr 1 2026,TRUE,LAA,1
Apr 3 2026,FALSE,CLE,-1
Apr 5 2026,FALSE,CLE,1
Apr 5 2026,FALSE,CLE,-1
Apr 6 2026,FALSE,TBR,-1
[etc.]
These are the first ten games for the Cubs. The Win field contains a +1 for wins and a -1 for losses. The plot of their progress looks like this:

The teams were running in parallel from mid-April through early May, before the Cubs’ disastrous slide. The Yankees had their own smaller slide starting in mid-June but seem to have recovered. History has taught Cub fans not to trust the team’s recent success; 60 more games is plenty of time for another slide (or two).
The plot was made with Python, Pandas, NumPy, and Matplotlib. Here’s the code:
python:
1: #!/usr/bin/env python3
2:
3: import pandas as pd
4: import numpy as np
5: from datetime import datetime
6: import matplotlib.pyplot as plt
7: from matplotlib.ticker import MultipleLocator, AutoMinorLocator
8: from matplotlib.dates import DateFormatter, YearLocator, MonthLocator
9:
10: # Read the files into dataframes
11: dfCubs = pd.read_csv('cubs-2026.csv', parse_dates=[0])
12: dfYankees = pd.read_csv('yankees-2026.csv', parse_dates=[0])
13:
14: # Calculate the games above .500
15: dfCubs['Above'] = np.cumsum(dfCubs.Win)
16: dfYankees['Above'] = np.cumsum(dfYankees.Win)
17:
18: # Create the plot with a given size in inches
19: fig, ax = plt.subplots(figsize=(6, 4))
20:
21: # Add lines for each team. Team colors from teamcolorcodes.com.
22: ax.plot(dfCubs.Date, dfCubs.Above, '-', color='#0E3386', lw=1.5, label='Cubs')
23: ax.plot(dfYankees.Date, dfYankees.Above, '.', color='#0C2340',ms=5, label='Yankees')
24:
25: # Set the limits
26: plt.ylim(ymin=-5, ymax=20)
27:
28: # Set the ticks and add a grid
29: ax.xaxis.set_major_locator(MonthLocator())
30: ax.xaxis.set_major_formatter(DateFormatter('%-m/%-d/%y'))
31: ax.yaxis.set_major_locator(MultipleLocator(5))
32: ax.grid(linewidth=.5, axis='x', which='major', color='#dddddd', linestyle='-')
33: ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')
34:
35: # Title and axis labels
36: plt.title('Cubs and Yankees 2026')
37: plt.ylabel('Games above .500')
38:
39: # Make the border and tick marks 0.5 points wide
40: [ i.set_linewidth(0.5) for i in ax.spines.values() ]
41: ax.tick_params(which='both', width=.5)
42:
43: # Add the legend
44: ax.legend(loc='lower right')
45:
46: # Save as PDF
47: plt.savefig('20260723-Cubs and Yankees 2026.png', format='png', dpi=200)
I wanted to use lines for both teams and let the team colors distinguish them, but the team colors are too close, so I used markers for the Yankees and added a legend. I did still use the team colors, which I got from the Team Color Codes website. I fiddled with the marker size and line width until the two looked to be of roughly equal importance.
It may seem like this is a lot of code to write for a simple plot, but I use a Typinator abbreviation to insert a code template and just tweak the template to get things looking the way I want. The Pandas and NumPy part of the code, Lines 10–16, was trivial.
Apple Park and Severance
July 21, 2026 at 6:24 PM by Dr. Drang
The indispensable Michael Tsai has an interesting post up today about Apple Park and whether its design isolates the people who work there, despite its having been designed—in part, at least—to encourage collaboration.
As usual, Michael has collected a group of choice quotes on the topic. I won’t link to any of them. You should just go to his blog, read the excerpts he’s assembled, and follow the links to see more. But I’ve often wondered how good Apple Park is as a working environment, especially when I watch Severance.
As you may know, the Lumon Industries building that Mark, Helly, Irving, and Dylan work in is a former Bell Labs facility in Holmdel, New Jersey. I first learned about the building several years ago, when I read Jon Gertner’s excellent history of The Labs, The Idea Factory. It’s a distinctive place designed by Eero Saarinen in the early 60s.

Image from GameRant.
Bell Labs was known for the fruitful collaboration of its researchers, often started through chance meetings in the hallways of other Bell Labs facilities. The new Holmdel building was supposed to further that sort of teamwork, but things didn’t work out as planned. Here’s Gertner:
The isolated Holmdel radio labs that had stood on the site for decades—the vast green fields where children would throw boomerangs on weekends, the gracious woodframe building around which engineers would test radio transmissions—were gone. Those labs had been razed. In their stead, on the center of 460 acres of former farmland, Bell Labs had commissioned an enormous modern building to accommodate its growing ranks.
I should mention here that those “vast green fields” and “isolated Holmdel radio labs” were where Karl Jansky invented radio astronomy. As for the building itself:
For obvious reasons, the building was soon nicknamed the Black Box. It was a steel-and-glass six-story structure, serious and austere, designed by the Finnish American architect Eero Saarinen. It was also a monument to architectural presumption. Saarinen, who died before his design was actually built, saw his creation as having the same kind of flexibility as Murray Hill— offices could easily be moved and partitioned, for instance—but with a crucial difference. He placed the building’s long connecting hallways on its glassy perimeter, with the windowless offices and labs in the interior. “Gone completely are the old claustrophobic, dreary, prison-like corridors,” Saarinen remarked with pride. Thanks to the floor-to-ceiling windows, the members of the technical staff would be liberated by unobstructed views of the countryside rather than chance encounters in the hallways.
I don’t want to hit you over the head with parallels, but does “floor-to-ceiling windows” remind you of any other place? Anyway, how well did the Holmdel building foster innovation?
[Labs employee Dick Frenkiel] soon came to realize that he had joined an organization that differed from its myth. The Black Box represented one aspect of this evolution. More to the point, the thrust of the work at Bell Labs seemed to have shifted decisively to big projects involving hundreds of people. Frenkiel’s Bell Labs didn’t seem to have anything to do with heroic research on a new amplifier, done by a few men in a hushed lab. It was about large teams attacking knotty problems for years on end.
I’m sorry, I said I didn’t want to hit you over the head with a̸ c̸a̸r̸ parallels, didn’t I?
Apple has lots of smart people. Some of them must know that the dystopian center of its hit TV show is the 1960s version of Apple Park.
Sum of cubes via difference tables
July 19, 2026 at 9:07 PM by Dr. Drang
Yesterday I watched the most recent Numberphile video, in which Ben Sparks explains a few finite series and the equations that simplify the calculation of their sums. He derives the formulas graphically, and it’s all very cleverly done, especially the one for the sum of cubes.
The idea is to get a simple polynomial expression in n for
As I said, Ben’s graphical method is really clever and easy to understand, and it leads to this expression:
I wanted to see if I could get that same result using a nongraphical and less clever method. The method that came to mind was to generate a handful of values, put them in a table, and start taking differences.
Here are some values of n, N, and the first four differences:
| n | N | Δ | Δ² | Δ³ | Δ⁴ |
|---|---|---|---|---|---|
| 1 | 1 | 8 | 19 | 18 | 6 |
| 2 | 9 | 27 | 37 | 24 | 6 |
| 3 | 36 | 64 | 61 | 30 | |
| 4 | 100 | 125 | 91 | ||
| 5 | 225 | 216 | |||
| 6 | 441 |
The differences are calculated by looking in the preceding column and subtracting the value in the same row from the value in the following row. This sort of thing is fairly easy to do by hand but is really easy to do in a spreadsheet. Here’s a screenshot of the table in Numbers, where I’m displaying the formula for the first difference, Δ:

That formula can be filled into all the difference cells, which is why it’s so easy. (I included only the first six rows in the table above because I figured you’d trust me that all the values in the fourth difference column, Δ⁴, are the same.)
The constant values in the fourth difference column mean N is a fourth-degree polynomial in n:
Because the constant fourth difference is 6, we know that
This comes from the fact that differences are analogous to derivatives, and
Once we know the degree of the polynomial and have , we can figure out the other coefficients by solving a set of four simultaneous equations for four different values of n and N. For example:
We could use any four of the six Ns we’ve calculated, but the first four are convenient. Let’s solve them in Python using NumPy and the solve function from the linalg submodule. We can do this interactively:
python:
from numpy import np
m = np.array([[1, 1, 1, 1], [1, 2, 4, 8], [1, 3, 9, 27], [1, 4, 16, 64]])
b = np.array([1 - 1**4/4, 9 - 2**4/4, 36 - 3**4/4, 100 - 4**4/4])
np.linalg.solve(m, b)
The last line returns
python:
array([ 1.77635684e-15, -3.99680289e-15, 2.50000000e-01, 5.00000000e-01])
Those first two elements of the solution array are at the lower limit of what a floating point number can be. So we can say
Therefore,
or, after collecting terms,
which is the formula Ben got in the video.
Doing basically the same thing in Mathematica is
m = {{1, 1, 1, 1}, {1, 2, 4, 8}, {1, 3, 9, 27}, {1, 4, 16, 64}};
b = {1 - 1^4/4, 9 - 2^4/4, 36 - 3^4/4, 100 - 4^4/4};
LinearSolve[m, b]
which returns
{0, 0, 1/4, 1/2}
This is the same as the Python answer but with no need to think about floating point precision.
A third way to solve the simultaneous equations is to do it in a spreadsheet. Here’s how that looks in Numbers:

where
- the block of yellow cells is the matrix
min the Python and Mathematica solutions; - the block of magenta cells is the inverse of
m; - the block of cyan cells is the vector
bin the Python and Mathematica solutions; and - the block of gray cells is the solution for through , determined by multiplying the magenta matrix by the cyan vector.
The spreadsheet uses a combination of MINVERSE and MMULT, functions that are also in Excel and Google Sheets. As with the Python solution, there are floating point artifacts here. They’re mostly hidden by my choice to show only four decimal places, but that -0.0000 for is a clue that these are not exact answers.
You might well ask why I’d bother using Python or Mathematica to solve the simultaneous equations if I already had a spreadsheet open to make the difference table. The answer is that I generally prefer working in an environment where my formulas and expressions are always visible. It makes things easier to debug, and I’m always debugging.
There are other ways to determine the coefficients. My favorite is a step-by-step procedure that simplifies the problem one polynomial degree at a time.
Given that we know from the difference table above, we can make a new table for
and its differences. By subtracting the fourth-degree term from N, we expect P to be a third-degree polynomial. Here are the first five rows of the difference table for P:
| n | P | Δ | Δ² | Δ³ |
|---|---|---|---|---|
| 1 | 0.75 | 4.25 | 6.50 | 3.00 |
| 2 | 5.00 | 10.75 | 9.50 | 3.00 |
| 3 | 15.75 | 20.25 | 12.50 | |
| 4 | 36.00 | 32.75 | ||
| 5 | 68.75 |
As expected, they become constant at the third difference. Using the same technique we used to get , we can say
Moving on, we make a table for
and its differences. Here are the first four rows of that table:
| n | Q | Δ | Δ² |
|---|---|---|---|
| 1 | 0.25 | 0.75 | 0.50 |
| 2 | 1.00 | 1.25 | 0.50 |
| 3 | 2.25 | 1.75 | |
| 4 | 4.00 |
These become constant at the second difference, and we can say
Finally, we make a table for
which is this:
| n | R |
|---|---|
| 1 | 0.00 |
| 2 | 0.00 |
| 3 | 0.00 |
| 4 | 0.00 |
Since all the R values are zero, the rest of the coefficients, and , must be zero, and we’re done.
If it’s not obvious why , consider this:
The only way for R to be zero for all values of n is if .
Building these tables is fairly easy, but it’s not as fast as building and solving the simultaneous equations. Still, there’s some satisfaction in marching to the answer this way. It’s basically a recursive solution, where we’re simplifying the problem with each step.
Using difference tables to work out the formula for the sum of cubes isn’t as slick as the graphical method shown in the video, but it does have the advantage of not requiring any ingenuity. I’m not opposed to ingenuity, but sometimes you want to just set the problem up, turn the mathematical crank, and get the answer.
Permanent Daylight Saving Time
July 17, 2026 at 12:48 PM by Dr. Drang
A couple of days ago, Casey Liss took a break from arguing about temperature scales to tweak me about the recent passage of the Sunshine Protection Act by the House. The Act would make Daylight Saving Time permanent, something Casey knows I disapprove of. A similar bill passed the Senate a few years ago, and Donald Trump has said he will sign this one, so there’s a decent chance it’ll become law. Let’s see what will happen if it does.
First, of course, there will be a lot of cheering from the people who moan about changing their clocks twice a year. Well, some of the moaners will cheer—the ones who wanted to eliminate DST and stay on Standard Time all year will grumble, but they’ll probably still be pleased to be released from that terrible burden.
I’m more interested in the consequences of permanent DST. You may recall my sunrise/sunset plots. Here’s one for Chicago in 2026:

The dirty yellow zones cover the DST period, which currently runs from the second Sunday in March to the first Sunday in November. A small change to the sunplot code extends that zone to the entire year:

I left the Standard Time lines in place for comparison, even though there won’t be any Standard Time if the Act becomes law.
As you can see, there will be a long stretch—more than two months—for which sunrise will be after 8:00.1 I’m sure this won’t bother many of you who don’t do anything before 8:00, but there are lots of people it will bother. And I bet we’ll hear from them, even though a good chunk of them will be from the current cohort of clock-change moaners.
People living near the western edge of a time zone will have even more morning darkness. You may recall my visit to a Louis Sullivan bank in West Lafayette, Indiana, a couple of weeks ago. My photos showed the sun shining on the north side of the bank, which happened because the sun rises late in West Lafayette. (By “late” I mean in local clock time. You could make an argument that the Sun, like Gandalf, is never late. Nor is it early. It rises precisely when it means to.)
Let’s see the sunrise/sunset times in West Lafayette under permanent DST:

Basically five months for which the sun never rises before 8:00. And about seven weeks for which it doesn’t rise before 9:00. No one deserves that—not even Boilermakers.
-
Yes, the graph is just for 2026, but sunrise times don’t change all that much from year to year. ↩