Skip to content

Commit a9f5e03

Browse files
committed
fixed converted and summary
1 parent d57a5e3 commit a9f5e03

2 files changed

Lines changed: 70 additions & 29 deletions

File tree

src/cell2sentence4longevity/explore.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def _build_summary_expressions(
8686
exprs.extend([
8787
pl.col('age_years').min().alias('age_years_min'),
8888
pl.col('age_years').max().alias('age_years_max'),
89-
pl.col('age_years').mean().alias('age_years_mean'),
89+
pl.col('age_years').mean().round(2).alias('age_years_mean'),
9090
pl.col('age_years').is_not_null().sum().alias('cells_with_age_years')
9191
])
9292
else:
@@ -101,7 +101,7 @@ def _build_summary_expressions(
101101
exprs.extend([
102102
pl.col('age_months').min().alias('age_months_min'),
103103
pl.col('age_months').max().alias('age_months_max'),
104-
pl.col('age_months').mean().alias('age_months_mean'),
104+
pl.col('age_months').mean().round(2).alias('age_months_mean'),
105105
pl.col('age_months').is_not_null().sum().alias('cells_with_age_months')
106106
])
107107
else:
@@ -305,6 +305,37 @@ def _add_categorical_summaries(
305305
return summary_df
306306

307307

308+
def _mask_age_months_for_non_mouse(summary_df: pl.DataFrame) -> pl.DataFrame:
309+
"""Ensure age-in-months summary columns are only populated for mouse datasets.
310+
311+
For rows where organism is not 'Mus musculus', all age_months-related columns
312+
are set to null while keeping the columns present in the schema.
313+
"""
314+
if 'organism' not in summary_df.columns:
315+
return summary_df
316+
317+
age_months_columns = [
318+
'age_months_min',
319+
'age_months_max',
320+
'age_months_mean',
321+
'cells_with_age_months',
322+
'unique_ages_months',
323+
]
324+
existing_age_months_columns = [
325+
column for column in age_months_columns if column in summary_df.columns
326+
]
327+
if not existing_age_months_columns:
328+
return summary_df
329+
330+
return summary_df.with_columns([
331+
pl.when(pl.col('organism') == 'Mus musculus')
332+
.then(pl.col(column))
333+
.otherwise(None)
334+
.alias(column)
335+
for column in existing_age_months_columns
336+
])
337+
338+
308339
def _coerce_all_null_object_columns(
309340
df: pl.DataFrame,
310341
obs_schema: dict[str, pl.DataType]
@@ -746,6 +777,7 @@ def extract_fields_from_h5ad(
746777

747778
# Add categorical summaries (tissues, cell types, etc.)
748779
summary_df = _add_categorical_summaries(summary_df, summary_lazy, summary_schema)
780+
summary_df = _mask_age_months_for_non_mouse(summary_df)
749781

750782
# Log statistics
751783
if 'dataset_id' in summary_df.columns:
@@ -1129,6 +1161,7 @@ def batch(
11291161

11301162
# Add categorical summaries (tissues, cell types, etc.)
11311163
summary_df = _add_categorical_summaries(summary_df, meta_lazy, meta_schema)
1164+
summary_df = _mask_age_months_for_non_mouse(summary_df)
11321165

11331166
# Write summary to temp file
11341167
summary_file = summary_temp_dir / f"summary_{idx:04d}.parquet"
@@ -1173,6 +1206,7 @@ def batch(
11731206
for organism in organisms:
11741207
if organism and organism != 'unknown':
11751208
organism_summaries = combined.filter(pl.col('organism') == organism)
1209+
organism_summaries = _mask_age_months_for_non_mouse(organism_summaries)
11761210

11771211
# Create filename: replace spaces with underscores, lowercase
11781212
safe_organism = organism.lower().replace(' ', '_').replace('-', '_')

src/cell2sentence4longevity/preprocessing/h5ad_converter.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -721,17 +721,18 @@ def convert_h5ad_to_parquet(
721721
# Process a batch
722722
batch_end = min(cell_idx + batch_size, n_cells)
723723

724-
# Create cell sentences for batch without materializing a large dense block
725-
# This keeps memory bounded even for very wide matrices or large h5ad files.
724+
# Create cell sentences for batch using a dense block slice of adata.X.
725+
# This is significantly faster than per-row indexing while keeping
726+
# memory bounded by the configured batch_size.
726727
batch_sentence_results: list[dict[str, str]] = []
727-
for row_idx in range(cell_idx, batch_end):
728-
cell_row = adata.X[row_idx]
729-
# For sparse matrices, getrow().toarray() returns a small dense vector
730-
if hasattr(cell_row, "toarray"):
731-
cell_expr = cell_row.toarray().ravel()
732-
else:
733-
# Dense backend: rely on numpy array view
734-
cell_expr = np.asarray(cell_row).ravel()
728+
cell_block = adata.X[cell_idx:batch_end]
729+
if hasattr(cell_block, "toarray"):
730+
cell_block = cell_block.toarray()
731+
else:
732+
cell_block = np.asarray(cell_block)
733+
734+
for local_idx in range(cell_block.shape[0]):
735+
cell_expr = np.asarray(cell_block[local_idx]).ravel()
735736
batch_sentence_results.append(
736737
create_cell_sentence(cell_expr, gene_symbols, top_genes)
737738
)
@@ -835,7 +836,7 @@ def convert_h5ad_to_parquet(
835836

836837
n_chunks = chunk_idx
837838
else:
838-
# Row-based chunking (original logic)
839+
# Row-based chunking (original logic, optimized to use block slices of adata.X)
839840
n_chunks = (n_cells + chunk_size - 1) // chunk_size
840841
action.log(message_type="using_row_based_chunking", chunk_size=chunk_size, n_chunks=n_chunks)
841842

@@ -844,15 +845,18 @@ def convert_h5ad_to_parquet(
844845
start_idx = chunk_idx * chunk_size
845846
end_idx = min(start_idx + chunk_size, n_cells)
846847

847-
# Create cell sentences for each cell in chunk without creating a large dense block.
848-
# This keeps memory bounded even for very wide matrices or extremely large h5ad files.
848+
# Create cell sentences for each cell in chunk using a dense block
849+
# slice of adata.X instead of per-row indexing. This is much faster
850+
# while still keeping memory bounded by chunk_size.
849851
chunk_sentence_results: list[dict[str, str]] = []
850-
for row_idx in range(start_idx, end_idx):
851-
cell_row = adata.X[row_idx]
852-
if hasattr(cell_row, "toarray"):
853-
cell_expr = cell_row.toarray().ravel()
854-
else:
855-
cell_expr = np.asarray(cell_row).ravel()
852+
cell_block = adata.X[start_idx:end_idx]
853+
if hasattr(cell_block, "toarray"):
854+
cell_block = cell_block.toarray()
855+
else:
856+
cell_block = np.asarray(cell_block)
857+
858+
for local_idx in range(cell_block.shape[0]):
859+
cell_expr = np.asarray(cell_block[local_idx]).ravel()
856860
chunk_sentence_results.append(
857861
create_cell_sentence(cell_expr, gene_symbols, top_genes)
858862
)
@@ -1216,15 +1220,18 @@ def convert_h5ad_to_train_test(
12161220
start_idx = chunk_idx * chunk_size
12171221
end_idx = min(start_idx + chunk_size, n_cells)
12181222

1219-
# Create cell sentences for each cell in chunk without materializing a large dense block.
1220-
# This keeps memory bounded even for very wide matrices or very large h5ad files.
1223+
# Create cell sentences for each cell in chunk using a dense block slice
1224+
# of adata.X instead of per-row indexing. This is much faster while
1225+
# keeping memory bounded by chunk_size.
12211226
sentence_results: list[dict[str, str]] = []
1222-
for row_idx in range(start_idx, end_idx):
1223-
cell_row = adata.X[row_idx]
1224-
if hasattr(cell_row, "toarray"):
1225-
cell_expr = cell_row.toarray().ravel()
1226-
else:
1227-
cell_expr = np.asarray(cell_row).ravel()
1227+
cell_block = adata.X[start_idx:end_idx]
1228+
if hasattr(cell_block, "toarray"):
1229+
cell_block = cell_block.toarray()
1230+
else:
1231+
cell_block = np.asarray(cell_block)
1232+
1233+
for local_idx in range(cell_block.shape[0]):
1234+
cell_expr = np.asarray(cell_block[local_idx]).ravel()
12281235
sentence_results.append(
12291236
create_cell_sentence(
12301237
cell_expr,

0 commit comments

Comments
 (0)