Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Data cleaning and preparation

import pandas as pd

# import pandas_datareader.data as web
print(pd.__version__)

from numpy import nan as NA
import numpy as np
0.25.0

Data cleaning and preparation

  • Significant amount of time (80%) is spent on data prep (loading, cleaning, transform, rearrange)

Handling missing data

  • Missing data occurs commonly
  • pandas tries to make working with missing data as painless as possible
    • E.g., descr stats exclude missing data by default
data = pd.Series([1, NA, 3.5, NA, 7])

data
0 1.0 1 NaN 2 3.5 3 NaN 4 7.0 dtype: float64
data.isnull()
0 False 1 True 2 False 3 True 4 False dtype: bool
data.notnull()
0 True 1 False 2 True 3 False 4 True dtype: bool
# Equivalent to: data[data.notnull()]
data.dropna()
0 1.0 2 3.5 4 7.0 dtype: float64

Filling in missing data

pd.obj.fillna

# Create a random data frame with missing data.
df = pd.DataFrame(np.random.randn(7, 3))

df.iloc[:4, 1] = df.iloc[:2, 2] = NA

df
Loading...
df.fillna(0)
Loading...
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA

df
Loading...
# Fill nans using forward fill.
df.fillna(method="ffill")
Loading...
# Use forward fill but with a limit.
df.fillna(method="ffill", limit=2)
Loading...

Data transformation

Removing dups

pd.obj.duplicated()

data = pd.DataFrame(
    {"k1": ["one", "two"] * 3 + ["two"], "k2": [1, 1, 2, 3, 3, 4, 4]}
)

data
Loading...
# Return if a row has been observed in a previous row or not.
data.duplicated()
0 False 1 False 2 False 3 False 4 False 5 False 6 True dtype: bool

pd.drop_duplicates()

# Remove the rows where `duplicated` is True.
data.drop_duplicates()
Loading...
# By default the method considers all the columns.
# One can specify a subset of duplicates.

data.duplicated(["k1"])
0 False 1 False 2 True 3 True 4 True 5 True 6 True dtype: bool
data.duplicated(["k1"], keep="last")
0 True 1 True 2 True 3 True 4 False 5 True 6 False dtype: bool
print(data)

# By default drop_duplicates keeps the first obeserved value combination
print("\n", data.drop_duplicates(keep="first"))

# keep='last' return the last one
print("\n", data.drop_duplicates(keep="last"))
    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

     k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4

     k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
6  two   4

Transforming data using function or mapping

data = pd.DataFrame(
    {
        "food": [
            "bacon",
            "pulled pork",
            "bacon",
            "Pastrami",
            "corned beef",
            "Bacon",
            "pastrami",
            "honey ham",
            "nova lox",
        ],
        "ounces": [4, 3, 12, 6, 7.5, 8, 3, 5, 6],
    }
)
data
Loading...
# Suppose you want to add a column indicating the animal each food came from.

meat_to_animal = {
    "bacon": "pig",
    "pulled pork": "pig",
    "pastrami": "cow",
    "corned beef": "cow",
    "honey ham": "pig",
    "nova lox": "salmon",
}
data["food"].str
<pandas.core.strings.StringMethods at 0x110ed1898>
# Get the lower case of one column.
# ".str" and ".dt" allow to select a column the algos for a given type.
lowercased = data["food"].str.lower()

print(lowercased)

data["animal"] = lowercased.map(meat_to_animal)

data
0          bacon
1    pulled pork
2          bacon
3       pastrami
4    corned beef
5          bacon
6       pastrami
7      honey ham
8       nova lox
Name: food, dtype: object
Loading...
# Using a single function.

# map() is called on each element of the obj.
# MEM: It's like python map()
data["food"].map(lambda x: meat_to_animal[x.lower()])
0 pig 1 pig 2 pig 3 cow 4 cow 5 pig 6 cow 7 pig 8 salmon Name: food, dtype: object

Replacing values

  • fillna is a special case of a more general replacement.

pd.obj.replace()

data = pd.Series([1.0, -999.0, 2.0, -999.0, -1000.0, 3.0])
data
0 1.0 1 -999.0 2 2.0 3 -999.0 4 -1000.0 5 3.0 dtype: float64
# Replace -999 with nan.
data.replace(-999, np.nan)
0 1.0 1 NaN 2 2.0 3 NaN 4 -1000.0 5 3.0 dtype: float64
# Replacing multiple values.
data.replace({-999: np.nan, -1000: 0})
0 1.0 1 NaN 2 2.0 3 NaN 4 0.0 5 3.0 dtype: float64
# Also columns and index can be replaced using .replace()

Discretization and binning

pd.cut()

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

bins = [18, 25, 35, 60, 100]

