Classifying SunPhoto users from 100k album names

I needed a rough way to understand what kinds of users and communities still exist on SunPhoto.

There was no category field I could query. No clean list saying this user is a pigeon breeder, this one collects African violets, and this one runs an old roleplay account based on some TV series.

What I did have was the sitemap.

Each album had a URL like this:

One album name is mostly useless. A full account with 20, 50 or 300 album names is a different story.

For example:

That is almost certainly a pigeon breeder.

The reason was practical: I’m redesigning SunPhoto and needed to know which communities are still active. Are there user types worth building features for, or types that have mostly left? Browsing random accounts manually wasn’t going to answer that at any useful scale.

The problem looked easy at first. Extract the words, match some keywords, done. Oh well.

Converting the sitemap into something usable

The sitemap was XML, with every album stored as a separate URL entry. Good for Google. Not very useful for this.

I converted it into JSON grouped by username:

Now I could look at the user as a whole. That matters because an album called expo_bucuresti tells me nothing on its own. The same album next to matca, pui, femela and porumbei_voiajori clearly belongs to an animal breeder.

I generated several separate datasets from different sitemap sections. The samples used during development contained thousands of users and close to 150,000 albums.

Why keyword matching didn’t work

Album names are messy. I found all of this:

  • Romanian with and without diacritics
  • English, Spanish and Hungarian
  • spelling mistakes
  • names like xX_MyAlbum_Xx
  • ordering prefixes like 0001, AAA and ZZZ
  • words written as C_O_N_T_A_C_T
  • albums called just Poze, Diverse, 2017 or Contact
  • animal breeds I had never heard of
  • plant cultivars that sounded like celebrities
  • fictional hotels and schools from roleplay accounts

And the same word can mean completely different things.

Mii de flori sounds like plants. It is also the name of a chicken breed. Heidi Klum may be a celebrity album. It can also be a rose cultivar. Hotel, school and room may describe real places, or they may be fictional locations from a roleplay account with occupied rooms, characters and adoption pages.

A keyword dictionary wasn’t going to cut it.

First attempt: rules

I started with simple rules.

Some words are strong enough to be useful on their own:

For plants:

For fandom accounts:

The first rule-based version worked for obvious accounts. Then it started failing on everything slightly unusual.

A pigeon breeder with 15 exhibition albums could look like an events account. A plant collector with albums named after people could look like a celebrity fan. A roleplay account with a hotel, rooms and a reception page could look like a business.

That wasn’t looking very good.

Creating the training data

There were no existing labels, so I had to create them.

The process was roughly:

  1. Run the first rules.
  2. Inspect the classifications.
  3. Fix recurring mistakes.
  4. Put uncertain users in a review list.
  5. Manually classify the remaining obvious cases.

The rules were used to surface and organize candidates — not to generate the final labels. Every user in the training set was manually reviewed. The rules helped me find the obvious cases faster and flag the ambiguous ones for closer inspection, but the label that went into training was always a human decision, not the rule’s output. I didn’t want the model learning the rules’ own biases.

The first few hundred were straightforward. After that I was Googling Romanian pigeon breeds and violet cultivar names to figure out whether an account was a collector or just had a weird naming convention, which wasn’t quite where I expected this project to go.

I also didn’t want hundreds of tiny categories used by one or two accounts. Those would look detailed but wouldn’t be useful for anything.

The final dataset had 1,877 users and 49,500 albums, with broad categories, optional detailed tags, and optional account modes. That became the training data.

From album names to numbers

The classifier doesn’t process albums one by one. It joins the username and all album names into one piece of text, cleans it up, then converts it to numbers.

The cleaning step handles the messiest cases. 0001_C_O_N_T_A_C_T becomes contact. ZZZ_PORUMBEI_VOIAJORI_2019 becomes porumbei voiajori 2019. Diacritics removed, underscores replaced with spaces, ordering prefixes stripped, words written one letter at a time rebuilt. The original album names aren’t touched — this cleaned version only exists inside the classifier.

After cleaning, the whole account looks like one document:

That gets converted to numbers using TfidfVectorizer from scikit-learn — single words, pairs, and groups of three, around 22,000 entries total. Every user becomes a long list of numbers, most of them zero.

The useful part of TF-IDF is that common words like poze, album and contact get low values because they appear everywhere and tell you nothing. Specific words like columbofil get high values because they appear in a much narrower set of accounts. A pigeon account ends up with high scores on porumbei, voiajori, matca and near-zero on everything else. That’s what the model actually learns from.

Training the category models

I didn’t train one model that picks exactly one category. I trained a separate yes/no classifier for every label.

Is this an animal account? Is this a plant account? Is this a pigeon account? Is this user a breeder? A seller?

One user can have several categories and modes at the same time. The implementation uses:

OneVsRestClassifier creates one binary classifier per label. SGDClassifier learns the numerical weight of every word and phrase.

The model starts with useless weights. Then it goes through the reviewed users, calculates a prediction, compares it with the label, adjusts the word weights if it was wrong, and repeats. After enough passes, words like matca, pui, femela and cuib get positive weights for animals and breeder. pui alone is ambiguous. pui brahma is not. camera is generic. camera ocupata appears almost exclusively in roleplay accounts. There’s no hardcoded rule — the model learns these associations from examples.

Why I still needed rules after the model

The statistical model was better than keyword rules, but it could still be confidently wrong.

A pigeon breeder may have many albums containing expo, expozitie, campionat, cupa. The model might decide: events account. But the same account also has porumbei, matca, pui, femela, mascul. The exhibitions are not the main subject. The animals are.

So there’s a second layer applied after the statistical prediction. Most of it isn’t special-casing — it’s enforcing logical constraints that should always be true regardless of what the model scores.

The breeder mode implies the animals category. grower implies plants_gardening. fan_account implies entertainment_fandom. If the model assigns breeder without animals, something went wrong and the constraint fixes it. Same for account size: one matching album is meaningful for a user with three albums total, but it shouldn’t classify a user with 500. Larger accounts need more repeated evidence before a label sticks.

The exception is the roleplay case — several fictional rooms, occupied characters and hotel pages is a pattern the model sometimes misreads as a real business. That one genuinely needed a rule.

Cutting down the taxonomy

The first versions supported many categories: animals, plants, fandom, events, travel, personal, business, art, food, vehicles, education, photography, fashion.

These worked reasonably well on the sample I built them on. Then I tested on different sitemap sections.

  • Pigeon breeders were becoming event accounts.
  • Plant catalogs were becoming businesses.
  • TV fan accounts were becoming personal and family.

I could have added another 50 special cases, then another 100. It would have looked better on that dataset and probably worse on the next one.

So I stopped. The final version keeps only the categories that worked consistently across several separate datasets: animals, plants_gardening, entertainment_fandom, other. Detailed tags and account modes are still kept when the evidence is strong enough — pigeons, breeder, seller, roleplay and so on — but the primary category is always one of those four.

Testing on data the model hadn’t seen

The classifier was tested on several non-overlapping sitemap sections — users not included in the original training set.

I manually checked 100 users from one of those datasets, sampled proportionally across the three supported categories plus other.

Of the 67 users that received one of the three supported categories, 4 were wrong — assigned the incorrect label. Of the full 100, 10 had the wrong result when you include users that should have received a category but were left as other instead.

So the main failure mode isn’t wrong categories. It’s missed ones. Some users end up as other because their album names contain only obscure breed names, cultivar names or character names.

A pigeon account with only breed names. A violet collection with only cultivar names. A fan account with only character names.

The model is conservative on purpose. When it assigns one of the supported categories, it’s usually right — the bigger issue is accounts it doesn’t assign anything to.

This is still a rough classification based only on usernames and album names. It doesn’t inspect the actual photos. It doesn’t understand every breed, plant cultivar or forgotten TV series from 2011. Neither do I, to be fair.

But it’s good enough to answer the original question. SunPhoto still has active communities — breeders, collectors, growers, fandom archives — and now I have a rough map of where they are and how they behave. That’s enough to start making decisions about which features are worth building and which communities are worth designing for.

The unsolved part is the other bucket. Some of those accounts are genuinely unclassifiable from album names alone — a pigeon breeder who only uses breed names, a violet collector who only uses cultivar names. The vocabulary is completely opaque without domain knowledge. If you’ve dealt with that problem, I’d be curious how.

Leave a Reply

Your email address will not be published. Required fields are marked *