Repository: pandas-dev/pandas (python)

Issue:
BUG: Rolling .apply() with method='table' ignores min_periods
### Pandas version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [X] I have confirmed this bug exists on the [main branch](https://pandas.pydata.org/docs/dev/getting_started/install.html#installing-the-development-version-of-pandas) of pandas.


### Reproducible Example

```python
import pandas as pd

df = pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], columns=['A', 'B'])
print(df)

# prints this:
#    A   B
# 0  1   2
# 1  3   4
# 2  5   6
# 3  7   8
# 4  9  10

df.rolling(3, min_periods=3).apply(lambda x: bool(print(x, '\n')))

# prints this, so as expected, only batches of 3 are being evaluated
# 0    1.0
# 1    3.0
# 2    5.0
# dtype: float64 
# 
# 1    3.0
# 2    5.0
# 3    7.0
# dtype: float64 
# 
# 2    5.0
# 3    7.0
# 4    9.0
# dtype: float64 
# 
# 0    2.0
# 1    4.0
# 2    6.0
# dtype: float64 
# 
# 1    4.0
# 2    6.0
# 3    8.0
# dtype: float64 
# 
# 2     6.0
# 3     8.0
# 4    10.0
# dtype: float64 
# 
# Out[33]: 
#      A    B
# 0  NaN  NaN
# 1  NaN  NaN
# 2  0.0  0.0
# 3  0.0  0.0
# 4  0.0  0.0

df.rolling(3, method='table', min_periods=3).apply(lambda x: bool(print(x, '\n')), raw=True, engine='numba')

# prints this, where we see that the first two batches of sizes 1 and 2 are also being evaluated
# [[1. 2.]]   <-- batch of size 1, illegal
# 
# [[1. 2.]    <-- batch of size 2, illegal
#  [3. 4.]] 
# 
# [[1. 2.]
#  [3. 4.]
#  [5. 6.]] 
# 
# [[3. 4.]
#  [5. 6.]
#  [7. 8.]] 
# 
# [[ 5.  6.]
#  [ 7.  8.]
#  [ 9. 10.]] 
# Out[34]: 
#      A    B
# 0  NaN  NaN
# 1  NaN  NaN
# 2  0.0  0.0
# 3  0.0  0.0
# 4  0.0  0.0
```


### Issue Description

See example above, which violates the contract imposed by the `min_periods` parameter.

### Expected Behavior

The `min_periods` parameter should be respected.

### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : d9cdd2ee5a58015ef6f4d15c7226110c9aab8140
python                : 3.12.3.final.0
python-bits           : 64
OS                    : Windows
OS-release            : 11
Version               : 10.0.22631
machine               : AMD64
processor             : Intel64 Family 6 Model 165 Stepping 2, GenuineIntel
byteorder             : little
LC_ALL                : None
LANG                  : None
LOCALE                : German_Switzerland.utf8
pandas                : 2.2.2
numpy                 : 1.26.4
pytz                  : 2024.1
dateutil              : 2.9.0
setuptools            : 69.5.1
pip                   : 24.0
Cython                : None
pytest                : None
hypothesis            : None
sphinx                : None
blosc                 : None
feather               : None
xlsxwriter            : None
lxml.etree            : 5.2.2
html5lib              : 1.1
pymysql               : None
psycopg2              : None
jinja2                : 3.1.4
IPython               : 8.24.0
pandas_datareader     : None
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : 4.12.3
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : None
fsspec                : None
gcsfs                 : None
matplotlib            : 3.8.4
numba                 : 0.59.1
numexpr               : None
odfpy                 : None
openpyxl              : None
pandas_gbq            : None
pyarrow               : 16.0.0
pyreadstat            : None
python-calamine       : None
pyxlsb                : None
s3fs                  : None
scipy                 : 1.13.0
sqlalchemy            : None
tables                : None
tabulate              : None
xarray                : None
xlrd                  : None
zstandard             : None
tzdata                : 2024.1
qtpy                  : None
pyqt5                 : None
</details>


