Home / Computer Science / Class XII / Data Handling using Pandas - II
Data Handling using Pandas - II — CUET Computer Science hero
Class XII 💻 Computer Science Ch 16 of 20

Data Handling using Pandas - II

CUET unit: Data Handling using Pandas - II

📌 Snapshot

  • Pandas II made easy: statistics, sorting, groupby, pivot tables, missing values and MySQL links explained.
  • How max, min, sum, count, mean, median, mode, quantile, var and std behave on columns, rows and groups.
  • Why pivot fails on duplicate entries, how pivot_table fixes it, and how dropna and fillna change your answers.
  • The exact steps to move a table between MySQL and a DataFrame using pymysql, sqlalchemy, read_sql and to_sql.

📖 Detailed Notes

Written and reviewed by Rohit Singh, Computer Science & Commerce faculty at UniDrill (MCA, Osmania University · 6 years teaching). Based on NCERT Informatics Practices Class XII, Chapter 3.

Which student is improving? Five lines can tell you

Twelve rows of marks sit in front of you: four students, three unit tests, five subjects. Your eyes cannot say who improved, which test was hardest or who is the most consistent. Pandas can answer each of those in one line. This chapter is the toolkit for that: summarise the data, sort it, group it, reshape it, clean it, and move it in and out of MySQL.

Every example below uses one DataFrame called df, built from the unit test marks (maximum 25 in each subject).

Code
import pandas as pd
marksUT = {'Name': ['Raman','Raman','Raman','Zuhaire','Zuhaire','Zuhaire',
'Ashravy','Ashravy','Ashravy','Mishti','Mishti','Mishti'],
'UT': [1,2,3,1,2,3,1,2,3,1,2,3],
'Maths': [22,21,14,20,23,22,23,24,12,15,18,17],
'Science': [21,20,19,17,15,18,19,22,25,22,21,18],
'S.St': [18,17,15,22,21,19,20,24,19,25,25,20],
'Hindi': [20,22,24,24,25,23,15,17,21,22,24,25],
'Eng': [21,24,23,19,15,13,22,21,23,22,23,20]}
df = pd.DataFrame(marksUT)

The resulting DataFrame has row labels 0 to 11. The marks for each row, in the order Maths, Science, S.St, Hindi, Eng, are:

Row Name UT Marks
0 Raman 1 22, 21, 18, 20, 21
1 Raman 2 21, 20, 17, 22, 24
2 Raman 3 14, 19, 15, 24, 23
3 Zuhaire 1 20, 17, 22, 24, 19
4 Zuhaire 2 23, 15, 21, 25, 15
5 Zuhaire 3 22, 18, 19, 23, 13
6 Ashravy 1 23, 19, 20, 15, 22
7 Ashravy 2 24, 22, 24, 17, 21
8 Ashravy 3 12, 25, 19, 21, 23
9 Mishti 1 15, 22, 25, 22, 22
10 Mishti 2 18, 21, 25, 24, 23
11 Mishti 3 17, 18, 20, 25, 20

Descriptive statistics: one function per question

Descriptive statistics squeeze many numbers into a few that describe them. Each function below is called on a whole DataFrame (one answer per column) or on a single column.

Function What it returns Works on text columns? Example on the Maths column
max() Largest value Yes (alphabetical for text) 24
min() Smallest value Yes 12
sum() Total Yes, but joins text end to end 231
count() Number of non missing values Yes 12
mean() Average Numbers only 19.25
median() Middle value of the sorted data Numbers only 20.5
mode() Most frequent value, returned as a Series Yes Hindi: 24
quantile(q) Value below which a fraction q of the data lies (default 0.5) Numbers only q = 0.25 gives 16.5
var() Variance Numbers only 15.840909
std() Standard deviation, the square root of the variance Numbers only 3.980064
describe() count, mean, std, min, 25%, 50%, 75% and max in one shot Numbers only see below

Rows or columns? The axis rule. By default (axis=0) a statistic runs down the rows and gives one answer per column. With axis=1 it runs across the columns and gives one answer per row.

Call Result
df['Maths'].max() One number: the highest Maths mark
df.max(numeric_only=True) One maximum for every numeric column
df[['Maths','Science']].max(axis=1) One maximum for every row, taken across the two subjects

Worked example: median of Maths in UT1. Write the statements to find the median Maths mark in Unit Test 1.

Code
dfMathsUT1 = df[df.UT == 1]['Maths']
print(dfMathsUT1.median())
Step What we do Result
1 df.UT == 1 builds a True/False filter and selects the UT1 rows Rows 0, 3, 6, 9
2 ['Maths'] keeps only that column 22, 20, 23, 15
3 Sort the values 15, 20, 22, 23
4 Four values means two middle ones (20 and 22), so the median is their average 21.0
Output
21.0

The common wrong step is to take the 2nd or 3rd value of the unsorted list. Median always works on sorted data.

Worked example: totals for one student. Find Raman's total marks in each subject, and in each unit test.

Code
dfRaman = df[df['Name'] == 'Raman']
subjects = ['Maths','Science','S.St','Hindi','Eng']
print(dfRaman[subjects].sum())
print(dfRaman[subjects].sum(axis=1))

The first sum() runs down the three rows and gives one total per subject. The second, with axis=1, runs across the five subjects and gives one total per unit test.

Subject Value
Maths 57
Science 60
S.St 50
Hindi 66
Eng 68
UT row Value
0 102
1 104
2 95

Pause and predict: df['Hindi'].mode() is run on the full data. Three rows show 24, two rows show 25, and every other mark appears once or twice. What does it return?

Check your answer: A Series with index 0 and the value 24. mode() always returns a Series, because when two values tie for most frequent, both are returned. Here 24 appears three times, which beats every other mark, so there is a single row.

Worked example: variance by hand. Mishti's Maths marks are 15, 18 and 17. Find the variance.

Variance is the average of the squared distances from the mean. Pandas divides by $n-1$, not $n$:

$$s^2 = \dfrac{\sum (x_i - \bar{x})^2}{n-1}$$

Step What we do Result
1 Mean: $(15+18+17)/3$ $\bar{x} = 16.667$
2 Squared distances $2.778,\ 1.778,\ 0.111$
3 Add them $4.667$
4 Divide by $n-1 = 2$ $s^2 = 2.333$
5 Square root gives std() $1.5275$

The common wrong step is dividing by 3, which gives 1.556 and matches none of the options. The same figure, 2.333333, appears for Mishti in the group results later in this chapter.

Beyond the textbook. For the exam: df.max(), df.sum() and df.min() work on the whole DataFrame, including the Name column, and df.mean() is used on numeric columns. In reality: recent versions of Pandas raise a TypeError if you call mean(), median() or std() on a DataFrame that still contains a text column, so you write df.mean(numeric_only=True) or select the numeric columns first. Using numeric_only=True is safe in every version.

describe() produces the whole summary at once. For three subject columns:

Code
print(df[['Maths','Science','Hindi']].describe())
Statistic Maths Science Hindi
count 12.000000 12.00000 12.000000
mean 19.250000 19.75000 21.833333
std 3.980064 2.66714 3.157483
min 12.000000 15.00000 15.000000
25% 16.500000 18.00000 20.750000
50% 20.500000 19.50000 22.500000
75% 22.250000 21.25000 24.000000
max 24.000000 25.00000 25.000000

The 25%, 50% and 75% rows are the first, second and third quartiles, which are the same values quantile(.25), quantile(.5) (the median) and quantile(.75) return. You can ask for several at once with df.quantile([.25, .75]).

Aggregation: many values in, one value out

Aggregation turns an array of values into a single number. The aggregate functions are max, min, sum, count, std and var. The method aggregate() (short form agg()) lets you name them as strings and ask for several at once.

Code
print(df[['Maths','Science']].aggregate(['max','min']))
print(df[['Maths','Science']].aggregate('sum', axis=1).head(3))
Statistic Maths Science
max 24 25
min 12 15

With axis=1, aggregate('sum', axis=1) adds Maths and Science for every row. The first three rows give 43, 41 and 33.

Sorting a DataFrame

sort_values() rearranges the rows by the values in one or more columns. The syntax is DataFrame.sort_values(by, axis=0, ascending=True). The row labels travel with their rows, so after sorting the index is no longer 0, 1, 2 in order.

