Mathew K Analytics

Lesson 18 · Real-World Data Analytics

Python Data Analytics #18: Company Fundamentals from Real Financial Statements

Video eighteen of the hundred-video real-world data analytics series. Analyzing real fundamentals for over three hundred real S&P 500 companies, valuation,…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Data Analytics 100, Video 18: Company Fundamentals from Real Financial Statements#

  • Video eighteen of the hundred-video real-world data analytics series.
  • Analyzing real fundamentals for over three hundred real S&P 500 companies, valuation, profitability, and dividends.
  • Let's get into it.

Part 1: A Real Snapshot, Not a Price Chart#

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
companies = pd.read_csv('sp500_fundamentals.csv')
companies.shape
(337, 14)

Part 2: Real Columns Available#

companies.columns.tolist()
companies.head(3)
Symbol Name Sector Price Price/Earnings Dividend Yield Earnings/Share 52 Week Low 52 Week High Market Cap EBITDA Price/Sales Price/Book SEC Filings
0 MMM 3M Industrial Conglomerates 153.13 29.504818 0.0204 5.19 139.34 177.41 7.986760e+10 6.240000e+09 3.191640 24.477303 http://www.sec.gov/cgi-bin/browse-edgar?action...
1 AOS A. O. Smith Building Products 56.72 15.125334 0.0250 3.75 54.16 81.87 7.817640e+09 7.953000e+08 2.050851 4.162936 http://www.sec.gov/cgi-bin/browse-edgar?action...
2 ABT Abbott Laboratories Health Care Equipment 85.60 23.977590 0.0294 3.57 81.97 139.06 1.490992e+11 1.174400e+10 3.303479 2.863930 http://www.sec.gov/cgi-bin/browse-edgar?action...

Part 3: Real Missing Data Audit#

companies.isna().sum().sort_values(ascending=False)
round(companies['Dividend Yield'].isna().mean() * 100, 1)
np.float64(22.3)

Part 4: Real Clean Subset for Valuation Analysis#

valuation_cols = ['Symbol', 'Name', 'Sector', 'Price', 'Price/Earnings', 'Market Cap', 'EBITDA', 'Price/Sales', 'Price/Book']
clean = companies.dropna(subset=valuation_cols).copy()
clean.shape
(282, 14)

Part 5: Real Market Cap Leaders#

top_market_cap = clean.nlargest(10, 'Market Cap')[['Symbol', 'Name', 'Market Cap']]
top_market_cap
Symbol Name Market Cap
19 GOOGL Alphabet Inc. (Class A) 4.607988e+12
39 AAPL Apple Inc. 4.583336e+12
20 GOOG Alphabet Inc. (Class C) 4.560616e+12
320 MSFT Microsoft 3.344578e+12
22 AMZN Amazon 2.911304e+12
72 AVGO Broadcom 2.115308e+12
314 META Meta Platforms 1.605578e+12
319 MU Micron Technology 1.095030e+12
290 LLY Lilly (Eli) 9.853742e+11
6 AMD Advanced Micro Devices 8.415529e+11

Part 6: Real Price-to-Earnings Distribution#

clean['Price/Earnings'].describe().round(2)
count    282.00
mean      34.96
std       44.84
min        2.44
25%       17.83
50%       26.02
75%       35.58
max      460.00
Name: Price/Earnings, dtype: float64

Part 7: Visualizing Real P/E Distribution#

plt.figure(figsize=(10, 5))
plt.hist(clean['Price/Earnings'].clip(upper=100), bins=40, color='steelblue', edgecolor='white')
plt.xlabel('Real Price-to-Earnings Ratio')
plt.ylabel('Real Number of Companies')
plt.title('Real P/E Ratio Distribution, S&P 500')
plt.tight_layout()
plt.savefig('pe_distribution.png', dpi=120)
plt.close()

Part 8: Real Cheapest Stocks by P/E#

