Skip to content

Commit b81fe6e

Browse files
authored
Merge branch 'master' into GH1056-local-file-urls-escaped-paths
2 parents 74fb81f + 9ef3865 commit b81fe6e

5 files changed

Lines changed: 109 additions & 58 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__pycache__
44
*.pyc
55
dist
6+
.venv/
67
.coverage
78
coverage.xml
89
.ipynb_checkpoints/

malariagen_data/anoph/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,9 @@ def _read_sample_sets_manifest(self, *, single_release: str):
607607
# Get today's date in ISO format
608608
today_date_iso = date.today().isoformat()
609609
# Add an "unrestricted_use" column, set to True if terms-of-use expiry date <= today's date.
610-
df["unrestricted_use"] = df[terms_of_use_expiry_date_column].apply(
611-
lambda d: True if pd.isna(d) else (d <= today_date_iso)
612-
)
610+
# Vectorized operation: True if NaN, else (d <= today_date_iso)
611+
s = df[terms_of_use_expiry_date_column]
612+
df["unrestricted_use"] = s.isna() | (s <= today_date_iso)
613613
# Make the "unrestricted_use" column a nullable boolean, to allow missing data.
614614
df["unrestricted_use"] = df["unrestricted_use"].astype(pd.BooleanDtype())
615615

malariagen_data/anoph/frq_base.py

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import re
21
from textwrap import dedent
32
from typing import Optional, Union, List
43