Worked example: sort with a tie-break. For Unit Test 3, sort the students by Science (low to high), and break ties using Hindi.

Code
dfUT3 = df[df.UT == 3]
print(dfUT3[['Name','Science','Hindi']].sort_values(by=['Science','Hindi']))
Row label Name Science Hindi
5 Zuhaire 18 23
11 Mishti 18 25
2 Raman 19 24
8 Ashravy 25 21

Zuhaire and Mishti both have 18 in Science. Python moves on to the second column in the list, Hindi, where 23 is smaller than 25, so Zuhaire comes first. For a descending sort add ascending=False, for example dfUT3.sort_values(by=['Eng'], ascending=False).

Pause and predict: In the output above, what is the row label of the last student, and why is it not 3?

Check your answer: It is 8 (Ashravy). Sorting moves whole rows, so each row keeps the label it had in df. Use reset_index(drop=True) if you want fresh labels.

groupby: split, apply, combine

groupby() is the Pandas version of SQL's GROUP BY. It works in three steps.

Step What happens In the unit test example
Split Rows are divided into groups by the key column(s) One group per student
Apply A function (sum, mean, max…) runs on each group Average the Maths marks
Combine The group answers are joined into a new DataFrame or Series One row per student
Code
g1 = df.groupby('Name')
print(g1.size())
print(g1.get_group('Raman')[['UT','Maths']])
Name Value
Ashravy 3
Mishti 3
Raman 3
Zuhaire 3
Row label UT Maths
0 1 22
1 2 21
2 3 14

Useful members of a groupby object are first() (first row of each group), size() (rows per group), groups (a dictionary of group name and its row labels) and get_group(name) (all the rows of one group). Group by more than one column with a list: df.groupby(['Name','UT']).

Worked example: average Maths in each unit test. Which test was the hardest for Maths?

Code
print(df.groupby('UT')['Maths'].aggregate('mean'))
UT Maths
1 20.00
2 21.50
3 16.25

UT3 has the lowest average, 16.25. You can check it by hand: the UT3 marks are 14, 22, 12 and 17, which add to 65, and $65/4 = 16.25$.

To get several statistics per group, pass a list. This is the statement for the mean, variance, standard deviation and median of Maths for each student:

Code
res = df.groupby('Name')['Maths'].agg(['mean','var','std','median'])
print(res)
Name mean var std median
Ashravy 19.666667 44.333333 6.658328 23.0
Mishti 16.666667 2.333333 1.527525 17.0
Raman 19.000000 19.000000 4.358899 21.0
Zuhaire 21.666667 2.333333 1.527525 22.0

Ashravy's variance (44.33) is by far the largest because the Maths marks swing between 24 and 12, while Mishti's and Zuhaire's are steady.

Beyond the textbook. For the exam: learn the three steps, split, apply, combine, and the members first, size, groups and get_group. In reality: df.groupby('Name')['Maths'].mean() does the same job as the SQL query SELECT AVG(Maths) FROM marks GROUP BY Name. The idea is identical, only the syntax differs, so what you learn in Querying and SQL Functions carries over. Note the spelling: in Python it is groupby, one lowercase word.

Altering the index

By default the row labels are 0, 1, 2… When you filter a DataFrame, the labels of the surviving rows stay as they were, so they are no longer continuous.

Code
dfUT1 = df[df.UT == 1][['Name','Maths']]
print(dfUT1)
Row label Name Maths
0 Raman 22
3 Zuhaire 20
6 Ashravy 23
9 Mishti 15

The labels are 0, 3, 6 and 9. These functions fix or change the index:

Function What it does
reset_index() Creates a fresh 0, 1, 2… index and keeps the old labels as a new column called index
reset_index(drop=True) Creates the fresh index and throws the old labels away
set_index('Name') Makes the Name column the row labels (the column leaves the data columns)
drop(columns=['index']) Removes a column, used to delete the leftover index column
Code
dfNew = dfUT1.reset_index(drop=True)
dfNew = dfNew.set_index('Name')
print(dfNew)
Name Maths
Raman 22
Zuhaire 20
Ashravy 23
Mishti 15

Reversing it with reset_index() moves Name back into a normal column and restores 0, 1, 2, 3.

