Introduction
Data manipulation is the heart of working with Excel files in Python. Real-world Excel data is rarely perfect - it has missing values, duplicates, inconsistent formatting, and requires transformation before analysis. This session teaches you how to clean, filter, group, and transform Excel data like a professional data analyst.
Why Data Manipulation Matters
- Real Data is Messy - Missing values, duplicates, errors
- Analysis Requires Clean Data - Garbage in, garbage out
- Business Insights - Extract meaningful information
- Report Generation - Transform raw data into presentations
- Automation - Standardize data processing workflows
Data Cleaning Fundamentals
Handling Missing Values
import pandas as pd
import numpy as np
# Create DataFrame with missing values
df = pd.DataFrame({
'Name': ['Alice', 'Bob', None, 'Diana', 'Eve'],
'Age': [25, 30, np.nan, 28, 32],
'Salary': [60000, np.nan, 70000, None, 75000],
'Department': ['Sales', 'IT', 'HR', None, 'IT']
})
# Check for missing values
print(df.isnull().sum())
print(df.isna().sum()) # Alternative
# Check if any missing values
print(df.isnull().any())
# Find rows with any missing values
rows_with_na = df[df.isnull().any(axis=1)]
Dropping Missing Values
# Drop rows with ANY missing values
df_clean = df.dropna()
# Drop rows where ALL values are missing
df_clean = df.dropna(how='all')
# Drop rows with missing values in specific columns
df_clean = df.dropna(subset=['Name', 'Age'])
# Drop columns with any missing values
df_clean = df.dropna(axis=1)
Filling Missing Values
# Fill with a specific value
df_filled = df.fillna(0)
df_filled = df.fillna('Unknown')
# Fill different columns with different values
df_filled = df.fillna({
'Age': df['Age'].mean(),
'Salary': df['Salary'].median(),
'Department': 'Unknown'
})
# Forward fill (use previous value)
df_filled = df.fillna(method='ffill')
# Backward fill (use next value)
df_filled = df.fillna(method='bfill')
# Interpolate (for numeric data)
df['Age'] = df['Age'].interpolate()
Removing Duplicates
# Check for duplicates
print(df.duplicated().sum())
# Show duplicate rows
duplicates = df[df.duplicated()]
# Remove duplicates (keep first occurrence)
df_unique = df.drop_duplicates()
# Remove duplicates based on specific columns
df_unique = df.drop_duplicates(subset=['Name'])
# Keep last occurrence instead of first
df_unique = df.drop_duplicates(keep='last')
# Mark all duplicates (not keeping any)
df_unique = df.drop_duplicates(keep=False)
Data Filtering and Selection
Boolean Indexing
# Single condition
high_earners = df[df['Salary'] > 65000]
young_employees = df[df['Age'] < 30]
it_dept = df[df['Department'] == 'IT']
# Multiple conditions (AND)
young_high_earners = df[(df['Age'] < 30) & (df['Salary'] > 65000)]
# Multiple conditions (OR)
sales_or_it = df[(df['Department'] == 'Sales') | (df['Department'] == 'IT')]
# NOT condition
not_sales = df[df['Department'] != 'Sales']
not_sales_alt = df[~(df['Department'] == 'Sales')]
# Multiple values (isin)
selected_depts = df[df['Department'].isin(['Sales', 'IT', 'HR'])]
# String contains
names_with_a = df[df['Name'].str.contains('a', case=False, na=False)]
# Between values
mid_age = df[df['Age'].between(28, 35)]
Query Method
# Using query() for more readable filtering
high_earners = df.query('Salary > 65000')
young_it = df.query('Age < 30 and Department == "IT"')
sales_or_hr = df.query('Department in ["Sales", "HR"]')
# Using variables in queries
min_salary = 60000
filtered = df.query('Salary > @min_salary')
Selecting Columns
# Select single column (returns Series)
names = df['Name']
# Select single column (returns DataFrame)
names_df = df[['Name']]
# Select multiple columns
subset = df[['Name', 'Age', 'Salary']]
# Select columns by data type
numeric_cols = df.select_dtypes(include=[np.number])
object_cols = df.select_dtypes(include=['object'])
# Select columns by name pattern
cols_with_a = df.filter(like='a')
cols_regex = df.filter(regex='^S') # Starts with S
loc and iloc
# loc: label-based selection
df.loc[0, 'Name'] # Single value
df.loc[0:2, 'Name':'Age'] # Slice
df.loc[df['Age'] > 30, ['Name', 'Salary']] # Conditional
# iloc: integer position-based selection
df.iloc[0, 0] # First row, first column
df.iloc[0:3, 0:2] # First 3 rows, first 2 columns
df.iloc[[0, 2, 4], :] # Specific rows, all columns
Data Sorting
Sort by Single Column
# Sort by Age (ascending)
df_sorted = df.sort_values('Age')
# Sort by Age (descending)
df_sorted = df.sort_values('Age', ascending=False)
# Sort in place
df.sort_values('Age', inplace=True)
Sort by Multiple Columns
# Sort by Department, then by Age
df_sorted = df.sort_values(['Department', 'Age'])
# Different sort orders for different columns
df_sorted = df.sort_values(
['Department', 'Age'],
ascending=[True, False]
)
Sort by Index
# Sort by row index
df_sorted = df.sort_index()
# Sort by column names
df_sorted = df.sort_index(axis=1)
Grouping and Aggregation
Basic GroupBy
# Group by single column
dept_groups = df.groupby('Department')
# Get group sizes
print(dept_groups.size())
# Aggregate specific column
avg_salary = df.groupby('Department')['Salary'].mean()
total_employees = df.groupby('Department').size()
Multiple Aggregations
# Multiple statistics for one column
dept_stats = df.groupby('Department')['Salary'].agg([
'count',
'mean',
'median',
'min',
'max',
'std'
])
# Different aggregations for different columns
summary = df.groupby('Department').agg({
'Age': ['mean', 'min', 'max'],
'Salary': ['sum', 'mean'],
'Name': 'count'
})
# Custom aggregation functions
def salary_range(x):
return x.max() - x.min()
dept_range = df.groupby('Department')['Salary'].agg(salary_range)
Named Aggregations (pandas 0.25+)
summary = df.groupby('Department').agg(
avg_salary=('Salary', 'mean'),
total_employees=('Name', 'count'),
max_age=('Age', 'max')
).reset_index()
Group by Multiple Columns
# Create multi-level grouping
df['Seniority'] = pd.cut(df['Age'], bins=[0, 28, 35, 100],
labels=['Junior', 'Mid', 'Senior'])
multi_group = df.groupby(['Department', 'Seniority']).agg({
'Salary': 'mean',
'Name': 'count'
})
Transform and Apply
# Transform: returns same shape as input
df['Salary_Normalized'] = df.groupby('Department')['Salary'].transform(
lambda x: (x - x.mean()) / x.std()
)
# Apply: can return different shape
def top_two(group):
return group.nlargest(2, 'Salary')
top_earners = df.groupby('Department').apply(top_two)
Pivot Tables
Basic Pivot Table
# Create pivot table
pivot = pd.pivot_table(
df,
values='Salary',
index='Department',
aggfunc='mean'
)
Multi-Level Pivot Tables
# Multiple row indices
pivot = pd.pivot_table(
df,
values='Salary',
index=['Department', 'Seniority'],
aggfunc='mean'
)
# With columns
pivot = pd.pivot_table(
df,
values='Salary',
index='Department',
columns='Seniority',
aggfunc='mean',
fill_value=0
)
Multiple Aggregations in Pivot
pivot = pd.pivot_table(
df,
values='Salary',
index='Department',
aggfunc=['mean', 'sum', 'count'],
margins=True # Add totals row
)
Data Transformation
Adding Calculated Columns
# Simple calculation
df['Salary_K'] = df['Salary'] / 1000
# Conditional calculations
df['Bonus'] = df['Salary'] * 0.10
df.loc[df['Department'] == 'Sales', 'Bonus'] = df['Salary'] * 0.15
# Using apply with lambda
df['Tax'] = df['Salary'].apply(lambda x: x * 0.25)
# Using apply with custom function
def calculate_grade(age):
if age < 25:
return 'Junior'
elif age < 35:
return 'Mid'
else:
return 'Senior'
df['Grade'] = df['Age'].apply(calculate_grade)
String Operations
# Convert case
df['Name_Upper'] = df['Name'].str.upper()
df['Name_Lower'] = df['Name'].str.lower()
df['Name_Title'] = df['Name'].str.title()
# String length
df['Name_Length'] = df['Name'].str.len()
# Contains
has_a = df[df['Name'].str.contains('a', case=False, na=False)]
# Replace
df['Name_Clean'] = df['Name'].str.replace('Dr.', '')
# Split
df[['First', 'Last']] = df['Full_Name'].str.split(' ', expand=True)
# Strip whitespace
df['Name'] = df['Name'].str.strip()
Binning and Categorizing
# Create bins
df['Age_Group'] = pd.cut(
df['Age'],
bins=[0, 25, 35, 50, 100],
labels=['<25', '25-35', '35-50', '50+']
)
# Quantile-based binning
df['Salary_Quartile'] = pd.qcut(
df['Salary'],
q=4,
labels=['Q1', 'Q2', 'Q3', 'Q4']
)
Date Operations
# Convert to datetime
df['Date'] = pd.to_datetime(df['Date'])
# Extract date components
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['Day'] = df['Date'].dt.day
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['DayName'] = df['Date'].dt.day_name()
# Date arithmetic
df['Days_Since'] = (pd.Timestamp.now() - df['Date']).dt.days
Merging and Joining
Merge DataFrames
# Inner join (default)
merged = pd.merge(df1, df2, on='ID')
# Left join
merged = pd.merge(df1, df2, on='ID', how='left')
# Right join
merged = pd.merge(df1, df2, on='ID', how='right')
# Outer join (full outer)
merged = pd.merge(df1, df2, on='ID', how='outer')
# Merge on multiple keys
merged = pd.merge(df1, df2, on=['ID', 'Year'])
# Merge on different column names
merged = pd.merge(
df1, df2,
left_on='EmployeeID',
right_on='EmpID'
)
Concatenating DataFrames
# Vertical concatenation (stack rows)
combined = pd.concat([df1, df2], ignore_index=True)
# Horizontal concatenation (side by side)
combined = pd.concat([df1, df2], axis=1)
# Concatenate with keys
combined = pd.concat([df1, df2], keys=['Q1', 'Q2'])
Reshaping Data
Melt (Wide to Long)
# Wide format
df_wide = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Q1': [100, 150],
'Q2': [110, 160],
'Q3': [120, 170]
})
# Convert to long format
df_long = pd.melt(
df_wide,
id_vars=['Name'],
value_vars=['Q1', 'Q2', 'Q3'],
var_name='Quarter',
value_name='Sales'
)
Pivot (Long to Wide)
# Convert back to wide format
df_wide_again = df_long.pivot(
index='Name',
columns='Quarter',
values='Sales'
)
Real-World Example: Sales Data Analysis
import pandas as pd
import numpy as np
# Read messy sales data
df = pd.read_excel('sales_data.xlsx')
# Step 1: Data Cleaning
print("Original shape:", df.shape)
# Remove duplicates
df = df.drop_duplicates()
# Handle missing values
df['Amount'] = df['Amount'].fillna(0)
df['Region'] = df['Region'].fillna('Unknown')
df = df.dropna(subset=['Date', 'Product'])
# Clean string columns
df['Product'] = df['Product'].str.strip().str.upper()
df['Region'] = df['Region'].str.title()
# Step 2: Data Transformation
df['Date'] = pd.to_datetime(df['Date'])
df['Month'] = df['Date'].dt.to_period('M')
df['Quarter'] = df['Date'].dt.to_period('Q')
# Step 3: Calculated Fields
df['Revenue'] = df['Quantity'] * df['Price']
df['Cost'] = df['Quantity'] * df['Unit_Cost']
df['Profit'] = df['Revenue'] - df['Cost']
df['Margin'] = (df['Profit'] / df['Revenue'] * 100).round(2)
# Step 4: Aggregation and Analysis
# Monthly summary by region
monthly_summary = df.groupby(['Month', 'Region']).agg({
'Revenue': 'sum',
'Profit': 'sum',
'Quantity': 'sum'
}).reset_index()
# Top products by revenue
top_products = df.groupby('Product').agg({
'Revenue': 'sum',
'Quantity': 'sum'
}).sort_values('Revenue', ascending=False).head(10)
# Regional performance
regional = df.groupby('Region').agg({
'Revenue': 'sum',
'Profit': 'sum',
'Margin': 'mean'
}).round(2)
# Step 5: Create pivot table
pivot_sales = pd.pivot_table(
df,
values='Revenue',
index='Product',
columns='Quarter',
aggfunc='sum',
fill_value=0,
margins=True
)
# Step 6: Export results
with pd.ExcelWriter('sales_analysis.xlsx') as writer:
monthly_summary.to_excel(writer, sheet_name='Monthly', index=False)
top_products.to_excel(writer, sheet_name='Top_Products')
regional.to_excel(writer, sheet_name='Regional')
pivot_sales.to_excel(writer, sheet_name='Pivot')
print("✓ Analysis complete!")
Best Practices
- Always check data first - Use
df.head(),df.info(),df.describe() - Handle missing values explicitly - Don't ignore NaN values
- Verify after operations - Check shape and content after transformations
- Use meaningful names - Clear column and variable names
- Document assumptions - Comment why you made specific choices
- Test on small samples - Verify logic before processing large files
- Save intermediate results - For debugging and review
- Use vectorized operations - Faster than loops
Summary
You've learned how to: - ✅ Clean messy data (missing values, duplicates) - ✅ Filter and select data effectively - ✅ Sort data by single and multiple columns - ✅ Group and aggregate data for insights - ✅ Create pivot tables - ✅ Transform data (calculate, categorize, reshape) - ✅ Merge and concatenate DataFrames - ✅ Apply real-world data analysis workflows
Next Steps
In Session 5, you'll learn how to format your cleaned and analyzed data into professional-looking Excel reports with colors, fonts, borders, and conditional formatting.