Pandas Introduction: Series, DataFrame, Reading Data

This pandas introduction gets you started fast: create Series and DataFrames, read CSV, Excel, and JSON files, inspect data with head, info, and describe, and run your first data analysis in minutes.

“If you torture the data long enough, it will confess to anything.”

Ronald Coase

Last Updated: July 2026 | Tested on: Python 3.14.6, Pandas 3.0.3 | Difficulty: Intermediate | Reading Time: 16 minutes

You have a spreadsheet of sales numbers. You want the total per city, the rows where revenue beat a target, and a quick average, and you want it in seconds, not after twenty minutes of dragging formulas around. NumPy gives you fast arrays, but real data has column names, mixed types, missing values, and dates. You need something that treats data like a spreadsheet but lets you drive it with code. That is what this pandas introduction is about. Pandas is the single most important library in the Python data world: if you touch data in Python, you touch Pandas.

Think of Pandas as a spreadsheet you can talk to in code. A spreadsheet has named columns and numbered rows, and you scroll, sort, and filter by clicking. Pandas gives you the same grid, but you ask for what you want in one line and it answers instantly, even when the grid has a few million rows. Under the hood Pandas sits on top of NumPy, so every column is really a fast NumPy array. On top of that it adds labeled rows and columns, automatic alignment when you combine data, and hundreds of ready-made methods for cleaning and analysis. You stop tracking “which column was that again” by hand.

Here is the everyday version. Rahul, a 27-year-old analyst, once loaded a two-million-row sales file, cleaned it, grouped it by region, and printed a summary report in about a dozen lines. Opening the same file in Excel froze his laptop. That moment, when the spreadsheet gives up and the code keeps going, is exactly when Pandas earns its place in your toolkit.

Key Methods.head().info().describe().shape.dtypesAxesaxis=0Rowsaxis=1ColumnsDataFrameIndexRow LabelsRangeIndex or CustomColumnsColumn Namesas IndexValues2D NumPy ArraySeries: namedtype: objectSeries: agedtype: int64Series: salarydtype: float64Python Pandas: DataFrame Anatomy of Index, Columns, and Values

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram breaks a Pandas DataFrame into its three parts: the index (row labels), the columns (each one a named Series), and the values (the NumPy array underneath). The inspection methods .head(), .info(), .describe(), and .shape each point to the part they reveal. Keep this pandas DataFrame anatomy picture handy. The first thing you do with any new dataset is call these methods to learn its shape, its column types, and its summary stats, well before you write a single line of analysis.

Prerequisites

Complete NumPy introduction. Familiarity with dictionaries tutorial and CSV and JSON tutorial is helpful.

Install and Verify

One command sets up everything this pandas introduction needs. Run the pip line below, then run the small verify script. If you see a version number and a tiny table print out, you are ready. If you see ModuleNotFoundError: No module named 'pandas' instead, the install did not land in the Python you are running, so check that pip and your interpreter point to the same place.

📄 Terminal: install Pandas

pip install pandas

📄 verify_pandas.py: check your installation

import pandas as pd
print(f"Pandas version: {pd.__version__}")
df = pd.DataFrame({"name": ["Rahul", "Viraj"], "age": [28, 25]})
print(df)

▶ Output

Pandas version: 3.0.3
    name  age
0  Rahul   28
1  Viraj   25

Series: Where Every Pandas Introduction Starts

A Series is a one-dimensional labeled array. Picture a single column lifted out of a spreadsheet: it has values, and it has an index (the row labels) sitting next to them. Every column inside a DataFrame is really a Series, so once you get Series, DataFrames stop feeling scary. A Series is like one column of names in your phone contacts: each name (the value) sits on its own row, and the row position or label tells you exactly where to find it.

📄 series_basics.py: creating and using a Series

import pandas as pd

# From a list
scores = pd.Series([85, 92, 78, 95, 88], name="exam_score")
print(f"Series:\n{scores}\n")

# With custom index
scores_named = pd.Series(
    [85, 92, 78, 95, 88],
    index=["Rahul", "Niranjan", "Pravin", "Viraj", "Sardar"],
    name="exam_score"
)
print(f"Named index:\n{scores_named}\n")

# Access by label and position
print(f"Viraj's score:   {scores_named['Viraj']}")
print(f"Position 0:      {scores_named.iloc[0]}")
print(f"Above 85:\n{scores_named[scores_named > 85]}\n")

