Skip to main content

polars cNLP

polars-cnlp adds clinical NLP (cnlp) and research-data-wrangling (rdw) expressions to Polars.

Importing polars_cnlp registers two expression namespaces:

  • .cnlp — clinical concept matching and contextual interpretation
  • .rdw — general research data-wrangling string helpers
import polars as pl
import polars_cnlp

Once imported, the namespaces are available directly on Polars expressions:

pl.col('note_text').cnlp.affirmed(r'\bpneumonia\b')
pl.col('code').rdw.starts_with_any(['A', 'B', 'C'])

Contents

Installation

pip install polars-cnlp

Then:

import polars as pl
import polars_cnlp

Quick start

PNEUMONIA = r'\bpneumonia\b'

df = pl.DataFrame({'note_text': [
    'Patient has pneumonia.',
    'No pneumonia.',
    'Possible pneumonia.',
    'History of pneumonia.',
    'Family history of pneumonia.',
    'Patient has asthma.',
    None,
]})

result = df.with_columns(
    pl.col('note_text').cnlp.affirmed(PNEUMONIA).alias('pneumonia'),
)

print(result)

affirmed() returns:

  • True when at least one matching finding is affirmed, current, and experienced by the patient.
  • False when the concept is mentioned but all matching findings are non-affirmed, historical, hypothetical, or attributed to another experiencer.
  • null when the concept is not mentioned or the input text is null.

For the example above:

[True, False, False, False, False, None, None]

Clinical NLP namespace: .cnlp

API overview

Method Input Output Typical Usage
contains() regex Boolean Does the note mention a pattern?
count() regex UInt32 How many times does one concept occur?
count_all() {label: regex} Struct[UInt32] Count several concepts at once
affirmed() regex Boolean/null Is one concept affirmed?
affirmed_any() sequence or mapping Boolean/null Is any requested concept affirmed?
affirmed_all() sequence or mapping Boolean/null Are all requested concepts affirmed?
affirmed_each() {label: regex} Struct[Boolean] Get affirmation status for every concept
find_best() regex or mapping Struct/null Return the highest-ranked contextual finding
find_all() regex or mapping List[Struct]/null Return every contextualized finding

The contextual methods use ConText by default. NegEx and custom algorithms can be selected with the algorithm= argument.

cnlp.contains

contains(pattern: str) -> pl.Expr

Check whether text contains a regular expression.

df = pl.DataFrame({'note_text': [
    'Pneumonia present.',
    'No respiratory findings.',
    None,
]})

result = df.select(
    pl.col('note_text').cnlp.contains(r'\bpneumonia\b').alias('has_pneumonia'),
)

Use cases:

  • build broad candidate cohorts before contextual analysis;
  • identify notes containing a disease, medication, procedure, or symptom;
  • combine regex presence checks with normal Polars filters.
candidates = df.filter(
    pl.col('note_text').cnlp.contains(r'\banaphyl(?:axis|actic)\b'),
)

contains() only checks whether the regex occurs. It does not determine whether the concept is negated, historical, hypothetical, or attributed to another experiencer.

cnlp.count

count(term: str) -> pl.Expr

Count occurrences of one concept.

PNEUMONIA = r'\bpneumonia\b'

df = pl.DataFrame({'note_text': [
    'Pneumonia. Pneumonia improved.',
    'No pneumonia.',
    'Asthma only.',
    None,
]})

result = df.select(
    pl.col('note_text').cnlp.count(PNEUMONIA).alias('pneumonia_count'),
)

Expected counts:

[2, 1, 0, None]

Use count() for mention frequency, simple NLP features, or identifying notes with repeated discussion of a concept.

Because count() does not perform contextual analysis, it is cheaper than find_all() or affirmed().

cnlp.count_all

count_all(terms: Mapping[str, str]) -> pl.Expr

Count several named concepts in one expression.

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
    'asthma': r'\basthma\b',
}

df = pl.DataFrame({'note_text': [
    'Pneumonia and asthma. Pneumonia again.',
    'Anaphylaxis.',
]})

result = df.select(
    pl.col('note_text').cnlp.count_all(terms).alias('counts'),
)

The result is a Struct resembling:

