diff --git a/docs/how-to/errors.md b/docs/how-to/errors.md index d718608fb..c6c131f7d 100644 --- a/docs/how-to/errors.md +++ b/docs/how-to/errors.md @@ -816,7 +816,7 @@ age = patients.age_on("2023-01-01") dataset.age_group = case( when(age < 10).then(1), when(age > 80).then(2), - default="unknown", + otherwise="unknown", ) ``` @@ -842,7 +842,7 @@ age = patients.age_on("2023-01-01") dataset.age_group = case( when(age < 10).then(1), when(age > 80).then(2), - default=0, + otherwise=0, ) ``` diff --git a/docs/how-to/examples.md b/docs/how-to/examples.md index a665535ba..25c3da2ae 100644 --- a/docs/how-to/examples.md +++ b/docs/how-to/examples.md @@ -107,7 +107,7 @@ dataset.age_band = case( when(age < 60).then("40-59"), when(age < 80).then("60-79"), when(age >= 80).then("80+"), - default="missing", + otherwise="missing", ) dataset.define_population(patients.exists_for_patient()) ``` @@ -227,7 +227,7 @@ dataset.imd_quintile = case( when(imd < int(32844 * 3 / 5)).then("3"), when(imd < int(32844 * 4 / 5)).then("4"), when(imd < int(32844 * 5 / 5)).then("5 (least deprived)"), - default="unknown" + otherwise="unknown" ) dataset.define_population(patients.exists_for_patient()) ``` diff --git a/docs/includes/generated_docs/language__functions.md b/docs/includes/generated_docs/language__functions.md index da02f7099..9b6f599a8 100644 --- a/docs/includes/generated_docs/language__functions.md +++ b/docs/includes/generated_docs/language__functions.md @@ -1,6 +1,6 @@

- case(*when_thens, default=None) + case(*when_thens, otherwise=None, default=None)

