Introduction to Business Analytics — HKUST
Last time (Topic 1):
Today (Topic 2):
Workflow:
isom2600 package.head(), .shape, .dtypes, .describe()Why it matters
A DataFrame is like an Excel spreadsheet in Python — rows, columns, and an index. Each column is a Series.
import pandas as pd
# Load from a CSV file
df = pd.read_csv("data.csv")
# Or load from the course package
df = isom2600.data.sp500()Schema illustration:
| Index | Open | High | Low | Close |
|---|---|---|---|---|
| 2020-01 | 3244 | 3285 | 3214 | 3258 |
| 2020-02 | 3248 | 3260 | 3225 | 3245 |
| 2020-03 | 3230 | 3251 | 3210 | 3240 |
Rows = observations; columns = variables (each column is a Series).
Always do this first — it’s like meeting someone new, say hello first!
df = isom2600.data.sp500()
df.head() # First 5 rows
df.tail() # Last 5 rows
df.shape # (rows, columns)
df.dtypes # Data type of each column
df.columns # List of column namesKey takeaway
Step 1 of every analysis: Load the data, then run .head(), .shape, and .dtypes to understand what you have.
.describe()Try it!
Load the S&P 500 data and answer:
Hint: df.shape, df.columns, df['Close'].mean(), df['Volume'].max()
.head(), .tail(), .iloc[]Why it matters
Business: “I only want to see the Revenue column.” Single brackets = single column = Series.
Key takeaway
Single brackets df['col'] → Series
Double brackets df[['col1','col2']] → DataFrame
Key takeaway
.iloc[] selects by integer position (just like list indexing). Use it when you know the row number.
How it works:
df['Return'] < -2 creates a boolean mask (True/False for each row)df[mask] keeps only the True rowsKey takeaway
Use & for AND, | for OR.
Important: Each condition must be in parentheses!
Try it!
Find customers with income above median AND spending above $500.
Hint: df[(df['Income'] > df['Income'].median()) & (df['Spending'] > 500)]
.loc[]Key takeaway
.loc[] = label names. .iloc[] = integer positions.
When the index is dates, .loc['2020-01':'2020-03'] is far more readable than .iloc[0:65].
.loc[] is Inclusive, .iloc[] is ExclusiveImportant
.loc['b':'d'] returns rows b, c, d — the end label 'd' is included.
.iloc[1:3] returns positions 1, 2 — the end index 3 is excluded (just like Python list slicing).
df = isom2600.data.sp500()
df.head(3)
# Open High Low Close Volume
# 2019-12-31 3215.2 3224.0 3212.1 3230.8 2.89e+09
# 2020-01-02 3244.7 3258.1 3235.5 3257.9 3.46e+09
# 2020-01-03 3226.4 3246.2 3222.3 3234.9 3.46e+09
n = len(df)
train_size = int(0.8 * n)
# Time series: first 80% train, last 20% test
train = df.iloc[:train_size]
test = df.iloc[train_size:]
print(f"Train: {len(train)}, Test: {len(test)}")Why it matters
For time series: never shuffle — temporal order matters. Shuffling would leak future information into the training set.
df = isom2600.data.publicCompany()
df.head(3)
# sector marketCap currentPrice forwardPE beta
# SI Financial Services 2.13e+09 67.17 8.46 3.04
# LESL Consumer Cyclical 2.52e+09 13.77 13.77 NaN
# CPB Consumer Defensive 1.51e+10 50.37 16.51 0.36
n = len(df)
train_size = int(0.8 * n)
# Cross-sectional: shuffle first, then split
df_shuffled = df.sample(frac=1, random_state=42)
train = df_shuffled.iloc[:train_size]
test = df_shuffled.iloc[train_size:]
print(f"Train: {len(train)}, Test: {len(test)}")Key takeaway
Cross-sectional data has no time order → shuffle with .sample(frac=1, random_state=42) before splitting. random_state makes the shuffle reproducible.
Why it matters
Columns are Series, so arithmetic between columns is element-wise (row by row). No loops needed!
Key takeaway
Use list comprehension to create categorical columns from numerical data. This is data labeling!
Key takeaway
.drop(columns=...) removes columns by name.
.drop() returns a new DataFrame — the original is unchanged (unless you use inplace=True).
Why it matters
Method chaining reads like plain English: “take Close, compute returns, drop NaN, sort descending, show top 5.”
Each method returns a Series/DataFrame, so the next method can be called immediately. Use parentheses () to wrap multiline chains.
axis=0 (down columns) vs. axis=1 (across rows)Why it matters
Finding the best and worst days is one of the first things analysts do when studying market behavior.
axis — Rows vs. Columnsaxis visualization:
col A col B
row 0 | 10 | 20
row 1 | 30 | 40
row 2 | 50 | 60
axis=0 down (collapse rows)
axis=1 right (collapse columns)
Key takeaway
axis=0 collapses rows ⇒ one result per column (default).
axis=1 collapses columns ⇒ one result per row.
axis in ActionImportant
Most pandas methods default to axis=0 (operate down each column). You only need to write axis=1 when you want to compute across columns for each row.
Key takeaway
.corr() computes pairwise Pearson correlation. Values range from \(-1\) (perfectly opposite) to \(+1\) (perfectly together).
Business: Is there a correlation between ad spend and revenue?
.isna().fillna().dropna()Why it matters
Real-world data is messy. Sensors fail, surveys get skipped, records get lost. Pandas represents missing values as NaN (Not a Number).
Key takeaway
When to fill: You need every row and can estimate the missing value reasonably.
When to drop: Missing values are few (\(<5\%\)) and random.
Never: Ignore NaN and hope for the best!
dropna() — Rows vs. Columns with axisImportant
dropna() defaults to axis=0 (drop rows).
dropna(axis=1) drops columns — useful when an entire column is mostly empty.
Use subset= to only check specific columns instead of all.
Key takeaway
pandas integrates with matplotlib. The .plot() method handles most chart types. Customize with figsize, title, color, xlabel, ylabel.
| Chart | Code | Use for |
|---|---|---|
| Line | df['Close'].plot() |
Trends over time |
| Histogram | .plot(kind='hist') |
Distribution of one variable |
| Bar | .plot(kind='bar') |
Comparing categories |
| Box | .plot(kind='box') |
Spread, median, outliers |
Tip: screenshot this reference slide.
Try it!
Using the S&P 500 data:
Plot the closing price as a line chart
Plot the distribution of daily returns as a histogram
Count Bull vs Bear days and plot as a bar chart
Bonus: Which month had the highest average closing price?
Hint: Convert index to datetime, then use .resample('ME').mean()
\[x_{\text{scaled}} = \dfrac{x - \min}{\max - \min}\]
Why it matters
Use min-max when you need bounded output in \([0,1]\) — e.g., neural network inputs, or comparing variables with very different scales (price vs. volume).
\[x_{\text{std}} = \dfrac{x - \mu}{\sigma} \qquad \Rightarrow \qquad \mu_{\text{new}} = 0,\; \sigma_{\text{new}} = 1\]
Key takeaway
Min-max: Use when distribution is unknown or bounded output required.
Z-score: Use when data is roughly bell-shaped — preserves relative distances.
Neither: Use when scale is meaningful (e.g., price ratios, percentages).
datetime with pd.to_datetime().shift() and returns with .pct_change()pd.to_datetime() — Parse Date StringsKey takeaway
Always convert date strings to datetime — it unlocks date filtering, resampling, and all time-arithmetic operations.
.shift() and .pct_change()Key takeaway
.shift(k) lags by \(k\) periods. .pct_change(k) computes \(\frac{x_t - x_{t-k}}{x_{t-k}}\). The first \(k\) rows are NaN.
Why it matters
Moving averages smooth noise. Traders use MA-20 (short-term), MA-50 (medium), MA-200 (long-term) as trend signals.
Rolling std = realized volatility — a key risk input.
Key takeaway
.rolling(n).mean() computes the average over the last \(n\) observations.
Key takeaway
.cumprod() compounds returns. .cummax() tracks the running peak.
Drawdown \(= \frac{\text{price} - \text{running max}}{\text{running max}}\) is a standard risk metric.
The 6-step pipeline:
read_csv()head(), shape, dtypesdf[cond], .loc[], .iloc[]df['new'] = ...describe(), corr().plot()Key takeaway
Every data analysis follows this pipeline. Today you learned the tools for each step. Practice on your own data!
Prof. Xuhu Wan · HKUST ISOM · Intro to Business Analytics