@@ -50,40 +49,39 @@ def _prep_samples_for_cohort_grouping(
5049

5150
# Add period column.
5251

53-
# Map supported period_by values to functions that return either the relevant pd.Period or pd.NaT per row.
54-
period_by_funcs = {
55-
"year": _make_sample_period_year,
56-
"quarter": _make_sample_period_quarter,
57-
"month": _make_sample_period_month,
52+
# Map supported period_by values to vectorized functions that create Period arrays.
53+
period_by_funcs_vectorized = {
54+
"year": _make_sample_periods_year_vectorized,
55+
"quarter": _make_sample_periods_quarter_vectorized,
56+
"month": _make_sample_periods_month_vectorized,
5857
}
5958

6059
# Get the matching function for the specified period_by value, or None.
61-
period_by_func = period_by_funcs.get(period_by)
60+
period_by_func_vectorized = period_by_funcs_vectorized.get(period_by)
6261

6362
# If there were no matching functions for the specified period_by value...
64-
if period_by_func is None:
63+
if period_by_func_vectorized is None:
6564
# Raise a ValueError if the specified period_by value is not a column in the DataFrame.
6665
if period_by not in df_samples.columns:
6766
raise ValueError(
6867
f"Invalid value for `period_by`: {period_by!r}. Either specify the name of an existing column "
6968
"or a supported period: 'year', 'quarter', or 'month'."
7069
)
7170

72-
# Raise a ValueError if the specified period_by column does not contain instances pd.Period.
73-
if (
74-
not df_samples[period_by]
75-
.apply(lambda value: pd.isnull(value) or isinstance(value, pd.Period))
76-
.all()
77-
):
78-
raise TypeError(
79-
f"Invalid values in {period_by!r} column. Must be either pandas.Period or null."
80-
)
71+
# Validate the specified period_by column contains pandas Periods (or nulls).
72+
s_period_by = df_samples[period_by]
73+
if not pd.api.types.is_period_dtype(s_period_by.dtype):
74+
non_null = s_period_by.dropna()
75+
if len(non_null) > 0 and not non_null.map(type).eq(pd.Period).all():
76+
raise TypeError(
77+
f"Invalid values in {period_by!r} column. Must be either pandas.Period or null."
78+
)
8179

8280
# Copy the specified period_by column to a new "period" column.
8381
df_samples["period"] = df_samples[period_by]
8482
else:
85-
# Apply the matching period_by function to create a new "period" column.
86-
df_samples["period"] = df_samples.apply(period_by_func, axis="columns")
83+
# Use the vectorized period creation function.
84+
df_samples["period"] = period_by_func_vectorized(df_samples)
8785

8886
# Validate area_by.
8987
if area_by not in df_samples.columns:
@@ -115,22 +113,39 @@ def _build_cohorts_from_sample_grouping(
115113
df_cohorts = df_cohorts.reset_index()
116114

117115
# Add cohort helper variables.
118-
cohort_period_start = df_cohorts["period"].apply(lambda v: v.start_time)
119-
cohort_period_end = df_cohorts["period"].apply(lambda v: v.end_time)
120-
df_cohorts["period_start"] = cohort_period_start
121-
df_cohorts["period_end"] = cohort_period_end
116+
# Vectorized extraction of period start/end times.
117+
period = df_cohorts["period"]
118+
if pd.api.types.is_period_dtype(period.dtype):
119+
df_cohorts["period_start"] = period.dt.start_time
120+
df_cohorts["period_end"] = period.dt.end_time
121+
else:
122+
# Fallback for object dtype Period values.
123+
df_cohorts["period_start"] = period.map(
124+
lambda v: v.start_time if pd.notna(v) else pd.NaT
125+
)
126+
df_cohorts["period_end"] = period.map(
127+
lambda v: v.end_time if pd.notna(v) else pd.NaT
128+
)
129+
122130
# Create a label that is similar to the cohort metadata,
123131
# although this won't be perfect.
132+
# Vectorized string operations
124133
if taxon_by == frq_params.taxon_by_default:
125-
df_cohorts["label"] = df_cohorts.apply(
126-
lambda v: f"{v.area}_{v[taxon_by][:4]}_{v.period}", axis="columns"
127-
)
134+
# Default case: area_taxon_short_period
135+
area_str = df_cohorts["area"].astype(str)
136+
taxon_short = df_cohorts[taxon_by].astype(str).str.slice(0, 4)
137+
period_str = df_cohorts["period"].astype(str)
138+
df_cohorts["label"] = area_str + "_" + taxon_short + "_" + period_str
128139
else:
129-
# Replace non-alphanumeric characters in the taxon with underscores.
130-
df_cohorts["label"] = df_cohorts.apply(
131-
lambda v: f"{v.area}_{re.sub(r'[^A-Za-z0-9]+', '_', str(v[taxon_by]))}_{v.period}",
132-
axis="columns",
140+
# Non-default case: replace non-alphanumeric characters with underscores
141+
area_str = df_cohorts["area"].astype(str)
142+
taxon_clean = (
143+
df_cohorts[taxon_by]
144+
.astype(str)
145+
.str.replace(r"[^A-Za-z0-9]+", "_", regex=True)
133146
)
147+
period_str = df_cohorts["period"].astype(str)
148+
df_cohorts["label"] = area_str + "_" + taxon_clean + "_" + period_str
134149

135150
# Apply minimum cohort size.
136151
df_cohorts = df_cohorts.query(f"size >= {min_cohort_size}").reset_index(drop=True)
@@ -187,6 +202,50 @@ def _make_sample_period_year(row):
187202
return pd.NaT
188203

189204

205+
def _make_sample_periods_month_vectorized(df_samples):
206+
year = df_samples["year"]
207+
month = df_samples["month"]
208+
valid = (year > 0) & (month > 0)
209+
210+
out = pd.Series(pd.NaT, index=df_samples.index, dtype="period[M]")
211+
if valid.any():
212+
out.loc[valid] = pd.PeriodIndex.from_fields(
213+
year=year.loc[valid].to_numpy(),
214+
month=month.loc[valid].to_numpy(),
215+
freq="M",
216+
)
217+
return out
218+
219+
220+
def _make_sample_periods_quarter_vectorized(df_samples):
221+
year = df_samples["year"]
222+
month = df_samples["month"]
223+
valid = (year > 0) & (month > 0)
224+
225+
out = pd.Series(pd.NaT, index=df_samples.index, dtype="period[Q-DEC]")
226+
if valid.any():
227+
out.loc[valid] = pd.PeriodIndex.from_fields(
228+
year=year.loc[valid].to_numpy(),
229+
month=month.loc[valid].to_numpy(),
230+
freq="Q-DEC",
231+
)
232+
return out
233+
234+
235+
def _make_sample_periods_year_vectorized(df_samples):
236+
year = df_samples["year"]
237+
valid = year > 0
238+
239+
out = pd.Series(pd.NaT, index=df_samples.index, dtype="period[Y-DEC]")
240+
if valid.any():
241+
out.loc[valid] = pd.PeriodIndex.from_fields(
242+
year=year.loc[valid].to_numpy(),
243+
month=np.full(int(valid.sum()), 12, dtype="int64"),
244+
freq="Y-DEC",
245+
)
246+
return out
247+
248+
190249
class AnophelesFrequencyAnalysis(AnophelesBase):
191250
def __init__(
192251
self,
@@ -277,14 +336,10 @@ def plot_frequencies_heatmap(
277336
index = list(index_names_as_list)
278337
df = df.reset_index().copy()
279338
if isinstance(index, list):
280-
index_col = (
281-
df[index]
282-
.astype(str)
283-
.apply(
284-
lambda row: ", ".join([o for o in row if o is not None]),
285-
axis="columns",
286-
)
287-
)
339+
idx_vals = df[index].astype(str).to_numpy()
340+
index_col = pd.Series(idx_vals[:, 0], index=df.index)
341+
for j in range(1, idx_vals.shape[1]):
342+
index_col = index_col + ", " + idx_vals[:, j]
288343
else:
289344
assert isinstance(index, str)
290345
index_col = df[index].astype(str)

malariagen_data/anoph/genome_features.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -446,29 +446,26 @@ def plot_genes(
446446

447447
# Put gene pointers (▲ or ▼) in a new column, depending on the strand.
448448
# Except if the gene_label is null or an empty string, which should not be shown.
449-
data["gene_pointer"] = data.apply(
450-
lambda row: ("▼" if row["strand"] == "+" else "▲")
451-
if row["gene_label"]
452-
else "",
453-
axis=1,
449+
data["gene_pointer"] = np.where(
450+
data["gene_label"] == "",
451+
"",
452+
np.where(data["strand"] == "+", "▼", "▲"),
454453
)
455454

456455
# Put the pointer above or below the gene rectangle, depending on + or - strand.
457456
neg_strand_pointer_y = orig_mid_y_range - 1.1
458457
pos_strand_pointer_y = orig_mid_y_range + 1.1
459-
data["pointer_y"] = data["strand"].apply(
460-
lambda strand: pos_strand_pointer_y
461-
if strand == "+"
462-
else neg_strand_pointer_y
458+
# Vectorized operation: use np.where instead of Series.apply
459+
data["pointer_y"] = np.where(
460+
data["strand"] == "+", pos_strand_pointer_y, neg_strand_pointer_y
463461
)
464462

465463
# Put the label above or below the gene rectangle, depending on + or - strand.
466464
neg_strand_label_y = orig_mid_y_range - 1.25
467465
pos_strand_label_y = orig_mid_y_range + 1.3
468-
data["label_y"] = data["strand"].apply(
469-
lambda strand: pos_strand_label_y
470-
if strand == "+"
471-
else neg_strand_label_y
466+
# Vectorized operation: use np.where instead of Series.apply
467+
data["label_y"] = np.where(
468+
data["strand"] == "+", pos_strand_label_y, neg_strand_label_y
472469
)
473470

474471
# Get the data as a ColumnDataSource.

malariagen_data/anoph/sample_metadata.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,8 @@ def _parse_general_metadata(
186186
df["release"] = release
187187

188188
# Derive a quarter column from month.
189-
df["quarter"] = df.apply(
190-
lambda row: ((row.month - 1) // 3) + 1 if row.month > 0 else -1,
191-
axis="columns",
192-
)
189+
# Vectorized operation: quarter = ((month - 1) // 3) + 1 if month > 0 else -1
190+
df["quarter"] = np.where(df["month"] > 0, ((df["month"] - 1) // 3) + 1, -1)
193191

194192
# Add study columns.
195193
study_info = self.lookup_study_info(sample_set=sample_set)

0 commit comments

Comments
 (0)