Net Worth

The main idea here is that, instead of visualizing with a regular pie chart, an interactive sunburst chart with extra tooltips (hover texts) would make inspecting different equity positions more intuitive and fun.

Create a randomized portfolio

To demonstrate this, I randomly selected 30 tickers from SP500 (a full list is available at DataHub) and added 2 ETFs to the list:

import pandas as pd

symbols_list = pd.read_csv("../src/data/random_samples_sp500.csv")
symbols_list.set_index("Category")
Symbol Name Sector
Category
Stock DXC DXC Technology Information Technology
Stock UAL United Airlines Holdings Industrials
Stock LHX L3Harris Technologies Industrials
Stock LNC Lincoln National Financials
Stock MTB M&T Bank Corp. Financials
Stock DLR Digital Realty Trust Inc Real Estate
Stock KR Kroger Co. Consumer Staples
Stock EBAY eBay Inc. Consumer Discretionary
Stock PH Parker-Hannifin Industrials
Stock WRK WestRock Materials
Stock FE FirstEnergy Corp Utilities
Stock AON Aon plc Financials
Stock LRCX Lam Research Information Technology
Stock CPRT Copart Inc Industrials
Stock NKE Nike Inc. Consumer Discretionary
Stock ROL Rollins Inc. Industrials
Stock ABBV AbbVie Inc. Health Care
Stock WYNN Wynn Resorts Ltd Consumer Discretionary
Stock JNPR Juniper Networks Information Technology
Stock MMM 3M Company Industrials
Stock LVS Las Vegas Sands Consumer Discretionary
Stock IBM International Business Machines Information Technology
Stock GRMN Garmin Ltd. Consumer Discretionary
Stock BRK.B Berkshire Hathaway Financials
Stock BDX Becton Dickinson Health Care
Stock TJX TJX Companies Inc. Consumer Discretionary
Stock KMB Kimberly-Clark Consumer Staples
Stock HPQ HP Inc. Information Technology
Stock MPC Marathon Petroleum Energy
Stock SLB Schlumberger Ltd. Energy
ETF VTI Vanguard Total Stock Market Index
ETF BND Vanguard Total Bond Market Bond

Now we can add some fictional positions based on these ticker symbols using the module yfinance:

import time
import yfinance as yf

def get_stock_info(symbol):
    time.sleep(3)
    stock = yf.Ticker(symbol.replace(".", "-"))
    print(f"\rLooking {symbol:s}...", end="")
    return stock.info

portfolio = symbols_list.copy()
portfolio["Info"] = portfolio["Symbol"].apply(lambda x: get_stock_info(x))

Since this process may take a while to run, I will continue by directly loading the result stored in a dataframe. You could check out the dictionaries stored under the “Info” column for each ticker but we will only use three of them to create a mock investment portfolio: “previousClose”, “fiftyTwoWeekLow” and “fiftyTwoWeekHigh”.

import random

# set the seed to make the result reproducible
random.seed(110)

portfolio = pd.read_pickle("../src/data/portfolio.pkl")

# assign a random number of shares to each symbol
portfolio["Quantity"] = portfolio["Symbol"].apply(lambda x: random.randint(2, 10))

# assign market value based on previousClose
portfolio["Market Value"] = portfolio["Info"].apply(lambda x: x["previousClose"])*portfolio["Quantity"]

# assign cost basis based on random price between 52 low and high
portfolio["Cost Basis"] = portfolio["Info"].apply(lambda x: random.uniform(x["fiftyTwoWeekLow"], x["fiftyTwoWeekHigh"]))*portfolio["Quantity"]

# calculate the gain/loss
portfolio["Gain/Loss"] = portfolio["Market Value"] - portfolio["Cost Basis"]

Now we can drop the “Info” column and check how the table looks like:

portfolio.drop(columns="Info", inplace=True)
portfolio.head()
Symbol Name Sector Category Quantity Market Value Cost Basis Gain/Loss
0 DXC DXC Technology Information Technology Stock 8 236.80 108.395839 128.404161
1 UAL United Airlines Holdings Industrials Stock 5 216.45 237.477996 -21.027996
2 LHX L3Harris Technologies Industrials Stock 8 1445.60 1298.143234 147.456766
3 LNC Lincoln National Financials Stock 9 462.69 331.289426 131.400574
4 MTB M&T Bank Corp. Financials Stock 6 865.44 966.269290 -100.829290

Looks good. Let’s we can add some reserve cash to the portfolio as well:

debit_pos = pd.DataFrame([["", "", "Debit", "Cash", 0, 2000.00, 0, 0]], columns=portfolio.columns)
sav_pos = pd.DataFrame([["", "", "Savings", "Cash", 0, 1000.00, 0, 0]], columns=portfolio.columns)
portfolio = pd.concat([portfolio, debit_pos, sav_pos])