Reshaping data: pivot and pivot_table

The shape of data is how it is laid out in rows and columns. A long list of (store, year, sales) rows is hard to compare. Reshaping turns it into a grid with one row per store and one column per year.

Code
data = {'Store': ['S1','S4','S3','S1','S2','S3','S1','S2','S3'],
'Year': [2016,2016,2016,2017,2017,2017,2018,2018,2018],
'Total_sales(Rs)': [12000,330000,420000,20000,10000,450000,30000,11000,89000]}
st = pd.DataFrame(data)
pivot1 = st.pivot(index='Store', columns='Year', values='Total_sales(Rs)')
print(pivot1)
Store / Year 2016 2017 2018
S1 12000.0 20000.0 30000.0
S2 NaN 10000.0 11000.0
S3 420000.0 450000.0 89000.0
S4 330000.0 NaN NaN

index names the column that becomes the row labels, columns the column whose values become the new headings, and values the column that fills the grid. Combinations that never occurred, such as S2 in 2016, appear as NaN. Questions are now one-liners: pivot1.loc['S1'].sum() gives 62000.0 for store S1 over all years, and pivot1.loc['S3'].max() gives 450000.0.

Worked example: why does pivot raise an error? In a stock table every pen colour appears twice:

Code
stock = pd.DataFrame({'Item': ['Pen','Pen','Pencil','Pencil','Pen','Pen'],
'Color': ['Red','Red','Black','Black','Blue','Blue'],
'Price(Rs)': [10,25,7,5,50,20],
'Units_in_stock': [50,10,47,34,55,14]})
stock.pivot(index='Item', columns='Color', values='Units_in_stock')
Step What we do Result
1 Pivot tries to put each (Item, Color) pair in one cell Pen and Red must fill one cell
2 But the pair Pen and Red occurs twice (stock 50 and 10) Two values, one cell
3 Pivot cannot choose, so it stops ValueError: Index contains duplicate entries, cannot reshape

pivot_table() solves this by aggregating the duplicates. Its syntax is pivot_table(data, values=None, index=None, columns=None, aggfunc='mean'), and the default aggregate is the mean.

Code
print(stock.pivot_table(index=['Item','Color']))
Item / Color Price(Rs) Units_in_stock
Pen / Blue 35.0 34.5
Pen / Red 17.5 30.0
Pencil / Black 6.0 40.5

The blue pen was priced at 50 and 20, so the table shows the mean, 35.0. You can choose another aggregate, such as aggfunc='sum', or several at once with a list like aggfunc=['sum','max','mean'], or a different function per column with a dictionary like aggfunc={'Price(Rs)': len, 'Units_in_stock': 'mean'}. The function len counts the rows behind each cell.

Feature pivot() pivot_table()
Duplicate entries for a cell Raises ValueError Aggregates them
Default aggregate None mean
Use it when Every index and column pair is unique Pairs repeat, or you want a summary
Missing combinations NaN NaN

Pause and predict: You call stock.pivot_table(index='Item', columns='Color', values='Units_in_stock'). What value appears for Pen and Blue?

Check your answer: 34.5, the mean of 55 and 14, because the default aggregate is the mean. Pencil and Blue is NaN, because no such row exists.

Handling missing values

A missing value is shown as NaN. It appears when data was not collected or the attribute does not apply. Missing values distort results, and there are two main strategies: drop the rows that have them, or fill (estimate) them.

Suppose a fourth test happened and Raman missed Maths, Science and English, and no retest is possible. The DataFrame below holds Raman's four rows, and only the UT4 row has NaN.

Code
import numpy as np
marks4 = pd.DataFrame({'Name': ['Raman']*4,
'UT': [1,2,3,4],
'Maths': [22,21,14,np.nan],
'Science': [21,20,19,np.nan],
'Hindi': [20,22,24,18],
'Eng': [21,24,23,np.nan]})
print(marks4.isnull().sum())
print(marks4.isnull().sum().sum())
Column Value
Name 0
UT 0
Maths 1
Science 1
Hindi 0
Eng 1

isnull() returns a True/False DataFrame, any() tells whether a column has at least one True, and sum() counts the Trues, since True counts as 1. The second line of the code adds the column counts and prints the total number of missing cells, 3.