# From a dictionary
populations = pd.Series({
    "Mumbai": 20_411_000,
    "Delhi": 16_787_000,
    "Bangalore": 8_443_000
})
print(f"Populations:\n{populations}")
print(f"\nMean: {populations.mean():,.0f}")
print(f"dtype: {populations.dtype}")

▶ Output

Series:
0    85
1    92
2    78
3    95
4    88
Name: exam_score, dtype: int64

Named index:
Rahul       85
Niranjan    92
Pravin      78
Viraj       95
Sardar      88
Name: exam_score, dtype: int64

Viraj's score:   95
Position 0:      85
Above 85:
Niranjan    92
Viraj       95
Sardar      88
Name: exam_score, dtype: int64

Populations:
Mumbai       20411000
Delhi        16787000
Bangalore     8443000
dtype: int64

Mean: 15,213,667
dtype: int64

What happened here: The first Series got the default index 0, 1, 2, and so on, just like row numbers in a spreadsheet. The second one used names as the index, so you can pull a value by label with scores_named['Viraj'] or by position with .iloc[0]. The boolean filter scores_named[scores_named > 85] keeps only the rows that pass the test, which is the same idea as filtering a spreadsheet, just shorter. Building a Series from a dictionary turns the keys into the index automatically. The dtype: int64 line is Pandas telling you it stored these as 64-bit integers, the fast NumPy type underneath.

DataFrame: The Star of the Show

A DataFrame is the whole spreadsheet: many columns, each one a Series, lined up and sharing one index. This is the object you will spend most of your data career with, which is why any honest pandas introduction spends most of its time here. The most common way to build one is from a dictionary, where each key becomes a column name and each list becomes that column’s values. Once you have a DataFrame, the very first thing to do is look at it. The five methods below, .shape, .dtypes, .head(), .info(), and .describe(), are your “get to know the data” routine.

📄 dataframe_basics.py: creating and inspecting a DataFrame

import pandas as pd

# From a dictionary of lists (most common)
df = pd.DataFrame({
    "name": ["Rahul", "Niranjan", "Viraj", "Pravin", "Sardar"],
    "age": [28, 31, 25, 29, 27],
    "department": ["Engineering", "Data Science", "Engineering", "Marketing", "Data Science"],
    "salary": [75000, 82000, 71000, 65000, 78000]
})
print(f"DataFrame:\n{df}\n")

# Inspection methods
print(f"Shape: {df.shape}")            # (rows, columns)
print(f"Columns: {list(df.columns)}")
print(f"Dtypes:\n{df.dtypes}\n")

print(f"Head (first 3):\n{df.head(3)}\n")
print(f"Info:")
df.info()
print(f"\nDescribe (stats):\n{df.describe()}")

▶ Output

DataFrame:
       name  age    department  salary
0     Rahul   28   Engineering   75000
1  Niranjan   31  Data Science   82000
2     Viraj   25   Engineering   71000
3    Pravin   29     Marketing   65000
4    Sardar   27  Data Science   78000

Shape: (5, 4)
Columns: ['name', 'age', 'department', 'salary']
Dtypes:
name            str
age           int64
department      str
salary        int64
dtype: object

Head (first 3):
       name  age    department  salary
0     Rahul   28   Engineering   75000
1  Niranjan   31  Data Science   82000
2     Viraj   25   Engineering   71000

Info:
<class 'pandas.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 4 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   name        5 non-null      str
 1   age         5 non-null      int64
 2   department  5 non-null      str
 3   salary      5 non-null      int64
dtypes: int64(2), str(2)
memory usage: 292.0 bytes

Describe (stats):
             age        salary
count   5.000000      5.000000
mean   28.000000  74200.000000
std     2.236068   6534.523701
min    25.000000  65000.000000
25%    27.000000  71000.000000
50%    28.000000  75000.000000
75%    29.000000  78000.000000
max    31.000000  82000.000000

What happened here: One small dictionary became a full table. .shape told you it has 5 rows and 4 columns. .dtypes shows the text columns as str and the number columns as int64. If you learned Pandas a couple of years ago, you may remember text columns showing up as object instead. That changed: in Pandas 3.0.3 a plain text column gets a real str dtype by default, which is clearer and lighter on memory.