portfolio.iloc[-5:, :]
Symbol Name Sector Category Quantity Market Value Cost Basis Gain/Loss
29 SLB Schlumberger Ltd. Energy Stock 9 223.56 231.512143 -7.952143
30 VTI Vanguard Total Stock Market Index ETF 6 1195.50 748.183640 447.316360
31 BND Vanguard Total Bond Market Bond ETF 4 349.08 356.440459 -7.360459
0 Debit Cash 0 2000.00 0.000000 0.000000
0 Savings Cash 0 1000.00 0.000000 0.000000

Create the sunburst chart

Now that’s done, we can finally start making the chart. In order to make a sunburst plot with plotly, we need to construct a hierarchical dataframe. An example can be found here. I adapted the sample function from the official site a bit to our needs here (click on “+” to reveal the code).

# HIDE CODE
def build_hierarchical_dataframe(df, levels, value_column,
                                 custom_column=None, cal_tot=True):
    """Build a hierarchy of levels for Sunburst and Treemap charts.
    """
    df_all_trees = pd.DataFrame(columns=['id', 'parent', 'value', 'custom'])
    for i, level in enumerate(levels):
        df_tree = pd.DataFrame(columns=['id', 'parent', 'value', 'custom'])
        dfg = df.groupby(levels[i:]).sum()
        dfg = dfg.reset_index()
        df_tree['id'] = dfg[level].copy()
        if i < len(levels) - 1:
            df_tree['parent'] = dfg[levels[i+1]].copy()
        else:
            if cal_tot:
                df_tree['parent'] = 'Total'
        df_tree['value'] = dfg[value_column]
        if custom_column:
            df_tree['custom'] = dfg[custom_column]
        df_all_trees = df_all_trees.append(df_tree, ignore_index=True)
    if cal_tot:
        total = pd.Series(dict(id='Total', parent='',
                                  value=df[value_column].sum()))
        df_all_trees = df_all_trees.append(total, ignore_index=True)

    return df_all_trees

So now we are ready to visualize the portfolio in an interactive sunburst plot. Try click on different wedges to interact with them.

# HIDE CODE
import plotly.graph_objects as go

# define the structure
levels = ["Symbol", "Sector", "Category"]
value_column = "Market Value"
custom_column = "Gain/Loss"

# reform the dataframe
df_all_trees = build_hierarchical_dataframe(portfolio, levels, value_column, custom_column=custom_column, cal_tot=True)

# add additional information to the dataframe
df_all_trees['value_dollar'] = df_all_trees['value'].apply(
    lambda x: f"$ {x:,.0f}")
df_all_trees['change'] = df_all_trees['custom'].apply(
    lambda x: f"$ {x:,.0f}" if x != 0 else "-")
df_all_trees['change_per'] = (df_all_trees['custom']/(
    df_all_trees['value']-df_all_trees['custom'])).apply(
        lambda x: f"{x*100:.1f}%" if x else '-').replace("nan", "-")
df_all_trees['change'] = df_all_trees['change'].replace("$ nan", "-")
df_all_trees['change_per'] = df_all_trees['change_per'].replace(
    "nan%", "-")
_df = portfolio[["Symbol", "Name"]]
df_all_trees['detail'] = df_all_trees["id"].apply(
    lambda x: _df[_df["Symbol"] == x][
        "Name"].values if x.isupper() else '')
df_all_trees["detail"] = df_all_trees["detail"].apply(
    lambda x: x[0] if len(x) else x)

# make the plot
fig = go.Figure(go.Sunburst(
    labels=df_all_trees['id'],
    parents=df_all_trees['parent'],
    values=df_all_trees['value'],
    branchvalues='total',
    textinfo='label+percent entry',
    customdata=df_all_trees[['value_dollar', 'change',
                             'change_per', 'detail']],
    maxdepth=4,
    # customize tooltip
    hovertemplate="<b>%{label}</b><br><br>"
                + "Market value: %{customdata[0]}<br>"
                + "Change: %{customdata[1]} (%{customdata[2]})<br>"
                + "<extra>%{customdata[3]}</extra>"    

))

fig.update_traces(textfont_size=14)
fig.update_layout(margin=dict(t=10, b=35, r=10, l=10))
fig.show()

Conclusion

I am a bit surprised how legit this randomized allocation looks at first glance (I strongly discourage to invest this way though!). The tooltips (or hover texts) can be used to display additional infos that can’t fit into the wedges. In the example above, I put value change, percentage change and ticker description in different sections of the tooltips. There are many customization options and can be found in the documentations here. Up next we will see how we can use the same dataframe to build a treemap to visualize investment performance.