{
    pneumonia: 2,
    anaphylaxis: 0,
    asthma: 1
}

Struct fields can be selected with normal Polars expressions:

result = (
    df.with_columns(
        pl.col('note_text').cnlp.count_all(terms).alias('counts'),
    )
    .select(
        pl.col('counts').struct.field('pneumonia'),
        pl.col('counts').struct.field('anaphylaxis'),
        pl.col('counts').struct.field('asthma'),
    )
)

Use count_all() when you need multiple count features for a known vocabulary.


Contextual interpretation

Contextual findings have three independent dimensions.

Assertion

affirmed
possible
negated

Temporality

current
historical
hypothetical

Experiencer

patient
other

The dimensions are independent. For example:

Possible pneumonia.

can be:

assertion   = possible
temporality = current
experiencer = patient

while:

If possible pneumonia develops...

can be:

assertion   = possible
temporality = hypothetical
experiencer = patient

cnlp.affirmed

affirmed(
    term: str,
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Determine whether a concept is affirmed.

PNEUMONIA = r'\bpneumonia\b'

df = pl.DataFrame({'note_text': [
    'Patient has pneumonia.',
    'No pneumonia.',
    'Possible pneumonia.',
    'History of pneumonia.',
    'Family history of pneumonia.',
    'If pneumonia develops, return to clinic.',
    'Patient has asthma.',
    None,
]})

result = df.select(
    pl.col('note_text').cnlp.affirmed(PNEUMONIA).alias('pneumonia'),
)

Expected result:

[
    True,  # affirmed, current, patient
    False,  # negated
    False,  # possible
    False,  # historical
    False,  # other experiencer
    False,  # hypothetical
    None,  # concept absent
    None,  # null input
]

A finding is considered affirmed only when:

assertion   = affirmed
temporality = current
experiencer = patient

Filtering to affirmed notes:

affirmed_notes = df.filter(
    pl.col('note_text').cnlp.affirmed(PNEUMONIA),
)

cnlp.affirmed_any

affirmed_any(
    terms: TermPatterns,
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Return whether any requested concept is affirmed.

The input can be a sequence:

patterns = [
    r'\bpneumonia\b',
    r'\banaphylaxis\b',
]

result = df.select(
    pl.col('note_text').cnlp.affirmed_any(patterns).alias('any_affirmed'),
)

or a mapping:

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
}

result = df.select(
    pl.col('note_text').cnlp.affirmed_any(terms).alias('any_affirmed'),
)

Semantics:

at least one concept affirmed                 -> True
at least one concept mentioned, none affirmed -> False
none of the requested concepts mentioned      -> null
null input                                     -> null

Example:

df = pl.DataFrame({'note_text': [
    'No pneumonia. Anaphylaxis present.',
    'No pneumonia.',
    'Asthma only.',
]})

result = df.select(
    pl.col('note_text').cnlp.affirmed_any(patterns).alias('result'),
)

Produces:

[True, False, None]

Typical use case:

cohort = df.filter(
    pl.col('note_text').cnlp.affirmed_any([
        r'\bpneumonia\b',
        r'\bbronchitis\b',
        r'\bbronchiolitis\b',
    ]),
)

cnlp.affirmed_all

affirmed_all(
    terms: TermPatterns,
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Return whether every requested concept is affirmed.

patterns = [
    r'\bpneumonia\b',
    r'\banaphylaxis\b',
]

df = pl.DataFrame({'note_text': [
    'Pneumonia and anaphylaxis are present.',
    'No pneumonia but anaphylaxis is present.',
    'Pneumonia present.',
    'Asthma only.',
]})

result = df.select(
    pl.col('note_text').cnlp.affirmed_all(patterns).alias('all_affirmed'),
)

The method uses three-valued logic:

True  + True  -> True
True  + False -> False
False + null  -> False
True  + null  -> null
null  + null  -> null

Typical use cases:

  • phenotypes requiring several co-occurring concepts;
  • validation rules where every required concept must be affirmed;
  • direct filtering with multiple clinical criteria.
cohort = df.filter(
    pl.col('note_text').cnlp.affirmed_all(patterns),
)

cnlp.affirmed_each

affirmed_each(
    terms: Mapping[str, str],
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Return independent affirmation status for every named concept.

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
}

df = pl.DataFrame({'note_text': [
    'No pneumonia. Anaphylaxis present.',
]})

result = df.select(
    pl.col('note_text').cnlp.affirmed_each(terms).alias('status'),
)

Produces a Struct resembling:

{
    'pneumonia': False,
    'anaphylaxis': True,
}

Each field is:

True  -> concept has an affirmed occurrence
False -> concept occurs but has no affirmed occurrence
null  -> concept is absent

Extract individual fields:

result = (
    df.with_columns(
        pl.col('note_text').cnlp.affirmed_each(terms).alias('status'),
    )
    .with_columns(
        pl.col('status').struct.field('pneumonia').alias('pneumonia'),
        pl.col('status').struct.field('anaphylaxis').alias('anaphylaxis'),
    )
)

Use affirmed_each() when you need several contextual Boolean features while processing the note once.


Rich contextual findings

Finding structure

find_best() and find_all() return findings with:

label        String/null
start        UInt64
end          UInt64
assertion    String
temporality  String
experiencer  String

start and end are UTF-8 byte offsets into the original text.

Example:

{
    'label': 'pneumonia',
    'start': 3,
    'end': 12,
    'assertion': 'negated',
    'temporality': 'current',
    'experiencer': 'patient',
}

cnlp.find_best

find_best(
    terms: FindPatterns,
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Return the highest-ranked contextualized finding.

Single concept:

result = df.select(
    pl.col('note_text')
    .cnlp.find_best(r'\bpneumonia\b')
    .alias('finding'),
)

When a single regex is passed, label is null.

Several named concepts:

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
    'asthma': r'\basthma\b',
}

df = pl.DataFrame({'note_text': [
    'No pneumonia. Possible anaphylaxis. Asthma present.',
]})

result = df.select(
    pl.col('note_text').cnlp.find_best(terms).alias('finding'),
)

finding = result['finding'][0]

The selected finding is asthma because it is affirmed and current, while pneumonia is negated and anaphylaxis is possible.

Ranking is deterministic:

  1. patient before other experiencer;
  2. current before historical before hypothetical;
  3. affirmed before possible before negated;
  4. earlier text occurrence;
  5. mapping order as the final tie-breaker.

Use cases:

  • select one representative finding;
  • reduce multiple candidate concepts to one feature;
  • return the preferred span for review or annotation.

cnlp.find_all

find_all(
    terms: FindPatterns,
*,
algorithm: AlgorithmLike = 'context',
prefilter: bool = False,
) -> pl.Expr

Return every contextualized finding in text order.

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
}

df = pl.DataFrame({'note_text': [
    'No pneumonia. Possible anaphylaxis. Pneumonia later developed.',
]})

result = df.select(
    pl.col('note_text').cnlp.find_all(terms).alias('findings'),
)

Semantics:

non-null note, matches found -> list of findings
non-null note, no matches     -> []
null note                     -> null

Use find_all() for:

  • annotation review;
  • NLP validation;
  • exact source spans;
  • mention-level datasets;
  • comparing contextual status across repeated mentions.

Choosing a context algorithm

All contextual methods use ConText by default:

pl.col('note_text').cnlp.affirmed(PNEUMONIA)

is equivalent to:

pl.col('note_text').cnlp.affirmed(
    PNEUMONIA,
    algorithm='context',
)

The algorithm= argument is available on:

affirmed
affirmed_any
affirmed_all
affirmed_each
find_best
find_all

contains, count, and count_all do not perform contextual analysis.

Prefiltering

All contextual methods accept prefilter=True:

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        PNEUMONIA,
        prefilter=True,
    ),
)