Take a sequence of condition-values of the form: @@ -9,8 +9,8 @@ when(condition).then(value) ``` And evaluate them in order, returning the value of the first condition which -evaluates True. If no condition matches and a `default` is specified then return -that, otherwise return NULL. +evaluates True. If no condition matches, return the `otherwise` value; if no +`otherwise` value is specified then return NULL. For example: ```py @@ -18,7 +18,7 @@ category = case( when(size < 10).then("small"), when(size < 20).then("medium"), when(size >= 20).then("large"), - default="unknown", + otherwise="unknown", ) ``` @@ -31,7 +31,7 @@ A simpler form is available when there is a single condition. This example: ```py category = case( when(size < 15).then("small"), - default="large", + otherwise="large", ) ``` @@ -39,6 +39,9 @@ can be rewritten as: ```py category = when(size < 15).then("small").otherwise("large") ``` + +Note that the `default` argument is an older alias for `otherwise`: it will be +removed in future versions of ehrQL and should not be used.
diff --git a/docs/includes/generated_docs/language__series.md b/docs/includes/generated_docs/language__series.md index 15b339709..c9c9746b7 100644 --- a/docs/includes/generated_docs/language__series.md +++ b/docs/includes/generated_docs/language__series.md @@ -75,14 +75,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above. +
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -199,14 +209,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -340,14 +360,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -481,14 +511,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -708,14 +748,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -913,14 +963,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -1163,14 +1223,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -1368,14 +1438,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -1594,14 +1674,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -1844,14 +1934,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -2081,14 +2181,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
@@ -2181,14 +2291,24 @@ NULL, and False otherwise. Return the inverse of `is_null()` above.
+
+ when_null_then(other) + 🔗 +
+
+Replace any NULL value in this series with the corresponding value in `other`. + +Note that `other` must be of the same type as this series. +
+
if_null_then(other) 🔗
-Replace any NULL value in this series with the corresponding value in `other`. +Deprecated alias for `when_null_then()` -Note that `other` must be of the same type as this series. +This will be removed in future versions of ehrQL and shoud not be used.
diff --git a/docs/includes/generated_docs/schemas/beta.tpp.md b/docs/includes/generated_docs/schemas/beta.tpp.md index b3b2b5057..183de3e9a 100644 --- a/docs/includes/generated_docs/schemas/beta.tpp.md +++ b/docs/includes/generated_docs/schemas/beta.tpp.md @@ -57,7 +57,7 @@ from which other larger geographic representations can be derived when(imd < int(32844 * 3 / 5)).then("3"), when(imd < int(32844 * 4 / 5)).then("4"), when(imd < int(32844 * 5 / 5)).then("5 (least deprived)"), - default="unknown" + otherwise="unknown" ) ``` @@ -241,7 +241,7 @@ spanning_addrs = addresses.where(addresses.start_date <= date).except_where( addresses.end_date < date ) ordered_addrs = spanning_addrs.sort_by( - case(when(addresses.has_postcode).then(1), default=0), + case(when(addresses.has_postcode).then(1), otherwise=0), addresses.start_date, addresses.end_date, addresses.address_id, diff --git a/docs/includes/generated_docs/specs.md b/docs/includes/generated_docs/specs.md index f20352901..074a1dbfb 100644 --- a/docs/includes/generated_docs/specs.md +++ b/docs/includes/generated_docs/specs.md @@ -1262,7 +1262,7 @@ returns the following patient series: ### 6.4 Replace missing values -#### 6.4.1 If null then integer column +#### 6.4.1 When null then integer column This example makes use of a patient-level table named `p` containing the following data: @@ -1274,7 +1274,7 @@ This example makes use of a patient-level table named `p` containing the followi | 4| | ```python -p.i1.if_null_then(0) +p.i1.when_null_then(0) ``` returns the following patient series: @@ -1287,7 +1287,7 @@ returns the following patient series: -#### 6.4.2 If null then boolean column +#### 6.4.2 When null then boolean column This example makes use of a patient-level table named `p` containing the following data: @@ -1299,7 +1299,7 @@ This example makes use of a patient-level table named `p` containing the followi | 4| | ```python -p.i1.is_in([101, 201]).if_null_then(False) +p.i1.is_in([101, 201]).when_null_then(False) ``` returns the following patient series: @@ -2760,7 +2760,7 @@ This example makes use of a patient-level table named `p` containing the followi case( when(p.i1 < 8).then(p.i1), when(p.i1 > 8).then(100), - default=0, + otherwise=0, ) ``` returns the following patient series: diff --git a/ehrql/query_language.py b/ehrql/query_language.py index 9c3ecdbd9..ba187ed12 100644 --- a/ehrql/query_language.py +++ b/ehrql/query_language.py @@ -203,7 +203,7 @@ def is_not_null(self): """ return self.is_null().__invert__() - def if_null_then(self, other): + def when_null_then(self, other): """ Replace any NULL value in this series with the corresponding value in `other`. @@ -211,9 +211,17 @@ def if_null_then(self, other): """ return case( when(self.is_not_null()).then(self), - default=self._cast(other), + otherwise=self._cast(other), ) + def if_null_then(self, other): + """ + Deprecated alias for `when_null_then()` + + This will be removed in future versions of ehrQL and shoud not be used. + """ + return self.when_null_then(other) + def is_in(self, other): """ Return a boolean series which is True for each value in this series which is @@ -251,7 +259,7 @@ def map_values(self, mapping, default=None): when(self == from_value).then(to_value) for from_value, to_value in mapping.items() ], - default=default, + otherwise=default, ) @@ -1347,11 +1355,11 @@ def __init__(self, condition, value): self._condition = condition self._value = value - def otherwise(self, default): - return case(self, default=default) + def otherwise(self, value): + return case(self, otherwise=value) -def case(*when_thens, default=None): +def case(*when_thens, otherwise=None, default=None): """ Take a sequence of condition-values of the form: ```py @@ -1359,8 +1367,8 @@ def case(*when_thens, default=None): ``` And evaluate them in order, returning the value of the first condition which - evaluates True. If no condition matches and a `default` is specified then return - that, otherwise return NULL. + evaluates True. If no condition matches, return the `otherwise` value; if no + `otherwise` value is specified then return NULL. For example: ```py @@ -1368,7 +1376,7 @@ def case(*when_thens, default=None): when(size < 10).then("small"), when(size < 20).then("medium"), when(size >= 20).then("large"), - default="unknown", + otherwise="unknown", ) ``` @@ -1381,7 +1389,7 @@ def case(*when_thens, default=None): ```py category = case( when(size < 15).then("small"), - default="large", + otherwise="large", ) ``` @@ -1390,14 +1398,20 @@ def case(*when_thens, default=None): category = when(size < 15).then("small").otherwise("large") ``` + Note that the `default` argument is an older alias for `otherwise`: it will be + removed in future versions of ehrQL and should not be used. """ + if default is not None: + if otherwise is not None: + raise ValueError("Use `otherwise` instead of `default`") + otherwise = default cases = _DictArg((case._condition, case._value) for case in when_thens) - # If we don't want a default then we shouldn't supply an argument, or else it will - # get converted into `Value(None)` which is not what we want - if default is None: + # If we don't want an `otherwise` value then we shouldn't supply an argument, or + # else it will get converted into `Value(None)` which is not what we want + if otherwise is None: return _apply(qm.Case, cases) else: - return _apply(qm.Case, cases, default) + return _apply(qm.Case, cases, otherwise) # HORIZONTAL AGGREGATION FUNCTIONS diff --git a/ehrql/tables/beta/tpp.py b/ehrql/tables/beta/tpp.py index 60d090670..39bb23667 100644 --- a/ehrql/tables/beta/tpp.py +++ b/ehrql/tables/beta/tpp.py @@ -61,7 +61,7 @@ class addresses(EventFrame): when(imd < int(32844 * 3 / 5)).then("3"), when(imd < int(32844 * 4 / 5)).then("4"), when(imd < int(32844 * 5 / 5)).then("5 (least deprived)"), - default="unknown" + otherwise="unknown" ) ``` @@ -152,7 +152,7 @@ def for_patient_on(self, date): self.end_date < date ) ordered_addrs = spanning_addrs.sort_by( - case(when(self.has_postcode).then(1), default=0), + case(when(self.has_postcode).then(1), otherwise=0), self.start_date, self.end_date, self.address_id, diff --git a/tests/spec/case_expressions/test_case.py b/tests/spec/case_expressions/test_case.py index ae063ee79..5d5cfdfd0 100644 --- a/tests/spec/case_expressions/test_case.py +++ b/tests/spec/case_expressions/test_case.py @@ -41,7 +41,7 @@ def test_case_with_default(spec_test): case( when(p.i1 < 8).then(p.i1), when(p.i1 > 8).then(100), - default=0, + otherwise=0, ), { 1: 6, diff --git a/tests/spec/series_ops/test_if_null_then.py b/tests/spec/series_ops/test_when_null_then.py similarity index 72% rename from tests/spec/series_ops/test_if_null_then.py rename to tests/spec/series_ops/test_when_null_then.py index f37957ba4..bc322ce0b 100644 --- a/tests/spec/series_ops/test_if_null_then.py +++ b/tests/spec/series_ops/test_when_null_then.py @@ -15,10 +15,10 @@ } -def test_if_null_then_integer_column(spec_test): +def test_when_null_then_integer_column(spec_test): spec_test( table_data, - p.i1.if_null_then(0), + p.i1.when_null_then(0), { 1: 101, 2: 201, @@ -28,10 +28,10 @@ def test_if_null_then_integer_column(spec_test): ) -def test_if_null_then_boolean_column(spec_test): +def test_when_null_then_boolean_column(spec_test): spec_test( table_data, - p.i1.is_in([101, 201]).if_null_then(False), + p.i1.is_in([101, 201]).when_null_then(False), { 1: True, 2: True, diff --git a/tests/spec/toc.py b/tests/spec/toc.py index 2683f6088..a8dda8c89 100644 --- a/tests/spec/toc.py +++ b/tests/spec/toc.py @@ -32,7 +32,7 @@ "test_equality", "test_containment", "test_map_values", - "test_if_null_then", + "test_when_null_then", "test_maximum_of_and_minimum_of_patient_series", "test_maximum_of_and_minimum_of_event_series", ], diff --git a/tests/unit/test_query_language.py b/tests/unit/test_query_language.py index 8f6a0dd0e..611bd9236 100644 --- a/tests/unit/test_query_language.py +++ b/tests/unit/test_query_language.py @@ -27,6 +27,7 @@ Series, StrEventSeries, StrPatientSeries, + case, compile, create_dataset, days, @@ -36,6 +37,7 @@ table_from_file, table_from_rows, weeks, + when, years, ) from ehrql.query_model.column_specs import ColumnSpec @@ -759,19 +761,30 @@ def test_duration_generate_intervals_rejects_invalid_arguments( ], ) def test_count_episodes_for_patient_rejects_invalid_arguments(maximum_gap, error): - @table - class e(EventFrame): - d = Series(date) - with pytest.raises((TypeError, ValueError), match=error): - e.d.count_episodes_for_patient(maximum_gap) + events.event_date.count_episodes_for_patient(maximum_gap) def test_count_episodes_for_patient_handles_weeks(): - @table - class e(EventFrame): - d = Series(date) - - using_days = e.d.count_episodes_for_patient(days(14)) - using_weeks = e.d.count_episodes_for_patient(weeks(2)) + using_days = events.event_date.count_episodes_for_patient(days(14)) + using_weeks = events.event_date.count_episodes_for_patient(weeks(2)) assert using_days._qm_node == using_weeks._qm_node + + +def test_case_accepts_default_as_alias_for_otherwise(): + case_otherwise = case(when(events.f > 10).then("foo"), otherwise="bar") + case_default = case(when(events.f > 10).then("foo"), default="bar") + + assert case_otherwise._qm_node == case_default._qm_node + + +def test_case_rejects_default_and_otherwise_supplied_together(): + with pytest.raises(ValueError, match="Use `otherwise` instead of `default`"): + case(when(events.f > 10).then("foo"), default="bar", otherwise="baz") + + +def test_if_null_then_alias(): + if_null = events.f.if_null_then(0.0) + when_null = events.f.when_null_then(0.0) + + assert if_null._qm_node == when_null._qm_node