Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9f514a8
no longer writing value distributions
a-hartens May 5, 2026
6c95ca6
adding multi index
a-hartens Jun 3, 2026
eca4b0b
adding tests
a-hartens Jun 3, 2026
67ec0c4
fixed event phenotype tests
a-hartens Jun 3, 2026
4395291
fixing multi index tests
a-hartens Jun 3, 2026
65beece
fixed all multi index tests
a-hartens Jun 3, 2026
6739fff
adding cohort and subcohort tests
a-hartens Jun 3, 2026
0288d37
black
a-hartens Jun 4, 2026
b6a9e07
adding deduplication of index table with person id and index date and…
a-hartens Jun 4, 2026
0074f03
black
a-hartens Jun 4, 2026
54b14b1
adding further value filter phenotype and test
a-hartens Jun 4, 2026
d1d2213
Merge branch 'feat/multiindex' into feat/furthervaluefilter
a-hartens Jun 4, 2026
4e2b302
adding further value filter phenotype tests
a-hartens Jun 4, 2026
46605ab
black
a-hartens Jun 4, 2026
537d8c2
adding docs
a-hartens Jun 4, 2026
d66b5b6
adding md file
a-hartens Jun 4, 2026
874fd5d
adding date format to mappers
a-hartens Jun 5, 2026
99b40eb
adding docstring
a-hartens Jun 5, 2026
d98175b
Merge branch 'feat/multiindex' into dev_ab1002
a-hartens Jun 5, 2026
97f42b3
backend native strings
a-hartens Jun 5, 2026
3294e87
Merge branch 'feat/timestringinmappers' into dev_ab1002
a-hartens Jun 5, 2026
2896be8
Merge branch 'feat/furthervaluefilter' into dev_ab1002
a-hartens Jun 5, 2026
e76c1f4
adding variable writing of subset tables entry
a-hartens Jun 5, 2026
55defe8
merged
a-hartens Jun 5, 2026
e88d0c9
setting lazy execution
a-hartens Jun 5, 2026
4b8924d
Merge branch 'feat/variablewriting' into dev_ab1002
a-hartens Jun 5, 2026
7c6dc38
fixed lazy execution
a-hartens Jun 5, 2026
e2cf9f1
problem with multiple backends. attempted fix
a-hartens Jun 5, 2026
c34d645
merged multiple backends
a-hartens Jun 5, 2026
afc7842
updates to snowflake connector
a-hartens Jun 6, 2026
12a0a4b
age and mappers clean date strings before formatting
a-hartens Jun 6, 2026
6be050c
multiindex support in waterfall reporter
a-hartens Jun 6, 2026
51c9916
Merge branch 'feat/multiindex' into dev_ab1002
a-hartens Jun 6, 2026
08b25bb
updating attrition for events
a-hartens Jun 6, 2026
57530cd
Merge branch 'feat/multiindex' into dev_ab1002
a-hartens Jun 6, 2026
8f25dbd
adding dont subset subset tables index
a-hartens Jun 7, 2026
ac91473
adding multiindex to subcohort
a-hartens Jun 7, 2026
e05eb79
fix to waterfall
a-hartens Jun 19, 2026
a4b2e1b
improved attrition detailed coloring of exclusion
a-hartens Jun 19, 2026
ea1bbeb
improved paneling
a-hartens Jun 22, 2026
56cafff
Merge branch 'main' into dev_ab1002_with_main
a-hartens Jun 29, 2026
4e18ff6
fix to multi index subset tables index
a-hartens Jun 30, 2026
432a564
black
a-hartens Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/api/phenotypes/further_value_filter_phenotype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# FurtherValueFilterPhenotype

