Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Percentage True aggregation function #2945

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def _get_connection_string(username, password, hostname, database):
ACADEMICS_SQL = os.path.join(RESOURCES, "academics_create.sql")
LIBRARY_SQL = os.path.join(RESOURCES, "library_without_checkouts.sql")
LIBRARY_CHECKOUTS_SQL = os.path.join(RESOURCES, "library_add_checkouts.sql")
FRAUDULENT_PAYMENTS_SQL = os.path.join(RESOURCES, "fraudulent_payments.sql")


@pytest.fixture
Expand Down Expand Up @@ -331,3 +332,20 @@ def make_table(table_name):
in table_names
}
return tables


@pytest.fixture
def engine_with_fraudulent_payment(engine_with_schema):
engine, schema = engine_with_schema
with engine.begin() as conn, open(FRAUDULENT_PAYMENTS_SQL) as f:
conn.execute(text(f"SET search_path={schema}"))
conn.execute(text(f.read()))
yield engine, schema


@pytest.fixture
def payments_db_table(engine_with_fraudulent_payment):
engine, schema = engine_with_fraudulent_payment
metadata = MetaData(bind=engine)
table = Table("Payments", metadata, schema=schema, autoload_with=engine)
return table
15 changes: 14 additions & 1 deletion db/functions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from abc import ABC, abstractmethod
import warnings

from sqlalchemy import column, not_, and_, or_, func, literal, cast, distinct
from sqlalchemy import column, not_, and_, or_, func, literal, cast, distinct, INTEGER
from sqlalchemy.dialects.postgresql import array_agg, TEXT, array
from sqlalchemy.sql import quoted_name
from sqlalchemy.sql.functions import GenericFunction, concat, percentile_disc, mode
Expand Down Expand Up @@ -420,6 +420,19 @@ def to_sa_expression(column_expr):
return sa_call_sql_function('sum', column_expr, return_type=PostgresType.NUMERIC)


class Percentage_True(DBFunction):
id = 'percentage_true'
name = 'percentage_true'
hints = tuple([
hints.aggregation,
])

@staticmethod
def to_sa_expression(column_expr):
column_expr = cast(column_expr, INTEGER)
return sa_call_sql_function('avg', 100 * column_expr, return_type=PostgresType.NUMERIC)


class Median(DBFunction):
id = 'median'
name = 'median'
Expand Down
124 changes: 124 additions & 0 deletions db/tests/resources/fraudulent_payments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
CREATE TABLE "Payments" (
id integer NOT NULL,
"Payment Mode" text,
"Is Fraudulent" boolean
);

CREATE SEQUENCE "Payments_id_seq"
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;

ALTER SEQUENCE "Payments_id_seq" OWNED BY "Payments".id;

ALTER TABLE ONLY "Payments" ALTER COLUMN id SET DEFAULT nextval('"Payments_id_seq"'::regclass);

INSERT INTO "Payments" (id, "Payment Mode", "Is Fraudulent") VALUES
(1, 'credit card', false),
(2, 'debit card', false),
(3, 'wallet', true),
(4, 'UPI', false),
(5, 'credit card', false),
(6, 'wallet', true),
(7, 'debit card', false),
(8, 'pay later', false),
(9, 'debit card', false),
(10, 'debit card', false),
(11, 'pay later', false),
(12, 'debit card', false),
(13, 'wallet', true),
(14, 'wallet', false),
(15, 'UPI', true),
(16, 'credit card', false),
(17, 'wallet', false),
(18, 'debit card', false),
(19, 'debit card', false),
(20, 'credit card', false),
(21, 'debit card', true),
(22, 'credit card', false),
(23, 'debit card', false),
(24, 'debit card', false),
(25, 'wallet', false),
(26, 'wallet', false),
(27, 'wallet', false),
(28, 'wallet', false),
(29, 'credit card', false),
(30, 'wallet', false),
(31, 'wallet', false),
(32, 'debit card', false),
(33, 'debit card', false),
(34, 'debit card', false),
(35, 'pay later', false),
(36, 'debit card', false),
(37, 'wallet', false),
(38, 'debit card', true),
(39, 'credit card', false),
(40, 'UPI', false),
(41, 'debit card', false),
(42, 'credit card', false),
(43, 'credit card', false),
(44, 'wallet', false),
(45, 'pay later', false),
(46, 'credit card', true),
(47, 'debit card', false),
(48, 'credit card', false),
(49, 'wallet', false),
(50, 'UPI', false),
(51, 'debit card', false),
(52, 'debit card', false),
(53, 'wallet', false),
(54, 'debit card', false),
(55, 'credit card', false),
(56, 'pay later', false),
(57, 'debit card', true),
(58, 'wallet', false),
(59, 'credit card', false),
(60, 'debit card', false),
(61, 'wallet', true),
(62, 'wallet', false),
(63, 'debit card', false),
(64, 'credit card', false),
(65, 'wallet', false),
(66, 'debit card', false),
(67, 'debit card', false),
(68, 'pay later', true),
(69, 'debit card', false),
(70, 'debit card', false),
(71, 'wallet', false),
(72, 'wallet', false),
(73, 'credit card', false),
(74, 'debit card', false),
(75, 'UPI', false),
(76, 'debit card', false),
(77, 'wallet', false),
(78, 'credit card', true),
(79, 'wallet', true),
(80, 'debit card', false),
(81, 'debit card', false),
(82, 'credit card', false),
(83, 'debit card', false),
(84, 'wallet', false),
(85, 'wallet', true),
(86, 'UPI', false),
(87, 'credit card', false),
(88, 'wallet', false),
(89, 'debit card', false),
(90, 'debit card', false),
(91, 'credit card', false),
(92, 'pay later', false),
(93, 'debit card', false),
(94, 'wallet', false),
(95, 'debit card', true),
(96, 'debit card', false),
(97, 'wallet', false),
(98, 'wallet', false),
(99, 'credit card', false),
(100, 'wallet', true);