# From "bins" create various categorical variables, then assign
# "ages" to the variables, returning a pd.Categorical array.
cats = pd.cut(ages, bins)

# pd.Categorical.
print(type(cats))
print("cats=\n", cats)
<class 'pandas.core.arrays.categorical.Categorical'>
cats=
 [(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
# Use int representation of the bins.
cats.codes
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
# Print the categorical values.
# They are upper close (a, b].
cats.categories
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], closed='right', dtype='interval[int64]')
pd.value_counts(cats)
(18, 25] 5 (35, 60] 3 (25, 35] 3 (60, 100] 1 dtype: int64
# Using lower close intervals.
cats = pd.cut(ages, bins, right=False)

cats
[[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)] Length: 12 Categories (4, interval[int64]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]
# Using custom labels for the categorical variables.
group_names = ["Youth", "YoungAdult", "MiddleAged", "Senior"]
cats = pd.cut(ages, bins, labels=group_names)

cats
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult] Length: 12 Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
# Using an integer number of bins will make compute equal-length bins
# between min and max values in the data.
# Uniform distributed numbers between 0 and 1.
np.random.seed(10)
data = np.random.rand(20)

print(data)
[0.77132064 0.02075195 0.63364823 0.74880388 0.49850701 0.22479665
 0.19806286 0.76053071 0.16911084 0.08833981 0.68535982 0.95339335
 0.00394827 0.51219226 0.81262096 0.61252607 0.72175532 0.29187607
 0.91777412 0.71457578]
print(min(data), max(data))
0.003948266327914451 0.9533933461949365
pd.cut(data, bins=4, precision=2)
[(0.72, 0.95], (0.003, 0.24], (0.48, 0.72], (0.72, 0.95], (0.48, 0.72], ..., (0.48, 0.72], (0.72, 0.95], (0.24, 0.48], (0.72, 0.95], (0.48, 0.72]] Length: 20 Categories (4, interval[float64]): [(0.003, 0.24] < (0.24, 0.48] < (0.48, 0.72] < (0.72, 0.95]]

pd.qcut()

# qcut() uses sample quantiles instead of fixed intervals.

data = np.random.randn(1000)

# Use 4 equally space quantiles.
cats = pd.qcut(data, 4)

cats
[(-3.205, -0.64], (-0.0251, 0.602], (0.602, 2.68], (-3.205, -0.64], (-3.205, -0.64], ..., (-3.205, -0.64], (0.602, 2.68], (-3.205, -0.64], (-3.205, -0.64], (-0.0251, 0.602]] Length: 1000 Categories (4, interval[float64]): [(-3.205, -0.64] < (-0.64, -0.0251] < (-0.0251, 0.602] < (0.602, 2.68]]
# Since the distribution is Gaussian there should be exactly the same proportion
# of numbers in each category.
pd.value_counts(cats)
(0.602, 2.68] 250 (-0.0251, 0.602] 250 (-0.64, -0.0251] 250 (-3.205, -0.64] 250 dtype: int64
# Passing custom quantiles.
cats = pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.0])
cats
[(-1.208, -0.0251], (-0.0251, 1.182], (1.182, 2.68], (-1.208, -0.0251], (-3.205, -1.208], ..., (-1.208, -0.0251], (1.182, 2.68], (-3.205, -1.208], (-1.208, -0.0251], (-0.0251, 1.182]] Length: 1000 Categories (4, interval[float64]): [(-3.205, -1.208] < (-1.208, -0.0251] < (-0.0251, 1.182] < (1.182, 2.68]]
pd.value_counts(cats)
(-0.0251, 1.182] 400 (-1.208, -0.0251] 400 (1.182, 2.68] 100 (-3.205, -1.208] 100 dtype: int64

Outliers

# Mean 0 and std 1.
data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()
Loading...
data.hist(bins=30)
# Find values in one of the columns exceeding 2.7 in abs value.
# Extract data in the pandas way.
col = data[2]
col[np.abs(col) > 2.7]
135 -3.112645 293 3.609161 366 -2.707230 398 -2.715141 575 2.947099 615 -2.721390 700 -2.795615 953 -2.831778 Name: 2, dtype: float64
# Use pandas data inside numpy functions.
(np.abs(data) > 2.9).head()
Loading...
# Find rows with at least one value larger than 2.9 in abs value.
data[(np.abs(data) > 2.9).any(axis=1)]
Loading...

Permutation

Indicator / Dummy variables

  • Convert a categorical variable into a dummy (or indicator) variable
  • If a column has k distinct values, then k columns with one-hot encoding will be added

pd.get_dummies()

df = pd.DataFrame(
    {"key": ["b", "b", "a", "c", "a", "b"], "data1": list(range(6))}
)