Pull request:
BUG: Check `min_periods` before applying the function
- [x] closes #58868
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/v3.0.0.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/ci/code_checks.sh b/ci/code_checks.sh
index 230b4d3daf..1e615b6df8 100755
--- a/ci/code_checks.sh
+++ b/ci/code_checks.sh
@@ -470,24 +470,16 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.merge PR07" \
         -i "pandas.merge_asof PR07,RT03" \
         -i "pandas.merge_ordered PR07" \
-        -i "pandas.option_context SA01" \
         -i "pandas.period_range RT03,SA01" \
         -i "pandas.pivot PR07" \
         -i "pandas.pivot_table PR07" \
         -i "pandas.plotting.andrews_curves RT03,SA01" \
-        -i "pandas.plotting.autocorrelation_plot RT03,SA01" \
         -i "pandas.plotting.lag_plot RT03,SA01" \
-        -i "pandas.plotting.parallel_coordinates PR07,RT03,SA01" \
         -i "pandas.plotting.scatter_matrix PR07,SA01" \
-        -i "pandas.plotting.table PR07,RT03,SA01" \
         -i "pandas.qcut PR07,SA01" \
-        -i "pandas.read_orc SA01" \
         -i "pandas.read_spss SA01" \
-        -i "pandas.reset_option SA01" \
         -i "pandas.set_eng_float_format RT03,SA01" \
-        -i "pandas.show_versions SA01" \
         -i "pandas.testing.assert_extension_array_equal SA01" \
-        -i "pandas.testing.assert_series_equal PR07,SA01" \
         -i "pandas.tseries.offsets.BDay PR02,SA01" \
         -i "pandas.tseries.offsets.BQuarterBegin PR02" \
         -i "pandas.tseries.offsets.BQuarterBegin.freqstr SA01" \
@@ -779,7 +771,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.tseries.offsets.YearEnd.nanos GL08" \
         -i "pandas.tseries.offsets.YearEnd.normalize GL08" \
         -i "pandas.tseries.offsets.YearEnd.rule_code GL08" \
-        -i "pandas.unique PR07" \
         -i "pandas.util.hash_pandas_object PR07,SA01" # There should be no backslash in the final line, please keep this comment in the last ignored function
 
     RET=$(($RET + $?)) ; echo $MSG "DONE"
diff --git a/doc/source/user_guide/missing_data.rst b/doc/source/user_guide/missing_data.rst
index 29f3fea899..69dfb406da 100644
--- a/doc/source/user_guide/missing_data.rst
+++ b/doc/source/user_guide/missing_data.rst
@@ -337,10 +337,8 @@ When taking the product, NA values or empty data will be treated as 1.
    pd.Series([], dtype="float64").prod()
 
 Cumulative methods like :meth:`~DataFrame.cumsum` and :meth:`~DataFrame.cumprod`
-ignore NA values by default preserve them in the result. This behavior can be changed
-with ``skipna``
-
-* Cumulative methods like :meth:`~DataFrame.cumsum` and :meth:`~DataFrame.cumprod` ignore NA values by default, but preserve them in the resulting arrays. To override this behaviour and include NA values, use ``skipna=False``.
+ignore NA values by default, but preserve them in the resulting array. To override
+this behaviour and include NA values in the calculation, use ``skipna=False``.
 
 
 .. ipython:: python
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 865996bdf8..872c5b6489 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -536,7 +536,7 @@ Groupby/resample/rolling
 - Bug in :meth:`DataFrameGroupBy.apply` that was returning a completely empty DataFrame when all return values of ``func`` were ``None`` instead of returning an empty DataFrame with the original columns and dtypes. (:issue:`57775`)
 - Bug in :meth:`DataFrameGroupBy.apply` with ``as_index=False`` that was returning :class:`MultiIndex` instead of returning :class:`Index`. (:issue:`58291`)
 - Bug in :meth:`DataFrameGroupby.transform` and :meth:`SeriesGroupby.transform` with a reducer and ``observed=False`` that coerces dtype to float when there are unobserved categories. (:issue:`55326`)
