Fixed metadata propagation in Dataframe.apply (issue #28283)
Co-authored-by: Mohamad Rkein <mohamad_rkein@usp.br>
Co-authored-by: Rafael Rodrigues <rrvsrafael@gmail.com>

In reference to #28283

- [ ] closes #xxxx
- [x] tests added / passed
- [x] Ensure all linting tests pass, see [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit) for how to run them
- [x] whatsnew entry

this appears to have xpassed some previously xfailed tests. @moha-rk you may need to adjust some other tests (as they maybe indirectly calling .apply) > this appears to have xpassed some previously xfailed tests. @moha-rk you may need to adjust some other tests (as they maybe indirectly calling .apply)

Thank you for your feedback, @jreback. I didn't notice the 'F's that appeared after my changes.

The method `DataFrame.nunique()` calls the method `DataFrame.apply()` on its implementation (pandas/core/frame.py), applying `Series.nunique()` (pandas/core/base.py) on each Series (column), so it already handles the metadata propagation. I don't know if that's a problem, but the "method" attribute used on the `__finalize__` call will stay as apply.

As for "mode()", the same thing happens when it's called by a DataFrame (pandas/core/frame.py). However, there's a catch. When "mode()" is called directly from a Series (pandas/core/base.py), this apply call doesn't exist. This is a problem because "Series.mode()" returns a Series containing the mode(s). I created a test with the "not_implemented_mark" to observe this behaviour. I don't know if calling __finalize__ on the Series.mode() function is a good idea, since this would cause it to be called one time for each column on the DataFrame call. Do you have any suggestion? For now, I'll leave it with the said mark.

Finally, the last method that changed was "DataFrame.transform()". This function creates a "FrameApply" object, and calls its function ".transform()". When following the series of function calls, we end on "transform_str_or_callable()" (pandas/core/apply.py), which uses the "DataFrame.apply()" method at its end, propagating the attributes. I removed the "not_implemented_mark".