Overview
This chapter, Data Handling using Pandas – II, builds on basic Pandas concepts to teach advanced, practical data-manipulation techniques used in real-world data analysis. It introduces methods for cleaning, transforming, reshaping, combining and summarising tabular data efficiently using Pandas DataFrame and Series. The chapter emphasises reliable preprocessing (handling missing or inconsistent data), powerful aggregation and grouping (groupby, pivot tables), combining datasets (merge, join, concat), and time-series operations (date parsing, resampling, rolling windows). Importance: mastering these topics enables students to prepare datasets for analysis or machine learning, extract meaningful summaries, join information from multiple sources, and work with real-life messy data — skills required for project work and data-driven problem solving. Key themes include data cleaning (dropna, fillna, replace), transformation (apply, map, astype), reshaping (melt, pivot, stack/unstack), combining datasets (merge, concat, join), grouping and aggregation (groupby, agg, pivot_table), and basic time-series handling (to_datetime, resample, rolling). What the student will learn: how to read…
Learning Objectives
- Define DataFrame and Series and distinguish between their structures and uses
- Explain methods to read and write data (read_csv, to_csv, read_excel, to_excel, read_json) and choose appropriate formats
- Demonstrate importing pandas and loading datasets into DataFrame for analysis
- Apply indexing and selection techniques using loc, iloc, at and iat to access rows and columns
- Clean datasets by detecting and handling missing or duplicate values using isnull, dropna, fillna, interpolate and drop_duplicates
- Transform data types and handle dates using astype and to_datetime, and perform datetime indexing and resampling
- Filter and sort data using boolean masks, query, sort_values and sort_index to answer exam-style questions
- Create and modify columns using assign, apply with lambda, map and replace to derive required features
Topics in this chapter
11 topics · tap a topic title to jump straight to it.
Merging, Joining and Concatenation
Merging, Joining and Concatenation
Key Point: pd.merge(left, right, how='inner'|'left'|'right'|'outer', on=None, left_on=None, right_on=None, left_index=False, right_index=False, suffixes=('_x','_y'), indicator=False, validate=None)
Overview: Merging, joining and concatenation are the primary ways to combine tabular data in pandas. They let you combine DataFrames by matching rows/columns (merge/join) or by stacking them vertically or horizontally (concatenate).
Merge (database-style joins): pd.merge() aligns rows using one or more keys (columns). It supports relational join types: inner, left, right, outer. Use on (same-named key in both), or left_on/right_on for different key names. You can also merge on index by setting left_index=True or right_index=True.
pd.merge(left, right, how="inner", on=None,
left_on=None, right_on=None,
left_index=False, right_index=False,
suffixes=("_x","_y"), indicator=False, validate=None)
Join semantics:
- inner: rows with keys present in both tables
- left: all rows from left table; matching rows from right
- right: all rows from right table; matching from left
- outer: union of keys; missing values filled with NaN
DataFrame.join: convenience method that joins on index by default. Equivalent to merge with index alignment. Useful for adding columns from another DataFrame where the index is the key.
df.join(other_df, how="left", on=None, lsuffix="", rsuffix="")
Concatenation: pd.concat() stacks DataFrames along an axis. axis=0 (default) appends rows; axis=1 stacks columns. Useful for combining datasets with the same columns (vertical) or same index (horizontal).
pd.concat([df1, df2, df3], axis=0, join="outer", ignore_index=False, keys=None)
Key concat options:
axis=0: stack rows (one below another)axis=1: join columns side-by-sideignore_index=True: reset the resulting index to sequential integerskeys: create a hierarchical index to indicate source framesjoin='outer'|'inner': how to align columns when axis=0
Differences in short: merge/join are relational (match on keys); concat stacks whole DataFrames. Use merge/join when you want to combine related rows by key. Use concat when you want to append or combine datasets with similar structure.
Practical tips:
- Use
indicator=Truein pd.merge to see source of each row (left_only/right_only/both). - Use
validateto assert relationship (e.g., "one_to_one", "one_to_many"). - Resolve overlapping column names with
suffixes. - For large datasets, ensure index/key columns are properly typed and consider sorting if you rely on ordered joins.
Example code snippet (student tables):
# merge two tables on student_id
merged = pd.merge(students_info, marks, how="left", on="student_id")
# concatenate monthly sales
all_sales = pd.concat([jan, feb, mar], axis=0, ignore_index=True)
# join by index
df_with_extra = df1.join(df2, how="outer")
- Student information and marks: students.csv (student_id, name, class) and marks.csv (student_id, subject, score). Use pd.merge(students, marks, on='student_id', how='left') to attach marks to every student. This gives missing scores as NaN for students without marks.
- Customer and orders: customers (customer_id, name, city) and orders (order_id, customer_id, total). Use pd.merge(customers, orders, left_on='customer_id', right_on='customer_id', how='inner') to get only customers who placed orders.
- Monthly sales concatenation: jan.csv, feb.csv, mar.csv containing same columns (date, store, sales). Use pd.concat([jan, feb, mar], axis=0, ignore_index=True) to create a single time-series sales DataFrame for analysis.
- Time-series sensor logs from devices split by day: concatenate files vertically and set a DatetimeIndex; then plot a continuous line of measurements over time.
- Adding additional attribute columns: df1 has index as product_id and df2 has product prices indexed by product_id. Use df1.join(df2, how='left') to attach price column to df1.
- \[pd.merge(left\]\[right\]\[how='inner'|'left'|'right'|'outer'\]\[on=None\]\[left_on=None\]\[right_on=None\]\[left_index=False\]\[right_index=False\]\[suffixes=('_x','_y')\]\[indicator=False\]\[validate=None)\]
- \[DataFrame.join(other\]\[on=None\]\[how='left'|'right'|'outer'|'inner'\]\[lsuffix=''\]\[rsuffix=''\]\[sort=False) # joins by index by default\]
- \[pd.concat(objs\]\[axis=0|1\]\[join='outer'|'inner'\]\[ignore_index=False\]\[keys=None\]\[verify_integrity=False)\]
- \[Common join types mapping: inner = intersection(keys)\]\[left = keys(left) ∪ matching(right)\]\[right = keys(right) ∪ matching(left)\]\[outer = union(keys(left)\]\[keys(right))\]
- \[Validate options examples: validate='one_to_one', 'one_to_many', 'many_to_one', 'many_to_many' (raises if relationship is violated)\]
Reshaping and Pivoting
Reshaping and Pivoting
Key Point: melt: df.melt(id_vars=[id_cols], value_vars=[measured_cols], var_name='variable', value_name='value')
What it is: Reshaping and pivoting are operations in Pandas used to change the layout of a DataFrame so it is easier to analyse or visualise. The two main forms are wide (many columns representing categories) and long (one column for a variable type and one for its value). Reshaping converts between these forms; pivoting rearranges rows and columns to summarise data.
Why it matters: Many datasets come in a wide format (e.g., monthly columns) but analysis and plotting tools often expect long format. Pivoting lets you create summary tables (for example, total sales per product per month) that are easy to read and plot.
Key operations:
- melt — converts wide -> long. You specify identifier columns (id_vars) that remain, and measured columns (value_vars) that are unpivoted into a single value column.
- pivot — reshapes long -> wide when each index/column pair has a single value. Use when data is uniquely identified by index & column.
- pivot_table — like pivot but supports aggregation when multiple values map to the same index/column pair (provides aggfunc).
- stack / unstack — move a level of column index to the row index or vice versa; useful with MultiIndex.
- transpose (T) — flip rows and columns.
Typical workflow:
- Inspect whether data is wide or long.
- Decide the shape needed for analysis or plotting (e.g., grouping, time series, comparison across categories).
- Use melt to make the data long or use pivot / pivot_table to create summary wide tables.
Important notes: pivot requires unique index/column/value triplets; pivot_table accepts duplicates and applies an aggregation function (sum, mean, count, etc.). stack/unstack preserve MultiIndex structure and are powerful for hierarchical data.
Small code examples (conceptual):
# melt (wide -> long)
df_long = df.melt(id_vars=['ID','Date'], value_vars=['Jan','Feb','Mar'], var_name='Month', value_name='Sales')
# pivot (long -> wide) — only if each (Date,Product) has one Amount
df_wide = df_long.pivot(index='Date', columns='Product', values='Amount')
# pivot_table with aggregation
pt = df.pivot_table(index='Region', columns='Product', values='Sales', aggfunc='sum', fill_value=0, margins=True)
# stack / unstack
s = df_wide.stack() # columns -> rows
u = s.unstack(level=0) # rows -> columns
- Retail monthly sales: Original dataset has columns Store, Product, Jan, Feb, Mar. Use melt to convert months into a single 'Month' column and 'Sales' column, then groupby Month & Product to plot trends.
- Survey responses: Questions as columns (Q1, Q2, Q3). Melt into respondent_id, question, answer to compute question-wise response distributions and draw bar charts or stacked bars.
- Sensor readings: Many sensors produce columns sensor_1 ... sensor_n. Melt to long format with columns timestamp, sensor_id, reading, then plot time-series per sensor or compute averages.
- School marks: Data with columns Student, Subject1, Subject2, Subject3. Melt to Student, Subject, Marks to compute class averages per subject and show boxplots by subject.
- Sales summary pivot_table: From transaction-level data with columns Date, Region, Product, Amount — create a pivot_table with index=Region, columns=Product, values=Amount, aggfunc='sum' to get a matrix of total sales per region-product.
- \[melt: df.melt(id_vars=[id_cols]\]\[value_vars=[measured_cols]\]\[var_name='variable'\]\[value_name='value')\]
- \[pivot: df.pivot(index='row_index'\]\[columns='column_index'\]\[values='value_col') # requires unique pairs\]
- \[pivot_table: df.pivot_table(index=[index_cols]\]\[columns=[column_cols]\]\[values=[value_cols]\]\[aggfunc='sum|mean|count'\]\[fill_value=0\]\[margins=True)\]
- \[stack / unstack: df.stack(level=-1) # columns -> rows\]\[df.unstack(level=-1) # rows -> columns\]
- \[transpose: df.T # swap rows and columns\]
- \[common aggregation functions: sum()\]\[mean()\]\[count()\]\[median()\]\[min()\]\[max()\]
Grouping and Aggregation
Grouping and Aggregation
Key Point: Count: n = number of rows in group
What is Grouping and Aggregation?
Grouping and aggregation is a way to summarize data by categories. In pandas the common pattern is called split-apply-combine:
- Split: split the DataFrame into groups based on values in one or more columns (using
groupby). - Apply: apply aggregation functions (like sum, mean, count) or custom functions to each group.
- Combine: combine the results into a new summarized DataFrame or Series.
Basic usage: df.groupby('col') creates a GroupBy object. Common aggregations are .sum(), .mean(), .count(), .min(), .max(), .std(), and .agg() for multiple aggregations.
Grouping by multiple columns: df.groupby(['col1','col2']) groups on the combination of values from both columns.
'agg' and named aggregation: use agg to compute several statistics at once. Example of named aggregation:
df.groupby('City').agg(Total_Sales=('Sales','sum'), Avg_Price=('Price','mean'))
transform vs apply vs agg vs filter:
agg(oraggregate) returns aggregated summary for each group (reduced size).transformreturns a Series or DataFrame with the same index as the input, useful to add group-level statistics as columns (e.g., normalize by group mean).applycan run a custom function on each group and return complex results (slower for simple aggregations).filterkeeps or removes entire groups based on a group-level condition (e.g., groups with count >= 10).
Resetting index and as_index: by default group keys become index. Use reset_index() or groupby(..., as_index=False) to keep group keys as columns.
Pivot tables: for two-dimensional summaries (rows × columns), use pd.pivot_table(df, index='A', columns='B', values='V', aggfunc='sum'), which is essentially grouping with an extra layout step.
Best practices and tips:
- Prefer vectorized aggregations (
.agg) over.applyfor performance. - Use categorical dtypes for columns with few unique values to speed up grouping.
- Use named aggregation for clear column names in results.
Short example (conceptual):
# Sales DataFrame columns: ['City','Product','Sales','Units']
sales_by_city = df.groupby('City').agg({'Sales':'sum','Units':'sum'})
avg_price_by_product = df.groupby('Product')['Sales'].sum() / df.groupby('Product')['Units'].sum() - 1) Sales by city: df.groupby('City').agg(Total_Sales=('Sales','sum'), Avg_Sales=('Sales','mean'), Orders=('OrderID','count')) — use to see which city has the highest total sales.
- 2) Student marks by class: marks_df.groupby('Class').agg(Avg_Marks=('Marks','mean'), Max_Marks=('Marks','max'), Students=('RollNo','count')) — useful to compare performance of classes.
- 3) Daily sensor readings: df.set_index('Timestamp').resample('D')['Temperature'].mean() or df.groupby(df['Timestamp'].dt.date)['Temperature'].agg(['mean','min','max']) — summarize time-series by day.
- 4) Employee salary by department and gender: df.groupby(['Department','Gender']).Salary.agg(['mean','median','std']).reset_index() — helps HR analyze pay distribution.
- 5) Product returns rate: returns = df.groupby('Product').agg(Total_Sold=('Sold','sum'), Total_Returned=('Returned','sum')) returns['Return_Rate'] = returns['Total_Returned'] / returns['Total_Sold'] — compute rates per product.
- \[Count: n = number of rows in group\]
- \[Sum: S = Σ x_i (sum of values x in the group)\]
- \[Mean (average): μ = S / n = (Σ x_i) / n\]
- \[Median: middle value of ordered group values\]
- \[Variance (population): σ² = (Σ (x_i - μ)²) / n\]
- \[Standard deviation: σ = sqrt(σ²)\]
Handling Missing Data
Handling Missing Data
Key Point: Percent missing (column) = (number of missing values / total rows) × 100
What are missing values?
Missing values (NaN, None) occur when data is not recorded or lost. In pandas they appear as NaN (float) or None (object).
Detecting missing values
Use:
df.isnull() # boolean mask
df.isnull().sum() # count missing per column
df.info() # shows non-null counts per column
How to handle missing values — main strategies
- Drop rows/columns when missingness is small or whole column is useless:
df.dropna(axis=0, how='any') # drop rows with any missing
df.dropna(axis=1, thresh=100) # keep columns with >=100 non-null - Fill (impute) with constants or summary stats:
df['Age'].fillna(0), or use mean/median/mode:
df['Age'].fillna(df['Age'].mean())Use median for skewed numeric distributions and mode for categorical values. - Forward / backward fill (time series): propagate last valid value:
df.fillna(method='ffill', limit=1)ormethod='bfill'. Useful for sensor/time data. - Interpolation: estimate intermediate values (linear, time, polynomial):
df['value'].interpolate(method='linear')ormethod='time'when index is datetime. - Group-wise imputation: fill using group statistics (e.g., group mean):
df['Salary'] = df.groupby('Dept')['Salary'].transform(lambda x: x.fillna(x.mean())) - Indicator / flag for missingness: keep a boolean column to mark imputed rows:
df['Age_missing'] = df['Age'].isnull() - Model-based imputation: advanced methods (KNN, MICE, regression) available in sklearn (
SimpleImputer,IterativeImputer) when simple methods bias results.
Best practices / cautions
- Decide per-variable: imputing a target variable incorrectly can introduce bias or leakage.
- Use median for skewed numeric data, mean for symmetric data, mode for categorical.
- For time series prefer forward/backward fill or interpolation.
- Record how many values were imputed and test models with/without imputation to check effects.
Quick workflow example (pandas)
# 1. detect
missing_counts = df.isnull().sum()
# 2. examine rows with missing in a column
df[df['Age'].isnull()]
# 3. impute numeric by median
df['Age'] = df['Age'].fillna(df['Age'].median())
# 4. impute categorical by mode
df['City'] = df['City'].fillna(df['City'].mode()[0])
Handling missing data carefully improves data quality and leads to more reliable analysis and models.
- Survey data: respondents skip 'income' — replace missing incomes with median income for respondents of the same age-group (group-wise median).
- Sales time series: a few missing daily sales values — use linear interpolation or forward-fill depending on pattern.
- Sensor network: intermittent NaNs from a temperature sensor — use forward-fill with a limit or model-based imputation if gaps are large.
- Student records: missing categorical field 'stream' — fill with the mode (most common stream) or leave as a separate category 'Unknown' and add a missing flag.
- Medical dataset: missing lab-test values — avoid naive mean imputation for the target variable; consider using model-based imputation and always add a missing indicator.
- \[Percent missing (column) = (number of missing values / total rows) × 100\]
- \[Mean = (1/n) Σ xi\]
- \[Median = middle value after sorting (or average of two middle values if n is even)\]
- \[Mode = value with highest frequency\]
- \[Linear interpolation between points (t0,y0) and (t1,y1): y(t) = y0 + (y1 - y0) * (t - t0) / (t1 - t0)\]
Hierarchical Indexing (MultiIndex)
Hierarchical Indexing (MultiIndex)
Key Point: Create MultiIndex from columns: df = df.set_index(['level1', 'level2'])
What is Hierarchical Indexing (MultiIndex)?
Hierarchical indexing (MultiIndex) is a feature of pandas that allows you to have multiple levels of index labels on rows and/or columns. Instead of a single index, a MultiIndex stores tuples of keys and lets you represent higher-dimensional data in a 2D DataFrame in a clear, structured way.
Why use MultiIndex?
- Represent grouped or multi-dimensional data compactly (for example region → product → month).
- Enable flexible selection, aggregation and reshaping along different levels.
- Work easily with pivoted tables, time series separated by category, or multi-key groupings.
How to create a MultiIndex
- From columns using
df.set_index(['A', 'B']). - From tuples using
pd.MultiIndex.from_tuples([(a1,b1),(a2,b2)]). - From product of levels with
pd.MultiIndex.from_product([levels1, levels2]).
Common operations
- Selection by level:
df.xs(key, level='level_name')ordf.loc[pd.IndexSlice[level1_value, level2_slice], :]. - Swap or reorder levels:
df.swaplevel(i, j)anddf.reorder_levels([...]). - Reshape:
df.stack()(columns -> rows) anddf.unstack(level)(rows -> columns). - Aggregation by level:
df.groupby(level='level_name').sum(). - Sorting:
df.sort_index(level=[...])to order by one or more index levels.
Selection examples (conceptual)
- Select all rows for product P in any region:
df.xs('P', level='product'). - Select specific region and product:
df.loc[('North','P'), :].
Best practices
- Name your index levels for clearer code:
df.index.names = ['region','product']. - Use
sort_indexon levels before slicing to ensure predictable results. - Prefer
xsandIndexSlicefor readable multi-level selection.
Short example (HTML code block)
# Suppose df has columns ['region','product','month','sales']
df = df.set_index(['region','product','month'])
# Total sales for each region and product
totals = df.groupby(level=['region','product']).sum()
# Sales for region 'East' and product 'Pen'
east_pen = df.xs(('East','Pen'))
- Sales dataset (region, product, month): Create a MultiIndex with df.set_index(['region','product','month']) and compute totals: df.groupby(level=['region','product']).sum(). Example result: a table with index (North, Pen) -> total sales.
- Student marks (class, student_id, subject): store marks with MultiIndex rows = ['class','student_id'] and columns as subjects. Get one student's marks: df.loc[('12A', 102), :]. Compute class average per subject: df.groupby(level='class').mean().
- Hospital records (ward, patient_id, date): index by ['ward','patient_id','date']. Use df.xs('Ward-3', level='ward') to get all records from Ward-3. Use df.unstack(level='date') to pivot dates into columns.
- Time series across categories (store, date): index = ['store','date'] lets you plot each store's sales over time by selecting df.xs(store_name, level='store') to obtain the series for plotting.
- \[Create MultiIndex from columns: df = df.set_index(['level1', 'level2'])\]
- \[Create MultiIndex from tuples: mi = pd.MultiIndex.from_tuples([(a,b)\]\[(c,d)]\]\[names=['L1','L2'])\]
- \[Select by cross-section: df.xs(key\]\[level='level_name') or df.xs((val1\]\[val2))\]
- \[Advanced selection: df.loc[pd.IndexSlice[val_level1\]\[val_level2], :]\]
- \[Stack / Unstack: stacked = df.stack()\]\[unstacked = df.unstack(level)\]
- \[Swap or reorder levels: df.swaplevel(i\]\[j)\]\[df.reorder_levels(['L2','L1'])\]
Applying Functions to Data
Applying Functions to Data
Key Point: Syntax patterns: - Series.map: df['col'].map(mapping_or_func) - Series.apply: df['col'].apply(func) - DataFrame.apply (row): df.apply(func, axis=1) - DataFrame.applymap: df.applymap(func) - Group aggregate: df.groupby('key')['val'].agg('mean') or .agg(['mean','sum'])
What it means
Applying functions to data in Pandas means transforming Series or DataFrame values by calling Python functions (built-in, NumPy or user-defined) either element-wise, row/column-wise, or group-wise to clean, compute, aggregate or reshape data.
Main methods
Series.map– element-wise mapping for Series (can take a function or dict). Useful for replacing values or mapping categories.Series.apply– apply function element-wise to a Series (returns Series).DataFrame.apply– apply function along an axis (rows or columns). Useaxis=0for columns,axis=1for rows.DataFrame.applymap– element-wise function for every cell in a DataFrame.Series.replace– map or replace specified values directly (fast for fixed replacements).groupby().apply / .agg / .transform– run functions on groups:.aggfor reductions,.transformwhen you want aligned result with original index,.applyfor custom group operations.- Vectorized NumPy functions (e.g.,
np.log,np.where) – generally fastest; prefer over Python-level loops or apply when possible.
Important parameters/notes
axisselects direction forDataFrame.apply(axis=1for row-wise).rawinapplycan pass ndarray (faster) instead of Series objects.- Return types vary:
applymay return Series, DataFrame, or scalar depending on function output. Use.aggto get predictable aggregated outputs. - Avoid
applywhen a vectorized Pandas/NumPy alternative exists for speed. - Handle missing values explicitly inside functions (use
fillnaor check forpd.isna).
Typical uses
data cleaning (string normalization, parsing), feature engineering (new columns from rows), converting units, categorical mapping, computing derived metrics (BMI, discounts), and group-level summaries (average by department).
- Map categories: df['gender_mapped'] = df['gender'].map({'M': 'Male', 'F': 'Female'})
- Element-wise transform with apply: df['price_after_tax'] = df['price'].apply(lambda x: x * 1.12)
- Row-wise computation (BMI): df['BMI'] = df.apply(lambda r: r['weight'] / ((r['height_cm'] / 100) ** 2), axis=1)
- Use vectorized NumPy for speed: df['log_income'] = np.log(df['income'] + 1)
- Group-wise aggregate: dept_mean = df.groupby('dept')['salary'].agg('mean')
- Group transform to attach group mean to rows: df['dept_avg'] = df.groupby('dept')['salary'].transform('mean')
- \[Syntax patterns: - Series.map: df['col'].map(mapping_or_func) - Series.apply: df['col'].apply(func) - DataFrame.apply (row): df.apply(func\]\[axis=1) - DataFrame.applymap: df.applymap(func) - Group aggregate: df.groupby('key')['val'].agg('mean') or .agg(['mean','sum'])\]
- \[BMI formula (example for row-wise apply): BMI = weight_kg / (height_m)^2 => df.apply(lambda r: r['weight'] / ((r['height_cm']/100)**2)\]\[axis=1)\]
- \[Min-max normalization (vectorized): x_norm = (x - x_min) / (x_max - x_min) => df['x_norm'] = (df['x'] - df['x'].min()) / (df['x'].max() - df['x'].min())\]
- \[Z-score standardization: z = (x - mean)/std => df['z'] = (df['x'] - df['x'].mean()) / df['x'].std()\]
- \[Conditional assignment using vectorized np.where: df['flag'] = np.where(df['score'] >= 50, 'pass', 'fail')\]
Working with Text Data
Working with Text Data
Key Point: String length: length_i = len(text_i) (use df['text'].str.len())
Overview
Text data (strings) appears in many real-life tables: names, emails, product reviews, tweets, URLs. Pandas provides a vectorized string accessor (.str) and many tools to clean, transform and analyze text efficiently without Python-level loops.
Typical workflow
- Load data (CSV/Excel) and identify text columns.
- Cleaning / normalization: lowercase, trim whitespace, remove punctuation, normalize encodings.
- Parsing / extraction: split fields, extract patterns (e.g., domains, codes) using regex.
- Tokenization and simple NLP: split into words, remove stopwords, count word frequencies or create dummy variables.
- Feature creation: string length, presence/absence flags, n-grams, or categorical dummies.
- Analysis & visualization: frequency counts, top terms, trends over time.
Key pandas operations & examples
- Access string methods:
df['text'].str.method(). These are vectorized and fast. - Lowercase / uppercase / title:
df['txt'].str.lower(),.str.upper(),.str.title() - Strip whitespace:
.str.strip() - Replace / remove patterns:
.str.replace(r"[^\w\s]", '', regex=True)to remove punctuation - Test / filter:
.str.contains('error', case=False, na=False),.str.startswith('http') - Split into lists or columns:
.str.split(',', expand=False)or.str.split(',', expand=True) - Extract with regex groups:
.str.extract(r'@(.+)$')to get email domain - Find all matches:
.str.findall(r'\w+') - Length:
.str.len()gives number of characters per string - Concatenate strings:
df['first'].str.cat(df['last'], sep=' ') - Convert to categories:
df['col'] = df['col'].astype('category')for memory & grouping - Create dummies from space-separated tags:
df['tags'].str.get_dummies(sep=' ')
Handling missing values
String ops may produce NaN. Use df['text'].fillna('') or pass na=False to boolean .str tests to avoid errors.
Regex in pandas
Regex powers extraction and replacement. Common methods: .str.contains(), .str.match(), .str.extract(), .str.replace(), .str.findall(). Example to extract phone code: df['phone'].str.extract(r'\((\d{3})\)').
Counting and frequencies
Use df['col'].value_counts() for categorical text. For word counts: split into words, explode and then value_counts:
words = df['review'].str.lower().str.replace(r"[^\w\s]", '', regex=True).str.split()
all_words = words.explode()
freq = all_words.value_counts()
Applying custom functions
For complex operations use .apply() or vectorized functions. Try to keep heavy ops outside Python loops. Example: sentiment flag
pos_words = {'good','great','excellent'}
def sentiment(s):
words = set(s.split())
return 'positive' if words & pos_words else 'neutral'
df['sentiment'] = df['review'].fillna('').str.lower().apply(sentiment)
Notes on performance
Prefer built-in vectorized .str methods and pandas functions (get_dummies, explode) over Python for-loops. For large-scale text mining (TF-IDF, embeddings) use scikit-learn or NLP libraries, then combine results in the DataFrame.
Example end-to-end snippet (clean + top words)
# clean, tokenize and get top 10 words
clean = df['review'].fillna('').str.lower().str.replace(r"[^\w\s]", '', regex=True)
words = clean.str.split().explode()
top10 = words.value_counts().head(10)
Curriculum links: string methods, regex basics, splitting and joining, value_counts, get_dummies, apply, handling NaNs.
- Cleaning tweets: remove URLs and punctuation, lowercase, remove stopwords, then count top hashtags and words to analyze trending topics.
- Extracting email domains from a user table: df['domain'] = df['email'].str.extract(r'@(.+)$') to analyze most common providers.
- Splitting full names into first and last: df[['first','last']] = df['name'].str.split(' ', n=1, expand=True).
- Product reviews: create a sentiment flag by searching for positive/negative keywords using .str.contains(..., case=False).
- Tags to dummy variables: df.merge(df['tags'].str.get_dummies(sep=','), left_index=True, right_index=True) to feed into classification.
- \[String length: length_i = len(text_i) (use df['text'].str.len())\]
- \[Relative frequency of a word in a document: TF(term\]\[doc) = (count of term in doc) / (total terms in doc)\]
- \[Inverse document frequency: IDF(term) = log(N / (1 + df_term)) where N = total documents\]\[df_term = documents containing term\]
- \[TF-IDF (weight): TF-IDF(term\]\[doc) = TF(term\]\[doc) * IDF(term)\]
- \[Jaccard similarity for two token sets A and B: J(A,B) = |A ∩ B| / |A ∪ B| (useful for comparing short texts)\]
Time Series and Date Functions
Time Series and Date Functions
Key Point: Rolling mean (window n): rolling_mean_t = (1/n) * sum_{i=0 to n-1} x_{t-i}
What is a Time Series? A time series is a sequence of observations indexed by time (e.g., daily stock prices, hourly temperature, monthly sales). In Pandas, time series data is handled efficiently using datetime-like dtypes, DatetimeIndex, PeriodIndex and timedeltas.
Key components in Pandas
- Datetime conversion: use pd.to_datetime to convert strings to datetime64[ns].
- Indexing: set a DatetimeIndex with df.set_index('date_col'), enabling time-based selection and resampling.
- Resampling and frequency: resample('M'), resample('D'), asfreq – to change observation frequency and aggregate (mean, sum, etc.).
- Rolling and expanding windows: rolling(window).mean(), expanding().sum() for moving statistics.
- Shifting and lead/lag: shift(1), pct_change() for temporal shifts and percent changes.
- Datetime accessors: df['date'].dt.year, .month, .dayofweek, .hour, .day_name() to extract components.
- Timedelta arithmetic: perform date difference operations and use pd.to_timedelta for durations.
- Periods and time spans: Period and PeriodIndex for fixed spans (e.g., '2019Q1').
- Time zones: tz_localize and tz_convert for timezone-aware datetimes.
Common workflows
- Convert and set index: pd.to_datetime -> set_index -> sort_index.
- Resample to desired frequency and aggregate: df.resample('M').sum() or .mean().
- Smooth or denoise: df['value'].rolling(7).mean() or use exponential smoothing.
- Feature extraction: create columns like month, weekday, hour for modelling or grouping.
- Handle missing timestamps: reindex with a complete date_range and interpolate/fill missing values.
Why these matter: Time-aware indexing and resampling simplify aggregation over calendars, enable trend and seasonality analysis, and prepare data for forecasting models.
- Convert and set index: import pandas as pd df['date'] = pd.to_datetime(df['date']) df = df.set_index('date').sort_index()
- Resample daily to monthly and compute sum: monthly = df.resample('M').sum()
- 7-day rolling average (smoothing): df['rolling7'] = df['value'].rolling(window=7, min_periods=1).mean()
- Percent change and shift (returns): df['pct_change'] = df['value'].pct_change() df['lag1'] = df['value'].shift(1)
- Extract components for features: df['month'] = df.index.month df['weekday'] = df.index.dayofweek
- Create a complete time index and fill missing with interpolation: idx = pd.date_range(start=df.index.min(), end=df.index.max(), freq='D') df = df.reindex(idx) df['value'] = df['value'].interpolate()
- \[Rolling mean (window n): rolling_mean_t = (1/n) * sum_{i=0 to n-1} x_{t-i}\]
- \[Exponential Moving Average (EMA): S_t = α * x_t + (1 - α) * S_{t-1}\]\[where α = 2 / (n + 1)\]
- \[Percent change between t and t-1: pct_change_t = (x_t - x_{t-1}) / x_{t-1}\]
- \[Time difference (timedelta): delta = end_time - start_time (gives days\]\[seconds\]\[total_seconds() available)\]
- \[Compound Annual Growth Rate (CAGR) for series (optional): CAGR = (V_end / V_start)^(1/years) - 1\]
Input/Output (Advanced)
Input/Output (Advanced)
Key Point: pd.read_csv(filepath_or_buffer, sep=',', header='infer', names=None, usecols=None, dtype=None, parse_dates=False, date_parser=None, chunksize=None, iterator=False, compression='infer', na_values=None, encoding=None, engine=None, on_bad_lines='error', low_memory=True)
Overview: Advanced Input/Output (I/O) in pandas covers efficient reading and writing of many file formats (CSV, Excel, JSON, SQL, Parquet, Feather, HDF5, compressed files, web/URL sources) and techniques to handle large datasets, control parsing, optimize memory, and reliably exchange data with databases and external systems.
Common formats & trade-offs:
- CSV/TSV: universal, text-based, large files but slower and larger on disk.
- Excel (.xlsx): convenient for spreadsheets but slower, not ideal for very large data.
- JSON: flexible for hierarchical data; may be slower and larger than binary formats.
- Parquet/Feather: columnar binary formats — fast I/O, smaller size, ideal for analytics and large data.
- HDF5: efficient for very large arrays/frames with random access, but more complex metadata.
- SQL databases: persistent storage with query capability; use read_sql/to_sql and SQLAlchemy.
Key advanced reading/writing concepts:
- Parsing control: use usecols, dtype, parse_dates, converters, header, names, sep, quoting to precisely parse input and avoid costly post-processing.
- Memory optimization: set dtype (e.g., category for repeated strings), specify integers with nullable dtypes if needed, read only required columns (usecols), and disable low_memory when types are known.
- Chunked processing: read_csv(..., chunksize=...) or use an iterator to process a file in manageable parts and avoid loading entire file into memory.
- Compressed files & URLs: pandas can read compressed files (gzip, bz2, zip, xz) directly via compression parameter and can read from HTTP/HTTPS/ftp URLs.
- Binary formats: prefer parquet/feather when speed and disk size matter; they preserve dtypes and are columnar for analytic workloads.
- Database I/O: use read_sql_query/read_sql_table and df.to_sql with SQLAlchemy; use chunksize for bulk writes and control if_exists behaviour.
- Error handling: control bad lines with on_bad_lines, specify na_values to map missing value tokens, and use dtype converters for robust parsing.
Typical advanced patterns (code examples):
# 1) Read CSV in chunks, process and append results
for chunk in pd.read_csv('big.csv', chunksize=100000, usecols=['A','B','date'], parse_dates=['date'], dtype={'A': 'int32'}):
chunk['B'] = chunk['B'].str.lower()
# aggregate or write processed chunk to disk / database
# 2) Optimize dtypes after a quick read
df = pd.read_csv('data.csv', nrows=1000)
# infer and then set smaller dtypes
df['category_col'] = df['category_col'].astype('category')
# then read full file with known dtypes
full = pd.read_csv('data.csv', dtype={'category_col': 'category', 'id': 'int32'})
# 3) Write parquet with compression
df.to_parquet('out.parquet', compression='snappy', index=False)
# 4) Read from SQL with query
import sqlalchemy
engine = sqlalchemy.create_engine('sqlite:///mydb.sqlite')
df = pd.read_sql_query('SELECT * FROM sales WHERE date >= ?', engine, params=['2024-01-01'])
# 5) Write to SQL in chunks
df.to_sql('sales', engine, if_exists='append', index=False, chunksize=5000)
Best practices:
- Prefer binary columnar formats (Parquet/Feather) for repeated analytics work.
- Always specify dtype or use a small sample to infer dtypes and then enforce them when reading full data.
- Use chunksize/iterator for files that don’t fit into memory; process and persist intermediate results.
- Use compression for storage but test read/write times: compressed text can be slower to read but saves storage and I/O bandwidth.
- When writing to databases, use bulk insert (chunksize) and appropriate dtype mapping to reduce overhead.
Notes on reliability and portability:
- Be explicit about encoding (e.g., encoding='utf-8') when reading/writing text files.
- When exchanging data with other systems, include schema documentation (column names, dtypes, date formats) to avoid ambiguities.
- Use consistent NA tokens with na_values and keep reproducible reading code (avoid automatic type conversion surprises).
- Read a very large CSV in chunks and compute an aggregate without loading all data: for chunk in pd.read_csv('big.csv', chunksize=100000, usecols=['group','value']): result = chunk.groupby('group')['value'].sum() # accumulate result into a store or file
- Optimize memory by predefining dtypes: read a sample, choose compact dtypes (int32, float32, category) and then read full data with dtype={'id':'int32','category':'category'}
- Read a compressed file directly: df = pd.read_csv('data.csv.gz', compression='gzip') or pd.read_parquet('data.parquet.gzip')
- Write to Parquet with compression for faster reloads: df.to_parquet('data.parquet', compression='snappy', index=False)
- Load data from a URL: df = pd.read_csv('https://example.com/data.csv') — handle timeouts and network errors in production
- Export DataFrame to SQL in chunks: df.to_sql('table_name', engine, if_exists='append', index=False, chunksize=5000)
- \[pd.read_csv(filepath_or_buffer\]\[sep=','\]\[header='infer'\]\[names=None\]\[usecols=None\]\[dtype=None\]\[parse_dates=False\]\[date_parser=None\]\[chunksize=None\]\[iterator=False\]\[compression='infer'\]\[na_values=None\]\[encoding=None\]\[engine=None\]\[on_bad_lines='error'\]\[low_memory=True)\]
- \[DataFrame.to_csv(path_or_buf\]\[sep=','\]\[index=True\]\[index_label=None\]\[header=True\]\[na_rep=''\]\[compression=None\]\[columns=None\]\[mode='w'\]\[line_terminator='\n')\]
- \[pd.read_excel(io\]\[sheet_name=0\]\[header=0\]\[names=None\]\[usecols=None\]\[dtype=None\]\[parse_dates=False\]\[engine=None)\]
- \[pd.read_json(path_or_buf\]\[orient=None\]\[typ='frame'\]\[dtype=True\]\[convert_dates=True\]\[lines=False)\]
- \[pd.read_parquet(path\]\[engine='auto'\]\[columns=None\]\[use_nullable_dtypes=False)\]
- \[DataFrame.to_parquet(path\]\[engine='auto'\]\[compression='snappy'\]\[index=True)\]
Descriptive Statistics and Summary
Descriptive Statistics and Summary
Key Point: Mean (arithmetic mean): mean = x̄ = (Σ xi) / n
Descriptive statistics are numerical summaries that describe the main features of a dataset: measures of central tendency (where the data cluster), measures of dispersion (how spread out the data are), and measures of shape (symmetry and peakedness). In data analysis they give a quick overview before modelling or further processing.
In the context of Pandas (Class 12 Informatics Practices, Data Handling using Pandas II), descriptive statistics and summary operations are used to inspect DataFrame contents and produce concise summaries. Pandas provides built-in methods such as df.describe(), df.mean(), df.median(), df.mode(), df.std(), df.var(), df.count(), df.min(), df.max(), df.quantile(), df.value_counts() and df.corr() to compute these metrics quickly.
Typical steps when summarizing a dataset with Pandas:
- Use
df.info()anddf.head()to check types and sample rows. - Use
df.describe()to get count, mean, std, min, quartiles and max for numeric columns. For all columns usedf.describe(include='all'). - Check missing values with
df.isnull().sum()and unique counts withdf.nunique(). - Compute specific statistics as needed:
df['col'].median(),df['col'].mode(),df['col'].quantile(0.75), etc. - Use
df.corr()to see linear relationships between numeric variables.
Descriptive statistics help answer questions such as: what is the typical value, how much do values vary, are there outliers, and are two variables related? These answers guide data cleaning, visualization, and modelling choices.
- Students' marks dataset: use df.describe() to find average marks, standard deviation, min/max, and quartiles. Identify students below the first quartile for targeted support and detect possible outliers above Q3 + 1.5*IQR.
- Retail sales data: compute daily average sales with df.groupby('date')['sales'].mean(), check sales variance with df['sales'].var(), and use df['product_id'].value_counts() to find best-selling products.
- Customer ratings: analyze product_rating column with df['rating'].mode() and df['rating'].median() to report central tendency, then plot a histogram to inspect rating distribution and skewness.
- Sensor readings (IoT): use df.resample('H').mean() for hourly averages, df['temperature'].std() to monitor variability, and df['temperature'].quantile([0.25, 0.5, 0.75]) to examine spread and detect abnormal behavior.
- \[Mean (arithmetic mean): mean = x̄ = (Σ xi) / n\]
- \[Median: if n odd\]\[median = middle ordered value\]\[if n even\]\[median = average of the two middle ordered values\]
- \[Mode: the value(s) that appear most frequently in the dataset\]
- \[Population variance: σ² = (1/n) Σ (xi - μ)²\]
- \[Sample variance (used commonly in statistics and by some libraries): s² = (1/(n-1)) Σ (xi - x̄)²\]
- \[Standard deviation: σ = sqrt(variance)\]\[s = sqrt(s²)\]
Indexing and Selection
Indexing and Selection
Key Point: Label selection: df.loc[row_label, col_label]
What it is
Indexing and selection in pandas are the techniques used to access, slice and extract subsets of data from Series and DataFrame objects. Proper indexing lets you retrieve rows, columns, single values, ranges and hierarchical (MultiIndex) parts efficiently.
Fundamental concepts
- Index: labels for rows (and columns). Can be numbers, strings, datetimes, or a MultiIndex (hierarchical labels).
- Label-based vs position-based:
.locuses labels;.ilocuses integer positions. Regular bracket accessdf['col']selects column by label. - Scalar access:
.at(label) and.iat(position) for fast single-value access. - Boolean indexing: Use boolean masks (conditions) to select rows that satisfy criteria.
- Chained assignment warning: Avoid constructions like
df[df['x']>0]['y'] = 0. Usedf.loc[mask, 'y'] = 0to safely set values.
Common selection patterns
- Select a column:
df['col']ordf.col(note:df.colonly when name is a valid attribute). - Select multiple columns:
df[['col1', 'col2']]. - Select rows by label:
df.loc['row_label']or rangedf.loc['a':'d'](inclusive). - Select rows by position:
df.iloc[0:5](end exclusive). - Select rows & columns:
df.loc[row_label, col_label]ordf.iloc[row_pos, col_pos]. - Boolean mask:
mask = df['marks'] > 75; df[mask]. - Query string:
df.query('marks > 75 and class == "12A"')(readable for multiple conditions).
Index operations
df.set_index('col')— make a column the index.df.reset_index()— move index back to a column.df.reindex(new_index, fill_value=...)— conform to a new index, optionally filling missing entries.- MultiIndex selection: use tuples, slices or
.xsto extract cross-sections.
Performance tips
- Use
.loc/.ilocand.at/.iatfor faster and clearer access. - Avoid expensive Python loops; use vectorized boolean masks and built-in methods like
df.query,df.isin,df.between.
Example workflow summary
Typical sequence: set an index (e.g., date), slice a range with .loc, apply a boolean mask to filter, and modify safe via .loc. Use reset_index to return to default integer index when needed.
- Select students scoring above 75: mask = df['marks'] > 75; selected = df[mask]
- Select rows for roll numbers 10 to 20 by position: df.iloc[9:20]
- Select rows for dates between 2020-01-01 and 2020-03-31 (date index): df.loc['2020-01-01':'2020-03-31']
- Get single value (label): val = df.at['S003','marks'] ; (position) val = df.iat[2,3]
- Set index to 'student_id' and get row by id: df = df.set_index('student_id'); row = df.loc['ID_102']
- Boolean with multiple conditions: high = df[(df['marks']>80) & (df['attendance']>=90)]
- \[Label selection: df.loc[row_label\]\[col_label]\]
- \[Position selection: df.iloc[row_pos\]\[col_pos]\]
- \[Single value fast access: df.at[row_label\]\[col_label] and df.iat[row_pos\]\[col_pos]\]
- \[Boolean mask creation: mask = (df['col'] operator value) e.g.\]\[mask = df['marks'] > 75\]
- \[Filter with mask: df_filtered = df[mask] or df.loc[mask]\]
- \[Multiple conditions: df[(df['a'] > x) & (df['b'] == y)] (use & for AND, | for OR, ~ for NOT)\]
Key Concepts
- Series
- One-dimensional labeled array capable of holding any data type. It is like a column in a table with an index for labels.
- DataFrame
- Two-dimensional tabular data structure with labeled rows and columns; the primary data structure in pandas.
- index
- Labels for the rows of a Series or DataFrame that allow fast lookups, slicing and alignment.
- read_csv
- Function to read a CSV file into a DataFrame, with options to parse columns, set index, handle missing values, etc.
- to_csv
- Method to write a DataFrame to a CSV file, optionally excluding index or specifying columns and separators.
- loc
- Label-based indexer to select rows and columns by labels or boolean arrays; inclusive of end labels.
- iloc
- Integer position-based indexer to select rows and columns by integer indices (zero-based).
- merge
- Function to combine two DataFrames based on one or more keys (SQL-style joins: inner, left, right, outer).
- concat
- Function to concatenate DataFrames along rows or columns, useful for stacking datasets vertically or horizontally.
- join
- Convenience method to join columns of another DataFrame to the calling DataFrame using the index or a key column.
- groupby
- Split-apply-combine operation: group rows by one or more keys to perform aggregated computations on each group.
- aggregate (agg)
- Apply one or more aggregation functions (like sum, mean, count) to groups or entire DataFrame columns.
- pivot_table
- Create a spreadsheet-style pivot table that aggregates values with an index and optional columns and aggregation function.
- melt
- Unpivot a DataFrame from wide to long format, turning columns into rows for easier analysis.
- apply
- Apply a function along an axis (rows or columns) of a DataFrame or to elements of a Series for custom transformations.
- map
- Element-wise mapping for a Series using a dict or function, commonly used to replace or transform values.
- fillna
- Replace NA/NaN values with a specified value or method (ffill, bfill) to handle missing data.
- dropna
- Remove rows or columns that contain missing values based on specified criteria (any/all, axis).
- sort_values
- Sort a DataFrame by one or more column values, ascending or descending, optionally inplace.
- value_counts
- Return a Series containing counts of unique values in a Series, useful for frequency distribution.
Practice Questions
-
Differentiate between a Series and a DataFrame in Pandas. / पांडास में Series और DataFrame में अंतर बताइए।
Show answer
A Series is a one-dimensional labeled array (like a single column with an index), while a DataFrame is a two-dimensional labeled tabular structure with rows and columns. / Series एक-आयामी लेबल युक्त सरणी है (अनुक्रमणिका वाले एकल कॉलम जैसी), जबकि DataFrame पंक्तियों और कॉलम वाली द्वि-आयामी लेबल युक्त सारणी संरचना है।
-
What is the difference between pd.merge() with how='inner' and how='outer'? / how='inner' और how='outer' के साथ pd.merge() में क्या अंतर है?
Show answer
An inner join returns only rows with keys present in both DataFrames (intersection), while an outer join returns the union of keys, filling unmatched values with NaN. / inner join केवल उन पंक्तियों को लौटाता है जिनकी कुंजियाँ दोनों DataFrame में हों (प्रतिच्छेदन), जबकि outer join कुंजियों का संघ लौटाता है और बेमेल मानों को NaN से भरता है।
-
When should pivot_table be used instead of pivot? / pivot के बजाय pivot_table का उपयोग कब करना चाहिए?
Show answer
Use pivot_table when multiple values map to the same index/column pair, because it supports an aggregation function (aggfunc); pivot requires unique index/column/value triplets. / pivot_table का उपयोग तब करें जब एक ही index/column जोड़े पर कई मान मैप हों, क्योंकि यह aggregation फलन (aggfunc) का समर्थन करता है; pivot के लिए अद्वितीय index/column/value त्रिक आवश्यक हैं।
-
Write a Pandas statement to compute total and average Sales grouped by City using named aggregation. / नामित एकत्रीकरण का उपयोग करके City के अनुसार समूहित कुल और औसत Sales की गणना के लिए एक पांडास कथन लिखिए।
Show answer
df.groupby('City').agg(Total_Sales=('Sales','sum'), Avg_Sales=('Sales','mean')) — this applies the split-apply-combine pattern. / df.groupby('City').agg(Total_Sales=('Sales','sum'), Avg_Sales=('Sales','mean')) — यह split-apply-combine पैटर्न लागू करता है।
-
List three methods to handle missing values in a numeric column and state when each is appropriate. / संख्यात्मक कॉलम में लुप्त मानों को संभालने की तीन विधियाँ बताइए और प्रत्येक कब उपयुक्त है।
Show answer
dropna (when missingness is small), fillna with mean/median (mean for symmetric, median for skewed data), and interpolate (for time-series). / dropna (जब लुप्तता कम हो), mean/median के साथ fillna (सममित के लिए mean, विषम के लिए median), और interpolate (समय-श्रृंखला के लिए)।
-
What does df.resample('M').sum() do, and what must the DataFrame have for it to work? / df.resample('M').sum() क्या करता है, और इसके कार्य करने के लिए DataFrame में क्या होना चाहिए?
Show answer
It re-samples a time series to monthly frequency and sums values within each month; the DataFrame must have a DatetimeIndex. / यह समय-श्रृंखला को मासिक आवृत्ति में पुनः नमूना लेता है और प्रत्येक माह के मानों का योग करता है; DataFrame में DatetimeIndex होना आवश्यक है।
-
Differentiate between agg, transform and apply on a groupby object. / groupby ऑब्जेक्ट पर agg, transform और apply में अंतर बताइए।
Show answer
agg returns a reduced aggregated result per group; transform returns a result aligned to the original index (same size); apply runs a custom function returning complex results. / agg प्रत्येक समूह के लिए घटा हुआ एकत्रित परिणाम लौटाता है; transform मूल अनुक्रमणिका से संरेखित परिणाम (समान आकार) लौटाता है; apply जटिल परिणाम लौटाने वाला कस्टम फलन चलाता है।
-
How does .loc differ from .iloc in row/column selection? / पंक्ति/कॉलम चयन में .loc, .iloc से कैसे भिन्न है?
Show answer
.loc selects by label and is inclusive of the end label, while .iloc selects by integer position (zero-based) and is end-exclusive. / .loc लेबल द्वारा चयन करता है और अंतिम लेबल को सम्मिलित करता है, जबकि .iloc पूर्णांक स्थिति (शून्य-आधारित) द्वारा चयन करता है और अंत को छोड़ देता है।
Related Laws & Principles
Explore allFoundational laws & principles connected to this chapter — tap to open in the Laws Explorer.