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 np0.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])
data0 1.0
1 NaN
2 3.5
3 NaN
4 7.0
dtype: float64data.isnull()0 False
1 True
2 False
3 True
4 False
dtype: booldata.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: float64Filling 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
dfLoading...
df.fillna(0)Loading...
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
dfLoading...
# 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]}
)
dataLoading...
# 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: boolpd.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: booldata.duplicated(["k1"], keep="last")0 True
1 True
2 True
3 True
4 False
5 True
6 False
dtype: boolprint(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],
}
)
dataLoading...
# 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)
data0 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: objectReplacing 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])
data0 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.codesarray([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.categoriesIntervalIndex([(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: int64Outliers¶
# 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))}
)
dfLoading...
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 stringTrue# 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)
dataDave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Wes NaN
dtype: objectdata.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: objectdata.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__']