cheapest = clean[clean['Price/Earnings'] > 0].nsmallest(10, 'Price/Earnings')[['Symbol', 'Name', 'Sector', 'Price/Earnings']]
cheapest
Symbol Name Sector Price/Earnings
106 CI Cigna Health Care Services 2.439539
101 CHTR Charter Communications Cable & Satellite 3.897457
18 ALL Allstate Property & Casualty Insurance 4.559513
118 CMCSA Comcast Cable & Satellite 4.876471
42 ACGL Arch Capital Group Property & Casualty Insurance 6.872307
164 EIX Edison International Electric Utilities 7.602174
7 AES AES Corporation Independent Power Producers & Energy Traders 7.640625
309 MKC McCormick & Company Packaged Foods & Meats 7.765574
47 T AT&T Integrated Telecommunication Services 8.157894
215 GIS General Mills Packaged Foods & Meats 8.266503

Part 9: Real Sector-Level Average Valuation#

sector_pe = clean.groupby('Sector')['Price/Earnings'].mean().sort_values(ascending=False)
sector_pe.head(10).round(2)
Sector
Real Estate Services                    244.27
Other Specialized REITs                 139.40
Semiconductors                          132.79
Distributors                            118.94
Fertilizers & Agricultural Chemicals     74.38
Electronic Components                    64.92
Data Center REITs                        62.30
Health Care REITs                        59.84
Aerospace & Defense                      59.42
Interactive Home Entertainment           57.47
Name: Price/Earnings, dtype: float64

Part 10: Real Sector Company Counts#

sector_counts = clean['Sector'].value_counts()
sector_counts.head(10)
Sector
Health Care Equipment                           11
Electric Utilities                              10
Aerospace & Defense                              8
Financial Exchanges & Data                       8
Oil & Gas Exploration & Production               7
Semiconductors                                   7
Packaged Foods & Meats                           7
Hotels, Resorts & Cruise Lines                   6
Building Products                                6
Industrial Machinery & Supplies & Components     6
Name: count, dtype: int64

Part 11: Real Dividend-Paying Companies#

dividend_payers = companies[companies['Dividend Yield'].notna()]
pct_paying = round(len(dividend_payers) / len(companies) * 100, 1)
pct_paying
77.7

Part 12: Real Highest Dividend Yields#

top_yield = dividend_payers.nlargest(10, 'Dividend Yield')[['Symbol', 'Name', 'Sector', 'Dividend Yield']]
top_yield
Symbol Name Sector Dividend Yield
119 CAG Conagra Brands Packaged Foods & Meats 0.1054
14 ARE Alexandria Real Estate Equities Office REITs 0.0821
83 CPB Campbell Soup Company Packaged Foods & Meats 0.0739
215 GIS General Mills Packaged Foods & Meats 0.0722
23 AMCR Amcor Paper & Plastic Packaging Products & Materials 0.0670
281 KHC Kraft Heinz Packaged Foods & Meats 0.0666
227 DOC Healthpeak Properties Health Care REITs 0.0637
298 LYB LyondellBasell Specialty Chemicals 0.0618
21 MO Altria Tobacco 0.0609
254 IP International Paper Paper & Plastic Packaging Products & Materials 0.0553

Part 13: Real Earnings Per Share Distribution#

eps_clean = companies.dropna(subset=['Earnings/Share'])
negative_eps_count = (eps_clean['Earnings/Share'] < 0).sum()
negative_eps_count
np.int64(24)

Part 14: Real Companies with Negative Earnings#

unprofitable = eps_clean[eps_clean['Earnings/Share'] < 0][['Symbol', 'Name', 'Sector', 'Earnings/Share']]
unprofitable.sort_values('Earnings/Share').head(10)
Symbol Name Sector Earnings/Share
199 FMC FMC Corporation Fertilizers & Agricultural Chemicals -19.62
96 CNC Centene Corporation Managed Health Care -13.05
263 SJM J.M. Smucker Company (The) Packaged Foods & Meats -11.79
325 TAP Molson Coors Beverage Company Brewers -10.55
94 CE Celanese Specialty Chemicals -9.86
322 MRNA Moderna Biotechnology -8.14
14 ARE Alexandria Real Estate Equities Office REITs -6.27
254 IP International Paper Paper & Plastic Packaging Products & Materials -5.19
281 KHC Kraft Heinz Packaged Foods & Meats -4.86
156 DOW Dow Inc. Commodity Chemicals -4.00

Part 15: Real Price-to-Book Ratio#

