Skip to content

Introduction to Pandas

Written by Luke Chang & Jin Cheong

Analyzing data requires being facile with manipulating and transforming datasets to be able to test specific hypotheses. Data come in all different types of flavors and there are many different tools in the Python ecosystem to work with pretty much any type of data you might encounter. For example, you might be interested in working with functional neuroimaging data that is four dimensional. Three dimensional matrices contain brain activations in space, and these data can change over time in the 4th dimension. This type of data is well suited for numpy and specialized brain imaging packages such as nilearn. The majority of data, however, is typically in some version of a two-dimensional observations by features format as might be seen in an excel spreadsheet, a SQL table, or in a comma delimited format (i.e., csv).

In Python, the Pandas library is a powerful tool to work with this type of data. This is a very large library with a tremendous amount of functionality. In this tutorial, we will cover the basics of how to load and manipulate data and will focus on common to data munging tasks.

For those interested in diving deeper into Pandas, there are many online resources. There is the Pandas online documention, stackoverflow, and medium blogposts. I highly recommend Jake Vanderplas's terrific Python Data Science Handbook. In addition, here is a brief video by Tal Yarkoni providing a useful introduction to pandas.

After the tutorial you will have the chance to apply the methods to a new set of data.

Pandas Objects

Pandas has several objects that are commonly used (i.e., Series, DataFrame, Index). At it's core, Pandas Objects are enhanced numpy arrays where columns and rows can have special names and there are lots of methods to operate on the data. See Jake Vanderplas's tutorial for a more in depth overview.

Series

A pandas Series is a one-dimensional array of indexed data.

import pandas as pd

data = pd.Series([1, 2, 3, 4, 5])
data
0
01
12
23
34
45

The indices can be integers like in the example above. Alternatively, the indices can be labels.

data_1 = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
data_1
0
a1
b2
c3

Also, Series can be easily created from dictionaries

data_2 = pd.Series({'A': 5, 'B': 3, 'C': 1})
data_2
0
A5
B3
C1

DataFrame

If a Series is a one-dimensional indexed array, the DataFrame is a two-dimensional indexed array. It can be thought of as a collection of Series objects, where each Series represents a column, or as an enhanced 2D numpy array.

In a DataFrame, the index refers to labels for each row, while columns describe each column.

First, let's create a DataFrame using random numbers generated from numpy.

import numpy as np
data_3 = pd.DataFrame(np.random.random((5, 3)))
data_3
012
00.6637760.3766180.397256
10.6172820.0125290.582649
20.2408060.7453540.555138
30.5071410.4169250.894476
40.1430190.8988910.612962

We could also initialize with column names

data_4 = pd.DataFrame(np.random.random((5, 3)), columns=['A', 'B', 'C'])
data_4
ABC
00.2754550.2542260.872723
10.7481980.6368700.171168
20.6889980.8106880.901967
30.9342230.2560540.121445
40.7154850.2894450.557504

Alternatively, we could create a DataFrame from multiple Series objects.

a = pd.Series([1, 2, 3, 4])
b = pd.Series(['a', 'b', 'c', 'd'])
data_5 = pd.DataFrame({'Numbers': a, 'Letters': b})
data_5
NumbersLetters
01a
12b
23c
34d

Or a python dictionary

data_6 = pd.DataFrame({'State': ['California', 'Colorado', 'New Hampshire'], 'Capital': ['Sacremento', 'Denver', 'Concord']})
data_6
StateCapital
0CaliforniaSacremento
1ColoradoDenver
2New HampshireConcord

Loading Data

Loading data is fairly straightfoward in Pandas. Type pd.read then press tab to see a list of functions that can load specific file formats such as: csv, excel, spss, and sql.

In this example, we will use pd.read_csv to load a .csv file into a dataframe. Note that read_csv() has many options that can be used to make sure you load the data correctly. You can explore the docstrings for a function to get more information about the inputs and general useage guidelines by running pd.read_csv?

To load a csv file we will need to specify either the relative or absolute path to the file.

The command pwd will print the path of the current working directory.

pwd
NameError: name 'pwd' is not defined

Traceback (most recent call last):
  File "", line 1, in <module>
    pwd
NameError: name 'pwd' is not defined. Did you mean: 'pd'? Or did you forget to import 'pwd'?

We will now load the Pandas has many ways to read data different data formats into a dataframe. Here we will use the pd.read_csv function.