For multiple concepts:

result = df.select(
    pl.col('note_text').cnlp.affirmed_each(
        terms,
        prefilter=True,
    ).alias('status'),
)

When enabled, polars-cnlp first checks whether any requested concept regex matches the note. If none match, it skips ConText, NegEx, or other contextual processing and returns the normal no-match result. This can improve performance when the requested concepts are uncommon. When most notes contain a matching concept, the prefilter may add some overhead.

prefilter changes execution only; it does not change result semantics.

ConText

from polars_cnlp.algorithms import ConText

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        PNEUMONIA,
        algorithm=ConText(),
    ),
)

ConText supports independent assertion, temporality, and experiencer dimensions.

Examples:

No pneumonia.
    assertion   = negated
    temporality = current
    experiencer = patient

Possible pneumonia.
    assertion   = possible
    temporality = current
    experiencer = patient

History of pneumonia.
    assertion   = affirmed
    temporality = historical
    experiencer = patient

If pneumonia develops...
    assertion   = affirmed
    temporality = hypothetical
    experiencer = patient

Family history of pneumonia.
    assertion   = affirmed
    temporality = historical
    experiencer = other

NegEx

Use NegEx when you specifically want NegEx-style trigger and fixed-window behavior.

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        PNEUMONIA,
        algorithm='negex',
    ),
)

