Skip to main content

Python implementation of Epistemic Network Analysis, ported from the R package rENA.

Project description

ena-python

CI PyPI Python License

ena-python is a Python implementation of Epistemic Network Analysis, ported from the R package rENA.

Formerly pyENA. Renamed because an unrelated package already owns pyena on PyPI — including the pyena module name — so keeping it would have collided for anyone who installed both.

It is a standalone library: import ena_python needs only NumPy and pandas. No SciPy, no R, no Node, no server, and no network access. That keeps it usable in a notebook, in a script, or in browser Python — see the runnable Pyodide demo, which installs ena-python from PyPI and runs a full analysis in a browser tab.

Status: early release. Every numeric path — accumulation across all window types and models, the SVD / mean / generalized / regression / hENA rotations, node positions, variance/eigenvalues, Cohen's d, and the correlation functions — is checked against real compiled rENA 0.3.1; see the parity table, which also records the one deliberate, documented divergence. The API may still change.

Install

pip install ena-python
import ena_python

The distribution is ena-python, the module is ena_python, and the command is ena-python.

Optional extras:

pip install "ena-python[plot]"     # Plotly figures
pip install "ena-python[web]"      # FastAPI service
pip install "ena-python[parquet]"  # .parquet input

Or from source:

pip install git+https://github.com/HUDongpin/ena-python

Quick start

import pandas as pd
from ena_python import ena

rows = pd.DataFrame(
    {
        "UserName": ["u1", "u1", "u2", "u2"],
        "Condition": ["A", "A", "B", "B"],
        "GroupName": ["g1", "g1", "g2", "g2"],
        "Data": [1, 0, 1, 1],
        "Design": [0, 1, 1, 0],
        "Collaboration": [1, 1, 0, 1],
    }
)

set_ = ena(
    data=rows,
    codes=["Data", "Design", "Collaboration"],
    units=["Condition", "UserName"],
    conversation=["Condition", "GroupName"],
)

print(set_.points)      # unit positions in ENA space
print(set_.variance)    # variance explained, per dimension

Step by step

from ena_python import accumulate, make_set

data = accumulate(
    rows,
    units=["Condition", "UserName"],
    conversation=["Condition", "GroupName"],
    codes=["Data", "Design", "Collaboration"],
)
set_ = make_set(data, dimensions=2)

payload = set_.to_dict()  # JSON-friendly: NaN -> None, NumPy scalars -> Python scalars

to_dict() does not include your input dataset. ENA is often run over discourse transcripts that carry personal data, so results do not echo the source back by default. Pass to_dict(include_raw=True) if you want raw and row_connection_counts in the payload.

Input data

One row per coded line. Columns:

Column kind Meaning
units Who/what the network is about (e.g. Condition, UserName)
conversation Which lines can connect to each other
codes Binary 0/1 columns, one per code
metadata Optional; carried through to results

Metadata must be constant within a unit. A metadata column that varies inside a unit has no unit-level value, so it is dropped and a UserWarning names it.

Interpreting the output

  • set_.points — unit coordinates for the retained dimensions.
  • set_.variance — variance explained for every dimension, normalized by the total across all of them (matching rENA). The first two entries sum to less than 1 whenever the data has rank > 2.
  • set_.rotation.eigenvaluesprcomp sdev² (s²/(n−1)) for every dimension, as in rENA.
  • set_.nodes — code node positions.

SVD signs are not canonical. As in any SVD, an axis may be mirrored relative to rENA or across platforms. Compare sign-aligned (see _sign_align in tests/test_r_oracle_parity.py).

A mean rotation's direction is arbitrary too. Passing rotation_params=[g1, g2] does not orient the axis from g1 toward g2 — swapping them yields an identical MR1, because the mean difference goes through a QR decomposition whose sign convention ignores the input's sign. rENA does the same. Read the group centroids to see which side is which rather than trusting the sign.

In the browser

ena-python runs under Pyodide with no server and no build step — the wheel is pure Python (a ~53 KB download) and its only dependencies (numpy, pandas) both ship as Pyodide packages.

const pyodide = await loadPyodide();
await pyodide.loadPackage(["micropip", "numpy", "pandas"]);
await pyodide.pyimport("micropip").install("ena-python");
pyodide.runPython(`from ena_python import ena; ...`);

examples/pyodide/ is a complete working page that does this and plots the result. On a 2026 laptop the analysis takes 0.02–0.07 s; the ~10–16 s cold start is almost entirely the browser downloading numpy and pandas as WebAssembly, which it then caches — about 7 MB, down from 20 MB before SciPy was dropped.

Plotting and serving

from ena_python.plotting import add_network, add_nodes, add_points, ena_plot

fig = ena_plot(set_)
add_network(fig)
add_points(fig)
add_nodes(fig)
fig.to_json()
uvicorn ena_python.web.api:app --reload   # /accumulate, /model, /ena, /plot

The web app is for localhost/trusted use: no authentication, and a row cap (ENA_PYTHON_WEB_MAX_ROWS, default 100000) is its only DoS guard. Put your own auth and limits in front of it before exposing it.

CLI

ena-python inspect examples/cli_sample.csv

