qe-ena 0.1.6 0.1.6.9004

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

Install

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.

Versions

Links in the wheel columns are the Python versions each wheel supports; pip and uv pick the right file automatically.

VersionPublishedSourceWheel
all platforms
Linux
x86_64 · glibc 2.27+
macOS
arm64 · macOS 26.0+
Windows
x64
0.1.6.90042026-10-10.tar.gz—3.11 3.12 3.133.11 3.12 3.133.11 3.12 3.13
0.1.62026-10-10.tar.gz—3.11 3.12 3.133.11 3.12 3.133.11 3.12 3.13
0.1.52026-10-06.tar.gzpy3———

Dependencies

For 0.1.6.

Description

qe-ena

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). The pyena project on PyPI is an unrelated package — install qe-ena from 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).


Installation

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/.

Development install

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

Quick Start

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)

Plotting

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

FirstGame − SecondGame network subtraction of RS.data with both group means

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.


Accessing Results

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)

Separate Accumulation

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 Methods

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)
)

Window Options

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

Further Reading

All files and SHA-256 hashes (22)
FileSizeSHA-256
qe_ena-0.1.6.9004.tar.gz186.7 KBc636da0d0207fdd80667f7f94b82e18aeb7b1b9898a00d81b0dbf3ff7ac943f6
qe_ena-0.1.6.9004-cp313-cp313-win_amd64.whl5.6 MBeef7a455d8e6c5fb7fad2714156baf9050214b7835ddf6313fbc20c1c6a96d5d
qe_ena-0.1.6.9004-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MB12f2d0aa988f7816bc10a90f496546b92df07e1c2207b2a0920924d6ac2dd911
qe_ena-0.1.6.9004-cp313-cp313-macosx_26_0_arm64.whl159.9 KBc9ea8c5c751998f2fe93509be01a52357c1537b38ae70c92c18ee075af4fe977
qe_ena-0.1.6.9004-cp312-cp312-win_amd64.whl5.6 MB0a122badbc1e1998dad02e73ed54f23717758212527ac6bf73f1aa85bc056c18
qe_ena-0.1.6.9004-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MB14e45a9e554d455ed5665fd6124088a63cae931858ca835af3874c005950db95
qe_ena-0.1.6.9004-cp312-cp312-macosx_26_0_arm64.whl159.9 KBba27c6772e8c4ceac894814676900f5dd2d3a177851825c67f8acd0994b221f9
qe_ena-0.1.6.9004-cp311-cp311-win_amd64.whl5.6 MBa17f4f31dd6a71d62b0f7e8a054a11ede14ee30713f757a57fe8abd67dfd586c
qe_ena-0.1.6.9004-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MB85e9d088735b4c32b66e6f33fe027328218204da0c5c080cbd85c97ee8265bac
qe_ena-0.1.6.9004-cp311-cp311-macosx_26_0_arm64.whl160.6 KBa8ea9956f279fc6926fe655699fa17e3deb031c5dc8a4997abd7faa9f5f86610
qe_ena-0.1.6.tar.gz186.7 KBf0828d6320f61dee4513c13c60fbd7297c01e6990d62fb52e69251db772d4153
qe_ena-0.1.6-cp313-cp313-win_amd64.whl5.6 MBf950c8c87954c7294dec88312832d56b8e03587b1674112e8c23a26febce9165
qe_ena-0.1.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MB39f246a85c8385f273f6817372e46976bd9cb50ae7a1cdca71fc9e086efd62b0
qe_ena-0.1.6-cp313-cp313-macosx_26_0_arm64.whl159.9 KBaa7967f545465c8d8bbbd21ce926cc50a00d19375d3338ee3fdfdfa72f087769
qe_ena-0.1.6-cp312-cp312-win_amd64.whl5.6 MB0c7a2de41e650d95e96687f23c732ef3ea936b18d1b6dc4216eeca4e8a65ef50
qe_ena-0.1.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MBb3b8c9b9d39106494bb2b4b3ee90b7426c56aad8af8accb3c9c310127e4b3190
qe_ena-0.1.6-cp312-cp312-macosx_26_0_arm64.whl159.9 KB4471a2a79423e82eb103b45bb4e742bdf44cc3964246ebdcbcf72579c0cac863
qe_ena-0.1.6-cp311-cp311-win_amd64.whl5.6 MB81ce501feaa33693e8089ec269e76706318a67abab9098fc662c50a0dce38aaf
qe_ena-0.1.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl11.6 MB152e180d9c4c0a8962ba4b0857a6d62b021dcbb85e8971761ef870ca555fc490
qe_ena-0.1.6-cp311-cp311-macosx_26_0_arm64.whl160.5 KB8cb5d566e6c5ae4849f2b0f25443baa2800f2455aa80072f1761f8a7016840de
qe_ena-0.1.5.tar.gz23.3 KB4fc5fc6836f9eabc533b7bef23cf8ba194f1c38ae66086d6e383b04768478cf9
qe_ena-0.1.5-py3-none-any.whl17.5 KBb054cf130b2811286327a2087051306cd505dbbf5e145e79bda3f3c7539c16ea