Aimify IP Material
Class 12 · Chapter 3

Data Handling using Pandas - II

Partly in syllabusUnit 1: Data Handling using Pandas and Data Visualization (25 marks)~14 periods
22 MCQs37 written Qs10 practicals

Overview

This chapter moves from storing data in DataFrames to summarising it. Students compute statistics such as mean, median, mode and quartiles, sort and group rows to answer questions like "total spend per category", handle missing values, and move data between Pandas and a MySQL database.

The CBSE 2025-26 Unit 1 topic list stops at Series/DataFrame operations and CSV import/export, but the learning outcomes include importing/exporting data between MySQL and Pandas, and the suggested practicals need quantile() (75th percentile), group by and duplicate-row filtering. Other statistics, describe(), sorting, missing values, re-indexing and pivoting are marked Enrichment; they are useful in practicals and projects.

Learning outcomes

  • Students will be able to find the 75th percentile (and other quantiles) of a Series or column and filter the values above it.
  • Students will be able to group rows with groupby() and compute totals, averages and counts per group.
  • Students will be able to compute max, min, count, sum, mean, median, mode, std and var on a Series or DataFrame, along rows or columns.
  • Students will be able to read the summary produced by describe().
  • Students will be able to sort a DataFrame by one or more columns with sort_values() and by labels with sort_index().
  • Students will be able to detect, drop and fill missing values with isnull(), dropna() and fillna().
  • Students will be able to import a MySQL table into a DataFrame with read_sql_query() and export a DataFrame to MySQL with to_sql().

Lesson plan

Period 1
Why summarise data?

Give the class a printout of 40 students' marks and ask: who is the topper, what is the class average, how many scored above 80? Note how slow it is by hand. Introduce descriptive statistics and the syllabus scope (in syllabus vs Enrichment).

Periods 2-3
max, min, count, sum, mean, median, mode

On the board, work out mean, median and mode of seven marks by hand. In the lab, students check them with Pandas, then repeat on a DataFrame with axis=0 and axis=1. Discuss why mode() returns a Series and why numeric_only=True is needed when a text column is present.

Periods 4-5
Quantiles and the 75th percentile

Sort ten marks on the board, find the position (n - 1) × 0.75 and interpolate by hand. Students verify with quantile(0.75) and then do CBSE practical 2: print the elements above the 75th percentile.

Period 6
Variance, standard deviation and describe()

Compare two batsmen with the same average but different consistency; the smaller standard deviation is the steadier player. Students run describe() and label each row of its output on a worksheet.

Period 7
Sorting

Students produce a rank list with sort_values(by='Total', ascending=False), break ties with a second column, and show the top three with head(3).

Periods 8-9
Group by

Act it out: students stand in groups by house colour and each group reports its total points. Then do CBSE practical 3 (quarterly sales: total expenditure per category) and extend to agg(['sum', 'mean', 'count']).

Period 10
Missing values

Give a marks sheet where absentees are blank. Students count NaN per column, compare dropna() with fillna(0) and with filling the column mean, and discuss which is fair for a result sheet.

Period 11
Altering the index and pivoting (Enrichment)

Show set_index(), reset_index() after a filter, and a quick pivot_table() of sales by quarter and category.

Periods 12-13
Pandas and MySQL

Demonstrate pip install pymysql sqlalchemy, create a student table in MySQL, read it with read_sql_query(), add a computed column in Pandas and write it back with to_sql(if_exists='replace', index=False). Students check the new table at the mysql> prompt.

Period 14
Revision and output-prediction test

Quick quiz from the output questions in this chapter; students explain each answer to a partner. Assign the practicals for the practical file.

Notes

What this chapter adds: summarising data

In Pandas - I we stored data in Series and DataFrames. Now we summarise it: a few numbers or a short table that describe hundreds of rows. Most functions here work on both a Series and a DataFrame:

  • On a Series they return a single value.
  • On a DataFrame they work column by column by default (axis=0) and return a Series with one value per column. With axis=1 they work row by row (for example, each student's total).

Missing values (NaN) are skipped by these functions unless stated otherwise.

The examples below use this result DataFrame of six students:

Python
import pandas as pd
df = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'],
                   'Maths': [88, 67, 92, 75, 59, 81],
                   'Science': [79, 72, 95, 81, 64, 81],
                   'English': [91, 58, 84, 77, 70, 86]})
print(df)
Output
     Name  Maths  Science  English
0   Aditi     88       79       91
1  Bharat     67       72       58
2  Chirag     92       95       84
3  Devika     75       81       77
4   Eshan     59       64       70
5  Fatima     81       81       86

Quantiles and the 75th percentile (Practical list)

A quantile splits sorted data at a fraction. s.quantile(q) returns the value below which a fraction q of the data lies:

  • q=0.25: first quartile (25th percentile)
  • q=0.5: median (the default)
  • q=0.75: third quartile (75th percentile)

How Pandas finds it (linear interpolation): sort the values, compute the position (n - 1) × q counting from 0, and if it falls between two values, take the matching fraction of the gap. For 10 marks and q=0.75, the position is 9 × 0.75 = 6.75, so the answer is the 7th value plus 0.75 of the gap to the 8th.

A list of fractions returns several quantiles at once. On a DataFrame, pass numeric_only=True when a text column is present.

CBSE practical: print the elements above the 75th percentile with s[s > s.quantile(0.75)].

Python
import pandas as pd
marks = pd.Series([56, 78, 91, 45, 67, 88, 72, 95, 60, 83])
print(sorted(marks))
print(marks.quantile(0.75))
print(marks[marks > marks.quantile(0.75)])
print(marks.quantile([0.25, 0.5, 0.75]))
Output
[45, 56, 60, 67, 72, 78, 83, 88, 91, 95]
86.75
2    91
5    88
7    95
dtype: int64
0.25    61.75
0.50    75.00
0.75    86.75
dtype: float64
Teacher tip

Check by hand: sorted, positions 6 and 7 (0-based) hold 83 and 88, so the 75th percentile is 83 + 0.75 × (88 − 83) = 86.75. Board questions sometimes say "quartile"; Q1, Q2 and Q3 are the 25th, 50th and 75th percentiles.

Quantiles of a DataFrame

df.quantile(q, numeric_only=True) gives the quantile of every numeric column. With a single q the result is a Series named after q; with a list of fractions the result is a DataFrame with one row per fraction.

Why numeric_only=True? Pandas 1.x quietly skipped text columns (with a warning in 1.5), but Pandas 2.0 and later raise TypeError if a text column is included. Passing the parameter makes the same code work in every version.

Python
import pandas as pd
df = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'],
                   'Maths': [88, 67, 92, 75, 59, 81],
                   'Science': [79, 72, 95, 81, 64, 81]})
print(df.quantile(0.75, numeric_only=True))
print(df.quantile([0.25, 0.75], numeric_only=True))
print(df[df['Maths'] > df['Maths'].quantile(0.75)]['Name'].tolist())
Output
Maths      86.25
Science    81.00
Name: 0.75, dtype: float64
      Maths  Science
0.25  69.00    73.75
0.75  86.25    81.00
['Aditi', 'Chirag']

Grouping rows with groupby() (Practical list)

df.groupby('col') splits the rows into groups that share the same value in col, applies a function to each group, and combines the answers into a new Series or DataFrame (split, apply, combine). The group values become the index of the result, sorted.

  • df.groupby('Category')['Expenditure'].sum(): total per category (a Series).
  • .mean(), .max(), .min(), .count() work the same way; .size() counts rows in each group.
  • df.groupby('Category').get_group('Lab'): the rows of one group.
  • df.groupby(['Quarter', 'Category']): groups on two columns.

Always select the numeric column(s) before applying sum()/mean(), or pass numeric_only=True. In Pandas 2.0 and later, text columns otherwise cause errors (mean) or get joined end to end (sum); Pandas 1.x silently dropped them.

CBSE practical 3 uses a quarterly sales file sales.csv with columns Quarter, Category, Item and Expenditure.

