Welcome to the world of **Data Handling**! π If you've ever wished you could manage a school register, a shop's inventory, or student marks all inside Python β **Pandas** makes it super easy. No complicated math, no confusion β just clean, simple data magic! πΌ
> [!TIP]
> **How to use these notes:** Read slowly, run every code example in Python, and focus on the π― Board Exam Tips! For IP students, the most important sections are **Series creation**, **DataFrame creation**, **loc/iloc**, and **add/delete operations**. All of these appear in board exams every year!
---
## 1.1 π Introduction β What is Pandas?
Imagine you have 500 students' marks in a school. You need to:
- Find who scored above 90
- Calculate the class average
- Sort by marks
Doing this manually takes hours. Writing raw Python code takes many lines. **Pandas does it in ONE line!** π
```mermaid
graph LR
PROB["π© Problem\nManaging big data\nin Python is painful"]
PANDAS["πΌ Pandas\nPowerful data tool\nbuilt on NumPy"]
SOL["π Solution\nAnalyse, filter, sort\nin seconds!"]
PROB --> PANDAS --> SOL
style PANDAS fill:#FF9800,color:#fff
style SOL fill:#4CAF50,color:#fff
```
::: grid
::: card π― | What Pandas Does | Handles tables of data like Excel | But MUCH faster and works inside Python code!
::: card ποΈ | Built On | NumPy (the math engine) | Pandas = NumPy + labels + convenience
::: card π | Who Uses It | Data Scientists, Companies, Researchers | Google, Amazon, ISRO use Pandas daily!
::: card β‘ | Why Fast | Vectorization β processes whole column at once | No slow loops needed
:::
> **Best Analogy:** Think of Python lists as a **hand-written notebook** and Pandas as an **Excel spreadsheet**. Both store data, but Excel has built-in tools for filtering, sorting, and calculations. That's exactly what Pandas adds to Python!
---
## 1.2 π Using Pandas
Before you can use Pandas, you need to **import** it. We give it a short nickname `pd` so we don't type "pandas" every time.
```python
# import_pandas.py
import pandas as pd # Standard practice β always use alias 'pd'
import numpy as np # Often imported together with pandas
print("Pandas version:", pd.__version__)
```
> [!NOTE]
> **Why alias 'pd'? π§ **
> This is a universal convention β every Pandas programmer in the world writes `import pandas as pd`. If you write `import pandas`, your code still works but you'd type `pandas.Series()` instead of `pd.Series()`. Short = better!
> [!IMPORTANT]
> **Board Exam Tip**
> "How do you import the Pandas library?" β **1-mark** question every year!
> Answer: `import pandas as pd`
> If they ask for numpy too: `import numpy as np`
---
## 1.3 ποΈ Pandas Data Structures
Pandas gives us **two** main data structures to store and manage data:
```mermaid
graph TD
PANDAS["πΌ PANDAS\nData Structures"]
S["π Series\n1-Dimensional\nLike one column"]
D["π DataFrame\n2-Dimensional\nLike a full table"]
PANDAS --> S
PANDAS --> D
S --> EX1["Marks of ONE student\nacross subjects:\nMath: 85, Sci: 90, Eng: 78"]
D --> EX2["Marks of ALL students\nacross ALL subjects\n(full spreadsheet!)"]
style S fill:#2196F3,color:#fff
style D fill:#9C27B0,color:#fff
```
::: grid
::: card π | Series | 1D β like a single column in Excel | Roll numbers, marks of one student, prices of items
::: card π | DataFrame | 2D β like a complete Excel sheet | Full class result, product catalogue, attendance register
:::
---
## 1.4 π Series β The Smart Column
A **Series** is a one-dimensional labeled array. It's like a Python list, but every element has a **label (called index)** attached to it.
```
Without Pandas (List): With Pandas (Series):
[85, 90, 78] 0 85
1 90
2 78
dtype: int64
```
The left column (0, 1, 2) = **Index (label)**
The right column (85, 90, 78) = **Values (data)**
---
### 1.4.1 Creating Series Objects π οΈ
**Method 1: From a List (most basic)**
```python
# series_from_list.py
import pandas as pd
marks = [85, 90, 78, 92, 65]
s = pd.Series(marks)
print(s)
```
**Output:**
```
0 85
1 90
2 78
3 92
4 65
dtype: int64
```
> Pandas automatically assigns index 0, 1, 2, 3, 4 when you don't specify one.
---
**Method 2: From a List WITH Custom Index**
```python
# series_custom_index.py
import pandas as pd
marks = [85, 90, 78]
students = ['Amit', 'Neha', 'Raj']
s = pd.Series(marks, index=students)
print(s)
```
**Output:**
```
Amit 85
Neha 90
Raj 78
dtype: int64
```
> Now instead of 0, 1, 2 β we have names as labels! Much more readable! π
---
**Method 3: From a Dictionary (Keys β Index, Values β Data)**
```python
# series_from_dict.py
import pandas as pd
data = {'Math': 95, 'Science': 88, 'English': 72, 'Hindi': 80}
s = pd.Series(data)
print(s)
```
**Output:**
```
Math 95
Science 88
English 72
Hindi 80
dtype: int64
```
> Dictionary keys automatically become the Index! This is the most natural way. π―
---
**Method 4: From a Scalar (Same Value Everywhere)**
```python
# series_scalar.py
import pandas as pd
s = pd.Series(100, index=['A', 'B', 'C', 'D'])
print(s)
```
**Output:**
```
A 100
B 100
C 100
D 100
dtype: int64
```
> Fills all positions with the same value β useful for creating blank/default data!
---
### 1.4.2 Creating Series Objects β Additional Functionality π§
**Using `dtype` to specify data type:**
```python
# series_dtype.py
import pandas as pd
s = pd.Series([1, 2, 3], dtype=float)
print(s)
```
**Output:**
```
0 1.0
1 2.0
2 3.0
dtype: float64
```
**Using `name` to give the Series a title:**
```python
# series_name.py
import pandas as pd
s = pd.Series([85, 90, 78], index=['Amit', 'Neha', 'Raj'], name='Marks')
print(s)
print("Series name:", s.name)
```
**Output:**
```
Amit 85
Neha 90
Raj 78
Name: Marks, dtype: int64
Series name: Marks
```
**Using NumPy array as input:**
```python
# series_numpy.py
import pandas as pd
import numpy as np
arr = np.array([10, 20, 30, 40])
s = pd.Series(arr)
print(s)
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Write code to create a Series from a dictionary with subject names as index." β **2-mark** question very common!
> Answer:
> ```python
> import pandas as pd
> d = {'Math': 85, 'Science': 90, 'English': 78}
> s = pd.Series(d)
> print(s)
> ```
---
### 1.4.3 Series Object Attributes π
Attributes are properties that tell you **about** the Series β like its size, type, values, etc.
**Let's create a Series to demonstrate:**
```python
import pandas as pd
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
```
| Attribute | What It Returns | Example Output |
| :--- | :--- | :--- |
| `s.values` | All data as a NumPy array | `[10 20 30 40]` |
| `s.index` | All index labels | `Index(['a','b','c','d'])` |
| `s.dtype` | Data type of values | `int64` |
| `s.size` | Total number of elements | `4` |
| `s.shape` | Dimensions as tuple | `(4,)` |
| `s.nbytes` | Memory used in bytes | `32` |
| `s.empty` | True if Series is empty | `False` |
| `s.name` | Name of the Series | `None` (if not set) |
| `s.ndim` | Number of dimensions | `1` |
```python
# series_attributes.py
import pandas as pd
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print("Values:", s.values)
print("Index:", s.index)
print("Data type:", s.dtype)
print("Size:", s.size)
print("Shape:", s.shape)
print("Is empty?", s.empty)
```
**Output:**
```
Values: [10 20 30 40]
Index: Index(['a', 'b', 'c', 'd'], dtype='object')
Data type: int64
Size: 4
Shape: (4,)
Is empty? False
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the difference between s.size and s.shape?" β **1-mark** question.
> `s.size` β returns a **number** (total elements e.g. 4)
> `s.shape` β returns a **tuple** showing dimensions e.g. (4,)
---
## 1.5 π Accessing a Series Object and its Elements
### 1.5.1 Accessing Individual Elements
Elements in a Series can be accessed using the **index label** (name) or **position number**.
```python
# access_series.py
import pandas as pd
s = pd.Series([85, 90, 78, 92], index=['Amit', 'Neha', 'Raj', 'Priya'])
```
**Using label (index name):**
```python
print(s['Neha']) # Output: 90
print(s['Raj']) # Output: 78
```
**Using position number:**
```python
print(s[0]) # Output: 85 (first element)
print(s[2]) # Output: 78 (third element)
```
**Accessing multiple elements (fancy indexing):**
```python
print(s[['Amit', 'Priya']]) # Pass a list of labels
```
**Output:**
```
Amit 85
Priya 92
dtype: int64
```
---
### 1.5.2 Extracting Slices from Series Object π°
Slicing means getting a **portion** (chunk) of the Series.
```python
# slice_series.py
import pandas as pd
s = pd.Series([10, 20, 30, 40, 50], index=['a', 'b', 'c', 'd', 'e'])
```
**Slicing by position (like Python lists):**
```python
print(s[1:4]) # Positions 1, 2, 3 (4 is EXCLUDED)
```
**Output:**
```
b 20
c 30
d 40
dtype: int64
```
**Slicing by label:**
```python
print(s['b':'d']) # Labels b, c, d (d is INCLUDED!)
```
**Output:**
```
b 20
c 30
d 40
dtype: int64
```
> [!WARNING]
> **Common Mistake β The Big Slice Trap!**
> `s[1:4]` using position β index 4 is **EXCLUDED** (like Python lists)
> `s['b':'d']` using label β 'd' is **INCLUDED** (label slicing is inclusive!)
> This is a favourite trick question in CBSE exams!
---
## 1.6 π οΈ Operations on Series Object
### 1.6.1 Modifying Elements of Series Object βοΈ
You can change values in a Series by assigning to the index:
```python
# modify_series.py
import pandas as pd
s = pd.Series([85, 90, 78], index=['Amit', 'Neha', 'Raj'])
print("Before:", s['Neha'])
s['Neha'] = 95 # Modify using label
print("After:", s['Neha'])
s[2] = 82 # Modify using position
print("Raj's new marks:", s['Raj'])
```
**Output:**
```
Before: 90
After: 95
Raj's new marks: 82
```
**Modifying multiple values:**
```python
s[['Amit', 'Raj']] = [88, 85] # Change multiple at once
print(s)
```
---
### 1.6.2 Renaming Indexes π·οΈ
You can rename the index labels of a Series using `.rename()` or by directly assigning a new index:
**Method 1: Assign new index directly**
```python
# rename_index.py
import pandas as pd
s = pd.Series([85, 90, 78], index=['A', 'B', 'C'])
s.index = ['Amit', 'Neha', 'Raj'] # Replace all index labels
print(s)
```
**Output:**
```
Amit 85
Neha 90
Raj 78
dtype: int64
```
**Method 2: Using rename() β selective renaming**
```python
s = s.rename({'Amit': 'AMIT', 'Neha': 'NEHA'}) # Rename specific labels
print(s)
```
**Output:**
```
AMIT 85
NEHA 90
Raj 78
dtype: int64
```
> [!NOTE]
> **rename() does NOT change the original! π§ **
> `s.rename(...)` returns a NEW Series. The original `s` is unchanged unless you write `s = s.rename(...)`. This is true for most Pandas operations!
---
### 1.6.3 The head() and tail() Functions π
When a Series has many elements, you don't want to print ALL of them. `head()` and `tail()` let you peek at the first/last few:
```python
# head_tail.py
import pandas as pd
s = pd.Series([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
print("First 3 elements:")
print(s.head(3))
print("\nLast 3 elements:")
print(s.tail(3))
print("\nDefault head (5):")
print(s.head()) # Default shows 5
```
**Output:**
```
First 3 elements:
0 10
1 20
2 30
dtype: int64
Last 3 elements:
7 80
8 90
9 100
dtype: int64
Default head (5):
0 10
1 20
2 30
3 40
4 50
dtype: int64
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What does `s.head()` return by default?" β **1-mark** question.
> Answer: By default (with no argument), `head()` returns the **first 5 elements**. `tail()` returns the **last 5 elements**.
---
### 1.6.4 Vector Operations on Series Objects β‘
**Vectorization** means performing an operation on **every element at once** β no loop needed!
```python
# vector_operations.py
import pandas as pd
s = pd.Series([10, 20, 30, 40, 50])
print("Original:", s.values)
print("Add 5:", (s + 5).values)
print("Multiply by 2:", (s * 2).values)
print("Square root:", (s ** 0.5).values)
print("Divide by 10:", (s / 10).values)
```
**Output:**
```
Original: [10 20 30 40 50]
Add 5: [15 25 35 45 55]
Multiply by 2: [20 40 60 80 100]
Square root: [3.16 4.47 5.48 6.32 7.07]
Divide by 10: [1. 2. 3. 4. 5.]
```
> Every operation applies to the **entire Series automatically** β no `for` loop needed! This is the biggest advantage of Pandas over plain Python lists.
---
### 1.6.5 Arithmetic on Series Objects β
When you do arithmetic between **two Series**, Pandas aligns them by their **index labels** before calculating:
```python
# arithmetic_series.py
import pandas as pd
marks_test1 = pd.Series({'Math': 45, 'Science': 40, 'English': 35})
marks_test2 = pd.Series({'Math': 50, 'Science': 45, 'English': 40})
total = marks_test1 + marks_test2
print("Total Marks:")
print(total)
percentage = (total / 2)
print("\nAverage:")
print(percentage)
```
**Output:**
```
Total Marks:
Math 95
Science 85
English 75
dtype: int64
Average:
Math 47.5
Science 42.5
English 37.5
dtype: float64
```
**What happens when indexes don't match?**
```python
# mismatched_arithmetic.py
import pandas as pd
s1 = pd.Series({'a': 10, 'b': 20, 'c': 30})
s2 = pd.Series({'b': 5, 'c': 15, 'd': 25})
print(s1 + s2)
```
**Output:**
```
a NaN
b 25.0
c 45.0
d NaN
dtype: float64
```
> When an index exists in one Series but NOT the other, the result is **NaN (Not a Number)** β representing a missing/unknown value!
> [!IMPORTANT]
> **Board Exam Tip**
> "What is NaN in Pandas?" β **1-mark** question.
> Answer: **NaN (Not a Number)** is Pandas' way of representing a **missing or undefined value**. It appears when arithmetic is performed between Series with non-matching index labels, or when data is missing in a dataset.
---
### 1.6.6 Filtering Entries in Series Objects π
Filtering means picking only the elements that match a condition:
```python
# filter_series.py
import pandas as pd
marks = pd.Series([85, 45, 92, 38, 75, 60],
index=['Amit', 'Neha', 'Raj', 'Priya', 'Sita', 'Karan'])
# Who scored more than 70?
print("Students who passed with distinction (>70):")
print(marks[marks > 70])
print("\nStudents who failed (<50):")
print(marks[marks < 50])
print("\nStudents who scored between 60 and 90:")
print(marks[(marks >= 60) & (marks <= 90)])
```
**Output:**
```
Students who passed with distinction (>70):
Amit 85
Raj 92
Sita 75
dtype: int64
Students who failed (<50):
Neha 45
Priya 38
dtype: int64
Students who scored between 60 and 90:
Amit 85
Sita 75
Karan 60
dtype: int64
```
::: grid
::: card > | Greater than | marks[marks > 70] | Elements above 70
::: card < | Less than | marks[marks < 50] | Elements below 50
::: card & | AND condition | (marks > 60) & (marks < 90) | Between 60 and 90
::: card | | OR condition | (marks < 40) | (marks > 90) | Below 40 OR above 90
:::
---
### 1.6.7 Sorting Series Values πΆ
Two ways to sort a Series:
```python
# sort_series.py
import pandas as pd
s = pd.Series([30, 10, 50, 20, 40], index=['c', 'a', 'e', 'b', 'd'])
print("Sort by VALUES (ascending):")
print(s.sort_values())
print("\nSort by VALUES (descending):")
print(s.sort_values(ascending=False))
print("\nSort by INDEX (label):")
print(s.sort_index())
```
**Output:**
```
Sort by VALUES (ascending):
a 10
b 20
c 30
d 40
e 50
dtype: int64
Sort by VALUES (descending):
e 50
d 40
c 30
b 20
a 10
dtype: int64
Sort by INDEX (label):
a 10
b 20
c 30
d 40
e 50
dtype: int64
```
| Method | What it sorts by | Default |
| :--- | :--- | :--- |
| `sort_values()` | The data values | ascending=True |
| `sort_index()` | The index labels | ascending=True |
> [!IMPORTANT]
> **Board Exam Tip**
> "Write code to sort a Series of prices in descending order." β **2-mark** question.
> Answer: `s.sort_values(ascending=False)`
---
## 1.7 π Series vs. 1D Data Structures
### 1.7.1 Series Objects vs. Lists
| Feature | Python List | Pandas Series |
| :--- | :--- | :--- |
| **Index** | Only 0, 1, 2... (numeric) | Custom labels (names, dates, etc.) |
| **Data Type** | Mixed types allowed | Uniform type (faster!) |
| **Math Operations** | Need loops | Vectorized β one line! |
| **Missing Data** | No concept | NaN support built-in |
| **Size** | Can grow/shrink | Fixed size after creation |
| **Example** | `[85, 90, 78]` | `pd.Series([85, 90, 78], index=['A','B','C'])` |
```python
# list_vs_series.py
import pandas as pd
# List: need loop to add 10 to all
my_list = [10, 20, 30]
result_list = [x + 10 for x in my_list] # Needs loop
# Series: no loop needed!
my_series = pd.Series([10, 20, 30])
result_series = my_series + 10 # No loop!
print("List result:", result_list)
print("Series result:", result_series.values)
```
---
### 1.7.2 Series Objects vs. Dictionaries
| Feature | Python Dictionary | Pandas Series |
| :--- | :--- | :--- |
| **Index** | Keys (any hashable type) | Index labels (same concept!) |
| **Order** | Maintained (Python 3.7+) | Always maintains order |
| **Math Operations** | Not supported | Vectorized math built-in |
| **Slicing** | Limited (no range slicing) | Full slicing support |
| **Alignment** | No | β
Auto-aligns by index during arithmetic |
| **Convert to Series** | `pd.Series(dict)` | Already a Series |
```python
# dict_vs_series.py
import pandas as pd
# Dictionary
d = {'Math': 85, 'Science': 90}
# d + 5 β This would cause ERROR!
# Series (from dictionary)
s = pd.Series(d)
print(s + 5) # β
Works perfectly!
```
**Output:**
```
Math 90
Science 95
dtype: int64
```
---
### 1.7.3 Difference between NumPy Arrays and Series Objects
| Feature | NumPy Array (1D) | Pandas Series |
| :--- | :--- | :--- |
| **Index** | Only 0, 1, 2... (numeric) | Custom index labels |
| **Data Type** | Must be uniform | Must be uniform |
| **NaN Handling** | Limited | Full NaN support |
| **Mathematical Ops** | β
Fast | β
Fast (uses NumPy inside) |
| **Label-based Access** | β Not available | β
`s['label']` works |
| **Index Alignment** | β No | β
Auto-aligns by label |
```python
# numpy_vs_series.py
import pandas as pd
import numpy as np
arr = np.array([10, 20, 30])
s = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
# NumPy array: only position access
print("NumPy arr[0]:", arr[0]) # Output: 10
# Series: both position AND label access
print("Series s['b']:", s['b']) # Output: 20
print("Series s[1]:", s[1]) # Output: 20
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Differentiate between a Pandas Series and a NumPy 1D Array." β **2-mark** question asked often!
> Key differences: Series has **custom labels** (NumPy only has numeric positions). Series supports **index alignment** in arithmetic. Series has better **NaN handling**.
---
## 1.8 π DataFrame Data Structure
A **DataFrame** is a 2-dimensional table with:
- **Rows** β each row is one record
- **Columns** β each column is one attribute
- **Row Index** β labels for rows
- **Column Labels** β headers for columns
```
Name Math Science English
0 Amit 85 88 75
1 Neha 90 92 88
2 Raj 78 80 72
3 Priya 92 95 89
β β
Row Index Column Labels
```
> Think of a DataFrame as a **collection of Series** β each column is a Series, all sharing the same row index!
---
## 1.9 π Creating and Displaying a DataFrame
### Method 1: Dictionary of Lists (Most Common! β)
Keys = Column names, Values = Column data as lists
```python
# df_from_dict.py
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya'],
'Math': [85, 90, 78, 92],
'Science': [88, 92, 80, 95],
'English': [75, 88, 72, 89]
}
df = pd.DataFrame(data)
print(df)
```
**Output:**
```
Name Math Science English
0 Amit 85 88 75
1 Neha 90 92 88
2 Raj 78 80 72
3 Priya 92 95 89
```
**With custom row index:**
```python
df = pd.DataFrame(data, index=['S1', 'S2', 'S3', 'S4'])
print(df)
```
**Output:**
```
Name Math Science English
S1 Amit 85 88 75
S2 Neha 90 92 88
S3 Raj 78 80 72
S4 Priya 92 95 89
```
---
### Method 2: List of Dictionaries
Each dictionary = one row. Keys = column names.
```python
# df_from_list_of_dicts.py
import pandas as pd
data = [
{'Name': 'Amit', 'Marks': 85, 'Grade': 'A'},
{'Name': 'Neha', 'Marks': 90, 'Grade': 'A+'},
{'Name': 'Raj', 'Marks': 78, 'Grade': 'B+'},
]
df = pd.DataFrame(data)
print(df)
```
**Output:**
```
Name Marks Grade
0 Amit 85 A
1 Neha 90 A+
2 Raj 78 B+
```
---
### Method 3: Dictionary of Series
```python
# df_from_series.py
import pandas as pd
marks = pd.Series([85, 90, 78], index=['Amit', 'Neha', 'Raj'])
grades = pd.Series(['A', 'A+', 'B+'], index=['Amit', 'Neha', 'Raj'])
df = pd.DataFrame({'Marks': marks, 'Grade': grades})
print(df)
```
**Output:**
```
Marks Grade
Amit 85 A
Neha 90 A+
Raj 78 B+
```
---
### Method 4: 2D NumPy Array
```python
# df_from_numpy.py
import pandas as pd
import numpy as np
arr = np.array([[85, 88, 75],
[90, 92, 88],
[78, 80, 72]])
df = pd.DataFrame(arr,
columns=['Math', 'Science', 'English'],
index=['Amit', 'Neha', 'Raj'])
print(df)
```
**Output:**
```
Math Science English
Amit 85 88 75
Neha 90 92 88
Raj 78 80 72
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Write code to create a DataFrame with student name, marks in 3 subjects." β **3-mark** question, most common!
> Use Method 1 (Dictionary of Lists) β it's the cleanest and most widely tested.
---
## 1.10 π’ DataFrame Attributes
Using this DataFrame for all examples:
```python
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya'],
'Marks': [85, 90, 78, 92],
'Grade': ['A', 'A+', 'B+', 'A+']
}
df = pd.DataFrame(data, index=['S1', 'S2', 'S3', 'S4'])
```
| Attribute | What It Returns | Output |
| :--- | :--- | :--- |
| `df.shape` | (rows, columns) as tuple | `(4, 3)` |
| `df.index` | Row labels | `Index(['S1','S2','S3','S4'])` |
| `df.columns` | Column names | `Index(['Name','Marks','Grade'])` |
| `df.dtypes` | Data type of each column | (table of types) |
| `df.values` | All data as 2D NumPy array | `array([[...]])` |
| `df.size` | Total cells (rows Γ cols) | `12` |
| `df.ndim` | Number of dimensions | `2` |
| `df.empty` | True if DataFrame is empty | `False` |
| `df.T` | Transpose (flip rowsβcols) | (flipped table) |
| `df.head(n)` | First n rows (default 5) | First 4 rows |
| `df.tail(n)` | Last n rows (default 5) | Last rows |
| `df.info()` | Summary of types and nulls | Technical summary |
| `df.describe()` | Statistics (mean, min, max) | Stats table |
```python
# df_attributes_demo.py
print("Shape:", df.shape)
print("Columns:", df.columns.tolist())
print("Index:", df.index.tolist())
print("Size:", df.size)
print("Dimensions:", df.ndim)
print("\nFirst 2 rows:")
print(df.head(2))
```
**Output:**
```
Shape: (4, 3)
Columns: ['Name', 'Marks', 'Grade']
Index: ['S1', 'S2', 'S3', 'S4']
Size: 12
Dimensions: 2
First 2 rows:
Name Marks Grade
S1 Amit 85 A
S2 Neha 90 A+
```
---
## 1.11 π DataFrame vs. Series and 2D NumPy Array
### 1.11.1 DataFrame vs. Series
| Feature | Series | DataFrame |
| :--- | :--- | :--- |
| **Dimensions** | 1D (one column) | 2D (rows + columns) |
| **Index** | One index (row labels) | Row index + Column labels |
| **Data** | One column of data | Multiple columns |
| **Access** | `s['label']` | `df['column']['row']` |
| **Relationship** | A single column | A collection of Series |
| **When to use** | One variable (marks only) | Multiple variables (name, marks, grade) |
```python
# series_vs_df.py
import pandas as pd
# Series β one column
s = pd.Series([85, 90, 78], name='Marks', index=['Amit', 'Neha', 'Raj'])
# DataFrame β multiple columns (collection of Series!)
df = pd.DataFrame({
'Marks': [85, 90, 78],
'Grade': ['A', 'A+', 'B+']
}, index=['Amit', 'Neha', 'Raj'])
print("Series:\n", s)
print("\nDataFrame:\n", df)
```
---
### 1.11.2 DataFrame vs. 2D NumPy Array (ndarray)
| Feature | 2D NumPy Array | Pandas DataFrame |
| :--- | :--- | :--- |
| **Row Labels** | Only 0, 1, 2... | Custom labels |
| **Column Labels** | Only 0, 1, 2... | Named columns |
| **Mixed Types** | β All must be same | β
Each column can differ |
| **NaN Support** | Limited | β
Full support |
| **Label Access** | β Position only | β
Name + position |
| **Pretty Printing** | Raw array format | Table format |
> [!IMPORTANT]
> **Board Exam Tip**
> "How is a DataFrame different from a 2D NumPy Array?" β **2-mark** question.
> Key: DataFrame has **named rows and columns** (not just numbers), supports **mixed data types** per column, and has **built-in NaN handling**. NumPy arrays are faster but less flexible.
---
## 1.12 π― Selecting or Accessing Data
This is the most important section! Learn it thoroughly β it is in EVERY board exam! π―
**Our reference DataFrame for all examples:**
```python
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya', 'Sita'],
'Marks': [85, 90, 78, 92, 65],
'Grade': ['A', 'A+', 'B+', 'A+', 'B'],
'City': ['Delhi', 'Mumbai', 'Delhi', 'Pune', 'Delhi']
}
df = pd.DataFrame(data, index=['R1', 'R2', 'R3', 'R4', 'R5'])
print(df)
```
```
Name Marks Grade City
R1 Amit 85 A Delhi
R2 Neha 90 A+ Mumbai
R3 Raj 78 B+ Delhi
R4 Priya 92 A+ Pune
R5 Sita 65 B Delhi
```
---
### 1.12.1 Selecting / Accessing a Column
```python
# select_one_column.py
print(df['Name']) # Returns a Series
print(type(df['Name'])) # <class 'pandas.core.series.Series'>
```
**Output:**
```
R1 Amit
R2 Neha
R3 Raj
R4 Priya
R5 Sita
Name: Name, dtype: object
```
---
### 1.12.2 Selecting / Accessing Multiple Columns
Use **double brackets** `[[ ]]` to select multiple columns β you're passing a **list** of column names:
```python
# select_multiple_columns.py
print(df[['Name', 'Marks']]) # Double brackets!
print(type(df[['Name', 'Marks']])) # DataFrame (not Series!)
```
**Output:**
```
Name Marks
R1 Amit 85
R2 Neha 90
R3 Raj 78
R4 Priya 92
R5 Sita 65
```
> [!WARNING]
> **Single vs Double Brackets!**
> `df['Name']` β returns a **Series** (one column)
> `df[['Name']]` β returns a **DataFrame** (still one column but in table form)
> `df[['Name', 'Marks']]` β returns a **DataFrame** with two columns
> This is one of the top mistakes in board exams!
---
### 1.12.3 Selecting a Subset using Row/Column Names β `loc[]` π―
`loc` = **L**abel-based access. Use **actual names** to select.
**Syntax:** `df.loc[row_label, column_label]`
```python
# loc_demo.py
# Access ONE cell: row R2, column Marks
print(df.loc['R2', 'Marks']) # Output: 90
# Access ONE full row:
print(df.loc['R3'])
# Access specific rows AND columns:
print(df.loc[['R1', 'R3'], ['Name', 'Marks']])
# Access a range of rows (INCLUSIVE on both ends!):
print(df.loc['R1':'R3'])
# Access all rows, specific columns:
print(df.loc[:, ['Name', 'City']])
```
**Output of `df.loc[['R1', 'R3'], ['Name', 'Marks']]`:**
```
Name Marks
R1 Amit 85
R3 Raj 78
```
---
### 1.12.4 Selecting Rows/Columns β `iloc[]` π’
`iloc` = **I**nteger-position-based access. Use **numbers** (0, 1, 2...) to select.
**Syntax:** `df.iloc[row_number, column_number]`
```python
# iloc_demo.py
# Access ONE cell: 1st row (index 1), 2nd column (index 1)
print(df.iloc[1, 1]) # Output: 90 (Neha's Marks)
# Access ONE full row:
print(df.iloc[2]) # 3rd row (Raj)
# Access a range of rows (EXCLUSIVE end!):
print(df.iloc[0:3]) # Rows 0, 1, 2 (NOT 3!)
# Access specific rows and columns by number:
print(df.iloc[[0, 2], [0, 1]]) # Rows 0,2 and Columns 0,1
```
**Output of `df.iloc[0:3]`:**
```
Name Marks Grade City
R1 Amit 85 A Delhi
R2 Neha 90 A+ Mumbai
R3 Raj 78 B+ Delhi
```
---
**`loc` vs `iloc` β The Critical Comparison:**
| Feature | `loc` | `iloc` |
| :--- | :--- | :--- |
| **Based on** | **Labels** (names) | **Integer positions** (numbers) |
| **Row access** | `df.loc['R2']` | `df.iloc[1]` |
| **Column access** | `df.loc[:, 'Name']` | `df.iloc[:, 0]` |
| **Slice end** | **INCLUSIVE** (includes last) | **EXCLUSIVE** (excludes last) |
| **Remember as** | **loc = Label** | **iloc = Integer** |
```mermaid
graph LR
LOC["loc\nUse LABELS\n'R1' to 'R3'\nInclusive end"]
ILOC["iloc\nUse NUMBERS\n0 to 2\nExclusive end"]
style LOC fill:#4CAF50,color:#fff
style ILOC fill:#2196F3,color:#fff
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the difference between loc and iloc?" β **2-mark** question, asked almost EVERY year!
> Answer: `loc` uses **row/column labels (names)** and the end of a slice is **inclusive**. `iloc` uses **integer positions (0,1,2...)** and the end of a slice is **exclusive** (like Python list slicing).
---
### 1.12.5 Selecting / Accessing Individual Value
Two ways to access one specific cell:
```python
# access_individual_value.py
# Method 1: Using loc (label-based)
print(df.loc['R2', 'Marks']) # Output: 90
# Method 2: Using iloc (position-based)
print(df.iloc[1, 1]) # Output: 90
# Method 3: Using at (fastest for single value, label-based)
print(df.at['R2', 'Marks']) # Output: 90
# Method 4: Using iat (fastest for single value, position-based)
print(df.iat[1, 1]) # Output: 90
```
> `at` and `iat` are slightly faster than `loc` and `iloc` for accessing a SINGLE value, but `loc` and `iloc` are more versatile for ranges.
---
### 1.12.6 Selecting DataFrame Rows/Columns Based on Boolean Conditions π
Filter rows based on a condition β this is the most practical use case!
```python
# boolean_filter.py
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya', 'Sita'],
'Marks': [85, 90, 78, 92, 65],
'City': ['Delhi', 'Mumbai', 'Delhi', 'Pune', 'Delhi']
}
df = pd.DataFrame(data, index=['R1', 'R2', 'R3', 'R4', 'R5'])
# Students with Marks > 80:
print("Marks above 80:")
print(df[df['Marks'] > 80])
# Students from Delhi:
print("\nStudents from Delhi:")
print(df[df['City'] == 'Delhi'])
# Students from Delhi with Marks > 75:
print("\nDelhi students with Marks > 75:")
print(df[(df['City'] == 'Delhi') & (df['Marks'] > 75)])
```
**Output (Marks > 80):**
```
Name Marks City
R1 Amit 85 Delhi
R2 Neha 90 Mumbai
R4 Priya 92 Pune
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Write code to display all students who scored more than 80 marks." β **2-mark** question!
> Answer: `print(df[df['Marks'] > 80])`
---
## 1.13 β Adding/Modifying Rows/Columns in DataFrames
### 1.13.1 Adding/Modifying a Column
**Adding a new column:**
```python
# add_column.py
import pandas as pd
df = pd.DataFrame({
'Name': ['Amit', 'Neha', 'Raj'],
'Marks': [85, 90, 78]
})
# Add new column with calculated values:
df['Percentage'] = df['Marks'] / 100 * 100
df['Grade'] = ['A', 'A+', 'B+']
df['Status'] = 'Pass' # Same value for all rows
print(df)
```
**Output:**
```
Name Marks Percentage Grade Status
0 Amit 85 85.0 A Pass
1 Neha 90 90.0 A+ Pass
2 Raj 78 78.0 B+ Pass
```
**Modifying an existing column:**
```python
# Increase all marks by 5 (bonus marks):
df['Marks'] = df['Marks'] + 5
print(df['Marks'])
```
**Output:**
```
0 90
1 95
2 83
Name: Marks, dtype: int64
```
---
### 1.13.2 Adding/Modifying a Row
**Adding a new row using `loc[]`:**
```python
# add_row.py
import pandas as pd
df = pd.DataFrame({
'Name': ['Amit', 'Neha'],
'Marks': [85, 90]
}, index=['R1', 'R2'])
# Add a new row:
df.loc['R3'] = ['Raj', 78]
print(df)
```
**Output:**
```
Name Marks
R1 Amit 85
R2 Neha 90
R3 Raj 78
```
**Modifying an existing row:**
```python
df.loc['R2'] = ['NEHA', 95] # Replace entire row R2
```
---
### 1.13.3 Modifying a Single Cell
```python
# modify_cell.py
import pandas as pd
df = pd.DataFrame({
'Name': ['Amit', 'Neha', 'Raj'],
'Marks': [85, 90, 78]
}, index=['R1', 'R2', 'R3'])
# Using loc:
df.loc['R2', 'Marks'] = 95
print("After update:", df.loc['R2', 'Marks']) # Output: 95
# Using iloc:
df.iloc[0, 1] = 88
print("After update:", df.iloc[0, 1]) # Output: 88
# Using at:
df.at['R3', 'Marks'] = 82
print("After update:", df.at['R3', 'Marks']) # Output: 82
```
---
## 1.14 β Deleting/Renaming Columns/Rows
### 1.14.1 Deleting Rows/Columns in a DataFrame
Using the `drop()` method:
```python
# delete_demo.py
import pandas as pd
df = pd.DataFrame({
'Name': ['Amit', 'Neha', 'Raj', 'Priya'],
'Marks': [85, 90, 78, 92],
'Grade': ['A', 'A+', 'B+', 'A+']
}, index=['R1', 'R2', 'R3', 'R4'])
```
**Delete a COLUMN (axis=1):**
```python
df2 = df.drop('Grade', axis=1) # Returns new df WITHOUT Grade column
print(df2)
```
**Output:**
```
Name Marks
R1 Amit 85
R2 Neha 90
R3 Raj 78
R4 Priya 92
```
**Delete a ROW (axis=0):**
```python
df3 = df.drop('R3', axis=0) # Removes Raj's row
print(df3)
```
**Output:**
```
Name Marks Grade
R1 Amit 85 A
R2 Neha 90 A+
R4 Priya 92 A+
```
**Delete multiple rows or columns:**
```python
df.drop(['R1', 'R4'], axis=0) # Delete multiple rows
df.drop(['Grade', 'Marks'], axis=1) # Delete multiple columns
```
**Using `inplace=True` to modify the original:**
```python
df.drop('Grade', axis=1, inplace=True) # Changes df itself, no need to reassign
```
> [!WARNING]
> **drop() doesn't change the original by default!**
> `df.drop('Grade', axis=1)` returns a NEW DataFrame β `df` itself is unchanged!
> Use `inplace=True` OR reassign: `df = df.drop('Grade', axis=1)` to actually change `df`.
> [!IMPORTANT]
> **Board Exam Tip**
> "Write code to delete the column 'Grade' from a DataFrame df." β **2-mark** question!
> Answer: `df = df.drop('Grade', axis=1)` or `df.drop('Grade', axis=1, inplace=True)`
> Remember: `axis=1` for columns, `axis=0` for rows!
---
### 1.14.2 Renaming Rows/Columns
Using the `rename()` method:
```python
# rename_demo.py
import pandas as pd
df = pd.DataFrame({
'Name': ['Amit', 'Neha'],
'Marks': [85, 90]
}, index=['R1', 'R2'])
# Rename COLUMNS:
df2 = df.rename(columns={'Name': 'StudentName', 'Marks': 'Score'})
print(df2)
```
**Output:**
```
StudentName Score
R1 Amit 85
R2 Neha 90
```
**Rename ROWS (index):**
```python
df3 = df.rename(index={'R1': 'Row1', 'R2': 'Row2'})
print(df3)
```
**Output:**
```
Name Marks
Row1 Amit 85
Row2 Neha 90
```
**Rename both at once:**
```python
df4 = df.rename(columns={'Name': 'StudentName'},
index={'R1': 'Row1'})
```
---
## 1.15 π΅ More on DataFrame Indexing β BOOLEAN INDEXING
Boolean indexing is about creating and using **True/False arrays** to filter data.
### 1.15.1 Creating DataFrames with Boolean Indexes
A **Boolean index** is a Series of True/False values used as row labels:
```python
# boolean_index.py
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya'],
'Marks': [85, 90, 78, 92],
}
df = pd.DataFrame(data)
# Creating a Boolean Series:
bool_index = pd.Series([True, False, True, False])
print("Boolean Series:")
print(bool_index)
print("\nDataFrame with Boolean Index:")
print(df[bool_index])
```
**Output:**
```
Boolean Series:
0 True
1 False
2 True
3 False
dtype: bool
DataFrame with Boolean Index:
Name Marks
0 Amit 85
2 Raj 78
```
> When you pass a Boolean Series to a DataFrame, it **keeps only the rows where the value is True** and skips the rows where it's False!
---
### 1.15.2 Accessing Rows from DataFrames with Boolean Indexes π
The real power β creating boolean conditions dynamically from data:
```python
# boolean_access.py
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj', 'Priya', 'Sita'],
'Marks': [85, 90, 78, 92, 65],
'Subject': ['Math', 'Science', 'Math', 'Science', 'Math'],
'City': ['Delhi', 'Mumbai', 'Delhi', 'Pune', 'Delhi']
}
df = pd.DataFrame(data)
# Step 1: Create a boolean condition (this creates True/False series)
condition = df['Marks'] > 80
print("Boolean condition:")
print(condition)
print()
# Step 2: Use it to filter the DataFrame
print("Students with Marks > 80:")
print(df[condition])
```
**Output:**
```
Boolean condition:
0 True
1 True
2 False
3 True
4 False
dtype: bool
Students with Marks > 80:
Name Marks Subject City
0 Amit 85 Math Delhi
1 Neha 90 Science Mumbai
3 Priya 92 Science Pune
```
**Combining Multiple Conditions:**
```python
# multiple_conditions.py
# AND condition (&): Both must be True
print("Math students scoring > 80:")
print(df[(df['Subject'] == 'Math') & (df['Marks'] > 80)])
# OR condition (|): At least one must be True
print("\nDelhi students OR students scoring > 88:")
print(df[(df['City'] == 'Delhi') | (df['Marks'] > 88)])
# NOT condition (~): Reverse True/False
print("\nStudents NOT from Delhi:")
print(df[~(df['City'] == 'Delhi')])
```
**Output of Math AND Marks > 80:**
```
Name Marks Subject City
0 Amit 85 Math Delhi
```
::: grid
::: card & | AND | Both conditions must be True | df[(df['Marks'] > 80) & (df['City'] == 'Delhi')]
::: card | | OR | At least one condition True | df[(df['Marks'] > 90) | (df['City'] == 'Delhi')]
::: card ~ | NOT | Reverses True/False | df[~(df['City'] == 'Mumbai')]
:::
> [!IMPORTANT]
> **Board Exam Tip**
> "Write a query to display students who scored more than 75 AND are from Delhi." β **3-mark** question!
> Answer: `df[(df['Marks'] > 75) & (df['City'] == 'Delhi')]`
> Remember: Use `&` (not `and`) and `|` (not `or`) inside Pandas conditions!
---
## β οΈ Common Errors and Misconceptions
| Mistake | What Goes Wrong | Correct Code |
| :--- | :--- | :--- |
| β `df['Name', 'Marks']` | TypeError | β
`df[['Name', 'Marks']]` (double brackets!) |
| β `df.loc[0:2]` thinking 2 is excluded | Includes index label 2! | β
Use `df.iloc[0:2]` if you want 2 excluded |
| β `df.drop('Name')` β column not deleted | axis not specified | β
`df.drop('Name', axis=1)` |
| β `df.drop('R1')` doesn't change original | drop() returns new df | β
`df = df.drop('R1', axis=0)` or `inplace=True` |
| β `df[(df['M'] > 80) and (df['C'] == 'Delhi')]` | Use of Python `and` | β
Use `&` not `and` |
| β `s[1:3]` for label slice includes 3 | Position slice excludes 3 | β
Label slice `s['b':'d']` INCLUDES 'd' |
| β Forgetting `import pandas as pd` | NameError: pd not defined | β
Always import at the top! |
---
## π Quick Revision β Exam Ready!
**Series β One-Line Summary:**
- Create: `pd.Series(list/dict/scalar, index=[...])`
- Access: `s['label']` or `s[position]`
- Slice: `s['a':'c']` (inclusive) or `s[0:3]` (exclusive)
- Sort: `sort_values()` / `sort_index()`
- Filter: `s[s > 50]`
**DataFrame β One-Line Summary:**
- Create: `pd.DataFrame(dict_of_lists, index=[...])`
- Column: `df['col']` β Series; `df[['col1','col2']]` β DataFrame
- Row by label: `df.loc['R1']`
- Row by number: `df.iloc[0]`
- Filter: `df[df['col'] > value]`
- Add column: `df['NewCol'] = values`
- Delete: `df.drop('col', axis=1)` or `df.drop('row', axis=0)`
- Rename: `df.rename(columns={'old':'new'})`
**loc vs iloc quick rule:**
- `loc` = Label β Inclusive slice
- `iloc` = Integer β Exclusive slice
---
## π― Sample Board Exam Questions
### Q1: Very Short Answer [1 mark each]
a) Which Pandas data structure is 2-dimensional?
**β DataFrame**
b) Write the command to import Pandas with alias pd.
**β `import pandas as pd`**
c) What does `df.shape` return for a DataFrame with 5 rows and 3 columns?
**β `(5, 3)` β a tuple (rows, columns)**
d) What does `head()` return by default?
**β First 5 rows**
e) Which attribute gives data type of all columns in a DataFrame?
**β `df.dtypes`**
---
### Q2: Short Answer [2 marks]
**Q: Differentiate between loc and iloc with example.**
`loc` uses **label names** to access rows/columns and the slice end is **inclusive**.
`iloc` uses **integer positions** (0, 1, 2...) and the slice end is **exclusive**.
```python
df.loc['R1':'R3'] # Returns R1, R2, R3 (R3 INCLUDED)
df.iloc[0:3] # Returns rows 0, 1, 2 (3 EXCLUDED)
```
---
### Q3: Program Writing [3 marks]
**Q: Create a DataFrame with columns Name, Age, City for 3 students. Display students from 'Delhi'.**
```python
import pandas as pd
data = {
'Name': ['Amit', 'Neha', 'Raj'],
'Age': [16, 17, 16],
'City': ['Delhi', 'Mumbai', 'Delhi']
}
df = pd.DataFrame(data)
print("Delhi students:")
print(df[df['City'] == 'Delhi'])
```
---
### Q4: Program Writing [3 marks]
**Q: Create a Series of 5 subjects and their marks. Display subjects with marks above 80. Sort in descending order.**
```python
import pandas as pd
marks = pd.Series(
[95, 72, 88, 65, 91],
index=['Math', 'English', 'Science', 'Hindi', 'IP']
)
print("Subjects above 80:")
print(marks[marks > 80])
print("\nSorted in descending order:")
print(marks.sort_values(ascending=False))
```
---
### Q5: Output Based [2 marks]
**Q: What is the output?**
```python
import pandas as pd
s = pd.Series([10, 20, 30, 40, 50], index=['a','b','c','d','e'])
print(s['b':'d'])
print(s[1:3])
```
**Output:**
```
b 20
c 30
d 40
dtype: int64
b 20
c 30
dtype: int64
```
*(Note: Label slice `'b':'d'` INCLUDES 'd'. Position slice `1:3` EXCLUDES index 3)*
---
## βοΈ Practice Problems
1. Create a Series of 6 students' marks and display only those who scored between 70 and 90.
2. Create a DataFrame for a product catalogue with columns: ProductName, Price, Quantity, Category. Display all products under βΉ500.
3. Write code to add a 'Total' column to a student DataFrame that has 'Test1' and 'Test2' columns.
4. Create a DataFrame and delete a specific row and column. Show the output before and after deletion.
5. Using `loc`, select rows R2 to R4 and columns 'Name' and 'Marks' from a DataFrame.
6. What is the difference between `sort_values()` and `sort_index()` in a Series? Give examples.
7. Write code to rename the column 'Marks' to 'Score' and the index label 'R1' to 'Row1'.
8. Create a DataFrame with student data. Using boolean indexing, display students who are from 'Mumbai' OR scored above 85.
9. Explain with code what happens when you add two Series with different index labels.
10. Write code to access the element at the 3rd row and 2nd column of a DataFrame using both `loc` and `iloc`.
Back to List
Calculating...
UNIT 1 : CH 1
Jul 10, 2026
π Data Handling Using Pandas β I
Learning Support
Need Help With This Chapter?
Save key topics for exam revision, ask questions to teachers, or submit content corrections.
Verified Doubts & Teacher Answers
Loading resolved questions for this note...