All articles

KajoData / tableau en

MoM and YoY in Tableau: percentage change with the right base

I explain MoM and YoY formulas by tracing their comparison values. Learn why missing months, zero bases and date filters can change the meaning of a result.

Kajo RudzińskiPublished Updated 13 min read

Two timelines with bridges comparing neighboring and distant periods and one missing step

Month over month growth looks like a simple formula: subtract the previous value from the current value, then divide by the previous value. In Tableau, the difficult part is often deciding which value really counts as previous. One missing month or a date filter can change the indicator’s meaning without producing a syntax error.

I begin with a control table. I want to see the month, current sales and comparison base together. Only when those columns make sense do I turn them into a chart. The green arrow can wait, especially if it would describe growth against a completely different period from the one promised in the title.

What MoM and YoY mean

MoM, or month over month, compares a month with the preceding month. YoY, or year over year, in a monthly analysis compares it with the same month in the previous year. February therefore compares with January for MoM, and with the previous February for YoY.

These indicators answer different questions. MoM highlights short term movement, but it can react strongly to seasonality. YoY keeps the month name consistent, yet the conditions may still differ: holidays, changes in the offer or an unusually weak comparison base can all matter. The percentage alone does not explain a cause.

With a positive base, I divide the difference between current and base values by the base value. Moving from 100 to 120 gives 20%. Moving back from 120 to 100 gives a decline of approximately 16.7%, because the denominator changes. I explain the arithmetic further in the percentage change article.

Prepare a month that also contains the year

This tutorial assumes a date field called [Order Date] and a numeric measure called [Sales]. You can use different names, but they must match your source. The data covers completed months and a consistent sales definition. I am not mixing currencies, actuals with targets, or gross amounts with net amounts.

I create a [Month] field representing the beginning of the month:

DATETRUNC('month', [Order Date])

The function truncates a date to the specified part, as described in the Tableau date documentation. In the view, I use a full monthly date that retains the year. A discrete month number alone could combine January values from several years into one group.

I sort months chronologically. To begin, I put [Month] on Rows and SUM([Sales]) on Text. Each row should represent one month in the series being analyzed. If I add region, I must decide whether each region needs a separate growth calculation. That decision comes before choosing a chart style.

I also check the source’s level of detail. One order line and one monthly total are different observations. If both contribute to the same measure, a growth formula can be arithmetically correct while using incorrect totals. I therefore validate ordinary sales before calculating the rate at which sales change.

LOOKUP refers to a position in the view

I create a field called [Previous month]:

LOOKUP(SUM([Sales]), -1)

LOOKUP retrieves an expression’s value at a position offset from the current position within the partition. When the target cannot be determined, it returns NULL. This is the behavior described in the table calculation function reference.

The important consequence is that minus one means the previous calculation position. On a complete monthly axis, that is the previous month. If February is absent between January and March, the preceding available position may be January. Calling the result MoM does not create the missing month.

I add this field beside sales in the control table. For the first month, I expect no comparison value if the available history begins there. For the second month, I want to see the first month’s sales. Reading those two amounts tells me more than inspecting the percentage alone, because it exposes the denominator’s origin.

Set addressing and partitions

Table calculations operate on the values in a view. Dimensions divide them into partitions and determine how the calculation moves between positions. The table calculation guide explains these concepts.

In a simple table with months on Rows, I calculate along month in ascending order. When using Specific Dimensions, I select the full [Month] field as the addressing dimension. If region defines separate series, I leave it as a partitioning dimension. The configuration has to match the worksheet’s actual structure.

I avoid prescribing a universal “Table Across” click. Moving month from columns to rows can make that shortcut inappropriate for the intended comparison. I inspect the sequence the calculation follows. With two regions, the second region’s first month must not borrow the first region’s final month as its comparison base.

Each field containing its own LOOKUP needs its configuration checked in the view where it is used. Rearranging a dashboard does not necessarily alter the calculation, but adding a dimension to a worksheet can change its detail. After such a change, I return to the control table rather than assume that the earlier interpretation still holds.

A MoM formula with an explicit base policy

In this example, I report percentage change only when the base is positive. I leave NULL for missing, zero or negative bases. This is a reporting policy to agree for your measure. Negative results can be compared in other analyses, but they need a separate interpretation rather than an unexamined copy of a sales indicator.

