blob: ed03cedb097162c54da161af621e673992c2231f [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#
import unittest
import logging
from pyspark.sql import functions as sf
from pyspark.sql.types import Row
from pyspark.sql.window import Window
from pyspark.testing.sqlutils import ReusedSQLTestCase
from pyspark.testing.utils import (
assertDataFrameEqual,
have_pandas,
have_pyarrow,
pandas_requirement_message,
pyarrow_requirement_message,
)
from pyspark.util import is_remote_only
if have_pandas:
import pandas as pd
if have_pyarrow:
import pyarrow as pa # noqa: F401
@unittest.skipIf(
not have_pandas or not have_pyarrow,
pandas_requirement_message or pyarrow_requirement_message,
)
class CogroupedApplyInPandasMiscTestsMixin:
def test_with_window_function(self):
# SPARK-42168: a window function with same partition keys but differing key order
ids = 2
days = 100
vals = 10000
parts = 10
id_df = self.spark.range(ids)
day_df = self.spark.range(days).withColumnRenamed("id", "day")
vals_df = self.spark.range(vals).withColumnRenamed("id", "value")
df = id_df.join(day_df).join(vals_df)
left_df = df.withColumnRenamed("value", "left").repartition(parts).cache()
# SPARK-42132: this bug requires us to alias all columns from df here
right_df = (
df.select(
sf.col("id").alias("id"), sf.col("day").alias("day"), sf.col("value").alias("right")
)
.repartition(parts)
.cache()
)
# note the column order is different to the groupBy("id", "day") column order below
window = Window.partitionBy("day", "id")
left_grouped_df = left_df.groupBy("id", "day")
right_grouped_df = right_df.withColumn(
"day_sum", sf.sum(sf.col("day")).over(window)
).groupBy("id", "day")
def cogroup(left: "pd.DataFrame", right: "pd.DataFrame") -> "pd.DataFrame":
return pd.DataFrame(
[
{
"id": (
left["id"][0]
if not left.empty
else (right["id"][0] if not right.empty else None)
),
"day": (
left["day"][0]
if not left.empty
else (right["day"][0] if not right.empty else None)
),
"lefts": len(left.index),
"rights": len(right.index),
}
]
)
df = left_grouped_df.cogroup(right_grouped_df).applyInPandas(
cogroup, schema="id long, day long, lefts integer, rights integer"
)
actual = df.orderBy("id", "day").take(days)
self.assertEqual(actual, [Row(0, day, vals, vals) for day in range(days)])
def test_with_local_data(self):
df1 = self.spark.createDataFrame(
[(1, 1.0, "a"), (2, 2.0, "b"), (1, 3.0, "c"), (2, 4.0, "d")], ("id", "v1", "v2")
)
df2 = self.spark.createDataFrame([(1, "x"), (2, "y"), (1, "z")], ("id", "v3"))
def summarize(left, right):
return pd.DataFrame(
{
"left_rows": [len(left)],
"left_columns": [len(left.columns)],
"right_rows": [len(right)],
"right_columns": [len(right.columns)],
}
)
df = (
df1.groupby("id")
.cogroup(df2.groupby("id"))
.applyInPandas(
summarize,
schema="left_rows long, left_columns long, right_rows long, right_columns long",
)
)
self.assertEqual(
df._show_string(),
"+---------+------------+----------+-------------+\n"
"|left_rows|left_columns|right_rows|right_columns|\n"
"+---------+------------+----------+-------------+\n"
"| 2| 3| 2| 2|\n"
"| 2| 3| 1| 2|\n"
"+---------+------------+----------+-------------+\n",
)
def test_arrow_batch_slicing(self):
m, n = 100000, 10000
df1 = self.spark.range(m).select((sf.col("id") % 2).alias("key"), sf.col("id").alias("v"))
cols = {f"col_{i}": sf.col("v") + i for i in range(10)}
df1 = df1.withColumns(cols)
df2 = self.spark.range(n).select((sf.col("id") % 4).alias("key"), sf.col("id").alias("v"))
cols = {f"col_{i}": sf.col("v") + i for i in range(20)}
df2 = df2.withColumns(cols)
def summarize(key, left, right):
assert len(left) == m / 2 or len(left) == 0, len(left)
assert len(right) == n / 4, len(right)
return pd.DataFrame(
{
"key": [key[0]],
"left_rows": [len(left)],
"left_columns": [len(left.columns)],
"right_rows": [len(right)],
"right_columns": [len(right.columns)],
}
)
schema = "key long, left_rows long, left_columns long, right_rows long, right_columns long"
expected = [
Row(key=0, left_rows=m / 2, left_columns=12, right_rows=n / 4, right_columns=22),
Row(key=1, left_rows=m / 2, left_columns=12, right_rows=n / 4, right_columns=22),
Row(key=2, left_rows=0, left_columns=12, right_rows=n / 4, right_columns=22),
Row(key=3, left_rows=0, left_columns=12, right_rows=n / 4, right_columns=22),
]
for maxRecords, maxBytes in [(1000, 2**31 - 1), (0, 4096), (1000, 4096)]:
with self.subTest(maxRecords=maxRecords, maxBytes=maxBytes):
with self.sql_conf(
{
"spark.sql.execution.arrow.maxRecordsPerBatch": maxRecords,
"spark.sql.execution.arrow.maxBytesPerBatch": maxBytes,
}
):
result = (
df1.groupby("key")
.cogroup(df2.groupby("key"))
.applyInPandas(summarize, schema=schema)
.sort("key")
.collect()
)
self.assertEqual(expected, result)
@unittest.skipIf(is_remote_only(), "Requires JVM access")
def test_cogroup_apply_in_pandas_with_logging(self):
import pandas as pd
def func_with_logging(left_pdf, right_pdf):
assert isinstance(left_pdf, pd.DataFrame)
assert isinstance(right_pdf, pd.DataFrame)
logger = logging.getLogger("test_pandas_cogrouped_map")
logger.warning(
f"pandas cogrouped map: {dict(v1=list(left_pdf['v1']), v2=list(right_pdf['v2']))}"
)
return pd.merge(left_pdf, right_pdf, on=["id"])
left_df = self.spark.createDataFrame([(1, 10), (2, 20), (1, 30)], ["id", "v1"])
right_df = self.spark.createDataFrame([(1, 100), (2, 200), (1, 300)], ["id", "v2"])
grouped_left = left_df.groupBy("id")
grouped_right = right_df.groupBy("id")
cogrouped_df = grouped_left.cogroup(grouped_right)
with self.sql_conf({"spark.sql.pyspark.worker.logging.enabled": "true"}):
assertDataFrameEqual(
cogrouped_df.applyInPandas(func_with_logging, "id long, v1 long, v2 long"),
[Row(id=1, v1=v1, v2=v2) for v1 in [10, 30] for v2 in [100, 300]]
+ [Row(id=2, v1=20, v2=200)],
)
logs = self.spark.tvf.python_worker_logs()
assertDataFrameEqual(
logs.select("level", "msg", "context", "logger"),
[
Row(
level="WARNING",
msg=f"pandas cogrouped map: {dict(v1=v1, v2=v2)}",
context={"func_name": func_with_logging.__name__},
logger="test_pandas_cogrouped_map",
)
for v1, v2 in [([10, 30], [100, 300]), ([20], [200])]
],
)
class CogroupedApplyInPandasMiscTests(CogroupedApplyInPandasMiscTestsMixin, ReusedSQLTestCase):
pass
if __name__ == "__main__":
from pyspark.testing import main
main()