What does the term "broadcasting" mean in Pandas documentation? - python

I'm reading through the Pandas documentation, and the term "broadcasting" is used extensively, but never really defined or explained.
What does it mean?

So the term broadcasting comes from numpy, simply put it explains the rules of the output that will result when you perform operations between n-dimensional arrays (could be panels, dataframes, series) or scalar values.
Broadcasting using a scalar value
So the simplest case is just multiplying by a scalar value:
In [4]:
s = pd.Series(np.arange(5))
s
Out[4]:
0 0
1 1
2 2
3 3
4 4
dtype: int32
In [5]:
s * 10
Out[5]:
0 0
1 10
2 20
3 30
4 40
dtype: int32
and we get the same expected results with a dataframe:
In [6]:
df = pd.DataFrame({'a':np.random.randn(4), 'b':np.random.randn(4)})
df
Out[6]:
a b
0 0.216920 0.652193
1 0.968969 0.033369
2 0.637784 0.856836
3 -2.303556 0.426238
In [7]:
df * 10
Out[7]:
a b
0 2.169204 6.521925
1 9.689690 0.333695
2 6.377839 8.568362
3 -23.035557 4.262381
So what is technically happening here is that the scalar value has been broadcasted along the same dimensions of the Series and DataFrame above.
Broadcasting using a 1-D array
Say we have a 2-D dataframe of shape 4 x 3 (4 rows x 3 columns) we can perform an operation along the x-axis by using a 1-D Series that is the same length as the row-length:
In [8]:
df = pd.DataFrame({'a':np.random.randn(4), 'b':np.random.randn(4), 'c':np.random.randn(4)})
df
Out[8]:
a b c
0 0.122073 -1.178127 -1.531254
1 0.011346 -0.747583 -1.967079
2 -0.019716 -0.235676 1.419547
3 0.215847 1.112350 0.659432
In [26]:
df.iloc[0]
Out[26]:
a 0.122073
b -1.178127
c -1.531254
Name: 0, dtype: float64
In [27]:
df + df.iloc[0]
Out[27]:
a b c
0 0.244146 -2.356254 -3.062507
1 0.133419 -1.925710 -3.498333
2 0.102357 -1.413803 -0.111707
3 0.337920 -0.065777 -0.871822
the above looks funny at first until you understand what is happening, I took the first row of values and added this row-wise to the df, it can be visualised using this pic (sourced from scipy):
The general rule is this:
In order to broadcast, the size of the trailing axes for both arrays
in an operation must either be the same size or one of them must be
one.
So if I tried to add a 1-D array that didn't match in length, say one with 4 elements, unlike numpy which will raise a ValueError, in Pandas you'll get a df full of NaN values:
In [30]:
df + pd.Series(np.arange(4))
Out[30]:
a b c 0 1 2 3
0 NaN NaN NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN NaN NaN
2 NaN NaN NaN NaN NaN NaN NaN
3 NaN NaN NaN NaN NaN NaN NaN
Now some of the great things about pandas is that it will try to align using existing column names and row labels, this can get in the way of trying to perform a fancier broadcasting like this:
In [55]:
df[['a']] + df.iloc[0]
Out[55]:
a b c
0 0.244146 NaN NaN
1 0.133419 NaN NaN
2 0.102357 NaN NaN
3 0.337920 NaN NaN
In the above I use double subscripting to force the shape to be (4,1) but we see a problem when trying to broadcast using the first row as the column alignment only aligns on the first column. To get the same form of broadcasting to occur like the diagram above shows we have to decompose to numpy arrays which then become anonymous data:
In [56]:
df[['a']].values + df.iloc[0].values
Out[56]:
array([[ 0.24414608, -1.05605392, -1.4091805 ],
[ 0.13341899, -1.166781 , -1.51990758],
[ 0.10235701, -1.19784299, -1.55096957],
[ 0.33792013, -0.96227987, -1.31540645]])
It's also possible to broadcast in 3-dimensions but I don't go near that stuff often but the numpy, scipy and pandas book have examples that show how that works.
Generally speaking the thing to remember is that aside from scalar values which are simple, for n-D arrays the minor/trailing axes length must match or one of them must be 1.
Update
it seems that the above now leads to ValueError: Unable to coerce to Series, length must be 1: given 3 in latest version of pandas 0.20.2
so you have to call .values on the df first:
In[42]:
df[['a']].values + df.iloc[0].values
Out[42]:
array([[ 0.244146, -1.056054, -1.409181],
[ 0.133419, -1.166781, -1.519908],
[ 0.102357, -1.197843, -1.55097 ],
[ 0.33792 , -0.96228 , -1.315407]])
To restore this back to the original df we can construct a df from the np array and pass the original columns in the args to the constructor:
In[43]:
pd.DataFrame(df[['a']].values + df.iloc[0].values, columns=df.columns)
Out[43]:
a b c
0 0.244146 -1.056054 -1.409181
1 0.133419 -1.166781 -1.519908
2 0.102357 -1.197843 -1.550970
3 0.337920 -0.962280 -1.315407