df
Loading...
pd.get_dummies(df["key"])
Loading...
# One can add a prefix with "prefix".
pd.get_dummies(df["key"], prefix="key")
Loading...
# One can combine pd.cut() to discretize with a pd.get_dummies() to compute indicator vars.

# Compute 10 random numbers.
np.random.seed(12345)
values = np.random.rand(10)

print("values=", values)

# Discretize with the given bins.
bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
pd.get_dummies(pd.cut(values, bins))
values= [0.92961609 0.31637555 0.18391881 0.20456028 0.56772503 0.5955447
 0.96451452 0.6531771  0.74890664 0.65356987]
Loading...

String manipulation

String methods

  • split()
  • join()
string = "hello my name is guido"
"guido" in string
True
# Find the index of a substring.
print(string.index("guido"))

print(string.find("guido"))
17
17
# Raises "ValueError: substring not found"
# string.index('Guido')

string.find("Guido")
-1
# Strip removes spaces on both sides.
" Guido    ".strip()
'Guido'

Regex

import re

text = "foo bar\t baz \tqux"
# Split using 1 or more spaces as separator.
re.split("\s+", text)
['foo', 'bar', 'baz', 'qux']
# If you want to call a regex several times, re.compile() speeds up things.
regex = re.compile("\s+")
regex.split(text)
regex.split(text)
['foo', 'bar', 'baz', 'qux']
# re.findall() finds all the matching substrings.
print(re.findall("\s", text))
[' ', '\t', ' ', ' ', '\t']
# re.finditer() returns an iterator to the regex matches.
print(re.finditer("\s", text))
print(list(re.finditer("\s", text)))
<callable_iterator object at 0x1135fe7f0>
[<re.Match object; span=(3, 4), match=' '>, <re.Match object; span=(7, 8), match='\t'>, <re.Match object; span=(8, 9), match=' '>, <re.Match object; span=(12, 13), match=' '>, <re.Match object; span=(13, 14), match='\t'>]
# re.search() finds only the first.
m = re.search("\s", text)
m
<re.Match object; span=(3, 4), match=' '>
print(m.start(), m.end())
3 4
# Extract the match.
text[m.start() : m.end()]
' '
# re.match() finds only the first, starting from the beginning of the string.
# Email pattern match.
# - alphanumerical chars . _ % + -
# - alphanumerical interspersed with .
# - capture the groups
pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"

regex = re.compile(pattern, flags=re.IGNORECASE)
m = regex.match("saggese@gmail.com")

m.groups()
('saggese', 'gmail', 'com')
text = """Dave dave@google.com
    Steve steve@gmail.com
    Rob rob@gmail.com
    Ryan ryan@yahoo.com
    """

# One can look for the patterns multiple times in the string.
regex.findall(text)
[('dave', 'google', 'com'), ('steve', 'gmail', 'com'), ('rob', 'gmail', 'com'), ('ryan', 'yahoo', 'com')]

Vectorized string functions in pandas

data = {
    "Dave": "dave@google.com",
    "Steve": "steve@gmail.com",
    "Rob": "rob@gmail.com",
    "Wes": np.nan,
}
data = pd.Series(data)

data
Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Wes NaN dtype: object
data.isnull()
Dave False Steve False Rob False Wes True dtype: bool
# You can use map() and string manipulation. The problem is that nan create problems,
# so one needs to handle it.
data.map(lambda x: x.lower() if isinstance(x, str) else "")
Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Wes dtype: object
data.str.contains("gmail")
Dave False Steve True Rob True Wes NaN dtype: object
# Lots of string function have a wrapper.
data.str.__dir__()
['_inferred_dtype', '_is_categorical', '_parent', '_orig', '__frozen', '__module__', '__doc__', '__init__', '_validate', '__getitem__', '__iter__', '_wrap_result', '_get_series_list', 'cat', 'split', 'rsplit', 'partition', 'rpartition', 'get', 'join', 'contains', 'match', 'replace', 'repeat', 'pad', 'center', 'ljust', 'rjust', 'zfill', 'slice', 'slice_replace', 'decode', 'encode', 'strip', 'lstrip', 'rstrip', 'wrap', 'get_dummies', 'translate', 'count', 'startswith', 'endswith', 'findall', 'extract', 'extractall', 'find', 'rfind', 'normalize', 'index', 'rindex', 'len', '_doc_args', 'lower', 'upper', 'title', 'capitalize', 'swapcase', 'casefold', 'isalnum', 'isalpha', 'isdigit', 'isspace', 'islower', 'isupper', 'istitle', 'isnumeric', 'isdecimal', '_make_accessor', '_freeze', '__setattr__', '__dict__', '__weakref__', '__repr__', '__hash__', '__str__', '__getattribute__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']