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 wrangling: join, combine, reshape

import pandas as pd

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

import numpy as np
0.25.0

Data wrangling: join, combine, reshape

  • Data can be spread across files or databases, or in a way that is difficult to analyze

Hierarchical indexing

  • Two or more index levels on an axis

    • Allows to work with high dimensional data using a lower dimensional form
  • Used in:

    • group based operations
    • pivot tables
    • reshaping
  • INV: A hierarchical index comes when a list of lists is used (instead of a simple list)

  • INV: Indexing in a hierarchical index is like indexing in a np array

Series hierarchical index

# Create a hierarchical index.
np.random.seed(10)

data = pd.Series(
    np.random.randn(9),
    index=[
        ["a", "a", "a", "b", "b", "c", "c", "d", "d"],
        [1, 2, 3, 1, 3, 1, 2, 2, 3],
    ],
)

data
a 1 1.331587 2 0.715279 3 -1.545400 b 1 -0.008384 3 0.621336 c 1 -0.720086 2 0.265512 d 2 0.108549 3 0.004291 dtype: float64
# There are
# - "levels", i.e., the values that are used as Cartesian product.
# - the subset of the indices (in the "labels" sets).
# E.g.,
#   (0, 0) -> (a, 1)
#   (0, 1) -> (a, 2)
# ...