You will only see object now when a column genuinely mixes types. .head(3) peeks at the first three rows, .info() gives a per-column summary with null counts and memory use (the exact byte count depends on your setup, it reads a bit higher if you have pyarrow installed, so do not worry if yours differs), and .describe() runs count, mean, std, min, max, and the quartiles on the numeric columns in one shot. That five-method routine is how you size up any new dataset in under a minute.

Reading Data: CSV, Excel, JSON

Real data lives in files, not in dictionaries you type by hand, so a pandas introduction has to cover the file readers early. Think of these readers as a universal translator at an airport: hand over a file in almost any common format and you get back the same clean table you already know how to work with. Pandas reads the common formats with one line each: read_csv, read_excel, and read_json. The example below builds a CSV (comma-separated values) file in memory so you can run it as-is with no file on disk, but the commented lines show exactly how you would load a real file or even a URL.

📄 reading_data.py: load data from files

import pandas as pd

# CSV, the most common format
# df = pd.read_csv("sales.csv")
# df = pd.read_csv("sales.csv", encoding="utf-8", parse_dates=["date"])

# Excel
# df = pd.read_excel("report.xlsx", sheet_name="Q1")

# JSON
# df = pd.read_json("api_response.json")

# From a URL (works with any public CSV)
# df = pd.read_csv("https://example.com/data.csv")

# Creating sample data for demonstration
import io
csv_data = """name,age,city,salary
Rahul,28,Mumbai,75000
Niranjan,31,Pune,82000
Viraj,25,Mumbai,71000
Pravin,29,Delhi,65000
Sardar,27,Pune,78000
Prathamesh,33,Mumbai,91000"""

df = pd.read_csv(io.StringIO(csv_data))
print(f"Loaded CSV ({df.shape[0]} rows, {df.shape[1]} columns):\n{df}\n")

# Column access
print(f"Names: {list(df['name'])}")
print(f"Mean salary: {df['salary'].mean():,.0f}")
print(f"Cities: {df['city'].unique().tolist()}")
print(f"City counts:\n{df['city'].value_counts()}")

▶ Output

Loaded CSV (6 rows, 4 columns):
         name  age    city  salary
0       Rahul   28  Mumbai   75000
1    Niranjan   31    Pune   82000
2       Viraj   25  Mumbai   71000
3      Pravin   29   Delhi   65000
4      Sardar   27    Pune   78000
5  Prathamesh   33  Mumbai   91000

Names: ['Rahul', 'Niranjan', 'Viraj', 'Pravin', 'Sardar', 'Prathamesh']
Mean salary: 77,000
Cities: ['Mumbai', 'Pune', 'Delhi']
City counts:
city
Mumbai    3
Pune      2
Delhi     1
Name: count, dtype: int64

What happened here: read_csv turned raw comma-separated text into a clean table, no manual splitting or parsing. After that, every column is a Series you can poke at: df['salary'].mean() averages a column, .unique() lists the distinct values (here turned into a plain list with .tolist()), and .value_counts() counts how often each value shows up, already sorted from most to least. In a real project you would swap the in-memory text for a file path or a URL on the commented lines and nothing else would change.

Basic Operations and Column Math

This is where Pandas really starts to pay off. You do math on whole columns at once, no loops. It is like typing a formula once at the top of a spreadsheet column and watching it fill every row instantly, except here you never touch the mouse. Writing df["salary"] * df["bonus_pct"] / 100 runs the calculation across every row in one go, which is both shorter to write and far faster than looping. Filtering works the same way: you write a condition once and Pandas keeps the rows that match. This is the everyday rhythm of data work, add a column, filter, sort, repeat.

📄 operations.py: adding columns, filtering, sorting

import pandas as pd

df = pd.DataFrame({
    "name": ["Rahul", "Niranjan", "Viraj", "Pravin", "Sardar"],
    "salary": [75000, 82000, 71000, 65000, 78000],
    "bonus_pct": [10, 15, 8, 12, 11]
})

# Add computed column
df["bonus"] = df["salary"] * df["bonus_pct"] / 100
df["total"] = df["salary"] + df["bonus"]
print(f"With computed columns:\n{df}\n")

# Filtering
high_earners = df[df["total"] > 80000]
print(f"Total > 80K:\n{high_earners}\n")

# Sorting
sorted_df = df.sort_values("total", ascending=False)
print(f"Sorted by total (descending):\n{sorted_df}\n")