Broadcasting on Pandas DataFrames with MultiIndex
Broadcasting is especially interesting with DataFrames which have a pandas.MultiIndex as I show you in the following example.
Pandas makes it possible to broadcast over the dimensions added via a multidimensional and even hierarchical index, and this is very powerfull, if you know how to use it. You don't need to code your loops and conditions. You can rely on what works already.
I filled two pandas.DataFrames, af and df with a pandas.MultiIndex on the 0-axis (the index) and 10 columns labeled with integeres refering for example to scenario data from a Monte-Carlo simulation.
The pandas.MultiIndexes of the af and df share some common levels in the names (I call them dimensions). Not all labels (newer pandas versions call them codes) need to be in the matching dimensions. In the example, the dimensions 'a' and 'c' are shared. In both frames the 'a'-dimensions has the entries (labels) ['A' and 'B'], whereas in the 'c' dimension the frames af and bf have the entries [0, 1, 2, 3] and [0, 1, 2] respectively.
Nonetheless, Broadcasting works fine. Which means in the following example, when multiplying the two frames, a group-wise multiplication for each group with matching entries in the matching dimensions is performed.
The following example shows broadcasting on multiplications, but it works for all binary operations between pandas.DataFrames on the left- and right-hand side.
Some observations
Note, that both frames can have additional dimensions. It is not necessary that one set of names is a subset of the other. In the example we have ['a', 'b', 'c'] and ['a', 'c', 'd'] for the af and bf frames respectively
The result spans up the whole space, as expected: ['a', 'b', 'c', 'd']
Since dimension 'c' does not have the entry (code) '3' in frame bf, whereas af has, the result fills the resulting block with NaNs.
Note, that pandas 1.0.3 has been used here. Broadcasting with more then one overlapping dimensions did not work with pandas version 0.23.4.
Broadcasting over the 0-axis and the 1-axis at the same time does also work. See the last two examples. For example, if you would like to multiply the af with only bf[0].to_frame(), the first scenario. But it will only be applied to the equally labeled columns (as broadcasting is intended).
Further Hints
If you want to multiply the af frame with a column vector (I need to apply some weights sometimes with additional dimensions), then you can easily implement it yourself. You can expand your dataframe to n = af.shape[1] columns and use then that one for multiplication. Have a look at numpy.tile on how to do it 'without' coding.
>>> af
Values 0 1 2 3 4 5 6 7 8 9
a b c
A a 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
b 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
c 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
B a 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
b 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
c 0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
1 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
2 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
3 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
>>> bf
Values 0 1 2 3 4 5 6 7 8 9
a c d
A 0 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
1 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
2 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
B 0 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
1 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
2 * 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
# 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.0
>>> af * bf
Values 0 1 2 3 4 5 6 7 8 9
a c b d
A 0 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
1 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
2 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
B 0 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
1 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
2 a * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
b * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
c * 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
# 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0 6.0
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
>>> af * bf[0] # Raises Error: ValueError: cannot join with no overlapping index names
# Removed that part
>>> af * bf[0].to_frame() # works consistently
0 1 2 3 4 5 6 7 8 9
a c b d
A 0 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
B 0 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 a * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
b * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
c * 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
# 6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
>>> cf = bf[0].to_frame()
>>> cf.columns = [3]
>>> af * cf # And as expected we can broadcast over the same column labels at the same time
0 1 2 3 4 5 6 7 8 9
a c b d
A 0 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
1 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
2 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
B 0 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
1 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
2 a * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
b * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
c * NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
# NaN NaN NaN 6.0 NaN NaN NaN NaN NaN NaN
3 a NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
c NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

Related

How to freeze first numbers in sequences between NaNs in Python pandas dataframe

