
How your follower list exposes your politics without a single post
I found this project on an old hard drive a few weeks ago. The code has aged badly, Twitter became X, and the API it depends on no longer exists in a usable form. The question behind it holds up better than the implementation does.
That question was: how much can you infer about a person without analyzing anything they publish?
When people discuss social media privacy, the focus is almost always on content. Posts, comments, photos, likes, reposts. That focus is reasonable. If someone writes about a party or a politician often enough, inferring their views takes no cleverness at all. I wanted to know what happens when you remove content from the equation entirely and look only at the shape of the network around an account.
The short answer is that you still get a signal. It is noisy, it is not a statement about the individual, and it is easy to over-read. But it exists, and it comes from data nobody thinks of as sensitive.
Reading the environment instead of the posts
At the time, Twitter's API made it trivial to fetch the accounts a user followed and the accounts following them. My program needed one input: a username. From there it walked the surrounding network and extracted exactly one field from each profile it found, the bio.
A bio is usually a sentence or two: a handful of keywords, some emoji, an employer, a city. People use that space to declare interests, affiliations, ideological labels, and support for particular figures. One bio tells you almost nothing about the person who is being analyzed. Several hundred of them start to describe the environment that person sits inside.
That distinction runs through the whole project. I was never measuring beliefs. I was measuring the bias of a neighborhood in a graph.
The dictionary layer
The first version ran on a hand-built dictionary of Polish parties, politicians, and ideological terms. Since the data is Polish, some context helps: Prawo i Sprawiedliwość (PiS) is the main national-conservative party, Platforma Obywatelska (PO) the large liberal-conservative and pro-European one, Konfederacja an alliance of nationalist and free-market factions, Polska 2050 a centrist movement, Nowa Lewica and Razem the two left-wing parties, and Polskie Stronnictwo Ludowe (PSL) an agrarian centrist party. I also mapped politicians strongly tied to a single camp, such as Sławomir Mentzen and Grzegorz Braun on the right and Adrian Zandberg on the left.
The matching itself was crude on purpose. A bio containing mentzen added to the Konfederacja score, zandberg added to Razem, and the dictionary also covered party names, abbreviations, common misspellings, and adjacent figures. The ideological side worked the same way: wolny rynek ("free market"), antykomunista, and antysocjalista fed the capitalism category, konserwaty served as a stem for conservative terms, and references to monarchy fed monarchism. Emoji counted too. A crown could register as a monarchist signal, while 🌹, 🚩, and ☭ went to socialism. Other rules covered nationalism, anarchism, feminism, Marxism, pro-life and pro-choice positions, LGBT support, and pacifism.
This was a pile of substring and regex rules, not a model that understood political language, and it shows. Reading the dictionary years later I found lib used as a fragment in more than one category, which guarantees overlapping matches on words like liberał and libertarianin. Every match simply incremented a counter for its party or ideology, with no weighting and no attempt to resolve conflicts.
Aggregation is what made the crude rules usable. Following one right-wing politician proves nothing, and any single bio can be ironic, stale, or joking. But if a large share of the accounts around a user mention Konfederacja, Mentzen, and free-market slogans, that becomes a measurable tilt, and if Razem, Zandberg, and left-wing slogans dominate instead, the tilt runs the other way.
What the aggregate output looked like
For each analyzed account, the tool produced four views: a left-versus-right split, party-level signals, ideological signals, and a position on a simplified political compass. It could analyze followers, accounts followed by the user, or both.
The simplest output was the left-versus-right split:
@johndoe
Political orientation distribution in the analyzed network.
| Label | Result |
|---|---|
| Right-wing | 33.65% |
| Left-wing | 66.35% |
In this run, 66.35% of the classified political signals fell on the left side of the economic axis and 33.65% on the right. The result happened to match the account owner's publicly stated political position, but one matching example is obviously not validation of the method. The ideological view preserved more detail instead of collapsing everything into two sides:
@johndoe
Ideological signals detected across the analyzed network.
| Label | Matches |
|---|---|
| Capitalism | 48 |
| Conservative | 20 |
| Monarchism | 10 |
| Pro-Gun | 3 |
| Minarchism | 36 |
| Pro-Life | 3 |
| Militarism | 0 |
| Nationalism | 6 |
| Socialism | 84 |
| Progressive | 29 |
| Marxism | 25 |
| BLM | 16 |
| Pro-Choice | 5 |
| Pro-LGBT | 4 |
| Feminist | 13 |
| Anarchism | 14 |
| Internationalism | 1 |
| Pacifism | 3 |
| Democracy | 3 |
| Christian | 17 |
| Atheist | 6 |
| Euroenthusiast | 10 |
| Eurosceptic | 7 |
Here, socialism produced the most matches, followed by capitalism and minarchism. That combination is not a contradiction: these are raw counts across many different profiles, not mutually exclusive labels assigned to the account owner. A politically heterogeneous network can therefore produce strong signals in opposing categories at the same time.
The tool also kept raw counts for individual political parties, but those were mostly specific to the Polish political landscape and are less useful without that context. The ideological breakdown above captures the same idea in a form that is easier to interpret outside Poland.
The compass was the weakest piece. Each ideological category carried a predefined directional weight, the program summed those weights across the analyzed profiles, and the totals were mapped onto compass coordinates and clipped to the chart bounds. In the main analysis path the accumulated score was never divided by the number of profiles examined, so a larger network produced larger absolute values and drifted toward the edge of the chart regardless of how balanced it actually was. A later test implementation divided both axes by the user count, which turns the output into an average weight per profile and is the version that should have shipped. The left-versus-right pie was normalized on its own terms as a percentage, while the party and ideology charts stayed raw. Three different normalization conventions in one tool is not a design, it is an accident.
Two extensions: a classifier and a map
Replacing keyword matching with a model
The dictionary can only find phrases I thought to write down. If a bio says "libertarianism" it matches; if the same worldview is expressed indirectly, string matching sees nothing.
So I built a small hand-labeled dataset. I collected Polish-language bios from the networks around politicians spanning the spectrum, including Zandberg and Magdalena Biejat on the left, Mentzen, Krzysztof Bosak, and Braun on the right, and Donald Tusk, Szymon Hołownia, and Władysław Kosiniak-Kamysz toward the center. The politicians were only a source of politically active profiles, not labels in themselves; I tagged every bio manually as left, center, or right. I then fine-tuned Polish transformer models, HerBERT and Polish RoBERTa, on that set: encode the bio, classify the vector.
Results were noisy and generally worse than the dictionary on obvious cases, which is what a small dataset buys you. The dataset also has a structural problem I did not appreciate at the time. Bios harvested from politicians' networks are drawn from unusually political accounts, so the classifier learned to separate loud political self-descriptions from each other rather than to handle the ordinary bio that reads "mom, cat, coffee." Against that background it will be poorly calibrated on normal users.
What made it worth keeping was that it occasionally classified bios containing no party name, no politician, and no dictionary keyword. On those cases it was working from something closer to meaning than to string overlap. I wrapped it in a minimal interface that returned left, center, and right scores for arbitrary text.
Putting the profiles on a map
Twitter profiles also carried a free-text location field. Where the string resolved to a real place, I geocoded it against OpenStreetMap data and plotted the profile on a map of Poland, coloring each pin by the party or orientation detected in that profile's bio. Hundreds of users typing "Warszawa" collapse into a single point, so I added small random jitter to the coordinates and generated both an interactive point map and a heat map. This layer used nothing beyond public profile metadata either.
What this does not show
The output is not a measurement of anyone's beliefs, and treating it as one would be a straightforward error. People follow opponents, journalists, and accounts they argue with. Bios are ironic, abandoned, and years out of date. Whoever the API happened to return introduces sampling bias, the dictionary matches fragments it should not, and the classifier is frequently wrong. The compass, in its main implementation, was mathematically broken in a way that systematically exaggerated large networks.
What the project does show is narrower and, to me, more interesting. Every input was individually harmless: a follower list, a following list, a one-line bio, a city name. Combined and aggregated, they produce an attribute the user never stated anywhere. You do not have to write "I support party X" for your social graph to carry a political signal, and the signal survives even a deliberately unsophisticated implementation like mine.
Poland was the test case only because it was the political environment I could evaluate by hand. The mechanism has nothing Polish about it. Analogous clusters form around Democrats and Republicans, Labour and Reform UK, CDU/CSU and AfD. Social graphs are not random samples; people cluster around professions, communities, and politics, and those correlations leave traces.
Public data is not unrestricted data
That the inputs are publicly accessible does not make processing them legally unrestricted. Under the GDPR, political opinions are a special category of personal data, and the European Data Protection Board has been explicit that special-category data can be created through inference, including by predicting political affiliation from information that is not itself sensitive. That is precisely what this tool does.
There is an exception for data the subject has manifestly made public, but the EDPB reads it narrowly. Public availability on its own does not clearly satisfy it, and it fits especially poorly when the sensitive attribute was inferred rather than disclosed. A bio saying "Warszawa, kot, rower" was not manifestly made public as a political opinion, whatever the aggregate says about the network around it.
- GDPR, Article 9: Processing of special categories of personal data
- EDPB Guidelines 8/2020 on the targeting of social media users, particularly the sections on explicit and inferred special-category data
The code aged, the problem didn't
The Twitter this ran against is gone. X restricts API access heavily, endpoints I depended on disappeared, and HerBERT-era models have been comfortably surpassed. None of that touches the underlying observation.
We still publish social connections, profile descriptions, affiliations, and locations, and the tooling for combining those signals has improved far more than the exposure has decreased. Where I used substring rules and a small classifier, the current equivalent would use graph embeddings or a graph neural network over the follower structure, a modern language model over the text, and geography and activity as additional features in the same representation. Whatever accuracy my version had, that version has more.
Which is why finding the project again was worth an afternoon. Not because the code was good, because it was not, but because the question it was built to answer is now easier to answer than it was then: how much can you learn about someone without reading anything they wrote, purely from the structure of the network around them?