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.3.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.3.0
File Size Uploaded Provenance
polars_cnlp-0.3.0.tar.gz 90.4 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for polars-cnlp 0.3.0
File
polars_cnlp-0.3.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
polars_cnlp-0.3.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.3.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.11 abi3 Linux glibc 2.17+ ARM64 Details
polars_cnlp-0.3.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
polars_cnlp-0.3.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 31.3 MB

Release files / polars_cnlp-0.3.0.tar.gz

Download URL polars_cnlp-0.3.0.tar.gz
Size 90.4 kB
Tags Source
SHA-256 checksum
How to use checksums
a474c65e7cba198c99bb113efa3f8f21587679e081ce221e6b5f13016edb261f
BLAKE2b-256 checksum
How to use checksums
487662ccafbe70e9f98cf64ecf74c93fbcdb2e2e05a7ffba632f42c2ad13d999
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

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

Download URL polars_cnlp-0.3.0-cp311-abi3-win_amd64.whl
Size 6.0 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
42ad9a3f267d43732af0fe830c18c53d501055efbd06d6385c05c16eeffbc686
BLAKE2b-256 checksum
How to use checksums
4f536acdc9b9443f090cecb428ada5a88602378133873b5181dea7c0bae33641
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

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

Download URL polars_cnlp-0.3.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
37a4f4b6816d1ce78d4cfaae1918f51fca47a322e762e3f6e170852215eb15e2
BLAKE2b-256 checksum
How to use checksums
e20e7da0d952de44ec00a7a5807e4182993e989394891499d92a3dca4f27caa6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

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

Download URL polars_cnlp-0.3.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
e92534a5ff270146f0b700bebf65069670ac3e812e1a47fe217f427a11269467
BLAKE2b-256 checksum
How to use checksums
3e7b7b4ff578efd9d9de9cacd708ef00be905b1ee603390c6433353e9c5e3beb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

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

Download URL polars_cnlp-0.3.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
98ed0d3118b71f42be61c66d32b9144038ab24e5a8ccebb26dd1a55c0ee5acd4
BLAKE2b-256 checksum
How to use checksums
a11c05201cbee2fb54a524af8a09e477da487f9093c241d591c96078d11a99df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

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

Download URL polars_cnlp-0.3.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
aaf5b074121b4ea055bd050a065f675e190fd7c476b9700436a4104dc0c55a48
BLAKE2b-256 checksum
How to use checksums
6b3757c22689b799bc957e211db84733e0e73f944e2af7fafba7f51cd7ab2753
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 23, 2026.

Transparency log

Security for polars-cnlp 0.3.0

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.

Full provenance

All 6 files in this release have been uploaded with provenance metadata.

Select from 6 files

Attestation for polars_cnlp-0.3.0.tar.gz

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 Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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