Tutorial by Justin Lumpkin and Daniel Wei
"Logrolling" is a historical term refering to the trading of political votes. At present, the term "horse trading" is more commonly used to describe the same thing.
This project analyzes disclosures from the Stop Trading on Congressional Knowledge (STOCK) Act passed into law in 2012. The STOCK Act requires congresspeople to report stock trades (as well as some other financial transactions) over $1000 within 45 days of the trade date.
At the start of 2020, several Congress members were embroiled in a scandal and legal investigation over alleged insider trading after receiving briefings about the coronavirus.
Prior reports such as those by Unusual Whales have considered these disclosures. Our methodology differs in that we consider the full range of possible purchase/sale values rather than assuming only the upperbound as Unusual Whales reports does. We also consider the big picture of the full time period in which the disclosure requirements have been in place where Unusual Whales mainly considers one year at a time. Furthermore, Unusual Whales has a vested interest in marketing their product (an ETF based on the stocks that politicians trade). Consequently, we would like to independently verify/qualify their results with a long-term focus in our methodology. Our data set only includes trades by senators, but Unusual Whales considers both senators and members of the House of Representatives. Lastly, we focus exclusively on stocks whereas Unusual Whales' analysis includes ETFs and other financial assets.
The STOCK Act is not considered to be very strict. "Congress and top Capitol Hill staff have violated the STOCK Act hundreds of times" by failing to disclose transactions on time according to a Business Insider report. The fines for late reporting are relatively small (starting at $200), and there is no public record of whether these fines were paid. It is difficult (if not impossible) to determine the extent to which this has led to underreporting in our data set.
In response to all of these scandals, at least 14 separate bills to ban or limit congressional stock trading are circulating in the House or Senate. With the most successful proposals currently accumulating about 60 cosponsors.
Our goal is to understand whether the senators that trade stocks are able to consistently beat the market.
This data set on kaggle compiles all of the trade disclosures from US senators into a csv format.
We use the following libraries:
pandas
for storing data in dataframesnumpy
for fast numerical computationsyfinance
and pandas_datareader.data
to access historical stock pricesmatplotlib
, matplotlib.pyplot
, and seaborn
to create graphical visualizationsrequests
and bs4.BeautifulSoup
for retrieving data from an html formatdatetime
for comparable date formatre
for cleaning datascipy.optimize
for exponential regressioncollections.default_dict
to improve the readability of our code when storing computed values in dictionariescopy
to avoid overwriting prior computations# install yfinance and upgrade dependencies
!pip install yfinance --quiet 2> /dev/null
!pip install --upgrade pandas --quiet 2> /dev/null
!pip install --upgrade pandas-datareader --quiet 2> /dev/null
# data structures and quality of life tools
import pandas as pd
import numpy as np
import re
import scipy.optimize
import datetime as dt
from collections import defaultdict
import copy
# libraries to collect data
import yfinance as yf
import requests
from bs4 import BeautifulSoup
import pandas_datareader.data as pdr
# libraries for visualization
import matplotlib.pyplot as plt
import matplotlib as mpl
# from matplotlib.pyplot import figure #Is this used?
import seaborn as sns
Next we read the financial disclosures from a mirror of the original kaggle data set.
# retrieve data
url = 'https://raw.githubusercontent.com/danielwei816/Logrollers/main/SenatorCleaned.csv'
disclosures = pd.read_csv(url)
# remove redundant index column
disclosures.drop("Unnamed: 0", inplace = True, axis = 1)
disclosures.head()
Name | Transaction.Date | Owner | Ticker | Asset.Name | Asset.Type | Type | Amount | Comment | |
---|---|---|---|---|---|---|---|---|---|
0 | Sheldon Whitehouse | 11/25/2014 | Self | M | Macy's, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- |
1 | Sheldon Whitehouse | 12/18/2014 | Self | BEAV | B/E Aerospace Inc. (NASDAQ) | Stock | Sale (Full) | 15001.0 | -- |
2 | Sheldon Whitehouse | 12/18/2014 | Self | KORS | Michael Kors Holdings Limited (NYSE) | Stock | Sale (Partial) | 1001.0 | -- |
3 | Sheldon Whitehouse | 12/18/2014 | Self | T | AT&T, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- |
4 | Sheldon Whitehouse | 12/18/2014 | Self | KEYS | Keysight Technologies, Inc. (NYSE) | Stock | Sale (Full) | 1001.0 | Agilent Tech common stock completed a spinoff ... |
One row in our data set does not record the amount. After some investigation through the periodic transactions database we found that this trade was over $50,000,000.
Using the query:
First Name
: James
Last Name
: Inhofe
Report Type
: Periodic Transactions
Date Range
: 10-01-2015 to 10-31-2015
the single result is a report that notes a sale of WFM on October 5th, 2015 of over \$50,000,000. This is the only report of this amount in the data set, so naturally, we were suspicious. [OpenSecrets](https://www.opensecrets.org/personal-finances/james-inhofe/net-worth?cid=N00005582&year=2018) puts Jim Inhofe's current net worth at about \$5,000,000. It seems unlikely that he lost at least 90% of his net worth since 2015, but we could not find a more concrete reason to dispute this report, so we could not justify removing it from the data set.
disclosures.loc[4825]
Name James M Inhofe Transaction.Date 10/5/2015 Owner Self Ticker WFM Asset.Name WFM Asset.Type Stock Type Sale (Full) Amount NaN Comment -- Name: 4825, dtype: object
Some trades did not have a stock ticker. We remove these rows from the data set because we cannot determine what stock these trades are related to. This causes problems when computing the number of shares retained that senators have retained after partial transactions.
Our analysis focuses on the relationship between senator stock trades and the wider movement of the US Stock Market. Consequently, we filter out all data points that do not involve stock trades.
# isolate stock trades
trades = disclosures[disclosures['Asset.Type'] == "Stock"]
# manually mark Inhofe's row
trades.at[4825, 'Amount'] = 50000000
# remove all trades without a stock ticker
trades = trades[trades['Ticker'].str.contains('--') == False]
trades.reset_index(inplace = True)
# drop previous index column
trades = trades.drop(['index'], axis=1)
len(trades)
12345
These changes leave us with 12345 disclosures.
Next, we'll ensure that the columns in our trades
Dataframe have reasonable names and data types for our analysis.
We prefer the following updated names to clarify what each column represents without unnecessary information. Note that we replace the Amount column with the name "Min_Value" due to the transaction ranges discussed later.
trades = trades.rename(columns={
'Transaction.Date': 'Date',
'Asset.Name': 'Asset_Name',
'Asset.Type': 'Asset_Type',
'Type': 'Transaction_Type',
'Amount': 'Min_Value'
})
trades.head()
Name | Date | Owner | Ticker | Asset_Name | Asset_Type | Transaction_Type | Min_Value | Comment | |
---|---|---|---|---|---|---|---|---|---|
0 | Sheldon Whitehouse | 11/25/2014 | Self | M | Macy's, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- |
1 | Sheldon Whitehouse | 12/18/2014 | Self | BEAV | B/E Aerospace Inc. (NASDAQ) | Stock | Sale (Full) | 15001.0 | -- |
2 | Sheldon Whitehouse | 12/18/2014 | Self | KORS | Michael Kors Holdings Limited (NYSE) | Stock | Sale (Partial) | 1001.0 | -- |
3 | Sheldon Whitehouse | 12/18/2014 | Self | T | AT&T, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- |
4 | Sheldon Whitehouse | 12/18/2014 | Self | KEYS | Keysight Technologies, Inc. (NYSE) | Stock | Sale (Full) | 1001.0 | Agilent Tech common stock completed a spinoff ... |
Below are the different types of transactions present in our data set:
trades['Transaction_Type'].unique()
array(['Purchase', 'Sale (Full)', 'Sale (Partial)', 'Exchange'], dtype=object)
Notice that all sales are either "Sale (Full)" or "Sale (Partial)". Let's simplify these naming schemes into "Full" and "Partial".
trades['Transaction_Type'] = trades['Transaction_Type'].apply(lambda item:
("Full" if item == "Sale (Full)"
else "Partial" if item == "Sale (Partial)"
else item)
)
trades['Transaction_Type'].unique()
array(['Purchase', 'Full', 'Partial', 'Exchange'], dtype=object)
len(trades[trades["Transaction_Type"] == "Exchange"])
86
86 transactions in our data set are listed as exchanges. An exchange occurs when shares of one stock are traded directly for another stock. This most often occurs during company mergers or acquisitions or when a firm undergoes a name change.
Again, we'll be dealing with exchanges that occur within one transaction. To integrate these into our wider data set, we'll break these exchanges into two transactions: a purchase and a sale. Afterward, we'll simply insert these transactions into the trades
dataframe.
Exchange transaction formatting is inconsistent. Some exchanges are listed in a single row with a line break between the ticker symbols in the Ticker
column. Other exchanges have a pair of entries corresponding to each side of the trade with some indication in the comment which direction the trade moved.
In many cases, the data is not entirely clear on which stock was given and which stock was received. We were able to include the trades that listed both the exchanged and received stock in one row, but decided to drop the rest of the rows due to the formatting inconsistencies.
Recommendation:
We recommend auditing the existing reports to clarify the data and issuing new requirements on how exchanges are reported so that future analysis can work through a consistent format.
# retrieve rows with an entire exchange
indexes = np.where((trades["Transaction_Type"] == "Exchange"))[0]
# create a regex that retrieves the new and old tickers + asset names
ticker_regex = re.compile("\^?(.*)\\r\\n\^?(.*)")
name_regex = re.compile("(.*) \(.*\\r\\n(.*) \(.*")
for i in indexes:
# break the transaction into two matching trades
sale = trades.loc[i].copy()
purchase = trades.loc[i].copy()
# match the expected format
ticker_match = ticker_regex.match(trades.at[i, "Ticker"])
name_match = name_regex.match(trades.at[i, "Asset_Name"])
# remove the exchange from the data set regardless of format
trades = trades.drop([i])
if ticker_match != None:
# add the purchase/sale pair if there was a match
sale['Ticker'] = ticker_match.group(1)
purchase['Ticker'] = ticker_match.group(2)
sale['Asset_Name'] = name_match.group(1)
purchase['Asset_Name'] = name_match.group(2)
sale['Transaction_Type'] = "Full"
purchase['Transaction_Type'] = "Purchase"
trades = trades.append(sale)
trades = trades.append(purchase)
# reset the table indices
trades.reset_index(inplace = True)
trades.drop(['index'], axis = 1, inplace = True)
trades.tail()
Name | Date | Owner | Ticker | Asset_Name | Asset_Type | Transaction_Type | Min_Value | Comment | |
---|---|---|---|---|---|---|---|---|---|
12320 | Thad Cochran | 12/5/2017 | Self | APTV | Aptiv PLC | Stock | Purchase | 1001.0 | -- |
12321 | Thomas R Carper | 2/5/2021 | Spouse | DD | DuPont de Nemours, Inc. | Stock | Full | 1001.0 | -- |
12322 | Thomas R Carper | 2/5/2021 | Spouse | IFF | International Flavors & Fragrances Inc. | Stock | Purchase | 1001.0 | -- |
12323 | William F Hagerty IV | 5/17/2021 | Self | FL4.SG | FLIR SYSTEMS INC. Registered Sh | Stock | Full | 15001.0 | FLIR Systems Merger With Teledyne Asset Held i... |
12324 | William F Hagerty IV | 5/17/2021 | Self | TDY | Teledyne Technologies Incorporated | Stock | Purchase | 15001.0 | FLIR Systems Merger With Teledyne Asset Held i... |
The rows displayed above are examples of exchange transactions that were split into corresponding purchase and sale pairs.
Our current data types are as follows:
trades.dtypes
Name object Date object Owner object Ticker object Asset_Name object Asset_Type object Transaction_Type object Min_Value float64 Comment object dtype: object
Date column:
Most of the columns make sense as strings, but we would prefer to have a datetime
object for the date column to make it easier to interface with Yahoo Finance's data set.
Min_Value column: The STOCK Act does not require disclosures of exact transaction amounts. As a result, the 'Min_Value' column in the data set corresponds to the lower bound of the disclosure range that the trade belongs to. We cast it to a float.
trades['Date'] = pd.to_datetime(trades['Date'])
trades['Min_Value'] = pd.to_numeric(trades['Min_Value'])
trades.dtypes
Name object Date datetime64[ns] Owner object Ticker object Asset_Name object Asset_Type object Transaction_Type object Min_Value float64 Comment object dtype: object
Our data spans nearly 10 years.
trades['Date'].max()-trades['Date'].min()
Timedelta('3531 days 00:00:00')
The table contains a comments column where senators can add any additional notes to their disclosure that they deem important to include. Because they are optional, we expect a relatively small fraction of disclosures to contain them.
About 7% (842/12325) of disclosures contained a comment. While this is a small minority, it prompted some further exploration in which we read each of the unique comments to determine if any factors were ones we wanted to include in our analysis. Many comments are repeated with only 124 unique comments in the data set.
Some example comments are displayed below.
comments = trades[trades['Comment'] != "--"]["Comment"]
print(comments.unique()[0:10])
print(f"Total comments: {len(comments)}\
\nUnique comments: {len(comments.unique())}\
\nTotal rows: {len(trades)}")
['Agilent Tech common stock completed a spinoff on 10/22/14 to Keysight Tech' 'KMB completed a spinoff of its health care business business now known as HYH on 11/01/14' 'Dividend Reinvestment' '2015 Issued IR RSUs vested and actual shares issued; part of shares sold w/ remainder in UBS CSSP' '2016 Issued IR RSUs vested and actual shares issued; part of shares sold w/ remainder in UBS CSSP' 'Transaction is due to sale of a security from redemption of incentive units vested on April 21, 2017' 'Complete Disposition of Ingersoll Rand Stock' 'Complete Disposition of Vulcan Materials Stock' 'Mandatory Cash Merger' 'WTR-Aqua America, Inc name change to WTRG-Essential Utilities, Inc on 2/3/2020.'] Total comments: 842 Unique comments: 124 Total rows: 12325
After looking through the full list of unique comments, we did not find any that impacted our methodology, but some raised questions.
The comments roughly fall into the following categories. We create these categories qualitatively based on reading the comments, so any analysis over these attributes would require manually annotating the comments with their class. Additionally, because these comments are optional, it is possible that some of the trades with no listed comment fall into one of these classes. Similarly, some trades may belong to multiple categories. A full list of unique comments is included in the Appendix.
Again, the questions in this section would require manual coding of data into one of these classes. As a result, this is something that we leave to future work.
Senators are not required to disclose the exact amount of money that was exchanged in a stock trade. Rather, they report the range in which their transaction amount fell into. Our current data set only reflects the lower bound of this range. We update our data set to reflect the full range by adding a lower bound, upper bound, and average transaction amount. This will allow us to determine conservative and liberal estimates of senator returns.
We use the average transaction amount to estimate an expected value of the return. This is imperfect because we do not know the distribution of trade amounts within each group. In general, one would expect that large trades are less common than small trades, meaning that each range is skewed right and that averaging the endpoints overestimates the average trade amount in each range. On the other hand, senators may be intentionally making multiple trades on the high end of a range to avoid having to report a higher amount range (which potentially corresponds to higher scrutiny). Furthermore, any number of interactions could affect the true value. As a result, we find that using the simplest approximation (while noting its limitations) is most appropriate.
The ranges for financial disclosures are as follows:
1. $1 - $1,000
2. $1,001 - $15,000
3. $15,001 - $50,000
4. $50,001 - $100,000
5. $100,001 - $250,000
6. $250,001 - $500,000
7. $500,001 - $1,000,000
8. $1,000,001 - $5,000,000
9. $5,000,001 - $25,000,000
10. $25,000,001 - $50,000,000
11. Over $50,000,000
Note that, as previously mentioned, senators are not required to report transactions below 1001 dollars. Such transactions are denoted in the Comments.
Market Watch estimates that all congress people together sold $175 million in 2021.
Our only trade in the 'over \$50,000,000' range was a sale. As a result, we use $175 million as an upper bound for a possible stock trade.
range_map = {
1: 1000,
1001: 15000,
15001: 50000,
50001: 100000,
100001: 250000,
250001: 500000,
500001: 1000000,
1000001: 5000000,
5000001: 25000000,
25000001: 50000000,
50000000: 175000000
}
# correct amount lower bound for trades under $1000 as determined by the comment
trades['Min_Value'] = trades.apply(lambda row:
1 if 'less than' in row['Comment'].lower() else row['Min_Value'], axis=1)
# compute maximum and average transaction amounts
trades['Max_Value'] = trades.apply(lambda row: range_map[row['Min_Value']], axis=1)
trades['Avg_Value'] = (trades['Min_Value'] + trades['Max_Value']) / 2
trades.head()
Name | Date | Owner | Ticker | Asset_Name | Asset_Type | Transaction_Type | Min_Value | Comment | Max_Value | Avg_Value | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | Sheldon Whitehouse | 2014-11-25 | Self | M | Macy's, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- | 15000 | 8000.5 |
1 | Sheldon Whitehouse | 2014-12-18 | Self | BEAV | B/E Aerospace Inc. (NASDAQ) | Stock | Full | 15001.0 | -- | 50000 | 32500.5 |
2 | Sheldon Whitehouse | 2014-12-18 | Self | KORS | Michael Kors Holdings Limited (NYSE) | Stock | Partial | 1001.0 | -- | 15000 | 8000.5 |
3 | Sheldon Whitehouse | 2014-12-18 | Self | T | AT&T, Inc. (NYSE) | Stock | Purchase | 1001.0 | -- | 15000 | 8000.5 |
4 | Sheldon Whitehouse | 2014-12-18 | Self | KEYS | Keysight Technologies, Inc. (NYSE) | Stock | Full | 1001.0 | Agilent Tech common stock completed a spinoff ... | 15000 | 8000.5 |
First we determine the frequency of trades of each of the various amounts. Note that the y-axis is on a log-scale. We find that the number of trades decline rapidly as the size of the trade increases with the exception of the \$1-\$1000 range. This is almost certainly because reporting of trades under \$1000 is not required by the STOCK Act. We show exponential regression curves (which appear linear due to the log scale). The green curve models all of the data whereas the orange curve models only required disclosures. We fit both models because we expect the optional disclosures to be dramatically underreported.
# obtain the sorted list of transaction amounts
ranges = list(set(trades['Min_Value']))
ranges.sort()
# find the frequency of each transaction amount
counts = [list(trades['Min_Value']).count(x) for x in ranges]
# fit exponential models to this data including and excluding the $1-$1000 range
xs = np.arange(0, len(counts))
def fit_exp(x, a, b):
return a * np.exp(-b * x)
init = (1e4, 1) # start with values near those we expect
params1, cv = scipy.optimize.curve_fit(fit_exp, xs[1:], counts[1:], init)
params2, cv = scipy.optimize.curve_fit(fit_exp, xs, counts, init)
# create x axis labels
labels = [f"\${x:,} - \${range_map[x]:,}" for x in ranges]
f, ax = plt.subplots(figsize=(14, 8))
# plot bars and regression lines
plt.bar(xs, counts, tick_label = labels, log = True)
plt.plot(xs, fit_exp(xs, *params1), label = "Above $1000", color = "orange")
plt.plot(xs, fit_exp(xs, *params2), label = "Full Model", color = "green")
# format and label the graph
plt.xticks(ticks = xs, rotation = 85)
plt.ylim(0.5, 1e5)
plt.title("Frequency of Trades by Amount")
plt.xlabel("Amount Range (US Dollars)")
plt.ylabel("Frequency")
plt.legend()
plt.show()
# print equations
print(f"Modeling all of the data gives\n\t{params2[0]:.2f} * e ^ (-{params2[1]:.2f}x)")
print(f"\nModeling required disclosures gives\n\t{params1[0]:.2f} * e ^ (-{params1[1]:.2f}x)")
Modeling all of the data gives 3774.76 * e ^ (-0.29x) Modeling required disclosures gives 42389.44 * e ^ (-1.51x)
The orange line appears to have a much better overall fit. While it appears to be far off for the rightmost bars, this is due to the log scale of the y-axis.
While regressing against the index of an ordinal $x$ variable is not particularly meaningful, the orange line is consistent with small transactions being much more common than large transactions. This suggests that we should expect a much larger share of the trades to be in the \$1 to \$1000 range if all trades were required to be disclosed.
This contextualizes our later findings because our computations assume that we have access to all of the trades over the time period of our data set.
Recommendation:
We would like to see new reporting requirements so that senators must report all individual stock trades regardless of the amount. This will improve the accuracy of our return computations and allow future researchers to compare these trades to larger ones (in terms of the percent return or other metrics).
# get unique senator names
names = list(set(trades['Name']))
# find the frequency of each senator in the data
counts = [list(trades['Name']).count(x) for x in names]
# sort from largest frequency to smallest frequency
trade_freq = dict(sorted(zip(names, counts), \
key = lambda item: item[1], reverse=True))
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(trade_freq.keys(), trade_freq.values())
# format and label the graph
plt.xticks(ticks = list(trade_freq.keys()), rotation = 85)
plt.title("Frequency of Trades by Senator (All Senators)")
plt.xlabel("Senator")
plt.ylabel("Number of Trades")
plt.show()
# print key statistics
print(f"{len(names)} unique senators")
print(f"The most frequent traders have made as many as {max(counts)} trades")
55 unique senators The most frequent traders have made as many as 3537 trades
Only 55 total senators have disclosed trades since the passage of the STOCK Act. The most frequent traders in the data set have made about one trade per calendar day over the roughly ten years that we have data (of course, the market is not open every day).
Because so many senators report a very small number of trades, I plot the top 7 traders and the rest of the senators separately.
top_traders = 7
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(list(trade_freq.keys())[:top_traders], list(trade_freq.values())[:top_traders])
# format and label the graph
plt.xticks(ticks = list(trade_freq.keys())[:top_traders], rotation = 85)
plt.title("Frequency of Trades by Senator (High Frequency)")
plt.xlabel("Senator")
plt.ylabel("Number of Trades")
plt.show()
# print the top traders with number of trades
[(k,v) for k,v in trade_freq.items()][:top_traders]
[(' Robert P Corker Jr.', 3537), (' David A Perdue Jr', 3410), (' Thad Cochran', 619), (' Shelley M Capito', 598), (' Sheldon Whitehouse', 560), (' Thomas R Carper', 553), (' Pat Roberts', 528)]
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(list(trade_freq.keys())[top_traders:], list(trade_freq.values())[top_traders:])
# format and label the graph
plt.xticks(ticks = list(trade_freq.keys())[top_traders:], rotation = 85)
plt.title("Frequency of Trades by Senator (Low Frequency)")
plt.xlabel("Senator")
plt.ylabel("Number of Trades")
plt.show()
Astonishingly, Bob Corker and David Perdue made roughly 56% (6947/12325) of the disclosed trades in our data set. This is an important limitation on the applicability of our findings to other senators.
Using the conservative estimates, senators have traded over \$200 million during the disclosure period. Jim Inhofe is the top trader at over \$58 million!
# get unique senator names
names = list(set(trades['Name']))
# find the volume for each senator
volumes = [trades[trades['Name'] == x]['Min_Value'].sum() for x in names]
# sort from largest volume to smallest volume
trade_vol = dict(sorted(zip(names, volumes), \
key = lambda item: item[1], reverse=True))
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(trade_vol.keys(), trade_vol.values(), log = False)
# format and label the graph
plt.xticks(ticks = list(trade_vol.keys()), rotation = 85)
ax.ticklabel_format(axis = 'y', style = 'plain')
ax.yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter('{x:,.0f}'))
plt.title("Volume of Trades by Senator (Conservative Transaction Amount Estimates)")
plt.xlabel("Senator")
plt.ylabel("Total US Dollars Transacted")
plt.show()
# print key statistics
print(f"Single senators have traded as much as ${max(volumes):,.2f}")
print(f"Senators traded a total of ${sum(volumes):,.2f}")
Single senators have traded as much as $58,042,259.00 Senators traded a total of $207,349,324.00
Using the average estimates, the Senate has traded nearly half a billion dollars led by Jim Inhofe with nearly \$126 million in trades during the disclosure period.
# get unique senator names
names = list(set(trades['Name']))
# find the volume for each senator
volumes = [trades[trades['Name'] == x]['Avg_Value'].sum() for x in names]
# sort from largest volume to smallest volume
trade_vol = dict(sorted(zip(names, volumes), \
key = lambda item: item[1], reverse=True))
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(trade_vol.keys(), trade_vol.values(), log = False)
# format and label the graph
plt.xticks(ticks = list(trade_vol.keys()), rotation = 85)
ax.ticklabel_format(axis = 'y', style = 'plain')
ax.yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter('{x:,.0f}'))
plt.title("Volume of Trades by Senator (Moderate Transaction Amount Estimates)")
plt.xlabel("Senator")
plt.ylabel("Total US Dollars Transacted")
plt.show()
# print key statistics
print(f"Single senators have traded as much as ${max(volumes):,.2f}")
print(f"Senators traded a total of ${sum(volumes):,.2f}")
Single senators have traded as much as $125,998,629.50 Senators traded a total of $497,987,162.00
Using the liberal estimates, senators as a whole have traded nearly \$800 million led by Jim Inhofe with over \$190 million in trades during the disclosure period.
# get unique senator names
names = list(set(trades['Name']))
# find the volume for each senator
volumes = [trades[trades['Name'] == x]['Max_Value'].sum() for x in names]
# sort from largest volume to smallest volume
trade_vol = dict(sorted(zip(names, volumes), \
key = lambda item: item[1], reverse=True))
# plot bars
f, ax = plt.subplots(figsize=(14, 8))
plt.bar(trade_vol.keys(), trade_vol.values(), log = False)
# format and label the graph
plt.xticks(ticks = list(trade_vol.keys()), rotation = 85)
ax.ticklabel_format(axis = 'y', style = 'plain')
ax.yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter('{x:,.0f}'))
plt.title("Volume of Trades by Senator (Liberal Transaction Amount Estimates)")
plt.xlabel("Senator")
plt.ylabel("Total US Dollars Transacted")
plt.show()
# print key statistics
print(f"Single senators have traded as much as ${max(volumes):,}")
print(f"Senators traded a total of ${sum(volumes):,}")
Single senators have traded as much as $193,955,000 Senators traded a total of $788,625,000
Even with the conservative estimates, the sheer volume of stock trading by just 55 individuals is immense. However, the variation of almost \$600 billion in the total trade volume illustrates one of our main concerns with this dataset. Because the transaction amount ranges are so wide, any conclusions that we draw hold a substantial degree of uncertainty.
We downloaded historical stock price data from the yfinance database. This took about 10 minutes to run, so we saved the data to a csv file and uploaded it to github for faster retrieval. Below is the code we originally used to retrieve the prices.
# stock_list = list(trades["Ticker"].unique())
# data = yf.download(stock_list, start=trades["Date"].min(), end=trades["Date"].max() + dt.timedelta(days=1))
# data['Adj Close'].reset_index().to_csv("tickers.csv")
The dataframe below has one date per row with the each ticker as a column.
price_history = pd.read_csv("https://raw.githubusercontent.com/danielwei816/Logrollers/main/tickers.csv")
price_history['Date'] = pd.to_datetime(price_history['Date'])
price_history.drop(['Unnamed: 0'], axis = 1, inplace = True)
price_history.tail()
Date | A | AA | AAGIY | AAL | AAN | AAON | AAPL | AAT | ABB | ... | YUM | YUMC | ZAYO | ZBH | ZION | ZIOP | ZM | ZMH | ZNGA | ZTS | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2495 | 2021-07-26 | 149.865753 | 38.875099 | 47.913841 | 22.080000 | 27.719999 | 60.000000 | 148.143631 | 35.804115 | 36.950001 | ... | 121.938988 | 64.216637 | NaN | 154.444763 | 50.569202 | NaN | 373.140015 | NaN | 10.35 | 200.258850 |
2496 | 2021-07-27 | 149.566513 | 38.186604 | 45.772606 | 21.459999 | 30.129999 | 59.869999 | 145.936234 | 35.960251 | 36.830002 | ... | 123.944145 | 60.990372 | NaN | 157.530762 | 50.765667 | NaN | 367.540009 | NaN | 10.14 | 201.305908 |
2497 | 2021-07-28 | 151.072662 | 38.146690 | 45.673016 | 21.629999 | 28.639999 | 60.840000 | 144.156403 | 36.253006 | 36.840000 | ... | 121.109268 | 62.105621 | NaN | 157.259888 | 51.433636 | NaN | 369.489990 | NaN | 10.25 | 202.702011 |
2498 | 2021-07-29 | 152.279572 | 39.284206 | 47.127060 | 21.170000 | 28.520000 | 62.110001 | 144.812653 | 36.321320 | 37.009998 | ... | 128.715027 | 62.135498 | NaN | 157.434021 | 51.905140 | NaN | 386.019989 | NaN | 10.21 | 203.549606 |
2499 | 2021-07-30 | 152.838135 | 40.062508 | 47.744534 | 20.379999 | 28.870001 | 62.150002 | 145.031403 | 36.038322 | 36.619999 | ... | 129.781799 | 61.926384 | NaN | 158.091843 | 51.227352 | NaN | 378.100006 | NaN | 10.10 | 202.133591 |
5 rows × 1199 columns
We are interested in computing the value of each senator's retained assets at the end of the data set.
To do this we access the price of the stock for each transaction, and we use the minimum, average, and maximum transaction amounts to determine the minimum, average, and maximum number of shares transacted respectively. $$number\_of\_shares = \cfrac{total\_value}{price}$$
trades["Min_Shares"] = [np.nan] * len(trades)
trades["Avg_Shares"] = [np.nan] * len(trades)
trades["Max_Shares"] = [np.nan] * len(trades)
for i, row in trades.iterrows():
ticker = row["Ticker"]
start = row["Date"]
# retrieve price for the stock on the transaction date
price_df = price_history[price_history['Date'] == row["Date"]].reset_index()
# if found, use this price to compute {min, avg, max} shares
if len(price_df) == 1:
price = price_df.loc[0, row["Ticker"]]
if not np.isnan(price):
trades.at[i, "Min_Shares"] = row["Min_Value"] / price
trades.at[i, "Avg_Shares"] = row["Avg_Value"] / price
trades.at[i, "Max_Shares"] = row["Max_Value"] / price
len(trades[~trades['Max_Shares'].isnull()])
10019
We are left with roughly 10,000 transactions that have shares numbers computed.
We are not able to compute data for tickers and dates that yfinance
does not have data for. This may occur if
yfinance
tracksyfinance
does not track the tickeryfinance
is missing data in the specified date range for the specified tickerKnowing the number of shares allows us to compute unrealized gains (the increase in value of assets that they have not yet sold). We can also determine (in some cases) that senators must have acquired some stock before the disclosure requirements went into effect.
Our approach to computing total returns is using the formula: $$return = total\_sales - total\_purchases + current\_holdings - initial\_holdings$$
The value of total_sales is computed by summing the (minumum, average, and maximum) value of each transaction with "Full" or "Partial" sale in the Transaction_Type
column. Similarly, total_purchases is computed by summing the possible values of each transaction with a "Purchase" action.
We compute the (minumum, average, and maximum) value of current holdings using the (minumum, average, and maximum) number of shares retained times the price on the last day that we have data for.
If we compute that a congress person has a negative number of shares retained, then we assume that they purchased that quantity of shares prior to the start of the disclosure requirements.
This approximation for shares retained (and for implied prior holdings) is imperfect because transactions under \$1000 are not reported and the minimum and maximum possible values can vary greatly. We cannot determine whether smaller transactions are causing errors in our estimates from this data.
First, we compute the difference between purchase and sale values. This is roughly equivalent to the realized gains (the profit from assets that have been sold) for each (senator, stock)
pair, but it also includes the cost of purchasing assets that have not yet been sold.
# nested dictionary: min_real_gains[senator][ticker]
# will return the minimum profit for that senator on that stock
# the default value of min_real_gains is a defaultdict with default 0
min_real_gains = defaultdict(lambda: defaultdict(float))
max_real_gains = defaultdict(lambda: defaultdict(float))
avg_real_gains = defaultdict(lambda: defaultdict(float))
# store total cost of purchasing stocks for each senator default is 0
min_cost = defaultdict(float)
max_cost = defaultdict(float)
avg_cost = defaultdict(float)
for (senator, stock), group in trades.groupby(['Name', 'Ticker']):
# sum purchase value
total_purchases_min = sum(group[group['Transaction_Type'] == 'Purchase']['Min_Value'])
total_purchases_max = sum(group[group['Transaction_Type'] == 'Purchase']['Max_Value'])
total_purchases_avg = sum(group[group['Transaction_Type'] == 'Purchase']['Avg_Value'])
# sum sale values
total_sales_min = sum(group[(group['Transaction_Type'] == 'Full') |
(group['Transaction_Type'] == 'Partial')]['Min_Value'])
total_sales_max = sum(group[(group['Transaction_Type'] == 'Full') |
(group['Transaction_Type'] == 'Partial')]['Max_Value'])
total_sales_avg = sum(group[(group['Transaction_Type'] == 'Full') |
(group['Transaction_Type'] == 'Partial')]['Avg_Value'])
# record the profit for the current (senator, stock) combination
min_real_gains[senator][stock] = total_sales_min - total_purchases_max
max_real_gains[senator][stock] = total_sales_max - total_purchases_min
avg_real_gains[senator][stock] = total_sales_avg - total_purchases_avg
# track total dollars invested for this senator
min_cost[senator] += total_purchases_min
max_cost[senator] += total_purchases_max
avg_cost[senator] += total_purchases_avg
As previously stated, senators may not sell 100% of their assets within this data set, so we compute the number of shares retained.
The number of shares held for each (senator, stock)
pair is the total number of shares purchased minus the total number of shares sold. We take the minimum purchases minus the maximum sales to obtain the minimum number of shares retained and vise versa for the maximum number of shares retained.
# nested dictionary: min_shares_held[senator][ticker] will return
# the minimum number of shares retained at the end of the disclosure period
min_shares_held = defaultdict(lambda: defaultdict(float))
max_shares_held = defaultdict(lambda: defaultdict(float))
avg_shares_held = defaultdict(lambda: defaultdict(float))
for i, row in trades.iterrows():
senator = row['Name']
stock = row['Ticker']
transaction_type = row['Transaction_Type']
trade_min_shares = row['Min_Shares'] if not np.isnan(row['Min_Shares']) else 0
trade_max_shares = row['Max_Shares'] if not np.isnan(row['Max_Shares']) else 0
trade_avg_shares = row['Avg_Shares'] if not np.isnan(row['Avg_Shares']) else 0
# Update number of shares held
if transaction_type == 'Purchase':
min_shares_held[senator][stock] += trade_min_shares
max_shares_held[senator][stock] += trade_max_shares
avg_shares_held[senator][stock] += trade_avg_shares
else:
min_shares_held[senator][stock] -= trade_max_shares
max_shares_held[senator][stock] -= trade_min_shares
avg_shares_held[senator][stock] -= trade_avg_shares
We compute the value of the shares retained.
Current Holding Value
shares_retained
is positiveInitial Holding Value
shares_retained
is negativeThe above values are added to the previously computed realized gains to obtain total gains.
Of course we cannot compute the value change of assets that were neither purchased nor sold during the disclosure period, and we cannot account for any transactions under \$1000 that may have occured.
start_date = trades['Date'].min()
end_date = trades['Date'].max()
min_total_gains = copy.deepcopy(min_real_gains)
max_total_gains = copy.deepcopy(max_real_gains)
avg_total_gains = copy.deepcopy(avg_real_gains)
for senator in min_shares_held:
min_shares = min_shares_held[senator]
max_shares = max_shares_held[senator]
avg_shares = avg_shares_held[senator]
# loop through the tickers. These are the same for min, avg, max shares
for stock in min_shares:
# retreive start and end prices of the stock over the disclosure period
curr_price = price_history[price_history['Date'] == end_date][stock].values[0]
start_price = price_history[price_history['Date'] == start_date][stock].values[0]
min_retained = min_shares[stock]
max_retained = max_shares[stock]
avg_retained = avg_shares[stock]
# calculate value of current/initial assets
min_unreal_gains = min_retained * curr_price if min_retained > 0 else min_retained * start_price
max_unreal_gains = max_retained * curr_price if max_retained > 0 else max_retained * start_price
avg_unreal_gains = avg_retained * curr_price if avg_retained > 0 else avg_retained * start_price
# adjust each senator's total cost if they have an initial asset
min_cost[senator] += -min_unreal_gains if min_unreal_gains < 0 else 0
max_cost[senator] += -max_unreal_gains if max_unreal_gains < 0 else 0
avg_cost[senator] += -avg_unreal_gains if avg_unreal_gains < 0 else 0
# sum realized and unrealized gains to get total gains
min_total_gains[senator][stock] += min_unreal_gains if not np.isnan(min_unreal_gains) else 0
max_total_gains[senator][stock] += max_unreal_gains if not np.isnan(max_unreal_gains) else 0
avg_total_gains[senator][stock] += avg_unreal_gains if not np.isnan(avg_unreal_gains) else 0
Let's see how the group of stocks that Senators have chosen to invest in have performed relative to the S&P 500. We'll start by aggregating the prices of all of the Senator stocks on each day. We use the S&P 500 because it is a standard market benchmark.
# sum all senator stock prices for all dates and compute percent return
price_history['agg_price'] = price_history.sum(axis=1, numeric_only=True)
start_agg_price = price_history.iloc[1]['agg_price']
end_agg_price = price_history.iloc[len(price_history)-1]['agg_price']
(end_agg_price - start_agg_price) / start_agg_price * 100
102.25719828397546
Now, let's get the price data for the S&P 500 over the same time period.
sp500 = yf.download(['^GSPC'], start=trades["Date"].min(), end=trades["Date"].max() + dt.timedelta(days=1)).reset_index()
sp500 = sp500[['Date', 'Adj Close']]
sp500 = sp500.rename(columns={'Adj Close': 'Price'})
start_sp500_price = sp500.loc[1, 'Price']
end_sp500_price = sp500.loc[len(sp500) - 1, 'Price']
sp500_change = (end_sp500_price - start_sp500_price) / start_sp500_price * 100
sp500_change
[*********************100%***********************] 1 of 1 completed
252.47801880667592
This is actually quite surprising. The 1198 stocks that Senators have chosen to invest in grew by approximately 102.26%, whereas the S&P 500 gained approximately 252.48% over the same time period. Based on the disclosures, it seems that the stocks that senators chose significantly underperform the S&P 500!
Now, let's try to visualize the percent gain for Senator stocks and the S&P500 over time.
# calculate percent gain for each group on each date
price_history['percent_gain'] = (price_history['agg_price'] - start_agg_price) / start_agg_price * 100
sp500['percent_gain'] = (sp500['Price'] - start_sp500_price) / start_sp500_price * 100
# plot lines
pct_merged = pd.merge(price_history[['Date', 'percent_gain']], sp500[['Date', 'percent_gain']], on=['Date'])
pct_merged = pct_merged.rename(columns={'percent_gain_x': 'Senator Stocks',
'percent_gain_y': 'S&P 500'})
sns.set(rc = {'figure.figsize':(15,8)},font_scale=1.5)
pct_plot = sns.lineplot(x='Date', y='value', hue='variable',
data=pd.melt(pct_merged, ['Date']))
# label plot
pct_plot.set_xlabel("Year", fontsize = 20)
pct_plot.set_ylabel("Percentage Gain", fontsize = 20)
pct_plot.set_title('Percent Gain of Senator Stocks vs S&P 500 Over TIme', fontsize = 20)
Text(0.5, 1.0, 'Percent Gain of Senator Stocks vs S&P 500 Over TIme')
We've shown that the stocks traded by senators are not outperforming the market with their trades over \$1000. However, some individual Senators do outperform the S&P 500. According to The American Enterprise Institute, only about 10% of professional active investment fund managers beat the market over a 10 year period.
First we compute the total gains for each senator by summing the gains across all stocks.
min_total_returns = {}
max_total_returns = {}
avg_total_returns = {}
# calculating total returns for each senator across all stocks purchased/sold
for senator in min_total_gains:
min_total_returns[senator] = sum([val for val in min_total_gains[senator].values()])
max_total_returns[senator] = sum([val for val in max_total_gains[senator].values()])
avg_total_returns[senator] = sum([val for val in avg_total_gains[senator].values()])
We compute the return on investment (ROI) for each Senator and compare those values to the S&P 500. We use the formula: $$ROI = \cfrac{total\_gains}{total\_cost}$$
min_roi = {}
max_roi = {}
avg_roi = {}
# compute percent retrusn for all senators across all investments
for senator in min_total_returns:
min_roi[senator] = min_total_returns[senator]/max_cost[senator] * 100
max_roi[senator] = max_total_returns[senator]/min_cost[senator] * 100
avg_roi[senator] = avg_total_returns[senator]/avg_cost[senator] * 100
# sort dictionaries based on the average roi
sorted_avg_roi = {k: v for k, v in sorted(avg_roi.items(), key=lambda item: avg_roi[item[0]], reverse=True)}
sorted_min_roi = {k: v for k, v in sorted(min_roi.items(), key=lambda item: avg_roi[item[0]], reverse=True)}
sorted_max_roi = {k: v for k, v in sorted(max_roi.items(), key=lambda item: avg_roi[item[0]], reverse=True)}
We show the minimum, average, and maximum returns for each senator sorted from largest to smallest average percent return.
# plot min, max, avg returns for all senators as well as the S&P 500
f, ax = plt.subplots(figsize=(15, 20))
sns.set_color_codes("muted")
sns.barplot(y=['S&P 500'] + list(sorted_max_roi.keys()), x=[0] + list(sorted_max_roi.values()), label='Maximum ROI', orient='h', color='orange')
sns.barplot(y=['S&P 500'] + list(sorted_avg_roi.keys()), x=[0] + list(sorted_avg_roi.values()), label='Average ROI', orient='h', color="g")
sns.barplot(y=['S&P 500'] + list(sorted_min_roi.keys()), x=[0] + list(sorted_min_roi.values()), label='Minimum ROI', orient='h', color='r')
sns.barplot(y=['S&P 500'] + list(sorted_min_roi.keys()), x=[sp500_change] + [0]*len(sorted_min_roi), orient='h', color="b")
ax.legend(ncol=2, loc="lower right", frameon=True)
ax.set(ylabel="Senator", xlabel="Percent Return on Investments", title='Percent Return on Investments by Senator')
[Text(0, 0.5, 'Senator'), Text(0.5, 0, 'Percent Return on Investments'), Text(0.5, 1.0, 'Percent Return on Investments by Senator')]
Using liberal estimates, 38 senators beat the market (ROI > 252%).
len([val for val in max_roi.items() if val[1] > sp500_change])
38
Using our average estimates, the seven senators below beat the S&P 500.
[val for val in avg_roi.items() if val[1] > sp500_change]
[(' A. Mitchell McConnell Jr.', 392.7092354234115), (' Barbara A Mikulski', 498.8457609991058), (' Daniel S Sullivan', 257.79433766608923), (' James M Inhofe', 1624.869850122107), (' Mark R Warner', 868.0109398806682), (' Michael F Bennet', 1999.9880000799994), (' Tina Smith', 533.2333737994525)]
Even with the most conservative estimates of their returns, the four senators below beat the S&P 500.
[val for val in min_roi.items() if val[1] > sp500_change]
[(' James M Inhofe', 553.846650964982), (' Mark R Warner', 530.4690056376435), (' Michael F Bennet', 950.003), (' Tina Smith', 310.2210358690841)]
The variation in these results points out some glaring flaws in the federal stock disclosure procedure. Because of the large ranges of transaction amounts, the actual amount that was traded can vary widely. From the above analysis, we see that only 7 senators beat the market with stock trades throughout the last 10 years using average estimates for the transaction amounts. However, that number skyrockets to 38 when using the upper end of the transaction ranges.
Because only a total of 55 senators report any transactions, the distinction between four and 38 senators outperforming the market is drastic. As previously stated, only 10% of active hedge fund managers beat their benchmark indicies.
# sum the profits over all senators
min_senate_returns = sum([val for val in min_total_returns.values()])
max_senate_returns = sum([val for val in max_total_returns.values()])
avg_senate_returns = sum([val for val in avg_total_returns.values()])
# sum the costs over all senators
min_senate_cost = sum([val for val in min_cost.values()])
max_senate_cost = sum([val for val in max_cost.values()])
avg_senate_cost = sum([val for val in avg_cost.values()])
# print percent returns
print(f"Minimum (percent) return: {min_senate_returns/max_senate_cost:.2%}")
print(f"Average (percent) return: {avg_senate_returns/avg_senate_cost:.2%}")
print(f"Maxmum (percent) return: {max_senate_returns/min_senate_cost:.2%}")
Minimum (percent) return: -70.46% Average (percent) return: 130.67% Maxmum (percent) return: 469.65%
Senators may dramatically lose or dramatically make money from their stock trades depending on which estimates you use. It is also unclear whether they beat the market based on this data. In other words, the use of the ranges is almost fully obscuring the returns that senators make. This strongly suggests the need for reporting exact (or rounded) amounts of the trades rather than using wide ranges.
We offer the following recommendations regarding STOCK Act disclosures to improve transparency of congressional stock trading and the quality of data for future researchers. It is overall unclear whether senators are performing well enough to suggest dubious trading practices.
Perhaps the simplest solution to achieve higher confidence in Congress is to ban all individual stock trades to err on the side of caution, but due to the inconclusivity of our results, we focus our recommendations on updates to the disclosure requirements. We suggest the following steps to greatly improve the data quality as a means to increase transparency.
Amount Ranges
One persistent struggle in this project has been the large ranges for the amount of each transaction.
Small Transactions
We frequently assumed that the disclosed transactions represented all of the transactions that occurred (for example when computing the number of shares retained).
Exchanges
While exchanges did not represent a large portion of our data set, we still had to omit entries due to the challenge of incorporating inconsistent formats.
Transaction_Type
"Exchange (Given)" and a corresponding entry for the new stock with "Exchange (Received)" in the action column. STOCK Act Enforcement
As mentioned in our introduction, Business Insider was unable to obtain confirmation from several senators that they paid the required fees (or obtained an ethics waiver) for late disclosures.
Brokered Trades
As mentioned in the Comments on Comments section, some trades indicate that a broker made the trade. Some Congress members use this as a defense against accusations that their trades are improper.
Stock Manager
should be a categorical column similar to the Owner
column so that future researchers can determine if these trades perform significantly differently than the rest of the trades. Self
, Joint
, Spouse
, Dependent
, Qualified Broker
, Other
.We find this to be an interesting area of research. As a result, we hope to see future work studying how the observed trends differ between parties, states, and clusters of senators. We are interested in extending this analysis to the disclosures for members of the House of Representatives and to financial assets other than stocks.
A particularly interesting question that came out of this project is "how do stock returns correlate to seniority in Congress?" where seniority is measured by number of years in Congress and/or by the number/importance of committee appointments.
We are also interested in whether transactions that were disclosed late perform differently than the average trade.
trades['Comment'].unique()
array(['--', 'Agilent Tech common stock completed a spinoff on 10/22/14 to Keysight Tech', 'KMB completed a spinoff of its health care business business now known as HYH on 11/01/14', 'Dividend Reinvestment', '2015 Issued IR RSUs vested and actual shares issued; part of shares sold w/ remainder in UBS CSSP', '2016 Issued IR RSUs vested and actual shares issued; part of shares sold w/ remainder in UBS CSSP', 'Transaction is due to sale of a security from redemption of incentive units vested on April 21, 2017', 'Complete Disposition of Ingersoll Rand Stock', 'Complete Disposition of Vulcan Materials Stock', 'Mandatory Cash Merger', 'WTR-Aqua America, Inc name change to WTRG-Essential Utilities, Inc on 2/3/2020.', "The filer's portfolio is managed by a third party investment professional. The filer was advised of these transactions on July 7, 2020. This activity is part of an ongoing long term portfolio diversification strategy that regularly includes similar transactions.", "The filer's portfolio is managed by a third party investment professional. The filer was advised of these transactions on April 7, 2020. This activity is part of an ongoing long-term portfolio diversification strategy that regularly includes similar transactions.", "The filer's portfolio is managed by a third party investment professional. The filer was advised of these transactions on June 2, 2020. This activity is part of an ongoing long-term portfolio diversification strategy that regularly includes similar transactions.", 'Filer notified of transaction >45 days after it occurred.', "The filer's portfolio is managed by a third party investment professional. The filer was advised of these transactions on April 13, 2021. This activity is part of an ongoing long-term portfolio diversification strategy that regularly includes similar transactions", 'This purchase is a reflection of Perdue exercising his stock option listed on previous disclosures', 'Full sale from Wells Fargo Acct', 'Full sale of Wells Fargo Acct', 'Full sale from Wells Fargo account.', 'Quarterly dividend reinvestment', 'Quarterly Dividend Reinvestment', 'R', 'SEP', 'Sep', 'Fr', 'FR', 'Full sale from Wells Fargo Acct. This full sale was less than $1,000 but disclosing anyway for full transparency.', 'sep', 'This comment was incorrectly labeled as "partial" when it should have been labeled "full"', 'This transaction was incorrectly labeled "partial" when it should have been labeled "full"', 'This purchase initially listed the wrong ticker symbol and is now corrected', 'Corrected the owner of the asset from "spouse" to "joint"', 'Rate: 6.25% Maturation Date: 6/1/2035', 'This asset was acquired through the reverse stock split from DowDuPont on June 3.', 'Transaction incorrectly noted as partial sale. Corrected to display a purchase.', 'Transaction incorrectly filed as a partial sale. Corrected to display purchase.', '20% Share in Brombach Family LP - Merrill Lynch', 'The wrong ticker was used in the initial reporting of this transaction. The Vanguard Emerging Markets ticker was used instead of the Vanguard Ultra-Short Term Bond ticker.', 'Less than $1001.', 'Purchase was less than $1001.', 'Transaction was less than $1000', 'Transaction was less than $1000.', 'Sale was less than $1000.', 'Less than $1000', 'Less than $1000.', 'Note: Transaction was less than $1,000.', 'Note: This purchase was less than $1,000.', 'Changed transaction date from 1/4/17 (Settlement date) to 12/29/16 (Trade date).', 'LINN ENERGY LLC PUBLICLY TRADED PARTNERSHIP', 'VANGUARD NATURAL RESOURCES PUBLICLY TRADED PARTNERSHIP', 'LEGACY RESERVES LTD PUBLICLY TRADED PARTNERSHIP', 'ENERGY TRANSFER PARTNERS PUBLICLY TRADED PARTNERSHIP', 'CARLYLE GROUP LTD PUBLICLY TRADED PARTNERSHIP', 'Shares acquired result from a stock dividend by UA - Under Armour Inc.', 'Transaction within Wells Fargo Advisors IRA - Clearbridge Managed Equities account.', 'Transaction within Wells Fargo Advisors IRA - NWQ Managed Equities account.', "Stock inherited from deceased mother's estate. Transfer paperwork signed 4/21/2017.", 'Notified to filer on or after March 16, 2020', 'Timing and method of April transactions implemented at discretion of third party advisers.', 'Transactions on lines 148 through 175 notified to filer on or after April 1, 2020', 'Notified to filer on or after March 1, 2020', 'Equity compensation by former employer with predetermined sale dates pursuant to SEC Rule 10b5-1 plan filed with such company', 'Equity pursuant to the company he founded with predetermined sale dates pursuant to SEC Rule 10b5-1 plan filed with such company', 'Equity pursuant to the company he founded with predetermined dates (exercise of stock options) pursuant to SEC Rule 10b5-1 plan filed with such company', 'Exercise of stock options prior to expiration date', 'Sale of exercised options (sold to cover taxes and cost of prior exercise of options); shares sold through a 10b5-1 plan, which is an SEC compliant pre-determined plan', 'Partial exercise of stock options', 'Sale of exercised options and shares received as compensation; shares sold through a 10b5-1 plan, which is an SEC compliant pre determined plan', 'Shares sold through a 10b5-1 plan, which is an SEC compliant pre-determined plan', 'Lines 1-19 notified to filer on or after March 1, 2020; lines 20-42 notified to filer on or after March 18, 2020', 'Originally reported as Purchase in error', 'Sale of two blocks', 'Underlying asset of MRW Biotech Investors, LLC. Previously held in New River Management V.', 'Underlying asset of MRW Biotech Investors, LLC', 'Underlying asset of MRW Biotech Investors, LLC. Previously held in New River V', 'r', 'Jt', '555', 'seo', 'jt', 'roth', 'ugma--lily', 'fran45', 'Child #2', 'Name change - Old Florida Bank became IberiaBank Corp. due to acquisition', 'Shares acquired through special dividend issued by AIV in the form of stock.', 'same transaction using settlement date was deleted in 11/18/14 report', 'Formerly reported as Isis Pharmaceuticals', 'LW received as a split from CAG on 11/09/16', 'Acquired as a result of a 9/21/16 inheritance', 'Acquired as a result of a 9/21/16 inheritance.', "Rec'd in spinoff from United Technologies & Raytheon Technologies merger //Due to standing separation agreement with financial advisor, Senator first notified of activity 5/26/20 in compliance with Senate disclosure process.", "Rec'd in spinoff from United Technologies & Raytheon Technologies merger // Due to standing separation agreement with financial advisor, Senator first notified of activity 5/26/20 in compliance with Senate disclosure process.", "Rec'd in spinoff from United Technologies & Raytheon Technologies merger// Due to standing separation agreement with financial advisor, Senator first notified of activity 5/26/20 in compliance with Senate disclosure process.", 'KLXI IS A SPINOFF OF BEAV', 'Overlooked in PTR report', 'Inadvertently omitted on original report', 'inadvertently omitted on original report', 'Split into 2 share classes A & C. He sold the A shares and kept the C shares.', 'KMB completed spinoff of healthcare business now known as HYH on 11/01/14.', 'Asset acquired through UBS accounts in small pieces(less than $1000) from Jan - Apr 2014.', 'This is NOT a purchase, it is a spinoff from the old DLPH.', 'This is a capital gain reinvestment.', 'The filer’s financial advisors actively manage his accounts without the filer’s day to day involvement. The filer was unaware that the specific transactions in this report occurred. Upon becoming aware of the transactions, the filer expeditiously prepared and submitted this report to the Committee.', 'Report amended to reflect correct ticker symbol.', 'Spin off from asset still held (Pentair plc) and then sold.', 'Asset held in Truchas Capital 401K Plan', 'Asset held in Charles Schwab - Rollover IRA', 'Remainder of stock was sold later in the year and under the threshold.', 'Asset owned by Roundstone Ventures LLC', 'Asset held by Roundstone Ventures, LLC', 'This transaction was filed on October 2 but was re-filed with the new system', 'ESRX merged with CI, Cigna.', 'Alcon Inc. (ALC) shares received in exchange for Novartis AG (NVS) shares as a result of spin-off.', 'Exchange occurred as a result of the Sprint/T-Mobile merger.', 'Shares of CTL received as a result of merger.', 'Exchange of stock due to corporate merger.', 'Exchange of stock due to corporate spin-off of the legacy assets and liabilities of NTRP not merged into PTPI.', 'Exchange of stock due to partial corporate spin-off and renaming.', 'Due to standing separation agreement with financial advisor, Senator first notified of activity 5/26/20 in compliance with Senate disclosure process.', "Post merger UTX changed it's name from United Technologies Corporation to Raytheon Technologies Corporation // Due to standing separation agreement with financial advisor, Senator first notified of activity 5/26/20 in compliance with Senate disclosure process.", 'Merger - stock of Access National Corporation exchanged for Union Bankshares Corporation', 'LIN acquired from PX due to mandatory stock merger.', 'FLIR Systems Merger With Teledyne Asset Held in Roundstone Ventures, LLC'], dtype=object)