Python implementation of Epistemic Network Analysis, ported from the R package rENA.
Project description
ena-python
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
pyenaon PyPI — including thepyenamodule 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 retaineddimensions.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.eigenvalues—prcompsdev²(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_varandy_var,rotate_by_regressionreturns a different y axis than rENA, deliberately. rENA means to regress y on the x-deflated points (ena.rotate.by.regression.RsetsV <- defAfirst), but that never takes effect:with.ena.matrixrebindsVto the raw points unless passed aV =argument, and a refactor dropped it. Its siblingena.rotate.by.generalizeddeflates 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf1fbec60d4fb4daae3ea85d671c32bd9cd7ed9271bd89845e04b6bdb1231aa5
|
|
| MD5 |
c308bcd6878567e07a2f3aff15ebdea9
|
|
| BLAKE2b-256 |
6c5321321e9bd3fa9ff8449940957607d53b384de4fb430dadacd3f9485d9a2b
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
843387e06a87d5ec6fa1de98160e5f7f2ceef37f291b0526e358bfc21ca6b84b
|
|
| MD5 |
61d476aeaa3145f96a178412216dc929
|
|
| BLAKE2b-256 |
b7bf39956bdb7b83f4eb403682ec23242b8fd3509ef24133c39a9f52c6e9f9f3
|