Repository: geopandas/geopandas (python)

Issue:
DOC: contributing guide: point users to env file to install deps?
#### Is your feature request related to a problem?

No

#### Describe the solution you'd like

Switch https://docs.geopandas.org/en/latest/community/contributing.html#installing-dependencies to point to a "geopandas_dev" environment file and put it in e.g. https://github.com/geopandas/geopandas/blob/master/ci/envs

Rather than have a user do a few manual import steps e.g.

```
conda create -n geopandas_dev python
conda activate geopandas_dev
conda install -c conda-forge pandas fiona shapely pyproj rtree pytest
python -m pip install pre-commit
pre-commit install
```

Create a geopandas_dev file which has https://github.com/geopandas/geopandas/blob/master/ci/envs/39-latest-conda-forge.yaml in it as well as others e.g. pre-commit and have a user do `conda/mambe env update -f 39-latest-conda-forge.yaml`

#### Additional context

https://github.com/geopandas/geopandas/blob/master/ci/envs/39-latest-conda-forge.yaml could be used but it's named "test" and doesn't have pre-commit.



Pull request description:
DOC: contributing guide: point users to environment file to install deps
Updated existing dev environment file and documentation to resolve #2220 

Patch:
diff --git a/ci/envs/38-dev.yaml b/ci/envs/310-dev.yaml
similarity index 98%
rename from ci/envs/38-dev.yaml
rename to ci/envs/310-dev.yaml
index 55cffdea..abff1cfd 100644
--- a/ci/envs/38-dev.yaml
+++ b/ci/envs/310-dev.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - conda-forge
 dependencies:
-  - python=3.8
+  - python=3.10
   - cython
   # required
   - shapely
diff --git a/ci/envs/39-rc.yaml b/ci/envs/39-rc.yaml
new file mode 100644
index 00000000..8b080a54
--- /dev/null
+++ b/ci/envs/39-rc.yaml
@@ -0,0 +1,31 @@
+name: test
+channels:
+  - conda-forge
+dependencies:
+  - python=3.9
+  - cython
+  # required
+  - shapely
+  - fiona
+  - pyproj
+  # testing
+  - pytest
+  - pytest-cov
+  - pytest-xdist
+  - fsspec
+  # optional
+  - rtree
+  - matplotlib-base
+  - mapclassify
+  - folium
+  - xyzservices
+  - scipy
+  - geopy
+  #- geopy
+  - SQLalchemy
+  - libspatialite
+  - pyarrow
+  - pip
+  - pip:
+    # dev versions of packages
+    - git+https://github.com/pydata/pandas.git@1.4.x
diff --git a/doc/source/community/contributing.rst b/doc/source/community/contributing.rst
index dfae1e1e..21444db8 100644
--- a/doc/source/community/contributing.rst
+++ b/doc/source/community/contributing.rst
@@ -155,7 +155,23 @@ An easy way to create a *GeoPandas* development environment is as follows:
 - Make sure that you have :ref:`cloned the repository <contributing.forking>`
 - ``cd`` to the *geopandas** source directory
 
-Tell conda to create a new environment, named ``geopandas_dev``, or any other name you would like
+Using the provided environment
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+*GeoPandas* provides an environment which includes the required dependencies for development.
+The environment file is located in the top level of the repo and is named ``environment-dev.yml``.
+You can create this environment by navigating to the the *GeoPandas* source directory
+and running::
+
+      conda env create -f environment-dev.yml
+
+This will create a new conda environment named ``geopandas_dev``.
+
+Creating the environment manually
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Alternatively, it is possible to create a development environment manually.  To do this,
+tell conda to create a new environment named ``geopandas_dev``, or any other name you would like
 for this environment, by running::
 
       conda create -n geopandas_dev python
@@ -163,6 +179,9 @@ for this environment, by running::
 This will create the new environment, and not touch any of your existing environments,
 nor any existing python installation.
 
+Working with the environment
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 To work in this environment, you need to ``activate`` it. The instructions below
 should work for both Windows, Mac and Linux::
 
@@ -183,12 +202,14 @@ See the full conda docs `here <http://conda.pydata.org/docs>`__.
 
 At this point you can easily do a *development* install, as detailed in the next sections.
 
