2020-10-19 13:16 修正。新しい列を作る必要は特にない。

pandas を使うと、例えば

horses.loc[:, 'horse_weight'] = horses.groupby('horse_id')['horse_weight'].ffill()

とすることで、horse_weight の中で欠損した値を、horse_id でグループ化した中での1つ前の値を使って埋めることができるが、1つ前の値が存在しない場合に、1つ後の値を使って補完 (bfill()) したい場合はどうしたら良いか。

# これは正しくない
horses.loc[:, 'horse_weight'] = horses.groupby('horse_id')['horse_weight'].ffill().bfill()

これはいけない。なぜなら ffill() が返す型は SeriesGroupBy ではなく Series であり、グループ化が解除されているため、グループに関係なく 1 つ後の値を使ってしまう。

Stack Overflow に回答があるが、apply() を使うのが正しい。

horses.loc[:, 'horse_weight'] = horses.groupby('horse_id')['horse_weight'].apply(lambda x: x.ffill().bfill())

ただ、実際やってみるとこれは結構遅かった。たぶん普通に SeriesGroupBy.ffill() やるときは何か良い感じの最適化がかかってる一方で、apply() の場合は律儀にグループ数分のループが走るからだと思う(未検証)。

ということで、愚直に

horses.loc[:, 'horse_weight_filled'] = horses.groupby('horse_id')['horse_weight'].ffill() # 別の列に ffill() した結果を入れておく
horses.loc[:, 'horse_weight_filled'] = horses.groupby('horse_id')['horse_weight_filled'].bfill()

として、新しい列に ffill() の結果を入れておき、その列に対して bfill() したほうが速い。その後、必要に応じて元の列を消して新しい列をリネームするなどすると良い。

よく考えたら別に新しい列を作る必要はなかった。

horses.loc[:, 'horse_weight'] = horses.groupby('horse_id')['horse_weight'].ffill()
horses.loc[:, 'horse_weight'] = horses.groupby('horse_id')['horse_weight'].bfill()

で良い。