data.index
MultiIndex([('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 3), ('c', 1), ('c', 2), ('d', 2), ('d', 3)], )

Partial indexing

# Partial indexing.

# Extract the values for the first level.
data["b"]
1 -0.008384 3 0.621336 dtype: float64
# Range of one level of an index.
data["b":"c"]
b 1 -0.008384 3 0.621336 c 1 -0.720086 2 0.265512 dtype: float64
# Select some indices with .loc[list].
data.loc[["b", "d"]]
b 1 -0.008384 3 0.621336 d 2 0.108549 3 0.004291 dtype: float64
# Select all the first level and part of the second.
# Indexing works like a numpy array.
data.loc[:, 2]
a 0.715279 c 0.265512 d 0.108549 dtype: float64
# .unstack() moves one level of indexing into the columns.
data2 = data.unstack()
data2
Loading...
# .stack() moves the column index into a hierarchical index.
data2.stack()
a 1 1.331587 2 0.715279 3 -1.545400 b 1 -0.008384 3 0.621336 c 1 -0.720086 2 0.265512 d 2 0.108549 3 0.004291 dtype: float64

Dataframe hierarchical index

  • In a df both columns and rows can have a hierarchical index
    • Note that indices in pandas have “values” and “names”
frame = pd.DataFrame(np.arange(12).reshape((4, 3)))
frame
Loading...
frame = pd.DataFrame(
    np.arange(12).reshape((4, 3)),
    index=["a a b b".split(), list(map(int, "1 2 1 2".split()))],
    columns=["Ohio Ohio Colorado".split(), "Green Red Green".split()],
)
frame
Loading...
frame.index
MultiIndex([('a', 1), ('a', 2), ('b', 1), ('b', 2)], )
frame.columns
MultiIndex([( 'Ohio', 'Green'), ( 'Ohio', 'Red'), ('Colorado', 'Green')], )
# The levels have no name.
print(frame.index.names)
print(frame.columns.names)
[None, None]
[None, None]
# Assign names to hierarchical levels.
frame.index.names = ["key1", "key2"]
frame.columns.names = ["state", "color"]

frame
Loading...
# A multiindex can be created by itself and used.
print(frame.columns)
MultiIndex([(    'Ohio', 'Green'),
            (    'Ohio',   'Red'),
            ('Colorado', 'Green')],
           names=['state', 'color'])
pd.MultiIndex.from_arrays(
    ["Ohio Ohio Colorado".split(), "Green Red Green".split()],
    names=["state", "color"],
)
MultiIndex([( 'Ohio', 'Green'), ( 'Ohio', 'Red'), ('Colorado', 'Green')], names=['state', 'color'])

Reordering and sorting levels

swaplevel()

# Swap (or reorder) the order of the levels on an axis.
display(frame)
frame.swaplevel("key1", "key2")
Loading...
Loading...

sort_index

# Sort the values in one specific level.
display(frame)
frame2 = frame.sort_index(
    # level=1 is key2
    # level="key2")
    level=1
)

frame2
Loading...
Loading...
frame2.sort_index(
    # level=0 is key1
    level=0
)
Loading...

Summary statistics by level

frame
Loading...
# frame.sum(level='key2')
frame.sum(level=1)
Loading...
frame.sum(level="color", axis=1)
Loading...

Indexing with df columns

  • One can create an index from a column

DataFrame.set_index()

# Build from dictionary: each (key, value) becomes a column.
# A new index is added.
frame = pd.DataFrame(
    {
        "a": list(range(7)),
        "b": list(range(7, 0, -1)),
        "c": ["one", "one", "one", "two", "two", "two", "two"],
        "d": [0, 1, 2, 0, 1, 2, 3],
    }
)

frame
Loading...
# Use two columns as index.
frame2 = frame.set_index(["c", "d"])

frame2
Loading...

DataFrame.reset_index()

# Move index to columns and reindex with unique integer
frame2.reset_index()
Loading...
# Two reset index creates two indices.
frame2.reset_index().reset_index()
Loading...

Combining and merging datasets

Db-style df joins

df1 = pd.DataFrame(
    {"key": ["b", "b", "a", "c", "a", "a", "b"], "data1": list(range(7))}
)
df2 = pd.DataFrame({"key": ["a", "b", "d"], "data2": list(range(3))})

print("df1=")
display(df1)
print("\ndf2=")
display(df2)
df1=
Loading...

df2=
Loading...
# We want to merge on column "key"
# This is a many-to-one join
# - the data in df1['key'] has multiple rows with 'a', and 'b'
# - df2['key'] has fewer
# so there will be a partial Cartesian product.

# Default values
# - Without specifying the columns to join, the common columns are used
# - inner join

# pd.merge(df1, df2, on='key')
pd.merge(df1, df2)
Loading...
# One can specify explicitly different column names
# E.g.,
#   pd.merge(df3, df4, left_on='lkey', right_on='rkey')

# - Indexes are discarded
# - Common columns not used for joined are renamed _x, _y
# To merge on index
#   pd.merge(df1, df2, left_on='key', right_index=True)

# Merge on index with multi-index
#   pd.merge(df1, df2, left_on=['key1', 'key2'], right_index=True)

# Merge on both indices
#   pd.merge(df1, df2, how='outer', left_index=True, right_index=True)

Concatenating along an axis

  • Aka stacking

Concat in numpy

arr = np.arange(12)
arr
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
arr = np.arange(12).reshape((3, 4))
arr
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
# Concatenate horizontally.
np.concatenate([arr, arr], axis=1)
array([[ 0, 1, 2, 3, 0, 1, 2, 3], [ 4, 5, 6, 7, 4, 5, 6, 7], [ 8, 9, 10, 11, 8, 9, 10, 11]])
# Concatenate vertically.
np.concatenate([arr, arr], axis=0)
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])

Concat in pandas Series

  • Since pandas objects (e.g., Series and DataFrame) have labeled axis
    • If indexes are different, should do union or intersection of labels?
    • Should we preserve the labels or discard it after concatenation?
s1 = pd.Series([0, 1], index="a b".split())
print("s1=\n%s" % s1)

s2 = pd.Series([2, 3, 4], index="c d e".split())
print("\ns2=\n%s" % s2)

s3 = pd.Series([5, 6], index="f g".split())
print("\ns3=\n%s" % s3)
s1=
a    0
b    1
dtype: int64

s2=
c    2
d    3
e    4
dtype: int64

s3=
f    5
g    6
dtype: int64
# Concat series along axis=0 producing another Series with index and values glued together.
pd.concat([s1, s2, s3], axis=0)
a 0 b 1 c 2 d 3 e 4 f 5 g 6 dtype: int64
# Concat series along axis=1, does a union of the indices and then merges into a DataFrame
# (sorted outer join).
pd.concat([s1, s2, s3], axis=1, sort=True)
Loading...
# concat() can also do a inner join.
pd.concat([s1, s2, s3], axis=1, join="inner")
Loading...
# If you want to identify where the data came from, you can use
# a hierarchical axis.
result = pd.concat([s1, s1, s3], keys="one two three".split())
print(result.index)
result
MultiIndex([(  'one', 'a'),
            (  'one', 'b'),
            (  'two', 'a'),
            (  'two', 'b'),
            ('three', 'f'),
            ('three', 'g')],
           )
one a 0 b 1 two a 0 b 1 three f 5 g 6 dtype: int64
# Unstack moves a level of hierarchical indexing into columns.
result.unstack()
Loading...

Concat in pandas DataFrame

df1 = pd.DataFrame(
    np.arange(6).reshape(3, 2), index=["a", "b", "c"], columns=["one", "two"]
)

print("df1=")
display(df1)

df2 = pd.DataFrame(
    5 + np.arange(4).reshape(2, 2), index=["a", "c"], columns=["three", "four"]
)

print("\ndf2=")
display(df2)
df1=
Loading...

df2=
Loading...
# Merge using hierarchical columns to track where the data is coming from.
# axis=1 means (columns)
pd.concat([df1, df2], axis=1, keys=["level1", "level2"])
/Users/saggese/.conda/envs/study/lib/python3.7/site-packages/ipykernel_launcher.py:3: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.

To accept the future behavior, pass 'sort=False'.

To retain the current behavior and silence the warning, pass 'sort=True'.

  This is separate from the ipykernel package so we can avoid doing imports until
Loading...
# "ignore_index=True" is used to ignore the indices and create a new
# numeric index.
pd.concat([df1, df2], axis=0, sort=True)
Loading...
pd.concat([df1, df2], axis=0, ignore_index=True, sort=True)
Loading...
# Check if there are duplicated indices.
# pd.concat([df1, df2], axis=0, verify_integrity=True)

Combining data with overlap

# np.where() performs a vectorized if-then-else operation.

a = pd.Series(
    [np.nan, 2.5, np.nan, 3.5, 4.5, np.nan], index="f e d c b a".split()
)
print(a)
f    NaN
e    2.5
d    NaN
c    3.5
b    4.5
a    NaN
dtype: float64
b = pd.Series(np.arange(len(a), dtype=np.float64), index="f e d c b a".split())
b[-1] = np.nan

display(b)
f 0.0 e 1.0 d 2.0 c 3.0 b 4.0 a NaN dtype: float64
# We can combine the two series by using the values from a and
# using values from b to fill the nan.

# Compute the null values
np.where(pd.isnull(a), b, a)
array([0. , 2.5, 2. , 3.5, 4.5, nan])
a.combine_first(b)
f 0.0 e 2.5 d 2.0 c 3.5 b 4.5 a NaN dtype: float64

Reshaping and pivoting

Reshaping with hierarchical indexing

  • stack: rotates (or pivots) from the columns to the rows
  • unstack: pivots from the rows into the columns
data = pd.DataFrame(
    np.arange(6).reshape((2, 3)),
    index=pd.Index(["Ohio", "Colorado"], name="state"),
    columns=pd.Index(["one", "two", "three"], name="number"),
)

data
Loading...
# Note that it produces a Series since it has a single column.
result = data.stack()

print(type(result))
print(type(result.index))
result
<class 'pandas.core.series.Series'>
<class 'pandas.core.indexes.multi.MultiIndex'>
state number Ohio one 0 two 1 three 2 Colorado one 3 two 4 three 5 dtype: int64
result.unstack()
Loading...
# One can unstack a different level than the innermost.
# Level 0 is the outermost.
result.unstack(0)
Loading...
# unstack() can produce nan if there are missing values.
s1 = pd.Series([0, 1, 2, 3], index=["a", "b", "c", "d"])
s2 = pd.Series([4, 5, 6], index=["c", "d", "e"])
# Concat vertically the series.
data2 = pd.concat([s1, s2], keys=["one", "two"])

print("data2=")
print(type(data2))
display(data2)

print("data2.unstack=")
data2.unstack()
data2=
<class 'pandas.core.series.Series'>
one a 0 b 1 c 2 d 3 two c 4 d 5 e 6 dtype: int64
data2.unstack=
Loading...
print("data2=")
# gives the original object.
data2.unstack().stack()
data2=
one a 0.0 b 1.0 c 2.0 d 3.0 two c 4.0 d 5.0 e 6.0 dtype: float64

Pivoting “long” to “wide” format

# git clone https://github.com/wesm/pydata-book
data = pd.read_csv("/Users/saggese/src/pydata-book/examples/macrodata.csv")

display(data.head())
display(data.tail())
Loading...
Loading...
# Combine year and quarter to create a kind of time interval type.
periods = pd.PeriodIndex(year=data.year, quarter=data.quarter, name="date")
print(periods)

data2 = data.copy()
data2.index = periods.to_timestamp("D", "end")

# The data is in "wide" format.
data2.head()
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
             '1960Q3', '1960Q4', '1961Q1', '1961Q2',
             ...
             '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
             '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
            dtype='period[Q-DEC]', name='date', length=203, freq='Q-DEC')