df = pd.read_csv('https://raw.githubusercontent.com/ljchang/dartbrains/master/data/salary/salary.csv', sep = ',')

Ways to check the dataframe

There are many ways to examine your dataframe. One easy way is to just call the dataframe variable itself.

df
salarygenderdepartmyearsagepublications
0862850bio26.064.072
1771250bio28.058.043
2719220bio10.038.023
3704990bio16.046.064
4666240bio11.041.023
.....................
72536621neuro1.031.03
73571851stat9.039.07
74522541stat2.032.09
75618851math23.060.09
76495421math3.033.05

77 rows × 6 columns

However, often the dataframes can be large and we may be only interested in seeing the first few rows. df.head() is useful for this purpose.

df.head()
salarygenderdepartmyearsagepublications
0862850bio26.064.072
1771250bio28.058.043
2719220bio10.038.023
3704990bio16.046.064
4666240bio11.041.023

On the top row, you have column names, that can be called like a dictionary (a dataframe can be essentially thought of as a dictionary with column names as the keys). The left most column (0,1,2,3,4...) is called the index of the dataframe. The default index is sequential integers, but it can be set to anything as long as each row is unique (e.g., subject IDs)

print("Indexes")
print(df.index)
print("Columns")
print(df.columns)
print("Columns are like keys of a dictionary")
print(df.keys())
Indexes
RangeIndex(start=0, stop=77, step=1)
Columns
Index(['salary', 'gender', 'departm', 'years', 'age', 'publications'], dtype='str')
Columns are like keys of a dictionary
Index(['salary', 'gender', 'departm', 'years', 'age', 'publications'], dtype='str')

You can access the values of a column by calling it directly. Single bracket returns a Series and double bracket returns a dataframe.

Let's return the first 10 rows of salary.

df['salary'][:10]
salary
086285
177125
271922
370499
466624
564451
664366
759344
858560
958294

shape is another useful method for getting the dimensions of the matrix.

We will print the number of rows and columns in this data set using fstring formatting. First, you need to specify a string starting with 'f', like this f'anything'. It is easy to insert variables with curly brackets like this f'rows: {rows}'.

Here is more info about formatting text.

rows, cols = df.shape
print(f'There are {rows} rows and {cols} columns in this data set')
There are 77 rows and 6 columns in this data set

Describing the data

We can use the .describe() method to get a quick summary of the continuous values of the data frame. We will .transpose() the output to make it slightly easier to read.

df.describe().transpose()
countmeanstdmin25%50%75%max
salary77.067748.51948115100.58143544687.057185.062607.075382.0112800.0
gender77.00.1428570.3877830.00.00.00.02.0
years76.014.9736848.6177701.08.014.023.034.0
age76.045.4868429.00591431.038.044.053.065.0
publications77.021.83116915.2405303.09.019.033.072.0

We can also get quick summary of a pandas series, or specific column of a pandas dataframe.

df.departm.describe()
departm
count77
unique7
topbio
freq16

Sometimes, you will want to know how many data points are associated with a specific variable for categorical data. The value_counts method can be used for this goal.

For example, how many males and females are in this dataset?

df['gender'].value_counts()
count
gender
067
19
21

You can see that there are more than 2 genders specified in our data.

This is likely an error in the data collection process. It's always up to the data analyst to decide what to do in these cases. Because we don't know what the true value should have been, let's just remove the row from the dataframe by finding all rows that are not '2'.

df_1 = df.loc[df['gender'] != 2]
df_1['gender'].value_counts()
count
gender
067
19

Dealing with missing values

Data are always messy and often have lots of missing values. There are many different ways, in which missing data might present NaN, None, or NA, Sometimes researchers code missing values with specific numeric codes such as 999999. It is important to find these as they can screw up your analyses if they are hiding in your data.

If the missing values are using a standard pandas or numpy value such as NaN, None, or NA, we can identify where the missing values are as booleans using the isnull() method.

The isnull() method will return a dataframe with True/False values on whether a datapoint is null or not a number (nan).

df_1.isnull()
salarygenderdepartmyearsagepublications
0FalseFalseFalseFalseFalseFalse
1FalseFalseFalseFalseFalseFalse
2FalseFalseFalseFalseFalseFalse
3FalseFalseFalseFalseFalseFalse
4FalseFalseFalseFalseFalseFalse
.....................
72FalseFalseFalseFalseFalseFalse
73FalseFalseFalseFalseFalseFalse
74FalseFalseFalseFalseFalseFalse
75FalseFalseFalseFalseFalseFalse
76FalseFalseFalseFalseFalseFalse