pb_clean = companies.dropna(subset=['Price/Book'])
pb_clean['Price/Book'].describe().round(2)
count    324.00
mean       2.68
std       50.78
min     -569.50
25%        1.62
50%        2.94
75%        6.69
max      497.96
Name: Price/Book, dtype: float64

Part 16: Real Negative Book Value Companies#

negative_book = pb_clean[pb_clean['Price/Book'] < 0][['Symbol', 'Name', 'Price/Book']]
len(negative_book)
24

Part 17: Real Correlation Among Valuation Metrics#

metric_cols = ['Price/Earnings', 'Price/Sales', 'Price/Book', 'Dividend Yield']
metric_corr = companies[metric_cols].corr()
metric_corr.round(3)
Price/Earnings Price/Sales Price/Book Dividend Yield
Price/Earnings 1.000 0.288 0.025 -0.089
Price/Sales 0.288 1.000 0.087 -0.286
Price/Book 0.025 0.087 1.000 -0.111
Dividend Yield -0.089 -0.286 -0.111 1.000

Part 18: Real Value Stock Screen#

value_screen = clean[(clean['Price/Earnings'] < 15) & (clean['Price/Earnings'] > 0) & (clean['Price/Book'] < 3)]
len(value_screen)
38

Part 19: Real Value Screen Results#

value_screen[['Symbol', 'Name', 'Sector', 'Price/Earnings', 'Price/Book']].sort_values('Price/Earnings').head(10)
Symbol Name Sector Price/Earnings Price/Book
106 CI Cigna Health Care Services 2.439539 1.738923
101 CHTR Charter Communications Cable & Satellite 3.897457 1.081229
18 ALL Allstate Property & Casualty Insurance 4.559513 1.795960
118 CMCSA Comcast Cable & Satellite 4.876471 1.007739
42 ACGL Arch Capital Group Property & Casualty Insurance 6.872307 1.344388
164 EIX Edison International Electric Utilities 7.602174 1.561335
7 AES AES Corporation Independent Power Producers & Energy Traders 7.640625 2.366893
309 MKC McCormick & Company Packaged Foods & Meats 7.765574 1.823606
47 T AT&T Integrated Telecommunication Services 8.157894 1.579014
194 FIS Fidelity National Information Services Transaction & Payment Processing Services 8.331396 1.391127

Part 20: Real Market Cap Buckets#

cap_bins = [0, 10e9, 50e9, 200e9, np.inf]
cap_labels = ['Small (<$10B)', 'Mid ($10-50B)', 'Large ($50-200B)', 'Mega (>$200B)']
clean['CapTier'] = pd.cut(clean['Market Cap'], bins=cap_bins, labels=cap_labels)
clean['CapTier'].value_counts()
CapTier
Mid ($10-50B)       142
Large ($50-200B)     89
Mega (>$200B)        32
Small (<$10B)        19
Name: count, dtype: int64

Part 21: Real Valuation by Market Cap Tier#

tier_pe = clean.groupby('CapTier', observed=True)['Price/Earnings'].mean()
tier_pe.round(2)
CapTier
Small (<$10B)       34.39
Mid ($10-50B)       31.37
Large ($50-200B)    37.30
Mega (>$200B)       44.72
Name: Price/Earnings, dtype: float64

Part 22: Real EBITDA Margin Proxy#

ebitda_clean = clean.dropna(subset=['EBITDA'])
ebitda_to_cap = (ebitda_clean['EBITDA'] / ebitda_clean['Market Cap'])
ebitda_to_cap.describe().round(4)
count    282.0000
mean       0.1028
std        0.0865
min       -0.0179
25%        0.0562
50%        0.0853
75%        0.1326
max        1.0806
dtype: float64

Part 23: Real Highest Cash-Generation Efficiency#

ebitda_clean = ebitda_clean.assign(EBITDA_to_Cap=ebitda_to_cap)
ebitda_clean.nlargest(10, 'EBITDA_to_Cap')[['Symbol', 'Name', 'EBITDA_to_Cap']]
Symbol Name EBITDA_to_Cap
101 CHTR Charter Communications 1.080556
38 APA APA Corporation 0.404677
118 CMCSA Comcast 0.398148
7 AES AES Corporation 0.358917
164 EIX Edison International 0.318551
18 ALL Allstate 0.279726
83 CPB Campbell Soup Company 0.277572
331 MOS Mosaic Company (The) 0.262370
47 T AT&T 0.257732
216 GM General Motors 0.243689