Is there a Pythonic way to, in a timeseries dataframe, by column, go down and pick the first number in a sequence, and then push it forward until the next NaN, and then take the next non-NaN number and push that one down until the next NaN, and so on (retaining the indices and NaNs).
For example, I would like to convert this dataframe:
DF = pd.DataFrame(data={'A':[np.nan,1,3,5,7,np.nan,2,4,6,np.nan], 'B':[8,6,4,np.nan,np.nan,9,7,3,np.nan,3], 'C':[np.nan,np.nan,4,2,6,np.nan,1,5,2,8]})
A B C
0 NaN 8.0 NaN
1 1.0 6.0 NaN
2 3.0 4.0 4.0
3 5.0 NaN 2.0
4 7.0 NaN 6.0
5 NaN 9.0 NaN
6 2.0 7.0 1.0
7 4.0 3.0 5.0
8 6.0 NaN 2.0
9 NaN 3.0 8.0
To this dataframe:
Result = pd.DataFrame(data={'A':[np.nan,1,1,1,1,np.nan,2,2,2,np.nan], 'B':[8,8,8,np.nan,np.nan,9,9,9,np.nan,3], 'C':[np.nan,np.nan,4,4,4,np.nan,1,1,1,1]})
A B C
0 NaN 8.0 NaN
1 1.0 8.0 NaN
2 1.0 8.0 4.0
3 1.0 NaN 4.0
4 1.0 NaN 4.0
5 NaN 9.0 NaN
6 2.0 9.0 1.0
7 2.0 9.0 1.0
8 2.0 NaN 1.0
9 NaN 3.0 1.0
I know I can use a loop to iterate down the columns to do this, but would appreciate some help on how to do it in a more efficient Pythonic way on a very large dataframe. Thank you.
IIUC:
# where DF is not NaN
mask = DF.notna()
Result = (DF.shift(-1) # fill the original NaN's with their next value
.mask(mask) # replace all the original non-NaN with NaN
.ffill() # forward fill
.fillna(DF.iloc[0]) # starting of the the columns with a non-NaN
.where(mask) # replace the original NaN's back
)
Output:
A B C
0 NaN 8.0 NaN
1 1.0 8.0 NaN
2 1.0 8.0 4.0
3 1.0 NaN 4.0
4 1.0 NaN 4.0
5 NaN 9.0 NaN
6 2.0 9.0 1.0
7 2.0 9.0 1.0
8 2.0 NaN 1.0
9 NaN 3.0 1.0

Transforming pandas dataframe, where column entries are column headers

