Affinitree is an interactive visualization and questionnaire system for Temperament and Character Inventory (TCI) data. It turns raw TCI scores into a two dimensional forest of nodes where each person is a tree. Clicking a node shows a radial profile of temperament and character traits, and the layout lets you see who is shaped most similarly at a glance.
Affinitree is built as a small Python package plus a Flask web app. The core pipeline is:
- Load base TCI scores and optional user scores
- Normalize trait scores and compute pairwise distances
- Embed individuals into 2D space with multidimensional scaling (MDS)
- Cluster individuals and assign roles such as Root, Trunk, Branch, Leaf
- Build an interactive Plotly figure plus radial trait charts
- Serve the visualization and questionnaire through a web interface
Design lineage: Affinitree captures a single TCI profile as a fixed point in a similarity map. Rhizome develops the idea toward relationships and positions that may change over time. Its evolving-network scenes are illustrative; the cohort study contains no longitudinal observations. The seven colors retain their original trait meanings: Novelty Seeking (orange), Harm Avoidance (red), Reward Dependence (pink), Persistence (brown), Self-Directedness (blue), Cooperativeness (green), and Self-Transcendence (purple). · Analysis implementation
I analyzed the 50 baseline score profiles in data/Neuma_TCI_Score.xlsx, aggregating 29 subscales into seven TCI composite dimensions. This cohort yields 1,225 distinct profile pairs for distance-based comparison.
- Trait relationships: Nine of 21 exploratory Pearson correlations remained below a 5% false-discovery-rate threshold after Benjamini–Hochberg adjustment. Persistence and Self-Transcendence showed the strongest observed association (r = 0.630).
- Dimensionality reduction: Two-dimensional metric MDS preserved much of the rank ordering of seven-dimensional Euclidean distances (Spearman ρ = 0.899), with normalized Stress-1 = 0.203.
- Neighborhood preservation: The 2D projection retained 54.4% of the original five-nearest-neighbor connections.
- Clustering diagnostic: Four-group Ward clustering gave a silhouette score of 0.160, compared with 0.241 for a two-group solution. These clusters should not be interpreted as validated psychological types.
The analysis is exploratory. The 50 profiles are not a representative population sample, subscale aggregation has not been independently validated against the instrument's scoring manual, and correlations do not establish causal or diagnostic claims. Questionnaire submissions are not included in these figures.
To regenerate the published aggregate statistics and website:
python scripts/research_analysis.py
python -m unittest discover -s tests -p 'test_research_analysis.py' -v
python -m http.server 8000 --directory docsThe local preview is available at http://localhost:8000/. The website is a standalone static presentation; it does not depend on the questionnaire backend. GitHub Pages can serve it directly from the docs/ folder on main.
- Interactive Plotly graph of all respondents
- Clickable nodes that show a radial bar chart of temperament and character
- Hierarchical clustering into four high level roles
- Simple TCI questionnaire web app backed by Flask
- Regenerates the visualization when new scores are added
- Data science pipeline split into reusable modules
The project is structured as a small package plus a web layer.
affinitree-app/
README.md
requirements.txt
data/
Neuma_TCI_Score.xlsx # base TCI scores
Neuma_TCI_Score_user.xlsx # optional user scores
fullQuestionnare.txt # raw questionnaire text
questions.json # parsed questionnaire
src/
affinitree/
__init__.py
config.py # paths, constants, trait metadata
data_io.py # load and merge score tables
preprocessing.py # normalization and feature selection
similarity.py # distance matrix construction
embedding.py # MDS or other 2D embedding
graph.py # networkx graph and clustering logic
radial_chart.py # Matplotlib radial chart generation
figure.py # Plotly figure construction
html_builder.py # fig -> full index.html string
web/
app.py # Flask application
templates/
index.html # visualization shell
tci_test.html # questionnaire UI
static/
js/
affinitree.js # client side interactions
css/
style.css # optional styling
scripts/
build_affinitree.py # CLI to regenerate index.html
convert_questions.py # fullQuestionnare.txt -> questions.json
- Python 3.10 or newer
- Git
- A virtual environment tool such as
venv
Clone the repository and install dependencies into a virtual environment.
git clone https://github.com/<your-user>/affinitree-app.git
cd affinitree-app
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt
Affinitree expects TCI score tables in the data/ directory.
-
Neuma_TCI_Score.xlsx- Base dataset of TCI scores
- One row per individual
- Columns for trait totals such as
NS Total,HA Total,RD Total,P Total,SD Total,CO Total,ST Totalplus any subscales used for the radial chart
-
Neuma_TCI_Score_user.xlsx(optional)- Additional rows for new questionnaire submissions
- Same schema as the base file
The questionnaire system uses:
data/fullQuestionnare.txtas the human editable sourcedata/questions.jsonas the structured representation consumed by the web app
You can regenerate questions.json after editing the text file.
python scripts/convert_questions.py
The Affinitree pipeline uses standard tools from the scientific Python stack.
Trait columns are normalized so that each dimension contributes fairly to distance calculations. By default Affinitree applies MinMaxScaler from scikit learn across selected trait totals.
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df_norm = pd.DataFrame(
scaler.fit_transform(df[trait_columns]),
columns=trait_columns,
index=df.index,
)
You can change the set of traits or the scaling strategy in preprocessing.py and config.py.
Pairwise distances between individuals are computed with pairwise_distances from sklearn.metrics. The default metric is Euclidean distance on normalized trait totals.
from sklearn.metrics import pairwise_distances
distance_matrix = pairwise_distances(df_norm.values, metric="euclidean")
The distance metric and any weights are configurable. This makes it possible to experiment with correlation or cosine distance if you want to focus on pattern rather than absolute level.
To visualize the high dimensional trait space the project uses multidimensional scaling to embed individuals into two dimensions while trying to preserve pairwise distances.
from sklearn.manifold import MDS
mds = MDS(
n_components=2,
dissimilarity="precomputed",
random_state=42,
)
coords = mds.fit_transform(distance_matrix)
The resulting coordinates are used as node positions in the graph. Other embedding methods can be added in embedding.py.
Hierarchical clustering groups individuals into a small number of clusters which are labeled with high level roles such as Root, Trunk, Branch, Leaf. This is implemented in graph.py with AgglomerativeClustering from scikit learn and then stored as node metadata on the networkx graph.
Each node has a radial bar chart that summarizes temperament and character traits.
Temperament:
- P Persistence
- HA Harm Avoidance
- NS Novelty Seeking
- RD Reward Dependence
Character:
- CO Cooperativeness
- ST Self Transcendence
- SD Self Directedness
radial_chart.py converts one row of trait totals into a Matplotlib polar plot and returns a base64 encoded PNG that is attached to the Plotly node as custom data. When you click a node the front end displays this chart next to the graph.
You can regenerate the static visualization from the command line. This reads the data files, rebuilds the entire graph, and writes index.html to the project root.
source .venv/bin/activate
python scripts/build_affinitree.py
After the script completes, open index.html in a browser and interact with the graph. Click on any node to see its radial trait chart.
If you host the repository through GitHub Pages, commit and push the new index.html to update the live demo.
The Flask app serves the questionnaire and can append new results to the user score table, then trigger a rebuild of the visualization.
-
Make sure dependencies are installed and the virtual environment is active.
-
Start the server.
python web/app.py -
Open the questionnaire in your browser.
http://127.0.0.1:5000/tci -
Complete the TCI questionnaire and submit answers.
-
The backend scores the responses, writes them into
Neuma_TCI_Score_user.xlsxor another configured store, and may call the Affinitree pipeline to regenerate the graph.
The exact scoring logic lives in web/app.py and uses data/questions.json as the source of truth for question metadata.
Several aspects of Affinitree can be adjusted without editing the entire pipeline.
Look in src/affinitree/config.py for:
- Paths to base and user score files
- The list of trait columns used for distance and for the radial chart
- Number of clusters and their role labels
- Default distance metrighc and embedding method
You can also parameterize the pipeline further by passing a configuration object into the functions in data_io.py, preprocessing.py, and embedding.py.
To work on the project itself:
-
Install dependencies.
pip install -r requirements.txt -
Run the pipeline on a small sample to confirm your environment.
python scripts/build_affinitree.py -
Start the Flask app during development.
export FLASK_APP=web.app export FLASK_ENV=development flask run -
Consider adding tests under
tests/and wiring up continuous integration through GitHub Actions so that the pipeline and questionnaire scoring are exercised on every push.
Affinitree is a creation by Affinitree1 and friends. The project stands on the shoulders of many open source libraries, especially NumPy, pandas, scikit learn, networkx, Matplotlib, Plotly, and Flask.
This project is licensed under the MIT License. See the LICENSE file for details.