Or:

from polars_cnlp.algorithms import NegEx

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        PNEUMONIA,
        algorithm=NegEx(),
    ),
)

Customize the window:

algorithm = NegEx(window=8)

NegEx and ConText intentionally do not have identical rule vocabularies or scope behavior.


Custom rules and algorithms

from polars_cnlp.algorithms import (
    ConText,
    ContextEffect,
    ContextRule,
    PseudoRule,
    RuleBased,
    RuleSet,
    TerminateRule,
)

Add rules to the defaults

Use additional_rules= when you want to keep the built-in ConText or NegEx rules and add only a few project-specific expressions.

For example, extend ConText with an additional negation phrase:

from polars_cnlp.algorithms import ConText, ContextEffect, ContextRule

algorithm = ConText(
    additional_rules=[
        ContextRule(
            pattern=r'\bfree\W+of\b',
            direction='forward',
            effect=ContextEffect(assertion='negated'),
        ),
    ],
)

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        r'\bpneumonia\b',
        algorithm=algorithm,
    ),
)

The built-in ConText rules remain active; the supplied rule is added to them.

The same approach works with NegEx:

from polars_cnlp.algorithms import NegEx, ContextEffect, ContextRule

algorithm = NegEx(
    additional_rules=[
        ContextRule(
            pattern=r'\bcovid\b',
            direction='forward',
            effect=ContextEffect(assertion='negated'),
        ),
    ],
)

result = df.select(
    pl.col('note_text').cnlp.affirmed_each(
        {
            'pneumonia': r'\bpneumonia\b',
            'anaphylaxis': r'\banaphylaxis\b',
        },
        algorithm=algorithm,
    ),
)

Use additional_rules= to extend the defaults. Use rules= instead when you want to replace the built-in rule set completely.

ConText()                         -> built-in ConText rules
ConText(additional_rules=[...])  -> built-in rules + additional rules
ConText(rules=RuleSet([...]))    -> replacement rule set

NegEx()                          -> built-in NegEx rules
NegEx(additional_rules=[...])    -> built-in rules + additional rules
NegEx(rules=RuleSet([...]))      -> replacement rule set

rules= and additional_rules= cannot be used together.

Concepts that also match modifier rules

A concept may also match a ConText or NegEx modifier rule. This can be useful when a term acts as a modifier for other concepts:

Kratom and morphine.
^^^^^^     ^^^^^^^^
modifier   target

A modifier is not applied to a target whose text span overlaps the modifier itself. The overlapping modifier is also ignored as a modifier boundary when resolving that target.

For example, if kratom is both a searched concept and a forward negation rule:

Kratom.

kratom does not negate itself.

Kratom and morphine.

kratom can still negate morphine.

No kratom.

the overlapping kratom rule does not prevent the separate no rule from negating kratom.

This behavior is based on overlapping text spans, not on whether the concept and modifier use identical regular expressions. If a concept pattern intentionally includes text matched by a modifier, that modifier will not contextualize that concept occurrence.

Replace the ConText rule set

Sometimes, we want a completely new set of rules. To do this, we'll create a new RuleSet and pass it to the algorithm constructor.

rules = RuleSet([
    ContextRule(
        pattern=r'\babsent\b',
        direction='forward',
        effect=ContextEffect(assertion='negated'),
    ),
])

algorithm = ConText(rules=rules)