76 rows × 6 columns

Suppose we wanted to count the number of missing values for each column in the dataset.

One thing that is nice about Python is that you can chain commands, which means that the output of one method can be the input into the next method. This allows us to write intuitive and concise code. Notice how we take the sum() of all of the null cases. We can chain the .null() and .sum() methods to see how many null values are added up in each column.

df_1.isnull().sum()
0
salary0
gender0
departm0
years1
age1
publications0

You can use the boolean indexing once again to see the datapoints that have missing values. We chained the method .any() which will check if there are any True values for a given axis. Axis=0 indicates rows, while Axis=1 indicates columns. So here we are creating a boolean index for row where any column has a missing value.

df_1[df_1.isnull().any(axis=1)]
salarygenderdepartmyearsagepublications
18647620chem25.0NaN29
241048280geolNaN50.044

You may look at where the values are not null. Note that indexes 18, and 24 are missing.

df_1[~df_1.isnull().any(axis=1)]
salarygenderdepartmyearsagepublications
0862850bio26.064.072
1771250bio28.058.043
2719220bio10.038.023
3704990bio16.046.064
4666240bio11.041.023
.....................
72536621neuro1.031.03
73571851stat9.039.07
74522541stat2.032.09
75618851math23.060.09
76495421math3.033.05

74 rows × 6 columns

There are different techniques for dealing with missing data. An easy one is to simply remove rows that have any missing values using the dropna() method.

df_1.dropna(inplace=True)

Now we can check to make sure the missing rows are removed. Let's also check the new dimensions of the dataframe.

rows_1, cols_1 = df_1.shape
print(f'There are {rows_1} rows and {cols_1} columns in this data set')
df_1.isnull().sum()
There are 74 rows and 6 columns in this data set
0
salary0
gender0
departm0
years0
age0
publications0

Create New Columns

You can create new columns to fit your needs. For instance you can set initialize a new column with zeros.

df_1['pubperyear'] = 0

Here we can create a new column pubperyear, which is the ratio of the number of papers published per year

df_1['pubperyear'] = df_1['publications'] / df_1['years']

Indexing and slicing Data

Indexing in Pandas can be tricky. There are many ways to index in pandas, for this tutorial we will focus on four: loc, iloc, boolean, and indexing numpy values. For a more in depth overview see Jake Vanderplas's tutorial](https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/03.02-Data-Indexing-and-Selection.ipynb), where he also covers more advanced topics, such as hierarchical indexing.

Indexing with Keys

First, we will cover indexing with keys using the .loc method. This method references the explicit index with a key name. It works for both index names and also column names. Note that often the keys for rows are integers by default.

In this example, we will return rows 10-20 on the salary column.

df_1.loc[10:20, 'salary']
salary
1056092
1154452
1254269
1355125
1497630
1582444
1676291
1775382
1962607
2060373

You can return multiple columns using a list.

df_1.loc[:10, ['salary', 'publications']]
salarypublications
08628572
17712543
27192223
37049964
46662423
.........
66436622
75934411
8585608
95829412
10560924

11 rows × 2 columns

Indexing with Integers

Next we wil try .iloc. This method references the implicit python index using integer indexing (starting from 0, exclusive of last number). You can think of this like row by column indexing using integers.

For example, let's grab the first 3 rows and columns.

df_1.iloc[0:3, 0:3]
salarygenderdepartm
0862850bio
1771250bio
2719220bio

Let's make a new data frame with just Males and another for just Females. Notice, how we added the .reset_index(drop=True) method? This is because assigning a new dataframe based on indexing another dataframe will retain the original index. We need to explicitly tell pandas to reset the index if we want it to start from zero.

male_df = df_1[df_1.gender == 0].reset_index(drop=True)
female_df = df_1[df_1.gender == 1].reset_index(drop=True)

Indexing with booleans

Boolean or logical indexing is useful if you need to sort the data based on some True or False value.

For instance, who are the people with salaries greater than 90K but lower than 100K ?

df_1[(df_1.salary > 90000) & (df_1.salary < 100000)]
salarygenderdepartmyearsagepublicationspubperyear
14976300chem34.064.0431.264706
30929510neuro11.041.0201.818182
54969360physics15.050.0171.133333