ena-python ena examples/cli_sample.csv \
  --units unit --conversation conv --codes A B C \
  --metadata group score --window-size-back 2 \
  --output ena.json

Reads CSV, TSV, and Parquet (Parquet needs ena-python[parquet]). --include-raw echoes the input into the JSON. See docs/cli_usage.md.

Parity with rENA

Golden fixtures are generated from real, compiled rENA 0.3.1 and stamped with provenance; the test suite refuses a fixture built from anything less authoritative. Be aware of what is and isn't covered:

Area Checked against rENA?
EndPoint accumulation, moving-stanza window Yes — incl. infinite windows vs the compiled C++ kernel
Conversation window Yes — accumulation and full model
AccumulatedTrajectory / SeparateTrajectory Yes — accumulation and full model
Sphere/skip normalization, centering Yes
SVD rotation, points, node positions, centroids Yes (sign-aligned)
variance, eigenvalues Yes — incl. a rank-3 case where a truncated denominator is wrong
Generalized rotation (gmr) Yes — numeric and categorical targets, and x+y
hENA regression / regression_2 Yes for the x axis; y axis diverges by design (see below)
hENA rotation_h Yes — incl. control variables
Mean rotation Yes — full basis incl. the SVD completion (its direction is QR-conventional, as in rENA — see above)
Cohen's d Yes — 6 cases, incl. mirrored inputs
ena_correlations, ena_correlation Yes — pearson/spearman and the Fisher-CI kernel

See docs/testing_strategy.md and reference/README.md.

Issues found in rENA 0.3.1

Porting turned up six places where rENA 0.3.1 behaves differently from what its own code, comments, or docs intend. They are written up with reproducible snippets in docs/rena-upstream-issues.md, and tracked here under the upstream-rena label.

# Issue Severity ena-python
1 ena.rotate.by.hena.regression does not deflate the y axis, returning axes ~0.8–0.97 collinear High — affects coordinates Deflates; axes orthogonal
2 Regression axes named after the first edge (A & B_reg), not the predictor; duplicate names with x+y Low Names after the predictor
3 The documented "lm(formula=V ~ ...)" param form errors Low Takes the plain formula
4 regression_2 errors cryptically on rank-deficient input Low Differs, but not better — see the doc
5 ena.rotation.h warns on every call Trivial n/a
6 ena.correlations(dims=) errors for any subset that is not 1:n Low Takes dimension names; any subset works

These are rENA issues, not ena-python bugs, reported as observations rather than accusations — they may be fixed upstream or intended. Only #1 changes ena-python's output relative to rENA:

Given both x_var and y_var, rotate_by_regression returns a different y axis than rENA, deliberately. rENA means to regress y on the x-deflated points (ena.rotate.by.regression.R sets V <- defA first), but that never takes effect: with.ena.matrix rebinds V to the raw points unless passed a V = argument, and a refactor dropped it. Its sibling ena.rotate.by.generalized deflates correctly on the same data. ena-python deflates, so its axes are orthogonal and its x axis matches rENA exactly. If rENA fixes this, ena-python will match it column-for-column.

The overwhelming majority of rENA matched ena-python exactly, kernel for kernel — and the two ena-python defects found in the same effort were worse (see CHANGELOG.md).

Development

python -m venv .venv && source .venv/bin/activate
python -m pip install -e ".[dev,plot,web]"
pytest
ruff check . && ruff format --check . && mypy src/ena_python && pytest

Regenerating fixtures needs R with rENA installed (install.packages("rENA")) — see reference/README.md. Everyday development does not need R.

Relationship to rENA, and citation

ena-python is an independent port. It is not affiliated with or endorsed by the rENA authors. All credit for the ENA method and the reference implementation belongs to the Epistemic Analytics group. If you use ena-python in research, cite the original ENA literature and rENA; see docs/migration_guide.md for the R-to-Python API map.

There is a separate, unrelated pyena package on PyPI by another author. This project is not it.

License

GPL-3.0-only, matching rENA (GPL-3), since ena-python is a derivative port. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ena_python-0.2.1.tar.gz (147.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ena_python-0.2.1-py3-none-any.whl (54.0 kB view details)

Uploaded Python 3

File details

Details for the file ena_python-0.2.1.tar.gz.

File metadata

  • Download URL: ena_python-0.2.1.tar.gz
  • Upload date:
  • Size: 147.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for ena_python-0.2.1.tar.gz
Algorithm Hash digest
SHA256 bf1fbec60d4fb4daae3ea85d671c32bd9cd7ed9271bd89845e04b6bdb1231aa5
MD5 c308bcd6878567e07a2f3aff15ebdea9
BLAKE2b-256 6c5321321e9bd3fa9ff8449940957607d53b384de4fb430dadacd3f9485d9a2b

See more details on using hashes here.

File details

Details for the file ena_python-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: ena_python-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 54.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for ena_python-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 843387e06a87d5ec6fa1de98160e5f7f2ceef37f291b0526e358bfc21ca6b84b
MD5 61d476aeaa3145f96a178412216dc929
BLAKE2b-256 b7bf39956bdb7b83f4eb403682ec23242b8fd3509ef24133c39a9f52c6e9f9f3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page