The [MoM] field is:

IF ISNULL([Previous month]) OR [Previous month] <= 0 THEN
    NULL
ELSE
    (SUM([Sales]) - [Previous month]) / [Previous month]
END

The syntax for IF and ISNULL is documented under logical functions. I format the result as a percentage. I do not also multiply it by one hundred: the numeric value 0.2 should display as 20%.

The following data is synthetic. Each result follows directly from the amounts shown:

Month Sales MoM base MoM
2026-01 100 unavailable unavailable
2026-02 120 100 20%
2026-03 90 120 -25%
2026-04 0 90 -100%
2026-05 40 0 unavailable

I do not assign May an arbitrary 100% increase. I show the amount, 40, and explain that the base is zero. April can have a -100% result because its base is positive. Those two situations deserve a conversation with the reader before the report becomes part of a recurring process.

Two stacks of values being compared while a magnifying lens checks an empty base

YoY needs a complete monthly series

I create [Previous year month] as follows:

LOOKUP(SUM([Sales]), -12)

Then I define [YoY]:

IF ISNULL([Previous year month]) OR [Previous year month] <= 0 THEN
    NULL
ELSE
    (SUM([Sales]) - [Previous year month]) / [Previous year month]
END

The policy is the same as for MoM; only the comparison value changes. January 2026 sales of 150 compared with January 2025 sales of 100 give 50%. This interpretation assumes a complete, correctly ordered sequence of monthly positions within the partition between those dates.

Four rows containing two January values and two February values are insufficient to demonstrate an offset of minus twelve. To trace that offset in a view, you need every monthly position from January 2025 through January 2026. Only then are the January positions exactly twelve steps apart.

If the source contains irregular periods, I do not treat an offset as a calendar. For that project, I prefer preparing a complete monthly sequence and explicit period matching in the model. Before choosing a method, I also need to establish whether an absent period means no sales or no information. Those are different business situations.

A missing month differs from a missing value

Tableau can show missing date headers when it can infer their domain. Show Missing Values helps expose a gap, but it does not decide what sales should be recorded there. The documentation also distinguishes an absent period from NULL in an existing row: Show or Hide Missing Values.

In practice, I ask the source owner what happened in February. Did the channel actually sell nothing, or did a file fail to arrive? Filling the gap with zero without checking can leave March with an unavailable percentage instead of an honest explanation of incomplete history. Ignoring the gap can compare March with January.

During review, I inspect February and its two neighboring months specifically. I read the comparison dates and amounts before the percentage. In a larger report, I include a note about input completeness. One missing period can affect both MoM and later YoY, so the problem extends beyond one blank cell.

A filter can remove the history you need

A normal date filter runs before table calculations. If I retain only 2026 in the view’s data, January loses December 2025 for MoM, and the entire year loses its 2025 YoY bases. The Tableau order of operations explains this sequence.

I therefore separate the range needed for computation from the range displayed to readers. For diagnosis, the simplest option is retaining all history in the table. For presentation, earlier headers can be hidden rather than removing their data with an ordinary filter. Tableau demonstrates this distinction in its calculation types guide.

A more developed solution can use a table calculation filter, but its behavior needs separate review. I do not switch filter types blindly. I record the expected result for January, apply the chosen range and compare the base. I repeat that inspection after applying region and product filters too.

Show amounts and identify incomplete months

In the tooltip, I include current sales, the comparison amount and percentage change. That lets readers see whether a large percentage comes from a large absolute movement or a very small starting point. Moving from 1 to 2 and moving from 100 to 200 both mean 100% growth, although the scale differs.

I do not compare half of October with all of September under an ordinary MoM label. I either use completed months or define a separate comparison for a month in progress against a comparable range. The description should identify day coverage, posting delays and the extraction cutoff where they matter.

For a measure that is itself a percentage, such as a share or margin rate, I distinguish relative change from percentage points. Moving from 20% to 25% is an increase of five percentage points, or 25% relative to the original rate. The label affects interpretation. I do not copy a sales growth formula into every measure that displays a percent sign.

A quick calculation or a custom formula

For a simple comparison, you can use Percent Difference From relative to the previous value. Tableau describes that option in the calculation types documentation linked above. It still requires the correct direction, partitions and periods. A menu shortcut leaves those assumptions in place.