This also works with the .loc method, which is what you need to do if you want to return specific columns

df_1.loc[(df_1.salary > 90000) & (df_1.salary < 100000), ['salary', 'gender']]
salarygender
14976300
30929510
54969360

Numpy indexing

Finally, you can also return a numpy matrix from a pandas data frame by accessing the .values property. This returns a numpy array that can be indexed using numpy integer indexing and slicing.

As an example, let's grab the last 10 rows and the first 3 columns.

df_1.values[-10:, :3]
array([[53638, 0, 'math'],
       [59139, 1, 'bio'],
       [52968, 1, 'bio'],
       [55949, 1, 'chem'],
       [58893, 1, 'neuro'],
       [53662, 1, 'neuro'],
       [57185, 1, 'stat'],
       [52254, 1, 'stat'],
       [61885, 1, 'math'],
       [49542, 1, 'math']], dtype=object)

Renaming

Part of cleaning up the data is renaming with more sensible names. This is easy to do with Pandas.

Renaming Columns

We can rename columns with the .rename method by passing in a dictionary using the {'Old Name':'New Name'}. We either need to assigne the result to a new variable or add inplace=True.

df_1.rename({'departm': 'department', 'pubperyear': 'pub_per_year'}, axis=1, inplace=True)

Renaming Rows

Often we may want to change the coding scheme for a variable. For example, it is hard to remember what zeros and ones mean in the gender variable. We can make this easier by changing these with a dictionary {0:'male', 1:'female'} with the replace method. We can do this inplace=True or we can assign it to a new variable. As an example, we will assign this to a new variable to also retain the original lablels.

df_1['gender_name'] = df_1['gender'].replace({0: 'male', 1: 'female'})
df_1.head()
salarygenderdepartmentyearsagepublicationspub_per_yeargender_name
0862850bio26.064.0722.769231male
1771250bio28.058.0431.535714male
2719220bio10.038.0232.300000male
3704990bio16.046.0644.000000male
4666240bio11.041.0232.090909male

Operations

One of the really fun things about pandas once you get the hang of it is how easy it is to perform operations on the data. It is trivial to compute simple summaries of the data. We can also leverage the object-oriented nature of a pandas object, we can chain together multiple commands.

For example, let's grab the mean of a few columns.

df_1.loc[:, ['years', 'age', 'publications']].mean()
0
years14.972973
age45.567568
publications21.662162

We can also turn these values into a plot with the plot method, which we will cover in more detail in future tutorials.

# '%matplotlib inline' command supported automatically in marimo
df_1.loc[:, ['years', 'age', 'publications']].mean().plot(kind='bar')

Perhaps we want to see if there are any correlations in our dataset. We can do this with the .corr method. More recent versions of Pandas might produce an error if there are any columns containing string data. To avoid this issue set numeric_only=True.

df_1.corr(numeric_only=True)
salarygenderyearsagepublicationspub_per_year
salary1.000000-0.3000710.3032750.2755340.427426-0.016988
gender-0.3000711.000000-0.275468-0.277098-0.2494100.024210
years0.303275-0.2754681.0000000.9581810.323965-0.541125
age0.275534-0.2770980.9581811.0000000.328285-0.498825
publications0.427426-0.2494100.3239650.3282851.0000000.399865
pub_per_year-0.0169880.024210-0.541125-0.4988250.3998651.000000

Merging Data

Another common data manipulation goal is to merge datasets. There are multiple ways to do this in pandas, we will cover concatenation, append, and merge.

Concatenation

Concatenation describes the process of stacking dataframes together. Older versions of pandas also had an .append() method, which has been deprecated since pandas 1.4. The main thing to consider is to make sure that the shapes of the two dataframes are the same as well as the index labels. For example, if we wanted to vertically stack two dataframe, they need to have the same column names.

Remember that we previously created two separate dataframes for males and females? Let's put them back together using the pd.concat method. Note how the index of this output retains the old index.

combined_data = pd.concat([female_df, male_df], axis = 0)

We can reset the index using the reset_index method.

pd.concat([male_df, female_df], axis = 0).reset_index(drop=True)
salarygenderdepartmyearsagepublicationspubperyear
0862850bio26.064.0722.769231
1771250bio28.058.0431.535714
2719220bio10.038.0232.300000
3704990bio16.046.0644.000000
4666240bio11.041.0232.090909
........................
69536621neuro1.031.033.000000
70571851stat9.039.070.777778
71522541stat2.032.094.500000
72618851math23.060.090.391304
73495421math3.033.051.666667