Python
import pandas as pd
sales = pd.read_csv('sales.csv')
print(sales)
print()
print(sales.groupby('Category')['Expenditure'].sum())
print()
print(sales.groupby('Category').size())
print()
print(sales.groupby('Category').get_group('Lab'))
Output
  Quarter    Category            Item  Expenditure
0      Q1  Stationery       Registers        18500
1      Q1      Sports    Cricket kits        42000
2      Q1         Lab       Chemicals        26000
3      Q2  Stationery     Chalk boxes         6500
4      Q2         Lab       Glassware        31500
5      Q2      Sports       Footballs        12000
6      Q3  Stationery   Printer paper        22000
7      Q3      Sports  Badminton sets        15500
8      Q4         Lab     Microscopes        64000
9      Q4  Stationery       Registers        17000

Category
Lab           121500
Sports         69500
Stationery     64000
Name: Expenditure, dtype: int64

Category
Lab           3
Sports        3
Stationery    4
dtype: int64

  Quarter Category         Item  Expenditure
2      Q1      Lab    Chemicals        26000
4      Q2      Lab    Glassware        31500
8      Q4      Lab  Microscopes        64000
Teacher tip

Link it to SQL, which students meet in Unit 2: SELECT Category, SUM(Expenditure) FROM sales GROUP BY Category; does the same job as the first groupby line.

More with groupby(): several functions and two columns

  • .agg(['sum', 'mean', 'count']) applies several functions to each group, giving one column per function.
  • .agg({'Expenditure': 'sum', 'Item': 'count'}) applies a different function to each column.
  • Grouping on two columns gives a result with a two-level index. Call .reset_index() to turn the group labels back into ordinary columns, which is handy before exporting to CSV.
  • sort_values() after a groupby ranks the groups.
Python
import pandas as pd
sales = pd.read_csv('sales.csv')
print(sales.groupby('Category')['Expenditure'].agg(['sum', 'mean', 'count']))
print()
q = sales.groupby(['Quarter', 'Category'])['Expenditure'].sum()
print(q.head(4))
print()
print(q.reset_index().head(3))
Output
               sum          mean  count
Category                               
Lab         121500  40500.000000      3
Sports       69500  23166.666667      3
Stationery   64000  16000.000000      4

Quarter  Category  
Q1       Lab           26000
         Sports        42000
         Stationery    18500
Q2       Lab           31500
Name: Expenditure, dtype: int64

  Quarter    Category  Expenditure
0      Q1         Lab        26000
1      Q1      Sports        42000
2      Q1  Stationery        18500

Maximum, minimum, count and sum (Enrichment)

  • max() / min(): largest / smallest value. On text, the comparison is alphabetical.
  • count(): number of non-NaN values.
  • sum(): total. On a DataFrame with a text column, sum() joins the strings, so pass numeric_only=True (or select the numeric columns).
  • idxmax() / idxmin(): the label where the maximum / minimum occurs, useful for "who topped?".

axis=1 applies the function across each row.

Python
import pandas as pd
df = pd.DataFrame({'Maths': [88, 67, 92, 75, 59, 81],
                   'Science': [79, 72, 95, 81, 64, 81],
                   'English': [91, 58, 84, 77, 70, 86]},
                  index=['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'])
print(df.max())
print(df.min(axis=1))
print(df.sum(axis=1).idxmax(), df['English'].idxmin())
print(df['Maths'].count(), df.count().sum())
Output
Maths      92
Science    95
English    91
dtype: int64
Aditi     79
Bharat    58
Chirag    84
Devika    75
Eshan     59
Fatima    81
dtype: int64
Chirag Bharat
6 18

Mean, median and mode (Enrichment)

  • mean(): arithmetic average.
  • median(): middle value after sorting; for an even count, the average of the two middle values.
  • mode(): the most frequent value(s). It always returns a Series (or a DataFrame for a DataFrame), because data can have more than one mode. Use s.mode()[0] for the first mode.

On a DataFrame with text columns, write df.mean(numeric_only=True) and df.median(numeric_only=True). Without it, Pandas 2.0 and later raise TypeError.

Python
import pandas as pd
runs = pd.Series([45, 12, 78, 45, 102, 12, 33])
print(runs.mean(), runs.median())
print(runs.mode())
print(runs.mode()[0])
df = pd.DataFrame({'House': ['Red', 'Blue', 'Red', 'Green'], 'Points': [12, 9, 15, 9]})
print(df.mean(numeric_only=True))
print(df.mode())
Output
46.714285714285715 45.0
0    12
1    45
dtype: int64
12
Points    11.25
dtype: float64
  House  Points
0   Red       9
Teacher tip

The output 0 12 / 1 45 shows two modes, labelled 0 and 1. Students often expect a single number; ask them why Pandas cannot give one here.

Variance and standard deviation (Enrichment)

Variance (var()) measures how far values spread from the mean: the average of squared differences from the mean. Standard deviation (std()) is its square root, in the same units as the data.

Pandas divides by n - 1 (the sample formula, ddof=1). NumPy's np.std() divides by n by default, so the two can differ slightly; pass ddof=0 to std() to get NumPy's value.

A small standard deviation means consistent values. Two batters can have the same average and very different consistency: below, both average 40, but batter B's scores swing far more.

Python
import pandas as pd
batterA = pd.Series([40, 42, 38, 41, 39])
batterB = pd.Series([5, 90, 12, 85, 8])
print(batterA.mean(), batterB.mean())
print(batterA.var(), round(batterB.var(), 2))
print(round(batterA.std(), 2), round(batterB.std(), 2))
print(round(batterA.std(ddof=0), 2))
Output
40.0 40.0
2.5 1889.5
1.58 43.47
1.41

Summary in one call: describe() (Enrichment)

describe() returns count, mean, std, min, 25%, 50% (median), 75% and max for every numeric column. On a Series of text it gives count, unique, top (most frequent) and freq instead. describe(include='all') covers both kinds of column.

It is a quick first look at any new data file before deeper analysis.

Python
import pandas as pd
df = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'],
                   'Maths': [88, 67, 92, 75, 59, 81],
                   'Science': [79, 72, 95, 81, 64, 81]})
print(df.describe())
print(df['Name'].describe())
Output
           Maths    Science
count   6.000000   6.000000
mean   77.000000  78.666667
std    12.569805  10.366613
min    59.000000  64.000000
25%    69.000000  73.750000
50%    78.000000  80.000000
75%    86.250000  81.000000
max    92.000000  95.000000
count         6
unique        6
top       Aditi
freq          1
Name: Name, dtype: object

Statistics functions at a glance

All of these skip NaN by default. On a DataFrame they work column-wise unless axis=1 is given.

FunctionReturnsNote
max(), min()Largest / smallest valueidxmax()/idxmin() give the label
count()Number of non-NaN valuesCompare: size counts NaN too
sum()TotalUse numeric_only=True with text columns
mean()AverageUse numeric_only=True with text columns
median()Middle valueSame as quantile(0.5)
mode()Most frequent value(s)Always a Series/DataFrame; mode()[0] for one value
quantile(q)Value at fraction q0.25, 0.5, 0.75 are the quartiles
var()VarianceDivides by n - 1
std()Standard deviationSquare root of variance
describe()Table of the aboveNumeric columns by default

Sorting a DataFrame (Enrichment)

  • df.sort_values(by='col') sorts rows by a column, in ascending order by default.
  • ascending=False sorts in descending order (rank lists).
  • by=['col1', 'col2'] sorts by the first column and breaks ties with the second; ascending can then be a list, one entry per column.
  • df.sort_index() sorts by row labels; sort_index(axis=1) sorts the columns by name.
  • NaN values go to the end by default.

Sorting returns a new DataFrame, so assign the result (df = df.sort_values(...)) to keep it.

Python
import pandas as pd
df = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'],
                   'Section': ['A', 'B', 'A', 'C', 'B', 'C'],
                   'Total': [258, 197, 271, 233, 193, 248]})
print(df.sort_values(by='Total', ascending=False).head(3))
print()
print(df.sort_values(by=['Section', 'Total'], ascending=[True, False]))
print()
print(df.sort_index(ascending=False).head(2))
Output
     Name Section  Total
