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.