My dataset has 12 columns, X1-X6 and Y1-Y6. The variables X and Y match to each other - the first record means: 80 parts of A, 10 parts of C, 2 parts of J and 8 parts of K (each row has 100 total).
I would like to be able to transform my dataset into a dataset in which the entries in columns X1-X6 are now the headers. See before and after datasets below.
My dataset (before):
X1 X2 X3 X4 X5 X6 Y1 Y2 Y3 Y4 Y5 Y6
0 A C J K NaN NaN 80.0 10.0 2.0 8.0 NaN NaN
1 F N O NaN NaN NaN 2.0 25.0 73.0 NaN NaN NaN
2 A H J M NaN NaN 70.0 6.0 15.0 9.0 NaN NaN
3 B I K P NaN NaN 0.5 1.5 2.0 96.0 NaN NaN
4 A B F H O P 83.0 4.0 9.0 2.0 1.0 1.0
5 A B F G NaN NaN 1.0 16.0 9.0 74.0 NaN NaN
6 A B D F L NaN 95.0 2.0 1.0 1.0 1.0 NaN
7 B F H P NaN NaN 0.2 0.4 0.4 99.0 NaN NaN
8 A D F L NaN NaN 35.0 12.0 30.0 23.0 NaN NaN
9 A B F I O NaN 95.0 0.3 0.1 1.6 3.0 NaN
10 B E G NaN NaN NaN 10.0 31.0 59.0 NaN NaN NaN
11 A F G L NaN NaN 24.0 6.0 67.0 3.0 NaN NaN
12 A C I NaN NaN NaN 65.0 30.0 5.0 NaN NaN NaN
13 A F G L NaN NaN 55.0 6.0 4.0 35.0 NaN NaN
14 A F J K L NaN 22.0 3.0 12.0 0.8 62.2 NaN
15 B F I P NaN NaN 0.6 1.2 0.2 98.0 NaN NaN
16 A B F H O NaN 27.0 6.0 46.0 13.0 8.0 NaN
The dataset I'd like to transform to:
A B C D E F G H I J K L M \
0 80.0 NaN 10.0 NaN NaN NaN NaN NaN NaN 2.0 8.0 NaN NaN
1 NaN NaN NaN NaN NaN 2.0 NaN NaN NaN NaN NaN NaN NaN
2 70.0 NaN NaN NaN NaN NaN NaN 6.0 NaN 15.0 NaN NaN 9.0
3 NaN 0.5 NaN NaN NaN NaN NaN NaN 1.5 NaN 2.0 NaN NaN
4 83.0 4.0 NaN NaN NaN 9.0 NaN 2.0 NaN NaN NaN NaN NaN
5 1.0 16.0 NaN NaN NaN 9.0 74.0 NaN NaN NaN NaN NaN NaN
6 95.0 2.0 NaN 1.0 NaN 1.0 NaN NaN NaN NaN NaN 1.0 NaN
7 NaN 0.2 NaN NaN NaN 0.4 NaN 0.4 NaN NaN NaN NaN NaN
8 35.0 NaN NaN 12.0 NaN 30.0 NaN NaN NaN NaN NaN 23.0 NaN
9 95.0 0.3 NaN NaN NaN 0.1 NaN NaN 1.6 NaN NaN NaN NaN
10 NaN 10.0 NaN NaN 31.0 NaN 59.0 NaN NaN NaN NaN NaN NaN
11 24.0 NaN NaN NaN NaN 6.0 67.0 NaN NaN NaN NaN 3.0 NaN
12 65.0 NaN 30.0 NaN NaN NaN NaN NaN 5.0 NaN NaN NaN NaN
13 55.0 NaN NaN NaN NaN 6.0 4.0 NaN NaN NaN NaN 35.0 NaN
14 22.0 NaN NaN NaN NaN 3.0 NaN NaN NaN 12.0 0.8 62.2 NaN
15 NaN 0.6 NaN NaN NaN 1.2 NaN NaN 0.2 NaN NaN NaN NaN
16 27.0 6.0 NaN NaN NaN 46.0 NaN 13.0 NaN NaN NaN NaN NaN
N O P
0 NaN NaN NaN
1 25.0 73.0 NaN
2 NaN NaN NaN
3 NaN NaN 96.0
4 NaN 1.0 1.0
5 NaN NaN NaN
6 NaN NaN NaN
7 NaN NaN 99.0
8 NaN NaN NaN
9 NaN 3.0 NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 NaN NaN NaN
13 NaN NaN NaN
14 NaN NaN NaN
15 NaN NaN 98.0
16 NaN 8.0 NaN
As you know that you want the Xi part to contain the column names for the new dataframe, while the Yi part would be the value, it is enough to change every line in a dict where Xi is the key and Yi the value. Then you use the list of that dictionnaries to feed the new dataframe:
data = list(df.apply(lambda x: {x['X'+ str(i)]: x['Y'+str(i)] for i in range(1,7)
if x['X'+str(i)]!= 'NaN'}, axis=1))
resul = pd.DataFrame(data)
print(resul)
gives:
A B C D E F ... K L M N O P
0 80.0 NaN 10.0 NaN NaN NaN ... 8.0 NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN 2.0 ... NaN NaN NaN 25.0 73.0 NaN
2 70.0 NaN NaN NaN NaN NaN ... NaN NaN 9.0 NaN NaN NaN
3 NaN 0.5 NaN NaN NaN NaN ... 2.0 NaN NaN NaN NaN 96.0
4 83.0 4.0 NaN NaN NaN 9.0 ... NaN NaN NaN NaN 1.0 1.0
5 1.0 16.0 NaN NaN NaN 9.0 ... NaN NaN NaN NaN NaN NaN
6 95.0 2.0 NaN 1.0 NaN 1.0 ... NaN 1.0 NaN NaN NaN NaN
7 NaN 0.2 NaN NaN NaN 0.4 ... NaN NaN NaN NaN NaN 99.0
8 35.0 NaN NaN 12.0 NaN 30.0 ... NaN 23.0 NaN NaN NaN NaN
9 95.0 0.3 NaN NaN NaN 0.1 ... NaN NaN NaN NaN 3.0 NaN
10 NaN 10.0 NaN NaN 31.0 NaN ... NaN NaN NaN NaN NaN NaN
11 24.0 NaN NaN NaN NaN 6.0 ... NaN 3.0 NaN NaN NaN NaN
12 65.0 NaN 30.0 NaN NaN NaN ... NaN NaN NaN NaN NaN NaN
13 55.0 NaN NaN NaN NaN 6.0 ... NaN 35.0 NaN NaN NaN NaN
14 22.0 NaN NaN NaN NaN 3.0 ... 0.8 62.2 NaN NaN NaN NaN
15 NaN 0.6 NaN NaN NaN 1.2 ... NaN NaN NaN NaN NaN 98.0
16 27.0 6.0 NaN NaN NaN 46.0 ... NaN NaN NaN NaN 8.0 NaN
[17 rows x 16 columns]
One way to handle this. Loop through each row, splitting the dataframe in half using iloc. Then build a new dictionary using zip, then create a resulting dataframe.
df_dict = {x: list(zip(df.iloc[x,0:6], df.iloc[x,6:12])) for x in range(df.shape[0])}
df1 = pd.DataFrame.from_dict(pd_dict, orient='index')
df1.sort_index(1)
A B C F H I J K M N O P nan
0 80.0 NaN 10.0 NaN NaN NaN 2.0 8.0 NaN NaN NaN NaN NaN
1 NaN NaN NaN 2.0 NaN NaN NaN NaN NaN 25.0 73.0 NaN NaN
2 70.0 NaN NaN NaN 6.0 NaN 15.0 NaN 9.0 NaN NaN NaN NaN
3 NaN 0.5 NaN NaN NaN 1.5 NaN 2.0 NaN NaN NaN 96. NaN
4 83.0 4.0 NaN 9.0 2.0 NaN NaN NaN NaN NaN 1.0 1.0 NaN