result = df.select(
    pl.col('note_text').cnlp.affirmed(
        PNEUMONIA,
        algorithm=algorithm,
    ),
)

Supplying rules= replaces the built-in rule set rather than silently extending it.

Multi-dimensional effects

algorithm = ConText(
    rules=RuleSet([
        ContextRule(
            pattern=r'\bfamily\W+record\W+of\b',
            direction='forward',
            effect=ContextEffect(
                temporality='historical',
                experiencer='other',
            ),
        ),
    ]),
)

Forward, backward, and bidirectional rules

ContextRule(
    pattern=r'\bno\b',
    direction='forward',
    effect=ContextEffect(assertion='negated'),
)
ContextRule(
    pattern=r'\bexcluded\b',
    direction='backward',
    effect=ContextEffect(assertion='negated'),
)
ContextRule(
    pattern=r'\bsuspected\b',
    direction='bidirectional',
    effect=ContextEffect(assertion='possible'),
)

Limit scope

max_scope is the maximum number of intervening tokens:

ContextRule(
    pattern=r'\bremote\b',
    direction='forward',
    effect=ContextEffect(temporality='historical'),
    max_scope=3,
)

max_targets limits the number of target occurrences modified:

ContextRule(
    pattern=r'\bstatus\W+post\b',
    direction='forward',
    effect=ContextEffect(temporality='historical'),
    max_targets=1,
)

Termination rules

negated = ContextEffect(assertion='negated')

rules = RuleSet([
    ContextRule(
        pattern=r'\babsent\b',
        direction='forward',
        effect=negated,
    ),
    TerminateRule(
        pattern=r'\bbut\b',
        effects=[negated],
    ),
])

For:

Absent fever but pneumonia present.

but prevents the earlier modifier from negating pneumonia.

Pseudo rules

negated = ContextEffect(assertion='negated')

rules = RuleSet([
    ContextRule(
        pattern=r'\bnegative\b',
        direction='forward',
        effect=negated,
    ),
    PseudoRule(
        pattern=r'\bnegative\W+attitude\b',
        effects=[negated],
    ),
])

For:

Negative attitude regarding pneumonia.

the pseudo rule prevents negative from incorrectly negating pneumonia.

Load a NegEx trigger file

from polars_cnlp.algorithms import NegEx, RuleSet

rules = RuleSet.from_negex_file(
    'negex_triggers.txt',
)

algorithm = NegEx(
    rules=rules,
)

Supported trigger categories include:

PREN
POST
PREP
POSP
PSEU
CONJ

This is useful when a project needs to reproduce a specific NegEx lexicon.


Research data-wrangling namespace: .rdw

The .rdw namespace contains general-purpose helpers useful in research datasets but not specific to clinical NLP.

API overview

Method Input Output Typical use
starts_with_any() collection of literal prefixes Boolean Match code families, identifiers, or other prefix-based values
ends_with_any() collection of literal suffixes Boolean Match file types, code endings, or other suffix-based values

Both methods treat the supplied values as literal strings rather than regular expressions.

rdw.starts_with_any

starts_with_any(prefixes: Sequence[str]) -> pl.Expr

Check if a column starts with any of a collection/list of strings.

Parameters

prefixes : List of prefixes.

Returns

Expr : Expression of data type Bool.

Example

df = pl.DataFrame({'code': [
    'A123',
    'B456',
    'C789',
    'X999',
    None,
]})

result = df.select(
    pl.col('code')
    .rdw.starts_with_any(['A', 'B', 'C'])
    .alias('selected'),
)

Expected result:

[True, True, True, False, None]

Values are treated as literal strings, not regular expressions.

df = pl.DataFrame({'value': [
    'a.b',
    'axb',
]})

result = df.select(
    pl.col('value').rdw.starts_with_any(['a.']).alias('result'),
)

Produces:

[True, False]

Use case: ICD families

respiratory = df.filter(
    pl.col('icd10').rdw.starts_with_any([
        'J12', 'J13', 'J14', 'J15', 'J16', 'J17', 'J18',
    ]),
)

Use case: identifier families

selected = df.filter(
    pl.col('study_id').rdw.starts_with_any([
        'CASE-',
        'CTRL-',
        'PILOT-',
    ]),
)

