I have a "base" DataFrame a that contains an identifier seq and join keys, as well as a "value" DataFrame b which will be merged:
import numpy as np
import pandas as pd
a = pd.DataFrame({
'id':range(11),
'seq':[
1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 5],
'class':[
'alpha', 'beta', 'gaga', np.nan, np.nan, np.nan, np.nan, np.nan,
np.nan, 'beta', 'beta'],
'style':[
'x', 'x', 'x', 'y', 'y', np.nan, np.nan, np.nan, np.nan, np.nan,
np.nan],
'drama':[
'no', 'no', 'no', 'yes', 'oh yes', 'no', 'yes', 'oh yes', np.nan,
'yes', 'oh yes']})
b = pd.DataFrame({
'class':[
'gaga', 'alpha', 'alpha', 'alpha', 'alpha', 'alpha', 'beta', 'beta',
'alpha', 'gaga', 'beta', 'beta', 'alpha', 'beta', 'gaga', 'alpha',
'beta', 'alpha', 'beta', 'gaga', 'gaga', 'beta', 'beta', 'beta',
'gaga'],
'style':[
'y', 'y', 'y', 'x', 'x', 'x', 'y', 'x', 'x', 'x', 'y', 'y', 'y', 'y',
'x', 'y', 'y', 'y', 'y', 'x', 'x', 'x', 'y', 'x', 'y'],
'drama':[
'yes', 'no', 'no', 'no', 'no', 'oh yes', 'oh yes', 'oh yes', 'oh yes',
'no', 'yes', 'oh yes', 'no', 'no', 'yes', 'yes', 'no', 'yes', 'oh yes',
'oh yes', 'oh yes', 'no', 'oh yes', 'yes', 'yes'],
'start':[
838, 727, 700, 840, 530, 507, 871, 585, 120, 164, 562, 750, 953, 733,
337, 307, 277, 972, 3, 805, 539, 600, 8, 382, 147],
'end':[
198, 328, 591, 427, 151, 126, 132, 149, 856, 725, 608, 726, 178, 521,
316, 154, 633, 4, 113, 881, 258, 32, 354, 259, 958]})
What I would like to do is create a function that allows for a sort of "recursive" join against b, which will join only on available non-null columns for each row. In a's case:
- Where
class,styleanddramaare non-null, will join on three keys; - Where
classis null, will join onstyleanddramaonly; - Where both
classandstyleare null, will join only ondrama; - Where all three columns are null, will "join" on the entire value DataFrame
b; - The null columns are not necessarily the same: for instance, if
styleis null, it will join onclassanddrama.
Result-wise, in this example the output would be the same as manually doing the not-so-intelligent:
ll = []
x, y, z, w, v = a.loc[:2], a.loc[3:4], a.loc[5:7], a.loc[8:8], a.loc[9:]
ll.append(x.merge(b, on=['class', 'style', 'drama']))
ll.append(y.drop(columns='class').merge(b, on=['style', 'drama']))
ll.append(z.drop(columns=['class', 'style']).merge(b, on='drama'))
ll.append(v.drop(columns='style').merge(b, on=['class', 'drama']))
# For all null values, get the entire DataFrame
w['placeholder'] = 1
b['placeholder'] = 1
ll.append(w
.drop(columns=['class', 'style', 'drama'])
.merge(b, on='placeholder')
.drop(columns='placeholder'))
result = pd.concat(ll)
However, doing manually in this case was only possible because I already knew beforehand how to segregate the "groups" (x, y, z, w and v) and which columns I would use for the merge operation in each one.
I have half-implemented it with very limited usability and using in my opinion a subpar way to deal with the columns:
def recjoin(base: pd.DataFrame, other: pd.DataFrame, keys: list) -> pd.DataFrame:
missing_cols = base.columns[base.isnull().any()]
if len(missing_cols) == 0:
result = base.merge(other, on=idx)
else:
nonmissing = base.dropna(subset=keys)
result_nonmissing = nonmissing.merge(other, on=keys)
id_missing = base.index.difference(nonmissing.index)
missing = base.loc[id_missing].drop(columns=missing_cols)
if isinstance(keys, str):
keys = [keys]
alt_keys = list(pd.Index(keys).difference(missing_cols))
result_missing = missing.merge(other, on=alt_keys)
result = pd.concat([nonmissing, missing])
return result
This way it will work if a.loc[:4] was passed to base, but not if it were a.loc[:7], because there is a variable number of NaN columns in the second case:
In [1]: a.loc[:4]
Out[1]:
id seq class style drama
0 0 1 alpha x no
1 1 1 beta x no
2 2 1 gaga x no
3 3 2 NaN y yes
4 4 2 NaN y oh yes
In [2]: a.loc[:7]
Out[2]:
id seq class style drama
0 0 1 alpha x no
1 1 1 beta x no
2 2 1 gaga x no
3 3 2 NaN y yes
4 4 2 NaN y oh yes
5 5 3 NaN NaN no
6 6 3 NaN NaN yes
7 7 3 NaN NaN oh yes
What is the best approach in this case so we don't fall on a iterrows solution?