# Multiple conditions
result = df[(df["salary"] >= 70000) & (df["bonus_pct"] >= 10)]
print(f"Salary >= 70K AND bonus >= 10%:\n{result}")

▶ Output

With computed columns:
       name  salary  bonus_pct    bonus    total
0     Rahul   75000         10   7500.0  82500.0
1  Niranjan   82000         15  12300.0  94300.0
2     Viraj   71000          8   5680.0  76680.0
3    Pravin   65000         12   7800.0  72800.0
4    Sardar   78000         11   8580.0  86580.0

Total > 80K:
       name  salary  bonus_pct    bonus    total
0     Rahul   75000         10   7500.0  82500.0
1  Niranjan   82000         15  12300.0  94300.0
4    Sardar   78000         11   8580.0  86580.0

Sorted by total (descending):
       name  salary  bonus_pct    bonus    total
1  Niranjan   82000         15  12300.0  94300.0
4    Sardar   78000         11   8580.0  86580.0
0     Rahul   75000         10   7500.0  82500.0
2     Viraj   71000          8   5680.0  76680.0
3    Pravin   65000         12   7800.0  72800.0

Salary >= 70K AND bonus >= 10%:
       name  salary  bonus_pct    bonus    total
0     Rahul   75000         10   7500.0  82500.0
1  Niranjan   82000         15  12300.0  94300.0
4    Sardar   78000         11   8580.0  86580.0

What happened here: Two new columns appeared without a single loop. df["bonus"] = df["salary"] * df["bonus_pct"] / 100 computed the bonus for all five people at once, and total added it on top. The filter df[df["total"] > 80000] kept only the high earners, sort_values reordered the rows, and the last filter combined two conditions with &. One thing to watch: wrap each condition in parentheses, because & binds tighter than the comparison operators. Forget the parentheses and Pandas throws an error.

Ecosystem: Polars as a Modern Alternative

Pandas is the established standard with the biggest ecosystem around it. But for very large datasets (millions of rows), Polars is a Rust-powered alternative that is much faster and lighter on memory. Polars 1.41.2 is mature enough for production, and you will compare the two side by side in the Pandas vs Polars comparison. For now, learn Pandas first. It is the one that scikit-learn, Matplotlib, Seaborn, and almost every data science tool expects you to hand them.

Common Mistakes

📄 Mistake 1: looping with iterrows() when a column operation exists

import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

# SLOW: row-by-row loop, painful on large data
for idx, row in df.iterrows():
    df.at[idx, "c"] = row["a"] + row["b"]

# FAST: one column operation, runs on every row at once
df["c"] = df["a"] + df["b"]

Why it matters: iterrows() walks one row at a time in slow Python. The column version pushes the work down into fast C and NumPy code, so on a million rows it can be dozens of times quicker. If you find yourself writing a for loop over a DataFrame, stop and ask whether a column operation does the same job.

📄 Mistake 2: chained indexing that silently does nothing

import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

# WRONG: chained indexing. Under Copy-on-Write this updates a
# throwaway copy, raises ChainedAssignmentError, and leaves df unchanged.
# df[df["a"] > 1]["b"] = 99

# RIGHT: one .loc call, selects rows and column together, then assigns
df.loc[df["a"] > 1, "b"] = 99

Why it matters: Pandas 3.0.3 turns on Copy-on-Write by default. With it, df[df["a"] > 1]["b"] = 99 writes to a temporary copy, never the real df, so Pandas raises a ChainedAssignmentError and your data stays exactly as it was. The fix is the same one experienced users have always reached for: do the selection and the assignment in a single .loc call. Think of .loc[rows, column] as giving Pandas one clear instruction instead of two half-instructions it has to guess about.

Practice Exercises

  1. Exercise 1: Create a DataFrame from a dict, display first 5 rows.
  2. Exercise 2: Read CSV, calculate summary statistics for numeric columns.
  3. Exercise 3: Load, clean, and export a summary report.

Conclusion

This pandas introduction gave you the two objects the entire Pandas world is built on: the Series, one labeled column, and the DataFrame, the whole labeled table made of many Series sharing an index. You can install and verify Pandas, build a DataFrame from a dictionary, read real data from CSV, Excel, and JSON, size up a fresh dataset with .head(), .info(), and .describe(), and do column math, filtering, and sorting without writing a single loop. That is the core rhythm of everyday data work, and you will repeat it on almost every dataset you ever touch.