Pandas Dataframe interpolating in sections delimited by indexes

My sample code is as follow:
import pandas as pd
dictx = {'col1':[1,'nan','nan','nan',5,'nan',7,'nan',9,'nan','nan','nan',13],\
'col2':[20,'nan','nan','nan',22,'nan',25,'nan',30,'nan','nan','nan',25],\
'col3':[15,'nan','nan','nan',10,'nan',14,'nan',13,'nan','nan','nan',9]}
df = pd.DataFrame(dictx).astype(float)
I'm trying to interpolate various segments which contain the value 'nan'.
For context, I'm trying to track bus speeds using GPS data provided by the city (São Paulo, Brazil), but the data is scarce and with parts that do not provide the information, as the e.g., but there're segments which I know for a fact that they are stopped, such as dawn, but the information come as 'nan' as well.
What I need:
I've been experimenting with dataframe.interpolate() parameters (limit and limit_diretcion) but came up short. If I set df.interpolate(limit=2) I will not only interpolate the data that I need but the data where it shouldn't. So I need to interpolate between sections defined by a limit
Desired output:
Out[7]:
col1 col2 col3
0 1.0 20.00 15.00
1 nan nan nan
2 nan nan nan
3 nan nan nan
4 5.0 22.00 10.00
5 6.0 23.50 12.00
6 7.0 25.00 14.00
7 8.0 27.50 13.50
8 9.0 30.00 13.00
9 nan nan nan
10 nan nan nan
11 nan nan nan
12 13.0 25.00 9.00
The logic that I've been trying to apply is basically trying to find nan's and calculating the difference between their indexes and so createing a new dataframe_temp to interpolate and only than add it to another creating a new dataframe_final. But this has become hard to achieve due to the fact that 'nan'=='nan' return False
This is a hack but may still be useful. Likely Pandas 0.23 will have a better solution.
https://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#dataframe-interpolate-has-gained-the-limit-area-kwarg
df_fw = df.interpolate(limit=1)
df_bk = df.interpolate(limit=1, limit_direction='backward')
df_fw.where(df_bk.notna())
col1 col2 col3
0 1.0 20.0 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.0 22.0 10.0
5 6.0 23.5 12.0
6 7.0 25.0 14.0
7 8.0 27.5 13.5
8 9.0 30.0 13.0
9 NaN NaN NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 13.0 25.0 9.0
Not a Hack
More legitimate way of handling it.
Generalized to handle any limit.
def interp(df, limit):
d = df.notna().rolling(limit + 1).agg(any).fillna(1)
d = pd.concat({
i: d.shift(-i).fillna(1)
for i in range(limit + 1)
}).prod(level=1)
return df.interpolate(limit=limit).where(d.astype(bool))
df.pipe(interp, 1)
col1 col2 col3
0 1.0 20.0 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.0 22.0 10.0
5 6.0 23.5 12.0
6 7.0 25.0 14.0
7 8.0 27.5 13.5
8 9.0 30.0 13.0
9 NaN NaN NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 13.0 25.0 9.0
Can also handle variation in NaN from column to column. Consider a different df
dictx = {'col1':[1,'nan','nan','nan',5,'nan','nan',7,'nan',9,'nan','nan','nan',13],\
'col2':[20,'nan','nan','nan',22,'nan',25,'nan','nan',30,'nan','nan','nan',25],\
'col3':[15,'nan','nan','nan',10,'nan',14,'nan',13,'nan','nan','nan',9,'nan']}
df = pd.DataFrame(dictx).astype(float)
df
col1 col2 col3
0 1.0 20.0 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.0 22.0 10.0
5 NaN NaN NaN
6 NaN 25.0 14.0
7 7.0 NaN NaN
8 NaN NaN 13.0
9 9.0 30.0 NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 NaN NaN 9.0
13 13.0 25.0 NaN
Then with limit=1
df.pipe(interp, 1)
col1 col2 col3
0 1.0 20.0 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.0 22.0 10.0
5 NaN 23.5 12.0
6 NaN 25.0 14.0
7 7.0 NaN 13.5
8 8.0 NaN 13.0
9 9.0 30.0 NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 NaN NaN 9.0
13 13.0 25.0 9.0
And with limit=2
df.pipe(interp, 2).round(2)
col1 col2 col3
0 1.00 20.00 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.00 22.00 10.0
5 5.67 23.50 12.0
6 6.33 25.00 14.0
7 7.00 26.67 13.5
8 8.00 28.33 13.0
9 9.00 30.00 NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 NaN NaN 9.0
13 13.00 25.00 9.0
Here is a way to selectively ignore rows which are consecutive runs of NaNs whose length is greater than a certain size (given by limit):
import numpy as np
import pandas as pd
dictx = {'col1':[1,'nan','nan','nan',5,'nan',7,'nan',9,'nan','nan','nan',13],\
'col2':[20,'nan','nan','nan',22,'nan',25,'nan',30,'nan','nan','nan',25],\
'col3':[15,'nan','nan','nan',10,'nan',14,'nan',13,'nan','nan','nan',9]}
df = pd.DataFrame(dictx).astype(float)
limit = 2
notnull = pd.notnull(df).all(axis=1)
# assign group numbers to the rows of df. Each group starts with a non-null row,
# followed by null rows
group = notnull.cumsum()
# find the index of groups having length > limit
ignore = (df.groupby(group).filter(lambda grp: len(grp)>limit)).index
# only ignore rows which are null
ignore = df.loc[~notnull].index.intersection(ignore)
keep = df.index.difference(ignore)
# interpolate only the kept rows
df.loc[keep] = df.loc[keep].interpolate()
print(df)
prints
col1 col2 col3
0 1.0 20.0 15.0
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 5.0 22.0 10.0
5 6.0 23.5 12.0
6 7.0 25.0 14.0
7 8.0 27.5 13.5
8 9.0 30.0 13.0
9 NaN NaN NaN
10 NaN NaN NaN
11 NaN NaN NaN
12 13.0 25.0 9.0
By changing the value of limit you can control how big the group has to be before it should be ignored.
This is a partial answer.
for i in list(df):
for x in range(len(df[i])):
if not df[i][x] > -100:
df[i][x] = 0
df
col1 col2 col3
0 1.0 20.0 15.0
1 0.0 0.0 0.0
2 0.0 0.0 0.0
3 0.0 0.0 0.0
4 5.0 22.0 10.0
5 0.0 0.0 0.0
6 7.0 25.0 14.0
7 0.0 0.0 0.0
8 9.0 30.0 13.0
9 0.0 0.0 0.0
10 0.0 0.0 0.0
11 0.0 0.0 0.0
12 13.0 25.0 9.0
Now,
df["col1"][1] == df["col2"][1]
True