Behavior:

empty prefix collection -> False for non-null input
empty-string prefix      -> True for non-null input
null input               -> null

rdw.ends_with_any

ends_with_any(suffixes: Sequence[str]) -> pl.Expr

Check if a column ends with any of a collection/list of strings.

Parameters

suffixes : List of suffixes.

Returns

Expr : Expression of data type Bool.

Example

df = pl.DataFrame({'filename': [
    'notes.parquet',
    'patients.csv',
    'readme.md',
    None,
]})

result = df.select(
    pl.col('filename')
    .rdw.ends_with_any(['.parquet', '.csv'])
    .alias('data_file'),
)

Expected result:

[True, True, False, None]

Suffixes are literal strings, not regular expressions.

Use case: procedure-code suffixes

selected = df.filter(
    pl.col('procedure_code').rdw.ends_with_any([
        '01',
        '02',
        '03',
    ]),
)

Use case: file filtering

files = df.filter(
    pl.col('path').rdw.ends_with_any([
        '.parquet',
        '.csv',
        '.sas7bdat',
    ]),
)

Behavior:

empty suffix collection -> False for non-null input
empty-string suffix      -> True for non-null input
null input               -> null

Lazy queries

All namespace methods are designed for normal Polars lazy expressions.

result = (
    pl.scan_parquet('notes/*.parquet')
    .filter(
        pl.col('note_text').cnlp.affirmed(
            r'\bpneumonia\b',
        ),
    )
    .select('patient_id', 'note_id', 'note_text')
    .collect()
)

Multi-concept processing:

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
}

result = (
    pl.scan_parquet('notes/*.parquet')
    .with_columns(
        pl.col('note_text')
        .cnlp.affirmed_each(terms)
        .alias('concept_status'),
    )
    .collect()
)

Choosing the right method

Use contains() when:

  • you only need regex presence;
  • context is unnecessary;
  • you are creating a broad candidate set.

Use count() or count_all() when:

  • mention frequency matters;
  • context does not;
  • you want count features.

Use affirmed() when:

  • you want a patient/current/affirmed Boolean phenotype for one concept.

Use affirmed_any() when:

  • several alternative concepts can satisfy a phenotype.

Use affirmed_all() when:

  • all required concepts must be affirmed.

Use affirmed_each() when:

  • you need one contextual Boolean feature per concept.

Use find_best() when:

  • one representative contextual finding is needed.

Use find_all() when:

  • mention-level context and spans matter;
  • you are validating NLP output;
  • different mentions in the same note must remain distinguishable.

Use .rdw.starts_with_any() and .rdw.ends_with_any() when:

  • matching literal prefixes or suffixes;
  • a list is clearer than constructing a regular expression;
  • the task is data wrangling rather than clinical NLP.

Example: clinical phenotype

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
    'asthma': r'\basthma\b',
}

phenotype = (
    df.lazy()
    .with_columns(
        pl.col('note_text')
        .cnlp.affirmed_each(terms)
        .alias('nlp'),
    )
    .with_columns(
        pl.col('nlp').struct.field('pneumonia').alias('pneumonia'),
        pl.col('nlp').struct.field('anaphylaxis').alias('anaphylaxis'),
        pl.col('nlp').struct.field('asthma').alias('asthma'),
    )
    .filter(
        pl.col('pneumonia').fill_null(False)
        | pl.col('anaphylaxis').fill_null(False)
        | pl.col('asthma').fill_null(False),
    )
    .collect()
)

Example: inspect findings during validation

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphylaxis\b',
}

review = (
    df.lazy()
    .with_columns(
        pl.col('note_text')
        .cnlp.find_all(terms)
        .alias('findings'),
    )
    .filter(
        pl.col('findings').list.len() > 0,
    )
    .collect()
)

This is useful for reviewing labels, spans, assertion, temporality, and experiencer.

Example: compare ConText and NegEx

comparison = df.select(
    'note_text',
    pl.col('note_text')
    .cnlp.affirmed(
        PNEUMONIA,
        algorithm='context',
    )
    .alias('context'),
    pl.col('note_text')
    .cnlp.affirmed(
        PNEUMONIA,
        algorithm='negex',
    )
    .alias('negex'),
)