Check Code Answer here
Is anything missing in each column? marks4.isnull().any() True for Maths, Science and Eng
How many missing in each column? marks4.isnull().sum() 1, 1 and 1
How many missing in the whole table? marks4.isnull().sum().sum() 3

Worked example: one student, three missing-value policies. Each teacher converts Raman's marks to a percentage out of the total marks of the tests held. The subjects have maximum 25 marks per test.

Teacher Policy Code idea Calculation Percentage
Maths Drop UT4 for Raman dropna() $\dfrac{57}{25 \times 3} \times 100$ 76.0
Science Treat the missing mark as zero fillna(0) $\dfrac{60}{25 \times 4} \times 100$ 60.0
English Repeat the previous test's mark fillna(method='pad') $\dfrac{21+24+23+23}{25 \times 4} \times 100$ 91.0
Code
r = marks4
print(r.dropna()['Maths'].sum() * 100 / (25 * len(r.dropna())))
print(r['Science'].fillna(0).sum() * 100 / (25 * len(r)))
print(r['Eng'].fillna(method='pad').sum() * 100 / (25 * len(r)))
Output
76.0
60.0
91.0

Same student, three different answers. Dropping reduces the number of rows, so the denominator falls from 100 to 75. Filling keeps four rows.

Common wrong step. If you do nothing and compute r['Maths'].sum() * 100 / (25 * 4), Pandas quietly skips the NaN while adding, and gives 57.0. That looks like a normal answer but it is the same as treating the missing test as zero. Missing data never raises an error, it just changes the result.

Function What it does Note
dropna() Drops every row that has at least one NaN The whole row goes, including the good values
fillna(value) Replaces NaN with the given value fillna(0) and fillna(1) are common
fillna(method='pad') Fills with the value before the gap Forward fill
fillna(method='bfill') Fills with the value after the gap Backward fill

Beyond the textbook. For the exam: write fillna(method='pad') and fillna(method='bfill'). In reality: newer Pandas versions deprecate the method= argument, and the same results come from ffill() (forward fill) and bfill(). Whichever you write, the idea is unchanged: any filled value is an estimate, so the results are close approximations and not the actual figures.

Import and export between Pandas and MySQL

Real data lives in files and databases, not in dictionaries you type. Pandas can read a MySQL table into a DataFrame (import) and write a DataFrame into a MySQL table (export). Two libraries make the connection possible.

Library Role Install with
pymysql The driver that speaks to MySQL pip install pymysql
sqlalchemy Provides create_engine(), which opens the connection pip install sqlalchemy

create_engine() takes a connection string. Its parts are the driver, the user name, the password, the host, the port and the database name:

Part Example
Driver mysql+pymysql
User name root
Password your MySQL password
Host and port localhost:3306 (3306 is the default port)
Database CARSHOWROOM

Importing. Three functions read a table into a DataFrame, each taking the connection (engine) as its second argument:

Function Reads
pandas.read_sql_query(query, sql_conn) The result of an SQL query
pandas.read_sql_table(table_name, sql_conn) A whole table
pandas.read_sql(sql, sql_conn) Either a query or a table
Code
import pandas as pd
import pymysql
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:yourpassword@localhost:3306/CARSHOWROOM')
df = pd.read_sql_query("SELECT CarId, CarName FROM INVENTORY WHERE Fueltype = 'CNG'", engine)
print(df)

If the INVENTORY table holds two CNG cars, E001 and E002, both of Car3, the output is:

Index CarId CarName
0 E001 Car3
1 E002 Car3

Exporting. DataFrame.to_sql(table, sql_conn, if_exists='fail', index=True) writes a DataFrame into a table. The argument if_exists decides what happens when the table already exists.

if_exists Behaviour
'fail' (default) Raises ValueError if the table already exists
'replace' Replaces the old table contents with the DataFrame
'append' Adds the rows to the existing table (column order must match)
Code
data = {'ShowRoomId': [1,2,3,4,5],
'Location': ['Delhi','Bangalore','Mumbai','Chandigarh','Kerala']}
df = pd.DataFrame(data)
df.to_sql('showroom_info', engine, if_exists='replace', index=False)

