Menu /
Who Runs Out of Cash First? A Runway Post-Mortem in pandas
Python / Blog

Who Runs Out of Cash First? A Runway Post-Mortem in pandas

Most portfolio reviews have one column people read first: runway, the months of cash a startup has left. Sort by it, shortest first, and the top of the list is where the meeting starts.

The portfolio here is Lanternfield’s. Lanternfield is an invented accelerator and seed fund, and its records are practice data on LucidPlexus: 1,000 applications, 91 of them funded, and for each funded startup one row a month (revenue, customers, burn and cash) from the month after the cheque until March 2026, or until it closed or was sold. The startups, the founders and the money are all made up, with a few faults planted for learners to find. We asked the records the portfolio question: who runs out of cash first?

Practise on the same dataFree to read · a free account previews and downloads
Lanternfield — Monthly metrics monthly_metrics.csv1,817 rows · 8 columns · CSV →Lanternfield — Funding outcomes funding_outcomes.csv1,000 rows · 6 columns · CSV →
Save both files in a folder called data next to your notebook and every snippet below runs as written in pandas. They are the same files this post-mortem used, faults included.

Burn, runway, and one month too many

Two definitions first. Net burn is what a startup spends in a month minus what it earns, so a negative burn means it’s making money. Runway is cash divided by burn: the months until the money runs out if nothing changes. It only means something when the burn is positive. A startup that earns more than it spends doesn’t have a runway; it has a business. Then the checks, before a single number gets quoted:

import pandas as pd

monthly = pd.read_csv("data/monthly_metrics.csv", parse_dates=["Month"])
monthly.columns = ["business", "month", "revenue", "customers",
                   "new", "lost", "burn", "cash"]
outcomes = pd.read_csv("data/funding_outcomes.csv", parse_dates=["Decision Date"])
outcomes.columns = ["business", "stage", "decided", "cheque", "stake", "status"]

print(len(monthly), "rows,", monthly.duplicated().sum(), "exact copy")
monthly = monthly.drop_duplicates().sort_values(["business", "month"])
gap = monthly.groupby("business")["month"].diff().dt.days > 31
print(monthly.loc[gap, ["business", "month"]].to_string(index=False))
Output
1817 rows, 1 exact copy
   business      month
Business 99 2023-08-01

The checks turn up one month listed twice (Business 43, December 2022), which would count that month’s burn twice in any total, so it goes. Business 99 has a gap: nothing for June and July 2023, which matters for anything that counts months. A third planted fault, a negative number of lost customers, doesn’t touch burn or cash. Now the runway, from each startup’s latest month:

funded = outcomes[outcomes["stage"] == "Funded"].set_index("business")
latest = (monthly.groupby("business").tail(1).set_index("business")
          .join(funded[["status", "cheque"]]))
active = latest[latest["status"] == "Active"].copy()
active["runway"] = active["cash"] / active["burn"].where(active["burn"] > 0)

print((active["burn"] <= 0).sum(), "of", len(active),
      "active startups earn more than they spend")
print(active.sort_values("runway")[["cash", "burn", "runway"]]
      .head(5).round(1))
Output
22 of 41 active startups earn more than they spend
                cash    burn  runway
business                            
Business 554  122729   32164     3.8
Business 607  767131  104225     7.4
Business 688  392802   49796     7.9
Business 571  131850   15811     8.3
Business 719  719147   82262     8.7
Runway in March 2026 for the 19 active startups that burn cash, shortest first. Business 554 has 3.8 months; 5 have less than nine; the median is 21.1 months.Months of cash left, March 20261224364860Business 5543.8Business 6077.4Business 6887.9Business 5718.3Business 7198.7Business 876Business 869Business 825Business 830Business 781Business 809Business 867Business 910Business 853Business 770Business 905Business 673Business 807Business 841101.6 →runway = cash ÷ burn, in months; line at a year
Fig. 1 — The 19 active startups that burn cash, shortest runway first. Business 554 has 3.8 months, and five have less than nine. The longest bar is cut at 60 months.

Of the 19 that burn cash, Business 554 has 3.8 months left, and four more have less than nine. So Business 554 runs out first, in about four months. Unless runway is a poor clock. It is, after all, a forecast: it assumes next month looks like this one. Forecasts can be checked against the past, and Lanternfield’s past includes startups that closed.

Runway’s track record

The test is simple. For each closed startup, take its runway in its first month, fresh from the cheque, and set it against how long the startup actually lasted:

first = (monthly.groupby("business").head(1).set_index("business")
         .join(funded[["status", "cheque", "decided"]]))
first["burning"] = first["burn"] > 0
first["runway"] = first["cash"] / first["burn"].where(first["burning"])
span = monthly.groupby("business")["month"].agg(["min", "max"])
first["months"] = ((span["max"].dt.year - span["min"].dt.year) * 12
                   + span["max"].dt.month - span["min"].dt.month + 1)

closed = first[first["status"] == "Closed"]
burners = closed[closed["burning"]]
print(len(burners), "closed startups burned cash in their first month")
print("median runway in month one:", burners["runway"].median().round(1))
print("median months they lasted: ", burners["months"].median())
print("closed before it ran out:  ",
      (burners["months"] < burners["runway"]).sum(), "of", len(burners))
