Chapter 1: Exploratory Data Analysis

  1. 2019-2026 Peter C. Bruce, Andrew Bruce, Peter Gedeck
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from matplotlib.collections import EllipseCollection
from matplotlib.colors import Normalize
from scipy.stats import trim_mean
from statsmodels import robust
import matplotlib.pyplot as plt
import mlba
import numpy as np
import pandas as pd
import random
import seaborn as sns
import wquantiles

random.seed(123)

# Location of the data files. Adjust this path if you keep the data
# files in a different directory.
from pathlib import Path
DATA_DIR = Path('../data')

Exploratory Data Analysis

Data Dictionaries and Catalogs

PROMPT = """
Improve the attached data dictionary and return it in YAML format. In addition
to the description of features, provide name, description, source, and size
of the dataset.

For each feature, provide the following information:
- feature name
- readable variable name (if required)
- definition of the variable
- data type
- measurement units
- allowed values (for categorical or nominal features, but only if you are sure)

Here is the data dictionary in YAML format:

{data_dictionary}

First ten lines of the dataset in CSV format:

{data_sample}
"""
chain = (
  PromptTemplate.from_template(PROMPT) |
  ChatOpenAI(model="gpt-5")
)
data_dictionary = """
name: ebay Auctions
description: >
    The dataset eBayAuctions.csv contains information on 1972 auctions that
    transacted on eBay.com during May-June 2004.
source: Copyright 2016 Galit Shmueli and Peter Bruce
size:
    observations: 1972
    features: 8
features:
    Category: Category of the auctioned item
    currency: "US: US dollar, GBP: English pound, EUR: Euro"
    sellerRating: >
        a rating by eBay, as a function of the number of "good" and  "bad"
        transactions the seller had on eBay
    Duration: Number of days the auction lasted (set by seller at auction start)
    endDay: Day of week that the auction closed
    ClosePrice: Price item sold at (converted into USD)
    OpenPrice: Initial price set by the seller (converted into USD)
    Competitive?: whether the auction had a single bid (0) or more (1)
"""

eBayAuctions = mlba.load_data("eBayAuctions")
data_sample = eBayAuctions.sample(10, random_state=123).to_csv(index=False)

context = {
    "data_dictionary": data_dictionary,
    "data_sample": data_sample,
}
result = chain.invoke(context)
print(result.content)
name: eBay Auctions (May–June 2004)
description: >
  Dataset of 1,972 completed auctions on eBay.com during May–June 2004.
  Records include listing category, listing currency, seller feedback rating,
  auction duration, day-of-week the auction ended, opening price (USD),
  closing price (USD), and an indicator of whether the auction received
  multiple bids (competitive). Note: OpenPrice and ClosePrice are converted
  to U.S. dollars regardless of the listing currency; currency records the
  currency in which the auction was originally listed.
source: Copyright 2016 Galit Shmueli and Peter Bruce (eBayAuctions.csv)
size:
  observations: 1972
  features: 8
features:
  - name: Category
    readable_name: category
    definition: Top-level category of the auctioned item as shown on the eBay listing.
    data_type: categorical (string)
    measurement_units: none

  - name: currency
    readable_name: listing_currency
    definition: Currency in which the auction was listed; prices in this dataset are converted to USD.
    data_type: categorical (string; code)
    measurement_units: none
    allowed_values:
      - code: US
        label: U.S. dollar (USD)
      - code: GBP
        label: British pound sterling (GBP)
      - code: EUR
        label: Euro (EUR)

  - name: sellerRating
    readable_name: seller_rating
    definition: Seller’s feedback rating on eBay at the time of the auction; higher values indicate more positive history.
    data_type: integer (non-negative)
    measurement_units: rating points

  - name: Duration
    readable_name: duration_days
    definition: Number of days the auction lasted (set by the seller at auction start).
    data_type: integer
    measurement_units: days
    allowed_values: [1, 3, 5, 7, 10]

  - name: endDay
    readable_name: end_day_of_week
    definition: Day of the week on which the auction closed.
    data_type: categorical (string; day-of-week)
    measurement_units: none
    allowed_values: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]

  - name: ClosePrice
    readable_name: close_price_usd
    definition: Final transaction price (winning bid) of the auction, converted to U.S. dollars.
    data_type: float (decimal)
    measurement_units: USD

  - name: OpenPrice
    readable_name: open_price_usd
    definition: Initial starting price set by the seller, converted to U.S. dollars.
    data_type: float (decimal)
    measurement_units: USD

  - name: Competitive?
    readable_name: is_competitive
    definition: Indicator of whether the auction received more than one bid (competitive) versus a single bid.
    data_type: integer (binary)
    measurement_units: none
    allowed_values:
      - value: 0
        label: Single bid (not competitive)
      - value: 1
        label: Multiple bids (competitive)