Having trouble sorting by row index and `sort_index()` isn't working

I'm attempting to sort the row indexes below from largest to smallest:
My first attempt was:
plot_df_dropoff.sort_index(by=["dropoff_latitude"], ascending=False)
But I get the a Key Value Error.
Second thought based on this link didn't work either. It returned None.
This seems so simple but I can't figure it out. Any help would be much appreciated.
id
pickup_longitude (-74.03, -74.025] (-74.025, -74.02] (-74.02, -74.015] (-74.015, -74.01] (-74.01, -74.005] (-74.005, -74] (-74, -73.995] (-73.995, -73.99] (-73.99, -73.985] (-73.985, -73.98] ... (-73.82, -73.815] (-73.815, -73.81] (-73.81, -73.805] (-73.805, -73.8] (-73.8, -73.795] (-73.795, -73.79] (-73.79, -73.785] (-73.785, -73.78] (-73.78, -73.775] (-73.775, -73.77]
pickup_latitude
(40.63, 40.64] 5.0 10.0 8.0 2.0 3.0 1.0 NaN 2.0 1.0 1.0 ... NaN NaN NaN NaN 1.0 NaN 7.0 1.0 NaN NaN
(40.64, 40.65] 2.0 2.0 14.0 16.0 2.0 4.0 6.0 3.0 5.0 11.0 ... NaN NaN NaN 149.0 164.0 3580.0 7532.0 11381.0 5596.0 NaN
(40.65, 40.66] NaN NaN NaN 2.0 22.0 41.0 11.0 2.0 4.0 13.0 ... NaN 1.0 146.0 7.0 3.0 201.0 81.0 2.0 1.0 2.0
(40.66, 40.67] NaN NaN NaN NaN NaN 2.0 60.0 143.0 180.0 122.0 ... NaN 4.0 24.0 126.0 15.0 47.0 32.0 4.0 3.0 3.0
(40.67, 40.68] NaN NaN 7.0 44.0 18.0 200.0 328.0 65.0 293.0 590.0 ... 3.0 3.0 1.0 131.0 1.0 1.0 2.0 1.0 1.0 2.0
And here is a smaller segment that might be easier to work with:
id \
pickup_longitude (-74.03, -74.025] (-74.025, -74.02] (-74.02, -74.015]
pickup_latitude
(40.63, 40.64] 5.0 10.0 8.0
(40.64, 40.65] 2.0 2.0 14.0
(40.65, 40.66] NaN NaN NaN
(40.66, 40.67] NaN NaN NaN
(40.67, 40.68] NaN NaN 7.0
(40.68, 40.69] NaN NaN NaN
(40.69, 40.7] NaN 1.0 1.0
(40.7, 40.71] 1.0 1.0 3841.0
(40.71, 40.72] NaN 2.0 6537.0
(40.72, 40.73] NaN NaN NaN
(40.73, 40.74] 9.0 2.0 NaN
You can reset index and sort by values.
Try:
>>>plot_df_dropoff.reset_index().sort_values(by=["dropoff_latitude"], ascending=False)
And as #JohnE mentioned, you can also just use sort_index():
>>>plot_df_dropoff.sort_index(ascending=False)