Output
18 closed startups burned cash in their first month
median runway in month one: 25.0
median months they lasted:  19.0
closed before it ran out:   15 of 18
For the 18 closed startups that burned cash in their first month: runway in month one against the months they lasted. 15 of 18 sit below the diagonal, so they closed before their first-month runway ran out.Runway promised vs. months lasted1224364860122436lasted its runwayB578: 209 →below the line: closed withrunway to spare (15 of 18)runway in month one (months) →↑ months lasted
Fig. 2 — Closed startups that burned cash from month one. The dashed line is “lasted exactly as long as its runway said”; 15 of 18 closed below it. One sits off the scale, at a runway of 209 months.

Fifteen of the 18 closed before their first-month runway ran out. Median runway was 25 months; the median closed startup lasted 19. Their last reports still show money in the bank: $129,049 at the median. Runway measures the time to zero, and companies don’t wait for zero; one that can’t raise again winds down while it can still pay its people. So runway overstates the time left. Discount it and move on? Not yet. The first-month numbers were hiding something more useful than a discount.

What the closed ones had in common

Look again at that output: nearly every startup that closed was burning cash in its very first month. That isn’t how most of the portfolio started: 42 of the 91 funded startups earned more than they spent from month one. So here’s a sharper test, on the intakes old enough to have had time to fail, the 39 startups funded in 2022 and 2023:

old = first[first["decided"].dt.year <= 2023]
print(pd.crosstab(old["burning"], old["status"]))
Output
status   Acquired  Active  Closed  Raised Follow-on
burning                                            
False           3       6       0                 7
True            0       1      13                 9

Of the 16 that earned money in their first month, none has closed: they’re active, sold, or have raised a follow-on round. Of the 23 that burned, 13 have closed. Burning from the start isn’t a death sentence, mind: 9 of those 23 raised a follow-on round, the best news a seed fund gets short of a sale. But every closure in those two intakes came from the burners.

That leaves the useful question. Month one comes after the money has been paid. Could the fund have seen this coming before it wrote the cheque? There is one number in these records that’s fixed before month one begins.

Then we looked at the cheques

The cheque. Lanternfield sets its size when it decides to invest, before the startup’s first month on the books, so put it beside the month-one burn:

print(pd.crosstab(first["cheque"] > 150_000, first["burning"],
                  rownames=["cheque over $150,000"],
                  colnames=["burning in month one"]))
Output
burning in month one  False  True 
cheque over $150,000              
False                    42      5
True                      0     44

Big cheque, burning startup.
Every single time.

All 91 funded startups by cheque size and by whether they burned cash in their first month. Cheque of $150,000: 42 earning, 5 burning. Cheque over $150,000: none earning, 44 burning. Closed startups are marked in teal.Who got which chequeearning inmonth oneburning inmonth onecheque of$150,00042 startups5 startupscheque over$150,0000startups44 startupsclosed by March 2026; one square, one startup
Fig. 3 — All 91 funded startups, one square each, by cheque size and by month one. Every startup that earned from month one got exactly $150,000; the corner for big cheques to startups that earned is empty. Closed startups in teal.

Read the corners. All 42 startups that earned money from month one received exactly $150,000, the smallest cheque in Lanternfield’s records. All 44 cheques above that, up to $500,000, went to startups that burned from the start, and 16 of those have closed. In these records, a cheque’s size says less about how well a startup would do than about how hard it would burn, and that was known on the day the cheque was signed. (A handful of startups that earned in month one have closed too, all from the 2024 intake and most of them after a single month of records. The data doesn’t say why.)

And the portfolio today?

burning_now = active[active["burn"] > 0]
print((burning_now["cheque"] > 150_000).sum(), "of", len(burning_now),
      "startups burning today had a cheque over $150,000")
Output
18 of 19 startups burning today had a cheque over $150,000

That means the five shortest runways in Fig. 1 belong to four big-cheque startups and Business 571. The list everyone reads first was, in a sense, written when the cheques were.

Runway, honestly
  1. Compute it only where burn is positive, and say what happened to the rest: where(burn > 0) leaves them as NaN, not a negative number of months.
  2. Date it. “3.8 months” means 3.8 months from March 2026, at March’s burn.
  3. Check it against the ones that already ended. Here it overstated the time left for 15 of 18.

Every snippet above ran in pandas 3.0.5 on the two practice files, and every output is pasted as it printed.

The committee meets in April

Lanternfield’s committee meets in April with a spring round to decide: $2.5 million and room for at most ten cheques. Of the 19 cheques over $150,000 it wrote in 2022 and 2023, 12 went to startups that have since closed and 7 to startups that raised a follow-on round. Not one is simply ticking over. Big cheques, here, meant all or nothing.

What the records can’t say is which way the arrow points. Did startups that were going to burn ask for big cheques, or did big cheques give startups something to burn? Either way, the committee has ten cheques to place this spring. Is a startup that needs a big one a worse bet, or only a bigger one?

References

On LucidPlexus
Elsewhere

Comments

No comments yet. Be the first to share your thoughts.