Estimates of Location

Example: Location Estimates of Population and Murder Rates

state = pd.read_csv(DATA_DIR / "state.csv")
print(f"Mean: {state['Population'].mean()}")
print(f"Trimmed mean: {trim_mean(state['Population'], 0.1)}")
print(f"Median: {state['Population'].median()}")
Mean: 6162876.3
Trimmed mean: 4783697.125
Median: 4436369.5
weighted_mean = np.average(state["Murder.Rate"], weights=state["Population"])
weighted_quantile = wquantiles.median(state["Murder.Rate"],
                                      weights=state["Population"])
print(f"Weighted mean: {weighted_mean}")
print(f"Weighted median: {weighted_quantile}")
Weighted mean: 4.445833981123393
Weighted median: 4.4

Estimates of Variability

Example: Variability Estimates of State Population

print(state["Population"].std())
print(state["Population"].quantile(0.75) - state["Population"].quantile(0.25))
print(robust.scale.mad(state["Population"]))
6848235.347401142
4847308.0
3849876.1459979336

Exploring the Data Distribution

Percentiles and Boxplots

state[["Murder.Rate"]].quantile([0.05, 0.25, 0.5, 0.75, 0.95])
Murder.Rate
0.05 1.600
0.25 2.425
0.50 4.000
0.75 5.550
0.95 6.510
ax = (state["Population"] / 1_000_000).plot.box(figsize=(3, 4))
ax.set_ylabel("Population (millions)")

plt.tight_layout()
plt.show()

Frequency Tables and Histograms

binned_population = pd.cut(state["Population"], 10)
binned_population.value_counts()
Population
(526935.67, 4232659.0]      24
(4232659.0, 7901692.0]      14
(7901692.0, 11570725.0]      6
(11570725.0, 15239758.0]     2
(15239758.0, 18908791.0]     1
(18908791.0, 22577824.0]     1
(22577824.0, 26246857.0]     1
(33584923.0, 37253956.0]     1
(26246857.0, 29915890.0]     0
(29915890.0, 33584923.0]     0
Name: count, dtype: int64
ax = (state["Population"] / 1_000_000).plot.hist(figsize=(4, 4))
ax.set_xlabel("Population (millions)")

plt.tight_layout()
plt.show()

Density Plots and Estimates

ax = state["Murder.Rate"].plot.hist(density=True, xlim=[0, 12],
    bins=range(1, 12))
state["Murder.Rate"].plot.density(ax=ax)
ax.set_xlabel("Murder Rate (per 100,000)")

plt.tight_layout()
plt.show()

Exploring Binary and Categorical Data

dfw = pd.read_csv(DATA_DIR / "dfw_airline.csv")
ax = dfw.transpose().plot.bar(figsize=(4, 4), legend=False)
ax.set_xlabel("Cause of delay")
ax.set_ylabel("Count")

plt.tight_layout()
plt.show()

Correlation

sp500_px = pd.read_csv(DATA_DIR / "sp500_data.csv.gz", index_col=0)
sp500_sym = pd.read_csv(DATA_DIR / "sp500_sectors.csv")
etf_symbols = sp500_sym[sp500_sym["sector"] == "etf"]["symbol"]
etfs = sp500_px.loc[sp500_px.index > "2012-07-01", etf_symbols]
sns.heatmap(etfs.corr(), vmin=-1, vmax=1,
  cmap=sns.diverging_palette(20, 220, as_cmap=True))

plt.tight_layout()
plt.show()

Scatterplots

# Filter data for dates July 2012 through June 2015
telecom = sp500_px.loc[sp500_px.index >= "2012-07-01", :]