SELECT pg_catalog.setval('"Payments_id_seq"', 100, true);

ALTER TABLE ONLY "Payments"
ADD CONSTRAINT "Payments_pkey" PRIMARY KEY (id);
3 changes: 2 additions & 1 deletion mathesar/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from db.transforms.operations.deserialize import deserialize_transformation
from db.transforms.operations.serialize import serialize_transformation
from db.transforms.base import Summarize
from db.functions.base import Count, ArrayAgg, Sum, Median, Mode
from db.functions.base import Count, ArrayAgg, Sum, Median, Mode, Percentage_True
from db.functions.packed import DistinctArrayAgg

from mathesar.api.exceptions.query_exceptions.exceptions import DeletedColumnAccess
Expand Down Expand Up @@ -422,6 +422,7 @@ def _get_default_display_name_for_agg_output_alias(
Sum.id: " sum",
Median.id: " median",
Mode.id: " mode",
Percentage_True.id: " percentage true"
}
suffix_to_add = map_of_agg_function_to_suffix.get(agg_function)
if suffix_to_add:
Expand Down
6 changes: 6 additions & 0 deletions mathesar/tests/api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,9 @@ def library_ma_tables(db_table_to_dj_table, library_db_tables):
for table_name, db_table
in library_db_tables.items()
}


@pytest.fixture
def payments_ma_table(db_table_to_dj_table, payments_db_table):
reset_reflection()
return db_table_to_dj_table(payments_db_table)
77 changes: 77 additions & 0 deletions mathesar/tests/api/query/test_aggregation_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,83 @@ def test_mode_aggregation(library_ma_tables, get_uid, client):
assert sorted(actual_records, key=lambda x: x['Checkout Month']) == expect_records


def test_percentage_true_aggregation(payments_ma_table, get_uid, client):
_ = payments_ma_table
payments = {
t["name"]: t for t in client.get("/api/db/v0/tables/").json()["results"]
}["Payments"]
columns = {
c["name"]: c for c in payments["columns"]
}
request_data = {
"name": get_uid(),
"base_table": payments["id"],
"initial_columns": [
{"id": columns["Payment Mode"]["id"], "alias": "Payment Mode"},
{"id": columns["Is Fraudulent"]["id"], "alias": "Is Fraudulent"},
],
"display_names": {
"Payment Mode": "Payment Mode",
"Percentage Fraudulent": "Percentage Fraudulent",
},
"display_options": {
"Payment Mode": {
display_option_origin: "Payment Mode",
},
"Is Fraudulent": {
display_option_origin: "Is Fraudulent",
},
},
"transformations": [
{
"spec": {
"grouping_expressions": [
{
"input_alias": "Payment Mode",
"output_alias": "Payment Mode",
}
],
"aggregation_expressions": [
{
"input_alias": "Is Fraudulent",
"output_alias": "Percentage Fraudulent",
"function": "percentage_true",
}
]
},
"type": "summarize",
}
]
}
response = client.post('/api/db/v0/queries/', data=request_data)
assert response.status_code == 201
query_id = response.json()['id']
expect_records = [
{
'Payment Mode': 'UPI',
'Percentage Fraudulent': 16.666666666666668
},
{
'Payment Mode': 'credit card',
'Percentage Fraudulent': 10.0
},
{
'Payment Mode': 'debit card',
'Percentage Fraudulent': 10.81081081081081
},
{
'Payment Mode': 'pay later',
'Percentage Fraudulent': 14.285714285714286
},
{
'Payment Mode': 'wallet',
'Percentage Fraudulent': 23.333333333333332
}
]
actual_records = client.get(f'/api/db/v0/queries/{query_id}/records/').json()['results']
assert sorted(actual_records, key=lambda x: x['Payment Mode']) == expect_records


def test_Mathesar_money_distinct_list_aggregation(library_ma_tables, get_uid, client):
_ = library_ma_tables
items = {
Expand Down
1 change: 1 addition & 0 deletions mathesar_ui/src/api/types/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const querySummarizationFunctionIds = [
'sum',
'median',
'mode',
'percentage_true',
] as const;

export type QuerySummarizationFunctionId =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ const functionsResponse: AbstractTypeSummarizationFunctionsResponse = {
label: 'Mode',
inputOutputTypeMap: mapInputTypesToTheSameOutputType(),
},
percentage_true: {
label: 'Percentage True',
inputOutputTypeMap: {
[abstractTypeCategory.Boolean]: abstractTypeCategory.Number,
},
},
};

export function getSummarizationFunctionsForAbstractType(
Expand Down