This is useful when validating whether an existing NegEx-based phenotype changes under richer ConText-style interpretation.


Regex patterns

Concepts are regular expressions.

Prefer explicit boundaries when appropriate:

r'\bpneumonia\b'

More flexible concepts can be written directly:

ANAPHYLAXIS = r'\banaphyl(?:axis|actic)\b'

Named mappings are recommended when results contain multiple concepts:

terms = {
    'pneumonia': r'\bpneumonia\b',
    'anaphylaxis': r'\banaphyl(?:axis|actic)\b',
}

The mapping key becomes the finding label or Struct field name.

Null behavior

polars-cnlp preserves the distinction between concept absence, non-affirmed concepts, and null input.

For contextual Boolean methods:

concept affirmed       -> True
concept non-affirmed   -> False
concept absent         -> null
input text null        -> null

For count():

concept absent         -> 0
input text null        -> null

For find_all():

concept absent         -> []
input text null        -> null

This distinction is intentional and works naturally with Polars' nullable expressions.

Metadata

Release files for polars-cnlp 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for polars-cnlp 0.2.0
File Size Uploaded
polars_cnlp-0.2.0.tar.gz 88.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for polars-cnlp 0.2.0
File
polars_cnlp-0.2.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.11 abi3 Linux glibc 2.17+ x86-64 Details
polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.11 abi3 Linux glibc 2.17+ ARM64 Details
polars_cnlp-0.2.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
polars_cnlp-0.2.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 31.4 MB

Release files / polars_cnlp-0.2.0.tar.gz

Download URL polars_cnlp-0.2.0.tar.gz
Size 88.6 kB
Tags Source
SHA-256 checksum
How to use checksums
28506493bfa06b707abe61124fef94c1f2d82ee32073c977c5fd664e89afa5fb
BLAKE2b-256 checksum
How to use checksums
e8945c988764af58441fd95cc71b5b7dcd1031a921a200f076668086cd957893
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / polars_cnlp-0.2.0-cp311-abi3-win_amd64.whl

Download URL polars_cnlp-0.2.0-cp311-abi3-win_amd64.whl
Size 6.1 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
6da54027d5d95ad75e7c5cdedeeb1701a98da553f83732555208e58e4ee5600e
BLAKE2b-256 checksum
How to use checksums
279935334201b679f2c18d7dfa6921057021c71ed476c619d7feb958d7a02f00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 6.8 MB
Tags CPython 3.11 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
6028b9b3015c943dc9f3085c37685e9f1ee19172b27e4d4154ddd677fcb808c6
BLAKE2b-256 checksum
How to use checksums
ebef90e9e4f14935ef56a56e58f8b9367ba75c514ba51c32c7573781ada1c3f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL polars_cnlp-0.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 6.8 MB
Tags CPython 3.11 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
2a42555a8867546426f12d3527963b875f66ca277150f104b5320e33a09081e8
BLAKE2b-256 checksum
How to use checksums
c64bee227b5be2f3bd9f052b795f982b1500757a4608e1d8a3422ef44ac55f47
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / polars_cnlp-0.2.0-cp311-abi3-macosx_11_0_arm64.whl

Download URL polars_cnlp-0.2.0-cp311-abi3-macosx_11_0_arm64.whl
Size 5.6 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
395af88e555bd7e0a457ab1d81ff21c65639fcb3ac2b3f5a7f9a8e6c3711a2a5
BLAKE2b-256 checksum
How to use checksums
0bfc81bf5ffbdeee2ff2ae1d73294f9559fee912325edc0eec48940c04fef47a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / polars_cnlp-0.2.0-cp311-abi3-macosx_10_12_x86_64.whl

Download URL polars_cnlp-0.2.0-cp311-abi3-macosx_10_12_x86_64.whl
Size 6.0 MB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
244ed3da7507855a2daaae6486dfccfa2e204a347c8296ef324f36b2f6e55a26
BLAKE2b-256 checksum
How to use checksums
e8578b6045d8ed123ed3cf2a2ce232a35f14027174419e81e5970ca04e070e77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

6 release files

0.1.0

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page