2  Chirag       A    271
0   Aditi       A    258
5  Fatima       C    248

     Name Section  Total
2  Chirag       A    271
0   Aditi       A    258
1  Bharat       B    197
4   Eshan       B    193
5  Fatima       C    248
3  Devika       C    233

     Name Section  Total
5  Fatima       C    248
4   Eshan       B    193

Missing values: detecting them (Enrichment)

Real data has gaps: an absent student, a sensor that failed. Pandas marks a missing value as NaN (np.nan); None in a numeric column also becomes NaN.

  • df.isnull() (same as isna()) gives True where a value is missing; notnull() is the opposite.
  • df.isnull().sum() counts missing values per column, and df.isnull().sum().sum() gives the total.
  • df.isnull().any() tells which columns have at least one missing value.
  • df[df['col'].isnull()] shows the rows missing that value.

NaN is not equal to anything, not even itself, so df['col'] == np.nan never finds missing values. Always use isnull().

Python
import pandas as pd
import numpy as np
ut = pd.DataFrame({'Maths': [18, np.nan, 22, 15],
                   'Science': [20, 17, np.nan, np.nan],
                   'English': [19, 21, 24, 16]},
                  index=['Aditi', 'Bharat', 'Chirag', 'Devika'])
print(ut.isnull())
print(ut.isnull().sum())
print(ut.isnull().sum().sum(), ut.isnull().any().tolist())
print(ut[ut['Science'].isnull()].index.tolist())
print(np.nan == np.nan)
Output
        Maths  Science  English
Aditi   False    False    False
Bharat   True    False    False
Chirag  False     True    False
Devika  False     True    False
Maths      1
Science    2
English    0
dtype: int64
3 [True, True, False]
['Chirag', 'Devika']
False

Missing values: dropping and filling (Enrichment)

Drop with dropna():

  • df.dropna() removes every row that has at least one NaN.
  • dropna(how='all') removes a row only if all its values are NaN.
  • dropna(axis=1) removes columns instead of rows.
  • dropna(thresh=n) keeps rows that have at least n non-NaN values.

Fill with fillna():

  • df.fillna(0): one value everywhere.
  • df.fillna({'Maths': 0, 'Science': 10}): a different value per column.
  • df.fillna(df.mean(numeric_only=True)): each column's NaN replaced by that column's mean (an estimate).

Both return new DataFrames, so assign the result. Dropping loses data; filling invents data. Choose with care and say which you did.

Python
import pandas as pd
import numpy as np
ut = pd.DataFrame({'Maths': [18, np.nan, 22, 15],
                   'Science': [20, 17, np.nan, np.nan],
                   'English': [19, 21, 24, 16]},
                  index=['Aditi', 'Bharat', 'Chirag', 'Devika'])
print(ut.dropna())
print(ut.dropna(axis=1))
print(ut.fillna(0))
print(ut.fillna(ut.mean()).round(1))
Output
       Maths  Science  English
Aditi   18.0     20.0       19
        English
Aditi        19
Bharat       21
Chirag       24
Devika       16
        Maths  Science  English
Aditi    18.0     20.0       19
Bharat    0.0     17.0       21
Chirag   22.0      0.0       24
Devika   15.0      0.0       16
        Maths  Science  English
Aditi    18.0     20.0       19
Bharat   18.3     17.0       21
Chirag   22.0     18.5       24
Devika   15.0     18.5       16
Teacher tip

fillna(method='ffill') from older books is deprecated in Pandas 2.1 and later. If you need forward fill, use df.ffill().

Altering the index: set_index() and reset_index() (Enrichment)

  • df.set_index('Name') makes a column the row index.
  • df.reset_index() moves the index back into a column and creates a fresh 0, 1, 2, ... index.
  • reset_index(drop=True) throws the old index away. This is useful after filtering or sorting, when the old labels are no longer continuous.

Both return new DataFrames.

Python
import pandas as pd
df = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika'],
                   'Section': ['A', 'B', 'A', 'C'], 'Maths': [88, 67, 92, 75]})
secA = df[df['Section'] == 'A']
print(secA)
print(secA.reset_index(drop=True))
print(df.set_index('Name').loc['Devika', 'Maths'])
Output
     Name Section  Maths
0   Aditi       A     88
2  Chirag       A     92
     Name Section  Maths
0   Aditi       A     88
1  Chirag       A     92
75

Reshaping with pivot_table() (Enrichment)

pd.pivot_table(df, index=..., columns=..., values=..., aggfunc=...) rearranges long data into a grid: the unique values of index become rows, those of columns become columns, and each cell holds aggfunc (for example 'sum') of values for that combination. Missing combinations are NaN, or the value given in fill_value.

df.pivot() does a similar job but fails if any combination appears twice, so pivot_table() is the safer choice.

Python
import pandas as pd
sales = pd.read_csv('sales.csv')
grid = pd.pivot_table(sales, index='Category', columns='Quarter',
                      values='Expenditure', aggfunc='sum', fill_value=0)
print(grid)
Output
Quarter        Q1     Q2     Q3     Q4
Category                              
Lab         26000  31500      0  64000
Sports      42000  12000  15500      0
Stationery  18500   6500  22000  17000

Connecting Pandas to MySQL

Data in schools and businesses usually lives in a database. Pandas can read a MySQL table into a DataFrame (import) and write a DataFrame back as a table (export). Two extra libraries are needed, installed once from the command prompt:

  • pip install pymysql : the driver that talks to MySQL
  • pip install sqlalchemy : provides create_engine()

create_engine() takes a connection string and returns an engine object that Pandas uses for every read or write:

'mysql+pymysql://username:password@host:port/database'

Here mysql+pymysql names the database and driver, host is usually localhost, and the default MySQL port is 3306.

Python
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
Teacher tip

Pandas 1.x does not work with SQLAlchemy 2.0. On older lab computers install pip install "sqlalchemy<2.0". Also check that the MySQL server is running and the database already exists; to_sql() creates tables but not databases.

The student table used in the MySQL examples

The Pandas examples that follow use a table student in the MySQL database school. Create it at the mysql> prompt with the SQL from Unit 2:

CREATE DATABASE school;

USE school;

CREATE TABLE student (RollNo INT PRIMARY KEY, Name VARCHAR(20), Section CHAR(3), Marks INT);

Insert the six rows, then check the table:

MySQL
SELECT * FROM student;
Output
+--------+--------+---------+-------+
| RollNo | Name   | Section | Marks |
+--------+--------+---------+-------+
|   1201 | Aditi  | 12A     |    88 |
|   1202 | Bharat | 12B     |    67 |
|   1203 | Chirag | 12A     |    92 |
|   1204 | Devika | 12C     |    75 |
|   1205 | Eshan  | 12B     |    59 |
|   1206 | Fatima | 12C     |    81 |
+--------+--------+---------+-------+
6 rows in set (0.00 sec)

Importing data from MySQL: read_sql_query(), read_sql_table(), read_sql()

Using the student table above, Pandas offers three functions, each returning a DataFrame:

  • pd.read_sql_query(query, engine): runs a SELECT query and loads its result.
  • pd.read_sql_table('table', engine): loads a whole table (needs SQLAlchemy).
  • pd.read_sql(query_or_table, engine): accepts either a query or a table name.

Select only what you need with a WHERE clause in the query, then analyse it in Pandas.

Python
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
df = pd.read_sql_query('SELECT * FROM student', engine)
print(df)
top = pd.read_sql_query('SELECT Name, Marks FROM student WHERE Marks > 80', engine)
print(top)
print(df.groupby('Section')['Marks'].mean())
Output
   RollNo    Name Section  Marks
0    1201   Aditi     12A     88
1    1202  Bharat     12B     67
2    1203  Chirag     12A     92
3    1204  Devika     12C     75
4    1205   Eshan     12B     59
5    1206  Fatima     12C     81
     Name  Marks
