This notebook covers basic DataFrame manipulation: how to access, modify, filter, and organize data within a DataFrame.
Topics covered:
- Column and row management (adding, removing, modifying)
- Accessing elements and slicing
- Filtering and sorting
- Basic aggregations on columns
For advanced transformations (groupby with multiple aggregations, element-wise operations like map/apply/transform, and reshaping with melt/pivot), see the DataFrame Transformations and Reshaping notebook.
import pandas as pd
df = pd.read_csv('data/grades.csv', index_col=0)
df
|
PIA |
SAA |
MIA |
SBD |
BDA |
| Alan Turing |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
| Claude Shannon |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
| John McCarthy |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
| Marvin Minsky |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
Column management
df.insert(0, 'DNI', ['11222333A', '22333222B', '33222333C', '44333222D']) # Insert DNI column at position 0
df.insert(1, 'Edad', [23, 23, 36, 36]) # Insert an Age column at position 1
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
# Or we can simply add a column at the end
df['Otro'] = [1,1,1,1] # If the column doesn't exist, it creates it; if it exists, it overwrites it
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
Otro |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
1 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
1 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
1 |
# Create a new column with normalized PIA grades
df['PIA_norm'] = (df['PIA'] - df['PIA'].min()) / (df['PIA'].max() - df['PIA'].min())
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
Otro |
PIA_norm |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
1 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
1 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
1 |
0.000000 |
df.drop('Otro', axis=1) # Returns a new DataFrame without the 'Otro' column, but doesn't modify the original DataFrame
#df # If we show the original DataFrame, we see that the 'Otro' column is still there
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
drop has the axis parameter which defaults to 0, indicating that rows will be deleted. If you want to delete columns, you must specify axis=1, or its equivalent axis='columns'.
df.drop('Otro', axis=1, inplace=True) # Deletes the 'Otro' column from the original DataFrame
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
[!IMPORTANT]
The inplace=True argument indicates that the modification is performed on the DataFrame itself, instead of returning a new DataFrame with the modification. It is a common practice in pandas (it is an optional parameter in most pandas functions), as it allows chaining operations on a DataFrame without needing to create intermediate variables.
You can also use the columns and index parameters to delete columns or rows, respectively.
df.drop(columns='Edad') # Returns another dataframe without 'Edad'
|
DNI |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
df.drop(columns=['PIA_norm', 'Edad']) # Returns another dataframe without 'PIA_norm' or 'Edad'
|
DNI |
PIA |
SAA |
MIA |
SBD |
BDA |
| Alan Turing |
11222333A |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
| Claude Shannon |
22333222B |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
| John McCarthy |
33222333C |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
| Marvin Minsky |
44333222D |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
Using assign for Adding Columns
The assign method provides a cleaner way to add computed columns, especially when method chaining. Unlike direct assignment (df['col'] = ...), assign returns a new DataFrame without modifying the original.
# assign returns a new DataFrame with the new column
df.assign(PIA_doubled=df['PIA'] * 2)
# Using lambda allows referencing columns created in the same assign call
df.assign(
average=lambda x: (x['PIA'] + x['SAA'] + x['MIA']) / 3,
passed=lambda x: x['average'] >= 5 # Uses the column just created above
)
Tip: assign is especially powerful when combined with groupby and transform for adding computed columns based on group statistics. See the DataFrame Transformations notebook for advanced examples.
Row management
# df.at['Alan Turing'] # Would error; at only works to access a specific cell
df.loc['Alan Turing'] # Retrieve a complete row as a Series
DNI 11222333A
Edad 23
PIA 7.0
SAA 9.0
MIA 7.0
SBD 10.0
BDA 9.9
PIA_norm 1.0
Name: Alan Turing, dtype: object
import numpy as np
df.loc['Arthur Samuel'] = ['55444333E', 25, 8, 8.0, 7.0, 9.0, 6.0, np.nan] # Add a new student
# Since the PIA_norm column requires the PIA column first, we cannot add the value of PIA_norm
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
The value of the "PIA_norm" column has to be calculated by processing the entire "PIA" column and then assigning the result to the "PIA_norm" column. That's why I chose to assign the constant np.nan (Not A Number) when adding a new row: to indicate that we don't have that information for now (we will need to recalculate the normalized values).
df.drop('Arthur Samuel') # Rows can be deleted like columns (in this case I don't use inplace=True because I don't want to modify the original DataFrame)
#df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
7.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
Accessing elements
print(df.at['Alan Turing','PIA']) # Access a specific cell more efficiently than loc
print(df.loc['Alan Turing','PIA'])
print(df.loc['Alan Turing'].at['PIA']) # loc returns a series with the row data, so we can use at to access a specific element of that series
print(df['PIA']['Alan Turing'])
print(df['PIA'].at['Alan Turing'])
print(df.iat[0,2]) # Access by integer position: first row, third column
print(df.iloc[0,2])
7.0
7.0
7.0
7.0
7.0
7.0
7.0
df.at['Alan Turing', 'PIA'] = 10 # Modify a value using the row index and column name
df.loc['Alan Turing', 'PIA'] = 10 # Equivalent to the above
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
6.5 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
df.iloc[1, 2] = 0 # Modify a grade using positions
df
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
0.0 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
dict_num_keys1 = {1: 'uno', 2: 'dos', 3: 'tres'}
dict_num_keys2 = {1: 'one', 2: 'two', 3: 'three'}
dict_num_keys3 = {1: 'asdasd', 2: 'asdasd', 3: 'asdasd'}
df_ints_as_keys = pd.DataFrame({1:dict_num_keys1, 2: dict_num_keys2, 3: dict_num_keys3})
print(df_ints_as_keys.iloc[1,1])
print(df_ints_as_keys.loc[1,1])
# .at is the fastest method for accessing a single scalar value using labels.
# Use it when you need to get or set a single value and performance is critical.
print(df.at['Alan Turing','PIA'])
# .loc is used for label-based selection. It can access a single value, a row, a column, or a slice.
# It is very flexible but slightly slower than .at for single-value access.
print(df.loc['Alan Turing','PIA'])
# This is an example of chained indexing. First, it selects the row as a Series with .loc,
# and then it accesses the value from the Series with .at.
# This is generally not recommended as it can be less efficient and lead to a SettingWithCopyWarning.
print(df.loc['Alan Turing'].at['PIA'])
# This is another form of chained indexing. It first selects the column as a Series,
# and then accesses the value by its index label.
# This is also discouraged in favor of .loc or .at for clarity and performance.
print(df['PIA']['Alan Turing'])
# Similar to the previous method, this selects the column first and then uses .at to get the value.
# While slightly more explicit, the direct access with df.at[...] or df.loc[...] is preferred.
print(df['PIA'].at['Alan Turing'])
Accessing parts of a DataFrame
df[['DNI']] # Returns a DataFrame with the DNI column
|
DNI |
| Alan Turing |
11222333A |
| Claude Shannon |
22333222B |
| John McCarthy |
33222333C |
| Marvin Minsky |
44333222D |
| Arthur Samuel |
55444333E |
df['DNI'] # Returns a series with the DNI column
Alan Turing 11222333A
Claude Shannon 22333222B
John McCarthy 33222333C
Marvin Minsky 44333222D
Arthur Samuel 55444333E
Name: DNI, dtype: object
df.DNI # Equivalent to the above
Alan Turing 11222333A
Claude Shannon 22333222B
John McCarthy 33222333C
Marvin Minsky 44333222D
Arthur Samuel 55444333E
Name: DNI, dtype: object
df[['DNI','PIA']] # Returns a DataFrame with the DNI and PIA columns
|
DNI |
PIA |
| Alan Turing |
11222333A |
10.0 |
| Claude Shannon |
22333222B |
0.0 |
| John McCarthy |
33222333C |
6.5 |
| Marvin Minsky |
44333222D |
5.7 |
| Arthur Samuel |
55444333E |
8.0 |
df.loc['Alan Turing', ['PIA','SAA','MIA']] # Access a row and several columns
PIA 10.0
SAA 9.0
MIA 7.0
Name: Alan Turing, dtype: object
Slicing
df.loc[:, 'PIA':'MIA'] # Returns a DataFrame of all rows with columns PIA, SAA and MIA
|
PIA |
SAA |
MIA |
| Alan Turing |
10.0 |
9.0 |
7.0 |
| Claude Shannon |
0.0 |
8.0 |
6.0 |
| John McCarthy |
6.5 |
8.5 |
6.0 |
| Marvin Minsky |
5.7 |
8.0 |
7.0 |
| Arthur Samuel |
8.0 |
8.0 |
7.0 |
df.iloc[:3, 0:3] # Returns a DataFrame of rows before row 3, with columns 0, 1 and 2
|
DNI |
Edad |
PIA |
| Alan Turing |
11222333A |
23 |
10.0 |
| Claude Shannon |
22333222B |
23 |
0.0 |
| John McCarthy |
33222333C |
36 |
6.5 |
df.loc[:'Marvin Minsky'] # Returns a DataFrame with rows up to and including Marvin Minsky
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Claude Shannon |
22333222B |
23 |
0.0 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
Creating a new DataFrame by applying filters
# Students who passed PIA and SAA
df[(df['PIA'] >= 5) & (df['SAA'] >= 5)]
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
df[(df['PIA'] >= 5) | (df['SAA'] >= 5)] [['PIA','SAA','MIA']] # Students who passed PIA or SAA with their grades in those modules and in MIA
|
PIA |
SAA |
MIA |
| Alan Turing |
10.0 |
9.0 |
7.0 |
| Claude Shannon |
0.0 |
8.0 |
6.0 |
| John McCarthy |
6.5 |
8.5 |
6.0 |
| Marvin Minsky |
5.7 |
8.0 |
7.0 |
| Arthur Samuel |
8.0 |
8.0 |
7.0 |
aprobados_pia_saa = df[(df['PIA'] >= 5) & (df['SAA'] >= 5)] # Students who passed PIA and SAA
aprobados_pia_saa.set_index('DNI', inplace=True) # Set DNI as index (the inplace=True argument modifies the original DataFrame instead of returning a new one)
aprobados_pia_saa[['PIA','SAA','MIA']] # Students who passed PIA and SAA, identified by DNI, with their grades in those modules and in MIA
|
PIA |
SAA |
MIA |
| DNI |
|
|
|
| 11222333A |
10.0 |
9.0 |
7.0 |
| 33222333C |
6.5 |
8.5 |
6.0 |
| 44333222D |
5.7 |
8.0 |
7.0 |
| 55444333E |
8.0 |
8.0 |
7.0 |
df[(df['PIA'] >= 5) | (df['SAA'] >= 5)].set_index('DNI')[['PIA','SAA','MIA']] # Same as above but in a single line
|
PIA |
SAA |
MIA |
| DNI |
|
|
|
| 11222333A |
10.0 |
9.0 |
7.0 |
| 22333222B |
0.0 |
8.0 |
6.0 |
| 33222333C |
6.5 |
8.5 |
6.0 |
| 44333222D |
5.7 |
8.0 |
7.0 |
| 55444333E |
8.0 |
8.0 |
7.0 |
Sorting
df.sort_values('PIA', ascending=False) # Sorts the DataFrame by the PIA column in descending order
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Claude Shannon |
22333222B |
23 |
0.0 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
df.sort_values(by=['PIA', 'SAA', 'MIA'], ascending=False) # Sorts the DataFrame by columns PIA, SAA and MIA (in that order of priority)
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
11222333A |
23 |
10.0 |
9.0 |
7.0 |
10.0 |
9.9 |
1.000000 |
| Arthur Samuel |
55444333E |
25 |
8.0 |
8.0 |
7.0 |
9.0 |
6.0 |
NaN |
| John McCarthy |
33222333C |
36 |
6.5 |
8.5 |
6.0 |
9.0 |
7.8 |
0.615385 |
| Marvin Minsky |
44333222D |
36 |
5.7 |
8.0 |
7.0 |
9.0 |
10.0 |
0.000000 |
| Claude Shannon |
22333222B |
23 |
0.0 |
8.0 |
6.0 |
9.0 |
6.9 |
0.615385 |
Basic Aggregations
Pandas provides simple methods for computing statistics over columns or rows.
df['PIA'].mean() # Mean of the PIA column
df.sum() # Sum of each column
DNI 11222333A22333222B33222333C44333222D55444333E
Edad 143
PIA 30.2
SAA 41.5
MIA 33.0
SBD 46.0
BDA 40.6
PIA_norm 2.230769
dtype: object
df.loc[:, ['PIA','SAA','MIA','BDA','SBD']].mean(axis='columns') # Average grade of each student in the five modules
# axis=1 indicates that the mean is calculated by rows
Alan Turing 9.18
Claude Shannon 5.98
John McCarthy 7.56
Marvin Minsky 7.94
Arthur Samuel 7.60
dtype: float64
df['SBD'].value_counts() # Returns the number of times each value appears in the SBD column
# four students got a 9 in SBD and one got a 10
SBD
9.0 4
10.0 1
Name: count, dtype: int64
df.isnull() # Returns a DataFrame with True in cells that contain NaN and False in those that don't
df.isna() # Equivalent to the above ("not available")
|
DNI |
Edad |
PIA |
SAA |
MIA |
SBD |
BDA |
PIA_norm |
| Alan Turing |
False |
False |
False |
False |
False |
False |
False |
False |
| Claude Shannon |
False |
False |
False |
False |
False |
False |
False |
False |
| John McCarthy |
False |
False |
False |
False |
False |
False |
False |
False |
| Marvin Minsky |
False |
False |
False |
False |
False |
False |
False |
False |
| Arthur Samuel |
False |
False |
False |
False |
False |
False |
False |
True |
df.isnull().sum() # Returns the number of null values in each column
DNI 0
Edad 0
PIA 0
SAA 0
MIA 0
SBD 0
BDA 0
PIA_norm 1
dtype: int64
For grouping operations (groupby, agg, group-wise transform and apply), element-wise transformations (map, apply, transform), and data reshaping (melt, pivot, pivot_table), see the DataFrame Transformations and Reshaping notebook.