Part 24: Visualizing Real Sector Valuation Spread#

top_sectors_by_count = clean['Sector'].value_counts().head(6).index
plt.figure(figsize=(11, 6))
plot_data = [clean.loc[clean['Sector'] == s, 'Price/Earnings'].clip(upper=80) for s in top_sectors_by_count]
plt.boxplot(plot_data, tick_labels=top_sectors_by_count)
plt.xticks(rotation=30, ha='right')
plt.ylabel('Real Price-to-Earnings Ratio')
plt.title('Real P/E Spread Across Top Sectors')
plt.tight_layout()
plt.savefig('sector_pe_boxplot.png', dpi=120)
plt.close()

Part 25: Real 52-Week Range Analysis#

range_clean = companies.dropna(subset=['52 Week Low', '52 Week High', 'Price'])
range_clean = range_clean.assign(PctFromHigh=(range_clean['Price'] - range_clean['52 Week High']) / range_clean['52 Week High'] * 100)
range_clean['PctFromHigh'].describe().round(1)
count    324.0
mean     -17.7
std       14.3
min      -69.5
25%      -25.7
50%      -13.8
75%       -7.3
max       -0.0
Name: PctFromHigh, dtype: float64

Part 26: Real Stocks Near Their 52-Week Low#

near_low = range_clean.nsmallest(10, 'PctFromHigh')[['Symbol', 'Name', 'Price', '52 Week High', 'PctFromHigh']]
near_low.round(2)
Symbol Name Price 52 Week High PctFromHigh
199 FMC FMC Corporation 13.66 44.78 -69.50
129 CSGP CoStar Group 32.20 97.43 -66.95
101 CHTR Charter Communications 144.05 422.29 -65.89
208 IT Gartner 162.20 434.12 -62.64
297 LULU Lululemon Athletica 131.18 340.25 -61.45
256 INTU Intuit 331.53 813.70 -59.26
250 PODD Insulet Corporation 144.94 354.88 -59.16
70 BSX Boston Scientific 48.31 109.50 -55.88
172 EPAM EPAM Systems 102.46 222.53 -53.96
221 GDDY GoDaddy 85.83 183.34 -53.19

Part 27: Saving the Real Cleaned Fundamentals Table#

clean.to_csv('sp500_fundamentals_clean.csv', index=False)
reloaded = pd.read_csv('sp500_fundamentals_clean.csv')
reloaded.shape[0] == clean.shape[0]
True

Part 28: Real Sanity Check on Value Screen#

(value_screen['Price/Earnings'] < 15).all() and (value_screen['Price/Book'] < 3).all()
np.True_

Part 29: Real Sector Diversity Check#

value_screen_sectors = value_screen['Sector'].nunique()
value_screen_sectors
23

Part 30: Real Recap Print#

print(f'Across {len(companies)} real S&P 500 companies, {pct_paying}% pay a dividend, {negative_eps_count} reported negative earnings, and {len(value_screen)} passed a simple real value screen across {value_screen_sectors} different sectors.')
Across 337 real S&P 500 companies, 77.7% pay a dividend, 24 reported negative earnings, and 38 passed a simple real value screen across 23 different sectors.

Wrap-Up: What You Learned#

  • Fundamental data is a real cross-sectional snapshot, one row per company, a genuinely different shape than the time-series data used throughout the rest of this domain.
  • Real missing data in financial statements is not random noise, it often reflects a genuine business reality, like a company simply not paying a dividend.
  • A single real ratio like P/E never tells the whole story, pairing it with real price-to-book or real sector context gives a fuller picture.
  • Grouping real companies by sector or market cap tier before comparing valuation is what makes the real comparison actually fair.
  • A real simple value screen, combining multiple real filters, is the same basic logic professional analysts use, just built here from first principles.
  • Next video: real Value-at-Risk and drawdown analysis, returning to time series data to quantify tail risk more rigorously.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.