0   Aditi     88
1  Chirag     92
2  Fatima     81
Section
12A    90.0
12B    63.0
12C    78.0
Name: Marks, dtype: float64
Teacher tip

A MySQL DECIMAL column arrives in Pandas as Python Decimal objects with dtype object. Convert with df['Fee'] = df['Fee'].astype(float) before doing statistics. INT and FLOAT columns arrive as numbers.

Exporting data to MySQL: to_sql()

df.to_sql('table', engine, if_exists='fail', index=True) writes a DataFrame into a MySQL table.

  • if_exists='fail' (default): raise ValueError if the table already exists.
  • if_exists='replace': drop the old table and create it again from the DataFrame.
  • if_exists='append': add the rows to the existing table; the column names must match.
  • index=False: do not write the DataFrame's row labels as a column (usually what you want).

The table is created automatically with column types chosen from the dtypes.

Python
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
clubs = pd.DataFrame({'ClubId': [1, 2, 3],
                      'Club': ['Robotics', 'Eco', 'Quiz'],
                      'Members': [24, 31, 18]})
clubs.to_sql('club', engine, if_exists='replace', index=False)
more = pd.DataFrame({'ClubId': [4], 'Club': ['Music'], 'Members': [27]})
more.to_sql('club', engine, if_exists='append', index=False)
print(pd.read_sql_query('SELECT * FROM club', engine))
Output
   ClubId      Club  Members
0       1  Robotics       24
1       2       Eco       31
2       3      Quiz       18
3       4     Music       27

Key terms

Descriptive statistics
Numbers that summarise data, such as mean, median, mode, range and standard deviation.
Mean
Sum of the values divided by their number; mean().
Median
Middle value of sorted data (average of the two middle values for an even count); median().
Mode
Most frequent value; mode() returns all modes as a Series.
Quantile
Value below which a given fraction of the data lies; quantile(q).
Percentile
A quantile expressed out of 100; the 75th percentile is quantile(0.75).
Quartile
The 25th, 50th and 75th percentiles, which split data into four equal parts.
Variance
Average squared distance of values from the mean (Pandas uses n - 1); var().
Standard deviation
Square root of the variance, a measure of spread in the data's own units; std().
Aggregation
Reducing many values to one, e.g. sum, mean or count; agg() applies several at once.
GroupBy
Splitting rows into groups by a column's values, applying a function to each group and combining the results.
Missing value
A value that is not available, shown as NaN in Pandas.
Imputation
Replacing missing values with an estimate such as 0 or the column mean, using fillna().
Pivot table
A grid that summarises data with one column's values as rows, another's as columns and an aggregate in each cell.
Connection string
Text such as mysql+pymysql://user:pwd@localhost:3306/db that tells SQLAlchemy how to reach a database.
SQLAlchemy engine
The object returned by create_engine() that Pandas uses to read from and write to a database.
pymysql
A Python driver library that lets Python programs talk to a MySQL server.

MCQs

