Repository: pandas-dev/pandas
Pull request #57021 (2024-01-22 23:23:02)

Backport PR #57018 on branch 2.2.x (REGR: merge_ordered with fill_method="ffill" and how="left")
Backport PR #57018: REGR: merge_ordered with fill_method="ffill" and how="left"


Patch:
diff --git a/doc/source/whatsnew/v2.2.1.rst b/doc/source/whatsnew/v2.2.1.rst
index 22a6c0cd10..75445c978d 100644
--- a/doc/source/whatsnew/v2.2.1.rst
+++ b/doc/source/whatsnew/v2.2.1.rst
@@ -13,7 +13,7 @@ including other versions of pandas.
 
 Fixed regressions
 ~~~~~~~~~~~~~~~~~
--
+- Fixed regression in :func:`merge_ordered` raising ``TypeError`` for ``fill_method="ffill"`` and ``how="left"`` (:issue:`57010`)
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_221.bug_fixes:
diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py
index 410301b769..646f40f614 100644
--- a/pandas/core/reshape/merge.py
+++ b/pandas/core/reshape/merge.py
@@ -1930,10 +1930,9 @@ class _OrderedMerge(_MergeOperation):
 
         if self.fill_method == "ffill":
             if left_indexer is None:
-                raise TypeError("left_indexer cannot be None")
-            left_indexer = cast("npt.NDArray[np.intp]", left_indexer)
-            right_indexer = cast("npt.NDArray[np.intp]", right_indexer)
-            left_join_indexer = libjoin.ffill_indexer(left_indexer)
+                left_join_indexer = None
+            else:
+                left_join_indexer = libjoin.ffill_indexer(left_indexer)
             if right_indexer is None:
                 right_join_indexer = None
             else:


Test patch:
diff --git a/ci/run_tests.sh b/ci/run_tests.sh
index 48ef21686a..39ab0890a3 100755
--- a/ci/run_tests.sh
+++ b/ci/run_tests.sh
@@ -10,7 +10,7 @@ echo PYTHONHASHSEED=$PYTHONHASHSEED
 
 COVERAGE="-s --cov=pandas --cov-report=xml --cov-append --cov-config=pyproject.toml"
 
-PYTEST_CMD="MESONPY_EDITABLE_VERBOSE=1 PYTHONDEVMODE=1 PYTHONWARNDEFAULTENCODING=1 pytest -r fEs -n $PYTEST_WORKERS --dist=loadfile $TEST_ARGS $COVERAGE $PYTEST_TARGET"
+PYTEST_CMD="MESONPY_EDITABLE_VERBOSE=1 PYTHONDEVMODE=1 PYTHONWARNDEFAULTENCODING=1 pytest -r fE -n $PYTEST_WORKERS --dist=loadfile $TEST_ARGS $COVERAGE $PYTEST_TARGET"
 
 if [[ "$PATTERN" ]]; then
   PYTEST_CMD="$PYTEST_CMD -m \"$PATTERN\""
diff --git a/pandas/tests/reshape/merge/test_merge_ordered.py b/pandas/tests/reshape/merge/test_merge_ordered.py
index abd61026b4..0bd3ca3cf2 100644
--- a/pandas/tests/reshape/merge/test_merge_ordered.py
+++ b/pandas/tests/reshape/merge/test_merge_ordered.py
@@ -219,3 +219,26 @@ class TestMergeOrdered:
             ValueError, match=re.escape("fill_method must be 'ffill' or None")
         ):
             merge_ordered(left, right, on="key", fill_method=invalid_method)
+
+    def test_ffill_left_merge(self):
+        # GH 57010
+        df1 = DataFrame(
+            {
+                "key": ["a", "c", "e", "a", "c", "e"],
+                "lvalue": [1, 2, 3, 1, 2, 3],
+                "group": ["a", "a", "a", "b", "b", "b"],
+            }
+        )
+        df2 = DataFrame({"key": ["b", "c", "d"], "rvalue": [1, 2, 3]})
+        result = merge_ordered(
+            df1, df2, fill_method="ffill", left_by="group", how="left"
+        )
+        expected = DataFrame(
+            {
+                "key": ["a", "c", "e", "a", "c", "e"],
+                "lvalue": [1, 2, 3, 1, 2, 3],
+                "group": ["a", "a", "a", "b", "b", "b"],
+                "rvalue": [np.nan, 2.0, 2.0, np.nan, 2.0, 2.0],
+            }
+        )
+        tm.assert_frame_equal(result, expected)