Better Data Visualization with Dual Axis Graphs in Python | by Byron Dolon | Towards Data Science

Visualizing data makes it easy for people to understand trends and make informed decisions. An important part of data visualization is picking the right chart to showcase your data. Furthermore, even specific charts like bar graphs and line graphs can be further customized to best explain the data you would like to showcase.
In addition to cosmetic changes like color and font, you might also consider adding additional features like average trend lines, forecasts, or implement a dual-axis. In this piece, we’ll cover how you can use a dual-axis in a line chart to make correlations and trends across data points more evident to your audience. We’ll also take a quick look at what a regular chart without a dual-axis would look like, so you can decide which of the two would best suit your visualization needs.
We will be using the Plotly library in Python for the data visualization, as well as Pandas for some initial data pre-processing, so make sure you have these two packages installed already. Then, import the following and get ready to follow along!
import pandas as pdimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport random
First, to generate some sample data, run the following code.
expense_data = {    "Person": random.choices(["A", "B"], k=20),    "Amount": random.sample(range(100, 200), 10) + random.sample(range(0, 99), 10),    "Category": ["Groceries"] * 10 + ["Restaurant"] * 10,    "Date": pd.to_datetime(pd.date_range('2020-01-01','2020-10-01', freq='MS').tolist() * 2)}df = pd.DataFrame(data=expense_data)
The data we’re going to visualize will be based on some randomly generated personal expense data. Above you can see we’re just randomly creating expense data for 10 months and loading it into a Pandas DataFrame. The above code should output 20 lines worth of data.
Our goal for this analysis is to compare the “Groceries” and “Restaurant” spending categories over time. As such, let’s next group the data by the Date and Category fields using a few lines of Pandas.
df_grouped = df.groupby(by=[pd.Grouper(key="Date", freq="1M"), "Category"])["Amount"]df_grouped = df_grouped.sum().reset_index()
Finally, using just one line of Plotly Express, we can create a line chart with the grouped DataFrame as input.
px.line(df_grouped, x="Date", y="Amount", color="Category")
Plotly Express allows us to come up with the above chart with just one line, where we input our DataFrame, x-axis values, y-axis values, and optionally the color parameter so have multiple colored lines in the chart (one for each category).
The chart looks pretty good, but as you can see it’s a bit difficult to compare the two categories. It intuitively makes sense, since you would expect that people, in general, spend more money on groceries than on restaurant trips every month. (Also, we hardcoded this logic into the code that generates this sample data- you can double-check it above.)
Next, let’s take a look at implementing a dual-axis to make it easier to compare the two different categories of spending.
First, we need to create an empty subplots figure using make_subplots (which we imported earlier). We’ll also define two variables to name our target categories.
# making dual axis and defining categoriesfig = make_subplots(specs=[[{"secondary_y": True}]])category_1 = "Groceries"category_2 = "Restaurant"
We’re not outputting anything yet, but it’s good to note that in the make_subplots method we’re passing "secondary_y": True inside specs to make sure we can properly implement the dual-axis later on.
Next, we’ll manually create the first line in our line chart.
# creating first plotfig.add_trace(    go.Scatter(        y=df_grouped.loc[df_grouped["Category"]==category_1, "Amount"],         x=df_grouped.loc[df_grouped["Category"]==category_1, "Date"],         name=category_1    ),    secondary_y=False,)
Before, using Plotly Express made it really easy for us to just pass one line of code to generate everything. Using the regular Plotly library means we’ll need to write a bit more code. Above, we’re using the add_trace method on the fig object we defined earlier to add the data from our previously grouped DataFrame. We also imported plotly.graph_objects as go earlier, so we can pass in the x and y column values. Finally, we set secondary_y to False, since this is just the first line in the chart.
If you run fig.show(), you should see something like this:
It’s a bit plain, but not bad so far! Right now, we just have the Groceries data. To add the second line with Restaurant data, we’ll run the following.
# creating next plotfig.add_trace(    go.Scatter(        y=df_grouped.loc[df_grouped["Category"]==category_2, "Amount"],         x=df_grouped.loc[df_grouped["Category"]==category_2, "Date"],         name=category_2    ),            secondary_y=True,)
It’s almost the exact same code, except we use category_2 and pass in secondary_y=True instead. Then, if you run fig.show()again, you should see something like this:
Looking even better! Unlike before, we can more easily see how the two categories of spending data behave over time relative to each other. This is just randomly generated data, but you can see in some months that when Groceries spending is high, Restaurant spending is comparatively lower (and vice versa).
We can make this a bit more clear by writing a bit more code to add axis labels like so:
fig.update_yaxes(title_text=category_1, secondary_y=False)fig.update_yaxes(title_text=category_2, secondary_y=True)
We’re using the same update_yaxes methods here, but passing in first False then True to the secondary_y parameter to appropriately label both axes.
This is pretty great so far, but I think we can clean things up a bit and move all the logic into a single function suitable for reuse. Here’s my implementation:
def create_dual_axis_graph(input_df, *args):    # making dual axis initial fig    dual_axis_fig = make_subplots(specs=[[{"secondary_y": True}]])        # defining categories from kwargs    categories = [*args]    assert len(categories) == 2, f"Must only provide two categories. You provided {len(categories)}."    # creating graph with loop    for count, category in enumerate(categories):        dual_axis_fig.add_trace(            go.Scatter(                y=input_df.loc[input_df["Category"]==category, "Amount"],                 x=input_df.loc[input_df["Category"]==category, "Date"],                 name=category            ),            secondary_y=count,        )    dual_axis_fig.update_yaxes(title_text=category, secondary_y=count)    return dual_axis_fig
We’re doing the exact same things as before, except we’re removing a bit of the repetition that was helpful for demonstration purposes. The create_dual_axis_graph function takes an input_df as the main argument (where you would provide your already grouped DataFrame like we did before), then *args as the names of the two categories you’d like to examine.
We put the *args in a list (and also check that the list only has two elements), then loop through that list and use the add_trace method again to add in the data for the x and y axes. We’ll also make use of enumerate in this looping process so either 0 or 1 (which are Boolean) can be passed into the secondary_y parameter of add_trace and update_yaxes, just like we did earlier.
Running the function should just take one line like this:
create_dual_axis_graph(df_grouped, "Groceries", "Restaurant").show()
This will give you the exact same dual-axis graph as before. Just to show what an incorrect implementation would look like, you could try this out:
create_dual_axis_graph(df_grouped, "Groceries", "Restaurant", "Appliances").show()
We defined an AssertionError earlier to make sure that only two arguments were passed into the list of categories. You could also define a way more comprehensive data validation step, like checking if the DataFrame had the right columns, if the column values had the right data types, if the categories provided were actually in the DataFrame, and much more. For this demo, however, the above function will suffice.
And that’s all, folks!
I hope this quick (semi end-to-end) data analysis and visualization demonstration was helpful for you! I like the dual-axis graph because it makes it really easy to compare two categories that aren’t necessary on the same scale, and because Plotly Express didn’t have a native way to do so, I thought it would be great to share this implementation with everyone.
Best of luck trying this out and using it on your own data!
Thanks a bunch again for reading! If you’re thinking about becoming a paying member on Medium, I’d really appreciate it if you sign up using my referral link below! This would let me directly receive a portion of your membership fees, so it would be a big help.
byrondolon.medium.com
More by me:- Check for a Substring in a Pandas DataFrame- Conditional Selection and Assignment With .loc in Pandas- 2 Easy Ways to Get Tables From a Website With Pandas- 5 (and a half) Lines of Code for Understanding Your Data with Pandas- Top 4 Repositories on GitHub to Learn Pandas