Getting Started
Installation & Import
pandas is conventionally imported as pd, with numpy as np since pandas uses numpy arrays under the hood. The two core data structures are Series (1D labeled array) and DataFrame (2D labeled table). Almost all tabular data manipulation in Python starts here.
# Install pandas (pick one)
pip install pandas
conda install pandas
# Standard import convention
import pandas as pd
import numpy as np # pandas relies heavily on numpy
# Check version
print(pd.__version__)
# Pandas is built on numpy: a Series is a 1D labeled array,
# a DataFrame is a 2D labeled table (dict of Series).Series & DataFrame
A Series has a single column of values plus an index (labels). A DataFrame is a collection of Series sharing a common index — think of it as a spreadsheet or SQL table. You can build DataFrames from dicts (keys=columns), lists of lists, numpy arrays, or files.
import pandas as pd
# Series: 1D labeled array
s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
# a 10
# b 20
# ...
# DataFrame: 2D table (dict of columns)
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"city": ["NYC", "LA", "SF"],
})
# From list of lists with column names
df2 = pd.DataFrame([[1, 2], [3, 4]], columns=["a", "b"])Creating a Sample DataFrame
For quick experiments, build a small DataFrame from a dict. For larger tests use numpy's random generators with a seed for reproducibility. date_range creates DatetimeIndex — the foundation for time-series work. Setting a meaningful index (dates, IDs) early makes later selection and joins much cleaner.
import pandas as pd
import numpy as np
# Quick test DataFrame with random data
df = pd.DataFrame({
"id": range(1, 6),
"name": ["Alice", "Bob", "Charlie", "David", "Eve"],
"age": [25, 30, 35, 28, 40],
"score": [85.5, 92.0, 78.3, 88.9, 95.2],
"pass": [True, True, False, True, True],
})
# Larger random DataFrame
rng = np.random.default_rng(42)
big = pd.DataFrame(rng.normal(size=(100, 4)), columns=list("ABCD"))
# Dates as index
dates = pd.date_range("2024-01-01", periods=5)
ts = pd.DataFrame({"value": [1, 2, 3, 4, 5]}, index=dates)Reading Data from Files
read_csv is the workhorse — always specify sep, encoding, parse_dates, and index_col when relevant to avoid post-load cleaning. Parquet is far faster and smaller than CSV for large datasets and preserves dtypes. For SQL, SQLAlchemy engines handle connection pooling. read_clipboard is surprisingly handy for quick copy-paste analysis.
import pandas as pd
# CSV — the most common
df = pd.read_csv("data.csv")
df = pd.read_csv("data.csv", sep=";", encoding="utf-8",
parse_dates=["date"], index_col="id")
# Excel (needs openpyxl)
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
# JSON
df = pd.read_json("data.json")
# Parquet (fast columnar format, great for large data)
df = pd.read_parquet("data.parquet")
# SQL (needs SQLAlchemy)
from sqlalchemy import create_engine
engine = create_engine("sqlite:///my.db")
df = pd.read_sql("SELECT * FROM users", engine)
# Clipboard (paste a table from a spreadsheet)
df = pd.read_clipboard()Saving Data
Always pass index=False when saving to CSV/Excel unless your index carries meaningful data — otherwise you get a spurious 'Unnamed: 0' column on reload. Parquet preserves dtypes across save/load (CSV doesn't). Use if_exists='append' to add rows to an existing SQL table, 'replace' to overwrite.
import pandas as pd
# CSV (no index by default is usually what you want)
df.to_csv("output.csv", index=False)
df.to_csv("output.csv", index=False, encoding="utf-8-sig") # Excel-friendly
# Excel (needs openpyxl)
df.to_excel("output.xlsx", sheet_name="Sheet1", index=False)
# Parquet (preserves dtypes, fast, compressed)
df.to_parquet("output.parquet")
# JSON
df.to_json("output.json", orient="records", indent=2)
# SQL
df.to_sql("users", engine, if_exists="replace", index=False)
# Multiple sheets to one Excel file
with pd.ExcelWriter("multi.xlsx") as writer:
df1.to_excel(writer, sheet_name="sheet1", index=False)
df2.to_excel(writer, sheet_name="sheet2", index=False)Data Inspection
Viewing Data: head, tail, sample
head/tail show the edges; sample shows random rows, which is more representative for sanity-checking distributions and formats. Use set_option to disable truncation when inspecting wide DataFrames — defaults hide columns and rows. Always look at sample data before any analysis to catch encoding issues, sentinel values, and type mismatches early.
import pandas as pd
# First/last rows
df.head() # first 5 (default)
df.head(10) # first 10
df.tail() # last 5
df.tail(3) # last 3
# Random sample (great for sanity checks)
df.sample(5) # 5 random rows
df.sample(frac=0.1, random_state=42) # 10% of rows
df.sample(n=5, replace=True) # sample with replacement (bootstrap)
# See the raw values (not truncated)
pd.set_option("display.max_rows", 200)
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 1000)Structure & Dtypes
info() is the single most useful inspection call — it shows dtypes, non-null counts per column, and memory. Watch for object dtype (usually strings stored inefficiently); converting to 'category' or 'string' dtype saves memory. memory_usage(deep=True) reveals the true cost of object columns, which is often much larger than reported.
# Shape and basic structure
df.shape # (rows, cols)
df.size # rows * cols
df.ndim # 2 for DataFrame, 1 for Series
len(df) # number of rows
# Column names and dtypes
df.columns # column labels
df.index # row labels
df.dtypes # dtype of each column
df.info() # concise summary: dtypes, non-null counts, memory
# Memory usage (helpful for big data)
df.memory_usage(deep=True)
df.info(memory_usage="deep")Descriptive Statistics
describe() gives a fast statistical summary — watch for mismatched count (missing values) and suspicious min/max. value_counts() is essential for categorical columns (shows frequency and class balance). corr() defaults to Pearson; pass method='spearman' for rank correlation. Always use numeric_only=True on mixed-type frames to avoid errors.
# Numeric summary (count, mean, std, min, quartiles, max)
df.describe()
df.describe(include="all") # include non-numeric too
df.describe(percentiles=[.1, .5, .9])
# Per-column stats
df["age"].mean()
df["age"].median()
df["age"].std()
df["age"].var()
df["age"].min(), df["age"].max()
df["age"].quantile(0.95)
# Non-numeric summaries
df["city"].value_counts()
df["city"].unique()
df["city"].nunique()
# Correlation and covariance
df.corr(numeric_only=True)
df.cov(numeric_only=True)Unique Values & Counts
value_counts is one of the most-used pandas methods — pair it with normalize=True for proportions and dropna=False to check missingness. crosstab builds a frequency table of two categoricals, useful for chi-square tests and confusion matrices. Binning continuous columns before value_counts reveals distribution shape.
# Unique values
df["city"].unique() # array of unique values
df["city"].nunique() # count of unique
df["city"].nunique(dropna=False) # include NaN
# Value counts (frequency table)
df["city"].value_counts()
df["city"].value_counts(normalize=True) # proportions
df["city"].value_counts(ascending=True)
df["city"].value_counts(dropna=False) # count NaN too
# Cross tabulation (frequency table of two columns)
pd.crosstab(df["city"], df["pass"])
# Binning then counting
df["age_group"] = pd.cut(df["age"], bins=[0, 18, 30, 50, 100],
labels=["child", "young", "adult", "senior"])
df["age_group"].value_counts()Checking for Missing Values
Always check missingness before analysis — isnull().sum() per column is the standard first look. Pay attention to columns with high missing fractions; they may need dropping or a different handling strategy. Rows with missing values in critical columns can be filtered with df[df.isnull().any(axis=1)]. The missingno library visualizes missingness patterns to spot structural data issues.
# Count missing per column
df.isnull().sum()
df.isna().sum() # alias
# Fraction missing
df.isnull().mean().mul(100).round(2) # % missing per column
# Total missing
df.isnull().sum().sum()
# Rows with any missing
df[df.isnull().any(axis=1)]
# Non-null counts
df.notnull().sum()
# Visualize missingness pattern
# (requires missingno: import missingno as msno; msno.matrix(df))Series Basics
Creating Series
A Series is a 1D array with labels (the index). The index lets you look up values by label, not just position. Specifying smaller dtypes (int8, float32) saves memory on large data — int8 holds -128..127, plenty for booleans-as-ints or small codes. Dict keys naturally become the index.
import pandas as pd
import numpy as np
# From a list
s = pd.Series([1, 2, 3, 4])
# From a list with custom index
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
# From a dict (keys become index)
s = pd.Series({"a": 1, "b": 2, "c": 3})
# From a numpy array
s = pd.Series(np.random.randn(5))
# Scalar broadcast to an index
s = pd.Series(5, index=["a", "b", "c"]) # [5, 5, 5]
# Specify dtype
s = pd.Series([1, 2, 3], dtype="float64")
s = pd.Series([1, 2, 3], dtype="int8") # save memoryIndexing & Selection
Label-based slicing (s['a':'c']) is INCLUSIVE on both ends — a common gotcha vs Python's positional slicing which is exclusive. Use .loc for labels and .iloc for positions to avoid ambiguity, especially with integer indexes. The default s[...] is overloaded (label or position depending on dtype), so explicit .loc/.iloc is safer in production code.
s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
# By label (index)
s["a"] # 10
s[["a", "c"]] # multiple labels -> Series
s["a":"c"] # slice INCLUSIVE of both ends (label-based)
# By position
s.iloc[0] # first by position
s.iloc[-1] # last
s.iloc[1:3] # EXCLUSIVE of end (positional, like Python)
# By label with .loc (explicit)
s.loc["a"]
s.loc["a":"c"] # inclusive
# Boolean indexing
s[s > 20]
s[(s > 10) & (s < 40)]Vectorized Operations
Operations on Series are vectorized (no Python loops) and align on index — when adding two Series, pandas matches labels and fills NaN where labels don't overlap. This index alignment is pandas' killer feature but can surprise you; use .values to operate on raw numpy arrays if you want pure positional math. Vectorized ops are 100-1000x faster than iterrows loops.