Slices and analysis contexts¶
Which interactions differ between two analysis results, and how do their activities compare? Import one network and join a result table onto its edge IDs. Each condition gets a slice and its own edge annotations.
The notebook uses the same synthetic guide data as
Annotations, selections, and views: put the CSV
files in a data/ directory beside this notebook. The activities are invented
values. The notebook requires annnet[polars] and does not depend on the other
guide chapters.
What a slice represents¶
A slice is a named membership set over the graph's existing identities. Several slices can include one edge without duplicating its topology. This makes slices useful for prior networks, selected solutions, train/test partitions, and alternative annotations or weighting schemes.
A layer, in contrast, is a coordinate of an endpoint placement. Use a slice when an analysis selects existing interactions; use layers when separate placements and connections between them belong to the model. The two can be combined.
Creating a slice from selection IDs records membership at that moment. It does not retain the selection's predicate. Later confidence updates do not rewrite the slice automatically.
import annnet as an
import polars as pl
interactions = pl.read_csv('data/interactions.csv')
anet = an.from_edge_frame(interactions, edge_id='edge_id', sign='effect', slice='prior')
anet.attrs.backend = 'polars'
Read the result table¶
Each row names an existing edge, a condition, and an activity. A missing row means the analysis did not select that edge in that condition. Activity zero would be a measured value, not an instruction to omit the edge.
results = pl.read_csv('data/edge_results.csv')
results
| edge_id | condition | activity |
|---|---|---|
| str | str | f64 |
| "egfr_grb2" | "control" | 1.0 |
| "grb2_sos1" | "control" | 0.8 |
| "sos1_ras" | "control" | 0.5 |
| "ras_raf" | "control" | 0.4 |
| "raf_mek" | "control" | 0.3 |
| … | … | … |
| "ras_raf" | "treated" | 1.2 |
| "raf_mek" | "treated" | 1.0 |
| "mek_erk" | "treated" | 0.9 |
| "erk_dusp6" | "treated" | 0.7 |
| "dusp6_erk" | "treated" | -0.5 |
Check the join before you write results. An unknown identifier means that the network and the result table do not agree. If you discard such a row without a message, you hide that mismatch. An empty set means that all result rows name edges of the network.
set(results['edge_id']) - set(anet.E)
set()
Store membership and values separately¶
The loop handles one condition per batch. Slice membership records which edges
were selected. edge_slices stores each activity under (condition, edge_id).
The original confidence, sign, and structural weight remain on the base edge.
for condition in ('control', 'treated'):
rows = results.filter(pl.col('condition') == condition)
anet.slices.add(condition, edges=rows['edge_id'].to_list(), method='synthetic example')
anet.attrs.update(
'edge_slices',
{
(condition, row['edge_id']): {'activity': row['activity']}
for row in rows.iter_rows(named=True)
},
)
{name: len(anet.slices.edges(name)) for name in ('prior', 'control', 'treated')}
{'prior': 11, 'control': 8, 'treated': 8}
Both result slices contain eight edges, but they are not the same eight. The graph still holds 11 structural edges. An edge selected twice has two contextual activity values and one structural identity.
comparison = anet.slices.compare('control', 'treated', axis='edges')
comparison
| edge_id | status |
|---|---|
| str | str |
| "akt_foxo3" | "a_only" |
| "dusp6_erk" | "b_only" |
| "egfr_akt" | "a_only" |
| "egfr_grb2" | "both" |
| "erk_dusp6" | "b_only" |
| "grb2_sos1" | "both" |
| "mek_erk" | "both" |
| "raf_mek" | "both" |
| "ras_raf" | "both" |
| "sos1_ras" | "both" |
Six records are shared. EGFR–AKT and AKT–FOXO3 occur only in control; ERK–DUSP6 and DUSP6–ERK occur only in treatment. These statements concern membership in the supplied result, not biological absence.
anet.slices.edge_frame(slices=['control', 'treated'], attrs=['activity'])
| slice_id | akt_foxo3 | dusp6_erk | egfr_akt | egfr_grb2 | erk_dusp6 | grb2_sos1 | mek_erk | raf_mek | ras_raf | sos1_ras |
|---|---|---|---|---|---|---|---|---|---|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| "control" | -0.3 | null | 0.5 | 1.0 | null | 0.8 | 0.2 | 0.3 | 0.4 | 0.5 |
| "treated" | null | -0.5 | null | 1.2 | 0.7 | 1.0 | 0.9 | 1.0 | 1.2 | 1.1 |
The wide table aligns values across conditions. Nulls show missing values for a pair. Use the long representation when you want to group or plot the results with your dataframe library.
anet.slices.edge_frame(slices=['control', 'treated'], attrs=['activity'], format='long')
| edge_id | slice_id | attr | value |
|---|---|---|---|
| str | str | str | f64 |
| "akt_foxo3" | "control" | "activity" | -0.3 |
| "dusp6_erk" | "control" | "activity" | null |
| "egfr_akt" | "control" | "activity" | 0.5 |
| "egfr_grb2" | "control" | "activity" | 1.0 |
| "erk_dusp6" | "control" | "activity" | null |
| … | … | … | … |
| "grb2_sos1" | "treated" | "activity" | 1.0 |
| "mek_erk" | "treated" | "activity" | 0.9 |
| "raf_mek" | "treated" | "activity" | 1.0 |
| "ras_raf" | "treated" | "activity" | 1.2 |
| "sos1_ras" | "treated" | "activity" | 1.1 |
Work on one condition¶
A slice view restricts membership and reads current parent annotations. A materialized graph is independent. Here it contains the eight selected treatment edges while the original graph continues to hold the whole prior.
treated = anet.view(slices='treated')
treated_anet = treated.materialize()
print('Prior:', len(anet.E), 'edges; treatment:', len(treated_anet.E), 'edges')
print('Treatment activity:', anet.attrs.row('edge_slices', ('treated', 'dusp6_erk')))
print('Base weight:', anet.E.at('dusp6_erk').weight)
Prior: 11 edges; treatment: 8 edges
Treatment activity: {'activity': -0.5}
Base weight: 1.0
Join values or filter membership¶
slice= joins contextual attributes onto the edge table; in_slice= filters
which edges get rows. An analysis that needs only treatment edges should use
the latter. Adding the contextual columns alone does not select a subgraph.
joined = anet.attrs.table('edges', derived=True, slice='treated')
filtered = anet.attrs.table('edges', derived=True, in_slice='treated')
print('Joined:', joined.height, 'rows; filtered:', filtered.height, 'rows')
Joined: 11 rows; filtered: 8 rows
Membership algebra also returns selections suitable for a view. The intersection below retains the six interactions selected by both conditions.
both = anet.view(slices=anet.slices.intersect(['control', 'treated']))
both.E.ids
('egfr_grb2', 'grb2_sos1', 'sos1_ras', 'ras_raf', 'raf_mek', 'mek_erk')
Contextual weights and mutation scope¶
An edge-slice weight overrides the effective weight in that slice. It does
not rewrite the base weight or automatically replace every global matrix.
Choose a context-aware operation when those overrides must affect computation.
anet.attrs.update('edge_slices', {('treated', 'mek_erk'): {'weight': 2.0}})
print('Base:', anet.E.at('mek_erk').weight)
print('Treated:', anet.E.effective_weight('mek_erk', 'treated'))
Base: 1.0 Treated: 2.0
New edges go into an explicit slice= or the active slice. The edge itself is
global structure. With propagate=, other slices also get it: 'shared' adds
it to each slice that holds both endpoints, 'all' to each slice that holds
one or both of them. The default 'none' adds it only to its own slice.
propagate= works for one edge, and with default_propagate= for a batch.
anet.add_edges('MEK', 'DUSP6', edge_id='mek_dusp6', slice='control', propagate='shared')
anet.slices.edge_presence('mek_dusp6')
['prior', 'control', 'treated']
MEK and DUSP6 are in the prior and in both result slices, so all three get the new edge.
Adding only nodes to a slice does not add the edges between them. Use
anet.slices.induce_edges(name) for that.
A global remove_edges call would remove an edge from every slice. To revise
one result, update that slice's membership or contextual annotations.
Use slices when several analyses refer to the same interaction identities. Continue with Layers when endpoint placements and connections between coordinates are part of the model.