Is there a way to get a "union" of several columns of pandas DataFrame?

I am not looking for merging/concatenating columns or replacing some values with other values (although...maybe yes?). But I have a large dataframe (>100 rows and columns) and I would like to extract columns that are "almost identical", i.e. that have >2 values (at the same index) in common and not no different values at other indexes (if there is a value in one column, there must be either the same value or a NaN in the other).
Here is an example of such a dataframe:
a = np.random.randint(1,10,10)
b = np.array([np.nan,2,np.nan,3,np.nan,6,8,1,2,np.nan])
c = np.random.randint(1,10,10)
d = np.array([7,2,np.nan,np.nan,np.nan,6,8,np.nan,2,2])
e = np.array([np.nan,2,np.nan,np.nan,np.nan,6,8,np.nan,np.nan,2])
f = np.array([np.nan,2,np.nan,3.0,7,np.nan,8,np.nan,np.nan,2])
df = pd.DataFrame({'A':a,'B':b,'C':c,'D':d,'E':e, 'F':f})
df.ix[3:6,'A']=np.nan
df.ix[4:8,'C']=np.nan
EDIT
keys=['S01_o4584','S02_o2531','S03_o7812','S03_o1122','S04_o5210','S04_o3212','S05_o4665','S06_o7425','S07_o3689','S08_o2371']
df['index']=keys
df = df.set_index('index')
A B C D E F
index
S01_o4584 8.0 NaN 9.0 7.0 NaN NaN
S02_o2531 8.0 2.0 5.0 2.0 2.0 2.0
S03_o7812 1.0 NaN 5.0 NaN NaN NaN
S03_o1122 NaN 3.0 6.0 NaN NaN 3.0
S04_o5210 NaN NaN NaN NaN NaN 7.0
S04_o3212 NaN 6.0 NaN 6.0 6.0 NaN
S05_o4665 NaN 8.0 NaN 8.0 8.0 8.0
S06_o7425 1.0 1.0 NaN NaN NaN NaN
S07_o3689 8.0 2.0 NaN 2.0 NaN NaN
S08_o2371 3.0 NaN 9.0 2.0 2.0 2.0
As you see, columns B, D (and newly E) have identical values at locations (indexes) S02_o2531,S04_o3212,S05_o4665 and S08_o2371, whereas at other location, one has a value while the other has s NaN.
My desired output would be:
index BD*E*
S01_o4584 7
S02_o2531 2
S03_o7812 NaN
S03_o1122 3
S04_o5210 NaN
S04_o3212 6
S05_o4665 8
S06_o7425 1
S07_o3689 2
S08_o2371 2
However, I can't combine columns that would then have two different values for the same beginning of the index: as you can see, column F also shares some of the indexes, but a new one is at S04_o5210, but the previous combined columns already have a value at "S04_" (index S04_o3212).
Is there a reasonably pythonic way to do it? I.e. 1) find the columns based on the condition that the values in them must be either identical or np.nan, not different. 2) set a condition that a column cannot be combined if it has the same beginning of the index of previously included values (I may probably need to split the string into two columns and do multiindex???) 3) combine them into the new Series/DataFrame.
def almost(df):
i, j = np.triu_indices(len(df.columns), 1)
v = df.values
d = v[:, i] - v[:, j]
m = (np.where(np.isnan(d), 0, d) == 0).all(0)
return pd.concat(
[
df.iloc[:, i_].combine_first(
df.iloc[:, j_]
).rename(
tuple(df.columns[[i_, j_]])
) for i_, j_ in zip(i[m], j[m])],
axis=1
)
almost(df)
B
D
0 7.0
1 2.0
2 NaN
3 3.0
4 NaN
5 6.0
6 8.0
7 1.0
8 2.0
9 2.0
how it works
i and j represent every combination of columns using numpy to get the indices of an upper triangle.
slice the underlying numpy array df.values with i and j and subtract them. Where the differences are nan, means one or the other were nan. Otherwise, difference should be zero if respective elements are the same.
since we can tolerate nan in one or the other, fill them with zero using np.where.
find where all rows are zero with (x == 0).all(0).
use the mask above to slice i and j and identify the columns that were matches.
build a dataframe of all matches with a pd.MultiIndex for columns that show what matches what.
cooler example
np.random.seed([3,1415])
m, n = 20, 26
df = pd.DataFrame(
np.random.randint(10, size=(m, n)),
columns=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
).mask(np.random.choice([True, False], (m, n), p=(.6, .4)))
df
almost(df)
A D G H I J K
J X K M N J K V S X
0 6.0 7.0 3.0 NaN 4.0 6.0 NaN 6.0 NaN 7.0
1 3.0 3.0 2.0 6.0 4.0 NaN 2.0 6.0 2.0 2.0
2 3.0 0.0 NaN 2.0 4.0 3.0 NaN 3.0 4.0 0.0
3 4.0 4.0 3.0 5.0 5.0 4.0 3.0 4.0 3.0 3.0
4 7.0 NaN NaN 7.0 3.0 7.0 NaN 7.0 NaN NaN
5 NaN NaN 2.0 0.0 5.0 NaN 2.0 2.0 2.0 2.0
6 NaN 8.0 NaN NaN 9.0 2.0 2.0 1.0 NaN 8.0
7 NaN 7.0 NaN 9.0 9.0 6.0 6.0 NaN NaN 7.0
8 NaN NaN 8.0 3.0 1.0 NaN NaN NaN 4.0 NaN
9 0.0 0.0 8.0 2.0 NaN 3.0 3.0 NaN NaN NaN
10 0.0 0.0 NaN 6.0 1.0 NaN NaN 8.0 NaN NaN
11 NaN NaN 3.0 NaN 9.0 3.0 3.0 NaN 3.0 3.0
12 5.0 NaN NaN NaN 6.0 5.0 NaN 5.0 8.0 NaN
13 NaN NaN NaN NaN 7.0 5.0 5.0 NaN NaN NaN
14 NaN NaN 6.0 4.0 8.0 8.0 8.0 NaN 0.0 NaN
15 8.0 8.0 7.0 NaN NaN NaN NaN NaN 2.0 NaN
16 4.0 4.0 4.0 4.0 9.0 9.0 9.0 6.0 4.0 NaN
17 NaN 4.0 NaN 4.0 2.0 8.0 8.0 4.0 NaN 4.0
18 NaN NaN 2.0 7.0 NaN NaN NaN NaN NaN NaN
19 NaN 7.0 6.0 3.0 5.0 NaN NaN 7.0 NaN 7.0
It sounds like the sticking point is how to detect "almost identical" columns, which are columns that only differ (if at all) in what values are missing. Given two column names, how do you check if they are almost identical? Note that if we find a difference that counts, it must be at an index for which neither column has NaN. In other words, the trick is to discard rows with a missing value and compare the rest:
tocheck = df[["B", "D"]].dropna()
if all(tocheck.B == tocheck.D):
print("B, D are almost identical")
Let's use this to iterate over all pairs of columns, and merge the ones that match:
for a, b in itertools.combinations(df.columns, 2):
if a not in df.columns or b not in df.columns: # Was one deleted already?
continue
tocheck = df[[a, b]].dropna()
if all(tocheck[a] == tocheck[b]):
print(b, "->", a)
df[a] = df[a].combine_first(df[b])
del df[b]
Note (in case you haven't noticed) that when multiple columns end up being merged, it's possible to have order-dependent behavior. For example:
A B C
0 NaN 1 2
1 10 NaN NaN
Here you could either merge B or C into A, but not both. Such problems aside, multiple columns can be merged into one since the merged column is saved in place of one of the compared columns.
et voila
test = df.B == df.D
df.loc[test,'myunion'] = df.loc[test, 'B']
df.loc[!test ,'myunion'] = df.loc[!test, 'B'].fillna(0) + df.loc[!test, 'D'].fillna(0)

Categories