-
+- Bug in :meth:`Rolling.apply` where the applied function could be called on fewer than ``min_period`` periods if ``method="table"``. (:issue:`58868`)
 
 Reshaping
 ^^^^^^^^^
diff --git a/pandas/_config/config.py b/pandas/_config/config.py
index 5bd6535b03..55d9e29686 100644
--- a/pandas/_config/config.py
+++ b/pandas/_config/config.py
@@ -323,6 +323,12 @@ def reset_option(pat: str) -> None:
     None
         No return value.
 
+    See Also
+    --------
+    get_option : Retrieve the value of the specified option.
+    set_option : Set the value of the specified option or options.
+    describe_option : Print the description for one or more registered options.
+
     Notes
     -----
     For all available options, please view the
@@ -414,6 +420,13 @@ def option_context(*args) -> Generator[None, None, None]:
     None
         No return value.
 
+    See Also
+    --------
+    get_option : Retrieve the value of the specified option.
+    set_option : Set the value of the specified option.
+    reset_option : Reset one or more options to their default value.
+    describe_option : Print the description for one or more registered options.
+
     Notes
     -----
     For all available options, please view the :ref:`User Guide <options.available>`
diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py
index b6d157eda8..0d97f8a298 100644
--- a/pandas/core/algorithms.py
+++ b/pandas/core/algorithms.py
@@ -319,6 +319,8 @@ def unique(values):
     Parameters
     ----------
     values : 1d array-like
+        The input array-like object containing values from which to extract
+        unique values.
 
     Returns
     -------