I use separate custom fields when I want to expose the comparison base and apply an explicit policy for nonpositive values. This makes an unavailable indicator easier to explain. I also avoid assuming that the built-in calculation and my custom formula follow the same convention for a negative denominator. This example deliberately excludes that situation from percentage reporting.

When handing over the worksheet, I record which method I chose. If someone later replaces the field with a quick calculation, they should compare behavior on the same small dataset, including the first month, a gap and zero or negative bases. Matching one ordinary increase does not establish equivalence in every case.

Overall growth differs from average channel growth

Consider another synthetic example. In January, the store has sales of 100 and online has 10. In February, the store remains at 100 while online rises to 20. Store growth is 0%; online growth is 100%. The whole business moves from 110 to 120, so its growth is approximately 9.1%.

An unweighted average of the two percentages would be 50%. That does not describe the change in the company’s total sales. The small channel and the large channel would receive equal weights. For an overall growth indicator, I start with current and base totals for the relevant scope, then calculate the percentage change.

This distinction matters when adding region, product or channel to a view. I need to know whether a headline card should show company growth or average growth across selected groups. Both can be meaningful questions, but they need different names and definitions. I do not average existing percentages merely because the result is easy to place in a card.

I also inspect the total after selecting a channel. If a filter affects one chart while a growth card still covers the whole business, the dashboard must say so clearly. Otherwise, readers may assume the headline number refers to their selection. That interpretation error can occur even when both worksheets contain correct formulas.

Record the assumptions when handing over a report

I include a short definition of the comparison: measure, monthly granularity, historical range and the treatment of a zero base. I state whether the current month is excluded and where to check source completeness. The person maintaining the report then has a reference point for future refreshes.

If the source changes how sales are recorded, a correctly configured LOOKUP cannot preserve comparability on its own. Someone must decide whether to restate history, flag a definition change or temporarily withhold the percentage. I record that limitation rather than rely on the author’s memory. The indicator should remain understandable months after the first meeting.

How I review the finished view

I begin with one channel and a few months whose values I can explain. I inspect the first period, an ordinary increase, a decrease, a zero current value and a zero base. Then I add another channel and confirm that the series stay separate at partition boundaries. Only after that do I examine the whole timeline.

I also check sorting behavior. Growth should follow chronology even if someone wants a ranking of the best months. A ranking can have its own view; the sequence used for period comparison should remain explicit and stable. Otherwise, “previous” starts to mean a neighbor in a ranking rather than the intended earlier month.

I compare results before and after changing the date filter. Finally, I read titles, units and the explanation of an unavailable indicator. A blank must be understandable as the absence of a valid comparison, rather than no change. For the broader process of building readable views, see my introduction to Tableau.

An exercise to finish the analysis

Start with the five synthetic months in the table and build three columns: Sales, Previous month and MoM. Explain the difference between April’s -100% and May’s unavailable result. Then remove February and inspect how the meaning of the previous position changes. Restore the full axis before calling the result MoM again.

For YoY, prepare thirteen consecutive months. Give the first and last months values of 100 and 150 respectively, and use any small positive amounts for the intervening months. January in the second year should refer to January in the first. Observe what happens when the first year is filtered out and explain the difference.

Finish by writing the assumptions beside the view: monthly granularity, chronological order, partitioning, retained history and the positive base policy. Another analyst should be able to understand those choices without guessing from the chart. That is the point at which I am comfortable presenting the growth indicator to someone else.

Key takeaways

Does LOOKUP with an offset of -12 always return last year?

No. It returns a value twelve positions earlier in the partition. That represents the same month last year only with a complete chronological monthly series and the correct calculation direction.

Why does MoM disappear after applying a date filter?

A normal date filter can remove the month needed as the comparison base before the table calculation runs. Retain the necessary history and distinguish the calculation range from the display range.

What growth should I show when the previous value is zero?

The usual percentage change formula has no defined result with a zero denominator. In this example I leave the indicator unavailable and show the amounts rather than substitute zero or an arbitrary percentage.

AI was used in an automated way while working on this article. If you want to learn how to do it too, check out this project.

Kajo Rudziński

Newsletter KajoData

Let’s keep in touch.

I write about data analysis, learning and changing careers. My newsletter and the sign-up form are in Polish. If that works for you, I’d be glad to see you there.