Tap an option to check it, or use Show answers above to reveal them all.

  1. s = pd.Series([10, 20, 30, 40, 50]). What is printed by print(s.quantile(0.75))?

    Answer

    (a) 40.0

    Position = (5 - 1) × 0.75 = 3, which is exactly the 4th value, 40.

  2. s = pd.Series([2, 4, 6, 8]). What is printed by print(s.quantile(0.5))?

    Answer

    (c) 5.0

    The 0.5 quantile is the median: the average of 4 and 6.

  3. Which statement prints the elements of Series s that are above the 75th percentile?

    Answer

    (b) print(s[s > s.quantile(0.75)])

    The comparison makes a Boolean Series and the outer brackets filter with it. Option 1 prints only True/False values; quantile(75) is invalid because q must be between 0 and 1.

  4. What does df.groupby('City')['Sales'].sum() return?

    Answer

    (c) A Series with the total sales for each city

    Rows are grouped by City, Sales is summed in each group, and the city names become the index of the resulting Series.

  5. Which method gives the number of rows in each group, including rows with NaN?

    Answer

    (d) size()

    size() counts rows; count() counts non-NaN values in each column.

  6. s = pd.Series([3, 7, 3, 9, 7, 1]). What is printed by print(s.mode().tolist())?

    Answer

    (a) [3, 7]

    3 and 7 each occur twice, so both are modes, in ascending order.

  7. s = pd.Series([12, 5, 9, 20]). What is printed by print(s.median())?

    Answer

    (c) 10.5

    Sorted: 5, 9, 12, 20. The median is the average of 9 and 12, i.e. 10.5.

  8. A DataFrame has a text column Name and numeric columns. Which call works in Pandas 1.x, 2.x and 3.x to get the mean of each numeric column?

    Answer

    (d) df.mean(numeric_only=True)

    numeric_only=True skips text columns. Without it, Pandas 2.0+ raises TypeError; there is no average() method.

  9. Which function returns count, mean, std, min, quartiles and max of numeric columns in one call?

    Answer

    (a) describe()

    describe() produces this summary table.

  10. What does df.sum(axis=1) compute?

    Answer

    (b) The total of each row

    axis=1 works across columns, giving one total per row (for example each student's total marks).

  11. s = pd.Series([4, None, 8, None, 6]). What is printed by print(s.count(), s.size)?

    Answer

    (c) 3 5

    count() ignores the two missing values; size counts all five elements.

  12. Which statement sorts DataFrame df in descending order of the column Marks?

    Answer

    (a) df.sort_values(by='Marks', ascending=False)

    sort_values sorts by column values; descending order is ascending=False.

  13. Which statement counts the missing values in each column of df?

    Answer

    (a) df.isnull().sum()

    isnull() gives True for NaN; summing True values counts them. isnull().count() counts all cells.

  14. What does df.dropna(how='all') do?

    Answer

    (d) Drops rows in which every value is NaN

    With how='all', a row is removed only when all of its values are missing.

  15. s = pd.Series([10, None, 30]). What is printed by print(s.fillna(s.mean()).tolist())?

    Answer

    (b) [10.0, 20.0, 30.0]

    The mean skips NaN: (10 + 30) / 2 = 20, which then fills the gap.

  16. Which library provides create_engine() for connecting Pandas to MySQL?

    Answer

    (b) sqlalchemy

    create_engine() comes from SQLAlchemy; pymysql is the driver it uses underneath.

  17. In create_engine('mysql+pymysql://root:abc@localhost:3306/shop'), what is shop?

    Answer

    (d) The database name

    The format is driver://user:password@host:port/database.

  18. Which call reads the result of an SQL SELECT statement into a DataFrame?

    Answer

    (a) pd.read_sql_query(query, engine)

    read_sql_query() runs the query through the engine and returns a DataFrame.

  19. df.to_sql('marks', engine, if_exists='append', index=False) is run when the table marks already exists. What happens?

    Answer

    (c) The rows of df are added to the table

    append inserts the rows into the existing table. fail raises ValueError and replace recreates the table.

  20. What is the default value of if_exists in to_sql()?

    Answer

    (d) 'fail'

    By default to_sql() refuses to touch an existing table and raises ValueError.

  21. df has columns Item and Price. What is printed by print(df.groupby('Item')['Price'].max().loc['Pen']) if the Pen rows have prices 10, 25 and 15?

    Answer

    (c) 25

    The largest Pen price is 25.

  22. Which statement is true about std() in Pandas?

    Answer

    (b) It divides by n - 1 by default

    Pandas uses the sample formula (ddof=1). The standard deviation is the square root of the variance.

Assertion–Reason

Choose: (A) Both A and R are true and R is the correct explanation of A. (B) Both A and R are true but R is not the correct explanation of A. (C) A is true but R is false. (D) A is false but R is true.
  1. Assertion (A): s.mode() can return more than one value.

    Reason (R): A data set can have two or more values that occur with the same highest frequency.

    Answer

    (A)

    Because several values can tie for most frequent, Pandas returns all of them as a Series.

  2. Assertion (A): df.mean() raises TypeError in Pandas 2.x when df contains a column of names.

    Reason (R): From Pandas 2.0, numeric_only defaults to False, so text columns are included in the calculation.

    Answer

    (A)

    Pandas tries to average the names and fails; R explains why. Passing numeric_only=True avoids it.

  3. Assertion (A): s.quantile(0.75) always returns one of the values present in s.

    Reason (R): By default Pandas uses linear interpolation between the two nearest values.

    Answer

    (D)

    A is false: interpolation can produce a value between two data points, e.g. 86.75. R is true.

  4. Assertion (A): df.count() may return a smaller number than len(df) for a column.

    Reason (R): count() does not count NaN values.

    Answer

    (A)

    Missing values are skipped by count() but are still rows, so the count can be smaller.

  5. Assertion (A): df.to_sql('result', engine) run twice raises an error the second time.

    Reason (R): to_sql() writes the DataFrame index as a column by default.

    Answer

    (B)

    Both are true. The error comes from the default if_exists='fail', not from the index, so R does not explain A.

Short answer

  1. 2 marks

    Write the output:

    s = pd.Series([15, 30, 45, 60, 75])
    print(s.quantile(0.25), s.quantile(0.75))
    print(s[s > s.quantile(0.75)])

    Answer

    With 5 values, positions for 0.25 and 0.75 are 1 and 3, so Q1 = 30 and Q3 = 60. Only 75 lies above 60.

    Text
    30.0 60.0
    4    75
    dtype: int64
  2. 2 marks

    Write the output:

    s = pd.Series([4, 9, 4, 7, 9, 4])
    print(s.mode())
    print(s.mean(), s.median())

    Answer

    4 occurs three times, so it is the only mode. Mean = 37 / 6. Sorted: 4, 4, 4, 7, 9, 9; median = (4 + 7) / 2.

    Text
    0    4
    dtype: int64
    6.166666666666667 5.5
  3. 2 marks

    Differentiate between count() and size with an example.

    Answer

    count() is a method that returns the number of non-NaN values; size is an attribute that returns the total number of elements including NaN. For a Series of 5, NaN, 7:

    Text
    2 3
  4. 2 marks

    Write the output:

    df = pd.DataFrame({'A': [1, 2, 3], 'B': [10, 20, 30]})
    print(df.sum())
    print(df.sum(axis=1).tolist())

    Answer

    By default sums are column-wise; axis=1 sums each row.

    Text
    A     6
    B    60
    dtype: int64
    [11, 22, 33]
  5. 2 marks

    A DataFrame sales has columns Region, Product and Amount. Write statements to display (a) total Amount per Region and (b) the number of sales per Product.

    Answer

    (a) print(sales.groupby('Region')['Amount'].sum())

    (b) print(sales.groupby('Product').size()) (or sales.groupby('Product')['Amount'].count())

  6. 2 marks

    Write the output:

    df = pd.DataFrame({'House': ['Red', 'Blue', 'Red', 'Blue', 'Red'], 'Pts': [5, 8, 3, 6, 4]})
    print(df.groupby('House')['Pts'].sum())

    Answer

    Blue = 8 + 6 and Red = 5 + 3 + 4. Group labels are sorted and become the index; the Series is named after the column.

    Text
    House
    Blue    14
    Red     12
    Name: Pts, dtype: int64
  7. 2 marks

    Write the statement to sort DataFrame emp by Dept in ascending order and, within each department, by Salary in descending order.

    Answer

    emp = emp.sort_values(by=['Dept', 'Salary'], ascending=[True, False])

  8. 2 marks

    What is the difference between dropna() and fillna()? Give one example of each.

    Answer

    dropna() removes rows (or columns with axis=1) that contain missing values, e.g. df = df.dropna().

    fillna() replaces missing values with a given value, e.g. df = df.fillna(0) or df['Marks'] = df['Marks'].fillna(df['Marks'].mean()).

  9. 2 marks

    Write the output:

    s = pd.Series([8, None, 4, None])
    print(s.isnull().sum())
    print(s.fillna(0).sum())

    Answer

    Two values are missing. After filling with 0 the sum is 12, shown as a float because the Series became float64 when None was converted to NaN.

    Text
    2
    12.0
  10. 2 marks

    Why does mode() return a Series and not a single value? How will you get one value?

    Answer

    Data can have more than one most-frequent value (for example 3 and 7 both appearing twice), so Pandas returns all modes as a Series sorted in ascending order. Use s.mode()[0] (or s.mode().iloc[0]) to take the first one.

  11. 2 marks

    Name the two libraries to be installed to connect Pandas with MySQL, and write the statement that creates the connection to database library on the local computer for user root with password book123.

    Answer

    pymysql (driver) and sqlalchemy (provides create_engine).

    engine = create_engine('mysql+pymysql://root:book123@localhost:3306/library')

  12. 2 marks

    Explain the three values of the if_exists parameter of to_sql().

    Answer
    • 'fail' (default): raise ValueError if the table already exists.
    • 'replace': drop the existing table and create a new one with the DataFrame's data.
    • 'append': insert the DataFrame's rows into the existing table.
  13. 2 marks

    Write the output:

    s = pd.Series([40, 42, 38, 40])
    print(s.var(), s.std())

    Answer

    Mean = 40. Squared differences: 0, 4, 4, 0, total 8. Pandas divides by n - 1 = 3, so variance = 8/3 ≈ 2.667, and std is its square root.

    Text
    2.6666666666666665 1.632993161855452

Long answer

  1. 3 marks

    Explain the split-apply-combine idea behind groupby(). For the DataFrame sales (columns Quarter, Category, Item, Expenditure), write statements to print (a) total expenditure per category, (b) the average expenditure per quarter, and (c) the category with the highest total.

    Answer

    Split the rows into groups by a column's values, apply a function (sum, mean, ...) to each group, and combine the answers into one result whose index is the group labels.

    Python
    import pandas as pd
    sales = pd.read_csv('sales.csv')
    cat = sales.groupby('Category')['Expenditure'].sum()
    print(cat)
    print(sales.groupby('Quarter')['Expenditure'].mean())
    print('Highest:', cat.idxmax())
    Output
    Category
    Lab           121500
    Sports         69500
    Stationery     64000
    Name: Expenditure, dtype: int64
    Quarter
    Q1    28833.333333
    Q2    16666.666667
    Q3    18750.000000
    Q4    40500.000000
    Name: Expenditure, dtype: float64
    Highest: Lab
  2. 5 marks

    Write a program to create a Series of 12 monthly electricity bills (in rupees) of a house and display: the mean, median, highest and lowest bill, the month of the highest bill, and the months whose bill is above the 75th percentile.

    Answer

    The months are used as the index, so idxmax() returns the month and the filter keeps month labels.

    Python
    import pandas as pd
    bills = pd.Series([1450, 1380, 1720, 2650, 3480, 3910, 3020, 2780, 2410, 1890, 1560, 1620],
                      index=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
    print('Mean:', round(bills.mean(), 2), 'Median:', bills.median())
    print('Highest:', bills.max(), 'in', bills.idxmax(), '| Lowest:', bills.min())
    q3 = bills.quantile(0.75)
    print('75th percentile:', q3)
    print(bills[bills > q3])
    Output
    Mean: 2322.5 Median: 2150.0
    Highest: 3910 in Jun | Lowest: 1380
    75th percentile: 2840.0
    May    3480
    Jun    3910
    Jul    3020
    dtype: int64
  3. 3 marks

    A unit-test sheet has missing marks for absent students. Explain three ways of handling missing values in Pandas with code, and state when each is suitable.

    Answer
    1. Drop the rows (dropna()): when only a few rows are incomplete and they can be left out of the analysis.
    2. Fill with a fixed value (fillna(0)): when missing truly means zero, e.g. an absent student scored nothing.
    3. Fill with an estimate (fillna(df.mean())): when the value exists but was not recorded and an approximate value is acceptable.
    Python
    import pandas as pd
    import numpy as np
    ut = pd.DataFrame({'Maths': [18, np.nan, 22, 15], 'Science': [20, 17, np.nan, 19]},
                      index=['Aditi', 'Bharat', 'Chirag', 'Devika'])
    print(ut.dropna())
    print(ut.fillna(0))
    print(ut.fillna(ut.mean()).round(2))
    Output
            Maths  Science
    Aditi    18.0     20.0
    Devika   15.0     19.0
            Maths  Science
    Aditi    18.0     20.0
    Bharat    0.0     17.0
    Chirag   22.0      0.0
    Devika   15.0     19.0
            Maths  Science
    Aditi   18.00    20.00
    Bharat  18.33    17.00
    Chirag  22.00    18.67
    Devika  15.00    19.00
  4. 5 marks

    Write a program that reads the student table (RollNo, Name, Section, Marks) of MySQL database school into a DataFrame, adds a column Grade ('A' for marks 80 and above, otherwise 'B'), and writes the result to a new MySQL table student_grade without the index. Explain each step.

    Answer
    1. create_engine() builds the connection from the connection string.
    2. read_sql_query() runs the SELECT and returns a DataFrame.
    3. A list comprehension over the Marks column builds the Grade column.
    4. to_sql() with if_exists='replace' creates (or recreates) the table; index=False keeps the 0, 1, 2 labels out of it.

    The final print reads the new table back to check it.

    Python
    import pandas as pd
    from sqlalchemy import create_engine
    engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
    df = pd.read_sql_query('SELECT * FROM student', engine)
    df['Grade'] = ['A' if m >= 80 else 'B' for m in df['Marks']]
    df.to_sql('student_grade', engine, if_exists='replace', index=False)
    print(pd.read_sql_query('SELECT * FROM student_grade', engine))
    Output
       RollNo    Name Section  Marks Grade
    0    1201   Aditi     12A     88     A
    1    1202  Bharat     12B     67     B
    2    1203  Chirag     12A     92     A
    3    1204  Devika     12C     75     B
    4    1205   Eshan     12B     59     B
    5    1206  Fatima     12C     81     A
  5. 3 marks

    Write the output of the following code:

    df = pd.DataFrame({'Name': ['Ira', 'Om', 'Pia', 'Raj'], 'Sec': ['A', 'B', 'A', 'B'], 'Marks': [72, 85, 90, 64]})
    print(df.sort_values(by='Marks', ascending=False).head(2))
    print(df.groupby('Sec')['Marks'].mean())
    print(df['Marks'].describe()[['min', 'max']])

    Answer

    Sorting keeps the original row labels (2 and 1). Section A's mean is (72 + 90) / 2 = 81 and B's is (85 + 64) / 2 = 74.5. describe() returns floats, so min and max print as 64.0 and 90.0.

    Text
      Name Sec  Marks
    2  Pia   A     90
    1   Om   B     85
    Sec
    A    81.0
    B    74.5
    Name: Marks, dtype: float64
    min    64.0
    max    90.0
    Name: Marks, dtype: float64
  6. 3 marks

    Compare mean, median and mode as measures of the "typical" value, using the daily wages (in rupees) of seven workers: 450, 480, 450, 500, 470, 450, 2500. Write code to compute all three and comment on which best represents the group.

    Answer

    One very high wage (2500) pulls the mean up to about 757, which no worker actually earns near. The median (470) and the mode (450) ignore the extreme value, so they describe a typical worker better. The median is usually the best single measure for data with outliers.

    Python
    import pandas as pd
    wages = pd.Series([450, 480, 450, 500, 470, 450, 2500])
    print(round(wages.mean(), 2))
    print(wages.median())
    print(wages.mode()[0])
    Output
    757.14
    470.0
    450
  7. 5 marks

    Write a program to create a DataFrame of six cricket players (Name, Team, Runs, Matches) using sample data. Display (a) the describe() summary, (b) players sorted by Runs (highest first), (c) team-wise total runs and number of players using agg(), (d) players whose Runs are above the 75th percentile.

    Answer

    describe() summarises the numeric columns Runs and Matches; sorting, agg() and a quantile filter answer the other parts.

    Python
    import pandas as pd
    ipl = pd.DataFrame({'Name': ['Kohli', 'Gill', 'Jaiswal', 'Rinku', 'Samson', 'Gaikwad'],
                        'Team': ['RCB', 'GT', 'RR', 'KKR', 'RR', 'CSK'],
                        'Runs': [741, 426, 435, 168, 531, 583],
                        'Matches': [15, 12, 16, 14, 16, 14]})
    print(ipl.describe())
    print(ipl.sort_values(by='Runs', ascending=False)[['Name', 'Runs']])
    print(ipl.groupby('Team')['Runs'].agg(['sum', 'count']))
    print(ipl[ipl['Runs'] > ipl['Runs'].quantile(0.75)]['Name'].tolist())
    Output
                 Runs    Matches
    count    6.000000   6.000000
    mean   480.666667  14.500000
    std    191.631591   1.516575
    min    168.000000  12.000000
    25%    428.250000  14.000000
    50%    483.000000  14.500000
    75%    570.000000  15.750000
    max    741.000000  16.000000
          Name  Runs
    0    Kohli   741
    5  Gaikwad   583
    4   Samson   531
    2  Jaiswal   435
    1     Gill   426
    3    Rinku   168
          sum  count
    Team            
    CSK   583      1
    GT    426      1
    KKR   168      1
    RCB   741      1
    RR    966      2
    ['Kohli', 'Gaikwad']

Case-based

Case study 1

The school canteen records every purchase in canteen.csv with columns Day, Item, Category (Snack/Drink/Meal) and Amount. The manager loads it with cn = pd.read_csv('canteen.csv') and wants a weekly report.

  1. 1 mark

    Write a statement to display the total Amount for each Category.

    Answer

    print(cn.groupby('Category')['Amount'].sum())

  2. 1 mark

    Write a statement to display the number of purchases made on each Day.

    Answer

    print(cn.groupby('Day').size())

  3. 1 mark

    Write a statement to display the five most expensive purchases.

    Answer

    print(cn.sort_values(by='Amount', ascending=False).head(5))

  4. 1 mark

    Write statements to find the 75th percentile of Amount and display only the purchases above it.

    Answer

    q3 = cn['Amount'].quantile(0.75)

    print(cn[cn['Amount'] > q3])

Case study 2

Ms. Rekha has the half-yearly marks of her class in a DataFrame:

hy = pd.DataFrame({'Maths': [78, None, 91, 66], 'Physics': [None, 58, 84, 70], 'Chem': [81, 62, 88, None]}, index=['Neel', 'Ojas', 'Prisha', 'Qadir'])

Blank cells are students who were absent.

  1. 1 mark

    Write the output of print(hy.isnull().sum()).

    Answer

    Each column has exactly one missing mark.

    Text
    Maths      1
    Physics    1
    Chem       1
    dtype: int64
  2. 1 mark

    Write the output of print(hy.dropna()).

    Answer

    Only Prisha has no missing mark. The values are floats because each column contained NaN.

    Text
            Maths  Physics  Chem
    Prisha   91.0     84.0  88.0
  3. 1 mark

    Write a statement to replace the missing marks with 0 and store the result back in hy.

    Answer

    hy = hy.fillna(0)

  4. 1 mark

    After filling with 0, write the output of print(hy.mean(axis=1)).

    Answer

    axis=1 averages each student's three marks; the 0s pull down the averages of the absent students.

    Text
    Neel      53.000000
    Ojas      40.000000
    Prisha    87.666667
    Qadir     45.333333
    dtype: float64
Case study 3

A bookshop in Kolkata keeps its stock in a MySQL database bookshop, table books (BookId, Title, Genre, Price, Qty). The owner's son, a Class XII student, wants to analyse it with Pandas. MySQL runs on the same computer; user root, password kol2025.

  1. 1 mark

    Write the import statements and the statement that creates the engine.

    Answer

    import pandas as pd

    from sqlalchemy import create_engine

    engine = create_engine('mysql+pymysql://root:kol2025@localhost:3306/bookshop')

  2. 1 mark

    Write a statement to load only the books of genre 'Fiction' into DataFrame fic.

    Answer

    fic = pd.read_sql_query("SELECT * FROM books WHERE Genre = 'Fiction'", engine)

  3. 1 mark

    Write a statement to load the full table and display the total quantity per Genre.

    Answer

    bk = pd.read_sql_table('books', engine) (or pd.read_sql('SELECT * FROM books', engine))

    print(bk.groupby('Genre')['Qty'].sum())

  4. 1 mark

    He adds a column Value = Price * Qty in Pandas. Write a statement to save bk as a new table stock_value, replacing it if it already exists and without the index.

    Answer

    bk.to_sql('stock_value', engine, if_exists='replace', index=False)

Practicals

1Elements above the 75th percentile

Given a Series of the runs scored by a batter in 12 innings, print the 75th percentile and all the elements that are above it. Also print how many innings these are. (CBSE suggested practical 2.)

Solution
Python
import pandas as pd
runs = pd.Series([34, 12, 87, 45, 102, 8, 66, 59, 23, 91, 40, 77],
                 index=['I' + str(i) for i in range(1, 13)])
q3 = runs.quantile(0.75)
print('75th percentile:', q3)
above = runs[runs > q3]
print(above)
print('Innings above 75th percentile:', above.count())
Output
75th percentile: 79.5
I3      87
I5     102
I10     91
dtype: int64
Innings above 75th percentile: 3
Viva questions (3)
  • What value of q gives the 75th percentile? 0.75.
  • What is another name for the 75th percentile? The third quartile (Q3).
  • Why can the 75th percentile be a decimal for whole-number data? Pandas interpolates between two sorted values.

2Quarterly sales: total expenditure per category

Create a DataFrame of quarterly sales where each row contains the item category, item name and expenditure (read sales.csv). Group the rows by category and print the total expenditure per category, highest first. (CBSE suggested practical 3.)

Solution
Python
import pandas as pd
sales = pd.read_csv('sales.csv')
print(sales[['Category', 'Item', 'Expenditure']])
total = sales.groupby('Category')['Expenditure'].sum()
print(total.sort_values(ascending=False))
Output
     Category            Item  Expenditure
0  Stationery       Registers        18500
1      Sports    Cricket kits        42000
2         Lab       Chemicals        26000
3  Stationery     Chalk boxes         6500
4         Lab       Glassware        31500
5      Sports       Footballs        12000
6  Stationery   Printer paper        22000
7      Sports  Badminton sets        15500
8         Lab     Microscopes        64000
9  Stationery       Registers        17000
Category
Lab           121500
Sports         69500
Stationery     64000
Name: Expenditure, dtype: int64
Viva questions (3)
  • What becomes the index of the result of groupby()? The group values (categories).
  • Why do we select the Expenditure column before sum()? To avoid summing text columns.
  • Which SQL clause does groupby() correspond to? GROUP BY.

3Group-wise summary with agg()

For the same quarterly sales data, print for each category: total, average and maximum expenditure and the number of items bought. Then print the total expenditure per quarter and category, with the group labels as ordinary columns.

Solution
Python
import pandas as pd
sales = pd.read_csv('sales.csv')
print(sales.groupby('Category')['Expenditure'].agg(['sum', 'mean', 'max', 'count']))
qc = sales.groupby(['Quarter', 'Category'])['Expenditure'].sum().reset_index()
print(qc)
Output
               sum          mean    max  count
Category                                      
Lab         121500  40500.000000  64000      3
Sports       69500  23166.666667  42000      3
Stationery   64000  16000.000000  22000      4
  Quarter    Category  Expenditure
0      Q1         Lab        26000
1      Q1      Sports        42000
2      Q1  Stationery        18500
3      Q2         Lab        31500
4      Q2      Sports        12000
5      Q2  Stationery         6500
6      Q3      Sports        15500
7      Q3  Stationery        22000
8      Q4         Lab        64000
9      Q4  Stationery        17000
Viva questions (3)
  • What does agg() take? A list (or dictionary) of functions to apply to each group.
  • What does reset_index() do after a groupby? Moves the group labels back into columns.
  • How many index levels does grouping on two columns give? Two.

4Exam result: attributes and statistics

Create a DataFrame for the examination result of six students (Name, Maths, Science, English). Display the row labels, column labels, data types and dimensions, and then the subject-wise highest, lowest, mean and median marks, each student's total, and the topper's name.

Solution
Python
import pandas as pd
res = pd.DataFrame({'Name': ['Aditi', 'Bharat', 'Chirag', 'Devika', 'Eshan', 'Fatima'],
                    'Maths': [88, 67, 92, 75, 59, 81],
                    'Science': [79, 72, 95, 81, 64, 81],
                    'English': [91, 58, 84, 77, 70, 86]})
print(res.index.tolist(), res.columns.tolist())
print(res.dtypes)
print('Shape:', res.shape)
marks = res[['Maths', 'Science', 'English']]
print(pd.DataFrame({'Max': marks.max(), 'Min': marks.min(),
                    'Mean': marks.mean().round(2), 'Median': marks.median()}))
res['Total'] = marks.sum(axis=1)
print(res[['Name', 'Total']])
print('Topper:', res.loc[res['Total'].idxmax(), 'Name'])
Output
[0, 1, 2, 3, 4, 5] ['Name', 'Maths', 'Science', 'English']
Name       object
Maths       int64
Science     int64
English     int64
dtype: object
Shape: (6, 4)
         Max  Min   Mean  Median
Maths     92   59  77.00    78.0
Science   95   64  78.67    80.0
English   91   58  77.67    80.5
     Name  Total
0   Aditi    258
1  Bharat    197
2  Chirag    271
3  Devika    233
4   Eshan    193
5  Fatima    248
Topper: Chirag
Viva questions (3)
  • What does idxmax() return? The label of the maximum value.
  • Why select marks before max()? To keep the Name column out of numeric statistics.
  • Which axis gives each student's total? axis=1.

5describe() and quartiles of a class

For the marks of 10 students in Maths and Science, display the describe() summary, the first and third quartiles of each subject, and the names of students above the 75th percentile in Maths.

Solution
Python
import pandas as pd
df = pd.DataFrame({'Name': ['Asha', 'Bilal', 'Charu', 'Dhruv', 'Ekta',
                            'Faiz', 'Gauri', 'Harsh', 'Indu', 'Jatin'],
                   'Maths': [56, 78, 91, 45, 67, 88, 72, 95, 60, 83],
                   'Science': [61, 74, 89, 52, 70, 81, 77, 92, 58, 85]})
print(df.describe())
print(df.quantile([0.25, 0.75], numeric_only=True))
print(df[df['Maths'] > df['Maths'].quantile(0.75)]['Name'].tolist())
Output
           Maths    Science
count  10.000000  10.000000
mean   73.500000  73.900000
std    16.446884  13.551958
min    45.000000  52.000000
25%    61.750000  63.250000
50%    75.000000  75.500000
75%    86.750000  84.000000
max    95.000000  92.000000
      Maths  Science
0.25  61.75    63.25
0.75  86.75    84.00
['Charu', 'Faiz', 'Harsh']
Viva questions (3)
  • Which row of describe() shows the median? The 50% row.
  • Why is numeric_only=True passed to quantile()? The Name column is text.
  • Does std in describe() divide by n or n - 1? n - 1.

6Sorting: rank list

Create a DataFrame of eight students with Name, Section and Percent. Display (a) a rank list in descending order of Percent with a Rank column 1, 2, 3, ..., (b) the top three students, and (c) students sorted by Section and, within a section, by Percent (highest first).

Solution
Python
import pandas as pd
df = pd.DataFrame({'Name': ['Kiran', 'Laila', 'Manav', 'Nidhi', 'Omar', 'Prachi', 'Rehan', 'Simran'],
                   'Section': ['B', 'A', 'A', 'C', 'B', 'C', 'A', 'B'],
                   'Percent': [81.5, 92.4, 67.0, 88.8, 74.2, 95.1, 79.9, 86.3]})
rank = df.sort_values(by='Percent', ascending=False).reset_index(drop=True)
rank.insert(0, 'Rank', range(1, len(rank) + 1))
print(rank)
print(rank.head(3))
print(df.sort_values(by=['Section', 'Percent'], ascending=[True, False]))
Output
   Rank    Name Section  Percent
0     1  Prachi       C     95.1
1     2   Laila       A     92.4
2     3   Nidhi       C     88.8
3     4  Simran       B     86.3
4     5   Kiran       B     81.5
5     6   Rehan       A     79.9
6     7    Omar       B     74.2
7     8   Manav       A     67.0
   Rank    Name Section  Percent
0     1  Prachi       C     95.1
1     2   Laila       A     92.4
2     3   Nidhi       C     88.8
     Name Section  Percent
1   Laila       A     92.4
6   Rehan       A     79.9
2   Manav       A     67.0
7  Simran       B     86.3
0   Kiran       B     81.5
4    Omar       B     74.2
5  Prachi       C     95.1
3   Nidhi       C     88.8
Viva questions (3)
  • Which parameter sorts in descending order? ascending=False.
  • Why is reset_index(drop=True) used? To number the sorted rows afresh and drop the old labels.
  • How do you sort by two columns? Pass a list to by.

7Handling missing values

A DataFrame stores the weekly attendance (days present out of 6) of five students for four weeks; NaN means the register page was missing. Display the number of missing entries per week and in total, the rows with any missing entry, the data with missing entries filled with each week's mean (rounded to 1 decimal place), and the data after dropping students with more than one missing week.

Solution
Python
import pandas as pd
import numpy as np
att = pd.DataFrame({'W1': [6, 5, np.nan, 6, 4],
                    'W2': [5, np.nan, np.nan, 6, 5],
                    'W3': [6, 6, 4, np.nan, 5],
                    'W4': [6, 5, 5, 6, np.nan]},
                   index=['Aryan', 'Bhavya', 'Charvi', 'Daksh', 'Eva'])
print(att.isnull().sum())
print('Total missing:', att.isnull().sum().sum())
print(att[att.isnull().any(axis=1)])
print(att.fillna(att.mean()).round(1))
print(att.dropna(thresh=3))
Output
W1    1
W2    2
W3    1
W4    1
dtype: int64
Total missing: 5
         W1   W2   W3   W4
Bhavya  5.0  NaN  6.0  5.0
Charvi  NaN  NaN  4.0  5.0
Daksh   6.0  6.0  NaN  6.0
Eva     4.0  5.0  5.0  NaN
         W1   W2   W3   W4
Aryan   6.0  5.0  6.0  6.0
Bhavya  5.0  5.3  6.0  5.0
Charvi  5.2  5.3  4.0  5.0
Daksh   6.0  6.0  5.2  6.0
Eva     4.0  5.0  5.0  5.5
         W1   W2   W3   W4
Aryan   6.0  5.0  6.0  6.0
Bhavya  5.0  NaN  6.0  5.0
Daksh   6.0  6.0  NaN  6.0
Eva     4.0  5.0  5.0  NaN
Viva questions (3)
  • What does isnull().any(axis=1) give? True for each row that has at least one NaN.
  • What does thresh=3 mean in dropna()? Keep rows with at least 3 non-NaN values.
  • Does mean() include NaN? No, it skips NaN.

8Filter duplicates, summarise and export to CSV

Read the quarterly sales file, remove duplicate items (same Category and Item, keeping the first entry), filter rows where Expenditure is above the overall mean, and export the category totals of the cleaned data to category_total.csv without the index. Read the file back to check it. (CBSE suggested practicals 5 and 6.)

Solution
Python
import pandas as pd
sales = pd.read_csv('sales.csv')
print(sales[sales.duplicated(subset=['Category', 'Item'])])
clean = sales.drop_duplicates(subset=['Category', 'Item'])
print(clean[clean['Expenditure'] > clean['Expenditure'].mean()])
tot = clean.groupby('Category')['Expenditure'].sum().reset_index()
tot.to_csv('category_total.csv', index=False)
print(pd.read_csv('category_total.csv'))
Output
  Quarter    Category       Item  Expenditure
9      Q4  Stationery  Registers        17000
  Quarter Category          Item  Expenditure
1      Q1   Sports  Cricket kits        42000
4      Q2      Lab     Glassware        31500
8      Q4      Lab   Microscopes        64000
     Category  Expenditure
0         Lab       121500
1      Sports        69500
2  Stationery        47000
Viva questions (3)
  • Which row was treated as a duplicate? The second Registers entry under Stationery.
  • What does subset do in duplicated()? Compares only the listed columns.
  • Why is reset_index() used before to_csv()? So Category is written as a normal column.

9Import a MySQL table into Pandas

The MySQL database school has a table student (RollNo, Name, Section, Marks). Read the whole table into a DataFrame, display it, display the section-wise highest and average marks, and read only the students with marks below 70 using an SQL query.

Solution
Python
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
df = pd.read_sql_query('SELECT * FROM student', engine)
print(df)
print(df.groupby('Section')['Marks'].agg(['max', 'mean']))
low = pd.read_sql_query('SELECT RollNo, Name, Marks FROM student WHERE Marks < 70', engine)
print(low)
Output
   RollNo    Name Section  Marks
0    1201   Aditi     12A     88
1    1202  Bharat     12B     67
2    1203  Chirag     12A     92
3    1204  Devika     12C     75
4    1205   Eshan     12B     59
5    1206  Fatima     12C     81
         max  mean
Section           
12A       92  90.0
12B       67  63.0
12C       81  78.0
   RollNo    Name  Marks
0    1202  Bharat     67
1    1205   Eshan     59
Viva questions (3)
  • Which two libraries must be installed? pymysql and sqlalchemy.
  • What does read_sql_query() return? A DataFrame.
  • What is the default port of MySQL? 3306.

10Export a DataFrame to MySQL

Create a DataFrame of school house points (House, Captain, Points). Write it to a MySQL table house_points (replace if it exists, no index). Then append one more house with if_exists='append' and read the table back sorted by Points.

Solution
Python
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:tiger@localhost:3306/school')
hp = pd.DataFrame({'House': ['Shivaji', 'Tagore', 'Raman'],
                   'Captain': ['Arnav', 'Mehak', 'Yuvraj'],
                   'Points': [412, 389, 455]})
hp.to_sql('house_points', engine, if_exists='replace', index=False)
extra = pd.DataFrame({'House': ['Kalam'], 'Captain': ['Zara'], 'Points': [430]})
extra.to_sql('house_points', engine, if_exists='append', index=False)
print(pd.read_sql_query('SELECT * FROM house_points ORDER BY Points DESC', engine))
Output
     House Captain  Points
0    Raman  Yuvraj     455
1    Kalam    Zara     430
2  Shivaji   Arnav     412
3   Tagore   Mehak     389
Viva questions (3)
  • What happens if if_exists is left out and the table exists? ValueError is raised.
  • Why pass index=False? So the DataFrame's row labels are not stored as an extra column.
  • Does to_sql() create the database? No, only the table; the database must already exist.

Common mistakes

  • Writing df.mean() on a DataFrame with a Name column. Pandas 2.0 and later raise TypeError; write df.mean(numeric_only=True) or select the numeric columns first.

  • Treating mode() as a single number. It returns a Series (there can be several modes); use s.mode()[0] when one value is needed.

  • Confusing count() with size/len(). count() skips NaN, so it can be smaller than the number of rows.

  • Expecting quantile(0.75) to be one of the data values. Pandas interpolates, so 86.75 is a valid answer for whole-number marks.

  • Writing sales.groupby('Category').sum() when the table has text columns. Select the column first: sales.groupby('Category')['Expenditure'].sum().

  • Searching for missing values with df['Marks'] == np.nan. NaN is never equal to anything; use df['Marks'].isnull().

  • Calling df.sort_values('Total') or df.fillna(0) and expecting df to change. These return new DataFrames; assign the result back.

  • Forgetting index=False in to_sql(), which adds an unwanted index column to the MySQL table, or running to_sql() twice with the default if_exists='fail' and getting ValueError.