Loading...
# We need to assign a name to the column otherwise when we "melt"
# the column is not going to have a name.
columns = pd.Index(["realgdp", "infl", "unemp"], name="item")
# Use reindex to extract columns.
data2 = data2.reindex(columns=columns)
print("columns=", columns)

data2.head()
columns= Index(['realgdp', 'infl', 'unemp'], dtype='object', name='item')
Loading...
# Put data in "long" format by moving the three columns into one.
ldata = data2.stack().reset_index()
# Rename column to value.
ldata = ldata.rename(columns={0: "value"})
ldata.head()
Loading...
# pivot() allows to transform from long format to wide format.
pivoted = ldata.pivot("date", "item", "value")
pivoted.head()
Loading...
# Add another row: now we have an item and 2 values for each.
ldata2 = ldata.copy()
ldata2["value2"] = np.random.randn(len(ldata))

display(ldata2.head())
Loading...
# Pivoting when there are multiple values creates a hierarchical
# data frame.
pivoted = ldata2.pivot("date", "item")

pivoted.head()
Loading...

Pivoting “wide” to “long” format

# The inverse operation to "pivot" is "melt".

df = pd.DataFrame(
    {
        "key": ["foo", "bar", "baz"],
        "A": [1, 2, 3],
        "B": [4, 5, 6],
        "C": [7, 8, 9],
    }
)

df
Loading...
# We need to decide which column is:
# - the variable
# - the value

melted = pd.melt(df, ["key"])

melted
Loading...