Expanding on Jianxun Li's idea and making it method-chain-able, we start with,
df = pd.DataFrame(
{
"val": [np.nan, 1, np.nan, 2, np.nan] + [np.nan, 1, 2, 3, np.nan]
}
)
Out[0]:
val
0 NaN
1 1.0
2 NaN
3 2.0
4 NaN
5 NaN
6 1.0
7 2.0
8 3.0
9 NaN
and then we can get the idx as follows,
(df.ffill().notna() & df.bfill().notna()).all(axis=1)
Out[1]:
0 False
1 True
2 True
3 True
4 True
5 True
6 True
7 True
8 True
9 False
dtype: bool
but we can put it into a chainable object like so,
(
df
.loc[lambda df_: (df_.ffill().notna() & df_.bfill().notna()).all(axis=1)]
)
Out[2]:
val
1 1.0
2 NaN
3 2.0
4 NaN
5 NaN
6 1.0
7 2.0
8 3.0
The beauty here is that it can be easily "multi-columned". Note: switch .any
for .all
to keep any outer row containing a non-nan value.
df = pd.DataFrame(
{
# "gp": ["a"] * 5 + ["b"] * 5,
"val": [np.nan, 1, np.nan, 2, np.nan] + [np.nan, 1, 2, 3, np.nan],
"val2": [1, 1, 3, 2, np.nan] + [np.nan, 1, 2, 3, np.nan]
}
)
(
df
.loc[lambda df_: (df_.ffill().notna() & df_.bfill().notna()).any(axis=1)]
)
Out[3]:
val val2
0 NaN 1.0
1 1.0 1.0
2 NaN 3.0
3 2.0 2.0
4 NaN NaN
5 NaN NaN
6 1.0 1.0
7 2.0 2.0
8 3.0 3.0
Lastly, it's groupby-able,
df = pd.DataFrame(
{
"gp": ["a"] * 5 + ["b"] * 5,
"val": [np.nan, 1, np.nan, 2, np.nan] + [np.nan, 1, 2, 3, np.nan],
"val2": [1, 1, 3, 2, np.nan] + [np.nan, 1, 2, 3, np.nan]
}
)
(
df
.loc[(
lambda df_: (
df_
.groupby("gp", group_keys=False)
.apply(lambda s: s.ffill().notna() & s.bfill().notna())
.all(axis=1)
)
)]
)
Out[4]:
gp val val2
1 a 1.0 1.0
2 a NaN 3.0
3 a 2.0 2.0
6 b 1.0 1.0
7 b 2.0 2.0
8 b 3.0 3.0