74 rows × 7 columns

We can also concatenate columns in addition to rows. Make sure that the number of rows are the same in each dataframe. For this example, we will just create two new data frames with a subset of the columns and then combine them again.

df1 = df_1[['salary', 'gender']]
df2 = df_1[['age', 'publications']]
df3 = pd.concat([df1, df2], axis=1)
df3.head()
salarygenderagepublications
086285064.072
177125058.043
271922038.023
370499046.064
466624041.023

Merge

The most powerful method of merging data is using the pd.merge method. This allows you to merge datasets of different shapes and sizes on specific variables that match. This is very common when you need to merge multiple sql tables together for example.

In this example, we are creating two separate data frames that have different states and columns and will merge on the State column.

First, we will only retain rows where there is a match across dataframes, using how=inner. This is equivalent to an 'and' join in sql.

df1_1 = pd.DataFrame({'State': ['California', 'Colorado', 'New Hampshire'], 'Capital': ['Sacremento', 'Denver', 'Concord']})
df2_1 = pd.DataFrame({'State': ['California', 'New Hampshire', 'New York'], 'Population': ['39512223', '1359711', '19453561']})
df3_1 = pd.merge(left=df1_1, right=df2_1, on='State', how='inner')
df3_1
StateCapitalPopulation
0CaliforniaSacremento39512223
1New HampshireConcord1359711

Notice how there are only two rows in the merged dataframe.

We can also be more inclusive and match on State column, but retain all rows. This is equivalent to an 'or' join.

df3_2 = pd.merge(left=df1_1, right=df2_1, on='State', how='outer')
df3_2
StateCapitalPopulation
0CaliforniaSacremento39512223
1ColoradoDenverNaN
2New HampshireConcord1359711
3New YorkNaN19453561

This is a very handy way to merge data when you have lots of files with missing data. See Jake Vanderplas's tutorial for a more in depth overview.

Grouping

We've seen above that it is very easy to summarize data over columns using the builtin functions such as pd.mean(). Sometimes we are interested in summarizing data over different groups of rows. For example, what is the mean of participants in Condition A compared to Condition B? This is suprisingly easy to compute in pandas using the groupby operator, where we aggregate data using a specific operation over different labels. One useful way to conceptualize this is using the Split, Apply, Combine operation (similar to map-reduce).
This figure is taken from Jake Vanderplas's tutorial and highlights how input data can be split on some key and then an operation such as sum can be applied separately to each split. Finally, the results of the applied function for each key can be combined into a new data frame.

Groupby

In this example, we will use the groupby operator to split the data based on gender labels and separately calculate the mean for each group. Note that newer versions of pandas might throw an error if you try to perform a numeric computation such as .mean() on a dataframe containing columns of string data. Use the flag numeric_only=True to avoid this issue.

df_1.groupby('gender_name').mean(numeric_only=True)
salarygenderyearsagepublicationspub_per_year
gender_name
female55719.6666671.08.66666738.88888911.5555562.043170
male69108.4923080.015.84615446.49230823.0615381.924709

Other default aggregation methods include .count(), .mean(), .median(), .min(), .max(), .std(), .var(), and .sum()

Transform

While the split, apply, combine operation that we just demonstrated is extremely usefuly to quickly summarize data based on a grouping key, the resulting data frame is compressed to one row per grouping label.

Sometimes, we would like to perform an operation over groups, but retain the original data shape. One common example is standardizing data within a subject or grouping variable. Normally, you might think to loop over subject ids and separately z-score or center a variable and then recombine the subject data using a vertical concatenation operation.

The transform method in pandas can make this much easier and faster!

Suppose we want to compute the standardized salary separately for each department. We can standardize using a z-score which requires subtracting the departmental mean from each professor's salary in that department, and then dividing it by the departmental standard deviation.

We can do this by using the groupby(key) method chained with the .transform(function) method. It will group the dataframe by the key column, perform the "function" transformation of the data and return data in same format. We can then assign the results to a new column in the dataframe.

df_1['salary_dept_z'] = (df_1['salary'] - df_1['salary'].groupby(df_1['department']).transform('mean')) / df_1['salary'].groupby(df_1['department']).transform('std')
df_1.head()
salarygenderdepartmentyearsagepublicationspub_per_yeargender_namesalary_dept_z
0862850bio26.064.0722.769231male2.468065
1771250bio28.058.0431.535714male1.493198
2719220bio10.038.0232.300000male0.939461
3704990bio16.046.0644.000000male0.788016
4666240bio11.041.0232.090909male0.375613

