Python ENA (Epistemic Network Analysis) modeling — sister package to rENA
Formerly published as pyena.
Python >=3.10 · License: GPL-3.0-only · API documentation · Source code · Homepage · Source · Issues
Also available for R: rENA · npm: @qe-libs/rena-wasm · Julia: rENA · C++ (Conan): rENA
uv
uv add qe-ena --index qe-libs=https://qe-libs.org/py/simple/
pip
pip install --extra-index-url https://qe-libs.org/py/simple/ qe-ena
requirements.txt
--extra-index-url https://qe-libs.org/py/simple/ qe-ena
Latest development build (0.1.6.9004)
pip install --extra-index-url https://qe-libs.org/py/dev/simple/ qe-ena
uv add qe-ena --index qe-libs-dev=https://qe-libs.org/py/dev/simple/
Development builds are built from main and served only by the
development index; the regular index installs releases.
Links in the wheel columns are the Python versions each wheel supports; pip and uv pick the right file automatically.
| Version | Published | Source | Wheel all platforms | Linux x86_64 · glibc 2.27+ | macOS arm64 · macOS 26.0+ | Windows x64 |
|---|---|---|---|---|---|---|
| 0.1.6.9004 | 2026-10-10 | .tar.gz | — | 3.11 3.12 3.13 | 3.11 3.12 3.13 | 3.11 3.12 3.13 |
| 0.1.6 | 2026-10-10 | .tar.gz | — | 3.11 3.12 3.13 | 3.11 3.12 3.13 | 3.11 3.12 3.13 |
| 0.1.5 | 2026-10-06 | .tar.gz | py3 | — | — | — |
For 0.1.6.
qe-lib<0.2,>=0.1.7numpy>=1.24pandas>=1.5pytest>=7 (extra: dev)Python implementation of Epistemic Network Analysis (ENA) — sister package to rENA.
Install it as qe-ena; import it as ena.
Previously published as
pyENA(import pyena). Thepyenaproject on PyPI is an unrelated package — installqe-enafrom the QE index as shown below.
ena runs its math in C++ shared with rENA: the ENA model code (rotations, node
positions, window estimation) is libena, compiled into qe-ena itself (ena.libena)
together with the accumulation it uses (tma's libtma), and generic numerics come from
qe-lib (import qe).
qe-ena requires qe-lib, which is also on the QE package index; other
dependencies (numpy, pandas) come from PyPI.
uv add qe-ena --index qe-libs=https://qe-libs.org/py/simple/
# or
pip install qe-ena --extra-index-url https://qe-libs.org/py/simple/
Development builds from main are on a separate index,
https://qe-libs.org/py/dev/simple/. See https://qe-libs.org/py/project/qe-ena/.
Building from a checkout compiles the libena extension: it needs a C++17
compiler, CMake and Armadillo (brew install
armadillo, apt install libarmadillo-dev), plus the headers that
scripts/sync-headers.sh vendors into python/include/ (libena from this repo,
libqe and libtma from Conan; LIBQE_INCLUDE / LIBTMA_INCLUDE point at local
checkouts instead, and a ../tma checkout is used while libtma is unpublished).
sh scripts/sync-headers.sh # from the repo root
pip install qe-lib --extra-index-url https://qe-libs.org/py/simple/
pip install "./python[dev]"
pytest python/tests --import-mode=importlib # tests the installed package
import pandas as pd
from ena import ENA
rs = pd.read_csv("../inst/extdata/rs.data.csv") # rENA's RS.data
CODES = ["Data", "Technical Constraints", "Performance Parameters",
"Client and Consultant Requests", "Design Reasoning", "Collaboration"]
# Units and conversations as rENA's RS.data examples: units by Condition +
# UserName, conversations by Condition + GroupName.
rs["unit_key"] = rs["Condition"] + "::" + rs["UserName"]
rs["convo_key"] = rs["Condition"] + "::" + rs["GroupName"]
model = ENA().fit(rs, "unit_key", "convo_key", CODES, window_size=4)
qe-ena models plot with qe-viz (import qeviz), the
interactive ENA / ONA network viewer used by rENA (pip install qe-viz
--extra-index-url https://qe-libs.org/py/simple/). The two conditions compared —
FirstGame − SecondGame, with each group's mean and 95% confidence interval:
import qeviz
p = (qeviz.from_pyena(model, group_col="Condition", title="FirstGame − SecondGame")
.edges("FirstGame", compare="SecondGame", color_scale="plot", magnify=3)
.group())
p # displays inline in Jupyter
p.export_html("rs-data.html") # or a self-contained HTML file

Blue edges are stronger in FirstGame, red in SecondGame. magnify=3 widens
the edges to make a subtraction's small differences readable (the plot says so).
See the qeviz README for single-group
networks, unit points, and networks or means from your own data.
model.line_weights_ # normalised adjacency vectors (n_units × n_connections)
model.row_connection_counts_ # row-level raw adjacency vectors (n_rows × n_connections)
model.points_ # unit positions in ENA space (n_units × dims)
model.centroids_ # network centroids (n_units × dims)
model.rotation_nodes_ # code node positions (n_codes × dims)
model.connection_counts_ # raw unit adjacency vectors (n_units × n_connections)
model.unit_labels_ # unit labels in order
model.connection_names_ # e.g. ["Data & Technical Constraints", ...]
model.variance_ # variance explained per dimension (= rENA's model$variance)
from ena import ENA, accumulate
accum = accumulate(rs, "unit_key", "convo_key", CODES, window_size=4)
accum.connection_counts_ # raw unit co-occurrence matrix
accum.row_connection_counts_ # raw row co-occurrence matrix
accum.unit_labels_ # unit labels
accum.connection_names_ # connection labels
accum.meta # per-unit metadata DataFrame
# Reuse the same accumulation with different rotations
from ena import mean_rotation, generalized_rotation
model_svd = ENA().fit(accum)
model_mr = ENA().fit(accum, rotation=mean_rotation(g1_mask, g2_mask))
model_gmr = ENA().fit(accum, rotation=generalized_rotation(meta["Condition"]))
| Rotation | Argument |
|---|---|
| SVD (default) | rotation=None |
| Means | mean_rotation(g1, g2) |
| Generalised (GMR) | generalized_rotation(x_var) |
| Regression (V ~ x) | regression_rotation(x_var) |
| Regression (x ~ V) | regression_rotation_2(x_var) |
| Custom matrix | rotation=my_ndarray |
from ena import ENA, mean_rotation, generalized_rotation, regression_rotation
meta = (rs.drop_duplicates("unit_key")
.set_index("unit_key")
.reindex(model.unit_labels_)
.reset_index())
# Means rotation
model_mr = ENA().accumulate(rs, "unit_key", "convo_key", CODES).fit(
rotation=mean_rotation(
meta["Condition"] == "FirstGame",
meta["Condition"] == "SecondGame",
)
)
# Generalised rotation
model_gmr = ENA().accumulate(rs, "unit_key", "convo_key", CODES).fit(
rotation=generalized_rotation(meta["Condition"])
)
# Regression rotation
import numpy as np
condition_bin = (meta["Condition"] == "FirstGame").astype(float).to_numpy()
model_reg = ENA().accumulate(rs, "unit_key", "convo_key", CODES).fit(
rotation=regression_rotation(condition_bin)
)
| Option | Argument |
|---|---|
| Window back | window_size=4 |
| Window forward | window_forward=2 |
| Infinite window | window_size=sys.maxsize |
| Binary co-occurrence | binary=True |
| Weighted co-occurrence | binary=False |
| File | Size | SHA-256 |
|---|---|---|
| qe_ena-0.1.6.9004.tar.gz | 186.7 KB | c636da0d0207fdd80667f7f94b82e18aeb7b1b9898a00d81b0dbf3ff7ac943f6 |
| qe_ena-0.1.6.9004-cp313-cp313-win_amd64.whl | 5.6 MB | eef7a455d8e6c5fb7fad2714156baf9050214b7835ddf6313fbc20c1c6a96d5d |
| qe_ena-0.1.6.9004-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | 12f2d0aa988f7816bc10a90f496546b92df07e1c2207b2a0920924d6ac2dd911 |
| qe_ena-0.1.6.9004-cp313-cp313-macosx_26_0_arm64.whl | 159.9 KB | c9ea8c5c751998f2fe93509be01a52357c1537b38ae70c92c18ee075af4fe977 |
| qe_ena-0.1.6.9004-cp312-cp312-win_amd64.whl | 5.6 MB | 0a122badbc1e1998dad02e73ed54f23717758212527ac6bf73f1aa85bc056c18 |
| qe_ena-0.1.6.9004-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | 14e45a9e554d455ed5665fd6124088a63cae931858ca835af3874c005950db95 |
| qe_ena-0.1.6.9004-cp312-cp312-macosx_26_0_arm64.whl | 159.9 KB | ba27c6772e8c4ceac894814676900f5dd2d3a177851825c67f8acd0994b221f9 |
| qe_ena-0.1.6.9004-cp311-cp311-win_amd64.whl | 5.6 MB | a17f4f31dd6a71d62b0f7e8a054a11ede14ee30713f757a57fe8abd67dfd586c |
| qe_ena-0.1.6.9004-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | 85e9d088735b4c32b66e6f33fe027328218204da0c5c080cbd85c97ee8265bac |
| qe_ena-0.1.6.9004-cp311-cp311-macosx_26_0_arm64.whl | 160.6 KB | a8ea9956f279fc6926fe655699fa17e3deb031c5dc8a4997abd7faa9f5f86610 |
| qe_ena-0.1.6.tar.gz | 186.7 KB | f0828d6320f61dee4513c13c60fbd7297c01e6990d62fb52e69251db772d4153 |
| qe_ena-0.1.6-cp313-cp313-win_amd64.whl | 5.6 MB | f950c8c87954c7294dec88312832d56b8e03587b1674112e8c23a26febce9165 |
| qe_ena-0.1.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | 39f246a85c8385f273f6817372e46976bd9cb50ae7a1cdca71fc9e086efd62b0 |
| qe_ena-0.1.6-cp313-cp313-macosx_26_0_arm64.whl | 159.9 KB | aa7967f545465c8d8bbbd21ce926cc50a00d19375d3338ee3fdfdfa72f087769 |
| qe_ena-0.1.6-cp312-cp312-win_amd64.whl | 5.6 MB | 0c7a2de41e650d95e96687f23c732ef3ea936b18d1b6dc4216eeca4e8a65ef50 |
| qe_ena-0.1.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | b3b8c9b9d39106494bb2b4b3ee90b7426c56aad8af8accb3c9c310127e4b3190 |
| qe_ena-0.1.6-cp312-cp312-macosx_26_0_arm64.whl | 159.9 KB | 4471a2a79423e82eb103b45bb4e742bdf44cc3964246ebdcbcf72579c0cac863 |
| qe_ena-0.1.6-cp311-cp311-win_amd64.whl | 5.6 MB | 81ce501feaa33693e8089ec269e76706318a67abab9098fc662c50a0dce38aaf |
| qe_ena-0.1.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | 11.6 MB | 152e180d9c4c0a8962ba4b0857a6d62b021dcbb85e8971761ef870ca555fc490 |
| qe_ena-0.1.6-cp311-cp311-macosx_26_0_arm64.whl | 160.5 KB | 8cb5d566e6c5ae4849f2b0f25443baa2800f2455aa80072f1761f8a7016840de |
| qe_ena-0.1.5.tar.gz | 23.3 KB | 4fc5fc6836f9eabc533b7bef23cf8ba194f1c38ae66086d6e383b04768478cf9 |
| qe_ena-0.1.5-py3-none-any.whl | 17.5 KB | b054cf130b2811286327a2087051306cd505dbbf5e145e79bda3f3c7539c16ea |