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 (0.1.0). The core pipeline — accumulation, the moving-stanza window, normalization, centering, SVD projection, node positioning, and mean rotation — is checked numerically against real rENA 0.3.1. Other parts are not; see Parity with rENA for exactly which. 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 (147 KB) 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 s; the ~18 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 Partly — MR1/SVD2 only
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.0.tar.gz (136.8 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.0-py3-none-any.whl (53.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ena_python-0.2.0.tar.gz
  • Upload date:
  • Size: 136.8 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.0.tar.gz
Algorithm Hash digest
SHA256 540be2b905266306e9bc3950ea7ae3be23aaaf8b804a311b86e83d5b0d0a5e89
MD5 8699bc261ce6784dfde3c797803054b9
BLAKE2b-256 10b0334d4ee9eb5944a967c28c8d673e53122f2942725e6885c10a2ed4da5b00

See more details on using hashes here.

File details

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

File metadata

  • Download URL: ena_python-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 53.9 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 28a27906fd19205654f73255b7a4c51a58be37216d44edadb3efb42908649c29
MD5 2f072f5b7c686d5a2f50e53d043cd31c
BLAKE2b-256 59e48189b14a07913363af02bb2629b443e1edd21bb003ad87ab4690c09699fa

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