causalatee.data.utils
utils
Shared helpers for causalatee's HF-datasets-native schema: entity-marker
parsing, whole-document -> per-sentence re-splitting, and same-granularity
schema derivations -- everything a dataset conversion_script.py or a
downstream evaluation harness needs that isn't specific to any one corpus.
Split into submodules by concern (all re-exported here, so
from causalatee.data.utils import X keeps working unchanged):
markers--parse_entity_markers/insert_entity_markers, the<eN>...</eN>marker <->(clean_text, segments_by_eid)roundtrip used by causalatee's identification schema.splitting--split_document_by_sentence(the generic core: given sentence ranges, entity segments, and "groups" of entity ids that must stay together, keep every group wholly inside one sentence, drop and COUNT the rest) plussplit_identification_to_sentences/split_extraction_to_sentences, single-example wrappers over it.
Several source corpora (BioCause, CaTeRS's brat annotations, CREST's own
aggregation) annotate causal relations over a whole document or section,
not a single sentence. A per-sentence schema cannot represent a relation
whose participants straddle a sentence boundary (discourse-level
causality really does occur, e.g. BioCause: "X requires Y ... [new
sentence] This substitution decreases Z") -- silently re-basing such a
span onto the wrong sentence's text is a real bug, not a hypothetical one
(previously observed: a marker landing mid-word, "hsplitting's job is to make the
correct behavior a single, tested, reusable piece of code instead of a
duplicated ad-hoc guard in every converter.
* batch -- Dataset.map(fn, batched=True, remove_columns=[...])-
ready adapters over the above:
identification_batch_to_sentences,
extraction_batch_to_sentences,
identification_batch_to_detection_sentences
(re-splitting to sentence level), and
identification_batch_to_detection /
identification_batch_to_extraction
(same-granularity detection/extraction tables derived from an identification table, for datasets like
CTB/ESL that only ship identification).
* validation -- verify_dataset, structural sanity checks
(conflicting/duplicate relation pairs, entity ids with no parseable
marker span) run over a converted split. Every conversion_script.py
should call this right before writing a split's parquet file and print
any returned errors as warnings.
IMPORTANT (see the markers module docstring for the full explanation): the <eN>...</eN>
markers only look like XML/HTML tags -- they are not, and do not need to
be well-nested. Do not "fix" a non-nested marker sequence, and do not add
crossing-span detection/dropping logic on the assumption that it's needed
here -- it isn't.
Typical usage, re-splitting an already-loaded whole-document identification split to sentence level (a detection or extraction split works the same way with the matching batch function)::
import spacy
from causalatee.data.utils import identification_batch_to_sentences
nlp = spacy.load("en_core_web_sm", disable=["ner", "lemmatizer", "tagger"])
def sentence_ranges(text):
return [(s.start_char, s.end_char) for s in nlp(text).sents]
per_sentence = whole_document_ds.map(
lambda batch: identification_batch_to_sentences(batch, sentence_ranges),
batched=True,
remove_columns=whole_document_ds.column_names,
)
Or, deriving a detection/extraction table at the SAME granularity for a dataset that only ships identification (no sentence splitting involved)::
from causalatee.data.utils import identification_batch_to_detection
detection_ds = identification_ds.map(
identification_batch_to_detection,
batched=True,
remove_columns=identification_ds.column_names,
)
See conf-causality-repro/evaluation/data.py for a complete example
that dispatches all three sub-tasks this way.
Functions:
extraction_batch_to_sentences
extraction_batch_to_sentences(batch: Mapping[str, Sequence], sentence_ranges_fn: Callable[[str], Sequence[Span]]) -> dict[str, list]
Batched wrapper around split_extraction_to_sentences.
Source code in causalatee/data/utils/batch.py
69 70 71 72 73 74 75 76 77 78 79 80 | |
identification_batch_to_detection
identification_batch_to_detection(batch: Mapping[str, Sequence]) -> dict[str, list]
Derive a causality-DETECTION batch (text + 0/1 label) from an IDENTIFICATION batch, at the SAME granularity (no sentence splitting).
A row is causal iff it has at least one ACTUALLY CAUSAL relation (see _filter_causal_relations -- a
merely non-empty relations list is NOT enough, since some converters explicitly list NoRelation entries for
non-causal pairs rather than omitting them). This matches how every converter in this project derives a
detection label from relation presence (e.g. CCNC's countercausal sentences, which still count as causal for
detection since they're about a causal relationship, just negating it). <eN>...</eN> markers are stripped
from text.
NOT the right choice for BioCause: its own detection convention counts a document as causal if it has ANY marked
entity, even an unpaired Effect-only one with no Cause and hence no relation -- see
identification_batch_to_detection_sentences
for that case.
Source code in causalatee/data/utils/batch.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
identification_batch_to_detection_sentences
identification_batch_to_detection_sentences(batch: Mapping[str, Sequence], sentence_ranges_fn: Callable[[str], Sequence[Span]]) -> dict[str, list]
Derive per-sentence causality-DETECTION rows (text + 0/1 label) from a whole-document IDENTIFICATION batch.
Some corpora's own detection table is whole-document/section only (e.g. BioCause) even though their identification table marks every entity -- cause+effect pairs AND unpaired effect-only ones alike. This reuses that richer information to still produce a per-sentence label: a sentence is causal if it contains ANY marked entity at all, matching the common "a document is causal if it contains any causal event, span or no span" convention.
Source code in causalatee/data/utils/batch.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
identification_batch_to_extraction
identification_batch_to_extraction(batch: Mapping[str, Sequence]) -> dict[str, list]
Derive a causal-candidate-EXTRACTION batch (text + entity spans) from an IDENTIFICATION batch, at the SAME granularity (no sentence splitting).
Only entities that participate in at least one ACTUALLY CAUSAL relation are kept (see
_filter_causal_relations) -- matching this project's "only causal_eids" convention used by other
converters throughout. Rows with no causal relation at all are dropped from the output entirely (nothing to
extract), so the output can have fewer rows than the input, same as this module's sentence-splitting adapters.
Source code in causalatee/data/utils/batch.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
identification_batch_to_sentences
identification_batch_to_sentences(batch: Mapping[str, Sequence], sentence_ranges_fn: Callable[[str], Sequence[Span]]) -> dict[str, list]
Batched wrapper around
split_identification_to_sentences.
sentence_ranges_fn(clean_text) -> [(start, end), ...] supplies sentence boundaries for the marker-stripped
text (any segmenter -- spaCy, nltk, a regex splitter).
Source code in causalatee/data/utils/batch.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
insert_entity_markers
insert_entity_markers(text: str, segments_by_eid: Mapping[str, Sequence[Span]]) -> str
Insert <eN>...</eN> tags around each id's segment(s).
Left-to-right, stack-based: closes innermost-first and opens outermost (longest span) first at each boundary, so
nested and disjoint spans come out correctly nested. Output need not be nested when spans cross -- see the
module docstring. Inverse of parse_entity_markers.
Source code in causalatee/data/utils/markers.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
parse_entity_markers
parse_entity_markers(text: str) -> tuple[str, dict[str, list[Span]]]
Strip <eN>/</eN> tags; return (clean_text, segments_by_eid).
An id may open/close more than once -- a discontinuous mention, rendered as repeated <eN>...</eN> occurrences
sharing the same id -- every occurrence accumulates onto that id's segment list, in left-to-right order. Inverse
of insert_entity_markers.
NOT an XML parser: matching is per entity id (module docstring has the details), so text does not need to be
well-nested -- markers for two genuinely crossing spans parse correctly.
Source code in causalatee/data/utils/markers.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
split_document_by_sentence
split_document_by_sentence(sent_ranges: Sequence[Span], entity_segments: Mapping[str, Sequence[Span]], groups: Sequence[Sequence[str]]) -> tuple[list[dict], int]
Assign each entity group to the one sentence it fits wholly inside.
A group is a list of entity ids that must be kept together (e.g. the two participants of a relation, or a
single unpaired entity). A group survives only if every entity id it references has ALL of its segments wholly
inside the SAME sentence's character range; groups that straddle a sentence boundary, or whose entities don't
overlap any known sentence at all, are dropped and counted rather than silently corrupted or discarded without a
trace.
Returns (rows, num_dropped): rows has one dict per input sentence range, in order, {"entities": {eid:
[(local_start, local_end), ...]}, "group_indices": [i, ...]} -- entity offsets already re-based to be local to
that sentence, and group_indices the indices into the input groups list that landed in this sentence
(empty for sentences with no surviving group, so callers can always align rows 1:1 with sent_ranges).
Source code in causalatee/data/utils/splitting.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
split_extraction_to_sentences
split_extraction_to_sentences(text: str, entities: Sequence[Sequence[int]], sent_ranges: Sequence[Span]) -> tuple[list[dict], int]
Whole-document causal-candidate-EXTRACTION example -> per-sentence rows.
entities is causalatee's entity field: a list of flat, even-length document-level offset lists ([s,
e] contiguous, [s1, e1, s2, e2, ...] discontinuous). Returns (rows, num_dropped) where each row is
{"text": ..., "entity": [[...], ...]}, one per sentence, and num_dropped counts entities that don't fit
wholly inside a single sentence.
Source code in causalatee/data/utils/splitting.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
split_identification_to_sentences
split_identification_to_sentences(text: str, relations: Sequence[Mapping], sent_ranges: Sequence[Span]) -> tuple[list[dict], int]
Whole-document causality-IDENTIFICATION example -> per-sentence rows.
text carries <eN>...</eN> entity markers (causalatee's identification schema); relations is the
matching list of {"relationship", "first": eid, "second": eid} dicts. sent_ranges are character offsets
into the MARKER-STRIPPED text (i.e. what parse_entity_markers
returns as clean_text), not the raw
text -- markers shift offsets, so a sentence segmenter should run on the clean text first. Returns (rows,
num_dropped) where each row is {"text": ..., "relations": [...]}, one per sentence, and num_dropped
counts relations whose two entities don't both fit inside a single sentence.
Source code in causalatee/data/utils/splitting.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
verify_dataset
verify_dataset(batch: Mapping[str, Sequence] | DataFrame, task: Task) -> list[str]
Run every applicable structural check over one converted split.
batch is column-oriented (batch["text"], batch["relations"], ...) -- same convention as the
batch module's Dataset.map-ready functions, so a pandas.DataFrame works directly by column name right
before .to_parquet(), with no conversion needed. Returns a flat list of human-readable error descriptions
(one entry per issue found), or [] if the split is clean.
Source code in causalatee/data/utils/validation.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |