The standard "3 to 6 months of expenses" rule treats every income stream the same, which is a bad assumption if you freelance, work commission, or run a small business on the side. This is a walkthrough for a small script that sizes an emergency fund target based on your actual income variability instead of a flat multiplier.
If you already track income in a spreadsheet or accounting tool, most of the inputs below are things you already have on hand, this is mostly about turning that existing data into a number that responds to your actual risk rather than a generic rule built around an average household that does not resemble yours.
Step 1: Pull your last 12 to 24 months of net income
Start with a simple list or CSV of monthly net income, ideally two years if you have it. The more variable your income, the more history you need to see a real worst-case month rather than a lucky one.
import statistics
monthly_income = [4200, 3100, 5400, 2800, 4900, 3300, 5100, 2600, 4700, 3900, 5200, 3000]
mean_income = statistics.mean(monthly_income)
stdev_income = statistics.pstdev(monthly_income)
worst_month = min(monthly_income)
print(f"Mean: {mean_income:.0f}, Stdev: {stdev_income:.0f}, Worst: {worst_month}")
Step 2: Calculate a volatility ratio
Divide the standard deviation by the mean to get a coefficient of variation. A steady W-2 salary will produce something close to zero. Freelance or commission income often lands between 0.2 and 0.5.
volatility_ratio = stdev_income / mean_income
print(f"Volatility ratio: {volatility_ratio:.2f}")
Step 3: Scale the base months target by volatility
Instead of a flat 3 to 6 months, use the volatility ratio to scale a base target upward. This is a simplified heuristic, not a formal actuarial model, but it captures the right direction: more volatility should mean more months held in reserve.
base_months = 3
scaled_months = base_months + (volatility_ratio * 12)
scaled_months = min(scaled_months, 12) # cap at 12 months as a sane ceiling
print(f"Suggested months of reserve: {scaled_months:.1f}")
Step 4: Multiply against true fixed costs, not total spending
The target should be based on fixed monthly obligations, rent or mortgage, insurance, minimum debt payments, not total discretionary spending. Total spending overstates what you would actually need to cover in a lean month.
fixed_monthly_costs = 2600
target_fund = scaled_months * fixed_monthly_costs
print(f"Target emergency fund: ${target_fund:,.0f}")
Step 5: Add a dependents adjustment
Volatility is not the only variable worth scaling for. Households with dependents generally have less flexibility to cut spending on short notice, so it is worth adding a separate multiplier rather than folding it into the volatility number.
dependents = 2
dependents_adjustment = 1 + (dependents * 0.08) # rough heuristic, tune to taste
final_target = target_fund * dependents_adjustment
print(f"Final adjusted target: ${final_target:,.0f}")
This is deliberately a rough heuristic rather than a precise formula. The point is not to pretend this script produces a scientifically exact number, it is to replace a flat, one-size-fits-all multiplier with something that actually responds to your specific income shape and household situation.
Step 6: Flag a rolling worst-quarter instead of a single worst month
A single worst month can be an outlier. A rolling three-month window is usually a better signal of what a genuinely bad stretch looks like, since it smooths out one unusually bad or unusually good month sitting next to otherwise normal ones.
def rolling_quarter_sums(income_list, window=3):
return [sum(income_list[i:i+window]) for i in range(len(income_list) - window + 1)]
quarters = rolling_quarter_sums(monthly_income)
worst_quarter = min(quarters)
worst_quarter_avg = worst_quarter / 3
print(f"Worst rolling quarter average: {worst_quarter_avg:.0f}")
Swapping this worst-quarter average in for the single worst-month figure in step 4 usually produces a more stable, defensible target, especially for income with genuine seasonality rather than random noise.
Putting it together
Run this against your own income history and you will likely land somewhere different from the generic 3-to-6-month range, especially if your income has any real seasonality or client concentration risk. The script above is intentionally simple. A more complete production version would pull data directly from a bank API or accounting export instead of a hardcoded list, weight recent months more heavily than older ones, and expose the dependents adjustment as a configurable input rather than a hardcoded constant. But even this basic version gives a more honest number than a flat multiplier pulled from a blog post.
One thing worth flagging if you build on this: resist the urge to over-engineer the volatility model. A coefficient of variation and a rolling worst-quarter figure get you most of the way to a defensible number. Fancier statistical approaches, like fitting a full distribution and computing a percentile-based value-at-risk, add complexity without meaningfully changing the output for most personal finance use cases.
If you want to take this further as a small side project, a natural next step is scheduling the script to run monthly against a live data source, an accounting API export or a bank transaction feed, and flag when the suggested target has moved by more than a small threshold since the last run. That turns a one-off calculation into an ongoing check, which matters more than the initial number does, since income situations drift gradually and most people never notice until the gap has become large.
A note on testing this against your own numbers before trusting it
Before relying on any version of this script for a real financial decision, run it against a few months you already remember clearly and sanity-check the output against your gut sense of how risky that period actually felt. If the script suggests a target that feels obviously too low or too high compared to your own memory of a rough stretch, the volatility model probably needs tuning, either a longer lookback window, a different weighting scheme, or a different definition of "worst case" than a simple rolling minimum.
This is generally true of any personal finance automation: the value is in making an implicit judgment call explicit and repeatable, not in replacing your own judgment entirely. Treat the output as a well-reasoned starting point for a conversation with yourself about risk, not a number to accept uncritically just because it came out of a script.
Where this fits alongside a proper financial plan
Nothing here replaces sitting down with a fee-only financial planner if your situation is complex enough to warrant one, particularly once equity compensation, multiple income streams, or significant dependents enter the picture. What a script like this is good for is getting a quick, defensible starting estimate before that conversation, or for households whose situation is straightforward enough that a full advisor engagement is not worth the cost. Either way, showing up to a planning conversation with a number you already understand the reasoning behind tends to produce a more useful discussion than showing up with either no number at all or a flat multiplier you picked without thinking about it.
If you would rather skip the spreadsheet math, EvvyTools's Emergency Fund Calculator walks through the same inputs, income stability, dependents, and expenses, with a guided form and milestone tracking instead of a script you have to maintain yourself.
For more on why the standard rule of thumb breaks down for variable-income and multi-dependent households specifically, there's a longer breakdown at the EvvyTools blog, covering the reasoning behind each adjustment in plain language rather than code.
External references worth a look: the Python documentation for the statistics module if you want to extend this with percentile-based calculations, the Bureau of Labor Statistics if you want real-world data on income variability by occupation type, and NumPy's documentation if you outgrow the standard library's statistics module for larger datasets.
Top comments (0)