diff --git a/flicker/dataframe.py b/flicker/dataframe.py
index 3745256..994db3b 100644
--- a/flicker/dataframe.py
+++ b/flicker/dataframe.py
@@ -1142,19 +1142,57 @@ class FlickerDataFrame(object):
         return df.agg({name: 'min'}).collect()[0][0]
 
     def rows_with_max(self, name, ignore_nan=True):
-        # Note the plural. This is because a large number of rows can have
-        # the max value.
-        # Note that this is one of those places where NaN behaves differently
-        # than null.
+        """Return a dataframe containing only the rows that correspond to the
+        maximum value of the specified column name.
+
+        CAVEATS:
+        (1) Note the plural. This is because a large number of rows can have
+            the max value.
+        (2) Note that this is one of those places where NaN behaves differently
+            than null.
+
+        Parameters
+        ----------
+        name: str
+            Column name whose maximum values are needed
+        ignore_nan: bool
+            If True, np.nan is ignored. Note that null (or None) is
+            already automatically ignored by pyspark. This argument is only
+            useful when the column is double or float dtype. If the column is
+            not double or float, we ignore this argument.
+
+        Returns
+        -------
+            FlickerDataFrame
+        """
         max_value = self.max(name=name, ignore_nan=ignore_nan)
         out = self._df[self._df[name].isin([max_value])]
         return self.__class__(out)
 
     def rows_with_min(self, name, ignore_nan=True):
-        # Note the plural. This is because a large number of rows can have
-        # the min value.
-        # Note that this is one of those places where NaN behaves differently
-        # than null.
+        """Return a dataframe containing only the rows that correspond to the
+        minimum value of the specified column name.
+
+        CAVEATS:
+        (1) Note the plural. This is because a large number of rows can have
+            the max value.
+        (2) Note that this is one of those places where NaN behaves differently
+            than null.
+
+        Parameters
+        ----------
+        name: str
+            Column name whose minimum values are needed
+        ignore_nan: bool
+            If True, np.nan is ignored. Note that null (or None) is
+            already automatically ignored by pyspark. This argument is only
+            useful when the column is double or float dtype. If the column is
+            not double or float, we ignore this argument.
+
+        Returns
+        -------
+            FlickerDataFrame
+        """
         min_value = self.min(name=name, ignore_nan=ignore_nan)
         out = self._df[self._df[name].isin([min_value])]
         return self.__class__(out)
@@ -1467,8 +1505,49 @@ class FlickerDataFrame(object):
 
     def join(self, other, on=None, how='inner',
              lsuffix=None, rsuffix=None, lprefix=None, rprefix=None):
-        # Note that any None column value is not matched on. It's ignored.
+        """Join with another FlickerDataFrame. This function provides a new
+        and arguably better API than pyspark.sql.DataFrame.join.
+
+        In x.join(y), x is considered the left dataframe and y is the right
+        dataframe.
 
+        The suffix and prefix arguments to this function are useful to prevent
+        duplicate column names in the joined dataframe.
+
+        Note that any None column value is not matched on. It's ignored.
+
+        Parameters
+        ----------
+        other: FlickerDataFrame
+        on: None or str or list of str or dict[str, str]
+            The join condition. If this is None, we pass it as-is to the
+            underlying pyspark.sql.DataFrame.join. If this is a str or list
+            of str, we interpret this as the column name(s) to join on. These
+            column names must exist in both dataframes. If this is a
+            dict (recommended), then the keys are considered the column names
+            in the left dataframe and values are considered the column names
+            in the right dataframe.
+        how: str
+            Default is "inner". Must be one of: "inner", "cross", "outer",
+            "full", "full_outer", "left", "left_outer", "right", "right_outer",
+            "left_semi", and "left_anti".
+        lsuffix: str or None
+            If non-None, this suffix is affixed to all the column names of the
+            left dataframe before joining.
+        rsuffix: str or None
+            If non-None, this suffix is affixed to all the column names of the
+            right dataframe before joining.
+        lprefix: str or None
+            If non-None, this prefix is affixed to all the column names of the
+            left dataframe before joining.
+        rprefix: str or None
+            If non-None, this prefix is affixed to all the column names of the
+            right dataframe before joining.
+
+        Returns
+        -------
+            FlickerDataFrame
+        """
         def _validate_prefix_suffix(value, name):
             if (value is not None) and \
                     (not isinstance(value, six.string_types)):
@@ -1906,6 +1985,41 @@ class FlickerDataFrame(object):
         """
         return FlickerGroupedData(self._df.groupBy(*items))
 
+    def union(self, other):
+        """Return a new dataframe that is a union of the current and other
+        dataframes. This function is a wrapper over
+        pyspark.sql.DataFrame.union.
+
+        This function is also similar to pandas.concat but is a poorer version
+        that resolved columns by position only.
+
+        This is equivalent to `UNION ALL` in SQL. As is standard in SQL, this
+        function resolves columns by position (not by name).
+
+        Parameters
+        ----------
+        other: FlickerDataFrame
+
+        Returns
+        -------
+            FlickerDataFrame
+
+        See Also
+        --------
+        FlickerDataFrame.concat
+        """
+        if not isinstance(other, FlickerDataFrame):
+            msg = ('other must be a FlickerDataFrame but you provided '
+                   'type(other)={}')
+            raise TypeError(msg.format(str(type(other))))
+
+        out = self._df.union(other._df)
+        if isinstance(out, pyspark.sql.DataFrame):
+            out = self.__class__(out)
+        return out
+
+    concat = union
+
     @property
     def write(self):
         """
diff --git a/flicker/version.py b/flicker/version.py
index 88b6962..fb02749 100644
--- a/flicker/version.py
+++ b/flicker/version.py
@@ -13,4 +13,4 @@
 #    limitations under the License.
 #
 
-__version__ = '0.0.14'
+__version__ = '0.0.15'

Add docstrings and tests for `rows_with_max` and `rows_with_min`
diff --git a/flicker/tests/test_union_concat.py b/flicker/tests/test_union_concat.py
new file mode 100644
index 0000000..631b259
--- /dev/null
+++ b/flicker/tests/test_union_concat.py
@@ -0,0 +1,29 @@
+# Copyright 2020 Flicker Contributors
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import division
+from builtins import range
+
+import pytest
+from flicker import FlickerDataFrame
+
+
+def test_union_and_concat(spark):
+    x = FlickerDataFrame.from_shape(spark, 2, 4,
+                                    ["year", "age", "name", "case"])
+    assert isinstance(x.union(x), FlickerDataFrame)
+    assert isinstance(x.concat(x), FlickerDataFrame)

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov
apt-get update apt-get install -y gcc apt-get install -y default-jdk
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
exceptiongroup==1.2.2
-e git+https://github.com/ankur-gupta/flicker.git@f7ca539092e9e14104d8f74eef03c65667305b8f#egg=flicker
importlib-metadata==6.7.0
iniconfig==2.0.0
numpy==1.21.6
packaging==24.0
pandas==1.3.5
pluggy==1.2.0
py4j==0.10.9.7
pyspark==3.4.4
pytest==7.4.4
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
pytz==2025.2
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
zipp==3.15.0

name: flicker
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - numpy==1.21.6
      - packaging==24.0
      - pandas==1.3.5
      - pluggy==1.2.0
      - py4j==0.10.9.7
      - pyspark==3.4.4
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/flicker

flicker/tests/test_union_concat.py::test_union_and_concat
flicker/version.py flicker/dataframe.py
flicker/version.py flicker/dataframe.py