This is an automated email from the ASF dual-hosted git repository.
timsaucer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-python.git
The following commit(s) were added to refs/heads/main by this push:
new 4a6c4d1 refactor: dataframe `join` params (#912)
4a6c4d1 is described below
commit 4a6c4d129af3e1eb207f64ee84285419afb26876
Author: Ion Koutsouris <[email protected]>
AuthorDate: Fri Nov 8 15:06:27 2024 +0100
refactor: dataframe `join` params (#912)
* refactor: dataframe join params
* chore: add description for on params
* fix type
* chore: change join param
* chore: update join params in tpch
* oops
* chore: final change
* Add support for join_keys as a positional argument
---------
Co-authored-by: Tim Saucer <[email protected]>
---
docs/source/user-guide/common-operations/joins.rst | 10 +--
examples/tpch/_tests.py | 6 +-
examples/tpch/q02_minimum_cost_supplier.py | 12 ++-
examples/tpch/q03_shipping_priority.py | 6 +-
examples/tpch/q04_order_priority_checking.py | 4 +-
examples/tpch/q05_local_supplier_volume.py | 13 +--
examples/tpch/q07_volume_shipping.py | 12 +--
examples/tpch/q08_market_share.py | 14 ++--
examples/tpch/q09_product_type_profit_measure.py | 13 +--
examples/tpch/q10_returned_item_reporting.py | 6 +-
.../tpch/q11_important_stock_identification.py | 6 +-
examples/tpch/q12_ship_mode_order_priority.py | 2 +-
examples/tpch/q13_customer_distribution.py | 4 +-
examples/tpch/q14_promotion_effect.py | 4 +-
examples/tpch/q15_top_supplier.py | 2 +-
examples/tpch/q16_part_supplier_relationship.py | 6 +-
examples/tpch/q17_small_quantity_order.py | 2 +-
examples/tpch/q18_large_volume_customer.py | 4 +-
examples/tpch/q19_discounted_revenue.py | 2 +-
examples/tpch/q20_potential_part_promotion.py | 11 ++-
examples/tpch/q21_suppliers_kept_orders_waiting.py | 8 +-
examples/tpch/q22_global_sales_opportunity.py | 2 +-
python/datafusion/dataframe.py | 95 ++++++++++++++++++++--
python/tests/test_dataframe.py | 66 ++++++++++++++-
src/dataframe.rs | 15 +---
25 files changed, 240 insertions(+), 85 deletions(-)
diff --git a/docs/source/user-guide/common-operations/joins.rst
b/docs/source/user-guide/common-operations/joins.rst
index 09fa145..40d9221 100644
--- a/docs/source/user-guide/common-operations/joins.rst
+++ b/docs/source/user-guide/common-operations/joins.rst
@@ -56,7 +56,7 @@ will be included in the resulting DataFrame.
.. ipython:: python
- left.join(right, join_keys=(["customer_id"], ["id"]), how="inner")
+ left.join(right, left_on="customer_id", right_on="id", how="inner")
The parameter ``join_keys`` specifies the columns from the left DataFrame and
right DataFrame that contains the values
that should match.
@@ -70,7 +70,7 @@ values for the corresponding columns.
.. ipython:: python
- left.join(right, join_keys=(["customer_id"], ["id"]), how="left")
+ left.join(right, left_on="customer_id", right_on="id", how="left")
Full Join
---------
@@ -80,7 +80,7 @@ is no match. Unmatched rows will have null values.
.. ipython:: python
- left.join(right, join_keys=(["customer_id"], ["id"]), how="full")
+ left.join(right, left_on="customer_id", right_on="id", how="full")
Left Semi Join
--------------
@@ -90,7 +90,7 @@ omitting duplicates with multiple matches in the right table.
.. ipython:: python
- left.join(right, join_keys=(["customer_id"], ["id"]), how="semi")
+ left.join(right, left_on="customer_id", right_on="id", how="semi")
Left Anti Join
--------------
@@ -101,4 +101,4 @@ the right table.
.. ipython:: python
- left.join(right, join_keys=(["customer_id"], ["id"]), how="anti")
\ No newline at end of file
+ left.join(right, left_on="customer_id", right_on="id", how="anti")
\ No newline at end of file
diff --git a/examples/tpch/_tests.py b/examples/tpch/_tests.py
index 903b535..13144ae 100644
--- a/examples/tpch/_tests.py
+++ b/examples/tpch/_tests.py
@@ -18,7 +18,7 @@
import pytest
from importlib import import_module
import pyarrow as pa
-from datafusion import col, lit, functions as F
+from datafusion import DataFrame, col, lit, functions as F
from util import get_answer_file
@@ -94,7 +94,7 @@ def check_q17(df):
)
def test_tpch_query_vs_answer_file(query_code: str, answer_file: str):
module = import_module(query_code)
- df = module.df
+ df: DataFrame = module.df
# Treat q17 as a special case. The answer file does not match the spec.
# Running at scale factor 1, we have manually verified this result does
@@ -121,5 +121,5 @@ def test_tpch_query_vs_answer_file(query_code: str,
answer_file: str):
cols = list(read_schema.names)
- assert df.join(df_expected, (cols, cols), "anti").count() == 0
+ assert df.join(df_expected, on=cols, how="anti").count() == 0
assert df.count() == df_expected.count()
diff --git a/examples/tpch/q02_minimum_cost_supplier.py
b/examples/tpch/q02_minimum_cost_supplier.py
index 2440fda..c4ccf8a 100644
--- a/examples/tpch/q02_minimum_cost_supplier.py
+++ b/examples/tpch/q02_minimum_cost_supplier.py
@@ -80,16 +80,20 @@ df_region = df_region.filter(col("r_name") ==
lit(REGION_OF_INTEREST))
# Now that we have the region, find suppliers in that region. Suppliers are
tied to their nation
# and nations are tied to the region.
-df_nation = df_nation.join(df_region, (["n_regionkey"], ["r_regionkey"]),
how="inner")
+df_nation = df_nation.join(
+ df_region, left_on=["n_regionkey"], right_on=["r_regionkey"], how="inner"
+)
df_supplier = df_supplier.join(
- df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner"
+ df_nation, left_on=["s_nationkey"], right_on=["n_nationkey"], how="inner"
)
# Now that we know who the potential suppliers are for the part, we can limit
out part
# supplies table down. We can further join down to the specific parts we've
identified
# as matching the request
-df = df_partsupp.join(df_supplier, (["ps_suppkey"], ["s_suppkey"]),
how="inner")
+df = df_partsupp.join(
+ df_supplier, left_on=["ps_suppkey"], right_on=["s_suppkey"], how="inner"
+)
# Locate the minimum cost across all suppliers. There are multiple ways you
could do this,
# but one way is to create a window function across all suppliers, find the
minimum, and
@@ -111,7 +115,7 @@ df = df.with_column(
df = df.filter(col("min_cost") == col("ps_supplycost"))
-df = df.join(df_part, (["ps_partkey"], ["p_partkey"]), how="inner")
+df = df.join(df_part, left_on=["ps_partkey"], right_on=["p_partkey"],
how="inner")
# From the problem statement, these are the values we wish to output
diff --git a/examples/tpch/q03_shipping_priority.py
b/examples/tpch/q03_shipping_priority.py
index c4e8f46..5ebab13 100644
--- a/examples/tpch/q03_shipping_priority.py
+++ b/examples/tpch/q03_shipping_priority.py
@@ -55,9 +55,9 @@ df_lineitem = df_lineitem.filter(col("l_shipdate") >
lit(DATE_OF_INTEREST))
# Join all 3 dataframes
-df = df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]),
how="inner").join(
- df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner"
-)
+df = df_customer.join(
+ df_orders, left_on=["c_custkey"], right_on=["o_custkey"], how="inner"
+).join(df_lineitem, left_on=["o_orderkey"], right_on=["l_orderkey"],
how="inner")
# Compute the revenue
diff --git a/examples/tpch/q04_order_priority_checking.py
b/examples/tpch/q04_order_priority_checking.py
index f10b74d..8bf02cb 100644
--- a/examples/tpch/q04_order_priority_checking.py
+++ b/examples/tpch/q04_order_priority_checking.py
@@ -66,7 +66,9 @@ df_orders = df_orders.filter(col("o_orderdate") >=
lit(date)).filter(
)
# Perform the join to find only orders for which there are lineitems outside
of expected range
-df = df_orders.join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner")
+df = df_orders.join(
+ df_lineitem, left_on=["o_orderkey"], right_on=["l_orderkey"], how="inner"
+)
# Based on priority, find the number of entries
df = df.aggregate(
diff --git a/examples/tpch/q05_local_supplier_volume.py
b/examples/tpch/q05_local_supplier_volume.py
index 2a83d2d..413a4ac 100644
--- a/examples/tpch/q05_local_supplier_volume.py
+++ b/examples/tpch/q05_local_supplier_volume.py
@@ -76,15 +76,18 @@ df_region = df_region.filter(col("r_name") ==
lit(REGION_OF_INTEREST))
# Join all the dataframes
df = (
- df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]), how="inner")
- .join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner")
+ df_customer.join(
+ df_orders, left_on=["c_custkey"], right_on=["o_custkey"], how="inner"
+ )
+ .join(df_lineitem, left_on=["o_orderkey"], right_on=["l_orderkey"],
how="inner")
.join(
df_supplier,
- (["l_suppkey", "c_nationkey"], ["s_suppkey", "s_nationkey"]),
+ left_on=["l_suppkey", "c_nationkey"],
+ right_on=["s_suppkey", "s_nationkey"],
how="inner",
)
- .join(df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner")
- .join(df_region, (["n_regionkey"], ["r_regionkey"]), how="inner")
+ .join(df_nation, left_on=["s_nationkey"], right_on=["n_nationkey"],
how="inner")
+ .join(df_region, left_on=["n_regionkey"], right_on=["r_regionkey"],
how="inner")
)
# Compute the final result
diff --git a/examples/tpch/q07_volume_shipping.py
b/examples/tpch/q07_volume_shipping.py
index a1d7d81..18c290d 100644
--- a/examples/tpch/q07_volume_shipping.py
+++ b/examples/tpch/q07_volume_shipping.py
@@ -90,20 +90,22 @@ df_nation = df_nation.with_column(
# Limit suppliers to either nation
df_supplier = df_supplier.join(
- df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner"
+ df_nation, left_on=["s_nationkey"], right_on=["n_nationkey"], how="inner"
).select(col("s_suppkey"), col("n_name").alias("supp_nation"))
# Limit customers to either nation
df_customer = df_customer.join(
- df_nation, (["c_nationkey"], ["n_nationkey"]), how="inner"
+ df_nation, left_on=["c_nationkey"], right_on=["n_nationkey"], how="inner"
).select(col("c_custkey"), col("n_name").alias("cust_nation"))
# Join up all the data frames from line items, and make sure the supplier and
customer are in
# different nations.
df = (
- df_lineitem.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner")
- .join(df_customer, (["o_custkey"], ["c_custkey"]), how="inner")
- .join(df_supplier, (["l_suppkey"], ["s_suppkey"]), how="inner")
+ df_lineitem.join(
+ df_orders, left_on=["l_orderkey"], right_on=["o_orderkey"], how="inner"
+ )
+ .join(df_customer, left_on=["o_custkey"], right_on=["c_custkey"],
how="inner")
+ .join(df_supplier, left_on=["l_suppkey"], right_on=["s_suppkey"],
how="inner")
.filter(col("cust_nation") != col("supp_nation"))
)
diff --git a/examples/tpch/q08_market_share.py
b/examples/tpch/q08_market_share.py
index 95fc0a8..7138ab6 100644
--- a/examples/tpch/q08_market_share.py
+++ b/examples/tpch/q08_market_share.py
@@ -89,27 +89,27 @@ df_regional_customers = df_region.filter(col("r_name") ==
customer_region)
# After this join we have all of the possible sales nations
df_regional_customers = df_regional_customers.join(
- df_nation, (["r_regionkey"], ["n_regionkey"]), how="inner"
+ df_nation, left_on=["r_regionkey"], right_on=["n_regionkey"], how="inner"
)
# Now find the possible customers
df_regional_customers = df_regional_customers.join(
- df_customer, (["n_nationkey"], ["c_nationkey"]), how="inner"
+ df_customer, left_on=["n_nationkey"], right_on=["c_nationkey"], how="inner"
)
# Next find orders for these customers
df_regional_customers = df_regional_customers.join(
- df_orders, (["c_custkey"], ["o_custkey"]), how="inner"
+ df_orders, left_on=["c_custkey"], right_on=["o_custkey"], how="inner"
)
# Find all line items from these orders
df_regional_customers = df_regional_customers.join(
- df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner"
+ df_lineitem, left_on=["o_orderkey"], right_on=["l_orderkey"], how="inner"
)
# Limit to the part of interest
df_regional_customers = df_regional_customers.join(
- df_part, (["l_partkey"], ["p_partkey"]), how="inner"
+ df_part, left_on=["l_partkey"], right_on=["p_partkey"], how="inner"
)
# Compute the volume for each line item
@@ -126,7 +126,7 @@ df_national_suppliers = df_nation.filter(col("n_name") ==
supplier_nation)
# Determine the suppliers by the limited nation key we have in our single row
df above
df_national_suppliers = df_national_suppliers.join(
- df_supplier, (["n_nationkey"], ["s_nationkey"]), how="inner"
+ df_supplier, left_on=["n_nationkey"], right_on=["s_nationkey"], how="inner"
)
# When we join to the customer dataframe, we don't want to confuse other
columns, so only
@@ -141,7 +141,7 @@ df_national_suppliers =
df_national_suppliers.select("s_suppkey")
# column only from suppliers in the nation we are evaluating.
df = df_regional_customers.join(
- df_national_suppliers, (["l_suppkey"], ["s_suppkey"]), how="left"
+ df_national_suppliers, left_on=["l_suppkey"], right_on=["s_suppkey"],
how="left"
)
# Use a case statement to compute the volume sold by suppliers in the nation
of interest
diff --git a/examples/tpch/q09_product_type_profit_measure.py
b/examples/tpch/q09_product_type_profit_measure.py
index 0295d30..aa47d76 100644
--- a/examples/tpch/q09_product_type_profit_measure.py
+++ b/examples/tpch/q09_product_type_profit_measure.py
@@ -65,13 +65,16 @@ df_nation =
ctx.read_parquet(get_data_path("nation.parquet")).select(
df = df_part.filter(F.strpos(col("p_name"), part_color) > lit(0))
# We have a series of joins that get us to limit down to the line items we need
-df = df.join(df_lineitem, (["p_partkey"], ["l_partkey"]), how="inner")
-df = df.join(df_supplier, (["l_suppkey"], ["s_suppkey"]), how="inner")
-df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner")
+df = df.join(df_lineitem, left_on=["p_partkey"], right_on=["l_partkey"],
how="inner")
+df = df.join(df_supplier, left_on=["l_suppkey"], right_on=["s_suppkey"],
how="inner")
+df = df.join(df_orders, left_on=["l_orderkey"], right_on=["o_orderkey"],
how="inner")
df = df.join(
- df_partsupp, (["l_suppkey", "l_partkey"], ["ps_suppkey", "ps_partkey"]),
how="inner"
+ df_partsupp,
+ left_on=["l_suppkey", "l_partkey"],
+ right_on=["ps_suppkey", "ps_partkey"],
+ how="inner",
)
-df = df.join(df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner")
+df = df.join(df_nation, left_on=["s_nationkey"], right_on=["n_nationkey"],
how="inner")
# Compute the intermediate values and limit down to the expressions we need
df = df.select(
diff --git a/examples/tpch/q10_returned_item_reporting.py
b/examples/tpch/q10_returned_item_reporting.py
index 25f81b2..94b398c 100644
--- a/examples/tpch/q10_returned_item_reporting.py
+++ b/examples/tpch/q10_returned_item_reporting.py
@@ -74,7 +74,7 @@ df = df_orders.filter(col("o_orderdate") >=
date_start_of_quarter).filter(
col("o_orderdate") < date_start_of_quarter + interval_one_quarter
)
-df = df.join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner")
+df = df.join(df_lineitem, left_on=["o_orderkey"], right_on=["l_orderkey"],
how="inner")
# Compute the revenue
df = df.aggregate(
@@ -83,8 +83,8 @@ df = df.aggregate(
)
# Now join in the customer data
-df = df.join(df_customer, (["o_custkey"], ["c_custkey"]), how="inner")
-df = df.join(df_nation, (["c_nationkey"], ["n_nationkey"]), how="inner")
+df = df.join(df_customer, left_on=["o_custkey"], right_on=["c_custkey"],
how="inner")
+df = df.join(df_nation, left_on=["c_nationkey"], right_on=["n_nationkey"],
how="inner")
# These are the columns the problem statement requires
df = df.select(
diff --git a/examples/tpch/q11_important_stock_identification.py
b/examples/tpch/q11_important_stock_identification.py
index 86ff229..707265e 100644
--- a/examples/tpch/q11_important_stock_identification.py
+++ b/examples/tpch/q11_important_stock_identification.py
@@ -52,9 +52,11 @@ df_nation = df_nation.filter(col("n_name") == lit(NATION))
# Find part supplies of within this target nation
-df = df_nation.join(df_supplier, (["n_nationkey"], ["s_nationkey"]),
how="inner")
+df = df_nation.join(
+ df_supplier, left_on=["n_nationkey"], right_on=["s_nationkey"], how="inner"
+)
-df = df.join(df_partsupp, (["s_suppkey"], ["ps_suppkey"]), how="inner")
+df = df.join(df_partsupp, left_on=["s_suppkey"], right_on=["ps_suppkey"],
how="inner")
# Compute the value of individual parts
diff --git a/examples/tpch/q12_ship_mode_order_priority.py
b/examples/tpch/q12_ship_mode_order_priority.py
index c3fc0d2..def2a6c 100644
--- a/examples/tpch/q12_ship_mode_order_priority.py
+++ b/examples/tpch/q12_ship_mode_order_priority.py
@@ -75,7 +75,7 @@ df = df.filter(
# We need order priority, so join order df to line item
-df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner")
+df = df.join(df_orders, left_on=["l_orderkey"], right_on=["o_orderkey"],
how="inner")
# Restrict to line items we care about based on the problem statement.
df = df.filter(col("l_commitdate") < col("l_receiptdate"))
diff --git a/examples/tpch/q13_customer_distribution.py
b/examples/tpch/q13_customer_distribution.py
index f8b6c13..67365a9 100644
--- a/examples/tpch/q13_customer_distribution.py
+++ b/examples/tpch/q13_customer_distribution.py
@@ -49,7 +49,9 @@ df_orders = df_orders.filter(
)
# Since we may have customers with no orders we must do a left join
-df = df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]), how="left")
+df = df_customer.join(
+ df_orders, left_on=["c_custkey"], right_on=["o_custkey"], how="left"
+)
# Find the number of orders for each customer
df = df.aggregate([col("c_custkey")],
[F.count(col("o_custkey")).alias("c_count")])
diff --git a/examples/tpch/q14_promotion_effect.py
b/examples/tpch/q14_promotion_effect.py
index 8224136..cd26ee2 100644
--- a/examples/tpch/q14_promotion_effect.py
+++ b/examples/tpch/q14_promotion_effect.py
@@ -57,7 +57,9 @@ df_lineitem = df_lineitem.filter(col("l_shipdate") >=
date_of_interest).filter(
)
# Left join so we can sum up the promo parts different from other parts
-df = df_lineitem.join(df_part, (["l_partkey"], ["p_partkey"]), "left")
+df = df_lineitem.join(
+ df_part, left_on=["l_partkey"], right_on=["p_partkey"], how="left"
+)
# Make a factor of 1.0 if it is a promotion, 0.0 otherwise
df = df.with_column("promo_factor", F.coalesce(col("promo_factor"), lit(0.0)))
diff --git a/examples/tpch/q15_top_supplier.py
b/examples/tpch/q15_top_supplier.py
index 44d5dd9..0bc316f 100644
--- a/examples/tpch/q15_top_supplier.py
+++ b/examples/tpch/q15_top_supplier.py
@@ -76,7 +76,7 @@ df = df.filter(col("total_revenue") == col("max_revenue"))
# Now that we know the supplier(s) with maximum revenue, get the rest of their
information
# from the supplier table
-df = df.join(df_supplier, (["l_suppkey"], ["s_suppkey"]), "inner")
+df = df.join(df_supplier, left_on=["l_suppkey"], right_on=["s_suppkey"],
how="inner")
# Return only the columns requested
df = df.select("s_suppkey", "s_name", "s_address", "s_phone", "total_revenue")
diff --git a/examples/tpch/q16_part_supplier_relationship.py
b/examples/tpch/q16_part_supplier_relationship.py
index cbdd998..a6a0c43 100644
--- a/examples/tpch/q16_part_supplier_relationship.py
+++ b/examples/tpch/q16_part_supplier_relationship.py
@@ -56,7 +56,7 @@ df_unwanted_suppliers = df_supplier.filter(
# Remove unwanted suppliers
df_partsupp = df_partsupp.join(
- df_unwanted_suppliers, (["ps_suppkey"], ["s_suppkey"]), "anti"
+ df_unwanted_suppliers, left_on=["ps_suppkey"], right_on=["s_suppkey"],
how="anti"
)
# Select the parts we are interested in
@@ -73,7 +73,9 @@ df_part = df_part.filter(
p_sizes = F.make_array(*[lit(s).cast(pa.int32()) for s in SIZES_OF_INTEREST])
df_part = df_part.filter(~F.array_position(p_sizes, col("p_size")).is_null())
-df = df_part.join(df_partsupp, (["p_partkey"], ["ps_partkey"]), "inner")
+df = df_part.join(
+ df_partsupp, left_on=["p_partkey"], right_on=["ps_partkey"], how="inner"
+)
df = df.select("p_brand", "p_type", "p_size", "ps_suppkey").distinct()
diff --git a/examples/tpch/q17_small_quantity_order.py
b/examples/tpch/q17_small_quantity_order.py
index ff49427..d7b43d4 100644
--- a/examples/tpch/q17_small_quantity_order.py
+++ b/examples/tpch/q17_small_quantity_order.py
@@ -51,7 +51,7 @@ df = df_part.filter(col("p_brand") == lit(BRAND)).filter(
)
# Combine data
-df = df.join(df_lineitem, (["p_partkey"], ["l_partkey"]), "inner")
+df = df.join(df_lineitem, left_on=["p_partkey"], right_on=["l_partkey"],
how="inner")
# Find the average quantity
window_frame = WindowFrame("rows", None, None)
diff --git a/examples/tpch/q18_large_volume_customer.py
b/examples/tpch/q18_large_volume_customer.py
index 4976154..165fce0 100644
--- a/examples/tpch/q18_large_volume_customer.py
+++ b/examples/tpch/q18_large_volume_customer.py
@@ -54,8 +54,8 @@ df = df.filter(col("total_quantity") > lit(QUANTITY))
# We've identified the orders of interest, now join the additional data
# we are required to report on
-df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), "inner")
-df = df.join(df_customer, (["o_custkey"], ["c_custkey"]), "inner")
+df = df.join(df_orders, left_on=["l_orderkey"], right_on=["o_orderkey"],
how="inner")
+df = df.join(df_customer, left_on=["o_custkey"], right_on=["c_custkey"],
how="inner")
df = df.select(
"c_name", "c_custkey", "o_orderkey", "o_orderdate", "o_totalprice",
"total_quantity"
diff --git a/examples/tpch/q19_discounted_revenue.py
b/examples/tpch/q19_discounted_revenue.py
index c2fe257..4aed0cb 100644
--- a/examples/tpch/q19_discounted_revenue.py
+++ b/examples/tpch/q19_discounted_revenue.py
@@ -72,7 +72,7 @@ df = df.filter(
(col("l_shipmode") == lit("AIR")) | (col("l_shipmode") == lit("AIR REG"))
)
-df = df.join(df_part, (["l_partkey"], ["p_partkey"]), "inner")
+df = df.join(df_part, left_on=["l_partkey"], right_on=["p_partkey"],
how="inner")
# Create the user defined function (UDF) definition that does the work
diff --git a/examples/tpch/q20_potential_part_promotion.py
b/examples/tpch/q20_potential_part_promotion.py
index 3a0edb1..d720cdc 100644
--- a/examples/tpch/q20_potential_part_promotion.py
+++ b/examples/tpch/q20_potential_part_promotion.py
@@ -70,7 +70,7 @@ df = df_lineitem.filter(col("l_shipdate") >=
lit(date)).filter(
)
# This will filter down the line items to the parts of interest
-df = df.join(df_part, (["l_partkey"], ["p_partkey"]), "inner")
+df = df.join(df_part, left_on="l_partkey", right_on="p_partkey", how="inner")
# Compute the total sold and limit ourselves to individual supplier/part
combinations
df = df.aggregate(
@@ -78,15 +78,18 @@ df = df.aggregate(
)
df = df.join(
- df_partsupp, (["l_partkey", "l_suppkey"], ["ps_partkey", "ps_suppkey"]),
"inner"
+ df_partsupp,
+ left_on=["l_partkey", "l_suppkey"],
+ right_on=["ps_partkey", "ps_suppkey"],
+ how="inner",
)
# Find cases of excess quantity
df.filter(col("ps_availqty") > lit(0.5) * col("total_sold"))
# We could do these joins earlier, but now limit to the nation of interest
suppliers
-df = df.join(df_supplier, (["ps_suppkey"], ["s_suppkey"]), "inner")
-df = df.join(df_nation, (["s_nationkey"], ["n_nationkey"]), "inner")
+df = df.join(df_supplier, left_on=["ps_suppkey"], right_on=["s_suppkey"],
how="inner")
+df = df.join(df_nation, left_on=["s_nationkey"], right_on=["n_nationkey"],
how="inner")
# Restrict to the requested data per the problem statement
df = df.select("s_name", "s_address").distinct()
diff --git a/examples/tpch/q21_suppliers_kept_orders_waiting.py
b/examples/tpch/q21_suppliers_kept_orders_waiting.py
index d3d57ac..27cf816 100644
--- a/examples/tpch/q21_suppliers_kept_orders_waiting.py
+++ b/examples/tpch/q21_suppliers_kept_orders_waiting.py
@@ -52,13 +52,13 @@ df_nation =
ctx.read_parquet(get_data_path("nation.parquet")).select(
df_suppliers_of_interest = df_nation.filter(col("n_name") ==
lit(NATION_OF_INTEREST))
df_suppliers_of_interest = df_suppliers_of_interest.join(
- df_supplier, (["n_nationkey"], ["s_nationkey"]), "inner"
+ df_supplier, left_on="n_nationkey", right_on="s_nationkey", how="inner"
)
# Find the failed orders and all their line items
df = df_orders.filter(col("o_orderstatus") == lit("F"))
-df = df_lineitem.join(df, (["l_orderkey"], ["o_orderkey"]), "inner")
+df = df_lineitem.join(df, left_on="l_orderkey", right_on="o_orderkey",
how="inner")
# Identify the line items for which the order is failed due to.
df = df.with_column(
@@ -102,7 +102,9 @@ df = df.select(
)
# Join to the supplier of interest list for the nation of interest
-df = df.join(df_suppliers_of_interest, (["suppkey"], ["s_suppkey"]), "inner")
+df = df.join(
+ df_suppliers_of_interest, left_on=["suppkey"], right_on=["s_suppkey"],
how="inner"
+)
# Count how many orders that supplier is the only failed supplier for
df = df.aggregate([col("s_name")],
[F.count(col("o_orderkey")).alias("numwait")])
diff --git a/examples/tpch/q22_global_sales_opportunity.py
b/examples/tpch/q22_global_sales_opportunity.py
index e6660e6..72dce52 100644
--- a/examples/tpch/q22_global_sales_opportunity.py
+++ b/examples/tpch/q22_global_sales_opportunity.py
@@ -62,7 +62,7 @@ df.show()
df = df.filter(col("c_acctbal") > col("avg_balance"))
# Limit results to customers with no orders
-df = df.join(df_orders, (["c_custkey"], ["o_custkey"]), "anti")
+df = df.join(df_orders, left_on="c_custkey", right_on="o_custkey", how="anti")
# Count up the customers and the balances
df = df.aggregate(
diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py
index e59f00d..efd4038 100644
--- a/python/datafusion/dataframe.py
+++ b/python/datafusion/dataframe.py
@@ -20,9 +20,8 @@ See :ref:`user_guide_concepts` in the online documentation
for more information.
"""
from __future__ import annotations
-
-
-from typing import Any, Iterable, List, Literal, TYPE_CHECKING
+import warnings
+from typing import Any, Iterable, List, TYPE_CHECKING, Literal, overload
from datafusion.record_batch import RecordBatchStream
from typing_extensions import deprecated
from datafusion.plan import LogicalPlan, ExecutionPlan
@@ -32,7 +31,7 @@ if TYPE_CHECKING:
import pandas as pd
import polars as pl
import pathlib
- from typing import Callable
+ from typing import Callable, Sequence
from datafusion._internal import DataFrame as DataFrameInternal
from datafusion.expr import Expr, SortExpr, sort_or_default
@@ -368,27 +367,105 @@ class DataFrame:
"""
return DataFrame(self.df.distinct())
+ @overload
+ def join(
+ self,
+ right: DataFrame,
+ on: str | Sequence[str],
+ how: Literal["inner", "left", "right", "full", "semi", "anti"] =
"inner",
+ *,
+ left_on: None = None,
+ right_on: None = None,
+ join_keys: None = None,
+ ) -> DataFrame: ...
+
+ @overload
def join(
self,
right: DataFrame,
+ on: None = None,
+ how: Literal["inner", "left", "right", "full", "semi", "anti"] =
"inner",
+ *,
+ left_on: str | Sequence[str],
+ right_on: str | Sequence[str],
+ join_keys: tuple[list[str], list[str]] | None = None,
+ ) -> DataFrame: ...
+
+ @overload
+ def join(
+ self,
+ right: DataFrame,
+ on: None = None,
+ how: Literal["inner", "left", "right", "full", "semi", "anti"] =
"inner",
+ *,
join_keys: tuple[list[str], list[str]],
- how: str,
+ left_on: None = None,
+ right_on: None = None,
+ ) -> DataFrame: ...
+
+ def join(
+ self,
+ right: DataFrame,
+ on: str | Sequence[str] | tuple[list[str], list[str]] | None = None,
+ how: Literal["inner", "left", "right", "full", "semi", "anti"] =
"inner",
+ *,
+ left_on: str | Sequence[str] | None = None,
+ right_on: str | Sequence[str] | None = None,
+ join_keys: tuple[list[str], list[str]] | None = None,
) -> DataFrame:
"""Join this :py:class:`DataFrame` with another :py:class:`DataFrame`.
- Join keys are a pair of lists of column names in the left and right
- dataframes, respectively. These lists must have the same length.
+ `on` has to be provided or both `left_on` and `right_on` in
conjunction.
Args:
right: Other DataFrame to join with.
- join_keys: Tuple of two lists of column names to join on.
+ on: Column names to join on in both dataframes.
how: Type of join to perform. Supported types are "inner", "left",
"right", "full", "semi", "anti".
+ left_on: Join column of the left dataframe.
+ right_on: Join column of the right dataframe.
+ join_keys: Tuple of two lists of column names to join on.
[Deprecated]
Returns:
DataFrame after join.
"""
- return DataFrame(self.df.join(right.df, join_keys, how))
+ # This check is to prevent breaking API changes where users prior to
+ # DF 43.0.0 would pass the join_keys as a positional argument instead
+ # of a keyword argument.
+ if isinstance(on, tuple) and len(on) == 2:
+ if isinstance(on[0], list) and isinstance(on[1], list):
+ join_keys = on # type: ignore
+ on = None
+
+ if join_keys is not None:
+ warnings.warn(
+ "`join_keys` is deprecated, use `on` or `left_on` with
`right_on`",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ left_on = join_keys[0]
+ right_on = join_keys[1]
+
+ if on:
+ if left_on or right_on:
+ raise ValueError(
+ "`left_on` or `right_on` should not provided with `on`"
+ )
+ left_on = on
+ right_on = on
+ elif left_on or right_on:
+ if left_on is None or right_on is None:
+ raise ValueError("`left_on` and `right_on` should both be
provided.")
+ else:
+ raise ValueError(
+ "either `on` or `left_on` and `right_on` should be provided."
+ )
+ if isinstance(left_on, str):
+ left_on = [left_on]
+ if isinstance(right_on, str):
+ right_on = [right_on]
+
+ return DataFrame(self.df.join(right.df, how, left_on, right_on))
def join_on(
self,
diff --git a/python/tests/test_dataframe.py b/python/tests/test_dataframe.py
index 7b20e9e..3304753 100644
--- a/python/tests/test_dataframe.py
+++ b/python/tests/test_dataframe.py
@@ -321,14 +321,72 @@ def test_join():
)
df1 = ctx.create_dataframe([[batch]], "r")
- df = df.join(df1, join_keys=(["a"], ["a"]), how="inner")
- df.show()
- df = df.sort(column("l.a"))
- table = pa.Table.from_batches(df.collect())
+ df2 = df.join(df1, on="a", how="inner")
+ df2.show()
+ df2 = df2.sort(column("l.a"))
+ table = pa.Table.from_batches(df2.collect())
+
+ expected = {"a": [1, 2], "c": [8, 10], "b": [4, 5]}
+ assert table.to_pydict() == expected
+
+ df2 = df.join(df1, left_on="a", right_on="a", how="inner")
+ df2.show()
+ df2 = df2.sort(column("l.a"))
+ table = pa.Table.from_batches(df2.collect())
expected = {"a": [1, 2], "c": [8, 10], "b": [4, 5]}
assert table.to_pydict() == expected
+ # Verify we don't make a breaking change to pre-43.0.0
+ # where users would pass join_keys as a positional argument
+ df2 = df.join(df1, (["a"], ["a"]), how="inner") # type: ignore
+ df2.show()
+ df2 = df2.sort(column("l.a"))
+ table = pa.Table.from_batches(df2.collect())
+
+ expected = {"a": [1, 2], "c": [8, 10], "b": [4, 5]}
+ assert table.to_pydict() == expected
+
+
+def test_join_invalid_params():
+ ctx = SessionContext()
+
+ batch = pa.RecordBatch.from_arrays(
+ [pa.array([1, 2, 3]), pa.array([4, 5, 6])],
+ names=["a", "b"],
+ )
+ df = ctx.create_dataframe([[batch]], "l")
+
+ batch = pa.RecordBatch.from_arrays(
+ [pa.array([1, 2]), pa.array([8, 10])],
+ names=["a", "c"],
+ )
+ df1 = ctx.create_dataframe([[batch]], "r")
+
+ with pytest.deprecated_call():
+ df2 = df.join(df1, join_keys=(["a"], ["a"]), how="inner")
+ df2.show()
+ df2 = df2.sort(column("l.a"))
+ table = pa.Table.from_batches(df2.collect())
+
+ expected = {"a": [1, 2], "c": [8, 10], "b": [4, 5]}
+ assert table.to_pydict() == expected
+
+ with pytest.raises(
+ ValueError, match=r"`left_on` or `right_on` should not provided with
`on`"
+ ):
+ df2 = df.join(df1, on="a", how="inner", right_on="test") # type:
ignore
+
+ with pytest.raises(
+ ValueError, match=r"`left_on` and `right_on` should both be provided."
+ ):
+ df2 = df.join(df1, left_on="a", how="inner") # type: ignore
+
+ with pytest.raises(
+ ValueError, match=r"either `on` or `left_on` and `right_on` should be
provided."
+ ):
+ df2 = df.join(df1, how="inner") # type: ignore
+
def test_join_on():
ctx = SessionContext()
diff --git a/src/dataframe.rs b/src/dataframe.rs
index dd5d89c..ee8fbbf 100644
--- a/src/dataframe.rs
+++ b/src/dataframe.rs
@@ -271,8 +271,9 @@ impl PyDataFrame {
fn join(
&self,
right: PyDataFrame,
- join_keys: (Vec<PyBackedStr>, Vec<PyBackedStr>),
how: &str,
+ left_on: Vec<PyBackedStr>,
+ right_on: Vec<PyBackedStr>,
) -> PyResult<Self> {
let join_type = match how {
"inner" => JoinType::Inner,
@@ -289,16 +290,8 @@ impl PyDataFrame {
}
};
- let left_keys = join_keys
- .0
- .iter()
- .map(|s| s.as_ref())
- .collect::<Vec<&str>>();
- let right_keys = join_keys
- .1
- .iter()
- .map(|s| s.as_ref())
- .collect::<Vec<&str>>();
+ let left_keys = left_on.iter().map(|s|
s.as_ref()).collect::<Vec<&str>>();
+ let right_keys = right_on.iter().map(|s|
s.as_ref()).collect::<Vec<&str>>();
let df = self.df.as_ref().clone().join(
right.df.as_ref().clone(),
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]