This worked well, but is also pretty verbose. We can simplify the syntax a little bit more using a lambda function, where we can define the zscore function.

calc_zscore = lambda x: (x - x.mean()) / x.std()
df_1['salary_dept_z'] = df_1['salary'].groupby(df_1['department']).transform(calc_zscore)
df_1.head()
salarygenderdepartmentyearsagepublicationspub_per_yeargender_namesalary_dept_z
0862850bio26.064.0722.769231male2.468065
1771250bio28.058.0431.535714male1.493198
2719220bio10.038.0232.300000male0.939461
3704990bio16.046.0644.000000male0.788016
4666240bio11.041.0232.090909male0.375613

For a more in depth overview of data aggregation and grouping, check out Jake Vanderplas's tutorial

Reshaping Data

The last topic we will cover in this tutorial is reshaping data. Data is often in the form of observations by features, in which there is a single row for each independent observation of data and a separate column for each feature of the data. This is commonly referred to as as the wide format. However, when running regression or plotting in libraries such as seaborn, we often want our data in the long format, in which each grouping variable is specified in a separate column.

In this section we cover how to go from wide to long using the melt operation and from long to wide using the pivot function.

Melt

To melt a dataframe into the long format, we need to specify which variables are the id_vars, which ones should be combined into a value_var, and finally, what we should label the column name for the value_var, and also for the var_name. We will call the values 'Ratings' and variables 'Condition'.

First, we need to create a dataset to play with.

data_7 = pd.DataFrame(np.vstack([np.arange(1, 6), np.random.random((3, 5))]).T, columns=['ID', 'A', 'B', 'C'])
data_7
IDABC
01.00.9870740.0226150.391333
12.00.9132960.4423430.206280
23.00.3643690.6621870.146400
34.00.7365000.6325060.676784
45.00.1393080.9607520.155930

Now, let's melt the dataframe into the long format.

df_long = pd.melt(data_7, id_vars='ID', value_vars=['A', 'B', 'C'], var_name='Condition', value_name='Rating')
df_long
IDConditionRating
01.0A0.987074
12.0A0.913296
23.0A0.364369
34.0A0.736500
45.0A0.139308
............
101.0C0.391333
112.0C0.206280
123.0C0.146400
134.0C0.676784
145.0C0.155930

15 rows × 3 columns

Notice how the id variable is repeated for each condition?

Pivot

We can also go back to the wide data format from a long dataframe using pivot. We just need to specify the variable containing the labels which will become the columns and the values column that will be broken into separate columns.

df_wide = df_long.pivot(index='ID', columns='Condition', values='Rating')
df_wide
ConditionABC
ID
1.00.9870740.0226150.391333
2.00.9132960.4423430.206280
3.00.3643690.6621870.146400
4.00.7365000.6325060.676784
5.00.1393080.9607520.155930

Exercises ( Homework)

The following exercises uses the dataset "salary_exercise.csv" adapted from material available here

These are the salary data used in Weisberg's book, consisting of observations on six variables for 52 tenure-track professors in a small college. The variables are:

  • sx = Sex, coded 1 for female and 0 for male
  • rk = Rank, coded
  • 1 for assistant professor,
  • 2 for associate professor, and
  • 3 for full professor
  • yr = Number of years in current rank
  • dg = Highest degree, coded 1 if doctorate, 0 if masters
  • yd = Number of years since highest degree was earned
  • sl = Academic year salary, in dollars.

Reference: S. Weisberg (1985). Applied Linear Regression, Second Edition. New York: John Wiley and Sons. Page 194.

Exercise 1

Read the salary_exercise.csv into a dataframe, and change the column names to a more readable format such as sex, rank, yearsinrank, degree, yearssinceHD, and salary.

Clean the data by excluding rows with any missing value.

What are the overall mean, standard deviation, min, and maximum of professors' salary?

salary_file_url = 'https://raw.githubusercontent.com/ljchang/dartbrains/master/data/salary/salary_exercise.csv'

Exercise 2

Create two separate dataframes based on the type of degree. Now calculate the mean salary of the 5 oldest professors of each degree type.

Exercise 3

What is the correlation between the standardized salary across all ranks and the standardized salary within ranks?