pandas is a fast, flexible, and expressive Python library for working with labeled, tabular, and time-series data. Its two core objects — the one-dimensional Series and the two-dimensional DataFrame — give you a single, consistent toolkit for loading data from almost any source, cleaning and reshaping it, grouping and aggregating it, joining it with other data, and writing the result back out.
This guide shows you how to use pandas: how to install it, the everyday tasks that make up real data work, and the recipes you reach for again and again. It is organized around what you want to do with your data — not around the library's internal module layout. If you need pandas' architecture, build system, or internals, see the pandas Admin Guide.
Note
Throughout this guide, pandas is imported under its conventional alias and NumPy alongside it:
import pandas as pd import numpy as npEvery example assumes these two imports are already in place.
1. Quick Start
The fastest way to understand pandas is to build a DataFrame, look at it, and ask it a question.
import pandas as pd
# Build a DataFrame from a dict of columns
df = pd.DataFrame({
"name": ["Ada", "Bram", "Cleo", "Dao"],
"team": ["A", "B", "A", "B"],
"score": [91, 72, 88, 64],
})
print(df)
name team score
0 Ada A 91
1 Bram B 72
2 Cleo A 88
3 Dao B 64
Now answer a real question — what is the average score per team? — in one line:
print(df.groupby("team")["score"].mean())
team
A 89.5
B 68.0
Name: score, dtype: float64
That single expression captures the split-apply-combine pattern that pandas is built around: split the rows into groups, apply a calculation, and combine the results back into a labeled object. The rest of this guide expands on every step.
Tip
The two objects you will use constantly are
Series(a single labeled column) andDataFrame(a table ofSeriesthat share one row index). Almost every method exists on both, and selecting a single column from aDataFramegives you aSeries.
2. Installation
pandas ships as a pre-built binary wheel for all common platforms, so a normal install requires no compiler.
pip (PyPI)
python -m pip install pandas
conda (conda-forge)
conda install -c conda-forge pandas
With optional extras
# Excel, Parquet, plotting, and HTML support in one go
python -m pip install "pandas[excel,parquet,plot,html]"
Verify the install and inspect your environment:
import pandas as pd
print(pd.__version__)
pd.show_versions() # full dependency + system report, useful for bug reports
3.0.0
INSTALLED VERSIONS
------------------
python : 3.13.x
pandas : 3.0.0
numpy : 2.x
...
2.1 Requirements
pandas requires Python 3.11 or newer and these runtime dependencies:
| Dependency | Role | Notes |
|---|---|---|
numpy |
Underlying numeric arrays | >=1.26, <3 |
python-dateutil |
Flexible date parsing | >=2.8.2 |
tzdata |
IANA time-zone database | Required on Windows and Emscripten only |
Important
numpyandpython-dateutilare hard dependencies — pandas refuses to import without them and raises anImportErrornaming the missing package.
2.2 Optional dependencies
Many pandas features activate only when an optional package is installed. You do not need any of these to start, but a feature will raise an ImportError telling you exactly which package to install when you first use it.
| Feature you want | Install |
|---|---|
Read/write Excel (.xlsx) |
openpyxl, or xlsxwriter (write), python-calamine/xlrd (read), odfpy (.ods) |
| Read/write Parquet | pyarrow (recommended) or fastparquet |
Read/write HDF5 (.h5) |
pytables |
| Read/write Feather / ORC | pyarrow |
| SQL databases | sqlalchemy plus a driver (psycopg2, pymysql, adbc-driver-*) |
HTML tables (read_html) |
lxml, html5lib, or beautifulsoup4 |
XML (read_xml / to_xml) |
lxml |
| Plotting | matplotlib |
| Styler HTML/Excel export | jinja2 |
Numba-accelerated apply / windows |
numba |
| Faster evaluation | numexpr, bottleneck |
| Arrow-backed dtypes | pyarrow (>=13) |
Cloud storage paths (s3://, gs://) |
fsspec + s3fs / gcsfs |
| SAS / SPSS / Stata via reader | pyreadstat |
| Clipboard I/O | pyqt/qtpy (Linux) |
Tip
When in doubt, install
pyarrowandmatplotlib. Between them they unlock Parquet, Feather, ORC, Arrow-backed dtypes, and all charting — the features most users reach for first.
3. The Two Core Objects
3.1 Series — a labeled column
A Series is a one-dimensional array of values paired with an index (the labels).
s = pd.Series([10, 20, 30], index=["a", "b", "c"], name="counts")
print(s)
a 10
b 20
c 30
Name: counts, dtype: int64
print(s["b"]) # label-based access -> 20
print(s.mean()) # 20.0
print(s * 2) # vectorized: every element doubled
3.2 DataFrame — a labeled table
A DataFrame is a collection of Series that share a single row index. You can build one from a dict of columns, a list of rows, NumPy arrays, or by reading a file.
From a dict of columns
df = pd.DataFrame({
"city": ["Phoenix", "Tucson", "Mesa"],
"pop": [1_650_000, 543_000, 504_000],
})
From a list of row dicts
df = pd.DataFrame([
{"city": "Phoenix", "pop": 1_650_000},
{"city": "Tucson", "pop": 543_000},
])
From a NumPy array
import numpy as np
df = pd.DataFrame(
np.arange(6).reshape(3, 2),
columns=["x", "y"],
)
3.3 First look at any DataFrame
When you receive an unfamiliar table, these methods orient you instantly:
| Method | What it tells you |
|---|---|
df.head(n) / df.tail(n) |
First / last n rows (default 5) |
df.shape |
(rows, columns) tuple |
df.info() |
Column names, non-null counts, dtypes, memory use |
df.describe() |
Summary statistics for numeric columns |
df.dtypes |
The dtype of each column |
df.columns / df.index |
The column and row labels |
df.sample(n) |
A random n-row sample |
df.info()
<class 'pandas.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 city 3 non-null object
1 pop 3 non-null int64
Note
A column of text shows as dtype
objectby default. To get pandas' dedicated, more memory-efficient string type, passdtype="string"when constructing, or calldf["city"] = df["city"].astype("string"). See §11 on dtypes.
4. Reading and Writing Data (I/O)
pandas' I/O layer is one of its biggest draws: a family of pd.read_* functions that pull data in and matching .to_* methods that write it back out. Every reader returns a DataFrame (or Series); every writer is a method on the object you want to save.
4.1 The reader / writer matrix
| Format | Read | Write | Optional dependency |
|---|---|---|---|
| CSV / delimited text | pd.read_csv |
df.to_csv |
built-in |
| Fixed-width text | pd.read_fwf |
— | built-in |
| Excel | pd.read_excel |
df.to_excel |
openpyxl / xlsxwriter |
| JSON | pd.read_json |
df.to_json |
built-in |
| HTML tables | pd.read_html |
df.to_html |
lxml / bs4 |
| XML | pd.read_xml |
df.to_xml |
lxml |
| Parquet | pd.read_parquet |
df.to_parquet |
pyarrow |
| Feather | pd.read_feather |
df.to_feather |
pyarrow |
| ORC | pd.read_orc |
df.to_orc |
pyarrow |
| HDF5 | pd.read_hdf |
df.to_hdf |
pytables |
| Stata | pd.read_stata |
df.to_stata |
built-in |
| SAS | pd.read_sas |
— | built-in / pyreadstat |
| SPSS | pd.read_spss |
— | pyreadstat |
| Pickle | pd.read_pickle |
df.to_pickle |
built-in |
| Clipboard | pd.read_clipboard |
df.to_clipboard |
platform |
| SQL | pd.read_sql / read_sql_query / read_sql_table |
df.to_sql |
sqlalchemy + driver |
| Iceberg | pd.read_iceberg |
df.to_iceberg |
pyiceberg |
4.2 CSV — the workhorse
df = pd.read_csv("sales.csv")
read_csv has the deepest option set of any reader. The ones you will use most:
| Parameter | Purpose | Example |
|---|---|---|
sep / delimiter |
Field separator | sep="\t" for TSV |
header |
Row number(s) to use as column names | header=None |
names |
Supply column names yourself | names=["a","b"] |
index_col |
Column to use as the row index | index_col=0 |
usecols |
Read only some columns | usecols=["date","total"] |
dtype |
Force column dtypes | dtype={"id": "int32"} |
parse_dates |
Parse columns as datetimes | parse_dates=["date"] |
nrows |
Read only the first N rows | nrows=1000 |
skiprows |
Skip leading rows | skiprows=3 |
na_values |
Extra strings to treat as missing | na_values=["N/A", "-"] |
encoding |
File text encoding | encoding="latin-1" |
chunksize |
Iterate in chunks (for big files) | chunksize=100_000 |
engine |
Parser backend | engine="pyarrow" |
compression |
De/compress on the fly | compression="gzip" |
Tip
Choose the right CSV engine for the job. The valid values are
"c"(the default, fast),"python"(most flexible, supports regex separators),"pyarrow"(multi-threaded, fastest for large files — requirespyarrow), and"python-fwf"(fixed-width). Pass it asengine=....
Reading a large file without exhausting memory:
total = 0
for chunk in pd.read_csv("huge.csv", chunksize=100_000):
total += chunk["amount"].sum()
print(total)
Writing back out — note index=False to avoid dumping the row labels as a column:
df.to_csv("clean.csv", index=False)
4.3 Excel
# Read one sheet
df = pd.read_excel("report.xlsx", sheet_name="Q1")
# Read every sheet into a dict of DataFrames
sheets = pd.read_excel("report.xlsx", sheet_name=None)
# Write multiple DataFrames to separate sheets
with pd.ExcelWriter("out.xlsx") as writer:
q1.to_excel(writer, sheet_name="Q1", index=False)
q2.to_excel(writer, sheet_name="Q2", index=False)
Note
When you give an existing workbook to
ExcelWriter, theif_sheet_existsargument controls collisions. Valid values are"error"(the default),"new","replace", and"overlay".
4.4 Parquet, Feather, and ORC — fast columnar formats
These binary formats preserve dtypes exactly, compress well, and load far faster than CSV. They require pyarrow.
df.to_parquet("data.parquet") # write
df = pd.read_parquet("data.parquet") # read
# Read just two columns from a large file — columnar formats make this cheap
df = pd.read_parquet("data.parquet", columns=["date", "total"])
Tip
For any dataset you will load more than once, save it as Parquet rather than CSV. Round-trips are faster, files are smaller, and — unlike CSV — dtypes survive the trip.
4.5 JSON
df = pd.read_json("records.json")
# Flatten nested JSON into a flat table
records = [{"id": 1, "user": {"name": "Ada", "city": "Phoenix"}}]
flat = pd.json_normalize(records)
# id user.name user.city
# 0 1 Ada Phoenix
to_json and read_json share an orient argument that controls the JSON shape: "records", "columns", "index", "split", "table", and "values".
4.6 SQL databases
from sqlalchemy import create_engine
engine = create_engine("postgresql+psycopg2://user:pw@host/db")
# Full table
df = pd.read_sql_table("customers", engine)
# Arbitrary query (use parameters, never string-format user input)
df = pd.read_sql_query(
"SELECT * FROM orders WHERE total > %(min)s",
engine,
params={"min": 100},
)
# Write a DataFrame to a table
df.to_sql("orders_clean", engine, if_exists="replace", index=False)
Warning
Build SQL with parameters (
params=...), not Python string formatting. Interpolating user input directly into a query string exposes you to SQL injection.
5. Selecting and Indexing Data
Getting the rows and columns you want is the most common pandas task. There are three doors into a DataFrame, and choosing the right one prevents most beginner confusion.
5.1 The three accessors
| Accessor | Selects by | Use for |
|---|---|---|
[] |
Column name (or boolean mask) | Quick column or filter |
.loc[] |
Labels (row label, column name) | Almost everything |
.iloc[] |
Integer position (0-based) | Position-based slicing |
df["score"] # one column -> Series
df[["name", "score"]] # several columns -> DataFrame
df.loc[0, "score"] # value at row label 0, column "score"
df.loc[0:2, ["name", "score"]] # label slice (END-INCLUSIVE)
df.iloc[0:2, [0, 2]] # position slice (END-EXCLUSIVE)
Important
.loclabel slices include the endpoint (df.loc[0:2]returns rows 0, 1, and 2), while.ilocposition slices exclude it (df.iloc[0:2]returns rows 0 and 1) — matching normal Python slicing. This is the single most common source of off-by-one surprises in pandas.
5.2 Boolean filtering (the workhorse)
Build a boolean Series and pass it inside [] or .loc[]:
df[df["score"] >= 80] # rows where score >= 80
df[(df["score"] >= 80) & (df["team"] == "A")] # AND
df[(df["team"] == "A") | (df["team"] == "B")] # OR
df[~(df["team"] == "A")] # NOT
Caution
Use
&,|, and~— not the Python keywordsand,or,not— for element-wise boolean logic, and wrap each condition in parentheses. The bitwise operators bind tighter than comparisons, sodf["a"] > 1 & df["b"] < 2is parsed asdf["a"] > (1 & df["b"]) < 2and raisesValueError: The truth value of a Series is ambiguous.
Other handy filters:
df[df["team"].isin(["A", "C"])] # membership
df[df["score"].between(70, 90)] # inclusive range
df[df["name"].str.startswith("A")] # string predicate
df.query("score >= 80 and team == 'A'") # string expression form
5.3 Adding, renaming, and dropping
df["passed"] = df["score"] >= 70 # new column from a vector
df = df.assign(bonus=df["score"] * 0.1) # functional, returns a new frame
df = df.rename(columns={"score": "points"})
df = df.drop(columns=["bonus"]) # drop columns
df = df.drop(index=[0, 1]) # drop rows by label
5.4 Setting and resetting the index
df = df.set_index("name") # promote a column to the row index
df = df.reset_index() # push the index back to a column
6. Cleaning Data and Handling Missing Values
Real data is messy. pandas represents "missing" with NaN (for floats), NaT (for datetimes), and the dtype-agnostic pd.NA. A consistent set of methods detects and fills it.
6.1 Detecting missing data
df.isna() # element-wise boolean mask of missing values
df.isna().sum() # count of missing values per column
df.notna() # the inverse
df["col"].isna().mean() # fraction missing in one column
Note
isnull/notnullare exact aliases ofisna/notna. Use whichever reads better — they behave identically.
6.2 Dropping vs. filling
df.dropna() # drop any row containing a missing value
df.dropna(how="all") # drop only rows that are entirely missing
df.dropna(subset=["score"]) # consider only specific columns
df.dropna(axis=1) # drop columns (instead of rows)
df.fillna(0) # replace missing with a constant
df.fillna({"score": 0, "city": "unknown"}) # per-column fills
df["score"].ffill() # forward-fill (carry last value forward)
df["score"].bfill() # back-fill
df["value"].interpolate() # numeric/temporal interpolation
Tip
howaccepts"any"(default — drop if any value is missing) or"all"(drop only if every value is missing).
6.3 Removing duplicates
df.duplicated() # boolean mask of duplicate rows
df.drop_duplicates() # keep the first occurrence of each row
df.drop_duplicates(subset=["id"], keep="last")
Note
The
keepargument chooses which duplicate survives:"first"(default),"last", orFalse(drop all duplicated rows).
6.4 Replacing and converting values
df["grade"] = df["grade"].replace({"A+": "A", "B-": "B"})
df["amount"] = pd.to_numeric(df["amount"], errors="coerce") # bad -> NaN
df["when"] = pd.to_datetime(df["when"], errors="coerce")
Important
The
errorsargument onpd.to_numeric,pd.to_datetime, andpd.to_timedeltaaccepts"raise"(default — stop on the first bad value) or"coerce"(turn unparseable values intoNaN/NaT). Use"coerce"when you expect dirty input and want to find the bad rows afterward with.isna().
6.5 Working with text columns — the .str accessor
Vectorized string methods live under .str and operate on a whole column at once:
df["name"].str.lower()
df["name"].str.strip()
df["name"].str.contains("a", case=False)
df["email"].str.split("@").str[1] # domain part
df["phone"].str.replace(r"\D", "", regex=True) # keep digits only
df["name"].str.len()
7. Transforming and Computing
7.1 Vectorized arithmetic
Operations apply to whole columns at once — no Python loop required, and far faster than one.
df["total"] = df["price"] * df["qty"]
df["pct"] = df["score"] / df["score"].sum() * 100
df["z"] = (df["x"] - df["x"].mean()) / df["x"].std()
7.2 Summary statistics
Every numeric Series and DataFrame exposes the standard reductions:
| Method | Result |
|---|---|
.sum() .mean() .median() |
Totals and central tendency |
.min() .max() |
Extremes |
.std() .var() |
Spread |
.count() |
Non-missing count |
.nunique() |
Number of distinct values |
.value_counts() |
Frequency table of a column |
.quantile(q) |
The q-th quantile |
.cumsum() .cumprod() |
Running totals |
.describe() |
All of the above at once |
df["team"].value_counts()
A 2
B 2
Name: count, dtype: int64
Note
By default reductions skip missing values (
skipna=True). Passskipna=Falseto make anyNaNpropagate into the result.
7.3 Applying your own functions
When a built-in method does not exist, map a function over the data:
df["score"].map(lambda v: "pass" if v >= 70 else "fail") # element-wise on a Series
df.apply(np.mean) # reduce each column
df.apply(lambda row: row["a"] + row["b"], axis=1) # across columns, per row
Tip
Reach for
applyonly when a vectorized method genuinely does not exist.df["x"] * 2is dramatically faster thandf["x"].apply(lambda v: v * 2)because it runs in compiled code rather than calling Python once per row.
7.4 Binning continuous values
pd.cut(df["age"], bins=[0, 18, 65, 120], labels=["child", "adult", "senior"])
pd.qcut(df["income"], q=4) # four equal-sized quantile buckets
8. Group By: Split–Apply–Combine
groupby is the heart of analytical pandas. It splits rows into groups by one or more keys, applies a function to each group, and combines the answers into a labeled result.
df.groupby("team")["score"].mean() # one stat, one column
df.groupby("team").agg({"score": "mean", "name": "count"})
df.groupby(["team", "passed"])["score"].sum() # multiple keys -> MultiIndex
8.1 Named aggregations
To control the output column names, use pd.NamedAgg (or the (column, func) tuple form):
df.groupby("team").agg(
avg_score=pd.NamedAgg(column="score", aggfunc="mean"),
n_players=pd.NamedAgg(column="name", aggfunc="count"),
)
avg_score n_players
team
A 89.5 2
B 68.0 2
8.2 transform vs. aggregate vs. filter
| Operation | Returns | Use when |
|---|---|---|
.agg(...) |
One row per group | You want a summary table |
.transform(...) |
Same shape as input | You want to broadcast a group stat back onto every row |
.filter(...) |
A subset of rows | You want to keep/drop entire groups by a condition |
# Add a column holding each row's group mean (transform keeps the original shape)
df["team_avg"] = df.groupby("team")["score"].transform("mean")
# Keep only groups with more than one member
df.groupby("team").filter(lambda g: len(g) > 1)
Tip
transformis the tool for "compare each value to its group" — for example computing a within-group z-score or the percent of a group total. It returns a result aligned to the original rows so you can assign it straight back as a new column.
9. Combining DataFrames
9.1 Concatenating (stacking)
pd.concat([df_jan, df_feb]) # stack rows (same columns)
pd.concat([df_left, df_right], axis=1) # stack columns (same index)
9.2 Merging / joining (database-style)
pd.merge aligns two frames on one or more key columns, just like a SQL join.
pd.merge(orders, customers, on="customer_id", how="left")
pd.merge(a, b, left_on="uid", right_on="id", how="inner")
The how argument selects the join type:
how |
Keeps |
|---|---|
"inner" |
Only keys present in both frames (default) |
"left" |
All keys from the left frame |
"right" |
All keys from the right frame |
"outer" |
The union of keys from both |
"cross" |
The Cartesian product (every row paired with every row) |
"left_anti" |
Left rows whose key is not in the right frame |
"right_anti" |
Right rows whose key is not in the left frame |
Tip
Pass
validate="one_to_one"(or"one_to_many","many_to_one") to have pandas assert the relationship between your keys. If the data violates it — e.g. a "unique" key that has duplicates — the merge raises immediately instead of silently producing a row explosion.
9.3 Specialized merges
# As-of (nearest-key) merge — essential for time-series alignment
pd.merge_asof(trades, quotes, on="time", by="symbol")
# Ordered merge with optional group-wise fill
pd.merge_ordered(a, b, on="date", fill_method="ffill")
10. Reshaping and Pivoting
| Function / method | Turns | Into |
|---|---|---|
df.pivot_table(...) |
Long rows | A wide summary grid (with aggregation) |
df.pivot(...) |
Long rows | Wide (no aggregation; keys must be unique) |
pd.melt(df, ...) |
Wide columns | Long key/value rows |
df.stack() / df.unstack() |
Move levels between rows and columns | |
pd.crosstab(a, b) |
Two vectors | A frequency cross-tabulation |
pd.get_dummies(df) |
Categorical column | One-hot indicator columns |
pd.pivot_table(
df, values="score", index="team", columns="passed", aggfunc="mean"
)
pd.melt(df, id_vars=["name"], value_vars=["math", "science"],
var_name="subject", value_name="grade")
pd.get_dummies(df["team"]) # -> columns team_A, team_B (0/1)
Note
pivotrequires the index/column combination to be unique and only reshapes;pivot_tableaggregates duplicate combinations withaggfunc(default"mean"). IfpivotraisesValueError: Index contains duplicate entries, switch topivot_table.
11. Data Types (dtypes)
Every column has a single dtype that determines how it is stored and what operations are valid. Picking good dtypes saves memory and prevents surprises.
11.1 The dtype families
| Family | Examples | Notes |
|---|---|---|
| NumPy numeric | int64, float64, bool |
Default; cannot hold missing values in integer columns |
| Nullable (masked) | Int64, Float64, boolean |
Capital first letter; support pd.NA |
| String | string |
Dedicated text type (vs. generic object) |
| Categorical | category |
Repeated labels stored once — big memory win |
| Datetime / timedelta | datetime64[ns], timedelta64[ns] |
Time-series native types |
| Period / Interval | period[M], interval |
Spans and ranges |
| Arrow-backed | int64[pyarrow], string[pyarrow] |
Backed by Apache Arrow (needs pyarrow) |
11.2 Converting dtypes
df["id"] = df["id"].astype("int32")
df["team"] = df["team"].astype("category") # memory + speed for repeated text
df["name"] = df["name"].astype("string")
df = df.convert_dtypes() # auto-pick best nullable dtypes
df = df.astype({"a": "Int64", "b": "float64"}) # several at once
Tip
Convert any column with few distinct repeated values (status codes, country names, categories) to
"category". On large frames this can cut memory use by an order of magnitude and speeds upgroupbyon that column.
11.3 Nullable integers and pd.NA
A plain NumPy int64 column cannot store missing values — pandas would silently promote it to float64. The nullable Int64 dtype (capital I) fixes this:
s = pd.Series([1, 2, None], dtype="Int64")
# 0 1
# 1 2
# 2 <NA>
# dtype: Int64
12. Working with Time Series
Time-series support is a flagship pandas feature: a rich timestamp type, calendar-aware date ranges, frequency conversion, and rolling-window statistics.
12.1 Timestamps and date ranges
pd.Timestamp("2026-06-04 09:30")
pd.to_datetime(df["when"]) # parse a column
pd.date_range("2026-01-01", periods=12, freq="MS") # 12 month-start dates
pd.bdate_range("2026-01-01", "2026-01-31") # business days only
Common frequency aliases for freq=: "D" (day), "B" (business day), "W" (week), "MS"/"ME" (month start/end), "QS"/"QE" (quarter), "YS"/"YE" (year), "h", "min", "s".
12.2 Datetime components — the .dt accessor
df["when"].dt.year
df["when"].dt.month_name()
df["when"].dt.dayofweek # Monday=0
df["when"].dt.normalize() # strip the time part
df["when"].dt.tz_localize("UTC").dt.tz_convert("US/Arizona")
12.3 Resampling (time-based group by)
When the index is a datetime, resample groups rows into time buckets:
ts = ts.set_index("when")
ts["sales"].resample("ME").sum() # monthly totals
ts["price"].resample("W").mean() # weekly average
12.4 Rolling and expanding windows
df["roll7"] = df["value"].rolling(window=7).mean() # 7-row moving average
df["cumavg"] = df["value"].expanding().mean() # cumulative average
df["ewm"] = df["value"].ewm(span=10).mean() # exponentially weighted
Tip
For an irregular time index, give
rollinga time-based window string such asrolling("30D")instead of an integer count — pandas then uses a true 30-day window regardless of how many rows fall inside it.
13. Plotting
With matplotlib installed, every Series and DataFrame gains a .plot accessor.
df["score"].plot(kind="line")
df.plot(x="date", y="total", kind="line")
df["team"].value_counts().plot(kind="bar")
df.plot.scatter(x="height", y="weight")
df["score"].plot(kind="hist", bins=20)
Valid kind values: "line", "bar", "barh", "hist", "box", "kde"/"density", "area", "pie", "scatter", and "hexbin".
Note
kind="scatter","hexbin", and"pie"apply toDataFrameobjects and need their axis columns specified. Some kinds raiseValueError: pie requires either y column or 'subplots=True'if you omit the column.
14. Displaying and Configuring Output
pandas truncates wide and tall output by default. Control this with the options system:
pd.set_option("display.max_rows", 100)
pd.set_option("display.max_columns", 50)
pd.set_option("display.precision", 3) # decimal places shown
pd.set_option("display.width", 120)
pd.get_option("display.max_rows") # read a setting
pd.reset_option("display.max_rows") # restore the default
pd.describe_option("display.max_rows") # see docs + current value
Scope a setting to a single block with a context manager so it reverts automatically:
with pd.option_context("display.max_rows", None):
print(df) # show every row, just here
Tip
Frequently useful display options:
display.max_rows,display.max_columns,display.precision,display.float_format(e.g.pd.set_option("display.float_format", "{:,.2f}".format)),display.max_colwidth, anddisplay.expand_frame_repr.
15. Common Recipes
Top N rows by a column
df.nlargest(5, "score")
df.nsmallest(3, "price")
Conditional new column (vectorized if/else)
df["band"] = np.where(df["score"] >= 70, "pass", "fail")
Multi-condition mapping
df["band"] = pd.cut(df["score"], [0, 60, 80, 100],
labels=["low", "mid", "high"])
Sort by one or more columns
df.sort_values(["team", "score"], ascending=[True, False])
df.sort_index()
Count and percentage in one table
counts = df["team"].value_counts()
pct = df["team"].value_counts(normalize=True).mul(100).round(1)
pd.DataFrame({"count": counts, "pct": pct})
Chained, readable pipeline
result = (
df
.query("score >= 70")
.assign(bonus=lambda d: d["score"] * 0.1)
.groupby("team", as_index=False)
.agg(total=("score", "sum"))
.sort_values("total", ascending=False)
)
Tip
Wrap a multi-step transformation in parentheses and put one method per line. This "method chaining" style reads top-to-bottom like a recipe and avoids littering your code with throwaway intermediate variables.
16. Troubleshooting
Error: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all()
Cause: You combined boolean conditions with Python's and/or, or forgot parentheses around comparisons.
Solution: Use &, |, ~ and wrap each condition: df[(df.a > 1) & (df.b < 2)].
Prevention: Treat parentheses around every comparison as mandatory in filters.
Error: KeyError: 'columnname'
Cause: The column or index label does not exist — often a stray space or a case mismatch.
Solution: Check df.columns.tolist(). Strip whitespace with df.columns = df.columns.str.strip().
Prevention: Inspect df.info() right after loading so you know the exact labels.
Error: SettingWithCopyWarning / changes that don't "stick"
Cause: Assigning into the result of a chained selection (df[df.a > 1]["b"] = 0) writes to a temporary copy.
Solution: Use a single .loc assignment: df.loc[df["a"] > 1, "b"] = 0.
Prevention: Whenever you both filter and assign, do it in one .loc[rows, cols] = value statement.
Error: ValueError: Index contains duplicate entries, cannot reshape
Cause: pivot (or unstack) found duplicate index/column key combinations.
Solution: Switch to pivot_table, which aggregates duplicates via aggfunc.
Prevention: Confirm key uniqueness with df.duplicated(subset=keys).any() before pivoting.
Error: ImportError: Missing optional dependency 'openpyxl' (or pyarrow, lxml, matplotlib, …)
Cause: The format/feature you used needs an optional package that is not installed.
Solution: pip install openpyxl (or whichever package the message names).
Prevention: Install pyarrow and matplotlib up front; they cover most optional features.
Error: MergeError / unexpected row explosion after merge
Cause: A key you assumed was unique actually has duplicates, so rows multiply (a many-to-many join).
Solution: Add validate="one_to_many" (or the right relationship) to make pandas check; deduplicate the key side first.
Prevention: Run df["key"].is_unique on the side you expect to be unique before merging.
Symptom: An integer column became float64 after introducing missing values
Cause: NumPy int64 cannot hold NaN, so pandas promotes the column to float64.
Solution: Use the nullable Int64 dtype: s.astype("Int64").
Prevention: Construct integer columns that may contain gaps with dtype="Int64" from the start.
Error: TypeError: only list-like objects are allowed to be passed to isin(), you passed a 'str'
Cause: You passed a single string to .isin(), which expects a collection.
Solution: Wrap it in a list: df[df["team"].isin(["A"])].
Prevention: Always give .isin() a list/set, even for one value.
Symptom: read_csv is slow or runs out of memory on a huge file
Cause: The whole file is being read into memory at once.
Solution: Read in chunks with chunksize=..., select only needed columns with usecols=..., or switch to engine="pyarrow".
Prevention: For repeated loads, convert the source to Parquet once and read that instead.
Symptom: Dates loaded as plain strings, so .dt accessor fails
Cause: read_csv does not parse dates unless told to.
Solution: Pass parse_dates=["col"], or convert afterward with pd.to_datetime(df["col"]).
Prevention: Specify parse_dates at read time.
17. Frequently Asked Questions
Q: What is the difference between a Series and a DataFrame?
A Series is one labeled column (1-D); a DataFrame is a table of Series sharing one row index (2-D). Selecting a single column from a DataFrame returns a Series.
Q: When should I use .loc vs .iloc?
Use .loc for label-based access (row/column names) and .iloc for integer position. .loc is what you want the vast majority of the time. Remember .loc slices include the endpoint; .iloc slices do not.
Q: How do I avoid the SettingWithCopyWarning?
Make filtered assignments in one statement with .loc: df.loc[mask, "col"] = value. Do not chain df[mask]["col"] = value.
Q: My numbers turned into floats with .0 everywhere — why?
A NumPy integer column was promoted to float64 because a missing value was introduced. Use the nullable Int64 dtype to keep integers and still allow pd.NA.
Q: How do I apply a function to every row?
df.apply(func, axis=1). But first check whether a vectorized expression can do the same thing far faster — apply calls Python once per row.
Q: How do I get the unique values / their counts in a column?
df["col"].unique() for the distinct values, df["col"].nunique() for the count, and df["col"].value_counts() for a frequency table.
Q: How do I rename columns?
df.rename(columns={"old": "new"}), or assign a fresh list to df.columns.
Q: How do I select rows where a column is in a set of values?
df[df["col"].isin(["a", "b", "c"])].
Q: Why is my filter raising "truth value of a Series is ambiguous"?
You used and/or instead of &/|, or omitted parentheses. Write df[(cond1) & (cond2)].
Q: How do I read just part of a giant CSV?
Use nrows= to cap rows, usecols= to limit columns, or chunksize= to iterate. For repeated work, convert to Parquet.
Q: What's the fastest on-disk format for round-tripping a DataFrame?
Parquet (via pyarrow). It is compact, fast, and — unlike CSV — preserves dtypes exactly.
Q: How do I see all rows/columns instead of the truncated view?
Raise or remove the display limits: pd.set_option("display.max_rows", None), or scope it with pd.option_context.
Q: Does pandas have a command-line tool?
No. pandas is a library you import (import pandas as pd) and use from Python, notebooks, or scripts. It ships no end-user CLI.
Q: Which Python versions are supported? Python 3.11 and newer. Free-threaded (no-GIL) CPython 3.13 is supported on an experimental basis.