ax = telecom.plot.scatter(x="T", y="VZ", figsize=(4, 4), marker="$\u25EF$")
ax.set_xlabel("ATT (T)")
ax.set_ylabel("Verizon (VZ)")
ax.axhline(0, color="grey", lw=1)
ax.axvline(0, color="grey", lw=1)

plt.tight_layout()
plt.show()

Exploring Two or More Variables

Hexagonal Binning and Contours pass:[<span class#“keep-together”>(Plotting Numeric-Versus-Numeric Data)]

kc_tax = pd.read_csv(DATA_DIR / "kc_tax.csv.gz")
kc_tax0 = kc_tax.loc[(kc_tax.TaxAssessedValue < 750000) &
                     (kc_tax.SqFtTotLiving > 100) &
                     (kc_tax.SqFtTotLiving < 3500), :]
kc_tax0.shape
(432693, 3)
ax = kc_tax0.plot.hexbin(x="SqFtTotLiving", y="TaxAssessedValue",
                         gridsize=30, sharex=False, figsize=(5, 4))
ax.set_xlabel("Finished Square Feet")
ax.set_ylabel("Tax-Assessed Value")

plt.tight_layout()
plt.show()

ax = sns.kdeplot(data=kc_tax0.sample(10000), x="SqFtTotLiving",
    y="TaxAssessedValue")
ax.set_xlabel("Finished Square Feet")
ax.set_ylabel("Tax-Assessed Value")

plt.tight_layout()
plt.show()

Two Categorical Variables

lc_loans = pd.read_csv(DATA_DIR / "lc_loans.csv")

crosstab = lc_loans.pivot_table(index="grade", columns="status",
                                aggfunc=len, margins=True)
print(crosstab)

df = crosstab.loc["A":"G", :].copy()
perc_crosstab = df.loc[:, "Charged Off":"Late"].div(df["All"], axis=0)
perc_crosstab["All"] = df["All"] / sum(df["All"])
print(perc_crosstab)
status  Charged Off  Current  Fully Paid  Late     All
grade                                                 
A              1562    50051       20408   469   72490
B              5302    93852       31160  2056  132370
C              6023    88928       23147  2777  120875
D              5007    53281       13681  2308   74277
E              2842    24639        5949  1374   34804
F              1526     8444        2328   606   12904
G               409     1990         643   199    3241
All           22671   321185       97316  9789  450961
status  Charged Off   Current  Fully Paid      Late       All
grade                                                        
A          0.021548  0.690454    0.281528  0.006470  0.160746
B          0.040054  0.709013    0.235401  0.015532  0.293529
C          0.049828  0.735702    0.191495  0.022974  0.268039
D          0.067410  0.717328    0.184189  0.031073  0.164708
E          0.081657  0.707936    0.170929  0.039478  0.077177
F          0.118258  0.654371    0.180409  0.046962  0.028614
G          0.126196  0.614008    0.198396  0.061401  0.007187

Categorical and Numeric Data

airline_stats = pd.read_csv(DATA_DIR / "airline_stats.csv")
ax = airline_stats.boxplot(by="airline", column="pct_carrier_delay")
ax.set_xlabel("")
ax.set_ylabel("Daily % of Delayed Flights")
plt.suptitle("")

plt.tight_layout()
plt.show()

ax = sns.violinplot(data=airline_stats, x="airline", y="pct_carrier_delay",
                    inner="quartile", color="white")
ax.set_xlabel("")
ax.set_ylabel("Daily % of Delayed Flights")

plt.tight_layout()
plt.show()

Visualizing Multiple Variables

zip_codes = [98188, 98105, 98108, 98126]
kc_tax_zip = kc_tax0.loc[kc_tax0.ZipCode.isin(zip_codes), :]

def hexbin(x, y, color, **kwargs):
    cmap = sns.light_palette(color, as_cmap=True)
    plt.hexbin(x, y, gridsize=25, cmap=cmap, **kwargs)

g = sns.FacetGrid(kc_tax_zip, col="ZipCode", col_wrap=2)
g.map(hexbin, "SqFtTotLiving", "TaxAssessedValue",
      extent=[0, 3500, 0, 700000])
g.set_axis_labels("Finished Square Feet", "Tax-Assessed Value")
g.set_titles("Zip code {col_name:.0f}")

plt.tight_layout()
plt.show()

Supplementary Material

Table 1-2. A few rows of the data.frame state of population and murder rate by state

print(state.head(8))
         State  Population  Murder.Rate Abbreviation
0      Alabama     4779736          5.7           AL
1       Alaska      710231          5.6           AK
2      Arizona     6392017          4.7           AZ
3     Arkansas     2915918          5.6           AR
4   California    37253956          4.4           CA
5     Colorado     5029196          2.8           CO
6  Connecticut     3574097          2.4           CT
7     Delaware      897934          5.8           DE

Table 1-4. Percentiles of murder rate by state

percentages = [0.05, 0.25, 0.5, 0.75, 0.95]
df = pd.DataFrame(state["Murder.Rate"].quantile(percentages))
df.index = [f"{p * 100}%" for p in percentages]
print(df.transpose())
             5.0%  25.0%  50.0%  75.0%  95.0%
Murder.Rate   1.6  2.425    4.0   5.55   6.51

Table 1-5. A frequency table of population by state

binnedPopulation = pd.cut(state["Population"], 10)
print(binnedPopulation.value_counts())

binnedPopulation.name = "binnedPopulation"
df = pd.concat([state, binnedPopulation], axis=1)
df = df.sort_values(by="Population")

groups = []
for group, subset in df.groupby(by="binnedPopulation", observed=False):
    groups.append({
        "BinRange": group,
        "Count": len(subset),
        "States": ",".join(subset.Abbreviation),
    })
pd.DataFrame(groups)
Population
(526935.67, 4232659.0]      24
(4232659.0, 7901692.0]      14
(7901692.0, 11570725.0]      6
(11570725.0, 15239758.0]     2
(15239758.0, 18908791.0]     1
(18908791.0, 22577824.0]     1
(22577824.0, 26246857.0]     1
(33584923.0, 37253956.0]     1
(26246857.0, 29915890.0]     0
(29915890.0, 33584923.0]     0
Name: count, dtype: int64
BinRange Count States
0 (526935.67, 4232659.0] 24 WY,VT,ND,AK,SD,DE,MT,RI,NH,ME,HI,ID,NE,WV,NM,N...
1 (4232659.0, 7901692.0] 14 KY,LA,SC,AL,CO,MN,WI,MD,MO,TN,AZ,IN,MA,WA
2 (7901692.0, 11570725.0] 6 VA,NJ,NC,GA,MI,OH
3 (11570725.0, 15239758.0] 2 PA,IL
4 (15239758.0, 18908791.0] 1 FL
5 (18908791.0, 22577824.0] 1 NY
6 (22577824.0, 26246857.0] 1 TX
7 (26246857.0, 29915890.0] 0
8 (29915890.0, 33584923.0] 0
9 (33584923.0, 37253956.0] 1 CA

Table 1-6. Percentage of delays by cause at Dallas/Fort Worth Airport

dfw = pd.read_csv(DATA_DIR / "dfw_airline.csv")
print(100 * dfw / dfw.to_numpy().sum())
     Carrier        ATC   Weather  Security    Inbound
0  23.022989  30.400781  4.025214  0.122937  42.428079

Table 1-7. Correlation between telecommunication stock returns

telecomSymbols = sp500_sym[sp500_sym["sector"] == "telecommunications_services"]["symbol"]

# Filter data for dates July 2012 through June 2015
telecom = sp500_px.loc[sp500_px.index >= "2012-07-01", telecomSymbols]
telecom.corr()
print(telecom)
                   T       CTL       FTR        VZ      LVLT
Date                                                        
2012-07-02  0.422496  0.140847  0.070879  0.554180 -0.519998
2012-07-03 -0.177448  0.066280  0.070879 -0.025976 -0.049999
2012-07-05 -0.160548 -0.132563  0.055128 -0.051956 -0.180000
2012-07-06  0.342205  0.132563  0.007875  0.140106 -0.359999
2012-07-09  0.136883  0.124279 -0.023626  0.253943  0.180000
...              ...       ...       ...       ...       ...
2015-06-25  0.049342 -1.600000 -0.040000 -0.187790 -0.330002
2015-06-26 -0.256586  0.039999 -0.070000  0.029650 -0.739998
2015-06-29 -0.098685 -0.559999 -0.060000 -0.504063 -1.360000
2015-06-30 -0.503298 -0.420000 -0.070000 -0.523829  0.199997
2015-07-01 -0.019737  0.080000 -0.050000  0.355811  0.139999

[754 rows x 5 columns]
etfs = sp500_px.loc[sp500_px.index > "2012-07-01",
                    sp500_sym[sp500_sym["sector"] == "etf"]["symbol"]]
print(etfs.head())
                 XLI       QQQ       SPY       DIA       GLD    VXX       USO  \
Date                                                                            
2012-07-02 -0.376098  0.096313  0.028223 -0.242796  0.419998 -10.40  0.000000   
2012-07-03  0.376099  0.481576  0.874936  0.728405  0.490006  -3.52  0.250000   
2012-07-05  0.150440  0.096313 -0.103487  0.149420  0.239991   6.56 -0.070000   
2012-07-06 -0.141040 -0.491201  0.018819 -0.205449 -0.519989  -8.80 -0.180000   
2012-07-09  0.244465 -0.048160 -0.056445 -0.168094  0.429992  -0.48  0.459999   

                 IWM       XLE       XLY       XLU       XLB       XTL  \
Date                                                                     
2012-07-02  0.534641  0.028186  0.095759  0.098311 -0.093713  0.019076   
2012-07-03  0.926067  0.995942  0.000000 -0.044686  0.337373  0.000000   
2012-07-05 -0.171848 -0.460387  0.306431 -0.151938  0.103086  0.019072   
2012-07-06 -0.229128  0.206706  0.153214  0.080437  0.018744 -0.429213   
2012-07-09 -0.190939 -0.234892 -0.201098 -0.035751 -0.168687  0.000000   

                 XLV       XLP       XLF       XLK  
Date                                                
2012-07-02 -0.009529  0.313499  0.018999  0.075668  
2012-07-03  0.000000  0.129087  0.104492  0.236462  
2012-07-05 -0.142955 -0.073766 -0.142490  0.066211  
2012-07-06 -0.095304  0.119865  0.066495 -0.227003  
2012-07-09  0.352630 -0.064548  0.018999  0.009457  

Figure 1-6. Correlation between ETF returns (Python version)

def plot_corr_ellipses(data, figsize=None, **kwargs):
    """ https://stackoverflow.com/a/34558488 """
    m = np.array(data)
    if not m.ndim == 2:
        raise ValueError("data must be a 2D array")
    _, ax = plt.subplots(1, 1, figsize=figsize, subplot_kw={"aspect": "equal"})
    ax.set_xlim(-0.5, m.shape[1] - 0.5)
    ax.set_ylim(-0.5, m.shape[0] - 0.5)
    ax.invert_yaxis()

    # xy locations of each ellipse center
    xy = np.indices(m.shape)[::-1].reshape(2, -1).T

    # set the relative sizes of the major/minor axes according to the strength of
    # the positive/negative correlation
    w = np.ones_like(m).ravel() + 0.01
    h = 1 - np.abs(m).ravel() - 0.01
    a = 45 * np.sign(m).ravel()

    ec = EllipseCollection(widths=w, heights=h, angles=a, units="x", offsets=xy,
                           norm=Normalize(vmin=-1, vmax=1),
                           transOffset=ax.transData, array=m.ravel(), **kwargs)
    ax.add_collection(ec)

    # if data is a DataFrame, use the row/column names as tick labels
    if isinstance(data, pd.DataFrame):
        ax.set_xticks(np.arange(m.shape[1]))
        ax.set_xticklabels(data.columns, rotation=90)
        ax.set_yticks(np.arange(m.shape[0]))
        ax.set_yticklabels(data.index)

    return ec, ax

m, ax = plot_corr_ellipses(etfs.corr(), figsize=(5, 4), cmap="bwr_r")
cb = plt.colorbar(m, ax=ax)
cb.set_label("Correlation coefficient")

plt.tight_layout()
plt.show()

Figure 1-6. Correlation between ETF returns using a heat map (R version)