diff --git a/pandas/core/window/numba_.py b/pandas/core/window/numba_.py
index eb06479fc3..824cf936b8 100644
--- a/pandas/core/window/numba_.py
+++ b/pandas/core/window/numba_.py
@@ -227,10 +227,10 @@ def generate_numba_table_func(
             stop = end[i]
             window = values[start:stop]
             count_nan = np.sum(np.isnan(window), axis=0)
-            sub_result = numba_func(window, *args)
             nan_mask = len(window) - count_nan >= minimum_periods
+            if nan_mask.any():
+                result[i, :] = numba_func(window, *args)
             min_periods_mask[i, :] = nan_mask
-            result[i, :] = sub_result
         result = np.where(min_periods_mask, result, np.nan)
         return result
 
diff --git a/pandas/io/orc.py b/pandas/io/orc.py
index d4b4fd9065..3bca8ea7ef 100644
--- a/pandas/io/orc.py
+++ b/pandas/io/orc.py
@@ -85,6 +85,14 @@ def read_orc(
     DataFrame
         DataFrame based on the ORC file.
 
+    See Also
+    --------
+    read_csv : Read a comma-separated values (csv) file into a pandas DataFrame.
+    read_excel : Read an Excel file into a pandas DataFrame.
+    read_spss : Read an SPSS file into a pandas DataFrame.
+    read_sas : Load a SAS file into a pandas DataFrame.
+    read_feather : Load a feather-format object into a pandas DataFrame.
+
     Notes
     -----
     Before using this function you should read the :ref:`user guide about ORC <io.orc>`
diff --git a/pandas/plotting/_misc.py b/pandas/plotting/_misc.py
index d79bb7152e..d8455f44ef 100644
--- a/pandas/plotting/_misc.py
+++ b/pandas/plotting/_misc.py
@@ -33,6 +33,7 @@ def table(ax: Axes, data: DataFrame | Series, **kwargs) -> Table:
     Parameters
     ----------
     ax : Matplotlib axes object
+        The axes on which to draw the table.
     data : DataFrame or Series
         Data for table contents.
     **kwargs
@@ -43,6 +44,12 @@ def table(ax: Axes, data: DataFrame | Series, **kwargs) -> Table:
     Returns
     -------
     matplotlib table object
+        The created table as a matplotlib Table object.
+
+    See Also
+    --------
+    DataFrame.plot : Make plots of DataFrame using matplotlib.
+    matplotlib.pyplot.table : Create a table from data in a Matplotlib plot.
 
     Examples
     --------
@@ -472,6 +479,7 @@ def parallel_coordinates(
     Parameters
     ----------
     frame : DataFrame
+        The DataFrame to be plotted.
     class_column : str
         Column name containing class names.
     cols : list, optional
@@ -498,6 +506,13 @@ def parallel_coordinates(
     Returns
     -------
     matplotlib.axes.Axes
+        The matplotlib axes containing the parallel coordinates plot.
+
+    See Also
+    --------
+    plotting.andrews_curves : Generate a matplotlib plot for visualizing clusters
+        of multivariate data.
+    plotting.radviz : Plot a multidimensional dataset in 2D.
 
     Examples
     --------
@@ -591,6 +606,12 @@ def autocorrelation_plot(series: Series, ax: Axes | None = None, **kwargs) -> Ax
     Returns
     -------
     matplotlib.axes.Axes
+        The matplotlib axes containing the autocorrelation plot.
+
+    See Also
+    --------
+    Series.autocorr : Compute the lag-N autocorrelation for a Series.
+    plotting.lag_plot : Lag plot for time series.
 
     Examples
     --------
diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py
index 6cdd96996c..c4fec39594 100644
--- a/pandas/util/_print_versions.py
+++ b/pandas/util/_print_versions.py
@@ -115,6 +115,11 @@ def show_versions(as_json: str | bool = False) -> None:
           Info will be written to that file in JSON format.
         * If True, outputs info in JSON format to the console.
 
+    See Also
+    --------
+    get_option : Retrieve the value of the specified option.
+    set_option : Set the value of the specified option or options.
+
     Examples
     --------
     >>> pd.show_versions()  # doctest: +SKIP


Test patch:
diff --git a/pandas/_testing/asserters.py b/pandas/_testing/asserters.py
index ecaa7e5507..4308407111 100644
--- a/pandas/_testing/asserters.py
+++ b/pandas/_testing/asserters.py
@@ -857,7 +857,9 @@ def assert_series_equal(
     Parameters
     ----------
     left : Series
+        First Series to compare.
     right : Series
+        Second Series to compare.
     check_dtype : bool, default True
         Whether to check the Series dtype is identical.
     check_index_type : bool or {'equiv'}, default 'equiv'
@@ -908,6 +910,11 @@ def assert_series_equal(
 
         .. versionadded:: 1.5.0
 
+    See Also
+    --------
+    testing.assert_index_equal : Check that two Indexes are equal.
+    testing.assert_frame_equal : Check that two DataFrames are equal.
+
     Examples
     --------
     >>> from pandas import testing as tm
diff --git a/pandas/tests/window/test_numba.py b/pandas/tests/window/test_numba.py
index 650eb911e4..3695ab8bf6 100644
--- a/pandas/tests/window/test_numba.py
+++ b/pandas/tests/window/test_numba.py
@@ -67,6 +67,21 @@ class TestEngine:
         )
         tm.assert_series_equal(result, expected)
 
+    def test_numba_min_periods(self):
+        # GH 58868
+        def last_row(x):
+            assert len(x) == 3
+            return x[-1]
+
+        df = DataFrame([[1, 2], [3, 4], [5, 6], [7, 8]])
+
+        result = df.rolling(3, method="table", min_periods=3).apply(
+            last_row, raw=True, engine="numba"
+        )
+
+        expected = DataFrame([[np.nan, np.nan], [np.nan, np.nan], [5, 6], [7, 8]])
+        tm.assert_frame_equal(result, expected)
+
     @pytest.mark.parametrize(
         "data",
         [