::: phenex.phenotypes.further_value_filter_phenotype
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- EventPhenotype: api/phenotypes/event_phenotype.md
- CodelistPhenotype: api/phenotypes/codelist_phenotype.md
- MeasurementPhenotype: api/phenotypes/measurement_phenotype.md
- FurtherValueFilterPhenotype: api/phenotypes/further_value_filter_phenotype.md
- AgePhenotype: api/phenotypes/age_phenotype.md
- SexPhenotype: api/phenotypes/sex_phenotype.md
- DeathPhenotype: api/phenotypes/death_phenotype.md
Expand Down
2 changes: 2 additions & 0 deletions phenex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CODETYPE_INFO,
MeasurementPhenotype,
MeasurementChangePhenotype,
FurtherValueFilterPhenotype,
AgePhenotype,
SexPhenotype,
BinPhenotype,
Expand Down Expand Up @@ -134,6 +135,7 @@
"CODETYPE_INFO",
"MeasurementPhenotype",
"MeasurementChangePhenotype",
"FurtherValueFilterPhenotype",
"AgePhenotype",
"SexPhenotype",
"BinPhenotype",
Expand Down
16 changes: 11 additions & 5 deletions phenex/aggregators/aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ def __init__(
self.preserve_nulls = preserve_nulls

def aggregate(self, input_table: Table):
# Ensure INDEX_DATE is in the aggregation index if the table has it
agg_index = list(self.aggregation_index)
if "INDEX_DATE" in input_table.columns and "INDEX_DATE" not in agg_index:
agg_index.append("INDEX_DATE")
# Define the window specification
partition_cols = [
getattr(input_table, col) if isinstance(col, str) else col
for col in self.aggregation_index
for col in agg_index
]
window_spec = ibis.window(
group_by=partition_cols, order_by=input_table[self.event_date_column]
Expand Down Expand Up @@ -124,7 +128,7 @@ def aggregate(self, input_table: Table):

# Apply the distinct reduction if required
if self.reduce:
selected_columns = self.aggregation_index + [self.event_date_column]
selected_columns = list(agg_index) + [self.event_date_column]
input_table = input_table.select(selected_columns).distinct()
input_table = input_table.mutate(VALUE=ibis.null().cast("int32"))

Expand Down Expand Up @@ -255,10 +259,12 @@ def __init__(
# otherwise, row count is preserved

def aggregate(self, input_table: Table):
# Ensure INDEX_DATE is in the aggregation index if the table has it
agg_index = list(self.aggregation_index)
if "INDEX_DATE" in input_table.columns and "INDEX_DATE" not in agg_index:
agg_index.append("INDEX_DATE")
# Get the aggregation index columns
_aggregation_index_cols = [
getattr(input_table, col) for col in self.aggregation_index
]
_aggregation_index_cols = [getattr(input_table, col) for col in agg_index]

# Determine the aggregation column
if self.aggregation_column is None:
Expand Down
138 changes: 126 additions & 12 deletions phenex/core/cohort.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Cohort:
description: A plain text description of the cohort.
data_period: Restrict all input data to a specific date range. The input data will be modified to look as if data outside the data_period was never recorded before any phenotypes are computed. See DataPeriodFilterNode for details on how the input data are affected by this parameter.
custom_reporters: Additional reporter instances to run on this cohort only, after the default Waterfall and Table1 reporters. Each reporter must implement ``execute(cohort)`` and ``to_json(path)``.
write_subset_tables_entry: If True (default), materialize the entry-subset tables to the destination database. If False, keep them as lazy expressions instead.
write_subset_tables_index: If True (default), materialize the index-subset tables to the destination database. If False, keep them as lazy expressions instead.

Attributes:
table (PhenotypeTable): The resulting index table after filtering (None until execute is called)
Expand All @@ -67,10 +69,41 @@ def __init__(
description: Optional[str] = None,
database: Optional[Database] = None,
custom_reporters: Optional[List] = None,
return_index: str = "first",
max_index_dates: Optional[int] = None,
write_subset_tables_entry: bool = True,
write_subset_tables_index: bool = True,
):
self.name = name
self.description = description
self.database = database
self.return_index = return_index
self.max_index_dates = max_index_dates

assert return_index in (
"first",
"last",
"all",
), f"return_index must be 'first', 'last', or 'all', got '{return_index}'"
if max_index_dates is not None:
assert (
isinstance(max_index_dates, int) and max_index_dates > 0
), f"max_index_dates must be a positive integer, got {max_index_dates}"

# When return_index requires multiple candidate dates, auto-set entry criterion
if return_index in ("last", "all"):
if (
hasattr(entry_criterion, "return_date")
and entry_criterion.return_date != "all"
):
logger.info(
f"Cohort '{name}': return_index='{return_index}' requires entry criterion "
f"return_date='all'. Auto-setting from '{entry_criterion.return_date}'."
)
entry_criterion.return_date = "all"

self.write_subset_tables_entry = write_subset_tables_entry
self.write_subset_tables_index = write_subset_tables_index
self.table = None # Will be set during execution to index table
self.subset_tables_entry = None # Will be set during execution
self.subset_tables_index = None # Will be set during execution
Expand Down Expand Up @@ -123,6 +156,7 @@ def __init__(
self.derived_tables_stage = None
self.entry_stage = None
self.index_stage = None
self.subset_index_stage = None
self.derived_tables_post_entry_stage = None
self.reporting_stage = None

Expand Down Expand Up @@ -340,6 +374,8 @@ def build_stages(self, tables: Dict[str, PhenexTable]):
entry_phenotype=self.entry_criterion,
inclusion_table_node=self.inclusions_table_node,
exclusion_table_node=self.exclusions_table_node,
return_index=self.return_index,
max_index_dates=self.max_index_dates,
)
index_nodes.append(self.index_table_node)

Expand All @@ -363,10 +399,26 @@ def build_stages(self, tables: Dict[str, PhenexTable]):
domains=entry_domains,
index_phenotype=self.index_table_node,
)
self.index_stage = NodeGroup(
name="index_stage",
nodes=self.subset_tables_index_nodes + index_nodes,
)
if self.write_subset_tables_index:
# Default: materialize the index-subset tables together with the
# index nodes in a single multithreaded stage.
self.subset_index_stage = None
self.index_stage = NodeGroup(
name="index_stage",
nodes=self.subset_tables_index_nodes + index_nodes,
)
else:
# Keep the index-subset tables in a separate stage so they can be
# executed without materializing to the destination database
# (see execute()).
self.index_stage = NodeGroup(
name="index_stage",
nodes=index_nodes,
)
self.subset_index_stage = NodeGroup(
name="subset_index_stage",
nodes=self.subset_tables_index_nodes,
)

#
# Post-index / reporting stage: OPTIONAL
Expand Down Expand Up @@ -618,14 +670,55 @@ def execute(

logger.info(f"Cohort '{self.name}': executing entry stage ...")

self.entry_stage.execute(
tables=tables,
con=con,
overwrite=overwrite,
n_threads=n_threads,
lazy_execution=lazy_execution,
table_name_prefix=self.name,
)
if self.write_subset_tables_entry:
self.entry_stage.execute(
tables=tables,
con=con,
overwrite=overwrite,
n_threads=n_threads,
lazy_execution=lazy_execution,
table_name_prefix=self.name,
)
else:
# Execute entry criterion in-memory so .table stays on the source
# backend, avoiding cross-backend joins with subset tables.
self.entry_criterion.execute(
tables=tables,
con=con,
overwrite=overwrite,
n_threads=n_threads,
table_name_prefix=self.name,
lazy_execution=lazy_execution,
)
# # Write entry criterion and its dependencies to dest
# if con is not None:
# prefix = re.sub(r"[^A-Za-z0-9_]", "_", self.name).upper()
# node = self.entry_criterion
# # if node.table is not None:
# # db_name = (
# # f"{prefix}__{node.name}"
# # if not node.name.startswith(prefix)
# # else node.name
# # )
# # con.create_table(node.table, db_name, overwrite=overwrite)
# # Remove entry_criterion from subset table children so it won't be
# # re-executed; its .table is already set and SubsetTable._execute
# # accesses it via self.index_phenotype.table.
for node in self.subset_tables_entry_nodes:
node._children = [
c for c in node._children if c is not self.entry_criterion
]
self.entry_stage.execute(
tables=tables,
con=None,
overwrite=overwrite,
n_threads=n_threads,
table_name_prefix=self.name,
)
# Restore children for correct dependency graphs in later stages
for node in self.subset_tables_entry_nodes:
node._children.insert(0, self.entry_criterion)

self.subset_tables_entry = tables = self.get_subset_tables_entry(tables)

logger.info(f"Cohort '{self.name}': completed entry stage.")
Expand Down Expand Up @@ -666,6 +759,27 @@ def execute(
)
self.table = self.index_table_node.table

if not self.write_subset_tables_index:
# Execute the index-subset tables in-memory so they are not
# materialized to the destination database. The index table is
# already computed, so detach it from the subset nodes' children to
# avoid re-executing it; SubsetTable accesses it via
# self.index_phenotype.table.
for node in self.subset_tables_index_nodes:
node._children = [
c for c in node._children if c is not self.index_table_node
]
self.subset_index_stage.execute(
tables=self.subset_tables_entry,
con=None,
overwrite=overwrite,
n_threads=n_threads,
table_name_prefix=self.name,
)
# Restore children for correct dependency graphs in later stages
for node in self.subset_tables_index_nodes:
node._children.insert(0, self.index_table_node)

logger.info(f"Cohort '{self.name}': completed index stage.")
logger.info(f"Cohort '{self.name}': executing reporting stage ...")

Expand Down
20 changes: 15 additions & 5 deletions phenex/core/exclusions_table_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,27 @@ def __init__(
self.index_phenotype = index_phenotype

def _execute(self, tables: Dict[str, Table]):
exclusions_table = self.index_phenotype.table.select(["PERSON_ID"])
# Build base from entry criterion; add INDEX_DATE if ALL phenotype tables have it
if self.phenotypes and all(
"INDEX_DATE" in pt.table.columns for pt in self.phenotypes
):
join_keys = ["PERSON_ID", "INDEX_DATE"]
exclusions_table = self.index_phenotype.table.mutate(
INDEX_DATE=self.index_phenotype.table.EVENT_DATE
).select(["PERSON_ID", "INDEX_DATE"])
else:
join_keys = ["PERSON_ID"]
exclusions_table = self.index_phenotype.table.select(["PERSON_ID"])

for pt in self.phenotypes:
pt_table = pt.table.select(["PERSON_ID", "BOOLEAN"]).rename(
pt_table = pt.table.select([*join_keys, "BOOLEAN"]).rename(
**{
f"{pt.name}_BOOLEAN": "BOOLEAN",
}
)
exclusions_table = exclusions_table.left_join(pt_table, ["PERSON_ID"])
columns = exclusions_table.columns
columns.remove("PERSON_ID_right")
exclusions_table = exclusions_table.left_join(pt_table, join_keys)
drop_cols = [f"{k}_right" for k in join_keys]
columns = [c for c in exclusions_table.columns if c not in drop_cols]
exclusions_table = exclusions_table.select(columns)

# fill all nones with False
Expand Down
21 changes: 16 additions & 5 deletions phenex/core/inclusions_table_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,28 @@ def __init__(
self.index_phenotype = index_phenotype

def _execute(self, tables: Dict[str, Table]):
inclusions_table = self.index_phenotype.table.select(["PERSON_ID"])
# Build base from entry criterion; add INDEX_DATE if ALL phenotype tables have it
if self.phenotypes and all(
"INDEX_DATE" in pt.table.columns for pt in self.phenotypes
):
join_keys = ["PERSON_ID", "INDEX_DATE"]
# Derive (PERSON_ID, INDEX_DATE) pairs from entry criterion
inclusions_table = self.index_phenotype.table.mutate(
INDEX_DATE=self.index_phenotype.table.EVENT_DATE
).select(["PERSON_ID", "INDEX_DATE"])
else:
join_keys = ["PERSON_ID"]
inclusions_table = self.index_phenotype.table.select(["PERSON_ID"])

for pt in self.phenotypes:
pt_table = pt.table.select(["PERSON_ID", "BOOLEAN"]).rename(
pt_table = pt.table.select([*join_keys, "BOOLEAN"]).rename(
**{
f"{pt.name}_BOOLEAN": "BOOLEAN",
}
)
inclusions_table = inclusions_table.left_join(pt_table, ["PERSON_ID"])
columns = inclusions_table.columns
columns.remove("PERSON_ID_right")
inclusions_table = inclusions_table.left_join(pt_table, join_keys)
drop_cols = [f"{k}_right" for k in join_keys]
columns = [c for c in inclusions_table.columns if c not in drop_cols]
inclusions_table = inclusions_table.select(columns)

# fill all nones with False
Expand Down
Loading
Loading