Repository: geopandas/geopandas

Issue:
ENH: expose points_from_xy as a GeoSeries method
> We have currently only one function of a similar nature - `geopandas.points_from_xy` which stands a bit out of general API. It feels ideal to have these functions exposed as `GeoSeries.from_wkt()` rather than top-level API as the one for points. 

We should probably think about exposing `points_from_xy` as a `GeoSeries.from_..` constructor as well. One reason that I like it being top-level (and returning an array and not GeoSeries) is that you have less concern about indexes. For example if you currently do:

```
geopandas.GeoDataFrame(
    {'col': [1, 2]}, geometry=geopandas.points_from_xy([1, 2], [3, 4]), index=['a', 'b']
)
```

this currently works. But if `points_from_xy` would return a GeoSeries, the above would no longer work (or at least, you would have to rewrite it to keep it working). 

Now, I think it's certainly still useful to have those `from_..` methods on GeoSeries as well (and the fact that those accept a Series and then preserve the index certainly helps.

_Originally posted by @jorisvandenbossche in https://github.com/geopandas/geopandas/issues/1710#issuecomment-787473730_


Discussion:

Resolving patch:
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -450,7 +450,8 @@ def from_xy(cls, x, y, z=None, index=None, crs=None, **kwargs):
         ----------
         x, y, z : iterable
         index : array-like or Index, optional
-            The index for the GeoSeries.
+            The index for the GeoSeries. If not given and all coordinate inputs
+            are Series with an equal index, that index is used.
         crs : value, optional
             Coordinate Reference System of the geometry objects. Can be anything
             accepted by
@@ -481,6 +482,14 @@ def from_xy(cls, x, y, z=None, index=None, crs=None, **kwargs):
         2    POINT (-3.00000 1.50000)
         dtype: geometry
         """
+        if index is None:
+            if (
+                isinstance(x, Series)
+                and isinstance(y, Series)
+                and x.index.equals(y.index)
+                and (z is None or (isinstance(z, Series) and x.index.equals(z.index)))
+            ):  # check if we can reuse index
+                index = x.index
         return cls(points_from_xy(x, y, z, crs=crs), index=index, crs=crs, **kwargs)
 
     @classmethod


Test patch:
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -331,6 +331,35 @@ def test_from_xy_points(self):
         assert_geoseries_equal(
             self.landmarks, GeoSeries.from_xy(x, y, index=index, crs=crs)
         )
+        assert_geoseries_equal(
+            self.landmarks,
+            GeoSeries.from_xy(self.landmarks.x, self.landmarks.y, crs=crs),
+        )
+
+    def test_from_xy_points_w_z(self):
+        index_values = [5, 6, 7]
+        x = pd.Series([0, -1, 2], index=index_values)
+        y = pd.Series([8, 3, 1], index=index_values)
+        z = pd.Series([5, -6, 7], index=index_values)
+        expected = GeoSeries(
+            [Point(0, 8, 5), Point(-1, 3, -6), Point(2, 1, 7)], index=index_values
+        )
+        assert_geoseries_equal(expected, GeoSeries.from_xy(x, y, z))
+
+    def test_from_xy_points_unequal_index(self):
+        x = self.landmarks.x
+        y = self.landmarks.y
+        y.index = -np.arange(len(y))
+        crs = self.landmarks.crs
+        assert_geoseries_equal(
+            self.landmarks, GeoSeries.from_xy(x, y, index=x.index, crs=crs)
+        )
+        unindexed_landmarks = self.landmarks.copy()
+        unindexed_landmarks.reset_index(inplace=True, drop=True)
+        assert_geoseries_equal(
+            unindexed_landmarks,
+            GeoSeries.from_xy(x, y, crs=crs),
+        )
 
     def test_from_xy_points_indexless(self):
         x = np.array([0.0, 3.0])