This creates the table showroom_info in the database. With index=True (the default) the DataFrame's row labels are also written as an extra column, so pass index=False when you only want the data.

Pause and predict: You run df.to_sql('showroom_info', engine) twice, with the same engine. What happens on the second run?

Check your answer: It raises a ValueError. The default is if_exists='fail', and the table was created by the first run. Pass 'append' to add rows or 'replace' to overwrite.

Exam traps

Trap Why it is tempting Truth
groupby is written GROUP BY or GroupBy SQL uses GROUP BY The Python method is groupby, one lowercase word
axis=0 returns row results "0" sounds like rows axis=0 (default) gives one answer per column, and axis=1 one answer per row
var() divides by n The school formula for a population does Pandas divides by $n-1$, so the answer is larger
mode() returns a number mean() and median() do It returns a Series, and more than one value on a tie
pivot() and pivot_table() are the same Both reshape pivot fails on duplicates, pivot_table aggregates them (default mean)
sum() of a column with NaN fails or gives NaN NaN usually spreads NaN is skipped, so the result silently ignores missing data
dropna() drops only the missing cell The cell is the problem It drops the whole row
to_sql() appends by default You usually want to add rows The default is fail, which errors if the table exists

60-second revision

Task Code Remember
Summary of numeric columns df.describe() 25%, 50%, 75% are the quartiles
Row-wise answer add axis=1 Default axis=0 is column-wise
Median, mode, variance median(), mode(), var() mode is a Series, var uses $n-1$
Several statistics together agg(['max','min']) Strings in a list
Sort sort_values(by=['A','B'], ascending=False) Row labels move with rows
Group and summarise groupby('key')['col'].mean() Split, apply, combine
Fresh index reset_index(drop=True) Without drop=True the old labels become a column
Row labels from a column set_index('Name')
Reshape pivot() or pivot_table() Duplicates need pivot_table
Find missing isnull().sum() Total: .sum().sum()
Handle missing dropna(), fillna(0), ffill() Filling is an estimate
Read from MySQL read_sql_query(query, engine) Needs pymysql and sqlalchemy
Write to MySQL to_sql('t', engine, if_exists='replace', index=False) Default if_exists is 'fail'

Where this chapter connects

Chapter Link
The Series and DataFrame basics used here Data Handling using Pandas I
Drawing charts from these results Plotting Data using Matplotlib
The SQL behind GROUP BY and the MySQL tables Querying and SQL Functions
Where the data comes from and how to read it Understanding Data

🎯 Practice MCQs

First 3 questions free · create a free account to unlock the rest — answers & explanations included, no payment needed

Q1. For a DataFrame df, the statement df.max(numeric_only=True) with no axis argument returns

▸ Show answer & explanation

Answer: D

The default axis is 0, so the function runs down the rows and gives one answer per column. One answer per row needs axis=1. A single overall number would need a further max() on the result. Pandas never returns the whole row from max().

Q2. Consider: (1) import pandas as pd (2) s = pd.Series([4, 8, 8, 10, 15]) (3) print(s.median(), s.mode()[0]). What is printed?

▸ Show answer & explanation

Answer: A

The five sorted values have 8 in the middle, so the median is 8.0 (a float). The value 8 occurs twice and is the mode, and mode() returns a Series whose first element is 8. The mean would be 9.0, which is why options C and D are tempting but wrong.

Q3. What does pd.Series([2, 4, 6]).var() return?

▸ Show answer & explanation

Answer: C

The mean is 4, and the squared distances 4, 0 and 4 add up to 8. Pandas divides by n minus 1, which is 2, giving 4.0. Dividing by n gives 2.6667 (option A), 2.0 is the standard deviation (the square root of 4.0), and 8.0 is the unscaled sum.

🔒 12 more practice MCQs

Create a free account to unlock every MCQ in this chapter — answers and explanations included. No payment needed.

Already registered? Just log in and they'll all appear here.

📊 Previous-Year Questions

Practise with real CUET Computer Science previous-year papers — every question solved, with the correct answer and a step-by-step explanation.

View solved CUET PYQ papers →

Ready to drill Computer Science?

Unlock all MCQs, chapter tests, mocks & PYQs for ₹199/year.

Get UniDrill Pro