+
 3) Installing Dependencies
 --------------------------
 
 To run *GeoPandas* in an development environment, you must first install
-*GeoPandas*'s dependencies. We suggest doing so using the following commands
-(executed after your development environment has been activated)::
+*GeoPandas*'s dependencies. If you used the provided environment in section 2, skip this
+step and continue to section 4. If you created the environment manually, we suggest installing
+dependencies using the following commands (executed after your development environment has been activated)::
 
     conda install -c conda-forge pandas fiona shapely pyproj rtree pytest
 
@@ -314,8 +335,8 @@ to auto-format your code. Additionally, many editors have plugins that will
 apply ``black`` as you edit files.
 
 Optionally (but recommended), you can setup `pre-commit hooks <https://pre-commit.com/>`_
-to automatically run ``black`` and ``flake8`` when you make a git commit. This
-can be done by installing ``pre-commit``::
+to automatically run ``black`` and ``flake8`` when you make a git commit. If you did not
+use the provided development environment in ``environment-dev.yml``, you must first install ``pre-commit``::
 
    $ python -m pip install pre-commit
 
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 443c28e8..3ce5df8c 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -86,7 +86,7 @@ master_doc = "index"
 
 # General information about the project.
 project = u"GeoPandas"
-copyright = u"2013–2021, GeoPandas developers"
+copyright = u"2013–2022, GeoPandas developers"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/doc/source/docs/user_guide/aggregation_with_dissolve.rst b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
index f6952dd3..c8a10cd3 100644
--- a/doc/source/docs/user_guide/aggregation_with_dissolve.rst
+++ b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
@@ -75,3 +75,25 @@ However it also accepts other summary statistic options as allowed by :meth:`pan
 * 'sum'
 * 'mean'
 * 'median'
+* function
+* string function name
+* list of functions and/or function names, e.g. [np.sum, 'mean']
+* dict of axis labels -> functions, function names or list of such.
+
+For example, to get the number of contries on each continent, 
+as well as the populations of the largest and smallest country of each,
+we can aggregate the ``'name'`` column using ``'count'``,
+and the ``'pop_est'`` column using ``'min'`` and ``'max'``:
+
+.. ipython:: python
+
+    world = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+    continents = world.dissolve(
+        by="continent",
+        aggfunc={
+            "name": "count",
+            "pop_est": ["min", "max"],
+        },
+    )
+
+   continents.head()
\ No newline at end of file
diff --git a/environment-dev.yml b/environment-dev.yml
index 8b619a9a..b3413e20 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -2,31 +2,39 @@ name: geopandas-dev
 channels:
     - conda-forge
 dependencies:
+    - python
     # required
     - fiona>=1.8
     - pandas>=0.25
+    - pygeos
     - pyproj>=2.2.0
     - shapely>=1.6
+    # testing
+    - pytest>=3.1.0
+    - pytest-cov
+    - pytest-xdist
+    - fsspec
+    - codecov
+    # styling
+    - black
+    - pre-commit
 
+    # optional
+    - folium
+    - xyzservices
+    - scipy
+    - libspatialite
+    - geoalchemy2
+    - pyarrow
+    # doctest testing
+    - pytest-doctestplus
+    # geocoding
+    - geopy
     # geodatabase access
     - psycopg2>=2.5.1
     - SQLAlchemy>=0.8.3
-
-    # geocoding
-    - geopy
-
     # plotting
     - matplotlib>=2.2
     - mapclassify
-
-    # testing
-    - pytest>=3.1.0
-    - pytest-cov
-    - codecov
-
     # spatial access methods
     - rtree>=0.8
-
-    # styling
-    - black
-    - pre-commit
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index e5ac045d..f233eee8 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -19,6 +19,8 @@ PANDAS_GE_10 = str(pd.__version__) >= LooseVersion("1.0.0")
 PANDAS_GE_11 = str(pd.__version__) >= LooseVersion("1.1.0")
 PANDAS_GE_115 = str(pd.__version__) >= LooseVersion("1.1.5")
 PANDAS_GE_12 = str(pd.__version__) >= LooseVersion("1.2.0")
+PANDAS_GE_13 = str(pd.__version__) >= LooseVersion("1.3.0")
+PANDAS_GE_14 = str(pd.__version__) >= LooseVersion("1.4.0")
 
 
 # -----------------------------------------------------------------------------
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index e29b2213..25c27933 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1476,6 +1476,12 @@ individually so that features may have different properties
         aggfunc : function or string, default "first"
             Aggregation function for manipulation of data associated
             with each group. Passed to pandas `groupby.agg` method.
+            Accepted combinations are:
+
+            - function
+            - string function name
+            - list of functions and/or function names, e.g. [np.sum, 'mean']
+            - dict of axis labels -> functions, function names or list of such.
         as_index : boolean, default True
             If true, groupby columns become index of result.
         level : int or str or sequence of int or sequence of str, default None
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index e17a4a3a..ba6ef8ae 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -860,7 +860,8 @@ GON (((-122.84000 49.00000, -120.0000...
             **style_kwds,
         )
 
-    if missing_kwds is not None and not expl_series[nan_idx].empty:
+    missing_data = not expl_series[nan_idx].empty
+    if missing_kwds is not None and missing_data:
         if color:
             if "color" not in missing_kwds:
                 missing_kwds["color"] = color
@@ -902,7 +903,7 @@ GON (((-122.84000 49.00000, -120.0000...
                         markeredgewidth=0,
                     )
                 )
-            if missing_kwds is not None:
+            if missing_kwds is not None and missing_data:
                 if "color" in merged_kwds:
                     merged_kwds["facecolor"] = merged_kwds["color"]
                 patches.append(
diff --git a/readthedocs.yml b/readthedocs.yml
index 6e710093..af979a2f 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,9 +1,12 @@
 version: 2
-formats: []
-conda:
-    environment: doc/environment.yml
+build:
+  os: ubuntu-20.04
+  tools:
+    python: mambaforge-4.10
 python:
-    version: 3
-    install:
-        - method: pip
-          path: .
+  install:
+  - method: pip
+    path: .
+conda:
+  environment: doc/environment.yml
+formats: []


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 6de9213a..e86739a3 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -60,7 +60,10 @@ jobs:
             os: windows-latest
             postgis: false
             dev: false
-          - env: ci/envs/38-dev.yaml
+          - env: ci/envs/39-rc.yaml
+            os: ubuntu-latest
+            dev: true
+          - env: ci/envs/310-dev.yaml
             os: ubuntu-latest
             dev: true
 
diff --git a/ci/envs/38-latest-conda-forge.yaml b/ci/envs/38-latest-conda-forge.yaml
index cf84fcf9..340d4b80 100644
--- a/ci/envs/38-latest-conda-forge.yaml
+++ b/ci/envs/38-latest-conda-forge.yaml
@@ -4,7 +4,7 @@ channels:
 dependencies:
   - python=3.8
   # required
-  - pandas=1.3.2  # temporary pin because 1.3.3 has regression for overlay (GH2101)
+  - pandas=1.2
   - shapely
   - fiona
   - pyproj
diff --git a/ci/envs/39-latest-conda-forge.yaml b/ci/envs/39-latest-conda-forge.yaml
index 05825b20..74795607 100644
--- a/ci/envs/39-latest-conda-forge.yaml
+++ b/ci/envs/39-latest-conda-forge.yaml
@@ -4,7 +4,7 @@ channels:
 dependencies:
   - python=3.9
   # required
-  - pandas
+  - pandas=1.3
   - shapely
   - fiona
   - pyproj
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 709ad3da..c6d04209 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -445,33 +445,44 @@ def test_unique():
     assert_array_equal(s.unique(), exp)
 
 
+def pd14_compat_index(index):
+    if compat.PANDAS_GE_14:
+        return from_shapely(index)
+    else:
+        return index
+
+
 def test_value_counts():
     # each object is considered unique
     s = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)])
     res = s.value_counts()
     with compat.ignore_shapely2_warnings():
-        exp = pd.Series([2, 1], index=[Point(0, 0), Point(1, 1)])
+        exp = pd.Series([2, 1], index=pd14_compat_index([Point(0, 0), Point(1, 1)]))
     assert_series_equal(res, exp)
     # Check crs doesn't make a difference - note it is not kept in output index anyway
     s2 = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)], crs="EPSG:4326")
     res2 = s2.value_counts()
     assert_series_equal(res2, exp)
+    if compat.PANDAS_GE_14:
+        # TODO should/ can we fix CRS being lost
+        assert s2.value_counts().index.array.crs is None
 
     # check mixed geometry
     s3 = GeoSeries([Point(0, 0), LineString([[1, 1], [2, 2]]), Point(0, 0)])
     res3 = s3.value_counts()
+    index = pd14_compat_index([Point(0, 0), LineString([[1, 1], [2, 2]])])
     with compat.ignore_shapely2_warnings():
-        exp3 = pd.Series([2, 1], index=[Point(0, 0), LineString([[1, 1], [2, 2]])])
+        exp3 = pd.Series([2, 1], index=index)
     assert_series_equal(res3, exp3)
 
     # check None is handled
     s4 = GeoSeries([Point(0, 0), None, Point(0, 0)])
     res4 = s4.value_counts(dropna=True)
     with compat.ignore_shapely2_warnings():
-        exp4_dropna = pd.Series([2], index=[Point(0, 0)])
+        exp4_dropna = pd.Series([2], index=pd14_compat_index([Point(0, 0)]))
     assert_series_equal(res4, exp4_dropna)
     with compat.ignore_shapely2_warnings():
-        exp4_keepna = pd.Series([2, 1], index=[Point(0, 0), None])
+        exp4_keepna = pd.Series([2, 1], index=pd14_compat_index([Point(0, 0), None]))
     res4_keepna = s4.value_counts(dropna=False)
     assert_series_equal(res4_keepna, exp4_keepna)
 
@@ -547,6 +558,44 @@ def test_groupby_groups(df):
     assert_frame_equal(res, exp)
 
 
+@pytest.mark.skipif(
+    compat.PANDAS_GE_13 and not compat.PANDAS_GE_14,
+    reason="this was broken in pandas 1.3.5 (GH-2294)",
+)
+@pytest.mark.parametrize("crs", [None, "EPSG:4326"])
+def test_groupby_metadata(crs):
+    # https://github.com/geopandas/geopandas/issues/2294
+    df = GeoDataFrame(
+        {
+            "geometry": [Point(0, 0), Point(1, 1), Point(0, 0)],
+            "value1": np.arange(3, dtype="int64"),
+            "value2": np.array([1, 2, 1], dtype="int64"),
+        },
+        crs=crs,
+    )
+
+    # dummy test asserting we can access the crs
+    def func(group):
+        assert isinstance(group, GeoDataFrame)
+        assert group.crs == crs
+
+    df.groupby("value2").apply(func)
+
+    # actual test with functionality
+    res = df.groupby("value2").apply(
+        lambda x: geopandas.sjoin(x, x[["geometry", "value1"]], how="inner")
+    )
+
+    expected = (
+        df.take([0, 2, 0, 2, 1])
+        .set_index("value2", drop=False, append=True)
+        .swaplevel()
+        .rename(columns={"value1": "value1_left"})
+        .assign(value1_right=[0, 0, 2, 2, 1])
+    )
+    assert_geodataframe_equal(res.drop(columns=["index_right"]), expected)
+
+
 def test_apply(s):
     # function that returns geometry preserves GeoSeries class
     def geom_func(geom):
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index d1d88519..55016870 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -404,7 +404,7 @@ class TestPointPlotting:
         ):
             self.df.plot(column="cats", categories=["cat1"])
 
-    def test_misssing(self):
+    def test_missing(self):
         self.df.loc[0, "values"] = np.nan
         ax = self.df.plot("values")
         cmap = plt.get_cmap()
@@ -428,6 +428,12 @@ class TestPointPlotting:
         np.testing.assert_array_equal(point_colors[0], leg_colors[0])
         np.testing.assert_array_equal(nan_color[0], leg_colors1[0])
 
+    def test_no_missing_and_missing_kwds(self):
+        # GH2210
+        df = self.df.copy()
+        df["category"] = df["values"].astype("str")
+        df.plot("category", missing_kwds={"facecolor": "none"}, legend=True)
+
 
 class TestPointZPlotting:
     def setup_method(self):