Next, you go deeper into pulling out exactly the rows and columns you want with Pandas: Data Selection with loc, iloc, Boolean Indexing. For the full path from first steps to job-ready, keep going through the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is the difference between Pandas and NumPy?

NumPy gives you fast numerical arrays with one data type per array. Pandas gives you labeled DataFrames with a different type allowed per column (strings, numbers, dates). Pandas is built on top of NumPy, so this pandas introduction assumes a little NumPy first. Use NumPy for pure math, and Pandas for tabular data with named columns.

How large a dataset can Pandas handle?

Pandas works well for data that fits in memory, often up to a few GB. For bigger data, reach for Polars (faster, lower memory), Dask (distributed Pandas), or load in chunks with read_csv(chunksize=N). The Pandas vs Polars comparison walks through these options.

Which Pandas version should I learn in 2026?

Learn Pandas 3.x. Pandas 3.0.3 makes Copy-on-Write the default, which removes a whole class of surprise mutations, and it gives text columns a real str dtype instead of the old object dtype. The core API (Application Programming Interface) you learn here is the same one used across the modern data stack, so you are learning current patterns, not legacy ones.

Why do text columns show str instead of object in Pandas 3.0.3?

In Pandas 3.0.3 a plain text column gets a dedicated str dtype by default, which is clearer to read and uses less memory than the old object dtype. You will still see object when a single column genuinely mixes types, for example strings and numbers together. If you need the older behavior you can still opt into it, but str is the sensible default now.

Why use import pandas as pd?

For the same reason as import numpy as np: it is a universal convention. Every tutorial, doc page, and team uses pd. It saves typing and makes your code instantly readable to other data people.

Interview Questions on Pandas

If you can walk through these without peeking, you are ready for this topic in an interview.

Q: What is the difference between a Series and a DataFrame?

A Series is a single one-dimensional labeled column: values plus an index sitting next to them. A DataFrame is a two-dimensional table made of many Series that share one index, so each column can hold a different dtype (text, numbers, dates). Every column you pull out of a DataFrame is itself a Series, which is why learning Series first makes DataFrames click.

Q: You read a sales CSV and the salary column comes in as text instead of int64, so .mean() fails. What do you check first?

Inspect the raw values for stray non-numeric characters like currency symbols, commas used as thousands separators, or empty strings, since a single bad cell forces the whole column to text. Convert it with pd.to_numeric(df["salary"], errors="coerce"), which turns junk into NaN, or clean the strings first. You can also pass thousands="," or a converter to read_csv so the column parses correctly on load.

Q: A colleague’s script loops over a million-row DataFrame with iterrows() and takes minutes. How do you speed it up?

Replace the row loop with a vectorized column operation, for example df["c"] = df["a"] + df["b"], which pushes the work down into fast C and NumPy code and can be dozens of times quicker. If the logic is too complex for plain arithmetic, try .apply() or a boolean mask before reaching for a Python loop. Treat iterrows() as a last resort, not a default.

Q: Why does df[df["a"] > 1]["b"] = 99 fail to update your DataFrame in Pandas 3.0.3?

That is chained indexing: the first bracket returns a temporary copy, and the assignment writes to that throwaway object instead of the original df. Under Copy-on-Write, which is the default in Pandas 3.0.3, Pandas raises a ChainedAssignmentError to warn you rather than silently doing nothing. Do the selection and assignment in one step with df.loc[df["a"] > 1, "b"] = 99.

Q: What does df.describe() return, and which columns does it cover by default?

It returns count, mean, std, min, the 25/50/75% quartiles, and max for each numeric column. Text columns are skipped by default, though df.describe(include="all") adds count, unique, top, and freq for them. It is the fastest way to sanity-check ranges and spot outliers on a brand new dataset.

Q: How do you filter a DataFrame on more than one condition at once?

Combine boolean masks with & (and) and | (or), wrapping each condition in parentheses, for example df[(df["salary"] >= 70000) & (df["bonus_pct"] >= 10)]. The parentheses matter because & binds tighter than the comparison operators, so leaving them out raises an error. Use the bitwise operators & and |, not the Python keywords and and or, which do not work element by element.

Reference: the complete, always-current details live in pandas official documentation.

Previous: NumPy Linear Algebra: dot, matmul, eigenvalues

Next: Pandas: Data Selection with loc, iloc, Boolean Indexing

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *