Data 3-5 Reshaping Dataframes#

Reshaping: Group by / aggregations, Pivot, Melt

1import pandas as pd
2exams = pd.read_csv('https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/exam-scores/exam-scores.csv')
3exams.sample(10)
Class_Section Exam_Version Completion_Time Made_Own_Study_Guide Did_Exam_Prep Assignment Studied_In_Groups Student_Score Percentage Letter_Grade
56 M02 C 60 N N Y 16 53.30% D
45 M02 B 50 Y Y N 28 93.30% A-
27 M01 D 60 Y N Y 21 70.00% C+
58 M02 D 25 N N Y 23 76.70% B-
43 M02 B 45 N N Y 24 80.00% B
18 M01 C 50 Y Y Y 27 90.00% A-
41 M02 B 35 N N Y 21 70.00% C+
20 M01 C 60 ? ? ? 22 73.30% C+
30 M02 A 25 ? ? ? 17 56.70% D
61 M02 D 45 Y Y N 22 73.30% C+

Group By#

When you group, you must summarize. For example you might want to know:

  • Average exam score by section

  • Number of students who took each exam

  • average grade based on whether students studied in groups.

  • total completion time by letter grade

A Pandas group by operation takes 3 parts:

  1. The column or list of columns to group by df.groupby(by=col)

  2. The columns to display in aggregate and the operation used.

  • The aggregate operators sum, min, max, mean, std, quartile, count

1# Example: Total number of exams take by section and the average score in each section:
2exams_by_section = exams.groupby(by=['Class_Section']).agg({ 'Class_Section': 'count', 'Student_Score': 'mean' })
3exams_by_section
Class_Section Student_Score
Class_Section
M01 29 23.000000
M02 36 22.527778
1# It makes sense to rename columns to make the output more readable:
2exams_by_section = exams.groupby(by=['Class_Section']).agg({ 'Class_Section': 'count', 'Student_Score': 'mean' })
3exams_by_section = exams_by_section.rename(columns={'Class_Section': 'Exam_Count', 'Student_Score': 'Average_Score'})
4exams_by_section
Exam_Count Average_Score
Class_Section
M01 29 23.000000
M02 36 22.527778

The grouped columns end up in the index.#

You can use df.index to add them back into a column (if you need to)

1exam_by_section = exams.groupby(by=['Class_Section']).agg({ 'Class_Section': 'count', 'Student_Score': 'mean' })
2exam_by_section = exam_by_section.rename(columns={'Class_Section': 'Exam_Count', 'Student_Score': 'Average_Score'})
3
4# Add it back!
5exam_by_section['Class_Section'] = exams_by_section.index
6exam_by_section
Exam_Count Average_Score Class_Section
Class_Section
M01 29 23.000000 M01
M02 36 22.527778 M02

Challenge 3-5-1#

https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/exam-scores/exam-scores.csv

Create a streamlit to allow the user to select one of the following:

  • one of: Made_Own_Study_Guide, Did_Exam_Prep Assignment, Studied_In_Groups

  • after the selection is made display a dataframe that summarized the count of students and the average student score by the selection

Pivot and Melt#

Pivot and melt are inverse operations

  • df.pivot() makes “long” data “wide” moving rows into columns.

  • df.melt() makes “wide” data “long” moving columns into rows.

NOTES:

  • The functions only move data, they are unable to summarize it.

  • the intersection of row/column must contain a single value. Multiple values under the same row/column will fail.

To set this up this example from exams let’s create a dataframe that summarizes the data. We will add the index columns back to the dataframe for clarity. Please Note this is not something that needs to be done typically. I just want to re-use the dataset for this example.

 1# Get average scores by section and exam version:
 2avg_scores_by_section_and_version = exams.groupby(by=['Class_Section', 'Exam_Version']).agg({'Student_Score': 'mean'})
 3
 4# add section and exam version back to dataframe
 5avg_scores_by_section_and_version['Class_Section'] = avg_scores_by_section_and_version.index.get_level_values('Class_Section')
 6avg_scores_by_section_and_version['Exam_Version'] = avg_scores_by_section_and_version.index.get_level_values('Exam_Version')
 7# reset the index
 8avg_scores_by_section_and_version = avg_scores_by_section_and_version.reset_index(drop=True)
 9#rename the Student_score to average score
10avg_scores_by_section_and_version =  avg_scores_by_section_and_version.rename(columns={'Student_Score': 'Average_Score'})
11#reorder the columns
12avg_scores_by_section_and_version = avg_scores_by_section_and_version[['Class_Section', 'Exam_Version', 'Average_Score']]
13
14#show
15avg_scores_by_section_and_version
Class_Section Exam_Version Average_Score
0 M01 A 25.428571
1 M01 B 23.571429
2 M01 C 23.714286
3 M01 D 19.750000
4 M02 A 22.400000
5 M02 B 23.222222
6 M02 C 21.777778
7 M02 D 22.750000

Pivot()#

Let’s pivot this data two different ways:

  • exam_version_in_col - a pivot where the exam version is in the column

  • class_section_in_col - a pivot where the class section is in the column

1exam_version_in_col = avg_scores_by_section_and_version.pivot(index='Class_Section', columns='Exam_Version', values='Average_Score')
2exam_version_in_col
Exam_Version A B C D
Class_Section
M01 25.428571 23.571429 23.714286 19.75
M02 22.400000 23.222222 21.777778 22.75
1class_section_in_col = avg_scores_by_section_and_version.pivot(index='Exam_Version', columns='Class_Section', values='Average_Score')
2class_section_in_col
Class_Section M01 M02
Exam_Version
A 25.428571 22.400000
B 23.571429 23.222222
C 23.714286 21.777778
D 19.750000 22.750000

Melt()#

We will now melt the data back into its original shape. Melt requires:

  • id_vars=list list of columns which remain in the melt

  • var_name=str column name of the columns to unpivot

  • value_name column name of the values to unpivot

  • For this example to work, the index values must be in a column, as there need to be id_vars

1# the value in this
2exam_version_in_col['Class_Section'] = exam_version_in_col.index
3melted1 = exam_version_in_col.melt(id_vars=["Class_Section"], var_name="Exam_Version", value_name='Average_Score')
4melted1
Class_Section Exam_Version Average_Score
0 M01 A 25.428571
1 M02 A 22.400000
2 M01 B 23.571429
3 M02 B 23.222222
4 M01 C 23.714286
5 M02 C 21.777778
6 M01 D 19.750000
7 M02 D 22.750000
1# melt the class_section_in_col
2class_section_in_col['Exam_Version'] = class_section_in_col.index
3melted2 = class_section_in_col.melt(id_vars=["Exam_Version"], var_name="Class_Section", value_name='Average_Score')
4melted2
Exam_Version Class_Section Average_Score
0 A M01 25.428571
1 B M01 23.571429
2 C M01 23.714286
3 D M01 19.750000
4 A M02 22.400000
5 B M02 23.222222
6 C M02 21.777778
7 D M02 22.750000

Pivot_table()#

The pd.pivot_table() function combines a groupby() with a pivot(). Its intended for when you need to pivot and aggregate in the pivot, avoiding a lot of extra code such as adding indexes as columns.

Here’s the examples above, but with a pivot_table on the original examsdata. We can skip the processing building avg_scores_by_section_and_version because pivot_table() allows us to summarize data.

1exam_version_in_col = exams.pivot_table(index='Class_Section', columns='Exam_Version', values='Student_Score', aggfunc='mean')
2exam_version_in_col
Exam_Version A B C D
Class_Section
M01 25.428571 23.571429 23.714286 19.75
M02 22.400000 23.222222 21.777778 22.75
1class_section_in_col = exams.pivot_table(index='Exam_Version', columns='Class_Section', values='Student_Score', aggfunc='mean')
2class_section_in_col
Class_Section M01 M02
Exam_Version
A 25.428571 22.400000
B 23.571429 23.222222
C 23.714286 21.777778
D 19.750000 22.750000

Challenge 3-5-2#

https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/exam-scores/exam-scores.csv

Let’s build an interactive pivot table in streamlit!

  • create a row and column selection widgets allowing the user to select one of the following columns:
    'Class_Section', 'Exam_Version', 'Made_Own_Study_Guide', 'Did_Exam_Prep Assignment', 'Studied_In_Groups','Letter_Grade'

  • create a measure column selestion widget which allows the user to select one of these columns:
    'Completion_Time','Student_Score'

  • build the pivot table dataframe from the inputs. use the average for the aggfunc

  • display the pivot table!

EXTRA CHALLENGE: Do not allow the name value in row and column!